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