Ran update_copyright.py
[hooke.git] / hooke / ui / gui / panel / plot.py
1 # Copyright (C) 2010 Massimo Sandal <devicerandom@gmail.com>
2 #                    Rolf Schmidt <rschmidt@alcor.concordia.ca>
3 #                    W. Trevor King <wking@drexel.edu>
4 #
5 # This file is part of Hooke.
6 #
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.
11 #
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.
16 #
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/>.
20
21 """Plot panel for Hooke.
22
23 Notes
24 -----
25 Originally based on `this example`_.
26
27 .. _this example:
28   http://matplotlib.sourceforge.net/examples/user_interfaces/embedding_in_wx2.html
29 """
30
31 import matplotlib
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
37 import wx
38
39 from ....util.callback import callback, in_callback
40 from ....util.si import ppSI, split_data_label
41 from . import Panel
42
43
44 class HookeFormatter (Formatter):
45     """:class:`matplotlib.ticker.Formatter` using SI prefixes.
46     """
47     def __init__(self, unit='', decimals=2):
48         self.decimals = decimals
49         self.unit = unit
50
51     def __call__(self, x, pos=None):
52         """Return the format for tick val `x` at position `pos`.
53         """
54         if x == 0:
55             return '0'
56         return ppSI(value=x, unit=self.unit, decimals=self.decimals)
57
58
59 class HookeScalarFormatter (ScalarFormatter):
60     """:class:`matplotlib.ticker.ScalarFormatter` using only multiples
61     of three in the mantissa.
62
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.
66     """
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
71
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
77
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')) \
85                    for loc in locs]
86             sigfigs.sort()
87             decimals = sigfigs[-1]
88         else:
89             decimals = self._decimals
90         self.format = '%1.' + str(decimals) + 'f'
91         if self._usetex:
92             self.format = '$%s$' % self.format
93         elif self._useMathText:
94             self.format = '$\mathdefault{%s}$' % self.format
95
96
97 class PlotPanel (Panel, wx.Panel):
98     """UI for graphical curve display.
99     """
100     def __init__(self, callbacks=None, **kwargs):
101         self.display_coordinates = False
102         self.style = 'line'
103         self._curve = None
104         self._x_column = None
105         self._y_column = None
106         super(PlotPanel, self).__init__(
107             name='plot', callbacks=callbacks, **kwargs)
108         self._c = {}
109         self._c['figure'] = Figure()
110         self._c['canvas'] = FigureCanvas(
111             parent=self, id=wx.ID_ANY, figure=self._c['figure'])
112
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.
117         self.SetSizer(sizer)
118         self.Fit()
119
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)
129
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)
142
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)
159         else:
160             sizer.Add(self._c['toolbar'], 0 , wx.LEFT | wx.EXPAND)
161         self._c['toolbar'].update()  # update the axes menu on the toolbar
162
163     def _set_color(self, rgbtuple=None):
164         """Set both figure and canvas colors to `rgbtuple`.
165         """
166         if rgbtuple == None:
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))
172
173     #def SetStatusText(self, text, field=1):
174     #    self.Parent.Parent.statusbar.SetStatusText(text, field)
175
176     def _on_size(self, event):
177         event.Skip()
178         wx.CallAfter(self._resize_canvas)
179
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)
183         pass
184
185     def _on_enter_axes(self, event):
186         self.display_coordinates = True
187
188     def _on_leave_axes(self, event):
189         self.display_coordinates = False
190         #self.SetStatusText('')
191
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)
196             else:
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)])
201             #TODO: pretty format
202             #self.SetStatusText(coordinateString)
203
204     def _on_x_column(self, event):
205         self._x_column = self._c['x column'].GetStringSelection()
206         self.update()
207
208     def _on_y_column(self, event):
209         self._y_column = self._c['y column'].GetStringSelection()
210         self.update()
211
212     def _resize_canvas(self):
213         w,h = self.GetClientSize()
214         if 'toolbar' in self._c:
215             tw,th = self._c['toolbar'].GetSizeTuple()
216         else:
217             th = 0
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()
222         self.Refresh()
223
224     def OnPaint(self, event):
225         print 'painting'
226         super(PlotPanel, self).OnPaint(event)
227         self._c['canvas'].draw()
228
229     def set_curve(self, curve, config=None):
230         self._curve = curve
231         columns = set()
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)
250
251     def update(self, config=None):
252         if config == None:
253             config = self._config  # use the last cached value
254         else:
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),
259             fontsize=12)
260         axes = self._c['figure'].add_subplot(1, 1, 1)
261
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)
270             axes.set_xlabel(x_n)
271             axes.set_ylabel(y_n)
272         else:
273             axes.set_xlabel(self._x_column)
274             axes.set_ylabel(self._y_column)
275
276         self._c['figure'].hold(True)
277         for i,data in enumerate(self._curve.data):
278             try:
279                 x_col = data.info['columns'].index(self._x_column)
280                 y_col = data.info['columns'].index(self._y_column)
281             except ValueError:
282                 continue  # data is missing a required column
283             axes.plot(data[:,x_col], data[:,y_col],
284                       '.',
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()