source: trunk/python/asaplotbase.py@ 1329

Last change on this file since 1329 was 1259, checked in by mar637, 18 years ago

Merge from Release2.1.0b tag

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 26.6 KB
Line 
1"""
2ASAP plotting class based on matplotlib.
3"""
4
5import sys
6from re import match
7
8import matplotlib
9
10from matplotlib.figure import Figure, Text
11from matplotlib.font_manager import FontProperties as FP
12from matplotlib.numerix import sqrt
13from matplotlib import rc, rcParams
14from asap import rcParams as asaprcParams
15from matplotlib.ticker import OldScalarFormatter
16from matplotlib.ticker import NullLocator
17from matplotlib.transforms import blend_xy_sep_transform
18
19if int(matplotlib.__version__.split(".")[1]) < 87:
20 print "Warning: matplotlib version < 0.87. This might cause errors. Please upgrade."
21
22#class MyFormatter(OldScalarFormatter):
23# def __call__(self, x, pos=None):
24# last = len(self.locs)-2
25# if pos==0:
26# return ''
27# else: return OldScalarFormatter.__call__(self, x, pos)
28
29class asaplotbase:
30 """
31 ASAP plotting base class based on matplotlib.
32 """
33
34 def __init__(self, rows=1, cols=0, title='', size=(8,6), buffering=False):
35 """
36 Create a new instance of the ASAPlot plotting class.
37
38 If rows < 1 then a separate call to set_panels() is required to define
39 the panel layout; refer to the doctext for set_panels().
40 """
41 self.is_dead = False
42 self.figure = Figure(figsize=size, facecolor='#ddddee')
43 self.canvas = None
44
45 self.set_title(title)
46 self.subplots = []
47 if rows > 0:
48 self.set_panels(rows, cols)
49
50 # Set matplotlib default colour sequence.
51 self.colormap = "green red black cyan magenta orange blue purple yellow pink".split()
52
53 c = asaprcParams['plotter.colours']
54 if isinstance(c,str) and len(c) > 0:
55 self.colormap = c.split()
56
57 self.lsalias = {"line": [1,0],
58 "dashdot": [4,2,1,2],
59 "dashed" : [4,2,4,2],
60 "dotted" : [1,2],
61 "dashdotdot": [4,2,1,2,1,2],
62 "dashdashdot": [4,2,4,2,1,2]
63 }
64
65 styles = "line dashed dotted dashdot".split()
66 c = asaprcParams['plotter.linestyles']
67 if isinstance(c,str) and len(c) > 0:
68 styles = c.split()
69 s = []
70 for ls in styles:
71 if self.lsalias.has_key(ls):
72 s.append(self.lsalias.get(ls))
73 else:
74 s.append('-')
75 self.linestyles = s
76
77 self.color = 0;
78 self.linestyle = 0;
79 self.attributes = {}
80 self.loc = 0
81
82 self.buffering = buffering
83
84 def clear(self):
85 """
86 Delete all lines from the plot. Line numbering will restart from 0.
87 """
88
89 for i in range(len(self.lines)):
90 self.delete(i)
91 self.axes.clear()
92 self.color = 0
93 self.lines = []
94
95 def palette(self, color, colormap=None, linestyle=0, linestyles=None):
96 if colormap:
97 if isinstance(colormap,list):
98 self.colormap = colormap
99 elif isinstance(colormap,str):
100 self.colormap = colormap.split()
101 if 0 <= color < len(self.colormap):
102 self.color = color
103 if linestyles:
104 self.linestyles = []
105 if isinstance(linestyles,list):
106 styles = linestyles
107 elif isinstance(linestyles,str):
108 styles = linestyles.split()
109 for ls in styles:
110 if self.lsalias.has_key(ls):
111 self.linestyles.append(self.lsalias.get(ls))
112 else:
113 self.linestyles.append(self.lsalias.get('line'))
114 if 0 <= linestyle < len(self.linestyles):
115 self.linestyle = linestyle
116
117 def delete(self, numbers=None):
118 """
119 Delete the 0-relative line number, default is to delete the last.
120 The remaining lines are NOT renumbered.
121 """
122
123 if numbers is None: numbers = [len(self.lines)-1]
124
125 if not hasattr(numbers, '__iter__'):
126 numbers = [numbers]
127
128 for number in numbers:
129 if 0 <= number < len(self.lines):
130 if self.lines[number] is not None:
131 for line in self.lines[number]:
132 line.set_linestyle('None')
133 self.lines[number] = None
134 self.show()
135
136 def get_line(self):
137 """
138 Get the current default line attributes.
139 """
140 return self.attributes
141
142
143 def hist(self, x=None, y=None, fmt=None, add=None):
144 """
145 Plot a histogram. N.B. the x values refer to the start of the
146 histogram bin.
147
148 fmt is the line style as in plot().
149 """
150 from matplotlib.numerix import array
151 from matplotlib.numerix.ma import MaskedArray
152 if x is None:
153 if y is None: return
154 x = range(len(y))
155
156 if len(x) != len(y):
157 return
158 l2 = 2*len(x)
159 x2 = range(l2)
160 y2 = range(12)
161 y2 = range(l2)
162 m2 = range(l2)
163 ymsk = y.raw_mask()
164 ydat = y.raw_data()
165 for i in range(l2):
166 x2[i] = x[i/2]
167 m2[i] = ymsk[i/2]
168
169 y2[0] = 0.0
170 for i in range(1,l2):
171 y2[i] = ydat[(i-1)/2]
172
173 self.plot(x2, MaskedArray(y2,mask=m2,copy=0), fmt, add)
174
175
176 def hold(self, hold=True):
177 """
178 Buffer graphics until subsequently released.
179 """
180 self.buffering = hold
181
182
183 def legend(self, loc=None):
184 """
185 Add a legend to the plot.
186
187 Any other value for loc else disables the legend:
188 1: upper right
189 2: upper left
190 3: lower left
191 4: lower right
192 5: right
193 6: center left
194 7: center right
195 8: lower center
196 9: upper center
197 10: center
198
199 """
200 if isinstance(loc, int):
201 self.loc = None
202 if 0 <= loc <= 10: self.loc = loc
203 else:
204 self.loc = None
205 #self.show()
206
207
208 def plot(self, x=None, y=None, fmt=None, add=None):
209 """
210 Plot the next line in the current frame using the current line
211 attributes. The ASAPlot graphics window will be mapped and raised.
212
213 The argument list works a bit like the matlab plot() function.
214 """
215 if x is None:
216 if y is None: return
217 x = range(len(y))
218
219 elif y is None:
220 y = x
221 x = range(len(y))
222 if fmt is None:
223 line = self.axes.plot(x, y)
224 else:
225 line = self.axes.plot(x, y, fmt)
226
227 # Add to an existing line?
228 i = None
229 if add is None or len(self.lines) < add < 0:
230 # Don't add.
231 self.lines.append(line)
232 i = len(self.lines) - 1
233 else:
234 if add == 0: add = len(self.lines)
235 i = add - 1
236 self.lines[i].extend(line)
237
238 # Set/reset attributes for the line.
239 gotcolour = False
240 for k, v in self.attributes.iteritems():
241 if k == 'color': gotcolour = True
242 for segment in self.lines[i]:
243 getattr(segment, "set_%s"%k)(v)
244
245 if not gotcolour and len(self.colormap):
246 for segment in self.lines[i]:
247 getattr(segment, "set_color")(self.colormap[self.color])
248 if len(self.colormap) == 1:
249 getattr(segment, "set_dashes")(self.linestyles[self.linestyle])
250
251 self.color += 1
252 if self.color >= len(self.colormap):
253 self.color = 0
254
255 if len(self.colormap) == 1:
256 self.linestyle += 1
257 if self.linestyle >= len(self.linestyles):
258 self.linestyle = 0
259
260 self.show()
261
262
263 def position(self):
264 """
265 Use the mouse to get a position from a graph.
266 """
267
268 def position_disable(event):
269 self.register('button_press', None)
270 print '%.4f, %.4f' % (event.xdata, event.ydata)
271
272 print 'Press any mouse button...'
273 self.register('button_press', position_disable)
274
275
276 def region(self):
277 """
278 Use the mouse to get a rectangular region from a plot.
279
280 The return value is [x0, y0, x1, y1] in world coordinates.
281 """
282
283 def region_start(event):
284 height = self.canvas.figure.bbox.height()
285 self.rect = {'fig': None, 'height': height,
286 'x': event.x, 'y': height - event.y,
287 'world': [event.xdata, event.ydata,
288 event.xdata, event.ydata]}
289 self.register('button_press', None)
290 self.register('motion_notify', region_draw)
291 self.register('button_release', region_disable)
292
293 def region_draw(event):
294 self.canvas._tkcanvas.delete(self.rect['fig'])
295 self.rect['fig'] = self.canvas._tkcanvas.create_rectangle(
296 self.rect['x'], self.rect['y'],
297 event.x, self.rect['height'] - event.y)
298
299 def region_disable(event):
300 self.register('motion_notify', None)
301 self.register('button_release', None)
302
303 self.canvas._tkcanvas.delete(self.rect['fig'])
304
305 self.rect['world'][2:4] = [event.xdata, event.ydata]
306 print '(%.2f, %.2f) (%.2f, %.2f)' % (self.rect['world'][0],
307 self.rect['world'][1], self.rect['world'][2],
308 self.rect['world'][3])
309
310 self.register('button_press', region_start)
311
312 # This has to be modified to block and return the result (currently
313 # printed by region_disable) when that becomes possible in matplotlib.
314
315 return [0.0, 0.0, 0.0, 0.0]
316
317
318 def register(self, type=None, func=None):
319 """
320 Register, reregister, or deregister events of type 'button_press',
321 'button_release', or 'motion_notify'.
322
323 The specified callback function should have the following signature:
324
325 def func(event)
326
327 where event is an MplEvent instance containing the following data:
328
329 name # Event name.
330 canvas # FigureCanvas instance generating the event.
331 x = None # x position - pixels from left of canvas.
332 y = None # y position - pixels from bottom of canvas.
333 button = None # Button pressed: None, 1, 2, 3.
334 key = None # Key pressed: None, chr(range(255)), shift,
335 win, or control
336 inaxes = None # Axes instance if cursor within axes.
337 xdata = None # x world coordinate.
338 ydata = None # y world coordinate.
339
340 For example:
341
342 def mouse_move(event):
343 print event.xdata, event.ydata
344
345 a = asaplot()
346 a.register('motion_notify', mouse_move)
347
348 If func is None, the event is deregistered.
349
350 Note that in TkAgg keyboard button presses don't generate an event.
351 """
352
353 if not self.events.has_key(type): return
354
355 if func is None:
356 if self.events[type] is not None:
357 # It's not clear that this does anything.
358 self.canvas.mpl_disconnect(self.events[type])
359 self.events[type] = None
360
361 # It seems to be necessary to return events to the toolbar.
362 if type == 'motion_notify':
363 self.canvas.mpl_connect(type + '_event',
364 self.figmgr.toolbar.mouse_move)
365 elif type == 'button_press':
366 self.canvas.mpl_connect(type + '_event',
367 self.figmgr.toolbar.press)
368 elif type == 'button_release':
369 self.canvas.mpl_connect(type + '_event',
370 self.figmgr.toolbar.release)
371
372 else:
373 self.events[type] = self.canvas.mpl_connect(type + '_event', func)
374
375
376 def release(self):
377 """
378 Release buffered graphics.
379 """
380 self.buffering = False
381 self.show()
382
383
384 def save(self, fname=None, orientation=None, dpi=None, papertype=None):
385 """
386 Save the plot to a file.
387
388 fname is the name of the output file. The image format is determined
389 from the file suffix; 'png', 'ps', and 'eps' are recognized. If no
390 file name is specified 'yyyymmdd_hhmmss.png' is created in the current
391 directory.
392 """
393 from asap import rcParams
394 if papertype is None:
395 papertype = rcParams['plotter.papertype']
396 if fname is None:
397 from datetime import datetime
398 dstr = datetime.now().strftime('%Y%m%d_%H%M%S')
399 fname = 'asap'+dstr+'.png'
400
401 d = ['png','.ps','eps']
402
403 from os.path import expandvars
404 fname = expandvars(fname)
405
406 if fname[-3:].lower() in d:
407 try:
408 if fname[-3:].lower() == ".ps":
409 from matplotlib import __version__ as mv
410 w = self.figure.figwidth.get()
411 h = self.figure.figheight.get()
412
413 if orientation is None:
414 # oriented
415 if w > h:
416 orientation = 'landscape'
417 else:
418 orientation = 'portrait'
419 from matplotlib.backends.backend_ps import papersize
420 pw,ph = papersize[papertype.lower()]
421 ds = None
422 if orientation == 'landscape':
423 ds = min(ph/w, pw/h)
424 else:
425 ds = min(pw/w, ph/h)
426 ow = ds * w
427 oh = ds * h
428 self.figure.set_figsize_inches((ow, oh))
429 self.figure.savefig(fname, orientation=orientation,
430 papertype=papertype.lower())
431 self.figure.set_figsize_inches((w, h))
432 print 'Written file %s' % (fname)
433 else:
434 if dpi is None:
435 dpi =150
436 self.figure.savefig(fname,dpi=dpi)
437 print 'Written file %s' % (fname)
438 except IOError, msg:
439 print 'Failed to save %s: Error msg was\n\n%s' % (fname, err)
440 return
441 else:
442 print "Invalid image type. Valid types are:"
443 print "'ps', 'eps', 'png'"
444
445
446 def set_axes(self, what=None, *args, **kwargs):
447 """
448 Set attributes for the axes by calling the relevant Axes.set_*()
449 method. Colour translation is done as described in the doctext
450 for palette().
451 """
452
453 if what is None: return
454 if what[-6:] == 'colour': what = what[:-6] + 'color'
455
456 key = "colour"
457 if kwargs.has_key(key):
458 val = kwargs.pop(key)
459 kwargs["color"] = val
460
461 getattr(self.axes, "set_%s"%what)(*args, **kwargs)
462
463 self.show(hardrefresh=False)
464
465
466 def set_figure(self, what=None, *args, **kwargs):
467 """
468 Set attributes for the figure by calling the relevant Figure.set_*()
469 method. Colour translation is done as described in the doctext
470 for palette().
471 """
472
473 if what is None: return
474 if what[-6:] == 'colour': what = what[:-6] + 'color'
475 #if what[-5:] == 'color' and len(args):
476 # args = (get_colour(args[0]),)
477
478 newargs = {}
479 for k, v in kwargs.iteritems():
480 k = k.lower()
481 if k == 'colour': k = 'color'
482 newargs[k] = v
483
484 getattr(self.figure, "set_%s"%what)(*args, **newargs)
485 self.show(hardrefresh=False)
486
487
488 def set_limits(self, xlim=None, ylim=None):
489 """
490 Set x-, and y-limits for each subplot.
491
492 xlim = [xmin, xmax] as in axes.set_xlim().
493 ylim = [ymin, ymax] as in axes.set_ylim().
494 """
495 for s in self.subplots:
496 self.axes = s['axes']
497 self.lines = s['lines']
498 oldxlim = list(self.axes.get_xlim())
499 oldylim = list(self.axes.get_ylim())
500 if xlim is not None:
501 for i in range(len(xlim)):
502 if xlim[i] is not None:
503 oldxlim[i] = xlim[i]
504 if ylim is not None:
505 for i in range(len(ylim)):
506 if ylim[i] is not None:
507 oldylim[i] = ylim[i]
508 self.axes.set_xlim(oldxlim)
509 self.axes.set_ylim(oldylim)
510 return
511
512
513 def set_line(self, number=None, **kwargs):
514 """
515 Set attributes for the specified line, or else the next line(s)
516 to be plotted.
517
518 number is the 0-relative number of a line that has already been
519 plotted. If no such line exists, attributes are recorded and used
520 for the next line(s) to be plotted.
521
522 Keyword arguments specify Line2D attributes, e.g. color='r'. Do
523
524 import matplotlib
525 help(matplotlib.lines)
526
527 The set_* methods of class Line2D define the attribute names and
528 values. For non-US usage, "colour" is recognized as synonymous with
529 "color".
530
531 Set the value to None to delete an attribute.
532
533 Colour translation is done as described in the doctext for palette().
534 """
535
536 redraw = False
537 for k, v in kwargs.iteritems():
538 k = k.lower()
539 if k == 'colour': k = 'color'
540
541 if 0 <= number < len(self.lines):
542 if self.lines[number] is not None:
543 for line in self.lines[number]:
544 getattr(line, "set_%s"%k)(v)
545 redraw = True
546 else:
547 if v is None:
548 del self.attributes[k]
549 else:
550 self.attributes[k] = v
551
552 if redraw: self.show(hardrefresh=False)
553
554
555 def set_panels(self, rows=1, cols=0, n=-1, nplots=-1, ganged=True):
556 """
557 Set the panel layout.
558
559 rows and cols, if cols != 0, specify the number of rows and columns in
560 a regular layout. (Indexing of these panels in matplotlib is row-
561 major, i.e. column varies fastest.)
562
563 cols == 0 is interpreted as a retangular layout that accomodates
564 'rows' panels, e.g. rows == 6, cols == 0 is equivalent to
565 rows == 2, cols == 3.
566
567 0 <= n < rows*cols is interpreted as the 0-relative panel number in
568 the configuration specified by rows and cols to be added to the
569 current figure as its next 0-relative panel number (i). This allows
570 non-regular panel layouts to be constructed via multiple calls. Any
571 other value of n clears the plot and produces a rectangular array of
572 empty panels. The number of these may be limited by nplots.
573 """
574 if n < 0 and len(self.subplots):
575 self.figure.clear()
576 self.set_title()
577
578 if rows < 1: rows = 1
579
580 if cols <= 0:
581 i = int(sqrt(rows))
582 if i*i < rows: i += 1
583 cols = i
584
585 if i*(i-1) >= rows: i -= 1
586 rows = i
587
588 if 0 <= n < rows*cols:
589 i = len(self.subplots)
590 self.subplots.append({})
591
592 self.subplots[i]['axes'] = self.figure.add_subplot(rows,
593 cols, n+1)
594 self.subplots[i]['lines'] = []
595
596 if i == 0: self.subplot(0)
597
598 self.rows = 0
599 self.cols = 0
600
601 else:
602 self.subplots = []
603
604 if nplots < 1 or rows*cols < nplots:
605 nplots = rows*cols
606 if ganged:
607 hsp,wsp = None,None
608 if rows > 1: hsp = 0.0001
609 if cols > 1: wsp = 0.0001
610 self.figure.subplots_adjust(wspace=wsp,hspace=hsp)
611 for i in range(nplots):
612 self.subplots.append({})
613 self.subplots[i]['lines'] = []
614 if not ganged:
615 self.subplots[i]['axes'] = self.figure.add_subplot(rows,
616 cols, i+1)
617 self.subplots[i]['axes'].xaxis.set_major_formatter(OldScalarFormatter())
618 else:
619 if i == 0:
620 self.subplots[i]['axes'] = self.figure.add_subplot(rows,
621 cols, i+1)
622 self.subplots[i]['axes'].xaxis.set_major_formatter(OldScalarFormatter())
623 else:
624 self.subplots[i]['axes'] = self.figure.add_subplot(rows,
625 cols, i+1,
626 sharex=self.subplots[0]['axes'],
627 sharey=self.subplots[0]['axes'])
628
629 # Suppress tick labelling for interior subplots.
630 if i <= (rows-1)*cols - 1:
631 if i+cols < nplots:
632 # Suppress x-labels for frames width
633 # adjacent frames
634 for tick in self.subplots[i]['axes'].xaxis.majorTicks:
635 tick.label1On = False
636 self.subplots[i]['axes'].xaxis.label.set_visible(False)
637 if i%cols:
638 # Suppress y-labels for frames not in the left column.
639 for tick in self.subplots[i]['axes'].yaxis.majorTicks:
640 tick.label1On = False
641 self.subplots[i]['axes'].yaxis.label.set_visible(False)
642 # disable the first tick of [1:ncol-1] of the last row
643 #if i+1 < nplots:
644 # self.subplots[i]['axes'].xaxis.majorTicks[0].label1On = False
645 self.rows = rows
646 self.cols = cols
647 self.subplot(0)
648
649 def tidy(self):
650 # this needs to be exceuted after the first "refresh"
651 nplots = len(self.subplots)
652 if nplots == 1: return
653 for i in xrange(nplots):
654 ax = self.subplots[i]['axes']
655 if i%self.cols:
656 ax.xaxis.majorTicks[0].label1On = False
657 else:
658 if i != 0:
659 ax.yaxis.majorTicks[-1].label1On = False
660
661
662 def set_title(self, title=None):
663 """
664 Set the title of the plot window. Use the previous title if title is
665 omitted.
666 """
667 if title is not None:
668 self.title = title
669
670 self.figure.text(0.5, 0.95, self.title, horizontalalignment='center')
671
672
673 def show(self, hardrefresh=True):
674 """
675 Show graphics dependent on the current buffering state.
676 """
677 if not hardrefresh: return
678 if not self.buffering:
679 if self.loc is not None:
680 for sp in self.subplots:
681 lines = []
682 labels = []
683 i = 0
684 for line in sp['lines']:
685 i += 1
686 if line is not None:
687 lines.append(line[0])
688 lbl = line[0].get_label()
689 if lbl == '':
690 lbl = str(i)
691 labels.append(lbl)
692
693 if len(lines):
694 fp = FP(size=rcParams['legend.fontsize'])
695 fsz = fp.get_size_in_points() - len(lines)
696 fp.set_size(max(fsz,6))
697 sp['axes'].legend(tuple(lines), tuple(labels),
698 self.loc, prop=fp)
699 else:
700 sp['axes'].legend((' '))
701
702 from matplotlib.artist import setp
703 fp = FP(size=rcParams['xtick.labelsize'])
704 xts = fp.get_size_in_points()- (self.cols)/2
705 fp = FP(size=rcParams['ytick.labelsize'])
706 yts = fp.get_size_in_points() - (self.rows)/2
707 for sp in self.subplots:
708 ax = sp['axes']
709 s = ax.title.get_size()
710 tsize = s-(self.cols+self.rows)
711 ax.title.set_size(tsize)
712 fp = FP(size=rcParams['axes.labelsize'])
713 setp(ax.get_xticklabels(), fontsize=xts)
714 setp(ax.get_yticklabels(), fontsize=yts)
715 origx = fp.get_size_in_points()
716 origy = origx
717 off = 0
718 if self.cols > 1: off = self.cols
719 xfsize = origx-off
720 ax.xaxis.label.set_size(xfsize)
721 off = 0
722 if self.rows > 1: off = self.rows
723 yfsize = origy-off
724 ax.yaxis.label.set_size(yfsize)
725
726 def subplot(self, i=None, inc=None):
727 """
728 Set the subplot to the 0-relative panel number as defined by one or
729 more invokations of set_panels().
730 """
731 l = len(self.subplots)
732 if l:
733 if i is not None:
734 self.i = i
735
736 if inc is not None:
737 self.i += inc
738
739 self.i %= l
740 self.axes = self.subplots[self.i]['axes']
741 self.lines = self.subplots[self.i]['lines']
742
743 def text(self, *args, **kwargs):
744 """
745 Add text to the figure.
746 """
747 self.figure.text(*args, **kwargs)
748 self.show()
749
750 def vline_with_label(self, x, y, label,
751 location='bottom', rotate=0.0, **kwargs):
752 """
753 Plot a vertical line with label.
754 It takes "world" values fo x and y.
755 """
756 ax = self.axes
757 # need this to suppress autoscaling during this function
758 self.axes.set_autoscale_on(False)
759 ymin = 0.0
760 ymax = 1.0
761 valign = 'center'
762 if location.lower() == 'top':
763 y = max(0.0, y)
764 elif location.lower() == 'bottom':
765 y = min(0.0, y)
766 lbloffset = 0.06
767 # a rough estimate for the bb of the text
768 if rotate > 0.0: lbloffset = 0.03*len(label)
769 peakoffset = 0.01
770 xy0 = ax.transData.xy_tup((x,y))
771 # get relative coords
772 xy = ax.transAxes.inverse_xy_tup(xy0)
773 if location.lower() == 'top':
774 ymax = 1.0-lbloffset
775 ymin = xy[1]+peakoffset
776 valign = 'bottom'
777 ylbl = ymax+0.01
778 elif location.lower() == 'bottom':
779 ymin = lbloffset
780 ymax = xy[1]-peakoffset
781 valign = 'top'
782 ylbl = ymin-0.01
783 trans = blend_xy_sep_transform(ax.transData, ax.transAxes)
784 l = ax.axvline(x, ymin, ymax, color='black', **kwargs)
785 t = ax.text(x, ylbl ,label, verticalalignment=valign,
786 horizontalalignment='center',
787 rotation=rotate,transform = trans)
788 self.axes.set_autoscale_on(True)
Note: See TracBrowser for help on using the repository browser.