2
Defines some chaco tools to provide draggable cursor functionality
4
For XY-plots, the cursor tool requires the index_sort flag to be set
5
to either 'ascending' or 'descending'.
9
- add some visual feedback to the user when a cursor is "grabbed"
10
(e.g. highlight the cursor line)
12
- update cursor position to the "selections" metadata on the owning
16
from __future__ import with_statement
18
# Major library imports
21
# Enthought library imports
22
from enthought.enable.tools.drag_tool import DragTool
23
from enthought.traits.api import Int, Property, cached_property, Float,\
24
Bool, Instance, Tuple, Disallow
27
from enthought.chaco.scatter_markers import CircleMarker
28
from enthought.chaco.base_xy_plot import BaseXYPlot
29
from enthought.chaco.base_2d_plot import Base2DPlot
30
from line_inspector import LineInspector
33
def CursorTool(component, *args, **kwds):
35
Factory function returning either a CursorTool1D or CursorTool2D instance
36
depending on whether the provided plot component is an XY-plot or a 2D plot.
38
if isinstance(component, BaseXYPlot):
39
return CursorTool1D(component, *args, **kwds)
40
elif isinstance(component, Base2DPlot):
41
return CursorTool2D(component, *args, **kwds)
43
raise NotImplementedError("cursors available for BaseXYPlot and Base2DPlot only")
46
class BaseCursorTool(LineInspector, DragTool):
48
Abstract base class for CursorTool objects
51
#if true, draw a small circle at the cursor/line intersection
52
show_marker = Bool(True)
54
#the radius of the marker in pixels
55
marker_size = Float(3.0)
57
#the marker object. this should probably be private
58
marker = Instance(CircleMarker, ())
60
#pick threshold, in screen units (pixels)
61
threshold = Float(5.0)
63
#The current index-value of the cursor. Over-ridden in subclasses
64
current_index = Disallow
66
#The current position of the cursor in data units
67
current_position = Property(depends_on=['current_index'])
69
#Stuff from line_inspector which is not required
71
inspect_mode = Disallow
72
is_interactive = Disallow
73
is_listener = Disallow
74
write_metadata = Disallow
75
metadata_name = Disallow
77
def _draw_marker(self, gc, sx, sy):
79
Ruthlessly hijacked from the scatterplot.py class. This design is silly; the
80
choice of rendering path should be encapsulated within the GC.
82
if sx < self.component.x or sx > self.component.x2 or \
83
sy < self.component.y or sy > self.component.y2:
87
marker_size = self.marker_size
91
gc.set_fill_color(self.color_)
95
# This is the fastest method - use one of the kiva built-in markers
96
if hasattr(gc, "draw_marker_at_points") \
97
and (gc.draw_marker_at_points(points,
99
marker.kiva_marker) != 0):
102
# The second fastest method - draw the path into a compiled path, then
103
# draw the compiled path at each point
104
elif hasattr(gc, 'draw_path_at_points'):
106
# import pdb; pdb.set_trace()
107
path = gc.get_empty_path()
108
marker.add_to_path(path, marker_size)
109
mode = marker.draw_mode
110
if not marker.antialias:
111
gc.set_antialias(False)
112
gc.draw_path_at_points(points, path, mode)
114
# Neither of the fast functions worked, so use the brute-force, manual way
116
if not marker.antialias:
117
gc.set_antialias(False)
120
gc.translate_ctm(sx, sy)
121
# Kiva GCs have a path-drawing interface
122
marker.add_to_path(gc, marker_size)
123
gc.draw_path(marker.draw_mode)
126
def normal_mouse_move(self, event):
127
""" Handles the mouse being moved.
131
def normal_mouse_leave(self, event):
132
""" Handles the mouse leaving the plot.
137
class CursorTool1D(BaseCursorTool):
139
This tools provides a draggable cursor bound to a XY plot component instance.
141
Note, be sure to select an drag_button which does not conflict with other tools
146
#The current index-value of the cursor
147
current_index = Int(0)
150
#if true, draws a line parallel to the index-axis
151
#through the cursor intersection point
152
show_value_line = Bool(True)
154
def _current_index_changed(self):
155
self.component.request_redraw()
158
def _get_current_position(self):
159
plot = self.component
160
ndx = self.current_index
161
x = plot.index.get_data()[ndx]
162
y = plot.value.get_data()[ndx]
165
def _set_current_position(self, traitname, args):
166
plot = self.component
167
ndx = plot.index.reverse_map(args[0])
169
self.current_index = ndx
171
def draw(self, gc, view_bounds=None):
172
""" Draws this tool on a graphics context.
174
Overrides LineInspector, BaseTool.
176
# We draw at different points depending on whether or not we are
177
# interactive. If both listener and interactive are true, then the
178
# selection metadata on the plot component takes precendence.
179
plot = self.component
183
sx, sy = plot.map_screen(self.current_position)
184
orientation = plot.orientation
186
if orientation == "h" and sx is not None:
187
self._draw_vertical_line(gc, sx)
189
self._draw_horizontal_line(gc, sy)
192
self._draw_marker(gc, sx, sy)
194
if self.show_value_line:
195
if orientation == "h" and sy is not None:
196
self._draw_horizontal_line(gc, sy)
198
self._draw_vertical_line(gc, sx)
200
def is_draggable(self, x, y):
201
plot = self.component
203
orientation = plot.orientation
204
sx, sy = plot.map_screen(self.current_position)
205
if orientation=='h' and numpy.abs(sx-x) <= self.threshold:
207
elif orientation=='v' and numpy.abs(sy-y) <= self.threshold:
211
def dragging(self, event):
212
x,y = event.x, event.y
213
plot = self.component
214
ndx = plot.map_index((x, y), threshold=0.0, index_only=True)
217
self.current_index = ndx
218
plot.request_redraw()
221
class CursorTool2D(BaseCursorTool):
225
current_index = Tuple(0,0)
227
def _current_index_changed(self):
228
self.component.request_redraw()
231
def _get_current_position(self):
232
plot = self.component
233
ndx, ndy = self.current_index
234
xdata, ydata = plot.index.get_data()
235
x = xdata.get_data()[ndx]
236
y = ydata.get_data()[ndy]
239
def _set_current_position(self, traitname, args):
240
plot = self.component
241
xds, yds = plot.index.get_data()
242
ndx = xds.reverse_map(args[0])
243
ndy = yds.reverse_map(args[1])
244
if ndx is not None and ndy is not None:
245
self.current_index = ndx, ndy
247
def is_draggable(self, x, y):
248
plot = self.component
250
orientation = plot.orientation
251
sx, sy = plot.map_screen([self.current_position])[0]
252
self._dragV = self._dragH = False
254
if numpy.abs(sx-x) <= self.threshold:
256
if numpy.abs(sy-y) <= self.threshold:
259
if numpy.abs(sx-x) <= self.threshold:
261
if numpy.abs(sy-y) <= self.threshold:
263
return self._dragV or self._dragH
266
def draw(self, gc, view_bounds=None):
267
""" Draws this tool on a graphics context.
269
Overrides LineInspector, BaseTool.
271
# We draw at different points depending on whether or not we are
272
# interactive. If both listener and interactive are true, then the
273
# selection metadata on the plot component takes precendence.
274
plot = self.component
277
sx, sy = plot.map_screen([self.current_position])[0]
278
orientation = plot.orientation
280
if orientation == "h":
282
self._draw_vertical_line(gc, sx)
284
self._draw_horizontal_line(gc, sy)
287
self._draw_horizontal_line(gc, sx)
289
self._draw_vertical_line(gc, sy)
291
if self.show_marker and sx is not None and sy is not None:
292
self._draw_marker(gc, sx, sy)
294
def dragging(self, event):
295
x,y = event.x, event.y
296
plot = self.component
297
ndx = plot.map_index((x, y), threshold=0.0, index_only=True)
300
newx, newy = self.current_index
301
if self._dragH and ndx[0] is not None:
303
if self._dragV and ndx[1] is not None:
305
self.current_index = newx, newy
306
plot.request_redraw()