Skip to content

Commit f401f85

Browse files
seismanweiji14
andauthored
Support various datetime types as input (#464)
Currently supported datetime types: - [numpy.datetime64](https://numpy.org/doc/stable/reference/arrays.datetime.html) - [pandas.DateTimeIndex](https://pandas.pydata.org/docs/user_guide/timeseries.html) - Raw datetime strings in both ISO and non-ISO formats, e.g., `2010-01-01`, `1/1/2018`, `Jul 5, 2019` - Python's [built-in datetime and date](https://docs.python.org/3/library/datetime.html) * Add a function array_to_datetime to convert any legal array/list to pandas.DateTimeIndex and numpy.datetime64 types * Test that xarray.DataArray datetimes can be plotted Co-authored-by: Wei Ji <[email protected]>
1 parent 0f12972 commit f401f85

File tree

5 files changed

+147
-13
lines changed

5 files changed

+147
-13
lines changed

pygmt/clib/conversion.py

+80
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Functions to convert data types into ctypes friendly formats.
33
"""
44
import numpy as np
5+
import pandas as pd
56

67
from ..exceptions import GMTInvalidInput
78

@@ -237,3 +238,82 @@ def kwargs_to_ctypes_array(argument, kwargs, dtype):
237238
if argument in kwargs:
238239
return dtype(*kwargs[argument])
239240
return None
241+
242+
243+
def array_to_datetime(array):
244+
"""
245+
Convert an 1d datetime array from various types into pandas.DatetimeIndex
246+
(i.e., numpy.datetime64).
247+
248+
If the input array is not in legal datetime formats, raise a "ParseError"
249+
exception.
250+
251+
Parameters
252+
----------
253+
array : list or 1d array
254+
The input datetime array in various formats.
255+
256+
Supported types:
257+
258+
- str
259+
- numpy.datetime64
260+
- pandas.DateTimeIndex
261+
- datetime.datetime and datetime.date
262+
263+
Returns
264+
-------
265+
array : 1d datetime array in pandas.DatetimeIndex (i.e., numpy.datetime64)
266+
267+
Examples
268+
--------
269+
>>> import datetime
270+
>>> # numpy.datetime64 array
271+
>>> x = np.array(
272+
... ["2010-06-01", "2011-06-01T12", "2012-01-01T12:34:56"],
273+
... dtype="datetime64",
274+
... )
275+
>>> array_to_datetime(x)
276+
DatetimeIndex(['2010-06-01 00:00:00', '2011-06-01 12:00:00',
277+
'2012-01-01 12:34:56'],
278+
dtype='datetime64[ns]', freq=None)
279+
280+
>>> # pandas.DateTimeIndex array
281+
>>> x = pd.date_range("2013", freq="YS", periods=3)
282+
>>> array_to_datetime(x) # doctest: +NORMALIZE_WHITESPACE
283+
DatetimeIndex(['2013-01-01', '2014-01-01', '2015-01-01'],
284+
dtype='datetime64[ns]', freq='AS-JAN')
285+
286+
>>> # Python's built-in date and datetime
287+
>>> x = [datetime.date(2018, 1, 1), datetime.datetime(2019, 1, 1)]
288+
>>> array_to_datetime(x) # doctest: +NORMALIZE_WHITESPACE
289+
DatetimeIndex(['2018-01-01', '2019-01-01'],
290+
dtype='datetime64[ns]', freq=None)
291+
292+
>>> # Raw datetime strings in various format
293+
>>> x = [
294+
... "2018",
295+
... "2018-02",
296+
... "2018-03-01",
297+
... "2018-04-01T01:02:03",
298+
... "5/1/2018",
299+
... "Jun 05, 2018",
300+
... "2018/07/02",
301+
... ]
302+
>>> array_to_datetime(x)
303+
DatetimeIndex(['2018-01-01 00:00:00', '2018-02-01 00:00:00',
304+
'2018-03-01 00:00:00', '2018-04-01 01:02:03',
305+
'2018-05-01 00:00:00', '2018-06-05 00:00:00',
306+
'2018-07-02 00:00:00'],
307+
dtype='datetime64[ns]', freq=None)
308+
309+
>>> # Mixed datetime types
310+
>>> x = [
311+
... "2018-01-01",
312+
... np.datetime64("2018-01-01"),
313+
... datetime.datetime(2018, 1, 1),
314+
... ]
315+
>>> array_to_datetime(x) # doctest: +NORMALIZE_WHITESPACE
316+
DatetimeIndex(['2018-01-01', '2018-01-01', '2018-01-01'],
317+
dtype='datetime64[ns]', freq=None)
318+
"""
319+
return pd.to_datetime(array)

pygmt/clib/session.py

+27-12
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
vectors_to_arrays,
2323
dataarray_to_matrix,
2424
as_c_contiguous,
25+
array_to_datetime,
2526
)
2627

2728
FAMILIES = [
@@ -48,12 +49,13 @@
4849
REGISTRATIONS = ["GMT_GRID_PIXEL_REG", "GMT_GRID_NODE_REG"]
4950

5051
DTYPES = {
51-
"float64": "GMT_DOUBLE",
52-
"float32": "GMT_FLOAT",
53-
"int64": "GMT_LONG",
54-
"int32": "GMT_INT",
55-
"uint64": "GMT_ULONG",
56-
"uint32": "GMT_UINT",
52+
np.float64: "GMT_DOUBLE",
53+
np.float32: "GMT_FLOAT",
54+
np.int64: "GMT_LONG",
55+
np.int32: "GMT_INT",
56+
np.uint64: "GMT_ULONG",
57+
np.uint32: "GMT_UINT",
58+
np.datetime64: "GMT_DATETIME",
5759
}
5860

5961

@@ -708,15 +710,22 @@ def _check_dtype_and_dim(self, array, ndim):
708710
True
709711
710712
"""
711-
if array.dtype.name not in DTYPES:
712-
raise GMTInvalidInput(
713-
"Unsupported numpy data type '{}'.".format(array.dtype.name)
714-
)
713+
# check the array has the given dimension
715714
if array.ndim != ndim:
716715
raise GMTInvalidInput(
717716
"Expected a numpy 1d array, got {}d.".format(array.ndim)
718717
)
719-
return self[DTYPES[array.dtype.name]]
718+
719+
# check the array has a valid/known data type
720+
if array.dtype.type not in DTYPES:
721+
try:
722+
# Try to convert any unknown numpy data types to np.datetime64
723+
array = np.asarray(array, dtype=np.datetime64)
724+
except ValueError:
725+
raise GMTInvalidInput(
726+
"Unsupported numpy data type '{}'.".format(array.dtype.type)
727+
)
728+
return self[DTYPES[array.dtype.type]]
720729

721730
def put_vector(self, dataset, column, vector):
722731
"""
@@ -762,7 +771,13 @@ def put_vector(self, dataset, column, vector):
762771
)
763772

764773
gmt_type = self._check_dtype_and_dim(vector, ndim=1)
765-
vector_pointer = vector.ctypes.data_as(ctp.c_void_p)
774+
if gmt_type == self["GMT_DATETIME"]:
775+
vector_pointer = (ctp.c_char_p * len(vector))()
776+
vector_pointer[:] = np.char.encode(
777+
np.datetime_as_string(array_to_datetime(vector))
778+
)
779+
else:
780+
vector_pointer = vector.ctypes.data_as(ctp.c_void_p)
766781
status = c_put_vector(
767782
self.session_pointer, dataset, column, gmt_type, vector_pointer
768783
)
17.8 KB
Loading

pygmt/tests/test_clib.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ def test_put_vector_invalid_dtype():
349349
mode="GMT_CONTAINER_ONLY",
350350
dim=[2, 3, 1, 0], # columns, rows, layers, dtype
351351
)
352-
data = np.array([37, 12, 556], dtype="complex128")
352+
data = np.array([37, 12, 556], dtype="object")
353353
with pytest.raises(GMTInvalidInput):
354354
lib.put_vector(dataset, column=1, vector=data)
355355

pygmt/tests/test_plot.py

+39
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
"""
33
Tests plot.
44
"""
5+
import datetime
56
import os
67

78
import numpy as np
9+
import pandas as pd
10+
import xarray as xr
11+
812
import pytest
913

1014
from .. import Figure
@@ -284,3 +288,38 @@ def test_plot_scalar_xy():
284288
fig.plot(x=0, y=0, style="t1c")
285289
fig.plot(x=1.5, y=-1.5, style="s1c")
286290
return fig
291+
292+
293+
@pytest.mark.mpl_image_compare
294+
def test_plot_datetime():
295+
"""Test various datetime input data"""
296+
fig = Figure()
297+
fig.basemap(projection="X15c/5c", region="2010-01-01/2020-01-01/0/10", frame=True)
298+
299+
# numpy.datetime64 types
300+
x = np.array(
301+
["2010-06-01", "2011-06-01T12", "2012-01-01T12:34:56"], dtype="datetime64"
302+
)
303+
y = [1.0, 2.0, 3.0]
304+
fig.plot(x, y, style="c0.2c", pen="1p")
305+
306+
# pandas.DatetimeIndex
307+
x = pd.date_range("2013", freq="YS", periods=3)
308+
y = [4, 5, 6]
309+
fig.plot(x, y, style="t0.2c", pen="1p")
310+
311+
# xarray.DataArray
312+
x = xr.DataArray(data=pd.date_range(start="2015-03", freq="QS", periods=3))
313+
y = [7.5, 6, 4.5]
314+
fig.plot(x, y, style="s0.2c", pen="1p")
315+
316+
# raw datetime strings
317+
x = ["2016-02-01", "2017-03-04T00:00"]
318+
y = [7, 8]
319+
fig.plot(x, y, style="a0.2c", pen="1p")
320+
321+
# the Python built-in datetime and date
322+
x = [datetime.date(2018, 1, 1), datetime.datetime(2019, 1, 1)]
323+
y = [8.5, 9.5]
324+
fig.plot(x, y, style="i0.2c", pen="1p")
325+
return fig

0 commit comments

Comments
 (0)