From 8e31570d0b5edf6894df8fa4020ba4641347fa71 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 24 Oct 2024 17:20:40 +0800 Subject: [PATCH 1/6] Figure.paragraph: Initial implementation focusing on input data --- doc/api/index.rst | 1 + pygmt/figure.py | 1 + pygmt/src/__init__.py | 1 + pygmt/src/paragraph.py | 93 +++++++++++++++++++ pygmt/tests/baseline/test_paragraph.png.dvc | 5 + ...raph_multiple_paragraphs_blankline.png.dvc | 5 + ...paragraph_multiple_paragraphs_list.png.dvc | 5 + pygmt/tests/test_paragraph.py | 67 +++++++++++++ 8 files changed, 178 insertions(+) create mode 100644 pygmt/src/paragraph.py create mode 100644 pygmt/tests/baseline/test_paragraph.png.dvc create mode 100644 pygmt/tests/baseline/test_paragraph_multiple_paragraphs_blankline.png.dvc create mode 100644 pygmt/tests/baseline/test_paragraph_multiple_paragraphs_list.png.dvc create mode 100644 pygmt/tests/test_paragraph.py diff --git a/doc/api/index.rst b/doc/api/index.rst index 3656bba286e..98cac179ea4 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -31,6 +31,7 @@ Plotting map elements Figure.inset Figure.legend Figure.logo + Figure.paragraph Figure.solar Figure.text Figure.timestamp diff --git a/pygmt/figure.py b/pygmt/figure.py index 56ad2c3d5cf..b7b74e7b51d 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -423,6 +423,7 @@ def _repr_html_(self) -> str: legend, logo, meca, + paragraph, plot, plot3d, psconvert, diff --git a/pygmt/src/__init__.py b/pygmt/src/__init__.py index 8905124f917..6b163a31792 100644 --- a/pygmt/src/__init__.py +++ b/pygmt/src/__init__.py @@ -38,6 +38,7 @@ from pygmt.src.makecpt import makecpt from pygmt.src.meca import meca from pygmt.src.nearneighbor import nearneighbor +from pygmt.src.paragraph import paragraph from pygmt.src.plot import plot from pygmt.src.plot3d import plot3d from pygmt.src.project import project diff --git a/pygmt/src/paragraph.py b/pygmt/src/paragraph.py new file mode 100644 index 00000000000..ad3467440b6 --- /dev/null +++ b/pygmt/src/paragraph.py @@ -0,0 +1,93 @@ +""" +paragraph - Typeset one or multiple paragraphs. +""" + +import io +from collections.abc import Sequence +from typing import Literal + +from pygmt._typing import AnchorCode +from pygmt.alias import Alias, AliasSystem +from pygmt.clib import Session +from pygmt.exceptions import GMTValueError +from pygmt.helpers import ( + _check_encoding, + build_arg_list, + is_nonstr_iter, + non_ascii_to_octal, +) + + +def paragraph( + self, + x: float | str, + y: float | str, + text: str | Sequence[str], + parwidth: float | str, + linespacing: float | str, + font: float | str | None = None, + angle: float | None = None, + justify: AnchorCode | None = None, + alignment: Literal["left", "center", "right", "justified"] = "left", +): + """ + Typeset one or multiple paragraphs. + + Parameters + ---------- + x/y + The x, y coordinates of the paragraph. + text + The paragraph text to typeset. If a sequence of strings is provided, each string + is treated as a separate paragraph. + parwidth + The width of the paragraph. + linespacing + The spacing between lines. + font + The font of the text. + angle + The angle of the text. + justify + The justification of the block of text, relative to the given x, y position. + alignment + The alignment of the text. Valid values are ``"left"``, ``"center"``, + ``"right"``, and ``"justified"``. + """ + self._activate_figure() + + _valid_alignments = {"left", "center", "right", "justified"} + if alignment not in _valid_alignments: + raise GMTValueError( + alignment, + description="value for parameter 'alignment'", + choices=_valid_alignments, + ) + + aliasdict = AliasSystem( + F=[ + Alias(font, name="font", prefix="+f"), + Alias(angle, name="angle", prefix="+a"), + Alias(justify, name="justify", prefix="+j"), + ] + ) + aliasdict.merge({"M": True}) + + confdict = {} + # Prepare the text string that will be passed to an io.StringIO object. + # Multiple paragraphs are separated by a blank line "\n\n". + _textstr: str = "\n\n".join(text) if is_nonstr_iter(text) else str(text) + # Check the encoding of the text string and convert it to octal if necessary. + if (encoding := _check_encoding(_textstr)) != "ascii": + _textstr = non_ascii_to_octal(_textstr, encoding=encoding) + confdict["PS_CHAR_ENCODING"] = encoding + + with Session() as lib: + with io.StringIO() as buffer: # Prepare the StringIO input. + buffer.write(f"> {x} {y} {linespacing} {parwidth} {alignment[0]}\n") + buffer.write(_textstr) + with lib.virtualfile_in(data=buffer) as vfile: + lib.call_module( + "text", + args=build_arg_list(aliasdict, infile=vfile, confdict=confdict), + ) diff --git a/pygmt/tests/baseline/test_paragraph.png.dvc b/pygmt/tests/baseline/test_paragraph.png.dvc new file mode 100644 index 00000000000..82906933e1d --- /dev/null +++ b/pygmt/tests/baseline/test_paragraph.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: c5b1df47e811475defb0db79e49cab3d + size: 27632 + hash: md5 + path: test_paragraph.png diff --git a/pygmt/tests/baseline/test_paragraph_multiple_paragraphs_blankline.png.dvc b/pygmt/tests/baseline/test_paragraph_multiple_paragraphs_blankline.png.dvc new file mode 100644 index 00000000000..a131677880d --- /dev/null +++ b/pygmt/tests/baseline/test_paragraph_multiple_paragraphs_blankline.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 0df1eb71a781f0b8cc7c48be860dd321 + size: 29109 + hash: md5 + path: test_paragraph_multiple_paragraphs_blankline.png diff --git a/pygmt/tests/baseline/test_paragraph_multiple_paragraphs_list.png.dvc b/pygmt/tests/baseline/test_paragraph_multiple_paragraphs_list.png.dvc new file mode 100644 index 00000000000..879799cc5db --- /dev/null +++ b/pygmt/tests/baseline/test_paragraph_multiple_paragraphs_list.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 167d4be24bca4e287b2056ecbfbb629a + size: 29076 + hash: md5 + path: test_paragraph_multiple_paragraphs_list.png diff --git a/pygmt/tests/test_paragraph.py b/pygmt/tests/test_paragraph.py new file mode 100644 index 00000000000..2193dc1384a --- /dev/null +++ b/pygmt/tests/test_paragraph.py @@ -0,0 +1,67 @@ +""" +Tests for Figure.paragraph. +""" + +import pytest +from pygmt import Figure + + +@pytest.mark.mpl_image_compare +def test_paragraph(): + """ + Test typesetting a single paragraph. + """ + fig = Figure() + fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True) + fig.paragraph( + x=4, + y=4, + text="This is a long paragraph. " * 10, + parwidth="5c", + linespacing="12p", + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_paragraph_multiple_paragraphs_list(): + """ + Test typesetting a single paragraph. + """ + fig = Figure() + fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True) + fig.paragraph( + x=4, + y=4, + text=[ + "This is the first paragraph. " * 5, + "This is the second paragraph. " * 5, + ], + parwidth="5c", + linespacing="12p", + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_paragraph_multiple_paragraphs_blankline(): + """ + Test typesetting a single paragraph. + """ + text = """ +This is the first paragraph. +This is the first paragraph. +This is the first paragraph. +This is the first paragraph. +This is the first paragraph. + +This is the second paragraph. +This is the second paragraph. +This is the second paragraph. +This is the second paragraph. +This is the second paragraph. +""" + fig = Figure() + fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True) + fig.paragraph(x=4, y=4, text=text, parwidth="5c", linespacing="12p") + return fig From a61584176fd6faf215f74cf4d41c8f2669a60f11 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 20 Oct 2025 19:34:44 +0800 Subject: [PATCH 2/6] Mention the Figure.paragraph method in Figure.text --- pygmt/src/text.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pygmt/src/text.py b/pygmt/src/text.py index 07027e10f5e..98b9e94dc37 100644 --- a/pygmt/src/text.py +++ b/pygmt/src/text.py @@ -71,6 +71,9 @@ def text_( # noqa: PLR0912, PLR0913, PLR0915 ZapfDingbats and ISO-8859-x (x can be 1-11, 13-16) encodings. Refer to :doc:`/techref/encodings` for the full list of supported non-ASCII characters. + For typesetting one or multiple paragraphs of text, see + :meth:`pygmt.Figure.paragraph`. + Full GMT docs at :gmt-docs:`text.html`. {aliases} From 966e2a4951ed9a3d17b181b3842377d72ffcfce8 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 21 Oct 2025 17:05:00 +0800 Subject: [PATCH 3/6] Fix styling --- pygmt/src/text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/text.py b/pygmt/src/text.py index 98b9e94dc37..1211bf15480 100644 --- a/pygmt/src/text.py +++ b/pygmt/src/text.py @@ -71,7 +71,7 @@ def text_( # noqa: PLR0912, PLR0913, PLR0915 ZapfDingbats and ISO-8859-x (x can be 1-11, 13-16) encodings. Refer to :doc:`/techref/encodings` for the full list of supported non-ASCII characters. - For typesetting one or multiple paragraphs of text, see + For typesetting one or multiple paragraphs of text, see :meth:`pygmt.Figure.paragraph`. Full GMT docs at :gmt-docs:`text.html`. From f606ff6177bd7753ed783c83950f11b52404fe67 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 21 Oct 2025 18:27:56 +0800 Subject: [PATCH 4/6] Add an inline example --- pygmt/src/paragraph.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pygmt/src/paragraph.py b/pygmt/src/paragraph.py index ad3467440b6..29f525edcc6 100644 --- a/pygmt/src/paragraph.py +++ b/pygmt/src/paragraph.py @@ -17,6 +17,8 @@ non_ascii_to_octal, ) +__doctest_skip__ = ["paragraph"] + def paragraph( self, @@ -53,6 +55,22 @@ def paragraph( alignment The alignment of the text. Valid values are ``"left"``, ``"center"``, ``"right"``, and ``"justified"``. + + Examples + -------- + >>> import pygmt + >>> + >>> fig = pygmt.Figure() + >>> fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True) + >>> fig.paragraph( + ... x=4, + ... y=4, + ... text="This is a long paragraph. " * 10, + ... parwidth="5c", + ... linespacing="12p", + ... font="12p", + ... ) + >>> fig.show() """ self._activate_figure() From c0c53cac62803af0655a500214d18c34aa95487a Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 21 Oct 2025 18:47:13 +0800 Subject: [PATCH 5/6] Improve docstrings --- pygmt/src/paragraph.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pygmt/src/paragraph.py b/pygmt/src/paragraph.py index 29f525edcc6..a3524bd7838 100644 --- a/pygmt/src/paragraph.py +++ b/pygmt/src/paragraph.py @@ -51,9 +51,10 @@ def paragraph( angle The angle of the text. justify - The justification of the block of text, relative to the given x, y position. + Set the alignment of the block of text, relative to the given x, y position. + Choose a :doc:`2-character justification code `. alignment - The alignment of the text. Valid values are ``"left"``, ``"center"``, + Set the alignment of the text. Valid values are ``"left"``, ``"center"``, ``"right"``, and ``"justified"``. Examples From dbb076db451a5b7327e7f8bbd099329b2cf17c79 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 21 Oct 2025 19:04:56 +0800 Subject: [PATCH 6/6] Add more docstrings --- pygmt/src/paragraph.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pygmt/src/paragraph.py b/pygmt/src/paragraph.py index a3524bd7838..151bb3f4bf0 100644 --- a/pygmt/src/paragraph.py +++ b/pygmt/src/paragraph.py @@ -35,6 +35,16 @@ def paragraph( """ Typeset one or multiple paragraphs. + This method typesets one or multiple paragraphs of text at a given position on the + figure. The text is flowed within a given paragraph width and with a specified line + spacing. The text can be aligned left, center, right, or justified. + + Multiple paragraphs can be provided as a sequence of strings, where each string + represents a separate paragraph, or as a single string with newline characters + separating the paragraphs. + + Full GMT docs at :gmt-docs:`text.html`. + Parameters ---------- x/y