diff --git a/src/traces/pie/attributes.js b/src/traces/pie/attributes.js
index 018250f2aa5..79590a9195a 100644
--- a/src/traces/pie/attributes.js
+++ b/src/traces/pie/attributes.js
@@ -6,6 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
+'use strict';
var colorAttrs = require('../../components/color/attributes');
var fontAttrs = require('../../plots/font_attributes');
diff --git a/src/traces/pie/calc.js b/src/traces/pie/calc.js
new file mode 100644
index 00000000000..fba44fabc37
--- /dev/null
+++ b/src/traces/pie/calc.js
@@ -0,0 +1,145 @@
+/**
+* Copyright 2012-2016, 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 isNumeric = require('fast-isnumeric');
+var tinycolor = require('tinycolor2');
+
+var Color = require('../../components/color');
+var helpers = require('./helpers');
+
+module.exports = function calc(gd, trace) {
+ var vals = trace.values,
+ labels = trace.labels,
+ cd = [],
+ fullLayout = gd._fullLayout,
+ colorMap = fullLayout._piecolormap,
+ allThisTraceLabels = {},
+ needDefaults = false,
+ vTotal = 0,
+ hiddenLabels = fullLayout.hiddenlabels || [],
+ i,
+ v,
+ label,
+ color,
+ hidden,
+ pt;
+
+ if(trace.dlabel) {
+ labels = new Array(vals.length);
+ for(i = 0; i < vals.length; i++) {
+ labels[i] = String(trace.label0 + i * trace.dlabel);
+ }
+ }
+
+ for(i = 0; i < vals.length; i++) {
+ v = vals[i];
+ if(!isNumeric(v)) continue;
+ v = +v;
+ if(v < 0) continue;
+
+ label = labels[i];
+ if(label === undefined || label === '') label = i;
+ label = String(label);
+ // only take the first occurrence of any given label.
+ // TODO: perhaps (optionally?) sum values for a repeated label?
+ if(allThisTraceLabels[label] === undefined) allThisTraceLabels[label] = true;
+ else continue;
+
+ color = tinycolor(trace.marker.colors[i]);
+ if(color.isValid()) {
+ color = Color.addOpacity(color, color.getAlpha());
+ if(!colorMap[label]) {
+ colorMap[label] = color;
+ }
+ }
+ // have we seen this label and assigned a color to it in a previous trace?
+ else if(colorMap[label]) color = colorMap[label];
+ // color needs a default - mark it false, come back after sorting
+ else {
+ color = false;
+ needDefaults = true;
+ }
+
+ hidden = hiddenLabels.indexOf(label) !== -1;
+
+ if(!hidden) vTotal += v;
+
+ cd.push({
+ v: v,
+ label: label,
+ color: color,
+ i: i,
+ hidden: hidden
+ });
+ }
+
+ if(trace.sort) cd.sort(function(a, b) { return b.v - a.v; });
+
+ /**
+ * now go back and fill in colors we're still missing
+ * this is done after sorting, so we pick defaults
+ * in the order slices will be displayed
+ */
+
+ if(needDefaults) {
+ for(i = 0; i < cd.length; i++) {
+ pt = cd[i];
+ if(pt.color === false) {
+ colorMap[pt.label] = pt.color = nextDefaultColor(fullLayout._piedefaultcolorcount);
+ fullLayout._piedefaultcolorcount++;
+ }
+ }
+ }
+
+ // include the sum of all values in the first point
+ if(cd[0]) cd[0].vTotal = vTotal;
+
+ // now insert text
+ if(trace.textinfo && trace.textinfo !== 'none') {
+ var hasLabel = trace.textinfo.indexOf('label') !== -1,
+ hasText = trace.textinfo.indexOf('text') !== -1,
+ hasValue = trace.textinfo.indexOf('value') !== -1,
+ hasPercent = trace.textinfo.indexOf('percent') !== -1,
+ thisText;
+
+ for(i = 0; i < cd.length; i++) {
+ pt = cd[i];
+ thisText = hasLabel ? [pt.label] : [];
+ if(hasText && trace.text[pt.i]) thisText.push(trace.text[pt.i]);
+ if(hasValue) thisText.push(helpers.formatPieValue(pt.v));
+ if(hasPercent) thisText.push(helpers.formatPiePercent(pt.v / vTotal));
+ pt.text = thisText.join('
');
+ }
+ }
+
+ return cd;
+};
+
+/**
+ * pick a default color from the main default set, augmented by
+ * itself lighter then darker before repeating
+ */
+var pieDefaultColors;
+
+function nextDefaultColor(index) {
+ if(!pieDefaultColors) {
+ // generate this default set on demand (but then it gets saved in the module)
+ var mainDefaults = Color.defaults;
+ pieDefaultColors = mainDefaults.slice();
+ for(var i = 0; i < mainDefaults.length; i++) {
+ pieDefaultColors.push(tinycolor(mainDefaults[i]).lighten(20).toHexString());
+ }
+ for(i = 0; i < Color.defaults.length; i++) {
+ pieDefaultColors.push(tinycolor(mainDefaults[i]).darken(20).toHexString());
+ }
+ }
+
+ return pieDefaultColors[index % pieDefaultColors.length];
+}
diff --git a/src/traces/pie/defaults.js b/src/traces/pie/defaults.js
new file mode 100644
index 00000000000..1fcedd69ec3
--- /dev/null
+++ b/src/traces/pie/defaults.js
@@ -0,0 +1,82 @@
+/**
+* Copyright 2012-2016, 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 Lib = require('../../lib');
+var attributes = require('./attributes');
+
+module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
+ function coerce(attr, dflt) {
+ return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
+ }
+
+ var coerceFont = Lib.coerceFont;
+
+ var vals = coerce('values');
+ if(!Array.isArray(vals) || !vals.length) {
+ traceOut.visible = false;
+ return;
+ }
+
+ var labels = coerce('labels');
+ if(!Array.isArray(labels)) {
+ coerce('label0');
+ coerce('dlabel');
+ }
+
+ var lineWidth = coerce('marker.line.width');
+ if(lineWidth) coerce('marker.line.color');
+
+ var colors = coerce('marker.colors');
+ if(!Array.isArray(colors)) traceOut.marker.colors = []; // later this will get padded with default colors
+
+ coerce('scalegroup');
+ // TODO: tilt, depth, and hole all need to be coerced to the same values within a scaleegroup
+ // (ideally actually, depth would get set the same *after* scaling, ie the same absolute depth)
+ // and if colors aren't specified we should match these up - potentially even if separate pies
+ // are NOT in the same sharegroup
+
+
+ var textData = coerce('text');
+ var textInfo = coerce('textinfo', Array.isArray(textData) ? 'text+percent' : 'percent');
+
+ coerce('hoverinfo', (layout._dataLength === 1) ? 'label+text+value+percent' : undefined);
+
+ if(textInfo && textInfo !== 'none') {
+ var textPosition = coerce('textposition'),
+ hasBoth = Array.isArray(textPosition) || textPosition === 'auto',
+ hasInside = hasBoth || textPosition === 'inside',
+ hasOutside = hasBoth || textPosition === 'outside';
+
+ if(hasInside || hasOutside) {
+ var dfltFont = coerceFont(coerce, 'textfont', layout.font);
+ if(hasInside) coerceFont(coerce, 'insidetextfont', dfltFont);
+ if(hasOutside) coerceFont(coerce, 'outsidetextfont', dfltFont);
+ }
+ }
+
+ coerce('domain.x');
+ coerce('domain.y');
+
+ // 3D attributes commented out until I finish them in a later PR
+ // var tilt = coerce('tilt');
+ // if(tilt) {
+ // coerce('tiltaxis');
+ // coerce('depth');
+ // coerce('shading');
+ // }
+
+ coerce('hole');
+
+ coerce('sort');
+ coerce('direction');
+ coerce('rotation');
+
+ coerce('pull');
+};
diff --git a/src/traces/pie/helpers.js b/src/traces/pie/helpers.js
new file mode 100644
index 00000000000..653f6dfad8c
--- /dev/null
+++ b/src/traces/pie/helpers.js
@@ -0,0 +1,21 @@
+/**
+* Copyright 2012-2016, 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';
+
+exports.formatPiePercent = function formatPiePercent(v) {
+ var vRounded = (v * 100).toPrecision(3);
+ if(vRounded.indexOf('.') !== -1) return vRounded.replace(/[.]?0+$/,'') + '%';
+ return vRounded + '%';
+};
+
+exports.formatPieValue = function formatPieValue(v) {
+ var vRounded = v.toPrecision(10);
+ if(vRounded.indexOf('.') !== -1) return vRounded.replace(/[.]?0+$/,'');
+ return vRounded;
+};
diff --git a/src/traces/pie/index.js b/src/traces/pie/index.js
index 8453da60e17..bde8f3ef91a 100644
--- a/src/traces/pie/index.js
+++ b/src/traces/pie/index.js
@@ -6,17 +6,11 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
var Plotly = require('../../plotly');
-var d3 = require('d3');
-var isNumeric = require('fast-isnumeric');
-var tinycolor = require('tinycolor2');
-
-var pie = module.exports = {};
-Plotly.Plots.register(pie, 'pie', ['pie', 'showLegend'], {
+Plotly.Plots.register(exports, 'pie', ['pie', 'showLegend'], {
description: [
'A data visualized by the sectors of the pie is set in `values`.',
'The sector labels are set in `labels`.',
@@ -24,928 +18,11 @@ Plotly.Plots.register(pie, 'pie', ['pie', 'showLegend'], {
].join(' ')
});
-pie.attributes = require('./attributes');
-
-pie.supplyDefaults = function(traceIn, traceOut, defaultColor, layout) {
- function coerce(attr, dflt) {
- return Plotly.Lib.coerce(traceIn, traceOut, pie.attributes, attr, dflt);
- }
-
- var coerceFont = Plotly.Lib.coerceFont;
-
- var vals = coerce('values');
- if(!Array.isArray(vals) || !vals.length) {
- traceOut.visible = false;
- return;
- }
-
- var labels = coerce('labels');
- if(!Array.isArray(labels)) {
- coerce('label0');
- coerce('dlabel');
- }
-
- var lineWidth = coerce('marker.line.width');
- if(lineWidth) coerce('marker.line.color');
-
- var colors = coerce('marker.colors');
- if(!Array.isArray(colors)) traceOut.marker.colors = []; // later this will get padded with default colors
-
- coerce('scalegroup');
- // TODO: tilt, depth, and hole all need to be coerced to the same values within a scaleegroup
- // (ideally actually, depth would get set the same *after* scaling, ie the same absolute depth)
- // and if colors aren't specified we should match these up - potentially even if separate pies
- // are NOT in the same sharegroup
-
-
- var textData = coerce('text');
- var textInfo = coerce('textinfo', Array.isArray(textData) ? 'text+percent' : 'percent');
-
- coerce('hoverinfo', (layout._dataLength === 1) ? 'label+text+value+percent' : undefined);
-
- if(textInfo && textInfo !== 'none') {
- var textPosition = coerce('textposition'),
- hasBoth = Array.isArray(textPosition) || textPosition === 'auto',
- hasInside = hasBoth || textPosition === 'inside',
- hasOutside = hasBoth || textPosition === 'outside';
-
- if(hasInside || hasOutside) {
- var dfltFont = coerceFont(coerce, 'textfont', layout.font);
- if(hasInside) coerceFont(coerce, 'insidetextfont', dfltFont);
- if(hasOutside) coerceFont(coerce, 'outsidetextfont', dfltFont);
- }
- }
-
- coerce('domain.x');
- coerce('domain.y');
-
- // 3D attributes commented out until I finish them in a later PR
- // var tilt = coerce('tilt');
- // if(tilt) {
- // coerce('tiltaxis');
- // coerce('depth');
- // coerce('shading');
- // }
-
- coerce('hole');
-
- coerce('sort');
- coerce('direction');
- coerce('rotation');
-
- coerce('pull');
-};
-
-pie.layoutAttributes = {
- /**
- * hiddenlabels is the pie chart analog of visible:'legendonly'
- * but it can contain many labels, and can hide slices
- * from several pies simultaneously
- */
- hiddenlabels: {valType: 'data_array'}
-};
-
-pie.supplyLayoutDefaults = function(layoutIn, layoutOut) {
- function coerce(attr, dflt) {
- return Plotly.Lib.coerce(layoutIn, layoutOut, pie.layoutAttributes, attr, dflt);
- }
- coerce('hiddenlabels');
-};
-
-pie.calc = function(gd, trace) {
- var vals = trace.values,
- labels = trace.labels,
- cd = [],
- fullLayout = gd._fullLayout,
- colorMap = fullLayout._piecolormap,
- allThisTraceLabels = {},
- needDefaults = false,
- vTotal = 0,
- hiddenLabels = fullLayout.hiddenlabels || [],
- i,
- v,
- label,
- color,
- hidden,
- pt;
-
- if(trace.dlabel) {
- labels = new Array(vals.length);
- for(i = 0; i < vals.length; i++) {
- labels[i] = String(trace.label0 + i * trace.dlabel);
- }
- }
-
- for(i = 0; i < vals.length; i++) {
- v = vals[i];
- if(!isNumeric(v)) continue;
- v = +v;
- if(v < 0) continue;
-
- label = labels[i];
- if(label === undefined || label === '') label = i;
- label = String(label);
- // only take the first occurrence of any given label.
- // TODO: perhaps (optionally?) sum values for a repeated label?
- if(allThisTraceLabels[label] === undefined) allThisTraceLabels[label] = true;
- else continue;
-
- color = tinycolor(trace.marker.colors[i]);
- if(color.isValid()) {
- color = Plotly.Color.addOpacity(color, color.getAlpha())
- if(!colorMap[label]) {
- colorMap[label] = color;
- }
- }
- // have we seen this label and assigned a color to it in a previous trace?
- else if(colorMap[label]) color = colorMap[label];
- // color needs a default - mark it false, come back after sorting
- else {
- color = false;
- needDefaults = true;
- }
-
- hidden = hiddenLabels.indexOf(label) !== -1;
-
- if(!hidden) vTotal += v;
-
- cd.push({
- v: v,
- label: label,
- color: color,
- i: i,
- hidden: hidden
- });
- }
-
- if(trace.sort) cd.sort(function(a, b) { return b.v - a.v; });
-
- /**
- * now go back and fill in colors we're still missing
- * this is done after sorting, so we pick defaults
- * in the order slices will be displayed
- */
-
- if(needDefaults) {
- for(i = 0; i < cd.length; i++) {
- pt = cd[i];
- if(pt.color === false) {
- colorMap[pt.label] = pt.color = nextDefaultColor(fullLayout._piedefaultcolorcount);
- fullLayout._piedefaultcolorcount++;
- }
- }
- }
-
- // include the sum of all values in the first point
- if(cd[0]) cd[0].vTotal = vTotal;
-
- // now insert text
- if(trace.textinfo && trace.textinfo !== 'none') {
- var hasLabel = trace.textinfo.indexOf('label') !== -1,
- hasText = trace.textinfo.indexOf('text') !== -1,
- hasValue = trace.textinfo.indexOf('value') !== -1,
- hasPercent = trace.textinfo.indexOf('percent') !== -1,
- thisText;
-
- for(i = 0; i < cd.length; i++) {
- pt = cd[i];
- thisText = hasLabel ? [pt.label] : [];
- if(hasText && trace.text[pt.i]) thisText.push(trace.text[pt.i]);
- if(hasValue) thisText.push(formatPieValue(pt.v));
- if(hasPercent) thisText.push(formatPiePercent(pt.v / vTotal));
- pt.text = thisText.join('
');
- }
- }
-
- return cd;
-};
-
-function formatPiePercent(v) {
- var vRounded = (v * 100).toPrecision(3);
- if(vRounded.indexOf('.') !== -1) return vRounded.replace(/[.]?0+$/,'') + '%';
- return vRounded + '%';
-}
-
-function formatPieValue(v) {
- var vRounded = v.toPrecision(10);
- if(vRounded.indexOf('.') !== -1) return vRounded.replace(/[.]?0+$/,'');
- return vRounded;
-}
-
-/**
- * pick a default color from the main default set, augmented by
- * itself lighter then darker before repeating
- */
-var pieDefaultColors;
-
-function nextDefaultColor(index) {
- if(!pieDefaultColors) {
- // generate this default set on demand (but then it gets saved in the module)
- var mainDefaults = Plotly.Color.defaults;
- pieDefaultColors = mainDefaults.slice();
- for(var i = 0; i < mainDefaults.length; i++) {
- pieDefaultColors.push(tinycolor(mainDefaults[i]).lighten(20).toHexString());
- }
- for(i = 0; i < Plotly.Color.defaults.length; i++) {
- pieDefaultColors.push(tinycolor(mainDefaults[i]).darken(20).toHexString());
- }
- }
-
- return pieDefaultColors[index % pieDefaultColors.length];
-}
-
-pie.plot = function(gd, cdpie) {
- var fullLayout = gd._fullLayout;
-
- scalePies(cdpie, fullLayout._size);
-
- var pieGroups = fullLayout._pielayer.selectAll('g.trace').data(cdpie);
-
- pieGroups.enter().append('g')
- .attr({
- 'stroke-linejoin': 'round', // TODO: miter might look better but can sometimes cause problems
- // maybe miter with a small-ish stroke-miterlimit?
- 'class': 'trace'
- });
- pieGroups.exit().remove();
- pieGroups.order();
-
- pieGroups.each(function(cd) {
- var pieGroup = d3.select(this),
- cd0 = cd[0],
- trace = cd0.trace,
- tiltRads = 0, //trace.tilt * Math.PI / 180,
- depthLength = (trace.depth||0) * cd0.r * Math.sin(tiltRads) / 2,
- tiltAxis = trace.tiltaxis || 0,
- tiltAxisRads = tiltAxis * Math.PI / 180,
- depthVector = [
- depthLength * Math.sin(tiltAxisRads),
- depthLength * Math.cos(tiltAxisRads)
- ],
- rSmall = cd0.r * Math.cos(tiltRads);
-
- var pieParts = pieGroup.selectAll('g.part')
- .data(trace.tilt ? ['top', 'sides'] : ['top']);
-
- pieParts.enter().append('g').attr('class', function(d) {
- return d + ' part';
- });
- pieParts.exit().remove();
- pieParts.order();
-
- setCoords(cd);
-
- pieGroup.selectAll('.top').each(function() {
- var slices = d3.select(this).selectAll('g.slice').data(cd);
-
- slices.enter().append('g')
- .classed('slice', true);
- slices.exit().remove();
-
- var quadrants = [
- [[],[]], // y<0: x<0, x>=0
- [[],[]] // y>=0: x<0, x>=0
- ],
- hasOutsideText = false;
-
- slices.each(function(pt) {
- if(pt.hidden) {
- d3.select(this).selectAll('path,g').remove();
- return;
- }
-
- quadrants[pt.pxmid[1] < 0 ? 0 : 1][pt.pxmid[0] < 0 ? 0 : 1].push(pt);
-
- var cx = cd0.cx + depthVector[0],
- cy = cd0.cy + depthVector[1],
- sliceTop = d3.select(this),
- slicePath = sliceTop.selectAll('path.surface').data([pt]),
- hasHoverData = false;
-
- function handleMouseOver() {
- // in case fullLayout or fullData has changed without a replot
- var fullLayout2 = gd._fullLayout,
- trace2 = gd._fullData[trace.index],
- hoverinfo = trace2.hoverinfo;
-
- if(hoverinfo === 'all') hoverinfo = 'label+text+value+percent+name';
-
- // in case we dragged over the pie from another subplot,
- // or if hover is turned off
- if(gd._dragging || fullLayout2.hovermode === false ||
- hoverinfo === 'none' || !hoverinfo) {
- return;
- }
-
- var rInscribed = getInscribedRadiusFraction(pt, cd0),
- hoverCenterX = cx + pt.pxmid[0] * (1 - rInscribed),
- hoverCenterY = cy + pt.pxmid[1] * (1 - rInscribed),
- thisText = [];
- if(hoverinfo.indexOf('label') !== -1) thisText.push(pt.label);
- if(trace2.text && trace2.text[pt.i] && hoverinfo.indexOf('text') !== -1) {
- thisText.push(trace2.text[pt.i]);
- }
- if(hoverinfo.indexOf('value') !== -1) thisText.push(formatPieValue(pt.v));
- if(hoverinfo.indexOf('percent') !== -1) thisText.push(formatPiePercent(pt.v / cd0.vTotal));
-
- Plotly.Fx.loneHover({
- x0: hoverCenterX - rInscribed * cd0.r,
- x1: hoverCenterX + rInscribed * cd0.r,
- y: hoverCenterY,
- text: thisText.join('
'),
- name: hoverinfo.indexOf('name') !== -1 ? trace2.name : undefined,
- color: pt.color,
- idealAlign: pt.pxmid[0] < 0 ? 'left' : 'right'
- },
- {
- container: fullLayout2._hoverlayer.node(),
- outerContainer: fullLayout2._paper.node()
- }
- );
-
- hasHoverData = true;
- }
-
- function handleMouseOut() {
- if(hasHoverData) {
- Plotly.Fx.loneUnhover(fullLayout._hoverlayer.node());
- hasHoverData = false;
- }
- }
-
- function handleClick (evt) {
- gd._hoverdata = [pt];
- gd._hoverdata.trace = cd.trace;
- Plotly.Fx.click(gd, { target: true });
- }
-
- slicePath.enter().append('path')
- .classed('surface', true)
- .style({'pointer-events': 'all'});
-
- sliceTop.select('path.textline').remove();
-
- sliceTop
- .on('mouseover', handleMouseOver)
- .on('mouseout', handleMouseOut)
- .on('click', handleClick);
-
- if(trace.pull) {
- var pull = +(Array.isArray(trace.pull) ? trace.pull[pt.i] : trace.pull) || 0;
- if(pull > 0) {
- cx += pull * pt.pxmid[0];
- cy += pull * pt.pxmid[1];
- }
- }
-
- pt.cxFinal = cx;
- pt.cyFinal = cy;
-
- function arc(start, finish, cw, scale) {
- return 'a' + (scale * cd0.r) + ',' + (scale * rSmall) + ' ' + tiltAxis + ' ' +
- pt.largeArc + (cw ? ' 1 ' : ' 0 ') +
- (scale * (finish[0] - start[0])) + ',' + (scale * (finish[1] - start[1]));
- }
-
- var hole = trace.hole;
- if(pt.v === cd0.vTotal) { // 100% fails bcs arc start and end are identical
- var outerCircle = 'M' + (cx + pt.px0[0]) + ',' + (cy + pt.px0[1]) +
- arc(pt.px0, pt.pxmid, true, 1) +
- arc(pt.pxmid, pt.px0, true, 1) + 'Z';
- if(hole) {
- slicePath.attr('d',
- 'M' + (cx + hole * pt.px0[0]) + ',' + (cy + hole * pt.px0[1]) +
- arc(pt.px0, pt.pxmid, false, hole) +
- arc(pt.pxmid, pt.px0, false, hole) +
- 'Z' + outerCircle);
- }
- else slicePath.attr('d', outerCircle);
- } else {
-
- var outerArc = arc(pt.px0, pt.px1, true, 1);
-
- if(hole) {
- var rim = 1 - hole;
- slicePath.attr('d',
- 'M' + (cx + hole * pt.px1[0]) + ',' + (cy + hole * pt.px1[1]) +
- arc(pt.px1, pt.px0, false, hole) +
- 'l' + (rim * pt.px0[0]) + ',' + (rim * pt.px0[1]) +
- outerArc +
- 'Z');
- } else {
- slicePath.attr('d',
- 'M' + cx + ',' + cy +
- 'l' + pt.px0[0] + ',' + pt.px0[1] +
- outerArc +
- 'Z');
- }
- }
-
- // add text
- var textPosition = Array.isArray(trace.textposition) ?
- trace.textposition[pt.i] : trace.textposition,
- sliceTextGroup = sliceTop.selectAll('g.slicetext')
- .data(pt.text && (textPosition !== 'none') ? [0] : []);
-
- sliceTextGroup.enter().append('g')
- .classed('slicetext', true);
- sliceTextGroup.exit().remove();
-
- sliceTextGroup.each(function() {
- var sliceText = d3.select(this).selectAll('text').data([0]);
-
- sliceText.enter().append('text')
- // prohibit tex interpretation until we can handle
- // tex and regular text together
- .attr('data-notex', 1);
- sliceText.exit().remove();
-
- sliceText.text(pt.text)
- .attr({
- 'class': 'slicetext',
- transform: '',
- 'data-bb': '',
- 'text-anchor': 'middle',
- x: 0,
- y: 0
- })
- .call(Plotly.Drawing.font, textPosition === 'outside' ?
- trace.outsidetextfont : trace.insidetextfont)
- .call(Plotly.util.convertToTspans);
- sliceText.selectAll('tspan.line').attr({x: 0, y: 0});
-
- // position the text relative to the slice
- // TODO: so far this only accounts for flat
- var textBB = Plotly.Drawing.bBox(sliceText.node()),
- transform;
-
- if(textPosition === 'outside') {
- transform = transformOutsideText(textBB, pt);
- } else {
- transform = transformInsideText(textBB, pt, cd0);
- if(textPosition === 'auto' && transform.scale < 1) {
- sliceText.call(Plotly.Drawing.font, trace.outsidetextfont);
- if(trace.outsidetextfont.family !== trace.insidetextfont.family ||
- trace.outsidetextfont.size !== trace.insidetextfont.size) {
- sliceText.attr({'data-bb': ''});
- textBB = Plotly.Drawing.bBox(sliceText.node());
- }
- transform = transformOutsideText(textBB, pt);
- }
- }
-
- var translateX = cx + pt.pxmid[0] * transform.rCenter + (transform.x || 0),
- translateY = cy + pt.pxmid[1] * transform.rCenter + (transform.y || 0);
-
- // save some stuff to use later ensure no labels overlap
- if(transform.outside) {
- pt.yLabelMin = translateY - textBB.height / 2;
- pt.yLabelMid = translateY;
- pt.yLabelMax = translateY + textBB.height / 2;
- pt.labelExtraX = 0;
- pt.labelExtraY = 0;
- hasOutsideText = true;
- }
-
- sliceText.attr('transform',
- 'translate(' + translateX + ',' + translateY + ')' +
- (transform.scale < 1 ? ('scale(' + transform.scale + ')') : '') +
- (transform.rotate ? ('rotate(' + transform.rotate + ')') : '') +
- 'translate(' +
- (-(textBB.left + textBB.right) / 2) + ',' +
- (-(textBB.top + textBB.bottom) / 2) +
- ')');
- });
- });
-
- // now make sure no labels overlap (at least within one pie)
- if(hasOutsideText) scootLabels(quadrants, trace);
- slices.each(function(pt) {
- if(pt.labelExtraX || pt.labelExtraY) {
- // first move the text to its new location
- var sliceTop = d3.select(this),
- sliceText = sliceTop.select('g.slicetext text');
-
- sliceText.attr('transform', 'translate(' + pt.labelExtraX + ',' + pt.labelExtraY + ')' +
- sliceText.attr('transform'));
-
- // then add a line to the new location
- var lineStartX = pt.cxFinal + pt.pxmid[0],
- lineStartY = pt.cyFinal + pt.pxmid[1],
- textLinePath = 'M' + lineStartX + ',' + lineStartY,
- finalX = (pt.yLabelMax - pt.yLabelMin) * (pt.pxmid[0] < 0 ? -1 : 1) / 4;
- if(pt.labelExtraX) {
- var yFromX = pt.labelExtraX * pt.pxmid[1] / pt.pxmid[0],
- yNet = pt.yLabelMid + pt.labelExtraY - (pt.cyFinal + pt.pxmid[1]);
-
- if(Math.abs(yFromX) > Math.abs(yNet)) {
- textLinePath +=
- 'l' + (yNet * pt.pxmid[0] / pt.pxmid[1]) + ',' + yNet +
- 'H' + (lineStartX + pt.labelExtraX + finalX);
- } else {
- textLinePath += 'l' + pt.labelExtraX + ',' + yFromX +
- 'v' + (yNet - yFromX) +
- 'h' + finalX;
- }
- } else {
- textLinePath +=
- 'V' + (pt.yLabelMid + pt.labelExtraY) +
- 'h' + finalX;
- }
-
- sliceTop.append('path')
- .classed('textline', true)
- .call(Plotly.Color.stroke, trace.outsidetextfont.color)
- .attr({
- 'stroke-width': Math.min(2, trace.outsidetextfont.size / 8),
- d: textLinePath,
- fill: 'none'
- });
- }
- });
- });
- });
-
- // This is for a bug in Chrome (as of 2015-07-22, and does not affect FF)
- // if insidetextfont and outsidetextfont are different sizes, sometimes the size
- // of an "em" gets taken from the wrong element at first so lines are
- // spaced wrong. You just have to tell it to try again later and it gets fixed.
- // I have no idea why we haven't seen this in other contexts. Also, sometimes
- // it gets the initial draw correct but on redraw it gets confused.
- setTimeout(function() {
- pieGroups.selectAll('tspan').each(function() {
- var s = d3.select(this);
- if(s.attr('dy')) s.attr('dy', s.attr('dy'));
- });
- }, 0);
-};
-
-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);
-}
-
-function transformInsideText(textBB, pt, cd0) {
- var textDiameter = Math.sqrt(textBB.width * textBB.width + textBB.height * textBB.height),
- textAspect = textBB.width / textBB.height,
- halfAngle = Math.PI * Math.min(pt.v / cd0.vTotal, 0.5),
- ring = 1 - cd0.trace.hole,
- rInscribed = getInscribedRadiusFraction(pt, cd0),
-
- // 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
- transform = {
- scale: rInscribed * cd0.r * 2 / textDiameter,
-
- // and the center position and rotation in this case
- rCenter: 1 - rInscribed,
- rotate: 0
- };
-
- if(transform.scale >= 1) return transform;
-
- // max size if text is rotated radially
- var Qr = textAspect + 1 / (2 * Math.tan(halfAngle)),
- maxHalfHeightRotRadial = cd0.r * Math.min(
- 1 / (Math.sqrt(Qr * Qr + 0.5) + Qr),
- ring / (Math.sqrt(textAspect * textAspect + ring / 2) + textAspect)
- ),
- radialTransform = {
- scale: maxHalfHeightRotRadial * 2 / textBB.height,
- rCenter: Math.cos(maxHalfHeightRotRadial / cd0.r) -
- maxHalfHeightRotRadial * textAspect / cd0.r,
- rotate: (180 / Math.PI * pt.midangle + 720) % 180 - 90
- },
-
- // max size if text is rotated tangentially
- aspectInv = 1 / textAspect,
- Qt = aspectInv + 1 / (2 * Math.tan(halfAngle)),
- maxHalfWidthTangential = cd0.r * Math.min(
- 1 / (Math.sqrt(Qt * Qt + 0.5) + Qt),
- ring / (Math.sqrt(aspectInv * aspectInv + ring / 2) + aspectInv)
- ),
- tangentialTransform = {
- scale: maxHalfWidthTangential * 2 / textBB.width,
- rCenter: Math.cos(maxHalfWidthTangential / cd0.r) -
- maxHalfWidthTangential / textAspect / cd0.r,
- rotate: (180 / Math.PI * pt.midangle + 810) % 180 - 90
- },
- // if we need a rotated transform, pick the biggest one
- // even if both are bigger than 1
- rotatedTransform = tangentialTransform.scale > radialTransform.scale ?
- tangentialTransform : radialTransform;
-
- if(transform.scale < 1 && rotatedTransform.scale > transform.scale) return rotatedTransform;
- return transform;
-}
-
-function transformOutsideText(textBB, pt) {
- var x = pt.pxmid[0],
- y = pt.pxmid[1],
- dx = textBB.width / 2,
- dy = textBB.height / 2;
-
- if(x < 0) dx *= -1;
- if(y < 0) dy *= -1;
-
- return {
- scale: 1,
- rCenter: 1,
- rotate: 0,
- x: dx + Math.abs(dy) * (dx > 0 ? 1 : -1) / 2,
- y: dy / (1 + x * x / (y * y)),
- outside: true
- };
-}
-
-function scootLabels(quadrants, trace) {
- var xHalf,
- yHalf,
- equatorFirst,
- farthestX,
- farthestY,
- xDiffSign,
- yDiffSign,
- thisQuad,
- oppositeQuad,
- wholeSide,
- i,
- thisQuadOutside,
- firstOppositeOutsidePt;
-
- function topFirst (a, b) { return a.pxmid[1] - b.pxmid[1]; }
- function bottomFirst (a, b) { return b.pxmid[1] - a.pxmid[1]; }
-
- function scootOneLabel(thisPt, prevPt) {
- if(!prevPt) prevPt = {};
-
- var prevOuterY = prevPt.labelExtraY + (yHalf ? prevPt.yLabelMax : prevPt.yLabelMin),
- thisInnerY = yHalf ? thisPt.yLabelMin : thisPt.yLabelMax,
- thisOuterY = yHalf ? thisPt.yLabelMax : thisPt.yLabelMin,
- thisSliceOuterY = thisPt.cyFinal + farthestY(thisPt.px0[1], thisPt.px1[1]),
- newExtraY = prevOuterY - thisInnerY,
- xBuffer,
- i,
- otherPt,
- otherOuterY,
- otherOuterX,
- newExtraX;
- // make sure this label doesn't overlap other labels
- // this *only* has us move these labels vertically
- if(newExtraY * yDiffSign > 0) thisPt.labelExtraY = newExtraY;
-
- // make sure this label doesn't overlap any slices
- if(!Array.isArray(trace.pull)) return; // this can only happen with array pulls
-
- for(i = 0; i < wholeSide.length; i++) {
- otherPt = wholeSide[i];
-
- // overlap can only happen if the other point is pulled more than this one
- if(otherPt === thisPt || ((trace.pull[thisPt.i] || 0) >= trace.pull[otherPt.i] || 0)) continue;
-
- if((thisPt.pxmid[1] - otherPt.pxmid[1]) * yDiffSign > 0) {
- // closer to the equator - by construction all of these happen first
- // move the text vertically to get away from these slices
- otherOuterY = otherPt.cyFinal + farthestY(otherPt.px0[1], otherPt.px1[1]);
- newExtraY = otherOuterY - thisInnerY - thisPt.labelExtraY;
-
- if(newExtraY * yDiffSign > 0) thisPt.labelExtraY += newExtraY;
-
- } else if((thisOuterY + thisPt.labelExtraY - thisSliceOuterY) * yDiffSign > 0) {
- // farther from the equator - happens after we've done all the
- // vertical moving we're going to do
- // move horizontally to get away from these more polar slices
-
- // if we're moving horz. based on a slice that's several slices away from this one
- // then we need some extra space for the lines to labels between them
- xBuffer = 3 * xDiffSign * Math.abs(i - wholeSide.indexOf(thisPt));
-
- otherOuterX = otherPt.cxFinal + farthestX(otherPt.px0[0], otherPt.px1[0]);
- newExtraX = otherOuterX + xBuffer - (thisPt.cxFinal + thisPt.pxmid[0]) - thisPt.labelExtraX;
-
- if(newExtraX * xDiffSign > 0) thisPt.labelExtraX += newExtraX;
- }
- }
- }
-
- for(yHalf = 0; yHalf < 2; yHalf++) {
- equatorFirst = yHalf ? topFirst : bottomFirst;
- farthestY = yHalf ? Math.max : Math.min;
- yDiffSign = yHalf ? 1 : -1;
-
- for(xHalf = 0; xHalf < 2; xHalf++) {
- farthestX = xHalf ? Math.max : Math.min;
- xDiffSign = xHalf ? 1 : -1;
-
- // first sort the array
- // note this is a copy of cd, so cd itself doesn't get sorted
- // but we can still modify points in place.
- thisQuad = quadrants[yHalf][xHalf];
- thisQuad.sort(equatorFirst);
-
- oppositeQuad = quadrants[1 - yHalf][xHalf];
- wholeSide = oppositeQuad.concat(thisQuad);
-
- thisQuadOutside = [];
- for(i = 0; i < thisQuad.length; i++) {
- if(thisQuad[i].yLabelMid !== undefined) thisQuadOutside.push(thisQuad[i]);
- }
-
- firstOppositeOutsidePt = false;
- for(i = 0; yHalf && i < oppositeQuad.length; i++) {
- if(oppositeQuad[i].yLabelMid !== undefined) {
- firstOppositeOutsidePt = oppositeQuad[i];
- break;
- }
- }
-
- // each needs to avoid the previous
- for(i = 0; i < thisQuadOutside.length; i++) {
- var prevPt = i && thisQuadOutside[i - 1];
- // bottom half needs to avoid the first label of the top half
- // top half we still need to call scootOneLabel on the first slice
- // so we can avoid other slices, but we don't pass a prevPt
- if(firstOppositeOutsidePt && !i) prevPt = firstOppositeOutsidePt;
- scootOneLabel(thisQuadOutside[i], prevPt);
- }
- }
- }
-}
-
-function scalePies(cdpie, plotSize) {
- var pieBoxWidth,
- pieBoxHeight,
- i,
- j,
- cd0,
- trace,
- tiltAxisRads,
- maxPull,
- scaleGroups = [],
- scaleGroup,
- minPxPerValUnit;
-
- // first figure out the center and maximum radius for each pie
- for(i = 0; i < cdpie.length; i++) {
- cd0 = cdpie[i][0];
- trace = cd0.trace;
- pieBoxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]);
- pieBoxHeight = plotSize.h * (trace.domain.y[1] - trace.domain.y[0]);
- tiltAxisRads = trace.tiltaxis * Math.PI / 180;
-
- maxPull = trace.pull;
- if(Array.isArray(maxPull)) {
- maxPull = 0;
- for(j = 0; j < trace.pull.length; j++) {
- if(trace.pull[j] > maxPull) maxPull = trace.pull[j];
- }
- }
-
- cd0.r = Math.min(
- pieBoxWidth / maxExtent(trace.tilt, Math.sin(tiltAxisRads), trace.depth),
- pieBoxHeight / maxExtent(trace.tilt, Math.cos(tiltAxisRads), trace.depth)
- ) / (2 + 2 * maxPull);
-
- cd0.cx = plotSize.l + plotSize.w * (trace.domain.x[1] + trace.domain.x[0])/2;
- cd0.cy = plotSize.t + plotSize.h * (2 - trace.domain.y[1] - trace.domain.y[0])/2;
-
- if(trace.scalegroup && scaleGroups.indexOf(trace.scalegroup) === -1) {
- scaleGroups.push(trace.scalegroup);
- }
- }
-
- // Then scale any pies that are grouped
- for(j = 0; j < scaleGroups.length; j++) {
- minPxPerValUnit = Infinity;
- scaleGroup = scaleGroups[j];
-
- for(i = 0; i < cdpie.length; i++) {
- cd0 = cdpie[i][0];
- if(cd0.trace.scalegroup === scaleGroup) {
- minPxPerValUnit = Math.min(minPxPerValUnit,
- cd0.r * cd0.r / cd0.vTotal);
- }
- }
-
- for(i = 0; i < cdpie.length; i++) {
- cd0 = cdpie[i][0];
- if(cd0.trace.scalegroup === scaleGroup) {
- cd0.r = Math.sqrt(minPxPerValUnit * cd0.vTotal);
- }
- }
- }
-
-}
-
-function setCoords(cd) {
- var cd0 = cd[0],
- trace = cd0.trace,
- tilt = trace.tilt,
- tiltAxisRads,
- tiltAxisSin,
- tiltAxisCos,
- tiltRads,
- crossTilt,
- inPlane,
- currentAngle = trace.rotation * Math.PI / 180,
- angleFactor = 2 * Math.PI / cd0.vTotal,
- firstPt = 'px0',
- lastPt = 'px1',
- i,
- cdi,
- currentCoords;
-
- if(trace.direction === 'counterclockwise') {
- for(i = 0; i < cd.length; i++) {
- if(!cd[i].hidden) break; // find the first non-hidden slice
- }
- if(i === cd.length) return; // all slices hidden
-
- currentAngle += angleFactor * cd[i].v;
- angleFactor *= -1;
- firstPt = 'px1';
- lastPt = 'px0';
- }
-
- if(tilt) {
- tiltRads = tilt * Math.PI / 180;
- tiltAxisRads = trace.tiltaxis * Math.PI / 180;
- crossTilt = Math.sin(tiltAxisRads) * Math.cos(tiltAxisRads);
- inPlane = 1 - Math.cos(tiltRads);
- tiltAxisSin = Math.sin(tiltAxisRads);
- tiltAxisCos = Math.cos(tiltAxisRads);
- }
-
- function getCoords(angle) {
- var xFlat = cd0.r * Math.sin(angle),
- yFlat = -cd0.r * Math.cos(angle);
-
- if(!tilt) return [xFlat, yFlat];
-
- return [
- xFlat * (1 - inPlane * tiltAxisSin * tiltAxisSin) + yFlat * crossTilt * inPlane,
- xFlat * crossTilt * inPlane + yFlat * (1 - inPlane * tiltAxisCos * tiltAxisCos),
- Math.sin(tiltRads) * (yFlat * tiltAxisCos - xFlat * tiltAxisSin)
- ];
- }
-
- currentCoords = getCoords(currentAngle);
-
- for(i = 0; i < cd.length; i++) {
- cdi = cd[i];
- if(cdi.hidden) continue;
-
- cdi[firstPt] = currentCoords;
-
- currentAngle += angleFactor * cdi.v / 2;
- cdi.pxmid = getCoords(currentAngle);
- cdi.midangle = currentAngle;
-
- currentAngle += angleFactor * cdi.v / 2;
- currentCoords = getCoords(currentAngle);
-
- cdi[lastPt] = currentCoords;
-
- cdi.largeArc = (cdi.v > cd0.vTotal / 2) ? 1 : 0;
- }
-}
-
-function maxExtent(tilt, tiltAxisFraction, depth) {
- if(!tilt) return 1;
- var sinTilt = Math.sin(tilt * Math.PI / 180);
- return Math.max(0.01, // don't let it go crazy if you tilt the pie totally on its side
- depth * sinTilt * Math.abs(tiltAxisFraction) +
- 2 * Math.sqrt(1 - sinTilt * sinTilt * tiltAxisFraction * tiltAxisFraction));
-}
-
-pie.style = function(gd) {
- gd._fullLayout._pielayer.selectAll('.trace').each(function(cd) {
- var cd0 = cd[0],
- trace = cd0.trace,
- traceSelection = d3.select(this);
-
- traceSelection.style({opacity: trace.opacity});
-
- traceSelection.selectAll('.top path.surface').each(function(pt) {
- d3.select(this).call(pie.styleOne, pt, trace);
- });
- });
-};
-
-pie.styleOne = function(s, pt, trace) {
- var lineColor = trace.marker.line.color;
- if(Array.isArray(lineColor)) lineColor = lineColor[pt.i] || Plotly.Color.defaultLine;
-
- var lineWidth = trace.marker.line.width || 0;
- if(Array.isArray(lineWidth)) lineWidth = lineWidth[pt.i] || 0;
-
- s.style({
- 'stroke-width': lineWidth,
- fill: pt.color
- })
- .call(Plotly.Color.stroke, lineColor);
-};
+exports.attributes = require('./attributes');
+exports.supplyDefaults = require('./defaults');
+exports.supplyLayoutDefaults = require('./layout_defaults');
+exports.layoutAttributes = require('./layout_attributes');
+exports.calc = require('./calc');
+exports.plot = require('./plot');
+exports.style = require('./style');
+exports.styleOne = require('./style_one');
diff --git a/src/traces/pie/layout_attributes.js b/src/traces/pie/layout_attributes.js
new file mode 100644
index 00000000000..bd15424d0de
--- /dev/null
+++ b/src/traces/pie/layout_attributes.js
@@ -0,0 +1,18 @@
+/**
+* Copyright 2012-2016, 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 = {
+ /**
+ * hiddenlabels is the pie chart analog of visible:'legendonly'
+ * but it can contain many labels, and can hide slices
+ * from several pies simultaneously
+ */
+ hiddenlabels: {valType: 'data_array'}
+};
diff --git a/src/traces/pie/layout_defaults.js b/src/traces/pie/layout_defaults.js
new file mode 100644
index 00000000000..f5f59b29647
--- /dev/null
+++ b/src/traces/pie/layout_defaults.js
@@ -0,0 +1,20 @@
+/**
+* Copyright 2012-2016, 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 Lib = require('../../lib');
+
+var layoutAttributes = require('./layout_attributes');
+
+module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) {
+ function coerce(attr, dflt) {
+ return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt);
+ }
+ coerce('hiddenlabels');
+};
diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js
new file mode 100644
index 00000000000..264902a03e2
--- /dev/null
+++ b/src/traces/pie/plot.js
@@ -0,0 +1,684 @@
+/**
+* Copyright 2012-2016, 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 d3 = require('d3');
+
+var Plotly = require('../../plotly');
+var Color = require('../../components/color');
+var helpers = require('./helpers');
+
+module.exports = function plot(gd, cdpie) {
+ var fullLayout = gd._fullLayout;
+
+ scalePies(cdpie, fullLayout._size);
+
+ var pieGroups = fullLayout._pielayer.selectAll('g.trace').data(cdpie);
+
+ pieGroups.enter().append('g')
+ .attr({
+ 'stroke-linejoin': 'round', // TODO: miter might look better but can sometimes cause problems
+ // maybe miter with a small-ish stroke-miterlimit?
+ 'class': 'trace'
+ });
+ pieGroups.exit().remove();
+ pieGroups.order();
+
+ pieGroups.each(function(cd) {
+ var pieGroup = d3.select(this),
+ cd0 = cd[0],
+ trace = cd0.trace,
+ tiltRads = 0, //trace.tilt * Math.PI / 180,
+ depthLength = (trace.depth||0) * cd0.r * Math.sin(tiltRads) / 2,
+ tiltAxis = trace.tiltaxis || 0,
+ tiltAxisRads = tiltAxis * Math.PI / 180,
+ depthVector = [
+ depthLength * Math.sin(tiltAxisRads),
+ depthLength * Math.cos(tiltAxisRads)
+ ],
+ rSmall = cd0.r * Math.cos(tiltRads);
+
+ var pieParts = pieGroup.selectAll('g.part')
+ .data(trace.tilt ? ['top', 'sides'] : ['top']);
+
+ pieParts.enter().append('g').attr('class', function(d) {
+ return d + ' part';
+ });
+ pieParts.exit().remove();
+ pieParts.order();
+
+ setCoords(cd);
+
+ pieGroup.selectAll('.top').each(function() {
+ var slices = d3.select(this).selectAll('g.slice').data(cd);
+
+ slices.enter().append('g')
+ .classed('slice', true);
+ slices.exit().remove();
+
+ var quadrants = [
+ [[],[]], // y<0: x<0, x>=0
+ [[],[]] // y>=0: x<0, x>=0
+ ],
+ hasOutsideText = false;
+
+ slices.each(function(pt) {
+ if(pt.hidden) {
+ d3.select(this).selectAll('path,g').remove();
+ return;
+ }
+
+ quadrants[pt.pxmid[1] < 0 ? 0 : 1][pt.pxmid[0] < 0 ? 0 : 1].push(pt);
+
+ var cx = cd0.cx + depthVector[0],
+ cy = cd0.cy + depthVector[1],
+ sliceTop = d3.select(this),
+ slicePath = sliceTop.selectAll('path.surface').data([pt]),
+ hasHoverData = false;
+
+ function handleMouseOver() {
+ // in case fullLayout or fullData has changed without a replot
+ var fullLayout2 = gd._fullLayout,
+ trace2 = gd._fullData[trace.index],
+ hoverinfo = trace2.hoverinfo;
+
+ if(hoverinfo === 'all') hoverinfo = 'label+text+value+percent+name';
+
+ // in case we dragged over the pie from another subplot,
+ // or if hover is turned off
+ if(gd._dragging || fullLayout2.hovermode === false ||
+ hoverinfo === 'none' || !hoverinfo) {
+ return;
+ }
+
+ var rInscribed = getInscribedRadiusFraction(pt, cd0),
+ hoverCenterX = cx + pt.pxmid[0] * (1 - rInscribed),
+ hoverCenterY = cy + pt.pxmid[1] * (1 - rInscribed),
+ thisText = [];
+ if(hoverinfo.indexOf('label') !== -1) thisText.push(pt.label);
+ if(trace2.text && trace2.text[pt.i] && hoverinfo.indexOf('text') !== -1) {
+ thisText.push(trace2.text[pt.i]);
+ }
+ if(hoverinfo.indexOf('value') !== -1) thisText.push(helpers.formatPieValue(pt.v));
+ if(hoverinfo.indexOf('percent') !== -1) thisText.push(helpers.formatPiePercent(pt.v / cd0.vTotal));
+
+ Plotly.Fx.loneHover({
+ x0: hoverCenterX - rInscribed * cd0.r,
+ x1: hoverCenterX + rInscribed * cd0.r,
+ y: hoverCenterY,
+ text: thisText.join('
'),
+ name: hoverinfo.indexOf('name') !== -1 ? trace2.name : undefined,
+ color: pt.color,
+ idealAlign: pt.pxmid[0] < 0 ? 'left' : 'right'
+ },
+ {
+ container: fullLayout2._hoverlayer.node(),
+ outerContainer: fullLayout2._paper.node()
+ }
+ );
+
+ hasHoverData = true;
+ }
+
+ function handleMouseOut() {
+ if(hasHoverData) {
+ Plotly.Fx.loneUnhover(fullLayout._hoverlayer.node());
+ hasHoverData = false;
+ }
+ }
+
+ function handleClick () {
+ gd._hoverdata = [pt];
+ gd._hoverdata.trace = cd.trace;
+ Plotly.Fx.click(gd, { target: true });
+ }
+
+ slicePath.enter().append('path')
+ .classed('surface', true)
+ .style({'pointer-events': 'all'});
+
+ sliceTop.select('path.textline').remove();
+
+ sliceTop
+ .on('mouseover', handleMouseOver)
+ .on('mouseout', handleMouseOut)
+ .on('click', handleClick);
+
+ if(trace.pull) {
+ var pull = +(Array.isArray(trace.pull) ? trace.pull[pt.i] : trace.pull) || 0;
+ if(pull > 0) {
+ cx += pull * pt.pxmid[0];
+ cy += pull * pt.pxmid[1];
+ }
+ }
+
+ pt.cxFinal = cx;
+ pt.cyFinal = cy;
+
+ function arc(start, finish, cw, scale) {
+ return 'a' + (scale * cd0.r) + ',' + (scale * rSmall) + ' ' + tiltAxis + ' ' +
+ pt.largeArc + (cw ? ' 1 ' : ' 0 ') +
+ (scale * (finish[0] - start[0])) + ',' + (scale * (finish[1] - start[1]));
+ }
+
+ var hole = trace.hole;
+ if(pt.v === cd0.vTotal) { // 100% fails bcs arc start and end are identical
+ var outerCircle = 'M' + (cx + pt.px0[0]) + ',' + (cy + pt.px0[1]) +
+ arc(pt.px0, pt.pxmid, true, 1) +
+ arc(pt.pxmid, pt.px0, true, 1) + 'Z';
+ if(hole) {
+ slicePath.attr('d',
+ 'M' + (cx + hole * pt.px0[0]) + ',' + (cy + hole * pt.px0[1]) +
+ arc(pt.px0, pt.pxmid, false, hole) +
+ arc(pt.pxmid, pt.px0, false, hole) +
+ 'Z' + outerCircle);
+ }
+ else slicePath.attr('d', outerCircle);
+ } else {
+
+ var outerArc = arc(pt.px0, pt.px1, true, 1);
+
+ if(hole) {
+ var rim = 1 - hole;
+ slicePath.attr('d',
+ 'M' + (cx + hole * pt.px1[0]) + ',' + (cy + hole * pt.px1[1]) +
+ arc(pt.px1, pt.px0, false, hole) +
+ 'l' + (rim * pt.px0[0]) + ',' + (rim * pt.px0[1]) +
+ outerArc +
+ 'Z');
+ } else {
+ slicePath.attr('d',
+ 'M' + cx + ',' + cy +
+ 'l' + pt.px0[0] + ',' + pt.px0[1] +
+ outerArc +
+ 'Z');
+ }
+ }
+
+ // add text
+ var textPosition = Array.isArray(trace.textposition) ?
+ trace.textposition[pt.i] : trace.textposition,
+ sliceTextGroup = sliceTop.selectAll('g.slicetext')
+ .data(pt.text && (textPosition !== 'none') ? [0] : []);
+
+ sliceTextGroup.enter().append('g')
+ .classed('slicetext', true);
+ sliceTextGroup.exit().remove();
+
+ sliceTextGroup.each(function() {
+ var sliceText = d3.select(this).selectAll('text').data([0]);
+
+ sliceText.enter().append('text')
+ // prohibit tex interpretation until we can handle
+ // tex and regular text together
+ .attr('data-notex', 1);
+ sliceText.exit().remove();
+
+ sliceText.text(pt.text)
+ .attr({
+ 'class': 'slicetext',
+ transform: '',
+ 'data-bb': '',
+ 'text-anchor': 'middle',
+ x: 0,
+ y: 0
+ })
+ .call(Plotly.Drawing.font, textPosition === 'outside' ?
+ trace.outsidetextfont : trace.insidetextfont)
+ .call(Plotly.util.convertToTspans);
+ sliceText.selectAll('tspan.line').attr({x: 0, y: 0});
+
+ // position the text relative to the slice
+ // TODO: so far this only accounts for flat
+ var textBB = Plotly.Drawing.bBox(sliceText.node()),
+ transform;
+
+ if(textPosition === 'outside') {
+ transform = transformOutsideText(textBB, pt);
+ } else {
+ transform = transformInsideText(textBB, pt, cd0);
+ if(textPosition === 'auto' && transform.scale < 1) {
+ sliceText.call(Plotly.Drawing.font, trace.outsidetextfont);
+ if(trace.outsidetextfont.family !== trace.insidetextfont.family ||
+ trace.outsidetextfont.size !== trace.insidetextfont.size) {
+ sliceText.attr({'data-bb': ''});
+ textBB = Plotly.Drawing.bBox(sliceText.node());
+ }
+ transform = transformOutsideText(textBB, pt);
+ }
+ }
+
+ var translateX = cx + pt.pxmid[0] * transform.rCenter + (transform.x || 0),
+ translateY = cy + pt.pxmid[1] * transform.rCenter + (transform.y || 0);
+
+ // save some stuff to use later ensure no labels overlap
+ if(transform.outside) {
+ pt.yLabelMin = translateY - textBB.height / 2;
+ pt.yLabelMid = translateY;
+ pt.yLabelMax = translateY + textBB.height / 2;
+ pt.labelExtraX = 0;
+ pt.labelExtraY = 0;
+ hasOutsideText = true;
+ }
+
+ sliceText.attr('transform',
+ 'translate(' + translateX + ',' + translateY + ')' +
+ (transform.scale < 1 ? ('scale(' + transform.scale + ')') : '') +
+ (transform.rotate ? ('rotate(' + transform.rotate + ')') : '') +
+ 'translate(' +
+ (-(textBB.left + textBB.right) / 2) + ',' +
+ (-(textBB.top + textBB.bottom) / 2) +
+ ')');
+ });
+ });
+
+ // now make sure no labels overlap (at least within one pie)
+ if(hasOutsideText) scootLabels(quadrants, trace);
+ slices.each(function(pt) {
+ if(pt.labelExtraX || pt.labelExtraY) {
+ // first move the text to its new location
+ var sliceTop = d3.select(this),
+ sliceText = sliceTop.select('g.slicetext text');
+
+ sliceText.attr('transform', 'translate(' + pt.labelExtraX + ',' + pt.labelExtraY + ')' +
+ sliceText.attr('transform'));
+
+ // then add a line to the new location
+ var lineStartX = pt.cxFinal + pt.pxmid[0],
+ lineStartY = pt.cyFinal + pt.pxmid[1],
+ textLinePath = 'M' + lineStartX + ',' + lineStartY,
+ finalX = (pt.yLabelMax - pt.yLabelMin) * (pt.pxmid[0] < 0 ? -1 : 1) / 4;
+ if(pt.labelExtraX) {
+ var yFromX = pt.labelExtraX * pt.pxmid[1] / pt.pxmid[0],
+ yNet = pt.yLabelMid + pt.labelExtraY - (pt.cyFinal + pt.pxmid[1]);
+
+ if(Math.abs(yFromX) > Math.abs(yNet)) {
+ textLinePath +=
+ 'l' + (yNet * pt.pxmid[0] / pt.pxmid[1]) + ',' + yNet +
+ 'H' + (lineStartX + pt.labelExtraX + finalX);
+ } else {
+ textLinePath += 'l' + pt.labelExtraX + ',' + yFromX +
+ 'v' + (yNet - yFromX) +
+ 'h' + finalX;
+ }
+ } else {
+ textLinePath +=
+ 'V' + (pt.yLabelMid + pt.labelExtraY) +
+ 'h' + finalX;
+ }
+
+ sliceTop.append('path')
+ .classed('textline', true)
+ .call(Color.stroke, trace.outsidetextfont.color)
+ .attr({
+ 'stroke-width': Math.min(2, trace.outsidetextfont.size / 8),
+ d: textLinePath,
+ fill: 'none'
+ });
+ }
+ });
+ });
+ });
+
+ // This is for a bug in Chrome (as of 2015-07-22, and does not affect FF)
+ // if insidetextfont and outsidetextfont are different sizes, sometimes the size
+ // of an "em" gets taken from the wrong element at first so lines are
+ // spaced wrong. You just have to tell it to try again later and it gets fixed.
+ // I have no idea why we haven't seen this in other contexts. Also, sometimes
+ // it gets the initial draw correct but on redraw it gets confused.
+ setTimeout(function() {
+ pieGroups.selectAll('tspan').each(function() {
+ var s = d3.select(this);
+ if(s.attr('dy')) s.attr('dy', s.attr('dy'));
+ });
+ }, 0);
+};
+
+
+function transformInsideText(textBB, pt, cd0) {
+ var textDiameter = Math.sqrt(textBB.width * textBB.width + textBB.height * textBB.height),
+ textAspect = textBB.width / textBB.height,
+ halfAngle = Math.PI * Math.min(pt.v / cd0.vTotal, 0.5),
+ ring = 1 - cd0.trace.hole,
+ rInscribed = getInscribedRadiusFraction(pt, cd0),
+
+ // 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
+ transform = {
+ scale: rInscribed * cd0.r * 2 / textDiameter,
+
+ // and the center position and rotation in this case
+ rCenter: 1 - rInscribed,
+ rotate: 0
+ };
+
+ if(transform.scale >= 1) return transform;
+
+ // max size if text is rotated radially
+ var Qr = textAspect + 1 / (2 * Math.tan(halfAngle)),
+ maxHalfHeightRotRadial = cd0.r * Math.min(
+ 1 / (Math.sqrt(Qr * Qr + 0.5) + Qr),
+ ring / (Math.sqrt(textAspect * textAspect + ring / 2) + textAspect)
+ ),
+ radialTransform = {
+ scale: maxHalfHeightRotRadial * 2 / textBB.height,
+ rCenter: Math.cos(maxHalfHeightRotRadial / cd0.r) -
+ maxHalfHeightRotRadial * textAspect / cd0.r,
+ rotate: (180 / Math.PI * pt.midangle + 720) % 180 - 90
+ },
+
+ // max size if text is rotated tangentially
+ aspectInv = 1 / textAspect,
+ Qt = aspectInv + 1 / (2 * Math.tan(halfAngle)),
+ maxHalfWidthTangential = cd0.r * Math.min(
+ 1 / (Math.sqrt(Qt * Qt + 0.5) + Qt),
+ ring / (Math.sqrt(aspectInv * aspectInv + ring / 2) + aspectInv)
+ ),
+ tangentialTransform = {
+ scale: maxHalfWidthTangential * 2 / textBB.width,
+ rCenter: Math.cos(maxHalfWidthTangential / cd0.r) -
+ maxHalfWidthTangential / textAspect / cd0.r,
+ rotate: (180 / Math.PI * pt.midangle + 810) % 180 - 90
+ },
+ // if we need a rotated transform, pick the biggest one
+ // even if both are bigger than 1
+ rotatedTransform = tangentialTransform.scale > radialTransform.scale ?
+ tangentialTransform : radialTransform;
+
+ if(transform.scale < 1 && rotatedTransform.scale > transform.scale) return rotatedTransform;
+ return transform;
+}
+
+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);
+}
+
+function transformOutsideText(textBB, pt) {
+ var x = pt.pxmid[0],
+ y = pt.pxmid[1],
+ dx = textBB.width / 2,
+ dy = textBB.height / 2;
+
+ if(x < 0) dx *= -1;
+ if(y < 0) dy *= -1;
+
+ return {
+ scale: 1,
+ rCenter: 1,
+ rotate: 0,
+ x: dx + Math.abs(dy) * (dx > 0 ? 1 : -1) / 2,
+ y: dy / (1 + x * x / (y * y)),
+ outside: true
+ };
+}
+
+function scootLabels(quadrants, trace) {
+ var xHalf,
+ yHalf,
+ equatorFirst,
+ farthestX,
+ farthestY,
+ xDiffSign,
+ yDiffSign,
+ thisQuad,
+ oppositeQuad,
+ wholeSide,
+ i,
+ thisQuadOutside,
+ firstOppositeOutsidePt;
+
+ function topFirst (a, b) { return a.pxmid[1] - b.pxmid[1]; }
+ function bottomFirst (a, b) { return b.pxmid[1] - a.pxmid[1]; }
+
+ function scootOneLabel(thisPt, prevPt) {
+ if(!prevPt) prevPt = {};
+
+ var prevOuterY = prevPt.labelExtraY + (yHalf ? prevPt.yLabelMax : prevPt.yLabelMin),
+ thisInnerY = yHalf ? thisPt.yLabelMin : thisPt.yLabelMax,
+ thisOuterY = yHalf ? thisPt.yLabelMax : thisPt.yLabelMin,
+ thisSliceOuterY = thisPt.cyFinal + farthestY(thisPt.px0[1], thisPt.px1[1]),
+ newExtraY = prevOuterY - thisInnerY,
+ xBuffer,
+ i,
+ otherPt,
+ otherOuterY,
+ otherOuterX,
+ newExtraX;
+ // make sure this label doesn't overlap other labels
+ // this *only* has us move these labels vertically
+ if(newExtraY * yDiffSign > 0) thisPt.labelExtraY = newExtraY;
+
+ // make sure this label doesn't overlap any slices
+ if(!Array.isArray(trace.pull)) return; // this can only happen with array pulls
+
+ for(i = 0; i < wholeSide.length; i++) {
+ otherPt = wholeSide[i];
+
+ // overlap can only happen if the other point is pulled more than this one
+ if(otherPt === thisPt || ((trace.pull[thisPt.i] || 0) >= trace.pull[otherPt.i] || 0)) continue;
+
+ if((thisPt.pxmid[1] - otherPt.pxmid[1]) * yDiffSign > 0) {
+ // closer to the equator - by construction all of these happen first
+ // move the text vertically to get away from these slices
+ otherOuterY = otherPt.cyFinal + farthestY(otherPt.px0[1], otherPt.px1[1]);
+ newExtraY = otherOuterY - thisInnerY - thisPt.labelExtraY;
+
+ if(newExtraY * yDiffSign > 0) thisPt.labelExtraY += newExtraY;
+
+ } else if((thisOuterY + thisPt.labelExtraY - thisSliceOuterY) * yDiffSign > 0) {
+ // farther from the equator - happens after we've done all the
+ // vertical moving we're going to do
+ // move horizontally to get away from these more polar slices
+
+ // if we're moving horz. based on a slice that's several slices away from this one
+ // then we need some extra space for the lines to labels between them
+ xBuffer = 3 * xDiffSign * Math.abs(i - wholeSide.indexOf(thisPt));
+
+ otherOuterX = otherPt.cxFinal + farthestX(otherPt.px0[0], otherPt.px1[0]);
+ newExtraX = otherOuterX + xBuffer - (thisPt.cxFinal + thisPt.pxmid[0]) - thisPt.labelExtraX;
+
+ if(newExtraX * xDiffSign > 0) thisPt.labelExtraX += newExtraX;
+ }
+ }
+ }
+
+ for(yHalf = 0; yHalf < 2; yHalf++) {
+ equatorFirst = yHalf ? topFirst : bottomFirst;
+ farthestY = yHalf ? Math.max : Math.min;
+ yDiffSign = yHalf ? 1 : -1;
+
+ for(xHalf = 0; xHalf < 2; xHalf++) {
+ farthestX = xHalf ? Math.max : Math.min;
+ xDiffSign = xHalf ? 1 : -1;
+
+ // first sort the array
+ // note this is a copy of cd, so cd itself doesn't get sorted
+ // but we can still modify points in place.
+ thisQuad = quadrants[yHalf][xHalf];
+ thisQuad.sort(equatorFirst);
+
+ oppositeQuad = quadrants[1 - yHalf][xHalf];
+ wholeSide = oppositeQuad.concat(thisQuad);
+
+ thisQuadOutside = [];
+ for(i = 0; i < thisQuad.length; i++) {
+ if(thisQuad[i].yLabelMid !== undefined) thisQuadOutside.push(thisQuad[i]);
+ }
+
+ firstOppositeOutsidePt = false;
+ for(i = 0; yHalf && i < oppositeQuad.length; i++) {
+ if(oppositeQuad[i].yLabelMid !== undefined) {
+ firstOppositeOutsidePt = oppositeQuad[i];
+ break;
+ }
+ }
+
+ // each needs to avoid the previous
+ for(i = 0; i < thisQuadOutside.length; i++) {
+ var prevPt = i && thisQuadOutside[i - 1];
+ // bottom half needs to avoid the first label of the top half
+ // top half we still need to call scootOneLabel on the first slice
+ // so we can avoid other slices, but we don't pass a prevPt
+ if(firstOppositeOutsidePt && !i) prevPt = firstOppositeOutsidePt;
+ scootOneLabel(thisQuadOutside[i], prevPt);
+ }
+ }
+ }
+}
+
+function scalePies(cdpie, plotSize) {
+ var pieBoxWidth,
+ pieBoxHeight,
+ i,
+ j,
+ cd0,
+ trace,
+ tiltAxisRads,
+ maxPull,
+ scaleGroups = [],
+ scaleGroup,
+ minPxPerValUnit;
+
+ // first figure out the center and maximum radius for each pie
+ for(i = 0; i < cdpie.length; i++) {
+ cd0 = cdpie[i][0];
+ trace = cd0.trace;
+ pieBoxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]);
+ pieBoxHeight = plotSize.h * (trace.domain.y[1] - trace.domain.y[0]);
+ tiltAxisRads = trace.tiltaxis * Math.PI / 180;
+
+ maxPull = trace.pull;
+ if(Array.isArray(maxPull)) {
+ maxPull = 0;
+ for(j = 0; j < trace.pull.length; j++) {
+ if(trace.pull[j] > maxPull) maxPull = trace.pull[j];
+ }
+ }
+
+ cd0.r = Math.min(
+ pieBoxWidth / maxExtent(trace.tilt, Math.sin(tiltAxisRads), trace.depth),
+ pieBoxHeight / maxExtent(trace.tilt, Math.cos(tiltAxisRads), trace.depth)
+ ) / (2 + 2 * maxPull);
+
+ cd0.cx = plotSize.l + plotSize.w * (trace.domain.x[1] + trace.domain.x[0])/2;
+ cd0.cy = plotSize.t + plotSize.h * (2 - trace.domain.y[1] - trace.domain.y[0])/2;
+
+ if(trace.scalegroup && scaleGroups.indexOf(trace.scalegroup) === -1) {
+ scaleGroups.push(trace.scalegroup);
+ }
+ }
+
+ // Then scale any pies that are grouped
+ for(j = 0; j < scaleGroups.length; j++) {
+ minPxPerValUnit = Infinity;
+ scaleGroup = scaleGroups[j];
+
+ for(i = 0; i < cdpie.length; i++) {
+ cd0 = cdpie[i][0];
+ if(cd0.trace.scalegroup === scaleGroup) {
+ minPxPerValUnit = Math.min(minPxPerValUnit,
+ cd0.r * cd0.r / cd0.vTotal);
+ }
+ }
+
+ for(i = 0; i < cdpie.length; i++) {
+ cd0 = cdpie[i][0];
+ if(cd0.trace.scalegroup === scaleGroup) {
+ cd0.r = Math.sqrt(minPxPerValUnit * cd0.vTotal);
+ }
+ }
+ }
+
+}
+
+function setCoords(cd) {
+ var cd0 = cd[0],
+ trace = cd0.trace,
+ tilt = trace.tilt,
+ tiltAxisRads,
+ tiltAxisSin,
+ tiltAxisCos,
+ tiltRads,
+ crossTilt,
+ inPlane,
+ currentAngle = trace.rotation * Math.PI / 180,
+ angleFactor = 2 * Math.PI / cd0.vTotal,
+ firstPt = 'px0',
+ lastPt = 'px1',
+ i,
+ cdi,
+ currentCoords;
+
+ if(trace.direction === 'counterclockwise') {
+ for(i = 0; i < cd.length; i++) {
+ if(!cd[i].hidden) break; // find the first non-hidden slice
+ }
+ if(i === cd.length) return; // all slices hidden
+
+ currentAngle += angleFactor * cd[i].v;
+ angleFactor *= -1;
+ firstPt = 'px1';
+ lastPt = 'px0';
+ }
+
+ if(tilt) {
+ tiltRads = tilt * Math.PI / 180;
+ tiltAxisRads = trace.tiltaxis * Math.PI / 180;
+ crossTilt = Math.sin(tiltAxisRads) * Math.cos(tiltAxisRads);
+ inPlane = 1 - Math.cos(tiltRads);
+ tiltAxisSin = Math.sin(tiltAxisRads);
+ tiltAxisCos = Math.cos(tiltAxisRads);
+ }
+
+ function getCoords(angle) {
+ var xFlat = cd0.r * Math.sin(angle),
+ yFlat = -cd0.r * Math.cos(angle);
+
+ if(!tilt) return [xFlat, yFlat];
+
+ return [
+ xFlat * (1 - inPlane * tiltAxisSin * tiltAxisSin) + yFlat * crossTilt * inPlane,
+ xFlat * crossTilt * inPlane + yFlat * (1 - inPlane * tiltAxisCos * tiltAxisCos),
+ Math.sin(tiltRads) * (yFlat * tiltAxisCos - xFlat * tiltAxisSin)
+ ];
+ }
+
+ currentCoords = getCoords(currentAngle);
+
+ for(i = 0; i < cd.length; i++) {
+ cdi = cd[i];
+ if(cdi.hidden) continue;
+
+ cdi[firstPt] = currentCoords;
+
+ currentAngle += angleFactor * cdi.v / 2;
+ cdi.pxmid = getCoords(currentAngle);
+ cdi.midangle = currentAngle;
+
+ currentAngle += angleFactor * cdi.v / 2;
+ currentCoords = getCoords(currentAngle);
+
+ cdi[lastPt] = currentCoords;
+
+ cdi.largeArc = (cdi.v > cd0.vTotal / 2) ? 1 : 0;
+ }
+}
+
+function maxExtent(tilt, tiltAxisFraction, depth) {
+ if(!tilt) return 1;
+ var sinTilt = Math.sin(tilt * Math.PI / 180);
+ return Math.max(0.01, // don't let it go crazy if you tilt the pie totally on its side
+ depth * sinTilt * Math.abs(tiltAxisFraction) +
+ 2 * Math.sqrt(1 - sinTilt * sinTilt * tiltAxisFraction * tiltAxisFraction));
+}
diff --git a/src/traces/pie/style.js b/src/traces/pie/style.js
new file mode 100644
index 00000000000..727caeb3613
--- /dev/null
+++ b/src/traces/pie/style.js
@@ -0,0 +1,27 @@
+/**
+* Copyright 2012-2016, 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 d3 = require('d3');
+
+var styleOne = require('./style_one');
+
+module.exports = function style(gd) {
+ gd._fullLayout._pielayer.selectAll('.trace').each(function(cd) {
+ var cd0 = cd[0],
+ trace = cd0.trace,
+ traceSelection = d3.select(this);
+
+ traceSelection.style({opacity: trace.opacity});
+
+ traceSelection.selectAll('.top path.surface').each(function(pt) {
+ d3.select(this).call(styleOne, pt, trace);
+ });
+ });
+};
diff --git a/src/traces/pie/style_one.js b/src/traces/pie/style_one.js
new file mode 100644
index 00000000000..4f26cbb2a26
--- /dev/null
+++ b/src/traces/pie/style_one.js
@@ -0,0 +1,25 @@
+/**
+* Copyright 2012-2016, 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 Color = require('../../components/color');
+
+module.exports = function styleOne(s, pt, trace) {
+ var lineColor = trace.marker.line.color;
+ if(Array.isArray(lineColor)) lineColor = lineColor[pt.i] || Color.defaultLine;
+
+ var lineWidth = trace.marker.line.width || 0;
+ if(Array.isArray(lineWidth)) lineWidth = lineWidth[pt.i] || 0;
+
+ s.style({
+ 'stroke-width': lineWidth,
+ fill: pt.color
+ })
+ .call(Color.stroke, lineColor);
+};