Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
eec0fb7
Initial implemention of the Position class
seisman Nov 16, 2025
69d4d42
Merge branch 'main' into params/position
seisman Nov 20, 2025
539f66f
Fix styling
seisman Nov 20, 2025
97f015f
Add tests and improve docstrings
seisman Nov 23, 2025
854804e
Add to API doc
seisman Nov 23, 2025
6b55dde
Add an inline doctest
seisman Nov 23, 2025
3d629cb
position is not required
seisman Nov 23, 2025
576b822
Default to plotcoords
seisman Nov 23, 2025
f54bec9
Updates
seisman Nov 23, 2025
5a2e20b
Merge branch 'main' into params/position
seisman Nov 24, 2025
2c59b7f
Improve the checking in Figure.logo
seisman Nov 24, 2025
d0b62ec
Merge branch 'main' into params/position
seisman Nov 24, 2025
fe18c87
Improve docstrings
seisman Nov 24, 2025
038161b
Improve docstrings
seisman Nov 24, 2025
a6e75bc
Improve docstrings
seisman Nov 25, 2025
3ec8c06
Improve docstrings
seisman Nov 25, 2025
339ce00
Improve docstrings
seisman Nov 25, 2025
4d616de
Revert changes in logo.py
seisman Nov 25, 2025
ad9e0aa
Simplify tests
seisman Nov 25, 2025
b084e5f
Validate values
seisman Nov 25, 2025
d4ad6e0
type will be validated in the Alias System
seisman Nov 25, 2025
7dc37bd
Use the image from the GMT docs
seisman Nov 25, 2025
bfecb2d
Fix width and alignment
seisman Nov 25, 2025
18b90b3
Improve docstrings
seisman Nov 25, 2025
6b1b5bc
Remove unneeded blank lines
seisman Nov 25, 2025
1eae742
Improve docstrings
seisman Nov 25, 2025
721b46f
Validate anchor code
seisman Nov 25, 2025
669b16d
Merge branch 'main' into params/position
seisman Nov 26, 2025
2a38111
Merge branch 'main' into params/position
seisman Nov 26, 2025
0f9ed6c
offset can be a single value
seisman Nov 26, 2025
7d1b076
Merge branch 'main' into params/position
seisman Nov 29, 2025
a779431
Merge branch 'main' into params/position
seisman Dec 1, 2025
2a9cc92
Merge branch 'main' into params/position
seisman Dec 4, 2025
10a0dfb
Use is_nonstr_iter to check the location parameter
seisman Dec 4, 2025
6f1c2c4
Merge remote-tracking branch 'origin/params/position' into params/pos…
seisman Dec 4, 2025
c27213f
Fix a typo [skip ci]
seisman Dec 4, 2025
d47aaeb
Fix a typo [skip ci]
seisman Dec 4, 2025
7fc6ffc
Fix the wrong logic in checking location
seisman Dec 4, 2025
d82f4ba
Add a tests for passing a single value to offset
seisman Dec 4, 2025
5d29e66
Merge branch 'main' into params/position
seisman Dec 5, 2025
563b5a1
Merge branch 'main' into params/position
seisman Dec 5, 2025
0ec021b
Merge branch 'main' into params/position
seisman Dec 6, 2025
ff23ac8
Merge branch 'main' into params/position
seisman Dec 6, 2025
c9c4222
Rename position to refpoint
seisman Dec 7, 2025
0064cde
Fix formatting
seisman Dec 7, 2025
90c7ea2
Merge branch 'main' into params/position
seisman Dec 8, 2025
9a19c1a
Merge branch 'main' into params/position
seisman Dec 8, 2025
ff6392d
Update pygmt/params/position.py
seisman Dec 9, 2025
2310b22
Update pygmt/params/position.py [skip ci]
seisman Dec 9, 2025
a3185e8
Fix styling
seisman Dec 9, 2025
e153ebf
Rename type to cstype
seisman Dec 9, 2025
ed31e0f
Merge branch 'main' into params/position
seisman Dec 9, 2025
bd6a33f
Add Figure.scalebar to plot a scale bar on maps
seisman Jul 26, 2025
4356536
Use new values for position_type
seisman Aug 7, 2025
02a2e77
Use new values for position_type
seisman Aug 7, 2025
8aaef2f
Updates
seisman Aug 10, 2025
1dfff80
Add docstrings
seisman Sep 6, 2025
37d02bc
fix
seisman Nov 26, 2025
17d8427
Update Figure.scalebar and add tests
seisman Dec 9, 2025
f5133e2
Add a test for Cartesian scale
seisman Dec 9, 2025
c5d70e2
Merge branch 'main' into feature/scalebar
seisman Dec 11, 2025
cb23c57
Fix docstrings
seisman Dec 11, 2025
4d04868
Improve docstrings
seisman Dec 11, 2025
de4e142
Add a parameter height for MAP_SCALE_HEIGHT
seisman Dec 11, 2025
e859593
Fix the inline test
seisman Dec 12, 2025
71880a3
Skip the doctest
seisman Dec 12, 2025
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
1 change: 1 addition & 0 deletions doc/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Plotting map elements
Figure.inset
Figure.legend
Figure.logo
Figure.scalebar
Figure.solar
Figure.text
Figure.timestamp
Expand Down
1 change: 1 addition & 0 deletions pygmt/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ def _repr_html_(self) -> str:
plot3d,
psconvert,
rose,
scalebar,
set_panel,
shift_origin,
solar,
Expand Down
1 change: 1 addition & 0 deletions pygmt/src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from pygmt.src.project import project
from pygmt.src.psconvert import psconvert
from pygmt.src.rose import rose
from pygmt.src.scalebar import scalebar
from pygmt.src.select import select
from pygmt.src.shift_origin import shift_origin
from pygmt.src.solar import solar
Expand Down
139 changes: 139 additions & 0 deletions pygmt/src/scalebar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""
scalebar - Add a scale bar.
"""

from collections.abc import Sequence
from typing import Literal

from pygmt.alias import Alias, AliasSystem
from pygmt.clib import Session
from pygmt.exceptions import GMTInvalidInput
from pygmt.helpers import build_arg_list, fmt_docstring
from pygmt.params import Box, Position

__doctest_skip__ = ["scalebar"]


@fmt_docstring
def scalebar( # noqa: PLR0913
self,
position: Position | None = None,
length: float | str | None = None,
height: float | str | None = None,
scale_position: float | Sequence[float] | bool = False,
Copy link
Member Author

Choose a reason for hiding this comment

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

GMT uses loc, GMT.jl uses scale_at_lat, and PyGMT uses scale_position?

Maybe scale_location or scale_lonlat?

label: str | bool = False,
label_alignment: Literal["left", "right", "top", "bottom"] | None = 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.

GMT and GMT.jl use align, currently PyGMT uses label_alignment, but maybe label_position?

unit: bool = False,
Copy link
Member Author

Choose a reason for hiding this comment

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

GMT and GMT.jl uses units, but I feel unit makes more sense here.

fancy: bool = False,
vertical: bool = False,
box: Box | bool = False,
verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"]
| bool = False,
panel: int | Sequence[int] | bool = False,
transparency: float | None = None,
perspective: float | Sequence[float] | str | bool = False,
):
"""
Add a scale bar on the map.

Parameters
----------
position
Specify the location of the scale bar. See :class:`pygmt.params.Position` for
more details.
length
Length of the scale bar in km. Append a suffix to specify different units. Valid
units are: **e**: meters; **f**: feet; **k**: kilometers; **M**: statute mile;
**n**: nautical miles; **u**: US Survey foot.
height
Height of the scale bar. Only works when ``fancy=True``. [Default is ``"5p"``].
scale_position
Specify the location where on a geographic map the scale applies. It can be:

- *slat*: Map scale is calculated for latitude *slat*
- (*slon*, *slat*): Map scale is calculated for latitude *slat* and longitude
*slon*, which is useful for oblique projections.
- ``True``: Map scale is calculated for the middle of the map.
- ``False``: Default to the location of the reference point.
label
Text string to use as the scale bar label. If ``False``, no label is drawn. If
``True``, the distance unit provided in the ``length`` parameter (default is km)
is used as the label. This parameter requires ``fancy=True``.
label_alignment
Alignment of the scale bar label. Choose from ``"left"``, ``"right"``,
``"top"``, or ``"bottom"``. [Default is ``"top"``].
fancy
If ``True``, draw a "fancy" scale bar, which is a segmented bar with alternating
black and white rectangles. If ``False``, draw a plain scale bar.
unit
If ``True``, append the unit to all distance annotations along the scale. For a
plain scale, this will instead select the unit to be appended to the distance
length. The unit is determined from the suffix in the ``length`` or defaults to
``"km"``.
vertical
If ``True``, plot a vertical rather than a horizontal Cartesian scale.
box
Draw a background box behind the directional rose. If set to ``True``, a simple
rectangular box is drawn using :gmt-term:`MAP_FRAME_PEN`. To customize the box
appearance, pass a :class:`pygmt.params.Box` object to control style, fill, pen,
and other box properties.
$perspective
$verbose
$transparency

Examples
--------
>>> import pygmt
>>> from pygmt.params import Box, Position
>>> fig = pygmt.Figure()
>>> fig.basemap(region=[0, 80, -30, 30], projection="M10c", frame=True)
>>> fig.scalebar(
... position=Position((10, 10), cstype="mapcoords"),
... length=1000,
... fancy=True,
... label="Scale",
... unit=True,
... )
>>> fig.show()
"""
self._activate_figure()

if position is None:
msg = "Parameter 'position' must be specified."
raise GMTInvalidInput(msg)
if length is None:
msg = "Parameter 'length' must be specified."
raise GMTInvalidInput(msg)

aliasdict = AliasSystem(
F=Alias(box, name="box"),
L=[
Alias(position, name="position"),
Alias(length, name="length", prefix="+w"),
Alias(
label_alignment,
name="label_alignment",
prefix="+a",
mapping={"left": "l", "right": "r", "top": "t", "bottom": "b"},
),
Alias(scale_position, name="scale_position", prefix="+c", sep="/", size=2),
Alias(fancy, name="fancy", prefix="+f"),
Alias(label, name="label", prefix="+l"),
Alias(unit, name="unit", prefix="+u"),
Alias(vertical, name="vertical", prefix="+v"),
],
).add_common(
V=verbose,
c=panel,
p=perspective,
t=transparency,
)

confdict = {}
if height is not None:
confdict["MAP_SCALE_HEIGHT"] = height

with Session() as lib:
lib.call_module(
module="basemap", args=build_arg_list(aliasdict, confdict=confdict)
)
5 changes: 5 additions & 0 deletions pygmt/tests/baseline/test_scalebar.png.dvc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
outs:
- md5: 0bc967a510306086752fae3b556cc7e2
size: 10201
hash: md5
path: test_scalebar.png
5 changes: 5 additions & 0 deletions pygmt/tests/baseline/test_scalebar_cartesian.png.dvc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
outs:
- md5: e09a7c67f6146530ea594694853b6f98
size: 6508
hash: md5
path: test_scalebar_cartesian.png
5 changes: 5 additions & 0 deletions pygmt/tests/baseline/test_scalebar_complete.png.dvc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
outs:
- md5: c018b219d3ebc719fb1b1686e074dcd9
size: 11749
hash: md5
path: test_scalebar_complete.png
72 changes: 72 additions & 0 deletions pygmt/tests/test_scalebar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""
Test Figure.scalebar.
"""

import pytest
from pygmt import Figure
from pygmt.exceptions import GMTInvalidInput
from pygmt.params import Position


@pytest.mark.mpl_image_compare
def test_scalebar():
"""
Create a map with a scale bar.
"""
fig = Figure()
fig.basemap(region=[100, 120, 20, 30], projection="M10c", frame=True)
fig.scalebar(position=Position((118, 22), cstype="mapcoords"), length=200)
return fig


@pytest.mark.mpl_image_compare
def test_scalebar_complete():
"""
Test all parameters of scalebar.
"""
fig = Figure()
fig.basemap(region=[100, 120, 20, 30], projection="M10c", frame=True)
fig.scalebar(
position=Position((110, 22), cstype="mapcoords"),
length=1000,
height="10p",
fancy=True,
label="Scale",
label_alignment="left",
scale_position=(110, 25),
unit=True,
box=True,
)
return fig


@pytest.mark.mpl_image_compare
def test_scalebar_cartesian():
"""
Test scale bar in Cartesian coordinates.
"""
fig = Figure()
fig.basemap(region=[0, 10, 0, 5], projection="X10c/5c", frame=True)
fig.scalebar(position=Position((2, 1), cstype="mapcoords"), length=1)
fig.scalebar(position=Position((4, 1), cstype="mapcoords"), length=1, vertical=True)
return fig


def test_scalebar_no_position():
"""
Test that an error is raised when position is not provided.
"""
fig = Figure()
fig.basemap(region=[100, 120, 20, 30], projection="M10c", frame=True)
with pytest.raises(GMTInvalidInput):
fig.scalebar(length=200)


def test_scalebar_no_length():
"""
Test that an error is raised when length is not provided.
"""
fig = Figure()
fig.basemap(region=[100, 120, 20, 30], projection="M10c", frame=True)
with pytest.raises(GMTInvalidInput):
fig.scalebar(position=Position((118, 22), cstype="mapcoords"))
Loading