diff --git a/doc/api/index.rst b/doc/api/index.rst index cf017a475bd..1d8316c6630 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -34,6 +34,7 @@ Plotting map elements Figure.logo Figure.solar Figure.text + Figure.timestamp Plotting tabular data ~~~~~~~~~~~~~~~~~~~~~ diff --git a/pygmt/figure.py b/pygmt/figure.py index 76c148caec4..e0ad3c2d47f 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -518,6 +518,7 @@ def _repr_html_(self): subplot, ternary, text, + timestamp, velo, wiggle, ) diff --git a/pygmt/src/__init__.py b/pygmt/src/__init__.py index 0ebc1289b04..087e8a063b4 100644 --- a/pygmt/src/__init__.py +++ b/pygmt/src/__init__.py @@ -51,6 +51,7 @@ from pygmt.src.surface import surface from pygmt.src.ternary import ternary from pygmt.src.text import text_ as text # "text" is an argument within "text_" +from pygmt.src.timestamp import timestamp from pygmt.src.triangulate import triangulate from pygmt.src.velo import velo from pygmt.src.which import which diff --git a/pygmt/src/timestamp.py b/pygmt/src/timestamp.py new file mode 100644 index 00000000000..7a450003dc0 --- /dev/null +++ b/pygmt/src/timestamp.py @@ -0,0 +1,102 @@ +""" +timestamp - Plot the GMT timestamp logo. +""" +from packaging.version import Version +from pygmt import __gmt_version__ +from pygmt.clib import Session +from pygmt.exceptions import GMTInvalidInput +from pygmt.helpers import build_arg_string, is_nonstr_iter + +__doctest_skip__ = ["timestamp"] + + +def timestamp( + self, + text=None, + label=None, + justification="BL", + offset=("-54p", "-54p"), + font="Helvetica,black", + timefmt="%Y %b %d %H:%M:%S", +): + r""" + Plot the GMT timestamp logo. + + Parameters + ---------- + text : None or str + If ``None``, the current UNIX timestamp is shown in the GMT timestamp + logo. Set this parameter to replace the UNIX timestamp with a + custom text string instead. The text must be less than 64 characters. + *Requires GMT>=6.5.0*. + label : None or str + The text string shown after the GMT timestamp logo. + justification : str + Justification of the timestamp. The *justification* is a two-character + code that is a combination of a horizontal (**L**\ (eft), + **C**\ (enter), or **R**\ (ight)) and a vertical (**T**\ (op), + **M**\ (iddle), or **B**\ (ottom)) code. + offset : str or tuple + *offset* or (*offset_x*, *offset_y*). + Offset the anchor point of the timestamp by *offset_x* and *offset_y*. + If a single value *offset* is given, *offset_y* = *offset_x* = + *offset*. + font : str + Font of the timestamp and the optional label. The parameter can't + change the font color for GMT<=6.4.0, only the font ID. + timefmt : str + Format string for the UNIX timestamp. The format string is parsed by + the C function ``strftime``, so that virtually any text can be used + (even not containing any time information). + + Examples + -------- + >>> # Plot the GMT timestamp logo. + >>> import pygmt + >>> fig = pygmt.Figure() + >>> fig.timestamp() + >>> fig.show() + + + >>> # Plot the GMT timestamp logo with a custom label. + >>> fig = pygmt.Figure() + >>> fig.timestamp(label="Powered by PyGMT") + >>> fig.show() + + """ + self._preprocess() # pylint: disable=protected-access + + # Build the options passed to the "plot" module + kwdict = {"T": True, "U": ""} + if label is not None: + kwdict["U"] += f"{label}" + kwdict["U"] += f"+j{justification}" + + if is_nonstr_iter(offset): # given a tuple + kwdict["U"] += "+o" + "/".join(f"{item}" for item in offset) + else: # given a single value + if "/" not in offset and Version(__gmt_version__) <= Version("6.4.0"): + # Giving a single offset doesn't work in GMT <= 6.4.0. + # See https://github.com/GenericMappingTools/gmt/issues/7107. + kwdict["U"] += f"+o{offset}/{offset}" + else: + kwdict["U"] += f"+o{offset}" + + # The +t modifier was added in GMT 6.5.0. + # See https://github.com/GenericMappingTools/gmt/pull/7127. + if text is not None: + if Version(__gmt_version__) < Version("6.5.0"): + raise GMTInvalidInput("The parameter 'text' requires GMT>=6.5.0.") + if len(str(text)) > 64: + raise GMTInvalidInput( + "The parameter 'text' must be less than 64 characters." + ) + kwdict["U"] += f"+t{text}" + + with Session() as lib: + lib.call_module( + module="plot", + args=build_arg_string( + kwdict, confdict={"FONT_LOGO": font, "FORMAT_TIME_STAMP": timefmt} + ), + ) diff --git a/pygmt/tests/baseline/test_timestamp.png.dvc b/pygmt/tests/baseline/test_timestamp.png.dvc new file mode 100644 index 00000000000..3e9981b1f71 --- /dev/null +++ b/pygmt/tests/baseline/test_timestamp.png.dvc @@ -0,0 +1,4 @@ +outs: +- md5: b4ca6cb54d80463606bb3d28eb077227 + size: 1668 + path: test_timestamp.png diff --git a/pygmt/tests/baseline/test_timestamp_font.png.dvc b/pygmt/tests/baseline/test_timestamp_font.png.dvc new file mode 100644 index 00000000000..963d3996369 --- /dev/null +++ b/pygmt/tests/baseline/test_timestamp_font.png.dvc @@ -0,0 +1,4 @@ +outs: +- md5: e1c3c80a85ebaadc0a820bcc7954a539 + size: 2840 + path: test_timestamp_font.png diff --git a/pygmt/tests/baseline/test_timestamp_justification.png.dvc b/pygmt/tests/baseline/test_timestamp_justification.png.dvc new file mode 100644 index 00000000000..70b12caa8bc --- /dev/null +++ b/pygmt/tests/baseline/test_timestamp_justification.png.dvc @@ -0,0 +1,4 @@ +outs: +- md5: c2bfca8624e4bf966eeb57bbc4aa0726 + size: 8843 + path: test_timestamp_justification.png diff --git a/pygmt/tests/baseline/test_timestamp_label.png.dvc b/pygmt/tests/baseline/test_timestamp_label.png.dvc new file mode 100644 index 00000000000..9fe316752bd --- /dev/null +++ b/pygmt/tests/baseline/test_timestamp_label.png.dvc @@ -0,0 +1,4 @@ +outs: +- md5: 868d6a5912b586bc7e095b9923ce8d83 + size: 3036 + path: test_timestamp_label.png diff --git a/pygmt/tests/baseline/test_timestamp_offset.png.dvc b/pygmt/tests/baseline/test_timestamp_offset.png.dvc new file mode 100644 index 00000000000..02276575ca2 --- /dev/null +++ b/pygmt/tests/baseline/test_timestamp_offset.png.dvc @@ -0,0 +1,4 @@ +outs: +- md5: be0021731b881e32e3d8a46d6fa8abc3 + size: 9450 + path: test_timestamp_offset.png diff --git a/pygmt/tests/test_timestamp.py b/pygmt/tests/test_timestamp.py new file mode 100644 index 00000000000..1c092a8918c --- /dev/null +++ b/pygmt/tests/test_timestamp.py @@ -0,0 +1,88 @@ +""" +Tests for Figure.timestamp. +""" +import pytest +from packaging.version import Version +from pygmt import Figure, __gmt_version__ + + +@pytest.fixture(scope="module", name="faketime") +def fixture_faketime(): + """ + Fake datetime that will be passed to the "timefmt" parameter, so that the + timestamp string always has a fixed value. + """ + return "9999-99-99T99:99:99" + + +@pytest.mark.mpl_image_compare +def test_timestamp(faketime): + """ + Test that the simplest timestamp() call works. + """ + fig = Figure() + fig.timestamp(timefmt=faketime) + return fig + + +@pytest.mark.mpl_image_compare +def test_timestamp_label(faketime): + """ + Check if the "label" parameter works. + """ + fig = Figure() + fig.timestamp(label="Powered by PyGMT", timefmt=faketime) + return fig + + +@pytest.mark.mpl_image_compare +def test_timestamp_justification(): + """ + Check if the "justification" parameter works. + + Only a subset of justification codes are tested to avoid overlapping + timestamps. + """ + fig = Figure() + fig.basemap(projection="X10c/5c", region=[0, 10, 0, 5], frame=0) + for just in ["BL", "BR", "TL", "TR"]: + fig.timestamp(justification=just, timefmt=just) + return fig + + +@pytest.mark.mpl_image_compare +def test_timestamp_offset(): + """ + Check if the "offset" parameter works. + """ + fig = Figure() + fig.basemap(projection="X10c/5c", region=[0, 10, 0, 5], frame="g1") + for offset in ["1c", "1c/2c", ("1c", "3c")]: + fig.timestamp(offset=offset, timefmt=f"offset={offset}") + return fig + + +@pytest.mark.mpl_image_compare +def test_timestamp_font(faketime): + """ + Test if the "font" parameter works. + """ + fig = Figure() + fig.timestamp(font="Times-Roman", label="Powered by GMT", timefmt=faketime) + return fig + + +@pytest.mark.skipif( + Version(__gmt_version__) < Version("6.5.0"), + reason="The 'text' parameter requires GMT>=6.5.0", +) +@pytest.mark.mpl_image_compare(filename="test_timestamp.png") +def test_timestamp_text(faketime): + """ + Test if the "text" parameter works. + + Requires GMT>=6.5.0. + """ + fig = Figure() + fig.timestamp(text=faketime) + return fig