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

49 

50import math 

51import random 

52import time 

53import calendar 

54import numpy as num 

55 

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

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

58 

59 

60__doc__ += parse_md(__file__) 

61 

62 

63guts_prefix = 'pf' 

64 

65point = 1. 

66inch = 72. 

67cm = 28.3465 

68 

69units_dict = { 

70 'point': point, 

71 'inch': inch, 

72 'cm': cm, 

73} 

74 

75_doc_units = "``'point'``, ``'inch'``, or ``'cm'``" 

76 

77 

78def apply_units(x, units): 

79 if isinstance(units, str): 

80 units = units_dict[units] 

81 

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) 

89 

90 

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

119 

120 

121graph_colors = [ 

122 tango_colors[_x] for _x in ( 

123 'scarletred2', 

124 'skyblue3', 

125 'chameleon3', 

126 'orange2', 

127 'plum2', 

128 'chocolate2', 

129 'butter2')] 

130 

131 

132def color(x=None): 

133 if x is None: 

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

135 

136 if isinstance(x, int): 

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

138 return graph_colors[x] 

139 else: 

140 return (0, 0, 0) 

141 

142 elif isinstance(x, str): 

143 if x in tango_colors: 

144 return tango_colors[x] 

145 

146 elif isinstance(x, tuple): 

147 return x 

148 

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

150 

151 

152def to01(c): 

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

154 

155 

156def nice_value(x): 

157 ''' 

158 Round x to nice value. 

159 ''' 

160 

161 if x == 0.0: 

162 return 0.0 

163 

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 

175 

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 

184 

185 return sign * 0.1 * exp 

186 

187 

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

218 

219papersizes = dict(_papersizes_list) 

220 

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

222 

223 

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

225 

226 ''' 

227 Get paper size from string. 

228 

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

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

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

232 

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

234 ''' 

235 

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

237 

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

239 if orientation == 'landscape': 

240 w, h = h, w 

241 

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

243 

244 

245papersize.__doc__ %= (_doc_papersizes, _doc_units) 

246 

247 

248class AutoScaleMode(StringChoice): 

249 ''' 

250 Mode of operation for auto-scaling. 

251 

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

269 

270 

271class AutoScaler(Object): 

272 

273 ''' 

274 Tunable 1D autoscaling based on data range. 

275 

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. 

279 

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

281 ''' 

282 

283 approx_ticks = Float.T( 

284 default=7.0, 

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

286 

287 mode = AutoScaleMode.T( 

288 default='auto', 

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

290 

291 exp = Int.T( 

292 optional=True, 

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

294 'notation by the given value.') 

295 

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

300 

301 inc = Float.T( 

302 optional=True, 

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

304 'the given value.') 

305 

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

313 

314 exp_factor = Int.T( 

315 default=3, 

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

317 

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

323 

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

334 

335 ''' 

336 Create new AutoScaler instance. 

337 

338 The parameters are described in the AutoScaler documentation. 

339 ''' 

340 

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) 

351 

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

353 

354 ''' 

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

356 

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

363 

364 data_min = min(data_range) 

365 data_max = max(data_range) 

366 

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

368 

369 a = self.mode 

370 if self.mode == 'auto': 

371 a = self.guess_autoscale_mode(data_min, data_max) 

372 

373 if override_mode is not None: 

374 a = override_mode 

375 

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 

397 

398 nmi = mi 

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

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

401 

402 nma = ma 

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

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

405 

406 mi, ma = nmi, nma 

407 

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

409 mi -= 1.0 

410 ma += 1.0 

411 

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

420 

421 if inc == 0.0: 

422 inc = 1.0 

423 

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) 

428 

429 if is_reverse: 

430 return ma, mi, -inc 

431 else: 

432 return mi, ma, inc 

433 

434 def make_exp(self, x): 

435 ''' 

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

437 

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

439 ''' 

440 

441 if self.exp is not None: 

442 return self.exp 

443 

444 x = abs(x) 

445 if x == 0.0: 

446 return 0 

447 

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

449 return 0 

450 

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

452 

453 def guess_autoscale_mode(self, data_min, data_max): 

454 ''' 

455 Guess mode of operation, based on data range. 

456 

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

458 or ``'symmetric'``. 

459 ''' 

460 

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 

479 

480 

481# below, some convenience functions for matplotlib plotting 

482 

483def mpl_init(fontsize=10): 

484 ''' 

485 Initialize Matplotlib rc parameters Pyrocko style. 

486 

487 Returns the matplotlib.pyplot module for convenience. 

488 ''' 

489 

490 import matplotlib 

491 

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

501 

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 

512 

513 from matplotlib import pyplot as plt 

514 return plt 

515 

516 

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

525 

526 ''' 

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

528 

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. 

534 

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

545 

546 left, top, right, bottom = map( 

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

548 

549 if w is not None: 

550 left = right = float(w) 

551 

552 if h is not None: 

553 top = bottom = float(h) 

554 

555 if all is not None: 

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

557 

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

559 

560 left *= ufac 

561 right *= ufac 

562 top *= ufac 

563 bottom *= ufac 

564 

565 width, height = fig.get_size_inches() 

566 

567 rel_wspace = None 

568 rel_hspace = None 

569 

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

574 

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

576 rel_wspace = wspace / wsub 

577 else: 

578 wsub = width - left - right 

579 

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

584 

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

586 rel_hspace = hspace / hsub 

587 else: 

588 hsub = height - top - bottom 

589 

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) 

597 

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

603 

604 return labelpos 

605 

606 

607mpl_margins.__doc__ %= _doc_units 

608 

609 

610def mpl_labelspace(axes): 

611 ''' 

612 Add some extra padding between label and ax annotations. 

613 ''' 

614 

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 

622 

623 

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

625 ''' 

626 Get paper size in inch from string. 

627 

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

629 :py:func:`pyplot.figure`. 

630 

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

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

633 

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

635 ''' 

636 

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

638 

639 

640mpl_papersize.__doc__ %= _doc_papersizes 

641 

642 

643class InvalidColorDef(ValueError): 

644 pass 

645 

646 

647def mpl_graph_color(i): 

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

649 

650 

651def mpl_color(x): 

652 ''' 

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

654 

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

661 

662 import matplotlib.colors 

663 

664 if x in tango_colors: 

665 return to01(tango_colors[x]) 

666 

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 

673 

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

675 return to01(vals) 

676 

677 except ValueError: 

678 try: 

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

680 except Exception: 

681 pass 

682 

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

684 

685 

686hours = 3600. 

687days = hours*24 

688approx_months = days*30.5 

689approx_years = days*365 

690 

691 

692nice_time_tinc_inc_approx_units = { 

693 'seconds': 1, 

694 'months': approx_months, 

695 'years': approx_years} 

696 

697 

698def nice_time_tick_inc(tinc_approx): 

699 

700 if tinc_approx >= approx_years: 

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

702 

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' 

708 

709 elif tinc_approx > days: 

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

711 

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] 

716 

717 for tinc in nice: 

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

719 return tinc, 'seconds' 

720 

721 else: 

722 return nice_value(tinc_approx), 'seconds' 

723 

724 

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] 

728 

729 

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

731 

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 

737 

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

739 

740 tick_times_year = arange2( 

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

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

743 tinc).astype(int) 

744 

745 times = [ 

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

747 for year in tick_times_year] 

748 

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

750 

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 

756 

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

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

759 

760 tick_times_ym = arange2( 

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

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

763 

764 times = [ 

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

766 for ym in tick_times_ym] 

767 

768 labels = [ 

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

770 

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

778 

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' 

791 

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

793 

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 

809 

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 

818 

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

820 

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

826 

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

828 times[0:0] = [tmin] 

829 

830 return times, labels 

831 

832 

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

834 

835 ''' 

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

837 

838 :param axes: Axes to be configured. 

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

840 

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

842 :type approx_ticks: float 

843 

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

848 

849 from matplotlib.ticker import Locator, Formatter 

850 

851 class labeled_float(float): 

852 pass 

853 

854 class TimeLocator(Locator): 

855 

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

857 self._approx_ticks = approx_ticks 

858 Locator.__init__(self) 

859 

860 def __call__(self): 

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

862 return self.tick_values(vmin, vmax) 

863 

864 def tick_values(self, vmin, vmax): 

865 if vmax < vmin: 

866 vmin, vmax = vmax, vmin 

867 

868 if vmin == vmax: 

869 return [] 

870 

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) 

879 

880 return self.raise_if_exceeds(ftimes) 

881 

882 class TimeFormatter(Formatter): 

883 

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

889 

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

891 axes.xaxis.set_major_formatter(TimeFormatter())