1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6'''
7Utility functions and defintions for a common plot style throughout Pyrocko.
9Functions with name prefix ``mpl_`` are Matplotlib specific. All others should
10be toolkit-agnostic.
12The following skeleton can be used to produce nice PDF figures, with absolute
13sizes derived from paper and font sizes
14(file :file:`/../../examples/plot_skeleton.py`
15in the Pyrocko source directory)::
17 from matplotlib import pyplot as plt
19 from pyrocko.plot import mpl_init, mpl_margins, mpl_papersize
20 # from pyrocko.plot import mpl_labelspace
22 fontsize = 9. # in points
24 # set some Pyrocko style defaults
25 mpl_init(fontsize=fontsize)
27 fig = plt.figure(figsize=mpl_papersize('a4', 'landscape'))
29 # let margins be proportional to selected font size, e.g. top and bottom
30 # margin are set to be 5*fontsize = 45 [points]
31 labelpos = mpl_margins(fig, w=7., h=5., units=fontsize)
33 axes = fig.add_subplot(1, 1, 1)
35 # positioning of axis labels
36 # mpl_labelspace(axes) # either: relative to axis tick labels
37 labelpos(axes, 2., 1.5) # or: relative to left/bottom paper edge
39 axes.plot([0, 1], [0, 9])
41 axes.set_xlabel('Time [s]')
42 axes.set_ylabel('Amplitude [m]')
44 fig.savefig('plot_skeleton.pdf')
46 plt.show()
48'''
49from __future__ import absolute_import
51import math
52import random
53import time
54import calendar
55import numpy as num
57from pyrocko.util import parse_md, time_to_str, arange2, to_time_float
58from pyrocko.guts import StringChoice, Float, Int, Bool, Tuple, Object
61try:
62 newstr = unicode
63except NameError:
64 newstr = str
67__doc__ += parse_md(__file__)
70guts_prefix = 'pf'
72point = 1.
73inch = 72.
74cm = 28.3465
76units_dict = {
77 'point': point,
78 'inch': inch,
79 'cm': cm,
80}
82_doc_units = "``'points'``, ``'inch'``, or ``'cm'``"
85def apply_units(x, units):
86 if isinstance(units, (str, newstr)):
87 units = units_dict[units]
89 if isinstance(x, (int, float)):
90 return x / units
91 else:
92 if isinstance(x, tuple):
93 return tuple(v / units for v in x)
94 else:
95 return list(v / units for v in x)
98tango_colors = {
99 'butter1': (252, 233, 79),
100 'butter2': (237, 212, 0),
101 'butter3': (196, 160, 0),
102 'chameleon1': (138, 226, 52),
103 'chameleon2': (115, 210, 22),
104 'chameleon3': (78, 154, 6),
105 'orange1': (252, 175, 62),
106 'orange2': (245, 121, 0),
107 'orange3': (206, 92, 0),
108 'skyblue1': (114, 159, 207),
109 'skyblue2': (52, 101, 164),
110 'skyblue3': (32, 74, 135),
111 'plum1': (173, 127, 168),
112 'plum2': (117, 80, 123),
113 'plum3': (92, 53, 102),
114 'chocolate1': (233, 185, 110),
115 'chocolate2': (193, 125, 17),
116 'chocolate3': (143, 89, 2),
117 'scarletred1': (239, 41, 41),
118 'scarletred2': (204, 0, 0),
119 'scarletred3': (164, 0, 0),
120 'aluminium1': (238, 238, 236),
121 'aluminium2': (211, 215, 207),
122 'aluminium3': (186, 189, 182),
123 'aluminium4': (136, 138, 133),
124 'aluminium5': (85, 87, 83),
125 'aluminium6': (46, 52, 54)}
128graph_colors = [
129 tango_colors[_x] for _x in (
130 'scarletred2',
131 'skyblue3',
132 'chameleon3',
133 'orange2',
134 'plum2',
135 'chocolate2',
136 'butter2')]
139def color(x=None):
140 if x is None:
141 return tuple([random.randint(0, 255) for _x in 'rgb'])
143 if isinstance(x, int):
144 if 0 <= x < len(graph_colors):
145 return graph_colors[x]
146 else:
147 return (0, 0, 0)
149 elif isinstance(x, (str, newstr)):
150 if x in tango_colors:
151 return tango_colors[x]
153 elif isinstance(x, tuple):
154 return x
156 assert False, "Don't know what to do with this color definition: %s" % x
159def to01(c):
160 return tuple(x/255. for x in c)
163def nice_value(x):
164 '''
165 Round x to nice value.
166 '''
168 if x == 0.0:
169 return 0.0
171 exp = 1.0
172 sign = 1
173 if x < 0.0:
174 x = -x
175 sign = -1
176 while x >= 1.0:
177 x /= 10.0
178 exp *= 10.0
179 while x < 0.1:
180 x *= 10.0
181 exp /= 10.0
183 if x >= 0.75:
184 return sign * 1.0 * exp
185 if x >= 0.375:
186 return sign * 0.5 * exp
187 if x >= 0.225:
188 return sign * 0.25 * exp
189 if x >= 0.15:
190 return sign * 0.2 * exp
192 return sign * 0.1 * exp
195_papersizes_list = [
196 ('a0', (2380., 3368.)),
197 ('a1', (1684., 2380.)),
198 ('a2', (1190., 1684.)),
199 ('a3', (842., 1190.)),
200 ('a4', (595., 842.)),
201 ('a5', (421., 595.)),
202 ('a6', (297., 421.)),
203 ('a7', (210., 297.)),
204 ('a8', (148., 210.)),
205 ('a9', (105., 148.)),
206 ('a10', (74., 105.)),
207 ('b0', (2836., 4008.)),
208 ('b1', (2004., 2836.)),
209 ('b2', (1418., 2004.)),
210 ('b3', (1002., 1418.)),
211 ('b4', (709., 1002.)),
212 ('b5', (501., 709.)),
213 ('archa', (648., 864.)),
214 ('archb', (864., 1296.)),
215 ('archc', (1296., 1728.)),
216 ('archd', (1728., 2592.)),
217 ('arche', (2592., 3456.)),
218 ('flsa', (612., 936.)),
219 ('halfletter', (396., 612.)),
220 ('note', (540., 720.)),
221 ('letter', (612., 792.)),
222 ('legal', (612., 1008.)),
223 ('11x17', (792., 1224.)),
224 ('ledger', (1224., 792.))]
226papersizes = dict(_papersizes_list)
228_doc_papersizes = ', '.join("``'%s'``" % k for (k, _) in _papersizes_list)
231def papersize(paper, orientation='landscape', units='point'):
233 '''
234 Get paper size from string.
236 :param paper: string selecting paper size. Choices: %s
237 :param orientation: ``'landscape'``, or ``'portrait'``
238 :param units: Units to be returned. Choices: %s
240 :returns: ``(width, height)``
241 '''
243 assert orientation in ('landscape', 'portrait')
245 w, h = papersizes[paper.lower()]
246 if orientation == 'landscape':
247 w, h = h, w
249 return apply_units((w, h), units)
252papersize.__doc__ %= (_doc_papersizes, _doc_units)
255class AutoScaleMode(StringChoice):
256 '''
257 Mode of operation for auto-scaling.
259 ================ ==================================================
260 mode description
261 ================ ==================================================
262 ``'auto'``: Look at data range and choose one of the choices
263 below.
264 ``'min-max'``: Output range is selected to include data range.
265 ``'0-max'``: Output range shall start at zero and end at data
266 max.
267 ``'min-0'``: Output range shall start at data min and end at
268 zero.
269 ``'symmetric'``: Output range shall by symmetric by zero.
270 ``'off'``: Similar to ``'min-max'``, but snap and space are
271 disabled, such that the output range always
272 exactly matches the data range.
273 ================ ==================================================
274 '''
275 choices = ['auto', 'min-max', '0-max', 'min-0', 'symmetric', 'off']
278class AutoScaler(Object):
280 '''
281 Tunable 1D autoscaling based on data range.
283 Instances of this class may be used to determine nice minima, maxima and
284 increments for ax annotations, as well as suitable common exponents for
285 notation.
287 The autoscaling process is guided by the following public attributes:
288 '''
290 approx_ticks = Float.T(
291 default=7.0,
292 help='Approximate number of increment steps (tickmarks) to generate.')
294 mode = AutoScaleMode.T(
295 default='auto',
296 help='''Mode of operation for auto-scaling.''')
298 exp = Int.T(
299 optional=True,
300 help='If defined, override automatically determined exponent for '
301 'notation by the given value.')
303 snap = Bool.T(
304 default=False,
305 help='If set to True, snap output range to multiples of increment. '
306 'This parameter has no effect, if mode is set to ``\'off\'``.')
308 inc = Float.T(
309 optional=True,
310 help='If defined, override automatically determined tick increment by '
311 'the given value.')
313 space = Float.T(
314 default=0.0,
315 help='Add some padding to the range. The value given, is the fraction '
316 'by which the output range is increased on each side. If mode is '
317 '``\'0-max\'`` or ``\'min-0\'``, the end at zero is kept fixed '
318 'at zero. This parameter has no effect if mode is set to '
319 '``\'off\'``.')
321 exp_factor = Int.T(
322 default=3,
323 help='Exponent of notation is chosen to be a multiple of this value.')
325 no_exp_interval = Tuple.T(
326 2, Int.T(),
327 default=(-3, 5),
328 help='Range of exponent, for which no exponential notation is a'
329 'allowed.')
331 def __init__(
332 self,
333 approx_ticks=7.0,
334 mode='auto',
335 exp=None,
336 snap=False,
337 inc=None,
338 space=0.0,
339 exp_factor=3,
340 no_exp_interval=(-3, 5)):
342 '''
343 Create new AutoScaler instance.
345 The parameters are described in the AutoScaler documentation.
346 '''
348 Object.__init__(
349 self,
350 approx_ticks=approx_ticks,
351 mode=mode,
352 exp=exp,
353 snap=snap,
354 inc=inc,
355 space=space,
356 exp_factor=exp_factor,
357 no_exp_interval=no_exp_interval)
359 def make_scale(self, data_range, override_mode=None):
361 '''
362 Get nice minimum, maximum and increment for given data range.
364 Returns ``(minimum, maximum, increment)`` or ``(maximum, minimum,
365 -increment)``, depending on whether data_range is ``(data_min,
366 data_max)`` or ``(data_max, data_min)``. If ``override_mode`` is
367 defined, the mode attribute is temporarily overridden by the given
368 value.
369 '''
371 data_min = min(data_range)
372 data_max = max(data_range)
374 is_reverse = (data_range[0] > data_range[1])
376 a = self.mode
377 if self.mode == 'auto':
378 a = self.guess_autoscale_mode(data_min, data_max)
380 if override_mode is not None:
381 a = override_mode
383 mi, ma = 0, 0
384 if a == 'off':
385 mi, ma = data_min, data_max
386 elif a == '0-max':
387 mi = 0.0
388 if data_max > 0.0:
389 ma = data_max
390 else:
391 ma = 1.0
392 elif a == 'min-0':
393 ma = 0.0
394 if data_min < 0.0:
395 mi = data_min
396 else:
397 mi = -1.0
398 elif a == 'min-max':
399 mi, ma = data_min, data_max
400 elif a == 'symmetric':
401 m = max(abs(data_min), abs(data_max))
402 mi = -m
403 ma = m
405 nmi = mi
406 if (mi != 0. or a == 'min-max') and a != 'off':
407 nmi = mi - self.space*(ma-mi)
409 nma = ma
410 if (ma != 0. or a == 'min-max') and a != 'off':
411 nma = ma + self.space*(ma-mi)
413 mi, ma = nmi, nma
415 if mi == ma and a != 'off':
416 mi -= 1.0
417 ma += 1.0
419 # make nice tick increment
420 if self.inc is not None:
421 inc = self.inc
422 else:
423 if self.approx_ticks > 0.:
424 inc = nice_value((ma-mi) / self.approx_ticks)
425 else:
426 inc = nice_value((ma-mi)*10.)
428 if inc == 0.0:
429 inc = 1.0
431 # snap min and max to ticks if this is wanted
432 if self.snap and a != 'off':
433 ma = inc * math.ceil(ma/inc)
434 mi = inc * math.floor(mi/inc)
436 if is_reverse:
437 return ma, mi, -inc
438 else:
439 return mi, ma, inc
441 def make_exp(self, x):
442 '''
443 Get nice exponent for notation of ``x``.
445 For ax annotations, give tick increment as ``x``.
446 '''
448 if self.exp is not None:
449 return self.exp
451 x = abs(x)
452 if x == 0.0:
453 return 0
455 if 10**self.no_exp_interval[0] <= x <= 10**self.no_exp_interval[1]:
456 return 0
458 return math.floor(math.log10(x)/self.exp_factor)*self.exp_factor
460 def guess_autoscale_mode(self, data_min, data_max):
461 '''
462 Guess mode of operation, based on data range.
464 Used to map ``'auto'`` mode to ``'0-max'``, ``'min-0'``, ``'min-max'``
465 or ``'symmetric'``.
466 '''
468 a = 'min-max'
469 if data_min >= 0.0:
470 if data_min < data_max/2.:
471 a = '0-max'
472 else:
473 a = 'min-max'
474 if data_max <= 0.0:
475 if data_max > data_min/2.:
476 a = 'min-0'
477 else:
478 a = 'min-max'
479 if data_min < 0.0 and data_max > 0.0:
480 if abs((abs(data_max)-abs(data_min)) /
481 (abs(data_max)+abs(data_min))) < 0.5:
482 a = 'symmetric'
483 else:
484 a = 'min-max'
485 return a
488# below, some convenience functions for matplotlib plotting
490def mpl_init(fontsize=10):
491 '''
492 Initialize Matplotlib rc parameters Pyrocko style.
494 Returns the matplotlib.pyplot module for convenience.
495 '''
497 import matplotlib
499 matplotlib.rcdefaults()
500 matplotlib.rc('font', size=fontsize)
501 matplotlib.rc('axes', linewidth=1.5)
502 matplotlib.rc('xtick', direction='out')
503 matplotlib.rc('ytick', direction='out')
504 ts = fontsize * 0.7071
505 matplotlib.rc('xtick.major', size=ts, width=0.5, pad=ts)
506 matplotlib.rc('ytick.major', size=ts, width=0.5, pad=ts)
507 matplotlib.rc('figure', facecolor='white')
509 try:
510 from cycler import cycler
511 matplotlib.rc(
512 'axes', prop_cycle=cycler(
513 'color', [to01(x) for x in graph_colors]))
514 except (ImportError, KeyError):
515 try:
516 matplotlib.rc('axes', color_cycle=[to01(x) for x in graph_colors])
517 except KeyError:
518 pass
520 from matplotlib import pyplot as plt
521 return plt
524def mpl_margins(
525 fig,
526 left=1.0, top=1.0, right=1.0, bottom=1.0,
527 wspace=None, hspace=None,
528 w=None, h=None,
529 nw=None, nh=None,
530 all=None,
531 units='inch'):
533 '''
534 Adjust Matplotlib subplot params with absolute values in user units.
536 Calls :py:meth:`matplotlib.figure.Figure.subplots_adjust` on ``fig`` with
537 absolute margin widths/heights rather than relative values. If ``wspace``
538 or ``hspace`` are given, the number of subplots must be given in ``nw``
539 and ``nh`` because ``subplots_adjust()`` treats the spacing parameters
540 relative to the subplot width and height.
542 :param units: Unit multiplier or unit as string: %s
543 :param left,right,top,bottom: margin space
544 :param w: set ``left`` and ``right`` at once
545 :param h: set ``top`` and ``bottom`` at once
546 :param all: set ``left``, ``top``, ``right``, and ``bottom`` at once
547 :param nw: number of subplots horizontally
548 :param nh: number of subplots vertically
549 :param wspace: horizontal spacing between subplots
550 :param hspace: vertical spacing between subplots
551 '''
553 left, top, right, bottom = map(
554 float, (left, top, right, bottom))
556 if w is not None:
557 left = right = float(w)
559 if h is not None:
560 top = bottom = float(h)
562 if all is not None:
563 left = right = top = bottom = float(all)
565 ufac = units_dict.get(units, units) / inch
567 left *= ufac
568 right *= ufac
569 top *= ufac
570 bottom *= ufac
572 width, height = fig.get_size_inches()
574 rel_wspace = None
575 rel_hspace = None
577 if wspace is not None:
578 wspace *= ufac
579 if nw is None:
580 raise ValueError('wspace must be given in combination with nw')
582 wsub = (width - left - right - (nw-1) * wspace) / nw
583 rel_wspace = wspace / wsub
584 else:
585 wsub = width - left - right
587 if hspace is not None:
588 hspace *= ufac
589 if nh is None:
590 raise ValueError('hspace must be given in combination with nh')
592 hsub = (height - top - bottom - (nh-1) * hspace) / nh
593 rel_hspace = hspace / hsub
594 else:
595 hsub = height - top - bottom
597 fig.subplots_adjust(
598 left=left/width,
599 right=1.0 - right/width,
600 bottom=bottom/height,
601 top=1.0 - top/height,
602 wspace=rel_wspace,
603 hspace=rel_hspace)
605 def labelpos(axes, xpos=0., ypos=0.):
606 xpos *= ufac
607 ypos *= ufac
608 axes.get_yaxis().set_label_coords(-((left-xpos) / wsub), 0.5)
609 axes.get_xaxis().set_label_coords(0.5, -((bottom-ypos) / hsub))
611 return labelpos
614mpl_margins.__doc__ %= _doc_units
617def mpl_labelspace(axes):
618 '''
619 Add some extra padding between label and ax annotations.
620 '''
622 xa = axes.get_xaxis()
623 ya = axes.get_yaxis()
624 for attr in ('labelpad', 'LABELPAD'):
625 if hasattr(xa, attr):
626 setattr(xa, attr, xa.get_label().get_fontsize())
627 setattr(ya, attr, ya.get_label().get_fontsize())
628 break
631def mpl_papersize(paper, orientation='landscape'):
632 '''
633 Get paper size in inch from string.
635 Returns argument suitable to be passed to the ``figsize`` argument of
636 :py:func:`pyplot.figure`.
638 :param paper: string selecting paper size. Choices: %s
639 :param orientation: ``'landscape'``, or ``'portrait'``
641 :returns: ``(width, height)``
642 '''
644 return papersize(paper, orientation=orientation, units='inch')
647mpl_papersize.__doc__ %= _doc_papersizes
650class InvalidColorDef(ValueError):
651 pass
654def mpl_graph_color(i):
655 return to01(graph_colors[i % len(graph_colors)])
658def mpl_color(x):
659 '''
660 Convert string into color float tuple ranged 0-1 for use with Matplotlib.
662 Accepts tango color names, matplotlib color names, and slash-separated
663 strings. In the latter case, if values are larger than 1., the color
664 is interpreted as 0-255 ranged. Single-valued (grayscale), three-valued
665 (color) and four-valued (color with alpha) are accepted. An
666 :py:exc:`InvalidColorDef` exception is raised when the convertion fails.
667 '''
669 import matplotlib.colors
671 if x in tango_colors:
672 return to01(tango_colors[x])
674 s = x.split('/')
675 if len(s) in (1, 3, 4):
676 try:
677 vals = list(map(float, s))
678 if all(0. <= v <= 1. for v in vals):
679 return vals
681 elif all(0. <= v <= 255. for v in vals):
682 return to01(vals)
684 except ValueError:
685 try:
686 return matplotlib.colors.colorConverter.to_rgba(x)
687 except Exception:
688 pass
690 raise InvalidColorDef('invalid color definition: %s' % x)
693def nice_time_tick_inc(tinc_approx):
694 hours = 3600.
695 days = hours*24
696 approx_months = days*30.5
697 approx_years = days*365
699 if tinc_approx >= approx_years:
700 return max(1.0, nice_value(tinc_approx / approx_years)), 'years'
702 elif tinc_approx >= approx_months:
703 nice = [1, 2, 3, 6]
704 for tinc in nice:
705 if tinc*approx_months >= tinc_approx or tinc == nice[-1]:
706 return tinc, 'months'
708 elif tinc_approx > days:
709 return nice_value(tinc_approx / days) * days, 'seconds'
711 elif tinc_approx >= 1.0:
712 nice = [
713 1., 2., 5., 10., 20., 30., 60., 120., 300., 600., 1200., 1800.,
714 1*hours, 2*hours, 3*hours, 6*hours, 12*hours, days, 2*days]
716 for tinc in nice:
717 if tinc >= tinc_approx or tinc == nice[-1]:
718 return tinc, 'seconds'
720 else:
721 return nice_value(tinc_approx), 'seconds'
724def time_tick_labels(tmin, tmax, tinc, tinc_unit):
726 if tinc_unit == 'years':
727 tt = time.gmtime(int(tmin))
728 tmin_year = tt[0]
729 if tt[1:6] != (1, 1, 0, 0, 0):
730 tmin_year += 1
732 tmax_year = time.gmtime(int(tmax))[0]
734 tick_times_year = arange2(
735 math.ceil(tmin_year/tinc)*tinc,
736 math.floor(tmax_year/tinc)*tinc,
737 tinc).astype(int)
739 times = [
740 to_time_float(calendar.timegm((year, 1, 1, 0, 0, 0)))
741 for year in tick_times_year]
743 labels = ['%04i' % year for year in tick_times_year]
745 elif tinc_unit == 'months':
746 tt = time.gmtime(int(tmin))
747 tmin_ym = tt[0] * 12 + (tt[1] - 1)
748 if tt[2:6] != (1, 0, 0, 0):
749 tmin_ym += 1
751 tt = time.gmtime(int(tmax))
752 tmax_ym = tt[0] * 12 + (tt[1] - 1)
754 tick_times_ym = arange2(
755 math.ceil(tmin_ym/tinc)*tinc,
756 math.floor(tmax_ym/tinc)*tinc, tinc).astype(int)
758 times = [
759 to_time_float(calendar.timegm((ym // 12, ym % 12 + 1, 1, 0, 0, 0)))
760 for ym in tick_times_ym]
762 labels = [
763 '%04i-%02i' % (ym // 12, ym % 12 + 1) for ym in tick_times_ym]
765 elif tinc_unit == 'seconds':
766 imin = int(num.ceil(tmin/tinc))
767 imax = int(num.floor(tmax/tinc))
768 nticks = imax - imin + 1
769 tmin_ticks = imin * tinc
770 times = tmin_ticks + num.arange(nticks) * tinc
771 times = times.tolist()
773 if tinc < 1e-6:
774 fmt = '%Y-%m-%d.%H:%M:%S.9FRAC'
775 elif tinc < 1e-3:
776 fmt = '%Y-%m-%d.%H:%M:%S.6FRAC'
777 elif tinc < 1.0:
778 fmt = '%Y-%m-%d.%H:%M:%S.3FRAC'
779 elif tinc < 60:
780 fmt = '%Y-%m-%d.%H:%M:%S'
781 elif tinc < 3600.*24:
782 fmt = '%Y-%m-%d.%H:%M'
783 else:
784 fmt = '%Y-%m-%d'
786 nwords = len(fmt.split('.'))
788 labels = [time_to_str(t, format=fmt) for t in times]
789 labels_weeded = []
790 have_ymd = have_hms = False
791 ymd = hms = ''
792 for ilab, lab in reversed(list(enumerate(labels))):
793 words = lab.split('.')
794 if nwords > 2:
795 words[2] = '.' + words[2]
796 if float(words[2]) == 0.0: # or (ilab == 0 and not have_hms):
797 have_hms = True
798 else:
799 hms = words[1]
800 words[1] = ''
801 else:
802 have_hms = True
804 if nwords > 1:
805 if words[1] in ('00:00', '00:00:00'): # or (ilab == 0 and not have_ymd): # noqa
806 have_ymd = True
807 else:
808 ymd = words[0]
809 words[0] = ''
810 else:
811 have_ymd = True
813 labels_weeded.append('\n'.join(reversed(words)))
815 labels = list(reversed(labels_weeded))
816 if (not have_ymd or not have_hms) and (hms or ymd):
817 words = ([''] if nwords > 2 else []) + [
818 hms if not have_hms else '',
819 ymd if not have_ymd else '']
821 labels[0:0] = ['\n'.join(words)]
822 times[0:0] = [tmin]
824 return times, labels
827def mpl_time_axis(axes, approx_ticks=5.):
829 '''
830 Configure x axis of a matplotlib axes object for interactive time display.
832 :param axes: Axes to be configured.
833 :type axes: :py:class:`matplotlib.axes.Axes`
835 :param approx_ticks: Approximate number of ticks to create.
836 :type approx_ticks: float
838 This function tries to use nice tick increments and tick labels for time
839 ranges from microseconds to years, similar to how this is handled in
840 Snuffler.
841 '''
843 from matplotlib.ticker import Locator, Formatter
845 class labeled_float(float):
846 pass
848 class TimeLocator(Locator):
850 def __init__(self, approx_ticks=5.):
851 self._approx_ticks = approx_ticks
852 Locator.__init__(self)
854 def __call__(self):
855 vmin, vmax = self.axis.get_view_interval()
856 return self.tick_values(vmin, vmax)
858 def tick_values(self, vmin, vmax):
859 if vmax < vmin:
860 vmin, vmax = vmax, vmin
862 if vmin == vmax:
863 return []
865 tinc_approx = (vmax - vmin) / self._approx_ticks
866 tinc, tinc_unit = nice_time_tick_inc(tinc_approx)
867 times, labels = time_tick_labels(vmin, vmax, tinc, tinc_unit)
868 ftimes = []
869 for t, label in zip(times, labels):
870 ftime = labeled_float(t)
871 ftime._mpl_label = label
872 ftimes.append(ftime)
874 return self.raise_if_exceeds(ftimes)
876 class TimeFormatter(Formatter):
878 def __call__(self, x, pos=None):
879 if isinstance(x, labeled_float):
880 return x._mpl_label
881 else:
882 return time_to_str(x, format='%Y-%m-%d %H:%M:%S.6FRAC')
884 axes.xaxis.set_major_locator(TimeLocator(approx_ticks=approx_ticks))
885 axes.xaxis.set_major_formatter(TimeFormatter())