datetick 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
datetick/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ from datetick.datetick import datetick
2
+ from importlib.metadata import version, PackageNotFoundError
3
+
4
+ try:
5
+ __version__ = version('datetick')
6
+ except PackageNotFoundError:
7
+ __version__ = 'unknown'
datetick/datetick.py ADDED
@@ -0,0 +1,694 @@
1
+ def datetick(*args,
2
+ axes=None,
3
+ set_cb=True,
4
+ adjust_last_xlabel=False,
5
+ adjust_first_xlabel=False,
6
+ adjust_xrange=False,
7
+ adjust_yrange=False,
8
+ debug=False):
9
+ """
10
+ datetick('x') or datetick('y') formats the major and minor tick labels
11
+ of the current figure.
12
+
13
+ datetick('x', axes=ax) or datetick('y', axes=ax) formats the given axes `ax`.
14
+
15
+ Example:
16
+ --------
17
+ import datetime as dt
18
+ import matplotlib.pyplot as plt
19
+ from datetick import datetick
20
+ d1 = dt.datetime(1900, 1, 2)
21
+ d2 = dt.datetime.fromordinal(10 + dt.datetime.toordinal(d1))
22
+ x = [d1, d2]
23
+ y = [0.0,1.0]
24
+ plt.clf()
25
+ plt.plot(x, y)
26
+ datetick('x')
27
+ """
28
+
29
+ # Based on spacepy/plot/utils.py on 07/10/2017, but many additions.
30
+ # See also https://github.com/JouleCai/geospacelab/blob/master/geospacelab/visualization/mpl/axis_ticks.py
31
+
32
+ # TODO: Use _numsize() to determine if figure width and height
33
+ # will cause overlap when default number major tick labels is used.
34
+ # TODO: If time[0].day > 28, need to make first tick at time[0].day = 28
35
+ # as needed.
36
+ # TODO: If first data point has fractional seconds, the plot won't have
37
+ # a major x-label right below it. This is due to the fact that
38
+ # MicrosecondLocator() does not take a keyword argument of
39
+ # "bymicroseconds".
40
+ # TODO: Adjust lower and upper limits as in 366*8 span
41
+
42
+ # Get all kwargs passed using locals
43
+ kwargs = {k: v for k, v in locals().items() if k != 'args'}
44
+
45
+ import warnings
46
+ from datetime import datetime
47
+
48
+ import matplotlib.dates as mpld
49
+
50
+ if len(args) == 0:
51
+ dir = 'x'
52
+ else:
53
+ dir = args[0]
54
+
55
+ plt = _get_plt(debug=debug)
56
+
57
+ if 'axes' in kwargs:
58
+ axes = kwargs['axes']
59
+ fig = axes.figure
60
+ else:
61
+ axes = plt.gca()
62
+ fig = plt.gcf()
63
+
64
+ fig.canvas.draw()
65
+ bbox = axes.dataLim
66
+
67
+ if dir == 'x':
68
+ datamin = bbox.x0
69
+ datamax = bbox.x1
70
+ lim = axes.get_xlim()
71
+ ticks = axes.get_xticks()
72
+ else:
73
+ datamin = bbox.y0
74
+ datamax = bbox.y1
75
+ lim = axes.get_ylim()
76
+ ticks = axes.get_yticks()
77
+
78
+ try:
79
+ mpld.num2date(lim[0])
80
+ except:
81
+ msg = 'Lower axis limit of %f is not a valid Matplotlib datenum' % lim[0]
82
+ raise ValueError(msg)
83
+ try:
84
+ mpld.num2date(lim[1])
85
+ except:
86
+ msg = 'Upper axis limit of %f is not a valid Matplotlib datenum' % lim[1]
87
+ raise ValueError(msg)
88
+
89
+ # If all values are NaN, datamin = np.inf and datamax = -np.inf.
90
+ msg = "is not a valid Matplotlib datenum. Are all data values NaN? Cannot use datetick()"
91
+ try:
92
+ mpld.num2date(datamin)
93
+ except:
94
+ warnings.warn(f'Minimum value of %f {msg}' % datamin)
95
+ return
96
+ try:
97
+ mpld.num2date(datamax)
98
+ except:
99
+ warnings.warn(f'Maximum value of %f {msg}' % datamax)
100
+ return
101
+
102
+ if datamin == datamax:
103
+ axes.set_xticks([mpld.num2date(datamin)])
104
+ xticklabel = datetime.strftime(mpld.num2date(datamin),'%Y-%m-%dT%H:%M:%S')
105
+ axes.set_xticklabels([xticklabel])
106
+ return
107
+
108
+ # Need to document why this was used. It creates
109
+ # problems if labels extend beyond the axis limits.
110
+ #tmin = np.min((lim[0], datamin))
111
+ #tmax = np.max((lim[1], datamax))
112
+ tmin = lim[0]
113
+ tmax = lim[1]
114
+
115
+ tspan = mpld.num2date((tmin, tmax))
116
+
117
+ deltaT = tspan[-1] - tspan[0]
118
+ if debug:
119
+ print("Total seconds: %s" % deltaT.total_seconds())
120
+
121
+ """
122
+ fmt1 is format of the tick labels
123
+
124
+ fmt2 contains additional information that is used for the first tick label
125
+ or when there is a major change. For example, if
126
+ fmt1 = %M:%S and fmt2 = %H,
127
+ the labels will have only minute and hour and the first tick will have a
128
+ label of %M:%S\n%H. If there is a change in hour somewhere on the axis,
129
+ that label will include the new hour.
130
+
131
+ Note that interval=... is specified even when it would seem to be redundant.
132
+ It is needed to workaround the bug discussed at stackoverflow.com/q/31072589
133
+ """
134
+
135
+ Mtick, mtick, fmt1, fmt2 = _locator(axes, deltaT, debug=debug)
136
+
137
+ if debug:
138
+ print(f'{dir} data min: {mpld.num2date(datamin)}')
139
+ print(f'Default {dir}lim[0]: {mpld.num2date(lim[0])}')
140
+ print(f'Default {dir}ticks[0]: {mpld.num2date(ticks[0])}')
141
+ print(f'{dir} data max: {mpld.num2date(datamax)}')
142
+ print(f'Default {dir}lim[-1]: {mpld.num2date(lim[-1])}')
143
+ print(f'Default {dir}ticks[-1]: {mpld.num2date(ticks[-1])}')
144
+ print(f'Default {dir}ticks:')
145
+ for i in range(0,len(ticks)):
146
+ print(f' {mpld.num2date(ticks[i])}')
147
+
148
+ if dir == 'x':
149
+ axes.xaxis.set_major_locator(Mtick)
150
+ axes.xaxis.set_minor_locator(mtick)
151
+ axes.xaxis.set_major_formatter(fmt1)
152
+ if adjust_xrange:
153
+ _adjust_range(dir, fig, axes, datamin, datamax, debug=debug)
154
+ fig.canvas.draw() # Render new labels so updated for next line
155
+ labels = [item.get_text() for item in axes.get_xticklabels()]
156
+ ticks = axes.get_xticks()
157
+ else:
158
+ axes.yaxis.set_major_locator(Mtick)
159
+ axes.yaxis.set_minor_locator(mtick)
160
+ axes.yaxis.set_major_formatter(fmt1)
161
+ if adjust_xrange:
162
+ _adjust_range(dir, fig, axes, datamin, datamax, debug=debug)
163
+ fig.canvas.draw() # Render new labels so updated for next line
164
+ labels = [item.get_text() for item in axes.get_yticklabels()]
165
+ ticks = axes.get_yticks()
166
+
167
+ if debug:
168
+ xl = axes.get_xlim()
169
+ print(f'New {dir}ticks:')
170
+ for i in range(0,len(ticks)):
171
+ note = ''
172
+ if ticks[i] < xl[0] or ticks[i] > xl[1]:
173
+ note = ' (will be clipped by mpl b/c outside of axis limits)'
174
+ print(f' {mpld.num2date(ticks[i])} {note}')
175
+
176
+ if len(labels) == 0:
177
+ if debug:
178
+ print('No labels.')
179
+ return
180
+
181
+ if debug:
182
+ print(f'fmt1 {dir}labels:')
183
+ for i in range(0,len(labels)):
184
+ print(f' {labels[i]}')
185
+
186
+ if fmt2 != '':
187
+ labels = _add_fmt2(fmt2, lim, ticks, deltaT, labels, dir, debug=debug)
188
+
189
+ if debug:
190
+ print(f'fmt1 + fmt2 {dir}labels:')
191
+ for i in range(0,len(labels)):
192
+ print(f' {labels[i].replace("\n", "\\n")}')
193
+
194
+ if dir == 'x':
195
+ # Without the set_xticks(), warning is generated:
196
+ # UserWarning: set_ticklabels() should only be used.
197
+ # Additional discussion: https://github.com/matplotlib/matplotlib/issues/18848
198
+ # The correct way to avoid the warning: https://stackoverflow.com/a/69126185
199
+ # Better: Create custom class:
200
+ # https://matplotlib.org/stable/gallery/ticks/date_index_formatter.html
201
+ axes.set_xticks(axes.get_xticks())
202
+ axes.set_xticklabels(labels)
203
+ _adjust_xlabels(axes,
204
+ adjust_first_xlabel=adjust_first_xlabel,
205
+ adjust_last_xlabel=adjust_last_xlabel,
206
+ debug=debug)
207
+
208
+ if dir == 'y':
209
+ axes.set_yticks(axes.get_yticks())
210
+ axes.set_yticks(axes.get_yticks())
211
+ axes.set_yticklabels(labels)
212
+
213
+ # Trigger update of ticks when limits change due to user interaction.
214
+ if 'set_cb':
215
+ def on_xlims_change(ax):
216
+ datetick('x', **{**kwargs, 'set_cb': False})
217
+
218
+ def on_ylims_change(ax):
219
+ datetick('y', **{**kwargs, 'set_cb': False})
220
+
221
+ if dir == 'x':
222
+ axes.callbacks.connect('xlim_changed', on_xlims_change)
223
+ else:
224
+ axes.callbacks.connect('ylim_changed', on_ylims_change)
225
+
226
+
227
+ def _adjust_range(dir, fig, axes, datamin, datamax, debug=False):
228
+ if dir == 'x':
229
+ ticks = axes.get_xticks()
230
+ else:
231
+ ticks = axes.get_yticks()
232
+ if len(ticks) >= 2:
233
+ fig.canvas.draw()
234
+ dt = ticks[1] - ticks[0]
235
+ pad = 0.05 * dt
236
+ first_candidates = ticks[ticks <= datamin]
237
+ last_candidates = ticks[ticks >= datamax]
238
+ first = first_candidates[-1] if len(first_candidates) > 0 else ticks[0] - dt
239
+ last = last_candidates[0] if len(last_candidates) > 0 else ticks[-1] + dt
240
+ if dir == 'x':
241
+ axes.set_xlim(first - pad, last + pad)
242
+ else:
243
+ axes.set_ylim(first - pad, last + pad)
244
+
245
+
246
+ def _add_fmt2(fmt2, lim, ticks, deltaT, labels, dir, debug=False):
247
+ from datetime import datetime
248
+ import matplotlib.dates as mpld
249
+
250
+ time = mpld.num2date(ticks)
251
+
252
+ nDays = deltaT.days
253
+ nSecs = deltaT.total_seconds()
254
+
255
+ first = 0
256
+ if ticks[0] < lim[0]:
257
+ if debug:
258
+ msg = 'First tick is less than lower axis limit. Applying fmt2 to second tick label.'
259
+ print(msg)
260
+ # Work-around for bug in Matplotlib where left-most tick is less than
261
+ # lower x-limit. Could more than one tick be less than lower x-limit?
262
+ first = 1
263
+
264
+ # Always apply fmt2 to first tick label
265
+ if debug:
266
+ print(f'Applying fmt2 to first tick label at {mpld.num2date(ticks[first])}.')
267
+ labels[first] = '%s\n%s' % (labels[first], datetime.strftime(time[first], fmt2))
268
+
269
+ for i in range(first+1, len(time)):
270
+ # First label will always have fmt1 applied.
271
+ # Modify labels after first under certain conditions.
272
+ modify = False
273
+
274
+ if time[i].year > time[i-1].year:
275
+ modify = True
276
+ if nDays < 60 and time[i].month > time[i-1].month:
277
+ modify = True
278
+ if nDays < 4 and time[i].day > time[i-1].day:
279
+ modify = True
280
+ if nSecs < 60*30 and time[i].hour > time[i-1].hour:
281
+ modify = True
282
+ if nSecs < 1 and time[i].minute > time[i-1].minute:
283
+ modify = True
284
+ if nSecs < 1 and time[i].second > time[i-1].second:
285
+ modify = True
286
+
287
+ if not modify:
288
+ continue
289
+
290
+ if i == first + 1 and dir == 'x':
291
+ # If first two major tick labels have fmt2 applied, the will
292
+ # likely run together. This keeps fmt2 label for second major
293
+ # tick.
294
+ if debug:
295
+ print(f'Removing fmt2 to first tick label at {mpld.num2date(ticks[first])} to avoid overlap with second label.')
296
+ print(f'Applying fmt2 to tick label at {mpld.num2date(ticks[i])}.')
297
+ labels[first] = labels[first].split('\n')[0]
298
+ labels[i] = '%s\n%s' % (labels[i], datetime.strftime(mpld.num2date(ticks[i]), fmt2))
299
+ else:
300
+ if debug:
301
+ print(f'Applying fmt2 to tick label at {mpld.num2date(ticks[i])}.')
302
+ labels[i] = '%s\n%s' % (labels[i], datetime.strftime(mpld.num2date(ticks[i]), fmt2))
303
+
304
+ return labels
305
+
306
+
307
+ def _get_plt(debug=False):
308
+ import warnings
309
+ import matplotlib
310
+ import matplotlib.rcsetup
311
+
312
+ # TODO: Determine earliest version of Matplotlib where the problems
313
+ # addressed here are fixed.
314
+ try: # Available in Matplotlib >= 3.9
315
+ if matplotlib.__version__.split(".") >= (3, 9):
316
+ import matplotlib.backends.backend_registry
317
+ backends = matplotlib.backends.backend_registry.list_builtin()
318
+ else:
319
+ backends = matplotlib.rcsetup.all_backends
320
+ except Exception:
321
+ # In earlier versions of Matplotlib (at least 3.3), the list of backends
322
+ # was not available and get_backend() did not return lower-case names.
323
+ backends = ['Qt5Agg', 'QT4Agg', 'GTKAgg', 'TKAgg', 'WXAgg']
324
+
325
+ if debug:
326
+ print(f'Matplotlib version: {matplotlib.__version__}')
327
+ print(f'Matplotlib backend: {matplotlib.get_backend()}')
328
+ print(f'Available backends: {backends}')
329
+
330
+ if matplotlib.get_backend() == 'MacOSX':
331
+ """
332
+ With MacOSX backend, draw() does not update the ticks. See warning at
333
+ https://matplotlib.org/3.3.0/tutorials/advanced/blitting.html
334
+ """
335
+ import sys
336
+ if debug:
337
+ print('Matplotlib backend is MacOSX. Switching backend globally to workaround draw() not updating ticks.')
338
+ if sys.version_info[0:2] < (3, 6):
339
+ # warnings.filterwarnings("ignore", '.*backend.*', category=UserWarning)
340
+ # the above should work and is better because more specific.
341
+ warnings.simplefilter("ignore", category=UserWarning)
342
+ for backend in backends:
343
+ cmd = f"matplotlib.use('{backend}', force=True)"
344
+ try:
345
+ if debug:
346
+ print(f"Trying {cmd}")
347
+ matplotlib.use(backend, force=True)
348
+ import matplotlib.pyplot as plt
349
+ if debug:
350
+ print(" Success.")
351
+ break
352
+ except:
353
+ if debug:
354
+ print(" Failure.")
355
+ continue
356
+ else:
357
+ try:
358
+ import matplotlib.pyplot as plt
359
+ except:
360
+ print('Failed: "import matplotlib.pyplot as plt". Switching backend globally.')
361
+ cmd = f"matplotlib.use('{backends}', force=True)"
362
+ for backend in backends:
363
+ try:
364
+ if debug:
365
+ print(f"Trying {cmd}")
366
+ matplotlib.use(backend, force=True)
367
+ import matplotlib.pyplot as plt
368
+ if debug:
369
+ print(" Success.")
370
+ break
371
+ except:
372
+ if debug:
373
+ print(" Failure.")
374
+ continue
375
+
376
+ return plt
377
+
378
+
379
+ def _locator(axes, deltaT, debug=False):
380
+ import matplotlib
381
+ import matplotlib.dates as mpld
382
+
383
+ def _millis(x, pos):
384
+ x = matplotlib.dates.num2date(x)
385
+ label = x.strftime('.%f')
386
+ label = label[0:3]
387
+ #label = label.rstrip(".")
388
+ return label
389
+
390
+ nHours = deltaT.days * 24.0 + deltaT.seconds/3600.0
391
+ if deltaT.total_seconds() < 0.1:
392
+ # < 0.1 second
393
+ Mtick = mpld.MicrosecondLocator(interval=10000)
394
+ mtick = mpld.MicrosecondLocator(interval=2000)
395
+ fmt1 = matplotlib.ticker.FuncFormatter(_millis)
396
+ fmt2 = '%H:%M:%S\n%Y-%m-%d'
397
+ if deltaT.total_seconds() < 0.5:
398
+ # < 0.5 seconds
399
+ # Locators don't locate at this resolution.
400
+ # Need to do this manually. See comment above.
401
+ Mtick = mpld.MicrosecondLocator(interval=50000)
402
+ mtick = mpld.MicrosecondLocator(interval=10000)
403
+ fmt1 = matplotlib.ticker.FuncFormatter(_millis)
404
+ fmt2 = '%H:%M:%S\n%Y-%m-%d'
405
+ if deltaT.total_seconds() < 1:
406
+ # < 1 second
407
+ # https://matplotlib.org/api/dates_api.html#matplotlib.dates.MicrosecondLocator
408
+ # MircosecondLocator() does not have a "bymicrosecond" option. If
409
+ # first point is not at zero microseconds, it won't be labeled.
410
+ Mtick = mpld.MicrosecondLocator(interval=100000)
411
+ mtick = mpld.MicrosecondLocator(interval=20000)
412
+ fmt1 = matplotlib.ticker.FuncFormatter(_millis)
413
+ #fmt1 = mpld.DateFormatter('%M:%S.%f')
414
+ fmt2 = '%H:%M:%S\n%Y-%m-%d'
415
+ elif deltaT.total_seconds() < 5:
416
+ # < 5 seconds
417
+ Mtick = mpld.SecondLocator(bysecond=list(range(0, 60, 1)) )
418
+ mtick = mpld.MicrosecondLocator(interval=200000)
419
+ fmt1 = mpld.DateFormatter('%M:%S')
420
+ fmt2 = '%Y-%m-%dT%H'
421
+ elif deltaT.total_seconds() < 10:
422
+ # < 10 seconds
423
+ Mtick = mpld.SecondLocator(bysecond=list(range(0, 60, 1)) )
424
+ mtick = mpld.MicrosecondLocator(interval=500000)
425
+ fmt1 = mpld.DateFormatter('%M:%S')
426
+ fmt2 = '%Y-%m-%dT%H'
427
+ elif deltaT.total_seconds() < 20:
428
+ # < 20 seconds
429
+ Mtick = mpld.SecondLocator(bysecond=list(range(0, 60, 2)) )
430
+ mtick = mpld.SecondLocator(bysecond=list(range(0, 60, 1)) )
431
+ fmt1 = mpld.DateFormatter('%M:%S')
432
+ fmt2 = '%Y-%m-%dT%H'
433
+ elif deltaT.total_seconds() < 30:
434
+ # < 30 seconds
435
+ Mtick = mpld.SecondLocator(bysecond=list(range(0, 60, 5)) )
436
+ mtick = mpld.SecondLocator(bysecond=list(range(0, 60, 1)) )
437
+ fmt1 = mpld.DateFormatter('%M:%S')
438
+ fmt2 = '%Y-%m-%dT%H'
439
+ elif deltaT.total_seconds() < 60:
440
+ # < 1 minute
441
+ Mtick = mpld.SecondLocator(bysecond=list(range(0, 60, 10)) )
442
+ mtick = mpld.SecondLocator(bysecond=list(range(0, 60, 2)) )
443
+ fmt1 = mpld.DateFormatter('%M:%S')
444
+ fmt2 = '%Y-%m-%dT%H'
445
+ elif deltaT.total_seconds() < 60*2:
446
+ # < 2 minutes
447
+ Mtick = mpld.SecondLocator(bysecond=list(range(0, 60, 20)) )
448
+ mtick = mpld.SecondLocator(bysecond=list(range(0, 60, 5)) )
449
+ fmt1 = mpld.DateFormatter('%M:%S')
450
+ fmt2 = '%Y-%m-%dT%H'
451
+ elif deltaT.total_seconds() < 60*3:
452
+ # < 3 minutes
453
+ Mtick = mpld.SecondLocator(bysecond=list(range(0, 60, 20)) )
454
+ mtick = mpld.SecondLocator(bysecond=list(range(0, 60, 5)) )
455
+ fmt1 = mpld.DateFormatter('%M:%S')
456
+ fmt2 = '%Y-%m-%dT%H'
457
+ elif deltaT.total_seconds() < 60*5:
458
+ # < 5 minutes
459
+ Mtick = mpld.SecondLocator(bysecond=list(range(0, 60, 30)) )
460
+ mtick = mpld.SecondLocator(bysecond=list(range(0, 60, 10)) )
461
+ fmt1 = mpld.DateFormatter('%M:%S')
462
+ fmt2 = '%Y-%m-%dT%H'
463
+ elif deltaT.total_seconds() < 60*10:
464
+ # < 10 minutes
465
+ Mtick = mpld.MinuteLocator(byminute=list(range(0, 60, 1)) )
466
+ mtick = mpld.SecondLocator(bysecond=list(range(0, 60, 15)) )
467
+ fmt1 = mpld.DateFormatter('%M:%S')
468
+ fmt2 = '%Y-%m-%dT%H'
469
+ elif deltaT.total_seconds() < 60*20:
470
+ # < 20 minutes
471
+ Mtick = mpld.MinuteLocator(byminute=list(range(0, 60, 2)) )
472
+ mtick = mpld.SecondLocator(bysecond=list(range(0, 60, 30)) )
473
+ fmt1 = mpld.DateFormatter('%M:%S')
474
+ fmt2 = '%Y-%m-%dT%H'
475
+ elif deltaT.total_seconds() < 60*30:
476
+ # < 30 minutes
477
+ Mtick = mpld.MinuteLocator(byminute=list(range(0, 60, 5)) )
478
+ mtick = mpld.MinuteLocator(byminute=list(range(0, 60, 1)) )
479
+ fmt1 = mpld.DateFormatter('%H:%M')
480
+ fmt2 = '%Y-%m-%d'
481
+ elif deltaT.total_seconds() < 60*60:
482
+ # < 60 minutes
483
+ Mtick = mpld.MinuteLocator(byminute=list(range(0, 60, 10)) )
484
+ mtick = mpld.MinuteLocator(byminute=list(range(0, 60, 2)) )
485
+ fmt1 = mpld.DateFormatter('%H:%M')
486
+ fmt2 = '%Y-%m-%d'
487
+ elif nHours < 2:
488
+ Mtick = mpld.MinuteLocator(byminute=list(range(0, 60, 15)) )
489
+ mtick = mpld.MinuteLocator(byminute=list(range(0, 60, 5)) )
490
+ fmt1 = mpld.DateFormatter('%H:%M')
491
+ fmt2 = '%Y-%m-%d'
492
+ elif nHours < 4:
493
+ Mtick = mpld.MinuteLocator(byminute=list(range(0, 60, 20)) )
494
+ mtick = mpld.MinuteLocator(byminute=list(range(0, 60, 5)) )
495
+ fmt1 = mpld.DateFormatter('%H:%M')
496
+ fmt2 = '%Y-%m-%d'
497
+ elif nHours < 6:
498
+ Mtick = mpld.HourLocator(byhour=list(range(0,24,1)) )
499
+ mtick = mpld.MinuteLocator(byminute=list(range(0, 60, 10)) )
500
+ fmt1 = mpld.DateFormatter('%H:%M')
501
+ fmt2 = '%Y-%m-%d'
502
+ elif nHours < 12:
503
+ Mtick = mpld.HourLocator(byhour=list(range(0,24,2)) )
504
+ mtick = mpld.MinuteLocator(byminute=list(range(0, 60, 30)) )
505
+ fmt1 = mpld.DateFormatter('%H:%M')
506
+ fmt2 = '%Y-%m-%d'
507
+ elif nHours < 24:
508
+ # < 1 day
509
+ Mtick = mpld.HourLocator(byhour=list(range(0, 24, 3)) )
510
+ mtick = mpld.HourLocator(byhour=list(range(0, 24, 1)) )
511
+ fmt1 = mpld.DateFormatter('%H')
512
+ fmt2 = '%Y-%m-%d'
513
+ elif nHours < 48:
514
+ # < 2 days
515
+ Mtick = mpld.HourLocator(byhour=list(range(0, 24, 4)) )
516
+ mtick = mpld.HourLocator(byhour=list(range(0, 24, 2)) )
517
+ fmt1 = mpld.DateFormatter('%H')
518
+ fmt2 = '%Y-%m-%d'
519
+ elif nHours < 72:
520
+ # < 3 days
521
+ Mtick = mpld.HourLocator(byhour = list(range(0, 24, 6)))
522
+ mtick = mpld.HourLocator(byhour = list(range(0, 24, 3)))
523
+ fmt1 = mpld.DateFormatter('%H')
524
+ fmt2 = '%Y-%m-%d'
525
+ elif nHours < 96:
526
+ # < 4 days
527
+ Mtick = mpld.HourLocator(byhour = list(range(0, 24, 12)))
528
+ mtick = mpld.HourLocator(byhour = list(range(0, 24, 3)))
529
+ fmt1 = mpld.DateFormatter('%H')
530
+ fmt2 = '%Y-%m-%d'
531
+ elif deltaT.days < 8:
532
+ Mtick = mpld.DayLocator(bymonthday=list(range(1, 32, 1)))
533
+ mtick = mpld.HourLocator(byhour=list(range(0, 24, 4)))
534
+ fmt1 = mpld.DateFormatter('%d')
535
+ fmt2 = '%Y-%m'
536
+ elif deltaT.days < 16:
537
+ Mtick = mpld.DayLocator(bymonthday=list(range(1, 32, 1)))
538
+ mtick = mpld.DayLocator(bymonthday=list(range(1, 32, 1)))
539
+ fmt1 = mpld.DateFormatter('%d')
540
+ fmt2 = '%Y-%m'
541
+ elif deltaT.days < 32:
542
+ Mtick = mpld.DayLocator(bymonthday=list(range(1, 32, 4)))
543
+ mtick = mpld.DayLocator(bymonthday=list(range(1, 32, 1)))
544
+ fmt1 = mpld.DateFormatter('%d')
545
+ fmt2 = '%Y-%m'
546
+ elif deltaT.days < 60:
547
+ Mtick = mpld.DayLocator(bymonthday=list(range(1, 32, 7)))
548
+ mtick = mpld.DayLocator(bymonthday=list(range(1, 32, 1)))
549
+ fmt1 = mpld.DateFormatter('%d')
550
+ fmt2 = '%Y-%m'
551
+ elif deltaT.days < 183:
552
+ Mtick = mpld.MonthLocator(bymonth=list(range(1, 13, 1)))
553
+ mtick = mpld.DayLocator(bymonthday=list(range(1, 32, 7)))
554
+ fmt1 = mpld.DateFormatter('%m')
555
+ fmt2 = '%Y'
556
+ elif deltaT.days < 367:
557
+ Mtick = mpld.MonthLocator(bymonth=list(range(1, 13, 1)))
558
+ mtick = mpld.MonthLocator(bymonth=list(range(1, 13, 1)))
559
+ fmt1 = mpld.DateFormatter('%m')
560
+ fmt2 = '%Y'
561
+ elif deltaT.days < 366*2:
562
+ Mtick = mpld.MonthLocator(bymonth=list(range(1, 13, 2)))
563
+ mtick = mpld.MonthLocator(bymonth=list(range(1, 13, 1)))
564
+ fmt1 = mpld.DateFormatter('%m')
565
+ fmt2 = '%Y'
566
+ elif deltaT.days < 366*8:
567
+ Mtick = mpld.YearLocator(1)
568
+ mtick = mpld.MonthLocator(bymonth=list(range(1, 13, 4)))
569
+ fmt1 = mpld.DateFormatter('%Y')
570
+ fmt2 = ''
571
+ elif deltaT.days < 366*15:
572
+ to = axes.lines[0].get_xdata()[0]
573
+ tf = axes.lines[0].get_xdata()[-1]
574
+ # Ideally would set byyear=list(range(to.year, tf.year,2)) but
575
+ # byyear is not a kwarg. Would need to something like
576
+ # https://stackoverflow.com/questions/48428729/matplotlib-dates-yearlocator-with-odd-intervals
577
+ Mtick = mpld.YearLocator(1)
578
+ mtick = mpld.YearLocator(1)
579
+ fmt1 = mpld.DateFormatter('%Y')
580
+ fmt2 = ''
581
+ if False:
582
+ xl = axes.get_xlim()
583
+ a = mpld.num2date(xl[0])
584
+ print(a)
585
+ import pdb;pdb.set_trace()
586
+ a = mpld.date2num(a.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))
587
+ b = mpld.num2date(xl[1])
588
+ b = mpld.date2num(b.replace(year=(b.year+1), month=1, day=1, hour=0, minute=0, second=0, microsecond=0))
589
+ axes.set_xlim([a, b])
590
+ elif deltaT.days < 366*40:
591
+ Mtick = mpld.YearLocator(5)
592
+ mtick = mpld.YearLocator(1)
593
+ fmt1 = mpld.DateFormatter('%Y')
594
+ fmt2 = ''
595
+ elif deltaT.days < 366*100:
596
+ Mtick = mpld.YearLocator(10)
597
+ mtick = mpld.YearLocator(2)
598
+ fmt1 = mpld.DateFormatter('%Y')
599
+ fmt2 = ''
600
+ elif deltaT.days < 366*200:
601
+ Mtick = mpld.YearLocator(20)
602
+ mtick = mpld.YearLocator(5)
603
+ fmt1 = mpld.DateFormatter('%Y')
604
+ fmt2 = ''
605
+ else:
606
+ Mtick = mpld.YearLocator(50)
607
+ mtick = mpld.YearLocator(10)
608
+ fmt1 = mpld.DateFormatter('%Y')
609
+ fmt2 = ''
610
+
611
+ return Mtick, mtick, fmt1, fmt2
612
+
613
+
614
+ def _adjust_xlabels(axes, adjust_first_xlabel=False, adjust_last_xlabel=False, debug=False):
615
+ xticklabels = axes.get_xticklabels()
616
+ lastlabel_text = xticklabels[-1].get_text()
617
+ firstlabel_text = xticklabels[0].get_text()
618
+ adjusted = False
619
+
620
+ if adjust_first_xlabel and '\n' in firstlabel_text:
621
+ if len(firstlabel_text.split('\n')[-1]) > 7:
622
+ if debug:
623
+ print(f'Adjusting first x-label: "{firstlabel_text.replace("\n", "\\n")}"')
624
+ adjusted = True
625
+ # If fmt1 in first label longer than YYYY-MM, set justification to left.
626
+ xticklabels[0].set_ha('left')
627
+ # Shift to right by 1/2 width of fmt1 in first label.
628
+ first_fmt1 = firstlabel_text.split('\n')[0]
629
+ offset = _numsize(axes, first_fmt1, +1, debug=debug)
630
+ xticklabels[0].set_transform(xticklabels[0].get_transform() - offset)
631
+
632
+ if adjust_last_xlabel and '\n' in lastlabel_text:
633
+ if len(lastlabel_text.split('\n')[-1]) > 7:
634
+ if debug:
635
+ print(f'Adjusting last x-label: "{lastlabel_text.replace("\n", "\\n")}"')
636
+ adjusted = True
637
+ # If fmt1 iin last label longer than YYYY-MM, set justification to right.
638
+ xticklabels[-1].set_ha('right')
639
+ # Shift to left by 1/2 width of fmt1 in last label.
640
+ last_fmt1 = lastlabel_text.split('\n')[0]
641
+ offset = _numsize(axes, last_fmt1, -1, debug=debug)
642
+ xticklabels[-1].set_transform(xticklabels[-1].get_transform() - offset)
643
+
644
+ if adjusted:
645
+ # Make all labels without newline slightly smaller than default fontsize
646
+ # so it is clearer that fmt2 applies to larger number.
647
+ if debug:
648
+ print('Adjusting font size of x-labels without newline')
649
+ for label in xticklabels:
650
+ if '\n' not in label.get_text():
651
+ label.set_fontsize(label.get_fontsize()*0.85)
652
+
653
+
654
+ def _numsize(ax, num, sign, debug=False):
655
+ '''Returns (width, height) of str(num) in pixels.
656
+
657
+ If ax is given, measures against that axes' renderer and DPI (correct).
658
+ Otherwise creates a temporary figure using rcParams figure.dpi.
659
+ '''
660
+ import re
661
+ import matplotlib
662
+ import matplotlib.figure
663
+ import matplotlib.backends.backend_agg
664
+
665
+ num = str(num)
666
+
667
+ #dpi = ax.figure.get_dpi() if ax is not None else matplotlib.rcParams['figure.dpi']
668
+ dpi = 72 # Why not use above dpi? On OS-X when dpi = 200 is returned, offset is wrong.
669
+ fig = matplotlib.figure.Figure(dpi=dpi)
670
+ canvas = matplotlib.backends.backend_agg.FigureCanvasAgg(fig)
671
+ ax_tmp = fig.add_subplot(111)
672
+ renderer = canvas.get_renderer()
673
+ fontsize = matplotlib.rcParams['xtick.labelsize']
674
+ t = ax_tmp.text(0.5, 0.5, num, fontsize=fontsize)
675
+
676
+ w, h, d = renderer.get_text_width_height_descent(num, t.get_fontproperties(), ismath=False)
677
+ dpi = ax.figure.get_dpi() if ax is not None else matplotlib.rcParams['figure.dpi']
678
+ delta = w/len(num)
679
+ # It seems like sign should not be needed, but does not work if
680
+ # + offset instead of - offset is used code that uses offset.
681
+ offset = matplotlib.transforms.ScaledTranslation(sign*delta/dpi, 0, ax.figure.dpi_scale_trans)
682
+
683
+ if debug:
684
+ print('_numsize():')
685
+ print(f' num = "{num}"')
686
+ print(f' fontsize = {fontsize}')
687
+ print(f' dpi = {dpi}')
688
+ print(f' width = {w}')
689
+ print(f' height = {h}')
690
+ print(f' descent = {d}')
691
+ print(f' delta = {delta}')
692
+ print(f' offset = {re.sub(r"\n\s+", "", str(offset))}')
693
+
694
+ return offset
@@ -0,0 +1,216 @@
1
+ Metadata-Version: 2.4
2
+ Name: datetick
3
+ Version: 0.9.0
4
+ Summary: Sensible date tick locator for matplotlib
5
+ Author: Brendan Gallagher
6
+ Author-email: Bob Weigel <rweigel@gmu.edu>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, http://pypi.python.org/pypi/datetick/
9
+ Requires-Python: >=3.7
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE.txt
12
+ Requires-Dist: matplotlib
13
+ Requires-Dist: numpy
14
+ Requires-Dist: python-dateutil
15
+ Dynamic: license-file
16
+
17
+ # datetick
18
+
19
+ Sensible numeric time and date tick labels for Matplotlib
20
+
21
+ # Motivation
22
+
23
+ Matplotlib's default time tick labels are often poor, and adjusting them requires using [locators and formatters](https://matplotlib.org/stable/api/ticker_api.html) on an ad-hoc basis. In addition, the interfaces for locators and formatters complex and non-intuitive and require study and experimentation.
24
+
25
+ `datetick()` contains logic for locators and formatters that apply to plots with arbitrary time ranges. One only needs to add the command `datetick()` after the usual Matplotlib `plt.plot(...)` command to have sensible and useable time tick labels.
26
+
27
+ # Usage
28
+
29
+ ```
30
+ import datetime as dt
31
+ import matplotlib.pyplot as plt
32
+ from datetick import datetick
33
+
34
+ dt1 = dt.datetime(2011, 1, 2)
35
+ dt2 = dt1 + dt.timedelta(days=1, hours=1, minutes=1)
36
+
37
+ plt.plot([dt1, dt2], [0.0,1.0])
38
+ datetick()
39
+ plt.show()
40
+ # or
41
+ # datetick('x') (use 'y' if y variable is a time)
42
+ # or
43
+ # datetick('x', axes=plt.gca())
44
+ # or
45
+ # fig, axes = plt.subplots(2)
46
+ # plt.plot([dt1, dt2], [0.0, 1.0])
47
+ # datetick('x', axes=axes[0])
48
+ ```
49
+
50
+ # Comparison to default Matplotlib
51
+
52
+ ![test/mpl-3.10.9/20010101000000.0-20010101000000.1.svg](test/mpl-3.10.9/20010101000000.0-20010101000000.1.svg)
53
+
54
+ ![test/mpl-3.10.9/20010101000000.0-20010101000000.2.svg](test/mpl-3.10.9/20010101000000.0-20010101000000.2.svg)
55
+
56
+ ![test/mpl-3.10.9/20010101000000.0-20010101000000.5.svg](test/mpl-3.10.9/20010101000000.0-20010101000000.5.svg)
57
+
58
+ ![test/mpl-3.10.9/20010101000000.01-20010101000000.99.svg](test/mpl-3.10.9/20010101000000.01-20010101000000.99.svg)
59
+
60
+ ![test/mpl-3.10.9/20010101000059.8-20010101000100.3.svg](test/mpl-3.10.9/20010101000059.8-20010101000100.3.svg)
61
+
62
+ ![test/mpl-3.10.9/20010101005959.8-20010101010003.0.svg](test/mpl-3.10.9/20010101005959.8-20010101010003.0.svg)
63
+
64
+ ![test/mpl-3.10.9/20010101235959.8-20010102000003.0.svg](test/mpl-3.10.9/20010101235959.8-20010102000003.0.svg)
65
+
66
+ ![test/mpl-3.10.9/20010101000000-20010101000001.svg](test/mpl-3.10.9/20010101000000-20010101000001.svg)
67
+
68
+ ![test/mpl-3.10.9/20010101000000-20010101000005.svg](test/mpl-3.10.9/20010101000000-20010101000005.svg)
69
+
70
+ ![test/mpl-3.10.9/20010101000059-20010101000103.svg](test/mpl-3.10.9/20010101000059-20010101000103.svg)
71
+
72
+ ![test/mpl-3.10.9/20010101005958-20010101010003.svg](test/mpl-3.10.9/20010101005958-20010101010003.svg)
73
+
74
+ ![test/mpl-3.10.9/20010101005959-20010101010004.svg](test/mpl-3.10.9/20010101005959-20010101010004.svg)
75
+
76
+ ![test/mpl-3.10.9/20010101235958-20010102000003.svg](test/mpl-3.10.9/20010101235958-20010102000003.svg)
77
+
78
+ ![test/mpl-3.10.9/20010101000000-20010101000007.svg](test/mpl-3.10.9/20010101000000-20010101000007.svg)
79
+
80
+ ![test/mpl-3.10.9/20010101000000-20010101000009.svg](test/mpl-3.10.9/20010101000000-20010101000009.svg)
81
+
82
+ ![test/mpl-3.10.9/20010101000001-20010101000006.svg](test/mpl-3.10.9/20010101000001-20010101000006.svg)
83
+
84
+ ![test/mpl-3.10.9/20010101235958-20010102000005.svg](test/mpl-3.10.9/20010101235958-20010102000005.svg)
85
+
86
+ ![test/mpl-3.10.9/20010101235959-20010102000005.svg](test/mpl-3.10.9/20010101235959-20010102000005.svg)
87
+
88
+ ![test/mpl-3.10.9/20010101005958-20010101010005.svg](test/mpl-3.10.9/20010101005958-20010101010005.svg)
89
+
90
+ ![test/mpl-3.10.9/20010101005958-20010101010007.svg](test/mpl-3.10.9/20010101005958-20010101010007.svg)
91
+
92
+ ![test/mpl-3.10.9/20010101005958-20010101010003_2.svg](test/mpl-3.10.9/20010101005958-20010101010003_2.svg)
93
+
94
+ ![test/mpl-3.10.9/20010101235956-20010102000010.svg](test/mpl-3.10.9/20010101235956-20010102000010.svg)
95
+
96
+ ![test/mpl-3.10.9/20010101235958-20010102000010.svg](test/mpl-3.10.9/20010101235958-20010102000010.svg)
97
+
98
+ ![test/mpl-3.10.9/20010101005956-20010101010010.svg](test/mpl-3.10.9/20010101005956-20010101010010.svg)
99
+
100
+ ![test/mpl-3.10.9/20010101005956-20010101010015.svg](test/mpl-3.10.9/20010101005956-20010101010015.svg)
101
+
102
+ ![test/mpl-3.10.9/20010101005956-20010101010006.svg](test/mpl-3.10.9/20010101005956-20010101010006.svg)
103
+
104
+ ![test/mpl-3.10.9/20010101000000-20010101000021.svg](test/mpl-3.10.9/20010101000000-20010101000021.svg)
105
+
106
+ ![test/mpl-3.10.9/20010101000000-20010101060000.svg](test/mpl-3.10.9/20010101000000-20010101060000.svg)
107
+
108
+ ![test/mpl-3.10.9/20010101000000-20010101090000.svg](test/mpl-3.10.9/20010101000000-20010101090000.svg)
109
+
110
+ ![test/mpl-3.10.9/20010101000000-20010101110000.svg](test/mpl-3.10.9/20010101000000-20010101110000.svg)
111
+
112
+ ![test/mpl-3.10.9/20010101000000-20010101110000_2.svg](test/mpl-3.10.9/20010101000000-20010101110000_2.svg)
113
+
114
+ ![test/mpl-3.10.9/20010101005958-20010101010028.svg](test/mpl-3.10.9/20010101005958-20010101010028.svg)
115
+
116
+ ![test/mpl-3.10.9/20010101000058-20010101000118.svg](test/mpl-3.10.9/20010101000058-20010101000118.svg)
117
+
118
+ ![test/mpl-3.10.9/20010101000000-20010101180000.svg](test/mpl-3.10.9/20010101000000-20010101180000.svg)
119
+
120
+ ![test/mpl-3.10.9/20010101000000-20010101120000.svg](test/mpl-3.10.9/20010101000000-20010101120000.svg)
121
+
122
+ ![test/mpl-3.10.9/20010101000000-20010101230000.svg](test/mpl-3.10.9/20010101000000-20010101230000.svg)
123
+
124
+ ![test/mpl-3.10.9/20010101020000-20010102010000.svg](test/mpl-3.10.9/20010101020000-20010102010000.svg)
125
+
126
+ ![test/mpl-3.10.9/20010101000000-20010102010000.svg](test/mpl-3.10.9/20010101000000-20010102010000.svg)
127
+
128
+ ![test/mpl-3.10.9/20010101000000-20010102230000.svg](test/mpl-3.10.9/20010101000000-20010102230000.svg)
129
+
130
+ ![test/mpl-3.10.9/20010101000000-20010102230000_2.svg](test/mpl-3.10.9/20010101000000-20010102230000_2.svg)
131
+
132
+ ![test/mpl-3.10.9/20010101000000-20010102230000_3.svg](test/mpl-3.10.9/20010101000000-20010102230000_3.svg)
133
+
134
+ ![test/mpl-3.10.9/20010101000000-20010102230000_4.svg](test/mpl-3.10.9/20010101000000-20010102230000_4.svg)
135
+
136
+ ![test/mpl-3.10.9/20010101060000-20010102070000.svg](test/mpl-3.10.9/20010101060000-20010102070000.svg)
137
+
138
+ ![test/mpl-3.10.9/20010101003000-20010102010000.svg](test/mpl-3.10.9/20010101003000-20010102010000.svg)
139
+
140
+ ![test/mpl-3.10.9/20010101000000-20010103000000.svg](test/mpl-3.10.9/20010101000000-20010103000000.svg)
141
+
142
+ ![test/mpl-3.10.9/20010101000000-20010103123000.svg](test/mpl-3.10.9/20010101000000-20010103123000.svg)
143
+
144
+ ![test/mpl-3.10.9/20010101000000-20010103235959.svg](test/mpl-3.10.9/20010101000000-20010103235959.svg)
145
+
146
+ ![test/mpl-3.10.9/20010101000000-20010105000000.svg](test/mpl-3.10.9/20010101000000-20010105000000.svg)
147
+
148
+ ![test/mpl-3.10.9/20010101000000-20010105000000_2.svg](test/mpl-3.10.9/20010101000000-20010105000000_2.svg)
149
+
150
+ ![test/mpl-3.10.9/20010130000000-20010201230000.svg](test/mpl-3.10.9/20010130000000-20010201230000.svg)
151
+
152
+ ![test/mpl-3.10.9/20011230000000-20020101230000.svg](test/mpl-3.10.9/20011230000000-20020101230000.svg)
153
+
154
+ ![test/mpl-3.10.9/20010101000000-20010109000000.svg](test/mpl-3.10.9/20010101000000-20010109000000.svg)
155
+
156
+ ![test/mpl-3.10.9/20010101000000-20010116230000.svg](test/mpl-3.10.9/20010101000000-20010116230000.svg)
157
+
158
+ ![test/mpl-3.10.9/20010130000000-20010204230000.svg](test/mpl-3.10.9/20010130000000-20010204230000.svg)
159
+
160
+ ![test/mpl-3.10.9/20011230000000-20020104230000.svg](test/mpl-3.10.9/20011230000000-20020104230000.svg)
161
+
162
+ ![test/mpl-3.10.9/20010101000000-20010131000000.svg](test/mpl-3.10.9/20010101000000-20010131000000.svg)
163
+
164
+ ![test/mpl-3.10.9/20010101000000-20010116230000_2.svg](test/mpl-3.10.9/20010101000000-20010116230000_2.svg)
165
+
166
+ ![test/mpl-3.10.9/20010130000000-20010215230000.svg](test/mpl-3.10.9/20010130000000-20010215230000.svg)
167
+
168
+ ![test/mpl-3.10.9/20011230000000-20010115230000.svg](test/mpl-3.10.9/20011230000000-20010115230000.svg)
169
+
170
+ ![test/mpl-3.10.9/20010101000000-20010202000000.svg](test/mpl-3.10.9/20010101000000-20010202000000.svg)
171
+
172
+ ![test/mpl-3.10.9/20010101000000-20010227230000.svg](test/mpl-3.10.9/20010101000000-20010227230000.svg)
173
+
174
+ ![test/mpl-3.10.9/20010115000000-20010216230000.svg](test/mpl-3.10.9/20010115000000-20010216230000.svg)
175
+
176
+ ![test/mpl-3.10.9/20011231000000-20020226230000.svg](test/mpl-3.10.9/20011231000000-20020226230000.svg)
177
+
178
+ ![test/mpl-3.10.9/20010101000000-20010502000000.svg](test/mpl-3.10.9/20010101000000-20010502000000.svg)
179
+
180
+ ![test/mpl-3.10.9/20010101000000-20010227230000_2.svg](test/mpl-3.10.9/20010101000000-20010227230000_2.svg)
181
+
182
+ ![test/mpl-3.10.9/20011231000000-20020226230000_2.svg](test/mpl-3.10.9/20011231000000-20020226230000_2.svg)
183
+
184
+ ![test/mpl-3.10.9/20010101000000-20010702000000.svg](test/mpl-3.10.9/20010101000000-20010702000000.svg)
185
+
186
+ ![test/mpl-3.10.9/20010101000000-20011231000000.svg](test/mpl-3.10.9/20010101000000-20011231000000.svg)
187
+
188
+ ![test/mpl-3.10.9/20010212000000-20020131000000.svg](test/mpl-3.10.9/20010212000000-20020131000000.svg)
189
+
190
+ ![test/mpl-3.10.9/20010101000000-20020103000000.svg](test/mpl-3.10.9/20010101000000-20020103000000.svg)
191
+
192
+ ![test/mpl-3.10.9/20010101000000-20021231000000.svg](test/mpl-3.10.9/20010101000000-20021231000000.svg)
193
+
194
+ ![test/mpl-3.10.9/20010401000000-20020430000000.svg](test/mpl-3.10.9/20010401000000-20020430000000.svg)
195
+
196
+ ![test/mpl-3.10.9/20011001000000-20031004000000.svg](test/mpl-3.10.9/20011001000000-20031004000000.svg)
197
+
198
+ ![test/mpl-3.10.9/20010101000000-20081231000000.svg](test/mpl-3.10.9/20010101000000-20081231000000.svg)
199
+
200
+ ![test/mpl-3.10.9/20010101000000-20030104000000.svg](test/mpl-3.10.9/20010101000000-20030104000000.svg)
201
+
202
+ ![test/mpl-3.10.9/20010101000000-20090104000000.svg](test/mpl-3.10.9/20010101000000-20090104000000.svg)
203
+
204
+ ![test/mpl-3.10.9/20010101000000-20120104000000.svg](test/mpl-3.10.9/20010101000000-20120104000000.svg)
205
+
206
+ ![test/mpl-3.10.9/20000101000000-20170104000000.svg](test/mpl-3.10.9/20000101000000-20170104000000.svg)
207
+
208
+ ![test/mpl-3.10.9/20010101000000-20180104000000.svg](test/mpl-3.10.9/20010101000000-20180104000000.svg)
209
+
210
+ ![test/mpl-3.10.9/20020101000000-20190104000000.svg](test/mpl-3.10.9/20020101000000-20190104000000.svg)
211
+
212
+ ![test/mpl-3.10.9/20030101000000-20200104000000.svg](test/mpl-3.10.9/20030101000000-20200104000000.svg)
213
+
214
+ ![test/mpl-3.10.9/20040101000000-20300104000000.svg](test/mpl-3.10.9/20040101000000-20300104000000.svg)
215
+
216
+ ![test/mpl-3.10.9/19500101000000-20120104000000.svg](test/mpl-3.10.9/19500101000000-20120104000000.svg)
@@ -0,0 +1,9 @@
1
+ datetick/__init__.py,sha256=Q_1eN-e8ewVpwdzExrVDYCIVwFJ4Le_AueDsdjoLZBA,200
2
+ datetick/datetick.py,sha256=g_ejtah9mvsctC9n27-jOczeCwvSClNhNRv8pIoJims,24860
3
+ datetick-0.9.0.dist-info/licenses/LICENSE.txt,sha256=UyVxGUH2kA6ZPDrjS3dtWGk5cUnsLnY7OlNSabyeCYA,1087
4
+ etc/release.py,sha256=-ApOOmegDxt4QUBc_ATr6YgHdZufM5ecXyj_eOn41U8,2737
5
+ test/datetick_test.py,sha256=GHHeMA7SUNCkBupOfI56dSE-l3BfoybMTokBQzf39LU,3412
6
+ datetick-0.9.0.dist-info/METADATA,sha256=DQXL_ajkgncha8NfMfGdAYfIHFe-P1NKOnvy2MJEdzA,10368
7
+ datetick-0.9.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ datetick-0.9.0.dist-info/top_level.txt,sha256=1ZRIT2tCJ-2z6LU-rrxHoJe2JmzN2uIN4WCZYolEAxg,18
9
+ datetick-0.9.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026- Bob Weigel, Brendan Gallagher
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ datetick
2
+ etc
3
+ test
etc/release.py ADDED
@@ -0,0 +1,93 @@
1
+ """
2
+ Usage:
3
+ python release.py
4
+ [path/to/pyproject.toml]
5
+ [--pypi-config-file path/to/pypirc]
6
+
7
+ Creates GitHub and PyPi releases with the same version number in pyproject.toml.
8
+
9
+ If path/to/pyproject.toml is not specified, it will look for it in the parent
10
+ directory of this script.
11
+
12
+ Requires:
13
+ A GitHub token file at ~/git/admin/etc/github_token (plain text, one line).
14
+ A PyPI config file at ~/git/admin/etc/pypirc
15
+ """
16
+
17
+ import os
18
+ import sys
19
+ import subprocess
20
+
21
+ try:
22
+ import tomllib
23
+ except ImportError:
24
+ try:
25
+ import tomli as tomllib
26
+ except ImportError:
27
+ cmd_list = [sys.executable, '-m', 'pip', 'install', 'tomli']
28
+ print("Executing:", " ".join(cmd_list))
29
+ subprocess.run(cmd_list, check=True)
30
+ import tomli as tomllib
31
+
32
+ def _install_deps():
33
+ try:
34
+ import build # noqa: F401
35
+ except ImportError:
36
+ cmd_list = [sys.executable, '-m', 'pip', 'install', 'build']
37
+ print("Executing:", " ".join(cmd_list))
38
+ subprocess.run(cmd_list, check=True)
39
+
40
+ try:
41
+ import twine # noqa: F401
42
+ except ImportError:
43
+ cmd_list = [sys.executable, '-m', 'pip', 'install', 'twine']
44
+ print("Executing:", " ".join(cmd_list))
45
+ subprocess.run(cmd_list, check=True)
46
+
47
+ def main(pypi_config_file):
48
+ if len(sys.argv) > 1:
49
+ toml_path = sys.argv[1]
50
+ else:
51
+ toml_path = os.path.join(os.path.dirname(__file__), '..', 'pyproject.toml')
52
+ toml_path = os.path.abspath(toml_path)
53
+
54
+ if not os.path.exists(toml_path):
55
+ print(f"Could not find pyproject.toml at {toml_path}")
56
+ sys.exit(1)
57
+
58
+ with open(toml_path, 'rb') as f:
59
+ data = tomllib.load(f)
60
+
61
+ version = data.get('project', {}).get('version')
62
+ if not version:
63
+ print(f"Could not find [project] version in {toml_path}")
64
+ sys.exit(1)
65
+
66
+ print(f"Creating release {version} on GitHub and PyPi")
67
+
68
+ # Check if gh CLI is installed
69
+ try:
70
+ subprocess.run(['gh', '--version'], check=True, stdout=subprocess.DEVNULL)
71
+ except (subprocess.CalledProcessError, FileNotFoundError):
72
+ print("Error: gh CLI is not installed. Please install it from https://cli.github.com/")
73
+ sys.exit(1)
74
+
75
+ cmd_list = ['gh', 'release', 'create', version, '-t', version, '-n', f'Release {version}']
76
+ #print("Executing:", " ".join(cmd_list))
77
+ #subprocess.run(cmd_list, check=True)
78
+
79
+ # Build package
80
+ cmd_list = [sys.executable, '-m', 'build']
81
+ print("Executing:", " ".join(cmd_list))
82
+ subprocess.run(cmd_list, check=True)
83
+ exit()
84
+
85
+ # Upload package to PyPi
86
+ cmd_list = ['twine', 'upload', '--config-file', pypi_config_file, 'dist/*']
87
+ subprocess.run(cmd_list, check=True)
88
+
89
+ if __name__ == '__main__':
90
+ _install_deps()
91
+ pypi_config_path = os.path.expanduser('~/git/admin/etc')
92
+ pypi_config_file = os.path.join(pypi_config_path, 'pypirc')
93
+ main(pypi_config_file)
test/datetick_test.py ADDED
@@ -0,0 +1,130 @@
1
+ # Create plots with varying time ranges.
2
+
3
+ import os
4
+ import yaml
5
+ import dateutil.parser
6
+ import matplotlib.pyplot as plt
7
+
8
+ from datetick import datetick
9
+
10
+ debug = False
11
+
12
+ def plot(ds1, ds2, **kwargs):
13
+ _, axes = plt.subplots(2, figsize=(8, 2))
14
+ plt.subplots_adjust(hspace=1.0)
15
+
16
+ dt1 = dateutil.parser.parse(ds1)
17
+ dt2 = dateutil.parser.parse(ds2)
18
+ x = [dt1, dt2]
19
+ xt = x[0] + (x[1] - x[0])/2
20
+ y = [0.0,0.0]
21
+
22
+ bbox = dict(boxstyle='round,pad=0.3', facecolor='white', edgecolor='gray', alpha=0.8)
23
+
24
+ axes[0].set_title(ds1 + ' - ' + ds2, fontfamily='monospace')
25
+ axes[0].plot(x, y, '*')
26
+ axes[0].text(xt, 0.00, 'matplotlib', ha='center', bbox=bbox)
27
+
28
+ text = 'datetick'
29
+ if kwargs:
30
+ bbox['facecolor'] = 'lightblue'
31
+ for key, value in kwargs.items():
32
+ text += f'\n{key}={value}'
33
+ axes[1].plot(x, y, '*')
34
+ axes[1].text(xt, 0.00, text, ha='center', bbox=bbox)
35
+ datetick('x', axes=axes[1], debug=debug, **kwargs)
36
+
37
+ for axis in axes:
38
+ axis.grid()
39
+ axis.spines[['top', 'right', 'left']].set_visible(False)
40
+ axis.yaxis.set_visible(False)
41
+
42
+ file = _savefig(ds1, ds2)
43
+
44
+ return file
45
+
46
+
47
+ def _savefig(ds1, ds2):
48
+ ds1 = ds1.replace(":","").replace("-","").replace("T","").replace("Z","")
49
+ ds2 = ds2.replace(":","").replace("-","").replace("T","").replace("Z","")
50
+
51
+ file = f'{_script_dir()}/{_mpl_version()}/{ds1}-{ds2}.svg'
52
+ if file in files:
53
+ v = 2
54
+ while file in files:
55
+ file = f'{_script_dir()}/{_mpl_version()}/{ds1}-{ds2}_{v}.svg'
56
+ v += 1
57
+
58
+ print("Writing", file)
59
+ dirname = os.path.dirname(file)
60
+ if not os.path.exists(dirname):
61
+ os.makedirs(dirname, exist_ok=True)
62
+
63
+ plt.savefig(file, bbox_inches='tight')
64
+ plt.close()
65
+
66
+ return file
67
+
68
+
69
+ def _append_to_readme(files):
70
+
71
+ readme = 'README.md' # Repo README
72
+ readme = os.path.join(_script_dir(), "..", readme)
73
+
74
+ with open(readme, 'r+') as file:
75
+ lines = file.readlines()
76
+
77
+ index = next(i for i, line in enumerate(lines) if "Comparison to default Matplotlib" in line)
78
+ del lines[index+1:]
79
+
80
+ image_links = []
81
+ for file in files:
82
+ # Make path relative to README
83
+ file = os.path.relpath(file, os.path.dirname(readme))
84
+ image_links.append(f'![{file}]({file})')
85
+
86
+ lines.append("\n" + "\n\n".join(image_links))
87
+
88
+ print(f"Updating {readme} with {len(files)} images")
89
+ with open(readme, 'w') as file:
90
+ file.writelines(lines)
91
+
92
+ def _create_subdir_readme(files):
93
+ # Create README in mpl subdir
94
+ image_links = []
95
+ for file in files:
96
+ # Make path relative to README
97
+ file = os.path.basename(file)
98
+ image_links.append(f'![{file}]({file})')
99
+
100
+ readme = os.path.join(_script_dir(), _mpl_version(), 'README.md')
101
+ print(f"Writing {readme} with {len(files)} images")
102
+ with open(readme, 'w') as file:
103
+ file.writelines("\n" + "\n\n".join(image_links))
104
+
105
+
106
+ def _mpl_version():
107
+ return f"mpl-{plt.matplotlib.__version__}"
108
+
109
+
110
+ def _script_dir():
111
+ return os.path.dirname(os.path.realpath(__file__))
112
+
113
+
114
+ if False:
115
+ plot('2001-01-01T00:00:58Z','2001-01-01T00:01:18Z', adjust_xrange=True)
116
+ #plot('2001-01-01T00:00:00Z','2001-01-02T23:00:00Z', adjust_first_xlabel=True)
117
+ exit()
118
+
119
+ test_file = os.path.join(_script_dir(), 'datetick_test.yaml')
120
+ with open(test_file, 'r') as file:
121
+ tests = yaml.safe_load(file)
122
+
123
+ files = []
124
+ for test in tests['main']:
125
+ kwargs = test[2] if len(test) > 2 else {}
126
+ file = plot(test[0], test[1], **kwargs)
127
+ files.append(file)
128
+
129
+ _append_to_readme(files)
130
+ _create_subdir_readme(files)