Skip to content

Commit 9497c87

Browse files
committed
Add Figure.vlines for plotting vertical lines
1 parent efbc0fb commit 9497c87

10 files changed

+262
-0
lines changed

Diff for: doc/api/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Plotting map elements
3636
Figure.solar
3737
Figure.text
3838
Figure.timestamp
39+
Figure.vlines
3940

4041
Plotting tabular data
4142
~~~~~~~~~~~~~~~~~~~~~

Diff for: pygmt/figure.py

+1
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ def _repr_html_(self) -> str:
436436
tilemap,
437437
timestamp,
438438
velo,
439+
vlines,
439440
wiggle,
440441
)
441442

Diff for: pygmt/src/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
from pygmt.src.timestamp import timestamp
5858
from pygmt.src.triangulate import triangulate
5959
from pygmt.src.velo import velo
60+
from pygmt.src.vlines import vlines
6061
from pygmt.src.which import which
6162
from pygmt.src.wiggle import wiggle
6263
from pygmt.src.x2sys_cross import x2sys_cross

Diff for: pygmt/src/vlines.py

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"""
2+
vlines - Plot vertical 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__ = ["vlines"]
11+
12+
13+
def vlines(
14+
self,
15+
x: float | Sequence[float],
16+
ymin: float | Sequence[float] | None = None,
17+
ymax: 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 vertical line(s).
25+
26+
This method is a high-level wrapper around :meth:`pygmt.Figure.plot` that focuses on
27+
plotting vertical lines at X-coordinates specified by the ``x`` parameter. The ``x``
28+
parameter can be a single value (for a single vertical line) or a sequence of values
29+
(for multiple vertical lines).
30+
31+
By default, the Y-coordinates of the start and end points of the lines are set to be
32+
the Y-limits of the current plot, but this can be overridden by specifying the
33+
``ymin`` and ``ymax`` parameters. ``ymin`` and ``ymax`` can be either a single value
34+
or a sequence of values. If a single value is provided, it is applied to all lines.
35+
If a sequence is provided, the length of ``ymin`` and ``ymax`` must match the length
36+
of ``x``.
37+
38+
The term "vertical" 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 straight lines along radius.
43+
- **Geographic** projection: lines are plotted as meridians along constant
44+
longitude.
45+
46+
Parameters
47+
----------
48+
x
49+
X-coordinates to plot the lines. It can be a single value (for a single line)
50+
or a sequence of values (for multiple lines).
51+
ymin/ymax
52+
Y-coordinates of the start/end point of the line(s). If ``None``, defaults to
53+
the Y-limits of the current plot. ``ymin`` and ``ymax`` can either be a single
54+
value or a sequence of values. If a single value is provided, it is applied to
55+
all lines. If a sequence is provided, the length of ``ymin`` and ``ymax`` must
56+
match the length of ``x``.
57+
pen
58+
Pen attributes for the line(s), in the format of *width,color,style*.
59+
label
60+
Label for the line(s), to be displayed in the legend.
61+
no_clip
62+
If ``True``, do not clip lines outside the plot region. Only makes sense in the
63+
Cartesian coordinate system.
64+
perspective
65+
Select perspective view and set the azimuth and elevation angle of the
66+
viewpoint. Refer to :meth:`pygmt.Figure.plot` for details.
67+
68+
Examples
69+
--------
70+
>>> import pygmt
71+
>>> fig = pygmt.Figure()
72+
>>> fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True)
73+
>>> fig.vlines(x=1, pen="1p,black", label="Line at x=1")
74+
>>> fig.vlines(x=2, ymin=2, ymax=8, pen="1p,red,-", label="Line at x=2")
75+
>>> fig.vlines(x=[3, 4], ymin=3, ymax=7, pen="1p,black,.", label="Lines at x=3,4")
76+
>>> fig.vlines(x=[5, 6], ymin=4, ymax=9, pen="1p,red", label="Lines at x=5,6")
77+
>>> fig.vlines(
78+
... x=[7, 8], ymin=[0, 1], ymax=[7, 8], pen="1p,blue", label="Lines at x=7,8"
79+
... )
80+
>>> fig.legend()
81+
>>> fig.show()
82+
"""
83+
self._preprocess()
84+
85+
# Determine the x limits from the current plot region if not specified.
86+
if ymin is None or ymax is None:
87+
ylimits = self.region[2:]
88+
if ymin is None:
89+
ymin = ylimits[0]
90+
if ymax is None:
91+
ymax = ylimits[1]
92+
93+
# Ensure y/xmin/xmax are 1-D arrays.
94+
_x = np.atleast_1d(x)
95+
_ymin = np.atleast_1d(ymin)
96+
_ymax = np.atleast_1d(ymax)
97+
98+
nlines = len(_x) # Number of lines to plot.
99+
100+
# Check if ymin/ymax are scalars or have the expected length.
101+
if _ymin.size not in {1, nlines} or _ymax.size not in {1, nlines}:
102+
msg = (
103+
f"'ymin' and 'ymax' are expected to be scalars or have lengths '{nlines}', "
104+
f"but lengths '{_ymin.size}' and '{_ymax.size}' are given."
105+
)
106+
raise GMTInvalidInput(msg)
107+
108+
# Repeat ymin/ymax to match the length of x if they are scalars.
109+
if nlines != 1:
110+
if _ymin.size == 1:
111+
_ymin = np.repeat(_ymin, nlines)
112+
if _ymax.size == 1:
113+
_ymax = np.repeat(_ymax, nlines)
114+
115+
# Call the Figure.plot method to plot the lines.
116+
for i in range(nlines):
117+
# Special handling for label.
118+
# 1. Only specify a label when plotting the first line.
119+
# 2. The -l option can accept comma-separated labels for labeling multiple lines
120+
# with auto-coloring enabled. We don't need this feature here, so we need to
121+
# replace comma with \054 if the label contains commas.
122+
_label = label.replace(",", "\\054") if label and i == 0 else None
123+
124+
self.plot(
125+
x=[_x[i], _x[i]],
126+
y=[_ymin[i], _ymax[i]],
127+
pen=pen,
128+
label=_label,
129+
no_clip=no_clip,
130+
perspective=perspective,
131+
straight_line="y",
132+
)

Diff for: pygmt/tests/baseline/test_vlines_clip.png.dvc

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: 4eb9c7fd7e3a803dcc3cde1409ad7fa7
3+
size: 7361
4+
hash: md5
5+
path: test_vlines_clip.png
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: 3fb4a271c670e4cbe647838b6fee5a8c
3+
size: 67128
4+
hash: md5
5+
path: test_vlines_geographic_global.png
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: 7a955781529e2205d9b856631c48ec7a
3+
size: 13893
4+
hash: md5
5+
path: test_vlines_multiple_lines.png

Diff for: pygmt/tests/baseline/test_vlines_one_line.png.dvc

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: 986772f58935f81e9596736e914acb78
3+
size: 13604
4+
hash: md5
5+
path: test_vlines_one_line.png
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: 1981df3bd9c57cd975b6e74946496175
3+
size: 44621
4+
hash: md5
5+
path: test_vlines_polar_projection.png

Diff for: pygmt/tests/test_vlines.py

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""
2+
Tests for Figure.vlines.
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_vlines_one_line():
12+
"""
13+
Plot one vertical line.
14+
"""
15+
fig = Figure()
16+
fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True)
17+
fig.vlines(1)
18+
fig.vlines(2, ymin=1)
19+
fig.vlines(3, ymax=9)
20+
fig.vlines(4, ymin=3, ymax=8)
21+
fig.vlines(5, ymin=4, ymax=8, pen="1p,blue", label="Line at y=5")
22+
fig.vlines(6, ymin=5, ymax=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_vlines_multiple_lines():
29+
"""
30+
Plot multiple vertical lines.
31+
"""
32+
fig = Figure()
33+
fig.basemap(region=[0, 16, 0, 10], projection="X10c/10c", frame=True)
34+
fig.vlines([1, 2])
35+
fig.vlines([3, 4, 5], ymin=[1, 2, 3])
36+
fig.vlines([6, 7, 8], ymax=[7, 8, 9])
37+
fig.vlines([9, 10], ymin=[1, 2], ymax=[9, 10])
38+
fig.vlines([11, 12], ymin=1, ymax=8, pen="1p,blue", label="Lines at y=11,12")
39+
fig.vlines(
40+
[13, 14], ymin=[3, 4], ymax=[7, 8], 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_vlines_clip():
48+
"""
49+
Plot vertical lines with clipping or not.
50+
"""
51+
fig = Figure()
52+
fig.basemap(region=[0, 10, 0, 4], projection="X10c/4c", frame=True)
53+
fig.vlines(1, ymin=-1, ymax=5)
54+
fig.vlines(2, ymin=-1, ymax=5, no_clip=True)
55+
return fig
56+
57+
58+
@pytest.mark.mpl_image_compare
59+
def test_vlines_geographic_global():
60+
"""
61+
Plot vertical lines in geographic coordinates.
62+
"""
63+
fig = Figure()
64+
fig.basemap(region=[-180, 180, -90, 90], projection="R15c", frame="a30g30")
65+
fig.vlines(30, pen="1p")
66+
fig.vlines(90, ymin=-60, pen="1p,blue")
67+
fig.vlines(-90, ymax=60, pen="1p,blue")
68+
fig.vlines(120, ymin=-60, ymax=60, pen="1p,blue")
69+
return fig
70+
71+
72+
@pytest.mark.mpl_image_compare
73+
def test_vlines_polar_projection():
74+
"""
75+
Plot vertical lines in polar projection.
76+
"""
77+
fig = Figure()
78+
fig.basemap(region=[0, 360, 0, 1], projection="P15c", frame=True)
79+
fig.vlines(0, pen="1p")
80+
fig.vlines(30, ymin=0, ymax=1, pen="1p")
81+
fig.vlines(60, ymin=0.5, pen="1p")
82+
fig.vlines(90, ymax=0.5, pen="1p")
83+
fig.vlines(120, ymin=0.25, ymax=0.75, pen="1p")
84+
return fig
85+
86+
87+
def test_vlines_invalid_input():
88+
"""
89+
Test invalid input for vlines.
90+
"""
91+
fig = Figure()
92+
fig.basemap(region=[0, 10, 0, 6], projection="X10c/6c", frame=True)
93+
with pytest.raises(GMTInvalidInput):
94+
fig.vlines(1, ymin=2, ymax=[3, 4])
95+
with pytest.raises(GMTInvalidInput):
96+
fig.vlines(1, ymin=[2, 3], ymax=4)
97+
with pytest.raises(GMTInvalidInput):
98+
fig.vlines(1, ymin=[2, 3], ymax=[4, 5])
99+
with pytest.raises(GMTInvalidInput):
100+
fig.vlines([1, 2], ymin=[2, 3, 4], ymax=3)
101+
with pytest.raises(GMTInvalidInput):
102+
fig.vlines([1, 2], ymin=[2, 3], ymax=[4, 5, 6])

0 commit comments

Comments
 (0)