Writing a custom viewer for glue¶
The simple way of defining new custom viewers described in Writing a simple custom data viewer are well-suited to developing new custom viewers that include simple Matplotlib plots, and for now is limited to Qt-based viewers. But in some cases, you may want to write a data viewer with more customized functionality, or that doesn’t depend on Matplotlib or Qt and may use an existing third-party widget.
In this tutorial, we will take a look at the pieces needed to build a data
viewer. The sections here are relevant regardless of whether you are building a
data viewer for e.g. Qt or Jupyter. If you are interested in building a Qt-based
viewer, you can then proceed to Writing a custom viewer for glue with Qt. If you are interested in
building a Matplotlib-based Qt viewer, you can then also make use of the
glue.viewers.matplotlib sub-package to simplify things as described in
Writing a custom viewer for glue with Qt and Matplotlib.
When we talk about a data viewer, we mean specifically one of the visualizations in glue (e.g. scatter plot, histogram, network diagram, etc.). Inside each visualization, there may be multiple datasets or subsets shown. For example, a dataset might be shown as markers of a certain color, while a subset might be shown in a different color. We refer to these as layers in the visualization, and these typically appear in a list on the left of the glue application window.
The first piece to construct when developing a new data viewer are state classes for the data viewer and layers, which you can think of as a conceptual representation of the data viewer and layers, but doesn’t contain any code specific to e.g. Qt or Jupyter or even the visualization library you are using. As an example, a scatter viewer will have a state class that indicates which attributes are shown on which axes, and what the limits of the axes are. Each layer then also has a state class which includes information for example about what the color of the layer should be, and whether it is currently visible or not.
from glue.viewers.common.state import ViewerState from echo import CallbackProperty
The latter is used to define properties on the state class and we can attach callback functions to them (more on this soon). Let’s now imagine we want to build a simple scatter plot viewer. Our state class would then look like:
class TutorialViewerState(ViewerState): x_att = CallbackProperty(docstring='The attribute to use on the x-axis') y_att = CallbackProperty(docstring='The attribute to use on the y-axis')
Once a state class is defined with callback properties, it is possible to attach callback functions to them:
>>> def on_x_att_change(value): ... print('x_att has changed and is now', value) >>> state = TutorialViewerState() >>> state.add_callback('x_att', on_x_att_change) >>> state.x_att = 'a' x_att has changed and is now a
What this means is that when you are defining the state class for your viewer, think about whether you want to change certain properties based on others. For example we could write a state class that changes x to match y (but not y to match x):
class TutorialViewerState(ViewerState): x_att = CallbackProperty(docstring='The attribute to use on the x-axis') y_att = CallbackProperty(docstring='The attribute to use on the y-axis') def __init__(self, *args, **kwargs): super(TutorialViewerState).__init__(*args, **kwargs) self.add_callback('y_att', self._on_y_att_change) def _on_y_att_change(self, value): self.x_att = self.y_att
The idea is to implement as much of the logic as possible here rather than relying on e.g. Qt events, so that your class can be re-used for e.g. both a Qt and Jupyter data viewer.
Note that the
ViewerState defines one
property by default, which is
layers - a container that is used to store
LayerState objects (see Layer state).
You shouldn’t need to add/remove layers from this manually, but you can attach
callback functions to
layers in case any of the layers change.
Similarly to the viewer state, you need to also define a state class for
layers in the visualization using
from glue.viewers.common.state import LayerState
LayerState class defines the following
properties by default:
visible: whether the layer is visible or not
zorder: a numerical value indicating (when relevant) which layer should appear in front of which (higher numbers mean the layer should be shown more in the foreground)
layer.style is itself a state class that includes global
settings for the data or subset, such as
Let’s say that you want to define a way to indicate in the layer whether to
use filled markers or not - this is not one of the settings in
so you can define it using:
class TutorialLayerState(LayerState): fill = CallbackProperty(False, docstring='Whether to show the markers as filled or not')
The optional first value in
CallbackProperty is the
default value that the property should be set to.
In some cases, you might want the properties on the state classes to be a
selection from a fixed set of values – for instance line style, or as
demonstrated in Viewer State, the attribute to show on an axis (since
it should be chosen from the existing data attributes). This can be
done by using the
which should be used as follows:
class TutorialViewerState(ViewerState): linestyle = SelectionCallbackProperty() def __init__(self, *args, **kwargs): super(TutorialViewerState).__init__(*args, **kwargs) MyExampleState.linestyle.set_choices(['solid', 'dashed', 'dotted'])
This then makes it so that the
linestyle property knows about what valid
values are, and this will come in useful when developing for example Qt widgets
so that they can automatically populate combo/selection boxes for example.
For the specific case of selecting attributes from the data, we also provide a
ComponentIDComboHelper that can
automatically keep the attributes for datasets in sync with the choices in a
SelectionCallbackProperty class. Here’s an example
of how to use it:
class TutorialViewerState(ViewerState): x_att = SelectionCallbackProperty(docstring='The attribute to use on the x-axis') y_att = SelectionCallbackProperty(docstring='The attribute to use on the y-axis') def __init__(self, *args, **kwargs): super(TutorialViewerState, self).__init__(*args, **kwargs) self._x_att_helper = ComponentIDComboHelper(self, 'x_att') self._y_att_helper = ComponentIDComboHelper(self, 'y_att') self.add_callback('layers', self._on_layers_change) def _on_layers_change(self, value): # self.layers_data is a shortcut for # [layer_state.layer for layer_state in self.layers] self._x_att_helper.set_multiple_data(self.layers_data) self._y_att_helper.set_multiple_data(self.layers_data)
Now whenever layers are added/removed, the choices for
will automatically be updated.
In the previous section, we saw that we can define classes to hold the conceptual state of viewers and of the layers in the viewers. The next type of class we are going to look at is the layer artist.
Conceptually, layer artists can be used to carry out the actual drawing and include any logic about how to convert data and subsets into layers in your visualization.
The minimal layer artist class looks like the following:
from glue.viewers.common.layer_artist import LayerArtist class TutorialLayerArtist(LayerArtist): _layer_artist_cls = TutorialLayerState def clear(self): pass def remove(self): pass def redraw(self): pass def update(self): pass
Each layer artist class has to define the four methods shown above. The
should remove the layer from the visualization, bearing in mind
that the layer might be added back (this can happen for example when toggling
the visibility of the layer property), the
should permanently remove the layer from the visualization, the
should force the layer to be redrawn, and
update the appearance of the layer as necessary before redrawing – note that
update() is called
for example when a subset has changed.
By default, layer artists inheriting from
LayerArtist will be
initialized with a reference to the layer state (accessible as
the viewer state (accessible as
This means that we can then do the following, assuming a layer state
fill property defined previously:
from glue.viewers.common.layer_artist import LayerArtist class TutorialLayerArtist(LayerArtist): _layer_artist_cls = TutorialLayerState def __init__(self, *args, **kwargs): super(MyLayerArtist, self).__init__(*args, **kwargs) self.state.add_callback('fill', self._on_fill_change) def _on_fill_change(self): # Make adjustments to the visualization layer here
In practice, you will likely need a reference to the overall visualization to be passed to the layer artist (for example the axes for a Matplotlib plot, or an OpenGL canvas). We will take a look at this after introducing the data viewer class in Data viewer.
Note that the layer artist doesn’t have to be specific to the front-end used either. If for instance you are developing a widget based on e.g. Matplotlib, and are then developing a Qt and Jupyter version of the viewer, you could write the layer artist in such a way that it only cares about the Matplotlib API and works for either the Qt or Jupyter viewers.
We have now seen how to define state classes for the viewer and layer, and layer artists. The final piece of the puzzle is the data viewer class itself, which brings everything together. The simplest definition of the data viewer class is:
from glue.viewers.common.viewer import Viewer class TutorialDataViewer(Viewer): LABEL = 'Tutorial viewer' _state_cls = TutorialViewerState _data_artist_cls = TutorialLayerArtist _subset_artist_cls = TutorialLayerArtist
In practice, this isn’t enough, since we need to actually set up the main
visualization and pass references to it to the layer artists. This can be
done in the initializer of the
TutorialDataViewer class. For example,
if you were building a Matplotlib-based viewer, assuming you imported Matplotlib
from matplotlib import pyplot as plt
you could do:
def __init__(self, *args, **kwargs): super(TutorialDataViewer, self).__init__(*args, **kwargs) self.axes = plt.subplot(1, 1, 1)
Note however that you need a way to pass the axes to the layer artist. The way
to do this is to add
axes as a positional argument for the
TutorialLayerArtist class defined previously then to add the following
method to the data viewer:
def get_layer_artist(self, cls, layer=None, layer_state=None): return cls(self.axes, self.state, layer=layer, layer_state=layer_state)
This method defines how the layer artists should be instantiated, and you can
see that we added a
self.axes positional argument, so that the layer artist
classes should now have access to the axes.
With this in place, what will happen now is that when a data viewer is created,
and when a new dataset or subset is added to it, the
layers attribute of
the viewer state class will automatically be updated to include a new
LayerState object. At the same time,
LayerArtist object will be
instantiated. The main task is therefore to implement the methods for the
LayerArtist (in particular
update()). You can then add
any required logic in the state classes if needed.