Writing a fully customized Qt viewer (advanced)¶
Motivation¶
The custom_viewer()
function and the
CustomViewer
class described in
Building Custom Data Viewers 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.
Displaying the widget in glue¶
Let’s imagine that you have a Qt widget class called MyWidget
the
inherits from QWidget
and implements a specific type of visualization you
are interested in:
class MyWidget(QWidget):
...
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
data_viewer
(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:
from glue.qt.widgets.data_viewer import DataViewer
class MyGlueWidget(DataViewer):
def __init__(self, session, parent=None):
super(MyGlueWidget, self).__init__(session, parent=parent)
self.my_widget = MyWidget()
self.setCentralWidget(self.my_widget)
# Register the viewer with glue
from glue.config import qt_client
qt_client.add(MyGlueWidget)
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:
To give your viewer a more meaningful name, you should give your class an
attribute called LABEL
:
class MyGlueWidget(DataViewer):
LABEL = "My first data viewer"
def __init__(self, session, parent=None):
super(MyGlueWidget, self).__init__(session, parent=parent)
self.my_widget = MyWidget()
self.setCentralWidget(self.my_widget)
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:
def add_data(self, data):
return True
Now you can open glue again, and this time you should be able to load a
dataset the usual way. When you drag this dataset onto the main canvas area,
you will be able to then select your custom viewer, and it should appear
(though the data itself will not). You can now expand the add_data
method
to actually add the data to MyWidget
, by accessing self.my_widget
,
for example:
def add_data(self, data):
self.my_widget.plot(data)
return True
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.
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 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:
class MyGlueWidget(DataViewer):
...
def __init__(self, session, parent=None):
...
self._options_widget = AnotherWidget(...)
...
def options_widget(self):
return self._options_widget
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¶
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:
from glue.core.message import DataCollectionAddMessage
class MyGlueWidget(DataViewer):
...
def register_to_hub(self, hub):
super(MyGlueWidget, self).register_to_hub(hub)
# Now we can subscribe to messages with the hub
hub.subscribe(self,
DataUpdateMessage,
handler=self._update_data)
def _update_data(self, msg):
# Process DataUpdateMessage here
Using layers¶
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 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.viewers.*.layer_artist
, but otherwise you will need
to create your own classes.
The minimal layer artist class looks like the following:
from glue.core.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
_layer_artist_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.