Skip to content

Commit e993667

Browse files
committed
Add support for rendering minor gridlines/ticks
The js-side of mpld3/mplexporter#73 and fixes mpld3#527 Also adds some tests for minor-grid. It seems that the uglify is getting tripped up by `??`, so we should consider upgrading it, because a little chunk of this code could be quite a bit less verbose with that. Anyways, as usual disclaimer about having co-developed this with gpt-5.1-codex, but also tested and carefully reviewed/cleaned it. Here's what it has to say: - carry filtered minor tick values/length into axis props and draw a separate minor grid/tick layer - streamline axis getGrid tick selection and style passthrough to grids
1 parent 640a97c commit e993667

File tree

3 files changed

+86
-5
lines changed

3 files changed

+86
-5
lines changed

mpld3/tests/test_elements.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,26 @@ def test_ticks():
154154
rep = fig_to_html(plt.gcf())
155155
# TODO: use casperjs here if available to confirm that the xticks
156156
# are rendeder as expected
157+
158+
159+
def test_minor_grid_export():
160+
fig, ax = plt.subplots()
161+
ax.set_ylim(0, 3)
162+
ax.minorticks_on()
163+
ax.grid(which='minor', axis='y', c='blue', ls='--', lw=2)
164+
rep = fig_to_dict(fig)
165+
y_axis = rep['axes'][0]['axes'][1]
166+
assert y_axis['minor_grid']['gridOn'] is True
167+
assert y_axis['minor_grid']['color'] == '#0000FF'
168+
assert y_axis['minor_grid']['dasharray'] == '6,6'
169+
assert y_axis['minor_grid']['linewidth'] == 2
170+
assert len(y_axis['minor_tickvalues']) > 0
171+
172+
173+
def test_minor_tick_locations():
174+
fig, ax = plt.subplots()
175+
ax.minorticks_on()
176+
ax.yaxis.set_minor_locator(plt.FixedLocator([0.5, 1.5, 2.5]))
177+
rep = fig_to_dict(fig)
178+
y_axis = rep['axes'][0]['axes'][1]
179+
assert y_axis['minor_tickvalues'] == [0.5, 1.5, 2.5]

src/core/axes.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ function mpld3_Axes(fig, props) {
127127
if (this.props.gridOn || axis.props.grid.gridOn) {
128128
this.elements.push(axis.getGrid());
129129
}
130+
if (axis.props.minor_grid && axis.props.minor_grid.gridOn) {
131+
this.elements.push(axis.getGrid('minor'));
132+
}
130133
}
131134

132135
// Add paths

src/core/axis.js

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,19 @@ mpld3_Axis.prototype.requiredProps = ["position"];
4242
mpld3_Axis.prototype.defaultProps = {
4343
nticks: 10,
4444
tickvalues: null,
45+
minor_tickvalues: null,
4546
tickformat: null,
4647
filtered_tickvalues: null,
4748
filtered_tickformat: null,
49+
filtered_minortickvalues: null,
4850
tickformat_formatter: null,
4951
fontsize: "11px",
5052
fontcolor: "black",
5153
axiscolor: "black",
5254
scale: "linear",
5355
grid: {},
56+
minor_grid: {},
57+
minorticklength: null,
5458
zorder: 0,
5559
visible: true
5660
};
@@ -81,17 +85,26 @@ function mpld3_Axis(ax, props) {
8185
this.tickFormat = null;
8286
}
8387

84-
mpld3_Axis.prototype.getGrid = function() {
88+
mpld3_Axis.prototype.getGrid = function(which) {
89+
which = which || 'major';
8590
this.filter_ticks(this.scale.domain());
91+
var isMinor = which === 'minor';
8692
var gridprop = {
8793
nticks: this.props.nticks,
8894
zorder: this.props.zorder,
89-
tickvalues: this.props.filtered_tickvalues ?? this.props.tickvalues,
95+
tickvalues: null,
9096
xy: this.props.xy
9197
}
92-
if (this.props.grid) {
93-
for (var key in this.props.grid) {
94-
gridprop[key] = this.props.grid[key];
98+
var gridstyle = isMinor ? this.props.minor_grid : this.props.grid;
99+
// NOTE: This could be simplified with ?? but our current uglify doesn't support it?
100+
var ticks = isMinor ? this.props.filtered_minortickvalues : this.props.filtered_tickvalues;
101+
if (ticks === null || ticks === undefined) {
102+
ticks = isMinor ? this.props.minor_tickvalues : this.props.tickvalues;
103+
}
104+
gridprop.tickvalues = ticks;
105+
if (gridstyle) {
106+
for (var key in gridstyle) {
107+
gridprop[key] = gridprop[key] || gridstyle[key];
95108
}
96109
}
97110
return new mpld3_Grid(this.ax, gridprop);
@@ -227,6 +240,20 @@ mpld3_Axis.prototype.draw = function() {
227240

228241
this.wrapTicks();
229242

243+
if (this.props.filtered_minortickvalues && this.props.filtered_minortickvalues.length > 0) {
244+
this.minorAxis = d3[scaleMethod](this.scale)
245+
.tickValues(this.props.filtered_minortickvalues)
246+
.tickSize(this.props.minorticklength || 4, 0, 0)
247+
.tickFormat("");
248+
this.minorElem = this.ax.baseaxes.append('g')
249+
.attr("transform", this.transform)
250+
.attr("class", this.cssclass + " minor")
251+
.call(this.minorAxis);
252+
} else {
253+
this.minorAxis = null;
254+
this.minorElem = null;
255+
}
256+
230257
// We create header-level CSS to style these elements, because
231258
// zooming/panning creates new elements with these classes.
232259
mpld3.insert_css("div#" + this.ax.fig.figid + " ." + this.cssclass + " line, " + " ." + this.cssclass + " path", {
@@ -257,6 +284,19 @@ mpld3_Axis.prototype.zoomed = function(transform) {
257284
} else {
258285
this.elem.call(this.axis);
259286
}
287+
288+
if (this.minorAxis) {
289+
this.minorAxis = this.minorAxis.tickValues(this.props.filtered_minortickvalues || []);
290+
if (transform) {
291+
if (this.props.xy == 'x') {
292+
this.minorElem.call(this.minorAxis.scale(transform.rescaleX(this.scale)));
293+
} else {
294+
this.minorElem.call(this.minorAxis.scale(transform.rescaleY(this.scale)));
295+
}
296+
} else {
297+
this.minorElem.call(this.minorAxis);
298+
}
299+
}
260300
};
261301

262302
mpld3_Axis.prototype.setTicks = function(nr, format) {
@@ -288,4 +328,19 @@ mpld3_Axis.prototype.filter_ticks = function(domain) {
288328
this.props.filtered_tickvalues = this.props.tickvalues;
289329
this.props.filtered_tickformat = this.props.tickformat;
290330
}
331+
332+
if (this.props.minor_tickvalues) {
333+
const that = this;
334+
const minorFilteredTickIndices = this.props.minor_tickvalues.map(function(d, i) {
335+
return i;
336+
}).filter(function(d, i) {
337+
const v = that.props.minor_tickvalues[d];
338+
return (v >= domain[0]) && (v <= domain[1]);
339+
});
340+
this.props.filtered_minortickvalues = this.props.minor_tickvalues.filter(function(d, i) {
341+
return minorFilteredTickIndices.includes(i);
342+
});
343+
} else {
344+
this.props.filtered_minortickvalues = this.props.minor_tickvalues;
345+
}
291346
}

0 commit comments

Comments
 (0)