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

Parallel Categories trace type for multi dimensional categorical data #1

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1a8a0f0
Initial parcats trace implementation
Nov 27, 2017
406cd24
color attribute fixes for rebase on 1.39.3
jonmmease Jul 27, 2018
ee7fd17
lint fixes
jonmmease Jul 27, 2018
3e43ef3
Remove customHovers, replace with loneHover
jonmmease Aug 7, 2018
0c9c75f
Bring back customHovers
jonmmease Aug 7, 2018
7b75100
Renamed `parcats.marker` -> `parcats.line`
jonmmease Aug 7, 2018
d2c5ae8
Use plots/domain and handleDomainDefaults
jonmmease Aug 8, 2018
a34dafa
Rename displayInd -> displayindex
jonmmease Aug 8, 2018
71e212b
Convert to simplified colorbar logic
jonmmease Aug 8, 2018
6797a83
Remove maxDimensionCount check
jonmmease Aug 9, 2018
2c14168
Cleanup supplyDefaults and add visible dimension property
jonmmease Aug 9, 2018
c8e3cc9
Added support for dimensions with visible=false
jonmmease Aug 10, 2018
6a5c20e
Fixed failing test (needed to rename displayInd -> displayindex)
jonmmease Aug 10, 2018
22346d0
Added mock with color hovermode
jonmmease Aug 10, 2018
3680084
Replace tooltip with hoverinfo
Aug 15, 2018
2d07f4d
Added `arrangement` property that is very similar to the sankey trace
Aug 15, 2018
7f90fc1
WIP towards categoryorder/categoryarray/categorylabels
Aug 16, 2018
5e60062
Full support for categoryorder, categoryarray, and categorylabels
Aug 17, 2018
66c90fa
Fixed tests to use new `categoryorder`, `categoryarray`, `categorylab…
Aug 20, 2018
69f6922
Add 'dimension' hovermode that uses multi-hoverlabel logic
Aug 20, 2018
f2aa9b9
Added labelfont and categorylabelfont top-level attributes
Aug 21, 2018
4eb5317
Review / cleanup attribute descriptions
Aug 21, 2018
a897388
Add `counts` attribute to parcats_hovermode_dimension mock
Aug 21, 2018
98d76ee
Refactor dimension and category dragging tests and test arrangements
Aug 23, 2018
66f21fe
Add tests for clicking on category and path with/without hoverinfo skip
Aug 23, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Plotly.register([
require('./pointcloud'),
require('./heatmapgl'),
require('./parcoords'),

require('./parcats'),
require('./scattermapbox'),

require('./sankey'),
Expand Down
11 changes: 11 additions & 0 deletions lib/parcats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Copyright 2012-2017, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

module.exports = require('../src/traces/parcats');
76 changes: 76 additions & 0 deletions src/components/fx/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,82 @@ exports.loneHover = function loneHover(hoverItem, opts) {
return hoverLabel.node();
};

// TODO: replace loneHover?

Choose a reason for hiding this comment

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

Interesting... can you say more? Is this just an extension of loneHover to support multiple labels or does it add something else? I only see one label at a time when I play with parcats.

Copy link
Owner Author

Choose a reason for hiding this comment

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

At one point I had added a mode that would display a separate tooltip for each color of the category you hover over. It got pretty unwieldy and I reverted back to loneHover. I'll remove this customHover function.

Choose a reason for hiding this comment

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

Unwieldy in the code or on screen? We have some upcoming sankey enhancements that may call for a very similar multi-label hover effect, though the details still need to be worked out. But if the code is clean and it was just not looking good in practice, it still may be worth keeping this around.

Copy link
Owner Author

Choose a reason for hiding this comment

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

On the screen. I think the implementation worked well enough. I'll look over it again and write a better explanation of what it does 🙂

exports.customHovers = function customHovers(hoverItems, opts) {

if(!Array.isArray(hoverItems)) {
hoverItems = [hoverItems];
}

var pointsData = hoverItems.map(function(hoverItem) {
return {
color: hoverItem.color || Color.defaultLine,
x0: hoverItem.x0 || hoverItem.x || 0,
x1: hoverItem.x1 || hoverItem.x || 0,
y0: hoverItem.y0 || hoverItem.y || 0,
y1: hoverItem.y1 || hoverItem.y || 0,
xLabel: hoverItem.xLabel,
yLabel: hoverItem.yLabel,
zLabel: hoverItem.zLabel,
text: hoverItem.text,
name: hoverItem.name,
idealAlign: hoverItem.idealAlign,

// optional extra bits of styling
borderColor: hoverItem.borderColor,
fontFamily: hoverItem.fontFamily,
fontSize: hoverItem.fontSize,
fontColor: hoverItem.fontColor,

// filler to make createHoverText happy
trace: {
index: 0,
hoverinfo: ''
},
xa: {_offset: 0},
ya: {_offset: 0},
index: 0
};
});


var container3 = d3.select(opts.container),
outerContainer3 = opts.outerContainer ?
d3.select(opts.outerContainer) : container3;

var fullOpts = {
hovermode: 'closest',
rotateLabels: false,
bgColor: opts.bgColor || Color.background,
container: container3,
outerContainer: outerContainer3
};

var hoverLabel = createHoverText(pointsData, fullOpts, opts.gd);

// Fix vertical overlap
var tooltipSpacing = 5;
var lastBottomY = 0;
hoverLabel
.sort(function(a, b) {return a.y0 - b.y0;})
.each(function(d) {
var topY = d.y0 - d.by / 2;

if((topY - tooltipSpacing) < lastBottomY) {
d.offset = (lastBottomY - topY) + tooltipSpacing;
} else {
d.offset = 0;
}

lastBottomY = topY + d.by + d.offset;
});


alignHoverText(hoverLabel, fullOpts.rotateLabels);

return hoverLabel.node();
};

// The actual implementation is here:
function _hover(gd, evt, subplot, noHoverEvent) {
if(!subplot) subplot = 'xy';
Expand Down
1 change: 1 addition & 0 deletions src/components/fx/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ module.exports = {
unhover: dragElement.unhover,

loneHover: require('./hover').loneHover,
customHovers: require('./hover').customHovers,
loneUnhover: loneUnhover,

click: require('./click')
Expand Down
182 changes: 182 additions & 0 deletions src/traces/parcats/attributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/**
* Copyright 2012-2017, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var extendFlat = require('../../lib/extend').extendFlat;
var colorAttributes = require('../../components/colorscale/attributes');

var scatterAttrs = require('../scatter/attributes');
var scatterMarkerAttrs = scatterAttrs.marker;
var colorbarAttrs = require('../../components/colorbar/attributes');

var marker = extendFlat({

Choose a reason for hiding this comment

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

This seems more like line to me, and that would also be consistent with parcoords.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Sounds good. I think I started out calling it marker because I thought it might be nice to style the outline of the paths, and to call this line. But I don't think that really makes sense anymore. I'll change in to line.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done in 7b75100

editType: 'calc'
}, colorAttributes('marker', {editType: 'calc'}),

Choose a reason for hiding this comment

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

If we at some point want to support selections similar to how parcoords works, perhaps we want Viridis here too? How well does this work with continuous color values, does it attempt to sort in color order or does it just keep distinct values together? Also I don't see a mock with a colorbar and in your brushing example it looks like you just use two distinct colors - does it support providing an array of color strings or just numeric values? Actually I'm not sure if parcoords supports strings...

Anyway if you're just using color as a marker of selected/deselected it seems a bit hacky to have to create a colorscale with the two colors you want. Perhaps instead (or in addition) we should use selectedpoints and add selected/unselected styling, with the default for unselected being a grey similar to parcoords and your brushing example.

Copy link
Owner Author

Choose a reason for hiding this comment

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

A path (thick line) is created for each unique combination of the dimension categories and raw color (the value as specified in marker.color). The relative height of the path is determined by the number of observations the path represents.

The display color of the path is computed based on the path's unique raw color value. If no colorscale is present, then the raw color is the display color (So yes, you can pass an array of color strings to marker.color). If a colorscale is present and the raw color is numeric then it is mapped to a display color using the usual scaling process.

So continuous colorscales are fine as long as the numeric color values are somewhat discrete. If there are 1000 unique values in the color array, then the diagram will sort of degenerate into a bunch of tiny paths. In my own analysis work I've either used a categorical colormap with an integer color array (this is the brushing scenario), or used a continuous colormap and manually discretized the color values. In both cases I would generally stay under 10 discrete colors.

The selection metaphor here is interesting. So far I'm just emitting the plotly_click and hover events and then executing selection like logic (highlighting clicked paths) from the outside.

{
showscale: scatterMarkerAttrs.showscale,
colorbar: colorbarAttrs,
shape: {
valType: 'enumerated',
values: ['straight', 'curved'],
dflt: 'curved',

Choose a reason for hiding this comment

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

Perhaps to match scatter we should have 'linear' and 'spline'? Though this curve is really a different thing, as it's constrained to be horizontal at both ends - an option we've discussed adding to scatter lines in fact - (related plotly#993 talks about monotone splines, which is yet another variant) ... but if we can decide on a name this should have in scatter, it would be good to use the same name here. What about 'hspline' for "spline with horizontal ends" (and we'd also add 'vspline' to scatter, though it wouldn't be added here unless we make a vertical orientation option for this trace type.)

Also, I wonder if the default should be 'linear'? Curved matches sankey, but straight matches parcoords and I think it actually makes connections a bit easier to follow - which may be why the existing implementations you showed use straight connections.

The other visual changes you made vs the existing implementations, which seem to come from sankey - a bar of color instead of a black line, a halo around the category labels - are clear improvements!

Copy link
Owner Author

Choose a reason for hiding this comment

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

'linear' + 'hspline' with 'linear' as default sounds good to me!

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done in 7b75100

role: 'info',
editType: 'plot',
description: 'Sets the shape of the paths'},
});

module.exports = {
domain: {

Choose a reason for hiding this comment

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

Please use plots/domain

Choose a reason for hiding this comment

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

... and correespondingly handleDomainDefaults in the defaults step. The reason for this, aside from 🌴, is to support layout.grid plotly#2399

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done in d2c5ae8

x: {
valType: 'info_array',
role: 'info',
items: [
{valType: 'number', min: 0, max: 1, editType: 'calc'},
{valType: 'number', min: 0, max: 1, editType: 'calc'}
],
dflt: [0, 1],
editType: 'calc',
description: [
'Sets the horizontal domain of this `parcats` trace',
'(in plot fraction).'
].join(' ')
},
y: {
valType: 'info_array',
role: 'info',
items: [
{valType: 'number', min: 0, max: 1, editType: 'calc'},
{valType: 'number', min: 0, max: 1, editType: 'calc'}
],
dflt: [0, 1],
editType: 'calc',
description: [
'Sets the vertical domain of this `parcats` trace',
'(in plot fraction).'
].join(' ')
},
editType: 'calc'
},

tooltip: {

Choose a reason for hiding this comment

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

this should be handled by hoverinfo

Copy link
Owner Author

Choose a reason for hiding this comment

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

Sounds good. I'll work through the comparison with Sankey, but I'm thinking the flaglist would be a combination of 'count' and/or 'probability', or 'none' to disable it all together.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done in 3680084

tooltip is gone and hoverinfo is added. As usual, hoverinfo can be one of skip, none, or all. Or a combination of count and probability.

Having the skip hoverinfo value also removed the need for the none hovermode enumeration value.

valType: 'boolean',
dflt: true,
role: 'info',
editType: 'plot',
description: 'Shows a tooltip when hover mode is `category` or `color`.'
},

hovermode: {
valType: 'enumerated',
values: ['none', 'category', 'color'],
dflt: 'category',
role: 'info',
editType: 'plot',
description: 'Sets the hover mode of the parcats diagram'
},
bundlecolors: {
valType: 'boolean',
dflt: true,
role: 'info',
editType: 'plot',
description: 'Sort paths so that like colors are bundled together'
},
sortpaths: {
valType: 'enumerated',
values: ['forward', 'backward'],
dflt: 'forward',
role: 'info',
editType: 'plot',
description: [
'If `forward` then sort paths based on dimensions from left to right.',
'If `backward` sort based on dimensions from right to left.'
].join(' ')
},
// labelfont: fontAttrs({
// editType: 'calc',
// description: 'Sets the font for the `dimension` labels.'
// }),
//
// catfont: fontAttrs({
// editType: 'calc',
// description: 'Sets the font for the `category` labels.'
// }),

dimensions: {
_isLinkedToArray: 'dimension',
label: {
valType: 'string',
role: 'info',
editType: 'calc',
description: 'The shown name of the dimension.'
},
catDisplayInds: {

Choose a reason for hiding this comment

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

(here and below: lowercase please)

I'm thinking categoryorder and categoryarray like we have for cartesian axes where you could initially specify one of the sort orders 'trace' | 'category ascending' | 'category descending' for categoryorder but as soon as the user moves things around it would switch to 'array' and create a categoryarray?

I guess catValues and catLabels allows you to use different data for values than you display... presumably the typical use here would be integer values and string labels? Makes sense, I'd just spell out the names completely: categoryvalues and categorylabels.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Ah yes, this is a nice parallel with Cartesian axes. To summarize:

  • catValues and catDisplayInds will be replaced by categoryarray
  • categoryorder will be introduced to control whether ordering is based on categoryarray, the trace, or the ascending/descending by category value. I believe the current default behavior is equivalent to trace, and having the ascending and descending modes will be nice.
  • categorylabels will be introduced to control the category label displayed. This will be an array the same length as categoryarray, and will only have an effect when categoryorder is array. Additionally, to keep things consistent, the categorylabels array will need to be reordered along with categoryarray when the user drags to reorder categories.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Ok, I think this worked out really well.
Done in 7f90fc1, 5e60062, and 66c90fa (Sorry the commit history got a little messy here)

valType: 'data_array',
role: 'info',
editType: 'calc',
dflt: [],
description: [
''
].join(' ')
},
catValues: {
valType: 'data_array',
role: 'info',
editType: 'calc',
dflt: [],
description: [
''
].join(' ')
},
catLabels: {
valType: 'data_array',
role: 'info',
editType: 'calc',
dflt: [],
description: [
''
].join(' ')
},
values: {
valType: 'data_array',
role: 'info',
dflt: [],
editType: 'calc',
description: [
'Dimension values. `values[n]` represents the category value of the `n`th point in the dataset,',
'therefore the `values` vector for all dimensions must be the same (longer vectors',
'will be truncated). Each value must an element of `catValues`.'
].join(' ')
},
displayInd: {

Choose a reason for hiding this comment

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

Again I'd spell out displayindex. Also reminds me we need a visible attribute per dimension.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Sure. Do you prefer all lowercase for the internal JavaScript object properties as well? Right now all of these are in camelCase, but I can change that.

Choose a reason for hiding this comment

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

Just use lowercase for attribute names in the figure JSON (data and layout). In the code, camelCase is preferred.

Copy link
Owner Author

@jonmmease jonmmease Aug 8, 2018

Choose a reason for hiding this comment

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

displayindex done in a34dafa

Still need to add dimension.visible attribute

Copy link
Owner Author

Choose a reason for hiding this comment

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

Visible done in 2c14168 and c8e3cc9

valType: 'integer',
role: 'info',
editType: 'calc',
description: [
'The display index of dimension, from left to right, zero indexed, defaults to dimension' +
'index.'
].join(' ')
},
editType: 'calc',
description: 'The dimensions (variables) of the parallel categories diagram.'
},

marker: marker,
counts: {
valType: 'number',
min: 0,
dflt: 1,
arrayOk: true,
role: 'info',
editType: 'calc',
description: [
'The number of observations represented by each state. Defaults to 1 so that each state represents ' +
'one observation'
]
}
};
34 changes: 34 additions & 0 deletions src/traces/parcats/base_plot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copyright 2012-2017, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var getModuleCalcData = require('../../plots/get_data').getModuleCalcData;
var parcatsPlot = require('./plot');

var PARCATS = 'parcats';
exports.name = PARCATS;

exports.plot = function(gd, traces, transitionOpts, makeOnCompleteCallback) {

var cdModuleAndOthers = getModuleCalcData(gd.calcdata, PARCATS);

if(cdModuleAndOthers.length) {
var calcData = cdModuleAndOthers[0];
parcatsPlot(gd, calcData, transitionOpts, makeOnCompleteCallback);
}
};

exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
var hadTable = (oldFullLayout._has && oldFullLayout._has('parcats'));
var hasTable = (newFullLayout._has && newFullLayout._has('parcats'));

if(hadTable && !hasTable) {
oldFullLayout._paperdiv.selectAll('.parcats').remove();
}
};
Loading