Skip to content

Even better cartesian trace updates and removal #2579

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
Apr 30, 2018

Conversation

etpinard
Copy link
Contributor

to be merged in #2574.

This PR implements @alexcjohnson 's idea (:pencil2: down in #2563 (comment)) of making cartesian trace module layers (e.g. <g.scatterlayer>, <g.barlayer>, etc) data driven. Getting this to work was easy. Getting this to work well -- that is making sure layers don't get removed/added when they don't need to -- was a little trickier. Why? In brief d3's select() can mutate node data in some scenario (see example). This is not the first time we noticed this behavior. Ricky once wrote:

// Note: .select is adequate but seems to mutate the node data,
// which is at least a bit suprising and causes problems elsewhere

Oh well, see 284c206 for more details.

This PR also makes heatmap/contour/carpet plot methods know how to remove old traces in da63ed4. These modules don't use selectAll().data() to build multiple traces. They instead use trace.uid to build and keep track of nodes. Previously, these old nodes were removed by looping over all traces and using some pretty nasty selectAll(). This significantly improves redraw perf on large (multi-subplot, multi-trace) graphs. For example, using this script with 30x30 subplots/traces and calling relayout(gd, 'xaxis.range', [/*any*/]) used to take ~8000ms, and now is under ~3000ms (which is still slow, but hey it's a start 😜 ).

- should fix some intermittent hangups when running the
  image tests locally
- make trace module layers 'data-driven', one layer per unique
  plot module. Use _module.layerName to denote reused plot methods.
- pass layer to trace _module.plot (as 3rd arg) to not mutate node data
  and allow a given _module.plot method to trace in multiple layers
- no need for previous `_plotMethods` stash attempt
- rename 'imagelayer' -> 'heatmaplayer', 'maplayer' -> 'contourlayer'
  for consistency for other trace modules
... instead of relying on slow selectAll() during subroutines.drawData
    and Plots.cleanPlot. This can be a major perf improvement on
    graph with many DOM nodes (e.g. from many traces and/or many subplots)
... to take into account take now not all trace module layers
    are added to each subplot.
- previously, Contour.plot called Heatmap.call for traces with
  'contour.coloring': 'heatmap' which plotted a heatmap in
  <g.imagelayer> always below the contours. Now as <g.heatmaplayer>
  does not always exist, we plot the heatmap fill inside <g.contourlayer>
  in a ensureSingle layer.
@@ -15,6 +15,7 @@ Histogram2D.attributes = require('./attributes');
Histogram2D.supplyDefaults = require('./defaults');
Histogram2D.calc = require('../heatmap/calc');
Histogram2D.plot = require('../heatmap/plot');
Histogram2D.layerName = 'heatmaplayer';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

N.B. Trace modules that reuse a plot method verbatim from another module need to declare layerName so that their traces are combined into the same trace module layer.

Copy link
Contributor Author

@etpinard etpinard Apr 24, 2018

Choose a reason for hiding this comment

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

.... there's probably a way to 🔪 layerName entirely by building some trace-to-layerName map object within the Registry, but oh well I didn't think adding a few of ⤴️ would be too bad. Thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I like it this way - explicit and clear.

'barlayer',
'carpetlayer',
'violinlayer',
'boxlayer',
'ohlclayer',
'scatterlayer'
'scattercarpetlayer', 'scatterlayer'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

N.B. On the other hand, trace modules (e.g. scattercarpet) that only wrap another plot method need a separate layer.

Copy link
Collaborator

Choose a reason for hiding this comment

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

is there a reason some of the new ones share lines?

Copy link
Contributor Author

@etpinard etpinard Apr 30, 2018

Choose a reason for hiding this comment

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

I put modules that almost have the same plot method on the same line. I thought that would make it easier to track. I can change that of course.


// use a heatmap to fill - draw it behind the lines
var heatmapColoringLayer = Lib.ensureSingle(contourLayer, 'g', 'heatmapcoloring');
Copy link
Contributor Author

Choose a reason for hiding this comment

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

See 48ee0d8 for explanation.

@etpinard etpinard added this to the 1.37.0 milestone Apr 24, 2018
cmd = getRunCI(_commands);
} else {
_commands = [containerCommands.setup].concat(_commands);
cmd = getRunLocal(_commands);
Copy link
Collaborator

Choose a reason for hiding this comment

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

so if I'm reading this right, locally we'll be restarting, pinging, and waiting 5 seconds at the beginning of every command? Seems a little heavy, but if it avoids random hangs (which waste a lot more than 5 secs when they happen) then it's OK.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah it's a little heavy, but at least now npm run test-image always worksTM.

I should probably give #1972 another go at some point in the next few weeks. We really don't need to be running pixel comparison tests with a server, a CLI runner should do just fine.

cdModule: cdModule
});
}
break;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this need to be a 2D loop? Could we just loop over modules, include the layer index in layerData, and sort by that at the end? I guess we'd also need to track which layers we've already included, to ensure uniqueness. But still seems faster than looping over all combinations of className and _module.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wanted to avoid sorting for 🐎 but you're probably right.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in d47276f

@alexcjohnson
Copy link
Collaborator

re: tricky .select() behavior - see d3/d3#1443 (comment)

I've added an extension to d3-selection, selection.selectWithoutDataPropagation which does d3.select(this.node().querySelector(name))

In case using selectAll in these instances becomes a 🐎 issue or something...

for(var i = 0; i < cdheatmaps.length; i++) {
plotOne(gd, plotinfo, cdheatmaps[i]);
heatmapLayer.selectAll('.hm > image').each(function(d) {
var oldTrace = d.trace || {};
Copy link
Collaborator

Choose a reason for hiding this comment

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

when is there no d.trace?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

when a heatmap trace is completely off screen:

var isOffScreen = (imageWidth <= 0 || imageHeight <= 0);
var plotgroup = plotinfo.plot.select('.imagelayer')
.selectAll('g.hm.' + id)
.data(isOffScreen ? [] : [0]);

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is not a big deal, but wouldn't an off-screen heatmap just not contribute to the '.hm > image' selection at all?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removing that || {} makes

it('should not draw traces that are off-screen', function(done) {
var mock = require('@mocks/heatmap_multi-trace.json'),
mockCopy = Lib.extendDeep({}, mock),
gd = createGraphDiv();
function assertImageCnt(cnt) {
var images = d3.selectAll('.hm').select('image');
expect(images.size()).toEqual(cnt);
}
Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() {
assertImageCnt(5);
return Plotly.relayout(gd, 'xaxis.range', [2, 3]);
}).then(function() {
assertImageCnt(2);
return Plotly.relayout(gd, 'xaxis.autorange', true);
}).then(function() {
assertImageCnt(5);
done();
});
});

fail with

image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

... I didn't investigate further.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Alright, not what my quick glance at the code suggested but you're right, no need to dig in any further.

var oldUid = classString.split('carpet')[1].split(/\s/)[0];
for(i = 0; i < cdcarpet.length; i++) {
if(oldUid === cdcarpet[i][0].trace.uid) return;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

🌴 🐎 perhaps make a common helper getUidsFromCalcdata or something, that can be called once up front and returns a hash {uid0: 1, uid1: 1, ...}, then if(!uidHash[oldUid]) d3.select(this).remove();?
Looks like the same helper would work for all modules in this commit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in d47276f

@alexcjohnson
Copy link
Collaborator

@etpinard this is amazing - you make it look so easy, I thought it would be way more involved 😅
Just a few 🐎 suggestions/questions, then this'll be ready to go!

- I don't see the need to optimize here using e.g a lookup object.
  traceLayerClasses.length ~ 10, and fullLayout._modules.length < 20,
  most often 1 or 2.
@@ -34,7 +34,7 @@ case $1 in

jasmine2)
retry npm run test-jasmine -- --tags=gl --skip-tags=noCI,flaky
retry npm run test-jasmine -- --tags=flaky --skip-tags=noCI
retry npm run test-jasmine -- --tags=flaky --skip-tags=noCI,gl
Copy link
Collaborator

Choose a reason for hiding this comment

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

wait, then will tests marked @flaky AND @gl run at all on CI?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

reverted in 8c9a7bf

@etpinard
Copy link
Contributor Author

@alexcjohnson thanks for digging up d3/d3#1443 (comment)! I guess that's something we'll just have to in mind going forward as we make our update patterns more d3-esque.

- so that we cover tests that are tagged @gl and @flaky

This reverts commit 83422f7.
@alexcjohnson
Copy link
Collaborator

Beautiful - thanks for those couple of changes. This is a huge improvement not just in 🐎 but in structure and maintainabilty! 💃

@etpinard etpinard merged commit f771310 into cartesian-trace-deletion Apr 30, 2018
@etpinard etpinard deleted the uid-trace-removal branch April 30, 2018 16:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants