1 # Copyright (C) 2010 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
32 matplotlib.use('WXAgg') # use wxpython with antigrain (agg) rendering
33 from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
34 from matplotlib.backends.backend_wx import NavigationToolbar2Wx as NavToolbar
35 from matplotlib.figure import Figure
36 from matplotlib.ticker import Formatter, ScalarFormatter
39 from ....util.callback import callback, in_callback
40 from ....util.si import ppSI, split_data_label
44 class HookeFormatter (Formatter):
45 """:class:`matplotlib.ticker.Formatter` using SI prefixes.
47 def __init__(self, unit='', decimals=2):
48 self.decimals = decimals
51 def __call__(self, x, pos=None):
52 """Return the format for tick val `x` at position `pos`.
56 return ppSI(value=x, unit=self.unit, decimals=self.decimals)
59 class HookeScalarFormatter (ScalarFormatter):
60 """:class:`matplotlib.ticker.ScalarFormatter` using only multiples
61 of three in the mantissa.
63 A fixed number of decimals can be displayed with the optional
64 parameter `decimals` . If `decimals` is `None` (default), the number
65 of decimals is defined from the current ticks.
67 def __init__(self, decimals=None, **kwargs):
68 # Can't use super() because ScalarFormatter is an old-style class :(.
69 ScalarFormatter.__init__(self, **kwargs)
70 self._decimals = decimals
72 def _set_orderOfMagnitude(self, *args, **kwargs):
73 """Sets the order of magnitude."""
74 # Can't use super() because ScalarFormatter is an old-style class :(.
75 ScalarFormatter._set_orderOfMagnitude(self, *args, **kwargs)
76 self.orderOfMagnitude -= self.orderOfMagnitude % 3
78 def _set_format(self, *args, **kwargs):
79 """Sets the format string to format all ticklabels."""
80 # Can't use super() because ScalarFormatter is an old-style class :(.
81 ScalarFormatter._set_format(self, *args, **kwargs)
82 if self._decimals is None or self._decimals < 0:
83 locs = (np.asarray(self.locs)-self.offset) / 10**self.orderOfMagnitude+1e-15
84 sigfigs = [len(str('%1.8f'% loc).split('.')[1].rstrip('0')) \
87 decimals = sigfigs[-1]
89 decimals = self._decimals
90 self.format = '%1.' + str(decimals) + 'f'
92 self.format = '$%s$' % self.format
93 elif self._useMathText:
94 self.format = '$\mathdefault{%s}$' % self.format
97 class PlotPanel (Panel, wx.Panel):
98 """UI for graphical curve display.
100 def __init__(self, callbacks=None, **kwargs):
101 self.display_coordinates = False
104 self._x_column = None
105 self._y_column = None
106 super(PlotPanel, self).__init__(
107 name='plot', callbacks=callbacks, **kwargs)
109 self._c['figure'] = Figure()
110 self._c['canvas'] = FigureCanvas(
111 parent=self, id=wx.ID_ANY, figure=self._c['figure'])
113 self._set_color(wx.NamedColor('WHITE'))
114 sizer = wx.BoxSizer(wx.VERTICAL)
115 sizer.Add(self._c['canvas'], 1, wx.LEFT | wx.TOP | wx.GROW)
116 self._setup_toolbar(sizer=sizer) # comment out to remove plot toolbar.
120 self.Bind(wx.EVT_SIZE, self._on_size)
121 self._c['figure'].canvas.mpl_connect(
122 'button_press_event', self._on_click)
123 self._c['figure'].canvas.mpl_connect(
124 'axes_enter_event', self._on_enter_axes)
125 self._c['figure'].canvas.mpl_connect(
126 'axes_leave_event', self._on_leave_axes)
127 self._c['figure'].canvas.mpl_connect(
128 'motion_notify_event', self._on_mouse_move)
130 def _setup_toolbar(self, sizer):
131 self._c['toolbar'] = NavToolbar(self._c['canvas'])
132 self._c['x column'] = wx.Choice(
133 parent=self._c['toolbar'], choices=[])
134 self._c['x column'].SetToolTip(wx.ToolTip('x column'))
135 self._c['toolbar'].AddControl(self._c['x column'])
136 self._c['x column'].Bind(wx.EVT_CHOICE, self._on_x_column)
137 self._c['y column'] = wx.Choice(
138 parent=self._c['toolbar'], choices=[])
139 self._c['y column'].SetToolTip(wx.ToolTip('y column'))
140 self._c['toolbar'].AddControl(self._c['y column'])
141 self._c['y column'].Bind(wx.EVT_CHOICE, self._on_y_column)
143 self._c['toolbar'].Realize() # call after putting items in the toolbar
144 if wx.Platform == '__WXMAC__':
145 # Mac platform (OSX 10.3, MacPython) does not seem to cope with
146 # having a toolbar in a sizer. This work-around gets the buttons
147 # back, but at the expense of having the toolbar at the top
148 self.SetToolBar(self._c['toolbar'])
149 elif wx.Platform == '__WXMSW__':
150 # On Windows platform, default window size is incorrect, so set
151 # toolbar width to figure width.
152 tw, th = toolbar.GetSizeTuple()
153 fw, fh = self._c['canvas'].GetSizeTuple()
154 # By adding toolbar in sizer, we are able to put it at the bottom
155 # of the frame - so appearance is closer to GTK version.
156 # As noted above, doesn't work for Mac.
157 self._c['toolbar'].SetSize(wx.Size(fw, th))
158 sizer.Add(self._c['toolbar'], 0 , wx.LEFT | wx.EXPAND)
160 sizer.Add(self._c['toolbar'], 0 , wx.LEFT | wx.EXPAND)
161 self._c['toolbar'].update() # update the axes menu on the toolbar
163 def _set_color(self, rgbtuple=None):
164 """Set both figure and canvas colors to `rgbtuple`.
167 rgbtuple = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE).Get()
168 col = [c/255.0 for c in rgbtuple]
169 self._c['figure'].set_facecolor(col)
170 self._c['figure'].set_edgecolor(col)
171 self._c['canvas'].SetBackgroundColour(wx.Colour(*rgbtuple))
173 #def SetStatusText(self, text, field=1):
174 # self.Parent.Parent.statusbar.SetStatusText(text, field)
176 def _on_size(self, event):
178 wx.CallAfter(self._resize_canvas)
180 def _on_click(self, event):
181 #self.SetStatusText(str(event.xdata))
182 #print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f'%(event.button, event.x, event.y, event.xdata, event.ydata)
185 def _on_enter_axes(self, event):
186 self.display_coordinates = True
188 def _on_leave_axes(self, event):
189 self.display_coordinates = False
190 #self.SetStatusText('')
192 def _on_mouse_move(self, event):
193 if 'toolbar' in self._c:
194 if event.guiEvent.m_shiftDown:
195 self._c['toolbar'].set_cursor(wx.CURSOR_RIGHT_ARROW)
197 self._c['toolbar'].set_cursor(wx.CURSOR_ARROW)
198 if self.display_coordinates:
199 coordinateString = ''.join(
200 ['x: ', str(event.xdata), ' y: ', str(event.ydata)])
202 #self.SetStatusText(coordinateString)
204 def _on_x_column(self, event):
205 self._x_column = self._c['x column'].GetStringSelection()
208 def _on_y_column(self, event):
209 self._y_column = self._c['y column'].GetStringSelection()
212 def _resize_canvas(self):
213 w,h = self.GetClientSize()
214 if 'toolbar' in self._c:
215 tw,th = self._c['toolbar'].GetSizeTuple()
218 dpi = float(self._c['figure'].get_dpi())
219 self._c['figure'].set_figwidth(w/dpi)
220 self._c['figure'].set_figheight((h-th)/dpi)
221 self._c['canvas'].draw()
224 def OnPaint(self, event):
226 super(PlotPanel, self).OnPaint(event)
227 self._c['canvas'].draw()
229 def set_curve(self, curve, config=None):
232 for data in curve.data:
233 columns = columns.union(set(data.info['columns']))
234 self._columns = sorted(columns)
235 if self._x_column not in self._columns:
236 self._x_column = self._columns[0]
237 if self._y_column not in self._columns:
238 self._y_column = self._columns[-1]
239 if 'x column' in self._c:
240 for i in range(self._c['x column'].GetCount()):
241 self._c['x column'].Delete(0)
242 self._c['x column'].AppendItems(self._columns)
243 self._c['x column'].SetStringSelection(self._x_column)
244 if 'y column' in self._c:
245 for i in range(self._c['y column'].GetCount()):
246 self._c['y column'].Delete(0)
247 self._c['y column'].AppendItems(self._columns)
248 self._c['y column'].SetStringSelection(self._y_column)
249 self.update(config=config)
251 def update(self, config=None):
253 config = self._config # use the last cached value
255 self._config = config # cache for later refreshes
256 self._c['figure'].clear()
257 self._c['figure'].suptitle(
258 self._hooke_frame._file_name(self._curve.name),
260 axes = self._c['figure'].add_subplot(1, 1, 1)
262 if config['plot SI format'] == 'True': # TODO: config should convert
263 d = int(config['plot decimals']) # TODO: config should convert
264 x_n, x_unit = split_data_label(self._x_column)
265 y_n, y_unit = split_data_label(self._y_column)
266 fx = HookeFormatter(decimals=d, unit=x_unit)
267 axes.xaxis.set_major_formatter(fx)
268 fy = HookeFormatter(decimals=d, unit=y_unit)
269 axes.yaxis.set_major_formatter(fy)
273 axes.set_xlabel(self._x_column)
274 axes.set_ylabel(self._y_column)
276 self._c['figure'].hold(True)
277 for i,data in enumerate(self._curve.data):
279 x_col = data.info['columns'].index(self._x_column)
280 y_col = data.info['columns'].index(self._y_column)
282 continue # data is missing a required column
283 axes.plot(data[:,x_col], data[:,y_col],
285 label=data.info['name'])
286 if config['plot legend'] == 'True': # HACK: config should convert
287 axes.legend(loc='best')
288 self._c['canvas'].draw()