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