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.