Skip to content

Parallel Categories trace type for multi dimensional categorical data #1

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

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 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
152 changes: 152 additions & 0 deletions src/traces/parcats/attributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* 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 domainAttrs = require('../../plots/domain').attributes;
var scatterAttrs = require('../scatter/attributes');
var scatterLineAttrs = scatterAttrs.line;
var colorbarAttrs = require('../../components/colorbar/attributes');

var line = extendFlat({
editType: 'calc'
}, colorAttributes('line', {editType: 'calc'}),
{
showscale: scatterLineAttrs.showscale,
colorbar: colorbarAttrs,
shape: {
valType: 'enumerated',
values: ['linear', 'hspline'],
dflt: 'linear',
role: 'info',
editType: 'plot',
description: 'Sets the shape of the paths'},
});

module.exports = {
domain: domainAttrs({name: 'parcats', trace: true, 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(' ')
},
displayindex: {
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.'
},

line: line,
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