Skip to content

Figure.legend: Refactor to simplify the logic of checking legend specification #3437

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

Merged
merged 3 commits into from
Sep 12, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 25 additions & 13 deletions pygmt/src/legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
legend - Plot a legend.
"""

import pathlib

from pygmt.clib import Session
from pygmt.exceptions import GMTInvalidInput
from pygmt.helpers import (
Expand All @@ -26,7 +28,13 @@
t="transparency",
)
@kwargs_to_strings(R="sequence", c="sequence_comma", p="sequence")
def legend(self, spec=None, position="JTR+jTR+o0.2c", box="+gwhite+p1p", **kwargs):
def legend(
self,
spec: str | pathlib.PurePath | None = None,
position="JTR+jTR+o0.2c",
box="+gwhite+p1p",
**kwargs,
):
r"""
Plot legends on maps.

Expand All @@ -42,10 +50,15 @@ def legend(self, spec=None, position="JTR+jTR+o0.2c", box="+gwhite+p1p", **kwarg

Parameters
----------
spec : None or str
Either ``None`` [Default] for using the automatically generated legend
specification file, or a *filename* pointing to the legend
specification file.
spec
The legend specification. It can be:

- ``None`` which means using the automatically generated legend specification
file
- A string or a :class:`pathlib.PurePath` object pointing to the legend
specification file

See :gmt-docs:`legend.html` for the definition of the legend specification.
{projection}
{region}
position : str
Expand Down Expand Up @@ -75,12 +88,11 @@ def legend(self, spec=None, position="JTR+jTR+o0.2c", box="+gwhite+p1p", **kwarg
if kwargs.get("F") is None:
kwargs["F"] = box

kind = data_kind(spec)
if kind not in {"vectors", "file"}: # kind="vectors" means spec is None
Copy link
Member Author

Choose a reason for hiding this comment

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

This line can also be written as:

if spec is not None or kind != "file"

It's written in the current way so that this line can be extended to check "stringio" kind with minor changes (xref: #3326).

It's not straightforward to see why kind="vectors" means spec is None. That's also why I want to refactor the data_kind function in #3351.

raise GMTInvalidInput(f"Unrecognized data type: {type(spec)}")
if kind == "file" and is_nonstr_iter(spec):
raise GMTInvalidInput("Only one legend specification file is allowed.")

with Session() as lib:
if spec is None:
specfile = ""
elif data_kind(spec) == "file" and not is_nonstr_iter(spec):
# Is a file but not a list of files
specfile = spec
else:
raise GMTInvalidInput(f"Unrecognized data type: {type(spec)}")
lib.call_module(module="legend", args=build_arg_list(kwargs, infile=specfile))
lib.call_module(module="legend", args=build_arg_list(kwargs, infile=spec))
80 changes: 41 additions & 39 deletions pygmt/tests/test_legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,44 @@
from pygmt.helpers import GMTTempFile


@pytest.fixture(scope="module", name="legend_spec")
def fixture_legend_spec():
"""
A string contains a legend specification.
"""
return """
G -0.1i
H 24 Times-Roman My Map Legend
D 0.2i 1p
N 2
V 0 1p
S 0.1i c 0.15i p300/12 0.25p 0.3i This circle is hachured
S 0.1i e 0.15i yellow 0.25p 0.3i This ellipse is yellow
S 0.1i w 0.15i green 0.25p 0.3i This wedge is green
S 0.1i f0.1i+l+t 0.25i blue 0.25p 0.3i This is a fault
S 0.1i - 0.15i - 0.25p,- 0.3i A dashed contour
S 0.1i v0.1i+a40+e 0.25i magenta 0.25p 0.3i This is a vector
S 0.1i i 0.15i cyan 0.25p 0.3i This triangle is boring
V 0 1p
D 0.2i 1p
N 1
G 0.05i
G 0.05i
G 0.05i
L 9 4 R Smith et al., @%5%J. Geophys. Res., 99@%%, 2000
G 0.1i
P
T Let us just try some simple text that can go on a few lines.
T There is no easy way to predetermine how many lines will be required,
T so we may have to adjust the box height to get the right size box.
"""


@pytest.mark.mpl_image_compare
def test_legend_position():
"""
Test that plots a position with each of the four legend coordinate systems.
Test positioning the legend with different coordinate systems.
"""

fig = Figure()
fig.basemap(region=[-2, 2, -2, 2], frame=True)
positions = ["jTR+jTR", "g0/1", "n0.2/0.2", "x4i/2i/2i"]
Expand All @@ -30,22 +62,18 @@ def test_legend_default_position():
"""
Test using the default legend position.
"""

fig = Figure()

fig.basemap(region=[-1, 1, -1, 1], frame=True)

fig.plot(x=[0], y=[0], style="p10p", label="Default")
fig.legend()

return fig


@pytest.mark.benchmark
@pytest.mark.mpl_image_compare
def test_legend_entries():
"""
Test different marker types/shapes.
Test legend using the automatically generated legend entries.
"""
fig = Figure()
fig.basemap(projection="x1i", region=[0, 7, 3, 7], frame=True)
Expand All @@ -59,45 +87,16 @@ def test_legend_entries():
fig.plot(data="@Table_5_11.txt", pen="1.5p,gray", label="My lines")
fig.plot(data="@Table_5_11.txt", style="t0.15i", fill="orange", label="Oranges")
fig.legend(position="JTR+jTR")

return fig


@pytest.mark.mpl_image_compare
def test_legend_specfile():
def test_legend_specfile(legend_spec):
"""
Test specfile functionality.
Test passing a legend specification file.
"""

specfile_contents = """
G -0.1i
H 24 Times-Roman My Map Legend
D 0.2i 1p
N 2
V 0 1p
S 0.1i c 0.15i p300/12 0.25p 0.3i This circle is hachured
S 0.1i e 0.15i yellow 0.25p 0.3i This ellipse is yellow
S 0.1i w 0.15i green 0.25p 0.3i This wedge is green
S 0.1i f0.1i+l+t 0.25i blue 0.25p 0.3i This is a fault
S 0.1i - 0.15i - 0.25p,- 0.3i A dashed contour
S 0.1i v0.1i+a40+e 0.25i magenta 0.25p 0.3i This is a vector
S 0.1i i 0.15i cyan 0.25p 0.3i This triangle is boring
V 0 1p
D 0.2i 1p
N 1
G 0.05i
G 0.05i
G 0.05i
L 9 4 R Smith et al., @%5%J. Geophys. Res., 99@%%, 2000
G 0.1i
P
T Let us just try some simple text that can go on a few lines.
T There is no easy way to predetermine how many lines will be required,
T so we may have to adjust the box height to get the right size box.
"""

with GMTTempFile() as specfile:
Path(specfile.name).write_text(specfile_contents, encoding="utf-8")
Path(specfile.name).write_text(legend_spec, encoding="utf-8")
fig = Figure()
fig.basemap(projection="x6i", region=[0, 1, 0, 1], frame=True)
fig.legend(specfile.name, position="JTM+jCM+w5i")
Expand All @@ -111,3 +110,6 @@ def test_legend_fails():
fig = Figure()
with pytest.raises(GMTInvalidInput):
fig.legend(spec=["@Table_5_11.txt"])

with pytest.raises(GMTInvalidInput):
fig.legend(spec=[1, 2])
Loading