From 9e9ab84bf613b9bdbe0234a7d866a1d1e3ceca73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tusz?= Date: Tue, 5 Jan 2016 15:54:59 -0500 Subject: [PATCH 1/2] Split pie trace into multiple files --- src/traces/pie/attributes.js | 1 + src/traces/pie/calc.js | 145 +++++ src/traces/pie/defaults.js | 82 +++ src/traces/pie/helpers.js | 21 + src/traces/pie/index.js | 941 +--------------------------- src/traces/pie/layout_attributes.js | 18 + src/traces/pie/layout_defaults.js | 20 + src/traces/pie/plot.js | 684 ++++++++++++++++++++ src/traces/pie/style.js | 27 + src/traces/pie/style_one.js | 25 + 10 files changed, 1032 insertions(+), 932 deletions(-) create mode 100644 src/traces/pie/calc.js create mode 100644 src/traces/pie/defaults.js create mode 100644 src/traces/pie/helpers.js create mode 100644 src/traces/pie/layout_attributes.js create mode 100644 src/traces/pie/layout_defaults.js create mode 100644 src/traces/pie/plot.js create mode 100644 src/traces/pie/style.js create mode 100644 src/traces/pie/style_one.js 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..06ddff00572 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.layoutDefaults = 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); +}; From c47255f5828779171dffea31ff1dbfac591f51cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tusz?= Date: Tue, 5 Jan 2016 16:20:22 -0500 Subject: [PATCH 2/2] Fixed function name (oopsie) --- src/traces/pie/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traces/pie/index.js b/src/traces/pie/index.js index 06ddff00572..bde8f3ef91a 100644 --- a/src/traces/pie/index.js +++ b/src/traces/pie/index.js @@ -20,7 +20,7 @@ Plotly.Plots.register(exports, 'pie', ['pie', 'showLegend'], { exports.attributes = require('./attributes'); exports.supplyDefaults = require('./defaults'); -exports.layoutDefaults = require('./layout_defaults'); +exports.supplyLayoutDefaults = require('./layout_defaults'); exports.layoutAttributes = require('./layout_attributes'); exports.calc = require('./calc'); exports.plot = require('./plot');