Skip to content

Commit 9290496

Browse files
committed
Better control of ScaleBar thickness.
Open questions: - Should width_fraction be deprecated? - Should length_fraction get the same treatment? If so, should fixed_value/fixed_units be rolled into `length`, e.g. as `length=(1, "um")`?
1 parent aff5f2c commit 9290496

File tree

3 files changed

+119
-18
lines changed

3 files changed

+119
-18
lines changed

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ scalebar = ScaleBar(
148148
dimension="si-length",
149149
label=None,
150150
length_fraction=None,
151+
thickness=None,
151152
height_fraction=None,
152153
width_fraction=None,
153154
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
253254

254255
![length fraction](doc/argument_length_fraction.png)
255256

257+
### thickness
258+
259+
Width and unit of the scale bar (valid units: "saxis", i.e. relative to the short axis size;
260+
"laxis", i.e. relative to the long axis size; "font", i.e. relative to the font size).
261+
Default: `None`, value from matplotlibrc or `(0.01, "saxis")`.
262+
256263
### height_fraction
257264

258-
**Deprecated**, use *width_fraction*.
265+
**Deprecated**, use *thickness* or *width_fraction*.
259266

260267
### width_fraction
261268

262269
Width of the scale bar as a fraction of the subplot's height.
270+
*thickness* is a more general way to set this parameter.
263271
Default: `None`, value from matplotlibrc or `0.01`.
264272

265273
### location
@@ -567,4 +575,4 @@ Copyright (c) 2015-2025 Philippe Pinard
567575
[i56]: https://github.com/ppinard/matplotlib-scalebar/pull/56
568576
[i58]: https://github.com/ppinard/matplotlib-scalebar/issues/58
569577
[i61]: https://github.com/ppinard/matplotlib-scalebar/pull/61
570-
[i62]: https://github.com/ppinard/matplotlib-scalebar/pull/62
578+
[i62]: https://github.com/ppinard/matplotlib-scalebar/pull/62

matplotlib_scalebar/scalebar.py

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@
3939

4040
# Standard library modules.
4141
import bisect
42-
import warnings
4342
import dataclasses
43+
import numbers
44+
import warnings
4445

4546
# Third party modules.
4647
import matplotlib
@@ -61,6 +62,7 @@
6162
AnchoredOffsetbox,
6263
)
6364
from matplotlib.patches import Rectangle
65+
from matplotlib.transforms import IdentityTransform, blended_transform_factory
6466

6567
# Local modules.
6668
from matplotlib_scalebar.dimension import (
@@ -96,10 +98,19 @@ def _validate_legend_loc(loc):
9698
return loc
9799

98100

101+
def _validate_dim(dim):
102+
if (len(dim) == 2
103+
and isinstance(dim[0], numbers.Real)
104+
and dim[1] in ["saxis", "laxis", "font"]):
105+
return dim
106+
else:
107+
raise ValueError("Not a valid dimension")
108+
109+
99110
defaultParams.update(
100111
{
101112
"scalebar.length_fraction": [0.2, validate_float],
102-
"scalebar.width_fraction": [0.01, validate_float],
113+
"scalebar.thickness": [(0.01, "saxis"), _validate_dim],
103114
"scalebar.location": ["upper right", _validate_legend_loc],
104115
"scalebar.pad": [0.2, validate_float],
105116
"scalebar.border_pad": [0.1, validate_float],
@@ -177,6 +188,7 @@ def __init__(
177188
dimension="si-length",
178189
label=None,
179190
length_fraction=None,
191+
thickness=None,
180192
height_fraction=None,
181193
width_fraction=None,
182194
location=None,
@@ -242,9 +254,15 @@ def __init__(
242254
This argument is ignored if a *fixed_value* is specified.
243255
:type length_fraction: :class:`float`
244256
245-
:arg width_fraction: width of the scale bar as a fraction of the
246-
axes's height (default: rcParams['scalebar.width_fraction'] or ``0.01``)
247-
:type width_fraction: :class:`float`
257+
:arg thickness: thickness of the scale bar, as a ``(value, unit)`` pair.
258+
Valid units are
259+
* "laxis": value is relative to the size of the parent axes in the
260+
"long" direction.
261+
* "saxis": value is relative to the size of the parent axes in the
262+
"short" direction.
263+
* "font": value is relative to the label fontsize.
264+
(default: rcParams['scalebar.thickness'] or ``(0.01, "saxis")``)
265+
:type thickness: ``tuple[float, str]``
248266
249267
:arg location: a location code (same as legend)
250268
(default: rcParams['scalebar.location'] or ``upper right``)
@@ -347,6 +365,12 @@ def __init__(
347365
)
348366
scale_formatter = scale_formatter or label_formatter
349367

368+
if width_fraction is not None:
369+
if thickness is not None:
370+
warnings.warn("Ignoring 'width_fraction', as 'thickness' is also set")
371+
else:
372+
thickness = (width_fraction, "saxis")
373+
350374
if (
351375
loc is not None
352376
and location is not None
@@ -359,7 +383,7 @@ def __init__(
359383
self.units = units
360384
self.label = label
361385
self.length_fraction = length_fraction
362-
self.width_fraction = width_fraction
386+
self.thickness = thickness
363387
self.location = location or loc
364388
self.pad = pad
365389
self.border_pad = border_pad
@@ -433,7 +457,7 @@ def _get_value(attr, default):
433457
return value
434458

435459
length_fraction = _get_value("length_fraction", 0.2)
436-
width_fraction = _get_value("width_fraction", 0.01)
460+
thickness_value, thickness_unit = _get_value("thickness", (0.01, "saxis"))
437461
location = _get_value("location", "upper right")
438462
if isinstance(location, str):
439463
location = self._LOCATIONS[location.lower()]
@@ -486,29 +510,44 @@ def _get_value(attr, default):
486510

487511
scale_text = self.scale_formatter(value, self.dimension.to_latex(units))
488512

489-
width_px = abs(ylim[1] - ylim[0]) * width_fraction
513+
if thickness_unit == "saxis":
514+
thickness = thickness_value
515+
transform = (ax.get_xaxis_transform()
516+
if rotation == "horizontal" else
517+
ax.get_yaxis_transform())
518+
elif thickness_unit == "laxis":
519+
thickness = thickness_value
520+
transform = (ax.get_yaxis_transform()
521+
if rotation == "horizontal" else
522+
ax.get_xaxis_transform())
523+
elif thickness_unit == "font":
524+
thickness = (font_properties.get_size() / 72 * thickness_value)
525+
transform = (
526+
blended_transform_factory(ax.transData, ax.figure.dpi_scale_trans)
527+
if rotation == "horizontal" else
528+
blended_transform_factory(ax.figure.dpi_scale_trans, ax.transData))
490529

491530
# Create scale bar
492531
if rotation == "horizontal":
493532
scale_rect = Rectangle(
494533
(0, 0),
495534
length_px,
496-
width_px,
535+
thickness,
497536
fill=True,
498537
facecolor=color,
499538
edgecolor="none",
500539
)
501540
else:
502541
scale_rect = Rectangle(
503542
(0, 0),
504-
width_px,
543+
thickness,
505544
length_px,
506545
fill=True,
507546
facecolor=color,
508547
edgecolor="none",
509548
)
510549

511-
scale_bar_box = AuxTransformBox(ax.transData)
550+
scale_bar_box = AuxTransformBox(transform)
512551
scale_bar_box.add_artist(scale_rect)
513552

514553
# Create scale text
@@ -627,30 +666,45 @@ def set_length_fraction(self, fraction):
627666

628667
length_fraction = property(get_length_fraction, set_length_fraction)
629668

669+
def get_thickness(self):
670+
return self._thickness
671+
672+
def set_thickness(self, thickness):
673+
if thickness is not None:
674+
_validate_dim(thickness)
675+
self._thickness = thickness
676+
677+
thickness = property(get_thickness, set_thickness)
678+
630679
def get_width_fraction(self):
631-
return self._width_fraction
680+
if self._thickness is None:
681+
return None
682+
elif self._thickness[1] == "saxis":
683+
return self._thickness[0]
684+
else:
685+
raise ValueError(f"thickness ({self._thickness}) is not a width fraction")
632686

633687
def set_width_fraction(self, fraction):
634688
if fraction is not None:
635689
fraction = float(fraction)
636690
if fraction <= 0.0 or fraction > 1.0:
637691
raise ValueError("Width fraction must be between [0.0, 1.0]")
638-
self._width_fraction = fraction
692+
self._thickness = (fraction, "saxis")
639693

640694
width_fraction = property(get_width_fraction, set_width_fraction)
641695

642696
def get_height_fraction(self):
643697
warnings.warn(
644698
"The get_height_fraction method is deprecated. "
645-
"Use get_width_fraction instead.",
699+
"Use get_thickness instead.",
646700
DeprecationWarning,
647701
)
648702
return self.width_fraction
649703

650704
def set_height_fraction(self, fraction):
651705
warnings.warn(
652706
"The set_height_fraction method is deprecated. "
653-
"Use set_width_fraction instead.",
707+
"Use set_thickness instead.",
654708
DeprecationWarning,
655709
)
656710
self.width_fraction = fraction

tests/test_scalebar.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def test_mpl_rcParams_update():
4747

4848
params = {
4949
"scalebar.length_fraction": 0.2,
50-
"scalebar.width_fraction": 0.01,
50+
"scalebar.thickness": (0.01, "saxis"),
5151
"scalebar.location": "upper right",
5252
"scalebar.pad": 0.2,
5353
"scalebar.border_pad": 0.1,
@@ -125,6 +125,45 @@ def test_scalebar_height_fraction(scalebar):
125125
scalebar.set_height_fraction(1.1)
126126

127127

128+
def test_scalebar_thickness_width_fraction(scalebar):
129+
assert scalebar.get_thickness() is None
130+
assert scalebar.thickness is None
131+
assert scalebar.get_width_fraction() is None
132+
assert scalebar.width_fraction is None
133+
134+
scalebar.set_width_fraction(0.2)
135+
assert scalebar.get_width_fraction() == pytest.approx(0.2, abs=1e-2)
136+
assert scalebar.width_fraction == pytest.approx(0.2, abs=1e-2)
137+
assert scalebar.get_thickness() == (0.2, "saxis")
138+
assert scalebar.thickness == (0.2, "saxis")
139+
140+
scalebar.thickness = (0.4, "saxis")
141+
assert scalebar.get_width_fraction() == pytest.approx(0.4, abs=1e-2)
142+
assert scalebar.width_fraction == pytest.approx(0.4, abs=1e-2)
143+
assert scalebar.get_thickness() == (0.4, "saxis")
144+
assert scalebar.thickness == (0.4, "saxis")
145+
146+
scalebar.width_fraction = 0.1
147+
assert scalebar.get_width_fraction() == pytest.approx(0.1, abs=1e-2)
148+
assert scalebar.width_fraction == pytest.approx(0.1, abs=1e-2)
149+
assert scalebar.get_thickness() == (0.1, "saxis")
150+
assert scalebar.thickness == (0.1, "saxis")
151+
152+
scalebar.set_thickness((0.3, "font"))
153+
with pytest.raises(ValueError):
154+
scalebar.get_width_fraction()
155+
with pytest.raises(ValueError):
156+
scalebar.width_fraction
157+
assert scalebar.get_thickness() == (0.3, "font")
158+
assert scalebar.thickness == (0.3, "font")
159+
160+
with pytest.raises(ValueError):
161+
scalebar.set_width_fraction(0.0)
162+
163+
with pytest.raises(ValueError):
164+
scalebar.set_width_fraction(1.1)
165+
166+
128167
def test_scalebar_location(scalebar):
129168
assert scalebar.get_location() is None
130169
assert scalebar.location is None

0 commit comments

Comments
 (0)