From aa9072fbb473e1214d453f375b93009d570c35df Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 26 Apr 2025 08:36:35 +0200 Subject: [PATCH 01/13] sensible defaults and unittest --- ultraplot/axes/base.py | 41 +++++++++++++++++++++++++ ultraplot/tests/test_colorbar.py | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 41c33bab3..034916e94 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -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 @@ -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, default : True + Controls the visibility of the frame. When set to False, the spines of the colorbar are hidden. +labelrotation : str, float, default: "auto" + Controls the rotation of the colorbar label. 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`. """ @@ -1033,6 +1042,8 @@ def _add_colorbar( linewidth=None, edgefix=None, rasterized=None, + outline: bool = True, + labelrotation: Union[str, float] = "auto", **kwargs, ): """ @@ -1220,6 +1231,7 @@ def _add_colorbar( extendfrac=extendfrac, **kwargs, ) + obj.outline.set_visible(outline) obj.ax.grid(False) # obj.minorlocator = minorlocator # backwards compatibility obj.update_ticks = guides._update_ticks.__get__(obj) # backwards compatible @@ -1280,6 +1292,35 @@ def _add_colorbar( case _: raise ValueError("Location not understood.") axis.set_label_position(labelloc) + 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 + # Horizontal colorbar + case (False, _, _): + if labelloc == "left": + kw_label["ha"] = "right" + kw_label["va"] = "center" + elif labelloc == "right": + kw_label["ha"] = "left" + kw_label["va"] = "center" + + 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(): diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index 98a2ec112..5a85b2a49 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -288,3 +288,55 @@ def test_label_placement_colorbar(): locs = "top bottom left right".split() for loc, labelloc in zip(locs, locs): 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") + fig, ax = uplt.subplots() + cbar = ax.colorbar(cmap, labelloc="top", loc="right", vert=False, labelrotation=23) + assert cbar.get_label().get_rotation() == 23 + + +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 is_vertical: + 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}") + + try: + assert actual_rotation == expected_rotation + except: + print(is_horizontal, labelloc, loc) + uplt.show(block=1) + + uplt.close(fig) From cbac9538423f3177b0a768f042390f1bfadafcf6 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 26 Apr 2025 08:41:32 +0200 Subject: [PATCH 02/13] rm debug --- ultraplot/tests/test_colorbar.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index 5a85b2a49..b2e2c8656 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -333,10 +333,5 @@ def test_auto_labelrotation(): actual_rotation = label.get_rotation() ax.set_title(f"loc={loc}, labelloc={labelloc}, rotation={actual_rotation}") - try: - assert actual_rotation == expected_rotation - except: - print(is_horizontal, labelloc, loc) - uplt.show(block=1) - + assert actual_rotation == expected_rotation uplt.close(fig) From 58140f41122fd04269b8b3d263dbca6f2da20274 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 26 Apr 2025 12:17:02 +0200 Subject: [PATCH 03/13] fix edgecases --- ultraplot/axes/base.py | 7 +++++++ ultraplot/tests/test_colorbar.py | 7 ++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 858ed37b8..138dd459b 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -1222,6 +1222,10 @@ 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, @@ -1229,6 +1233,7 @@ def _add_colorbar( format=formatter, drawedges=grid, extendfrac=extendfrac, + orientation=orientation, **kwargs, ) obj.outline.set_visible(outline) @@ -1319,6 +1324,8 @@ def _add_colorbar( elif labelloc == "left": kw_label["va"] = "top" labelrotation = -90 + case (True, None, _): + labelrotation = 90 # Horizontal colorbar case (False, _, _): if labelloc == "left": diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index 2a1e2fddb..c118d7065 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -296,9 +296,14 @@ def test_label_rotation_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) - assert cbar.get_label().get_rotation() == 23 + # Get the label Text object + for which in "xy": + tmp = getattr(cbar.ax, f"{which}axis").label + if tmp.get_text() == mylabel: + assert label.get_rotation() == 23 def test_auto_labelrotation(): From 7ba2c5bb10facd8e4619c487b09ad0ff5922a456 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 26 Apr 2025 12:20:07 +0200 Subject: [PATCH 04/13] update outline --- ultraplot/axes/base.py | 7 ++++--- ultraplot/internals/rcsetup.py | 5 +++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 138dd459b..7baa07066 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -544,8 +544,8 @@ 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, default : True - Controls the visibility of the frame. When set to False, the spines of the colorbar are hidden. +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: "auto" Controls the rotation of the colorbar label. 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. @@ -1042,7 +1042,7 @@ def _add_colorbar( linewidth=None, edgefix=None, rasterized=None, - outline: bool = True, + outline: Union[bool, None] = None, labelrotation: Union[str, float] = "auto", **kwargs, ): @@ -1236,6 +1236,7 @@ def _add_colorbar( orientation=orientation, **kwargs, ) + outline = _not_none(outline, rc["colorbar.outline"]) obj.outline.set_visible(outline) obj.ax.grid(False) # obj.minorlocator = minorlocator # backwards compatibility diff --git a/ultraplot/internals/rcsetup.py b/ultraplot/internals/rcsetup.py index 27eecd1ab..ff1d5bd55 100644 --- a/ultraplot/internals/rcsetup.py +++ b/ultraplot/internals/rcsetup.py @@ -992,6 +992,11 @@ 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.fancybox": ( False, _validate_bool, From 606c2e2093bf265ef497063935e5f104686b6960 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 26 Apr 2025 12:22:23 +0200 Subject: [PATCH 05/13] black formatting --- ultraplot/tests/test_colorbar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index c118d7065..8d0bc968b 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -341,6 +341,7 @@ def test_auto_labelrotation(): assert actual_rotation == expected_rotation uplt.close(fig) + @pytest.mark.mpl_image_compare def test_label_placement_fig_colorbar2(): """ @@ -351,4 +352,3 @@ def test_label_placement_fig_colorbar2(): fig, axs = uplt.subplots(nrows=1, ncols=2) fig.colorbar(cmap, loc="bottom", label="My Label", labelloc="right") return fig - From e835090b3822ddff619a04067fbf7652a057a753 Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Sat, 26 Apr 2025 12:53:01 +0200 Subject: [PATCH 06/13] Update ultraplot/tests/test_colorbar.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ultraplot/tests/test_colorbar.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index 8d0bc968b..345a5c4e7 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -303,8 +303,7 @@ def test_label_rotation_colorbar(): for which in "xy": tmp = getattr(cbar.ax, f"{which}axis").label if tmp.get_text() == mylabel: - assert label.get_rotation() == 23 - + label = tmp def test_auto_labelrotation(): from itertools import product From 91ff1c872a770e800d4854e6b589fc5daf5d10b8 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 26 Apr 2025 12:57:21 +0200 Subject: [PATCH 07/13] trigger black formating --- ultraplot/tests/test_colorbar.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index 345a5c4e7..d4da129f1 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -336,7 +336,6 @@ def test_auto_labelrotation(): 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) From 4fd73b21ca7560c0c068083c1e2b4bcafa2e3b2d Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 26 Apr 2025 12:58:48 +0200 Subject: [PATCH 08/13] black formatting --- ultraplot/tests/test_colorbar.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index d4da129f1..b10cfade4 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -305,6 +305,7 @@ def test_label_rotation_colorbar(): if tmp.get_text() == mylabel: label = tmp + def test_auto_labelrotation(): from itertools import product From 2d7cc17e2efcf917229fc844884ed0ad4ade02c9 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sun, 27 Apr 2025 08:16:26 +0200 Subject: [PATCH 09/13] add validator through rc --- ultraplot/axes/base.py | 3 ++- ultraplot/internals/rcsetup.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 7baa07066..67355a779 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -1043,7 +1043,7 @@ def _add_colorbar( edgefix=None, rasterized=None, outline: Union[bool, None] = None, - labelrotation: Union[str, float] = "auto", + labelrotation: Union[str, float] = None, **kwargs, ): """ @@ -1312,6 +1312,7 @@ 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. diff --git a/ultraplot/internals/rcsetup.py b/ultraplot/internals/rcsetup.py index ff1d5bd55..2484235cf 100644 --- a/ultraplot/internals/rcsetup.py +++ b/ultraplot/internals/rcsetup.py @@ -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. @@ -997,6 +1006,11 @@ def copy(self): _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, From 6887d7c27291364ce7dee29801bf4812d881fb0b Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sun, 27 Apr 2025 08:19:10 +0200 Subject: [PATCH 10/13] update docstring --- 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 67355a779..e3a7a1bcb 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -546,8 +546,8 @@ 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: "auto" - Controls the rotation of the colorbar label. 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. +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. From fba8125593f0314670fb41377d481ddb870a7bec Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sun, 27 Apr 2025 13:59:18 +0200 Subject: [PATCH 11/13] new baseline --- ultraplot/axes/base.py | 9 +++++---- ultraplot/tests/test_colorbar.py | 10 +++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index e3a7a1bcb..2793d5601 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -1331,17 +1331,18 @@ def _add_colorbar( # Horizontal colorbar case (False, _, _): if labelloc == "left": - kw_label["ha"] = "right" kw_label["va"] = "center" + labelrotation = 90 elif labelloc == "right": - kw_label["ha"] = "left" kw_label["va"] = "center" - - labelrotation = 0 + labelrotation = 270 + else: + labelrotation = 0 case Number(): pass case _: labelrotation = 0 + print(labelrotation) kw_label.update({"rotation": labelrotation}) axis.label.update(kw_label) diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index b10cfade4..9a1a01207 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -330,11 +330,11 @@ def test_auto_labelrotation(): is_horizontal = not is_vertical expected_rotation = 0 - if is_vertical: - if labelloc == "left": - expected_rotation = 90 - elif labelloc == "right": - expected_rotation = 270 + 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 From 77d4f62d84697dc735adcf92c4238153e4728c93 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Mon, 28 Apr 2025 08:54:11 +0200 Subject: [PATCH 12/13] rm debug --- ultraplot/axes/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 2793d5601..73d451ccb 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -1342,7 +1342,6 @@ def _add_colorbar( pass case _: labelrotation = 0 - print(labelrotation) kw_label.update({"rotation": labelrotation}) axis.label.update(kw_label) From 5fe1f8c095eeaf109b33a6a98c4df984a011a911 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Mon, 28 Apr 2025 08:55:10 +0200 Subject: [PATCH 13/13] finish test --- ultraplot/tests/test_colorbar.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index 9a1a01207..2ae91c02f 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -304,6 +304,8 @@ def test_label_rotation_colorbar(): 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():