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