Skip to content

Commit e606c7b

Browse files
committed
[PoC] refactor auto-margin pipeline
- Plots.autoMargin and Plots.doAutoMargin no longer call Plotly.plot redraw !!! - 🔪 `fullLayout._replotting` flag - 🔪 now useless `_redrawFromAutoMarginCount` counter - call component `pushMargin` before `draw`, call Plots.doAutoMargin once after all `pushMargin` are done - merge `lsInner` into `layoutStyles` - 🔪 now obsolete `Axes.allowAutoMargin` and `_pushmarginIds` stash - adapt Plotly.plot pipeline - interplay between pushMargin and doAutoRangeAndConstraints is still a WIP
1 parent 06d7abf commit e606c7b

File tree

6 files changed

+55
-161
lines changed

6 files changed

+55
-161
lines changed

src/plot_api/plot_api.js

+53-82
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,6 @@ function plot(gd, data, layout, config) {
8989
'but this container doesn\'t yet have a plot.', gd);
9090
}
9191

92-
function addFrames() {
93-
if(frames) {
94-
return exports.addFrames(gd, frames);
95-
}
96-
}
97-
9892
// transfer configuration options to gd until we move over to
9993
// a more OO like model
10094
setPlotContext(gd, config);
@@ -147,10 +141,6 @@ function plot(gd, data, layout, config) {
147141
return plotLegacyPolar(gd, data, layout);
148142
}
149143

150-
// so we don't try to re-call Plotly.plot from inside
151-
// legend and colorbar, if margins changed
152-
fullLayout._replotting = true;
153-
154144
// make or remake the framework if we need to
155145
if(graphWasEmpty) makePlotFramework(gd);
156146

@@ -163,11 +153,6 @@ function plot(gd, data, layout, config) {
163153
// clear gradient defs on each .plot call, because we know we'll loop through all traces
164154
Drawing.initGradients(gd);
165155

166-
// save initial show spikes once per graph
167-
if(graphWasEmpty) Axes.saveShowSpikeInitial(gd);
168-
169-
// prepare the data and find the autorange
170-
171156
// generate calcdata, if we need to
172157
// to force redoing calcdata, just delete it before calling Plotly.plot
173158
var recalc = !gd.calcdata || gd.calcdata.length !== (gd._fullData || []).length;
@@ -182,8 +167,9 @@ function plot(gd, data, layout, config) {
182167
if(gd._context.responsive) {
183168
if(!gd._responsiveChartHandler) {
184169
// Keep a reference to the resize handler to purge it down the road
185-
gd._responsiveChartHandler = function() { if(!Lib.isHidden(gd)) Plots.resize(gd); };
186-
170+
gd._responsiveChartHandler = function() {
171+
if(!Lib.isHidden(gd)) Plots.resize(gd);
172+
};
187173
// Listen to window resize
188174
window.addEventListener('resize', gd._responsiveChartHandler);
189175
}
@@ -195,7 +181,11 @@ function plot(gd, data, layout, config) {
195181
* start async-friendly code - now we're actually drawing things
196182
*/
197183

198-
var oldMargins = Lib.extendFlat({}, fullLayout._size);
184+
function addFrames() {
185+
if(frames) {
186+
return exports.addFrames(gd, frames);
187+
}
188+
}
199189

200190
// draw framework first so that margin-pushing
201191
// components can position themselves correctly
@@ -210,19 +200,11 @@ function plot(gd, data, layout, config) {
210200
}
211201

212202
if(!fullLayout._glcanvas && fullLayout._has('gl')) {
213-
fullLayout._glcanvas = fullLayout._glcontainer.selectAll('.gl-canvas').data([{
214-
key: 'contextLayer',
215-
context: true,
216-
pick: false
217-
}, {
218-
key: 'focusLayer',
219-
context: false,
220-
pick: false
221-
}, {
222-
key: 'pickLayer',
223-
context: false,
224-
pick: true
225-
}], function(d) { return d.key; });
203+
fullLayout._glcanvas = fullLayout._glcontainer.selectAll('.gl-canvas').data([
204+
{ key: 'contextLayer', context: true, pick: false },
205+
{ key: 'focusLayer', context: false, pick: false },
206+
{ key: 'pickLayer', context: false, pick: true }
207+
], function(d) { return d.key; });
226208

227209
fullLayout._glcanvas.enter().append('canvas')
228210
.attr('class', function(d) {
@@ -278,37 +260,31 @@ function plot(gd, data, layout, config) {
278260
return Plots.previousPromises(gd);
279261
}
280262

281-
// draw anything that can affect margins.
282-
function marginPushers() {
283-
// First reset the list of things that are allowed to change the margins
284-
// So any deleted traces or components will be wiped out of the
285-
// automargin calculation.
286-
// This means *every* margin pusher must be listed here, even if it
287-
// doesn't actually try to push the margins until later.
288-
Plots.clearAutoMarginIds(gd);
263+
var oldMargins = Lib.extendFlat({}, fullLayout._size);
289264

290-
subroutines.drawMarginPushers(gd);
291-
Axes.allowAutoMargin(gd);
265+
function pushMargin() {
266+
Registry.getComponentMethod('rangeselector', 'pushMargin')(gd);
267+
Registry.getComponentMethod('sliders', 'pushMargin')(gd);
268+
Registry.getComponentMethod('updatemenus', 'pushMargin')(gd);
269+
Registry.getComponentMethod('legend', 'pushMargin')(gd);
270+
Registry.getComponentMethod('colorbar', 'pushMargin')(gd);
271+
if(hasCartesian) Axes.pushMargin(gd);
292272

293-
Plots.doAutoMargin(gd);
294-
return Plots.previousPromises(gd);
273+
return Lib.syncOrAsync([Plots.previousPromises, Plots.doAutoMargin], gd);
295274
}
296275

297276
// in case the margins changed, draw margin pushers again
298-
function marginPushersAgain() {
299-
if(!Plots.didMarginChange(oldMargins, fullLayout._size)) return;
277+
function pushMarginAgain() {
300278

301-
return Lib.syncOrAsync([
302-
marginPushers,
303-
subroutines.layoutStyles
304-
], gd);
279+
if(Plots.didMarginChange(oldMargins, fullLayout._size)) {
280+
oldMargins = Lib.extendFlat({}, fullLayout._size);
281+
return pushMargin();
282+
}
305283
}
306284

307285
function positionAndAutorange() {
308-
if(!recalc) {
309-
doAutoRangeAndConstraints();
310-
return;
311-
}
286+
if(!hasCartesian) return;
287+
if(!recalc) return doAutoRangeAndConstraints();
312288

313289
// TODO: autosize extra for text markers and images
314290
// see https://github.com/plotly/plotly.js/issues/1111
@@ -324,48 +300,49 @@ function plot(gd, data, layout, config) {
324300

325301
subroutines.doAutoRangeAndConstraints(gd);
326302

327-
// store initial ranges *after* enforcing constraints, otherwise
328-
// we will never look like we're at the initial ranges
329-
if(graphWasEmpty) Axes.saveRangeInitial(gd);
330-
331303
// this one is different from shapes/annotations calcAutorange
332304
// the others incorporate those components into ax._extremes,
333305
// this one actually sets the ranges in rangesliders.
334306
Registry.getComponentMethod('rangeslider', 'calcAutorange')(gd);
335307
}
336308

337-
// draw ticks, titles, and calculate axis scaling (._b, ._m)
309+
function saveInitial() {
310+
if(graphWasEmpty && hasCartesian) {
311+
// store initial ranges *after* enforcing constraints, otherwise
312+
// we will never look like we're at the initial ranges
313+
Axes.saveRangeInitial(gd);
314+
// save initial show spikes once per graph
315+
Axes.saveShowSpikeInitial(gd);
316+
}
317+
}
318+
338319
function drawAxes() {
339-
return Axes.draw(gd, graphWasEmpty ? '' : 'redraw');
320+
if(hasCartesian) {
321+
return Axes.draw(gd, graphWasEmpty ? '' : 'redraw');
322+
}
340323
}
341324

342325
var seq = [
343326
Plots.previousPromises,
344327
addFrames,
345328
drawFramework,
346-
marginPushers,
347-
marginPushersAgain
348-
];
349-
350-
if(hasCartesian) seq.push(positionAndAutorange);
351-
352-
seq.push(subroutines.layoutStyles);
353-
if(hasCartesian) seq.push(drawAxes);
354-
355-
seq.push(
329+
positionAndAutorange,
330+
pushMargin,
331+
pushMarginAgain,
332+
pushMarginAgain,
333+
positionAndAutorange,
334+
saveInitial,
335+
subroutines.layoutStyles,
356336
subroutines.drawData,
337+
subroutines.drawMarginPushers,
338+
drawAxes,
357339
subroutines.finalDraw,
358340
initInteractions,
359341
Plots.addLinks,
360342
Plots.rehover,
361343
Plots.redrag,
362-
// TODO: doAutoMargin is only needed here for axis automargin, which
363-
// happens outside of marginPushers where all the other automargins are
364-
// calculated. Would be much better to separate margin calculations from
365-
// component drawing - see https://github.com/plotly/plotly.js/issues/2704
366-
Plots.doAutoMargin,
367344
Plots.previousPromises
368-
);
345+
];
369346

370347
// even if everything we did was synchronous, return a promise
371348
// so that the caller doesn't care which route we took
@@ -379,13 +356,7 @@ function plot(gd, data, layout, config) {
379356
}
380357

381358
function emitAfterPlot(gd) {
382-
var fullLayout = gd._fullLayout;
383-
384-
if(fullLayout._redrawFromAutoMarginCount) {
385-
fullLayout._redrawFromAutoMarginCount--;
386-
} else {
387-
gd.emit('plotly_afterplot');
388-
}
359+
gd.emit('plotly_afterplot');
389360
}
390361

391362
function setPlotConfig(obj) {

src/plot_api/subroutines.js

+2-12
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,6 @@ var SVG_TEXT_ANCHOR_START = 'start';
3131
var SVG_TEXT_ANCHOR_MIDDLE = 'middle';
3232
var SVG_TEXT_ANCHOR_END = 'end';
3333

34-
exports.layoutStyles = function(gd) {
35-
return Lib.syncOrAsync([Plots.doAutoMargin, lsInner], gd);
36-
};
37-
3834
function overlappingDomain(xDomain, yDomain, domains) {
3935
for(var i = 0; i < domains.length; i++) {
4036
var existingX = domains[i][0];
@@ -50,7 +46,7 @@ function overlappingDomain(xDomain, yDomain, domains) {
5046
return false;
5147
}
5248

53-
function lsInner(gd) {
49+
exports.layoutStyles = function(gd) {
5450
var fullLayout = gd._fullLayout;
5551
var gs = fullLayout._size;
5652
var pad = gs.p;
@@ -302,7 +298,7 @@ function lsInner(gd) {
302298
Axes.makeClipPaths(gd);
303299

304300
return Plots.previousPromises(gd);
305-
}
301+
};
306302

307303
function shouldShowLinesOrTicks(ax, subplot) {
308304
return (ax.ticks || ax.showline) &&
@@ -563,9 +559,6 @@ exports.drawData = function(gd) {
563559
Registry.getComponentMethod('annotations', 'draw')(gd);
564560
Registry.getComponentMethod('images', 'draw')(gd);
565561

566-
// Mark the first render as complete
567-
fullLayout._replotting = false;
568-
569562
return Plots.previousPromises(gd);
570563
};
571564

@@ -673,9 +666,6 @@ exports.doAutoRangeAndConstraints = function(gd) {
673666
}
674667
};
675668

676-
// An initial paint must be completed before these components can be
677-
// correctly sized and the whole plot re-margined. fullLayout._replotting must
678-
// be set to false before these will work properly.
679669
exports.finalDraw = function(gd) {
680670
// TODO: rangesliders really belong in marginPushers but they need to be
681671
// drawn after data - can we at least get the margin pushing part separated

src/plots/cartesian/axes.js

-29
Original file line numberDiff line numberDiff line change
@@ -2975,35 +2975,6 @@ function selectTickLabel(gTick) {
29752975
return mj.empty() ? s.select('text') : mj;
29762976
}
29772977

2978-
/**
2979-
* Find all margin pushers for 2D axes and reserve them for later use
2980-
* Both label and rangeslider automargin calculations happen later so
2981-
* we need to explicitly allow their ids in order to not delete them.
2982-
*
2983-
* TODO: can we pull the actual automargin calls forward to avoid this hack?
2984-
* We're probably also doing multiple redraws in this case, would be faster
2985-
* if we can just do the whole calculation ahead of time and draw once.
2986-
*/
2987-
axes.allowAutoMargin = function(gd) {
2988-
var axList = axes.list(gd, '', true);
2989-
for(var i = 0; i < axList.length; i++) {
2990-
var ax = axList[i];
2991-
if(ax.automargin) {
2992-
Plots.allowAutoMargin(gd, axAutoMarginID(ax));
2993-
if(ax.mirror) {
2994-
Plots.allowAutoMargin(gd, axMirrorAutoMarginID(ax));
2995-
}
2996-
}
2997-
if(Registry.getComponentMethod('rangeslider', 'isVisible')(ax)) {
2998-
Plots.allowAutoMargin(gd, rangeSliderAutoMarginID(ax));
2999-
}
3000-
}
3001-
};
3002-
3003-
function axAutoMarginID(ax) { return ax._id + '.automargin'; }
3004-
function axMirrorAutoMarginID(ax) { return axAutoMarginID(ax) + '.mirror'; }
3005-
function rangeSliderAutoMarginID(ax) { return ax._id + '.rangeslider'; }
3006-
30072978
// swap all the presentation attributes of the axes showing these traces
30082979
axes.swap = function(gd, traces) {
30092980
var axGroups = makeAxisGroups(gd, traces);

src/plots/cartesian/dragbox.js

-4
Original file line numberDiff line numberDiff line change
@@ -539,9 +539,6 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
539539
return;
540540
}
541541

542-
// prevent axis drawing from monkeying with margins until we're done
543-
gd._fullLayout._replotting = true;
544-
545542
if(xActive === 'ew' || yActive === 'ns') {
546543
if(xActive) {
547544
dragAxList(xaxes, dx);
@@ -778,7 +775,6 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
778775
Lib.syncOrAsync([
779776
Plots.previousPromises,
780777
function() {
781-
gd._fullLayout._replotting = false;
782778
Registry.call('_guiRelayout', gd, updates);
783779
}
784780
], gd);

src/plots/cartesian/set_convert.js

-1
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,6 @@ module.exports = function setConvert(ax, fullLayout) {
487487
}
488488

489489
if(!isFinite(ax._m) || !isFinite(ax._b) || ax._length < 0) {
490-
fullLayout._replotting = false;
491490
throw new Error('Something went wrong with axis scaling');
492491
}
493492
};

0 commit comments

Comments
 (0)