Skip to content

Commit

Permalink
Figure.paragraph: Initial implementation focusing on input data
Browse files Browse the repository at this point in the history
  • Loading branch information
seisman committed Jan 6, 2025
1 parent 7c38ce7 commit a617468
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 0 deletions.
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_option_f_upper(
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 = (
"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_option_f_upper(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)
)
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

0 comments on commit a617468

Please sign in to comment.