6 from math
import log10
, sqrt
7 from matplotlib
.backend_bases
import LocationEvent
8 from matplotlib
.backends
.backend_gtkagg
import FigureCanvasGTKAgg
as FigureCanvas
9 from matplotlib
.backends
.backend_gtkagg
import NavigationToolbar2GTKAgg
as NavigationToolbar
10 from matplotlib
.widgets
import SpanSelector
, RectangleSelector
12 IOSCOPY_COL_TEXT
= 0 # Combo box text
13 IOSCOPY_COL_X10
= 1 # x10 mode status
14 IOSCOPY_COL_VIS
= 2 # Combobox items sensitive
15 IOSCOPY_COL_SPAN
= 3 # Span mode status
16 IOSCOPY_COL_HADJ
= 4 # Horizontal scrollbar adjustment
17 IOSCOPY_COL_VADJ
= 5 # Vertical scrollbar adjustment
19 DEFAULT_ZOOM_FACTOR
= 0.8
20 DEFAULT_PAN_FACTOR
= 10
22 class MyRectangleSelector(RectangleSelector
):
23 """ FIXME: To be removed once upstream has merged PR #658
24 https://github.com/matplotlib/matplotlib/pull/658
27 def ignore(self
, event
):
28 'return ``True`` if *event* should be ignored'
29 # If RectangleSelector is not active :
33 # If canvas was locked
34 if not self
.canvas
.widgetlock
.available(self
):
37 # Only do rectangle selection if event was triggered
38 # with a desired button
39 if self
.validButtons
is not None:
40 if not event
.button
in self
.validButtons
:
43 # If no button was pressed yet ignore the event if it was out
45 if self
.eventpress
== None:
46 return event
.inaxes
!= self
.ax
48 # If a button was pressed, check if the release-button is the
49 # same. If event is out of axis, limit the data coordinates to axes
51 if event
.button
== self
.eventpress
.button
and event
.inaxes
!= self
.ax
:
52 (xdata
, ydata
) = self
.ax
.transData
.inverted().transform_point((event
.x
, event
.y
))
53 x0
, x1
= self
.ax
.get_xbound()
54 y0
, y1
= self
.ax
.get_ybound()
55 xdata
= max(x0
, xdata
)
56 xdata
= min(x1
, xdata
)
57 ydata
= max(y0
, ydata
)
58 ydata
= min(y1
, ydata
)
63 # If a button was pressed, check if the release-button is the
65 return (event
.inaxes
!=self
.ax
or
66 event
.button
!= self
.eventpress
.button
)
68 class IOscopy_GTK_Figure(oscopy
.Figure
):
69 def __init__(self
, sigs
={}, fig
=None, title
=''):
70 oscopy
.Figure
.__init
__(self
, None, fig
)
71 self
._TARGET
_TYPE
_SIGNAL
= 10354
72 self
._to
_figure
= [("oscopy-signals", gtk
.TARGET_SAME_APP
,\
73 self
._TARGET
_TYPE
_SIGNAL
)]
78 hbox1
= gtk
.HBox() # The window
79 vbox1
= gtk
.VBox() # The Graphs
80 hbox1
.pack_start(vbox1
)
82 canvas
= FigureCanvas(self
)
83 canvas
.mpl_connect('button_press_event', self
._button
_press
)
84 canvas
.mpl_connect('scroll_event', self
._mouse
_scroll
)
85 canvas
.mpl_connect('axes_enter_event', self
._axes
_enter
)
86 canvas
.mpl_connect('axes_leave_event', self
._axes
_leave
)
87 canvas
.mpl_connect('figure_enter_event', self
._figure
_enter
)
88 canvas
.mpl_connect('figure_leave_event', self
._figure
_leave
)
89 canvas
.mpl_connect('key_press_event', self
._key
_press
)
90 canvas
.mpl_connect('draw_event', self
._update
_scrollbars
)
91 w
.connect('delete-event', lambda w
, e
: w
.hide() or True)
92 w
.drag_dest_set(gtk
.DEST_DEFAULT_MOTION |\
93 gtk
.DEST_DEFAULT_HIGHLIGHT |\
94 gtk
.DEST_DEFAULT_DROP
,
95 self
._to
_figure
, gtk
.gdk
.ACTION_COPY
)
97 hbar
= gtk
.HScrollbar()
98 hbar
.set_sensitive(False)
100 vbox1
.pack_start(hbar
, False, False)
101 vbar
= gtk
.VScrollbar()
102 vbar
.set_sensitive(False)
104 hbox1
.pack_start(vbar
, False, False)
106 vbox1
.pack_start(canvas
)
108 toolbar
= NavigationToolbar(canvas
, w
)
109 vbox1
.pack_start(toolbar
, False, False)
111 vbox2
= gtk
.VBox() # The right-side menu
112 store
= gtk
.ListStore(gobject
.TYPE_STRING
, # String displayed
113 gobject
.TYPE_BOOLEAN
, # x10 mode status
114 gobject
.TYPE_BOOLEAN
, # Combobox item sensitive
115 gobject
.TYPE_BOOLEAN
, # Span mode status
116 gobject
.TYPE_PYOBJECT
, # Horizontal Adjustment
117 gobject
.TYPE_PYOBJECT
, # Vertical Adjustment
119 iter = store
.append([_('All Graphs'), False, True, False, gtk
.Adjustment(), gtk
.Adjustment()])
121 iter = store
.append([_('Graph %d') % (i
+ 1), False, True if i
< len(self
.graphs
) else False, False, gtk
.Adjustment(), gtk
.Adjustment()])
122 self
._cbx
_store
= store
124 graphs_cbx
= gtk
.ComboBox(store
)
125 cell
= gtk
.CellRendererText()
126 graphs_cbx
.pack_start(cell
, True)
127 graphs_cbx
.add_attribute(cell
, 'text', IOSCOPY_COL_TEXT
)
128 graphs_cbx
.add_attribute(cell
, 'sensitive', IOSCOPY_COL_VIS
)
129 graphs_cbx
.set_active(0)
130 vbox2
.pack_start(graphs_cbx
, False, False)
132 x10_toggle_btn
= gtk
.ToggleButton('x10 mode')
133 x10_toggle_btn
.set_mode(True)
134 x10_toggle_btn
.connect('toggled', self
.x10_toggle_btn_toggled
,
137 span_toggle_btn
= gtk
.ToggleButton(_('Span'))
138 span_toggle_btn
.set_mode(True)
139 span_toggle_btn
.connect('toggled', self
.span_toggle_btn_toggled
,
142 self
._cbx
= graphs_cbx
143 self
._btn
= x10_toggle_btn
145 graphs_cbx
.connect('changed', self
.graphs_cbx_changed
, x10_toggle_btn
,
146 span_toggle_btn
, store
)
147 vbox2
.pack_start(x10_toggle_btn
, False, False)
148 vbox2
.pack_start(span_toggle_btn
, False, False)
150 hbox1
.pack_start(vbox2
, False, False)
157 # # Update canvas for SpanSelector of Graphs
158 for gr
in self
.graphs
:
159 if hasattr(gr
, 'span'):
162 def set_layout(self
, layout
='quad'):
163 oscopy
.Figure
.set_layout(self
, layout
)
164 iter = self
._cbx
_store
.get_iter_first()
165 for g
in self
.graphs
:
166 iter = self
._cbx
_store
.iter_next(iter)
167 if self
._layout
== 'horiz':
168 g
.span
= SpanSelector(g
, g
.onselect
, 'horizontal',
170 g
.span
.visible
= self
._cbx
_store
.get_value(iter, IOSCOPY_COL_SPAN
)
171 self
.hbar
.set_sensitive(True)
173 self
.vbar
.set_sensitive(False)
175 elif self
._layout
== 'vert':
176 g
.span
= SpanSelector(g
, g
.onselect
, 'vertical',
178 g
.span
.visible
= self
._cbx
_store
.get_value(iter, IOSCOPY_COL_SPAN
)
179 self
.hbar
.set_sensitive(False)
181 self
.vbar
.set_sensitive(True)
183 elif self
._layout
== 'quad':
184 g
.span
= MyRectangleSelector(g
, g
.onselect
, rectprops
=dict(facecolor
='red', edgecolor
= 'black', alpha
=0.5, fill
=True),
186 g
.span
.active
= self
._cbx
_store
.get_value(iter, IOSCOPY_COL_SPAN
)
187 self
.hbar
.set_sensitive(True)
189 self
.vbar
.set_sensitive(True)
192 def graphs_cbx_changed(self
, graphs_cbx
, x10_toggle_btn
, span_toggle_btn
, store
):
193 iter = graphs_cbx
.get_active_iter()
194 if store
.get_string_from_iter(iter) == '0':
195 # Do all the graphs have same state?
196 store
.iter_next(iter)
197 val
= store
.get_value(iter, IOSCOPY_COL_X10
)
198 while iter is not None:
199 if val
!= store
.get_value(iter, IOSCOPY_COL_X10
):
200 # Yes, set the button into inconsistent state
201 x10_toggle_btn
.set_inconsistent(True)
203 iter = store
.iter_next(iter)
204 iter = store
.get_iter_first()
205 store
.iter_next(iter)
206 val
= store
.get_value(iter, IOSCOPY_COL_SPAN
)
207 while iter is not None:
208 if val
!= store
.get_value(iter, IOSCOPY_COL_SPAN
):
209 # Yes, set the button into inconsistent state
210 span_toggle_btn
.set_inconsistent(True)
212 iter = store
.iter_next(iter)
213 self
.hbar
.set_adjustment(store
.get_value(iter, IOSCOPY_COL_HADJ
))
214 self
.vbar
.set_adjustment(store
.get_value(iter, IOSCOPY_COL_VADJ
))
216 x10_toggle_btn
.set_inconsistent(False)
217 x10_toggle_btn
.set_active(store
.get_value(iter, IOSCOPY_COL_X10
))
218 span_toggle_btn
.set_inconsistent(False)
219 span_toggle_btn
.set_active(store
.get_value(iter, IOSCOPY_COL_SPAN
))
220 self
.hbar
.set_adjustment(store
.get_value(iter, IOSCOPY_COL_HADJ
))
221 self
.vbar
.set_adjustment(store
.get_value(iter, IOSCOPY_COL_VADJ
))
223 def x10_toggle_btn_toggled(self
, x10_toggle_btn
, graphs_cbx
, store
):
225 iter = graphs_cbx
.get_active_iter()
226 a
= x10_toggle_btn
.get_active()
227 val
= -.1 if a
else -1 # x10 zoom
228 x10_toggle_btn
.set_inconsistent(False)
231 store
.set_value(iter, IOSCOPY_COL_X10
, a
)
232 grnum
= int(store
.get_string_from_iter(iter))
233 if store
.get_string_from_iter(iter) == '0':
234 # Set the value for all graphs
235 iter = store
.iter_next(iter)
236 while iter is not None:
237 store
.set_value(iter, IOSCOPY_COL_X10
, a
)
238 grnum
= int(store
.get_string_from_iter(iter))
239 if grnum
> len(self
.graphs
):
241 g
= self
.graphs
[grnum
- 1]
243 iter = store
.iter_next(iter)
245 g
= self
.graphs
[grnum
- 1]
247 self
.canvas
.draw_idle()
249 def _zoom_x10(self
, g
, a
):
250 layout
= self
._layout
251 result
= g
.dataLim
.frozen()
252 if layout
== 'horiz' or layout
== 'quad':
253 g
.set_xlim(*result
.intervalx
)
254 if layout
== 'vert' or layout
== 'quad':
255 g
.set_ylim(*result
.intervaly
)
257 result
= g
.bbox
.expanded(0.1, 0.1).transformed(g
.transData
.inverted())
258 if layout
== 'horiz' or layout
== 'quad':
259 g
.set_xlim(*result
.intervalx
)
260 if layout
== 'vert' or layout
== 'quad':
261 g
.set_ylim(*result
.intervaly
)
263 def span_toggle_btn_toggled(self
, span_toggle_btn
, graphs_cbx
, store
):
264 iter = graphs_cbx
.get_active_iter()
265 a
= span_toggle_btn
.get_active()
266 span_toggle_btn
.set_inconsistent(False)
268 store
.set_value(iter, IOSCOPY_COL_SPAN
, a
)
269 grnum
= int(store
.get_string_from_iter(iter))
270 if store
.get_string_from_iter(iter) == '0':
271 # Set the value for all graphs
272 iter = store
.iter_next(iter)
273 while iter is not None:
274 store
.set_value(iter, IOSCOPY_COL_SPAN
, a
)
275 grnum
= int(store
.get_string_from_iter(iter))
276 if grnum
> len(self
.graphs
):
278 if hasattr(self
.graphs
[grnum
- 1].span
, 'active'):
279 self
.graphs
[grnum
- 1].span
.active
= a
280 elif hasattr(self
.graphs
[grnum
- 1].span
, 'visible'):
281 self
.graphs
[grnum
- 1].span
.visible
= a
282 iter = store
.iter_next(iter)
284 if hasattr(self
.graphs
[grnum
- 1].span
, 'active'):
285 self
.graphs
[grnum
- 1].span
.active
= a
286 elif hasattr(self
.graphs
[grnum
- 1].span
, 'visible'):
287 self
.graphs
[grnum
- 1].span
.visible
= a
290 def _key_press(self
, event
):
291 if event
.inaxes
is not None:
294 self
._zoom
_on
_event
(event
, DEFAULT_ZOOM_FACTOR
)
295 self
.canvas
.draw_idle()
296 elif event
.key
== 'Z':
297 self
._zoom
_on
_event
(event
, 1. / DEFAULT_ZOOM_FACTOR
)
298 self
.canvas
.draw_idle()
299 elif event
.key
== 'l':
300 result
= g
.bbox
.translated(-DEFAULT_PAN_FACTOR
, 0).transformed(g
.transData
.inverted())
301 g
.set_xlim(*result
.intervalx
)
302 g
.set_ylim(*result
.intervaly
)
303 self
.canvas
.draw_idle()
304 elif event
.key
== 'r':
305 result
= g
.bbox
.translated(DEFAULT_PAN_FACTOR
, 0).transformed(g
.transData
.inverted())
306 g
.set_xlim(*result
.intervalx
)
307 g
.set_ylim(*result
.intervaly
)
308 self
.canvas
.draw_idle()
311 def _zoom_on_event(self
, event
, factor
):
313 if g
is None or factor
== 1:
316 result
= g
.bbox
.expanded(factor
, factor
).transformed(g
.transData
.inverted())
317 # Localisation of event.xdata in the new transform
318 if layout
== 'horiz' or layout
== 'quad':
319 g
.set_xlim(*result
.intervalx
)
320 if layout
== 'vert' or layout
== 'quad':
321 g
.set_ylim(*result
.intervaly
)
322 # Then place it under cursor
323 b
= g
.transData
.transform_point([event
.xdata
, event
.ydata
])
324 result
= g
.bbox
.translated(-(event
.x
- b
[0]), -(event
.y
- b
[1])).transformed(g
.transData
.inverted())
325 if layout
== 'horiz' or layout
== 'quad':
326 g
.set_xlim(*result
.intervalx
)
327 if layout
== 'vert' or layout
== 'quad':
328 g
.set_ylim(*result
.intervaly
)
329 # Limit to data boundaries
330 (dxmin
, dxmax
) = (g
.dataLim
.xmin
, g
.dataLim
.xmax
)
331 (xmin
, xmax
) = g
.get_xbound()
332 g
.set_xbound(max(dxmin
, xmin
), min(dxmax
, xmax
))
333 (dymin
, dymax
) = (g
.dataLim
.ymin
, g
.dataLim
.ymax
)
334 (ymin
, ymax
) = g
.get_ybound()
335 g
.set_ybound(max(dymin
, ymin
), min(dymax
, ymax
))
337 def drag_data_received_cb(self
, widget
, drag_context
, x
, y
, selection
,\
338 target_type
, time
, ctxtsignals
):
339 # Event handling issue: this drag and drop callback is
340 # processed before matplotlib callback _axes_enter. Therefore
341 # when dropping, self._current_graph is not valid: it contains
343 # The workaround is to retrieve the Graph by creating a Matplotlib
344 # LocationEvent considering inverse 'y' coordinates
345 if target_type
== self
._TARGET
_TYPE
_SIGNAL
:
347 my_y
= canvas
.allocation
.height
- y
348 event
= LocationEvent('axes_enter_event', canvas
, x
, my_y
)
350 for name
in selection
.data
.split():
351 signals
[name
] = ctxtsignals
[name
]
352 if event
.inaxes
is not None:
354 event
.inaxes
.insert(signals
)
358 oscopy
.Figure
.add(self
, args
)
359 store
= self
._cbx
_store
360 iter = store
.get_iter_first()
361 iter = store
.iter_next(iter) # First item always sensitive
362 while iter is not None:
363 grnum
= int(store
.get_string_from_iter(iter))
364 if grnum
> len(self
.graphs
):
365 store
.set_value(iter, IOSCOPY_COL_VIS
, False)
367 store
.set_value(iter, IOSCOPY_COL_VIS
, True)
368 iter = store
.iter_next(iter)
370 def delete(self
, args
):
371 oscopy
.Figure
.delete(self
, args
)
372 store
= self
._cbx
_store
373 iter = store
.get_iter_first()
374 iter = store
.iter_next(iter) # First item always sensitive
375 while iter is not None:
376 grnum
= int(store
.get_string_from_iter(iter))
377 if grnum
> len(self
.graphs
):
378 store
.set_value(iter, IOSCOPY_COL_VIS
, False)
380 store
.set_value(iter, IOSCOPY_COL_VIS
, True)
381 iter = store
.iter_next(iter)
383 def _button_press(self
, event
):
384 if event
.button
== 3:
385 menu
= self
._create
_figure
_popup
_menu
(event
.canvas
.figure
, event
.inaxes
)
387 menu
.popup(None, None, None, event
.button
, event
.guiEvent
.time
)
389 def _mouse_scroll(self
, event
):
390 if event
.button
== 'up':
391 if event
.inaxes
is None:
393 self
._zoom
_on
_event
(event
, DEFAULT_ZOOM_FACTOR
)
394 self
.canvas
.draw_idle()
395 elif event
.button
== 'down':
396 if event
.inaxes
is None:
398 self
._zoom
_on
_event
(event
, 1. / DEFAULT_ZOOM_FACTOR
)
399 self
.canvas
.draw_idle()
402 def _axes_enter(self
, event
):
403 # self._figure_enter(event)
404 # self._current_graph = event.inaxes
406 # axes_num = event.canvas.figure.axes.index(event.inaxes) + 1
407 # fig_num = self._ctxt.figures.index(self._current_figure) + 1
408 # self._app_exec('%%oselect %d-%d' % (fig_num, axes_num))
411 def _axes_leave(self
, event
):
412 # Unused for better user interaction
413 # self._current_graph = None
416 def _figure_enter(self
, event
):
417 # self._current_figure = event.canvas.figure
418 # if hasattr(event, 'inaxes') and event.inaxes is not None:
419 # axes_num = event.canvas.figure.axes.index(event.inaxes) + 1
422 # fig_num = self._ctxt.figures.index(self._current_figure) + 1
423 # self._app_exec('%%oselect %d-%d' % (fig_num, axes_num))
424 self
.canvas
.grab_focus()
427 def _figure_leave(self
, event
):
428 # self._current_figure = None
431 def _create_figure_popup_menu(self
, figure
, graph
):
432 figmenu
= gui
.menus
.FigureMenu()
433 return figmenu
.create_menu(figure
, graph
)
435 def _update_scrollbars(self
, event
):
436 for grnum
, gr
in enumerate(self
.graphs
):
437 self
._update
_graph
_adj
(grnum
, gr
)
438 # Then for all graphs...
440 def _update_graph_adj(self
, grnum
, g
):
441 (lower
, upper
) = (0, 1)
443 hadj
= self
._cbx
_store
[grnum
+ 1][IOSCOPY_COL_HADJ
]
444 vadj
= self
._cbx
_store
[grnum
+ 1][IOSCOPY_COL_VADJ
]
446 (xdmin
, xdmax
) = (g
.dataLim
.xmin
, g
.dataLim
.xmax
)
447 (ydmin
, ydmax
) = (g
.dataLim
.ymin
, g
.dataLim
.ymax
)
448 b0
= g
.transData
.transform_point([xdmin
, ydmin
])
449 b1
= g
.transData
.transform_point([xdmax
, ydmax
])
451 # Get view bounds in pixels
452 (xvmin
, xvmax
) = (g
.bbox
.xmin
, g
.bbox
.xmax
)
453 (yvmin
, yvmax
) = (g
.bbox
.ymin
, g
.bbox
.ymax
)
455 xpage_size
= (xvmax
- xvmin
) / (b1
[0] - b0
[0])
456 ypage_size
= (yvmax
- yvmin
) / (b1
[1] - b0
[1])
458 xvalue
= (xvmin
- b0
[0]) / (b1
[0] - b0
[0])
459 yvalue
= (yvmin
- b0
[1]) / (b1
[1] - b0
[1])
461 xstep_increment
= xpage_size
/ 10
462 xpage_increment
= xpage_size
463 hadj
.configure(xvalue
, lower
, upper
,
464 xstep_increment
, xpage_increment
, xpage_size
)
465 ystep_increment
= ypage_size
/ 10
466 ypage_increment
= ypage_size
467 vadj
.configure(yvalue
, lower
, upper
,
468 ystep_increment
, ypage_increment
, ypage_size
)
470 layout
= property(oscopy
.Figure
.get_layout
, set_layout
)