Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add documentation about writing viewers for glue #1740

Merged
merged 10 commits into from
May 24, 2018
26 changes: 14 additions & 12 deletions doc/customizing_guide/custom_viewer.rst
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
Building Custom Data Viewers
============================
Writing a simple custom data viewer
===================================

.. figure:: images/bball_3.png
:align: center

Glue's standard data viewers (scatter plots, images, histograms) are
useful in a wide variety of data exploration settings. However, they
represent a *tiny* fraction of the ways to view a particular
dataset. For this reason, Glue provides a simple mechanism
for creating custom visualizations using matplotlib.

Creating a :func:`custom data viewer <glue.custom_viewer>` requires writing a little bit of Matplotlib
code but involves little to no GUI programming. The next several
sections illustrate how to build a custom data viewer by example.
Glue's standard data viewers (scatter plots, images, histograms) are useful in a
wide variety of data exploration settings. However, they represent a *tiny*
fraction of the ways to view a particular dataset. For this reason, Glue
provides a way to create more deta viewers that me better suited to what you
need.

There are several ways to do this - the tutorial on this page shows the easiest
way for users to develop a new custom visualization, provided that it can be
made using Matplotlib and tht you don't want do have to do any GUI programming.
If you are interested in building more advanced custom viewers, see
:ref:`state-qt-viewer`.

The Goal: Basketball Shot Charts
--------------------------------
In Basketball, Shot Charts show the spatial distribution of shots

In basketball, Shot Charts show the spatial distribution of shots
for a particiular player, team, or game. The `New York Times <http://www.nytimes.com/interactive/2012/06/11/sports/basketball/nba-shot-analysis.html?_r=0>`_ has a nice example.

There are three basic features that we might want to incorporate into
Expand Down
65 changes: 32 additions & 33 deletions doc/customizing_guide/full_custom_qt_viewer.rst
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
:orphan:

Writing a fully customized Qt viewer (advanced)
===============================================

Motivation
----------

The :func:`~glue.custom_viewer` function and the
:class:`~glue.viewers.custom.qt.custom_viewer.CustomViewer` class described in
:doc:`custom_viewer` are well-suited to developing new custom viewers that
include some kind of Matplotlib plot. But in some cases, you may want to
write a Qt data viewer that doesn't depend on Matplotlib, or may use an
existing widget. In this tutorial, we will assume that you have implemented a
Qt widget that contains the functionality you want, and we will focus on
looking at how to get it to work inside glue.

If you don't already have an existing widget, but want to make sure it will
work outside glue, start off by developing the widget outside of glue, then
use the instructions below to make it usable inside glue.
This tutorial is intended for people who want to develop a fully custom Qt-based
viewer for glue that does not use Matplotlib and does not use the state class
infrastructure. If you don't already have an existing widget, but want to make
sure it will work outside glue, start off by developing the widget outside of
glue, then use the instructions below to make it usable inside glue.

Displaying the widget in glue
-----------------------------
Expand All @@ -31,12 +26,10 @@ Now let's say we want to use this widget in glue, without having to change
anything in ``MyWidget``. The best way to do this is to create a new class,
``MyGlueWidget``, that will wrap around ``MyWidget`` and make it
glue-compatible. The glue widget should inherit from
:class:`~glue.viewers.common.qt.data_viewer` (this class does a few
:class:`~glue.viewers.common.qt.data_viewer.DataViewer` (this class does a few
boilerplate things such as, for example, adding the ability to drag and drop
data onto your data viewer).

The simplest glue widget wrapper that you can write that will show
``MyWidget`` is::
data onto your data viewer). The simplest glue widget wrapper that you can write
that will show ``MyWidget`` is::

from glue.qt.widgets.data_viewer import DataViewer

Expand All @@ -54,8 +47,8 @@ The simplest glue widget wrapper that you can write that will show
If you put the contents above into a ``config.py`` file then launch glue in
the same folder as the ``config.py`` file, you will then be able to go to the
**Canvas** menu, select **New Data Viewer**, and you should then be presented
with the window to select a data view, which should contain an 'Override
This' entry:
with the window to select a data view, which should contain an **Override
this** entry:

.. image:: images/select_override.png
:width: 200px
Expand All @@ -79,7 +72,7 @@ Passing data to the widget
Now we want to be able to pass data to this viewer. To do this, you should
define the ``add_data`` method which should take a single argument and return
`True` if adding the data succeeded, and `False` otherwise. So for now, let's
simply return True and do nothing::
simply return `True` and do nothing::

def add_data(self, data):
return True
Expand All @@ -99,16 +92,20 @@ However, this will simply plot the initial data and plot more data if you
drag datasets onto the window, but you will not for example be able to remove
datasets, show subsets, and so on. In some cases, that may be fine, and you
can stop at this point, but in other cases, if you want to define a way to
interact with subsets, propagate selections, and so on, you will need to set
up a glue client, which is discussed in `Setting up a client`_. But first, let's take a look at how we can add side panels in the dashboard which can include for example options for controlling the appearance or contents of your visualization.
interact with subsets, propagate selections, and so on, you will need to listen
for events, which is discussed in `Listening for events`_.

But first, let's take a look at how we can add side panels in the dashboard
which can include for example options for controlling the appearance or contents
of your visualization.

Adding side panels
------------------

In the glue interface, under the data manager is an area we refer to as the
dashboard, where different data viewers can include options for controlling
the appearance or content of visualizations (this is the area indicated as C
in :doc:getting-started). You can add any widget to the two available spaces.
In the glue interface, under the data collection is an area we refer to as the
dashboard, where different data viewers can include options for controlling the
appearance or content of visualizations (this is the area indicated as **C** in
:doc:getting-started). You can add any widget to the two available spaces.

In your wrapper class, ``MyGlueWidget`` in the example above, you will need to
define a method called ``options_widget``, which returns an instantiated widget
Expand Down Expand Up @@ -139,10 +136,13 @@ sets the widget for the middle widget in the dashboard. However, this will
default to a list of layers which can normally be used as-is (see `Using
Layers`_)

Setting up a client
-------------------
Listening for events
--------------------

Once the data viewer has been instantiated, the main glue application will call the ``register_to_hub`` method on the data viewer, and will pass it the hub as an argument. This allows you to set up your data viewer as a client that can listen to specific messages from the hub::
Once the data viewer has been instantiated, the main glue application will
automatically call the ``register_to_hub`` method on the data viewer, and will
pass it the hub as an argument. This allows you to set up your data viewer as a
client that can listen to specific messages from the hub::

from glue.core.message import DataCollectionAddMessage

Expand Down Expand Up @@ -171,10 +171,9 @@ By default, any sub-class of `~glue.viewers.common.qt.data_viewer` will
also include a list of layers in the central panel in the dashboard. Layers can
be thought of as specific components of visualizations - for example, in a
scatter plot, the main dataset will be a layer, while each individual subset
will have its own layer. The 'vertical' order of the layers (i.e. which one
will have its own layer. The order of the layers (which controls which one
appears in front of which) can then be set by dragging the layers around, and
the color/style of the layers can also be set from this list of layers (by
control-clicking on any layer).
the color/style of the layers can also be set from this list of layers.

Conceptually, layer artists can be used to carry out the actual drawing and
include any logic about how to convert data into visualizations. If you are
Expand Down Expand Up @@ -210,7 +209,7 @@ specific layer artist). So when the user adds a dataset, the viewer should do
something along the lines of::

layer_artist = MyLayerArtist(data, ...)
self._container.append(layer_artist)
self._layer_artist_container.append(layer_artist)
layer_artist.redraw()

If the user removes a layer from the list of layers by e.g. hitting the
Expand Down
86 changes: 86 additions & 0 deletions doc/customizing_guide/matplotlib_qt_viewer.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
.. _matplotlib-qt-viewer:

Writing a custom viewer for glue (using Matplotlib)
===================================================

If you are a user trying to build a very simple viewer using Matplotlib, you may
want to check out :doc:`custom_viewer` -- the present tutorial is intended for
people who wish to write and distribute a viewer using Matplotlib with full
control over layout and beahvior. This tutorial assumes that you have already
gone over the :ref:`state-qt-viewer` tutorial.

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 :ref:`state-qt-viewer` using this
infrastructure.

State classes
-------------

The :class:`~glue.viewers.matplotlib.state.MatplotlibDataViewerState` class
provides a subclass of :class:`~glue.viewers.common.state.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 :class:`~glue.viewers.matplotlib.state.MatplotlibLayerState` class
provides a subclass of :class:`~glue.viewers.common.state.LayerState` which
adds the ``color`` and ``alpha`` property (and keeps them in sync with
``layer.style.color`` and ``layer.style.alpha``).

Layer artist
------------

The :class:`~glue.viewers.matplotlib.layer_artist.MatplotlibLayerArtist` class
implements the
:meth:`~glue.viewers.matplotlib.layer_artist.MatplotlibLayerArtist.redraw`,
:meth:`~glue.viewers.matplotlib.layer_artist.MatplotlibLayerArtist.remove`, and
:meth:`~glue.viewers.matplotlib.layer_artist.MatplotlibLayerArtist.clear`
methods assuming that all the contents of the layer use Matplotlib artists. In
the ``__init__`` of your
:class:`~glue.viewers.matplotlib.layer_artist.MatplotlibLayerArtist` sub-class,
you should make sure you add all artist references to the ``mpl_artists``
property for this to work.

Data viewer
-----------

The :class:`~glue.viewers.matplotlib.qt.data_viewer.MatplotlibDataViewer` class
adds functionality on top of the base
:class:`~glue.viewers.common.qt.data_viewer_with_state.DataViewerWithState`
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
:class:`~glue.viewers.matplotlib.state.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 :ref:`state-qt-viewer` and
update/improve it to use the infrastructure described above:

.. literalinclude:: mpl_viewer/config.py

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

.. image:: mpl_viewer/tutorial_viewer.png
:width: 600px
:align: center

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.
140 changes: 140 additions & 0 deletions doc/customizing_guide/mpl_viewer/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
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)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading