]>
Commit | Line | Data |
---|---|---|
d14a1e28 RD |
1 | """ |
2 | This is a port of Konrad Hinsen's tkPlotCanvas.py plotting module. | |
3 | After thinking long and hard I came up with the name "wxPlotCanvas.py". | |
1fded56b | 4 | |
d14a1e28 RD |
5 | This file contains two parts; first the re-usable library stuff, then, after |
6 | a "if __name__=='__main__'" test, a simple frame and a few default plots | |
7 | for testing. | |
1fded56b | 8 | |
d14a1e28 RD |
9 | Harm van der Heijden, feb 1999 |
10 | ||
11 | Original comment follows below: | |
12 | # This module defines a plot widget for Tk user interfaces. | |
13 | # It supports only elementary line plots at the moment. | |
14 | # See the example at the end for documentation... | |
15 | # | |
16 | # Written by Konrad Hinsen <hinsen@cnrs-orleans.fr> | |
17 | # With contributions from RajGopal Srinivasan <raj@cherubino.med.jhmi.edu> | |
18 | # Last revision: 1998-7-28 | |
19 | # | |
b881fc78 RD |
20 | """ |
21 | # 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net) | |
22 | # | |
23 | # o Updated for V2.5 compatability | |
24 | # o wx.SpinCtl has some issues that cause the control to | |
25 | # lock up. Noted in other places using it too, it's not this module | |
26 | # that's at fault. | |
27 | # o Added deprecation warning. | |
28 | # | |
29 | ||
30 | import warnings | |
31 | import wx | |
32 | ||
33 | warningmsg = r"""\ | |
34 | ||
35 | THIS MODULE IS NOW DEPRECATED | |
36 | ||
37 | This module has been replaced by wxPyPlot, which in wxPython | |
38 | can be found in wx.lib.plot.py. | |
39 | ||
d14a1e28 RD |
40 | """ |
41 | ||
b881fc78 | 42 | warnings.warn(warningmsg, DeprecationWarning, stacklevel=2) |
d14a1e28 RD |
43 | |
44 | # Not everybody will have Numeric, so let's be cool about it... | |
45 | try: | |
46 | import Numeric | |
47 | except: | |
48 | # bummer! | |
49 | msg = """This module requires the Numeric module, which could not be | |
50 | imported. It probably is not installed (it's not part of the standard | |
51 | Python distribution). See the Python site (http://www.python.org) for | |
52 | information on downloading source or binaries.""" | |
53 | ||
73b79b99 RD |
54 | print msg |
55 | if wx.Platform == '__WXMSW__' and wx.GetApp() is not None: | |
b881fc78 RD |
56 | d = wx.MessageDialog(None, msg, "Numeric not found") |
57 | if d.ShowModal() == wx.ID_CANCEL: | |
58 | d = wx.MessageDialog(None, "I kid you not! Pressing Cancel won't help you!", "Not a joke", wx.OK) | |
d14a1e28 | 59 | d.ShowModal() |
73b79b99 | 60 | raise |
d14a1e28 RD |
61 | |
62 | # | |
63 | # Plotting classes... | |
64 | # | |
65 | class PolyPoints: | |
66 | ||
67 | def __init__(self, points, attr): | |
68 | self.points = Numeric.array(points) | |
69 | self.scaled = self.points | |
70 | self.attributes = {} | |
71 | for name, value in self._attributes.items(): | |
72 | try: | |
73 | value = attr[name] | |
74 | except KeyError: pass | |
75 | self.attributes[name] = value | |
76 | ||
77 | def boundingBox(self): | |
78 | return Numeric.minimum.reduce(self.points), \ | |
79 | Numeric.maximum.reduce(self.points) | |
80 | ||
81 | def scaleAndShift(self, scale=1, shift=0): | |
82 | self.scaled = scale*self.points+shift | |
83 | ||
84 | ||
85 | class PolyLine(PolyPoints): | |
86 | ||
87 | def __init__(self, points, **attr): | |
88 | PolyPoints.__init__(self, points, attr) | |
89 | ||
90 | _attributes = {'color': 'black', | |
91 | 'width': 1} | |
92 | ||
93 | def draw(self, dc): | |
94 | color = self.attributes['color'] | |
95 | width = self.attributes['width'] | |
96 | arguments = [] | |
b881fc78 | 97 | dc.SetPen(wx.Pen(wx.NamedColour(color), width)) |
d14a1e28 RD |
98 | dc.DrawLines(map(tuple,self.scaled)) |
99 | ||
100 | ||
101 | class PolyMarker(PolyPoints): | |
102 | ||
103 | def __init__(self, points, **attr): | |
104 | ||
105 | PolyPoints.__init__(self, points, attr) | |
106 | ||
107 | _attributes = {'color': 'black', | |
108 | 'width': 1, | |
109 | 'fillcolor': None, | |
110 | 'size': 2, | |
b881fc78 | 111 | 'fillstyle': wx.SOLID, |
d14a1e28 RD |
112 | 'outline': 'black', |
113 | 'marker': 'circle'} | |
114 | ||
115 | def draw(self, dc): | |
116 | color = self.attributes['color'] | |
117 | width = self.attributes['width'] | |
118 | size = self.attributes['size'] | |
119 | fillcolor = self.attributes['fillcolor'] | |
120 | fillstyle = self.attributes['fillstyle'] | |
121 | marker = self.attributes['marker'] | |
122 | ||
b881fc78 | 123 | dc.SetPen(wx.Pen(wx.NamedColour(color),width)) |
d14a1e28 | 124 | if fillcolor: |
b881fc78 | 125 | dc.SetBrush(wx.Brush(wx.NamedColour(fillcolor),fillstyle)) |
d14a1e28 | 126 | else: |
b881fc78 | 127 | dc.SetBrush(wx.Brush(wx.NamedColour('black'), wx.TRANSPARENT)) |
d14a1e28 RD |
128 | |
129 | self._drawmarkers(dc, self.scaled, marker, size) | |
130 | ||
131 | def _drawmarkers(self, dc, coords, marker,size=1): | |
132 | f = eval('self._' +marker) | |
133 | for xc, yc in coords: | |
134 | f(dc, xc, yc, size) | |
135 | ||
136 | def _circle(self, dc, xc, yc, size=1): | |
d7403ad2 | 137 | dc.DrawEllipse(xc-2.5*size,yc-2.5*size, 5.*size,5.*size) |
d14a1e28 RD |
138 | |
139 | def _dot(self, dc, xc, yc, size=1): | |
140 | dc.DrawPoint(xc,yc) | |
141 | ||
142 | def _square(self, dc, xc, yc, size=1): | |
143 | dc.DrawRectangle(xc-2.5*size,yc-2.5*size,5.*size,5.*size) | |
144 | ||
145 | def _triangle(self, dc, xc, yc, size=1): | |
146 | dc.DrawPolygon([(-0.5*size*5,0.2886751*size*5), | |
147 | (0.5*size*5,0.2886751*size*5), | |
148 | (0.0,-0.577350*size*5)],xc,yc) | |
149 | ||
150 | def _triangle_down(self, dc, xc, yc, size=1): | |
151 | dc.DrawPolygon([(-0.5*size*5,-0.2886751*size*5), | |
152 | (0.5*size*5,-0.2886751*size*5), | |
153 | (0.0,0.577350*size*5)],xc,yc) | |
154 | ||
155 | def _cross(self, dc, xc, yc, size=1): | |
d7403ad2 RD |
156 | dc.DrawLine(xc-2.5*size, yc-2.5*size, xc+2.5*size,yc+2.5*size) |
157 | dc.DrawLine(xc-2.5*size,yc+2.5*size, xc+2.5*size,yc-2.5*size) | |
d14a1e28 RD |
158 | |
159 | def _plus(self, dc, xc, yc, size=1): | |
d7403ad2 RD |
160 | dc.DrawLine(xc-2.5*size,yc, xc+2.5*size,yc) |
161 | dc.DrawLine(xc,yc-2.5*size,xc, yc+2.5*size) | |
d14a1e28 RD |
162 | |
163 | class PlotGraphics: | |
164 | ||
165 | def __init__(self, objects): | |
166 | self.objects = objects | |
167 | ||
168 | def boundingBox(self): | |
169 | p1, p2 = self.objects[0].boundingBox() | |
170 | for o in self.objects[1:]: | |
171 | p1o, p2o = o.boundingBox() | |
172 | p1 = Numeric.minimum(p1, p1o) | |
173 | p2 = Numeric.maximum(p2, p2o) | |
174 | return p1, p2 | |
175 | ||
176 | def scaleAndShift(self, scale=1, shift=0): | |
177 | for o in self.objects: | |
178 | o.scaleAndShift(scale, shift) | |
179 | ||
180 | def draw(self, canvas): | |
181 | for o in self.objects: | |
182 | o.draw(canvas) | |
183 | ||
184 | def __len__(self): | |
185 | return len(self.objects) | |
186 | ||
187 | def __getitem__(self, item): | |
188 | return self.objects[item] | |
189 | ||
190 | ||
b881fc78 | 191 | class PlotCanvas(wx.Window): |
d14a1e28 RD |
192 | |
193 | def __init__(self, parent, id=-1, | |
b881fc78 | 194 | pos = wx.DefaultPosition, size = wx.DefaultSize, |
d14a1e28 | 195 | style = 0, name = 'plotCanvas'): |
b881fc78 | 196 | wx.Window.__init__(self, parent, id, pos, size, style, name) |
d14a1e28 | 197 | self.border = (1,1) |
b881fc78 RD |
198 | self.SetClientSize((400,400)) |
199 | self.SetBackgroundColour("white") | |
d14a1e28 | 200 | |
b881fc78 RD |
201 | self.Bind(wx.EVT_SIZE,self.reconfigure) |
202 | self.Bind(wx.EVT_PAINT, self.OnPaint) | |
d14a1e28 RD |
203 | self._setsize() |
204 | self.last_draw = None | |
205 | # self.font = self._testFont(font) | |
206 | ||
207 | def OnPaint(self, event): | |
b881fc78 | 208 | pdc = wx.PaintDC(self) |
d14a1e28 RD |
209 | if self.last_draw is not None: |
210 | apply(self.draw, self.last_draw + (pdc,)) | |
211 | ||
212 | def reconfigure(self, event): | |
b881fc78 | 213 | (new_width,new_height) = self.GetClientSize() |
d14a1e28 RD |
214 | if new_width == self.width and new_height == self.height: |
215 | return | |
216 | self._setsize() | |
217 | # self.redraw() | |
218 | ||
219 | def _testFont(self, font): | |
220 | if font is not None: | |
221 | bg = self.canvas.cget('background') | |
222 | try: | |
223 | item = CanvasText(self.canvas, 0, 0, anchor=NW, | |
224 | text='0', fill=bg, font=font) | |
225 | self.canvas.delete(item) | |
226 | except TclError: | |
227 | font = None | |
228 | return font | |
229 | ||
230 | def _setsize(self): | |
b881fc78 | 231 | (self.width,self.height) = self.GetClientSize(); |
d14a1e28 RD |
232 | self.plotbox_size = 0.97*Numeric.array([self.width, -self.height]) |
233 | xo = 0.5*(self.width-self.plotbox_size[0]) | |
234 | yo = self.height-0.5*(self.height+self.plotbox_size[1]) | |
235 | self.plotbox_origin = Numeric.array([xo, yo]) | |
236 | ||
237 | def draw(self, graphics, xaxis = None, yaxis = None, dc = None): | |
b881fc78 | 238 | if dc == None: dc = wx.ClientDC(self) |
d14a1e28 RD |
239 | dc.BeginDrawing() |
240 | dc.Clear() | |
241 | self.last_draw = (graphics, xaxis, yaxis) | |
242 | p1, p2 = graphics.boundingBox() | |
243 | xaxis = self._axisInterval(xaxis, p1[0], p2[0]) | |
244 | yaxis = self._axisInterval(yaxis, p1[1], p2[1]) | |
245 | text_width = [0., 0.] | |
246 | text_height = [0., 0.] | |
247 | if xaxis is not None: | |
248 | p1[0] = xaxis[0] | |
249 | p2[0] = xaxis[1] | |
250 | xticks = self._ticks(xaxis[0], xaxis[1]) | |
251 | bb = dc.GetTextExtent(xticks[0][1]) | |
252 | text_height[1] = bb[1] | |
253 | text_width[0] = 0.5*bb[0] | |
254 | bb = dc.GetTextExtent(xticks[-1][1]) | |
255 | text_width[1] = 0.5*bb[0] | |
256 | else: | |
257 | xticks = None | |
258 | if yaxis is not None: | |
259 | p1[1] = yaxis[0] | |
260 | p2[1] = yaxis[1] | |
261 | yticks = self._ticks(yaxis[0], yaxis[1]) | |
262 | for y in yticks: | |
263 | bb = dc.GetTextExtent(y[1]) | |
264 | text_width[0] = max(text_width[0],bb[0]) | |
265 | h = 0.5*bb[1] | |
266 | text_height[0] = h | |
267 | text_height[1] = max(text_height[1], h) | |
268 | else: | |
269 | yticks = None | |
270 | text1 = Numeric.array([text_width[0], -text_height[1]]) | |
271 | text2 = Numeric.array([text_width[1], -text_height[0]]) | |
272 | scale = (self.plotbox_size-text1-text2) / (p2-p1) | |
273 | shift = -p1*scale + self.plotbox_origin + text1 | |
274 | self._drawAxes(dc, xaxis, yaxis, p1, p2, | |
275 | scale, shift, xticks, yticks) | |
276 | graphics.scaleAndShift(scale, shift) | |
277 | graphics.draw(dc) | |
278 | dc.EndDrawing() | |
279 | ||
280 | def _axisInterval(self, spec, lower, upper): | |
281 | if spec is None: | |
282 | return None | |
283 | if spec == 'minimal': | |
284 | if lower == upper: | |
285 | return lower-0.5, upper+0.5 | |
286 | else: | |
287 | return lower, upper | |
288 | if spec == 'automatic': | |
289 | range = upper-lower | |
290 | if range == 0.: | |
291 | return lower-0.5, upper+0.5 | |
292 | log = Numeric.log10(range) | |
293 | power = Numeric.floor(log) | |
294 | fraction = log-power | |
295 | if fraction <= 0.05: | |
296 | power = power-1 | |
297 | grid = 10.**power | |
298 | lower = lower - lower % grid | |
299 | mod = upper % grid | |
300 | if mod != 0: | |
301 | upper = upper - mod + grid | |
302 | return lower, upper | |
303 | if type(spec) == type(()): | |
304 | lower, upper = spec | |
305 | if lower <= upper: | |
306 | return lower, upper | |
307 | else: | |
308 | return upper, lower | |
309 | raise ValueError, str(spec) + ': illegal axis specification' | |
310 | ||
311 | def _drawAxes(self, dc, xaxis, yaxis, | |
312 | bb1, bb2, scale, shift, xticks, yticks): | |
b881fc78 | 313 | dc.SetPen(wx.Pen(wx.NamedColour('BLACK'),1)) |
d14a1e28 RD |
314 | if xaxis is not None: |
315 | lower, upper = xaxis | |
316 | text = 1 | |
317 | for y, d in [(bb1[1], -3), (bb2[1], 3)]: | |
318 | p1 = scale*Numeric.array([lower, y])+shift | |
319 | p2 = scale*Numeric.array([upper, y])+shift | |
d7403ad2 | 320 | dc.DrawLine(p1[0],p1[1], p2[0],p2[1]) |
d14a1e28 RD |
321 | for x, label in xticks: |
322 | p = scale*Numeric.array([x, y])+shift | |
d7403ad2 | 323 | dc.DrawLine(p[0],p[1], p[0],p[1]+d) |
d14a1e28 | 324 | if text: |
d7403ad2 | 325 | dc.DrawText(label, p[0],p[1]) |
d14a1e28 RD |
326 | text = 0 |
327 | ||
328 | if yaxis is not None: | |
329 | lower, upper = yaxis | |
330 | text = 1 | |
331 | h = dc.GetCharHeight() | |
332 | for x, d in [(bb1[0], -3), (bb2[0], 3)]: | |
333 | p1 = scale*Numeric.array([x, lower])+shift | |
334 | p2 = scale*Numeric.array([x, upper])+shift | |
d7403ad2 | 335 | dc.DrawLine(p1[0],p1[1], p2[0],p2[1]) |
d14a1e28 RD |
336 | for y, label in yticks: |
337 | p = scale*Numeric.array([x, y])+shift | |
d7403ad2 | 338 | dc.DrawLine(p[0],p[1], p[0]-d,p[1]) |
d14a1e28 | 339 | if text: |
b881fc78 | 340 | dc.DrawText(label, |
d7403ad2 | 341 | p[0]-dc.GetTextExtent(label)[0], p[1]-0.5*h) |
d14a1e28 RD |
342 | text = 0 |
343 | ||
344 | def _ticks(self, lower, upper): | |
345 | ideal = (upper-lower)/7. | |
346 | log = Numeric.log10(ideal) | |
347 | power = Numeric.floor(log) | |
348 | fraction = log-power | |
349 | factor = 1. | |
350 | error = fraction | |
351 | for f, lf in self._multiples: | |
352 | e = Numeric.fabs(fraction-lf) | |
353 | if e < error: | |
354 | error = e | |
355 | factor = f | |
356 | grid = factor * 10.**power | |
357 | if power > 3 or power < -3: | |
358 | format = '%+7.0e' | |
359 | elif power >= 0: | |
360 | digits = max(1, int(power)) | |
361 | format = '%' + `digits`+'.0f' | |
362 | else: | |
363 | digits = -int(power) | |
364 | format = '%'+`digits+2`+'.'+`digits`+'f' | |
365 | ticks = [] | |
366 | t = -grid*Numeric.floor(-lower/grid) | |
367 | while t <= upper: | |
368 | ticks.append( (t, format % (t,)) ) | |
369 | t = t + grid | |
370 | return ticks | |
371 | ||
372 | _multiples = [(2., Numeric.log10(2.)), (5., Numeric.log10(5.))] | |
373 | ||
374 | def redraw(self,dc=None): | |
375 | if self.last_draw is not None: | |
376 | apply(self.draw, self.last_draw + (dc,)) | |
377 | ||
378 | def clear(self): | |
379 | self.canvas.delete('all') | |
380 | ||
381 | #--------------------------------------------------------------------------- | |
382 | # if running standalone... | |
383 | # | |
384 | # ...a sample implementation using the above | |
385 | # | |
386 | ||
387 | ||
388 | if __name__ == '__main__': | |
389 | def _InitObjects(): | |
390 | # 100 points sin function, plotted as green circles | |
391 | data1 = 2.*Numeric.pi*Numeric.arange(200)/200. | |
392 | data1.shape = (100, 2) | |
393 | data1[:,1] = Numeric.sin(data1[:,0]) | |
394 | markers1 = PolyMarker(data1, color='green', marker='circle',size=1) | |
395 | ||
396 | # 50 points cos function, plotted as red line | |
397 | data1 = 2.*Numeric.pi*Numeric.arange(100)/100. | |
398 | data1.shape = (50,2) | |
399 | data1[:,1] = Numeric.cos(data1[:,0]) | |
400 | lines = PolyLine(data1, color='red') | |
401 | ||
402 | # A few more points... | |
403 | pi = Numeric.pi | |
404 | markers2 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.), | |
405 | (3.*pi/4., -1)], color='blue', | |
406 | fillcolor='green', marker='cross') | |
407 | ||
408 | return PlotGraphics([markers1, lines, markers2]) | |
409 | ||
410 | ||
b881fc78 | 411 | class AppFrame(wx.Frame): |
d14a1e28 | 412 | def __init__(self, parent, id, title): |
b881fc78 RD |
413 | wx.Frame.__init__(self, parent, id, title, |
414 | wx.DefaultPosition, (400, 400)) | |
d14a1e28 RD |
415 | |
416 | # Now Create the menu bar and items | |
b881fc78 | 417 | self.mainmenu = wx.MenuBar() |
d14a1e28 | 418 | |
b881fc78 | 419 | menu = wx.Menu() |
d14a1e28 | 420 | menu.Append(200, '&Print...', 'Print the current plot') |
b881fc78 | 421 | self.Bind(wx.EVT_MENU, self.OnFilePrint, id=200) |
d14a1e28 | 422 | menu.Append(209, 'E&xit', 'Enough of this already!') |
b881fc78 | 423 | self.Bind(wx.EVT_MENU, self.OnFileExit, id=209) |
d14a1e28 RD |
424 | self.mainmenu.Append(menu, '&File') |
425 | ||
b881fc78 | 426 | menu = wx.Menu() |
d14a1e28 | 427 | menu.Append(210, '&Draw', 'Draw plots') |
b881fc78 | 428 | self.Bind(wx.EVT_MENU,self.OnPlotDraw, id=210) |
d14a1e28 | 429 | menu.Append(211, '&Redraw', 'Redraw plots') |
b881fc78 | 430 | self.Bind(wx.EVT_MENU,self.OnPlotRedraw, id=211) |
d14a1e28 | 431 | menu.Append(212, '&Clear', 'Clear canvas') |
b881fc78 | 432 | self.Bind(wx.EVT_MENU,self.OnPlotClear, id=212) |
d14a1e28 RD |
433 | self.mainmenu.Append(menu, '&Plot') |
434 | ||
b881fc78 | 435 | menu = wx.Menu() |
d14a1e28 | 436 | menu.Append(220, '&About', 'About this thing...') |
b881fc78 | 437 | self.Bind(wx.EVT_MENU, self.OnHelpAbout, id=220) |
d14a1e28 RD |
438 | self.mainmenu.Append(menu, '&Help') |
439 | ||
440 | self.SetMenuBar(self.mainmenu) | |
441 | ||
442 | # A status bar to tell people what's happening | |
443 | self.CreateStatusBar(1) | |
444 | ||
445 | self.client = PlotCanvas(self) | |
446 | ||
447 | def OnFilePrint(self, event): | |
b881fc78 | 448 | d = wx.MessageDialog(self, |
d14a1e28 | 449 | """As of this writing, printing support in wxPython is shaky at best. |
b881fc78 RD |
450 | Are you sure you want to do this?""", "Danger!", wx.YES_NO) |
451 | if d.ShowModal() == wx.ID_YES: | |
452 | psdc = wx.PostScriptDC("out.ps", True, self) | |
d14a1e28 RD |
453 | self.client.redraw(psdc) |
454 | ||
455 | def OnFileExit(self, event): | |
456 | self.Close() | |
457 | ||
458 | def OnPlotDraw(self, event): | |
459 | self.client.draw(_InitObjects(),'automatic','automatic'); | |
460 | ||
461 | def OnPlotRedraw(self,event): | |
462 | self.client.redraw() | |
463 | ||
464 | def OnPlotClear(self,event): | |
465 | self.client.last_draw = None | |
b881fc78 | 466 | dc = wx.ClientDC(self.client) |
d14a1e28 RD |
467 | dc.Clear() |
468 | ||
469 | def OnHelpAbout(self, event): | |
b881fc78 | 470 | about = wx.MessageDialog(self, __doc__, "About...", wx.OK) |
d14a1e28 RD |
471 | about.ShowModal() |
472 | ||
473 | ||
474 | ||
b881fc78 | 475 | class MyApp(wx.App): |
d14a1e28 | 476 | def OnInit(self): |
b881fc78 RD |
477 | frame = AppFrame(None, -1, "wxPlotCanvas") |
478 | frame.Show(True) | |
d14a1e28 | 479 | self.SetTopWindow(frame) |
b881fc78 | 480 | return True |
d14a1e28 RD |
481 | |
482 | ||
483 | app = MyApp(0) | |
484 | app.MainLoop() | |
485 | ||
486 | ||
487 | ||
488 | ||
489 | #---------------------------------------------------------------------------- |