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_qt.config import qt_client
from glue.core.data_combo_helper import ComponentIDComboHelper

from echo import CallbackProperty, SelectionCallbackProperty
from 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_qt.viewers.matplotlib.data_viewer import MatplotlibDataViewer

from glue_qt.utils 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
        self._connections = 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
        self._connection = 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.