]>
git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/wxPlotCanvas.py
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".
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
9 Harm van der Heijden, feb 1999
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...
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
22 from wxPython
import wx
24 # Not everybody will have Numeric, so let's be cool about it...
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."""
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
)
48 def __init__(self
, points
, attr
):
49 self
.points
= Numeric
.array(points
)
50 self
.scaled
= self
.points
52 for name
, value
in self
._attributes
.items():
56 self
.attributes
[name
] = value
58 def boundingBox(self
):
59 return Numeric
.minimum
.reduce(self
.points
), \
60 Numeric
.maximum
.reduce(self
.points
)
62 def scaleAndShift(self
, scale
=1, shift
=0):
63 self
.scaled
= scale
*self
.points
+shift
66 class PolyLine(PolyPoints
):
68 def __init__(self
, points
, **attr
):
69 PolyPoints
.__init
__(self
, points
, attr
)
71 _attributes
= {'color': 'black',
75 color
= self
.attributes
['color']
76 width
= self
.attributes
['width']
78 dc
.SetPen(wx
.wxPen(wx
.wxNamedColour(color
), width
))
79 dc
.DrawLines(map(tuple,self
.scaled
))
82 class PolyMarker(PolyPoints
):
84 def __init__(self
, points
, **attr
):
86 PolyPoints
.__init
__(self
, points
, attr
)
88 _attributes
= {'color': 'black',
92 'fillstyle': wx
.wxSOLID
,
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']
104 dc
.SetPen(wx
.wxPen(wx
.wxNamedColour(color
),width
))
106 dc
.SetBrush(wx
.wxBrush(wx
.wxNamedColour(fillcolor
),fillstyle
))
108 dc
.SetBrush(wx
.wxBrush(wx
.wxNamedColour('black'), wx
.wxTRANSPARENT
))
110 self
._drawmarkers
(dc
, self
.scaled
, marker
, size
)
112 def _drawmarkers(self
, dc
, coords
, marker
,size
=1):
113 f
= eval('self._' +marker
)
114 for xc
, yc
in coords
:
117 def _circle(self
, dc
, xc
, yc
, size
=1):
118 dc
.DrawEllipse(xc
-2.5*size
,yc
-2.5*size
,5.*size
,5.*size
)
120 def _dot(self
, dc
, xc
, yc
, size
=1):
123 def _square(self
, dc
, xc
, yc
, size
=1):
124 dc
.DrawRectangle(xc
-2.5*size
,yc
-2.5*size
,5.*size
,5.*size
)
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
)
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
)
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
)
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
)
146 def __init__(self
, objects
):
147 self
.objects
= objects
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
)
157 def scaleAndShift(self
, scale
=1, shift
=0):
158 for o
in self
.objects
:
159 o
.scaleAndShift(scale
, shift
)
161 def draw(self
, canvas
):
162 for o
in self
.objects
:
166 return len(self
.objects
)
168 def __getitem__(self
, item
):
169 return self
.objects
[item
]
172 class PlotCanvas(wx
.wxWindow
):
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
)
179 self
.SetClientSizeWH(400,400)
180 self
.SetBackgroundColour(wx
.wxNamedColour("white"))
182 wx
.EVT_SIZE(self
,self
.reconfigure
)
183 wx
.EVT_PAINT(self
, self
.OnPaint
)
185 self
.last_draw
= None
186 # self.font = self._testFont(font)
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
,))
193 def reconfigure(self
, event
):
194 (new_width
,new_height
) = self
.GetClientSizeTuple()
195 if new_width
== self
.width
and new_height
== self
.height
:
200 def _testFont(self
, font
):
202 bg
= self
.canvas
.cget('background')
204 item
= CanvasText(self
.canvas
, 0, 0, anchor
=NW
,
205 text
='0', fill
=bg
, font
=font
)
206 self
.canvas
.delete(item
)
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
])
218 def draw(self
, graphics
, xaxis
= None, yaxis
= None, dc
= None):
219 if dc
== None: dc
= wx
.wxClientDC(self
)
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:
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]
239 if yaxis
is not None:
242 yticks
= self
._ticks
(yaxis
[0], yaxis
[1])
244 bb
= dc
.GetTextExtent(y
[1])
245 text_width
[0] = max(text_width
[0],bb
[0])
248 text_height
[1] = max(text_height
[1], h
)
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
)
261 def _axisInterval(self
, spec
, lower
, upper
):
264 if spec
== 'minimal':
266 return lower
-0.5, upper
+0.5
269 if spec
== 'automatic':
272 return lower
-0.5, upper
+0.5
273 log
= Numeric
.log10(range)
274 power
= Numeric
.floor(log
)
279 lower
= lower
- lower
% grid
282 upper
= upper
- mod
+ grid
284 if type(spec
) == type(()):
290 raise ValueError, str(spec
) + ': illegal axis specification'
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:
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
)
306 dc
.DrawText(label
,p
[0],p
[1])
309 if yaxis
is not None:
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])
321 dc
.DrawText(label
,p
[0]-dc
.GetTextExtent(label
)[0],
325 def _ticks(self
, lower
, upper
):
326 ideal
= (upper
-lower
)/7.
327 log
= Numeric
.log10(ideal
)
328 power
= Numeric
.floor(log
)
332 for f
, lf
in self
._multiples
:
333 e
= Numeric
.fabs(fraction
-lf
)
337 grid
= factor
* 10.**power
338 if power
> 3 or power
< -3:
341 digits
= max(1, int(power
))
342 format
= '%' + `digits`
+'.0f'
345 format
= '%'+`digits
+2`
+'.'+`digits`
+'f'
347 t
= -grid
*Numeric
.floor(-lower
/grid
)
349 ticks
.append( (t
, format
% (t
,)) )
353 _multiples
= [(2., Numeric
.log10(2.)), (5., Numeric
.log10(5.))]
355 def redraw(self
,dc
=None):
356 if self
.last_draw
is not None:
357 apply(self
.draw
, self
.last_draw
+ (dc
,))
360 self
.canvas
.delete('all')
362 #---------------------------------------------------------------------------
363 # if running standalone...
365 # ...a sample implementation using the above
369 if __name__
== '__main__':
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)
377 # 50 points cos function, plotted as red line
378 data1
= 2.*Numeric
.pi
*Numeric
.arange(100)/100.
380 data1
[:,1] = Numeric
.cos(data1
[:,0])
381 lines
= PolyLine(data1
, color
='red')
383 # A few more points...
385 markers2
= PolyMarker([(0., 0.), (pi
/4., 1.), (pi
/2, 0.),
386 (3.*pi
/4., -1)], color
='blue',
387 fillcolor
='green', marker
='cross')
389 return PlotGraphics([markers1
, lines
, markers2
])
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))
397 # Now Create the menu bar and items
398 self
.mainmenu
= wx
.wxMenuBar()
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')
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')
417 menu
.Append(220, '&About', 'About this thing...')
418 wx
.EVT_MENU(self
, 220, self
.OnHelpAbout
)
419 self
.mainmenu
.Append(menu
, '&Help')
421 self
.SetMenuBar(self
.mainmenu
)
423 # A status bar to tell people what's happening
424 self
.CreateStatusBar(1)
426 self
.client
= PlotCanvas(self
)
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
)
436 def OnFileExit(self
, event
):
439 def OnPlotDraw(self
, event
):
440 self
.client
.draw(_InitObjects(),'automatic','automatic');
442 def OnPlotRedraw(self
,event
):
445 def OnPlotClear(self
,event
):
446 self
.client
.last_draw
= None
447 dc
= wx
.wxClientDC(self
.client
)
450 def OnHelpAbout(self
, event
):
451 about
= wx
.wxMessageDialog(self
, __doc__
, "About...", wx
.wxOK
)
456 class MyApp(wx
.wxApp
):
458 frame
= AppFrame(wx
.NULL
, -1, "wxPlotCanvas")
460 self
.SetTopWindow(frame
)
470 #----------------------------------------------------------------------------