Skip to content

Commit ca50611

Browse files
committed
Figure.legend: Support passing a StringIO object as the legend specification
1 parent 2bb65df commit ca50611

File tree

2 files changed

+81
-52
lines changed

2 files changed

+81
-52
lines changed

pygmt/src/legend.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
legend - Plot a legend.
33
"""
44

5+
import io
6+
import pathlib
7+
58
from pygmt.clib import Session
69
from pygmt.exceptions import GMTInvalidInput
710
from pygmt.helpers import (
@@ -26,7 +29,13 @@
2629
t="transparency",
2730
)
2831
@kwargs_to_strings(R="sequence", c="sequence_comma", p="sequence")
29-
def legend(self, spec=None, position="JTR+jTR+o0.2c", box="+gwhite+p1p", **kwargs):
32+
def legend(
33+
self,
34+
spec: str | pathlib.PurePath | io.StringIO | None = None,
35+
position="JTR+jTR+o0.2c",
36+
box="+gwhite+p1p",
37+
**kwargs,
38+
):
3039
r"""
3140
Plot legends on maps.
3241
@@ -42,10 +51,15 @@ def legend(self, spec=None, position="JTR+jTR+o0.2c", box="+gwhite+p1p", **kwarg
4251
4352
Parameters
4453
----------
45-
spec : None or str
46-
Either ``None`` [Default] for using the automatically generated legend
47-
specification file, or a *filename* pointing to the legend
48-
specification file.
54+
spec
55+
The legend specification. It can be:
56+
57+
- ``None`` means using the automatically generated legend specification file.
58+
- A string or a :class:`pathlib.PurePath` object pointing to the legend
59+
specification file.
60+
- A :class:`io.StringIO` object containing the legend specification.
61+
62+
See :gmt-docs:`legend.html` for the definition of the legend specification.
4963
{projection}
5064
{region}
5165
position : str
@@ -75,12 +89,12 @@ def legend(self, spec=None, position="JTR+jTR+o0.2c", box="+gwhite+p1p", **kwarg
7589
if kwargs.get("F") is None:
7690
kwargs["F"] = box
7791

92+
kind = data_kind(spec)
93+
if kind not in {"vectors", "file", "stringio"}: # kind="vectors" means spec is None
94+
raise GMTInvalidInput(f"Unrecognized data type: {type(spec)}")
95+
if kind == "file" and is_nonstr_iter(spec):
96+
raise GMTInvalidInput("Only one legend specification file is allowed.")
97+
7898
with Session() as lib:
79-
if spec is None:
80-
specfile = ""
81-
elif data_kind(spec) == "file" and not is_nonstr_iter(spec):
82-
# Is a file but not a list of files
83-
specfile = spec
84-
else:
85-
raise GMTInvalidInput(f"Unrecognized data type: {type(spec)}")
86-
lib.call_module(module="legend", args=build_arg_list(kwargs, infile=specfile))
99+
with lib.virtualfile_in(data=spec, required_data=False) as vintbl:
100+
lib.call_module(module="legend", args=build_arg_list(kwargs, infile=vintbl))

pygmt/tests/test_legend.py

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Test Figure.legend.
33
"""
44

5+
import io
56
from pathlib import Path
67

78
import pytest
@@ -10,12 +11,44 @@
1011
from pygmt.helpers import GMTTempFile
1112

1213

14+
@pytest.fixture(scope="module", name="legend_spec")
15+
def fixture_legend_spec():
16+
"""
17+
A string contains a legend specification.
18+
"""
19+
return """
20+
G -0.1i
21+
H 24 Times-Roman My Map Legend
22+
D 0.2i 1p
23+
N 2
24+
V 0 1p
25+
S 0.1i c 0.15i p300/12 0.25p 0.3i This circle is hachured
26+
S 0.1i e 0.15i yellow 0.25p 0.3i This ellipse is yellow
27+
S 0.1i w 0.15i green 0.25p 0.3i This wedge is green
28+
S 0.1i f0.1i+l+t 0.25i blue 0.25p 0.3i This is a fault
29+
S 0.1i - 0.15i - 0.25p,- 0.3i A dashed contour
30+
S 0.1i v0.1i+a40+e 0.25i magenta 0.25p 0.3i This is a vector
31+
S 0.1i i 0.15i cyan 0.25p 0.3i This triangle is boring
32+
V 0 1p
33+
D 0.2i 1p
34+
N 1
35+
G 0.05i
36+
G 0.05i
37+
G 0.05i
38+
L 9 4 R Smith et al., @%5%J. Geophys. Res., 99@%%, 2000
39+
G 0.1i
40+
P
41+
T Let us just try some simple text that can go on a few lines.
42+
T There is no easy way to predetermine how many lines will be required,
43+
T so we may have to adjust the box height to get the right size box.
44+
"""
45+
46+
1347
@pytest.mark.mpl_image_compare
1448
def test_legend_position():
1549
"""
16-
Test that plots a position with each of the four legend coordinate systems.
50+
Test positioning the legend with different coordinate systems.
1751
"""
18-
1952
fig = Figure()
2053
fig.basemap(region=[-2, 2, -2, 2], frame=True)
2154
positions = ["jTR+jTR", "g0/1", "n0.2/0.2", "x4i/2i/2i"]
@@ -30,22 +63,18 @@ def test_legend_default_position():
3063
"""
3164
Test using the default legend position.
3265
"""
33-
3466
fig = Figure()
35-
3667
fig.basemap(region=[-1, 1, -1, 1], frame=True)
37-
3868
fig.plot(x=[0], y=[0], style="p10p", label="Default")
3969
fig.legend()
40-
4170
return fig
4271

4372

4473
@pytest.mark.benchmark
4574
@pytest.mark.mpl_image_compare
4675
def test_legend_entries():
4776
"""
48-
Test different marker types/shapes.
77+
Test legend using the automatically generated legend entries.
4978
"""
5079
fig = Figure()
5180
fig.basemap(projection="x1i", region=[0, 7, 3, 7], frame=True)
@@ -59,48 +88,31 @@ def test_legend_entries():
5988
fig.plot(data="@Table_5_11.txt", pen="1.5p,gray", label="My lines")
6089
fig.plot(data="@Table_5_11.txt", style="t0.15i", fill="orange", label="Oranges")
6190
fig.legend(position="JTR+jTR")
62-
6391
return fig
6492

6593

6694
@pytest.mark.mpl_image_compare
67-
def test_legend_specfile():
95+
def test_legend_specfile(legend_spec):
6896
"""
69-
Test specfile functionality.
97+
Test passing a legend specification file.
7098
"""
71-
72-
specfile_contents = """
73-
G -0.1i
74-
H 24 Times-Roman My Map Legend
75-
D 0.2i 1p
76-
N 2
77-
V 0 1p
78-
S 0.1i c 0.15i p300/12 0.25p 0.3i This circle is hachured
79-
S 0.1i e 0.15i yellow 0.25p 0.3i This ellipse is yellow
80-
S 0.1i w 0.15i green 0.25p 0.3i This wedge is green
81-
S 0.1i f0.1i+l+t 0.25i blue 0.25p 0.3i This is a fault
82-
S 0.1i - 0.15i - 0.25p,- 0.3i A dashed contour
83-
S 0.1i v0.1i+a40+e 0.25i magenta 0.25p 0.3i This is a vector
84-
S 0.1i i 0.15i cyan 0.25p 0.3i This triangle is boring
85-
V 0 1p
86-
D 0.2i 1p
87-
N 1
88-
G 0.05i
89-
G 0.05i
90-
G 0.05i
91-
L 9 4 R Smith et al., @%5%J. Geophys. Res., 99@%%, 2000
92-
G 0.1i
93-
P
94-
T Let us just try some simple text that can go on a few lines.
95-
T There is no easy way to predetermine how many lines will be required,
96-
T so we may have to adjust the box height to get the right size box.
97-
"""
98-
9999
with GMTTempFile() as specfile:
100-
Path(specfile.name).write_text(specfile_contents, encoding="utf-8")
100+
Path(specfile.name).write_text(legend_spec, encoding="utf-8")
101101
fig = Figure()
102102
fig.basemap(projection="x6i", region=[0, 1, 0, 1], frame=True)
103103
fig.legend(specfile.name, position="JTM+jCM+w5i")
104+
return fig
105+
106+
107+
@pytest.mark.mpl_image_compare(filename="test_legend_specfile.png")
108+
def test_legend_stringio(legend_spec):
109+
"""
110+
Test passing an legend specification via an io.StringIO object.
111+
"""
112+
spec = io.StringIO(legend_spec)
113+
fig = Figure()
114+
fig.basemap(projection="x6i", region=[0, 1, 0, 1], frame=True)
115+
fig.legend(spec, position="JTM+jCM+w5i")
104116
return fig
105117

106118

@@ -111,3 +123,6 @@ def test_legend_fails():
111123
fig = Figure()
112124
with pytest.raises(GMTInvalidInput):
113125
fig.legend(spec=["@Table_5_11.txt"])
126+
127+
with pytest.raises(GMTInvalidInput):
128+
fig.legend(spec=[1, 2])

0 commit comments

Comments
 (0)