Skip to content

Add the Box class for specifying the box of GMT embellishments #3995

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

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions doc/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,16 @@ Xarray Integration
GMTBackendEntrypoint
GMTDataArrayAccessor

Common Parameters
-----------------

.. currentmodule:: pygmt.params

.. autosummary::
:toctree: generated

Box

Enums
-----

Expand Down
3 changes: 2 additions & 1 deletion examples/gallery/embellishments/inset.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

# %%
import pygmt
from pygmt.params import Box

fig = pygmt.Figure()
# Create the primary figure, setting the region to Madagascar, the land color
Expand All @@ -19,7 +20,7 @@
# Create an inset, placing it in the Top Left (TL) corner with a width of 3.5 cm and
# x- and y-offsets of 0.2 cm. The margin is set to 0, and the border is "gold" with a
# pen size of 1.5 points.
with fig.inset(position="jTL+w3.5c+o0.2c", margin=0, box="+p1.5p,gold"):
with fig.inset(position="jTL+w3.5c+o0.2c", margin=0, box=Box(pen="1.5p,gold")):
# Create a figure in the inset using coast. This example uses the azimuthal
# orthogonal projection centered at 47E, 20S. The land color is set to
# "gray" and Madagascar is highlighted in "red3".
Expand Down
3 changes: 2 additions & 1 deletion examples/gallery/embellishments/inset_rectangle_region.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

# %%
import pygmt
from pygmt.params import Box

# Set the region of the main figure
region = [137.5, 141, 34, 37]
Expand All @@ -34,7 +35,7 @@
# a pen of "1p".
with fig.inset(
position="jBR+o0.1c",
box="+gwhite+p1p",
box=Box(fill="white", pen="1p"),
region=[129, 146, 30, 46],
projection="U54S/3c",
):
Expand Down
3 changes: 2 additions & 1 deletion examples/gallery/embellishments/scalebar.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

# %%
import pygmt
from pygmt.params import Box

# Create a new Figure instance
fig = pygmt.Figure()
Expand Down Expand Up @@ -105,7 +106,7 @@
# Fill the box in white with a transparency of 30 percent, add a solid
# outline in darkgray (gray30) with a thickness of 0.5 points, and use
# rounded edges with a radius of 3 points
box="+gwhite@30+p0.5p,gray30,solid+r3p",
box=Box(fill="white@30", pen="0.5p,gray30,solid", radius="3p"),
)

fig.show()
Expand Down
7 changes: 4 additions & 3 deletions examples/gallery/images/cross_section.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

# %%
import pygmt
from pygmt.params import Box

# Define region of study area
# lon_min, lon_max, lat_min, lat_max in degrees East and North
Expand Down Expand Up @@ -44,9 +45,9 @@
# corner with an offset ("+o") of 0.7 centimeters and 0.3 centimeters in x- or y-
# directions, respectively; move the x-label above the horizontal colorbar ("+ml")
position="jBR+o0.7c/0.8c+h+w5c/0.3c+ml",
# Add a box around the colobar with a fill ("+g") in "white" color and a
# transparency ("@") of 30 % and with a 0.8-points thick, black, outline ("+p")
box="+gwhite@30+p0.8p,black",
# Add a box around the colobar, filled in "white" and a 30% transparency, with a
# 0.8-points thick, black, outline.
box=Box(pen="0.8p,black", fill="white@30"),
# Add x- and y-labels ("+l")
frame=["x+lElevation", "y+lm"],
)
Expand Down
3 changes: 2 additions & 1 deletion examples/gallery/lines/hlines_vlines.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# In Cartesian coordinate systems lines are plotted as straight lines.

import pygmt
from pygmt.params import Box

fig = pygmt.Figure()

Expand All @@ -31,7 +32,7 @@
fig.hlines(
y=[2, 3], xmin=[0, 1], xmax=[7, 7.5], pen="1.5p,dodgerblue3", label="Lines 7 & 8"
)
fig.legend(position="JBR+jBR+o0.2c", box="+gwhite+p1p")
fig.legend(position="JBR+jBR+o0.2c", box=Box(pen="1p", fill="white"))

fig.shift_origin(xshift="w+2c")

Expand Down
7 changes: 4 additions & 3 deletions examples/tutorials/advanced/insets.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

# %%
import pygmt
from pygmt.params import Box

# %%
# Prior to creating an inset figure, a larger figure must first be plotted. In
Expand Down Expand Up @@ -48,7 +49,7 @@
water="lightblue",
frame="a",
)
with fig.inset(position="jBL+w3c", box="+pblack+glightred"):
with fig.inset(position="jBL+w3c", box=Box(pen="black", fill="lightred")):
# pass is used to exit the with statement as no plotting methods are
# called
pass
Expand All @@ -72,7 +73,7 @@
water="lightblue",
frame="a",
)
with fig.inset(position="jBL+w3c+o0.5c/0.2c", box="+pblack+glightred"):
with fig.inset(position="jBL+w3c+o0.5c/0.2c", box=Box(pen="black", fill="lightred")):
pass
fig.show()

Expand All @@ -97,7 +98,7 @@
# parameters.
with fig.inset(
position="jBL+o0.5c/0.2c",
box="+pblack",
box=Box(pen="black"),
region=[-80, -65, 35, 50],
projection="M3c",
):
Expand Down
5 changes: 3 additions & 2 deletions examples/tutorials/advanced/legends.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import io

import pygmt
from pygmt.params import Box

# %%
# Create an auto-legend
Expand Down Expand Up @@ -90,7 +91,7 @@

# Add a box with a 2-points thick blue, solid outline and a white fill with a
# transparency of 70 percentage ("@30").
fig.legend(position="jTL+o0.3c/0.2c", box="+p2p,blue+gwhite@30")
fig.legend(position="jTL+o0.3c/0.2c", box=Box(pen="2p,blue", fill="white@30"))

fig.show()

Expand Down Expand Up @@ -152,7 +153,7 @@
fig.basemap(region=[-5, 5, -5, 5], projection="M10c", frame=True)

# Pass the io.StringIO object to the "spec" parameter
fig.legend(spec=spec_io, position="jMC+w9c", box="+p1p,gray50+ggray95")
fig.legend(spec=spec_io, position="jMC+w9c", box=Box(pen="1p,gray50", fill="gray95"))

fig.show()

Expand Down
5 changes: 5 additions & 0 deletions pygmt/params/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""
Classes for common parameters in PyGMT.
"""

from pygmt.params.box import Box
88 changes: 88 additions & 0 deletions pygmt/params/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""
Base class for common parameters shared in PyGMT.
"""


class BaseParam:
"""
Base class for parameters in PyGMT.

To define a new parameter class, inherit from this class and define the attributes
that correspond to the parameters you want to include. The class should also
implement the ``_aliases`` property, which returns a list of ``Alias`` objects. Each
``Alias`` object represents a parameter and its value, and the ``__str__`` method
will concatenate these values into a single string that can be passed to GMT.

Examples
--------
>>> from typing import Any
>>> import dataclasses
>>> from pygmt.params.base import BaseParam
>>> from pygmt.alias import Alias
>>>
>>> @dataclasses.dataclass(repr=False)
... class Test(BaseParam):
... par1: Any = None
... par2: Any = None
... par3: Any = None
...
... @property
... def _aliases(self):
... return [
... Alias(self.par1),
... Alias(self.par2, prefix="+a"),
... Alias(self.par3, prefix="+b", separator="/"),
... ]

>>> var = Test(par1="val1")
>>> str(var)
'val1'
>>> repr(var)
"Test(par1='val1')"

>>> var = Test(par1="val1", par2="val2", par3=("val3a", "val3b"))
>>> str(var)
'val1+aval2+bval3a/val3b'
>>> repr(var)
"Test(par1='val1', par2='val2', par3=('val3a', 'val3b'))"
"""

def __post_init__(self):
"""
Post-initialization method to _validate the _aliases property.
"""
self._validate()

def _validate(self):
"""
Validate the parameters of the object.

This method should be overridden in subclasses to perform any necessary
validation on the parameters.
"""

@property
def _aliases(self):
"""
List of Alias objects representing the parameters of this class.

This property must be implemented in subclasses to define the parameters
and their aliases.
"""
msg = "The _aliases property must be implemented in subclasses."
raise NotImplementedError(msg)

def __str__(self):
"""
String representation of the object that can be passed to GMT directly.
"""
return "".join(
[alias._value for alias in self._aliases if alias._value is not None]
)

def __repr__(self):
"""
String representation of the object.
"""
params = ", ".join(f"{k}={v!r}" for k, v in vars(self).items() if v is not None)
return f"{self.__class__.__name__}({params})"
128 changes: 128 additions & 0 deletions pygmt/params/box.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""
The Box class for specifying the box around GMT embellishments.
"""

import dataclasses
from collections.abc import Sequence

from pygmt.alias import Alias
from pygmt.exceptions import GMTValueError
from pygmt.params.base import BaseParam


@dataclasses.dataclass(repr=False)
class Box(BaseParam):
"""
Class for specifying the box around GMT embellishments.

Attributes
----------
clearance
Set clearances between the embellishment and the box border. It can be either a
scalar value or a sequence of two/four values.

- a scalar value means a uniform clearance in all four directions.
- a sequence of two values means separate clearances in x- and y-directions.
- a sequence of four values means separate clearances for left/right/bottom/top.
fill
Fill for the box. Default is no fill.
pen
Pen attributes for the box outline.
radius
Draw a rounded rectangular border instead of sharp. Passing a value with unit
to control the corner radius [Default is ``"6p"``].
inner_gap
Gap between the outer and inner borders. Default is ``"2p"``.
inner_pen
Pen attributes for the inner border. Default to :gmt-term:`MAP_DEFAULT_PEN`.
shading_offset
Place an offset background shaded region behind the box. A sequence of two
values (dx, dy) indicates the shift relative to the foreground frame. Default is
``("4p", "-4p")``.
shading_fill
Fill for the shading region. Default is ``"gray50"``.

Examples
--------
>>> from pygmt.params import Box
>>> str(Box(fill="red@20"))
'+gred@20'
>>> str(Box(clearance=(0.2, 0.2), fill="red@20", pen="blue"))
'+c0.2/0.2+gred@20+pblue'
>>> str(Box(clearance=(0.2, 0.2), pen="blue", radius=True))
'+c0.2/0.2+pblue+r'
>>> str(Box(clearance=(0.1, 0.2, 0.3, 0.4), pen="blue", radius="10p"))
'+c0.1/0.2/0.3/0.4+pblue+r10p'
>>> str(
... Box(
... clearance=0.2,
... pen="blue",
... radius="10p",
... shading_offset=("5p", "5p"),
... shading_fill="lightred",
... )
... )
'+c0.2+pblue+r10p+s5p/5p/lightred'
>>> str(Box(clearance=0.2, inner_gap="2p", inner_pen="1p,red", pen="blue"))
'+c0.2+i2p/1p,red+pblue'
>>> str(Box(clearance=0.2, shading_offset=("5p", "5p"), shading_fill="lightred"))
'+c0.2+s5p/5p/lightred'
"""

# The GMT CLI syntax is:
#
# -F[+cclearances][+gfill][+i[[gap/]pen]][+p[pen]][+r[radius]][+s[[dx/dy/][shade]]]
clearance: float | str | Sequence[float | str] | None = None
fill: str | None = None
inner_gap: float | str | None = None
inner_pen: str | None = None
pen: str | None = None
radius: str | bool = False
shading_offset: Sequence[float | str] | None = None
shading_fill: str | None = None

def _validate(self):
"""
Validate the parameters.
"""
# shading_offset must be a sequence of two values or None.
if self.shading_offset and (
not isinstance(self.shading_offset, Sequence)
or len(self.shading_offset) != 2
):
raise GMTValueError(
self.shading_offset,
description="value for parameter 'shading_offset'",
reason="Must be a sequence of two values (dx, dy) or None.",
)

@property
def _innerborder(self) -> list[str | float] | None:
"""
Inner border of the box, formatted as a list of 1-2 values, or None.
"""
return [v for v in (self.inner_gap, self.inner_pen) if v is not None] or None

@property
def _shading(self) -> list[str | float] | None:
"""
Shading for the box, formatted as a list of 1-3 values, or None.
"""
_shading_offset = self.shading_offset or []
return [
v for v in (*_shading_offset, self.shading_fill) if v is not None
] or None

@property
def _aliases(self):
"""
Aliases for the parameter.
"""
return [
Alias(self.clearance, prefix="+c", separator="/", size=[1, 2, 4]),
Alias(self.fill, prefix="+g"),
Alias(self._innerborder, prefix="+i", separator="/", size=[1, 2]),
Alias(self.pen, prefix="+p"),
Alias(self.radius, prefix="+r"),
Alias(self._shading, prefix="+s", separator="/", size=[1, 2, 3]),
]
Loading
Loading