Skip to content

Commit 076de50

Browse files
Figure.colorbar: Add position/length/width and more parameters to specify colorbar position and properties (#4048)
Co-authored-by: Yvonne Fröhlich <94163266+yvonnefroehlich@users.noreply.github.com>
1 parent b243fef commit 076de50

File tree

4 files changed

+300
-32
lines changed

4 files changed

+300
-32
lines changed

pygmt/src/_common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ def _parse_position(
345345
case str() if position in _valid_anchors: # Anchor code
346346
position = Position(position, cstype="inside")
347347
case str(): # Raw GMT command string.
348-
if any(v is not None for v in kwdict.values()):
348+
if any(v is not None and v is not False for v in kwdict.values()):
349349
msg = (
350350
"Parameter 'position' is given with a raw GMT command string, and "
351351
f"conflicts with parameters {', '.join(repr(c) for c in kwdict)}."

pygmt/src/colorbar.py

Lines changed: 193 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,122 @@
55
from collections.abc import Sequence
66
from typing import Literal
77

8+
from pygmt._typing import AnchorCode
89
from pygmt.alias import Alias, AliasSystem
910
from pygmt.clib import Session
11+
from pygmt.exceptions import GMTValueError
1012
from pygmt.helpers import build_arg_list, fmt_docstring, use_alias
11-
from pygmt.params import Box
13+
from pygmt.helpers.utils import is_nonstr_iter
14+
from pygmt.params import Box, Position
15+
from pygmt.src._common import _parse_position
1216

1317
__doctest_skip__ = ["colorbar"]
1418

1519

20+
def _alias_option_D( # noqa: N802, PLR0913
21+
position=None,
22+
length=None,
23+
width=None,
24+
orientation=None,
25+
reverse=False,
26+
nan=False,
27+
nan_position=None,
28+
fg_triangle=False,
29+
bg_triangle=False,
30+
triangle_height=None,
31+
move_text=None,
32+
label_as_column=False,
33+
):
34+
"""
35+
Return a list of Alias objects for the -D option.
36+
"""
37+
# Build the +e modifier from fg_triangle/bg_triangle/triangle_height
38+
if fg_triangle and bg_triangle:
39+
modifier_e = ""
40+
elif fg_triangle:
41+
modifier_e = "f"
42+
elif bg_triangle:
43+
modifier_e = "b"
44+
else:
45+
modifier_e = None
46+
if modifier_e is not None and triangle_height is not None:
47+
modifier_e = f"{modifier_e}{triangle_height}"
48+
49+
# Build the +m modifier from move_text/label_as_column
50+
modifier_m = None
51+
if move_text or label_as_column:
52+
modifier_m = ""
53+
54+
_valids = {"annotations", "label", "unit"}
55+
if move_text is not None:
56+
if (isinstance(move_text, str) and move_text not in _valids) or (
57+
is_nonstr_iter(move_text) and not all(v in _valids for v in move_text)
58+
):
59+
raise GMTValueError(
60+
move_text,
61+
description="move_text",
62+
choices=_valids,
63+
)
64+
if isinstance(move_text, str):
65+
modifier_m = move_text[0]
66+
elif is_nonstr_iter(move_text):
67+
modifier_m = "".join(item[0] for item in move_text)
68+
if label_as_column:
69+
modifier_m += "c"
70+
71+
return [
72+
Alias(position, name="position"),
73+
Alias(length, name="length", prefix="+w"), # +wlength/width
74+
Alias(width, name="width", prefix="/"),
75+
Alias(
76+
orientation,
77+
name="orientation",
78+
mapping={"horizontal": "+h", "vertical": "+v"},
79+
),
80+
Alias(reverse, name="reverse", prefix="+r"),
81+
Alias(
82+
nan,
83+
name="nan",
84+
prefix="+n" if nan_position in {"start", None} else "+N",
85+
),
86+
Alias(
87+
modifier_e,
88+
name="fg_triangle/bg_triangle/triangle_height",
89+
prefix="+e",
90+
),
91+
Alias(modifier_m, name="move_text/label_as_column", prefix="+m"),
92+
]
93+
94+
1695
@fmt_docstring
17-
@use_alias(C="cmap", D="position", L="equalsize", Z="zfile")
96+
@use_alias(C="cmap", L="equalsize", Z="zfile")
1897
def colorbar( # noqa: PLR0913
1998
self,
99+
position: Position | Sequence[float | str] | AnchorCode | None = None,
100+
length: float | str | None = None,
101+
width: float | str | None = None,
102+
orientation: Literal["horizontal", "vertical"] | None = None,
103+
reverse: bool = False,
104+
nan: bool = False,
105+
nan_position: Literal["start", "end"] | None = None,
106+
bg_triangle: bool = False,
107+
fg_triangle: bool = False,
108+
triangle_height: float | None = None,
109+
move_text: Literal["annotations", "label", "unit"] | Sequence[str] | None = None,
110+
label_as_column: bool = False,
111+
box: Box | bool = False,
20112
truncate: Sequence[float] | None = None,
21113
shading: float | Sequence[float] | bool = False,
22114
log: bool = False,
23115
scale: float | None = None,
24116
projection: str | None = None,
25-
box: Box | bool = False,
26-
frame: str | Sequence[str] | bool = False,
27117
region: Sequence[float | str] | str | None = None,
118+
frame: str | Sequence[str] | bool = False,
28119
verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"]
29120
| bool = False,
30121
panel: int | Sequence[int] | bool = False,
31-
transparency: float | None = None,
32122
perspective: float | Sequence[float] | str | bool = False,
123+
transparency: float | None = None,
33124
**kwargs,
34125
):
35126
r"""
@@ -70,33 +161,70 @@ def colorbar( # noqa: PLR0913
70161
- p = perspective
71162
- t = transparency
72163
164+
.. hlist::
165+
:columns: 1
166+
167+
- D = position, **+w**: length/width, **+h**/**+v**: orientation,
168+
**+r**: reverse, **+n**: nan/nan_position,
169+
**+e**: fg_triangle/bg_triangle/triangle_height,
170+
**+m**: move_text/label_as_column
171+
73172
Parameters
74173
----------
75-
frame : str or list
76-
Set colorbar boundary frame, labels, and axes attributes.
77174
$cmap
78-
position : str
79-
[**g**\|\ **j**\|\ **J**\|\ **n**\|\ **x**]\ *refpoint*\
80-
[**+w**\ *length*\ [/\ *width*]]\ [**+e**\ [**b**\|\ **f**][*length*]]\
81-
[**+h**\|\ **v**][**+j**\ *justify*]\
82-
[**+m**\ [**a**\|\ **c**\|\ **l**\|\ **u**]]\
83-
[**+n**\ [*txt*]][**+o**\ *dx*\ [/*dy*]].
84-
Define the reference point on the map for the color scale using one of
85-
four coordinate systems: (1) Use **g** for map (user) coordinates, (2)
86-
use **j** or **J** for setting *refpoint* via a
87-
:doc:`2-character justification code </techref/justification_codes>`
88-
that refers to the (invisible) map domain rectangle,
89-
(3) use **n** for normalized (0-1) coordinates, or (4) use **x** for
90-
plot coordinates (inches, cm, etc.). All but **x** requires both
91-
``region`` and ``projection`` to be specified. Append **+w** followed
92-
by the length and width of the colorbar. If width is not specified
93-
then it is set to 4% of the given length. Give a negative length to
94-
reverse the scale bar. Append **+h** to get a horizontal scale
95-
[Default is vertical (**+v**)]. By default, the anchor point on the
96-
scale is assumed to be the bottom left corner (**BL**), but this can
97-
be changed by appending **+j** followed by a
98-
:doc:`2-character justification code </techref/justification_codes>`
99-
*justify*.
175+
position
176+
Position of the colorbar on the plot. It can be specified in multiple ways:
177+
178+
- A :class:`pygmt.params.Position` object to fully control the reference point,
179+
anchor point, and offset.
180+
- A sequence of two values representing the x- and y-coordinates in plot
181+
coordinates, e.g., ``(1, 2)`` or ``("1c", "2c")``.
182+
- A :doc:`2-character justification code </techref/justification_codes>` for a
183+
position inside the plot, e.g., ``"TL"`` for Top Left corner inside the plot.
184+
185+
If not specified, defaults to bottom-center outside of the plot.
186+
length
187+
width
188+
Length and width of the colorbar. If length is given with a unit ``%`` then it
189+
is in percentage of the corresponding plot side dimension (i.e., plot width for
190+
a horizontal colorbar, or plot height for a vertical colorbar). If width is
191+
given with unit ``%`` then it is in percentage of the bar length. [Length
192+
defaults to 80% of the corresponding plot side dimension, and width defaults
193+
to 4% of the bar length].
194+
orientation
195+
Set the colorbar orientation to either ``"horizontal"`` or ``"vertical"``.
196+
[Default is vertical, unless ``position`` is set to bottom-center or top-center
197+
with ``cstype="outside"`` or ``cstype="inside"``, then horizontal is the
198+
default].
199+
reverse
200+
Reverse the positive direction of the bar.
201+
nan
202+
Draw a rectangle filled with the NaN color (via the **N** entry in the CPT or
203+
:gmt-term:`COLOR_NAN` if no such entry) at the start or end of the colorbar
204+
(controlled via ``nan_position``). If a string is given, use that string as the
205+
label for the NaN color.
206+
nan_position
207+
Set the position of the NaN rectangle. Choose either ``"start"`` or ``"end"`` to
208+
place the NaN color rectangle at the start or end of the colorbar [Default is
209+
``"start"``].
210+
bg_triangle
211+
fg_triangle
212+
If ``True``, draw triangles for the back- or foreground colors [Default is
213+
no triangles]. The back- and/or foreground colors are taken from the **B**
214+
and **F** entries in the CPT. If no such entries exist, then the system default
215+
colors for **B** and **F** are used instead (:gmt-term:`COLOR_BACKGROUND` and
216+
:gmt-term:`COLOR_FOREGROUND`).
217+
triangles_height
218+
Height of the triangles for back- and foreground colors [Default is half
219+
of the bar width].
220+
move_text
221+
Move text (annotations, label, and unit) to opposite side. Accept a sequence of
222+
strings containing one or more of ``"annotations"``, ``"label"``, and
223+
``"unit"``. The default placements of these texts depend on the colorbar
224+
orientation and position.
225+
label_as_column
226+
Print a vertical label as a column of characters (does not work with special
227+
characters).
100228
box
101229
Draw a background box behind the colorbar. If set to ``True``, a simple
102230
rectangular box is drawn using :gmt-term:`MAP_FRAME_PEN`. To customize the box
@@ -138,6 +266,10 @@ def colorbar( # noqa: PLR0913
138266
may be in plot distance units or given as relative fractions and will
139267
be automatically scaled so that the sum of the widths equals the
140268
requested colorbar length.
269+
$projection
270+
$region
271+
frame
272+
Set colorbar boundary frame, labels, and axes attributes.
141273
$verbose
142274
$panel
143275
$perspective
@@ -162,7 +294,39 @@ def colorbar( # noqa: PLR0913
162294
"""
163295
self._activate_figure()
164296

297+
position = _parse_position(
298+
position,
299+
kwdict={
300+
"length": length,
301+
"width": width,
302+
"orientation": orientation,
303+
"reverse": reverse,
304+
"nan": nan,
305+
"nan_position": nan_position,
306+
"bg_triangle": bg_triangle,
307+
"fg_triangle": fg_triangle,
308+
"triangle_height": triangle_height,
309+
"move_text": move_text,
310+
"label_as_column": label_as_column,
311+
},
312+
default=None, # Use GMT's default behavior if position is not provided.
313+
)
314+
165315
aliasdict = AliasSystem(
316+
D=_alias_option_D(
317+
position=position,
318+
length=length,
319+
width=width,
320+
orientation=orientation,
321+
reverse=reverse,
322+
nan=nan,
323+
nan_position=nan_position,
324+
bg_triangle=bg_triangle,
325+
fg_triangle=fg_triangle,
326+
triangle_height=triangle_height,
327+
move_text=move_text,
328+
label_as_column=label_as_column,
329+
),
166330
F=Alias(box, name="box"),
167331
G=Alias(truncate, name="truncate", sep="/", size=2),
168332
I=Alias(shading, name="shading", sep="/", size=2),

pygmt/tests/test_colorbar.py

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
import pytest
66
from pygmt import Figure
7+
from pygmt.alias import AliasSystem
8+
from pygmt.exceptions import GMTInvalidInput
9+
from pygmt.params.position import Position
10+
from pygmt.src.colorbar import _alias_option_D
711

812

913
@pytest.mark.benchmark
@@ -13,7 +17,12 @@ def test_colorbar():
1317
Create a simple colorbar.
1418
"""
1519
fig = Figure()
16-
fig.colorbar(cmap="gmt/rainbow", position="x0c/0c+w4c", frame=True)
20+
fig.colorbar(
21+
cmap="gmt/rainbow",
22+
position=Position((0, 0), cstype="plotcoords"),
23+
length=4,
24+
frame=True,
25+
)
1726
return fig
1827

1928

@@ -26,3 +35,91 @@ def test_colorbar_shading_list():
2635
fig.basemap(region=[0, 10, 0, 2], projection="X10c/2c", frame="a")
2736
fig.colorbar(cmap="gmt/geo", shading=[-0.7, 0.2], frame=True)
2837
return fig
38+
39+
40+
def test_colorbar_alias_D(): # noqa: N802
41+
"""
42+
Test the parameters for the -D option.
43+
"""
44+
45+
def alias_wrapper(**kwargs):
46+
"""
47+
A wrapper function for testing the parameters of -D option.
48+
"""
49+
return AliasSystem(D=_alias_option_D(**kwargs)).get("D")
50+
51+
argstr = alias_wrapper(position=Position("TL", offset=0.2), length=4, width=0.5)
52+
assert argstr == "jTL+o0.2+w4/0.5"
53+
54+
assert alias_wrapper(orientation="horizontal") == "+h"
55+
assert alias_wrapper(orientation="vertical") == "+v"
56+
57+
assert alias_wrapper(reverse=True) == "+r"
58+
59+
assert alias_wrapper(nan=True) == "+n"
60+
assert alias_wrapper(nan=True, nan_position="end") == "+N"
61+
62+
assert alias_wrapper(fg_triangle=True, bg_triangle=True) == "+e"
63+
assert alias_wrapper(fg_triangle=True) == "+ef"
64+
assert alias_wrapper(bg_triangle=True) == "+eb"
65+
assert alias_wrapper(fg_triangle=True, triangle_height=0.4) == "+ef0.4"
66+
argstr = alias_wrapper(fg_triangle=True, bg_triangle=True, triangle_height=0.3)
67+
assert argstr == "+e0.3"
68+
69+
assert alias_wrapper(move_text="annotations") == "+ma"
70+
assert alias_wrapper(move_text="label") == "+ml"
71+
assert alias_wrapper(move_text="unit") == "+mu"
72+
assert alias_wrapper(move_text=["annotations", "label", "unit"]) == "+malu"
73+
assert alias_wrapper(label_as_column=True) == "+mc"
74+
argstr = alias_wrapper(move_text=["annotations", "label"], label_as_column=True)
75+
assert argstr == "+malc"
76+
77+
argstr = alias_wrapper(
78+
position=Position("BR", offset=(0.1, 0.2)),
79+
length=5,
80+
width=0.4,
81+
orientation="vertical",
82+
reverse=True,
83+
nan=True,
84+
nan_position="start",
85+
bg_triangle=True,
86+
triangle_height=0.2,
87+
move_text=["annotations", "unit"],
88+
label_as_column=True,
89+
)
90+
assert argstr == "jBR+o0.1/0.2+w5/0.4+v+r+n+eb0.2+mauc"
91+
92+
93+
@pytest.mark.mpl_image_compare(filename="test_colorbar.png")
94+
def test_colorbar_position_deprecated_syntax():
95+
"""
96+
Check that passing the deprecated GMT CLI syntax string to 'position' works.
97+
"""
98+
fig = Figure()
99+
fig.colorbar(cmap="gmt/rainbow", position="x0/0+w4c", frame=True)
100+
return fig
101+
102+
103+
def test_image_position_mixed_syntax():
104+
"""
105+
Test that mixing deprecated GMT CLI syntax string with new parameters.
106+
"""
107+
fig = Figure()
108+
with pytest.raises(GMTInvalidInput):
109+
fig.colorbar(cmap="gmt/rainbow", position="x0/0", length="4c")
110+
with pytest.raises(GMTInvalidInput):
111+
fig.colorbar(cmap="gmt/rainbow", position="x0/0", width="0.5c")
112+
with pytest.raises(GMTInvalidInput):
113+
fig.colorbar(cmap="gmt/rainbow", position="x0/0", orientation="horizontal")
114+
with pytest.raises(GMTInvalidInput):
115+
fig.colorbar(cmap="gmt/rainbow", position="x0/0", reverse=True)
116+
with pytest.raises(GMTInvalidInput):
117+
fig.colorbar(cmap="gmt/rainbow", position="x0/0", nan=True)
118+
with pytest.raises(GMTInvalidInput):
119+
fig.colorbar(
120+
cmap="gmt/rainbow", position="x0/0", fg_triangle=True, bg_triangle=True
121+
)
122+
with pytest.raises(GMTInvalidInput):
123+
fig.colorbar(cmap="gmt/rainbow", position="x0/0", move_text="label")
124+
with pytest.raises(GMTInvalidInput):
125+
fig.colorbar(cmap="gmt/rainbow", position="x0/0", label_as_column=True)

0 commit comments

Comments
 (0)