8000 improve timedelta2num precision, more refactoring, add tests accordingly · matplotlib/matplotlib@1069e94 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1069e94

Browse files
committed
improve timedelta2num precision, more refactoring, add tests accordingly
1 parent a4c6593 commit 1069e94

File tree

2 files changed

+237
-42
lines changed

2 files changed

+237
-42
lines changed

lib/matplotlib/dates.py

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -309,38 +309,35 @@ def get_epoch():
309309
return _epoch
310310

311311

312-
def _dt64_to_ordinalf(d):
312+
def _dt64_to_ordinalf(d, *, is_timedelta=False):
313313
"""
314314
Convert `numpy.datetime64` or an `numpy.ndarray` of those types to
315315
Gregorian date as UTC float relative to the epoch (see `.get_epoch`).
316316
Roundoff is float64 precision. Practically: microseconds for dates
317317
between 290301 BC, 294241 AD, milliseconds for larger dates
318318
(see `numpy.datetime64`).
319+
`is_timedelta` indicates that the converted values are timedelta instead
320+
of datetime. The converted floating point timedelta values are relative
321+
to `timedelta(0)`.
319322
"""
320323

321324
# the "extra" ensures that we at least allow the dynamic range out to
322325
# seconds. That should get out to +/-2e11 years.
323-
dseconds = d.astype('datetime64[s]')
326+
if is_timedelta:
327+
dseconds = d.astype('timedelta64[s]')
328+
dt = dseconds.astype(np.float64)
329+
else:
330+
dseconds = d.astype('datetime64[s]')
331+
t0 = np.datetime64(get_epoch(), 's')
332+
dt = (dseconds - t0).astype(np.float64)
333+
324334
extra = (d - dseconds).astype('timedelta64[ns]')
325-
t0 = np.datetime64(get_epoch(), 's')
326-
dt = (dseconds - t0).astype(np.float64)
327335
dt += extra.astype(np.float64) / 1.0e9
328336
dt = dt / SEC_PER_DAY
329337

330338
return _nat_to_nan(dt, d)
331339

332340

333-
def _timedelta64_to_ordinalf(t):
334-
"""
335-
Convert `numpy.timedelta64` or an ndarray of those types to a timedelta
336-
as float. Roundoff is float64 precision.
337-
TODO: precision ok, can be improved? be more concrete here
338-
"""
339-
td = t.astype('timedelta64[us]').astype(np.float64) / MUSECONDS_PER_DAY
340-
341-
return _nat_to_nan(td, t)
342-
343-
344341
def _nat_to_nan(ordf, timeval):
345342
"""
346343
Replace all values in the converted array `ordf` that were 'NaT'
@@ -458,7 +455,7 @@ def date2num(d):
458455
The Gregorian calendar is assumed; this is not universal practice.
459456
For details see the module docstring.
460457
"""
461-
return _timevalue2num(d, A3E2 np.datetime64)
458+
return _timevalue2num(d)
462459

463460

464461
def timedelta2num(t):
@@ -474,10 +471,10 @@ def timedelta2num(t):
474471
float or sequence of floats
475472
Number of days. For example, 1 day 12 hours returns 1.5.
476473
"""
477-
return _timevalue2num(t, np.timedelta64)
474+
return _timevalue2num(t, is_timedelta=True)
478475

479476

480-
def _timevalue2num(v, v_cls):
477+
def _timevalue2num(v, *, is_timedelta=False):
481478
"""
482479
Convert datetime or timedelta to Matplotlibs representation as days
483480
(since the epoch for datetime) as float.
@@ -486,14 +483,9 @@ def _timevalue2num(v, v_cls):
486483
----------
487484
v: `datetime.datetime`, `numpy.datetime64`, `datetime.timedelta` or
488485
`numpy.timedelta64` or sequences of these
489-
v_cls: class `numpy.timedelta64` or `numpy.datetime64` depending on
490-
whether to convert datetime or timedelta values
486+
is_timedelta: `bool`, indicates that a timedelta object is converted
487+
instead of a datetime object
491488
"""
492-
if v_cls is np.datetime64:
493-
dtype = 'datetime64[us]'
494-
else:
495-
dtype = 'timedelta64[us]'
496-
497489
# Unpack in case of e.g. Pandas or xarray object
498490
v = cbook._unpack_to_numpy(v)
499491

@@ -506,8 +498,10 @@ def _timevalue2num(v, v_cls):
506498
mask = np.ma.getmask(v)
507499
v = np.asarray(v)
508500

509-
# convert to datetime64 arrays, if not already:
510-
if not np.issubdtype(v.dtype, v_cls):
501+
# convert to timedelta64/datetime64 arrays, if not already:
502+
if is_timedelta and not np.issubdtype(v.dtype, np.timedelta64):
503+
v = v.astype('timedelta64[us]')
504+
elif not is_timedelta and not np.issubdtype(v.dtype, np.datetime64):
511505
# datetime arrays
512506
if not v.size:
513507
# deals with an empty array...
@@ -517,13 +511,10 @@ def _timevalue2num(v, v_cls):
517511
# make datetime naive:
518512
v = [dt.astimezone(UTC).replace(tzinfo=None) for dt in v]
519513
v = np.asarray(v)
520-
v = v.astype(dtype)
514+
v = v.astype('datetime64[us]')
521515

522516
v = np.ma.masked_array(v, mask=mask) if masked else v
523-
if v_cls is np.datetime64:
524-
v = _dt64_to_ordinalf(v)
525-
else:
526-
v = _timedelta64_to_ordinalf(v)
517+
v = _dt64_to_ordinalf(v, is_timedelta=is_timedelta)
527518

528519
return v if iterable else v[0]
529520

@@ -2107,7 +2098,8 @@ def __init__(self, minticks=5, maxticks=None):
21072098
self.maxticks = dict.fromkeys(self._freqs, maxticks)
21082099

21092100
self.intervald = {
2110-
DAILY: [1, 2, 3, 7, 14, 21],
2101+
DAILY: [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000,
2102+
10000, 20000, 50000, 100000, 200000, 500000, 1000000],
21112103
HOURLY: [1, 2, 3, 4, 6, 12],
21122104
MINUTELY: [1, 5, 10, 15, 30],
21132105
SECONDLY: [1, 5, 10, 15, 30],

lib/matplotlib/tests/test_dates.py

Lines changed: 212 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -107,18 +107,32 @@ def test_timedelta_numpy():
107107
np.testing.assert_equal(h.get_ydata(orig=False), hnp.get_ydata(orig=False))
108108

109109

110+
@pytest.mark.parametrize('num, td', [(100, datetime.timedelta(100)),
111+
(100 + (1 / 86400),
112+
datetime.timedelta(100, 1)),
113+
(100 + (1 / 8640000000),
114+
datetime.timedelta(100, 0, 10)),
115+
(1 / 86400000000,
116+
datetime.timedelta(0, 0, 1))])
117+
def test_timedelta2num(num, td):
118+
# conversion to num
119+
assert mdates.timedelta2num(td) == num
120+
121+
110122
@pytest.mark.parametrize('t0', [datetime.timedelta(100),
111123
112-
[datetime.timedelta(100, 1),
113-
datetime.timedelta(101, 1)],
124+
[datetime.timedelta(10, 1),
125+
datetime.timedelta(11, 1)],
126+
127+
[[datetime.timedelta(10, 0),
128+
datetime.timedelta(11, 0)],
129+
[datetime.timedelta(12, 0),
130+
datetime.timedelta(13, 0)]]
114131
115-
[[datetime.timedelta(100, 0, 1),
116-
datetime.timedelta(101, 0, 1)],
117-
[datetime.timedelta(102, 0, 1),
118-
datetime.timedelta(103, 0, 1)]]])
132+
])
119133
@pytest.mark.parametrize('dtype', ['timedelta64[s]',
120-
'timedelta64[us]',
121134
'timedelta64[ms]',
135+
'timedelta64[us]',
122136
'timedelta64[ns]'])
123137
def test_timedelta_timedelta2num_numpy(t0, dtype):
124138
time = mdates.timedelta2num(t0)
@@ -128,11 +142,11 @@ def test_timedelta_timedelta2num_numpy(t0, dtype):
128142

129143

130144
@pytest.mark.parametrize('dtype', ['timedelta64[s]',
131-
'timedelta64[us]',
132145
'timedelta64[ms]',
146+
'timedelta64[us]',
133147
'timedelta64[ns]'])
134148
def test_timedelta2num_NaT(dtype):
135-
t0 = datetime.timedelta(100, 0, 1)
149+
t0 = datetime.timedelta(100, 1)
136150
tmpl = [mdates.timedelta2num(t0), np.nan]
137151
tnp = np.array([t0, 'NaT'], dtype=dtype)
138152
nptime = mdates.timedelta2num(tnp)
@@ -1238,6 +1252,8 @@ def test_tz_utc():
12381252

12391253
@pytest.mark.parametrize("x, tdelta",
12401254
[(1, datetime.timedelta(days=1)),
1255+
(100.00000001736,
1256+
6B28 datetime.timedelta(days=100, microseconds=1500)),
12411257
([1, 1.5], [datetime.timedelta(days=1),
12421258
datetime.timedelta(days=1.5)])])
12431259
def test_num2timedelta(x, tdelta):
@@ -1448,3 +1464,190 @@ def test_DateFormatter_settz():
14481464
# Set tzinfo
14491465
formatter.set_tzinfo('Pacific/Kiritimati')
14501466
assert formatter(time) == '2011-Jan-01 14:00'
1467+
1468+
1469+
@pytest.mark.parametrize(
1470+
"t_delta, expected",
1471+
(
1472+
[datetime.timedelta(days=141),
1473+
['80 days, 0:00:00', '100 days, 0:00:00', '120 days, 0:00:00',
1474+
'140 days, 0:00:00', '160 days, 0:00:00', '180 days, 0:00:00',
1475+
'200 days, 0:00:00', '220 days, 0:00:00', '240 days, 0:00:00',
1476+
'260 days, 0:00:00']
1477+
],
1478+
[datetime.timedelta(hours=40),
1479+
['99 days, 18:00:00', '100 days, 0:00:00', '100 days, 6:00:00',
1480+
'100 days, 12:00:00', '100 days, 18:00:00', '101 days, 0:00:00',
1481+
'101 days, 6:00:00', '101 days, 12:00:00', '101 days, 18:00:00']
1482+
],
1483+
[datetime.timedelta(minutes=20),
1484+
['99 days, 23:55:00', '100 days, 0:00:00', '100 days, 0:05:00',
1485+
'100 days, 0:10:00', '100 days, 0:15:00', '100 days, 0:20:00',
1486+
'100 days, 0:25:00']
1487+
],
1488+
[datetime.timedelta(seconds=40),
1489+
['100 days, 0:00:00', '100 days, 0:00:05', '100 days, 0:00:10',
1490+
'100 days, 0:00:15', '100 days, 0:00:20', '100 days, 0:00:25',
1491+
'100 days, 0:00:30', '100 days, 0:00:35', '100 days, 0:00:40',
1492+
'100 days, 0:00:45']
1493+
],
1494+
[datetime.timedelta(microseconds=1500),
1495+
['100 days, 0:00:00',
1496+
'100 days, 0:00:00.000500', '100 days, 0:00:00.001000',
1497+
'100 days, 0:00:00.001500', '100 days, 0:00:00.002000']
1498+
],)
1499+
)
1500+
def test_auto_timedelta_locator(t_delta, expected):
1501+
def _create_auto_timedelta_locator(delta1, delta2):
1502+
locator = mdates.AutoTimedeltaLocator()
1503+
locator.create_dummy_axis()
1504+
locator.axis.set_view_interval(mdates.timedelta2num(delta1),
1505+
mdates.timedelta2num(delta2))
1506+
return locator
1507+
1508+
dt1 = datetime.timedelta(days=100)
1509+
1510+
dt2 = dt1 + t_delta
1511+
locator = _create_auto_timedelta_locator(dt1, dt2)
1512+
assert list(map(str, mdates.num2timedelta(locator()))) == expected
1513+
1514+
1515+
@pytest.mark.parametrize(
1516+
"kwargs, dt1, expected",
1517+
[ # note that the output string format here differs from strftimedelta!
1518+
[{'interval': 25},
1519+
datetime.timedelta(days=137),
1520+
['-25 days, 0:00:00', '0:00:00', '25 days, 0:00:00',
1521+
'50 days, 0:00:00', '75 days, 0:00:00', '100 days, 0:00:00',
1522+
'125 days, 0:00:00', '150 days, 0:00:00']],
1523+
1524+
[{'freq': mdates.HOURLY, 'interval': 12},
1525+
datetime.timedelta(days=3),
1526+
['-1 day, 12:00:00', '0:00:00', '12:00:00', '1 day, 0:00:00',
1527+
'1 day, 12:00:00', '2 days, 0:00:00', '2 days, 12:00:00',
1528+
'3 days, 0:00:00', '3 days, 12:00:00']],
1529+
1530+
[{'freq': mdates.MINUTELY, 'interval': 20},
1531+
datetime.timedelta(hours=2),
1532+
['-1 day, 23:40:00', '0:00:00', '0:20:00', '0:40:00',
1533+
'1:00:00', '1:20:00', '1:40:00', '2:00:00', '2:20:00']],
1534+
1535+
[{'freq': mdates.SECONDLY, 'interval': 15},
1536+
datetime.timedelta(minutes=1),
1537+
['-1 day, 23:59:45', '0:00:00', '0:00:15', '0:00:30',
1538+
'0:00:45', '0:01:00', '0:01:15']],
1539+
1540+
[{'freq': mdates.MICROSECONDLY, 'interval': 300000},
1541+
datetime.timedelta(seconds=2),
1542+
['-1 day, 23:59:59.700000', '0:00:00', '0:00:00.300000',
1543+
'0:00:00.600000', '0:00:00.900000', '0:00:01.200000',
1544+
'0:00:01.500000', '0:00:01.800000', '0:00:02.100000']]
1545+
]
1546+
)
1547+
def test_timedelta_locators_fixed(kwargs, dt1, expected):
1548+
dt0 = datetime.timedelta(days=0)
1549+
locator = mdates.TimedeltaLocator(**kwargs)
1550+
locator.create_dummy_axis()
1551+
locator.axis.set_view_interval(*mdates.timedelta2num([dt0, dt1]))
1552+
assert list(map(str, mdates.num2timedelta(locator()))) == expected
1553+
1554+
1555+
@pytest.mark.parametrize(
1556+
"td, fmt, expected",
1557+
[
1558+
(datetime.timedelta(days=1), "%d %day, %h:%m", "1 day, 00:00"),
1559+
(datetime.timedelta(days=2.25), "%d %day, %h:%m", "2 days, 06:00"),
1560+
(datetime.timedelta(seconds=362), "%h:%m:%s.%ms", "00:06:02.000"),
1561+
(datetime.timedelta(microseconds=1250), "%s.%ms%us", "00.001250"),
1562+
(datetime.timedelta(days=-0.25), "%h:%m", "-06:00"),
1563+
(datetime.timedelta(days=-1.5), "%d %day, %h:%m", "-1 day, 12:00"),
1564+
(datetime.timedelta(days=2), "%H hours", "48 hours"),
1565+
(datetime.timedelta(days=0.25), "%M min", "360 min"),
1566+
(datetime.timedelta(seconds=362.13), "%S.%ms", "362.130")
1567+
]
1568+
)
1569+
def test_strftimedelta(td, fmt, expected):
1570+
assert mdates.strftimedelta(td, fmt) == expected
1571+
1572+
1573+
@pytest.mark.parametrize(
1574+
"t_delta, fmt, kwargs, expected, expected_offset",
1575+
[
1576+
[datetime.timedelta(days=141), "%d %day", {},
1577+
['100 days', '120 days', '140 days', '160 days', '180 days',
1578+
'200 days', '220 days', '240 days', '260 days'],
1579+
""],
1580+
1581+
# [datetime.timedelta(hours=40), "%H:%m",
1582+
# {'offset_fmt': '%d %day', 'offset_on': 'days'},
1583+
# ['4:00', '8:00', '12:00', '16:00', '20:00', '24:00',
1584+
# '28:00', '32:00', '36:00', '40:00'],
1585+
# "100 days"],
1586+
#
1587+
# [datetime.timedelta(minutes=30), "%M:%s.0",
1588+
# {'offset_fmt': '%d %day, %h:%m', 'offset_on': 'hours'},
1589+
# ['42:00.0', '45:00.0', '48:00.0', '51:00.0', '54:00.0',
1590+
# '57:00.0', '60:00.0', '63:00.0', '66:00.0', '69:00.0'],
1591+
# "100 days, 03:00"],
1592+
#
1593+
# [datetime.timedelta(seconds=30), "%S.%ms",
1594+
# {'offset_fmt': '%d %day, %h:%m', 'offset_on': 'minutes'},
1595+
# ['0.000', '3.000', '6.000', '9.000', '12.000', '15.000',
1596+
# '18.000', '21.000', '24.000', '27.000', '30.000'],
1597+
# "100 days, 03:40"],
1598+
#
1599+
# [datetime.timedelta(microseconds=600), "%S.%ms%us",
1600+
# {'offset_fmt': '%d %day, %h:%m:%s', 'offset_on': 'seconds'},
1601+
# ['0.999900', '1.000000', '1.000100', '1.000200', '1.000300',
1602+
# '1.000400', '1.000500', '1.000600', '1.000700'],
1603+
# "100 days, 03:39:59"]
1604+
]
1605+
)
1606+
def test_timdelta_formatter(t_delta, fmt, kwargs, expected, expected_offset):
1607+
def _create_timedelta_locator(td1, td2, fmt, kwargs):
1608+
fig, ax = plt.subplots()
1609+
1610+
locator = mdates.AutoTimedeltaLocator()
1611+
formatter = mdates.TimedeltaFormatter(fmt, **kwargs)
1612+
ax.yaxis.set_major_locator(locator)
1613+
ax.yaxis.set_major_formatter(formatter)
1614+
ax.set_ylim(td1, td2)
1615+
fig.canvas.draw()
1616+
sts = [st.get_text() for st in ax.get_yticklabels()]
1617+
offset_text = ax.yaxis.get_offset_text().get_text()
1618+
return sts, offset_text
1619+
1620+
td1 = datetime.timedelta(days=100, hours=3, minutes=40)
1621+
td2 = td1 + t_delta
1622+
strings, offset_string = _create_timedelta_locator(
1623+
td1, td2, fmt, kwargs
1624+
)
1625+
assert strings == expected
1626+
assert offset_string == expected_offset
1627+
1628+
1629+
def test_timedelta_formatter_usetex():
1630+
formatter = mdates.TimedeltaFormatter("%h:%m", offset_on='days',
1631+
offset_fmt="%d %day", usetex=True)
1632+
values = [datetime.timedelta(days=0, hours=12),
1633+
datetime.timedelta(days=1, hours=0),
1634+
datetime.timedelta(days=1, hours=12),
1635+
datetime.timedelta(days=2, hours=0)]
1636+
1637+
labels = formatter.format_ticks(mdates.date2num(values))
1638+
1639+
start = '$\\mathdefault{'
1640+
i_start = len(start)
1641+
end = '}$'
1642+
i_end = -len(end)
1643+
1644+
def verify(string):
1645+
assert string[:i_start] == start
1646+
assert string[i_end:] == end
1647+
1648+
# assert ticks are tex formatted
1649+
for lbl in labels:
1650+
verify(lbl)
1651+
1652+
# assert offset is tex formatted
1653+
verify(formatter.get_offset())

0 commit comments

Comments
 (0)
0