Skip to content
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

Cross-chart context management #1989

Closed
wants to merge 9 commits into from
Closed

Cross-chart context management #1989

wants to merge 9 commits into from

Conversation

dy
Copy link
Contributor

@dy dy commented Sep 5, 2017

Replaces #1986.
Following regl-summer meeting.

Makes parcoords use shared canvases and enables viewport/scissor instead of padding canvas.

There is single image diff gl2d_parcoord_2.
That seems to be due to pixel snapping by browser padding/margin, whereas viewport keeps precise values.
All other tests pass.

@dy dy requested review from monfera and etpinard September 6, 2017 21:31
@dy dy mentioned this pull request Sep 6, 2017
Copy link
Contributor

@etpinard etpinard left a comment

Choose a reason for hiding this comment

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

Thanks very much @dfcreative for taking this on!

You're up to a solid start.


// create canvases only in case if there is at least one regl component
// FIXME: probably there is a better d3 way of doing so
for(var i = 0; i < fullLayout._modules.length; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

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

using fullLayout._has would clean this up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fullLayout._has works for plots, not traces, if I get it right

fullLayout._glcanvas = fullLayout._glcontainer.enter().append('div')
.classed('gl-container', true)
.selectAll('.gl-canvas')
.data([{
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to make objects here: .data(['context', 'focus', 'pick'], String) would suffice, where String is the key function.

Copy link
Contributor Author

@dy dy Sep 7, 2017

Choose a reason for hiding this comment

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

@etpinard these objects are used by parcoords later, strings do not allow for putting additional stuff in them.
In particular, https://github.com/plotly/plotly.js/pull/1989/files#diff-2fad23de82bf8c7625ea8e11436233b7R301

for(var i = 0; i < fullLayout._modules.length; i++) {
var module = fullLayout._modules[i];
if(module.categories && module.categories.indexOf('gl') >= 0) {
fullLayout._glcanvas.enter().append('canvas')
Copy link
Contributor

Choose a reason for hiding this comment

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

Ok. so this create the canvases properly, but we'll need to clear them when fullLayout._has('gl') === false.

Copy link
Contributor Author

@dy dy Sep 7, 2017

Choose a reason for hiding this comment

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

It is a bit more complicated, from interaction tests... Working on that. Sometimes we deal with

Plotly.plot(document.body, [{}]);
Plotly.restyle(document.body, {type: 'parcoords', x: [[1]]}, 0);

extendDeep(
{},
Copy link
Contributor

Choose a reason for hiding this comment

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

Why did you change this?

colorAttributes and friends are used in other place in plotly.js, so we can't just extend them here. We need to make a copy of them and then overwrite a few keys.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@etpinard colorAttributes(str) returns a new object every call, no need to copy a copy.

@@ -28,8 +27,6 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout)
var hasParcoords = (newFullLayout._has && newFullLayout._has('parcoords'));

if(hadParcoords && !hasParcoords) {
oldFullLayout._paperdiv.selectAll('.parcoords-line-layers').remove();
oldFullLayout._paperdiv.selectAll('.parcoords-line-layers').remove();
Copy link
Contributor

Choose a reason for hiding this comment

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

Where is this done now?

Copy link
Contributor Author

@dy dy Sep 7, 2017

Choose a reason for hiding this comment

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

Since we keep canvases, I guessed we have to store them instead of recreating every time.
Actually removing and creating anew would solve the problem... Should I try to do that?

x: 0,
y: 0,
width: canvas.width,
height: canvas.height
Copy link
Contributor

Choose a reason for hiding this comment

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

Very nice simplification 👌

@@ -248,6 +261,13 @@ module.exports = function(canvasGL, lines, canvasWidth, canvasHeight, initialDim
}
},

viewport: {
Copy link
Contributor

Choose a reason for hiding this comment

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

Lovely ❤️

@@ -253,8 +244,7 @@ function styleExtentTexts(selection) {
.style('user-select', 'none');
}

module.exports = function(root, svg, styledData, layout, callbacks) {

module.exports = function(root, svg, parcoordsLineLayers, styledData, layout, callbacks) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Adding parcoordsLineLayers to this renderer function is fine, because their creation is no longer subordinated to this, the layers need to come from the outside so they're also usable for scatter plot etc.

@@ -13,8 +13,9 @@ var parcoords = require('./parcoords');
module.exports = function plot(gd, cdparcoords) {

var fullLayout = gd._fullLayout;
var svg = fullLayout._paper;
var svg = fullLayout._toppaper;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For proper z-index.
.gl-container resides between two svg layers, unlike .parcoord-layers container, which was always behind svg layers.

Copy link
Contributor

Choose a reason for hiding this comment

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

Right, nice. So the parcoord axes are now drawn in _toppaper, correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@etpinard yep, I tried to test FPS - seems to work fine

@monfera
Copy link
Contributor

monfera commented Sep 7, 2017

Looks great! Performance seems to be identical too.

There's some slight increase to what gets cleared on the right hand side, compare:

image

with new:

image

No big deal but it'd be nice to just repaint (clear) just the impacted area.
Perhaps the issue also happens on the left side too but isn't visible due to cropping.

function clear(regl, x, y, width, height) {
    var gl = regl._gl;
    gl.enable(gl.SCISSOR_TEST);
    gl.scissor(x, y, width, height);
+    regl.clear({color: [0.5 + 0.5 * Math.random(), 0.5 + 0.5 * Math.random(), 0.5 + 0.5 * Math.random(), 1], depth: 1}); // clearing is done in scissored panel only
-    regl.clear({color: [0, 0, 0, 0], depth: 1}); // clearing is done in scissored panel only
}

@monfera monfera changed the title Better regl context management Cross-chart context management Sep 7, 2017
@dy
Copy link
Contributor Author

dy commented Sep 7, 2017

@monfera I am facing an issue here when we have multiple parcoord traces put one over the other.
As we expected, one trace clears part of the other trace.
image

Discussing that with @etpinard that seems to be an important feature and we can't just drop it with this PR.

I don't see easy fixes for that but creating a separate div container for every trace. Possibly we could do some separate texture rendering with subsequent merging render pass, but that is out of scope of the PR.

What are your thoughts on that?

@monfera
Copy link
Contributor

monfera commented Sep 7, 2017

@dfcreative superimposingparcoords plots on one another don't make too much sense especially given that we already have a context layer. I can only think of forced examples. Also, if the overlaid parcoords are simply rendered, they should look fine (if not, then there's an initial clear which could be disabled), whatever you render on top will partially or fully occlude what's below. Real problems start to happen with interactions, e.g. what does it even mean to drag&drop a column of superimposed parcoords, if the lower parcoord layer stays the same, it'll be a mess; but columns can't move together as the overlaid parcoords may have different columns.

So it feels there's no use case for overlaying multiple parcoords without adding a bunch of glue functionality at least.

See next comment for more.

@monfera
Copy link
Contributor

monfera commented Sep 7, 2017

From dy/plotly-contrib#14

multiple overlaying parcoords instances: how do we workaround that?

As mentioned above, the need is probably extremely rare or nonexistent, especially given that the overlaid parcoords would still render and interact independently.

But the question is generally applicable, for example, for scatter plots. While it's not the immediate step, the very idea of sharing canvases is to remain below the 16 WebGL context limit, which assumes that the same canvas may have a bunch of scatterplots on them, and overlaying would have a use there too. I'm thinking of the following possibilities, please let's extend it if there are other solutions:

  1. Declare overlaps illegal - but this excludes valid, if rare use cases and our last chat concluded that overlaps should be supported
  2. Redraw the entire scene graph upon every rAF loop - but it'd be prohibitively expensive in rendering time and totally non-scalable
  3. Identify overlaps and redraw a plot only if something else that overlaps with it changed - the benefit of this is that the 99.9% of the cases, when there's no overlap, would still be fast, and the 0.1% of cases would be correct. The worst case is if plots serially overlap and one plot change ripples through a full screen rerender, even this is not too bad as it must be incredibly rare. Note that the granularity of overlap detection needs not be the entire plot, it could just be a panel, as parcoords, see gifs above. It could be similar with an eventual WebGL SPLOM or small multiples.
  4. Using gl.bindFramebuffer and texture to render a plot (or its panels) into texture(s), and doing either of the above two solutions, except the programmatic drawing gives way to pasting in the appropriate texture. It can be fancy, e.g. only the texture part corresponding to the dirtied area is copied. A drawback is that there's less control over compositing (instead of drawing, we paste a homogeneous RGBA blob), but for our use it's not a problem.
  5. Layout management, quadtree etc. - since the above represents added cost, it can be minimized by being smart about what to redraw. Eg. if there are no points in an otherwise overlaid corner, there's no reason to redraw it. But with this, we're getting into reimplementing Skia, compositing and SVG functionality in WebGL.

I'd check if current scattergl overlaps actually work or not. Probably they do for an initial render, but it's not the initial render that causes the main problem; the initial render could just draw atop. I'd test if interactions such as zoom, pan, screenshotting etc. work now with overlapping scattergl. If not, maybe plots can be run as static, ie. no interaction, therefore no problem.

Barring this, option 3 looks reasonable. A global dictionary would keep track of overlapping panels or plots and redraw in the rare case there's overlap.

Whatever the solution, it'd be great to abstract out the notion of the panel, e.g. what's between an axis pair in parcoords (already called panel), or what would be a subplot on a SPLOM or small multiple. These can behave independently of each other, and the use of such smaller tiles would minimize the overall area of rectangles that overlap.

A tricky question is the pick layer: the colors (RGBA encoding) should become global in that both the layer (plot) and the point (data point represented with a marker or line) can be recovered. A simpler option is if, as per painter's algo, only the top layer in an overlapping stack would capture mouse events.

clearing canvases: what's strategy?

Current parcoords strategy is, minimize clear, b/c when you clear, you must redraw there too :-) The consequence of this is just above. An alternative is to not use preserveDrawingBuffer.

gl3d plots: should we introduce shared canvas there?

The motive for sharing canvas is to not run out of WebGL contexts. It's less likely that 3D widgets are numerous, eg. a grid of various surfaces, than having SPLOM or 2D small multiples, but it can happen, so it's a prioritization question @etpinard ? Also, I think overlaying multiple 3D widgets, or 2D + 3D widgets would be very rare or useless, so maybe we could follow option 1 for these.

In other words, if there's a need to support lots of independent 3D widgets, then they could be rendered onto the same set of canvas (may or may not be the same set as the 2D canvases), as long as overlaps are prohibited or unsupported (imo).

@etpinard
Copy link
Contributor

etpinard commented Sep 8, 2017

superimposing parcoords plots on one another don't make too much sense especially given that we already have a context layer. I can only think of forced examples.

@monfera I see your point from a developer's perspective. It is true that superimposing parcoords traces isn't a typical nor a useful use case. But from a user's perspective, superimposed subplots is part of the plotly.js API, it should just work. Moreover from a maintainer's perspective, superimposing parcoords does work currently, so we can't just break that.

So, I think we're left with two options:

  • Leave parcoods untouched for now. It works fine, users are happy about it. It does live in its own universe -- which is far from ideal -- but I can't think of any upsides from spending multiple weeks trying to make overlapping parcoords subplots on shared-canvases work.
  • Restrict parcoords domain values to be non-overlapping (in a setPositions method, similar to how different bar traces interact). This would be a (slight) breaking change. We can vote on it. Most importantly, we should make sure our users don't mind.

@monfera
Copy link
Contributor

monfera commented Sep 8, 2017

@etpinard I totally see your point ie. the value of the standard and pragmatism, and it was the dataviz perspective rather than the developer perspective from which overlapping parcoords didn't look particularly useful to me. In other words, there are hundreds of other parcoords enhancements that would be more useful than enabling overlaps (again, from a dataviz pov). I'll check with our users though to be sure.

The standard of being able to overplot anything with anything may have come from 1) it's a given with SVG; 2) it's also a given with WebGL as long as contexts are kept separate. This ticket is partly about undoing 2) to let a user have a few parcoords and/or other WebGL things.

I should have no vote in which way it falls because of having no resource / product management or architect role but glad to chime in:

Siding with your option 2 (rule out overlapping) has the benefit that it retains freedom of movement, ie. we can postpone the questionable functionality of parcoords overlap until a customer strongly needs it. If that happens, perhaps it won't just be a 'dumb' overplot anyway.

By contrast, going with (retaining) option 1 just binds us to a functionality that has tech drawbacks, e.g.

  • easier to run out of WebGL contexts
  • must preserve the separation indefinitely
  • not being able to share a GPU crossfilter state between parcoords and the new scattergl

for little to no offsetting benefit.

@monfera
Copy link
Contributor

monfera commented Sep 8, 2017

^^ I think the biggies are the abilities to share

  • GL contexts (full dashboard without exceeding the 16 contexts)
  • GPU crossfilter, ie. all 2D WebGL plots could participate in ultrafast crossfiltering; think of a parcoords plus multiple scatterplots or a SPLOM on a dashboard

@monfera
Copy link
Contributor

monfera commented Sep 18, 2017

We have client feedback confirming that there's no need for overlapping parallel coordinates. A subsequent future option (maybe layering related, maybe not - served to a good extent by currently supported functions) is under clarification.

@monfera
Copy link
Contributor

monfera commented Sep 18, 2017

It looks like the function in question wouldn't be achieved (or achievable) by distinct overlapping parcoords, so there's no blocker to switching parcoordsto a design that precludes overlapping.

@dy dy mentioned this pull request Nov 14, 2017
@etpinard
Copy link
Contributor

Closing. Superseded #2159

@etpinard etpinard closed this Nov 17, 2017
@etpinard etpinard deleted the make-parcoords-friend branch November 17, 2017 16:23
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.

None yet

4 participants