From 830b3e3faee915145f5e38d950f579c49e89aa1a Mon Sep 17 00:00:00 2001 From: Lucas Beyer Date: Mon, 1 Dec 2025 11:18:28 +0100 Subject: [PATCH 1/4] line-width was missing from gridlines. --- mplexporter/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mplexporter/utils.py b/mplexporter/utils.py index f9467d7..3702e08 100644 --- a/mplexporter/utils.py +++ b/mplexporter/utils.py @@ -268,9 +268,11 @@ def get_grid_style(axis): color = export_color(gridlines[0].get_color()) alpha = gridlines[0].get_alpha() dasharray = get_dasharray(gridlines[0]) + linewidth = gridlines[0].get_linewidth() return dict(gridOn=True, color=color, dasharray=dasharray, + linewidth=linewidth, alpha=alpha) else: return {"gridOn": False} From 5822c55ba110755de48eb701ab21f45733768532 Mon Sep 17 00:00:00 2001 From: Lucas Beyer Date: Mon, 1 Dec 2025 23:21:49 +0100 Subject: [PATCH 2/4] Also export minor grid. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This will allow to render both major and minor gridlines in mpld3, which fixes the following issue: https://github.com/mpld3/mpld3/issues/527 As per usual, disclaimer that I co-developed this with gpt-5.1-codex, having it figure out the issues and give implementation recommendations, with me testing, verifying, and tidying up the code. Here's what it has to say, especially wrt the change in API call: - include minor tick values/length and minor grid style in axis props so minor ticks/grids render in mpld3 - read grid color/linewidth/linestyle from tick kwargs (and rcParams fallback) instead of inspecting gridlines[0], avoiding the get_gridlines(which=…) API that isn’t available on matplotlib 3.10” Rationale for the kw/rc approach: `Axis.get_gridlines()` doesn’t accept `which` on matplotlib 3.10, so probing `gridlines[0]` for minor/major fails. Pulling style from the tick keyword dict (which matplotlib populates with `grid_*` fields when you call `ax.grid(...)`) plus `rcParams` defaults gives the same style without needing `get_gridlines(which=…)`, keeping compatibility and matching user-set grid styles. (I verified, indeed get_gridlines does not allow specifying which ones - seems like an omission in matplotlib API to me) --- mplexporter/utils.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/mplexporter/utils.py b/mplexporter/utils.py index 3702e08..c09e267 100644 --- a/mplexporter/utils.py +++ b/mplexporter/utils.py @@ -212,6 +212,10 @@ def get_axis_properties(axis): else: props['tickvalues'] = None + minor_locator = axis.get_minor_locator() + props['minor_tickvalues'] = list(axis.get_minorticklocs()) if minor_locator else None + props['minorticklength'] = axis._minor_tick_kw.get('size', None) + # Find tick formats props['tickformat_formatter'] = "" formatter = axis.get_major_formatter() @@ -255,6 +259,7 @@ def get_axis_properties(axis): # Get associated grid props['grid'] = get_grid_style(axis) + props['minor_grid'] = get_grid_style(axis, which='minor') # get axis visibility props['visible'] = axis.get_visible() @@ -262,21 +267,24 @@ def get_axis_properties(axis): return props -def get_grid_style(axis): - gridlines = axis.get_gridlines() - if axis._major_tick_kw['gridOn'] and len(gridlines) > 0: - color = export_color(gridlines[0].get_color()) - alpha = gridlines[0].get_alpha() - dasharray = get_dasharray(gridlines[0]) - linewidth = gridlines[0].get_linewidth() - return dict(gridOn=True, - color=color, - dasharray=dasharray, - linewidth=linewidth, - alpha=alpha) - else: +def get_grid_style(axis, which='major'): + tick_kw = axis._minor_tick_kw if which == 'minor' else axis._major_tick_kw + + if not tick_kw.get('gridOn'): return {"gridOn": False} + rc = matplotlib.rcParams + color = export_color(tick_kw.get('grid_color', tick_kw.get('grid_c', rc['grid.color']))) + alpha = tick_kw.get('grid_alpha', rc['grid.alpha']) + dasharray = _dasharray_from_linestyle(tick_kw.get('grid_linestyle', tick_kw.get('grid_ls', rc['grid.linestyle']))) + linewidth = tick_kw.get('grid_linewidth', tick_kw.get('grid_lw', rc['grid.linewidth'])) + + return dict(gridOn=True, + color=color, + dasharray=dasharray, + linewidth=linewidth, + alpha=alpha) + def get_figure_properties(fig): return {'figwidth': fig.get_figwidth(), From 2c2104aab6e4d672129ce9856d08d4d405d0379d Mon Sep 17 00:00:00 2001 From: Lucas Beyer Date: Tue, 2 Dec 2025 19:13:34 +0100 Subject: [PATCH 3/4] Bring minor and major ticks/tickabels to parity Minor ticklabels were missing altogether, --- mplexporter/utils.py | 57 ++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/mplexporter/utils.py b/mplexporter/utils.py index c09e267..1fc3440 100644 --- a/mplexporter/utils.py +++ b/mplexporter/utils.py @@ -186,6 +186,29 @@ def get_text_style(text): return style +def _tick_format_props(formatter, tickvalues, labels): + if isinstance(formatter, ticker.NullFormatter): + return "", "" + if isinstance(formatter, ticker.StrMethodFormatter): + convertor = StrMethodTickFormatterConvertor(formatter) + return convertor.output, "str_method" + if isinstance(formatter, ticker.PercentFormatter): + return { + "xmax": formatter.xmax, + "decimals": formatter.decimals, + "symbol": formatter.symbol, + }, "percent" + if hasattr(ticker, 'IndexFormatter') and isinstance(formatter, ticker.IndexFormatter): + return [text.get_text() for text in labels], "index" + if isinstance(formatter, ticker.FixedFormatter): + return list(formatter.seq), "fixed" + if isinstance(formatter, ticker.FuncFormatter) and tickvalues: + return [formatter(value) for value in tickvalues], "func" + if not any(label.get_visible() for label in labels): + return "", "" + return None, "" + + def get_axis_properties(axis): """Return the property dictionary for a matplotlib.Axis instance""" props = {} @@ -215,37 +238,13 @@ def get_axis_properties(axis): minor_locator = axis.get_minor_locator() props['minor_tickvalues'] = list(axis.get_minorticklocs()) if minor_locator else None props['minorticklength'] = axis._minor_tick_kw.get('size', None) + props['majorticklength'] = axis._major_tick_kw.get('size', None) # Find tick formats - props['tickformat_formatter'] = "" - formatter = axis.get_major_formatter() - if isinstance(formatter, ticker.NullFormatter): - props['tickformat'] = "" - elif isinstance(formatter, ticker.StrMethodFormatter): - convertor = StrMethodTickFormatterConvertor(formatter) - props['tickformat'] = convertor.output - props['tickformat_formatter'] = "str_method" - elif isinstance(formatter, ticker.PercentFormatter): - props['tickformat'] = { - "xmax": formatter.xmax, - "decimals": formatter.decimals, - "symbol": formatter.symbol, - } - props['tickformat_formatter'] = "percent" - elif hasattr(ticker, 'IndexFormatter') and isinstance(formatter, ticker.IndexFormatter): - # IndexFormatter was dropped in matplotlib 3.5 - props['tickformat'] = [text.get_text() for text in axis.get_ticklabels()] - props['tickformat_formatter'] = "index" - elif isinstance(formatter, ticker.FixedFormatter): - props['tickformat'] = list(formatter.seq) - props['tickformat_formatter'] = "fixed" - elif isinstance(formatter, ticker.FuncFormatter) and props['tickvalues']: - props['tickformat'] = [formatter(value) for value in props['tickvalues']] - props['tickformat_formatter'] = "func" - elif not any(label.get_visible() for label in axis.get_ticklabels()): - props['tickformat'] = "" - else: - props['tickformat'] = None + props['minor_tickformat'], props['minor_tickformat_formatter'] = _tick_format_props( + axis.get_minor_formatter(), props['minor_tickvalues'], axis.get_minorticklabels()) + props['tickformat'], props['tickformat_formatter'] = _tick_format_props( + axis.get_major_formatter(), props['tickvalues'], axis.get_ticklabels()) # Get axis scale props['scale'] = axis.get_scale() From 81af9ee36830973c9ecc7c8da53bcc3eb0f69fa3 Mon Sep 17 00:00:00 2001 From: Lucas Beyer Date: Tue, 16 Dec 2025 09:28:51 +0100 Subject: [PATCH 4/4] Make FuncFormatter work with non-fixed locator The previous fix to make FuncFormatter work, in https://github.com/mpld3/mplexporter/pull/67/files and https://github.com/mpld3/mpld3/commit/e7fa282e72328e25ee2b644c4123606a6e86b9a6 had two issues still: 1) the mpl FuncFormatter API also takes the index as second argument, which was missing. 2) it exported `tickvalues` only in the FixedLocator case, so these were missing for non-fixed FuncFormatter and hence the FuncFormatter codepath, which also requires them to be present, was never hit with non-fixed locators. Also add a test, and let tests import `matplotlib` via the backend-setting mechanism, not only `plt`. This fixes both. Again, this was figured out and helped with gpt-codex and my careful review+cleanup. Codex even identified the two original commits I'm linking above :) --- mplexporter/tests/__init__.py | 7 ++----- mplexporter/tests/test_utils.py | 24 ++++++++++++++++++++++-- mplexporter/utils.py | 4 ++-- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/mplexporter/tests/__init__.py b/mplexporter/tests/__init__.py index 7aa97e7..11a2bf8 100644 --- a/mplexporter/tests/__init__.py +++ b/mplexporter/tests/__init__.py @@ -1,9 +1,6 @@ import os -MPLBE = os.environ.get('MPLBE', 'Agg') - -if MPLBE: - import matplotlib +import matplotlib +if MPLBE := os.environ.get('MPLBE', 'Agg'): matplotlib.use(MPLBE) - import matplotlib.pyplot as plt diff --git a/mplexporter/tests/test_utils.py b/mplexporter/tests/test_utils.py index 51dad80..05da716 100644 --- a/mplexporter/tests/test_utils.py +++ b/mplexporter/tests/test_utils.py @@ -1,5 +1,5 @@ -from numpy.testing import assert_allclose, assert_equal -from . import plt +from numpy.testing import assert_, assert_allclose, assert_equal +from . import plt, matplotlib from .. import utils @@ -33,3 +33,23 @@ def test_axis_w_fixed_formatter(): # NOTE: Issue #471 # assert_equal(props['tickformat'], labels) + +def test_funcformatter_exports_major_ticklabels(): + # Test both log and normal cases: + for scale, x, y in [ + ("linear", [0, 1], [0, 1]), + ("log", [1, 10, 100], [0, 1, 2]), + ]: + fig, ax = plt.subplots() + ax.set_xscale(scale) + ax.plot([1, 10, 100], [0, 1, 2]) + ax.xaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(lambda x, pos: f"t{pos}")) + + props = utils.get_axis_properties(ax.xaxis) + plt.close(fig) + + assert_equal(props["scale"], scale) + assert_equal(props["tickformat_formatter"], "func") + assert_(props["tickvalues"] is not None) + assert_equal(len(props["tickvalues"]), len(props["tickformat"])) + assert_equal(props["tickformat"][:3], ["t0", "t1", "t2"]) diff --git a/mplexporter/utils.py b/mplexporter/utils.py index 1fc3440..d02b4b0 100644 --- a/mplexporter/utils.py +++ b/mplexporter/utils.py @@ -203,7 +203,7 @@ def _tick_format_props(formatter, tickvalues, labels): if isinstance(formatter, ticker.FixedFormatter): return list(formatter.seq), "fixed" if isinstance(formatter, ticker.FuncFormatter) and tickvalues: - return [formatter(value) for value in tickvalues], "func" + return [formatter(value, i) for i, value in enumerate(tickvalues)], "func" if not any(label.get_visible() for label in labels): return "", "" return None, "" @@ -230,7 +230,7 @@ def get_axis_properties(axis): # Use tick values if appropriate locator = axis.get_major_locator() props['nticks'] = len(locator()) - if isinstance(locator, ticker.FixedLocator): + if isinstance(locator, ticker.FixedLocator) or isinstance(axis.get_major_formatter(), ticker.FuncFormatter): props['tickvalues'] = list(locator()) else: props['tickvalues'] = None