Author: Eric A. Meyer

# Data Exploration with Chaco

# Structure of today's tutorial

- Overview of Chaco
- Basic plots and interactors
- Creating simple interactors
- Core concepts: data model, layout, interaction model
- More complex plots
- Walkthrough of some examples

# Overview

Chaco is a plot application toolkit for Python. You use it to build stand-alone plotting applications, or embed it inside any application that needs to visualize numerical data.

Sample plotting applications:

- batch plotting of data (csv -> png)
- display for realtime data acquisition
- visual plot construction kit
- visual editor for tweaking input parameters to simulations
- mapping and GIS applications

# Chaco Features

- Different rendering layers, backbuffering
- Container model for layout and event dispatch
- Modular and extensible architecture
- Data model/filtering pipeline
- Vector drawing engine

# A first look

# tutorial1.py # Create some data from scipy import arange, pi, sin numpoints = 100 step = 4*pi / numpoints x = arange(-2*pi, 2*pi+step/2, step) y = sin(x) # Create the plot and set its size from enthought import chaco myplot = chaco.create_line_plot((x,y), bgcolor="white", add_grid=True, add_axis=True) myplot.padding = 50 myplot.bounds = [400,400] # Create a graphics context for offline rendering plot_gc = chaco.PlotGraphicsContext(myplot.outer_bounds) plot_gc.render_component(myplot) plot_gc.save("tutorial1.png")

# tutorial1.py

# Creating a window

# tutorial2.py import wx from enthought import chaco # Import the plot from Tutorial 1 from tutorial1 import myplot from enthought.enable2.wx_backend.api import Window class PlotFrame(wx.Frame): def __init__(self, *args, **kw): kw["size"] = (600,600) wx.Frame.__init__( *(self,) + args, **kw ) # Use Enable.Window to bridge between the vector-drawing world of Chaco and WX self.plot_window = Window(parent=self, component=myplot) sizer = wx.BoxSizer(wx.HORIZONTAL) sizer.Add(self.plot_window.control, 1, wx.EXPAND) self.SetSizer(sizer) self.SetAutoLayout(True) self.Show(True) return

# Opening the window

## Normal python (standalone app)

from tutorial2 import PlotFrame import wx app = wx.PySimpleApp() frame = PlotFrame(None) app.MainLoop()

## From within IPython

# This requires invoking IPython as "ipython -pylab" or "ipython -wthread" from tutorial2 import PlotFrame frame = PlotFrame(None)

# Fun from within IPython

We can tweak plot attributes on the fly: plot.vgrid.visible = False plot.x_axis.visible = False plot.request_redraw()

# Fun from within IPython

We can even write functions to do the tweaking for us! # tutorial2_ipython.py def ytitle(text): plot.y_axis.title = text plot.request_redraw() def xrange(low, high): plot.x_mapper.range.low = low plot.x_mapper.range.high = high plot.request_redraw() def yrange(low, high): plot.y_mapper.range.low = low plot.y_mapper.range.high = high plot.request_redraw()

# The Joy of Traits

Since all Chaco primitives use traits, we can easily bring up property sheets.

# Adding a basic interactor

All Chaco components can have tools on them, and any events they receive get forwarded on to their tools. # tutorial3.py from tutorial2 import myplot, PlotFrame, main from enthought.chaco.tools import PanTool myplot.tools.append(PanTool(myplot))

# Adding zoom

Because the zoom tool draws a visible "zoom box", we need to add it to the list of overlays instead of the list of tools. (It will still get events.) # tutorial4.py from tutorial2 import myplot, PlotFrame, main from enthought.chaco.tools import SimpleZoom # Create an instance of the tool and add it as an overlay.. myplot.overlays.append(SimpleZoom(myplot, tool_mode="box", always_on=True))

# Coordinating different tools

PanTool and SimpleZoom both key off the left mouse click/drag. Fortunately, PanTool is not a "stateful" interaction, and there is a way to tell SimpleZoom to play nicely with others: # tutorial5.py from tutorial2 import myplot, PlotFrame, main from enthought.chaco.tools import PanTool, SimpleZoom myplot.tools.append(PanTool(myplot)) zoom = SimpleZoom(myplot, tool_mode="box", always_on=False) myplot.overlays.append(zoom)

# Writing our first interactor

Diagnostic tool to dump out the events we are getting: # tutorial6.py # AbstractController is the base class for non-rendering tools from enthought.chaco.api import AbstractController class EventPrinter(AbstractController): def dispatch(self, event, suffix): # event' is a MouseEvent or KeyEvent object from the enable2 # package, and suffix is a text string print suffix, "event received at (%d,%d)" % (event.x, event.y) from tutorial2 import myplot, PlotFrame, main myplot.tools.append(EventPrinter(myplot))

# Looking at data

We don't really want to know the mouse position; we want to know about coordinates in data space: # tutorial7.py from enthought.chaco.api import AbstractController class DataPrinter(AbstractController): def dispatch(self, event, suffix): # We are assuming that self.component is an X-Y plot x = self.component.x_mapper.map_data(event.x) y = self.component.y_mapper.map_data(event.y) print suffix, "event received at (%d,%d)" % (x, y) from tutorial2 import myplot, PlotFrame, main myplot.tools.append(DataPrinter(myplot))

# Digging deeper

# from tutorial1.py myplot = chaco.create_line_plot((x,y)) What does the create_line_plot() actually do?

- creates datasources
- creates range objects and mappers
- creates the plot object

# Datasources

# plot_factory.py:create_line_plot() index_data, value_data = transpose(data) index = ArrayDataSource(index_data) value = ArrayDataSource(value_data, sort_order="none") ArrayDataSource is just a subclass of AbstractDataSource which works with Numeric/numpy arrays. class AbstractDataSource(HasTraits): value_dimension = DimensionTrait index_dimension = DimensionTrait metadata = Dict get_data() -> array get_data_mask() -> array, mask is_masked() -> bool get_size() -> int get_bounds() -> array

# Ranges, Mappers, and the Plot

index_range = DataRange() index_range.add(index) index_mapper = LinearMapper(range=index_range) value_range = DataRange() value_range.add(value) value_mapper = value_mapper_class(range=value_range) plot = LinePlot(index=index, value=value, index_mapper = index_mapper, value_mapper = value_mapper, orientation = orientation, color = color, line_width = width, line_style = dash, border_visible = True)

# Two plots

We can use a container to put two plots on the screen. def _create_plot(self): x = arange(-5.0, 15.0, 20.0/100) y = jn(0, x) left_plot = create_line_plot((x,y), bgcolor="white",add_grid=True, add_axis=True) left_plot.tools.append(PanTool(left_plot)) self.left_plot = left_plot y = jn(1, x) right_plot = create_line_plot((x,y), bgcolor="white", add_grid=True, add_axis=True) right_plot.tools.append(PanTool(right_plot)) right_plot.y_axis.orientation = "right" self.right_plot = right_plot container = HPlotContainer(spacing=20, padding=50, bgcolor="lightgray") container.add(left_plot) container.add(right_plot) return container

# Connecting the two plots

We're going to link the X dimension of the two plots. Really, all we have to do to achieve this is to set the horizontal range on the two plots to be the same. # tutorial9.py class PlotFrame2(PlotFrame): def _create_plot(self): container = super(PlotFrame2, self)._create_plot() self.right_plot.index_mapper.range = self.left_plot.index_mapper.range return container

# Connecting the two plots (cont.)

We can connect the Y dimension, too. And let's throw in a zoom tool. # tutorial9b.py self.right_plot.value_mapper.range = self.left_plot.value_mapper.range self.left_plot.overlays.append( SimpleZoom(self.left_plot, tool_mode="box", always_on=False)) self.right_plot.overlays.append( SimpleZoom(self.right_plot, tool_mode="box", always_on=False))

# Linked views, but not connected data

Adding a line inspector will show the problem. # tutorial10.py from tutorial9b import PlotFrame2 class PlotFrame3(PlotFrame2): def _create_plot(self): container = super(PlotFrame2, self)._create_plot() self.left_plot.overlays.append( LineInspector(component=self.left_plot, write_metadata=True, is_listener=True)) self.right_plot.overlays.append( LineInspector(component=self.right_plot, write_metadata=True, is_listener=True)) return container

# Connecting the data

# tutorial10b.py class PlotFrame3(PlotFrame2): def _create_plot(self): container = super(PlotFrame2, self)._create_plot() self.left_plot.overlays.append( LineInspector(component=self.left_plot, write_metadata=True, is_listener=True)) self.right_plot.overlays.append( LineInspector(component=self.right_plot, write_metadata=True, is_listener=True)) self.right_plot.index = self.left_plot.index return container

# Why Index-Value instead of X-Y?

Because you are then immune to a plot's physical layout. # tutorial11.py class PlotFrame4(PlotFrame3): def _create_plot(self): container = super(PlotFrame4, self)._create_plot() plot = self.right_plot plot.orientation = "v" plot.hgrid.mapper = plot.index_mapper plot.vgrid.mapper = plot.value_mapper plot.y_axis.mapper = plot.index_mapper plot.x_axis.mapper = plot.value_mapper self.left_plot.overlays.append( LineInspector(component=self.left_plot, write_metadata=True, is_listener=True, color="blue", axis="value")) self.right_plot.overlays.append( LineInspector(component=self.right_plot, write_metadata=True, is_listener=True, color="blue", axis="value")) return container

# Examples

# Visual Components

- Renderers
- Containers
- Tools and tool overlays
- Legends, annotations, etc.

# Visual Components (cont.)

- have bounds, position, padding, border, bgcolor
- can be placed inside containers
- can be horizontally and vertically resized (or not)
- implement draw (and request draw on their underlays and overlays)
- implement dispatch (and dispatch to underlays and overlays)

# More about Containers

- Layout
- Dispatch
- Draw order & backbuffering

# Walkthrough of interesting examples

# How to get it

http://code.enthought.com/chaco

Mailing lists: chaco-users@enthought.com, enthought-dev@enthought.com

Get Enthon! http://code.enthought.com/enthon