]> git.saurik.com Git - wxWidgets.git/blob - wxPython/tests/wxPlotCanvas.py
don't generate an endless stream of asserts for the 0-sized images
[wxWidgets.git] / wxPython / tests / 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 # Now a sample implementation using the above...
360 #
361
362 if __name__ == '__main__':
363
364 class AppFrame(wx.wxFrame):
365 def __init__(self, parent, id, title):
366 wx.wxFrame.__init__(self, parent, id, title,
367 wx.wxPyDefaultPosition, wx.wxSize(400, 400))
368
369 # Now Create the menu bar and items
370 self.mainmenu = wx.wxMenuBar()
371
372 menu = wx.wxMenu()
373 menu.Append(200, '&Print...', 'Print the current plot')
374 wx.EVT_MENU(self, 200, self.OnFilePrint)
375 menu.Append(209, 'E&xit', 'Enough of this already!')
376 wx.EVT_MENU(self, 209, self.OnFileExit)
377 self.mainmenu.Append(menu, '&File')
378
379 menu = wx.wxMenu()
380 menu.Append(210, '&Draw', 'Draw plots')
381 wx.EVT_MENU(self,210,self.OnPlotDraw)
382 menu.Append(211, '&Redraw', 'Redraw plots')
383 wx.EVT_MENU(self,211,self.OnPlotRedraw)
384 menu.Append(212, '&Clear', 'Clear canvas')
385 wx.EVT_MENU(self,212,self.OnPlotClear)
386 self.mainmenu.Append(menu, '&Plot')
387
388 menu = wx.wxMenu()
389 menu.Append(220, '&About', 'About this thing...')
390 wx.EVT_MENU(self, 220, self.OnHelpAbout)
391 self.mainmenu.Append(menu, '&Help')
392
393 self.SetMenuBar(self.mainmenu)
394
395 # A status bar to tell people what's happening
396 self.CreateStatusBar(1)
397
398 self.client = PlotCanvas(self)
399
400 def OnFilePrint(self, event):
401 d = wx.wxMessageDialog(self,
402 """As of this writing, printing support in wxPython is shaky at best.
403 Are you sure you want to do this?""", "Danger!", wx.wxYES_NO)
404 if d.ShowModal() == wx.wxID_YES:
405 psdc = wx.wxPostScriptDC("out.ps", wx.TRUE, self)
406 self.client.redraw(psdc)
407
408 def OnFileExit(self, event):
409 self.Close()
410
411 def OnPlotDraw(self, event):
412 self.client.draw(InitObjects(),'automatic','automatic');
413
414 def OnPlotRedraw(self,event):
415 self.client.redraw()
416
417 def OnPlotClear(self,event):
418 self.client.last_draw = None
419 dc = wx.wxClientDC(self.client)
420 dc.Clear()
421
422 def OnHelpAbout(self, event):
423 about = wx.wxMessageDialog(self, __doc__, "About...", wx.wxOK)
424 about.ShowModal()
425
426 def OnCloseWindow(self, event):
427 self.Destroy()
428
429 def InitObjects():
430 # 100 points sin function, plotted as green circles
431 data1 = 2.*Numeric.pi*Numeric.arange(200)/200.
432 data1.shape = (100, 2)
433 data1[:,1] = Numeric.sin(data1[:,0])
434 markers1 = PolyMarker(data1, color='green', marker='circle',size=1)
435
436 # 50 points cos function, plotted as red line
437 data1 = 2.*Numeric.pi*Numeric.arange(100)/100.
438 data1.shape = (50,2)
439 data1[:,1] = Numeric.cos(data1[:,0])
440 lines = PolyLine(data1, color='red')
441
442 # A few more points...
443 pi = Numeric.pi
444 markers2 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
445 (3.*pi/4., -1)], color='blue',
446 fillcolor='green', marker='cross')
447
448 return PlotGraphics([markers1, lines, markers2])
449
450
451 class MyApp(wx.wxApp):
452 def OnInit(self):
453 frame = AppFrame(wx.NULL, -1, "wxPlotCanvas")
454 frame.Show(wx.TRUE)
455 self.SetTopWindow(frame)
456 return wx.TRUE
457
458
459 app = MyApp(0)
460 app.MainLoop()