Skip to content

Commit 78cd352

Browse files
committed
Add Figure.scalebar to plot a scale bar on maps
1 parent 3dd7379 commit 78cd352

File tree

9 files changed

+247
-3
lines changed

9 files changed

+247
-3
lines changed

doc/api/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Plotting map elements
3131
Figure.inset
3232
Figure.legend
3333
Figure.logo
34+
Figure.scalebar
3435
Figure.solar
3536
Figure.text
3637
Figure.timestamp

pygmt/figure.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,7 @@ def _repr_html_(self) -> str:
427427
plot3d,
428428
psconvert,
429429
rose,
430+
scalebar,
430431
set_panel,
431432
shift_origin,
432433
solar,

pygmt/src/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from pygmt.src.project import project
4444
from pygmt.src.psconvert import psconvert
4545
from pygmt.src.rose import rose
46+
from pygmt.src.scalebar import scalebar
4647
from pygmt.src.select import select
4748
from pygmt.src.shift_origin import shift_origin
4849
from pygmt.src.solar import solar

pygmt/src/_common.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ def _parse_position(
251251
position: Position | Sequence[float | str] | str | None,
252252
kwdict: dict[str, Any],
253253
default: Position | None,
254-
) -> Position | str:
254+
) -> Position | str | None:
255255
"""
256256
Parse the "position" parameter for embellishment-plotting functions.
257257
@@ -269,7 +269,8 @@ def _parse_position(
269269
The keyword arguments dictionary that conflicts with ``position`` if
270270
``position`` is given as a raw GMT command string.
271271
default
272-
The default Position object to use if ``position`` is ``None``.
272+
The default Position object to use if ``position`` is ``None``. If ``default``
273+
is ``None``, the GMT default is used.
273274
274275
Returns
275276
-------
@@ -349,7 +350,7 @@ def _parse_position(
349350
position = Position(position, cstype="plotcoords")
350351
case Position(): # Already a Position object.
351352
pass
352-
case None if default is not None: # Set default position.
353+
case None: # Set default position.
353354
position = default
354355
case _:
355356
msg = f"Invalid type for parameter 'position': {type(position)}."

pygmt/src/scalebar.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
"""
2+
scalebar - Add a scale bar.
3+
"""
4+
5+
from collections.abc import Sequence
6+
from typing import Literal
7+
8+
from pygmt._typing import AnchorCode
9+
from pygmt.alias import Alias, AliasSystem
10+
from pygmt.clib import Session
11+
from pygmt.exceptions import GMTInvalidInput
12+
from pygmt.helpers import build_arg_list, fmt_docstring
13+
from pygmt.params import Box, Position
14+
from pygmt.src._common import _parse_position
15+
16+
__doctest_skip__ = ["scalebar"]
17+
18+
19+
@fmt_docstring
20+
def scalebar( # noqa: PLR0913
21+
self,
22+
position: Position | Sequence[float | str] | AnchorCode | None = None,
23+
length: float | str | None = None,
24+
height: float | str | None = None,
25+
scale_position: float | Sequence[float] | bool = False,
26+
label: str | bool = False,
27+
label_alignment: Literal["left", "right", "top", "bottom"] | None = None,
28+
unit: bool = False,
29+
fancy: bool = False,
30+
vertical: bool = False,
31+
box: Box | bool = False,
32+
verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"]
33+
| bool = False,
34+
panel: int | Sequence[int] | bool = False,
35+
perspective: float | Sequence[float] | str | bool = False,
36+
transparency: float | None = None,
37+
):
38+
"""
39+
Add a scale bar on the plot.
40+
41+
Parameters
42+
----------
43+
position
44+
Position of the scale bar on the plot. It can be specified in multiple ways:
45+
46+
- A :class:`pygmt.params.Position` object to fully control the reference point,
47+
anchor point, and offset.
48+
- A sequence of two values representing the x and y coordinates in plot
49+
coordinates, e.g., ``(1, 2)`` or ``("1c", "2c")``.
50+
- A :doc:`2-character justification code </techref/justification_codes>` for a
51+
position inside the plot, e.g., ``"TL"`` for Top Left corner inside the plot.
52+
53+
If not specified, defaults to the bottom-left corner of the plot with a 0.2-cm
54+
and 0.4-cm offset in the x- and y-directions, respectively.
55+
length
56+
Length of the scale bar in km. Append a suffix to specify different units. Valid
57+
units are: **e**: meters; **f**: feet; **k**: kilometers; **M**: statute mile;
58+
**n**: nautical miles; **u**: US Survey foot.
59+
height
60+
Height of the scale bar. Only works when ``fancy=True``. [Default is ``"5p"``].
61+
scale_position
62+
Specify the location where on a geographic map the scale applies. It can be:
63+
64+
- *slat*: Map scale is calculated for latitude *slat*
65+
- (*slon*, *slat*): Map scale is calculated for latitude *slat* and longitude
66+
*slon*, which is useful for oblique projections.
67+
- ``True``: Map scale is calculated for the middle of the map.
68+
- ``False``: Default to the location of the reference point.
69+
label
70+
Text string to use as the scale bar label. If ``False``, no label is drawn. If
71+
``True``, the distance unit provided in the ``length`` parameter (default is km)
72+
is used as the label. This parameter requires ``fancy=True``.
73+
label_alignment
74+
Alignment of the scale bar label. Choose from ``"left"``, ``"right"``,
75+
``"top"``, or ``"bottom"``. [Default is ``"top"``].
76+
fancy
77+
If ``True``, draw a "fancy" scale bar, which is a segmented bar with alternating
78+
black and white rectangles. If ``False``, draw a plain scale bar.
79+
unit
80+
If ``True``, append the unit to all distance annotations along the scale. For a
81+
plain scale, this will instead select the unit to be appended to the distance
82+
length. The unit is determined from the suffix in the ``length`` or defaults to
83+
``"km"``.
84+
vertical
85+
If ``True``, plot a vertical rather than a horizontal Cartesian scale.
86+
box
87+
Draw a background box behind the directional rose. If set to ``True``, a simple
88+
rectangular box is drawn using :gmt-term:`MAP_FRAME_PEN`. To customize the box
89+
appearance, pass a :class:`pygmt.params.Box` object to control style, fill, pen,
90+
and other box properties.
91+
$verbose
92+
$panel
93+
$perspective
94+
$transparency
95+
96+
Examples
97+
--------
98+
>>> import pygmt
99+
>>> from pygmt.params import Box, Position
100+
>>> fig = pygmt.Figure()
101+
>>> fig.basemap(region=[0, 80, -30, 30], projection="M10c", frame=True)
102+
>>> fig.scalebar(
103+
... position=Position((10, 10), cstype="mapcoords"),
104+
... length=1000,
105+
... fancy=True,
106+
... label="Scale",
107+
... unit=True,
108+
... )
109+
>>> fig.show()
110+
"""
111+
self._activate_figure()
112+
113+
position = _parse_position(
114+
position,
115+
kwdict={
116+
"length": length,
117+
"height": height,
118+
"label_alignment": label_alignment,
119+
"scale_position": scale_position,
120+
"fancy": fancy,
121+
"label": label,
122+
"unit": unit,
123+
"vertical": vertical,
124+
},
125+
default=Position("BL", offset=(0.2, 0.4)), # Default to "BL" with offset.
126+
)
127+
128+
if length is None:
129+
msg = "Parameter 'length' must be specified."
130+
raise GMTInvalidInput(msg)
131+
132+
aliasdict = AliasSystem(
133+
F=Alias(box, name="box"),
134+
L=[
135+
Alias(position, name="position"),
136+
Alias(length, name="length", prefix="+w"),
137+
Alias(
138+
label_alignment,
139+
name="label_alignment",
140+
prefix="+a",
141+
mapping={"left": "l", "right": "r", "top": "t", "bottom": "b"},
142+
),
143+
Alias(scale_position, name="scale_position", prefix="+c", sep="/", size=2),
144+
Alias(fancy, name="fancy", prefix="+f"),
145+
Alias(label, name="label", prefix="+l"),
146+
Alias(unit, name="unit", prefix="+u"),
147+
Alias(vertical, name="vertical", prefix="+v"),
148+
],
149+
).add_common(
150+
V=verbose,
151+
c=panel,
152+
p=perspective,
153+
t=transparency,
154+
)
155+
156+
confdict = {}
157+
if height is not None:
158+
confdict["MAP_SCALE_HEIGHT"] = height
159+
160+
with Session() as lib:
161+
lib.call_module(
162+
module="basemap", args=build_arg_list(aliasdict, confdict=confdict)
163+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: ad7658ed25f1a9f0a1ba74a0ffa84a4b
3+
size: 10207
4+
hash: md5
5+
path: test_scalebar.png
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: e09a7c67f6146530ea594694853b6f98
3+
size: 6508
4+
hash: md5
5+
path: test_scalebar_cartesian.png
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: c018b219d3ebc719fb1b1686e074dcd9
3+
size: 11749
4+
hash: md5
5+
path: test_scalebar_complete.png

pygmt/tests/test_scalebar.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""
2+
Test Figure.scalebar.
3+
"""
4+
5+
import pytest
6+
from pygmt import Figure
7+
from pygmt.exceptions import GMTInvalidInput
8+
from pygmt.params import Position
9+
10+
11+
@pytest.mark.mpl_image_compare
12+
def test_scalebar():
13+
"""
14+
Create a map with a scale bar.
15+
"""
16+
fig = Figure()
17+
fig.basemap(region=[100, 120, 20, 30], projection="M10c", frame=True)
18+
fig.scalebar(length=500)
19+
return fig
20+
21+
22+
@pytest.mark.mpl_image_compare
23+
def test_scalebar_complete():
24+
"""
25+
Test all parameters of scalebar.
26+
"""
27+
fig = Figure()
28+
fig.basemap(region=[100, 120, 20, 30], projection="M10c", frame=True)
29+
fig.scalebar(
30+
position=Position((110, 22), cstype="mapcoords"),
31+
length=1000,
32+
height="10p",
33+
fancy=True,
34+
label="Scale",
35+
label_alignment="left",
36+
scale_position=(110, 25),
37+
unit=True,
38+
box=True,
39+
)
40+
return fig
41+
42+
43+
@pytest.mark.mpl_image_compare
44+
def test_scalebar_cartesian():
45+
"""
46+
Test scale bar in Cartesian coordinates.
47+
"""
48+
fig = Figure()
49+
fig.basemap(region=[0, 10, 0, 5], projection="X10c/5c", frame=True)
50+
fig.scalebar(position=Position((2, 1), cstype="mapcoords"), length=1)
51+
fig.scalebar(position=Position((4, 1), cstype="mapcoords"), length=1, vertical=True)
52+
return fig
53+
54+
55+
def test_scalebar_no_length():
56+
"""
57+
Test that an error is raised when length is not provided.
58+
"""
59+
fig = Figure()
60+
fig.basemap(region=[100, 120, 20, 30], projection="M10c", frame=True)
61+
with pytest.raises(GMTInvalidInput):
62+
fig.scalebar(position=Position((118, 22), cstype="mapcoords"))

0 commit comments

Comments
 (0)