]>
Commit | Line | Data |
---|---|---|
7d255c9c HH |
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". | |
4 | ||
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. | |
8 | ||
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 | # | |
20 | """ | |
21 | ||
22 | from wxPython import wx | |
7d255c9c HH |
23 | |
24 | # Not everybody will have Numeric, so let's be cool about it... | |
25 | try: | |
26 | import Numeric | |
27 | except: | |
28 | # bummer! | |
1e4a197e | 29 | d = wx.wxMessageDialog(wx.NULL, |
7d255c9c HH |
30 | """This module requires the Numeric module, which could not be imported. |
31 | It probably is not installed (it's not part of the standard Python | |
1e4a197e RD |
32 | distribution). See the Python site (http://www.python.org) for |
33 | information on downloading source or binaries.""", | |
7d255c9c HH |
34 | "Numeric not found") |
35 | if d.ShowModal() == wx.wxID_CANCEL: | |
1e4a197e RD |
36 | d = wx.wxMessageDialog(wx.NULL, "I kid you not! Pressing Cancel won't help you!", "Not a joke", wx.wxOK) |
37 | d.ShowModal() | |
7d255c9c HH |
38 | import sys |
39 | sys.exit() | |
40 | ||
41 | # | |
42 | # Plotting classes... | |
43 | # | |
44 | class PolyPoints: | |
45 | ||
46 | def __init__(self, points, attr): | |
47 | self.points = Numeric.array(points) | |
1e4a197e | 48 | self.scaled = self.points |
7d255c9c HH |
49 | self.attributes = {} |
50 | for name, value in self._attributes.items(): | |
51 | try: | |
52 | value = attr[name] | |
53 | except KeyError: pass | |
54 | self.attributes[name] = value | |
55 | ||
56 | def boundingBox(self): | |
57 | return Numeric.minimum.reduce(self.points), \ | |
58 | Numeric.maximum.reduce(self.points) | |
59 | ||
60 | def scaleAndShift(self, scale=1, shift=0): | |
61 | self.scaled = scale*self.points+shift | |
62 | ||
63 | ||
64 | class PolyLine(PolyPoints): | |
65 | ||
66 | def __init__(self, points, **attr): | |
67 | PolyPoints.__init__(self, points, attr) | |
68 | ||
69 | _attributes = {'color': 'black', | |
70 | 'width': 1} | |
71 | ||
72 | def draw(self, dc): | |
1e4a197e RD |
73 | color = self.attributes['color'] |
74 | width = self.attributes['width'] | |
75 | arguments = [] | |
76 | dc.SetPen(wx.wxPen(wx.wxNamedColour(color), width)) | |
77 | dc.DrawLines(map(tuple,self.scaled)) | |
7d255c9c HH |
78 | |
79 | ||
80 | class PolyMarker(PolyPoints): | |
81 | ||
82 | def __init__(self, points, **attr): | |
83 | ||
84 | PolyPoints.__init__(self, points, attr) | |
85 | ||
86 | _attributes = {'color': 'black', | |
87 | 'width': 1, | |
88 | 'fillcolor': None, | |
89 | 'size': 2, | |
1e4a197e | 90 | 'fillstyle': wx.wxSOLID, |
7d255c9c HH |
91 | 'outline': 'black', |
92 | 'marker': 'circle'} | |
93 | ||
94 | def draw(self, dc): | |
1e4a197e RD |
95 | color = self.attributes['color'] |
96 | width = self.attributes['width'] | |
7d255c9c HH |
97 | size = self.attributes['size'] |
98 | fillcolor = self.attributes['fillcolor'] | |
99 | fillstyle = self.attributes['fillstyle'] | |
100 | marker = self.attributes['marker'] | |
101 | ||
1e4a197e RD |
102 | dc.SetPen(wx.wxPen(wx.wxNamedColour(color),width)) |
103 | if fillcolor: | |
104 | dc.SetBrush(wx.wxBrush(wx.wxNamedColour(fillcolor),fillstyle)) | |
105 | else: | |
106 | dc.SetBrush(wx.wxBrush(wx.wxNamedColour('black'), wx.wxTRANSPARENT)) | |
7d255c9c | 107 | |
1e4a197e | 108 | self._drawmarkers(dc, self.scaled, marker, size) |
7d255c9c HH |
109 | |
110 | def _drawmarkers(self, dc, coords, marker,size=1): | |
111 | f = eval('self._' +marker) | |
112 | for xc, yc in coords: | |
113 | f(dc, xc, yc, size) | |
114 | ||
115 | def _circle(self, dc, xc, yc, size=1): | |
1e4a197e | 116 | dc.DrawEllipse(xc-2.5*size,yc-2.5*size,5.*size,5.*size) |
7d255c9c HH |
117 | |
118 | def _dot(self, dc, xc, yc, size=1): | |
1e4a197e | 119 | dc.DrawPoint(xc,yc) |
7d255c9c HH |
120 | |
121 | def _square(self, dc, xc, yc, size=1): | |
1e4a197e RD |
122 | dc.DrawRectangle(xc-2.5*size,yc-2.5*size,5.*size,5.*size) |
123 | ||
7d255c9c | 124 | def _triangle(self, dc, xc, yc, size=1): |
1e4a197e RD |
125 | dc.DrawPolygon([(-0.5*size*5,0.2886751*size*5), |
126 | (0.5*size*5,0.2886751*size*5), | |
127 | (0.0,-0.577350*size*5)],xc,yc) | |
7d255c9c HH |
128 | |
129 | def _triangle_down(self, dc, xc, yc, size=1): | |
1e4a197e RD |
130 | dc.DrawPolygon([(-0.5*size*5,-0.2886751*size*5), |
131 | (0.5*size*5,-0.2886751*size*5), | |
132 | (0.0,0.577350*size*5)],xc,yc) | |
7d255c9c HH |
133 | |
134 | def _cross(self, dc, xc, yc, size=1): | |
1e4a197e RD |
135 | dc.DrawLine(xc-2.5*size,yc-2.5*size,xc+2.5*size,yc+2.5*size) |
136 | dc.DrawLine(xc-2.5*size,yc+2.5*size,xc+2.5*size,yc-2.5*size) | |
7d255c9c HH |
137 | |
138 | def _plus(self, dc, xc, yc, size=1): | |
1e4a197e RD |
139 | dc.DrawLine(xc-2.5*size,yc,xc+2.5*size,yc) |
140 | dc.DrawLine(xc,yc-2.5*size,xc,yc+2.5*size) | |
7d255c9c HH |
141 | |
142 | class PlotGraphics: | |
143 | ||
144 | def __init__(self, objects): | |
145 | self.objects = objects | |
146 | ||
147 | def boundingBox(self): | |
1e4a197e RD |
148 | p1, p2 = self.objects[0].boundingBox() |
149 | for o in self.objects[1:]: | |
150 | p1o, p2o = o.boundingBox() | |
151 | p1 = Numeric.minimum(p1, p1o) | |
152 | p2 = Numeric.maximum(p2, p2o) | |
153 | return p1, p2 | |
7d255c9c HH |
154 | |
155 | def scaleAndShift(self, scale=1, shift=0): | |
1e4a197e RD |
156 | for o in self.objects: |
157 | o.scaleAndShift(scale, shift) | |
7d255c9c HH |
158 | |
159 | def draw(self, canvas): | |
1e4a197e RD |
160 | for o in self.objects: |
161 | o.draw(canvas) | |
7d255c9c HH |
162 | |
163 | def __len__(self): | |
1e4a197e | 164 | return len(self.objects) |
7d255c9c HH |
165 | |
166 | def __getitem__(self, item): | |
1e4a197e | 167 | return self.objects[item] |
7d255c9c HH |
168 | |
169 | ||
170 | class PlotCanvas(wx.wxWindow): | |
171 | ||
172 | def __init__(self, parent, id = -1): | |
1e4a197e RD |
173 | wx.wxWindow.__init__(self, parent, id, wx.wxPyDefaultPosition, wx.wxPyDefaultSize) |
174 | self.border = (1,1) | |
175 | self.SetClientSizeWH(400,400) | |
176 | self.SetBackgroundColour(wx.wxNamedColour("white")) | |
7d255c9c | 177 | |
1e4a197e RD |
178 | wx.EVT_SIZE(self,self.reconfigure) |
179 | self._setsize() | |
180 | self.last_draw = None | |
181 | # self.font = self._testFont(font) | |
7d255c9c HH |
182 | |
183 | def OnPaint(self, event): | |
1e4a197e RD |
184 | pdc = wx.wxPaintDC(self) |
185 | if self.last_draw is not None: | |
186 | apply(self.draw, self.last_draw + (pdc,)) | |
7d255c9c HH |
187 | |
188 | def reconfigure(self, event): | |
1e4a197e | 189 | (new_width,new_height) = self.GetClientSizeTuple() |
7d255c9c HH |
190 | if new_width == self.width and new_height == self.height: |
191 | return | |
192 | self._setsize() | |
193 | # self.redraw() | |
194 | ||
195 | def _testFont(self, font): | |
1e4a197e RD |
196 | if font is not None: |
197 | bg = self.canvas.cget('background') | |
198 | try: | |
199 | item = CanvasText(self.canvas, 0, 0, anchor=NW, | |
200 | text='0', fill=bg, font=font) | |
201 | self.canvas.delete(item) | |
202 | except TclError: | |
203 | font = None | |
204 | return font | |
7d255c9c HH |
205 | |
206 | def _setsize(self): | |
1e4a197e RD |
207 | (self.width,self.height) = self.GetClientSizeTuple(); |
208 | self.plotbox_size = 0.97*Numeric.array([self.width, -self.height]) | |
209 | xo = 0.5*(self.width-self.plotbox_size[0]) | |
210 | yo = self.height-0.5*(self.height+self.plotbox_size[1]) | |
211 | self.plotbox_origin = Numeric.array([xo, yo]) | |
7d255c9c HH |
212 | |
213 | def draw(self, graphics, xaxis = None, yaxis = None, dc = None): | |
1e4a197e RD |
214 | if dc == None: dc = wx.wxClientDC(self) |
215 | dc.BeginDrawing() | |
216 | dc.Clear() | |
217 | self.last_draw = (graphics, xaxis, yaxis) | |
218 | p1, p2 = graphics.boundingBox() | |
219 | xaxis = self._axisInterval(xaxis, p1[0], p2[0]) | |
220 | yaxis = self._axisInterval(yaxis, p1[1], p2[1]) | |
221 | text_width = [0., 0.] | |
222 | text_height = [0., 0.] | |
223 | if xaxis is not None: | |
224 | p1[0] = xaxis[0] | |
225 | p2[0] = xaxis[1] | |
226 | xticks = self._ticks(xaxis[0], xaxis[1]) | |
227 | bb = dc.GetTextExtent(xticks[0][1]) | |
228 | text_height[1] = bb[1] | |
229 | text_width[0] = 0.5*bb[0] | |
230 | bb = dc.GetTextExtent(xticks[-1][1]) | |
231 | text_width[1] = 0.5*bb[0] | |
232 | else: | |
233 | xticks = None | |
234 | if yaxis is not None: | |
235 | p1[1] = yaxis[0] | |
236 | p2[1] = yaxis[1] | |
237 | yticks = self._ticks(yaxis[0], yaxis[1]) | |
238 | for y in yticks: | |
239 | bb = dc.GetTextExtent(y[1]) | |
240 | text_width[0] = max(text_width[0],bb[0]) | |
241 | h = 0.5*bb[1] | |
242 | text_height[0] = h | |
243 | text_height[1] = max(text_height[1], h) | |
244 | else: | |
245 | yticks = None | |
246 | text1 = Numeric.array([text_width[0], -text_height[1]]) | |
247 | text2 = Numeric.array([text_width[1], -text_height[0]]) | |
248 | scale = (self.plotbox_size-text1-text2) / (p2-p1) | |
249 | shift = -p1*scale + self.plotbox_origin + text1 | |
250 | self._drawAxes(dc, xaxis, yaxis, p1, p2, | |
7d255c9c | 251 | scale, shift, xticks, yticks) |
1e4a197e RD |
252 | graphics.scaleAndShift(scale, shift) |
253 | graphics.draw(dc) | |
254 | dc.EndDrawing() | |
7d255c9c HH |
255 | |
256 | def _axisInterval(self, spec, lower, upper): | |
1e4a197e RD |
257 | if spec is None: |
258 | return None | |
259 | if spec == 'minimal': | |
260 | if lower == upper: | |
261 | return lower-0.5, upper+0.5 | |
262 | else: | |
263 | return lower, upper | |
264 | if spec == 'automatic': | |
265 | range = upper-lower | |
266 | if range == 0.: | |
267 | return lower-0.5, upper+0.5 | |
268 | log = Numeric.log10(range) | |
269 | power = Numeric.floor(log) | |
270 | fraction = log-power | |
271 | if fraction <= 0.05: | |
272 | power = power-1 | |
273 | grid = 10.**power | |
274 | lower = lower - lower % grid | |
275 | mod = upper % grid | |
276 | if mod != 0: | |
277 | upper = upper - mod + grid | |
278 | return lower, upper | |
279 | if type(spec) == type(()): | |
280 | lower, upper = spec | |
281 | if lower <= upper: | |
282 | return lower, upper | |
283 | else: | |
284 | return upper, lower | |
285 | raise ValueError, str(spec) + ': illegal axis specification' | |
7d255c9c HH |
286 | |
287 | def _drawAxes(self, dc, xaxis, yaxis, | |
288 | bb1, bb2, scale, shift, xticks, yticks): | |
1e4a197e RD |
289 | dc.SetPen(wx.wxPen(wx.wxNamedColour('BLACK'),1)) |
290 | if xaxis is not None: | |
291 | lower, upper = xaxis | |
292 | text = 1 | |
293 | for y, d in [(bb1[1], -3), (bb2[1], 3)]: | |
294 | p1 = scale*Numeric.array([lower, y])+shift | |
295 | p2 = scale*Numeric.array([upper, y])+shift | |
296 | dc.DrawLine(p1[0],p1[1],p2[0],p2[1]) | |
297 | for x, label in xticks: | |
298 | p = scale*Numeric.array([x, y])+shift | |
299 | dc.DrawLine(p[0],p[1],p[0],p[1]+d) | |
300 | if text: | |
301 | dc.DrawText(label,p[0],p[1]) | |
302 | text = 0 | |
303 | ||
304 | if yaxis is not None: | |
305 | lower, upper = yaxis | |
306 | text = 1 | |
307 | h = dc.GetCharHeight() | |
308 | for x, d in [(bb1[0], -3), (bb2[0], 3)]: | |
309 | p1 = scale*Numeric.array([x, lower])+shift | |
310 | p2 = scale*Numeric.array([x, upper])+shift | |
311 | dc.DrawLine(p1[0],p1[1],p2[0],p2[1]) | |
312 | for y, label in yticks: | |
313 | p = scale*Numeric.array([x, y])+shift | |
314 | dc.DrawLine(p[0],p[1],p[0]-d,p[1]) | |
315 | if text: | |
316 | dc.DrawText(label,p[0]-dc.GetTextExtent(label)[0], | |
317 | p[1]-0.5*h) | |
318 | text = 0 | |
7d255c9c HH |
319 | |
320 | def _ticks(self, lower, upper): | |
1e4a197e RD |
321 | ideal = (upper-lower)/7. |
322 | log = Numeric.log10(ideal) | |
323 | power = Numeric.floor(log) | |
324 | fraction = log-power | |
325 | factor = 1. | |
326 | error = fraction | |
327 | for f, lf in self._multiples: | |
328 | e = Numeric.fabs(fraction-lf) | |
329 | if e < error: | |
330 | error = e | |
331 | factor = f | |
332 | grid = factor * 10.**power | |
7d255c9c HH |
333 | if power > 3 or power < -3: |
334 | format = '%+7.0e' | |
335 | elif power >= 0: | |
336 | digits = max(1, int(power)) | |
337 | format = '%' + `digits`+'.0f' | |
338 | else: | |
339 | digits = -int(power) | |
340 | format = '%'+`digits+2`+'.'+`digits`+'f' | |
1e4a197e RD |
341 | ticks = [] |
342 | t = -grid*Numeric.floor(-lower/grid) | |
343 | while t <= upper: | |
344 | ticks.append(t, format % (t,)) | |
345 | t = t + grid | |
346 | return ticks | |
7d255c9c HH |
347 | |
348 | _multiples = [(2., Numeric.log10(2.)), (5., Numeric.log10(5.))] | |
349 | ||
350 | def redraw(self,dc=None): | |
1e4a197e RD |
351 | if self.last_draw is not None: |
352 | apply(self.draw, self.last_draw + (dc,)) | |
7d255c9c HH |
353 | |
354 | def clear(self): | |
355 | self.canvas.delete('all') | |
356 | ||
357 | # | |
358 | # Now a sample implementation using the above... | |
359 | # | |
360 | ||
361 | if __name__ == '__main__': | |
362 | ||
363 | class AppFrame(wx.wxFrame): | |
1e4a197e RD |
364 | def __init__(self, parent, id, title): |
365 | wx.wxFrame.__init__(self, parent, id, title, | |
366 | wx.wxPyDefaultPosition, wx.wxSize(400, 400)) | |
367 | ||
368 | # Now Create the menu bar and items | |
369 | self.mainmenu = wx.wxMenuBar() | |
370 | ||
371 | menu = wx.wxMenu() | |
372 | menu.Append(200, '&Print...', 'Print the current plot') | |
373 | wx.EVT_MENU(self, 200, self.OnFilePrint) | |
374 | menu.Append(209, 'E&xit', 'Enough of this already!') | |
375 | wx.EVT_MENU(self, 209, self.OnFileExit) | |
376 | self.mainmenu.Append(menu, '&File') | |
377 | ||
378 | menu = wx.wxMenu() | |
379 | menu.Append(210, '&Draw', 'Draw plots') | |
380 | wx.EVT_MENU(self,210,self.OnPlotDraw) | |
381 | menu.Append(211, '&Redraw', 'Redraw plots') | |
382 | wx.EVT_MENU(self,211,self.OnPlotRedraw) | |
383 | menu.Append(212, '&Clear', 'Clear canvas') | |
384 | wx.EVT_MENU(self,212,self.OnPlotClear) | |
385 | self.mainmenu.Append(menu, '&Plot') | |
386 | ||
387 | menu = wx.wxMenu() | |
388 | menu.Append(220, '&About', 'About this thing...') | |
389 | wx.EVT_MENU(self, 220, self.OnHelpAbout) | |
390 | self.mainmenu.Append(menu, '&Help') | |
391 | ||
392 | self.SetMenuBar(self.mainmenu) | |
393 | ||
394 | # A status bar to tell people what's happening | |
395 | self.CreateStatusBar(1) | |
396 | ||
397 | self.client = PlotCanvas(self) | |
398 | ||
399 | def OnFilePrint(self, event): | |
400 | d = wx.wxMessageDialog(self, | |
7d255c9c HH |
401 | """As of this writing, printing support in wxPython is shaky at best. |
402 | Are you sure you want to do this?""", "Danger!", wx.wxYES_NO) | |
403 | if d.ShowModal() == wx.wxID_YES: | |
1e4a197e RD |
404 | psdc = wx.wxPostScriptDC("out.ps", wx.TRUE, self) |
405 | self.client.redraw(psdc) | |
7d255c9c | 406 | |
1e4a197e RD |
407 | def OnFileExit(self, event): |
408 | self.Close() | |
7d255c9c | 409 | |
1e4a197e RD |
410 | def OnPlotDraw(self, event): |
411 | self.client.draw(InitObjects(),'automatic','automatic'); | |
7d255c9c | 412 | |
1e4a197e RD |
413 | def OnPlotRedraw(self,event): |
414 | self.client.redraw() | |
7d255c9c | 415 | |
1e4a197e RD |
416 | def OnPlotClear(self,event): |
417 | self.client.last_draw = None | |
418 | dc = wx.wxClientDC(self.client) | |
419 | dc.Clear() | |
7d255c9c | 420 | |
1e4a197e RD |
421 | def OnHelpAbout(self, event): |
422 | about = wx.wxMessageDialog(self, __doc__, "About...", wx.wxOK) | |
423 | about.ShowModal() | |
7d255c9c | 424 | |
1e4a197e RD |
425 | def OnCloseWindow(self, event): |
426 | self.Destroy() | |
7d255c9c HH |
427 | |
428 | def InitObjects(): | |
1e4a197e RD |
429 | # 100 points sin function, plotted as green circles |
430 | data1 = 2.*Numeric.pi*Numeric.arange(200)/200. | |
431 | data1.shape = (100, 2) | |
432 | data1[:,1] = Numeric.sin(data1[:,0]) | |
433 | markers1 = PolyMarker(data1, color='green', marker='circle',size=1) | |
7d255c9c | 434 | |
1e4a197e RD |
435 | # 50 points cos function, plotted as red line |
436 | data1 = 2.*Numeric.pi*Numeric.arange(100)/100. | |
437 | data1.shape = (50,2) | |
438 | data1[:,1] = Numeric.cos(data1[:,0]) | |
439 | lines = PolyLine(data1, color='red') | |
7d255c9c | 440 | |
1e4a197e RD |
441 | # A few more points... |
442 | pi = Numeric.pi | |
443 | markers2 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.), | |
444 | (3.*pi/4., -1)], color='blue', | |
445 | fillcolor='green', marker='cross') | |
7d255c9c | 446 | |
1e4a197e | 447 | return PlotGraphics([markers1, lines, markers2]) |
7d255c9c HH |
448 | |
449 | ||
450 | class MyApp(wx.wxApp): | |
1e4a197e RD |
451 | def OnInit(self): |
452 | frame = AppFrame(wx.NULL, -1, "wxPlotCanvas") | |
453 | frame.Show(wx.TRUE) | |
454 | self.SetTopWindow(frame) | |
455 | return wx.TRUE | |
7d255c9c HH |
456 | |
457 | ||
458 | app = MyApp(0) | |
459 | app.MainLoop() |