Skip to content

Commit a319330

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 a319330

3 files changed

Lines changed: 135 additions & 19 deletions

File tree

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: 85 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
1313
The following parameters are available for customization in the matplotlibrc:
1414
- scalebar.length_fraction
15-
- scalebar.height_fraction
15+
- scalebar.thickness
1616
- scalebar.location
1717
- scalebar.pad
1818
- scalebar.border_pad
@@ -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,11 @@
6162
AnchoredOffsetbox,
6263
)
6364
from matplotlib.patches import Rectangle
65+
from matplotlib.transforms import (
66+
Affine2D,
67+
IdentityTransform,
68+
blended_transform_factory,
69+
)
6470

6571
# Local modules.
6672
from matplotlib_scalebar.dimension import (
@@ -96,10 +102,19 @@ def _validate_legend_loc(loc):
96102
return loc
97103

98104

105+
def _validate_dim(dim):
106+
if (len(dim) == 2
107+
and isinstance(dim[0], numbers.Real)
108+
and dim[1] in ["saxis", "laxis", "lw", "font", "pt"]):
109+
return dim
110+
else:
111+
raise ValueError("Not a valid dimension")
112+
113+
99114
defaultParams.update(
100115
{
101116
"scalebar.length_fraction": [0.2, validate_float],
102-
"scalebar.width_fraction": [0.01, validate_float],
117+
"scalebar.thickness": [(0.01, "saxis"), _validate_dim],
103118
"scalebar.location": ["upper right", _validate_legend_loc],
104119
"scalebar.pad": [0.2, validate_float],
105120
"scalebar.border_pad": [0.1, validate_float],
@@ -177,6 +192,7 @@ def __init__(
177192
dimension="si-length",
178193
label=None,
179194
length_fraction=None,
195+
thickness=None,
180196
height_fraction=None,
181197
width_fraction=None,
182198
location=None,
@@ -242,9 +258,17 @@ def __init__(
242258
This argument is ignored if a *fixed_value* is specified.
243259
:type length_fraction: :class:`float`
244260
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`
261+
:arg thickness: thickness of the scale bar, as a ``(value, unit)`` pair.
262+
Valid units are
263+
* "laxis": value is relative to the size of the parent axes in the
264+
"long" direction.
265+
* "saxis": value is relative to the size of the parent axes in the
266+
"short" direction.
267+
* "font": value is relative to the label fontsize.
268+
* "lw": value is relative to ``rcParams["lines.linewidth"]``.
269+
* "pt": value is in points.
270+
(default: rcParams['scalebar.thickness'] or ``(0.01, "saxis")``)
271+
:type thickness: ``tuple[float, str]``
248272
249273
:arg location: a location code (same as legend)
250274
(default: rcParams['scalebar.location'] or ``upper right``)
@@ -347,6 +371,12 @@ def __init__(
347371
)
348372
scale_formatter = scale_formatter or label_formatter
349373

374+
if width_fraction is not None:
375+
if thickness is not None:
376+
warnings.warn("Ignoring 'width_fraction', as 'thickness' is also set")
377+
else:
378+
thickness = (width_fraction, "saxis")
379+
350380
if (
351381
loc is not None
352382
and location is not None
@@ -359,7 +389,7 @@ def __init__(
359389
self.units = units
360390
self.label = label
361391
self.length_fraction = length_fraction
362-
self.width_fraction = width_fraction
392+
self.thickness = thickness
363393
self.location = location or loc
364394
self.pad = pad
365395
self.border_pad = border_pad
@@ -433,7 +463,7 @@ def _get_value(attr, default):
433463
return value
434464

435465
length_fraction = _get_value("length_fraction", 0.2)
436-
width_fraction = _get_value("width_fraction", 0.01)
466+
thickness_value, thickness_unit = _get_value("thickness", (0.01, "saxis"))
437467
location = _get_value("location", "upper right")
438468
if isinstance(location, str):
439469
location = self._LOCATIONS[location.lower()]
@@ -486,29 +516,53 @@ def _get_value(attr, default):
486516

487517
scale_text = self.scale_formatter(value, self.dimension.to_latex(units))
488518

489-
width_px = abs(ylim[1] - ylim[0]) * width_fraction
519+
axis_order = slice(None) if rotation == "horizontal" else slice(None, None, -1)
520+
if thickness_unit == "saxis":
521+
thickness = thickness_value
522+
transform = (ax.get_xaxis_transform()
523+
if rotation == "horizontal" else
524+
ax.get_yaxis_transform())
525+
elif thickness_unit == "laxis":
526+
thickness = thickness_value
527+
flip = Affine2D([[0, 1, 0], [1, 0, 0], [0, 0, 1]])
528+
transform = (
529+
flip
530+
+ blended_transform_factory(
531+
*(ax.transAxes, IdentityTransform())[axis_order])
532+
+ flip
533+
+ blended_transform_factory(
534+
*(ax.transData, IdentityTransform())[axis_order])
535+
)
536+
elif thickness_unit in {"font", "lw", "pt"}:
537+
thickness = thickness_value * {
538+
"font": font_properties.get_size(),
539+
"lw": matplotlib.rcParams["lines.linewidth"],
540+
"pt": 1,
541+
}[thickness_unit] / 72
542+
transform = blended_transform_factory(
543+
*(ax.transData, ax.figure.dpi_scale_trans)[axis_order])
490544

491545
# Create scale bar
492546
if rotation == "horizontal":
493547
scale_rect = Rectangle(
494548
(0, 0),
495549
length_px,
496-
width_px,
550+
thickness,
497551
fill=True,
498552
facecolor=color,
499553
edgecolor="none",
500554
)
501555
else:
502556
scale_rect = Rectangle(
503557
(0, 0),
504-
width_px,
558+
thickness,
505559
length_px,
506560
fill=True,
507561
facecolor=color,
508562
edgecolor="none",
509563
)
510564

511-
scale_bar_box = AuxTransformBox(ax.transData)
565+
scale_bar_box = AuxTransformBox(transform)
512566
scale_bar_box.add_artist(scale_rect)
513567

514568
# Create scale text
@@ -627,30 +681,45 @@ def set_length_fraction(self, fraction):
627681

628682
length_fraction = property(get_length_fraction, set_length_fraction)
629683

684+
def get_thickness(self):
685+
return self._thickness
686+
687+
def set_thickness(self, thickness):
688+
if thickness is not None:
689+
_validate_dim(thickness)
690+
self._thickness = thickness
691+
692+
thickness = property(get_thickness, set_thickness)
693+
630694
def get_width_fraction(self):
631-
return self._width_fraction
695+
if self._thickness is None:
696+
return None
697+
elif self._thickness[1] == "saxis":
698+
return self._thickness[0]
699+
else:
700+
raise ValueError(f"thickness ({self._thickness}) is not a width fraction")
632701

633702
def set_width_fraction(self, fraction):
634703
if fraction is not None:
635704
fraction = float(fraction)
636705
if fraction <= 0.0 or fraction > 1.0:
637706
raise ValueError("Width fraction must be between [0.0, 1.0]")
638-
self._width_fraction = fraction
707+
self._thickness = (fraction, "saxis")
639708

640709
width_fraction = property(get_width_fraction, set_width_fraction)
641710

642711
def get_height_fraction(self):
643712
warnings.warn(
644713
"The get_height_fraction method is deprecated. "
645-
"Use get_width_fraction instead.",
714+
"Use get_thickness instead.",
646715
DeprecationWarning,
647716
)
648717
return self.width_fraction
649718

650719
def set_height_fraction(self, fraction):
651720
warnings.warn(
652721
"The set_height_fraction method is deprecated. "
653-
"Use set_width_fraction instead.",
722+
"Use set_thickness instead.",
654723
DeprecationWarning,
655724
)
656725
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)