Skip to content

Commit

Permalink
add Sunburst trace module
Browse files Browse the repository at this point in the history
- use d3-hierarchy (for now) to help out with the logic
- add special `plotly_sunburstclick` event to zoom in/out
- adapt pie's transformInsideText for sunbursts' ring
  • Loading branch information
etpinard committed Mar 1, 2019
1 parent 6695e76 commit a7e7e65
Show file tree
Hide file tree
Showing 17 changed files with 1,150 additions and 16 deletions.
4 changes: 3 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ Plotly.register([
require('./histogram'),
require('./histogram2d'),
require('./histogram2dcontour'),
require('./pie'),
require('./contour'),
require('./scatterternary'),
require('./violin'),

require('./pie'),
require('./sunburst'),

require('./scatter3d'),
require('./surface'),
require('./isosurface'),
Expand Down
11 changes: 11 additions & 0 deletions lib/sunburst.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Copyright 2012-2019, 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/sunburst');
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"country-regex": "^1.1.0",
"d3": "^3.5.12",
"d3-force": "^1.0.6",
"d3-hierarchy": "^1.1.8",
"d3-interpolate": "1",
"d3-sankey-circular": "0.32.0",
"delaunay-triangulate": "^1.1.6",
Expand Down
3 changes: 2 additions & 1 deletion src/components/fx/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,8 @@ var pointKeyMap = {
locations: 'location',
labels: 'label',
values: 'value',
'marker.colors': 'color'
'marker.colors': 'color',
parents: 'parent'
};

function getPointKey(astr) {
Expand Down
3 changes: 3 additions & 0 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -3864,6 +3864,9 @@ function makePlotFramework(gd) {
// single pie layer for the whole plot
fullLayout._pielayer = fullLayout._paper.append('g').classed('pielayer', true);

// single sunbursrt layer for the whole plot
fullLayout._sunburstlayer = fullLayout._paper.append('g').classed('sunbursrtlayer', true);

// fill in image server scrape-svg
fullLayout._glimages = fullLayout._paper.append('g').classed('glimages', true);

Expand Down
3 changes: 2 additions & 1 deletion src/plots/plots.js
Original file line number Diff line number Diff line change
Expand Up @@ -2725,8 +2725,9 @@ plots.doCalcdata = function(gd, traces) {
gd._hmpixcount = 0;
gd._hmlumcount = 0;

// for sharing colors across pies (and for legend)
// for sharing colors across pies / sunbursts (and for legend)
fullLayout._piecolormap = {};
fullLayout._sunburstcolormap = {};

// If traces were specified and this trace was not included,
// then transfer it over from the old calcdata:
Expand Down
29 changes: 16 additions & 13 deletions src/traces/pie/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -513,16 +513,17 @@ function prerenderTitles(cdpie, gd) {
function transformInsideText(textBB, pt, cd0) {
var textDiameter = Math.sqrt(textBB.width * textBB.width + textBB.height * textBB.height);
var textAspect = textBB.width / textBB.height;
var halfAngle = Math.PI * Math.min(pt.v / cd0.vTotal, 0.5);
var ring = 1 - cd0.trace.hole;
var rInscribed = getInscribedRadiusFraction(pt, cd0);
var halfAngle = pt.halfangle;
var ring = pt.ring;
var rInscribed = pt.rInscribed;
var r = cd0.r || pt.rpx1;

// max size text can be inserted inside without rotating it
// this inscribes the text rectangle in a circle, which is then inscribed
// in the slice, so it will be an underestimate, which some day we may want
// to improve so this case can get more use
var transform = {
scale: rInscribed * cd0.r * 2 / textDiameter,
scale: rInscribed * r * 2 / textDiameter,

// and the center position and rotation in this case
rCenter: 1 - rInscribed,
Expand All @@ -533,28 +534,28 @@ function transformInsideText(textBB, pt, cd0) {

// max size if text is rotated radially
var Qr = textAspect + 1 / (2 * Math.tan(halfAngle));
var maxHalfHeightRotRadial = cd0.r * Math.min(
var maxHalfHeightRotRadial = r * Math.min(
1 / (Math.sqrt(Qr * Qr + 0.5) + Qr),
ring / (Math.sqrt(textAspect * textAspect + ring / 2) + textAspect)
);
var radialTransform = {
scale: maxHalfHeightRotRadial * 2 / textBB.height,
rCenter: Math.cos(maxHalfHeightRotRadial / cd0.r) -
maxHalfHeightRotRadial * textAspect / cd0.r,
rCenter: Math.cos(maxHalfHeightRotRadial / r) -
maxHalfHeightRotRadial * textAspect / r,
rotate: (180 / Math.PI * pt.midangle + 720) % 180 - 90
};

// max size if text is rotated tangentially
var aspectInv = 1 / textAspect;
var Qt = aspectInv + 1 / (2 * Math.tan(halfAngle));
var maxHalfWidthTangential = cd0.r * Math.min(
var maxHalfWidthTangential = r * Math.min(
1 / (Math.sqrt(Qt * Qt + 0.5) + Qt),
ring / (Math.sqrt(aspectInv * aspectInv + ring / 2) + aspectInv)
);
var tangentialTransform = {
scale: maxHalfWidthTangential * 2 / textBB.width,
rCenter: Math.cos(maxHalfWidthTangential / cd0.r) -
maxHalfWidthTangential / textAspect / cd0.r,
rCenter: Math.cos(maxHalfWidthTangential / r) -
maxHalfWidthTangential / textAspect / r,
rotate: (180 / Math.PI * pt.midangle + 810) % 180 - 90
};
// if we need a rotated transform, pick the biggest one
Expand All @@ -569,8 +570,7 @@ function transformInsideText(textBB, pt, cd0) {
function getInscribedRadiusFraction(pt, cd0) {
if(pt.v === cd0.vTotal && !cd0.trace.hole) return 1;// special case of 100% with no hole

var halfAngle = Math.PI * Math.min(pt.v / cd0.vTotal, 0.5);
return Math.min(1 / (1 + 1 / Math.sin(halfAngle)), (1 - cd0.trace.hole) / 2);
return Math.min(1 / (1 + 1 / Math.sin(pt.halfangle)), pt.ring / 2);
}

function transformOutsideText(textBB, pt) {
Expand Down Expand Up @@ -838,7 +838,6 @@ function scalePies(cdpie, plotSize) {
}
}
}

}

function setCoords(cd) {
Expand Down Expand Up @@ -885,6 +884,10 @@ function setCoords(cd) {
cdi[lastPt] = currentCoords;

cdi.largeArc = (cdi.v > cd0.vTotal / 2) ? 1 : 0;

cdi.halfangle = Math.PI * Math.min(cdi.v / cd0.vTotal, 0.5);
cdi.ring = 1 - trace.hole;
cdi.rInscribed = getInscribedRadiusFraction(cdi, cd0);
}
}

Expand Down
152 changes: 152 additions & 0 deletions src/traces/sunburst/attributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* Copyright 2012-2019, 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 plotAttrs = require('../../plots/attributes');
var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes');
var domainAttrs = require('../../plots/domain').attributes;
var pieAtts = require('../pie/attributes');

var extendFlat = require('../../lib/extend').extendFlat;

// TODO should we use singular `label`, `parent` and `value`?

module.exports = {
labels: {
valType: 'data_array',
editType: 'calc',
description: [
'Sets the labels of each of the sunburst sectors.'
].join(' ')
},
parents: {
valType: 'data_array',
editType: 'calc',
description: [
'Sets the parent sectors for each of the sunburst sectors.',
'Empty string items \'\' are understood to reference',
'the root node in the hierarchy.',
'If `ids` is filled, `parents` items are understood to be "ids" themselves.',
'When `ids` is not set, plotly attempts to find matching items in `labels`,',
'but beware there must be unique.'
].join(' ')
},

values: {
valType: 'data_array',
editType: 'calc',
description: [
'Sets the values associated with each of the sunburst sectors.',
'Use with `branchvalues` to determine how the values are summed.'
].join(' ')
},
branchvalues: {
valType: 'enumerated',
values: ['total', 'extra'],
dflt: 'extra',
editType: 'calc',
role: 'info',
description: [
'Determines how the items in `values` are summed.',
'When set to *total*, items in `values` are taken to be value of all its descendants.',
'When set to *extra*, items in `values` corresponding to the root and the branches sectors',
'are taken to be the extra part not part of the sum of the values at their leaves.'
].join(' ')
},

level: {
valType: 'any',
editType: 'plot',
role: 'info',
dflt: '',
description: [
'Sets the level from which this sunburst trace hierarchy is rendered.',
'Set `level` to `\'\'` to start the sunburst from the root node in the hierarchy.',
'Must be an "id" if `ids` is filled in, otherwise plotly attempts to find a matching',
'item in `labels`.'
].join(' ')
},
maxdepth: {
valType: 'integer',
editType: 'plot',
role: 'info',
dflt: -1,
description: [
'Sets the number of rendered sunburst rings from any given `level`.',
'Set `maxdepth` to *-1* to render all the levels in the hierarchy.'
].join(' ')
},

marker: {
colors: {
valType: 'data_array',
editType: 'calc',
description: [
'Sets the color of each sector of this sunburst chart.',
'If not specified, the default trace color set is used',
'to pick the sector colors.'
].join(' ')
},

// colorinheritance: {
// valType: 'enumerated',
// values: ['per-branch', 'per-label', false]
// },

line: {
color: pieAtts.marker.line.color,
width: pieAtts.marker.line.width,
editType: 'calc'
},
editType: 'calc'
},

leaf: {
opacity: {
valType: 'number',
editType: 'style',
role: 'style',
min: 0,
max: 1,
dflt: 0.7,
description: 'Sets the opacity of the leaves.'
},
textposition: {
valType: 'enumerated',
role: 'info',
values: ['inside', 'outside', 'auto'],
dflt: 'inside',
editType: 'plot',
description: 'Specifies the location of the leaf text labels.'
},
editType: 'plot'
},

text: pieAtts.text,
textinfo: extendFlat({}, pieAtts.textinfo, {editType: 'plot'}),
textfont: pieAtts.textfont,

hovertext: pieAtts.hovertext,
hoverinfo: extendFlat({}, plotAttrs.hoverinfo, {
flags: ['label', 'text', 'value', 'name'],
dflt: 'label+name'
}),
hovertemplate: hovertemplateAttrs(),

insidetextfont: pieAtts.insidetextfont,
outsidetextfont: pieAtts.outsidetextfont,

domain: domainAttrs({name: 'sunburst', trace: true, editType: 'calc'}),

// TODO Might want the same defaults as for pie traces?
// TODO maybe drop for v1 release
sort: pieAtts.sort,
direction: pieAtts.direction,
rotation: pieAtts.rotation
};
32 changes: 32 additions & 0 deletions src/traces/sunburst/base_plot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Copyright 2012-2019, 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 Registry = require('../../registry');
var getModuleCalcData = require('../../plots/get_data').getModuleCalcData;

var name = exports.name = 'sunburst';

exports.plot = function(gd) {
var _module = Registry.getModule(name);
var cdmodule = getModuleCalcData(gd.calcdata, _module)[0];

if(cdmodule.length) {
_module.plot(gd, cdmodule);
}
};

exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
var had = (oldFullLayout._has && oldFullLayout._has(name));
var has = (newFullLayout._has && newFullLayout._has(name));

if(had && !has) {
oldFullLayout._sunburstlayer.selectAll('g.trace').remove();
}
};
Loading

0 comments on commit a7e7e65

Please sign in to comment.