]>
git.saurik.com Git - wxWidgets.git/blob - wxPython/wxPython/lib/wxPlotCanvas.py
926333c92d05eb1efb869365e5f38d1b686b3c74
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
25 # Not everybody will have Numeric, so let's be cool about it...
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."""
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
)
49 def __init__(self
, points
, attr
):
50 self
.points
= Numeric
.array(points
)
51 self
.scaled
= self
.points
53 for name
, value
in self
._attributes
.items():
57 self
.attributes
[name
] = value
59 def boundingBox(self
):
60 return Numeric
.minimum
.reduce(self
.points
), \
61 Numeric
.maximum
.reduce(self
.points
)
63 def scaleAndShift(self
, scale
=1, shift
=0):
64 self
.scaled
= scale
*self
.points
+shift
67 class PolyLine(PolyPoints
):
69 def __init__(self
, points
, **attr
):
70 PolyPoints
.__init
__(self
, points
, attr
)
72 _attributes
= {'color': 'black',
76 color
= self
.attributes
['color']
77 width
= self
.attributes
['width']
79 dc
.SetPen(wx
.wxPen(wx
.wxNamedColour(color
), width
))
80 dc
.DrawLines(map(tuple,self
.scaled
))
83 class PolyMarker(PolyPoints
):
85 def __init__(self
, points
, **attr
):
87 PolyPoints
.__init
__(self
, points
, attr
)
89 _attributes
= {'color': 'black',
93 'fillstyle': wx
.wxSOLID
,
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']
105 dc
.SetPen(wx
.wxPen(wx
.wxNamedColour(color
),width
))
107 dc
.SetBrush(wx
.wxBrush(wx
.wxNamedColour(fillcolor
),fillstyle
))
109 dc
.SetBrush(wx
.wxBrush(wx
.wxNamedColour('black'), wx
.wxTRANSPARENT
))
111 self
._drawmarkers
(dc
, self
.scaled
, marker
, size
)
113 def _drawmarkers(self
, dc
, coords
, marker
,size
=1):
114 f
= eval('self._' +marker
)
115 for xc
, yc
in coords
:
118 def _circle(self
, dc
, xc
, yc
, size
=1):
119 dc
.DrawEllipse(xc
-2.5*size
,yc
-2.5*size
,5.*size
,5.*size
)
121 def _dot(self
, dc
, xc
, yc
, size
=1):
124 def _square(self
, dc
, xc
, yc
, size
=1):
125 dc
.DrawRectangle(xc
-2.5*size
,yc
-2.5*size
,5.*size
,5.*size
)
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
)
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
)
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
)
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
)
147 def __init__(self
, objects
):
148 self
.objects
= objects
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
)
158 def scaleAndShift(self
, scale
=1, shift
=0):
159 for o
in self
.objects
:
160 o
.scaleAndShift(scale
, shift
)
162 def draw(self
, canvas
):
163 for o
in self
.objects
:
167 return len(self
.objects
)
169 def __getitem__(self
, item
):
170 return self
.objects
[item
]
173 class PlotCanvas(wx
.wxWindow
):
175 def __init__(self
, parent
, id = -1):
176 wx
.wxWindow
.__init
__(self
, parent
, id, wx
.wxPyDefaultPosition
, wx
.wxPyDefaultSize
)
178 self
.SetClientSizeWH(400,400)
179 self
.SetBackgroundColour(wx
.wxNamedColour("white"))
181 wx
.EVT_SIZE(self
,self
.reconfigure
)
182 wx
.EVT_PAINT(self
, self
.OnPaint
)
184 self
.last_draw
= None
185 # self.font = self._testFont(font)
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
,))
192 def reconfigure(self
, event
):
193 (new_width
,new_height
) = self
.GetClientSizeTuple()
194 if new_width
== self
.width
and new_height
== self
.height
:
199 def _testFont(self
, font
):
201 bg
= self
.canvas
.cget('background')
203 item
= CanvasText(self
.canvas
, 0, 0, anchor
=NW
,
204 text
='0', fill
=bg
, font
=font
)
205 self
.canvas
.delete(item
)
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
])
217 def draw(self
, graphics
, xaxis
= None, yaxis
= None, dc
= None):
218 if dc
== None: dc
= wx
.wxClientDC(self
)
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:
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]
238 if yaxis
is not None:
241 yticks
= self
._ticks
(yaxis
[0], yaxis
[1])
243 bb
= dc
.GetTextExtent(y
[1])
244 text_width
[0] = max(text_width
[0],bb
[0])
247 text_height
[1] = max(text_height
[1], h
)
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
)
260 def _axisInterval(self
, spec
, lower
, upper
):
263 if spec
== 'minimal':
265 return lower
-0.5, upper
+0.5
268 if spec
== 'automatic':
271 return lower
-0.5, upper
+0.5
272 log
= Numeric
.log10(range)
273 power
= Numeric
.floor(log
)
278 lower
= lower
- lower
% grid
281 upper
= upper
- mod
+ grid
283 if type(spec
) == type(()):
289 raise ValueError, str(spec
) + ': illegal axis specification'
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:
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
)
305 dc
.DrawText(label
,p
[0],p
[1])
308 if yaxis
is not None:
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])
320 dc
.DrawText(label
,p
[0]-dc
.GetTextExtent(label
)[0],
324 def _ticks(self
, lower
, upper
):
325 ideal
= (upper
-lower
)/7.
326 log
= Numeric
.log10(ideal
)
327 power
= Numeric
.floor(log
)
331 for f
, lf
in self
._multiples
:
332 e
= Numeric
.fabs(fraction
-lf
)
336 grid
= factor
* 10.**power
337 if power
> 3 or power
< -3:
340 digits
= max(1, int(power
))
341 format
= '%' + `digits`
+'.0f'
344 format
= '%'+`digits
+2`
+'.'+`digits`
+'f'
346 t
= -grid
*Numeric
.floor(-lower
/grid
)
348 ticks
.append( (t
, format
% (t
,)) )
352 _multiples
= [(2., Numeric
.log10(2.)), (5., Numeric
.log10(5.))]
354 def redraw(self
,dc
=None):
355 if self
.last_draw
is not None:
356 apply(self
.draw
, self
.last_draw
+ (dc
,))
359 self
.canvas
.delete('all')
361 #---------------------------------------------------------------------------
362 # if running standalone...
364 # ...a sample implementation using the above
368 if __name__
== '__main__':
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)
376 # 50 points cos function, plotted as red line
377 data1
= 2.*Numeric
.pi
*Numeric
.arange(100)/100.
379 data1
[:,1] = Numeric
.cos(data1
[:,0])
380 lines
= PolyLine(data1
, color
='red')
382 # A few more points...
384 markers2
= PolyMarker([(0., 0.), (pi
/4., 1.), (pi
/2, 0.),
385 (3.*pi
/4., -1)], color
='blue',
386 fillcolor
='green', marker
='cross')
388 return PlotGraphics([markers1
, lines
, markers2
])
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))
396 # Now Create the menu bar and items
397 self
.mainmenu
= wx
.wxMenuBar()
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')
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')
416 menu
.Append(220, '&About', 'About this thing...')
417 wx
.EVT_MENU(self
, 220, self
.OnHelpAbout
)
418 self
.mainmenu
.Append(menu
, '&Help')
420 self
.SetMenuBar(self
.mainmenu
)
422 # A status bar to tell people what's happening
423 self
.CreateStatusBar(1)
425 self
.client
= PlotCanvas(self
)
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
)
435 def OnFileExit(self
, event
):
438 def OnPlotDraw(self
, event
):
439 self
.client
.draw(_InitObjects(),'automatic','automatic');
441 def OnPlotRedraw(self
,event
):
444 def OnPlotClear(self
,event
):
445 self
.client
.last_draw
= None
446 dc
= wx
.wxClientDC(self
.client
)
449 def OnHelpAbout(self
, event
):
450 about
= wx
.wxMessageDialog(self
, __doc__
, "About...", wx
.wxOK
)
455 class MyApp(wx
.wxApp
):
457 frame
= AppFrame(wx
.NULL
, -1, "wxPlotCanvas")
459 self
.SetTopWindow(frame
)
469 #----------------------------------------------------------------------------