Skip to content

Commit 86c46f0

Browse files
weiji14seisman
andauthored
Enhance text with extra functionality and aliases (#481)
Adding extra functionality to text, including aliases that were missing in initial implementation at #321, in support of the text tutorial at #480. Allow passing str type into angle argument of text. Aliased pen(W), clearance(C), fill(G), and offset(D). Enable text placement using position (`-F+c`) argument instead of x/y pairs. Reorder docstring to say 'angle, font, justify' instead of 'font, angle, justify'. Added test_text_position to plot at all 9 possible positions. Removed check for missing file in textfiles, and checked that plotting text from an external file (@Table_5_11.txt) works. Co-authored-by: Dongdong Tian <[email protected]>
1 parent 0267dd1 commit 86c46f0

9 files changed

+229
-39
lines changed

pygmt/base_plotting.py

+104-37
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
"""
55
import contextlib
66
import csv
7-
import os
87
import numpy as np
98
import pandas as pd
109

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

815814
@fmt_docstring
816-
@use_alias(R="region", J="projection", B="frame")
815+
@use_alias(
816+
R="region",
817+
J="projection",
818+
B="frame",
819+
C="clearance",
820+
D="offset",
821+
G="fill",
822+
W="pen",
823+
)
817824
@kwargs_to_strings(
818825
R="sequence",
819826
textfiles="sequence_space",
@@ -826,20 +833,22 @@ def text(
826833
textfiles=None,
827834
x=None,
828835
y=None,
836+
position=None,
829837
text=None,
830838
angle=None,
831839
font=None,
832840
justify=None,
833841
**kwargs,
834842
):
835843
"""
836-
Plot or typeset text on maps
844+
Plot or typeset text strings of variable size, font type, and
845+
orientation.
837846
838-
Used to be pstext.
847+
Must provide at least one of the following combinations as input:
839848
840-
Takes in textfile(s) or (x,y,text) triples as input.
841-
842-
Must provide at least *textfiles* or *x*, *y*, and *text*.
849+
- *textfiles*
850+
- *x*, *y*, and *text*
851+
- *position* and *text*
843852
844853
Full option list at :gmt-docs:`text.html`
845854
@@ -849,70 +858,128 @@ def text(
849858
----------
850859
textfiles : str or list
851860
A text data file name, or a list of filenames containing 1 or more
852-
records with (x, y[, font, angle, justify], text).
861+
records with (x, y[, angle, font, justify], text).
853862
x/y : float or 1d arrays
854863
The x and y coordinates, or an array of x and y coordinates to plot
855864
the text
865+
position : str
866+
Sets reference point on the map for the text by using x,y
867+
coordinates extracted from *region* instead of providing them
868+
through *x* and *y*. Specify with a two letter (order independent)
869+
code, chosen from:
870+
871+
* Horizontal: L(eft), C(entre), R(ight)
872+
* Vertical: T(op), M(iddle), B(ottom)
873+
874+
For example, position="TL" plots the text at the Upper Left corner
875+
of the map.
856876
text : str or 1d array
857877
The text string, or an array of strings to plot on the figure
858-
angle: int, float or bool
878+
angle: int, float, str or bool
859879
Set the angle measured in degrees counter-clockwise from
860880
horizontal. E.g. 30 sets the text at 30 degrees. If no angle is
861-
given then the input textfile(s) must have this as a column.
881+
explicitly given (i.e. angle=True) then the input textfile(s) must
882+
have this as a column.
862883
font : str or bool
863884
Set the font specification with format "size,font,color" where size
864885
is text size in points, font is the font to use, and color sets the
865886
font color. E.g. "12p,Helvetica-Bold,red" selects a 12p red
866-
Helvetica-Bold font. If no font info is given then the input
867-
textfile(s) must have this information in one of its columns.
868-
justify: str or bool
887+
Helvetica-Bold font. If no font info is explicitly given (i.e.
888+
font=True), then the input textfile(s) must have this information
889+
in one of its columns.
890+
justify : str or bool
869891
Set the alignment which refers to the part of the text string that
870892
will be mapped onto the (x,y) point. Choose a 2 character
871893
combination of L, C, R (for left, center, or right) and T, M, B for
872894
top, middle, or bottom. E.g., BL for lower left. If no
873-
justification is given then the input textfile(s) must have this as
874-
a column.
895+
justification is explicitly given (i.e. justify=True), then the
896+
input textfile(s) must have this as a column.
875897
{J}
876898
{R}
899+
clearance : str
900+
``[dx/dy][+to|O|c|C]``
901+
Adjust the clearance between the text and the surrounding box
902+
[15%]. Only used if *pen* or *fill* are specified. Append the unit
903+
you want ('c' for cm, 'i' for inch, or 'p' for point; if not given
904+
we consult 'PROJ_LENGTH_UNIT') or '%' for a percentage of the
905+
font size. Optionally, use modifier '+t' to set the shape of the
906+
textbox when using *fill* and/or *pen*. Append lower case 'o' to
907+
get a straight rectangle [Default]. Append upper case 'O' to get a
908+
rounded rectangle. In paragraph mode (*paragraph*) you can also
909+
append lower case 'c' to get a concave rectangle or append upper
910+
case 'C' to get a convex rectangle.
911+
fill : str
912+
Sets the shade or color used for filling the text box [Default is
913+
no fill].
914+
offset : str
915+
``[j|J]dx[/dy][+v[pen]]``
916+
Offsets the text from the projected (x,y) point by dx,dy [0/0]. If
917+
dy is not specified then it is set equal to dx. Use offset='j' to
918+
offset the text away from the point instead (i.e., the text
919+
justification will determine the direction of the shift). Using
920+
offset='J' will shorten diagonal offsets at corners by sqrt(2).
921+
Optionally, append '+v' which will draw a line from the original
922+
point to the shifted point; append a pen to change the attributes
923+
for this line.
924+
pen : str
925+
Sets the pen used to draw a rectangle around the text string
926+
(see *clearance*) [Default is width = default, color = black,
927+
style = solid].
877928
"""
878929
kwargs = self._preprocess(**kwargs)
879930

880-
kind = data_kind(textfiles, x, y, text)
881-
if kind == "vectors" and text is None:
882-
raise GMTInvalidInput("Must provide text with x and y.")
883-
if kind == "file":
884-
for textfile in textfiles.split(" "): # ensure that textfile(s) exist
885-
if not os.path.exists(textfile):
886-
raise GMTInvalidInput(f"Cannot find the file: {textfile}")
931+
# Ensure inputs are either textfiles, x/y/text, or position/text
932+
if position is None:
933+
kind = data_kind(textfiles, x, y, text)
934+
elif position is not None:
935+
if x is not None or y is not None:
936+
raise GMTInvalidInput(
937+
"Provide either position only, or x/y pairs, not both"
938+
)
939+
kind = "vectors"
887940

888-
if angle is not None or font is not None or justify is not None:
941+
if kind == "vectors" and text is None:
942+
raise GMTInvalidInput("Must provide text with x/y pairs or position")
943+
944+
# Build the `-F` argument in gmt text.
945+
if (
946+
position is not None
947+
or angle is not None
948+
or font is not None
949+
or justify is not None
950+
):
889951
if "F" not in kwargs.keys():
890952
kwargs.update({"F": ""})
891-
if angle is not None and isinstance(angle, (int, float)):
953+
if angle is not None and isinstance(angle, (int, float, str)):
892954
kwargs["F"] += f"+a{str(angle)}"
893955
if font is not None and isinstance(font, str):
894956
kwargs["F"] += f"+f{font}"
895957
if justify is not None and isinstance(justify, str):
896958
kwargs["F"] += f"+j{justify}"
959+
if position is not None and isinstance(position, str):
960+
kwargs["F"] += f'+c{position}+t"{text}"'
897961

898962
with GMTTempFile(suffix=".txt") as tmpfile:
899963
with Session() as lib:
900964
if kind == "file":
901965
fname = textfiles
902966
elif kind == "vectors":
903-
pd.DataFrame.from_dict(
904-
{
905-
"x": np.atleast_1d(x),
906-
"y": np.atleast_1d(y),
907-
"text": np.atleast_1d(text),
908-
}
909-
).to_csv(
910-
tmpfile.name,
911-
sep="\t",
912-
header=False,
913-
index=False,
914-
quoting=csv.QUOTE_NONE,
915-
)
967+
if position is not None:
968+
fname = ""
969+
else:
970+
pd.DataFrame.from_dict(
971+
{
972+
"x": np.atleast_1d(x),
973+
"y": np.atleast_1d(y),
974+
"text": np.atleast_1d(text),
975+
}
976+
).to_csv(
977+
tmpfile.name,
978+
sep="\t",
979+
header=False,
980+
index=False,
981+
quoting=csv.QUOTE_NONE,
982+
)
916983
fname = tmpfile.name
917984

918985
arg_str = " ".join([fname, build_arg_string(kwargs)])
Loading
1.87 KB
Loading
Loading
2.45 KB
Loading
10.3 KB
Loading
Loading
Loading

pygmt/tests/test_text.py

+125-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import pytest
88

99
from .. import Figure
10-
from ..exceptions import GMTInvalidInput
10+
from ..exceptions import GMTCLibError, GMTInvalidInput
11+
from ..helpers import GMTTempFile
1112

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

7980

81+
@pytest.mark.mpl_image_compare
82+
def test_text_input_remote_filename():
83+
"""
84+
Run text by passing in a remote filename to textfiles
85+
"""
86+
fig = Figure()
87+
fig.text(region=[0, 6.5, 0, 6.5], textfiles="@Table_5_11.txt")
88+
return fig
89+
90+
8091
@pytest.mark.mpl_image_compare
8192
def test_text_input_multiple_filenames():
8293
"""
@@ -92,10 +103,48 @@ def test_text_nonexistent_filename():
92103
Run text by passing in a list of filenames with one that does not exist
93104
"""
94105
fig = Figure()
95-
with pytest.raises(GMTInvalidInput):
106+
with pytest.raises(GMTCLibError):
96107
fig.text(region=[10, 70, -5, 10], textfiles=[POINTS_DATA, "notexist.txt"])
97108

98109

110+
@pytest.mark.mpl_image_compare
111+
def test_text_position(region):
112+
"""
113+
Print text at center middle (CM) and eight other positions
114+
(Top/Middle/Bottom x Left/Centre/Right).
115+
"""
116+
fig = Figure()
117+
fig.text(region=region, projection="x1c", frame="a", position="CM", text="C M")
118+
for position in ("TL", "TC", "TR", "ML", "MR", "BL", "BC", "BR"):
119+
fig.text(position=position, text=position)
120+
return fig
121+
122+
123+
def test_text_xy_with_position_fails(region):
124+
"""
125+
Run text by providing both x/y pairs and position arguments.
126+
"""
127+
fig = Figure()
128+
with pytest.raises(GMTInvalidInput):
129+
fig.text(
130+
region=region, projection="x1c", x=1.2, y=2.4, position="MC", text="text"
131+
)
132+
133+
134+
@pytest.mark.mpl_image_compare
135+
def test_text_position_offset_with_line(region):
136+
"""
137+
Print text at centre middle (CM) and eight other positions
138+
(Top/Middle/Bottom x Left/Centre/Right), offset by 0.5 cm, with a line
139+
drawn from the original to the shifted point.
140+
"""
141+
fig = Figure()
142+
fig.text(region=region, projection="x1c", frame="a", position="CM", text="C M")
143+
for position in ("TL", "TC", "TR", "ML", "MR", "BL", "BC", "BR"):
144+
fig.text(position=position, text=position, offset="j0.5c+v")
145+
return fig
146+
147+
99148
@pytest.mark.mpl_image_compare
100149
def test_text_angle_30(region, projection):
101150
"""
@@ -130,6 +179,58 @@ def test_text_font_bold(region, projection):
130179
return fig
131180

132181

182+
@pytest.mark.mpl_image_compare
183+
def test_text_fill(region, projection):
184+
"""
185+
Print text with blue color fill
186+
"""
187+
fig = Figure()
188+
fig.text(
189+
region=region,
190+
projection=projection,
191+
x=1.2,
192+
y=1.2,
193+
text="blue fill around text",
194+
fill="blue",
195+
)
196+
return fig
197+
198+
199+
@pytest.mark.mpl_image_compare
200+
def test_text_pen(region, projection):
201+
"""
202+
Print text with thick green dashed pen
203+
"""
204+
fig = Figure()
205+
fig.text(
206+
region=region,
207+
projection=projection,
208+
x=1.2,
209+
y=1.2,
210+
text="green pen around text",
211+
pen="thick,green,dashed",
212+
)
213+
return fig
214+
215+
216+
@pytest.mark.mpl_image_compare
217+
def test_text_round_clearance(region, projection):
218+
"""
219+
Print text with round rectangle box clearance
220+
"""
221+
fig = Figure()
222+
fig.text(
223+
region=region,
224+
projection=projection,
225+
x=1.2,
226+
y=1.2,
227+
text="clearance around text",
228+
clearance="90%+tO",
229+
pen="default,black,dashed",
230+
)
231+
return fig
232+
233+
133234
@pytest.mark.mpl_image_compare
134235
def test_text_justify_bottom_right_and_top_left(region, projection):
135236
"""
@@ -172,3 +273,25 @@ def test_text_justify_parsed_from_textfile():
172273
D="j0.45/0+vred", # draw red-line from xy point to text label (city name)
173274
)
174275
return fig
276+
277+
278+
@pytest.mark.mpl_image_compare
279+
def test_text_angle_font_justify_from_textfile():
280+
"""
281+
Print text with x, y, angle, font, justify, and text arguments parsed from
282+
the textfile.
283+
"""
284+
fig = Figure()
285+
with GMTTempFile(suffix=".txt") as tempfile:
286+
with open(tempfile.name, "w") as tmpfile:
287+
tmpfile.write("114 0.5 30 22p,Helvetica-Bold,black LM BORNEO")
288+
fig.text(
289+
region=[113, 117.5, -0.5, 3],
290+
projection="M5c",
291+
frame="a",
292+
textfiles=tempfile.name,
293+
angle=True,
294+
font=True,
295+
justify=True,
296+
)
297+
return fig

0 commit comments

Comments
 (0)