From b24f406b47e4039876dbcd36eb3819ea62d180da Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 8 Apr 2024 20:15:30 +0200 Subject: [PATCH] Add a (skippable) check that the axes have equal aspect ratio. When the scalebar is not drawn on an aspect where imshow() has been called (e.g., a point cloud made with plot()), the axes aspect ratio is not automatically set to 1, in which case the scale bar will be wrong (or rather, it will only be correct in the direction (horizontal or vertical) in which it is drawn). To avoid mistakes, add a check for the aspect ratio, emitting a warning when appropriate. The check can be skipped by using new variants for `rotation`: it can now be set to "horizontal-only" ("the scalebar only applies to the horizontal direction") or "vertical-only". (This could also have been a separate kwarg, but something like `check_aspect=False` reads a bit awkwardly to me.) --- README.md | 11 ++++++++- matplotlib_scalebar/scalebar.py | 17 +++++++++++--- matplotlib_scalebar/test_scalebar.py | 34 ++++++++++++++++++++++++++-- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3d2389d..298dbe5 100644 --- a/README.md +++ b/README.md @@ -311,7 +311,16 @@ Default: `False` ### rotation Whether to create a scale bar based on the x-axis (default) or y-axis. -*rotation* can either be `horizontal` or `vertical`. +*rotation* can either be `horizontal`, `vertical`, `horizontal-only`, or +`vertical-only`. + +By default, matplotlib_scalebar checks whether the axes have equal aspect ratio +(so that the scale bar applies both for the x and the y directions), and emits +a warning if this is not the case. This warning can be suppressed by setting +*rotation* to `horizontal-only` ("the colorbar only applies to the horizontal +direction") or `vertical-only` ("the colorbar only applies to the vertical +direction"). + Note you might have to adjust *scale_loc* and *label_loc* to achieve desired layout. Default: `None`, value from matplotlibrc or `horizontal`. diff --git a/matplotlib_scalebar/scalebar.py b/matplotlib_scalebar/scalebar.py index 92382b1..17f17fb 100644 --- a/matplotlib_scalebar/scalebar.py +++ b/matplotlib_scalebar/scalebar.py @@ -83,7 +83,7 @@ "label_loc", _VALID_LABEL_LOCATIONS, ignorecase=True ) -_VALID_ROTATIONS = ["horizontal", "vertical"] +_VALID_ROTATIONS = ["horizontal", "horizontal-only", "vertical", "vertical-only"] _validate_rotation = ValidateInStrings("rotation", _VALID_ROTATIONS, ignorecase=True) @@ -298,8 +298,11 @@ def __init__( :arg animated: animation state (default: ``False``) :type animated: :class`bool` - :arg rotation: either ``horizontal`` or ``vertical`` - (default: rcParams['scalebar.rotation'] or ``horizontal``) + :arg rotation: ``horizontal``, ``vertical``, ``horizontal-only``, or ``vertical-only`` + (default: rcParams['scalebar.rotation'] or ``horizontal``). + By default, ScaleBar checks that it is getting drawn on an axes + with equal aspect ratio and emits a warning if this is not the case. + The -only variants suppress that check. :type rotation: :class:`str` :arg bbox_to_anchor: box that is used to position the scalebar @@ -426,6 +429,14 @@ def _get_value(attr, default): fixed_value = self.fixed_value fixed_units = self.fixed_units or self.units rotation = _get_value("rotation", "horizontal").lower() + if rotation.endswith("-only"): + rotation = rotation[:-5] + else: # Check aspect ratio. + if self.axes.get_aspect() != 1: + warnings.warn( + f"Drawing scalebar on axes with unequal aspect ratio; " + f"either call ax.set_aspect(1) or suppress the warning with " + f"rotation='{rotation}-only'.") label = self.label # Create text properties diff --git a/matplotlib_scalebar/test_scalebar.py b/matplotlib_scalebar/test_scalebar.py index 0abb906..0a93791 100644 --- a/matplotlib_scalebar/test_scalebar.py +++ b/matplotlib_scalebar/test_scalebar.py @@ -2,6 +2,8 @@ # Standard library modules. +import warnings + # Third party modules. import matplotlib @@ -33,7 +35,7 @@ def scalebar(): yield scalebar - plt.draw() + plt.close(fig) def test_mpl_rcParams_update(): @@ -298,7 +300,8 @@ def test_label_formatter(scalebar): assert scalebar.label_formatter(value, units) == "m 5" -@pytest.mark.parametrize("rotation", ["horizontal", "vertical"]) +@pytest.mark.parametrize("rotation", [ + "horizontal", "vertical", "horizontal-only", "vertical-only"]) def test_rotation(scalebar, rotation): assert scalebar.get_rotation() is None assert scalebar.rotation is None @@ -311,6 +314,33 @@ def test_rotation(scalebar, rotation): scalebar.set_rotation("h") +def test_rotation_checks_aspect(): + fig, ax = plt.subplots() + sb = ScaleBar(0.5) + ax.add_artist(sb) + + with warnings.catch_warnings(): + warnings.simplefilter("error") + + for aspect in ["auto", 2]: + ax.set_aspect(aspect) # Warns if not using the -only variants. + for rotation in ["horizontal", "vertical"]: + sb.rotation = rotation + with pytest.warns(): + fig.canvas.draw() + sb.rotation = rotation + "-only" + fig.canvas.draw() + + ax.set_aspect("equal") # Never warn. + for rotation in ["horizontal", "vertical"]: + sb.rotation = rotation + fig.canvas.draw() + sb.rotation = rotation + "-only" + fig.canvas.draw() + + plt.close(fig) + + def test_bbox_to_anchor(scalebar): assert scalebar.get_bbox_to_anchor() is None assert scalebar.bbox_to_anchor is None