diff --git a/README.md b/README.md index 67bff55..9a6cde3 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,7 @@ scalebar = ScaleBar( dimension="si-length", label=None, length_fraction=None, + thickness=None, height_fraction=None, width_fraction=None, location=None, @@ -253,13 +254,20 @@ In the example below, the scale bar for a *length_fraction* of 0.25 and 0.5 is t ![length fraction](doc/argument_length_fraction.png) +### thickness + +Width and unit of the scale bar (valid units: "saxis", i.e. relative to the short axis size; +"laxis", i.e. relative to the long axis size; "font", i.e. relative to the font size). +Default: `None`, value from matplotlibrc or `(0.01, "saxis")`. + ### height_fraction -**Deprecated**, use *width_fraction*. +**Deprecated**, use *thickness* or *width_fraction*. ### width_fraction Width of the scale bar as a fraction of the subplot's height. +*thickness* is a more general way to set this parameter. Default: `None`, value from matplotlibrc or `0.01`. ### location @@ -567,4 +575,4 @@ Copyright (c) 2015-2025 Philippe Pinard [i56]: https://github.com/ppinard/matplotlib-scalebar/pull/56 [i58]: https://github.com/ppinard/matplotlib-scalebar/issues/58 [i61]: https://github.com/ppinard/matplotlib-scalebar/pull/61 -[i62]: https://github.com/ppinard/matplotlib-scalebar/pull/62 \ No newline at end of file +[i62]: https://github.com/ppinard/matplotlib-scalebar/pull/62 diff --git a/matplotlib_scalebar/scalebar.py b/matplotlib_scalebar/scalebar.py index efd5a5d..4db6a85 100644 --- a/matplotlib_scalebar/scalebar.py +++ b/matplotlib_scalebar/scalebar.py @@ -12,7 +12,7 @@ The following parameters are available for customization in the matplotlibrc: - scalebar.length_fraction - - scalebar.height_fraction + - scalebar.thickness - scalebar.location - scalebar.pad - scalebar.border_pad @@ -39,8 +39,9 @@ # Standard library modules. import bisect -import warnings import dataclasses +import numbers +import warnings # Third party modules. import matplotlib @@ -61,6 +62,7 @@ AnchoredOffsetbox, ) from matplotlib.patches import Rectangle +from matplotlib.transforms import IdentityTransform, blended_transform_factory # Local modules. from matplotlib_scalebar.dimension import ( @@ -96,10 +98,19 @@ def _validate_legend_loc(loc): return loc +def _validate_dim(dim): + if (len(dim) == 2 + and isinstance(dim[0], numbers.Real) + and dim[1] in ["saxis", "laxis", "font"]): + return dim + else: + raise ValueError("Not a valid dimension") + + defaultParams.update( { "scalebar.length_fraction": [0.2, validate_float], - "scalebar.width_fraction": [0.01, validate_float], + "scalebar.thickness": [(0.01, "saxis"), _validate_dim], "scalebar.location": ["upper right", _validate_legend_loc], "scalebar.pad": [0.2, validate_float], "scalebar.border_pad": [0.1, validate_float], @@ -177,6 +188,7 @@ def __init__( dimension="si-length", label=None, length_fraction=None, + thickness=None, height_fraction=None, width_fraction=None, location=None, @@ -242,9 +254,15 @@ def __init__( This argument is ignored if a *fixed_value* is specified. :type length_fraction: :class:`float` - :arg width_fraction: width of the scale bar as a fraction of the - axes's height (default: rcParams['scalebar.width_fraction'] or ``0.01``) - :type width_fraction: :class:`float` + :arg thickness: thickness of the scale bar, as a ``(value, unit)`` pair. + Valid units are + * "laxis": value is relative to the size of the parent axes in the + "long" direction. + * "saxis": value is relative to the size of the parent axes in the + "short" direction. + * "font": value is relative to the label fontsize. + (default: rcParams['scalebar.thickness'] or ``(0.01, "saxis")``) + :type thickness: ``tuple[float, str]`` :arg location: a location code (same as legend) (default: rcParams['scalebar.location'] or ``upper right``) @@ -347,6 +365,12 @@ def __init__( ) scale_formatter = scale_formatter or label_formatter + if width_fraction is not None: + if thickness is not None: + warnings.warn("Ignoring 'width_fraction', as 'thickness' is also set") + else: + thickness = (width_fraction, "saxis") + if ( loc is not None and location is not None @@ -359,7 +383,7 @@ def __init__( self.units = units self.label = label self.length_fraction = length_fraction - self.width_fraction = width_fraction + self.thickness = thickness self.location = location or loc self.pad = pad self.border_pad = border_pad @@ -433,7 +457,7 @@ def _get_value(attr, default): return value length_fraction = _get_value("length_fraction", 0.2) - width_fraction = _get_value("width_fraction", 0.01) + thickness_value, thickness_unit = _get_value("thickness", (0.01, "saxis")) location = _get_value("location", "upper right") if isinstance(location, str): location = self._LOCATIONS[location.lower()] @@ -486,14 +510,29 @@ def _get_value(attr, default): scale_text = self.scale_formatter(value, self.dimension.to_latex(units)) - width_px = abs(ylim[1] - ylim[0]) * width_fraction + if thickness_unit == "saxis": + thickness = thickness_value + transform = (ax.get_xaxis_transform() + if rotation == "horizontal" else + ax.get_yaxis_transform()) + elif thickness_unit == "laxis": + thickness = thickness_value + transform = (ax.get_yaxis_transform() + if rotation == "horizontal" else + ax.get_xaxis_transform()) + elif thickness_unit == "font": + thickness = (font_properties.get_size() / 72 * thickness_value) + transform = ( + blended_transform_factory(ax.transData, ax.figure.dpi_scale_trans) + if rotation == "horizontal" else + blended_transform_factory(ax.figure.dpi_scale_trans, ax.transData)) # Create scale bar if rotation == "horizontal": scale_rect = Rectangle( (0, 0), length_px, - width_px, + thickness, fill=True, facecolor=color, edgecolor="none", @@ -501,14 +540,14 @@ def _get_value(attr, default): else: scale_rect = Rectangle( (0, 0), - width_px, + thickness, length_px, fill=True, facecolor=color, edgecolor="none", ) - scale_bar_box = AuxTransformBox(ax.transData) + scale_bar_box = AuxTransformBox(transform) scale_bar_box.add_artist(scale_rect) # Create scale text @@ -627,22 +666,37 @@ def set_length_fraction(self, fraction): length_fraction = property(get_length_fraction, set_length_fraction) + def get_thickness(self): + return self._thickness + + def set_thickness(self, thickness): + if thickness is not None: + _validate_dim(thickness) + self._thickness = thickness + + thickness = property(get_thickness, set_thickness) + def get_width_fraction(self): - return self._width_fraction + if self._thickness is None: + return None + elif self._thickness[1] == "saxis": + return self._thickness[0] + else: + raise ValueError(f"thickness ({self._thickness}) is not a width fraction") def set_width_fraction(self, fraction): if fraction is not None: fraction = float(fraction) if fraction <= 0.0 or fraction > 1.0: raise ValueError("Width fraction must be between [0.0, 1.0]") - self._width_fraction = fraction + self._thickness = (fraction, "saxis") width_fraction = property(get_width_fraction, set_width_fraction) def get_height_fraction(self): warnings.warn( "The get_height_fraction method is deprecated. " - "Use get_width_fraction instead.", + "Use get_thickness instead.", DeprecationWarning, ) return self.width_fraction @@ -650,7 +704,7 @@ def get_height_fraction(self): def set_height_fraction(self, fraction): warnings.warn( "The set_height_fraction method is deprecated. " - "Use set_width_fraction instead.", + "Use set_thickness instead.", DeprecationWarning, ) self.width_fraction = fraction diff --git a/tests/test_scalebar.py b/tests/test_scalebar.py index 1093296..7c6e776 100644 --- a/tests/test_scalebar.py +++ b/tests/test_scalebar.py @@ -47,7 +47,7 @@ def test_mpl_rcParams_update(): params = { "scalebar.length_fraction": 0.2, - "scalebar.width_fraction": 0.01, + "scalebar.thickness": (0.01, "saxis"), "scalebar.location": "upper right", "scalebar.pad": 0.2, "scalebar.border_pad": 0.1, @@ -125,6 +125,45 @@ def test_scalebar_height_fraction(scalebar): scalebar.set_height_fraction(1.1) +def test_scalebar_thickness_width_fraction(scalebar): + assert scalebar.get_thickness() is None + assert scalebar.thickness is None + assert scalebar.get_width_fraction() is None + assert scalebar.width_fraction is None + + scalebar.set_width_fraction(0.2) + assert scalebar.get_width_fraction() == pytest.approx(0.2, abs=1e-2) + assert scalebar.width_fraction == pytest.approx(0.2, abs=1e-2) + assert scalebar.get_thickness() == (0.2, "saxis") + assert scalebar.thickness == (0.2, "saxis") + + scalebar.thickness = (0.4, "saxis") + assert scalebar.get_width_fraction() == pytest.approx(0.4, abs=1e-2) + assert scalebar.width_fraction == pytest.approx(0.4, abs=1e-2) + assert scalebar.get_thickness() == (0.4, "saxis") + assert scalebar.thickness == (0.4, "saxis") + + scalebar.width_fraction = 0.1 + assert scalebar.get_width_fraction() == pytest.approx(0.1, abs=1e-2) + assert scalebar.width_fraction == pytest.approx(0.1, abs=1e-2) + assert scalebar.get_thickness() == (0.1, "saxis") + assert scalebar.thickness == (0.1, "saxis") + + scalebar.set_thickness((0.3, "font")) + with pytest.raises(ValueError): + scalebar.get_width_fraction() + with pytest.raises(ValueError): + scalebar.width_fraction + assert scalebar.get_thickness() == (0.3, "font") + assert scalebar.thickness == (0.3, "font") + + with pytest.raises(ValueError): + scalebar.set_width_fraction(0.0) + + with pytest.raises(ValueError): + scalebar.set_width_fraction(1.1) + + def test_scalebar_location(scalebar): assert scalebar.get_location() is None assert scalebar.location is None