Skip to content

Commit

Permalink
Merge pull request #814 from astrofrog/layer-artist-docs
Browse files Browse the repository at this point in the history
Create documentation about layer artists
  • Loading branch information
astrofrog committed Jan 11, 2016
2 parents 557a145 + d600f86 commit c4189ca
Show file tree
Hide file tree
Showing 17 changed files with 102 additions and 44 deletions.
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ v0.7 (unreleased)

* Python 2.6 is no longer supported. [#804]

* The ``artist_container`` argument to client classes has been renamed to
``layer_artist_container``. [#814]

* Added documentation about how to use layer artists in custom Qt data viewers.
[#814]

v0.6 (2015-11-20)
-----------------

Expand Down
78 changes: 65 additions & 13 deletions doc/customizing_guide/full_custom_qt_viewer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,10 @@ 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 two methods called ``layer_view`` and ``options_widget``, which
each return an instantiated widget that should be included in the dashboard.
These names are because the top space is usually used for showing which
layers are included in a plot, and the bottom space is used for options (such
as the number of bins in histograms).
In your wrapper class, ``MyGlueWidget`` in the example above, you will need to
define a method called ``options_widget``, which returns an instantiated widget
that should be included in the dashboard on the bottom left of the glue window,
and can contain options to control the data viewer.

For example, you could do::

Expand All @@ -125,20 +123,21 @@ For example, you could do::

def __init__(self, session, parent=None):
...
self._layer_view = UsefulWidget(...)
self._options_widget = AnotherWidget(...)

...

def layer_view(self):
return self._layer_view

def options_widget(self):
return self._options_widget

Note that despite the name, you can actually set the widgets to what you
want, and the important thing is that ``layer_view`` is the top one, and
``options_widget`` the bottom one.
Note that despite the name, you can actually use the options widget to what you
want, and the important thing is that ``options_widget`` is the bottom left
pane in the dashboard on the left.

Note that you can also similarly define (via a method) ``layer_view``, which
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
-------------------
Expand All @@ -164,3 +163,56 @@ Once the data viewer has been instantiated, the main glue application will call
def _update_data(self, msg):

# Process DataUpdateMessage here
Using layers
------------

By default, any sub-class of `~glue.qt.widgets.data_viewer.DataViewer` 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
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).

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
using Matplotlib for your visualization, there are a number of pre-existing
layer artists in `glue.client.layer_artist`, but otherwise you will need to
create your own classes.

The minimal layer artist class looks like the following::

from glue.clients.layer_artist import LayerArtistBase
class MyLayerArtist(LayerArtistBase):
def clear(self):
pass

def redraw(self):
pass

def update(self):
pass

Essentially, each layer artist has to define the three methods shown above. The
``clear`` method should remove the layer from the visualization, the ``redraw``
method should redraw the entire visualization, and ``update``, should update
the apparance of the layer as necessary before redrawing.

In the data viewer, when the user adds a dataset or a subset, the list of
layers should then be updated. The layers are kept in a list in the
``_container`` attribute of the data viewer, and layers can be added and
removed with ``append`` and ``remove`` (both take one argument, which is a
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)
layer_artist.redraw()

If the user removes a layer from the list of layers by e.g. hitting the
backspace key, the ``clear`` method is called, followed by the ``redraw``
method.
4 changes: 2 additions & 2 deletions glue/clients/histogram_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ class HistogramClient(Client):
xmin = UpdateProperty(None, relim=True)
xmax = UpdateProperty(None, relim=True)

def __init__(self, data, figure, artist_container=None):
def __init__(self, data, figure, layer_artist_container=None):
super(HistogramClient, self).__init__(data)

self._artists = artist_container or LayerArtistContainer()
self._artists = layer_artist_container or LayerArtistContainer()
self._figure, self._axes = init_mpl(figure=figure, axes=None)
self._component = None
self._saved_nbins = None
Expand Down
8 changes: 4 additions & 4 deletions glue/clients/image_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ class ImageClient(VizClient):
display_attribute = CallbackProperty(None)
display_aspect = CallbackProperty('equal')

def __init__(self, data, artist_container=None):
def __init__(self, data, layer_artist_container=None):

VizClient.__init__(self, data)

self.artists = artist_container
self.artists = layer_artist_container
if self.artists is None:
self.artists = LayerArtistContainer()

Expand Down Expand Up @@ -705,8 +705,8 @@ def clear_crosshairs(self):

class MplImageClient(ImageClient):

def __init__(self, data, figure=None, axes=None, artist_container=None):
super(MplImageClient, self).__init__(data, artist_container)
def __init__(self, data, figure=None, axes=None, layer_artist_container=None):
super(MplImageClient, self).__init__(data, layer_artist_container)

if axes is not None:
raise ValueError("ImageClient does not accept an axes")
Expand Down
4 changes: 2 additions & 2 deletions glue/clients/scatter_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class ScatterClient(Client):
jitter = CallbackProperty()

def __init__(self, data=None, figure=None, axes=None,
artist_container=None):
layer_artist_container=None):
"""
Create a new ScatterClient object
Expand All @@ -52,7 +52,7 @@ def __init__(self, data=None, figure=None, axes=None,
"""
Client.__init__(self, data=data)
figure, axes = init_mpl(figure, axes)
self.artists = artist_container
self.artists = layer_artist_container
if self.artists is None:
self.artists = LayerArtistContainer()

Expand Down
4 changes: 2 additions & 2 deletions glue/clients/viz_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,13 @@ class GenericMplClient(Client):
"""

def __init__(self, data=None, figure=None, axes=None,
artist_container=None, axes_factory=None):
layer_artist_container=None, axes_factory=None):

super(GenericMplClient, self).__init__(data=data)
if axes_factory is None:
axes_factory = self.create_axes
figure, self.axes = init_mpl(figure, axes, axes_factory=axes_factory)
self.artists = artist_container
self.artists = layer_artist_container
if self.artists is None:
self.artists = LayerArtistContainer()

Expand Down
6 changes: 3 additions & 3 deletions glue/core/application_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ class ViewerBase(HubListener, PropertySetMixin):

# the glue.clients.layer_artist.LayerArtistContainer
# class/subclass to use
_container_cls = None
_layer_artist_container_cls = None

def __init__(self, session):

Expand All @@ -271,7 +271,7 @@ def __init__(self, session):
self._session = session
self._data = session.data_collection
self._hub = None
self._container = self._container_cls()
self._layer_artist_container = self._layer_artist_container_cls()

def register_to_hub(self, hub):
self._hub = hub
Expand Down Expand Up @@ -393,7 +393,7 @@ def layers(self):
A layer is a visual representation of a dataset or subset within
the viewer"""
return tuple(self._container)
return tuple(self._layer_artist_container)

def __gluestate__(self, context):
return dict(session=context.id(self._session),
Expand Down
4 changes: 2 additions & 2 deletions glue/plugins/ginga_viewer/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@

class GingaClient(ImageClient):

def __init__(self, data, canvas=None, artist_container=None):
super(GingaClient, self).__init__(data, artist_container)
def __init__(self, data, canvas=None, layer_artist_container=None):
super(GingaClient, self).__init__(data, layer_artist_container)
self._setup_ginga(canvas)

def _setup_ginga(self, canvas):
Expand Down
2 changes: 1 addition & 1 deletion glue/plugins/ginga_viewer/qt_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def _get_default_tools():
return []

def make_client(self):
return GingaClient(self._data, self.canvas, self._container)
return GingaClient(self._data, self.canvas, self._layer_artist_container)

def make_central_widget(self):

Expand Down
4 changes: 2 additions & 2 deletions glue/qt/custom_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,7 @@ def _build_ui(self, callback):
for k in sorted(self.ui):
v = self.ui[k]
w = FormElement.auto(v)
w.container = self.widget._container
w.container = self.widget._layer_artist_container
w.add_callback(callback)
self._settings[k] = w
if w.ui is not None:
Expand Down Expand Up @@ -876,7 +876,7 @@ def __init__(self, session, parent=None):
self.option_widget = self._build_ui()
self.client = CustomClient(self._data,
self.central_widget.canvas.fig,
artist_container=self._container,
layer_artist_container=self._layer_artist_container,
coordinator=self._coordinator)

self.make_toolbar()
Expand Down
10 changes: 5 additions & 5 deletions glue/qt/widgets/data_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class DataViewer(ViewerBase, QMainWindow):
* An automatic call to unregister on window close
* Drag and drop support for adding data
"""
_container_cls = QtLayerArtistContainer
_layer_artist_container_cls = QtLayerArtistContainer
LABEL = 'Override this'

def __init__(self, session, parent=None):
Expand All @@ -38,7 +38,7 @@ def __init__(self, session, parent=None):
ViewerBase.__init__(self, session)
self.setWindowIcon(get_qapp().windowIcon())
self._view = LayerArtistView()
self._view.setModel(self._container.model)
self._view.setModel(self._layer_artist_container.model)
self._tb_vis = {} # store whether toolbars are enabled
self.setAttribute(Qt.WA_DeleteOnClose)
self.setAcceptDrops(True)
Expand All @@ -50,11 +50,11 @@ def __init__(self, session, parent=None):
self.statusBar().setStyleSheet("QStatusBar{font-size:10px}")

# close window when last plot layer deleted
self._container.on_empty(lambda: self.close(warn=False))
self._container.on_changed(self.update_window_title)
self._layer_artist_container.on_empty(lambda: self.close(warn=False))
self._layer_artist_container.on_changed(self.update_window_title)

def remove_layer(self, layer):
self._container.pop(layer)
self._layer_artist_container.pop(layer)

def dragEnterEvent(self, event):
""" Accept the event if it has data layers"""
Expand Down
2 changes: 1 addition & 1 deletion glue/qt/widgets/dendro_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def __init__(self, session, parent=None):
self.ui = load_ui('dendrowidget', self.option_widget)
self.client = DendroClient(self._data,
self.central_widget.canvas.fig,
artist_container=self._container)
layer_artist_container=self._layer_artist_container)

self._connect()

Expand Down
6 changes: 3 additions & 3 deletions glue/qt/widgets/histogram_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def __init__(self, session, parent=None):
self._tweak_geometry()
self.client = HistogramClient(self._data,
self.central_widget.canvas.fig,
artist_container=self._container)
layer_artist_container=self._layer_artist_container)
self._init_limits()
self.make_toolbar()
self._connect()
Expand Down Expand Up @@ -130,7 +130,7 @@ def _update_attributes(self):

found = False
for d in self._data:
if d not in self._container:
if d not in self._layer_artist_container:
continue
item = QtGui.QStandardItem(d.label)
item.setData(_hash(d), role=Qt.UserRole)
Expand Down Expand Up @@ -225,7 +225,7 @@ def _remove_data(self, data):
pass

def data_present(self, data):
return data in self._container
return data in self._layer_artist_container

def register_to_hub(self, hub):
super(HistogramWidget, self).register_to_hub(hub)
Expand Down
2 changes: 1 addition & 1 deletion glue/qt/widgets/image_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ class ImageWidget(ImageWidgetBase):
def make_client(self):
return MplImageClient(self._data,
self.central_widget.canvas.fig,
artist_container=self._container)
layer_artist_container=self._layer_artist_container)

def make_central_widget(self):
return MplWidget()
Expand Down
2 changes: 1 addition & 1 deletion glue/qt/widgets/scatter_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def __init__(self, session, parent=None):

self.client = ScatterClient(self._data,
self.central_widget.canvas.fig,
artist_container=self._container)
layer_artist_container=self._layer_artist_container)

self._connect()
self.unique_fields = set()
Expand Down
2 changes: 1 addition & 1 deletion glue/qt/widgets/tests/test_histogram_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def assert_component_integrity(self, dc=None, widget=None):
combo = widget.ui.attributeCombo
row = 0
for data in dc:
if data not in widget._container:
if data not in widget._layer_artist_container:
continue
assert combo.itemText(row) == data.label
assert combo.itemData(row) == _hash(data)
Expand Down
2 changes: 1 addition & 1 deletion glue/qt/widgets/tests/test_image_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def fail():
assert False

self.widget.add_data(self.im)
self.widget._container.on_empty(fail)
self.widget._layer_artist_container.on_empty(fail)
self.widget.rgb_mode = True
self.widget.rgb_mode = False

Expand Down

0 comments on commit c4189ca

Please sign in to comment.