]> git.saurik.com Git - wxWidgets.git/blob - utils/wxPython/lib/wxPlotCanvas.py
more change documentation
[wxWidgets.git] / utils / wxPython / lib / wxPlotCanvas.py
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
23 import string
24
25 # Not everybody will have Numeric, so let's be cool about it...
26 try:
27 import Numeric
28 except:
29 # bummer!
30 d = wx.wxMessageDialog(wx.NULL,
31 """This module requires the Numeric module, which could not be imported.
32 It probably is not installed (it's not part of the standard Python
33 distribution). See the Python site (http://www.python.org) for
34 information on downloading source or binaries.""",
35 "Numeric not found")
36 if d.ShowModal() == wx.wxID_CANCEL:
37 d = wx.wxMessageDialog(wx.NULL, "I kid you not! Pressing Cancel won't help you!", "Not a joke", wx.wxOK)
38 d.ShowModal()
39 raise ImportError
40
41 #
42 # Plotting classes...
43 #
44 class PolyPoints:
45
46 def __init__(self, points, attr):
47 self.points = Numeric.array(points)
48 self.scaled = self.points
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):
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))
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,
90 'fillstyle': wx.wxSOLID,
91 'outline': 'black',
92 'marker': 'circle'}
93
94 def draw(self, dc):
95 color = self.attributes['color']
96 width = self.attributes['width']
97 size = self.attributes['size']
98 fillcolor = self.attributes['fillcolor']
99 fillstyle = self.attributes['fillstyle']
100 marker = self.attributes['marker']
101
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))
107
108 self._drawmarkers(dc, self.scaled, marker, size)
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):
116 dc.DrawEllipse(xc-2.5*size,yc-2.5*size,5.*size,5.*size)
117
118 def _dot(self, dc, xc, yc, size=1):
119 dc.DrawPoint(xc,yc)
120
121 def _square(self, dc, xc, yc, size=1):
122 dc.DrawRectangle(xc-2.5*size,yc-2.5*size,5.*size,5.*size)
123
124 def _triangle(self, dc, xc, yc, size=1):
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)
128
129 def _triangle_down(self, dc, xc, yc, size=1):
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)
133
134 def _cross(self, dc, xc, yc, size=1):
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)
137
138 def _plus(self, dc, xc, yc, size=1):
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)
141
142 class PlotGraphics:
143
144 def __init__(self, objects):
145 self.objects = objects
146
147 def boundingBox(self):
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
154
155 def scaleAndShift(self, scale=1, shift=0):
156 for o in self.objects:
157 o.scaleAndShift(scale, shift)
158
159 def draw(self, canvas):
160 for o in self.objects:
161 o.draw(canvas)
162
163 def __len__(self):
164 return len(self.objects)
165
166 def __getitem__(self, item):
167 return self.objects[item]
168
169
170 class PlotCanvas(wx.wxWindow):
171
172 def __init__(self, parent, id = -1):
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"))
177
178 wx.EVT_SIZE(self,self.reconfigure)
179 self._setsize()
180 self.last_draw = None
181 # self.font = self._testFont(font)
182
183 def OnPaint(self, event):
184 pdc = wx.wxPaintDC(self)
185 if self.last_draw is not None:
186 apply(self.draw, self.last_draw + (pdc,))
187
188 def reconfigure(self, event):
189 (new_width,new_height) = self.GetClientSizeTuple()
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):
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
205
206 def _setsize(self):
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])
212
213 def draw(self, graphics, xaxis = None, yaxis = None, dc = None):
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,
251 scale, shift, xticks, yticks)
252 graphics.scaleAndShift(scale, shift)
253 graphics.draw(dc)
254 dc.EndDrawing()
255
256 def _axisInterval(self, spec, lower, upper):
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'
286
287 def _drawAxes(self, dc, xaxis, yaxis,
288 bb1, bb2, scale, shift, xticks, yticks):
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
319
320 def _ticks(self, lower, upper):
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
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'
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
347
348 _multiples = [(2., Numeric.log10(2.)), (5., Numeric.log10(5.))]
349
350 def redraw(self,dc=None):
351 if self.last_draw is not None:
352 apply(self.draw, self.last_draw + (dc,))
353
354 def clear(self):
355 self.canvas.delete('all')
356
357 #---------------------------------------------------------------------------
358 # if running standalone...
359 #
360 # ...a sample implementation using the above
361 #
362
363
364 if __name__ == '__main__':
365 def _InitObjects():
366 # 100 points sin function, plotted as green circles
367 data1 = 2.*Numeric.pi*Numeric.arange(200)/200.
368 data1.shape = (100, 2)
369 data1[:,1] = Numeric.sin(data1[:,0])
370 markers1 = PolyMarker(data1, color='green', marker='circle',size=1)
371
372 # 50 points cos function, plotted as red line
373 data1 = 2.*Numeric.pi*Numeric.arange(100)/100.
374 data1.shape = (50,2)
375 data1[:,1] = Numeric.cos(data1[:,0])
376 lines = PolyLine(data1, color='red')
377
378 # A few more points...
379 pi = Numeric.pi
380 markers2 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
381 (3.*pi/4., -1)], color='blue',
382 fillcolor='green', marker='cross')
383
384 return PlotGraphics([markers1, lines, markers2])
385
386
387 class AppFrame(wx.wxFrame):
388 def __init__(self, parent, id, title):
389 wx.wxFrame.__init__(self, parent, id, title,
390 wx.wxPyDefaultPosition, wx.wxSize(400, 400))
391
392 # Now Create the menu bar and items
393 self.mainmenu = wx.wxMenuBar()
394
395 menu = wx.wxMenu()
396 menu.Append(200, '&Print...', 'Print the current plot')
397 wx.EVT_MENU(self, 200, self.OnFilePrint)
398 menu.Append(209, 'E&xit', 'Enough of this already!')
399 wx.EVT_MENU(self, 209, self.OnFileExit)
400 self.mainmenu.Append(menu, '&File')
401
402 menu = wx.wxMenu()
403 menu.Append(210, '&Draw', 'Draw plots')
404 wx.EVT_MENU(self,210,self.OnPlotDraw)
405 menu.Append(211, '&Redraw', 'Redraw plots')
406 wx.EVT_MENU(self,211,self.OnPlotRedraw)
407 menu.Append(212, '&Clear', 'Clear canvas')
408 wx.EVT_MENU(self,212,self.OnPlotClear)
409 self.mainmenu.Append(menu, '&Plot')
410
411 menu = wx.wxMenu()
412 menu.Append(220, '&About', 'About this thing...')
413 wx.EVT_MENU(self, 220, self.OnHelpAbout)
414 self.mainmenu.Append(menu, '&Help')
415
416 self.SetMenuBar(self.mainmenu)
417
418 # A status bar to tell people what's happening
419 self.CreateStatusBar(1)
420
421 self.client = PlotCanvas(self)
422
423 def OnFilePrint(self, event):
424 d = wx.wxMessageDialog(self,
425 """As of this writing, printing support in wxPython is shaky at best.
426 Are you sure you want to do this?""", "Danger!", wx.wxYES_NO)
427 if d.ShowModal() == wx.wxID_YES:
428 psdc = wx.wxPostScriptDC("out.ps", wx.TRUE, self)
429 self.client.redraw(psdc)
430
431 def OnFileExit(self, event):
432 self.Close()
433
434 def OnPlotDraw(self, event):
435 self.client.draw(_InitObjects(),'automatic','automatic');
436
437 def OnPlotRedraw(self,event):
438 self.client.redraw()
439
440 def OnPlotClear(self,event):
441 self.client.last_draw = None
442 dc = wx.wxClientDC(self.client)
443 dc.Clear()
444
445 def OnHelpAbout(self, event):
446 about = wx.wxMessageDialog(self, __doc__, "About...", wx.wxOK)
447 about.ShowModal()
448
449 def OnCloseWindow(self, event):
450 self.Destroy()
451
452
453 class MyApp(wx.wxApp):
454 def OnInit(self):
455 frame = AppFrame(wx.NULL, -1, "wxPlotCanvas")
456 frame.Show(wx.TRUE)
457 self.SetTopWindow(frame)
458 return wx.TRUE
459
460
461 app = MyApp(0)
462 app.MainLoop()
463
464
465
466
467 #----------------------------------------------------------------------------