From 543eb0033cb6a3adc681f4d53dd3fca538aa8398 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 3 Jul 2025 18:06:47 +0200 Subject: [PATCH 01/24] adjust value slighlty --- ultraplot/axes/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 8f41e4fb..5014dbf4 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -2150,7 +2150,7 @@ def _parse_colorbar_inset( fontsize = _fontsize_to_pt(fontsize) scale = 1.2 if orientation == "vertical": - scale = 1.8 # we need a little more room + scale *= 2.1 # we need a little more room if label is not None: labspace += 2 * scale * fontsize / 72 else: From 8bc5cd7f7ff64521a52ee21279f8337ef43a1fa3 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 4 Jul 2025 10:04:01 +0200 Subject: [PATCH 02/24] clean up imports --- ultraplot/axes/base.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 5014dbf4..692a9550 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -7,8 +7,8 @@ import inspect import re import types -from numbers import Integral -from typing import Union, Iterable +from numbers import Integral, Number +from typing import Union, Iterable, MutableMapping from collections.abc import Iterable as IterableType try: @@ -31,8 +31,6 @@ 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 from packaging import version From d6b485838dc4b92a83da2cfa3d737136f74b062c Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 4 Jul 2025 10:04:46 +0200 Subject: [PATCH 03/24] refactor positioning labels depending on colorbar loc --- ultraplot/axes/base.py | 179 +++++++++++++++++++++-------------------- 1 file changed, 92 insertions(+), 87 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 692a9550..50ca423d 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -1288,82 +1288,23 @@ def _add_colorbar( width=tickwidth * tickwidthratio, ) # noqa: E501 - if _is_horizontal_loc(loc): - if labelloc is None or _is_horizontal_label(labelloc): - obj.set_label(label) - elif _is_vertical_label(labelloc): - obj.ax.set_ylabel(label) - else: - raise ValueError("Could not determine position") - - elif _is_vertical_loc(loc): - if labelloc is None or _is_vertical_label(labelloc): - obj.set_label(label) - elif _is_horizontal_label(labelloc): - obj.ax.set_xlabel(label) - else: - raise ValueError("Could not determine position") - - elif loc == "fill": - if labelloc is None: - obj.set_label(label) - elif _is_vertical_label(labelloc): - obj.ax.set_ylabel(label) - elif _is_horizontal_label(labelloc): - obj.ax.set_xlabel(label) - else: - raise ValueError("Could not determine position") + # Set label and label location + long_or_short_axis = _get_axis_for( + labelloc, loc, orientation=orientation, ax=obj + ) + long_or_short_axis.set_label_text(label) + long_or_short_axis.set_label_position(labelloc) - else: - # Default to setting label on long axis - obj.set_label(label) - - # Set axis properties if labelloc is specified - if labelloc is not None: - if _is_horizontal_loc(loc) and _is_vertical_label(labelloc): - axis = obj._short_axis() - elif _is_vertical_loc(loc) and _is_horizontal_label(labelloc): - axis = obj._short_axis() - elif loc == "fill": - if _is_horizontal_label(labelloc): - axis = obj._long_axis() - elif _is_vertical_label(labelloc): - axis = obj._short_axis() - - 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 + # Note kw_label is update in place + _determine_label_rotation( + labelrotation, + labelloc=labelloc, + orientation=orientation, + kw_label=kw_label, + ) - kw_label.update({"rotation": labelrotation}) - axis.label.update(kw_label) + long_or_short_axis.label.update(kw_label) # Assume ticks are set on the long axis(!)) if hasattr(obj, "_long_axis"): # mpl <=3.9 @@ -3690,21 +3631,85 @@ def _get_pos_from_locator( return (x, y) -def _is_horizontal_loc(loc): - """Check if location is horizontally oriented.""" - return any(keyword in loc for keyword in ["top", "bottom", "upper", "lower"]) - - -def _is_vertical_loc(loc): - """Check if location is vertically oriented.""" - return loc in ("left", "right") +def _get_axis_for( + labelloc: str, + loc: str, + *, + ax: Axes, + orientation: str, +) -> Axes: + """ + Helper function to determine the axis for a label. + Particularly used for colorbars but can be used for other purposes + """ + def get_short_or_long(which): + if hasattr(ax, f"{which}_axis"): + return getattr(ax, f"{which}_axis") + return getattr(ax, f"_{which}_axis")() + + short = get_short_or_long("short") + long = get_short_or_long("long") + # if the orientation is horizontal, + # the short axis is the y-axis, and the long axis is the + # x-axis. The inverse holds true for vertical orientation. + label_axis = None + + if "left" in labelloc or "right" in labelloc: + # Vertical label, use short axis + label_axis = short if orientation == "horizontal" else long + elif "top" in labelloc or "bottom" in labelloc: + label_axis = long if orientation == "horizontal" else short + # For fill or none, we use default locations. + # This would be the long axis for horizontal orientation + # and the short axis for vertical orientation. + elif labelloc is None or loc == "fill": + if orientation == "horizontal": + label_axis = long + elif orientation == "vertical": + label_axis = short -def _is_horizontal_label(labelloc): - """Check if label location is horizontal.""" - return labelloc in ("top", "bottom") + if label_axis is None: + raise ValueError( + f"Could not determine label axis for {labelloc=}, with {orientation=}." + ) + print(label_axis, labelloc, orientation) + return label_axis -def _is_vertical_label(labelloc): - """Check if label location is vertical.""" - return labelloc in ("left", "right") +def _determine_label_rotation( + labelrotation: str | Number, + labelloc: str, + orientation: str, + kw_label: MutableMapping, +): + """ + Note we update kw_label in place. + """ + if labelrotation == "auto": + # Automatically determine label rotation based on location, we also align the label to make it look + # extra nice for 90 degree rotations + if orientation == "horizontal": + if labelloc in ["left", "right"]: + labelrotation = 90 if "left" else -90 + kw_label["ha"] = "center" + kw_label["va"] = "bottom" if "left" in labelloc else "top" + elif labelloc in ["top", "bottom"]: + labelrotation = 0 + kw_label["ha"] = "center" + kw_label["va"] = "bottom" if "top" in labelloc else "top" + elif orientation == "vertical": + if labelloc in ["left", "right"]: + labelrotation = 90 if "left" in labelloc else 90 + kw_label["ha"] = "center" + kw_label["va"] = "bottom" if "left" in labelloc else "top" + elif labelloc in ["top", "bottom"]: + labelrotation = 0 + kw_label["ha"] = "right" if "top" in labelloc else "left" + kw_label["va"] = "center" + + if not isinstance(labelrotation, (int, float)): + raise ValueError( + f"Label rotation must be a number or 'auto', got {labelrotation!r}." + ) + kw_label.update({"rotation": labelrotation}) From 7d238809e05e30f2f7871ec7f70835ea5e49a631 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 4 Jul 2025 10:06:23 +0200 Subject: [PATCH 04/24] rm debug comment --- ultraplot/axes/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 50ca423d..a2225196 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -3673,7 +3673,6 @@ def get_short_or_long(which): raise ValueError( f"Could not determine label axis for {labelloc=}, with {orientation=}." ) - print(label_axis, labelloc, orientation) return label_axis From 7f140fccae6c915e371099f22096879b3cc8ed3a Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 4 Jul 2025 10:28:44 +0200 Subject: [PATCH 05/24] add labelloc handling with helper --- ultraplot/axes/base.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index a2225196..42f60ed9 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -1289,6 +1289,11 @@ def _add_colorbar( ) # noqa: E501 # Set label and label location + labelloc = _infer_labelloc_if_none( + labelloc, + orientation=orientation, + loc=loc, + ) long_or_short_axis = _get_axis_for( labelloc, loc, orientation=orientation, ax=obj ) @@ -3712,3 +3717,26 @@ def _determine_label_rotation( f"Label rotation must be a number or 'auto', got {labelrotation!r}." ) kw_label.update({"rotation": labelrotation}) + + +def _infer_labelloc_if_none(labelloc: None | str, orientation: str, loc: str) -> str: + if labelloc is None: + # Determine a sensible default + if orientation == "horizontal": + # set default to bottom + # independent on wheter the loc + # is inset or not + labelloc = "bottom" + if labelloc == "top": + labelloc = "top" + elif orientation == "vertical": + # Set default to right + labelloc = "right" + # ..unless we are on the left + if loc == "left": + labelloc = "left" + else: + raise ValueError( + f"Could not determine labelloc position for {orientation=!r}, with {loc=}." + ) + return labelloc From b99c0fa1af427eac88af48e44b0ee3d97d2bc928 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 4 Jul 2025 10:29:10 +0200 Subject: [PATCH 06/24] more edge cases fixing --- ultraplot/axes/base.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 42f60ed9..bdae5474 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -3660,19 +3660,23 @@ def get_short_or_long(which): # x-axis. The inverse holds true for vertical orientation. label_axis = None - if "left" in labelloc or "right" in labelloc: - # Vertical label, use short axis - label_axis = short if orientation == "horizontal" else long - elif "top" in labelloc or "bottom" in labelloc: - label_axis = long if orientation == "horizontal" else short + label_axis = None # For fill or none, we use default locations. # This would be the long axis for horizontal orientation # and the short axis for vertical orientation. - elif labelloc is None or loc == "fill": + if labelloc is None or loc == "fill": if orientation == "horizontal": label_axis = long elif orientation == "vertical": label_axis = short + # if the orientation is horizontal, + # the short axis is the y-axis, and the long axis is the + # x-axis. The inverse holds true for vertical orientation. + elif "left" in labelloc or "right" in labelloc: + # Vertical label, use short axis + label_axis = short if orientation == "horizontal" else long + elif "top" in labelloc or "bottom" in labelloc: + label_axis = long if orientation == "horizontal" else short if label_axis is None: raise ValueError( @@ -3709,8 +3713,8 @@ def _determine_label_rotation( kw_label["va"] = "bottom" if "left" in labelloc else "top" elif labelloc in ["top", "bottom"]: labelrotation = 0 - kw_label["ha"] = "right" if "top" in labelloc else "left" - kw_label["va"] = "center" + kw_label["ha"] = "center" + kw_label["va"] = "bottom" if "top" in labelloc else "top" if not isinstance(labelrotation, (int, float)): raise ValueError( From 11d106d05b28a296a698c72a5a98979ca87f724e Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 4 Jul 2025 10:36:27 +0200 Subject: [PATCH 07/24] spelling and rm comment --- ultraplot/axes/base.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index bdae5474..cca9c89f 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -1301,7 +1301,7 @@ def _add_colorbar( long_or_short_axis.set_label_position(labelloc) labelrotation = _not_none(labelrotation, rc["colorbar.labelrotation"]) - # Note kw_label is update in place + # Note kw_label is updated in place _determine_label_rotation( labelrotation, labelloc=labelloc, @@ -3655,10 +3655,6 @@ def get_short_or_long(which): short = get_short_or_long("short") long = get_short_or_long("long") - # if the orientation is horizontal, - # the short axis is the y-axis, and the long axis is the - # x-axis. The inverse holds true for vertical orientation. - label_axis = None label_axis = None # For fill or none, we use default locations. From 52fa8791cd1b0e2e9e1fee90d39dfae52c68ac90 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 4 Jul 2025 10:56:20 +0200 Subject: [PATCH 08/24] fix error --- ultraplot/axes/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index cca9c89f..829f72fd 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -3695,7 +3695,7 @@ def _determine_label_rotation( # extra nice for 90 degree rotations if orientation == "horizontal": if labelloc in ["left", "right"]: - labelrotation = 90 if "left" else -90 + labelrotation = 90 if "left" in labelloc else -90 kw_label["ha"] = "center" kw_label["va"] = "bottom" if "left" in labelloc else "top" elif labelloc in ["top", "bottom"]: @@ -3704,7 +3704,7 @@ def _determine_label_rotation( kw_label["va"] = "bottom" if "top" in labelloc else "top" elif orientation == "vertical": if labelloc in ["left", "right"]: - labelrotation = 90 if "left" in labelloc else 90 + labelrotation = 90 if "left" in labelloc else -90 kw_label["ha"] = "center" kw_label["va"] = "bottom" if "left" in labelloc else "top" elif labelloc in ["top", "bottom"]: From 9c7e7e2fe3d711051f8c6e71dda0c0c0c720748e Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 4 Jul 2025 10:56:28 +0200 Subject: [PATCH 09/24] add typecheck --- ultraplot/axes/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 829f72fd..09e77d4f 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -3720,6 +3720,8 @@ def _determine_label_rotation( def _infer_labelloc_if_none(labelloc: None | str, orientation: str, loc: str) -> str: + if not isinstance(labelloc, str | None): + raise ValueError(f"Invalid labelloc {labelloc!r}. Must be a string or None.") if labelloc is None: # Determine a sensible default if orientation == "horizontal": From aeb0ec7bfb9e4228b81d802145cebbf1db834464 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 4 Jul 2025 10:56:53 +0200 Subject: [PATCH 10/24] parametrize autorotation --- ultraplot/tests/test_colorbar.py | 58 +++++++++++++++----------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index 4e3712ba..b04b7782 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -309,39 +309,37 @@ def test_label_rotation_colorbar(): break -def test_auto_labelrotation(): - from itertools import product - - locs = ["top", "bottom", "left", "right"] - labellocs = ["top", "bottom", "left", "right"] - +@pytest.mark.parametrize( + ("loc", "labelloc"), + product(["top", "bottom", "left", "right"], ["top", "bottom", "left", "right"]), +) +def test_auto_labelrotation(loc, labelloc): 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) + 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 From 6dd722a2c6f34fbf0abee5cf9a1dceb5e8f74c49 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 4 Jul 2025 10:57:12 +0200 Subject: [PATCH 11/24] relax msg checking error to make it more robust to api changes --- ultraplot/tests/test_colorbar.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index b04b7782..5ab58b2a 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -410,7 +410,7 @@ def test_colorbar_invalid_horizontal_label(cbarloc, invalid_labelloc): # Test ValueError cases - invalid labelloc for different colorbar locations # Horizontal colorbar location with invalid labelloc - with pytest.raises(ValueError, match="Could not determine position"): + with pytest.raises(ValueError): ax.colorbar(cmap, loc=cbarloc, labelloc=invalid_labelloc, label=title) uplt.close(fig) @@ -434,7 +434,7 @@ def test_colorbar_invalid_vertical_label(cbarloc, invalid_labelloc): cmap = uplt.Colormap("plasma_r") title = "Test Label" fig, ax = uplt.subplots() - with pytest.raises(ValueError, match="Could not determine position"): + with pytest.raises(ValueError): ax.colorbar(cmap, loc=cbarloc, labelloc=invalid_labelloc, label=title) uplt.close(fig) @@ -447,7 +447,7 @@ def test_colorbar_invalid_fill_label_placement(invalid_labelloc): cmap = uplt.Colormap("plasma_r") title = "Test Label" fig, ax = uplt.subplots() - with pytest.raises(ValueError, match="Could not determine position"): + with pytest.raises(ValueError): ax.colorbar(cmap, loc="fill", labelloc=invalid_labelloc, label=title) From f2ca1195c15281dbdd3c82bc327c9ee42be7a701 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 4 Jul 2025 11:19:10 +0200 Subject: [PATCH 12/24] bug fixes --- ultraplot/axes/base.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 09e77d4f..9c01c5df 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -3660,11 +3660,8 @@ def get_short_or_long(which): # For fill or none, we use default locations. # This would be the long axis for horizontal orientation # and the short axis for vertical orientation. - if labelloc is None or loc == "fill": - if orientation == "horizontal": - label_axis = long - elif orientation == "vertical": - label_axis = short + if labelloc is None: + label_axis = long # if the orientation is horizontal, # the short axis is the y-axis, and the long axis is the # x-axis. The inverse holds true for vertical orientation. From d990f6644bf3dd2be2bda5a2bad4cbadee0a79f7 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 4 Jul 2025 11:19:22 +0200 Subject: [PATCH 13/24] parametrize another test --- ultraplot/tests/test_colorbar.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index 5ab58b2a..8c0c5bcb 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -278,7 +278,8 @@ def test_draw_edges(rng): return fig -def test_label_placement_colorbar(rng): +@pytest.mark.parametrize("loc", ["top", "bottom", "left", "right"]) +def test_label_placement_colorbar(rng, loc): """ Ensure that all potential combinations of colorbar label placement is possible. @@ -286,9 +287,7 @@ def test_label_placement_colorbar(rng): data = rng.random((10, 10)) fig, ax = uplt.subplots() h = ax.imshow(data) - locs = "top bottom left right".split() - for loc, labelloc in zip(locs, locs): - ax.colorbar(h, loc=loc, labelloc=labelloc) + ax.colorbar(h, loc=loc, labelloc=loc) def test_label_rotation_colorbar(): From 444d7b85abaaca2ae8b5a704863adc5af10bb570 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 4 Jul 2025 11:48:59 +0200 Subject: [PATCH 14/24] swap angles --- ultraplot/axes/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 9c01c5df..fe63b641 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -3692,7 +3692,7 @@ def _determine_label_rotation( # extra nice for 90 degree rotations if orientation == "horizontal": if labelloc in ["left", "right"]: - labelrotation = 90 if "left" in labelloc else -90 + labelrotation = -90 if "left" in labelloc else 90 kw_label["ha"] = "center" kw_label["va"] = "bottom" if "left" in labelloc else "top" elif labelloc in ["top", "bottom"]: @@ -3701,7 +3701,7 @@ def _determine_label_rotation( kw_label["va"] = "bottom" if "top" in labelloc else "top" elif orientation == "vertical": if labelloc in ["left", "right"]: - labelrotation = 90 if "left" in labelloc else -90 + labelrotation = -90 if "left" in labelloc else 90 kw_label["ha"] = "center" kw_label["va"] = "bottom" if "left" in labelloc else "top" elif labelloc in ["top", "bottom"]: From e3ebd20f37a4b046c73776a7f6a20ba50d11f5b0 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 4 Jul 2025 12:03:56 +0200 Subject: [PATCH 15/24] rm helper and default to tick pos --- ultraplot/axes/base.py | 36 ++++-------------------------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index fe63b641..398bad74 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -1289,14 +1289,11 @@ def _add_colorbar( ) # noqa: E501 # Set label and label location - labelloc = _infer_labelloc_if_none( - labelloc, - orientation=orientation, - loc=loc, - ) long_or_short_axis = _get_axis_for( labelloc, loc, orientation=orientation, ax=obj ) + if labelloc is None: + labelloc = long_or_short_axis.get_ticks_position() long_or_short_axis.set_label_text(label) long_or_short_axis.set_label_position(labelloc) @@ -3692,7 +3689,7 @@ def _determine_label_rotation( # extra nice for 90 degree rotations if orientation == "horizontal": if labelloc in ["left", "right"]: - labelrotation = -90 if "left" in labelloc else 90 + labelrotation = 90 kw_label["ha"] = "center" kw_label["va"] = "bottom" if "left" in labelloc else "top" elif labelloc in ["top", "bottom"]: @@ -3701,7 +3698,7 @@ def _determine_label_rotation( kw_label["va"] = "bottom" if "top" in labelloc else "top" elif orientation == "vertical": if labelloc in ["left", "right"]: - labelrotation = -90 if "left" in labelloc else 90 + labelrotation = 90 kw_label["ha"] = "center" kw_label["va"] = "bottom" if "left" in labelloc else "top" elif labelloc in ["top", "bottom"]: @@ -3714,28 +3711,3 @@ def _determine_label_rotation( f"Label rotation must be a number or 'auto', got {labelrotation!r}." ) kw_label.update({"rotation": labelrotation}) - - -def _infer_labelloc_if_none(labelloc: None | str, orientation: str, loc: str) -> str: - if not isinstance(labelloc, str | None): - raise ValueError(f"Invalid labelloc {labelloc!r}. Must be a string or None.") - if labelloc is None: - # Determine a sensible default - if orientation == "horizontal": - # set default to bottom - # independent on wheter the loc - # is inset or not - labelloc = "bottom" - if labelloc == "top": - labelloc = "top" - elif orientation == "vertical": - # Set default to right - labelloc = "right" - # ..unless we are on the left - if loc == "left": - labelloc = "left" - else: - raise ValueError( - f"Could not determine labelloc position for {orientation=!r}, with {loc=}." - ) - return labelloc From 194fdd5f8b15801ae541383b36d1ed116ec4f304 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 4 Jul 2025 12:14:36 +0200 Subject: [PATCH 16/24] some restores --- ultraplot/axes/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 398bad74..af085bfd 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -3657,7 +3657,7 @@ def get_short_or_long(which): # For fill or none, we use default locations. # This would be the long axis for horizontal orientation # and the short axis for vertical orientation. - if labelloc is None: + if not isinstance(labelloc, str): label_axis = long # if the orientation is horizontal, # the short axis is the y-axis, and the long axis is the @@ -3689,7 +3689,7 @@ def _determine_label_rotation( # extra nice for 90 degree rotations if orientation == "horizontal": if labelloc in ["left", "right"]: - labelrotation = 90 + labelrotation = 270 if "left" in labelloc else 90 kw_label["ha"] = "center" kw_label["va"] = "bottom" if "left" in labelloc else "top" elif labelloc in ["top", "bottom"]: @@ -3698,7 +3698,7 @@ def _determine_label_rotation( kw_label["va"] = "bottom" if "top" in labelloc else "top" elif orientation == "vertical": if labelloc in ["left", "right"]: - labelrotation = 90 + labelrotation = 90 if "left" in labelloc else 270 kw_label["ha"] = "center" kw_label["va"] = "bottom" if "left" in labelloc else "top" elif labelloc in ["top", "bottom"]: From 302b618a79b6f7063db45a1796919ea76583a57c Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 4 Jul 2025 12:14:59 +0200 Subject: [PATCH 17/24] some restores --- ultraplot/axes/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index af085bfd..354f4973 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -3689,7 +3689,7 @@ def _determine_label_rotation( # extra nice for 90 degree rotations if orientation == "horizontal": if labelloc in ["left", "right"]: - labelrotation = 270 if "left" in labelloc else 90 + labelrotation = 90 if "left" in labelloc else -90 kw_label["ha"] = "center" kw_label["va"] = "bottom" if "left" in labelloc else "top" elif labelloc in ["top", "bottom"]: @@ -3698,7 +3698,7 @@ def _determine_label_rotation( kw_label["va"] = "bottom" if "top" in labelloc else "top" elif orientation == "vertical": if labelloc in ["left", "right"]: - labelrotation = 90 if "left" in labelloc else 270 + labelrotation = 90 if "left" in labelloc else -90 kw_label["ha"] = "center" kw_label["va"] = "bottom" if "left" in labelloc else "top" elif labelloc in ["top", "bottom"]: From dbf496ec33504285d91e103fc003033c2ae59610 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 4 Jul 2025 12:31:07 +0200 Subject: [PATCH 18/24] further tweaks --- ultraplot/axes/base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 354f4973..05cac34a 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -3691,7 +3691,7 @@ def _determine_label_rotation( if labelloc in ["left", "right"]: labelrotation = 90 if "left" in labelloc else -90 kw_label["ha"] = "center" - kw_label["va"] = "bottom" if "left" in labelloc else "top" + kw_label["va"] = "bottom" elif labelloc in ["top", "bottom"]: labelrotation = 0 kw_label["ha"] = "center" @@ -3700,7 +3700,8 @@ def _determine_label_rotation( if labelloc in ["left", "right"]: labelrotation = 90 if "left" in labelloc else -90 kw_label["ha"] = "center" - kw_label["va"] = "bottom" if "left" in labelloc else "top" + kw_label["va"] = "bottom" + # kw_label["va"] = "bottom" if "left" in labelloc else "top" elif labelloc in ["top", "bottom"]: labelrotation = 0 kw_label["ha"] = "center" From 6a029aaab1203c167d56e81a23ee90730b06fcc9 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 4 Jul 2025 13:35:46 +0200 Subject: [PATCH 19/24] reorganize test and input to match with new defaults --- ultraplot/axes/base.py | 5 ++--- ultraplot/tests/test_colorbar.py | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 05cac34a..808696a6 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -3691,7 +3691,7 @@ def _determine_label_rotation( if labelloc in ["left", "right"]: labelrotation = 90 if "left" in labelloc else -90 kw_label["ha"] = "center" - kw_label["va"] = "bottom" + kw_label["va"] = "bottom" if "left" in labelloc else "bottom" elif labelloc in ["top", "bottom"]: labelrotation = 0 kw_label["ha"] = "center" @@ -3700,8 +3700,7 @@ def _determine_label_rotation( if labelloc in ["left", "right"]: labelrotation = 90 if "left" in labelloc else -90 kw_label["ha"] = "center" - kw_label["va"] = "bottom" - # kw_label["va"] = "bottom" if "left" in labelloc else "top" + kw_label["va"] = "bottom" if "left" in labelloc else "bottom" elif labelloc in ["top", "bottom"]: labelrotation = 0 kw_label["ha"] = "center" diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index 8c0c5bcb..bea3ad1e 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -17,7 +17,6 @@ def test_outer_align(): ax.plot(np.empty((0, 4)), labels=list("abcd")) ax.legend(loc="bottom", align="right", ncol=2) ax.legend(loc="left", align="bottom", ncol=1) - ax.colorbar("magma", loc="r", align="top", shrink=0.5, label="label", extend="both") ax.colorbar( "magma", loc="top", @@ -30,7 +29,18 @@ def test_outer_align(): labelloc="top", labelweight="bold", ) - ax.colorbar("magma", loc="right", extend="both", label="test extensions") + ax.colorbar( + "magma", + loc="r", + align="top", + shrink=0.5, + label="label", + extend="both", + labelrotation=90, + ) + ax.colorbar( + "magma", loc="right", extend="both", label="test extensions", labelrotation=90 + ) fig.suptitle("Align demo") return fig From 75c0b0516bb7542a3ff9532b65c96b36ff9c2f8a Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 5 Jul 2025 17:29:15 +0200 Subject: [PATCH 20/24] checkpoint --- ultraplot/axes/base.py | 144 +++++++++++++++++++---------------------- 1 file changed, 67 insertions(+), 77 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 808696a6..4ab29f85 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -94,7 +94,7 @@ # Projection docstring _proj_docstring = """ -proj, projection : \ +proj, projection : \\ str, `cartopy.crs.Projection`, or `~mpl_toolkits.basemap.Basemap`, optional The map projection specification(s). If ``'cart'`` or ``'cartesian'`` (the default), a `~ultraplot.axes.CartesianAxes` is created. If ``'polar'``, @@ -157,7 +157,7 @@ # Transform docstring # Used for text and add_axes _transform_docstring = """ -transform : {'data', 'axes', 'figure', 'subfigure'} \ +transform : {'data', 'axes', 'figure', 'subfigure'} \\ or `~matplotlib.transforms.Transform`, optional The transform used to interpret the bounds. Can be a `~matplotlib.transforms.Transform` instance or a string representing @@ -175,7 +175,7 @@ This is similar to `matplotlib.axes.Axes.inset_axes`. Parameters ----------- +----------- bounds : 4-tuple of float The (left, bottom, width, height) coordinates for the axes. %(axes.transform)s @@ -194,17 +194,17 @@ Passed to `~Axes.indicate_inset_zoom`. Other parameters ----------------- +----------------- **kwargs Passed to `ultraplot.axes.Axes`. Returns -------- +-------- ultraplot.axes.Axes The inset axes. See also --------- +--------- Axes.indicate_inset_zoom matplotlib.axes.Axes.inset_axes matplotlib.axes.Axes.indicate_inset @@ -215,26 +215,26 @@ This will replace previously drawn zoom indicators. Parameters ----------- +----------- %(artist.patch)s zorder : float, default: 3.5 The `zorder `__ of the indicators. Should be greater than the zorder of elements in the parent axes. Other parameters ----------------- +----------------- **kwargs Passed to `~matplotlib.patches.Patch`. Note ----- +----- This command must be called from the inset axes rather than the parent axes. It is called automatically when ``zoom=True`` is passed to `~Axes.inset_axes` and whenever the axes are drawn (so the line positions always track the axis limits even if they are later changed). See also --------- +--------- matplotlib.axes.Axes.indicate_inset matplotlib.axes.Axes.indicate_inset_zoom """ @@ -258,7 +258,7 @@ Add a panel axes. Parameters ----------- +----------- side : str, optional The panel location. Valid location keys are as follows. @@ -283,13 +283,13 @@ is determined by figure-wide `sharex` and `sharey` settings. Other parameters ----------------- +----------------- **kwargs Passed to `ultraplot.axes.CartesianAxes`. Supports all valid `~ultraplot.axes.CartesianAxes.format` keywords. Returns -------- +-------- ultraplot.axes.CartesianAxes The panel axes. """ @@ -355,15 +355,15 @@ abctitlepad : float, default: :rc:`abc.titlepad` The horizontal padding between a-b-c labels and titles in the same location. %(units.pt)s -ltitle, ctitle, rtitle, ultitle, uctitle, urtitle, lltitle, lctitle, lrtitle \ +ltitle, ctitle, rtitle, ultitle, uctitle, urtitle, lltitle, lctitle, lrtitle \\ : str or sequence, optional Shorthands for the below keywords. -lefttitle, centertitle, righttitle, upperlefttitle, uppercentertitle, upperrighttitle, \ +lefttitle, centertitle, righttitle, upperlefttitle, uppercentertitle, upperrighttitle, \\ lowerlefttitle, lowercentertitle, lowerrighttitle : str or sequence, optional Additional titles in specific positions (see `title` for details). This works as an alternative to the ``ax.format(title='Title', titleloc=loc)`` workflow and permits adding more than one title-like label for a single axes. -a, alpha, fc, facecolor, ec, edgecolor, lw, linewidth, ls, linestyle : default: \ +a, alpha, fc, facecolor, ec, edgecolor, lw, linewidth, ls, linestyle : default: \\ :rc:`axes.alpha`, :rc:`axes.facecolor`, :rc:`axes.edgecolor`, :rc:`axes.linewidth`, '-' Additional settings applied to the background patch, and their shorthands. Their defaults values are the ``'axes'`` properties. @@ -376,7 +376,7 @@ Labels for the subplots lying along the left, top, right, and bottom edges of the figure. The length of each list must match the number of subplots along the corresponding edge. -leftlabelpad, toplabelpad, rightlabelpad, bottomlabelpad : float or unit-spec, default\ +leftlabelpad, toplabelpad, rightlabelpad, bottomlabelpad : float or unit-spec, default\\ : :rc:`leftlabel.pad`, :rc:`toplabel.pad`, :rc:`rightlabel.pad`, :rc:`bottomlabel.pad` The padding between the labels and the axes content. %(units.pt)s @@ -415,7 +415,7 @@ settings passed to `~ultraplot.config.Configurator.context`. """ docstring._snippet_manager["rc.init"] = _rc_format_docstring.format( - "Remaining keyword arguments are passed to `matplotlib.axes.Axes`.\n " + "Remaining keyword arguments are passed to `matplotlib.axes.Axes`.\\n " ) docstring._snippet_manager["rc.format"] = _rc_format_docstring.format("") docstring._snippet_manager["axes.format"] = _axes_format_docstring @@ -424,7 +424,7 @@ # Colorbar docstrings _colorbar_args_docstring = """ -mappable : mappable, colormap-spec, sequence of color-spec, \ +mappable : mappable, colormap-spec, sequence of color-spec, \\ or sequence of `~matplotlib.artist.Artist` There are four options here: @@ -532,16 +532,16 @@ or :rc:`tick.width` if `linewidth` was not passed. tickwidthratio : float, default: :rc:`tick.widthratio` Relative scaling of `tickwidth` used to determine minor tick widths. -ticklabelcolor, ticklabelsize, ticklabelweight \ +ticklabelcolor, ticklabelsize, ticklabelweight \\ : default: :rc:`tick.labelcolor`, :rc:`tick.labelsize`, :rc:`tick.labelweight`. The font color, size, and weight for colorbar tick labels labelloc, labellocation : {'bottom', 'top', 'left', 'right'} The colorbar label location. Inherits from `tickloc` by default. Default is toward the outside of the subplot for outer colorbars and ``'bottom'`` for inset colorbars. -labelcolor, labelsize, labelweight \ +labelcolor, labelsize, labelweight \\ : default: :rc:`label.color`, :rc:`label.size`, and :rc:`label.weight`. The font color, size, and weight for the colorbar label. -a, alpha, framealpha, fc, facecolor, framecolor, ec, edgecolor, ew, edgewidth : default\ +a, alpha, framealpha, fc, facecolor, framecolor, ec, edgecolor, ew, edgewidth : default\\ : :rc:`colorbar.framealpha`, :rc:`colorbar.framecolor` For inset colorbars only. Controls the transparency and color of the background frame. @@ -598,8 +598,8 @@ from the artists in the tuple (if there are multiple unique labels in the tuple group of artists, the tuple group is expanded into unique legend entries -- otherwise, the tuple group elements are drawn on top of eachother). For details - on matplotlib legend handlers and tuple groups, see the matplotlib `legend guide \ -`__. + on matplotlib legend handlers and tuple groups, see the matplotlib `legend guide \\ +-`__. """ _legend_kwargs_docstring = """ frame, frameon : bool, optional @@ -629,12 +629,12 @@ titlefontsize, titlefontweight, titlefontcolor : optional The font size, weight, and color for the legend title. Font size is interpreted by `~ultraplot.utils.units`. The default size is `fontsize`. -borderpad, borderaxespad, handlelength, handleheight, handletextpad, \ +borderpad, borderaxespad, handlelength, handleheight, handletextpad, \\ labelspacing, columnspacing : unit-spec, optional Various matplotlib `~matplotlib.axes.Axes.legend` spacing arguments. %(units.em)s -a, alpha, framealpha, fc, facecolor, framecolor, ec, edgecolor, ew, edgewidth \ -: default: :rc:`legend.framealpha`, :rc:`legend.facecolor`, :rc:`legend.edgecolor`, \ +a, alpha, framealpha, fc, facecolor, framecolor, ec, edgecolor, ew, edgewidth \\ +: default: :rc:`legend.framealpha`, :rc:`legend.facecolor`, :rc:`legend.edgecolor`, \\ :rc:`axes.linewidth` The opacity, face color, edge color, and edge width for the legend frame. c, color, lw, linewidth, m, marker, ls, linestyle, dashes, ms, markersize : optional @@ -992,7 +992,7 @@ def _add_guide_panel(self, loc="fill", align="center", length=0, **kwargs): s.set_visible(False) ax.xaxis.set_visible(False) ax.yaxis.set_visible(False) - ax.patch.set_facecolor("none") + ax.patch.set_facecolor("none") # ignore axes.alpha application ax._panel_hidden = True ax._panel_align[align] = bbox return ax @@ -1113,7 +1113,7 @@ def _add_colorbar( warnings._warn_ultraplot( f"The colorbar() keyword {key!r} was deprecated in v0.10. To " "achieve the same effect, you can pass 'nbins' to the new default " - f"locator DiscreteLocator using {name}_kw={{'nbins': {nbins}}}." + f"locator DiscreteLocator using {name}_kw={{'nbins': {nbins}}}. " ) # Generate and prepare the colorbar axes @@ -1131,7 +1131,7 @@ def _add_colorbar( kwargs.update({"label": label, "length": length, "width": width}) extendsize = _not_none(extendsize, rc["colorbar.insetextend"]) cax, kwargs = self._parse_colorbar_inset( - loc=loc, pad=pad, **kwargs + loc=loc, labelloc=labelloc, pad=pad, **kwargs ) # noqa: E501 # Parse the colorbar mappable @@ -2064,6 +2064,7 @@ def _parse_colorbar_inset( tickloc=None, ticklocation=None, orientation=None, + labelloc=None, **kwargs, ): """ @@ -2091,7 +2092,7 @@ def _parse_colorbar_inset( fontsize = _fontsize_to_pt(fontsize) scale = 1.2 if orientation == "vertical": - scale *= 2.1 # we need a little more room + scale = 1.8 # we need a little more room if label is not None: labspace += 2 * scale * fontsize / 72 else: @@ -2108,71 +2109,60 @@ def _parse_colorbar_inset( # Determine where labels will appear based on orientation and tick location if orientation == "horizontal": - # For horizontal colorbars: 'top' or 'bottom' - labels_on_top = ticklocation == "top" - labels_on_bottom = ticklocation == "bottom" - # Frame is always the same size, slightly larger to accommodate labels frame_width = 2 * xpad + length frame_height = 2 * ypad + width + labspace else: # vertical - # For vertical colorbars: 'left' or 'right' - labels_on_left = ticklocation == "left" - labels_on_right = ticklocation == "right" - # Frame is always the same size, slightly larger to accommodate labels frame_width = 2 * xpad + width + labspace frame_height = 2 * ypad + length + labeltop = labelbottom = labelright = labelleft = 0 + labelspacing = 0.5 * fontsize / 72 + match labelloc: + case "left": + labelleft = labelspacing + case "right": + labelright = labelspacing + case "bottom": + labelbottom = labelspacing + case "top": + labeltop = labelspacing + # Location in axes-relative coordinates # Bounds are x0, y0, width, height in axes-relative coordinates if loc == "upper right": bounds_frame = [1 - frame_width, 1 - frame_height] - if orientation == "horizontal": - # Position colorbar within frame, accounting for label position - cb_x = 1 - frame_width + xpad - cb_y = 1 - frame_height + ypad + (labspace if labels_on_bottom else 0) - bounds_inset = [cb_x, cb_y] - else: # vertical - cb_x = 1 - frame_width + xpad + (labspace if labels_on_left else 0) - cb_y = 1 - frame_height + ypad - bounds_inset = [cb_x, cb_y] - + # Position colorbar within frame, accounting for label position + cb_x = 1 - frame_width + xpad + cb_y = 1 - frame_height + ypad elif loc == "upper left": bounds_frame = [0, 1 - frame_height] - if orientation == "horizontal": - cb_x = xpad - cb_y = 1 - frame_height + ypad + (labspace if labels_on_bottom else 0) - bounds_inset = [cb_x, cb_y] - else: # vertical - cb_x = xpad + (labspace if labels_on_left else 0) - cb_y = 1 - frame_height + ypad - bounds_inset = [cb_x, cb_y] - + cb_x = xpad + cb_y = 1 - frame_height + ypad elif loc == "lower left": bounds_frame = [0, 0] - if orientation == "horizontal": - cb_x = xpad - cb_y = ypad + (labspace if labels_on_bottom else 0) - bounds_inset = [cb_x, cb_y] - else: # vertical - cb_x = xpad + (labspace if labels_on_left else 0) - cb_y = ypad - bounds_inset = [cb_x, cb_y] - + cb_x = xpad + cb_y = ypad else: # lower right bounds_frame = [1 - frame_width, 0] - if orientation == "horizontal": - cb_x = 1 - frame_width + xpad - cb_y = ypad + (labspace if labels_on_bottom else 0) - bounds_inset = [cb_x, cb_y] - else: # vertical - cb_x = 1 - frame_width + xpad + (labspace if labels_on_left else 0) - cb_y = ypad - bounds_inset = [cb_x, cb_y] + cb_x = 1 - frame_width + xpad + cb_y = ypad + + # Center colorbar in frame + if orientation == "vertical" and labelloc in ("top", "bottom"): + cb_x += (frame_width - 2 * xpad - width) / 2 + elif orientation == "horizontal" and labelloc in ("left", "right"): + cb_y += (frame_height - 2 * ypad - width) / 2 + + if orientation == "horizontal": + labelbottom *= 1.5 + if orientation == "vertical": + labeltop *= -0.25 # Set final bounds with proper dimensions + bounds_inset = [cb_x + labelleft, cb_y + labeltop + labelbottom] if orientation == "horizontal": bounds_inset.extend((length, width)) else: # vertical @@ -3301,7 +3291,7 @@ def colorbar(self, mappable, values=None, loc=None, location=None, **kwargs): shrink Alias for `length`. This is included for consistency with `matplotlib.figure.Figure.colorbar`. - length \ + length \\ : float or unit-spec, default: :rc:`colorbar.length` or :rc:`colorbar.insetlength` The colorbar length. For outer colorbars, units are relative to the axes width or height (default is :rcraw:`colorbar.length`). For inset @@ -3455,7 +3445,7 @@ def text( borderinvert : bool, optional If ``True``, the text and border colors are swapped. borderstyle : {'miter', 'round', 'bevel'}, optional - The `line join style \ + The `line join style \\ `__ used for the border. bbox : bool, default: False From 0f7de1a128236cc5a2756f90c41927cbb0a453b3 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sun, 6 Jul 2025 16:07:46 +0200 Subject: [PATCH 21/24] this looks nice but not all cases are covered --- ultraplot/axes/base.py | 94 ++++++++++++++++++++++++++++-------------- 1 file changed, 62 insertions(+), 32 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 4ab29f85..9fc29f99 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -2091,8 +2091,8 @@ def _parse_colorbar_inset( fontsize = rc["xtick.labelsize"] fontsize = _fontsize_to_pt(fontsize) scale = 1.2 - if orientation == "vertical": - scale = 1.8 # we need a little more room + if orientation == "vertical" and labelloc in ("left", "right"): + scale = 2 # we need a little more room if label is not None: labspace += 2 * scale * fontsize / 72 else: @@ -2118,51 +2118,81 @@ def _parse_colorbar_inset( frame_width = 2 * xpad + width + labspace frame_height = 2 * ypad + length - labeltop = labelbottom = labelright = labelleft = 0 - labelspacing = 0.5 * fontsize / 72 - match labelloc: - case "left": - labelleft = labelspacing - case "right": - labelright = labelspacing - case "bottom": - labelbottom = labelspacing - case "top": - labeltop = labelspacing - # Location in axes-relative coordinates # Bounds are x0, y0, width, height in axes-relative coordinates + xframe = yframe = 0 # lower left corner of frame if loc == "upper right": - bounds_frame = [1 - frame_width, 1 - frame_height] + xframe = 1 - frame_width + yframe = 1 - frame_height # Position colorbar within frame, accounting for label position - cb_x = 1 - frame_width + xpad - cb_y = 1 - frame_height + ypad + cb_x = xframe + xpad + cb_y = yframe + ypad + elif loc == "upper left": - bounds_frame = [0, 1 - frame_height] + yframe = 1 - frame_height cb_x = xpad - cb_y = 1 - frame_height + ypad + cb_y = yframe + ypad elif loc == "lower left": - bounds_frame = [0, 0] cb_x = xpad cb_y = ypad else: # lower right - bounds_frame = [1 - frame_width, 0] - cb_x = 1 - frame_width + xpad + xframe = 1 - frame_width + cb_x = xframe + xpad cb_y = ypad - # Center colorbar in frame - if orientation == "vertical" and labelloc in ("top", "bottom"): - cb_x += (frame_width - 2 * xpad - width) / 2 - elif orientation == "horizontal" and labelloc in ("left", "right"): - cb_y += (frame_height - 2 * ypad - width) / 2 - + # Adjust colorbar position based on label location + u = 0.5 * labspace + if orientation == "vertical" and labelloc == "left": + # Move more to the right to accomodate label + cb_x += u + if orientation == "vertical" and labelloc == "top": + cb_x += u + if "upper" in loc: + cb_y -= u + yframe -= u + frame_height += u + frame_width += u + if "right" in loc: + xframe -= u + cb_x -= u + elif "lower" in loc: + frame_height += u + frame_width += u + if "right" in loc: + xframe -= u + cb_x -= u + if orientation == "vertical" and labelloc == "bottom": + if "left" in loc: + cb_x += u + frame_width += u + else: + xframe -= u + frame_width += u + if "lower" in loc: + cb_y += u + frame_height += u + if "upper" in loc: + yframe -= u + frame_height += u if orientation == "horizontal": - labelbottom *= 1.5 - if orientation == "vertical": - labeltop *= -0.25 + cb_y += 2 * u + if "upper" in loc and labelloc == "bottom": + yframe -= u + frame_height += u + elif "lower" in loc and labelloc == "bottom": + frame_height += u + cb_y += 0.5 * u + elif "upper" in loc and labelloc == "top": + cb_y -= 1.5 * u + yframe -= u + frame_height += u + elif "lower" in loc and labelloc == "top": + frame_height += u + cb_y -= 0.5 * u # Set final bounds with proper dimensions - bounds_inset = [cb_x + labelleft, cb_y + labeltop + labelbottom] + bounds_inset = [cb_x, cb_y] + bounds_frame = [xframe, yframe] if orientation == "horizontal": bounds_inset.extend((length, width)) else: # vertical From 616805e234dd3b917f956ac979c5d476d3cbbc78 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sun, 6 Jul 2025 16:11:21 +0200 Subject: [PATCH 22/24] minor refactor --- ultraplot/axes/base.py | 139 ++++++++++++++++++++++------------------- 1 file changed, 73 insertions(+), 66 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 9fc29f99..6a10a45b 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -2086,7 +2086,7 @@ def _parse_colorbar_inset( xpad = units(pad, "em", "ax", axes=self, width=True) ypad = units(pad, "em", "ax", axes=self, width=False) - # Extra space accounting for colorbar label and tick labels + # Calculate space requirements for labels and ticks labspace = rc["xtick.major.size"] / 72 fontsize = rc["xtick.labelsize"] fontsize = _fontsize_to_pt(fontsize) @@ -2098,36 +2098,27 @@ def _parse_colorbar_inset( else: labspace += scale * fontsize / 72 - # Determine space for labels + # Convert to axes-relative coordinates if orientation == "horizontal": labspace /= self._get_size_inches()[1] else: labspace /= self._get_size_inches()[0] - # Bounds are x0, y0, width, height in axes-relative coordinates - # Location in axes-relative coordinates - # Determine where labels will appear based on orientation and tick location - + # Initial frame dimensions (will be adjusted based on label position) if orientation == "horizontal": - # Frame is always the same size, slightly larger to accommodate labels frame_width = 2 * xpad + length frame_height = 2 * ypad + width + labspace - else: # vertical - # Frame is always the same size, slightly larger to accommodate labels frame_width = 2 * xpad + width + labspace frame_height = 2 * ypad + length - # Location in axes-relative coordinates - # Bounds are x0, y0, width, height in axes-relative coordinates - xframe = yframe = 0 # lower left corner of frame + # Initialize frame position and colorbar position + xframe = yframe = 0 # frame lower left corner if loc == "upper right": xframe = 1 - frame_width yframe = 1 - frame_height - # Position colorbar within frame, accounting for label position cb_x = xframe + xpad cb_y = yframe + ypad - elif loc == "upper left": yframe = 1 - frame_height cb_x = xpad @@ -2140,59 +2131,75 @@ def _parse_colorbar_inset( cb_x = xframe + xpad cb_y = ypad - # Adjust colorbar position based on label location - u = 0.5 * labspace - if orientation == "vertical" and labelloc == "left": - # Move more to the right to accomodate label - cb_x += u - if orientation == "vertical" and labelloc == "top": - cb_x += u - if "upper" in loc: - cb_y -= u - yframe -= u - frame_height += u - frame_width += u - if "right" in loc: - xframe -= u - cb_x -= u - elif "lower" in loc: - frame_height += u - frame_width += u - if "right" in loc: - xframe -= u - cb_x -= u - if orientation == "vertical" and labelloc == "bottom": - if "left" in loc: - cb_x += u - frame_width += u - else: - xframe -= u - frame_width += u - if "lower" in loc: - cb_y += u - frame_height += u - if "upper" in loc: - yframe -= u - frame_height += u - if orientation == "horizontal": - cb_y += 2 * u - if "upper" in loc and labelloc == "bottom": - yframe -= u - frame_height += u - elif "lower" in loc and labelloc == "bottom": - frame_height += u - cb_y += 0.5 * u - elif "upper" in loc and labelloc == "top": - cb_y -= 1.5 * u - yframe -= u - frame_height += u - elif "lower" in loc and labelloc == "top": - frame_height += u - cb_y -= 0.5 * u - - # Set final bounds with proper dimensions + # Adjust frame and colorbar position based on label location + label_offset = 0.5 * labspace + + if orientation == "vertical": + if labelloc == "left": + # Move colorbar right to make room for left labels + cb_x += label_offset + + elif labelloc == "top": + # Center colorbar horizontally and extend frame for top labels + cb_x += label_offset + if "upper" in loc: + # Upper positions: extend frame downward + cb_y -= label_offset + yframe -= label_offset + frame_height += label_offset + frame_width += label_offset + if "right" in loc: + xframe -= label_offset + cb_x -= label_offset + elif "lower" in loc: + # Lower positions: extend frame upward + frame_height += label_offset + frame_width += label_offset + if "right" in loc: + xframe -= label_offset + cb_x -= label_offset + + elif labelloc == "bottom": + # Extend frame for bottom labels + if "left" in loc: + cb_x += label_offset + frame_width += label_offset + else: # right + xframe -= label_offset + frame_width += label_offset + + if "lower" in loc: + cb_y += label_offset + frame_height += label_offset + elif "upper" in loc: + yframe -= label_offset + frame_height += label_offset + + elif orientation == "horizontal": + # Base vertical adjustment for horizontal colorbars + cb_y += 2 * label_offset + + if labelloc == "bottom": + if "upper" in loc: + yframe -= label_offset + frame_height += label_offset + elif "lower" in loc: + frame_height += label_offset + cb_y += 0.5 * label_offset + + elif labelloc == "top": + if "upper" in loc: + cb_y -= 1.5 * label_offset + yframe -= label_offset + frame_height += label_offset + elif "lower" in loc: + frame_height += label_offset + cb_y -= 0.5 * label_offset + + # Set final bounds bounds_inset = [cb_x, cb_y] bounds_frame = [xframe, yframe] + if orientation == "horizontal": bounds_inset.extend((length, width)) else: # vertical @@ -2200,7 +2207,7 @@ def _parse_colorbar_inset( bounds_frame.extend((frame_width, frame_height)) - # Make axes and frame with zorder matching default legend zorder + # Create axes and frame cls = mproj.get_projection_class("ultraplot_cartesian") locator = self._make_inset_locator(bounds_inset, self.transAxes) ax = cls(self.figure, locator(self, None).bounds, zorder=5) From 28541d1bb5499079f42e3f16e6d5e08ecc1d25c7 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sun, 6 Jul 2025 16:28:59 +0200 Subject: [PATCH 23/24] resize with other label rotation --- ultraplot/axes/base.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 6a10a45b..685b119f 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -1131,7 +1131,7 @@ def _add_colorbar( kwargs.update({"label": label, "length": length, "width": width}) extendsize = _not_none(extendsize, rc["colorbar.insetextend"]) cax, kwargs = self._parse_colorbar_inset( - loc=loc, labelloc=labelloc, pad=pad, **kwargs + loc=loc, labelloc=labelloc, labelrotation = labelrotation, pad=pad, **kwargs ) # noqa: E501 # Parse the colorbar mappable @@ -2065,6 +2065,7 @@ def _parse_colorbar_inset( ticklocation=None, orientation=None, labelloc=None, + labelrotation=None, **kwargs, ): """ @@ -2134,6 +2135,38 @@ def _parse_colorbar_inset( # Adjust frame and colorbar position based on label location label_offset = 0.5 * labspace + # Account for label rotation if specified + labelrotation = _not_none(labelrotation, 0) # default to 0 degrees + if labelrotation != 0 and label is not None: + # Estimate label text dimensions + import math + + # Rough estimate of text width (characters * font size * 0.6) + estimated_text_width = len(str(label)) * fontsize * 0.6 / 72 + text_height = fontsize / 72 + + # Convert rotation to radians + angle_rad = math.radians(abs(labelrotation)) + + # Calculate rotated dimensions + rotated_width = estimated_text_width * math.cos( + angle_rad + ) + text_height * math.sin(angle_rad) + rotated_height = estimated_text_width * math.sin( + angle_rad + ) + text_height * math.cos(angle_rad) + + # Convert back to axes-relative coordinates + if orientation == "horizontal": + # For horizontal colorbars, rotation affects vertical space + rotation_offset = rotated_height / self._get_size_inches()[1] + else: + # For vertical colorbars, rotation affects horizontal space + rotation_offset = rotated_width / self._get_size_inches()[0] + + # Use the larger of the original offset or rotation-adjusted offset + label_offset = max(label_offset, rotation_offset) + if orientation == "vertical": if labelloc == "left": # Move colorbar right to make room for left labels From 1785fff421c20c1de035930cc8b10fc32cb9c5db Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sun, 6 Jul 2025 16:46:17 +0200 Subject: [PATCH 24/24] add labelloc to inset tests --- ultraplot/tests/test_colorbar.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index bea3ad1e..97436238 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -494,7 +494,7 @@ def test_colorbar_label_no_labelloc(loc): @pytest.mark.parametrize( - ("loc", "orientation"), + ("loc", "orientation", "labelloc"), product( [ "upper left", @@ -503,9 +503,10 @@ def test_colorbar_label_no_labelloc(loc): "lower right", ], ["horizontal", "vertical"], + ["left", "right", "top", "bottom"], ), ) -def test_inset_colorbar_orientation(loc, orientation): +def test_inset_colorbar_orientation(loc, orientation, labelloc): """ """ cmap = uplt.Colormap("viko") fig, ax = uplt.subplots() @@ -513,6 +514,7 @@ def test_inset_colorbar_orientation(loc, orientation): cmap, loc=loc, orientation=orientation, + labellocation=labelloc, label="My Label", ) found = False