1# http://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

4# ---|P------/S----------~Lg---------- 

5 

6''' 

7Utility functions and defintions for a common plot style throughout Pyrocko. 

8 

9Functions with name prefix ``mpl_`` are Matplotlib specific. All others should 

10be toolkit-agnostic. 

11 

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):: 

16 

17 from matplotlib import pyplot as plt 

18 

19 from pyrocko.plot import mpl_init, mpl_margins, mpl_papersize 

20 # from pyrocko.plot import mpl_labelspace 

21 

22 fontsize = 9. # in points 

23 

24 # set some Pyrocko style defaults 

25 mpl_init(fontsize=fontsize) 

26 

27 fig = plt.figure(figsize=mpl_papersize('a4', 'landscape')) 

28 

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) 

32 

33 axes = fig.add_subplot(1, 1, 1) 

34 

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 

38 

39 axes.plot([0, 1], [0, 9]) 

40 

41 axes.set_xlabel('Time [s]') 

42 axes.set_ylabel('Amplitude [m]') 

43 

44 fig.savefig('plot_skeleton.pdf') 

45 

46 plt.show() 

47 

48''' 

49from __future__ import absolute_import 

50 

51import math 

52import random 

53import time 

54import calendar 

55import numpy as num 

56 

57from pyrocko.util import parse_md, time_to_str, arange2, to_time_float 

58from pyrocko.guts import StringChoice, Float, Int, Bool, Tuple, Object 

59 

60 

61try: 

62 newstr = unicode 

63except NameError: 

64 newstr = str 

65 

66 

67__doc__ += parse_md(__file__) 

68 

69 

70guts_prefix = 'pf' 

71 

72point = 1. 

73inch = 72. 

74cm = 28.3465 

75 

76units_dict = { 

77 'point': point, 

78 'inch': inch, 

79 'cm': cm, 

80} 

81 

82_doc_units = "``'points'``, ``'inch'``, or ``'cm'``" 

83 

84 

85def apply_units(x, units): 

86 if isinstance(units, (str, newstr)): 

87 units = units_dict[units] 

88 

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) 

96 

97 

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)} 

126 

127 

128graph_colors = [ 

129 tango_colors[_x] for _x in ( 

130 'scarletred2', 

131 'skyblue3', 

132 'chameleon3', 

133 'orange2', 

134 'plum2', 

135 'chocolate2', 

136 'butter2')] 

137 

138 

139def color(x=None): 

140 if x is None: 

141 return tuple([random.randint(0, 255) for _x in 'rgb']) 

142 

143 if isinstance(x, int): 

144 if 0 <= x < len(graph_colors): 

145 return graph_colors[x] 

146 else: 

147 return (0, 0, 0) 

148 

149 elif isinstance(x, (str, newstr)): 

150 if x in tango_colors: 

151 return tango_colors[x] 

152 

153 elif isinstance(x, tuple): 

154 return x 

155 

156 assert False, "Don't know what to do with this color definition: %s" % x 

157 

158 

159def to01(c): 

160 return tuple(x/255. for x in c) 

161 

162 

163def nice_value(x): 

164 ''' 

165 Round x to nice value. 

166 ''' 

167 

168 if x == 0.0: 

169 return 0.0 

170 

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 

182 

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 

191 

192 return sign * 0.1 * exp 

193 

194 

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.))] 

225 

226papersizes = dict(_papersizes_list) 

227 

228_doc_papersizes = ', '.join("``'%s'``" % k for (k, _) in _papersizes_list) 

229 

230 

231def papersize(paper, orientation='landscape', units='point'): 

232 

233 ''' 

234 Get paper size from string. 

235 

236 :param paper: string selecting paper size. Choices: %s 

237 :param orientation: ``'landscape'``, or ``'portrait'`` 

238 :param units: Units to be returned. Choices: %s 

239 

240 :returns: ``(width, height)`` 

241 ''' 

242 

243 assert orientation in ('landscape', 'portrait') 

244 

245 w, h = papersizes[paper.lower()] 

246 if orientation == 'landscape': 

247 w, h = h, w 

248 

249 return apply_units((w, h), units) 

250 

251 

252papersize.__doc__ %= (_doc_papersizes, _doc_units) 

253 

254 

255class AutoScaleMode(StringChoice): 

256 ''' 

257 Mode of operation for auto-scaling. 

258 

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'] 

276 

277 

278class AutoScaler(Object): 

279 

280 ''' 

281 Tunable 1D autoscaling based on data range. 

282 

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. 

286 

287 The autoscaling process is guided by the following public attributes: 

288 ''' 

289 

290 approx_ticks = Float.T( 

291 default=7.0, 

292 help='Approximate number of increment steps (tickmarks) to generate.') 

293 

294 mode = AutoScaleMode.T( 

295 default='auto', 

296 help='''Mode of operation for auto-scaling.''') 

297 

298 exp = Int.T( 

299 optional=True, 

300 help='If defined, override automatically determined exponent for ' 

301 'notation by the given value.') 

302 

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\'``.') 

307 

308 inc = Float.T( 

309 optional=True, 

310 help='If defined, override automatically determined tick increment by ' 

311 'the given value.') 

312 

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\'``.') 

320 

321 exp_factor = Int.T( 

322 default=3, 

323 help='Exponent of notation is chosen to be a multiple of this value.') 

324 

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.') 

330 

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)): 

341 

342 ''' 

343 Create new AutoScaler instance. 

344 

345 The parameters are described in the AutoScaler documentation. 

346 ''' 

347 

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) 

358 

359 def make_scale(self, data_range, override_mode=None): 

360 

361 ''' 

362 Get nice minimum, maximum and increment for given data range. 

363 

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 ''' 

370 

371 data_min = min(data_range) 

372 data_max = max(data_range) 

373 

374 is_reverse = (data_range[0] > data_range[1]) 

375 

376 a = self.mode 

377 if self.mode == 'auto': 

378 a = self.guess_autoscale_mode(data_min, data_max) 

379 

380 if override_mode is not None: 

381 a = override_mode 

382 

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 

404 

405 nmi = mi 

406 if (mi != 0. or a == 'min-max') and a != 'off': 

407 nmi = mi - self.space*(ma-mi) 

408 

409 nma = ma 

410 if (ma != 0. or a == 'min-max') and a != 'off': 

411 nma = ma + self.space*(ma-mi) 

412 

413 mi, ma = nmi, nma 

414 

415 if mi == ma and a != 'off': 

416 mi -= 1.0 

417 ma += 1.0 

418 

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.) 

427 

428 if inc == 0.0: 

429 inc = 1.0 

430 

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) 

435 

436 if is_reverse: 

437 return ma, mi, -inc 

438 else: 

439 return mi, ma, inc 

440 

441 def make_exp(self, x): 

442 ''' 

443 Get nice exponent for notation of ``x``. 

444 

445 For ax annotations, give tick increment as ``x``. 

446 ''' 

447 

448 if self.exp is not None: 

449 return self.exp 

450 

451 x = abs(x) 

452 if x == 0.0: 

453 return 0 

454 

455 if 10**self.no_exp_interval[0] <= x <= 10**self.no_exp_interval[1]: 

456 return 0 

457 

458 return math.floor(math.log10(x)/self.exp_factor)*self.exp_factor 

459 

460 def guess_autoscale_mode(self, data_min, data_max): 

461 ''' 

462 Guess mode of operation, based on data range. 

463 

464 Used to map ``'auto'`` mode to ``'0-max'``, ``'min-0'``, ``'min-max'`` 

465 or ``'symmetric'``. 

466 ''' 

467 

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 

486 

487 

488# below, some convenience functions for matplotlib plotting 

489 

490def mpl_init(fontsize=10): 

491 ''' 

492 Initialize Matplotlib rc parameters Pyrocko style. 

493 

494 Returns the matplotlib.pyplot module for convenience. 

495 ''' 

496 

497 import matplotlib 

498 

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') 

508 

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 

519 

520 from matplotlib import pyplot as plt 

521 return plt 

522 

523 

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'): 

532 

533 ''' 

534 Adjust Matplotlib subplot params with absolute values in user units. 

535 

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. 

541 

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 ''' 

552 

553 left, top, right, bottom = map( 

554 float, (left, top, right, bottom)) 

555 

556 if w is not None: 

557 left = right = float(w) 

558 

559 if h is not None: 

560 top = bottom = float(h) 

561 

562 if all is not None: 

563 left = right = top = bottom = float(all) 

564 

565 ufac = units_dict.get(units, units) / inch 

566 

567 left *= ufac 

568 right *= ufac 

569 top *= ufac 

570 bottom *= ufac 

571 

572 width, height = fig.get_size_inches() 

573 

574 rel_wspace = None 

575 rel_hspace = None 

576 

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') 

581 

582 wsub = (width - left - right - (nw-1) * wspace) / nw 

583 rel_wspace = wspace / wsub 

584 else: 

585 wsub = width - left - right 

586 

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') 

591 

592 hsub = (height - top - bottom - (nh-1) * hspace) / nh 

593 rel_hspace = hspace / hsub 

594 else: 

595 hsub = height - top - bottom 

596 

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) 

604 

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)) 

610 

611 return labelpos 

612 

613 

614mpl_margins.__doc__ %= _doc_units 

615 

616 

617def mpl_labelspace(axes): 

618 ''' 

619 Add some extra padding between label and ax annotations. 

620 ''' 

621 

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 

629 

630 

631def mpl_papersize(paper, orientation='landscape'): 

632 ''' 

633 Get paper size in inch from string. 

634 

635 Returns argument suitable to be passed to the ``figsize`` argument of 

636 :py:func:`pyplot.figure`. 

637 

638 :param paper: string selecting paper size. Choices: %s 

639 :param orientation: ``'landscape'``, or ``'portrait'`` 

640 

641 :returns: ``(width, height)`` 

642 ''' 

643 

644 return papersize(paper, orientation=orientation, units='inch') 

645 

646 

647mpl_papersize.__doc__ %= _doc_papersizes 

648 

649 

650class InvalidColorDef(ValueError): 

651 pass 

652 

653 

654def mpl_graph_color(i): 

655 return to01(graph_colors[i % len(graph_colors)]) 

656 

657 

658def mpl_color(x): 

659 ''' 

660 Convert string into color float tuple ranged 0-1 for use with Matplotlib. 

661 

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 ''' 

668 

669 import matplotlib.colors 

670 

671 if x in tango_colors: 

672 return to01(tango_colors[x]) 

673 

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 

680 

681 elif all(0. <= v <= 255. for v in vals): 

682 return to01(vals) 

683 

684 except ValueError: 

685 try: 

686 return matplotlib.colors.colorConverter.to_rgba(x) 

687 except Exception: 

688 pass 

689 

690 raise InvalidColorDef('invalid color definition: %s' % x) 

691 

692 

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 

698 

699 if tinc_approx >= approx_years: 

700 return max(1.0, nice_value(tinc_approx / approx_years)), 'years' 

701 

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' 

707 

708 elif tinc_approx > days: 

709 return nice_value(tinc_approx / days) * days, 'seconds' 

710 

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] 

715 

716 for tinc in nice: 

717 if tinc >= tinc_approx or tinc == nice[-1]: 

718 return tinc, 'seconds' 

719 

720 else: 

721 return nice_value(tinc_approx), 'seconds' 

722 

723 

724def time_tick_labels(tmin, tmax, tinc, tinc_unit): 

725 

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 

731 

732 tmax_year = time.gmtime(int(tmax))[0] 

733 

734 tick_times_year = arange2( 

735 math.ceil(tmin_year/tinc)*tinc, 

736 math.floor(tmax_year/tinc)*tinc, 

737 tinc).astype(int) 

738 

739 times = [ 

740 to_time_float(calendar.timegm((year, 1, 1, 0, 0, 0))) 

741 for year in tick_times_year] 

742 

743 labels = ['%04i' % year for year in tick_times_year] 

744 

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 

750 

751 tt = time.gmtime(int(tmax)) 

752 tmax_ym = tt[0] * 12 + (tt[1] - 1) 

753 

754 tick_times_ym = arange2( 

755 math.ceil(tmin_ym/tinc)*tinc, 

756 math.floor(tmax_ym/tinc)*tinc, tinc).astype(int) 

757 

758 times = [ 

759 to_time_float(calendar.timegm((ym // 12, ym % 12 + 1, 1, 0, 0, 0))) 

760 for ym in tick_times_ym] 

761 

762 labels = [ 

763 '%04i-%02i' % (ym // 12, ym % 12 + 1) for ym in tick_times_ym] 

764 

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() 

772 

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' 

785 

786 nwords = len(fmt.split('.')) 

787 

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 

803 

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 

812 

813 labels_weeded.append('\n'.join(reversed(words))) 

814 

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 ''] 

820 

821 labels[0:0] = ['\n'.join(words)] 

822 times[0:0] = [tmin] 

823 

824 return times, labels 

825 

826 

827def mpl_time_axis(axes, approx_ticks=5.): 

828 

829 ''' 

830 Configure x axis of a matplotlib axes object for interactive time display. 

831 

832 :param axes: Axes to be configured. 

833 :type axes: :py:class:`matplotlib.axes.Axes` 

834 

835 :param approx_ticks: Approximate number of ticks to create. 

836 :type approx_ticks: float 

837 

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 ''' 

842 

843 from matplotlib.ticker import Locator, Formatter 

844 

845 class labeled_float(float): 

846 pass 

847 

848 class TimeLocator(Locator): 

849 

850 def __init__(self, approx_ticks=5.): 

851 self._approx_ticks = approx_ticks 

852 Locator.__init__(self) 

853 

854 def __call__(self): 

855 vmin, vmax = self.axis.get_view_interval() 

856 return self.tick_values(vmin, vmax) 

857 

858 def tick_values(self, vmin, vmax): 

859 if vmax < vmin: 

860 vmin, vmax = vmax, vmin 

861 

862 if vmin == vmax: 

863 return [] 

864 

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) 

873 

874 return self.raise_if_exceeds(ftimes) 

875 

876 class TimeFormatter(Formatter): 

877 

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') 

883 

884 axes.xaxis.set_major_locator(TimeLocator(approx_ticks=approx_ticks)) 

885 axes.xaxis.set_major_formatter(TimeFormatter())