| 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 | #---------------------------------------------------------------------------- |