Skip to content

Commit 425e1b6

Browse files
committed
Figure.paragraph: Initial implementation focusing on input data
1 parent ca4503a commit 425e1b6

7 files changed

+195
-0
lines changed

pygmt/figure.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ def _repr_html_(self) -> str:
426426
legend,
427427
logo,
428428
meca,
429+
paragraph,
429430
plot,
430431
plot3d,
431432
psconvert,

pygmt/src/__init__.py

Lines changed: 1 addition & 0 deletions
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

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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_font_angle_justify(
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", 45, None)
37+
'+f10p+a45'
38+
>>> _parse_font_angle_justify("10p,Helvetica-Bold", 45, "CM")
39+
'+f10p,Helvetica-Bold+a45+jCM'
40+
"""
41+
args = {"+f": font, "+a": angle, "+j": justify}
42+
if all(arg is None for arg in args.values()):
43+
return None
44+
return "".join(f"{prefix}{arg}" for prefix, arg in args.items() if arg is not None)
45+
46+
47+
def paragraph(
48+
self,
49+
x: float | str,
50+
y: float | str,
51+
text: str | Sequence[str],
52+
parwidth: float | str,
53+
linespacing: float | str,
54+
font: float | str | None = None,
55+
angle: float | None = None,
56+
justify: AnchorCode | None = None,
57+
alignment: Literal["left", "center", "right", "justified"] = "left",
58+
):
59+
"""
60+
Typeset one or multiple paragraphs.
61+
62+
Parameters
63+
----------
64+
x/y
65+
The x, y coordinates of the paragraph.
66+
text
67+
The paragraph text to typeset. If a sequence of strings is provided, each string
68+
is treated as a separate paragraph.
69+
parwidth
70+
The width of the paragraph.
71+
linespacing
72+
The spacing between lines.
73+
font
74+
The font of the text.
75+
angle
76+
The angle of the text.
77+
justify
78+
The justification of the block of text, relative to the given x, y position.
79+
alignment
80+
The alignment of the text. Valid values are ``"left"``, ``"center"``,
81+
``"right"``, and ``"justified"``.
82+
"""
83+
self._preprocess()
84+
85+
# Validate 'alignment' argument.
86+
if alignment not in {"left", "center", "right", "justified"}:
87+
msg = (
88+
"Invalid value for 'alignment': {alignment}. "
89+
"Valid values are 'left', 'center', 'right', and 'justified'."
90+
)
91+
raise GMTInvalidInput(msg)
92+
93+
confdict = {}
94+
# Prepare the keyword dictionary for the module options
95+
kwdict = {"M": True, "F": _parse_font_angle_justify(font, angle, justify)}
96+
# Prepare the text string that will be passed to an io.StringIO object.
97+
# Multiple paragraphs are separated by a blank line "\n\n".
98+
_textstr: str = "\n\n".join(text) if is_nonstr_iter(text) else str(text)
99+
# Check the encoding of the text string and convert it to octal if necessary.
100+
if (encoding := _check_encoding(_textstr)) != "ascii":
101+
_textstr = non_ascii_to_octal(_textstr, encoding=encoding)
102+
confdict["PS_CHAR_ENCODING"] = encoding
103+
104+
with Session() as lib:
105+
with io.StringIO() as buffer: # Prepare the StringIO input.
106+
buffer.write(f"> {x} {y} {linespacing} {parwidth} {alignment[0]}\n")
107+
buffer.write(_textstr)
108+
with lib.virtualfile_in(data=buffer) as vfile:
109+
lib.call_module(
110+
"text", args=build_arg_list(kwdict, infile=vfile, confdict=confdict)
111+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: c5b1df47e811475defb0db79e49cab3d
3+
size: 27632
4+
hash: md5
5+
path: test_paragraph.png
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: 0df1eb71a781f0b8cc7c48be860dd321
3+
size: 29109
4+
hash: md5
5+
path: test_paragraph_multiple_paragraphs_blankline.png
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: 167d4be24bca4e287b2056ecbfbb629a
3+
size: 29076
4+
hash: md5
5+
path: test_paragraph_multiple_paragraphs_list.png

pygmt/tests/test_paragraph.py

Lines changed: 67 additions & 0 deletions
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)