Skip to content

Commit a7e7e65

Browse files
committed
add Sunburst trace module
- 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
1 parent 6695e76 commit a7e7e65

17 files changed

+1150
-16
lines changed

lib/index.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ Plotly.register([
1818
require('./histogram'),
1919
require('./histogram2d'),
2020
require('./histogram2dcontour'),
21-
require('./pie'),
2221
require('./contour'),
2322
require('./scatterternary'),
2423
require('./violin'),
2524

25+
require('./pie'),
26+
require('./sunburst'),
27+
2628
require('./scatter3d'),
2729
require('./surface'),
2830
require('./isosurface'),

lib/sunburst.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Copyright 2012-2019, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
module.exports = require('../src/traces/sunburst');

package-lock.json

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"country-regex": "^1.1.0",
6767
"d3": "^3.5.12",
6868
"d3-force": "^1.0.6",
69+
"d3-hierarchy": "^1.1.8",
6970
"d3-interpolate": "1",
7071
"d3-sankey-circular": "0.32.0",
7172
"delaunay-triangulate": "^1.1.6",

src/components/fx/helpers.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,8 @@ var pointKeyMap = {
223223
locations: 'location',
224224
labels: 'label',
225225
values: 'value',
226-
'marker.colors': 'color'
226+
'marker.colors': 'color',
227+
parents: 'parent'
227228
};
228229

229230
function getPointKey(astr) {

src/plot_api/plot_api.js

+3
Original file line numberDiff line numberDiff line change
@@ -3864,6 +3864,9 @@ function makePlotFramework(gd) {
38643864
// single pie layer for the whole plot
38653865
fullLayout._pielayer = fullLayout._paper.append('g').classed('pielayer', true);
38663866

3867+
// single sunbursrt layer for the whole plot
3868+
fullLayout._sunburstlayer = fullLayout._paper.append('g').classed('sunbursrtlayer', true);
3869+
38673870
// fill in image server scrape-svg
38683871
fullLayout._glimages = fullLayout._paper.append('g').classed('glimages', true);
38693872

src/plots/plots.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -2725,8 +2725,9 @@ plots.doCalcdata = function(gd, traces) {
27252725
gd._hmpixcount = 0;
27262726
gd._hmlumcount = 0;
27272727

2728-
// for sharing colors across pies (and for legend)
2728+
// for sharing colors across pies / sunbursts (and for legend)
27292729
fullLayout._piecolormap = {};
2730+
fullLayout._sunburstcolormap = {};
27302731

27312732
// If traces were specified and this trace was not included,
27322733
// then transfer it over from the old calcdata:

src/traces/pie/plot.js

+16-13
Original file line numberDiff line numberDiff line change
@@ -513,16 +513,17 @@ function prerenderTitles(cdpie, gd) {
513513
function transformInsideText(textBB, pt, cd0) {
514514
var textDiameter = Math.sqrt(textBB.width * textBB.width + textBB.height * textBB.height);
515515
var textAspect = textBB.width / textBB.height;
516-
var halfAngle = Math.PI * Math.min(pt.v / cd0.vTotal, 0.5);
517-
var ring = 1 - cd0.trace.hole;
518-
var rInscribed = getInscribedRadiusFraction(pt, cd0);
516+
var halfAngle = pt.halfangle;
517+
var ring = pt.ring;
518+
var rInscribed = pt.rInscribed;
519+
var r = cd0.r || pt.rpx1;
519520

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

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

534535
// max size if text is rotated radially
535536
var Qr = textAspect + 1 / (2 * Math.tan(halfAngle));
536-
var maxHalfHeightRotRadial = cd0.r * Math.min(
537+
var maxHalfHeightRotRadial = r * Math.min(
537538
1 / (Math.sqrt(Qr * Qr + 0.5) + Qr),
538539
ring / (Math.sqrt(textAspect * textAspect + ring / 2) + textAspect)
539540
);
540541
var radialTransform = {
541542
scale: maxHalfHeightRotRadial * 2 / textBB.height,
542-
rCenter: Math.cos(maxHalfHeightRotRadial / cd0.r) -
543-
maxHalfHeightRotRadial * textAspect / cd0.r,
543+
rCenter: Math.cos(maxHalfHeightRotRadial / r) -
544+
maxHalfHeightRotRadial * textAspect / r,
544545
rotate: (180 / Math.PI * pt.midangle + 720) % 180 - 90
545546
};
546547

547548
// max size if text is rotated tangentially
548549
var aspectInv = 1 / textAspect;
549550
var Qt = aspectInv + 1 / (2 * Math.tan(halfAngle));
550-
var maxHalfWidthTangential = cd0.r * Math.min(
551+
var maxHalfWidthTangential = r * Math.min(
551552
1 / (Math.sqrt(Qt * Qt + 0.5) + Qt),
552553
ring / (Math.sqrt(aspectInv * aspectInv + ring / 2) + aspectInv)
553554
);
554555
var tangentialTransform = {
555556
scale: maxHalfWidthTangential * 2 / textBB.width,
556-
rCenter: Math.cos(maxHalfWidthTangential / cd0.r) -
557-
maxHalfWidthTangential / textAspect / cd0.r,
557+
rCenter: Math.cos(maxHalfWidthTangential / r) -
558+
maxHalfWidthTangential / textAspect / r,
558559
rotate: (180 / Math.PI * pt.midangle + 810) % 180 - 90
559560
};
560561
// if we need a rotated transform, pick the biggest one
@@ -569,8 +570,7 @@ function transformInsideText(textBB, pt, cd0) {
569570
function getInscribedRadiusFraction(pt, cd0) {
570571
if(pt.v === cd0.vTotal && !cd0.trace.hole) return 1;// special case of 100% with no hole
571572

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

576576
function transformOutsideText(textBB, pt) {
@@ -838,7 +838,6 @@ function scalePies(cdpie, plotSize) {
838838
}
839839
}
840840
}
841-
842841
}
843842

844843
function setCoords(cd) {
@@ -885,6 +884,10 @@ function setCoords(cd) {
885884
cdi[lastPt] = currentCoords;
886885

887886
cdi.largeArc = (cdi.v > cd0.vTotal / 2) ? 1 : 0;
887+
888+
cdi.halfangle = Math.PI * Math.min(cdi.v / cd0.vTotal, 0.5);
889+
cdi.ring = 1 - trace.hole;
890+
cdi.rInscribed = getInscribedRadiusFraction(cdi, cd0);
888891
}
889892
}
890893

src/traces/sunburst/attributes.js

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* Copyright 2012-2019, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var plotAttrs = require('../../plots/attributes');
12+
var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes');
13+
var domainAttrs = require('../../plots/domain').attributes;
14+
var pieAtts = require('../pie/attributes');
15+
16+
var extendFlat = require('../../lib/extend').extendFlat;
17+
18+
// TODO should we use singular `label`, `parent` and `value`?
19+
20+
module.exports = {
21+
labels: {
22+
valType: 'data_array',
23+
editType: 'calc',
24+
description: [
25+
'Sets the labels of each of the sunburst sectors.'
26+
].join(' ')
27+
},
28+
parents: {
29+
valType: 'data_array',
30+
editType: 'calc',
31+
description: [
32+
'Sets the parent sectors for each of the sunburst sectors.',
33+
'Empty string items \'\' are understood to reference',
34+
'the root node in the hierarchy.',
35+
'If `ids` is filled, `parents` items are understood to be "ids" themselves.',
36+
'When `ids` is not set, plotly attempts to find matching items in `labels`,',
37+
'but beware there must be unique.'
38+
].join(' ')
39+
},
40+
41+
values: {
42+
valType: 'data_array',
43+
editType: 'calc',
44+
description: [
45+
'Sets the values associated with each of the sunburst sectors.',
46+
'Use with `branchvalues` to determine how the values are summed.'
47+
].join(' ')
48+
},
49+
branchvalues: {
50+
valType: 'enumerated',
51+
values: ['total', 'extra'],
52+
dflt: 'extra',
53+
editType: 'calc',
54+
role: 'info',
55+
description: [
56+
'Determines how the items in `values` are summed.',
57+
'When set to *total*, items in `values` are taken to be value of all its descendants.',
58+
'When set to *extra*, items in `values` corresponding to the root and the branches sectors',
59+
'are taken to be the extra part not part of the sum of the values at their leaves.'
60+
].join(' ')
61+
},
62+
63+
level: {
64+
valType: 'any',
65+
editType: 'plot',
66+
role: 'info',
67+
dflt: '',
68+
description: [
69+
'Sets the level from which this sunburst trace hierarchy is rendered.',
70+
'Set `level` to `\'\'` to start the sunburst from the root node in the hierarchy.',
71+
'Must be an "id" if `ids` is filled in, otherwise plotly attempts to find a matching',
72+
'item in `labels`.'
73+
].join(' ')
74+
},
75+
maxdepth: {
76+
valType: 'integer',
77+
editType: 'plot',
78+
role: 'info',
79+
dflt: -1,
80+
description: [
81+
'Sets the number of rendered sunburst rings from any given `level`.',
82+
'Set `maxdepth` to *-1* to render all the levels in the hierarchy.'
83+
].join(' ')
84+
},
85+
86+
marker: {
87+
colors: {
88+
valType: 'data_array',
89+
editType: 'calc',
90+
description: [
91+
'Sets the color of each sector of this sunburst chart.',
92+
'If not specified, the default trace color set is used',
93+
'to pick the sector colors.'
94+
].join(' ')
95+
},
96+
97+
// colorinheritance: {
98+
// valType: 'enumerated',
99+
// values: ['per-branch', 'per-label', false]
100+
// },
101+
102+
line: {
103+
color: pieAtts.marker.line.color,
104+
width: pieAtts.marker.line.width,
105+
editType: 'calc'
106+
},
107+
editType: 'calc'
108+
},
109+
110+
leaf: {
111+
opacity: {
112+
valType: 'number',
113+
editType: 'style',
114+
role: 'style',
115+
min: 0,
116+
max: 1,
117+
dflt: 0.7,
118+
description: 'Sets the opacity of the leaves.'
119+
},
120+
textposition: {
121+
valType: 'enumerated',
122+
role: 'info',
123+
values: ['inside', 'outside', 'auto'],
124+
dflt: 'inside',
125+
editType: 'plot',
126+
description: 'Specifies the location of the leaf text labels.'
127+
},
128+
editType: 'plot'
129+
},
130+
131+
text: pieAtts.text,
132+
textinfo: extendFlat({}, pieAtts.textinfo, {editType: 'plot'}),
133+
textfont: pieAtts.textfont,
134+
135+
hovertext: pieAtts.hovertext,
136+
hoverinfo: extendFlat({}, plotAttrs.hoverinfo, {
137+
flags: ['label', 'text', 'value', 'name'],
138+
dflt: 'label+name'
139+
}),
140+
hovertemplate: hovertemplateAttrs(),
141+
142+
insidetextfont: pieAtts.insidetextfont,
143+
outsidetextfont: pieAtts.outsidetextfont,
144+
145+
domain: domainAttrs({name: 'sunburst', trace: true, editType: 'calc'}),
146+
147+
// TODO Might want the same defaults as for pie traces?
148+
// TODO maybe drop for v1 release
149+
sort: pieAtts.sort,
150+
direction: pieAtts.direction,
151+
rotation: pieAtts.rotation
152+
};

src/traces/sunburst/base_plot.js

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Copyright 2012-2019, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var Registry = require('../../registry');
12+
var getModuleCalcData = require('../../plots/get_data').getModuleCalcData;
13+
14+
var name = exports.name = 'sunburst';
15+
16+
exports.plot = function(gd) {
17+
var _module = Registry.getModule(name);
18+
var cdmodule = getModuleCalcData(gd.calcdata, _module)[0];
19+
20+
if(cdmodule.length) {
21+
_module.plot(gd, cdmodule);
22+
}
23+
};
24+
25+
exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
26+
var had = (oldFullLayout._has && oldFullLayout._has(name));
27+
var has = (newFullLayout._has && newFullLayout._has(name));
28+
29+
if(had && !has) {
30+
oldFullLayout._sunburstlayer.selectAll('g.trace').remove();
31+
}
32+
};

0 commit comments

Comments
 (0)