From 425e1b6abe642e0072b835f423b52d02083d9542 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 24 Oct 2024 17:20:40 +0800 Subject: [PATCH 1/2] Figure.paragraph: Initial implementation focusing on input data --- pygmt/figure.py | 1 + pygmt/src/__init__.py | 1 + pygmt/src/paragraph.py | 111 ++++++++++++++++++ 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 +++++++++++ 7 files changed, 195 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/pygmt/figure.py b/pygmt/figure.py index f9c8478747d..054843843c3 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -426,6 +426,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..5293603c7b3 --- /dev/null +++ b/pygmt/src/paragraph.py @@ -0,0 +1,111 @@ +""" +paragraph - Typeset one or multiple paragraphs. +""" + +import io +from collections.abc import Sequence +from typing import Literal + +from pygmt._typing import AnchorCode +from pygmt.clib import Session +from pygmt.exceptions import GMTInvalidInput +from pygmt.helpers import ( + _check_encoding, + build_arg_list, + is_nonstr_iter, + non_ascii_to_octal, +) + + +def _parse_font_angle_justify( + font: float | str | None, angle: float | None, justify: AnchorCode | None +) -> str | None: + """ + Parse the font, angle, and justification arguments and return the string to be + appended to the module options. + + Examples + -------- + >>> _parse_font_angle_justify(None, None, None) + >>> _parse_font_angle_justify("10p", None, None) + '+f10p' + >>> _parse_font_angle_justify(None, 45, None) + '+a45' + >>> _parse_font_angle_justify(None, None, "CM") + '+jCM' + >>> _parse_font_angle_justify("10p", 45, None) + '+f10p+a45' + >>> _parse_font_angle_justify("10p,Helvetica-Bold", 45, "CM") + '+f10p,Helvetica-Bold+a45+jCM' + """ + args = {"+f": font, "+a": angle, "+j": justify} + if all(arg is None for arg in args.values()): + return None + return "".join(f"{prefix}{arg}" for prefix, arg in args.items() if arg is not None) + + +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._preprocess() + + # Validate 'alignment' argument. + if alignment not in {"left", "center", "right", "justified"}: + msg = ( + "Invalid value for 'alignment': {alignment}. " + "Valid values are 'left', 'center', 'right', and 'justified'." + ) + raise GMTInvalidInput(msg) + + confdict = {} + # Prepare the keyword dictionary for the module options + kwdict = {"M": True, "F": _parse_font_angle_justify(font, angle, justify)} + # 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(kwdict, 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 5707c88561fd085d0c6bd43d1dc38512e325e40d Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 2 May 2025 20:33:52 +0800 Subject: [PATCH 2/2] Add to API page --- doc/api/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/api/index.rst b/doc/api/index.rst index 618217468c2..8ecca3a50f5 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