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