Writing a custom viewer for glue

Motivation

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.

Terminology

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.

State classes

Overview

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.

Viewer state

To create a viewer, we import the base ViewerState class, as well as the CallbackProperty class:

from glue.viewers.common.state import ViewerState
from glue.external.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.

Layer state

Similarly to the viewer state, you need to also define a state class for layers in the visualization using LayerState:

from glue.viewers.common.state import LayerState

The LayerState class defines the following properties by default:

  • layer: the Data or Subset attached to the layer (the naming of this property is historical/confusing and may be changed to data in future).
  • 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)

Furthermore, layer.style is itself a state class that includes global settings for the data or subset, such as color and alpha.

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 layer.style, 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.

Multi-choice properties

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 SelectionCallbackProperty class, 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 class 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 x_att and y_att will automatically be updated.

Layer artist

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 clear() method 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 remove() method should permanently remove the layer from the visualization, the redraw() method should force the layer to be redrawn, and update() should 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 state) and the viewer state (accessible as _viewer_state).

This means that we can then do the following, assuming a layer state with the 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.

Data viewer

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 as:

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, a 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.

Further reading

If you are interested in building a viewer for the Qt front-end of glue, you can find out more about this and see a complete example in Writing a custom viewer for glue with Qt. Even if you want to develop a viewer for a different front-end, you may find the Qt example useful.