7
###############################
8
Interactive plotting with Chaco
9
###############################
14
This tutorial is an introduction to Chaco. We're going to build several
15
mini-applications of increasing capability and complexity. Chaco was designed to
16
be used primarily by scientific programmers, and this tutorial requires only
17
basic familiarity with Python.
19
Knowledge of NumPy can be helpful for certain parts of the tutorial. Knowledge
20
of GUI programming concepts such as widgets, windows, and events are helpful
21
for the last portion of the tutorial, but it is not required.
23
This tutorial demonstrates using Chaco with Traits UI, so knowledge of the
24
Traits framework is also helpful. We don't use very many sophisticated aspects
25
of Traits or Traits UI, and it is entirely possible to pick it up as you go
26
through the tutorial. This tutorial applies to Enthought Tool Suite
29
It's also worth pointing out that you don't *have* to use Traits UI in order to
30
use Chaco --- you can integrate Chaco directly with Qt or wxPython --- but for
31
this tutorial, we use Traits UI to make things easier.
37
By the end of this tutorial, you will have learned how to:
39
- create plots of various types
41
- arrange plots in various layouts
43
- configure and dynamically modify your plots using Traits UI
45
- interact with plots using tools
47
- create custom, stateful tools that interact with mouse and keyboard
53
Chaco is a *plotting application toolkit*. This means that it can build
54
both static plots and dynamic data visualizations that let you
55
interactively explore your data. Here are four basic examples of Chaco plots:
57
.. image:: images/tornado.png
60
This plot shows a static "tornado plot" with a categorical Y axis and continuous
61
X axis. The plot is resizable, but the user cannot interact or explore the data
64
.. image:: images/simple_line.png
67
This is an overlaid composition of line and scatter plots with a legend. Unlike
68
the previous plot, the user can pan and zoom this plot, exploring the
69
relationship between data curves in areas that appear densely overlapping.
70
Furthermore, the user can move the legend to an arbitrary position on the plot,
71
and as they resize the plot, the legend maintains the same screen-space
72
separation relative to its closest corner.
74
.. image:: images/regression.png
77
This example starts to demonstrate interacting with the data set in an
78
exploratory way. Whereas interactivity in the previous example was limited to
79
basic pan and zoom (which are fairly common in most plotting libraries), this is
80
an example of a more advanced interaction that allows a level of data
81
exploration beyond the standard view manipulations.
83
With this example, the user can select a region of data space, and a simple
84
line fit is applied to the selected points. The equation of the line is
85
then displayed in a text label.
87
The lasso selection tool and regression overlay are both built in to Chaco,
88
but they serve an additional purpose of demonstrating how one can build complex
89
data-centric interactions and displays on top of the Chaco framework.
91
.. image:: ../images/scalar_function.png
94
This is a much more complex demonstration of Chaco's capabilities. The user
95
can view the cross sections of a 2-D scalar-valued function. The cross sections
96
update in real time as the user moves the mouse, and the "bubble" on each line
97
plot represents the location of the cursor along that dimension. By using
98
drop-down menus (not show here), the user can change plot attributes like the
99
colormap and the number of contour levels used in the center plot, as well as
100
the actual function being plotted.
103
Script-oriented plotting
104
========================
106
We distinguish between "static" plots and "interactive visualizations"
107
because these different applications of a library affect the structure
108
of how the library is written, as well as the code you write to use the
111
Here is a simple example of the "script-oriented" approach for creating
112
a static plot. This is probably familiar to anyone who has used Gnuplot,
113
MATLAB, or Matplotlib::
116
from chaco.shell import *
118
x = np.linspace(-2*pi, 2*pi, 100)
126
This creates this plot:
128
.. image:: images/script_oriented.png
131
The basic structure of this example is that we generate some data, then we call
132
functions to plot the data and configure the plot. There is a global concept of
133
"the active plot", and the functions do high-level manipulations on it. The
134
generated plot is then usually saved to disk for inclusion in a journal article
135
or presentation slides.
137
Now, as it so happens, this particular example uses the `chaco.shell`
138
script plotting package, so when you run this script, the plot that Chaco opens
139
does have some basic interactivity. You can pan and zoom, and even move forwards
140
and backwards through your zoom history. But ultimately it's a pretty static
144
.. _line_plot_example:
146
Application-oriented plotting
147
=============================
149
The second approach to plotting can be thought of as "application-oriented", for
150
lack of a better term. There is definitely a bit more code, and the plot
151
initially doesn't look much different, but it sets us up to do more interesting
152
things, as you will see later on::
154
from traits.api import HasTraits, Instance
155
from traitsui.api import View, Item
156
from chaco.api import Plot, ArrayPlotData
157
from enable.component_editor import ComponentEditor
158
from numpy import linspace, sin
160
class LinePlot(HasTraits):
161
plot = Instance(Plot)
164
Item('plot',editor=ComponentEditor(), show_label=False),
165
width=500, height=500, resizable=True, title="Chaco Plot")
168
super(LinePlot, self).__init__()
170
x = linspace(-14, 14, 100)
172
plotdata = ArrayPlotData(x=x, y=y)
174
plot = Plot(plotdata)
175
plot.plot(("x", "y"), type="line", color="blue")
176
plot.title = "sin(x) * x^3"
180
if __name__ == "__main__":
181
LinePlot().configure_traits()
184
This produces a plot similar to the previous script-oriented code snippet:
186
.. image:: images/first_plot.png
189
So, this is our first "real" Chaco plot. We will walk through this code and
190
look at what each bit does. This example serves as the basis for many of the
194
Application-oriented plotting, step by step
195
===========================================
197
Let's start with the basics. First, we declare a class to represent our
198
plot, called :class:`LinePlot`::
200
class LinePlot(HasTraits):
201
plot = Instance(Plot)
203
This class uses the Enthought Traits package, and all of our objects subclass
204
from :class:`HasTraits`.
206
Next, we declare a Traits UI View for this class::
209
Item('plot',editor=ComponentEditor(), show_label=False),
210
width=500, height=500, resizable=True, title="Chaco Plot")
212
Inside this view, we are placing a reference to the :attr:`plot` trait and
213
telling Traits UI to use the :class:`ComponentEditor` (imported from
214
:mod:`enable.component_editor`) to display it. If the
215
trait were an Int or Str or Float, Traits could automatically pick an
216
appropriate GUI element to display it. Since Traits UI doesn't natively know
217
how to display Chaco components, we explicitly tell it what kind of editor to
220
The other parameters in the :class:`View` constructor are pretty
221
self-explanatory, and the
222
`Traits UI User's Guide <http://code.enthought.com/projects/traits/docs/html/TUIUG/index.html>`_
223
documents all the various properties
224
you can set here. For our purposes, this Traits View is sort of boilerplate. It
225
gets us a nice little window that we can resize. We'll be using something like
226
this View in most of the examples in the rest of the tutorial.
228
Now, let's look at the constructor, where the real work gets done::
231
super(LinePlot, self).__init__()
232
x = linspace(-14, 14, 100)
234
plotdata = ArrayPlotData(x=x, y=y)
236
The first thing we do here is call the super-class's :meth:`__init__` method,
237
which ensures that all the Traits machinery is properly set up, even though the
238
:meth:`__init__` method is overridden. Then we create some mock data, just like
239
in the script-oriented approach. But rather than directly calling some sort of
240
plotting function to throw up a plot, we create this :class:`ArrayPlotData`
241
object and stick the data in there. The ArrayPlotData object is a simple
242
structure that associates a name with a NumPy array.
244
In a script-oriented approach to plotting, whenever you have to update the data
245
or tweak any part of the plot, you basically re-run the entire script. Chaco's
246
model is based on having objects representing each of the little pieces of a
247
plot, and they all use Traits events to notify one another that some attribute
248
has changed. So, the ArrayPlotData is an object that interfaces your
249
data with the rest of the objects in the plot. In a later example we'll see
250
how we can use the ArrayPlotData to quickly swap data items in and
251
out, without affecting the rest of the plot.
253
The next line creates an actual :class:`Plot` object, and gives it the
254
ArrayPlotData instance we created previously::
256
plot = Plot(plotdata)
258
Chaco's Plot object serves two roles: it is both a container of
259
renderers, which are the objects that do the actual task of transforming data
260
into lines and markers and colors on the screen, and it is a factory for
261
instantiating renderers. Once you get more familiar with Chaco, you can choose
262
to not use the Plot object, and instead directly create renderers and containers
263
manually. Nonetheless, the Plot object does a lot of nice housekeeping that is
264
useful in a large majority of use cases.
266
Next, we call the :meth:`plot` method on the Plot object we just created::
268
plot.plot(("x", "y"), type="line", color="blue")
270
This creates a blue line plot of the data items named "x" and "y". Note that
271
we are not passing in an actual array here; we are passing in the names of arrays
272
in the ArrayPlotData we created previously.
274
This method call creates a new renderer --- in this case a line renderer --- and
277
This may seem kind of redundant or roundabout to folks who are used to passing
278
in a pile of NumPy arrays to a plot function, but consider this:
279
ArrayPlotData objects can be shared between multiple Plots. If you
280
want several different plots of the same data, you don't have to externally
281
keep track of which plots are holding on to identical copies of what data, and
282
then remember to shove in new data into every single one of those plots. The
283
ArrayPlotData object acts almost like a symlink between consumers of data and
284
the actual data itself.
286
Next, we set a title on the plot::
288
plot.title = "sin(x) * x^3"
290
And then we set our :attr:`plot` trait to the new plot::
294
The last thing we do in this script is set up some code to run when the script
297
if __name__ == "__main__":
298
LinePlot().configure_traits()
300
This one-liner instantiates a LinePlot object and calls its
301
:meth:`configure_traits` method. This brings up a dialog with a traits editor for
302
the object, built up according to the View we created earlier. In our
303
case, the editor just displays our :attr:`plot` attribute using the
310
We can use the same pattern to build a scatter plot::
312
from traits.api import HasTraits, Instance
313
from traitsui.api import View, Item
314
from chaco.api import Plot, ArrayPlotData
315
from enable.component_editor import ComponentEditor
316
from numpy import linspace, sin
318
class ScatterPlot(HasTraits):
319
plot = Instance(Plot)
322
Item('plot',editor=ComponentEditor(), show_label=False),
323
width=500, height=500, resizable=True, title="Chaco Plot")
326
super(ScatterPlot, self).__init__()
328
x = linspace(-14, 14, 100)
330
plotdata = ArrayPlotData(x = x, y = y)
332
plot = Plot(plotdata)
333
plot.plot(("x", "y"), type="scatter", color="blue")
334
plot.title = "sin(x) * x^3"
338
if __name__ == "__main__":
339
ScatterPlot().configure_traits()
341
Note that we have only changed the *type* argument to the :meth:`plot.plot` call
342
and the name of the class from :class:`LinePlot` to :class:`ScatterPlot`. This
343
produces the following:
345
.. image:: images/scatter.png
351
Image plots can be created in a similar fashion::
353
from traits.api import HasTraits, Instance
354
from traitsui.api import View, Item
355
from chaco.api import Plot, ArrayPlotData, jet
356
from enable.component_editor import ComponentEditor
357
from numpy import exp, linspace, meshgrid
359
class ImagePlot(HasTraits):
360
plot = Instance(Plot)
363
Item('plot', editor=ComponentEditor(), show_label=False),
364
width=500, height=500, resizable=True, title="Chaco Plot")
367
super(ImagePlot, self).__init__()
369
x = linspace(0, 10, 50)
370
y = linspace(0, 5, 50)
371
xgrid, ygrid = meshgrid(x, y)
372
z = exp(-(xgrid*xgrid+ygrid*ygrid)/100)
373
plotdata = ArrayPlotData(imagedata = z)
375
plot = Plot(plotdata)
376
plot.img_plot("imagedata", colormap=jet)
380
if __name__ == "__main__":
381
ImagePlot().configure_traits()
384
There are a few more steps to create the input Z data, and we also call a
385
different method on the Plot object --- :meth:`img_plot` instead of
386
:meth:`plot`. The details of the method parameters are not that important
387
right now; this is just to demonstrate how we can apply the same basic pattern
388
from the "first plot" example above to do other kinds of plots.
390
.. image:: images/image_plot.png
397
Earlier we said that the Plot object is both a container of renderers and a
398
factory (or generator) of renderers. This modification of the previous example
399
illustrates this point. We only create a single instance of Plot, but we call
400
its :meth:`plot()` method twice. Each call creates a new renderer and adds it to
401
the Plot object's list of renderers. Also notice that we are reusing the *x*
402
array from the ArrayPlotData::
404
from traits.api import HasTraits, Instance
405
from traitsui.api import View, Item
406
from chaco.api import Plot, ArrayPlotData
407
from enable.component_editor import ComponentEditor
408
from numpy import cos, linspace, sin
410
class OverlappingPlot(HasTraits):
412
plot = Instance(Plot)
415
Item('plot',editor=ComponentEditor(), show_label=False),
416
width=500, height=500, resizable=True, title="Chaco Plot")
419
super(OverlappingPlot, self).__init__()
421
x = linspace(-14, 14, 100)
424
plotdata = ArrayPlotData(x=x, y=y, y2=y2)
426
plot = Plot(plotdata)
427
plot.plot(("x", "y"), type="scatter", color="blue")
428
plot.plot(("x", "y2"), type="line", color="red")
432
if __name__ == "__main__":
433
OverlappingPlot().configure_traits()
435
This code generates the following plot:
437
.. image:: images/overlapping_plot.png
444
So far we've only seen single plots, but frequently we need to plot data side
445
by side. Chaco uses various subclasses of :class:`Container` to do layout.
446
Horizontal containers (:class:`HPlotContainer`) place components horizontally:
448
.. image:: images/hplotcontainer.png
451
Vertical containers (:class:`VPlotContainer`) array component vertically:
453
.. image:: images/vplotcontainer.png
456
Grid container (:class:`GridPlotContainer`) lays plots out in a grid:
458
.. image:: images/gridcontainer.png
461
Overlay containers (:class:`OverlayPlotContainer`) just overlay plots on top of
464
.. image:: images/simple_line.png
467
You've actually already seen OverlayPlotContainer --- the Plot
468
class is actually a special subclass of OverlayPlotContainer. All of
469
the plots inside this container appear to share the same X- and Y-axis, but this
470
is not a requirement of the container. For instance, the following plot shows
471
plots sharing only the X-axis:
473
.. image:: images/multiyaxis.png
480
Containers can have any Chaco component added to them. The following code
481
creates a separate Plot instance for the scatter plot and the line
482
plot, and adds them both to the HPlotContainer object::
484
from traits.api import HasTraits, Instance
485
from traitsui.api import View, Item
486
from chaco.api import HPlotContainer, ArrayPlotData, Plot
487
from enable.component_editor import ComponentEditor
488
from numpy import linspace, sin
490
class ContainerExample(HasTraits):
492
plot = Instance(HPlotContainer)
494
traits_view = View(Item('plot', editor=ComponentEditor(), show_label=False),
495
width=1000, height=600, resizable=True, title="Chaco Plot")
498
super(ContainerExample, self).__init__()
500
x = linspace(-14, 14, 100)
502
plotdata = ArrayPlotData(x=x, y=y)
504
scatter = Plot(plotdata)
505
scatter.plot(("x", "y"), type="scatter", color="blue")
507
line = Plot(plotdata)
508
line.plot(("x", "y"), type="line", color="blue")
510
container = HPlotContainer(scatter, line)
511
self.plot = container
513
if __name__ == "__main__":
514
ContainerExample().configure_traits()
517
This produces the following plot:
519
.. image:: images/container_example.png
523
There are many parameters you can configure on a container, like background
524
color, border thickness, spacing, and padding. We insert some more
525
lines between lines 20 and 21 of the previous example to make the two plots
528
.. code-block:: python
530
container = HPlotContainer(scatter, line)
531
container.spacing = 0
533
scatter.padding_right = 0
535
line.padding_left = 0
536
line.y_axis.orientation = "right"
538
self.plot = container
540
Something to note here is that all Chaco components have both bounds and
541
padding (or margin). In order to make our plots touch, we need to zero out the
542
padding on the appropriate side of each plot. We also move the Y-axis for the
543
line plot (which is on the right hand side) to the right side.
545
This produces the following:
547
.. image:: images/container_nospace.png
551
Dynamically changing plots
552
==========================
554
So far, the stuff you've seen is pretty standard: building up a plot of some
555
sort and doing some layout on them. Now we start taking advantage
556
of the underlying framework.
558
Chaco is written using Traits. This means that all the graphical bits you
559
see --- and many of the bits you don't see --- are all objects with various
560
traits, generating events, and capable of responding to events.
562
We're going to modify our previous ScatterPlot example to demonstrate some
563
of these capabilities. Here is the full listing of the modified code::
565
from traits.api import HasTraits, Instance, Int
566
from traitsui.api import View, Group, Item
567
from enable.api import ColorTrait
568
from enable.component_editor import ComponentEditor
569
from chaco.api import marker_trait, Plot, ArrayPlotData
570
from numpy import linspace, sin
572
class ScatterPlotTraits(HasTraits):
574
plot = Instance(Plot)
575
color = ColorTrait("blue")
576
marker = marker_trait
580
Group(Item('color', label="Color", style="custom"),
581
Item('marker', label="Marker"),
582
Item('marker_size', label="Size"),
583
Item('plot', editor=ComponentEditor(), show_label=False),
584
orientation = "vertical"),
585
width=800, height=600, resizable=True, title="Chaco Plot")
588
super(ScatterPlotTraits, self).__init__()
590
x = linspace(-14, 14, 100)
592
plotdata = ArrayPlotData(x = x, y = y)
594
plot = Plot(plotdata)
596
self.renderer = plot.plot(("x", "y"), type="scatter", color="blue")[0]
599
def _color_changed(self):
600
self.renderer.color = self.color
602
def _marker_changed(self):
603
self.renderer.marker = self.marker
605
def _marker_size_changed(self):
606
self.renderer.marker_size = self.marker_size
608
if __name__ == "__main__":
609
ScatterPlotTraits().configure_traits()
612
Let's step through the changes.
614
First, we add traits for color, marker type, and marker size::
616
class ScatterPlotTraits(HasTraits):
617
plot = Instance(Plot)
618
color = ColorTrait("blue")
619
marker = marker_trait
622
We also change our Traits UI View to include references to these
623
new traits. We put them in a Traits UI :class:`Group` so that we can control
624
the layout in the dialog a little better --- here, we're setting the layout
625
orientation of the elements in the dialog to "vertical". ::
629
Item('color', label="Color", style="custom"),
630
Item('marker', label="Marker"),
631
Item('marker_size', label="Size"),
632
Item('plot', editor=ComponentEditor(), show_label=False),
633
orientation = "vertical" ),
634
width=500, height=500, resizable=True,
637
Now we have to do something with those traits. We modify the
638
constructor so that we grab a handle to the renderer that is created by
639
the call to :meth:`plot`::
641
self.renderer = plot.plot(("x", "y"), type="scatter", color="blue")[0]
643
Recall that a Plot is a container for renderers and a factory for them. When
644
called, its :meth:`plot` method returns a list of the renderers that the call
645
created. In previous examples we've been just ignoring or discarding the return
646
value, since we had no use for it. In this case, however, we grab a
647
reference to that renderer so that we can modify its attributes in later
650
The :meth:`plot` method returns a list of renderers because for some values
651
of the *type* argument, it will create multiple renderers. In our case here,
652
we are just doing a scatter plot, and this creates just a single renderer.
654
Next, we define some Traits event handlers. These are specially-named
655
methods that are called whenever the value of a particular trait changes. Here
656
is the handler for :attr:`color` trait::
658
def _color_changed(self):
659
self.renderer.color = self.color
661
This event handler is called whenever the value of :attr:`self.color` changes,
662
whether due to user interaction with a GUI, or due to code elsewhere. (The
663
Traits framework automatically calls this method because its name follows the
664
name template of :samp:`\_{traitname}_changed`.) Since this method is called
665
after the new value has already been updated, we can read out the new value just
666
by accessing :attr:`self.color`. We just copy the color to the scatter renderer.
667
You can see why we needed to hold on to the renderer in the constructor.
669
Now we do the same thing for the marker type and marker size traits::
671
def _marker_changed(self):
672
self.renderer.marker = self.marker
674
def _marker_size_changed(self):
675
self.renderer.marker_size = self.marker_size
677
Running the code produces an app that looks like this:
679
.. image:: images/traits.png
682
Depending on your platform, the color editor/swatch at the top may look different.
683
This is how it looks on Mac OS X. All of the controls here are "live". If you
684
modify them, the plot updates.
687
.. _data_chooser_example:
689
Dynamically changing plot content
690
=================================
692
Traits are not just useful for tweaking visual features. For instance, you can
693
use them to select among several data items. This next example is based on
694
the earlier :ref:`LinePlot example <line_plot_example>`, and we’ll walk through the modifications: ::
696
from scipy.special import jn
698
class DataChooser(HasTraits):
700
plot = Instance(Plot)
702
data_name = Enum("jn0", "jn1", "jn2")
705
Item('data_name', label="Y data"),
706
Item('plot', editor=ComponentEditor(), show_label=False),
707
width=800, height=600, resizable=True,
708
title="Data Chooser")
711
x = linspace(-5, 10, 100)
713
# jn is the Bessel function
714
self.data = {"jn0": jn(0, x),
718
self.plotdata = ArrayPlotData(x = x, y = self.data["jn0"])
720
plot = Plot(self.plotdata)
721
plot.plot(("x", "y"), type="line", color="blue")
724
def _data_name_changed(self):
725
self.plotdata.set_data("y", self.data[self.data_name])
728
First, we add an Enumeration trait to select a particular data name ::
730
data_name = Enum("jn0", "jn1", "jn2")
732
and a corresponding ``Item`` in the Traits UI View ::
734
Item('data_name', label="Y data")
736
By default, an ``Enum`` trait will be displayed as a drop-down. In the
737
constructor, we create a dictionary that maps the data names to actual
740
# jn is the Bessel function
741
self.data = {“jn0”: jn(0, x),
745
When we initialize the ArrayPlotData, we’ll set ``y`` to the ``jn0`` array. ::
747
self.plotdata = ArrayPlotData(x = x, y = self.data[“jn0”])
748
plot = Plot(self.plotdata)
750
Note that we are storing a reference to the ``plotdata`` object.
751
In previous examples, there was no need to keep a reference around (except
752
for the one stored inside the Plot object).
754
Finally, we create an event handler for the “data_name” Trait. Any time the
755
``data_name`` trait changes, we’re going to look it up in the ``self.data``
756
dictionary, and push that value into the ``y`` data item in ``ArrayPlotData``. ::
758
def _data_name_changed(self):
759
self.plotdata.set_data("y", self.data[self.data_name])
761
Note that there is no actual copying of data here, we’re just passing around
764
The final plot looks like this:
766
.. image:: images/data_chooser_example.png
770
.. _connected_plots_example:
775
One of the features of Chaco’s architecture is that all the underlying
776
components of a plot are live objects, connected via events.
777
In the next set of examples, we’ll look at how to hook some of those up.
779
First, we are going to make two separate plots look at the same data
780
space region. This is the full code::
782
class ConnectedRange(HasTraits):
784
container = Instance(HPlotContainer)
786
traits_view = View(Item('container', editor=ComponentEditor(),
788
width=1000, height=600, resizable=True,
789
title="Connected Range")
792
x = linspace(-14, 14, 100)
794
plotdata = ArrayPlotData(x = x, y = y)
796
scatter = Plot(plotdata)
797
scatter.plot(("x", "y"), type="scatter", color="blue")
799
line = Plot(plotdata)
800
line.plot(("x", "y"), type="line", color="blue")
802
self.container = HPlotContainer(scatter, line)
804
scatter.tools.append(PanTool(scatter))
805
scatter.tools.append(ZoomTool(scatter))
807
line.tools.append(PanTool(line))
808
line.tools.append(ZoomTool(line))
810
scatter.range2d = line.range2d
813
First, we define a "horizontal" container that displays the plots side
816
container = Instance(HPlotContainer)
818
traits_view = View(Item('container', editor=ComponentEditor(),
820
width=1000, height=600, resizable=True,
821
title="Connected Range")
824
In the constructor, we define some data and create two plots of it,
825
a line plot and a scatter plot, insert them in the container, and add
826
pan and zoom tools to both.
828
The most important part of the code is the last line of the constructor::
830
scatter.range2d = line.range2d
832
Chaco has a concept of *data range* to express bounds in data space.
833
There are a series of objects representing this concept.
834
The standard 2D plots that we have considered so far all
835
have a two-dimensional range on them.
837
In this line, we are replacing the range on the scatter plot
838
with the range from the line plot. The two plots now share the same
839
range object, **and will change together in response to
840
changes to the data space bounds**. For example, panning
841
or zooming one of the plots
842
will result in the same transformation in the other:
844
.. image:: images/connected_range_example.png
848
Plot orientation, index and value
849
=================================
851
We can modify the :ref:`connected plots example <connected_plots_example>`
852
such that the two plots only share one of the axes. The 2D data range
853
trait is actually composed of two 1D data ranges, and we can access them
854
independently. So to link up the x-axes we can substitute the line ::
856
scatter.range2d = line.range2d
860
scatter.index_range = line.index_range
862
Now the plot can move independently on the y-axis and are link on the x-axis.
864
You may have notices that we referred to the x-axis range as *index* range.
865
The terms *index* and *value* are quite common in Chaco:
866
As it is possible to easily change the orientation of most Chaco plots,
867
we want some way to differentiate between the abscissa and the ordinate axes.
868
If we just stuck with *x* and *y*, things would get pretty confusing after
869
a change in orientation, as one would now, for instance, change the y-axis
870
by referring to it as ``x_range``.
872
Instead, in Chaco we refer to the data domain as *index*, and to the co-domain
873
(the set of possible values) as *value*.
875
To illustrate how flexible this concept is, we can switch the orientation
876
of the line plot by substituting ::
878
line = Plot(plotdata)
882
line = Plot(plotdata, orientation="v", default_origin="top left")
884
The ``default_origin`` parameter sets the index axis to be increasing
885
downwards. As a result of these changes, now changes to the
886
scatter plot index axis (the *x* axis) produces equivalent changes in the
887
line plot index axis (the *y* axis):
889
.. image:: images/connected_index_example.png
896
Chaco components can also be connected beyond the boundary of a single window.
897
We will again modify the :ref:`LinePlot example <line_plot_example>`. This
898
time, we will create a scatter plot and a line plot with connected ranges
899
in different windows.
901
First of all, we define a Traits UI view of a customizable plot.
902
This is the full code that we will analyze step by step below ::
904
class PlotEditor(HasTraits):
906
plot = Instance(Plot)
908
plot_type = Enum("scatter", "line")
910
orientation = Enum("horizontal", "vertical")
912
traits_view = View(Item('orientation', label="Orientation"),
913
Item('plot', editor=ComponentEditor(),
915
width=500, height=500, resizable=True,
918
def __init__(self, *args, **kw):
919
super(PlotEditor, self).__init__(*args, **kw)
921
x = linspace(-14, 14, 100)
923
plotdata = ArrayPlotData(x = x, y = y)
925
plot = Plot(plotdata)
926
plot.plot(("x", "y"), type=self.plot_type, color="blue")
928
plot.tools.append(PanTool(plot))
929
plot.tools.append(ZoomTool(plot))
933
def _orientation_changed(self):
934
if self.orientation == "vertical":
935
self.plot.orientation = "v"
937
self.plot.orientation = "h"
940
The plot defines two traits, one for the plot type (scatter of line plot) ::
942
plot_type = Enum("scatter", "line")
944
and one for the orientation of the plot ::
946
orientation = Enum("horizontal", "vertical")
948
The ``plot_type`` trait will not be exposed in the UI, but we add a
949
Traits UI item for the orientation: ::
951
traits_view = View(Item('orientation', label="Orientation"), ...)
953
Since the ``orientation`` trait is an Enum, this will appear as a drop-down
956
The constructor is very similar to the one used in the previous examples,
957
except that we create a new plot of the type specified in the ``plot_type``
960
plot.plot(("x", "y"), type=self.plot_type, color="blue")
962
Finally, we wrote a Trait event handler for the ``orientation`` trait,
963
which changes the orientation of the plot as required: ::
965
def _orientation_changed(self):
966
if self.orientation == "vertical":
967
self.plot.orientation = "v"
969
self.plot.orientation = "h"
972
The :class:`PlotEditor` represents one window. When running the application,
973
we can easily create two separate windows, and connect their axes in
976
if __name__ == "__main__":
978
# create two plots, one of type "scatter", one of type "line"
979
scatter = PlotEditor(plot_type = "scatter")
980
line = PlotEditor(plot_type = "line")
982
# connect the axes of the two plots
983
scatter.plot.range2d = line.plot.range2d
987
scatter.configure_traits()
989
In the last two lines, we open Traits UI editors on both objects.
990
Note that we call :meth:`edit_traits()` on the first object,
991
and :meth:`configure_traits()` on the second object.
992
The technical reason for this is that :meth:`configure_traits()`
993
will start the wxPython main loop (thereby blocking the script until the
994
window is closed), whereas :meth:`edit_traits()` will not. Thus, when
995
opening multiple windows, we would call :meth:`edit_traits()`
996
on all but the last one.
998
Here is a screenshot of the two windows in action:
1000
.. image:: images/connected_windows_example.png
1004
Plot tools: adding interactions
1005
===============================
1007
An important feature of Chaco is that it is possible to write re-usable
1008
tools to interact directly with the plots.
1010
Chaco takes a modular approach to interactivity. Instead of begin hard-coded
1011
into specific plot types or plot renderers,
1012
the interaction logic is factored out into classes we call *tools*.
1013
An advantage of this approach is that we can add new plot types
1014
and container types and still use the old interactions, as long as we
1015
adhere to certain basic interfaces.
1017
Thus far, none of the example plots we’ve built are truly interactive,
1018
e.g., you cannot pan or zoom them. In the next example, we will modify
1019
the :ref:`LinePlot example <line_plot_example>` so that we can pan and zoom. ::
1021
from chaco.tools.api import PanTool, ZoomTool, DragZoom
1023
class ToolsExample(HasTraits):
1025
plot = Instance(Plot)
1028
Item('plot',editor=ComponentEditor(), show_label=False),
1029
width=500, height=500,
1034
x = linspace(-14, 14, 100)
1036
plotdata = ArrayPlotData(x = x, y = y)
1037
plot = Plot(plotdata)
1038
plot.plot(("x", "y"), type="line", color="blue")
1040
# append tools to pan, zoom, and drag
1041
plot.tools.append(PanTool(plot))
1042
plot.tools.append(ZoomTool(plot))
1043
plot.tools.append(DragZoom(plot, drag_button="right"))
1048
The example illustrates the general usage pattern: we create a new instance of
1049
a Tool, giving it a reference
1050
to the Plot, and then we append that tool to a list of tools on the plot.
1051
This looks a little redundant, but there is a reason why the tools
1052
need a reference back to the plot: the tools use methods and attributes
1054
to transform and interpret the events that it receives, as well as act
1055
on those events. Most tools will also modify the attributes on the plot.
1056
The pan and zoom tools, for instance, modify the data ranges on the
1057
component handed in to it.
1059
Dynamically controlling interactions
1060
====================================
1062
One of the nice things about having interactivity bundled up into modular
1063
tools is that one can dynamically control when the interaction are allowed
1064
and when they are not.
1066
We will modify the previous example so that we can externally control
1067
what interactions are available on a plot.
1069
First, we add a new trait to hold a list of names of the tools.
1070
This is similar to adding a list of data items
1071
in the :ref:`DataChooser example <data_chooser_example>`.
1072
However, instead of a drop-down (which is the default editor
1073
for an Enumeration trait), we tell Traits that we would like a
1074
check list by creating a :class:`CheckListEditor`, so that we will be able
1075
to select multiple tools. We give the CheckListEditor a list of possible
1076
values, which are just the names of the tools. Notice that these are
1077
strings, and not the tool classes themselves.
1079
.. code-block:: python
1082
from enthought.traits.ui.api import CheckListEditor
1084
class ToolsExample(HasTraits):
1086
plot = Instance(Plot)
1088
tools = List(editor=CheckListEditor(values = ["PanTool",
1089
"SimpleZoom", "DragZoom"]))
1092
In the constructor, we do not add the interactive tools:
1094
.. code-block:: python
1098
x = linspace(-14, 14, 100)
1100
plotdata = ArrayPlotData(x = x, y = y)
1101
plot = Plot(plotdata)
1102
plot.plot(("x", "y"), type="line", color="blue")
1105
Instead, we write a trait event handler for the ``tools`` trait:
1107
.. code-block:: python
1110
def _tools_changed(self):
1111
classes = [eval(class_name) for class_name in self.tools]
1113
# Remove all tools from the plot
1114
plot_tools = self.plot.tools
1115
for tool in plot_tools:
1116
plot_tools.remove(tool)
1118
# Create new instances for the selected tool classes
1120
self.plot.tools.append(cls(self.plot))
1124
classes = [eval(class_name) for class_name in self.tools]
1126
converts the value of the ``tools`` trait (a string) to a Tool class. In the
1127
of the method, we remove all the existing tools from the plot ::
1129
# Remove all tools from the plot
1130
plot_tools = self.plot.tools
1131
for tool in plot_tools:
1132
plot_tools.remove(tool)
1134
and create new ones for the selected items: ::
1136
# Create new instances for the selected tool classes
1138
self.plot.tools.append(cls(self.plot))
1141
Here is a screenshot of the final result:
1143
.. image:: images/tool_chooser_example.png
1147
Writing a custom tool
1148
=====================
1150
It is easy to extend and customize the Chaco framework:
1151
the main Chaco components define clear interfaces, so one can write a
1152
custom plot or tool, plug it in, and it will play well with the existing
1155
Our next step is to write a simple, custom tool that will
1156
print out the position on the plot under the mouse cursor.
1157
This can be done in just a few lines: ::
1159
from enable.api import BaseTool
1161
class CustomTool(BaseTool):
1162
def normal_mouse_move(self, event):
1163
print "Screen point:", event.x, event.y
1165
:class:`BaseTool` is an abstract class that forms the interface for tools.
1166
It defines a set of methods that are called for the
1167
most common mouse and keyboard events. In this case, we define a callback
1168
for the ``mouse_move`` event. The prefix ``normal`` indicated the
1169
state of the tool, which we will cover next.
1171
All events have an ``x`` and a ``y`` position, and our custom tools is
1172
just going to print it out.
1174
.. image:: images/custom_tool_example.png
1177
Other event callbacks correspond to mouse gestures (``mouse_enter``,
1178
``mouse_leave``, ``mouse_wheel``), mouse clicks (``left_down``, ``left_up``,
1179
``right_down``, ``right_up``), and key presses (``key_pressed``).
1184
Chaco tools are stateful. You can think of them as state machines that
1185
toggle states based on the events they receive. All tools have at least
1186
one state, called "normal". That is why the callback in the previous
1187
example began with the prefix ``normal_``.
1189
Our next tool is going to have two states, "normal" and "mousedown".
1190
We are going to enter the "mousedown" state when we detect a "left down"
1191
event, and we will exit that state when we detect a "left up" event: ::
1193
class CustomTool(BaseTool):
1195
event_state = Enum("normal", "mousedown")
1197
def normal_mouse_move(self, event):
1198
print "Screen:", event.x, event.y
1200
def normal_left_down(self, event):
1201
self.event_state = "mousedown"
1202
event.handled = True
1204
def mousedown_left_up(self, event):
1205
self.event_state = "normal"
1206
event.handled = True
1208
Every event has a ``handled`` boolean attribute that can be set to announce
1209
that it has been taken care of. Handled events are not propagated further.
1211
So far, the custom tool would stop printing to screen while the left mouse
1212
button is pressed. This is because while the tools is in the "mousedown" state,
1213
a mouse move event looks for a ``mousedown_mouse_move`` callback method.
1214
We can write an implementation for it that maps the screen coordinates in
1217
.. code-block:: python
1219
def mousedown_mouse_move(self, event):
1220
print "Data:", self.component.map_data((event.x, event.y))
1222
The ``self.component`` attribute contains a reference to the underlying
1223
plot. This is why tools need to be given a reference to a plot when
1224
they are constructed: almost all tools need to use some capabilities
1225
(like ``map_data``) of the components for which they are receiving events.
1228
.. image:: images/custom_tool_stateful_example.png
1235
This concludes this tutorial. For further information, please refer
1236
to the :ref:`Resources` page, or visit the :ref:`User guide`.
1239
*This tutorial is based on the "Interactive plotting with Chaco" tutorial
1240
that was presented by Peter Wang at Scipy 2008*