Skip to content

Commit a617468

Browse files
committed
Figure.paragraph: Initial implementation focusing on input data
1 parent 7c38ce7 commit a617468

File tree

4 files changed

+182
-0
lines changed

4 files changed

+182
-0
lines changed

pygmt/figure.py

+1
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ def _repr_html_(self) -> str:
424424
legend,
425425
logo,
426426
meca,
427+
paragraph,
427428
plot,
428429
plot3d,
429430
psconvert,

pygmt/src/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from pygmt.src.makecpt import makecpt
3939
from pygmt.src.meca import meca
4040
from pygmt.src.nearneighbor import nearneighbor
41+
from pygmt.src.paragraph import paragraph
4142
from pygmt.src.plot import plot
4243
from pygmt.src.plot3d import plot3d
4344
from pygmt.src.project import project

pygmt/src/paragraph.py

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""
2+
paragraph - Typeset one or multiple paragraphs.
3+
"""
4+
5+
import io
6+
from collections.abc import Sequence
7+
from typing import Literal
8+
9+
from pygmt._typing import AnchorCode
10+
from pygmt.clib import Session
11+
from pygmt.exceptions import GMTInvalidInput
12+
from pygmt.helpers import (
13+
_check_encoding,
14+
build_arg_list,
15+
is_nonstr_iter,
16+
non_ascii_to_octal,
17+
)
18+
19+
20+
def _parse_option_f_upper(
21+
font: float | str | None, angle: float | None, justify: AnchorCode | None
22+
) -> str | None:
23+
"""
24+
Parse the font, angle, and justification arguments and return the string to be
25+
appended to the module options.
26+
27+
Examples
28+
--------
29+
>>> _parse_font_angle_justify(None, None, None)
30+
>>> _parse_font_angle_justify("10p", None, None)
31+
'+f10p'
32+
>>> _parse_font_angle_justify(None, 45, None)
33+
'+a45'
34+
>>> _parse_font_angle_justify(None, None, "CM")
35+
'+jCM'
36+
>>> _parse_font_angle_justify("10p,Helvetica-Bold", 45, "CM")
37+
'+f10p,Helvetica-Bold+a45+jCM'
38+
"""
39+
args = ((font, "+f"), (angle, "+a"), (justify, "+j"))
40+
if all(arg is None for arg, _ in args):
41+
return None
42+
return "".join(f"{flag}{arg}" for arg, flag in args if arg is not None)
43+
44+
45+
def paragraph(
46+
self,
47+
x: float | str,
48+
y: float | str,
49+
text: str | Sequence[str],
50+
parwidth: float | str,
51+
linespacing: float | str,
52+
font: float | str | None = None,
53+
angle: float | None = None,
54+
justify: AnchorCode | None = None,
55+
alignment: Literal["left", "center", "right", "justified"] = "left",
56+
):
57+
"""
58+
Typeset one or multiple paragraphs.
59+
60+
Parameters
61+
----------
62+
x/y
63+
The x, y coordinates of the paragraph.
64+
text
65+
The paragraph text to typeset. If a sequence of strings is provided, each
66+
string is treated as a separate paragraph.
67+
parwidth
68+
The width of the paragraph.
69+
linespacing
70+
The spacing between lines.
71+
font
72+
The font of the text.
73+
angle
74+
The angle of the text.
75+
justify
76+
The justification of the block of text, relative to the given x, y position.
77+
alignment
78+
The alignment of the text. Valid values are ``"left"``, ``"center"``,
79+
``"right"``, and ``"justified"``.
80+
"""
81+
self._preprocess()
82+
83+
# Validate 'alignment' argument.
84+
if alignment not in {"left", "center", "right", "justified"}:
85+
msg = (
86+
"Invalid value for 'alignment': {alignment}. "
87+
"Valid values are 'left', 'center', 'right', and 'justified'."
88+
)
89+
raise GMTInvalidInput(msg)
90+
91+
confdict = {}
92+
# Prepare the keyword dictionary for the module options
93+
kwdict = {"M": True, "F": _parse_option_f_upper(font, angle, justify)}
94+
95+
# Initialize a stringio object for storing the input data.
96+
stringio = io.StringIO()
97+
# The header line.
98+
stringio.write(f"> {x} {y} {linespacing} {parwidth} {alignment[0]}\n")
99+
# The text string to be written to the stringio object.
100+
# Multiple paragraphs are separated by a blank line "\n\n".
101+
_textstr: str = "\n\n".join(text) if is_nonstr_iter(text) else str(text)
102+
# Check the encoding of the text string and convert it to octal if necessary.
103+
if (encoding := _check_encoding(_textstr)) != "ascii":
104+
_textstr = non_ascii_to_octal(_textstr, encoding=encoding)
105+
confdict["PS_CHAR_ENCODING"] = encoding
106+
# Write the text string to the stringio object.
107+
stringio.write(_textstr)
108+
109+
with Session() as lib:
110+
with lib.virtualfile_from_stringio(stringio) as vfile:
111+
lib.call_module(
112+
"text", args=build_arg_list(kwdict, infile=vfile, confdict=confdict)
113+
)

pygmt/tests/test_paragraph.py

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""
2+
Tests for Figure.paragraph.
3+
"""
4+
5+
import pytest
6+
from pygmt import Figure
7+
8+
9+
@pytest.mark.mpl_image_compare
10+
def test_paragraph():
11+
"""
12+
Test typesetting a single paragraph.
13+
"""
14+
fig = Figure()
15+
fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True)
16+
fig.paragraph(
17+
x=4,
18+
y=4,
19+
text="This is a long paragraph. " * 10,
20+
parwidth="5c",
21+
linespacing="12p",
22+
)
23+
return fig
24+
25+
26+
@pytest.mark.mpl_image_compare
27+
def test_paragraph_multiple_paragraphs_list():
28+
"""
29+
Test typesetting a single paragraph.
30+
"""
31+
fig = Figure()
32+
fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True)
33+
fig.paragraph(
34+
x=4,
35+
y=4,
36+
text=[
37+
"This is the first paragraph. " * 5,
38+
"This is the second paragraph. " * 5,
39+
],
40+
parwidth="5c",
41+
linespacing="12p",
42+
)
43+
return fig
44+
45+
46+
@pytest.mark.mpl_image_compare
47+
def test_paragraph_multiple_paragraphs_blankline():
48+
"""
49+
Test typesetting a single paragraph.
50+
"""
51+
text = """
52+
This is the first paragraph.
53+
This is the first paragraph.
54+
This is the first paragraph.
55+
This is the first paragraph.
56+
This is the first paragraph.
57+
58+
This is the second paragraph.
59+
This is the second paragraph.
60+
This is the second paragraph.
61+
This is the second paragraph.
62+
This is the second paragraph.
63+
"""
64+
fig = Figure()
65+
fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True)
66+
fig.paragraph(x=4, y=4, text=text, parwidth="5c", linespacing="12p")
67+
return fig

0 commit comments

Comments
 (0)