-
Notifications
You must be signed in to change notification settings - Fork 233
Add the Position class for GMT embellishment placement #4212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 28 commits
eec0fb7
69d4d42
539f66f
97f015f
854804e
6b55dde
3d629cb
576b822
f54bec9
5a2e20b
2c59b7f
d0b62ec
fe18c87
038161b
a6e75bc
3ec8c06
339ce00
4d616de
ad9e0aa
b084e5f
d4ad6e0
7dc37bd
bfecb2d
18b90b3
6b1b5bc
1eae742
721b46f
669b16d
2a38111
0f9ed6c
7d1b076
a779431
2a9cc92
10a0dfb
6f1c2c4
c27213f
d47aaeb
7fc6ffc
d82f4ba
5d29e66
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -214,6 +214,7 @@ Class-style Parameters | |
|
|
||
| Box | ||
| Pattern | ||
| Position | ||
|
|
||
| Enums | ||
| ----- | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| """ | ||
| The Position class for positioning GMT embellishments. | ||
| """ | ||
|
|
||
| import dataclasses | ||
| from collections.abc import Sequence | ||
| from typing import Literal | ||
|
|
||
| from pygmt._typing import AnchorCode | ||
| from pygmt.alias import Alias | ||
| from pygmt.exceptions import GMTValueError | ||
| from pygmt.params.base import BaseParam | ||
|
|
||
|
|
||
| @dataclasses.dataclass(repr=False) | ||
| class Position(BaseParam): | ||
| """ | ||
| Class for positioning embellishments on a plot. | ||
|
|
||
| .. figure:: https://docs.generic-mapping-tools.org/dev/_images/GMT_anchor.png | ||
| :width: 600 px | ||
| :align: center | ||
|
|
||
| The placement of a GMT embellishment (represented by a green rectangle) in | ||
| relation to the underlying plot (represented by a bisque rectangle). | ||
|
|
||
| This class provides flexible positioning for GMT embellishments (e.g., logo, scale, | ||
| rose) by defining a *reference point* on the plot and an *anchor point* on the | ||
| embellishment. The embellishment is positioned so these two points overlap. | ||
|
|
||
| **Conceptual Model** | ||
|
|
||
| Think of it like dropping an anchor from a boat: | ||
|
|
||
| 1. The boat navigates to the *reference point* (a location on the plot) | ||
| 2. The *anchor point* (a specific point on the embellishment) is aligned with the | ||
| *reference point* | ||
| 3. The embellishment is "dropped" at that position | ||
|
|
||
| **Reference Point** | ||
|
|
||
| The *reference point* can be specified in five different ways using the ``type`` and | ||
| ``location`` attributes: | ||
|
|
||
| ``type="mapcoords"`` Map Coordinates | ||
| Use data/geographic coordinates. Specify ``location`` as | ||
| (*longitude*, *latitude*). Useful when tying the embellishment to a specific | ||
| geographic location. | ||
|
|
||
| **Example:** ``location=(135, 20), type="mapcoords"``. | ||
|
|
||
| ``type="plotcoords"`` Plot Coordinates | ||
| Use plot coordinates as distances from the lower-left plot origin. Specify | ||
| ``location`` as (*x*, *y*) with units (e.g., inches, centimeters, points). | ||
| Useful for precise layout control. | ||
|
|
||
| **Example:** ``location=("2c", "2.5c"), type="plotcoords"`` | ||
|
|
||
| ``type="boxcoords"`` Normalized Coordinates | ||
| Use normalized coordinates where (0, 0) is the lower-left corner and (1, 1) is | ||
| the upper-right corner of the bounding box of the current plot. Specify | ||
| ``location`` as (*nx*, *ny*). Useful for positioning relative to plot dimensions | ||
| without units. | ||
|
Comment on lines
+60
to
+64
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| **Example:** ``location=(0.2, 0.1), type="boxcoords"`` | ||
|
|
||
| ``type="inside"`` Inside Plot | ||
| Select one of the nine :doc:`justification codes </techref/justification_codes>` | ||
| as the *reference point*. The *anchor point* defaults to be the same as the | ||
| *reference point*, so the embellishment is placed inside the plot. | ||
|
|
||
| **Example:** ``location="TL", type="inside"`` [anchor point defaults to "TL"] | ||
|
|
||
| ``type="outside"`` Outside Plot | ||
| Similar to ``type="inside"``, but the *anchor point* defaults to the mirror | ||
| opposite of the *reference point*. Useful for placing embellishments outside | ||
| the plot boundaries (e.g., color bars). | ||
|
|
||
| **Example:** ``location="TL", type="outside"`` [anchor point defaults to "BR"] | ||
|
|
||
| **Anchor Point** | ||
|
|
||
| The *anchor point* determines which part of the embellishment aligns with the | ||
| *reference point*. It uses one of nine | ||
| :doc:`justification codes </techref/justification_codes>`. | ||
|
|
||
| Set ``anchor`` explicitly to override these defaults. If not set, the default | ||
| *anchor* behaviors are: | ||
|
|
||
| - ``type="inside"``: Same as the *reference point* justification code | ||
| - ``type="outside"``: Mirror opposite of the *reference point* justification code | ||
| - Other types: ``"MC"`` (middle center) for map rose and scale, ``"BL"`` | ||
| (bottom-left) for other embellishments | ||
|
|
||
| **Offset** | ||
|
|
||
| The ``offset`` parameter shifts the *anchor point* from its default position. | ||
| Offsets are applied to the projected plot coordinates, with positive values moving | ||
| in the direction indicated by the *anchor point*'s justification code. It should be | ||
| a single value (applied to both x and y) or as (*offset_x*, *offset_y*). | ||
|
|
||
| Examples | ||
| -------- | ||
| Position the GMT logo at map coordinates (3, 3) with the logo's middle-left point as | ||
| the anchor, offset by (0.2, 0.2): | ||
|
|
||
| >>> import pygmt | ||
| >>> from pygmt.params import Position | ||
| >>> fig = pygmt.Figure() | ||
| >>> fig.basemap(region=[0, 10, 0, 10], projection="X10c", frame=True) | ||
| >>> fig.logo( | ||
| ... position=Position((3, 3), type="mapcoords", anchor="ML", offset=(0.2, 0.2)), | ||
| ... box=True, | ||
| ... ) | ||
| >>> fig.show() | ||
|
|
||
| Position the GMT logo at the top-left corner inside the plot: | ||
|
|
||
| >>> fig = pygmt.Figure() | ||
| >>> fig.basemap(region=[0, 10, 0, 10], projection="X10c", frame=True) | ||
| >>> fig.logo(position=Position("TL", type="inside", offset="0.2c"), box=True) | ||
| >>> fig.show() | ||
| """ | ||
|
|
||
| #: Location of the reference point on the plot. The format depends on ``type``: | ||
| #: | ||
| #: - ``type="mapcoords"``: (*longitude*, *latitude*) | ||
| #: - ``type="plotcoords"``: (*x*, *y*) with plot units | ||
| #: - ``type="boxcoords"``: (*nx*, *ny*) | ||
| #: - ``type="inside"`` or ``"outside"``: | ||
| #: :doc:`2-character justification codes </techref/justification_codes>` | ||
| location: Sequence[float | str] | AnchorCode | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The docstring above calls this 'reference point', so how about something like
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe In fact, I'd prefer to make it a positional-only parameter, so it will be used like In this case, the specific parameter name doesn't really matter, though
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I did think of positional-only, but a little tricky with dataclasses as you said. Ok to got with
seisman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| #: Types of the reference point. Valid values are: | ||
seisman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| #: | ||
| #: - ``"mapcoords"``: Map/Data coordinates | ||
| #: - ``"plotcoords"``: Plot coordinates | ||
| #: - ``"boxcoords"``: Normalized coordinates | ||
| #: - ``"inside"`` or ``"outside"``: Justification codes | ||
| #: | ||
| #: If not specified, defaults to ``"inside"`` if ``location`` is a justification | ||
| #: code; otherwise defaults to ``"plotcoords"``. | ||
|
Comment on lines
+142
to
+143
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In GMT, the default is "plotcoords", i.e., the code x. Thus, When |
||
| type: ( | ||
| Literal["mapcoords", "inside", "outside", "boxcoords", "plotcoords"] | None | ||
| ) = None | ||
|
|
||
| #: Anchor point on the embellishment using a | ||
| #: :doc:`2-character justification codes </techref/justification_codes>`. | ||
seisman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| #: If ``None``, defaults are applied based on ``type`` (see above). | ||
| anchor: AnchorCode | None = None | ||
|
|
||
| #: Offset for the anchor point as a single value or (*offset_x*, *offset_y*). | ||
| #: If a single value is given, the offset is applied to both x and y directions. | ||
| offset: Sequence[float | str] | None = None | ||
|
|
||
| def _validate(self): | ||
| """ | ||
| Validate the parameters. | ||
| """ | ||
| _valid_anchors = {f"{h}{v}" for v in "TMB" for h in "LCR"} | { | ||
| f"{v}{h}" for v in "TMB" for h in "LCR" | ||
| } | ||
|
Comment on lines
+161
to
+163
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We probably can reuse these codes to validate anchor codes in other wrappers, e.g., the |
||
|
|
||
| # Default to "inside" if type is not specified and location is an anchor code. | ||
| if self.type is None: | ||
| self.type = "inside" if isinstance(self.location, str) else "plotcoords" | ||
|
|
||
| # Validate the location based on type. | ||
| match self.type: | ||
| case "mapcoords" | "plotcoords" | "boxcoords": | ||
| if not isinstance(self.location, Sequence) or len(self.location) != 2: | ||
seisman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| raise GMTValueError( | ||
| self.location, | ||
| description="reference point", | ||
| reason="Expect a sequence of two values.", | ||
| ) | ||
| case "inside" | "outside": | ||
| if self.location not in _valid_anchors: | ||
| raise GMTValueError( | ||
| self.location, | ||
| description="reference point", | ||
| reason="Expect a valid 2-character justification code.", | ||
| ) | ||
| # Validate the anchor if specified. | ||
| if self.anchor is not None and self.anchor not in _valid_anchors: | ||
| raise GMTValueError( | ||
| self.anchor, | ||
| description="anchor point", | ||
| reason="Expect a valid 2-character justification code.", | ||
| ) | ||
|
|
||
| @property | ||
| def _aliases(self): | ||
| return [ | ||
| Alias( | ||
| self.type, | ||
| name="type", | ||
| mapping={ | ||
| "mapcoords": "g", | ||
| "boxcoords": "n", | ||
| "plotcoords": "x", | ||
| "inside": "j", | ||
| "outside": "J", | ||
| }, | ||
| ), | ||
| Alias(self.location, name="location", sep="/", size=2), | ||
| Alias(self.anchor, name="anchor", prefix="+j"), | ||
| Alias(self.offset, name="offset", prefix="+o", sep="/", size=2), | ||
seisman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| """ | ||
| Test the Position class. | ||
| """ | ||
|
|
||
| import pytest | ||
| from pygmt.exceptions import GMTValueError | ||
| from pygmt.params import Position | ||
|
|
||
|
|
||
| def test_params_position_types(): | ||
| """ | ||
| Test the Position class with different types of coordinate systems. | ||
| """ | ||
| # Default type is "plotcoords" for (x,y) and "inside" for anchor codes. | ||
| assert str(Position((1, 2))) == "x1/2" | ||
| assert str(Position("TL")) == "jTL" | ||
|
|
||
| assert str(Position((10, 20), type="mapcoords")) == "g10/20" | ||
| assert str(Position((0.1, 0.2), type="boxcoords")) == "n0.1/0.2" | ||
| assert str(Position(("5c", "3c"), type="plotcoords")) == "x5c/3c" | ||
| assert str(Position("MR", type="inside")) == "jMR" | ||
| assert str(Position("BR", type="outside")) == "JBR" | ||
|
|
||
seisman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def test_params_position_anchor_offset(): | ||
| """ | ||
| Test the Position class with anchor and offset parameters. | ||
| """ | ||
| assert str(Position((10, 20), type="mapcoords", anchor="TL")) == "g10/20+jTL" | ||
| assert str(Position((10, 20), type="mapcoords", offset=(1, 2))) == "g10/20+o1/2" | ||
| pos = Position("TL", type="inside", anchor="MC", offset=("1c", "2c")) | ||
| assert str(pos) == "jTL+jMC+o1c/2c" | ||
|
|
||
seisman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def test_params_position_invalid_location(): | ||
| """ | ||
| Test that invalid location inputs raise GMTValueError. | ||
| """ | ||
| with pytest.raises(GMTValueError): | ||
| Position("invalid", type="mapcoords") | ||
| with pytest.raises(GMTValueError): | ||
| Position((1, 2, 3), type="mapcoords") | ||
| with pytest.raises(GMTValueError): | ||
| Position(5, type="plotcoords") | ||
| with pytest.raises(GMTValueError): | ||
| Position((0.5,), type="boxcoords") | ||
| with pytest.raises(GMTValueError): | ||
| Position((10, 20), type="inside") | ||
| with pytest.raises(GMTValueError): | ||
| Position("TT", type="outside") | ||
|
|
||
seisman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def test_params_position_invalid_anchor(): | ||
| """ | ||
| Test that invalid anchor inputs raise GMTValueError. | ||
| """ | ||
| with pytest.raises(GMTValueError): | ||
| Position((10, 20), type="mapcoords", anchor="XX") | ||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docstrings are modified from the GMT documentation at https://docs.generic-mapping-tools.org/dev/reference/features.html#reference-and-anchor-point-specification.