From 9d68bf48db97e009bbe9657bd3b9fb6df672bce5 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Tue, 29 Dec 2015 16:48:41 +0000 Subject: [PATCH 1/3] Create documentation about layer artists --- .../full_custom_qt_viewer.rst | 78 +++++++++++++++---- 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/doc/customizing_guide/full_custom_qt_viewer.rst b/doc/customizing_guide/full_custom_qt_viewer.rst index c687c48d1..d17fca626 100644 --- a/doc/customizing_guide/full_custom_qt_viewer.rst +++ b/doc/customizing_guide/full_custom_qt_viewer.rst @@ -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:: @@ -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 ------------------- @@ -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. \ No newline at end of file From a8c05bf9c2b199fc03168d6602482834562166cc Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 31 Dec 2015 11:59:59 +0000 Subject: [PATCH 2/3] Rename _container and artist_container to _layer_artist_container and layer_artist_container --- CHANGES.md | 3 +++ glue/clients/histogram_client.py | 4 ++-- glue/clients/image_client.py | 8 ++++---- glue/clients/scatter_client.py | 4 ++-- glue/clients/viz_client.py | 4 ++-- glue/core/application_base.py | 6 +++--- glue/plugins/ginga_viewer/client.py | 4 ++-- glue/plugins/ginga_viewer/qt_widget.py | 2 +- glue/qt/custom_viewer.py | 4 ++-- glue/qt/widgets/data_viewer.py | 10 +++++----- glue/qt/widgets/dendro_widget.py | 2 +- glue/qt/widgets/histogram_widget.py | 6 +++--- glue/qt/widgets/image_widget.py | 2 +- glue/qt/widgets/scatter_widget.py | 2 +- glue/qt/widgets/tests/test_histogram_widget.py | 2 +- glue/qt/widgets/tests/test_image_widget.py | 2 +- 16 files changed, 34 insertions(+), 31 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4ce156191..0f0799d41 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,9 @@ 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] + v0.6 (2015-11-20) ----------------- diff --git a/glue/clients/histogram_client.py b/glue/clients/histogram_client.py index 5422b28fc..a667adfb2 100644 --- a/glue/clients/histogram_client.py +++ b/glue/clients/histogram_client.py @@ -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 diff --git a/glue/clients/image_client.py b/glue/clients/image_client.py index d6b189f65..b7228f709 100644 --- a/glue/clients/image_client.py +++ b/glue/clients/image_client.py @@ -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() @@ -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") diff --git a/glue/clients/scatter_client.py b/glue/clients/scatter_client.py index 167804980..411971f0f 100644 --- a/glue/clients/scatter_client.py +++ b/glue/clients/scatter_client.py @@ -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 @@ -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() diff --git a/glue/clients/viz_client.py b/glue/clients/viz_client.py index 57a538df2..9cc193174 100644 --- a/glue/clients/viz_client.py +++ b/glue/clients/viz_client.py @@ -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() diff --git a/glue/core/application_base.py b/glue/core/application_base.py index fc2ff999d..b67ee50b3 100644 --- a/glue/core/application_base.py +++ b/glue/core/application_base.py @@ -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): @@ -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 @@ -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), diff --git a/glue/plugins/ginga_viewer/client.py b/glue/plugins/ginga_viewer/client.py index 589091909..707f4fd4a 100644 --- a/glue/plugins/ginga_viewer/client.py +++ b/glue/plugins/ginga_viewer/client.py @@ -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): diff --git a/glue/plugins/ginga_viewer/qt_widget.py b/glue/plugins/ginga_viewer/qt_widget.py index c0b309b96..a06d02aa8 100644 --- a/glue/plugins/ginga_viewer/qt_widget.py +++ b/glue/plugins/ginga_viewer/qt_widget.py @@ -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): diff --git a/glue/qt/custom_viewer.py b/glue/qt/custom_viewer.py index 2439bf15f..37cbb5703 100644 --- a/glue/qt/custom_viewer.py +++ b/glue/qt/custom_viewer.py @@ -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: @@ -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() diff --git a/glue/qt/widgets/data_viewer.py b/glue/qt/widgets/data_viewer.py index 7bf7e3f1e..d38d92250 100644 --- a/glue/qt/widgets/data_viewer.py +++ b/glue/qt/widgets/data_viewer.py @@ -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): @@ -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) @@ -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""" diff --git a/glue/qt/widgets/dendro_widget.py b/glue/qt/widgets/dendro_widget.py index 9b9d2e0bf..45062a349 100644 --- a/glue/qt/widgets/dendro_widget.py +++ b/glue/qt/widgets/dendro_widget.py @@ -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() diff --git a/glue/qt/widgets/histogram_widget.py b/glue/qt/widgets/histogram_widget.py index 0b5c037df..57f3fbecc 100644 --- a/glue/qt/widgets/histogram_widget.py +++ b/glue/qt/widgets/histogram_widget.py @@ -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() @@ -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) @@ -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) diff --git a/glue/qt/widgets/image_widget.py b/glue/qt/widgets/image_widget.py index f319634b1..a5e1dc1ce 100644 --- a/glue/qt/widgets/image_widget.py +++ b/glue/qt/widgets/image_widget.py @@ -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() diff --git a/glue/qt/widgets/scatter_widget.py b/glue/qt/widgets/scatter_widget.py index 3c28b57ab..63b721e00 100644 --- a/glue/qt/widgets/scatter_widget.py +++ b/glue/qt/widgets/scatter_widget.py @@ -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() diff --git a/glue/qt/widgets/tests/test_histogram_widget.py b/glue/qt/widgets/tests/test_histogram_widget.py index 9112ed6df..48c397b09 100644 --- a/glue/qt/widgets/tests/test_histogram_widget.py +++ b/glue/qt/widgets/tests/test_histogram_widget.py @@ -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) diff --git a/glue/qt/widgets/tests/test_image_widget.py b/glue/qt/widgets/tests/test_image_widget.py index f11bdea3f..f2b343097 100644 --- a/glue/qt/widgets/tests/test_image_widget.py +++ b/glue/qt/widgets/tests/test_image_widget.py @@ -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 From d600f867814df909c7f712388b24bab7734f6cfe Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 11 Jan 2016 11:47:38 +0000 Subject: [PATCH 3/3] Added entry to changelog [ci skip] --- CHANGES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 0f0799d41..2cc7e2ad3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,9 @@ v0.7 (unreleased) * 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) -----------------