Skip to content

Commit efbc0fb

Browse files
michaelgrundseismanyvonnefroehlichactions-bot
authored
Add Figure.hlines for plotting horizontal lines (#923)
Co-authored-by: Dongdong Tian <[email protected]> Co-authored-by: Yvonne Fröhlich <[email protected]> Co-authored-by: actions-bot <[email protected]>
1 parent 85c78f8 commit efbc0fb

11 files changed

+291
-0
lines changed

doc/api/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Plotting map elements
2929
Figure.basemap
3030
Figure.coast
3131
Figure.colorbar
32+
Figure.hlines
3233
Figure.inset
3334
Figure.legend
3435
Figure.logo

pygmt/figure.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ def _repr_html_(self) -> str:
417417
grdimage,
418418
grdview,
419419
histogram,
420+
hlines,
420421
image,
421422
inset,
422423
legend,

pygmt/src/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from pygmt.src.grdview import grdview
3030
from pygmt.src.grdvolume import grdvolume
3131
from pygmt.src.histogram import histogram
32+
from pygmt.src.hlines import hlines
3233
from pygmt.src.image import image
3334
from pygmt.src.info import info
3435
from pygmt.src.inset import inset

pygmt/src/hlines.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"""
2+
hlines - Plot horizontal lines.
3+
"""
4+
5+
from collections.abc import Sequence
6+
7+
import numpy as np
8+
from pygmt.exceptions import GMTInvalidInput
9+
10+
__doctest_skip__ = ["hlines"]
11+
12+
13+
def hlines(
14+
self,
15+
y: float | Sequence[float],
16+
xmin: float | Sequence[float] | None = None,
17+
xmax: float | Sequence[float] | None = None,
18+
pen: str | None = None,
19+
label: str | None = None,
20+
no_clip: bool = False,
21+
perspective: str | bool | None = None,
22+
):
23+
"""
24+
Plot one or multiple horizontal line(s).
25+
26+
This method is a high-level wrapper around :meth:`pygmt.Figure.plot` that focuses on
27+
plotting horizontal lines at Y-coordinates specified by the ``y`` parameter. The
28+
``y`` parameter can be a single value (for a single horizontal line) or a sequence
29+
of values (for multiple horizontal lines).
30+
31+
By default, the X-coordinates of the start and end points of the lines are set to
32+
be the X-limits of the current plot, but this can be overridden by specifying the
33+
``xmin`` and ``xmax`` parameters. ``xmin`` and ``xmax`` can be either a single
34+
value or a sequence of values. If a single value is provided, it is applied to all
35+
lines. If a sequence is provided, the length of ``xmin`` and ``xmax`` must match
36+
the length of ``y``.
37+
38+
The term "horizontal" lines can be interpreted differently in different coordinate
39+
systems:
40+
41+
- **Cartesian** coordinate system: lines are plotted as straight lines.
42+
- **Polar** projection: lines are plotted as arcs along a constant radius.
43+
- **Geographic** projection: lines are plotted as parallels along constant latitude.
44+
45+
Parameters
46+
----------
47+
y
48+
Y-coordinates to plot the lines. It can be a single value (for a single line)
49+
or a sequence of values (for multiple lines).
50+
xmin/xmax
51+
X-coordinates of the start/end point of the line(s). If ``None``, defaults to
52+
the X-limits of the current plot. ``xmin`` and ``xmax`` can be either a single
53+
value or a sequence of values. If a single value is provided, it is applied to
54+
all lines. If a sequence is provided, the length of ``xmin`` and ``xmax`` must
55+
match the length of ``y``.
56+
pen
57+
Pen attributes for the line(s), in the format of *width,color,style*.
58+
label
59+
Label for the line(s), to be displayed in the legend.
60+
no_clip
61+
If ``True``, do not clip lines outside the plot region. Only makes sense in the
62+
Cartesian coordinate system.
63+
perspective
64+
Select perspective view and set the azimuth and elevation angle of the
65+
viewpoint. Refer to :meth:`pygmt.Figure.plot` for details.
66+
67+
Examples
68+
--------
69+
>>> import pygmt
70+
>>> fig = pygmt.Figure()
71+
>>> fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True)
72+
>>> fig.hlines(y=1, pen="1p,black", label="Line at y=1")
73+
>>> fig.hlines(y=2, xmin=2, xmax=8, pen="1p,red,-", label="Line at y=2")
74+
>>> fig.hlines(y=[3, 4], xmin=3, xmax=7, pen="1p,black,.", label="Lines at y=3,4")
75+
>>> fig.hlines(y=[5, 6], xmin=4, xmax=9, pen="1p,red", label="Lines at y=5,6")
76+
>>> fig.hlines(
77+
... y=[7, 8], xmin=[0, 1], xmax=[7, 8], pen="1p,blue", label="Lines at y=7,8"
78+
... )
79+
>>> fig.legend()
80+
>>> fig.show()
81+
"""
82+
self._preprocess()
83+
84+
# Determine the x limits from the current plot region if not specified.
85+
if xmin is None or xmax is None:
86+
xlimits = self.region[:2]
87+
if xmin is None:
88+
xmin = xlimits[0]
89+
if xmax is None:
90+
xmax = xlimits[1]
91+
92+
# Ensure y/xmin/xmax are 1-D arrays.
93+
_y = np.atleast_1d(y)
94+
_xmin = np.atleast_1d(xmin)
95+
_xmax = np.atleast_1d(xmax)
96+
97+
nlines = len(_y) # Number of lines to plot.
98+
99+
# Check if xmin/xmax are scalars or have the expected length.
100+
if _xmin.size not in {1, nlines} or _xmax.size not in {1, nlines}:
101+
msg = (
102+
f"'xmin' and 'xmax' are expected to be scalars or have lengths '{nlines}', "
103+
f"but lengths '{_xmin.size}' and '{_xmax.size}' are given."
104+
)
105+
raise GMTInvalidInput(msg)
106+
107+
# Repeat xmin/xmax to match the length of y if they are scalars.
108+
if nlines != 1:
109+
if _xmin.size == 1:
110+
_xmin = np.repeat(_xmin, nlines)
111+
if _xmax.size == 1:
112+
_xmax = np.repeat(_xmax, nlines)
113+
114+
# Call the Figure.plot method to plot the lines.
115+
for i in range(nlines):
116+
# Special handling for label.
117+
# 1. Only specify a label when plotting the first line.
118+
# 2. The -l option can accept comma-separated labels for labeling multiple lines
119+
# with auto-coloring enabled. We don't need this feature here, so we need to
120+
# replace comma with \054 if the label contains commas.
121+
_label = label.replace(",", "\\054") if label and i == 0 else None
122+
123+
# By default, points are connected as great circle arcs in geographic coordinate
124+
# systems and straight lines in Cartesian coordinate systems (including polar
125+
# projection). To plot "horizontal" lines along constant latitude (in geographic
126+
# coordinate systems) or constant radius (in polar projection), we need to
127+
# resample the line to at least 4 points.
128+
npoints = 4 # 2 for Cartesian, at least 4 for geographic and polar projections.
129+
self.plot(
130+
x=np.linspace(_xmin[i], _xmax[i], npoints),
131+
y=[_y[i]] * npoints,
132+
pen=pen,
133+
label=_label,
134+
no_clip=no_clip,
135+
perspective=perspective,
136+
straight_line="x",
137+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: e87ea1b80ae5d32d49e9ad94a5c25f96
3+
size: 7199
4+
hash: md5
5+
path: test_hlines_clip.png
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: b7055f03ff5bc152c0f6b72f2d39f32c
3+
size: 29336
4+
hash: md5
5+
path: test_hlines_geographic_global_d.png
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: ab2e7717cad6ac4132fd3e3af1fefa89
3+
size: 29798
4+
hash: md5
5+
path: test_hlines_geographic_global_g.png
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: 70c8decbffd37fc48b2eb9ff84442ec0
3+
size: 14139
4+
hash: md5
5+
path: test_hlines_multiple_lines.png
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: 121970f75d34c552e632cacc692f09e9
3+
size: 13685
4+
hash: md5
5+
path: test_hlines_one_line.png
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: 0c0eeb160dd6beb06bb6d3dcc264127a
3+
size: 57789
4+
hash: md5
5+
path: test_hlines_polar_projection.png

pygmt/tests/test_hlines.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""
2+
Tests for Figure.hlines.
3+
"""
4+
5+
import pytest
6+
from pygmt import Figure
7+
from pygmt.exceptions import GMTInvalidInput
8+
9+
10+
@pytest.mark.mpl_image_compare
11+
def test_hlines_one_line():
12+
"""
13+
Plot one horizontal line.
14+
"""
15+
fig = Figure()
16+
fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True)
17+
fig.hlines(1)
18+
fig.hlines(2, xmin=1)
19+
fig.hlines(3, xmax=9)
20+
fig.hlines(4, xmin=3, xmax=8)
21+
fig.hlines(5, xmin=4, xmax=8, pen="1p,blue", label="Line at y=5")
22+
fig.hlines(6, xmin=5, xmax=7, pen="1p,red", label="Line at y=6")
23+
fig.legend()
24+
return fig
25+
26+
27+
@pytest.mark.mpl_image_compare
28+
def test_hlines_multiple_lines():
29+
"""
30+
Plot multiple horizontal lines.
31+
"""
32+
fig = Figure()
33+
fig.basemap(region=[0, 10, 0, 16], projection="X10c/10c", frame=True)
34+
fig.hlines([1, 2])
35+
fig.hlines([3, 4, 5], xmin=[1, 2, 3])
36+
fig.hlines([6, 7, 8], xmax=[7, 8, 9])
37+
fig.hlines([9, 10], xmin=[1, 2], xmax=[9, 10])
38+
fig.hlines([11, 12], xmin=1, xmax=9, pen="1p,blue", label="Lines at y=11,12")
39+
fig.hlines(
40+
[13, 14], xmin=[3, 4], xmax=[8, 9], pen="1p,red", label="Lines at y=13,14"
41+
)
42+
fig.legend()
43+
return fig
44+
45+
46+
@pytest.mark.mpl_image_compare
47+
def test_hlines_clip():
48+
"""
49+
Plot horizontal lines with clipping or not.
50+
"""
51+
fig = Figure()
52+
fig.basemap(region=[0, 10, 0, 4], projection="X10c/4c", frame=True)
53+
fig.hlines(1, xmin=-2, xmax=12)
54+
fig.hlines(2, xmin=-2, xmax=12, no_clip=True)
55+
return fig
56+
57+
58+
@pytest.mark.mpl_image_compare
59+
@pytest.mark.parametrize("region", ["g", "d"])
60+
def test_hlines_geographic_global(region):
61+
"""
62+
Plot horizontal lines in geographic coordinates.
63+
"""
64+
fig = Figure()
65+
fig.basemap(region=region, projection="R15c", frame=True)
66+
# Plot lines with longitude range of 0 to 360.
67+
fig.hlines(10, pen="1p")
68+
fig.hlines(20, xmin=0, xmax=360, pen="1p")
69+
fig.hlines(30, xmin=0, xmax=180, pen="1p")
70+
fig.hlines(40, xmin=180, xmax=360, pen="1p")
71+
fig.hlines(50, xmin=0, xmax=90, pen="1p")
72+
fig.hlines(60, xmin=90, xmax=180, pen="1p")
73+
fig.hlines(70, xmin=180, xmax=270, pen="1p")
74+
fig.hlines(80, xmin=270, xmax=360, pen="1p")
75+
76+
# Plot lines with longitude range of -180 to 180.
77+
fig.hlines(-10, pen="1p,red")
78+
fig.hlines(-20, xmin=-180, xmax=180, pen="1p,red")
79+
fig.hlines(-30, xmin=-180, xmax=0, pen="1p,red")
80+
fig.hlines(-40, xmin=0, xmax=180, pen="1p,red")
81+
fig.hlines(-50, xmin=-180, xmax=-90, pen="1p,red")
82+
fig.hlines(-60, xmin=-90, xmax=0, pen="1p,red")
83+
fig.hlines(-70, xmin=0, xmax=90, pen="1p,red")
84+
fig.hlines(-80, xmin=90, xmax=180, pen="1p,red")
85+
return fig
86+
87+
88+
@pytest.mark.mpl_image_compare
89+
def test_hlines_polar_projection():
90+
"""
91+
Plot horizontal lines in polar projection.
92+
"""
93+
fig = Figure()
94+
fig.basemap(region=[0, 360, 0, 1], projection="P15c", frame=True)
95+
fig.hlines(0.1, pen="1p")
96+
fig.hlines(0.2, xmin=0, xmax=360, pen="1p")
97+
fig.hlines(0.3, xmin=0, xmax=180, pen="1p")
98+
fig.hlines(0.4, xmin=180, xmax=360, pen="1p")
99+
fig.hlines(0.5, xmin=0, xmax=90, pen="1p")
100+
fig.hlines(0.6, xmin=90, xmax=180, pen="1p")
101+
fig.hlines(0.7, xmin=180, xmax=270, pen="1p")
102+
fig.hlines(0.8, xmin=270, xmax=360, pen="1p")
103+
return fig
104+
105+
106+
def test_hlines_invalid_input():
107+
"""
108+
Test invalid input for hlines.
109+
"""
110+
fig = Figure()
111+
fig.basemap(region=[0, 10, 0, 6], projection="X10c/6c", frame=True)
112+
with pytest.raises(GMTInvalidInput):
113+
fig.hlines(1, xmin=2, xmax=[3, 4])
114+
with pytest.raises(GMTInvalidInput):
115+
fig.hlines(1, xmin=[2, 3], xmax=4)
116+
with pytest.raises(GMTInvalidInput):
117+
fig.hlines(1, xmin=[2, 3], xmax=[4, 5])
118+
with pytest.raises(GMTInvalidInput):
119+
fig.hlines([1, 2], xmin=[2, 3, 4], xmax=3)
120+
with pytest.raises(GMTInvalidInput):
121+
fig.hlines([1, 2], xmin=[2, 3], xmax=[4, 5, 6])

0 commit comments

Comments
 (0)