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'''
50import math
51import random
52import time
53import calendar
54import numpy as num
56from pyrocko.util import parse_md, time_to_str, arange2, to_time_float
57from pyrocko.guts import StringChoice, Float, Int, Bool, Tuple, Object
60__doc__ += parse_md(__file__)
63guts_prefix = 'pf'
65point = 1.
66inch = 72.
67cm = 28.3465
69units_dict = {
70 'point': point,
71 'inch': inch,
72 'cm': cm,
73}
75_doc_units = "``'point'``, ``'inch'``, or ``'cm'``"
78def apply_units(x, units):
79 if isinstance(units, str):
80 units = units_dict[units]
82 if isinstance(x, (int, float)):
83 return x / units
84 else:
85 if isinstance(x, tuple):
86 return tuple(v / units for v in x)
87 else:
88 return list(v / units for v in x)
91tango_colors = {
92 'butter1': (252, 233, 79),
93 'butter2': (237, 212, 0),
94 'butter3': (196, 160, 0),
95 'chameleon1': (138, 226, 52),
96 'chameleon2': (115, 210, 22),
97 'chameleon3': (78, 154, 6),
98 'orange1': (252, 175, 62),
99 'orange2': (245, 121, 0),
100 'orange3': (206, 92, 0),
101 'skyblue1': (114, 159, 207),
102 'skyblue2': (52, 101, 164),
103 'skyblue3': (32, 74, 135),
104 'plum1': (173, 127, 168),
105 'plum2': (117, 80, 123),
106 'plum3': (92, 53, 102),
107 'chocolate1': (233, 185, 110),
108 'chocolate2': (193, 125, 17),
109 'chocolate3': (143, 89, 2),
110 'scarletred1': (239, 41, 41),
111 'scarletred2': (204, 0, 0),
112 'scarletred3': (164, 0, 0),
113 'aluminium1': (238, 238, 236),
114 'aluminium2': (211, 215, 207),
115 'aluminium3': (186, 189, 182),
116 'aluminium4': (136, 138, 133),
117 'aluminium5': (85, 87, 83),
118 'aluminium6': (46, 52, 54)}
121graph_colors = [
122 tango_colors[_x] for _x in (
123 'scarletred2',
124 'skyblue3',
125 'chameleon3',
126 'orange2',
127 'plum2',
128 'chocolate2',
129 'butter2')]
132def color(x=None):
133 if x is None:
134 return tuple([random.randint(0, 255) for _x in 'rgb'])
136 if isinstance(x, int):
137 if 0 <= x < len(graph_colors):
138 return graph_colors[x]
139 else:
140 return (0, 0, 0)
142 elif isinstance(x, str):
143 if x in tango_colors:
144 return tango_colors[x]
146 elif isinstance(x, tuple):
147 return x
149 assert False, "Don't know what to do with this color definition: %s" % x
152def to01(c):
153 return tuple(x/255. for x in c)
156def nice_value(x):
157 '''
158 Round x to nice value.
159 '''
161 if x == 0.0:
162 return 0.0
164 exp = 1.0
165 sign = 1
166 if x < 0.0:
167 x = -x
168 sign = -1
169 while x >= 1.0:
170 x /= 10.0
171 exp *= 10.0
172 while x < 0.1:
173 x *= 10.0
174 exp /= 10.0
176 if x >= 0.75:
177 return sign * 1.0 * exp
178 if x >= 0.375:
179 return sign * 0.5 * exp
180 if x >= 0.225:
181 return sign * 0.25 * exp
182 if x >= 0.15:
183 return sign * 0.2 * exp
185 return sign * 0.1 * exp
188_papersizes_list = [
189 ('a0', (2380., 3368.)),
190 ('a1', (1684., 2380.)),
191 ('a2', (1190., 1684.)),
192 ('a3', (842., 1190.)),
193 ('a4', (595., 842.)),
194 ('a5', (421., 595.)),
195 ('a6', (297., 421.)),
196 ('a7', (210., 297.)),
197 ('a8', (148., 210.)),
198 ('a9', (105., 148.)),
199 ('a10', (74., 105.)),
200 ('b0', (2836., 4008.)),
201 ('b1', (2004., 2836.)),
202 ('b2', (1418., 2004.)),
203 ('b3', (1002., 1418.)),
204 ('b4', (709., 1002.)),
205 ('b5', (501., 709.)),
206 ('archa', (648., 864.)),
207 ('archb', (864., 1296.)),
208 ('archc', (1296., 1728.)),
209 ('archd', (1728., 2592.)),
210 ('arche', (2592., 3456.)),
211 ('flsa', (612., 936.)),
212 ('halfletter', (396., 612.)),
213 ('note', (540., 720.)),
214 ('letter', (612., 792.)),
215 ('legal', (612., 1008.)),
216 ('11x17', (792., 1224.)),
217 ('ledger', (1224., 792.))]
219papersizes = dict(_papersizes_list)
221_doc_papersizes = ', '.join("``'%s'``" % k for (k, _) in _papersizes_list)
224def papersize(paper, orientation='landscape', units='point'):
226 '''
227 Get paper size from string.
229 :param paper: string selecting paper size. Choices: %s
230 :param orientation: ``'landscape'``, or ``'portrait'``
231 :param units: Units to be returned. Choices: %s
233 :returns: ``(width, height)``
234 '''
236 assert orientation in ('landscape', 'portrait')
238 w, h = papersizes[paper.lower()]
239 if orientation == 'landscape':
240 w, h = h, w
242 return apply_units((w, h), units)
245papersize.__doc__ %= (_doc_papersizes, _doc_units)
248class AutoScaleMode(StringChoice):
249 '''
250 Mode of operation for auto-scaling.
252 ================ ==================================================
253 mode description
254 ================ ==================================================
255 ``'auto'``: Look at data range and choose one of the choices
256 below.
257 ``'min-max'``: Output range is selected to include data range.
258 ``'0-max'``: Output range shall start at zero and end at data
259 max.
260 ``'min-0'``: Output range shall start at data min and end at
261 zero.
262 ``'symmetric'``: Output range shall by symmetric by zero.
263 ``'off'``: Similar to ``'min-max'``, but snap and space are
264 disabled, such that the output range always
265 exactly matches the data range.
266 ================ ==================================================
267 '''
268 choices = ['auto', 'min-max', '0-max', 'min-0', 'symmetric', 'off']
271class AutoScaler(Object):
273 '''
274 Tunable 1D autoscaling based on data range.
276 Instances of this class may be used to determine nice minima, maxima and
277 increments for ax annotations, as well as suitable common exponents for
278 notation.
280 The autoscaling process is guided by the following public attributes:
281 '''
283 approx_ticks = Float.T(
284 default=7.0,
285 help='Approximate number of increment steps (tickmarks) to generate.')
287 mode = AutoScaleMode.T(
288 default='auto',
289 help='''Mode of operation for auto-scaling.''')
291 exp = Int.T(
292 optional=True,
293 help='If defined, override automatically determined exponent for '
294 'notation by the given value.')
296 snap = Bool.T(
297 default=False,
298 help='If set to True, snap output range to multiples of increment. '
299 'This parameter has no effect, if mode is set to ``\'off\'``.')
301 inc = Float.T(
302 optional=True,
303 help='If defined, override automatically determined tick increment by '
304 'the given value.')
306 space = Float.T(
307 default=0.0,
308 help='Add some padding to the range. The value given, is the fraction '
309 'by which the output range is increased on each side. If mode is '
310 '``\'0-max\'`` or ``\'min-0\'``, the end at zero is kept fixed '
311 'at zero. This parameter has no effect if mode is set to '
312 '``\'off\'``.')
314 exp_factor = Int.T(
315 default=3,
316 help='Exponent of notation is chosen to be a multiple of this value.')
318 no_exp_interval = Tuple.T(
319 2, Int.T(),
320 default=(-3, 5),
321 help='Range of exponent, for which no exponential notation is a'
322 'allowed.')
324 def __init__(
325 self,
326 approx_ticks=7.0,
327 mode='auto',
328 exp=None,
329 snap=False,
330 inc=None,
331 space=0.0,
332 exp_factor=3,
333 no_exp_interval=(-3, 5)):
335 '''
336 Create new AutoScaler instance.
338 The parameters are described in the AutoScaler documentation.
339 '''
341 Object.__init__(
342 self,
343 approx_ticks=approx_ticks,
344 mode=mode,
345 exp=exp,
346 snap=snap,
347 inc=inc,
348 space=space,
349 exp_factor=exp_factor,
350 no_exp_interval=no_exp_interval)
352 def make_scale(self, data_range, override_mode=None):
354 '''
355 Get nice minimum, maximum and increment for given data range.
357 Returns ``(minimum, maximum, increment)`` or ``(maximum, minimum,
358 -increment)``, depending on whether data_range is ``(data_min,
359 data_max)`` or ``(data_max, data_min)``. If ``override_mode`` is
360 defined, the mode attribute is temporarily overridden by the given
361 value.
362 '''
364 data_min = min(data_range)
365 data_max = max(data_range)
367 is_reverse = (data_range[0] > data_range[1])
369 a = self.mode
370 if self.mode == 'auto':
371 a = self.guess_autoscale_mode(data_min, data_max)
373 if override_mode is not None:
374 a = override_mode
376 mi, ma = 0, 0
377 if a == 'off':
378 mi, ma = data_min, data_max
379 elif a == '0-max':
380 mi = 0.0
381 if data_max > 0.0:
382 ma = data_max
383 else:
384 ma = 1.0
385 elif a == 'min-0':
386 ma = 0.0
387 if data_min < 0.0:
388 mi = data_min
389 else:
390 mi = -1.0
391 elif a == 'min-max':
392 mi, ma = data_min, data_max
393 elif a == 'symmetric':
394 m = max(abs(data_min), abs(data_max))
395 mi = -m
396 ma = m
398 nmi = mi
399 if (mi != 0. or a == 'min-max') and a != 'off':
400 nmi = mi - self.space*(ma-mi)
402 nma = ma
403 if (ma != 0. or a == 'min-max') and a != 'off':
404 nma = ma + self.space*(ma-mi)
406 mi, ma = nmi, nma
408 if mi == ma and a != 'off':
409 mi -= 1.0
410 ma += 1.0
412 # make nice tick increment
413 if self.inc is not None:
414 inc = self.inc
415 else:
416 if self.approx_ticks > 0.:
417 inc = nice_value((ma-mi) / self.approx_ticks)
418 else:
419 inc = nice_value((ma-mi)*10.)
421 if inc == 0.0:
422 inc = 1.0
424 # snap min and max to ticks if this is wanted
425 if self.snap and a != 'off':
426 ma = inc * math.ceil(ma/inc)
427 mi = inc * math.floor(mi/inc)
429 if is_reverse:
430 return ma, mi, -inc
431 else:
432 return mi, ma, inc
434 def make_exp(self, x):
435 '''
436 Get nice exponent for notation of ``x``.
438 For ax annotations, give tick increment as ``x``.
439 '''
441 if self.exp is not None:
442 return self.exp
444 x = abs(x)
445 if x == 0.0:
446 return 0
448 if 10**self.no_exp_interval[0] <= x <= 10**self.no_exp_interval[1]:
449 return 0
451 return math.floor(math.log10(x)/self.exp_factor)*self.exp_factor
453 def guess_autoscale_mode(self, data_min, data_max):
454 '''
455 Guess mode of operation, based on data range.
457 Used to map ``'auto'`` mode to ``'0-max'``, ``'min-0'``, ``'min-max'``
458 or ``'symmetric'``.
459 '''
461 a = 'min-max'
462 if data_min >= 0.0:
463 if data_min < data_max/2.:
464 a = '0-max'
465 else:
466 a = 'min-max'
467 if data_max <= 0.0:
468 if data_max > data_min/2.:
469 a = 'min-0'
470 else:
471 a = 'min-max'
472 if data_min < 0.0 and data_max > 0.0:
473 if abs((abs(data_max)-abs(data_min)) /
474 (abs(data_max)+abs(data_min))) < 0.5:
475 a = 'symmetric'
476 else:
477 a = 'min-max'
478 return a
481# below, some convenience functions for matplotlib plotting
483def mpl_init(fontsize=10):
484 '''
485 Initialize Matplotlib rc parameters Pyrocko style.
487 Returns the matplotlib.pyplot module for convenience.
488 '''
490 import matplotlib
492 matplotlib.rcdefaults()
493 matplotlib.rc('font', size=fontsize)
494 matplotlib.rc('axes', linewidth=1.5)
495 matplotlib.rc('xtick', direction='out')
496 matplotlib.rc('ytick', direction='out')
497 ts = fontsize * 0.7071
498 matplotlib.rc('xtick.major', size=ts, width=0.5, pad=ts)
499 matplotlib.rc('ytick.major', size=ts, width=0.5, pad=ts)
500 matplotlib.rc('figure', facecolor='white')
502 try:
503 from cycler import cycler
504 matplotlib.rc(
505 'axes', prop_cycle=cycler(
506 'color', [to01(x) for x in graph_colors]))
507 except (ImportError, KeyError):
508 try:
509 matplotlib.rc('axes', color_cycle=[to01(x) for x in graph_colors])
510 except KeyError:
511 pass
513 from matplotlib import pyplot as plt
514 return plt
517def mpl_margins(
518 fig,
519 left=1.0, top=1.0, right=1.0, bottom=1.0,
520 wspace=None, hspace=None,
521 w=None, h=None,
522 nw=None, nh=None,
523 all=None,
524 units='inch'):
526 '''
527 Adjust Matplotlib subplot params with absolute values in user units.
529 Calls :py:meth:`matplotlib.figure.Figure.subplots_adjust` on ``fig`` with
530 absolute margin widths/heights rather than relative values. If ``wspace``
531 or ``hspace`` are given, the number of subplots must be given in ``nw``
532 and ``nh`` because ``subplots_adjust()`` treats the spacing parameters
533 relative to the subplot width and height.
535 :param units: Unit multiplier or unit as string: %s
536 :param left,right,top,bottom: margin space
537 :param w: set ``left`` and ``right`` at once
538 :param h: set ``top`` and ``bottom`` at once
539 :param all: set ``left``, ``top``, ``right``, and ``bottom`` at once
540 :param nw: number of subplots horizontally
541 :param nh: number of subplots vertically
542 :param wspace: horizontal spacing between subplots
543 :param hspace: vertical spacing between subplots
544 '''
546 left, top, right, bottom = map(
547 float, (left, top, right, bottom))
549 if w is not None:
550 left = right = float(w)
552 if h is not None:
553 top = bottom = float(h)
555 if all is not None:
556 left = right = top = bottom = float(all)
558 ufac = units_dict.get(units, units) / inch
560 left *= ufac
561 right *= ufac
562 top *= ufac
563 bottom *= ufac
565 width, height = fig.get_size_inches()
567 rel_wspace = None
568 rel_hspace = None
570 if wspace is not None:
571 wspace *= ufac
572 if nw is None:
573 raise ValueError('wspace must be given in combination with nw')
575 wsub = (width - left - right - (nw-1) * wspace) / nw
576 rel_wspace = wspace / wsub
577 else:
578 wsub = width - left - right
580 if hspace is not None:
581 hspace *= ufac
582 if nh is None:
583 raise ValueError('hspace must be given in combination with nh')
585 hsub = (height - top - bottom - (nh-1) * hspace) / nh
586 rel_hspace = hspace / hsub
587 else:
588 hsub = height - top - bottom
590 fig.subplots_adjust(
591 left=left/width,
592 right=1.0 - right/width,
593 bottom=bottom/height,
594 top=1.0 - top/height,
595 wspace=rel_wspace,
596 hspace=rel_hspace)
598 def labelpos(axes, xpos=0., ypos=0.):
599 xpos *= ufac
600 ypos *= ufac
601 axes.get_yaxis().set_label_coords(-((left-xpos) / wsub), 0.5)
602 axes.get_xaxis().set_label_coords(0.5, -((bottom-ypos) / hsub))
604 return labelpos
607mpl_margins.__doc__ %= _doc_units
610def mpl_labelspace(axes):
611 '''
612 Add some extra padding between label and ax annotations.
613 '''
615 xa = axes.get_xaxis()
616 ya = axes.get_yaxis()
617 for attr in ('labelpad', 'LABELPAD'):
618 if hasattr(xa, attr):
619 setattr(xa, attr, xa.get_label().get_fontsize())
620 setattr(ya, attr, ya.get_label().get_fontsize())
621 break
624def mpl_papersize(paper, orientation='landscape'):
625 '''
626 Get paper size in inch from string.
628 Returns argument suitable to be passed to the ``figsize`` argument of
629 :py:func:`pyplot.figure`.
631 :param paper: string selecting paper size. Choices: %s
632 :param orientation: ``'landscape'``, or ``'portrait'``
634 :returns: ``(width, height)``
635 '''
637 return papersize(paper, orientation=orientation, units='inch')
640mpl_papersize.__doc__ %= _doc_papersizes
643class InvalidColorDef(ValueError):
644 pass
647def mpl_graph_color(i):
648 return to01(graph_colors[i % len(graph_colors)])
651def mpl_color(x):
652 '''
653 Convert string into color float tuple ranged 0-1 for use with Matplotlib.
655 Accepts tango color names, matplotlib color names, and slash-separated
656 strings. In the latter case, if values are larger than 1., the color
657 is interpreted as 0-255 ranged. Single-valued (grayscale), three-valued
658 (color) and four-valued (color with alpha) are accepted. An
659 :py:exc:`InvalidColorDef` exception is raised when the convertion fails.
660 '''
662 import matplotlib.colors
664 if x in tango_colors:
665 return to01(tango_colors[x])
667 s = x.split('/')
668 if len(s) in (1, 3, 4):
669 try:
670 vals = list(map(float, s))
671 if all(0. <= v <= 1. for v in vals):
672 return vals
674 elif all(0. <= v <= 255. for v in vals):
675 return to01(vals)
677 except ValueError:
678 try:
679 return matplotlib.colors.colorConverter.to_rgba(x)
680 except Exception:
681 pass
683 raise InvalidColorDef('invalid color definition: %s' % x)
686hours = 3600.
687days = hours*24
688approx_months = days*30.5
689approx_years = days*365
692nice_time_tinc_inc_approx_units = {
693 'seconds': 1,
694 'months': approx_months,
695 'years': approx_years}
698def nice_time_tick_inc(tinc_approx):
700 if tinc_approx >= approx_years:
701 return max(1.0, nice_value(tinc_approx / approx_years)), 'years'
703 elif tinc_approx >= approx_months:
704 nice = [1, 2, 3, 6]
705 for tinc in nice:
706 if tinc*approx_months >= tinc_approx or tinc == nice[-1]:
707 return tinc, 'months'
709 elif tinc_approx > days:
710 return nice_value(tinc_approx / days) * days, 'seconds'
712 elif tinc_approx >= 1.0:
713 nice = [
714 1., 2., 5., 10., 20., 30., 60., 120., 300., 600., 1200., 1800.,
715 1*hours, 2*hours, 3*hours, 6*hours, 12*hours, days, 2*days]
717 for tinc in nice:
718 if tinc >= tinc_approx or tinc == nice[-1]:
719 return tinc, 'seconds'
721 else:
722 return nice_value(tinc_approx), 'seconds'
725def nice_time_tick_inc_approx_secs(tinc_approx):
726 v, unit = nice_time_tick_inc(tinc_approx)
727 return v * nice_time_tinc_inc_approx_units[unit]
730def time_tick_labels(tmin, tmax, tinc, tinc_unit):
732 if tinc_unit == 'years':
733 tt = time.gmtime(int(tmin))
734 tmin_year = tt[0]
735 if tt[1:6] != (1, 1, 0, 0, 0):
736 tmin_year += 1
738 tmax_year = time.gmtime(int(tmax))[0]
740 tick_times_year = arange2(
741 math.ceil(tmin_year/tinc)*tinc,
742 math.floor(tmax_year/tinc)*tinc,
743 tinc).astype(int)
745 times = [
746 to_time_float(calendar.timegm((year, 1, 1, 0, 0, 0)))
747 for year in tick_times_year]
749 labels = ['%04i' % year for year in tick_times_year]
751 elif tinc_unit == 'months':
752 tt = time.gmtime(int(tmin))
753 tmin_ym = tt[0] * 12 + (tt[1] - 1)
754 if tt[2:6] != (1, 0, 0, 0):
755 tmin_ym += 1
757 tt = time.gmtime(int(tmax))
758 tmax_ym = tt[0] * 12 + (tt[1] - 1)
760 tick_times_ym = arange2(
761 math.ceil(tmin_ym/tinc)*tinc,
762 math.floor(tmax_ym/tinc)*tinc, tinc).astype(int)
764 times = [
765 to_time_float(calendar.timegm((ym // 12, ym % 12 + 1, 1, 0, 0, 0)))
766 for ym in tick_times_ym]
768 labels = [
769 '%04i-%02i' % (ym // 12, ym % 12 + 1) for ym in tick_times_ym]
771 elif tinc_unit == 'seconds':
772 imin = int(num.ceil(tmin/tinc))
773 imax = int(num.floor(tmax/tinc))
774 nticks = imax - imin + 1
775 tmin_ticks = imin * tinc
776 times = tmin_ticks + num.arange(nticks) * tinc
777 times = times.tolist()
779 if tinc < 1e-6:
780 fmt = '%Y-%m-%d.%H:%M:%S.9FRAC'
781 elif tinc < 1e-3:
782 fmt = '%Y-%m-%d.%H:%M:%S.6FRAC'
783 elif tinc < 1.0:
784 fmt = '%Y-%m-%d.%H:%M:%S.3FRAC'
785 elif tinc < 60:
786 fmt = '%Y-%m-%d.%H:%M:%S'
787 elif tinc < 3600.*24:
788 fmt = '%Y-%m-%d.%H:%M'
789 else:
790 fmt = '%Y-%m-%d'
792 nwords = len(fmt.split('.'))
794 labels = [time_to_str(t, format=fmt) for t in times]
795 labels_weeded = []
796 have_ymd = have_hms = False
797 ymd = hms = ''
798 for ilab, lab in reversed(list(enumerate(labels))):
799 words = lab.split('.')
800 if nwords > 2:
801 words[2] = '.' + words[2]
802 if float(words[2]) == 0.0: # or (ilab == 0 and not have_hms):
803 have_hms = True
804 else:
805 hms = words[1]
806 words[1] = ''
807 else:
808 have_hms = True
810 if nwords > 1:
811 if words[1] in ('00:00', '00:00:00'): # or (ilab == 0 and not have_ymd): # noqa
812 have_ymd = True
813 else:
814 ymd = words[0]
815 words[0] = ''
816 else:
817 have_ymd = True
819 labels_weeded.append('\n'.join(reversed(words)))
821 labels = list(reversed(labels_weeded))
822 if (not have_ymd or not have_hms) and (hms or ymd):
823 words = ([''] if nwords > 2 else []) + [
824 hms if not have_hms else '',
825 ymd if not have_ymd else '']
827 labels[0:0] = ['\n'.join(words)]
828 times[0:0] = [tmin]
830 return times, labels
833def mpl_time_axis(axes, approx_ticks=5.):
835 '''
836 Configure x axis of a matplotlib axes object for interactive time display.
838 :param axes: Axes to be configured.
839 :type axes: :py:class:`matplotlib.axes.Axes`
841 :param approx_ticks: Approximate number of ticks to create.
842 :type approx_ticks: float
844 This function tries to use nice tick increments and tick labels for time
845 ranges from microseconds to years, similar to how this is handled in
846 Snuffler.
847 '''
849 from matplotlib.ticker import Locator, Formatter
851 class labeled_float(float):
852 pass
854 class TimeLocator(Locator):
856 def __init__(self, approx_ticks=5.):
857 self._approx_ticks = approx_ticks
858 Locator.__init__(self)
860 def __call__(self):
861 vmin, vmax = self.axis.get_view_interval()
862 return self.tick_values(vmin, vmax)
864 def tick_values(self, vmin, vmax):
865 if vmax < vmin:
866 vmin, vmax = vmax, vmin
868 if vmin == vmax:
869 return []
871 tinc_approx = (vmax - vmin) / self._approx_ticks
872 tinc, tinc_unit = nice_time_tick_inc(tinc_approx)
873 times, labels = time_tick_labels(vmin, vmax, tinc, tinc_unit)
874 ftimes = []
875 for t, label in zip(times, labels):
876 ftime = labeled_float(t)
877 ftime._mpl_label = label
878 ftimes.append(ftime)
880 return self.raise_if_exceeds(ftimes)
882 class TimeFormatter(Formatter):
884 def __call__(self, x, pos=None):
885 if isinstance(x, labeled_float):
886 return x._mpl_label
887 else:
888 return time_to_str(x, format='%Y-%m-%d %H:%M:%S.6FRAC')
890 axes.xaxis.set_major_locator(TimeLocator(approx_ticks=approx_ticks))
891 axes.xaxis.set_major_formatter(TimeFormatter())