Skip to content

Fix relayout constrained axes + various multi-subplot perf improvements #2628

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 14, 2018
35 changes: 15 additions & 20 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -1689,19 +1689,9 @@ exports.relayout = function relayout(gd, astr, val) {
// subroutine of its own so that finalDraw always gets
// executed after drawData
seq.push(
// TODO
// no test fail when commenting out doAutoRangeAndConstraints,
// but I think we do need this (maybe just the enforce part?)
// Am I right?
// More info in:
// https://github.com/plotly/plotly.js/issues/2540
subroutines.doAutoRangeAndConstraints,
// TODO
// can target specific axes,
// do not have to redraw all axes here
// See:
// https://github.com/plotly/plotly.js/issues/2547
subroutines.doTicksRelayout,
function(gd) {
return subroutines.doTicksRelayout(gd, specs.rangesAltered);
},
subroutines.drawData,
subroutines.finalDraw
);
Expand All @@ -1728,6 +1718,10 @@ exports.relayout = function relayout(gd, astr, val) {
});
};

var AX_RANGE_RE = /^[xyz]axis[0-9]*\.range(\[[0|1]\])?$/;
var AX_AUTORANGE_RE = /^[xyz]axis[0-9]*\.autorange$/;
var AX_DOMAIN_RE = /^[xyz]axis[0-9]*\.domain(\[[0|1]\])?$/;

function _relayout(gd, aobj) {
var layout = gd.layout,
fullLayout = gd._fullLayout,
Expand Down Expand Up @@ -1806,7 +1800,7 @@ function _relayout(gd, aobj) {
var plen = p.parts.length;
// p.parts may end with an index integer if the property is an array
var pend = plen - 1;
while(pend > 0 && typeof p.parts[plen - 1] !== 'string') { pend--; }
while(pend > 0 && typeof p.parts[pend] !== 'string') pend--;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha, nice catch!

// last property in chain (leaf node)
var pleaf = p.parts[pend];
// leaf plus immediate parent
Expand Down Expand Up @@ -1841,11 +1835,11 @@ function _relayout(gd, aobj) {
fullLayout[ai] = gd._initialAutoSize[ai];
}
// check autorange vs range
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.range(\[[0|1]\])?$/)) {
else if(pleafPlus.match(AX_RANGE_RE)) {
recordAlteredAxis(pleafPlus);
Lib.nestedProperty(fullLayout, ptrunk + '._inputRange').set(null);
}
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.autorange$/)) {
else if(pleafPlus.match(AX_AUTORANGE_RE)) {
recordAlteredAxis(pleafPlus);
Lib.nestedProperty(fullLayout, ptrunk + '._inputRange').set(null);
var axFull = Lib.nestedProperty(fullLayout, ptrunk).get();
Expand All @@ -1855,7 +1849,7 @@ function _relayout(gd, aobj) {
axFull._input.domain = axFull._inputDomain.slice();
}
}
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.domain(\[[0|1]\])?$/)) {
else if(pleafPlus.match(AX_DOMAIN_RE)) {
Lib.nestedProperty(fullLayout, ptrunk + '._inputDomain').set(null);
}

Expand Down Expand Up @@ -2061,6 +2055,7 @@ function _relayout(gd, aobj) {

return {
flags: flags,
rangesAltered: rangesAltered,
undoit: undoit,
redoit: redoit,
eventData: Lib.extendDeep({}, redoit)
Expand Down Expand Up @@ -2174,8 +2169,9 @@ exports.update = function update(gd, traceUpdate, layoutUpdate, _traces) {
if(relayoutFlags.layoutstyle) seq.push(subroutines.layoutStyles);
if(relayoutFlags.axrange) {
seq.push(
subroutines.doAutoRangeAndConstraints,
subroutines.doTicksRelayout,
function(gd) {
return subroutines.doTicksRelayout(gd, relayoutSpecs.rangesAltered);
Copy link
Contributor Author

@etpinard etpinard May 11, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N.B. @alexcjohnson I didn't implement per-axis "axrange" edit for Plotly.react. My main concern was improvement perf on dragend axis range relayout calls. I didn't think adding an exception to the react diffing algo was worth it for this case.

},
subroutines.drawData,
subroutines.finalDraw
);
Expand Down Expand Up @@ -2340,7 +2336,6 @@ exports.react = function(gd, data, layout, config) {
if(relayoutFlags.layoutstyle) seq.push(subroutines.layoutStyles);
if(relayoutFlags.axrange) {
seq.push(
subroutines.doAutoRangeAndConstraints,
subroutines.doTicksRelayout,
subroutines.drawData,
subroutines.finalDraw
Expand Down
8 changes: 6 additions & 2 deletions src/plot_api/subroutines.js
Original file line number Diff line number Diff line change
Expand Up @@ -448,8 +448,12 @@ exports.doLegend = function(gd) {
return Plots.previousPromises(gd);
};

exports.doTicksRelayout = function(gd) {
Axes.doTicks(gd, 'redraw');
exports.doTicksRelayout = function(gd, rangesAltered) {
if(rangesAltered) {
Axes.doTicks(gd, Object.keys(rangesAltered), true);
} else {
Axes.doTicks(gd, 'redraw');
}

if(gd._fullLayout._hasOnlyLargeSploms) {
clearGlCanvases(gd);
Expand Down
84 changes: 56 additions & 28 deletions src/plots/cartesian/axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -1456,6 +1456,10 @@ axes.findSubplotsWithAxis = function(subplots, ax) {
// makeClipPaths: prepare clipPaths for all single axes and all possible xy pairings
axes.makeClipPaths = function(gd) {
var fullLayout = gd._fullLayout;

// for more info: https://github.com/plotly/plotly.js/issues/2595
if(fullLayout._hasOnlyLargeSploms) return;

var fullWidth = {_offset: 0, _length: fullLayout.width, _id: ''};
var fullHeight = {_offset: 0, _length: fullLayout.height, _id: ''};
var xaList = axes.list(gd, 'x', true);
Expand Down Expand Up @@ -1494,28 +1498,50 @@ axes.makeClipPaths = function(gd) {
});
};

// doTicks: draw ticks, grids, and tick labels
// axid: 'x', 'y', 'x2' etc,
// blank to do all,
// 'redraw' to force full redraw, and reset:
// ax._r (stored range for use by zoom/pan)
// ax._rl (stored linearized range for use by zoom/pan)
// or can pass in an axis object directly
axes.doTicks = function(gd, axid, skipTitle) {
/** Main axis drawing routine!
*
* This routine draws axis ticks and much more (... grids, labels, title etc.)
* Supports multiple argument signatures.
* N.B. this thing is async in general (because of MathJax rendering)
*
* @param {DOM element} gd : graph div
* @param {object or string or array of strings} arg : polymorphic argument
* @param {boolean} skipTitle : optional flag to skip axis title draw/update
* @return {promise}
*
* Signature 1: Axes.doTicks(gd, ax)
* where ax is an axis object as in fullLayout
*
* Signature 2: Axes.doTicks(gd, axId)
* where axId is a axis id string
*
* Signature 3: Axes.doTicks(gd, 'redraw')
* use this to clear and redraw all axes on graph
*
* Signature 4: Axes.doTicks(gd, '')
* use this to draw all axes on graph w/o the selectAll().remove()
* of the 'redraw' signature
*
* Signature 5: Axes.doTicks(gd, [axId, axId2, ...])
* where the items are axis id string,
* use this to update multiple axes in one call
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was already a bit out of control, do we really want to add another signature?

Can we perhaps break it into 2 separate functions, one for signatures 1 & 2 and a second for 3, 4, 5? That's how it functions anyway - 3,4,5 just map into a bunch of calls to signature 2 (ignoring skipTitle) and then return.

Copy link
Contributor Author

@etpinard etpinard May 11, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure that would be nice. But since doTicks is one of our most referenced function in existing issues, I'm a little reluctant to do so.

But if you insist, than I'm thinking of making Axes.doTicks the multi-cartesian-axis version (that accepts '', 'redraw' and arrays of axis ids), and adding Axes.doTicksSingle (that accepts one axis id string or an axis object).

On master, we have:

image

*
* N.B signatures 3, 4 and 5 reset:
* - ax._r (stored range for use by zoom/pan)
* - ax._rl (stored linearized range for use by zoom/pan)
*/
axes.doTicks = function(gd, arg, skipTitle) {
var fullLayout = gd._fullLayout;
var ax;
var independent = false;
var ax;

// allow passing an independent axis object instead of id
if(typeof axid === 'object') {
ax = axid;
axid = ax._id;
if(Lib.isPlainObject(arg)) {
ax = arg;
independent = true;
}
else {
ax = axes.getFromId(gd, axid);
} else if(!arg || arg === 'redraw' || Array.isArray(arg)) {
var axList = (!arg || arg === 'redraw') ? axes.listIds(gd) : arg;

if(axid === 'redraw') {
if(arg === 'redraw') {
fullLayout._paper.selectAll('g.subplot').each(function(subplot) {
var plotinfo = fullLayout._plots[subplot];
var xa = plotinfo.xaxis;
Expand All @@ -1530,22 +1556,24 @@ axes.doTicks = function(gd, axid, skipTitle) {
});
}

if(!axid || axid === 'redraw') {
return Lib.syncOrAsync(axes.list(gd, '', true).map(function(ax) {
return function() {
if(!ax._id) return;
var axDone = axes.doTicks(gd, ax._id);
ax._r = ax.range.slice();
ax._rl = Lib.simpleMap(ax._r, ax.r2l);
return axDone;
};
}));
}
return Lib.syncOrAsync(axList.map(function(a) {
return function() {
if(!a) return;
var axDone = axes.doTicks(gd, a);
var ax = axes.getFromId(gd, a);
ax._r = ax.range.slice();
ax._rl = Lib.simpleMap(ax._r, ax.r2l);
return axDone;
};
}));
} else {
ax = axes.getFromId(gd, arg);
}

// set scaling to pixels
ax.setScale();

var axid = ax._id;
var axLetter = axid.charAt(0);
var counterLetter = axes.counterLetter(axid);
var vals = ax._vals = axes.calcTicks(ax);
Expand Down
33 changes: 33 additions & 0 deletions test/jasmine/tests/cartesian_interact_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,39 @@ describe('axis zoom/pan and main plot zoom', function() {
.catch(failTest)
.then(done);
});

it('updates axis layout when the constraints require it', function(done) {
function _assert(xGridCnt) {
var xGrid = d3.select(gd).selectAll('.gridlayer > .x > path.xgrid');
expect(xGrid.size()).toEqual(xGridCnt);
}

Plotly.plot(gd, [{
x: [1, 1.5, 0, -1.5, -1, -1.5, 0, 1.5, 1],
y: [0, 1.5, 1, 1.5, 0, -1.5, -1, -1.5, 0],
line: {shape: 'spline'}
}], {
xaxis: {constrain: 'domain'},
yaxis: {scaleanchor: 'x'},
width: 700,
height: 500
})
.then(function() {
_assert(2);

return Plotly.relayout(gd, {
'xaxis.range[0]': 0,
'xaxis.range[1]': 1,
'yaxis.range[0]': 0,
'yaxis.range[1]': 1
});
})
.then(function() {
_assert(1);
})
.catch(failTest)
.then(done);
});
});

describe('Event data:', function() {
Expand Down
17 changes: 16 additions & 1 deletion test/jasmine/tests/plot_api_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ describe('Test plot api', function() {
});

it('should trigger minimal sequence for cartesian axis range updates', function() {
var seq = ['doAutoRangeAndConstraints', 'doTicksRelayout', 'drawData', 'finalDraw'];
var seq = ['doTicksRelayout', 'drawData', 'finalDraw'];

function _assert(msg) {
expect(gd.calcdata).toBeDefined();
Expand Down Expand Up @@ -669,6 +669,21 @@ describe('Test plot api', function() {
destroyGraphDiv();
});
});

it('should trigger calc on axis range updates when constraints are present', function() {
gd = mock({
data: [{
y: [1, 2, 1]
}],
layout: {
xaxis: {constrain: 'domain'},
yaxis: {scaleanchor: 'x'}
}
});

Plotly.relayout(gd, 'xaxis.range[0]', 0);
expect(gd.calcdata).toBeUndefined();
});
});

describe('Plotly.restyle subroutines switchboard', function() {
Expand Down