1 # Copyright (C) 2010-2011 Massimo Sandal <devicerandom@gmail.com>
2 # Rolf Schmidt <rschmidt@alcor.concordia.ca>
3 # W. Trevor King <wking@drexel.edu>
5 # This file is part of Hooke.
7 # Hooke is free software: you can redistribute it and/or modify it
8 # under the terms of the GNU Lesser General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # Hooke is distributed in the hope that it will be useful, but WITHOUT
13 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
15 # Public License for more details.
17 # You should have received a copy of the GNU Lesser General Public
18 # License along with Hooke. If not, see
19 # <http://www.gnu.org/licenses/>.
21 """Plot panel for Hooke.
25 Originally based on `this example`_.
28 http://matplotlib.sourceforge.net/examples/user_interfaces/embedding_in_wx2.html
34 matplotlib.use('WXAgg') # use wxpython with antigrain (agg) rendering
35 from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
36 from matplotlib.backends.backend_wx import NavigationToolbar2Wx as NavToolbar
37 from matplotlib.figure import Figure
38 from matplotlib.ticker import Formatter, ScalarFormatter
42 from ....util.callback import callback, in_callback
43 from ....util.si import ppSI, split_data_label
44 from ..dialog.selection import Selection
48 class HookeFormatter (Formatter):
49 """:class:`matplotlib.ticker.Formatter` using SI prefixes.
51 def __init__(self, unit='', decimals=2):
52 self.decimals = decimals
55 def __call__(self, x, pos=None):
56 """Return the format for tick val `x` at position `pos`.
60 return ppSI(value=x, unit=self.unit, decimals=self.decimals)
63 class HookeScalarFormatter (ScalarFormatter):
64 """:class:`matplotlib.ticker.ScalarFormatter` using only multiples
65 of three in the mantissa.
67 A fixed number of decimals can be displayed with the optional
68 parameter `decimals` . If `decimals` is `None` (default), the number
69 of decimals is defined from the current ticks.
71 def __init__(self, decimals=None, **kwargs):
72 # Can't use super() because ScalarFormatter is an old-style class :(.
73 ScalarFormatter.__init__(self, **kwargs)
74 self._decimals = decimals
76 def _set_orderOfMagnitude(self, *args, **kwargs):
77 """Sets the order of magnitude."""
78 # Can't use super() because ScalarFormatter is an old-style class :(.
79 ScalarFormatter._set_orderOfMagnitude(self, *args, **kwargs)
80 self.orderOfMagnitude -= self.orderOfMagnitude % 3
82 def _set_format(self, *args, **kwargs):
83 """Sets the format string to format all ticklabels."""
84 # Can't use super() because ScalarFormatter is an old-style class :(.
85 ScalarFormatter._set_format(self, *args, **kwargs)
86 if self._decimals is None or self._decimals < 0:
87 locs = (np.asarray(self.locs)-self.offset) / 10**self.orderOfMagnitude+1e-15
88 sigfigs = [len(str('%1.8f'% loc).split('.')[1].rstrip('0')) \
91 decimals = sigfigs[-1]
93 decimals = self._decimals
94 self.format = '%1.' + str(decimals) + 'f'
96 self.format = '$%s$' % self.format
97 elif self._useMathText:
98 self.format = '$\mathdefault{%s}$' % self.format
101 class PlotPanel (Panel, wx.Panel):
102 """UI for graphical curve display.
104 def __init__(self, callbacks=None, **kwargs):
105 self.display_coordinates = False
109 self._x_column = None
110 self._y_columns = [] # TODO: select right/left scales?
113 super(PlotPanel, self).__init__(
114 name='plot', callbacks=callbacks, **kwargs)
116 self._c['figure'] = Figure()
117 self._c['canvas'] = FigureCanvas(
118 parent=self, id=wx.ID_ANY, figure=self._c['figure'])
120 self._set_color(wx.NamedColour('WHITE'))
121 sizer = wx.BoxSizer(wx.VERTICAL)
122 sizer.Add(self._c['canvas'], 1, wx.LEFT | wx.TOP | wx.GROW)
123 self._setup_toolbar(sizer=sizer) # comment out to remove plot toolbar.
127 self.Bind(wx.EVT_SIZE, self._on_size)
128 self._c['figure'].canvas.mpl_connect(
129 'button_press_event', self._on_click)
130 self._c['figure'].canvas.mpl_connect(
131 'axes_enter_event', self._on_enter_axes)
132 self._c['figure'].canvas.mpl_connect(
133 'axes_leave_event', self._on_leave_axes)
134 self._c['figure'].canvas.mpl_connect(
135 'motion_notify_event', self._on_mouse_move)
137 def _setup_toolbar(self, sizer):
138 self._c['toolbar'] = NavToolbar(self._c['canvas'])
139 self._c['x column'] = wx.Choice(
140 parent=self._c['toolbar'], choices=[])
141 self._c['x column'].SetToolTip(wx.ToolTip('x column'))
142 self._c['toolbar'].AddControl(self._c['x column'])
143 self._c['x column'].Bind(wx.EVT_CHOICE, self._on_x_column)
144 self._c['y column'] = wx.Button(
145 parent=self._c['toolbar'], label='y column(s)')
146 self._c['y column'].SetToolTip(wx.ToolTip('y column'))
147 self._c['toolbar'].AddControl(self._c['y column'])
148 self._c['y column'].Bind(wx.EVT_BUTTON, self._on_y_column)
150 self._c['toolbar'].Realize() # call after putting items in the toolbar
151 if wx.Platform == '__WXMAC__':
152 # Mac platform (OSX 10.3, MacPython) does not seem to cope with
153 # having a toolbar in a sizer. This work-around gets the buttons
154 # back, but at the expense of having the toolbar at the top
155 self.SetToolBar(self._c['toolbar'])
156 elif wx.Platform == '__WXMSW__':
157 # On Windows platform, default window size is incorrect, so set
158 # toolbar width to figure width.
159 tw, th = self._c['toolbar'].GetSizeTuple()
160 fw, fh = self._c['canvas'].GetSizeTuple()
161 # By adding toolbar in sizer, we are able to put it at the bottom
162 # of the frame - so appearance is closer to GTK version.
163 # As noted above, doesn't work for Mac.
164 self._c['toolbar'].SetSize(wx.Size(fw, th))
165 sizer.Add(self._c['toolbar'], 0 , wx.LEFT | wx.EXPAND)
167 sizer.Add(self._c['toolbar'], 0 , wx.LEFT | wx.EXPAND)
168 self._c['toolbar'].update() # update the axes menu on the toolbar
170 def _set_color(self, rgbtuple=None):
171 """Set both figure and canvas colors to `rgbtuple`.
174 rgbtuple = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE).Get()
175 col = [c/255.0 for c in rgbtuple]
176 self._c['figure'].set_facecolor(col)
177 self._c['figure'].set_edgecolor(col)
178 self._c['canvas'].SetBackgroundColour(wx.Colour(*rgbtuple))
180 def _set_status_text(self, text):
181 in_callback(self, text)
183 def _on_size(self, event):
185 wx.CallAfter(self._resize_canvas)
187 def _on_click(self, event):
188 if self._curve == None:
190 d = self._config.get('plot decimals', 2)
191 x,y = (event.xdata, event.ydata)
194 xt = ppSI(value=x, unit=self._x_unit, decimals=d)
195 yt = ppSI(value=y, unit=self._y_unit, decimals=d)
197 for data in self._curve.data:
199 x_col = data.info['columns'].index(self._x_column)
201 continue # data is missing a required column
202 index = numpy.absolute(data[:,x_col]-x).argmin()
203 point_indexes.append((data.info['name'], index))
204 self._set_status_text(
207 ', '.join(['%s: %d' % (n,i) for n,i in point_indexes])))
209 def _on_enter_axes(self, event):
210 self.display_coordinates = True
212 def _on_leave_axes(self, event):
213 self.display_coordinates = False
214 #self.SetStatusText('')
216 def _on_mouse_move(self, event):
217 if 'toolbar' in self._c:
218 if event.guiEvent.shiftDown:
219 self._c['toolbar'].set_cursor(wx.CURSOR_RIGHT_ARROW)
221 self._c['toolbar'].set_cursor(wx.CURSOR_ARROW)
222 if self.display_coordinates:
223 coordinateString = ''.join(
224 ['x: ', str(event.xdata), ' y: ', str(event.ydata)])
226 #self.SetStatusText(coordinateString)
228 def _on_x_column(self, event):
229 self._x_column = self._c['x column'].GetStringSelection()
232 def _on_y_column(self, event):
233 if not hasattr(self, '_columns') or len(self._columns) == 0:
237 options=self._columns,
238 message='Select visible y column(s).',
240 selection_style='multiple',
242 title='Select y column(s)',
243 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
246 if s.canceled == True:
248 self._y_columns = [self._columns[i] for i in s.selected]
250 if len(self._y_columns) == 0:
251 self._y_columns = self._columns[-1:]
254 def _resize_canvas(self):
255 w,h = self.GetClientSize()
256 if 'toolbar' in self._c:
257 tw,th = self._c['toolbar'].GetSizeTuple()
260 dpi = float(self._c['figure'].get_dpi())
261 self._c['figure'].set_figwidth(w/dpi)
262 self._c['figure'].set_figheight((h-th)/dpi)
263 self._c['canvas'].draw()
266 def OnPaint(self, event):
268 super(PlotPanel, self).OnPaint(event)
269 self._c['canvas'].draw()
271 def set_curve(self, curve, config=None):
274 for data in curve.data:
275 columns = columns.union(set(data.info['columns']))
276 self._columns = sorted(columns)
277 if self._x_column not in self._columns:
278 self._x_column = self._columns[0]
279 self._y_columns = [y for y in self._y_columns if y in self._columns]
280 if len(self._y_columns) == 0:
281 self._y_columns = self._columns[-1:]
282 if 'x column' in self._c:
283 for i in range(self._c['x column'].GetCount()):
284 self._c['x column'].Delete(0)
285 self._c['x column'].AppendItems(self._columns)
286 self._c['x column'].SetStringSelection(self._x_column)
287 self.update(config=config)
289 def update(self, config=None):
291 config = self._config # use the last cached value
293 self._config = config # cache for later refreshes
294 self._c['figure'].clear()
295 self._c['figure'].suptitle(
296 self._hooke_frame._file_name(self._curve.name),
298 axes = self._c['figure'].add_subplot(1, 1, 1)
300 if config['plot SI format'] == True:
301 d = config['plot decimals']
302 x_n, self._x_unit = split_data_label(self._x_column)
303 y_n, self._y_unit = split_data_label(self._y_columns[0])
304 for y_column in self._y_columns[1:]:
305 y_n, y_unit = split_data_label(y_column)
306 if y_unit != self._y_unit:
307 log = logging.getLogger('hooke')
308 log.warn('y-axes unit mismatch: %s != %s, using %s.'
309 % (self._y_unit, y_unit, self._y_unit))
310 fx = HookeFormatter(decimals=d, unit=self._x_unit)
311 axes.xaxis.set_major_formatter(fx)
312 fy = HookeFormatter(decimals=d, unit=self._y_unit)
313 axes.yaxis.set_major_formatter(fy)
315 if len(self._y_columns) == 1:
320 axes.set_xlabel(self._x_column)
321 if len(self._y_columns) == 1:
322 axes.set_ylabel(self._y_columns[0])
324 self._c['figure'].hold(True)
325 for i,data in enumerate(self._curve.data):
326 for y_column in self._y_columns:
328 x_col = data.info['columns'].index(self._x_column)
329 y_col = data.info['columns'].index(y_column)
331 continue # data is missing a required column
332 axes.plot(data[:,x_col], data[:,y_col],
334 label=('%s, %s' % (data.info['name'], y_column)))
335 if config['plot legend'] == True:
336 axes.legend(loc='best')
337 self._c['canvas'].draw()