Skip to content

sensible defaults and unittest #189

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

Merged
merged 16 commits into from
Apr 28, 2025
Merged
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
50 changes: 50 additions & 0 deletions ultraplot/axes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import matplotlib.text as mtext
import matplotlib.ticker as mticker
import matplotlib.transforms as mtransforms
from typing import Union
from numbers import Number
import numpy as np
from matplotlib import cbook

Expand Down Expand Up @@ -542,6 +544,13 @@
Whether to rasterize the colorbar solids. The matplotlib default was ``True``
but ultraplot changes this to ``False`` since rasterization can cause misalignment
between the color patches and the colorbar outline.
outline : bool, None default : None
Controls the visibility of the frame. When set to False, the spines of the colorbar are hidden. If set to `None` it uses the `rc['colorbar.outline']` value.
labelrotation : str, float, default: None
Controls the rotation of the colorbar label. When set to None it takes on the value of `rc["colorbar.labelrotation"]`. When set to auto it produces a sensible default where the rotation is adjusted to where the colorbar is located. For example, a horizontal colorbar with a label to the left or right will match the horizontal alignment and rotate the label to 0 degrees. Users can provide a float to rotate to any arbitrary angle.



**kwargs
Passed to `~matplotlib.figure.Figure.colorbar`.
"""
Expand Down Expand Up @@ -1033,6 +1042,8 @@ def _add_colorbar(
linewidth=None,
edgefix=None,
rasterized=None,
outline: Union[bool, None] = None,
labelrotation: Union[str, float] = None,
**kwargs,
):
"""
Expand Down Expand Up @@ -1211,15 +1222,22 @@ def _add_colorbar(
# Create colorbar and update ticks and axis direction
# NOTE: This also adds the guides._update_ticks() monkey patch that triggers
# updates to DiscreteLocator when parent axes is drawn.
orientation = _not_none(
kwargs.pop("orientation", None), kwargs.pop("vert", None)
)

obj = cax._colorbar_fill = cax.figure.colorbar(
mappable,
cax=cax,
ticks=locator,
format=formatter,
drawedges=grid,
extendfrac=extendfrac,
orientation=orientation,
**kwargs,
)
outline = _not_none(outline, rc["colorbar.outline"])
obj.outline.set_visible(outline)
obj.ax.grid(False)
# obj.minorlocator = minorlocator # backwards compatibility
obj.update_ticks = guides._update_ticks.__get__(obj) # backwards compatible
Expand Down Expand Up @@ -1294,6 +1312,38 @@ def _add_colorbar(
case _:
raise ValueError("Location not understood.")
axis.set_label_position(labelloc)
labelrotation = _not_none(labelrotation, rc["colorbar.labelrotation"])
if labelrotation == "auto":
# When set to auto, we make the colorbar appear "natural". For example, when we have a
# horizontal colorbar on the top, but we want the label to the sides, we make sure that the horizontal alignment is correct and the labelrotation is horizontal. Below produces "sensible defaults", but can be overridden by the user.
match (vert, labelloc, loc):
# Vertical colorbars
case (True, "left", "left" | "right"):
labelrotation = 90
case (True, "right", "left" | "right"):
if labelloc == "right":
kw_label["va"] = "bottom"
elif labelloc == "left":
kw_label["va"] = "top"
labelrotation = -90
case (True, None, _):
labelrotation = 90
# Horizontal colorbar
case (False, _, _):
if labelloc == "left":
kw_label["va"] = "center"
labelrotation = 90
elif labelloc == "right":
kw_label["va"] = "center"
labelrotation = 270
else:
labelrotation = 0
case Number():
pass
case _:
labelrotation = 0

kw_label.update({"rotation": labelrotation})
axis.label.update(kw_label)
# Assume ticks are set on the long axis(!)
for label in obj._long_axis().get_ticklabels():
Expand Down
19 changes: 19 additions & 0 deletions ultraplot/internals/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,15 @@ def _validate_units(value):
return _validate_units


def _validate_float_or_auto(value):
if value == "auto":
return value
try:
return float(value)
except (ValueError, TypeError):
raise ValueError(f"Value must be a float or 'auto', got {value!r}")


def _rst_table():
"""
Return the setting names and descriptions in an RST-style table.
Expand Down Expand Up @@ -992,6 +1001,16 @@ def copy(self):
'Length of rectangular or triangular "extensions" for panel colorbars.'
+ _addendum_em,
),
"colorbar.outline": (
True,
_validate_bool,
"Whether to draw a frame around the colorbar.",
),
"colorbar.labelrotation": (
"auto",
_validate_float_or_auto,
"Rotation of colorbar labels.",
),
"colorbar.fancybox": (
False,
_validate_bool,
Expand Down
53 changes: 53 additions & 0 deletions ultraplot/tests/test_colorbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,59 @@ def test_label_placement_colorbar():
ax.colorbar(h, loc=loc, labelloc=labelloc)


def test_label_rotation_colorbar():
"""
Ensure that all potential combinations of colorbar
label rotation is possible.
"""
cmap = uplt.colormaps.get_cmap("viridis")
mylabel = "My Label"
fig, ax = uplt.subplots()
cbar = ax.colorbar(cmap, labelloc="top", loc="right", vert=False, labelrotation=23)
# Get the label Text object
for which in "xy":
tmp = getattr(cbar.ax, f"{which}axis").label
if tmp.get_text() == mylabel:
label = tmp
assert label.get_rotation() == 23
break


def test_auto_labelrotation():
from itertools import product

locs = ["top", "bottom", "left", "right"]
labellocs = ["top", "bottom", "left", "right"]

cmap = uplt.colormaps.get_cmap("viridis")
mylabel = "My Label"

for loc, labelloc in product(locs, labellocs):
fig, ax = uplt.subplots()
cbar = ax.colorbar(cmap, loc=loc, labelloc=labelloc, label=mylabel)

# Get the label Text object
for which in "xy":
tmp = getattr(cbar.ax, f"{which}axis").label
if tmp.get_text() == mylabel:
label = tmp
break

is_vertical = loc in ("left", "right")
is_horizontal = not is_vertical

expected_rotation = 0
if labelloc == "left":
expected_rotation = 90
elif labelloc == "right":
expected_rotation = 270

actual_rotation = label.get_rotation()
ax.set_title(f"loc={loc}, labelloc={labelloc}, rotation={actual_rotation}")
assert actual_rotation == expected_rotation
uplt.close(fig)


@pytest.mark.mpl_image_compare
def test_label_placement_fig_colorbar2():
"""
Expand Down
Loading