3 """Plot panel for Hooke.
\r
7 Originally based on `this example`_.
\r
10 http://matplotlib.sourceforge.net/examples/user_interfaces/embedding_in_wx2.html
\r
14 matplotlib.use('WXAgg') # use wxpython with antigrain (agg) rendering
\r
15 from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
\r
16 from matplotlib.backends.backend_wx import NavigationToolbar2Wx as NavToolbar
\r
17 from matplotlib.figure import Figure
\r
18 from matplotlib.ticker import Formatter, ScalarFormatter
\r
21 from ....util.callback import callback, in_callback
\r
22 from ....util.si import ppSI, split_data_label
\r
26 class HookeFormatter (Formatter):
\r
27 """:class:`matplotlib.ticker.Formatter` using SI prefixes.
\r
29 def __init__(self, unit='', decimals=2):
\r
30 self.decimals = decimals
\r
33 def __call__(self, x, pos=None):
\r
34 """Return the format for tick val `x` at position `pos`.
\r
38 return ppSI(value=x, unit=self.unit, decimals=self.decimals)
\r
41 class HookeScalarFormatter (ScalarFormatter):
\r
42 """:class:`matplotlib.ticker.ScalarFormatter` using only multiples
\r
43 of three in the mantissa.
\r
45 A fixed number of decimals can be displayed with the optional
\r
46 parameter `decimals` . If `decimals` is `None` (default), the number
\r
47 of decimals is defined from the current ticks.
\r
49 def __init__(self, decimals=None, **kwargs):
\r
50 # Can't use super() because ScalarFormatter is an old-style class :(.
\r
51 ScalarFormatter.__init__(self, **kwargs)
\r
52 self._decimals = decimals
\r
54 def _set_orderOfMagnitude(self, *args, **kwargs):
\r
55 """Sets the order of magnitude."""
\r
56 # Can't use super() because ScalarFormatter is an old-style class :(.
\r
57 ScalarFormatter._set_orderOfMagnitude(self, *args, **kwargs)
\r
58 self.orderOfMagnitude -= self.orderOfMagnitude % 3
\r
60 def _set_format(self, *args, **kwargs):
\r
61 """Sets the format string to format all ticklabels."""
\r
62 # Can't use super() because ScalarFormatter is an old-style class :(.
\r
63 ScalarFormatter._set_format(self, *args, **kwargs)
\r
64 if self._decimals is None or self._decimals < 0:
\r
65 locs = (np.asarray(self.locs)-self.offset) / 10**self.orderOfMagnitude+1e-15
\r
66 sigfigs = [len(str('%1.8f'% loc).split('.')[1].rstrip('0')) \
\r
69 decimals = sigfigs[-1]
\r
71 decimals = self._decimals
\r
72 self.format = '%1.' + str(decimals) + 'f'
\r
74 self.format = '$%s$' % self.format
\r
75 elif self._useMathText:
\r
76 self.format = '$\mathdefault{%s}$' % self.format
\r
79 class PlotPanel (Panel, wx.Panel):
\r
80 """UI for graphical curve display.
\r
82 def __init__(self, callbacks=None, **kwargs):
\r
83 self.display_coordinates = False
\r
86 self._x_column = None
\r
87 self._y_column = None
\r
88 super(PlotPanel, self).__init__(
\r
89 name='plot', callbacks=callbacks, **kwargs)
\r
91 self._c['figure'] = Figure()
\r
92 self._c['canvas'] = FigureCanvas(
\r
93 parent=self, id=wx.ID_ANY, figure=self._c['figure'])
\r
95 self._set_color(wx.NamedColor('WHITE'))
\r
96 sizer = wx.BoxSizer(wx.VERTICAL)
\r
97 sizer.Add(self._c['canvas'], 1, wx.LEFT | wx.TOP | wx.GROW)
\r
98 self._setup_toolbar(sizer=sizer) # comment out to remove plot toolbar.
\r
99 self.SetSizer(sizer)
\r
102 self.Bind(wx.EVT_SIZE, self._on_size)
\r
103 self._c['figure'].canvas.mpl_connect(
\r
104 'button_press_event', self._on_click)
\r
105 self._c['figure'].canvas.mpl_connect(
\r
106 'axes_enter_event', self._on_enter_axes)
\r
107 self._c['figure'].canvas.mpl_connect(
\r
108 'axes_leave_event', self._on_leave_axes)
\r
109 self._c['figure'].canvas.mpl_connect(
\r
110 'motion_notify_event', self._on_mouse_move)
\r
112 def _setup_toolbar(self, sizer):
\r
113 self._c['toolbar'] = NavToolbar(self._c['canvas'])
\r
114 self._c['x column'] = wx.Choice(
\r
115 parent=self._c['toolbar'], choices=[])
\r
116 self._c['x column'].SetToolTip(wx.ToolTip('x column'))
\r
117 self._c['toolbar'].AddControl(self._c['x column'])
\r
118 self._c['x column'].Bind(wx.EVT_CHOICE, self._on_x_column)
\r
119 self._c['y column'] = wx.Choice(
\r
120 parent=self._c['toolbar'], choices=[])
\r
121 self._c['y column'].SetToolTip(wx.ToolTip('y column'))
\r
122 self._c['toolbar'].AddControl(self._c['y column'])
\r
123 self._c['y column'].Bind(wx.EVT_CHOICE, self._on_y_column)
\r
125 self._c['toolbar'].Realize() # call after putting items in the toolbar
\r
126 if wx.Platform == '__WXMAC__':
\r
127 # Mac platform (OSX 10.3, MacPython) does not seem to cope with
\r
128 # having a toolbar in a sizer. This work-around gets the buttons
\r
129 # back, but at the expense of having the toolbar at the top
\r
130 self.SetToolBar(self._c['toolbar'])
\r
131 elif wx.Platform == '__WXMSW__':
\r
132 # On Windows platform, default window size is incorrect, so set
\r
133 # toolbar width to figure width.
\r
134 tw, th = toolbar.GetSizeTuple()
\r
135 fw, fh = self._c['canvas'].GetSizeTuple()
\r
136 # By adding toolbar in sizer, we are able to put it at the bottom
\r
137 # of the frame - so appearance is closer to GTK version.
\r
138 # As noted above, doesn't work for Mac.
\r
139 self._c['toolbar'].SetSize(wx.Size(fw, th))
\r
140 sizer.Add(self._c['toolbar'], 0 , wx.LEFT | wx.EXPAND)
\r
142 sizer.Add(self._c['toolbar'], 0 , wx.LEFT | wx.EXPAND)
\r
143 self._c['toolbar'].update() # update the axes menu on the toolbar
\r
145 def _set_color(self, rgbtuple=None):
\r
146 """Set both figure and canvas colors to `rgbtuple`.
\r
148 if rgbtuple == None:
\r
149 rgbtuple = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE).Get()
\r
150 col = [c/255.0 for c in rgbtuple]
\r
151 self._c['figure'].set_facecolor(col)
\r
152 self._c['figure'].set_edgecolor(col)
\r
153 self._c['canvas'].SetBackgroundColour(wx.Colour(*rgbtuple))
\r
155 #def SetStatusText(self, text, field=1):
\r
156 # self.Parent.Parent.statusbar.SetStatusText(text, field)
\r
158 def _on_size(self, event):
\r
160 wx.CallAfter(self._resize_canvas)
\r
162 def _on_click(self, event):
\r
163 #self.SetStatusText(str(event.xdata))
\r
164 #print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f'%(event.button, event.x, event.y, event.xdata, event.ydata)
\r
167 def _on_enter_axes(self, event):
\r
168 self.display_coordinates = True
\r
170 def _on_leave_axes(self, event):
\r
171 self.display_coordinates = False
\r
172 #self.SetStatusText('')
\r
174 def _on_mouse_move(self, event):
\r
175 if 'toolbar' in self._c:
\r
176 if event.guiEvent.m_shiftDown:
\r
177 self._c['toolbar'].set_cursor(wx.CURSOR_RIGHT_ARROW)
\r
179 self._c['toolbar'].set_cursor(wx.CURSOR_ARROW)
\r
180 if self.display_coordinates:
\r
181 coordinateString = ''.join(
\r
182 ['x: ', str(event.xdata), ' y: ', str(event.ydata)])
\r
183 #TODO: pretty format
\r
184 #self.SetStatusText(coordinateString)
\r
186 def _on_x_column(self, event):
\r
187 self._x_column = self._c['x column'].GetStringSelection()
\r
190 def _on_y_column(self, event):
\r
191 self._y_column = self._c['y column'].GetStringSelection()
\r
194 def _resize_canvas(self):
\r
195 w,h = self.GetClientSize()
\r
196 if 'toolbar' in self._c:
\r
197 tw,th = self._c['toolbar'].GetSizeTuple()
\r
200 dpi = float(self._c['figure'].get_dpi())
\r
201 self._c['figure'].set_figwidth(w/dpi)
\r
202 self._c['figure'].set_figheight((h-th)/dpi)
\r
203 self._c['canvas'].draw()
\r
206 def OnPaint(self, event):
\r
208 super(PlotPanel, self).OnPaint(event)
\r
209 self._c['canvas'].draw()
\r
211 def set_curve(self, curve, config=None):
\r
212 self._curve = curve
\r
214 for data in curve.data:
\r
215 columns = columns.union(set(data.info['columns']))
\r
216 self._columns = sorted(columns)
\r
217 if self._x_column not in self._columns:
\r
218 self._x_column = self._columns[0]
\r
219 if self._y_column not in self._columns:
\r
220 self._y_column = self._columns[-1]
\r
221 if 'x column' in self._c:
\r
222 for i in range(self._c['x column'].GetCount()):
\r
223 self._c['x column'].Delete(0)
\r
224 self._c['x column'].AppendItems(self._columns)
\r
225 self._c['x column'].SetStringSelection(self._x_column)
\r
226 if 'y column' in self._c:
\r
227 for i in range(self._c['y column'].GetCount()):
\r
228 self._c['y column'].Delete(0)
\r
229 self._c['y column'].AppendItems(self._columns)
\r
230 self._c['y column'].SetStringSelection(self._y_column)
\r
231 self.update(config=config)
\r
233 def update(self, config=None):
\r
235 config = self._config # use the last cached value
\r
237 self._config = config # cache for later refreshes
\r
238 self._c['figure'].clear()
\r
239 self._c['figure'].suptitle(
\r
240 self._hooke_frame._file_name(self._curve.name),
\r
242 axes = self._c['figure'].add_subplot(1, 1, 1)
\r
244 if config['plot SI format'] == 'True': # TODO: config should convert
\r
245 d = int(config['plot decimals']) # TODO: config should convert
\r
246 x_n, x_unit = split_data_label(self._x_column)
\r
247 y_n, y_unit = split_data_label(self._y_column)
\r
248 fx = HookeFormatter(decimals=d, unit=x_unit)
\r
249 axes.xaxis.set_major_formatter(fx)
\r
250 fy = HookeFormatter(decimals=d, unit=y_unit)
\r
251 axes.yaxis.set_major_formatter(fy)
\r
252 axes.set_xlabel(x_n)
\r
253 axes.set_ylabel(y_n)
\r
255 axes.set_xlabel(self._x_column)
\r
256 axes.set_ylabel(self._y_column)
\r
258 self._c['figure'].hold(True)
\r
259 for i,data in enumerate(self._curve.data):
\r
261 x_col = data.info['columns'].index(self._x_column)
\r
262 y_col = data.info['columns'].index(self._y_column)
\r
264 continue # data is missing a required column
\r
265 axes.plot(data[:,x_col], data[:,y_col],
\r
267 label=data.info['name'])
\r
268 if config['plot legend'] == 'True': # HACK: config should convert
\r
269 axes.legend(loc='best')
\r
270 self._c['canvas'].draw()
\r