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