Skip to content

Commit 613bdc1

Browse files
Add Figure.scalebar to plot a scale bar on maps (#4015)
Co-authored-by: Yvonne Fröhlich <94163266+yvonnefroehlich@users.noreply.github.com>
1 parent 677e1cf commit 613bdc1

File tree

8 files changed

+215
-0
lines changed

8 files changed

+215
-0
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/scalebar.py

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

0 commit comments

Comments
 (0)