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