Skip to content
1 change: 1 addition & 0 deletions doc/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Plotting map elements
Figure.legend
Figure.logo
Figure.magnetic_rose
Figure.paragraph
Figure.scalebar
Figure.solar
Figure.text
Expand Down
1 change: 1 addition & 0 deletions pygmt/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ def _repr_html_(self) -> str:
logo,
magnetic_rose,
meca,
paragraph,
plot,
plot3d,
psconvert,
Expand Down
1 change: 1 addition & 0 deletions pygmt/src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,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
Expand Down
139 changes: 139 additions & 0 deletions pygmt/src/paragraph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""
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,
)

__doctest_skip__ = ["paragraph"]


def paragraph( # noqa: PLR0913
self,
x: float | str,
y: float | str,
text: str | Sequence[str],
parwidth: float | str,
linespacing: float | str,
font: str | None = None,
angle: float | None = None,
justify: AnchorCode | None = None,
fill: str | None = None,
pen: str | None = None,
alignment: Literal["left", "center", "right", "justified"] = "left",
):
r"""
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 a blank line (``\n\n``)
separating the paragraphs.

Full GMT docs at :gmt-docs:`text.html`.

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
Set the alignment of the block of text, relative to the given x, y position.
Choose a :doc:`2-character justification code </techref/justification_codes>`.
fill
Set color for filling the paragraph box [Default is no fill].
pen
Set the pen used to draw a rectangle around the paragraph [Default is
``"0.25p,black,solid"``].
alignment
Set 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()

_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"),
],
G=fill,
W=pen,
)
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)

if _textstr == "":
raise GMTValueError(
text,
description="text",
reason="'text' must be a non-empty string or sequence of strings.",
)

# 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),
)
3 changes: 3 additions & 0 deletions pygmt/src/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ def text_( # noqa: PLR0912, PLR0913
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 more paragraphs of text, see
:meth:`pygmt.Figure.paragraph`.

Full GMT docs at :gmt-docs:`text.html`.

$aliases
Expand Down
5 changes: 5 additions & 0 deletions pygmt/tests/baseline/test_paragraph.png.dvc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
outs:
- md5: c5b1df47e811475defb0db79e49cab3d
size: 27632
hash: md5
path: test_paragraph.png
5 changes: 5 additions & 0 deletions pygmt/tests/baseline/test_paragraph_alignment.png.dvc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
outs:
- md5: a0ef6e989b11a252ec2a7ef497f3c789
size: 36274
hash: md5
path: test_paragraph_alignment.png
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
outs:
- md5: 6f55167eb6bc626b2bfee89ffe73faad
size: 48604
hash: md5
path: test_paragraph_font_angle_justify.png
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
outs:
- md5: 167d4be24bca4e287b2056ecbfbb629a
size: 29076
hash: md5
path: test_paragraph_multiple_paragraphs.png
97 changes: 97 additions & 0 deletions pygmt/tests/test_paragraph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""
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(filename="test_paragraph_multiple_paragraphs.png")
@pytest.mark.parametrize("inputtype", ["list", "string"])
def test_paragraph_multiple_paragraphs(inputtype):
"""
Test typesetting multiple paragraphs.
"""
if inputtype == "list":
text = [
"This is the first paragraph. " * 5,
"This is the second paragraph. " * 5,
]
else:
text = (
"This is the first paragraph. " * 5
+ "\n\n" # Separate the paragraphs with a blank line.
+ "This is the second paragraph. " * 5
)

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


@pytest.mark.mpl_image_compare
def test_paragraph_alignment():
"""
Test typesetting a single paragraph with different alignments.
"""
fig = Figure()
fig.basemap(region=[0, 10, 0, 8], projection="X10c/8c", frame=True)
for x, y, alignment in [
(5, 1, "left"),
(5, 3, "right"),
(5, 5, "center"),
(5, 7, "justified"),
]:
fig.paragraph(
x=x,
y=y,
text=alignment.upper() + " : " + "This is a long paragraph. " * 5,
parwidth="8c",
linespacing="12p",
alignment=alignment,
)
return fig


@pytest.mark.mpl_image_compare
def test_paragraph_font_angle_justify():
"""
Test typesetting a single paragraph with font, angle, and justify options.
"""
fig = Figure()
fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True)
fig.paragraph(
x=1,
y=4,
text="This is a long paragraph. " * 10,
parwidth="8c",
linespacing="12p",
font="10p,Helvetica-Bold,red",
angle=45,
justify="TL",
)
return fig
Loading