-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Splom zoom perf #2527
Splom zoom perf #2527
Conversation
... (i.e. out of plot_api.js), and move Plots.addLinks out of drawData into its own step in Plotly.plot.
- this represents the minimal sequence for '(x|y)axis.range' relayout calls which are pretty common (e.g. on zoom/pan mouseup). - by bypassing drawFramework, lsInner and initInteraction, this can save ~1000ms on 50x50 subplot grids.
- split minimal updateFx part out of initInteractions - set maindrag cursor class (which depends only on layout.dragmode) on <g .draglayer> instead of inner <rect> to update it for all subplots in < 1ms (that's a > 600ms improvement on 50x50 grids) - use gd._fullLayout instead of scoped fullLayout in initInteractions and makeDragBox to ensure correct reference after doModeBar()
- allow fullLayout._has(/*trace type*/) to work - use registry category hash object (instead of categories.indexOf) to find categories in fullLayout._modules
- replace indexOf with hash objects lookups - add 'svg' and 'draggedPts' trace module categories - speed up updateSubplots (called on pan and scroll) by splitting it into splom, scattergl, svg and draggedPts blocks, (draggedPts is very slow, svg can be slow at 50x50) - ... some scope variable clean up
return true; | ||
} | ||
var name = modules[i].name; | ||
if(name === category) return true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so calls like fullLayout._has('scattergl')
just works w/o having to set a scattergl
category in the trace module.
// should trigger a full initInteractions. | ||
exports.updateFx = function(fullLayout) { | ||
var cursor = fullLayout.dragmode === 'pan' ? 'move' : 'crosshair'; | ||
setCursor(fullLayout._draggers, cursor); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, great solution - much simpler than what I was thinking, and at least as fast 🐎
@@ -34,7 +34,7 @@ Scatter.animatable = true; | |||
Scatter.moduleType = 'trace'; | |||
Scatter.name = 'scatter'; | |||
Scatter.basePlotModule = require('../../plots/cartesian'); | |||
Scatter.categories = ['cartesian', 'symbols', 'markerColorscale', 'errorBarsOK', 'showLegend', 'scatter-like']; | |||
Scatter.categories = ['cartesian', 'svg', 'symbols', 'markerColorscale', 'errorBarsOK', 'showLegend', 'scatter-like', 'draggedPts']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMPORTANT (and possibly worth refactoring)
scattergl and splom traces are currently declared with a 'cartesian'
category so that cartesian axis logic that they share just works. But this means that not all 'cartesian' traces are svg or more precisely not all 'cartesian' traces are plotted in <g .cartersianlayer>
. So to avoid looping over all subplots on drag to shift plotinfo.plot
and its clip <rect>
for scattergl and splom traces, I made an 'svg'
category. This is a slight misnomer though, as heatmaps and histogram2d are here considered svg even though they are built using a <canvas>
. Oh well. The 'svg' category mirrors the 'gl' and 'regl' category listed in scattergl and splom trace modules. Any objections?
Moreover, I added a 'draggedPts' category to avoid those costly selectAll('.point')
on pan when we don't need them. For example, graphs with contour lines feel faster on pan as we no longer traverse the <g .cartesianlayer>
looking for points to scaled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is fine - 'cartesian'
means "data relates to cartesian axes" and 'svg'
means "data is drawn into an SVG container" (heatmaps use a canvas to generate the image but then put it into an svg <image>
element)
'draggedPts'
is still potentially overkill, as the trace type doesn't necessarily tell you if the traces contain these elements - all of these trace types can be drawn with no points or text, only lines and fills...
@@ -2188,7 +2188,7 @@ axes.doTicks = function(gd, axid, skipTitle) { | |||
} | |||
drawTicks(mainPlotinfo[axLetter + 'axislayer'], tickpath); | |||
|
|||
tickSubplots = Object.keys(ax._linepositions); | |||
tickSubplots = Object.keys(ax._linepositions || {}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunate, but important as relinkPrivateKeys
does relink empty objects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... does not relink
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does not relink empty objects.
var name = modules[i].name; | ||
if(name === category) return true; | ||
// N.B. this is modules[i] along with 'categories' as a hash object | ||
var _module = Registry.modules[name]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBH I've forgotten why we wrapped modules like this in the registry... not a big deal, but would it be better to attach the hash directly to the module, or even make categories
as a hash from the beginning? Is it ever important that it's a list?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Referencing #2174 where we like to make building custom trace modules more user friendly:
- we should try to not mutate the input module object, so that means not replacing the
categories
list with a hash object nor augmenting the input module object with e.g. acategoryHash
key. - listing categories as an array feels more natural than
{cat1: 1, cat2: 1}
hash object
So, I'm voting for the status quo.
// TODO | ||
// no test fail when commenting out doAutoRangeAndConstraints, | ||
// but I think we do need this (maybe just the enforce part?) | ||
// Am I right? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh, I'd have thought so... perhaps worth spending a little time looking for something that does fail if this is removed, so it can be 🔒 down.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alexcjohnson Tests are currently passing even when subroutines.doAutoRangeAndConstraints
is commented out, because of this block:
plotly.js/src/plot_api/plot_api.js
Lines 2088 to 2107 in 2a25820
// figure out if we need to recalculate axis constraints | |
var constraints = fullLayout._axisConstraintGroups || []; | |
for(axId in rangesAltered) { | |
for(i = 0; i < constraints.length; i++) { | |
var group = constraints[i]; | |
if(group[axId]) { | |
// Always recalc if we're changing constrained ranges. | |
// Otherwise it's possible to violate the constraints by | |
// specifying arbitrary ranges for all axes in the group. | |
// this way some ranges may expand beyond what's specified, | |
// as they do at first draw, to satisfy the constraints. | |
flags.calc = true; | |
for(var groupAxId in group) { | |
if(!rangesAltered[groupAxId]) { | |
Axes.getFromId(gd, groupAxId)._constraintShrinkable = true; | |
} | |
} | |
} | |
} | |
} |
which makes axis range relayout calls altering constrained axes go though the calc: true
edit pathway which (of course) calls doAutoRangeAndConstraints
itself.
Interestingly, commenting out that block which sets flags.calc = true
does not make any test fails when subroutines.doAutoRangeAndConstraints
is called during axrange
edits. Perhaps we don't need?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't look to me as though I wrote all that great test coverage for this section... the two key points of it are:
- You can
relayout
a constrained axis, particularly to a smaller range, and have the other axes in the group update (expanding or shrinking) to continue to meet the constraints (note that this won't work withreact
, there you'd have to manually update all axes in the constraint group) - If you
relayout
(orreact
) two or more axes in a group in such a way as to explicitly violate the constraints, one or more of those axes will expand (never shrink) its range to fit the constraints.
So if you want to clean this up we should first add a few tests explicitly for ^^. I see some gui tests and one restyle test that uses constraints, but unfortunately no relayout
or react
tests of these edge cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok if I open up an issue about this and defer this to a later PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok if I open up an issue about this and defer this to a later PR?
Yep for sure - that was the "if you want to clean this up" clause
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to -> #2540
There would also be some advantages to keeping separate track of each trace / object that called |
|
||
for(var k in fullLayout._plots) { | ||
var shapelayer = fullLayout._plots[k].shapelayer; | ||
if(shapelayer) shapelayer.selectAll('path').remove(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting - why is this better? Wasn't fullLayout._shapeSubplotLayers
essentially just an array of all the shapelayer
nodes? Or is the win actually below in subroutines.js that you don't have to do a selectAll
to find these?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or is the win actually below in subroutines.js that you don't have to do a selectAll to find these?
That. selectAll
are bad.
src/plots/cartesian/dragbox.js
Outdated
@@ -1105,7 +1105,7 @@ function calcLinks(gd, xaHash, yaHash) { | |||
var yaHashLinked = {}; | |||
var yaxesLinked = []; | |||
for(yLinkID in yLinks) { | |||
var ya = getFromId(gd, xLinkID); | |||
var ya = getFromId(gd, yLinkID); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yikes, good catch!
Unfinished work has been ✏️ in:
|
(to get tests to pass)
@etpinard really impressive - and counterintuitive - work here! |
to be merged into #2505
This PR features performance improvements on zoom interactions geared towards splom traces, but will benefit all cartesian subplots especially on multi-subplot graphs.
I opened a separate PR because I'm hoping to improve a few more things before the splom PR is ready to be merged (cc @dy )
Things improved so far
relayout(gd, 'xaxis.range', [])
is now using a special (much) faster relayout pathwayrelayout(gd, 'dragmode', '')
now only has to update one DOM node per graph!Things I'll like to improve in the coming days
editType: 'calc'
which is slow as a 🐢. I'll try to cache the autorange value somewhere (and clearing it when new data comes in) and use theaxrange
pathway to speed thing up. This will benefit big data gl and svg graphs too!Plots.supplyDefaults
takes > 100ms at 50x50 subplots), Getting rid ofindexOf
into axis lists would be a good start.selectAll
on panaxrange
pathway even more by targeting minimal set of axes to update (viaAxes.doTicks
) instead of redraw ticks and labels for all axes.cc @alexcjohnson