Writing a custom viewer for glue with Qt and Matplotlib

If you are a user trying to build a very simple viewer using Matplotlib, you may want to check out Writing a simple custom data viewer – the present tutorial is intended for people who wish to write and distribute a viewer using Matplotlib with full control over layout and behavior. This tutorial assumes that you have already gone over the Writing a custom viewer for glue and Writing a custom viewer for glue with Qt tutorials.

Glue provides a set of base classes for the state classes, layer artist, and data viewer which already take care of a number of aspects common to all Matplotlib-based viewers. We describe each of these in turn in the following sections, then simplify the example from Writing a custom viewer for glue with Qt using this infrastructure.

State classes

The MatplotlibDataViewerState class provides a subclass of ViewerState which adds properties related to:

  • the appearance of the plot (font and tick sizes)
  • the limits of the current view (this currently assumes 2D plots)
  • the aspect ratio of the axes
  • whether the axes are log or linear

Note that it does not add e.g. x_att and y_att since not all Matplotlib- based viewers will require the same number of attributes, and since some viewers may define attributes that aren’t specific to the x or y axis (e.g. in the case of networks).

The MatplotlibLayerState class provides a subclass of LayerState which adds the color and alpha property (and keeps them in sync with layer.style.color and layer.style.alpha).

Layer artist

The MatplotlibLayerArtist class implements the redraw(), remove(), and clear() methods assuming that all the contents of the layer use Matplotlib artists. In the __init__ of your MatplotlibLayerArtist sub-class, you should make sure you add all artist references to the mpl_artists property for this to work.

Data viewer

The MatplotlibDataViewer class adds functionality on top of the base DataViewer class:

  • It automatically sets up the Matplotlib axes
  • It keeps the x/y limits of the plot, the scale (linear/log), the font/tick parameters, and the aspect ratio in sync with the MatplotlibDataViewerState
  • It adds tools for saving, zooming, panning, and resetting the view
  • It recognizes the global glue preferences for foreground/background color

Functional example

Let’s now take the take full example from Writing a custom viewer for glue with Qt and update/improve it to use the infrastructure described above. As before if you want to try this out, you can copy the code below into a file called config.py in the directory from where you are starting glue. In addition you will also need the viewer_state.ui file.

import os

import numpy as np

from qtpy.QtWidgets import QWidget, QVBoxLayout, QCheckBox

from glue.config import qt_client
from glue.core.data_combo_helper import ComponentIDComboHelper

from glue.external.echo import CallbackProperty, SelectionCallbackProperty
from glue.external.echo.qt import (connect_checkable_button,
                                   autoconnect_callbacks_to_qt)

from glue.viewers.matplotlib.layer_artist import MatplotlibLayerArtist
from glue.viewers.matplotlib.state import MatplotlibDataViewerState, MatplotlibLayerState
from glue.viewers.matplotlib.qt.data_viewer import MatplotlibDataViewer

from glue.utils.qt import load_ui


class TutorialViewerState(MatplotlibDataViewerState):

    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)
        self.add_callback('x_att', self._on_attribute_change)
        self.add_callback('y_att', self._on_attribute_change)

    def _on_layers_change(self, value):
        self._x_att_helper.set_multiple_data(self.layers_data)
        self._y_att_helper.set_multiple_data(self.layers_data)

    def _on_attribute_change(self, value):
        if self.x_att is not None:
            self.x_axislabel = self.x_att.label
        if self.y_att is not None:
            self.y_axislabel = self.y_att.label


class TutorialLayerState(MatplotlibLayerState):
    fill = CallbackProperty(False, docstring='Whether to show the markers as filled or not')


class TutorialLayerArtist(MatplotlibLayerArtist):

    _layer_state_cls = TutorialLayerState

    def __init__(self, axes, *args, **kwargs):

        super(TutorialLayerArtist, self).__init__(axes, *args, **kwargs)

        self.artist = self.axes.plot([], [], 'o', mec='none')[0]
        self.mpl_artists.append(self.artist)

        self.state.add_callback('fill', self._on_visual_change)
        self.state.add_callback('visible', self._on_visual_change)
        self.state.add_callback('zorder', self._on_visual_change)
        self.state.add_callback('color', self._on_visual_change)
        self.state.add_callback('alpha', self._on_visual_change)

        self._viewer_state.add_callback('x_att', self._on_attribute_change)
        self._viewer_state.add_callback('y_att', self._on_attribute_change)

    def _on_visual_change(self, value=None):

        self.artist.set_visible(self.state.visible)
        self.artist.set_zorder(self.state.zorder)
        self.artist.set_markeredgecolor(self.state.color)
        if self.state.fill:
            self.artist.set_markerfacecolor(self.state.color)
        else:
            self.artist.set_markerfacecolor('white')
        self.artist.set_alpha(self.state.alpha)

        self.redraw()

    def _on_attribute_change(self, value=None):

        if self._viewer_state.x_att is None or self._viewer_state.y_att is None:
            return

        x = self.state.layer[self._viewer_state.x_att]
        y = self.state.layer[self._viewer_state.y_att]

        self.artist.set_data(x, y)

        self.axes.set_xlim(np.nanmin(x), np.nanmax(x))
        self.axes.set_ylim(np.nanmin(y), np.nanmax(y))

        self.redraw()

    def update(self):
        self._on_attribute_change()
        self._on_visual_change()


class TutorialViewerStateWidget(QWidget):

    def __init__(self, viewer_state=None, session=None):

        super(TutorialViewerStateWidget, self).__init__()

        self.ui = load_ui('viewer_state.ui', self,
                          directory=os.path.dirname(__file__))

        self.viewer_state = viewer_state
        autoconnect_callbacks_to_qt(self.viewer_state, self.ui)


class TutorialLayerStateWidget(QWidget):

    def __init__(self, layer_artist):

        super(TutorialLayerStateWidget, self).__init__()

        self.checkbox = QCheckBox('Fill markers')
        layout = QVBoxLayout()
        layout.addWidget(self.checkbox)
        self.setLayout(layout)

        self.layer_state = layer_artist.state
        connect_checkable_button(self.layer_state, 'fill', self.checkbox)


class TutorialDataViewer(MatplotlibDataViewer):

    LABEL = 'Tutorial viewer'
    _state_cls = TutorialViewerState
    _options_cls = TutorialViewerStateWidget
    _layer_style_widget_cls = TutorialLayerStateWidget
    _data_artist_cls = TutorialLayerArtist
    _subset_artist_cls = TutorialLayerArtist


qt_client.add(TutorialDataViewer)

While the code is not much shorter, there is additional functionality available. In particular, the viewer now has standard Matplotlib buttons in the toolbar:

../_images/tutorial_viewer.png

In addition, the layer artist has been improved to take into account the color and transparency given by the layer state (via the _on_visual_change method), and the axis labels are now set in the viewer state class.

Further reading

To find out how to add tools to your custom viewer, see the Custom tools for viewers and custom toolbars tutorial.