Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/plopp/graphics/colormapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import scipp as sc
from matplotlib.colorbar import ColorbarBase
from matplotlib.colors import Colormap, LinearSegmentedColormap, LogNorm, Normalize
from matplotlib.ticker import FuncFormatter

from ..backends.matplotlib.utils import fig_to_bytes
from ..core.limits import find_limits, fix_empty_range
Expand Down Expand Up @@ -186,6 +187,7 @@ def __init__(
fig = plt.Figure(figsize=(height_inches * 0.2, height_inches))
self.cax = fig.add_axes([0.05, 0.02, 0.2, 0.98])
self.colorbar = ColorbarBase(self.cax, cmap=self.cmap, norm=self.normalizer)
self._update_colorbar_formatter()
self.cax.yaxis.set_label_coords(-0.9, 0.5)
if self._clabel is not None:
self.cax.set_ylabel(self._clabel)
Expand Down Expand Up @@ -276,6 +278,9 @@ def apply_limits(self):
self.normalizer.vmax = self._cmax["data"]

if self.colorbar is not None:
# Re-apply the formatter after updating normalizer limits,
# as matplotlib may have reset it when the normalizer changed
self._update_colorbar_formatter()
self._update_colorbar_widget()
self.notify_artists()

Expand Down Expand Up @@ -378,6 +383,33 @@ def ylabel(self) -> str | None:
def ylabel(self, lab: str):
self.clabel = lab

def _update_colorbar_formatter(self):
"""
Update the colorbar tick formatter based on the current normalization.
For log scale, use a plain text formatter to avoid mathtext parsing errors
during tight_layout() calculations.
"""
if self.colorbar is not None and self.cax is not None:
if self._logc:
# Use a custom formatter that produces plain text labels,
# avoiding mathtext notation that can cause parsing errors
# during tight_layout() when matplotlib measures text extent
def log_formatter(x, pos):
if x <= 0:
return ''
exponent = int(np.round(np.log10(x)))
mantissa = x / (10**exponent)
# For powers of 10, show as "10^N" without math mode
if abs(mantissa - 1.0) < 0.05:
return f'1e{exponent:+d}'
# For other values, use exponential notation
return f'{x:.1e}'

self.cax.yaxis.set_major_formatter(FuncFormatter(log_formatter))
else:
# Reset to default formatter for linear scale
self.cax.yaxis.set_major_formatter(mpl.ticker.ScalarFormatter())

def toggle_norm(self):
"""
Toggle the norm flag, between `linear` and `log`.
Expand All @@ -388,6 +420,7 @@ def toggle_norm(self):
self._cmax["data"] = -np.inf
if self.colorbar is not None:
self.colorbar.mappable.norm = self.normalizer
self._update_colorbar_formatter()
self.autoscale()
if self._canvas is not None:
self._canvas.draw()
Expand Down
40 changes: 40 additions & 0 deletions tests/graphics/colormapper_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,3 +398,43 @@ def test_toolbar_log_norm_button_toggles_colormapper_norm(
assert fig.view.colormapper.norm == 'linear'
fig.toolbar['lognorm'].value = True
assert fig.view.colormapper.norm == 'log'


def test_logc_tight_layout_does_not_raise_parse_error():
r"""
Test that calling tight_layout() on a figure with logc colorbar does not raise
a ValueError from matplotlib's mathtext parser. This reproduces the issue
where LogNorm tick labels like "$\mathdefault{10^{-33}}$" cause parsing errors.
"""
import warnings

# Create data - the specific range from the user's report
da = sc.DataArray(
data=sc.array(dims=['y', 'x'], values=np.arange(1, 801).reshape(20, 40)),
)
mapper = ColorMapper(cbar=True, logc=True)
artist = DummyChild(data=da, colormapper=mapper)
mapper.add_artist('data', artist)

mapper.autoscale()

# Call tight_layout() on the colorbar figure
# This is what causes the error in the user's report
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
try:
# Draw the figure first to ensure rendering is done
mapper.cax.get_figure().canvas.draw()
# Now call tight_layout which can trigger the mathtext parsing issue
mapper.cax.get_figure().tight_layout()
except ValueError as e:
# Check if this is the mathtext parsing error we're trying to fix
error_str = str(e)
if 'ParseException' in error_str and (
'$' in error_str or 'mathdefault' in error_str
):
pytest.fail(
f"tight_layout() raised mathtext parsing error with logc=True: {e}"
)
# Re-raise if it's a different ValueError
raise
Loading