Skip to content

Commit

Permalink
Merge pull request #2963 from jonmmease/parcats
Browse files Browse the repository at this point in the history
Parallel Categories (parcats) trace type for multi dimensional categorical data
  • Loading branch information
etpinard committed Oct 1, 2018
2 parents 5c62c8e + 99f9a47 commit cf2fdc4
Show file tree
Hide file tree
Showing 33 changed files with 5,098 additions and 6 deletions.
2 changes: 1 addition & 1 deletion lib/index.js
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
@@ -0,0 +1,11 @@
/**
* Copyright 2012-2018, 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');
75 changes: 75 additions & 0 deletions src/components/fx/hover.js
Expand Up @@ -153,6 +153,81 @@ exports.loneHover = function loneHover(hoverItem, opts) {
return hoverLabel.node();
};

exports.multiHovers = function multiHovers(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
6 changes: 4 additions & 2 deletions src/components/fx/index.js
Expand Up @@ -13,6 +13,7 @@ var Lib = require('../../lib');
var dragElement = require('../dragelement');
var helpers = require('./helpers');
var layoutAttributes = require('./layout_attributes');
var hoverModule = require('./hover');

module.exports = {
moduleType: 'component',
Expand Down Expand Up @@ -41,10 +42,11 @@ module.exports = {
castHoverOption: castHoverOption,
castHoverinfo: castHoverinfo,

hover: require('./hover').hover,
hover: hoverModule.hover,
unhover: dragElement.unhover,

loneHover: require('./hover').loneHover,
loneHover: hoverModule.loneHover,
multiHovers: hoverModule.multiHovers,
loneUnhover: loneUnhover,

click: require('./click')
Expand Down
202 changes: 202 additions & 0 deletions src/traces/parcats/attributes.js
@@ -0,0 +1,202 @@
/**
* Copyright 2012-2018, 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 plotAttrs = require('../../plots/attributes');
var fontAttrs = require('../../plots/font_attributes');
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.',
'If `linear`, paths are composed of straight lines.',
'If `hspline`, paths are composed of horizontal curved splines'
].join(' ')
}
});

module.exports = {
domain: domainAttrs({name: 'parcats', trace: true, editType: 'calc'}),
hoverinfo: extendFlat({}, plotAttrs.hoverinfo, {
flags: ['count', 'probability'],
editType: 'plot'
// plotAttrs.hoverinfo description is appropriate
}),
hoveron: {
valType: 'enumerated',
values: ['category', 'color', 'dimension'],
dflt: 'category',
role: 'info',
editType: 'plot',
description: [
'Sets the hover interaction mode for the parcats diagram.',
'If `category`, hover interaction take place per category.',
'If `color`, hover interactions take place per color per category.',
'If `dimension`, hover interactions take place across all categories per dimension.'
].join(' ')
},
arrangement: {
valType: 'enumerated',
values: ['perpendicular', 'freeform', 'fixed'],
dflt: 'perpendicular',
role: 'style',
editType: 'plot',
description: [
'Sets the drag interaction mode for categories and dimensions.',
'If `perpendicular`, the categories can only move along a line perpendicular to the paths.',
'If `freeform`, the categories can freely move on the plane.',
'If `fixed`, the categories and dimensions are stationary.'
].join(' ')
},
bundlecolors: {
valType: 'boolean',
dflt: true,
role: 'info',
editType: 'plot',
description: 'Sort paths so that like colors are bundled together within each category.'
},
sortpaths: {
valType: 'enumerated',
values: ['forward', 'backward'],
dflt: 'forward',
role: 'info',
editType: 'plot',
description: [
'Sets the path sorting algorithm.',
'If `forward`, sort paths based on dimension categories from left to right.',
'If `backward`, sort paths based on dimensions categories from right to left.'
].join(' ')
},
labelfont: fontAttrs({
editType: 'calc',
description: 'Sets the font for the `dimension` labels.'
}),

tickfont: 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.'
},
categoryorder: {
valType: 'enumerated',
values: [
'trace', 'category ascending', 'category descending', 'array'
],
dflt: 'trace',
role: 'info',
editType: 'calc',
description: [
'Specifies the ordering logic for the categories in the dimension.',
'By default, plotly uses *trace*, which specifies the order that is present in the data supplied.',
'Set `categoryorder` to *category ascending* or *category descending* if order should be determined by',
'the alphanumerical order of the category names.',
'Set `categoryorder` to *array* to derive the ordering from the attribute `categoryarray`. If a category',
'is not found in the `categoryarray` array, the sorting behavior for that attribute will be identical to',
'the *trace* mode. The unspecified categories will follow the categories in `categoryarray`.'
].join(' ')
},
categoryarray: {
valType: 'data_array',
role: 'info',
editType: 'calc',
description: [
'Sets the order in which categories in this dimension appear.',
'Only has an effect if `categoryorder` is set to *array*.',
'Used with `categoryorder`.'
].join(' ')
},
ticktext: {
valType: 'data_array',
role: 'info',
editType: 'calc',
description: [
'Sets alternative tick labels for the categories in this dimension.',
'Only has an effect if `categoryorder` is set to *array*.',
'Should be an array the same length as `categoryarray`',
'Used with `categoryorder`.'
].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).'
].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.',
visible: {
valType: 'boolean',
dflt: true,
role: 'info',
editType: 'calc',
description: 'Shows the dimension when set to `true` (the default). Hides the dimension for `false`.'
}
},

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'
].join(' ')
},

// Hide unsupported top-level properties from plot-schema
customdata: undefined,
hoverlabel: undefined,
ids: undefined,
legendgroup: undefined,
opacity: undefined,
selectedpoints: undefined,
showlegend: undefined
};
34 changes: 34 additions & 0 deletions src/traces/parcats/base_plot.js
@@ -0,0 +1,34 @@
/**
* Copyright 2012-2018, 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();
}
};

0 comments on commit cf2fdc4

Please sign in to comment.