1
2
3
4
5
6
7
8
9
10 """
11 A simple tool for plotting functions. Each new C{Plot} object opens a
12 new window, containing the plot for a sinlge function. See the
13 documentation for L{Plot} for information about creating new plots.
14
15 Example plots
16 =============
17 Plot sin(x) from -10 to 10, with a step of 0.1:
18 >>> Plot(math.sin)
19
20 Plot cos(x) from 0 to 2*pi, with a step of 0.01:
21 >>> Plot(math.cos, slice(0, 2*math.pi, 0.01))
22
23 Plot a list of points (connected by lines).
24 >>> points = ([1,1], [3,8], [5,3], [6,12], [1,24])
25 >>> Plot(points)
26
27 Plot a list of y-values (connected by lines). Each value[i] is
28 plotted at x=i.
29 >>> values = [x**2 for x in range(200)]
30 >>> Plot(values)
31
32 Plot a function with logarithmic axes.
33 >>> def f(x): return 5*x**2+2*x+8
34 >>> Plot(f, slice(1,10,.1), scale='log')
35
36 Plot the same function with semi-logarithmic axes.
37 >>> Plot(f, slice(1,10,.1),
38 scale='log-linear') # logarithmic x; linear y
39 >>> Plot(f, slice(1,10,.1),
40 scale='linear-log') # linear x; logarithmic y
41
42 BLT
43 ===
44 If U{BLT<http://incrtcl.sourceforge.net/blt/>} and
45 U{PMW<http://pmw.sourceforge.net/>} are both installed, then BLT is
46 used to plot graphs. Otherwise, a simple Tkinter-based implementation
47 is used. The Tkinter-based implementation does I{not} display axis
48 values.
49
50 @group Plot Frame Implementations: PlotFrameI, CanvasPlotFrame,
51 BLTPlotFrame
52 """
53
54
55
56 __all__ = ['Plot']
57
58
59
60
61
62
63
64
65
66 from types import *
67 from math import log, log10, ceil, floor
68 import Tkinter, sys, time
69 from nltk_lite.draw import ShowText, in_idle
70
72 """
73 A frame for plotting graphs. If BLT is present, then we use
74 BLTPlotFrame, since it's nicer. But we fall back on
75 CanvasPlotFrame if BLTPlotFrame is unavaibale.
76 """
77 - def postscript(self, filename):
78 'Print the contents of the plot to the given file'
79 raise AssertionError, 'PlotFrameI is an interface'
81 'Set the scale for the axes (linear/logarithmic)'
82 raise AssertionError, 'PlotFrameI is an interface'
86 - def zoom(self, i1, j1, i2, j2):
87 'Zoom to the given range'
88 raise AssertionError, 'PlotFrameI is an interface'
90 'Return the visible area rect (in plot coordinates)'
91 raise AssertionError, 'PlotFrameI is an interface'
93 'mark the zoom region, for drag-zooming'
94 raise AssertionError, 'PlotFrameI is an interface'
96 'adjust the zoom region marker, for drag-zooming'
97 raise AssertionError, 'PlotFrameI is an interface'
99 'delete the zoom region marker (for drag-zooming)'
100 raise AssertionError, 'PlotFrameI is an interface'
101 - def bind(self, *args):
102 'bind an event to a function'
103 raise AssertionError, 'PlotFrameI is an interface'
105 'unbind an event'
106 raise AssertionError, 'PlotFrameI is an interface'
107
110 self._root = root
111 self._original_rng = rng
112 self._original_vals = vals
113
114 self._frame = Tkinter.Frame(root)
115 self._frame.pack(expand=1, fill='both')
116
117
118 self._canvas = Tkinter.Canvas(self._frame, background='white')
119 self._canvas['scrollregion'] = (0,0,200,200)
120
121
122 sb1 = Tkinter.Scrollbar(self._frame, orient='vertical')
123 sb1.pack(side='right', fill='y')
124 sb2 = Tkinter.Scrollbar(self._frame, orient='horizontal')
125 sb2.pack(side='bottom', fill='x')
126 self._canvas.pack(side='left', fill='both', expand=1)
127
128
129 sb1.config(command=self._canvas.yview)
130 sb2['command']=self._canvas.xview
131 self._canvas['yscrollcommand'] = sb1.set
132 self._canvas['xscrollcommand'] = sb2.set
133
134 self._width = self._height = -1
135 self._canvas.bind('<Configure>', self._configure)
136
137
138 self.config_axes(0, 0)
139
146
147 - def postscript(self, filename):
148 (x0, y0, w, h) = self._canvas['scrollregion'].split()
149 self._canvas.postscript(file=filename, x=float(x0), y=float(y0),
150 width=float(w)+2, height=float(h)+2)
151
153 self._canvas.delete('all')
154 (i1, j1, i2, j2) = self.visible_area()
155
156
157 xzero = -self._imin*self._dx
158 yzero = self._ymax+self._jmin*self._dy
159 neginf = min(self._imin, self._jmin, -1000)*1000
160 posinf = max(self._imax, self._jmax, 1000)*1000
161 self._canvas.create_line(neginf,yzero,posinf,yzero,
162 fill='gray', width=2)
163 self._canvas.create_line(xzero,neginf,xzero,posinf,
164 fill='gray', width=2)
165
166
167 if self._xlog:
168 (i1, i2) = (10**(i1), 10**(i2))
169 (imin, imax) = (10**(self._imin), 10**(self._imax))
170
171 di = (i2-i1)/1000.0
172
173 di = 10.0**(int(log10(di)))
174
175 i = ceil(imin/di)*di
176 while i <= imax:
177 if i > 10*di: di *= 10
178 x = log10(i)*self._dx - log10(imin)*self._dx
179 self._canvas.create_line(x, neginf, x, posinf, fill='gray')
180 i += di
181 else:
182
183 di = max((i2-i1)/10.0, (self._imax-self._imin)/100)
184
185 di = 2.0**(int(log(di)/log(2)))
186
187 i = int(self._imin/di)*di
188
189 while i <= self._imax:
190 x = (i-self._imin)*self._dx
191 self._canvas.create_line(x, neginf, x, posinf, fill='gray')
192 i += di
193
194
195 if self._ylog:
196 (j1, j2) = (10**(j1), 10**(j2))
197 (jmin, jmax) = (10**(self._jmin), 10**(self._jmax))
198
199 dj = (j2-j1)/1000.0
200
201 dj = 10.0**(int(log10(dj)))
202
203 j = ceil(jmin/dj)*dj
204 while j <= jmax:
205 if j > 10*dj: dj *= 10
206 y = log10(jmax)*self._dy - log10(j)*self._dy
207 self._canvas.create_line(neginf, y, posinf, y, fill='gray')
208 j += dj
209 else:
210
211 dj = max((j2-j1)/10.0, (self._jmax-self._jmin)/100)
212
213 dj = 2.0**(int(log(dj)/log(2)))
214
215 j = int(self._jmin/dj)*dj
216
217 while j <= self._jmax:
218 y = (j-self._jmin)*self._dy
219 self._canvas.create_line(neginf, y, posinf, y, fill='gray')
220 j += dj
221
222
223 line = []
224 for (i,j) in zip(self._rng, self._vals):
225 x = (i-self._imin) * self._dx
226 y = self._ymax-((j-self._jmin) * self._dy)
227 line.append( (x,y) )
228 if len(line) == 1: line.append(line[0])
229 self._canvas.create_line(line, fill='black')
230
232 if hasattr(self, '_rng'):
233 (i1, j1, i2, j2) = self.visible_area()
234 zoomed=1
235 else:
236 zoomed=0
237
238 self._xlog = xlog
239 self._ylog = ylog
240 if xlog: self._rng = [log10(x) for x in self._original_rng]
241 else: self._rng = self._original_rng
242 if ylog: self._vals = [log10(x) for x in self._original_vals]
243 else: self._vals = self._original_vals
244
245 self._imin = min(self._rng)
246 self._imax = max(self._rng)
247 if self._imax == self._imin:
248 self._imin -= 1
249 self._imax += 1
250 self._jmin = min(self._vals)
251 self._jmax = max(self._vals)
252 if self._jmax == self._jmin:
253 self._jmin -= 1
254 self._jmax += 1
255
256 if zoomed:
257 self.zoom(i1, j1, i2, j2)
258 else:
259 self.zoom(self._imin, self._jmin, self._imax, self._jmax)
260
265
266 - def zoom(self, i1, j1, i2, j2):
267 w = self._width
268 h = self._height
269 self._xmax = (self._imax-self._imin)/(i2-i1) * w
270 self._ymax = (self._jmax-self._jmin)/(j2-j1) * h
271 self._canvas['scrollregion'] = (0, 0, self._xmax, self._ymax)
272 self._dx = self._xmax/(self._imax-self._imin)
273 self._dy = self._ymax/(self._jmax-self._jmin)
274 self._plot()
275
276
277 self._canvas.xview('moveto', (i1-self._imin)/(self._imax-self._imin))
278 self._canvas.yview('moveto', (self._jmax-j2)/(self._jmax-self._jmin))
279
281 xview = self._canvas.xview()
282 yview = self._canvas.yview()
283 i1 = self._imin + xview[0] * (self._imax-self._imin)
284 i2 = self._imin + xview[1] * (self._imax-self._imin)
285 j1 = self._jmax - yview[1] * (self._jmax-self._jmin)
286 j2 = self._jmax - yview[0] * (self._jmax-self._jmin)
287 return (i1, j1, i2, j2)
288
290 self._canvas.create_rectangle(0,0,0,0, tag='zoom')
291
293 x0 = self._canvas.canvasx(x0)
294 y0 = self._canvas.canvasy(y0)
295 x1 = self._canvas.canvasx(x1)
296 y1 = self._canvas.canvasy(y1)
297 self._canvas.coords('zoom', x0, y0, x1, y1)
298
300 self._canvas.delete('zoom')
301
304
307
308
309
310 self._imin = min(rng)
311 self._imax = max(rng)
312 if self._imax == self._imin:
313 self._imin -= 1
314 self._imax += 1
315 self._jmin = min(vals)
316 self._jmax = max(vals)
317 if self._jmax == self._jmin:
318 self._jmin -= 1
319 self._jmax += 1
320
321
322 self._root = root
323 self._frame = Tkinter.Frame(root)
324 self._frame.pack(expand=1, fill='both')
325
326
327 try:
328 import Pmw
329
330
331 reload(Pmw.Blt)
332
333 Pmw.initialise()
334 self._graph = Pmw.Blt.Graph(self._frame)
335 except:
336 raise ImportError('Pmw not installed!')
337
338
339 sb1 = Tkinter.Scrollbar(self._frame, orient='vertical')
340 sb1.pack(side='right', fill='y')
341 sb2 = Tkinter.Scrollbar(self._frame, orient='horizontal')
342 sb2.pack(side='bottom', fill='x')
343 self._graph.pack(side='left', fill='both', expand='yes')
344 self._yscroll = sb1
345 self._xscroll = sb2
346
347
348 sb1['command'] = self._yview
349 sb2['command'] = self._xview
350
351
352 self._graph.line_create('plot', xdata=tuple(rng),
353 ydata=tuple(vals), symbol='')
354 self._graph.legend_configure(hide=1)
355 self._graph.grid_configure(hide=0)
356 self._set_scrollbars()
357
365
367 (i1, j1, i2, j2) = self.visible_area()
368 (imin, imax) = (self._imin, self._imax)
369 (jmin, jmax) = (self._jmin, self._jmax)
370
371 if command[0] == 'moveto':
372 f = float(command[1])
373 elif command[0] == 'scroll':
374 dir = int(command[1])
375 if command[2] == 'pages':
376 f = (i1-imin)/(imax-imin) + dir*(i2-i1)/(imax-imin)
377 elif command[2] == 'units':
378 f = (i1-imin)/(imax-imin) + dir*(i2-i1)/(10*(imax-imin))
379 else: return
380 else: return
381
382 f = max(f, 0)
383 f = min(f, 1-(i2-i1)/(imax-imin))
384 self.zoom(imin + f*(imax-imin), j1,
385 imin + f*(imax-imin)+(i2-i1), j2)
386 self._set_scrollbars()
387
389 (i1, j1, i2, j2) = self.visible_area()
390 (imin, imax) = (self._imin, self._imax)
391 (jmin, jmax) = (self._jmin, self._jmax)
392
393 if command[0] == 'moveto':
394 f = 1.0-float(command[1]) - (j2-j1)/(jmax-jmin)
395 elif command[0] == 'scroll':
396 dir = -int(command[1])
397 if command[2] == 'pages':
398 f = (j1-jmin)/(jmax-jmin) + dir*(j2-j1)/(jmax-jmin)
399 elif command[2] == 'units':
400 f = (j1-jmin)/(jmax-jmin) + dir*(j2-j1)/(10*(jmax-jmin))
401 else: return
402 else: return
403
404 f = max(f, 0)
405 f = min(f, 1-(j2-j1)/(jmax-jmin))
406 self.zoom(i1, jmin + f*(jmax-jmin),
407 i2, jmin + f*(jmax-jmin)+(j2-j1))
408 self._set_scrollbars()
409
411 self._graph.xaxis_configure(logscale=xlog)
412 self._graph.yaxis_configure(logscale=ylog)
413
416
417 - def zoom(self, i1, j1, i2, j2):
418 self._graph.xaxis_configure(min=i1, max=i2)
419 self._graph.yaxis_configure(min=j1, max=j2)
420 self._set_scrollbars()
421
423 (i1, i2) = self._graph.xaxis_limits()
424 (j1, j2) = self._graph.yaxis_limits()
425 return (i1, j1, i2, j2)
426
428 self._graph.marker_create("line", name="zoom", dashes=(2, 2))
429
431 (i1, j1) = self._graph.invtransform(press_x, press_y)
432 (i2, j2) = self._graph.invtransform(release_x, release_y)
433 coords = (i1, j1, i2, j1, i2, j2, i1, j2, i1, j1)
434 self._graph.marker_configure("zoom", coords=coords)
435
437 self._graph.marker_delete("zoom")
438
441
442 - def postscript(self, filename):
443 self._graph.postscript_output(filename)
444
445
447 """
448 A simple graphical tool for plotting functions. Each new C{Plot}
449 object opens a new window, containing the plot for a sinlge
450 function. Multiple plots in the same window are not (yet)
451 supported. The C{Plot} constructor supports several mechanisms
452 for defining the set of points to plot.
453
454 Example plots
455 =============
456 Plot the math.sin function over the range [-10:10:.1]:
457 >>> import math
458 >>> Plot(math.sin)
459
460 Plot the math.sin function over the range [0:1:.001]:
461 >>> Plot(math.sin, slice(0, 1, .001))
462
463 Plot a list of points:
464 >>> points = ([1,1], [3,8], [5,3], [6,12], [1,24])
465 >>> Plot(points)
466
467 Plot a list of values, at x=0, x=1, x=2, ..., x=n:
468 >>> Plot(x**2 for x in range(20))
469 """
470 - def __init__(self, vals, rng=None, **kwargs):
471 """
472 Create a new C{Plot}.
473
474 @param vals: The set of values to plot. C{vals} can be a list
475 of y-values; a list of points; or a function.
476 @param rng: The range over which to plot. C{rng} can be a
477 list of x-values, or a slice object. If no range is
478 specified, a default range will be used. Note that C{rng}
479 may I{not} be specified if C{vals} is a list of points.
480 @keyword scale: The scales that should be used for the axes.
481 Possible values are:
482 - C{'linear'}: both axes are linear.
483 - C{'log-linear'}: The x axis is logarithmic; and the y
484 axis is linear.
485 - C{'linear-log'}: The x axis is linear; and the y axis
486 is logarithmic.
487 - C{'log'}: Both axes are logarithmic.
488 By default, C{scale} is C{'linear'}.
489 """
490
491 if type(rng) is SliceType:
492 (start, stop, step) = (rng.start, rng.stop, rng.step)
493 if step>0 and stop>start:
494 rng = [start]
495 i = 0
496 while rng[-1] < stop:
497 rng.append(start+i*step)
498 i += 1
499 elif step<0 and stop<start:
500 rng = [start]
501 i = 0
502 while rng[-1] > stop:
503 rng.append(start+i*step)
504 i += 1
505 else:
506 rng = []
507
508
509 if type(vals) in (FunctionType, BuiltinFunctionType,
510 MethodType):
511 if rng is None: rng = [x*0.1 for x in range(-100, 100)]
512 try: vals = [vals(i) for i in rng]
513 except TypeError:
514 raise TypeError, 'Bad range type: %s' % type(rng)
515
516
517 elif type(vals) not in (ListType, TupleType):
518 raise ValueError, 'Bad values type: %s' % type(vals)
519
520
521 elif len(vals) > 0 and type(vals[0]) in (ListType, TupleType):
522 if rng is not None:
523 estr = "Can't specify a range when vals is a list of points."
524 raise ValueError, estr
525 (rng, vals) = zip(*vals)
526
527
528 elif type(rng) in (ListType, TupleType):
529 if len(rng) != len(vals):
530 estr = 'Range list and value list have different lengths.'
531 raise ValueError, estr
532
533
534 elif rng is None:
535 rng = range(len(vals))
536
537
538 else:
539 raise TypeError, 'Bad range type: %s' % type(rng)
540
541
542 if len(vals) == 0:
543 raise ValueError, 'Nothing to plot!'
544
545
546 self._rng = rng
547 self._vals = vals
548
549
550 self._imin = min(rng)
551 self._imax = max(rng)
552 if self._imax == self._imin:
553 self._imin -= 1
554 self._imax += 1
555 self._jmin = min(vals)
556 self._jmax = max(vals)
557 if self._jmax == self._jmin:
558 self._jmin -= 1
559 self._jmax += 1
560
561
562 if len(self._rng) != len(self._vals):
563 raise ValueError("Rng and vals have different lengths")
564 if len(self._rng) == 0:
565 raise ValueError("Nothing to plot")
566
567
568 self._root = Tkinter.Tk()
569 self._init_bindings(self._root)
570
571
572 try:
573 self._plot = BLTPlotFrame(self._root, vals, rng)
574 except ImportError:
575 self._plot = CanvasPlotFrame(self._root, vals, rng)
576
577
578 self._ilog = Tkinter.IntVar(self._root); self._ilog.set(0)
579 self._jlog = Tkinter.IntVar(self._root); self._jlog.set(0)
580 scale = kwargs.get('scale', 'linear')
581 if scale in ('log-linear', 'log_linear', 'log'): self._ilog.set(1)
582 if scale in ('linear-log', 'linear_log', 'log'): self._jlog.set(1)
583 self._plot.config_axes(self._ilog.get(), self._jlog.get())
584
585
586 self._plot.bind("<ButtonPress-1>", self._zoom_in_buttonpress)
587 self._plot.bind("<ButtonRelease-1>", self._zoom_in_buttonrelease)
588 self._plot.bind("<ButtonPress-2>", self._zoom_out)
589 self._plot.bind("<ButtonPress-3>", self._zoom_out)
590
591 self._init_menubar(self._root)
592
599
601 menubar = Tkinter.Menu(parent)
602
603 filemenu = Tkinter.Menu(menubar, tearoff=0)
604 filemenu.add_command(label='Print to Postscript', underline=0,
605 command=self.postscript, accelerator='Ctrl-p')
606 filemenu.add_command(label='Exit', underline=1,
607 command=self.destroy, accelerator='Ctrl-x')
608 menubar.add_cascade(label='File', underline=0, menu=filemenu)
609
610 zoommenu = Tkinter.Menu(menubar, tearoff=0)
611 zoommenu.add_command(label='Zoom in', underline=5,
612 command=self._zoom_in, accelerator='left click')
613 zoommenu.add_command(label='Zoom out', underline=5,
614 command=self._zoom_out, accelerator='right click')
615 zoommenu.add_command(label='View 100%', command=self._zoom_all,
616 accelerator='Ctrl-a')
617 menubar.add_cascade(label='Zoom', underline=0, menu=zoommenu)
618
619 axismenu = Tkinter.Menu(menubar, tearoff=0)
620 if self._imin > 0: xstate = 'normal'
621 else: xstate = 'disabled'
622 if self._jmin > 0: ystate = 'normal'
623 else: ystate = 'disabled'
624 axismenu.add_checkbutton(label='Log X axis', underline=4,
625 variable=self._ilog, state=xstate,
626 command=self._log)
627 axismenu.add_checkbutton(label='Log Y axis', underline=4,
628 variable=self._jlog, state=ystate,
629 command=self._log)
630 menubar.add_cascade(label='Axes', underline=0, menu=axismenu)
631
632 helpmenu = Tkinter.Menu(menubar, tearoff=0)
633 helpmenu.add_command(label='About', underline=0,
634 command=self.about)
635 helpmenu.add_command(label='Instructions', underline=0,
636 command=self.help, accelerator='F1')
637 menubar.add_cascade(label='Help', underline=0, menu=helpmenu)
638
639 parent.config(menu=menubar)
640
641 - def _log(self, *e):
643
645 """
646 Dispaly an 'about' dialog window for the NLTK plot tool.
647 """
648 ABOUT = ("NLTK Plot Tool\n"
649 "<http://nltk.sourceforge.net>")
650 TITLE = 'About: Plot Tool'
651 if isinstance(self._plot, BLTPlotFrame):
652 ABOUT += '\n\nBased on the BLT Widget'
653 try:
654 from tkMessageBox import Message
655 Message(message=ABOUT, title=TITLE).show()
656 except:
657 ShowText(self._root, TITLE, ABOUT)
658
659 - def help(self, *e):
660 """
661 Display a help window.
662 """
663 doc = __doc__.split('\n@', 1)[0].strip()
664 import re
665 doc = re.sub(r'[A-Z]{([^}<]*)(<[^>}]*>)?}', r'\1', doc)
666 self._autostep = 0
667
668 try:
669 ShowText(self._root, 'Help: Plot Tool', doc,
670 width=75, font='fixed')
671 except:
672 ShowText(self._root, 'Help: Plot Tool', doc, width=75)
673
674
675 - def postscript(self, *e):
676 """
677 Print the (currently visible) contents of the plot window to a
678 postscript file.
679 """
680 from tkFileDialog import asksaveasfilename
681 ftypes = [('Postscript files', '.ps'),
682 ('All files', '*')]
683 filename = asksaveasfilename(filetypes=ftypes, defaultextension='.ps')
684 if not filename: return
685 self._plot.postscript(filename)
686
688 """
689 Cloase the plot window.
690 """
691 if self._root is None: return
692 self._root.destroy()
693 self._root = None
694
695 - def mainloop(self, *varargs, **kwargs):
696 """
697 Enter the mainloop for the window. This method must be called
698 if a Plot is constructed from a non-interactive Python program
699 (e.g., from a script); otherwise, the plot window will close
700 as soon se the script completes.
701 """
702 if in_idle(): return
703 self._root.mainloop(*varargs, **kwargs)
704
705 - def _zoom(self, i1, j1, i2, j2):
706
707 if i1 > i2: (i1,i2) = (i2,i1)
708 if j1 > j2: (j1,j2) = (j2,j1)
709
710
711 if i1 < self._imin:
712 i2 = min(self._imax, i2 + (self._imin - i1))
713 i1 = self._imin
714 if i2 > self._imax:
715 i1 = max(self._imin, i1 - (i2 - self._imax))
716 i2 = self._imax
717
718
719 if j1 < self._jmin:
720 j2 = min(self._jmax, j2 + self._jmin - j1)
721 j1 = self._jmin
722 if j2 > self._jmax:
723 j1 = max(self._jmin, j1 - (j2 - self._jmax))
724 j2 = self._jmax
725
726
727 if i1 == i2: i2 += 1
728 if j1 == j2: j2 += 1
729
730 if self._ilog.get(): i1 = max(1e-100, i1)
731 if self._jlog.get(): j1 = max(1e-100, j1)
732
733
734 self._plot.zoom(i1, j1, i2, j2)
735
742
746
757
759 (i1, j1, i2, j2) = self._plot.visible_area()
760 di = (i2-i1)*0.1
761 dj = (j2-j1)*0.1
762 self._zoom(i1+di, j1+dj, i2-di, j2-dj)
763
765 (i1, j1, i2, j2) = self._plot.visible_area()
766 di = -(i2-i1)*0.1
767 dj = -(j2-j1)*0.1
768 self._zoom(i1+di, j1+dj, i2-di, j2-dj)
769
771 self._zoom(self._imin, self._jmin, self._imax, self._jmax)
772
773
774 if __name__ == '__main__':
775 from math import sin
776
777 Plot(lambda x:abs(x**2-sin(20*x**3))+.1,
778 [0.01*x for x in range(1,100)], scale='log').mainloop()
779