From 555489a647f840f063f367e6d3733c687da3da96 Mon Sep 17 00:00:00 2001 From: Simon Heybrock Date: Mon, 3 Nov 2025 08:35:57 +0000 Subject: [PATCH 1/3] Add test for logc colorbar tight_layout mathtext parsing error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test that calling tight_layout() on a figure with logc=True does not raise a ValueError from matplotlib's mathtext parser. This test currently fails with the error from error_message.txt where LogNorm tick labels like "$\mathdefault{10^{-33}}$" cause parsing errors during layout calculations. Original task: A user is reporting that plotting a 2D data array with coords on logc (data range ~[0,800]) results in @error_message.txt - can you investigate? 🤖 Generated with Claude Code Co-Authored-By: Claude --- tests/graphics/colormapper_test.py | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/graphics/colormapper_test.py b/tests/graphics/colormapper_test.py index c6b9105c..1612d4be 100644 --- a/tests/graphics/colormapper_test.py +++ b/tests/graphics/colormapper_test.py @@ -398,3 +398,41 @@ 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 From fb40c4f33bd40a2ee8b17e36ff987d8f7e3bdcdd Mon Sep 17 00:00:00 2001 From: Simon Heybrock Date: Mon, 3 Nov 2025 08:37:20 +0000 Subject: [PATCH 2/3] Fix matplotlib mathtext parsing error in logc colorbar tick labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When plotting 2D data with logc=True and calling tight_layout(), matplotlib's default LogNorm formatter generates tick labels with mathtext notation like "$\mathdefault{10^{-33}}$". During tight_layout() text extent calculations, matplotlib re-parses these labels and encounters parsing errors due to nested dollar sign delimiters. Solution: Set a custom FuncFormatter for logc colorbars that generates plain text labels (e.g., "1e+02") instead of mathtext notation. This formatter is: - Applied when the colorbar is created - Re-applied after normalizer limits change (in apply_limits) - Re-applied when toggling between linear and log scales This prevents the mathtext parsing errors while maintaining readable tick labels. Fixes issue where .plot(logc=True).to_widget() raises ValueError on data ranges like [0, 800]. Resolves: test_logc_tight_layout_does_not_raise_parse_error Original task: A user is reporting that plotting a 2D data array with coords on logc (data range ~[0,800]) results in @error_message.txt - can you investigate? 🤖 Generated with Claude Code Co-Authored-By: Claude --- src/plopp/graphics/colormapper.py | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/plopp/graphics/colormapper.py b/src/plopp/graphics/colormapper.py index 00869acd..29d0fba7 100644 --- a/src/plopp/graphics/colormapper.py +++ b/src/plopp/graphics/colormapper.py @@ -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 @@ -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) @@ -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() @@ -378,6 +383,32 @@ 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`. @@ -388,6 +419,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() From ed7c0ad02129d6ff3317c7b908ec5742c0c73fb2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 08:41:02 +0000 Subject: [PATCH 3/3] Apply automatic formatting --- src/plopp/graphics/colormapper.py | 3 ++- tests/graphics/colormapper_test.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/plopp/graphics/colormapper.py b/src/plopp/graphics/colormapper.py index 29d0fba7..1c8d51e6 100644 --- a/src/plopp/graphics/colormapper.py +++ b/src/plopp/graphics/colormapper.py @@ -398,12 +398,13 @@ def log_formatter(x, pos): if x <= 0: return '' exponent = int(np.round(np.log10(x))) - mantissa = x / (10 ** exponent) + 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 diff --git a/tests/graphics/colormapper_test.py b/tests/graphics/colormapper_test.py index 1612d4be..980f3090 100644 --- a/tests/graphics/colormapper_test.py +++ b/tests/graphics/colormapper_test.py @@ -430,7 +430,9 @@ def test_logc_tight_layout_does_not_raise_parse_error(): 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): + 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}" )