The DataSetSelection Class

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:

  1. Create a default DataSetSelection using createSelection.

  2. Set the action to be SA_INSERT or SA_REPLACE.

  3. Assign DataSet pointers to all of the required axes.

  4. Pass the DataSetSelection to Plot::updateTraces.

  5. When a DataSetSelection is received by Plot::updateTraces, the call is dispatched to insertTrace or removeTrace according to the action in the DataSetSelection.

  6. 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.

Validating DataSetSelection Objects

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.

Related Changes

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.

DataSetList

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.

Ideas for Further Changes

More descriptive ("self-documenting") DataSetSelectionThere 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 DataSetSelectionWindowNow 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.