Skip to content

parcoords #1256

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 7 commits into from
Feb 23, 2017
Merged

parcoords #1256

merged 7 commits into from
Feb 23, 2017

Conversation

monfera
Copy link
Contributor

@monfera monfera commented Dec 15, 2016

Introduces parcoordswith panel trellising, GPU based crossfilter, hybrid SVG + GPU and foundations for multivariate plots such as SPLOM (scatterplot matrices), slope diagrams, strip plots and small multiple plots.

#1071
Examples:

A collection of todo items:

Essential / short term:

  • Avoid repetitive map calls in uniforms setting (PR feedback: Étienne)
  • Fix a regression involving incremental rendering (introduced with above)
  • Add jasmine tests
  • Add dimension.constraintrange attributes that represent initial interval constraints (PR feedback: Étienne)
  • Add event callback on filtering / reset (client discussion)
  • Add dimension.range (PR feedback: Étienne)
  • Multiple parcoords rows if called with multiple data items or Plotly.plot is called again (PR feedback: Étienne)
  • Doing some more code regularization in parcoords.js and layout.js
  • ... once the regularization is done, iterate on restyle/relayout
  • dimension.visible parcoords #1256 (comment)
  • dimension lengths PR feedback from Étienne
  • Fire restyle on filter brush mouseup, rather than the current event type (PR feedback: Étienne)
  • Phase out the integer attribute in favor of a Plotly axis style attribute (PR feedback: Étienne, Alex)
  • Add more defaults.js logic
    • trim dimension.values lengths to uniform length
    • ensure that non-finite dimension.values are handled properly
    • ensure that *range arrays only contain finite numbers
    • do the right thing in special cases (0, 1, 2, 63, 64, 65 dimensions specified)
    • do the right thing in special cases (0 or 1 lines specified)
    • use default color or singular color if user hasn't supplied a line or line.colorscale attribute
  • Knife superfluous stying attributes that are still present (PR feedback: Étienne)
  • Delete the devtesting directory before merge
  • Raising a hover event
  • Add a toSVG method like this (PR feedback: Étienne)
  • Rework test cases to avoid Plotly.plot calls
  • Change cursor filter brush and axis drag shapes as per Alex' feedback

In subsequent PRs (general expectations or gathered during PR discussion):

  • Vertical / slanted text orientation, or other way of avoiding overlap - parcoords supports up to 64 dimensions and dimension names can be of arbitrary length, so not as trivial as it sounds (also a PR feedback by Étienne)
  • Solve the image mock testing issue that now prevents image generation even locally
  • Add a 'Reset All' mode bar interaction (PR feedback: Étienne, Alex)

Discussed future steps:

  • (Optional) Consider a per-dimension Reset interaction hot spot near the dimension axis label on top (PR feedback: Alex)
  • Later: unify code with the Cartesian axis, or at least much of the logic, for later support of ordinal/nominal, date types and Plotly standard axis attributes; also consider cursor shapes (PR feedback: Alex)
  • Canvas pixelRatio configurability
  • Consider serialization of the crossfilter-ability; write up thoughts (asked by Jack)
  • Example: Plotly animations with parcoords (and other cross-cutting functionality e.g. transforms)
  • Example: Plotly transforms (filters, groups) with parcoords
  • Add text labels for ordinal dimensions
  • add some logic to autocalculate line thickness / alpha in function of line count

@monfera monfera force-pushed the parcoords-squashed-2 branch 6 times, most recently from 01c52f4 to e1498e4 Compare December 15, 2016 19:33
@@ -62,6 +62,8 @@ function menuDefaults(menuIn, menuOut, layoutOut) {
coerce('borderwidth');
}

// todo consider unifying it with other similar array coercions
// - though `Array.isArray(buttonIn.args)` is quite spedific...
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 Author

Choose a reason for hiding this comment

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

🔪'd

role: 'info',
description: 'The shown name of the dimension.'
},
integer: {
Copy link
Contributor

Choose a reason for hiding this comment

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

what is this?

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 integer variables, there's

  1. No fractional ticks (e.g. there's no 1.5 tick between 1 and 2
  2. Brushing snaps to the integer value (see below)

integer snapping

The reason is that, as per client discussion, in the 1st iteration we'll have no support for ordinal and nominal dimensions, i.e. such user dimensions will map to integers. But going forward, we should do it with some unified (full-featured Plotly) axis approach, with proper ordinal, nominal, date etc. support.

Copy link
Contributor

Choose a reason for hiding this comment

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

OK. But a boolean integer can't possibly cover the ordinal / nominal and date axis type?

Something like x or position if you tell that vertical parcood is a thing (which would default to 0,1,2,....) sounds like the way to go here?

Copy link
Contributor Author

@monfera monfera Dec 15, 2016

Choose a reason for hiding this comment

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

Correct, a boolean integer won't cover these axis types. I believe we shouldn't keep it in the API - it's just a placeholder for a preview of the functionality, not a final API element. I'll try to change it such that it follows established denotation of the alternative axis types.

Re x or position: I don't know how it's related to axis type, we don't currently have support for vertical parcoord (i.e. all of our parcoords axes are currently vertical, and the panels are laid out horizontally). The integer thing is merely an indication if the axis is integer (see that on Cylinder material and Block material there are no fractional ticks though there would be room for it; and that the magenta axis brushing bar 'snaps' to the integer values).

}
),

lines: {
Copy link
Contributor

@etpinard etpinard Dec 15, 2016

Choose a reason for hiding this comment

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

a line and lines attribute container? No thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

lines is the old stuff, I'm just not fully clear on whether you want to keep some of what's in there, which is why I haven't yet removed it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please suggest if I should unexpose lines, padding and filterbar for the time being, or if not, I'll put attributes still in lines into line, or resolve it some other way you deem best.

dimensionIn = dimensionsIn[i];
dimensionOut = {};

if(!Lib.isPlainObject(dimensionIn) || !Array.isArray(dimensionIn.values)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

probably best to keep data[i].dimensions.length === fullData[i].dimenstions.length for restyle purposes.

Non plain-object dimensions items and items with values.length === 0, should be skipped during the plot only.

Copy link
Contributor

Choose a reason for hiding this comment

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

++ I'd vote for adding a dimenstions[i].visible boolean to easily toggle dimensions visibility.

Copy link
Collaborator

Choose a reason for hiding this comment

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

++ I'd vote for adding a dimensions[i].visible boolean to easily toggle dimensions visibility.

How would this work in the UI? ie how would you get a dimension back after hiding it? Some dropdown with hidden dimensions in it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@alexcjohnson having caught up on this item with Étienne, the addition of a UI interaction was deferred for now. I created a visible attribute in the meantime to have API control.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just summing up the current status of this feedback item - as code is right now -, which fell this way in part due to preexisting code regularity, but I'm glad to iterate, as there are some mismatches to the feedback, I just want to make sure work toward it is deemed valuable:

  • The dimensions[i].visible attribute has been added.

  • Each dimension has an id (defaults to the label if unsupplied) - on Plotly.restyle, dimensions of matching id are updated; dimensions with a new id are added. My pre-feedback assumption was that the user can add dimensions incrementally.

  • Currently, there's no special handling for values.length === 0 however the line count is determined as the minimum of values.length across dimensions, so in this case, no line gets rendered. I like the idea to handle values.length === 0 as a special case, to implicitly mean that the dimension be removed. Also, the code already skips non-isPlainObject dimensions and dimensions whose values isn't an array which I think is what you meant; I'll reify both with a test case shortly.

Copy link
Contributor

Choose a reason for hiding this comment

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

Each dimension has an id (defaults to the label if unsupplied) - on Plotly.restyle, dimensions of matching id are updated; dimensions with a new id are added. My pre-feedback assumption was that the user can add dimensions incrementally

No need to use id if data[i].dimensions.length === fullData[i].dimenstions.length, visible: false means that particular dimension is hidden (not deleted).

Currently, there's no special handling for values.length === 0

dimensions with values.length === 0 should be set to have visible: false

* LICENSE file in the root directory of this source tree.
*/
'use strict';
var jet = [[0, 0, 255], [0, 8, 255], [0, 35, 255], [0, 77, 255], [0, 126, 255], [0, 175, 255], [0, 217, 255], [0, 245, 255], [0, 254, 255], [0, 255, 245], [0, 255, 218], [0, 255, 176], [0, 255, 128], [0, 255, 78], [0, 255, 36], [0, 255, 8], [0, 255, 0], [8, 255, 0], [36, 255, 0], [77, 255, 0], [127, 255, 0], [176, 255, 0], [217, 255, 0], [245, 255, 0], [254, 255, 0], [255, 245, 0], [255, 217, 0], [255, 176, 0], [255, 127, 0], [255, 77, 0], [255, 36, 0], [255, 8, 0], [255, 0, 0]
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 Author

@monfera monfera Dec 15, 2016

Choose a reason for hiding this comment

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

This is in the devtesting directory. It's not used and the entire directory will be deleted. Our example uses "Jet" or whichever colorscale you want, as usual with other plots. This was one of your main feedback items. The reason for devtesting is that it avoided me spending 10-15 seconds on each rebuild, letting me get a refreshed view in 1-2 seconds. I'm locally using budo for this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Wait. You've been dev'ing in plotly.js w/o npm start or npm run watch?

Ok, sure. But next time, don't commit local-development only files.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got it! Glad to remove it ASAP, together with several other changes you asked. FWIW I continuously had a watch as well, and refreshed the plotly generated test plot, but slightly less frequently. BIG time saver.

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: removed devtesting altogether!

Parcoords.moduleType = 'trace';
Parcoords.name = 'parcoords';
Parcoords.basePlotModule = require('./base_plot');
Parcoords.categories = ['parcoords', 'lineColorscale'];
Copy link
Contributor

Choose a reason for hiding this comment

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

should be

Parcoords.categories = ['gl'];

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

resolution: [canvasWidth, canvasHeight],
viewBoxPosition: [x + overdrag, 0],
viewBoxSize: [panelSizeX, canvasPanelSizeY],
var1A: utils.range(16).map(function(d) {return d === i ? 1 : 0;}),
Copy link
Contributor

Choose a reason for hiding this comment

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

a good old for-loop here should be faster.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, see the code comments above this piece.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

... now the 16 map calls are replaced with one common loop.


varying vec4 fragColor;

void main() {
Copy link
Contributor

Choose a reason for hiding this comment

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

all glsl files should be in a new folder src/traces/parcoords/shaders/

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

@@ -0,0 +1,137 @@
/**
Copy link
Contributor

Choose a reason for hiding this comment

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

naming modules like these helpers.js would be more inline with the rest of the repo.

Copy link
Contributor Author

@monfera monfera Dec 15, 2016

Choose a reason for hiding this comment

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

Phasing out this file altogether is on my task list (see todo code comment on top of the file) - it was merely put in to have no plotly utils dependency while iterative development went on locally with budo (for above mentioned fast refreshes).

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 - utils.js is now removed!

@monfera
Copy link
Contributor Author

monfera commented Dec 15, 2016

@etpinard thanks for your fantastic, timely feedback! Just pushed the low hanging fruit items. Working on the rest one by one. There are only a couple items above where I could use some direction.

package.json Outdated
@@ -103,6 +104,7 @@
"fs-extra": "^1.0.0",
"fuse.js": "^2.2.0",
"glob": "^7.0.0",
"glslify": "^4.0.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

we'll need to add a transforms field to the package.json so users can bundle plotly.js w/o having to worry about -t glslify

Copy link
Contributor Author

@monfera monfera Jan 4, 2017

Choose a reason for hiding this comment

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

@etpinard UPDATE: I added this to package.json to permit npm install with glslify present:

  "browserify": {
    "transform": ["glslify"]
  }

=============================================================
I made a custom build with npm run build for testing purposes (http://codepen.io/monfera/pen/ObrRGP) and it worked fine, the transforms were added here and here. Please let me know if it's insufficient, and in this case, how should I go about testing whether the package.json change was successful or not, because the build seems good as it is.

Perhaps you're referring to users who just put plotly.js in as one of their dependencies in their package.json and install their system with npm install, instead of using a bundle? Should I test for this?

@alexcjohnson
Copy link
Collaborator

@monfera this is looking really slick! Just a comment about the interactions: I didn't find resetting a single constraint (click in the unselected portion of its axis) to be particularly discoverable. I notice there's a "zoom out" cursor that most browsers support, can we use that when it's available? And perhaps also reset that axis when you double-click on the selection bar, akin to how our cartesian interaction works?

Also for parity with cartesian, can we make the cursor on the bottom s-resize, on the top n-resize, and in the middle ns-resize? And ew-resize for reordering axes?

@monfera
Copy link
Contributor Author

monfera commented Dec 16, 2016

@alexcjohnson yes there's plans to add a Reset All interaction to the mode bar, and perhaps per-dimension Reset interactions near the axis titles. What do you mean by the use of the zoom out cursor? That this cursor gets activated if the filter is active and the user hovers over the non-filtered part, on which a click removes the filter? Btw. this part serves not only a reset function but also a 'start new selection' interaction.

I'll look into the cursor changes you've suggested. Also, there hasn't been work toward unifying with Cartesian axes, so a bunch of things may become closer in API and behavior as well.

@alexcjohnson
Copy link
Collaborator

What do you mean by the use of the zoom out cursor? That this cursor gets activated if the filter is active and the user hovers over the non-filtered part, on which a click removes the filter?

yes, that's what I had in mind. But...

Btw. this part serves not only a reset function but also a 'start new selection' interaction.

Ah, I hadn't discovered that at all, but it makes sense. That complicates it a little though... might be even harder to discover that you can start a new selection this way if the cursor becomes "zoom-out". Alright, so perhaps lets leave this cursor as it is at least for now. I suppose if we do add a reset button to each axis we might get rid of reset on an off-selection click... though there's something nice about having it all together as you have now if we can make it intuitive enough.

I'll look into the cursor changes you've suggested.

Cool, I think the other ones I suggested still stand.

Also, there hasn't been work toward unifying with Cartesian axes, so a bunch of things may become closer in API and behavior as well.

Your call to what extent to do this - as this comes together it's seeming to me that the interactions and drawing are sufficiently different that you shouldn't try to reuse cartesian for these parts, just mimic its feel to the extent you can. But there are likely still benefits to using the cartesian code and attributes for tick selection & formatting and using cartesian attributes for some other style aspects.

@monfera
Copy link
Contributor Author

monfera commented Dec 19, 2016

@etpinard could you please offer feedback on the planned plotly event emission?
monfera@bec31aa

@monfera monfera self-assigned this Dec 20, 2016
@monfera monfera force-pushed the parcoords-squashed-2 branch from 188d7db to f350521 Compare December 21, 2016 00:34
@monfera
Copy link
Contributor Author

monfera commented Dec 28, 2016

@etpinard may I just update gd.data in the callback directly, and emit a plotly_restyle event, without ever calling Plotly.restyle? It's quite slow (UI hangs for a second) and I ran into other troubles (below), so I'd need a decision before proceeding.


I dismissed the string based Plotly.restyle, because I wanted to avoid multiple API calls (passing on an array to 'dimensions[' + i + '].constraintrange' didn't work):

var filterChangedCallback = function(value) {
    var range = value.changedDimension.domainFilter;
    var i = value.changedDimension.index;
    Plotly.restyle(gd, 'dimensions[' + i + '].constraintrange[0]', range[0]).then(function() {
        Plotly.restyle(gd, 'dimensions[' + i + '].constraintrange[1]', range[1]);
    });
};

Then I tried the object form of Plotly.restyle, but for some reason, it worked once, but then gd.data became deteriorated: the dimensions array somehow got assigned to element 0 of the original dimensions array and failed. If this kind of issue rings a bell, I'd be glad to learn. The only special thing about dimensions is that it's an array and uses _isLinkedToArray in attributes.js as you told. In any case it causes UI hang on each mouseup.

var newData = Lib.extendDeep(gd.data)[modelIndex];
newData.dimensions[i].constraintrange[0] = range[0];
newData.dimensions[i].constraintrange[1] = range[1];
Plotly.restyle(gd, newData, modelIndex);

Simply reinvoking Plotly.plot with the added or changed dimensions[i].constraintrange values worked fine, but I guess you don't want me to finalize this solution because it's not a restyle event, and also causes a brief UI hang.

newData = Lib.extendDeep(gd.data);
newData[modelIndex].dimensions[i].constraintrange = range.slice();
Plotly.plot(gd, newData);

@alexcjohnson
Copy link
Collaborator

@monfera

may I just update gd.data in the callback directly, and emit a plotly_restyle event, without ever calling Plotly.restyle?

It's important for extensibility that the Plotly.restyle (-> Plotly.update, not quite sure the status of that) calls work as expected. So I suppose it would be fine to not use it during regular interactions if there's a perf benefit from skirting it, but when a user does call it, it needs to work. At some point we will rearchitect that whole system so the trace modules can control what happens in a restyle... and some modules do already short-circuit the regular machinery there (eg annotations, though that's in relayout).

passing on an array to 'dimensions[' + i + '].constraintrange' didn't work

Then I tried the object form of Plotly.restyle, but for some reason, it worked once, but then gd.data became deteriorated

Not quite sure what's going on here, but one thing to be aware of is that restyling with an array argument you need to wrap it in another (length-1) array to distinguish from the case of restyling multiple traces with different values.

@monfera
Copy link
Contributor Author

monfera commented Dec 28, 2016

@alexcjohnson thanks for answering!

restyling with an array argument you need to wrap it in another

In this case, the above newData is an object, so I think the issue of an array argument didn't come into play. Plotly.restyle signals error if the second argument is an array, or an array of an array.

In line with your input, I'm planning to pursue two separate tracks:

  1. Ensure that Plotly.restyle doesn't mangle gd.data - there is a test case for it which passes (correctly retains the dimensions as an array) so there might be some problem in the circumstances, I'll check.

  2. Achieve the goal of updating gd.data for serialization directly, and emit an event, avoiding the heavier weight Plotly.restyle, or, if there are overriding reasons for calling Plotly.restyle, then perf optimizing the parcoords specific processing such that UI hang is minimized.

@monfera monfera force-pushed the parcoords-squashed-2 branch from 3e192e0 to edc2710 Compare January 4, 2017 16:57
@monfera
Copy link
Contributor Author

monfera commented Jan 10, 2017

@etpinard regarding a not yet discussed part of a comment above, maybe you've seen it before:

I'm running into an issue with the dimensions attribute on Plotly.restyle . Perhaps because it's _isLinkedToArray. When I call Plotly.restyle(gd, {dimensions: [.....], ...}) then newData arriving to defaults.js doesn't receive the full dimensions array; it only receives the first array element.

So I expect that data[0].dimensions === [dim1, dim2, .. dimN] and I get data[0].dimensions === dim1 in defaults.js after a restyle call.

Glad to investigate if it doesn't ring a bell.

};
}

var orig = sorter(gdDimensionsOriginalOrder[i].filter(function(d) {return d.visible === void(0) || d.visible;}));
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe the logic is a little off here when visible: false traces are present.

For example:

  • Plot the gl2d_parcoords mock
  • call Plotly.restyle(gd, 'dimenstion[0].visible', false)
  • swap the Cylinder material dimension and Block width dimensions

gifrecord_2017-02-09_115308

  • then gd.data[0].dimensions[0] is the Cylinder material dimension and the original gd.data[0].dimensions[0] container (with id Block height) is now (strangely 😕 ) in dimensions[9].
  • so then if a user tries to toggle the Block height dimension back in with Plotly.restyle(gd, 'dimensions[0].visible', true), the graph remain unchanged.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes this is how it works. It's hard to interpret the location of something that isn't visible, esp. after sorting the (visible) axes. For this reason, everything that's invisible gets sorted to the end by the axis drag&drop logic. I believe it shouldn't pose a problem because there's a generated Plotly.restyle upon axis drag&drop and the dimensions are on gd.data in a sorted manner. It was discussed it with a client at the time, motivated by the above challenges (i.e. what's the location of an invisible dimension after other dimensions get manually repositioned in the neighborhood).

Copy link
Contributor

Choose a reason for hiding this comment

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

I disagree. I think visible: false dimensions should not be moved when other dimensions are swapped on drag.

In the above example, I'd expect the original dimensions[0] to remain there after the drag event and that the Cylinder material dimension and Block width dimensions get swapped between dimensions[1] and dimensions[2].

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'm glad to change it, so the new logic would be such that whatever is invisible always retains its place, e.g. if we have [dim[0], dim[1], dim[2]] and dim[1] is invisible, and the user moves dim[0] to the right, then the new state is [dim[2], dim[1], dim[0]].

Copy link
Contributor

Choose a reason for hiding this comment

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

Correct. In other words, visible: false dimensions cannot be moved on drag events.

Copy link
Contributor

Choose a reason for hiding this comment

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

Looking good now ✅

.append('canvas')
.attr('class', function(d) {return 'parcoords-lines ' + (d.context ? 'context' : d.pick ? 'pick' : 'focus');})
.style('box-sizing', 'content-box')
.style('transform', 'translate(' + (-c.overdrag) + 'px, 0)')
Copy link
Contributor

@etpinard etpinard Feb 9, 2017

Choose a reason for hiding this comment

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

This here is incompatible with the current master since commit 4aa1a45 where @alexcjohnson made the test dashboard catch negative translate values which can cause issues in some browsers (e.g. I.E.)

Trying to plot any of the gl2d_parcoords_* mock in the test dashboards leads to:

image

Copy link
Contributor

Choose a reason for hiding this comment

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

(Neglect deleted comment in case notification shows up. Missed that this was an html element vs. an svg element)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @etpinard I now rebased, and even the mock baseline failed until the transform got redone to avoid the negative value.

@monfera
Copy link
Contributor Author

monfera commented Feb 14, 2017

@etienne with these pushed commits above, the outstanding PR feedback items have been addressed.


if(actuallyVisible) {
var values = coerce('values');
var visible = coerce('visible', values.length > 0);
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks!


function canvasToImage() {
var canvas = this;
var rect = canvas.getBoundingClientRect();
Copy link
Contributor

Choose a reason for hiding this comment

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

We should try to remove these expensive getBoundingClientRect calls and replace them with the appropriate domain + layout.width / layout.height formulae.

I believe these might be the reason behind the issues with autosize: true image exports.


@monfera This is now the last remaining 🚫 item. 🎉 🎉 🎉

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
Collaborator

Choose a reason for hiding this comment

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

This is a good point: we should definitely minimize our use of getBoundingClientRect, and it might be worthwhile to look through the places we use it and see which ones we can remove and which others we may need to alter.

The one you point to in drawing @rreusser is a case we really do need it - there's no other way AFAIK to get the dimensions and positioning of text, but it's important HOW it's invoked: on a test element attached directly to <body>, ie not part of gd. That way we can guarantee that it's attached to the DOM and displayed, which is not always the case with gd itself when the plot is first rendered - imagine creating a bunch of plots in different tabs of your app, most of which are hidden initially.

I think we can say that during interactions getBoundingClientRect is safe on elements within gd (it has to be visible to be interacted with, right? I guess in principle not if an app developer triggers something like a hover effect manually...) but a quick search shows we're probably using it in unsafe ways during initial rendering.

Copy link
Contributor

@rreusser rreusser Feb 16, 2017

Choose a reason for hiding this comment

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

There is canvas.measureText that I think might measure without forcing a draw (which I think is the reason getBoundingClientRect is slow right? It's one of those getters that triggers a draw?).

Aaaaand survey says: wrong? Looking at some references, I see reference to a jsperf that indicates they're about the same speed (unfortunately jsperf seems not to exist anymore, so… no link). So probably nothing to do here for now beyond being aware of generally minimizing usage.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Just to be clear: I was making more than a performance point. getBoundingClientRect will actually fail if an element is not displayed, so it can actually be an error, not just potentially slow.

gd.style.display='none'
gd.getBoundingClientRect()
> ClientRect {top: 0, right: 0, bottom: 0, left: 0, width: 0}
gd.style.display=''
gd.getBoundingClientRect()
> ClientRect {top: 80, right: 1100, bottom: 580, left: 0, width: 1100}

@etpinard etpinard added this to the 1.24.0 milestone Feb 16, 2017
@monfera
Copy link
Contributor Author

monfera commented Feb 20, 2017

@etpinard Got the root cause for the outstanding snapshot bug: to_image sets clonedGd.style.position = 'absolute'; then plots.js gets the computed width of the div which, when position is absolute, reports a different value than what was reported on the original div for the screen rendering.

Looking at the screen printing behavior on other plots I guess that the intention is to generate a fixed, e.g. 700px wide snapshot even if the user exploits the full width of the browser window, for example, by specifying autosize: true or not specifying a width. (position: absolute has other purposes too - letting the div move outside the visible part of the document - so I'm not 100% sure of the intent part) I.e. by default, users end up with screenshots that are different to the size and aspect ratio they currently see on the screen (I suppose there's pros and cons and there was a convention or tiebreaker reason, I'd be glad to learn).

If the above is correct, then the WebGL layers (line layers) must be rerendered in toSVG with the changed width value. It's doable: either new WebGLRenderingContext instantiations happen, risking that the user runs out of contexts, or the rerendering should be done in the preexisting contexts (I'd prefer this), in which case there might be a minute visible flash when screenshotting.

I'm going ahead with the latter approach unless you have some other preference.

@etpinard
Copy link
Contributor

Looking at the screen printing behavior on other plots I guess that the intention is to generate a fixed, e.g. 700px wide snapshot even if the user exploits the full width of the browser window,

Not quite. Plotly.toImage has width / height parameters that allows users to specify the snapshot dimensions.

As I was trying to explain in #1256 (comment), base plot toSVG should not rely on the DOM nodes' dimensions to get the correct snapshot size. Instead, toSVG should rely on the parcoords domain and layout width / height for this task. Here's what gl3d does.


});

it('Should emit a \'plotly_hover\' event', function(done) {
Copy link
Contributor

Choose a reason for hiding this comment

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

@monfera this test is failing locally on my machine:

image

in both Chrome and FF.

Copy link
Contributor

Choose a reason for hiding this comment

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

This one still fails on my machine. Not sure what's up.

I'll try to debug this tomorrow morning.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe somebody else could pull down this branch and run npm run test-jasmine -- tests/parcoords_test.js to check?

Copy link
Contributor

Choose a reason for hiding this comment

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

@monfera after some investigation, the mouseover event fires on hover, but found here is false. Any idea about why that can happen?

Copy link
Contributor

@etpinard etpinard Feb 23, 2017

Choose a reason for hiding this comment

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

@monfera this patch 1400e0f makes the parcoords test pass on my machine. Can you try it on yours?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes I'll try, thanks! Stepping out now in the evening, will check later. Re the question, mouseover fires on the entire surface of the canvas; it's after reading out the pick pixels when there's a decision if a line was found or not.

Parcoords.meta = {
description: [
'Parallel coordinates for multidimensional exploratory data analysis.',
'The samples are in `data`.',
Copy link
Contributor

@etpinard etpinard Feb 21, 2017

Choose a reason for hiding this comment

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

should be 'The samples are in dimensions.', no?


"dimensions": [
{
"id": "Block height",
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 for all those id fields in the mocks!

@monfera monfera force-pushed the parcoords-squashed-2 branch from b63ef4e to 7005671 Compare February 22, 2017 13:19
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.

Amazing work 💃

@monfera I'll let the honours of merging this 💪 PR.

@monfera
Copy link
Contributor Author

monfera commented Feb 23, 2017

Thanks @etpinard for the thorough and patient review process, and thanks @alexcjohnson and @rreusser for your comments as well!

@monfera monfera merged commit 85ca2b2 into plotly:master Feb 23, 2017
@monfera monfera deleted the parcoords-squashed-2 branch February 23, 2017 21:17
function makeItem(i, ii, x, y, panelSizeX, canvasPanelSizeY, crossfilterDimensionIndex, scatter, I, leftmost, rightmost) {
var loHi, abcd, d, index;
var leftRight = [i, ii];
var filterEpsilon = verticalPadding / canvasPanelSizeY;
Copy link
Collaborator

Choose a reason for hiding this comment

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

It doesn't look like this was ever discussed during the original PR. In order to make it so constraint ranges are honored as precisely as possible I'm planning to remove this epsilon. Or at least make it very small so it only catches floating point rounding errors, though provisionally it doesn't even look like I need to do that, if we're careful about transforming data and constraints identically. @monfera @etpinard any concerns if I do that? It doesn't really matter if you're setting these ranges interactively and just exploring the data, but if you set constraintrange: [901, 999] we really should exclude 900 and 1000. Multiselect cannot be as precise of course, I'm just talking about single-range selections here.

Copy link
Contributor

Choose a reason for hiding this comment

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

any concerns if I do that?

None from me 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature something new
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants