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

Enhance text with extra functionality and aliases #481

Merged
merged 16 commits into from Jun 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
141 changes: 104 additions & 37 deletions pygmt/base_plotting.py
Expand Up @@ -4,7 +4,6 @@
"""
import contextlib
import csv
import os
import numpy as np
import pandas as pd

Expand Down Expand Up @@ -813,7 +812,15 @@ def legend(self, spec=None, position="JTR+jTR+o0.2c", box="+gwhite+p1p", **kwarg
lib.call_module("legend", arg_str)

@fmt_docstring
@use_alias(R="region", J="projection", B="frame")
@use_alias(
R="region",
J="projection",
B="frame",
C="clearance",
D="offset",
G="fill",
W="pen",
)
@kwargs_to_strings(
R="sequence",
textfiles="sequence_space",
Expand All @@ -826,20 +833,22 @@ def text(
textfiles=None,
x=None,
y=None,
position=None,
text=None,
angle=None,
font=None,
justify=None,
**kwargs,
):
"""
Plot or typeset text on maps
Plot or typeset text strings of variable size, font type, and
orientation.

Used to be pstext.
Must provide at least one of the following combinations as input:

Takes in textfile(s) or (x,y,text) triples as input.

Must provide at least *textfiles* or *x*, *y*, and *text*.
- *textfiles*
- *x*, *y*, and *text*
- *position* and *text*

Full option list at :gmt-docs:`text.html`

Expand All @@ -849,70 +858,128 @@ def text(
----------
textfiles : str or list
A text data file name, or a list of filenames containing 1 or more
records with (x, y[, font, angle, justify], text).
records with (x, y[, angle, font, justify], text).
x/y : float or 1d arrays
The x and y coordinates, or an array of x and y coordinates to plot
the text
position : str
Sets reference point on the map for the text by using x,y
coordinates extracted from *region* instead of providing them
through *x* and *y*. Specify with a two letter (order independent)
code, chosen from:

* Horizontal: L(eft), C(entre), R(ight)
* Vertical: T(op), M(iddle), B(ottom)

For example, position="TL" plots the text at the Upper Left corner
of the map.
text : str or 1d array
The text string, or an array of strings to plot on the figure
angle: int, float or bool
angle: int, float, str or bool
Set the angle measured in degrees counter-clockwise from
horizontal. E.g. 30 sets the text at 30 degrees. If no angle is
given then the input textfile(s) must have this as a column.
explicitly given (i.e. angle=True) then the input textfile(s) must
have this as a column.
font : str or bool
Set the font specification with format "size,font,color" where size
is text size in points, font is the font to use, and color sets the
font color. E.g. "12p,Helvetica-Bold,red" selects a 12p red
Helvetica-Bold font. If no font info is given then the input
textfile(s) must have this information in one of its columns.
justify: str or bool
Helvetica-Bold font. If no font info is explicitly given (i.e.
font=True), then the input textfile(s) must have this information
in one of its columns.
justify : str or bool
Set the alignment which refers to the part of the text string that
will be mapped onto the (x,y) point. Choose a 2 character
combination of L, C, R (for left, center, or right) and T, M, B for
top, middle, or bottom. E.g., BL for lower left. If no
justification is given then the input textfile(s) must have this as
a column.
justification is explicitly given (i.e. justify=True), then the
input textfile(s) must have this as a column.
{J}
{R}
clearance : str
``[dx/dy][+to|O|c|C]``
Adjust the clearance between the text and the surrounding box
[15%]. Only used if *pen* or *fill* are specified. Append the unit
you want ('c' for cm, 'i' for inch, or 'p' for point; if not given
we consult 'PROJ_LENGTH_UNIT') or '%' for a percentage of the
font size. Optionally, use modifier '+t' to set the shape of the
textbox when using *fill* and/or *pen*. Append lower case 'o' to
get a straight rectangle [Default]. Append upper case 'O' to get a
rounded rectangle. In paragraph mode (*paragraph*) you can also
append lower case 'c' to get a concave rectangle or append upper
case 'C' to get a convex rectangle.
fill : str
Sets the shade or color used for filling the text box [Default is
no fill].
offset : str
``[j|J]dx[/dy][+v[pen]]``
Offsets the text from the projected (x,y) point by dx,dy [0/0]. If
dy is not specified then it is set equal to dx. Use offset='j' to
offset the text away from the point instead (i.e., the text
justification will determine the direction of the shift). Using
offset='J' will shorten diagonal offsets at corners by sqrt(2).
Optionally, append '+v' which will draw a line from the original
point to the shifted point; append a pen to change the attributes
for this line.
pen : str
Sets the pen used to draw a rectangle around the text string
(see *clearance*) [Default is width = default, color = black,
style = solid].
"""
kwargs = self._preprocess(**kwargs)

kind = data_kind(textfiles, x, y, text)
if kind == "vectors" and text is None:
raise GMTInvalidInput("Must provide text with x and y.")
if kind == "file":
for textfile in textfiles.split(" "): # ensure that textfile(s) exist
if not os.path.exists(textfile):
raise GMTInvalidInput(f"Cannot find the file: {textfile}")
# Ensure inputs are either textfiles, x/y/text, or position/text
if position is None:
kind = data_kind(textfiles, x, y, text)
elif position is not None:
if x is not None or y is not None:
raise GMTInvalidInput(
"Provide either position only, or x/y pairs, not both"
)
kind = "vectors"

if angle is not None or font is not None or justify is not None:
if kind == "vectors" and text is None:
raise GMTInvalidInput("Must provide text with x/y pairs or position")

# Build the `-F` argument in gmt text.
if (
position is not None
or angle is not None
or font is not None
or justify is not None
):
if "F" not in kwargs.keys():
kwargs.update({"F": ""})
if angle is not None and isinstance(angle, (int, float)):
if angle is not None and isinstance(angle, (int, float, str)):
kwargs["F"] += f"+a{str(angle)}"
if font is not None and isinstance(font, str):
kwargs["F"] += f"+f{font}"
if justify is not None and isinstance(justify, str):
kwargs["F"] += f"+j{justify}"
if position is not None and isinstance(position, str):
kwargs["F"] += f'+c{position}+t"{text}"'

with GMTTempFile(suffix=".txt") as tmpfile:
with Session() as lib:
if kind == "file":
fname = textfiles
elif kind == "vectors":
pd.DataFrame.from_dict(
{
"x": np.atleast_1d(x),
"y": np.atleast_1d(y),
"text": np.atleast_1d(text),
}
).to_csv(
tmpfile.name,
sep="\t",
header=False,
index=False,
quoting=csv.QUOTE_NONE,
)
if position is not None:
fname = ""
else:
pd.DataFrame.from_dict(
{
"x": np.atleast_1d(x),
"y": np.atleast_1d(y),
"text": np.atleast_1d(text),
}
).to_csv(
tmpfile.name,
sep="\t",
header=False,
index=False,
quoting=csv.QUOTE_NONE,
)
Comment on lines +970 to +982
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pd.DataFrame.from_dict(
{
"x": np.atleast_1d(x),
"y": np.atleast_1d(y),
"text": np.atleast_1d(text),
}
).to_csv(
tmpfile.name,
sep="\t",
header=False,
index=False,
quoting=csv.QUOTE_NONE,
)
file_context = lib.virtualfile_from_vectors(
np.atleast_1d(x), np.atleast_1d(y), np.atleast_1d(text)
)

@leouieda mentioned before at #321 (comment) to use virtualfiles, and I finally figured out what he meant. Unfortunately, virtualfile_from_vectors doesn't yet support str types. This is the error on _check_dtype_and_dim:

        if array.dtype.name not in DTYPES:
            raise GMTInvalidInput(
>               "Unsupported numpy data type '{}'.".format(array.dtype.name)
            )
E           pygmt.exceptions.GMTInvalidInput: Unsupported numpy data type 'str864'.

From what I can tell, we'll need to use GMT_Put_Strings is that right? We should do this refactor in a separate PR, this one is already getting a bit too long.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, as I understand it, we need to use GMT_Put_Strings to pass trailing strings to GMT.

fname = tmpfile.name

arg_str = " ".join([fname, build_arg_string(kwargs)])
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added pygmt/tests/baseline/test_text_fill.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added pygmt/tests/baseline/test_text_pen.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added pygmt/tests/baseline/test_text_position.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
127 changes: 125 additions & 2 deletions pygmt/tests/test_text.py
Expand Up @@ -7,7 +7,8 @@
import pytest

from .. import Figure
from ..exceptions import GMTInvalidInput
from ..exceptions import GMTCLibError, GMTInvalidInput
from ..helpers import GMTTempFile

TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
POINTS_DATA = os.path.join(TEST_DATA_DIR, "points.txt")
Expand Down Expand Up @@ -77,6 +78,16 @@ def test_text_input_single_filename():
return fig


@pytest.mark.mpl_image_compare
def test_text_input_remote_filename():
"""
Run text by passing in a remote filename to textfiles
"""
fig = Figure()
fig.text(region=[0, 6.5, 0, 6.5], textfiles="@Table_5_11.txt")
return fig


@pytest.mark.mpl_image_compare
def test_text_input_multiple_filenames():
"""
Expand All @@ -92,10 +103,48 @@ def test_text_nonexistent_filename():
Run text by passing in a list of filenames with one that does not exist
"""
fig = Figure()
with pytest.raises(GMTInvalidInput):
with pytest.raises(GMTCLibError):
fig.text(region=[10, 70, -5, 10], textfiles=[POINTS_DATA, "notexist.txt"])


@pytest.mark.mpl_image_compare
def test_text_position(region):
"""
Print text at center middle (CM) and eight other positions
(Top/Middle/Bottom x Left/Centre/Right).
"""
fig = Figure()
fig.text(region=region, projection="x1c", frame="a", position="CM", text="C M")
seisman marked this conversation as resolved.
Show resolved Hide resolved
for position in ("TL", "TC", "TR", "ML", "MR", "BL", "BC", "BR"):
fig.text(position=position, text=position)
return fig


def test_text_xy_with_position_fails(region):
"""
Run text by providing both x/y pairs and position arguments.
"""
fig = Figure()
with pytest.raises(GMTInvalidInput):
fig.text(
region=region, projection="x1c", x=1.2, y=2.4, position="MC", text="text"
)


@pytest.mark.mpl_image_compare
def test_text_position_offset_with_line(region):
weiji14 marked this conversation as resolved.
Show resolved Hide resolved
"""
Print text at centre middle (CM) and eight other positions
(Top/Middle/Bottom x Left/Centre/Right), offset by 0.5 cm, with a line
drawn from the original to the shifted point.
"""
fig = Figure()
fig.text(region=region, projection="x1c", frame="a", position="CM", text="C M")
for position in ("TL", "TC", "TR", "ML", "MR", "BL", "BC", "BR"):
fig.text(position=position, text=position, offset="j0.5c+v")
return fig


@pytest.mark.mpl_image_compare
def test_text_angle_30(region, projection):
"""
Expand Down Expand Up @@ -130,6 +179,58 @@ def test_text_font_bold(region, projection):
return fig


@pytest.mark.mpl_image_compare
def test_text_fill(region, projection):
"""
Print text with blue color fill
"""
fig = Figure()
fig.text(
region=region,
projection=projection,
x=1.2,
y=1.2,
text="blue fill around text",
fill="blue",
)
return fig


@pytest.mark.mpl_image_compare
def test_text_pen(region, projection):
"""
Print text with thick green dashed pen
"""
fig = Figure()
fig.text(
region=region,
projection=projection,
x=1.2,
y=1.2,
text="green pen around text",
pen="thick,green,dashed",
)
return fig


@pytest.mark.mpl_image_compare
def test_text_round_clearance(region, projection):
"""
Print text with round rectangle box clearance
"""
fig = Figure()
fig.text(
region=region,
projection=projection,
x=1.2,
y=1.2,
text="clearance around text",
clearance="90%+tO",
pen="default,black,dashed",
)
return fig


@pytest.mark.mpl_image_compare
def test_text_justify_bottom_right_and_top_left(region, projection):
"""
Expand Down Expand Up @@ -172,3 +273,25 @@ def test_text_justify_parsed_from_textfile():
D="j0.45/0+vred", # draw red-line from xy point to text label (city name)
)
return fig


@pytest.mark.mpl_image_compare
def test_text_angle_font_justify_from_textfile():
"""
Print text with x, y, angle, font, justify, and text arguments parsed from
the textfile.
"""
fig = Figure()
with GMTTempFile(suffix=".txt") as tempfile:
with open(tempfile.name, "w") as tmpfile:
tmpfile.write("114 0.5 30 22p,Helvetica-Bold,black LM BORNEO")
fig.text(
region=[113, 117.5, -0.5, 3],
projection="M5c",
frame="a",
textfiles=tempfile.name,
angle=True,
font=True,
justify=True,
)
return fig