From ec9b8004a35234876b33ede753e163b0be95f3a1 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 26 Jul 2025 15:22:43 +0800 Subject: [PATCH 1/5] Add Figure.scalebar to plot a scale bar on maps --- pygmt/figure.py | 1 + pygmt/src/__init__.py | 1 + pygmt/src/scalebar.py | 88 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 pygmt/src/scalebar.py diff --git a/pygmt/figure.py b/pygmt/figure.py index 474cd91179b..64b13663a83 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -449,6 +449,7 @@ def _repr_html_(self) -> str: plot3d, psconvert, rose, + scalebar, set_panel, shift_origin, solar, diff --git a/pygmt/src/__init__.py b/pygmt/src/__init__.py index 8905124f917..2aa4e6b5587 100644 --- a/pygmt/src/__init__.py +++ b/pygmt/src/__init__.py @@ -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 diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py new file mode 100644 index 00000000000..f24e8cada63 --- /dev/null +++ b/pygmt/src/scalebar.py @@ -0,0 +1,88 @@ +""" +scalebar - Add a scale bar. +""" + +from collections.abc import Sequence +from typing import Literal + +from pygmt._typing import AnchorCode +from pygmt.alias import Alias, AliasSystem +from pygmt.clib import Session +from pygmt.helpers import build_arg_list + + +def scalebar( # noqa: PLR0913 + self, + position, + length, + position_type: Literal["user", "justify", "mirror", "normalize", "plot"] + | None = None, + label_alignment: Literal["left", "right", "top", "bottom"] | None = None, + scale_position=None, + justify: AnchorCode | None = None, + offset: Sequence[float | str] | None = None, + label: str | bool = False, + fancy: bool = False, + unit: bool = False, + vertical: bool = False, + box=None, +): + """ + Add a scale bar. + + Parameters + ---------- + TODO + + Examples + -------- + >>> import pygmt + >>> from pygmt.params import Box + >>> fig = pygmt.Figure() + >>> fig.basemap(region=[0, 80, -30, 30], projection="M10c", frame=True) + >>> fig.scalebar( + ... position=(10, 10), + ... position_type="user", + ... length=1000, + ... fancy=True, + ... label="Scale", + ... unit=True, + ... ) + >>> fig.show() + """ + self._preprocess() + + aliasdict = AliasSystem( + L=[ + Alias( + position_type, + name="position_type", + mapping={ + "user": "g", + "justify": "j", + "mirror": "J", + "normalize": "n", + "plot": "x", + }, + ), + Alias(position, name="position", separator="/"), + 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", separator="/"), + Alias(fancy, name="fancy", prefix="+f"), + Alias(justify, name="justify", prefix="+j"), + Alias(label, name="label", prefix="+l"), + Alias(offset, name="offset", prefix="+o", separator="/", size=[1, 2]), + Alias(unit, name="unit", prefix="+u"), + Alias(vertical, name="vertical", prefix="+v"), + ], + F=Alias(box, name="box"), + ) + + with Session() as lib: + lib.call_module(module="basemap", args=build_arg_list(aliasdict)) From 3e77f7638f98026057eb90ce073f297ba31fa32f Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 7 Aug 2025 16:27:13 +0800 Subject: [PATCH 2/5] Use new values for position_type --- pygmt/src/scalebar.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index f24e8cada63..805e2295295 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -15,8 +15,9 @@ def scalebar( # noqa: PLR0913 self, position, length, - position_type: Literal["user", "justify", "mirror", "normalize", "plot"] - | None = None, + position_type: Literal[ + "mapcoords", "inside", "outside", "boxcoords", "plotcoords" + ] = "mapcoords", label_alignment: Literal["left", "right", "top", "bottom"] | None = None, scale_position=None, justify: AnchorCode | None = None, @@ -30,6 +31,24 @@ def scalebar( # noqa: PLR0913 """ Add a scale bar. + Parameters + ---------- + position/position_type + Location of the rose. The actual meaning of this parameter depends on the + ``position_type`` parameter. + - ``position_type="mapcoords"``: *position* is given as (x, y) in user + coordinates. + - ``position_type="boxcoords"``: *position* is given as (nx, ny) in normalized + coordinates, where (0, 0) is the lower-left corner and (1, 1) is the + upper-right corner of the map. + - ``position_type="plotcoords"``: *position* is given as (x, y) in plot + coordinates. + - ``position_type="inside"``: *position* is given as a two-character + justification code, meaning the anchor point of the rose is inside the map + bounding box. + - ``position_type="outside"``: *position* is given as a two-character + justification code, but the rose is outside the map bounding box. + Parameters ---------- TODO @@ -58,14 +77,14 @@ def scalebar( # noqa: PLR0913 position_type, name="position_type", mapping={ - "user": "g", - "justify": "j", - "mirror": "J", - "normalize": "n", - "plot": "x", + "mapcoords": "g", + "inside": "j", + "outside": "J", + "boxcoords": "n", + "plotcoords": "x", }, ), - Alias(position, name="position", separator="/"), + Alias(position, name="position", separator="/", size=2), Alias(length, name="length", prefix="+w"), Alias( label_alignment, From 7ab2a8506b39f99e63093001ad9ce7fec558bc95 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 7 Aug 2025 16:38:21 +0800 Subject: [PATCH 3/5] Use new values for position_type --- pygmt/src/scalebar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index 805e2295295..8249a5d7123 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -61,7 +61,7 @@ def scalebar( # noqa: PLR0913 >>> fig.basemap(region=[0, 80, -30, 30], projection="M10c", frame=True) >>> fig.scalebar( ... position=(10, 10), - ... position_type="user", + ... position_type="mapcoords", ... length=1000, ... fancy=True, ... label="Scale", From d5543feeb68a1dfa8bb1f48e84f0545e65cd25ab Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 10 Aug 2025 20:37:31 +0800 Subject: [PATCH 4/5] Updates --- doc/api/index.rst | 1 + pygmt/src/scalebar.py | 33 ++++++++++++++++++++------------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/doc/api/index.rst b/doc/api/index.rst index 7828a225652..830ddad82e1 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -31,6 +31,7 @@ Plotting map elements Figure.inset Figure.legend Figure.logo + Figure.scalebar Figure.solar Figure.text Figure.timestamp diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index 8249a5d7123..a5980d932f2 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -16,12 +16,16 @@ def scalebar( # noqa: PLR0913 position, length, position_type: Literal[ - "mapcoords", "inside", "outside", "boxcoords", "plotcoords" + "mapcoords", + "boxcoords", + "plotcoords", + "inside", + "outside", ] = "mapcoords", label_alignment: Literal["left", "right", "top", "bottom"] | None = None, scale_position=None, justify: AnchorCode | None = None, - offset: Sequence[float | str] | None = None, + anchor_offset: Sequence[float | str] | None = None, label: str | bool = False, fancy: bool = False, unit: bool = False, @@ -34,20 +38,21 @@ def scalebar( # noqa: PLR0913 Parameters ---------- position/position_type - Location of the rose. The actual meaning of this parameter depends on the - ``position_type`` parameter. + Location of the map scale bar. The actual meaning of this parameter depends + on the ``position_type`` parameter. - ``position_type="mapcoords"``: *position* is given as (x, y) in user coordinates. - ``position_type="boxcoords"``: *position* is given as (nx, ny) in normalized coordinates, where (0, 0) is the lower-left corner and (1, 1) is the - upper-right corner of the map. + upper-right corner of the plot. - ``position_type="plotcoords"``: *position* is given as (x, y) in plot - coordinates. + coordinates, i.e., the distances in inches, centimeters, or points from the + lower left plot origin. - ``position_type="inside"``: *position* is given as a two-character - justification code, meaning the anchor point of the rose is inside the map + justification code, meaning the anchor point of the rose is inside the plot bounding box. - ``position_type="outside"``: *position* is given as a two-character - justification code, but the rose is outside the map bounding box. + justification code, but the rose is outside the plot bounding box. Parameters ---------- @@ -78,13 +83,13 @@ def scalebar( # noqa: PLR0913 name="position_type", mapping={ "mapcoords": "g", - "inside": "j", - "outside": "J", "boxcoords": "n", "plotcoords": "x", + "inside": "j", + "outside": "J", }, ), - Alias(position, name="position", separator="/", size=2), + Alias(position, name="position", sep="/", size=2), Alias(length, name="length", prefix="+w"), Alias( label_alignment, @@ -92,11 +97,13 @@ def scalebar( # noqa: PLR0913 prefix="+a", mapping={"left": "l", "right": "r", "top": "t", "bottom": "b"}, ), - Alias(scale_position, name="scale_position", prefix="+c", separator="/"), + Alias(scale_position, name="scale_position", prefix="+c", sep="/"), Alias(fancy, name="fancy", prefix="+f"), Alias(justify, name="justify", prefix="+j"), Alias(label, name="label", prefix="+l"), - Alias(offset, name="offset", prefix="+o", separator="/", size=[1, 2]), + Alias( + anchor_offset, name="anchor_offset", prefix="+o", sep="/", size=[1, 2] + ), Alias(unit, name="unit", prefix="+u"), Alias(vertical, name="vertical", prefix="+v"), ], From 8acb461d0798dadb801ebf7736290a2401fdfe83 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 6 Sep 2025 23:30:44 +0800 Subject: [PATCH 5/5] Add docstrings --- pygmt/src/scalebar.py | 144 +++++++++++++++++++++++++++++++----------- 1 file changed, 108 insertions(+), 36 deletions(-) diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index a5980d932f2..6607cbf7652 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -8,55 +8,118 @@ from pygmt._typing import AnchorCode from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session +from pygmt.exceptions import GMTInvalidInputError from pygmt.helpers import build_arg_list +from pygmt.params import Box def scalebar( # noqa: PLR0913 self, - position, - length, + position: Sequence[float | str] | AnchorCode | None = None, position_type: Literal[ - "mapcoords", - "boxcoords", - "plotcoords", - "inside", - "outside", - ] = "mapcoords", - label_alignment: Literal["left", "right", "top", "bottom"] | None = None, - scale_position=None, - justify: AnchorCode | None = None, + "mapcoords", "boxcoords", "plotcoords", "inside", "outside" + ] = "plotcoords", + anchor: AnchorCode | None = None, anchor_offset: Sequence[float | str] | None = None, + length: float | str | None = None, + label_alignment: Literal["left", "right", "top", "bottom"] | None = None, + scale_position: float | tuple[float, float] | bool = False, label: str | bool = False, fancy: bool = False, unit: bool = False, vertical: bool = False, - box=None, + box: Box | bool = False, + perspective: str | bool = False, + verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] + | bool = False, + transparency: float | None = None, ): """ - Add a scale bar. + Add a scale bar on the map. + + The scale bar is plotted at the location defined by the reference point (specified + by the **position** and *position_type** parameters) and anchor point (specified by + the **anchor** and **anchor_offset** parameters). Refer to + :doc:`/techref/reference_anchor_points` for details about the positioning. Parameters ---------- position/position_type - Location of the map scale bar. The actual meaning of this parameter depends - on the ``position_type`` parameter. - - ``position_type="mapcoords"``: *position* is given as (x, y) in user + Specify the reference point on the map for the directional rose. The reference + point can be specified in five different ways, which is selected by the + **position_type** parameter. The actual reference point is then given by the + coordinates or code specified by the **position** parameter. + + The **position_type** parameter can be one of the following: + + - ``"mapcoords"``: **position** is given as (*longitude*, *latitude*) in map coordinates. - - ``position_type="boxcoords"``: *position* is given as (nx, ny) in normalized - coordinates, where (0, 0) is the lower-left corner and (1, 1) is the - upper-right corner of the plot. - - ``position_type="plotcoords"``: *position* is given as (x, y) in plot - coordinates, i.e., the distances in inches, centimeters, or points from the - lower left plot origin. - - ``position_type="inside"``: *position* is given as a two-character - justification code, meaning the anchor point of the rose is inside the plot - bounding box. - - ``position_type="outside"``: *position* is given as a two-character - justification code, but the rose is outside the plot bounding box. + - ``"boxcoords"``: **position** is given as (*nx*, *ny*) in normalized + coordinates, i.e., fractional coordinates between 0 and 1 in both the x and y + directions. For example, (0, 0) is the lower-left corner and (1, 1) is the + upper-right corner of the plot bounding box. + - ``"plotcoords"``: **position** is given as (x, y) in plot coordinates, i.e., + the distances in inches, centimeters, or points from the lower left plot + origin. + - ``"inside"`` or ``"outside"``: **position** is one of the nine + :doc:`2-character justification codes `, meaning + placing the reference point at specific locations, either inside or outside + the plot bounding box. + anchor + Anchor point of the directional rose, specified by one of the + :doc:`2-character justification codes `. + The default value depends on the **position_type** parameter. - Parameters - ---------- - TODO + - ``position_type="inside"``: **anchor** defaults to the same as **position**. + - ``position_type="outside"``: **anchor** defaults to the mirror opposite of + **position**. + - Otherwise, **anchor** defaults to ``"MC"`` (middle center). + anchor_offset + *offset* or (*offset_x*, *offset_y*). + Offset the anchor point by *offset_x* and *offset_y*. If a single value *offset* + is given, *offset_y* = *offset_x* = *offset*. + length + Length of the scale bar in km. You can append different units to the length, + which are: + - **e**: meters + - **f**: feet + - **k**: kilometers + - **M**: statute mile + - **n**: nautical miles + - **u**: US Survey foot + 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. The 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. A fancy scale bar 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 -------- @@ -76,7 +139,15 @@ def scalebar( # noqa: PLR0913 """ self._preprocess() + if position is None: + msg = "Parameter 'position' must be specified." + raise GMTInvalidInputError(msg) + if length is None: + msg = "Parameter 'length' must be specified." + raise GMTInvalidInputError(msg) + aliasdict = AliasSystem( + F=Alias(box, name="box"), L=[ Alias( position_type, @@ -90,6 +161,8 @@ def scalebar( # noqa: PLR0913 }, ), Alias(position, name="position", sep="/", size=2), + Alias(anchor, name="justify", prefix="+j"), + Alias(anchor_offset, name="anchor_offset", prefix="+o", sep="/", size=2), Alias(length, name="length", prefix="+w"), Alias( label_alignment, @@ -97,17 +170,16 @@ def scalebar( # noqa: PLR0913 prefix="+a", mapping={"left": "l", "right": "r", "top": "t", "bottom": "b"}, ), - Alias(scale_position, name="scale_position", prefix="+c", sep="/"), + Alias(scale_position, name="scale_position", prefix="+c", sep="/", size=2), Alias(fancy, name="fancy", prefix="+f"), - Alias(justify, name="justify", prefix="+j"), Alias(label, name="label", prefix="+l"), - Alias( - anchor_offset, name="anchor_offset", prefix="+o", sep="/", size=[1, 2] - ), Alias(unit, name="unit", prefix="+u"), Alias(vertical, name="vertical", prefix="+v"), ], - F=Alias(box, name="box"), + p=Alias(perspective, name="perspective"), + ).add_common( + V=verbose, + t=transparency, ) with Session() as lib: