3652579aceb6349969e33b3f0c6a94201bf43b11
[hooke.git] / hooke / ui / gui / panel / plot.py
1 # Copyright\r
2 \r
3 """Plot panel for Hooke.\r
4 \r
5 Notes\r
6 -----\r
7 Originally based on `this example`_.\r
8 \r
9 .. _this example:\r
10   http://matplotlib.sourceforge.net/examples/user_interfaces/embedding_in_wx2.html\r
11 """\r
12 \r
13 import matplotlib\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
19 import wx\r
20 \r
21 from ....util.callback import callback, in_callback\r
22 from ....util.si import ppSI, split_data_label\r
23 from . import Panel\r
24 \r
25 \r
26 class HookeFormatter (Formatter):\r
27     """:class:`matplotlib.ticker.Formatter` using SI prefixes.\r
28     """\r
29     def __init__(self, unit='', decimals=2):\r
30         self.decimals = decimals\r
31         self.unit = unit\r
32 \r
33     def __call__(self, x, pos=None):\r
34         """Return the format for tick val `x` at position `pos`.\r
35         """\r
36         if x == 0:\r
37             return '0'\r
38         return ppSI(value=x, unit=self.unit, decimals=self.decimals)\r
39 \r
40 \r
41 class HookeScalarFormatter (ScalarFormatter):\r
42     """:class:`matplotlib.ticker.ScalarFormatter` using only multiples\r
43     of three in the mantissa.\r
44 \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
48     """\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
53 \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
59 \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
67                    for loc in locs]\r
68             sigfigs.sort()\r
69             decimals = sigfigs[-1]\r
70         else:\r
71             decimals = self._decimals\r
72         self.format = '%1.' + str(decimals) + 'f'\r
73         if self._usetex:\r
74             self.format = '$%s$' % self.format\r
75         elif self._useMathText:\r
76             self.format = '$\mathdefault{%s}$' % self.format\r
77 \r
78 \r
79 class PlotPanel (Panel, wx.Panel):\r
80     """UI for graphical curve display.\r
81     """\r
82     def __init__(self, callbacks=None, **kwargs):\r
83         self.display_coordinates = False\r
84         self.style = 'line'\r
85         self._curve = None\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
90         self._c = {}\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
94 \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
100         self.Fit()\r
101 \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
111 \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
124 \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
141         else:\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
144 \r
145     def _set_color(self, rgbtuple=None):\r
146         """Set both figure and canvas colors to `rgbtuple`.\r
147         """\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
154 \r
155     #def SetStatusText(self, text, field=1):\r
156     #    self.Parent.Parent.statusbar.SetStatusText(text, field)\r
157 \r
158     def _on_size(self, event):\r
159         event.Skip()\r
160         wx.CallAfter(self._resize_canvas)\r
161 \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
165         pass\r
166 \r
167     def _on_enter_axes(self, event):\r
168         self.display_coordinates = True\r
169 \r
170     def _on_leave_axes(self, event):\r
171         self.display_coordinates = False\r
172         #self.SetStatusText('')\r
173 \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
178             else:\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
185 \r
186     def _on_x_column(self, event):\r
187         self._x_column = self._c['x column'].GetStringSelection()\r
188         self.update()\r
189 \r
190     def _on_y_column(self, event):\r
191         self._y_column = self._c['y column'].GetStringSelection()\r
192         self.update()\r
193 \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
198         else:\r
199             th = 0\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
204         self.Refresh()\r
205 \r
206     def OnPaint(self, event):\r
207         print 'painting'\r
208         super(PlotPanel, self).OnPaint(event)\r
209         self._c['canvas'].draw()\r
210 \r
211     def set_curve(self, curve, config=None):\r
212         self._curve = curve\r
213         columns = set()\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
232 \r
233     def update(self, config=None):\r
234         if config == None:\r
235             config = self._config  # use the last cached value\r
236         else:\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
241             fontsize=12)\r
242         axes = self._c['figure'].add_subplot(1, 1, 1)\r
243 \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
254         else:\r
255             axes.set_xlabel(self._x_column)\r
256             axes.set_ylabel(self._y_column)\r
257 \r
258         self._c['figure'].hold(True)\r
259         for i,data in enumerate(self._curve.data):\r
260             try:\r
261                 x_col = data.info['columns'].index(self._x_column)\r
262                 y_col = data.info['columns'].index(self._y_column)\r
263             except ValueError:\r
264                 continue  # data is missing a required column\r
265             axes.plot(data[:,x_col], data[:,y_col],\r
266                       '.',\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