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
17 DEFAULT_ZOOM_FACTOR
= 0.8
18 DEFAULT_PAN_FACTOR
= 10
21 # Stupid class to store center coordinates when zooming
26 class MyRectangleSelector(RectangleSelector
):
27 """ FIXME: To be removed once upstream has merged PR #658
28 https://github.com/matplotlib/matplotlib/pull/658
31 def ignore(self
, event
):
32 'return ``True`` if *event* should be ignored'
33 # If RectangleSelector is not active :
37 # If canvas was locked
38 if not self
.canvas
.widgetlock
.available(self
):
41 # Only do rectangle selection if event was triggered
42 # with a desired button
43 if self
.validButtons
is not None:
44 if not event
.button
in self
.validButtons
:
47 # If no button was pressed yet ignore the event if it was out
49 if self
.eventpress
== None:
50 return event
.inaxes
!= self
.ax
52 # If a button was pressed, check if the release-button is the
53 # same. If event is out of axis, limit the data coordinates to axes
55 if event
.button
== self
.eventpress
.button
and event
.inaxes
!= self
.ax
:
56 (xdata
, ydata
) = self
.ax
.transData
.inverted().transform_point((event
.x
, event
.y
))
57 x0
, x1
= self
.ax
.get_xbound()
58 y0
, y1
= self
.ax
.get_ybound()
59 xdata
= max(x0
, xdata
)
60 xdata
= min(x1
, xdata
)
61 ydata
= max(y0
, ydata
)
62 ydata
= min(y1
, ydata
)
67 # If a button was pressed, check if the release-button is the
69 return (event
.inaxes
!=self
.ax
or
70 event
.button
!= self
.eventpress
.button
)
72 class IOscopy_GTK_Figure(oscopy
.Figure
):
73 def __init__(self
, sigs
={}, fig
=None, title
=''):
74 oscopy
.Figure
.__init
__(self
, None, fig
)
75 self
._TARGET
_TYPE
_SIGNAL
= 10354
76 self
._to
_figure
= [("oscopy-signals", gtk
.TARGET_SAME_APP
,\
77 self
._TARGET
_TYPE
_SIGNAL
)]
82 hbox1
= gtk
.HBox() # The window
83 vbox1
= gtk
.VBox() # The Graphs
84 hbox1
.pack_start(vbox1
)
86 canvas
= FigureCanvas(self
)
87 canvas
.mpl_connect('button_press_event', self
._button
_press
)
88 canvas
.mpl_connect('scroll_event', self
._mouse
_scroll
)
89 canvas
.mpl_connect('axes_enter_event', self
._axes
_enter
)
90 canvas
.mpl_connect('axes_leave_event', self
._axes
_leave
)
91 canvas
.mpl_connect('figure_enter_event', self
._figure
_enter
)
92 canvas
.mpl_connect('figure_leave_event', self
._figure
_leave
)
93 canvas
.mpl_connect('key_press_event', self
._key
_press
)
94 w
.connect('delete-event', lambda w
, e
: w
.hide() or True)
95 w
.drag_dest_set(gtk
.DEST_DEFAULT_MOTION |\
96 gtk
.DEST_DEFAULT_HIGHLIGHT |\
97 gtk
.DEST_DEFAULT_DROP
,
98 self
._to
_figure
, gtk
.gdk
.ACTION_COPY
)
100 vbox1
.pack_start(canvas
)
102 toolbar
= NavigationToolbar(canvas
, w
)
103 vbox1
.pack_start(toolbar
, False, False)
105 vbox2
= gtk
.VBox() # The right-side menu
106 store
= gtk
.ListStore(gobject
.TYPE_STRING
, # String displayed
107 gobject
.TYPE_BOOLEAN
, # x10 mode status
108 gobject
.TYPE_BOOLEAN
, # Combobox item sensitive
109 gobject
.TYPE_BOOLEAN
, # Span mode status
110 # gobject.TYPE_FLOAT, # Zoom factor
112 iter = store
.append([_('All Graphs'), False, True, False])
114 iter = store
.append([_('Graph %d') % (i
+ 1), False, True if i
< len(self
.graphs
) else False, False])
115 self
._cbx
_store
= store
117 graphs_cbx
= gtk
.ComboBox(store
)
118 cell
= gtk
.CellRendererText()
119 graphs_cbx
.pack_start(cell
, True)
120 graphs_cbx
.add_attribute(cell
, 'text', IOSCOPY_COL_TEXT
)
121 graphs_cbx
.add_attribute(cell
, 'sensitive', IOSCOPY_COL_VIS
)
122 graphs_cbx
.set_active(0)
123 vbox2
.pack_start(graphs_cbx
, False, False)
125 x10_toggle_btn
= gtk
.ToggleButton('x10 mode')
126 x10_toggle_btn
.set_mode(True)
127 x10_toggle_btn
.connect('toggled', self
.x10_toggle_btn_toggled
,
130 span_toggle_btn
= gtk
.ToggleButton(_('Span'))
131 span_toggle_btn
.set_mode(True)
132 span_toggle_btn
.connect('toggled', self
.span_toggle_btn_toggled
,
135 self
._cbx
= graphs_cbx
136 self
._btn
= x10_toggle_btn
138 graphs_cbx
.connect('changed', self
.graphs_cbx_changed
, x10_toggle_btn
,
139 span_toggle_btn
, store
)
140 vbox2
.pack_start(x10_toggle_btn
, False, False)
141 vbox2
.pack_start(span_toggle_btn
, False, False)
143 hbox1
.pack_start(vbox2
, False, False)
150 # # Update canvas for SpanSelector of Graphs
151 for gr
in self
.graphs
:
152 if hasattr(gr
, 'span'):
155 def set_layout(self
, layout
='quad'):
156 oscopy
.Figure
.set_layout(self
, layout
)
157 iter = self
._cbx
_store
.get_iter_first()
158 for g
in self
.graphs
:
159 iter = self
._cbx
_store
.iter_next(iter)
160 if self
._layout
== 'horiz':
161 g
.span
= SpanSelector(g
, g
.onselect
, 'horizontal',
163 g
.span
.visible
= self
._cbx
_store
.get_value(iter, IOSCOPY_COL_SPAN
)
164 elif self
._layout
== 'vert':
165 g
.span
= SpanSelector(g
, g
.onselect
, 'vertical',
167 g
.span
.visible
= self
._cbx
_store
.get_value(iter, IOSCOPY_COL_SPAN
)
168 elif self
._layout
== 'quad':
169 g
.span
= MyRectangleSelector(g
, g
.onselect
, rectprops
=dict(facecolor
='red', edgecolor
= 'black', alpha
=0.5, fill
=True),
171 g
.span
.active
= self
._cbx
_store
.get_value(iter, IOSCOPY_COL_SPAN
)
173 def graphs_cbx_changed(self
, graphs_cbx
, x10_toggle_btn
, span_toggle_btn
, store
):
174 iter = graphs_cbx
.get_active_iter()
175 if store
.get_string_from_iter(iter) == '0':
176 # Do all the graphs have same state?
177 store
.iter_next(iter)
178 val
= store
.get_value(iter, IOSCOPY_COL_X10
)
179 while iter is not None:
180 if val
!= store
.get_value(iter, IOSCOPY_COL_X10
):
181 # Yes, set the button into inconsistent state
182 x10_toggle_btn
.set_inconsistent(True)
184 iter = store
.iter_next(iter)
185 iter = store
.get_iter_first()
186 store
.iter_next(iter)
187 val
= store
.get_value(iter, IOSCOPY_COL_SPAN
)
188 while iter is not None:
189 if val
!= store
.get_value(iter, IOSCOPY_COL_SPAN
):
190 # Yes, set the button into inconsistent state
191 span_toggle_btn
.set_inconsistent(True)
193 iter = store
.iter_next(iter)
195 x10_toggle_btn
.set_inconsistent(False)
196 x10_toggle_btn
.set_active(store
.get_value(iter, IOSCOPY_COL_X10
))
197 span_toggle_btn
.set_inconsistent(False)
198 span_toggle_btn
.set_active(store
.get_value(iter, IOSCOPY_COL_SPAN
))
200 def x10_toggle_btn_toggled(self
, x10_toggle_btn
, graphs_cbx
, store
):
202 iter = graphs_cbx
.get_active_iter()
203 a
= x10_toggle_btn
.get_active()
204 val
= -.1 if a
else -1 # x10 zoom
205 x10_toggle_btn
.set_inconsistent(False)
207 store
.set_value(iter, IOSCOPY_COL_X10
, a
)
208 grnum
= int(store
.get_string_from_iter(iter))
209 if store
.get_string_from_iter(iter) == '0':
210 # Set the value for all graphs
211 iter = store
.iter_next(iter)
212 while iter is not None:
213 store
.set_value(iter, IOSCOPY_COL_X10
, a
)
214 grnum
= int(store
.get_string_from_iter(iter))
215 if grnum
> len(self
.graphs
):
217 self
._zoom
(grnum
, center
, val
)
218 iter = store
.iter_next(iter)
220 self
._zoom
(grnum
, center
, val
)
223 def span_toggle_btn_toggled(self
, span_toggle_btn
, graphs_cbx
, store
):
224 iter = graphs_cbx
.get_active_iter()
225 a
= span_toggle_btn
.get_active()
226 span_toggle_btn
.set_inconsistent(False)
228 store
.set_value(iter, IOSCOPY_COL_SPAN
, a
)
229 grnum
= int(store
.get_string_from_iter(iter))
230 if store
.get_string_from_iter(iter) == '0':
231 # Set the value for all graphs
232 iter = store
.iter_next(iter)
233 while iter is not None:
234 store
.set_value(iter, IOSCOPY_COL_SPAN
, a
)
235 grnum
= int(store
.get_string_from_iter(iter))
236 if grnum
> len(self
.graphs
):
238 if hasattr(self
.graphs
[grnum
- 1].span
, 'active'):
239 self
.graphs
[grnum
- 1].span
.active
= a
240 elif hasattr(self
.graphs
[grnum
- 1].span
, 'visible'):
241 self
.graphs
[grnum
- 1].span
.visible
= a
242 iter = store
.iter_next(iter)
244 if hasattr(self
.graphs
[grnum
- 1].span
, 'active'):
245 self
.graphs
[grnum
- 1].span
.active
= a
246 elif hasattr(self
.graphs
[grnum
- 1].span
, 'visible'):
247 self
.graphs
[grnum
- 1].span
.visible
= a
250 def _key_press(self
, event
):
251 if event
.inaxes
is not None:
253 grnum
= self
.graphs
.index(g
) + 1
255 center
.x
= event
.xdata
256 center
.y
= event
.ydata
258 self
._zoom
(grnum
, center
, DEFAULT_ZOOM_FACTOR
)
259 self
.canvas
.draw_idle()
260 elif event
.key
== 'Z':
261 self
._zoom
(grnum
, center
, 1 / DEFAULT_ZOOM_FACTOR
)
262 self
.canvas
.draw_idle()
263 elif event
.key
== 'l':
264 result
= g
.bbox
.translated(-DEFAULT_PAN_FACTOR
, 0).transformed(g
.transData
.inverted())
265 g
.set_xlim(*result
.intervalx
)
266 g
.set_ylim(*result
.intervaly
)
267 self
.canvas
.draw_idle()
268 elif event
.key
== 'r':
269 result
= g
.bbox
.translated(DEFAULT_PAN_FACTOR
, 0).transformed(g
.transData
.inverted())
270 g
.set_xlim(*result
.intervalx
)
271 g
.set_ylim(*result
.intervaly
)
272 self
.canvas
.draw_idle()
275 def _zoom(self
, grnum
, center
=None, factor
=1):
276 # In which layout are we (horiz, vert, quad ?)
278 gr
= self
.graphs
[grnum
- 1]
279 [xmin
, xmax
, ymin
, ymax
] = [None for x
in xrange(4)]
280 [(xmin_cur
, xmax_cur
), (ymin_cur
, ymax_cur
)] = gr
.range
281 [(xmin_new
, xmax_new
), (ymin_new
, ymax_new
)] = gr
.range
283 # Get the bounds of the data (min, max)
284 if layout
== 'horiz' or layout
== 'quad':
285 (xmin
, xmax
) = (gr
.dataLim
.xmin
, gr
.dataLim
.xmax
)
287 if layout
== 'vert' or layout
== 'quad':
288 (ymin
, ymax
) = (gr
.dataLim
.ymin
, gr
.dataLim
.ymax
)
290 # Calculate the x10 (linear or log scale ?) and set it
292 logx
= True if sc
== 'logx' or sc
== 'loglog' else False
293 logy
= True if sc
== 'logy' or sc
== 'loglog' else False
294 if xmin
is not None and xmax
is not None:
296 cx
= log10(center
.x
) if center
is not None else None
297 curb
= (log10(xmin_cur
), log10(xmax_cur
)) # Current bounds
298 datab
= (log10(xmin
), log10(xmax
)) # Data bounds
299 newb
= self
._compute
_zoom
_range
(curb
, datab
, cx
, factor
)
300 (xmin_new
, xmax_new
) = newb
# New bounds
301 xmin_new
= pow(10, xmin_new
)
302 xmax_new
= pow(10, xmax_new
)
304 cx
= center
.x
if center
is not None else None
305 curb
= (xmin_cur
, xmax_cur
) # Current bounds
306 datab
= (xmin
, xmax
) # Data bounds
307 newb
= self
._compute
_zoom
_range
(curb
, datab
, cx
, factor
)
308 (xmin_new
, xmax_new
) = newb
# New bounds
309 gr
.set_xbound(xmin_new
, xmax_new
)
311 if ymin
is not None and ymax
is not None:
313 cy
= log10(center
.y
) if center
is not None else None
314 curb
= (log10(ymin_cur
), log10(ymax_cur
)) # Current bounds
315 datab
= (log10(ymin
), log10(ymax
)) # Data bounds
316 newb
= self
._compute
_zoom
_range
(curb
, datab
, cy
, factor
)
317 (ymin_new
, ymax_new
) = newb
# New bounds
318 ymin_new
= pow(10, ymin_new
)
319 ymax_new
= pow(10, ymax_new
)
321 cy
= center
.y
if center
is not None else None
322 curb
= (ymin_cur
, ymax_cur
) # Current bounds
323 datab
= (ymin
, ymax
) # Data bounds
324 newb
= self
._compute
_zoom
_range
(curb
, datab
, cy
, factor
)
325 (ymin_new
, ymax_new
) = newb
# New bounds
326 gr
.set_ybound(ymin_new
, ymax_new
)
328 def _compute_zoom_range(self
, curb
, datab
, center
=None, factor
=-1):
329 # Zoom is relative to current bounds when factor > 0
330 # otherwise is relative to data bounds
333 (data_min
, data_max
) = datab
334 (min_cur
, max_cur
) = curb
if factor
> 0 else datab
336 return (data_min
, data_max
)
337 curf
= (max_cur
- min_cur
) / (data_max
- data_min
)
338 factor
= factor
* curf
340 center
= (max_cur
+ min_cur
) / 2
341 pos
= (center
- min_cur
) / (max_cur
- min_cur
)
342 min_new
= center
- (data_max
- data_min
) * (abs(factor
) * pos
)
343 max_new
= center
+ (data_max
- data_min
) * (abs(factor
) * (1 - pos
))
344 if min_new
> max_new
:
345 return (max_new
, min_new
)
347 return (min_new
, max_new
)
349 def drag_data_received_cb(self
, widget
, drag_context
, x
, y
, selection
,\
350 target_type
, time
, ctxtsignals
):
351 # Event handling issue: this drag and drop callback is
352 # processed before matplotlib callback _axes_enter. Therefore
353 # when dropping, self._current_graph is not valid: it contains
355 # The workaround is to retrieve the Graph by creating a Matplotlib
356 # LocationEvent considering inverse 'y' coordinates
357 if target_type
== self
._TARGET
_TYPE
_SIGNAL
:
359 my_y
= canvas
.allocation
.height
- y
360 event
= LocationEvent('axes_enter_event', canvas
, x
, my_y
)
362 for name
in selection
.data
.split():
363 signals
[name
] = ctxtsignals
[name
]
364 if event
.inaxes
is not None:
366 event
.inaxes
.insert(signals
)
370 oscopy
.Figure
.add(self
, args
)
371 store
= self
._cbx
_store
372 iter = store
.get_iter_first()
373 iter = store
.iter_next(iter) # First item always sensitive
374 while iter is not None:
375 grnum
= int(store
.get_string_from_iter(iter))
376 if grnum
> len(self
.graphs
):
377 store
.set_value(iter, IOSCOPY_COL_VIS
, False)
379 store
.set_value(iter, IOSCOPY_COL_VIS
, True)
380 iter = store
.iter_next(iter)
382 def delete(self
, args
):
383 oscopy
.Figure
.delete(self
, args
)
384 store
= self
._cbx
_store
385 iter = store
.get_iter_first()
386 iter = store
.iter_next(iter) # First item always sensitive
387 while iter is not None:
388 grnum
= int(store
.get_string_from_iter(iter))
389 if grnum
> len(self
.graphs
):
390 store
.set_value(iter, IOSCOPY_COL_VIS
, False)
392 store
.set_value(iter, IOSCOPY_COL_VIS
, True)
393 iter = store
.iter_next(iter)
395 def _button_press(self
, event
):
396 if event
.button
== 3:
397 menu
= self
._create
_figure
_popup
_menu
(event
.canvas
.figure
, event
.inaxes
)
399 menu
.popup(None, None, None, event
.button
, event
.guiEvent
.time
)
401 def _mouse_scroll(self
, event
):
402 if event
.button
== 'up':
403 if event
.inaxes
is None:
406 grnum
= self
.graphs
.index(g
) + 1
408 center
.x
= event
.xdata
409 center
.y
= event
.ydata
410 self
._zoom
(grnum
, center
, DEFAULT_ZOOM_FACTOR
)
411 self
.canvas
.draw_idle()
412 elif event
.button
== 'down':
413 if event
.inaxes
is None:
416 grnum
= self
.graphs
.index(g
) + 1
418 center
.x
= event
.xdata
419 center
.y
= event
.ydata
420 self
._zoom
(grnum
, center
, 1. / DEFAULT_ZOOM_FACTOR
)
421 self
.canvas
.draw_idle()
424 def _axes_enter(self
, event
):
425 # self._figure_enter(event)
426 # self._current_graph = event.inaxes
428 # axes_num = event.canvas.figure.axes.index(event.inaxes) + 1
429 # fig_num = self._ctxt.figures.index(self._current_figure) + 1
430 # self._app_exec('%%oselect %d-%d' % (fig_num, axes_num))
433 def _axes_leave(self
, event
):
434 # Unused for better user interaction
435 # self._current_graph = None
438 def _figure_enter(self
, event
):
439 # self._current_figure = event.canvas.figure
440 # if hasattr(event, 'inaxes') and event.inaxes is not None:
441 # axes_num = event.canvas.figure.axes.index(event.inaxes) + 1
444 # fig_num = self._ctxt.figures.index(self._current_figure) + 1
445 # self._app_exec('%%oselect %d-%d' % (fig_num, axes_num))
446 self
.canvas
.grab_focus()
449 def _figure_leave(self
, event
):
450 # self._current_figure = None
453 def _create_figure_popup_menu(self
, figure
, graph
):
454 figmenu
= gui
.menus
.FigureMenu()
455 return figmenu
.create_menu(figure
, graph
)
458 layout
= property(oscopy
.Figure
.get_layout
, set_layout
)