The plotlib code seemed to be evolving into managing groups of
datasets identified by axis names. Each plot type had a list of axes
(AxisSelectMap
) to which the
DataSetSelectionWindow
could assign
DataSet
instances according to a filter. Some
Plot
classes supported
addDataSet
while for others only
addDataSets
made sense. Plus all the clients of
DataSetSelectionWindow
had to keep track of
connecting the right signals to the right slots according to whether an
insert or replace operation was needed. Lastly,
Plot
instances were stashing context values as
members so that they would have the information needed to complete an
insert or replace operation when fired by the DataSetSelectionWindow
. For example,
PlotQwt
needs to know if a new
DataSet
should be added to the right or left axis,
and replacements need to know which trace is being replaced.
I tried to consolidate these issues with a DataSetSelection
class. The
notion of the DataSetSelection
is just the set of datasets that a kind of plot needs
to generate a trace, also known as an overlay. It's an aggregation of the
AxisSelectMap
, filter, operation (insert or
replace), and Plot
-specific information that
describes the context for adding or replacing a trace on a
Plot
. It contains the input selected by the user
necessary to generate a kind of Plot
. For XY, the
selection expects dataset assignments for the X and Y axes. For Skewt,
there are five axes needing assignment. The DataSetSelection
is self-contained and
encapsulates this context information so that it can be passed around
between Plot
and DataSetSelectionWindow
without the need for stashing any context information
in the Plot
class itself.
So the set of traces on a Plot
is really a set of DataSetSelection
containing the datasets selected for that trace as well as per-trace
Plot
-specific information, such as the right or left axis and a curve id
(ie, for qwt). The Plot
base class manages the set of DataSetSelection
as a vector
of traces, and the public interface provides basic operations on those
traces: insert, replace, and remove.
Each DataSetSelection
has an ID
independent of vector index, since the vector index can change as traces
are added and removed.
Plot
subclasses extend DataSetSelection
with the needed per-trace
context information by subclassing it. The Plot
class provides a virtual
method createSelection
by which subclasses create and
return a default template for their particular kind of trace. So here's
the basic sequence to inserting or replacing a trace:
Create a default DataSetSelection
using createSelection
.
Set the action to be SA_INSERT or SA_REPLACE.
Assign DataSet
pointers to all of the required axes.
Pass the DataSetSelection
to Plot::updateTraces
.
When a DataSetSelection
is received by
Plot::updateTraces
, the call is dispatched to
insertTrace
or
removeTrace
according to the action in the
DataSetSelection
.
Subclasses can override insertTrace
and
removeTrace
as needed, but ultimately must
call Plot::insertTrace
or
Plot::removeTrace
. The Plot
base methods
give the trace a new ID, add it to the traces vector (or replace an
existing trace), and request a replot.
When the DataSetSelectionWindow
is driving the changes, the Plot
class
creates a DataSetSelection
template and sets the action accordingly, then passes the
DataSetSelection
to the DataSetSelectionWindow
. The Plot
connects the DataSetSelectionWindow
signal
selectedDataSets(DataSetSelection&)
to the plot's
updateTraces(DataSetSelection&)
slot. So there
only needs to be a single connection to DataSetSelectionWindow
regardless of the action,
and the action of creating, popping up, and connecting to the DataSetSelectionWindow
can
all be done in one method in the Plot
base class,
popupDataSetSelectWin
.
A DataSetSelection
manages DataSet
objects in the
AxisSelectMap
. The
AxisSelectMap
is now a vector of
(string
, DataSetRef
) pairs
which can be indexed either by index number or axis name. Since it's a
vector, the ordering of the axes is always the same, but it still has the
convenience of indexing by axis name similar to std::map. The
DataSetRef
is a subclass of DataSet
which allows
the DataSetBack
reference to be null. So
DataSetRef
instances can be copied and assigned
freely without any concern for memory management. The class will acquire
and release its DataSetBack
references
automatically. When a DataSetSelection
is destroyed, any
DataSetBack
references held by the
DataSetRef
instances in the
AxisSelectMap
will be released.
Since DataSetSelection
is just a base class which different kinds of plots
may need to extend, selections must be passed around as pointers or
references. The convention in the interface is this: returning a pointer
implies that the caller takes ownership and must delete the pointer.
Passing by reference means the caller retains ownership. Passing by const
reference of course means the called method cannot change the selection at
all. All DataSetSelection
classes must implement the virtual
clone
method so that an exact, deep copy of a
selection can always be made. For example, when DataSetSelectionWindow
receives the
template selection, it first clones the selection to make its own copy
which it is responsible for deleting. However, the copy includes any
extensions which a Plot
will need when the selection is passed back into
the Plot
instance through updateTraces
.
Rather than force every user of DataSetSelection
to manage the identifier
for each instance and keep them unique, the DataSetSelection
base class automatically
generates a unique identifier for each DataSetSelection
instance. There is a DataSetSelection
method to assign a new, unique identifier for an existing instance, and
there is also a method to copy the identifier between instances, such as
when one DataSetSelection
is taking the place of another but keeping the same
identifier. The point is that the application can never create a DataSetSelection
whose identifier accidentally collides with an
existing DataSetSelection
.
If insertTrace() and replaceTrace() are meant to be the public entry points, then it would be nice to save all of the subclass implementations from checking that the selections are valid before plotting them. The public entry point could be updateTraces(), which validates the dss before dispatching the update to the appropriate insertTrace() or replaceTrace() call. Then add other public entry points like addTrace() and deleteTrace(), which set the action in the dss and pass that into updateTraces(), so that all incoming traces will be validated in one place.
DataSetSelection::valid
is virtual so
that it can be extended by subclasses, but the basic form of validation may
suffice for most cases and avoid the need to subclass DataSetSelection
and override
valid
just to check validity. The method
DataSetSelection::validate
accepts a reference to
another DataSetSelection
and comparies it against itself. If the DataSetSelection
being
validated has the same number of axes, and all of those axes have been
assigned valid DataSet
instances which match the filter, then that DataSetSelection
is considered valid. The Plot
public interface uses this scheme to
validate DataSetSelection
instances being passed into the Plot
. A prototype
selection as returned by the virtual method
Plot::createSelection
is created and called to
validate the candidate DataSetSelection
.
Plot
is no longer passed into the DataSetSelectionWindow
constructor, just
the datasource. Also, Plot
connects to the DataSetSelectionWindow
signals rather than
vice-versa, so that DataSetSelectionWindow
does not need to know anything about the Plot
class. That makes it more useful for selecting inputs to other things,
like derivations.
DataSetSelectionWindow
was emitting a new
DSS_Memento
in its signal, which meant a memory leak
if the signal was not connected to anything. Instead it now emits a copy,
and the Plot
slot receives a copy. Rather than passing a null
DSS_Memento
pointer to the DataSetSelectionWindow
constructor to
indicate there is no state to restore, a DSS_Memento
now has a default constructor whose initial state returns true from
isEmpty
, meaning the
DSS_Memento
has no state from which to restore.
I started inserting DataSetList
in
place of std::vector<const DataSet*> everywhere I could,
and then ran into a conflict with std::vector<DataSet*>
being used in some places. I decided the distinction wasn't worth having
two DataSetList
types, so I made
DataSetList
a vector of non-const pointers and put
it everywhere. If anyone objects, let's talk about it. I'm not sure I
like it either, but getting the alternative correct and consistent seemed
problematic to me at the time.
More descriptive ("self-documenting") DataSetSelection
. There is a placeholder in the DataSetSelection
class for some
descriptive text. The idea is that the creator of a template could set a
description suitable for display to the user, such as "Replacing XY trace
ATX-ATWH", or "Inserting time-series trace on right axis." Then DataSetSelectionWindow
can
show this text to give the user some feedback that they're changing what
they had intended to change.
Non-modal DataSetSelectionWindow
. Now that all of the DataSetSelectionWindow
context is contained in its
DataSetSelection
template, it should be possible to try non-modal DataSetSelectionWindow
. This would
be helped by the above item for more descriptive DataSetSelection
, so that multiple
DataSetSelectionWindow
instances could be easily distinguishable by the user.