Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Figure.paragraph to typeset one or multiple paragraph of text strings #3709

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pygmt/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ def _repr_html_(self) -> str:
legend,
logo,
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 @@ -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
Expand Down
113 changes: 113 additions & 0 deletions pygmt/src/paragraph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""
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,Helvetica-Bold", 45, "CM")
'+f10p,Helvetica-Bold+a45+jCM'
"""
args = ((font, "+f"), (angle, "+a"), (justify, "+j"))
if all(arg is None for arg, _ in args):
return None
return "".join(f"{flag}{arg}" for arg, flag in args 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 = (

Check warning on line 85 in pygmt/src/paragraph.py

View check run for this annotation

Codecov / codecov/patch

pygmt/src/paragraph.py#L85

Added line #L85 was not covered by tests
"Invalid value for 'alignment': {alignment}. "
"Valid values are 'left', 'center', 'right', and 'justified'."
)
raise GMTInvalidInput(msg)

Check warning on line 89 in pygmt/src/paragraph.py

View check run for this annotation

Codecov / codecov/patch

pygmt/src/paragraph.py#L89

Added line #L89 was not covered by tests

confdict = {}
# Prepare the keyword dictionary for the module options
kwdict = {"M": True, "F": _parse_font_angle_justify(font, angle, justify)}

# Initialize a stringio object for storing the input data.
stringio = io.StringIO()
# The header line.
stringio.write(f"> {x} {y} {linespacing} {parwidth} {alignment[0]}\n")
# The text string to be written to the 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
# Write the text string to the stringio object.
stringio.write(_textstr)

with Session() as lib:
with lib.virtualfile_from_stringio(stringio) as vfile:
lib.call_module(
"text", args=build_arg_list(kwdict, infile=vfile, confdict=confdict)
)
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
outs:
- md5: 0df1eb71a781f0b8cc7c48be860dd321
size: 29109
hash: md5
path: test_paragraph_multiple_paragraphs_blankline.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_list.png
67 changes: 67 additions & 0 deletions pygmt/tests/test_paragraph.py
Original file line number Diff line number Diff line change
@@ -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
Loading