From c59e58f01eb22ff92e74f985f06051ff6187c246 Mon Sep 17 00:00:00 2001 From: Lucas Beyer Date: Mon, 1 Dec 2025 23:21:49 +0100 Subject: [PATCH 1/3] 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 22f488a..7a8594a 100644 --- a/mplexporter/utils.py +++ b/mplexporter/utils.py @@ -233,6 +233,10 @@ def get_axis_properties(axis): props['nticks'] = len(tick_locs) props['tickvalues'] = tick_locs if isinstance(locator, ticker.FixedLocator) else 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() @@ -279,6 +283,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() @@ -286,21 +291,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 99b6ebe3778ea969e0e5692961d73a5421f88a18 Mon Sep 17 00:00:00 2001 From: Lucas Beyer Date: Mon, 1 Dec 2025 23:21:49 +0100 Subject: [PATCH 2/3] 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 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mplexporter/utils.py b/mplexporter/utils.py index 7a8594a..d9dc891 100644 --- a/mplexporter/utils.py +++ b/mplexporter/utils.py @@ -237,6 +237,10 @@ def get_axis_properties(axis): props['minor_tickvalues'] = list(axis.get_minorticklocs()) if minor_locator else None props['minorticklength'] = axis._minor_tick_kw.get('size', 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() From 9f77a297af814d5f24ab956345b23ec93cec4cd9 Mon Sep 17 00:00:00 2001 From: Lucas Beyer Date: Tue, 2 Dec 2025 19:13:34 +0100 Subject: [PATCH 3/3] Bring minor and major ticks/tickabels to parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A few things were missing: - minor ticklabels altogether - major tick length setting - default tick length was arbitrary - make it same as matplotlib. This change brings both to parity, and makes sure they remain by using the same code for handling of most tick-related things, whether major or minor (except tickNr which only makes sense for major). After this, ticks and ticklabels have parity with standard matplotlib, I tested multiple scenarios (automatic vs manual setting and labeling, linear vs log axis, and similar) and compared against stock matplotlib. As usual, disclaiming I co-developed this with gpt-5.1-codex. We went through a lot of iterations here, both machine and me coding to get to this solution. Here's what it has to say: > Export and render minor ticks/grids with the same formatter/length handling as majors, including custom formatter overrides > Consolidate tick value/formatter application via a shared helper for both major and minor axes > Apply Matplotlib tick lengths to both major and minor ticks; keep minor labels hidden when only positions are set by default > Add tests for minor grid/tick export, minor label defaults, and major tick length” --- mplexporter/utils.py | 66 +++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/mplexporter/utils.py b/mplexporter/utils.py index d9dc891..f42ad1b 100644 --- a/mplexporter/utils.py +++ b/mplexporter/utils.py @@ -209,6 +209,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, i) for i, value in enumerate(tickvalues)], "fixed" + 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 = {} @@ -236,44 +259,17 @@ 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) - - 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): + major_formatter = axis.get_major_formatter() + if isinstance(major_formatter, ticker.FuncFormatter) and props['tickvalues'] is None: # It's impossible for JS to re-run our function, so run it now and save as Fixed. - if props['tickvalues'] is None: - props['tickvalues'] = tick_locs - props['tickformat'] = [formatter(value, i) for i, value in enumerate(props['tickvalues'])] - props['tickformat_formatter'] = "fixed" - elif not any(label.get_visible() for label in axis.get_ticklabels()): - props['tickformat'] = "" - else: - props['tickformat'] = None + props['tickvalues'] = tick_locs + 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( + major_formatter, props['tickvalues'], axis.get_ticklabels()) # Get axis scale props['scale'] = axis.get_scale()