'use strict'; var d3 = require('@plotly/d3'); var Plots = require('../../plots/plots'); var Fx = require('../../components/fx'); var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); var Lib = require('../../lib'); var strScale = Lib.strScale; var strTranslate = Lib.strTranslate; var svgTextUtils = require('../../lib/svg_text_utils'); var uniformText = require('../bar/uniform_text'); var recordMinTextSize = uniformText.recordMinTextSize; var clearMinTextSize = uniformText.clearMinTextSize; var TEXTPAD = require('../bar/constants').TEXTPAD; var helpers = require('./helpers'); var eventData = require('./event_data'); var isValidTextValue = require('../../lib').isValidTextValue; function plot(gd, cdModule) { var isStatic = gd._context.staticPlot; var fullLayout = gd._fullLayout; var gs = fullLayout._size; clearMinTextSize('pie', fullLayout); prerenderTitles(cdModule, gd); layoutAreas(cdModule, gs); var plotGroups = Lib.makeTraceGroups(fullLayout._pielayer, cdModule, 'trace').each(function(cd) { var plotGroup = d3.select(this); var cd0 = cd[0]; var trace = cd0.trace; setCoords(cd); // TODO: miter might look better but can sometimes cause problems // maybe miter with a small-ish stroke-miterlimit? plotGroup.attr('stroke-linejoin', 'round'); plotGroup.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 ]; var hasOutsideText = false; slices.each(function(pt, i) { if(pt.hidden) { d3.select(this).selectAll('path,g').remove(); return; } // to have consistent event data compared to other traces pt.pointNumber = pt.i; pt.curveNumber = trace.index; quadrants[pt.pxmid[1] < 0 ? 0 : 1][pt.pxmid[0] < 0 ? 0 : 1].push(pt); var cx = cd0.cx; var cy = cd0.cy; var sliceTop = d3.select(this); var slicePath = sliceTop.selectAll('path.surface').data([pt]); slicePath.enter().append('path') .classed('surface', true) .style({'pointer-events': isStatic ? 'none' : 'all'}); sliceTop.call(attachFxHandlers, gd, cd); if(trace.pull) { var pull = +helpers.castOption(trace.pull, pt.pts) || 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) { var dx = scale * (finish[0] - start[0]); var dy = scale * (finish[1] - start[1]); return 'a' + (scale * cd0.r) + ',' + (scale * cd0.r) + ' 0 ' + pt.largeArc + (cw ? ' 1 ' : ' 0 ') + dx + ',' + dy; } 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 formatSliceLabel(gd, pt, cd0); var textPosition = helpers.castOption(trace.textposition, pt.pts); var 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 = Lib.ensureSingle(d3.select(this), 'text', '', function(s) { // prohibit tex interpretation until we can handle // tex and regular text together s.attr('data-notex', 1); }); var font = Lib.ensureUniformFontSize(gd, textPosition === 'outside' ? determineOutsideTextFont(trace, pt, fullLayout.font) : determineInsideTextFont(trace, pt, fullLayout.font) ); sliceText.text(pt.text) .attr({ class: 'slicetext', transform: '', 'text-anchor': 'middle' }) .call(Drawing.font, font) .call(svgTextUtils.convertToTspans, gd); // position the text relative to the slice var textBB = Drawing.bBox(sliceText.node()); var transform; if(textPosition === 'outside') { transform = transformOutsideText(textBB, pt); } else { transform = transformInsideText(textBB, pt, cd0); if(textPosition === 'auto' && transform.scale < 1) { var newFont = Lib.ensureUniformFontSize(gd, trace.outsidetextfont); sliceText.call(Drawing.font, newFont); textBB = Drawing.bBox(sliceText.node()); transform = transformOutsideText(textBB, pt); } } var textPosAngle = transform.textPosAngle; var textXY = textPosAngle === undefined ? pt.pxmid : getCoords(cd0.r, textPosAngle); transform.targetX = cx + textXY[0] * transform.rCenter + (transform.x || 0); transform.targetY = cy + textXY[1] * transform.rCenter + (transform.y || 0); computeTransform(transform, textBB); // save some stuff to use later ensure no labels overlap if(transform.outside) { var targetY = transform.targetY; pt.yLabelMin = targetY - textBB.height / 2; pt.yLabelMid = targetY; pt.yLabelMax = targetY + textBB.height / 2; pt.labelExtraX = 0; pt.labelExtraY = 0; hasOutsideText = true; } transform.fontSize = font.size; recordMinTextSize(trace.type, transform, fullLayout); cd[i].transform = transform; Lib.setTransormAndDisplay(sliceText, transform); }); }); // add the title var titleTextGroup = d3.select(this).selectAll('g.titletext') .data(trace.title.text ? [0] : []); titleTextGroup.enter().append('g') .classed('titletext', true); titleTextGroup.exit().remove(); titleTextGroup.each(function() { var titleText = Lib.ensureSingle(d3.select(this), 'text', '', function(s) { // prohibit tex interpretation as above s.attr('data-notex', 1); }); var txt = trace.title.text; if(trace._meta) { txt = Lib.templateString(txt, trace._meta); } titleText.text(txt) .attr({ class: 'titletext', transform: '', 'text-anchor': 'middle', }) .call(Drawing.font, trace.title.font) .call(svgTextUtils.convertToTspans, gd); var transform; if(trace.title.position === 'middle center') { transform = positionTitleInside(cd0); } else { transform = positionTitleOutside(cd0, gs); } titleText.attr('transform', strTranslate(transform.x, transform.y) + strScale(Math.min(1, transform.scale)) + strTranslate(transform.tx, transform.ty)); }); // now make sure no labels overlap (at least within one pie) if(hasOutsideText) scootLabels(quadrants, trace); plotTextLines(slices, trace); if(hasOutsideText && trace.automargin) { // TODO if we ever want to improve perf, // we could reuse the textBB computed above together // with the sliceText transform info var traceBbox = Drawing.bBox(plotGroup.node()); var domain = trace.domain; var vpw = gs.w * (domain.x[1] - domain.x[0]); var vph = gs.h * (domain.y[1] - domain.y[0]); var xgap = (0.5 * vpw - cd0.r) / gs.w; var ygap = (0.5 * vph - cd0.r) / gs.h; Plots.autoMargin(gd, 'pie.' + trace.uid + '.automargin', { xl: domain.x[0] - xgap, xr: domain.x[1] + xgap, yb: domain.y[0] - ygap, yt: domain.y[1] + ygap, l: Math.max(cd0.cx - cd0.r - traceBbox.left, 0), r: Math.max(traceBbox.right - (cd0.cx + cd0.r), 0), b: Math.max(traceBbox.bottom - (cd0.cy + cd0.r), 0), t: Math.max(cd0.cy - cd0.r - traceBbox.top, 0), pad: 5 }); } }); }); // 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() { plotGroups.selectAll('tspan').each(function() { var s = d3.select(this); if(s.attr('dy')) s.attr('dy', s.attr('dy')); }); }, 0); } // TODO add support for transition function plotTextLines(slices, trace) { slices.each(function(pt) { var sliceTop = d3.select(this); if(!pt.labelExtraX && !pt.labelExtraY) { sliceTop.select('path.textline').remove(); return; } // first move the text to its new location var sliceText = sliceTop.select('g.slicetext text'); pt.transform.targetX += pt.labelExtraX; pt.transform.targetY += pt.labelExtraY; Lib.setTransormAndDisplay(sliceText, pt.transform); // then add a line to the new location var lineStartX = pt.cxFinal + pt.pxmid[0]; var lineStartY = pt.cyFinal + pt.pxmid[1]; var textLinePath = 'M' + lineStartX + ',' + lineStartY; var 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]; var 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; } Lib.ensureSingle(sliceTop, 'path', 'textline') .call(Color.stroke, trace.outsidetextfont.color) .attr({ 'stroke-width': Math.min(2, trace.outsidetextfont.size / 8), d: textLinePath, fill: 'none' }); }); } function attachFxHandlers(sliceTop, gd, cd) { var cd0 = cd[0]; var cx = cd0.cx; var cy = cd0.cy; var trace = cd0.trace; var isFunnelArea = trace.type === 'funnelarea'; // hover state vars // have we drawn a hover label, so it should be cleared later if(!('_hasHoverLabel' in trace)) trace._hasHoverLabel = false; // have we emitted a hover event, so later an unhover event should be emitted // note that click events do not depend on this - you can still get them // with hovermode: false or if you were earlier dragging, then clicked // in the same slice that you moused up in if(!('_hasHoverEvent' in trace)) trace._hasHoverEvent = false; sliceTop.on('mouseover', function(pt) { // in case fullLayout or fullData has changed without a replot var fullLayout2 = gd._fullLayout; var trace2 = gd._fullData[trace.index]; if(gd._dragging || fullLayout2.hovermode === false) return; var hoverinfo = trace2.hoverinfo; if(Array.isArray(hoverinfo)) { // super hacky: we need to pull out the *first* hoverinfo from // pt.pts, then put it back into an array in a dummy trace // and call castHoverinfo on that. // TODO: do we want to have Fx.castHoverinfo somehow handle this? // it already takes an array for index, for 2D, so this seems tricky. hoverinfo = Fx.castHoverinfo({ hoverinfo: [helpers.castOption(hoverinfo, pt.pts)], _module: trace._module }, fullLayout2, 0); } 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(trace2.hovertemplate || (hoverinfo !== 'none' && hoverinfo !== 'skip' && hoverinfo)) { var rInscribed = pt.rInscribed || 0; var hoverCenterX = cx + pt.pxmid[0] * (1 - rInscribed); var hoverCenterY = cy + pt.pxmid[1] * (1 - rInscribed); var separators = fullLayout2.separators; var text = []; if(hoverinfo && hoverinfo.indexOf('label') !== -1) text.push(pt.label); pt.text = helpers.castOption(trace2.hovertext || trace2.text, pt.pts); if(hoverinfo && hoverinfo.indexOf('text') !== -1) { var tx = pt.text; if(Lib.isValidTextValue(tx)) text.push(tx); } pt.value = pt.v; pt.valueLabel = helpers.formatPieValue(pt.v, separators); if(hoverinfo && hoverinfo.indexOf('value') !== -1) text.push(pt.valueLabel); pt.percent = pt.v / cd0.vTotal; pt.percentLabel = helpers.formatPiePercent(pt.percent, separators); if(hoverinfo && hoverinfo.indexOf('percent') !== -1) text.push(pt.percentLabel); var hoverLabel = trace2.hoverlabel; var hoverFont = hoverLabel.font; var bbox = []; Fx.loneHover({ trace: trace, x0: hoverCenterX - rInscribed * cd0.r, x1: hoverCenterX + rInscribed * cd0.r, y: hoverCenterY, _x0: isFunnelArea ? cx + pt.TL[0] : hoverCenterX - rInscribed * cd0.r, _x1: isFunnelArea ? cx + pt.TR[0] : hoverCenterX + rInscribed * cd0.r, _y0: isFunnelArea ? cy + pt.TL[1] : hoverCenterY - rInscribed * cd0.r, _y1: isFunnelArea ? cy + pt.BL[1] : hoverCenterY + rInscribed * cd0.r, text: text.join('
'), name: (trace2.hovertemplate || hoverinfo.indexOf('name') !== -1) ? trace2.name : undefined, idealAlign: pt.pxmid[0] < 0 ? 'left' : 'right', color: helpers.castOption(hoverLabel.bgcolor, pt.pts) || pt.color, borderColor: helpers.castOption(hoverLabel.bordercolor, pt.pts), fontFamily: helpers.castOption(hoverFont.family, pt.pts), fontSize: helpers.castOption(hoverFont.size, pt.pts), fontColor: helpers.castOption(hoverFont.color, pt.pts), nameLength: helpers.castOption(hoverLabel.namelength, pt.pts), textAlign: helpers.castOption(hoverLabel.align, pt.pts), hovertemplate: helpers.castOption(trace2.hovertemplate, pt.pts), hovertemplateLabels: pt, eventData: [eventData(pt, trace2)] }, { container: fullLayout2._hoverlayer.node(), outerContainer: fullLayout2._paper.node(), gd: gd, inOut_bbox: bbox }); pt.bbox = bbox[0]; trace._hasHoverLabel = true; } trace._hasHoverEvent = true; gd.emit('plotly_hover', { points: [eventData(pt, trace2)], event: d3.event }); }); sliceTop.on('mouseout', function(evt) { var fullLayout2 = gd._fullLayout; var trace2 = gd._fullData[trace.index]; var pt = d3.select(this).datum(); if(trace._hasHoverEvent) { evt.originalEvent = d3.event; gd.emit('plotly_unhover', { points: [eventData(pt, trace2)], event: d3.event }); trace._hasHoverEvent = false; } if(trace._hasHoverLabel) { Fx.loneUnhover(fullLayout2._hoverlayer.node()); trace._hasHoverLabel = false; } }); sliceTop.on('click', function(pt) { // TODO: this does not support right-click. If we want to support it, we // would likely need to change pie to use dragElement instead of straight // map subplot event binding. Or perhaps better, make a simple wrapper with the // right mousedown, mousemove, and mouseup handlers just for a left/right click // map subplots would use this too. var fullLayout2 = gd._fullLayout; var trace2 = gd._fullData[trace.index]; if(gd._dragging || fullLayout2.hovermode === false) return; gd._hoverdata = [eventData(pt, trace2)]; Fx.click(gd, d3.event); }); } function determineOutsideTextFont(trace, pt, layoutFont) { var color = helpers.castOption(trace.outsidetextfont.color, pt.pts) || helpers.castOption(trace.textfont.color, pt.pts) || layoutFont.color; var family = helpers.castOption(trace.outsidetextfont.family, pt.pts) || helpers.castOption(trace.textfont.family, pt.pts) || layoutFont.family; var size = helpers.castOption(trace.outsidetextfont.size, pt.pts) || helpers.castOption(trace.textfont.size, pt.pts) || layoutFont.size; var weight = helpers.castOption(trace.outsidetextfont.weight, pt.pts) || helpers.castOption(trace.textfont.weight, pt.pts) || layoutFont.weight; var style = helpers.castOption(trace.outsidetextfont.style, pt.pts) || helpers.castOption(trace.textfont.style, pt.pts) || layoutFont.style; var variant = helpers.castOption(trace.outsidetextfont.variant, pt.pts) || helpers.castOption(trace.textfont.variant, pt.pts) || layoutFont.variant; var textcase = helpers.castOption(trace.outsidetextfont.textcase, pt.pts) || helpers.castOption(trace.textfont.textcase, pt.pts) || layoutFont.textcase; var lineposition = helpers.castOption(trace.outsidetextfont.lineposition, pt.pts) || helpers.castOption(trace.textfont.lineposition, pt.pts) || layoutFont.lineposition; var shadow = helpers.castOption(trace.outsidetextfont.shadow, pt.pts) || helpers.castOption(trace.textfont.shadow, pt.pts) || layoutFont.shadow; return { color: color, family: family, size: size, weight: weight, style: style, variant: variant, textcase: textcase, lineposition: lineposition, shadow: shadow, }; } function determineInsideTextFont(trace, pt, layoutFont) { var customColor = helpers.castOption(trace.insidetextfont.color, pt.pts); if(!customColor && trace._input.textfont) { // Why not simply using trace.textfont? Because if not set, it // defaults to layout.font which has a default color. But if // textfont.color and insidetextfont.color don't supply a value, // a contrasting color shall be used. customColor = helpers.castOption(trace._input.textfont.color, pt.pts); } var family = helpers.castOption(trace.insidetextfont.family, pt.pts) || helpers.castOption(trace.textfont.family, pt.pts) || layoutFont.family; var size = helpers.castOption(trace.insidetextfont.size, pt.pts) || helpers.castOption(trace.textfont.size, pt.pts) || layoutFont.size; var weight = helpers.castOption(trace.insidetextfont.weight, pt.pts) || helpers.castOption(trace.textfont.weight, pt.pts) || layoutFont.weight; var style = helpers.castOption(trace.insidetextfont.style, pt.pts) || helpers.castOption(trace.textfont.style, pt.pts) || layoutFont.style; var variant = helpers.castOption(trace.insidetextfont.variant, pt.pts) || helpers.castOption(trace.textfont.variant, pt.pts) || layoutFont.variant; var textcase = helpers.castOption(trace.insidetextfont.textcase, pt.pts) || helpers.castOption(trace.textfont.textcase, pt.pts) || layoutFont.textcase; var lineposition = helpers.castOption(trace.insidetextfont.lineposition, pt.pts) || helpers.castOption(trace.textfont.lineposition, pt.pts) || layoutFont.lineposition; var shadow = helpers.castOption(trace.insidetextfont.shadow, pt.pts) || helpers.castOption(trace.textfont.shadow, pt.pts) || layoutFont.shadow; return { color: customColor || Color.contrast(pt.color), family: family, size: size, weight: weight, style: style, variant: variant, textcase: textcase, lineposition: lineposition, shadow: shadow, }; } function prerenderTitles(cdModule, gd) { var cd0, trace; // Determine the width and height of the title for each pie. for(var i = 0; i < cdModule.length; i++) { cd0 = cdModule[i][0]; trace = cd0.trace; if(trace.title.text) { var txt = trace.title.text; if(trace._meta) { txt = Lib.templateString(txt, trace._meta); } var dummyTitle = Drawing.tester.append('text') .attr('data-notex', 1) .text(txt) .call(Drawing.font, trace.title.font) .call(svgTextUtils.convertToTspans, gd); var bBox = Drawing.bBox(dummyTitle.node(), true); cd0.titleBox = { width: bBox.width, height: bBox.height, }; dummyTitle.remove(); } } } function transformInsideText(textBB, pt, cd0) { var r = cd0.r || pt.rpx1; var rInscribed = pt.rInscribed; var isEmpty = pt.startangle === pt.stopangle; if(isEmpty) { return { rCenter: 1 - rInscribed, scale: 0, rotate: 0, textPosAngle: 0 }; } var ring = pt.ring; var isCircle = (ring === 1) && (Math.abs(pt.startangle - pt.stopangle) === Math.PI * 2); var halfAngle = pt.halfangle; var midAngle = pt.midangle; var orientation = cd0.trace.insidetextorientation; var isHorizontal = orientation === 'horizontal'; var isTangential = orientation === 'tangential'; var isRadial = orientation === 'radial'; var isAuto = orientation === 'auto'; var allTransforms = []; var newT; if(!isAuto) { // max size if text is placed (horizontally) at the top or bottom of the arc var considerCrossing = function(angle, key) { if(isCrossing(pt, angle)) { var dStart = Math.abs(angle - pt.startangle); var dStop = Math.abs(angle - pt.stopangle); var closestEdge = dStart < dStop ? dStart : dStop; if(key === 'tan') { newT = calcTanTransform(textBB, r, ring, closestEdge, 0); } else { // case of 'rad' newT = calcRadTransform(textBB, r, ring, closestEdge, Math.PI / 2); } newT.textPosAngle = angle; allTransforms.push(newT); } }; // to cover all cases with trace.rotation added var i; if(isHorizontal || isTangential) { // top for(i = 4; i >= -4; i -= 2) considerCrossing(Math.PI * i, 'tan'); // bottom for(i = 4; i >= -4; i -= 2) considerCrossing(Math.PI * (i + 1), 'tan'); } if(isHorizontal || isRadial) { // left for(i = 4; i >= -4; i -= 2) considerCrossing(Math.PI * (i + 1.5), 'rad'); // right for(i = 4; i >= -4; i -= 2) considerCrossing(Math.PI * (i + 0.5), 'rad'); } } if(isCircle || isAuto || isHorizontal) { // max size text can be inserted inside without rotating it // this inscribes the text rectangle in a circle, which is then inscribed // in the slice, so it will be an underestimate, which some day we may want // to improve so this case can get more use var textDiameter = Math.sqrt(textBB.width * textBB.width + textBB.height * textBB.height); newT = { scale: rInscribed * r * 2 / textDiameter, // and the center position and rotation in this case rCenter: 1 - rInscribed, rotate: 0 }; newT.textPosAngle = (pt.startangle + pt.stopangle) / 2; if(newT.scale >= 1) return newT; allTransforms.push(newT); } if(isAuto || isRadial) { newT = calcRadTransform(textBB, r, ring, halfAngle, midAngle); newT.textPosAngle = (pt.startangle + pt.stopangle) / 2; allTransforms.push(newT); } if(isAuto || isTangential) { newT = calcTanTransform(textBB, r, ring, halfAngle, midAngle); newT.textPosAngle = (pt.startangle + pt.stopangle) / 2; allTransforms.push(newT); } var id = 0; var maxScale = 0; for(var k = 0; k < allTransforms.length; k++) { var s = allTransforms[k].scale; if(maxScale < s) { maxScale = s; id = k; } if(!isAuto && maxScale >= 1) { // respect test order for non-auto options break; } } return allTransforms[id]; } function isCrossing(pt, angle) { var start = pt.startangle; var stop = pt.stopangle; return ( (start > angle && angle > stop) || (start < angle && angle < stop) ); } function calcRadTransform(textBB, r, ring, halfAngle, midAngle) { r = Math.max(0, r - 2 * TEXTPAD); // max size if text is rotated radially var a = textBB.width / textBB.height; var s = calcMaxHalfSize(a, halfAngle, r, ring); return { scale: s * 2 / textBB.height, rCenter: calcRCenter(a, s / r), rotate: calcRotate(midAngle) }; } function calcTanTransform(textBB, r, ring, halfAngle, midAngle) { r = Math.max(0, r - 2 * TEXTPAD); // max size if text is rotated tangentially var a = textBB.height / textBB.width; var s = calcMaxHalfSize(a, halfAngle, r, ring); return { scale: s * 2 / textBB.width, rCenter: calcRCenter(a, s / r), rotate: calcRotate(midAngle + Math.PI / 2) }; } function calcRCenter(a, b) { return Math.cos(b) - a * b; } function calcRotate(t) { return (180 / Math.PI * t + 720) % 180 - 90; } function calcMaxHalfSize(a, halfAngle, r, ring) { var q = a + 1 / (2 * Math.tan(halfAngle)); return r * Math.min( 1 / (Math.sqrt(q * q + 0.5) + q), ring / (Math.sqrt(a * a + ring / 2) + a) ); } function getInscribedRadiusFraction(pt, cd0) { if(pt.v === cd0.vTotal && !cd0.trace.hole) return 1;// special case of 100% with no hole return Math.min(1 / (1 + 1 / Math.sin(pt.halfangle)), pt.ring / 2); } function transformOutsideText(textBB, pt) { var x = pt.pxmid[0]; var y = pt.pxmid[1]; var dx = textBB.width / 2; var 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 positionTitleInside(cd0) { var textDiameter = Math.sqrt(cd0.titleBox.width * cd0.titleBox.width + cd0.titleBox.height * cd0.titleBox.height); return { x: cd0.cx, y: cd0.cy, scale: cd0.trace.hole * cd0.r * 2 / textDiameter, tx: 0, ty: - cd0.titleBox.height / 2 + cd0.trace.title.font.size }; } function positionTitleOutside(cd0, plotSize) { var scaleX = 1; var scaleY = 1; var maxPull; var trace = cd0.trace; // position of the baseline point of the text box in the plot, before scaling. // we anchored the text in the middle, so the baseline is on the bottom middle // of the first line of text. var topMiddle = { x: cd0.cx, y: cd0.cy }; // relative translation of the text box after scaling var translate = { tx: 0, ty: 0 }; // we reason below as if the baseline is the top middle point of the text box. // so we must add the font size to approximate the y-coord. of the top. // note that this correction must happen after scaling. translate.ty += trace.title.font.size; maxPull = getMaxPull(trace); if(trace.title.position.indexOf('top') !== -1) { topMiddle.y -= (1 + maxPull) * cd0.r; translate.ty -= cd0.titleBox.height; } else if(trace.title.position.indexOf('bottom') !== -1) { topMiddle.y += (1 + maxPull) * cd0.r; } var rx = applyAspectRatio(cd0.r, cd0.trace.aspectratio); var maxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]) / 2; if(trace.title.position.indexOf('left') !== -1) { // we start the text at the left edge of the pie maxWidth = maxWidth + rx; topMiddle.x -= (1 + maxPull) * rx; translate.tx += cd0.titleBox.width / 2; } else if(trace.title.position.indexOf('center') !== -1) { maxWidth *= 2; } else if(trace.title.position.indexOf('right') !== -1) { maxWidth = maxWidth + rx; topMiddle.x += (1 + maxPull) * rx; translate.tx -= cd0.titleBox.width / 2; } scaleX = maxWidth / cd0.titleBox.width; scaleY = getTitleSpace(cd0, plotSize) / cd0.titleBox.height; return { x: topMiddle.x, y: topMiddle.y, scale: Math.min(scaleX, scaleY), tx: translate.tx, ty: translate.ty }; } function applyAspectRatio(x, aspectratio) { return x / ((aspectratio === undefined) ? 1 : aspectratio); } function getTitleSpace(cd0, plotSize) { var trace = cd0.trace; var pieBoxHeight = plotSize.h * (trace.domain.y[1] - trace.domain.y[0]); // use at most half of the plot for the title return Math.min(cd0.titleBox.height, pieBoxHeight / 2); } function getMaxPull(trace) { var maxPull = trace.pull; if(!maxPull) return 0; var j; if(Lib.isArrayOrTypedArray(maxPull)) { maxPull = 0; for(j = 0; j < trace.pull.length; j++) { if(trace.pull[j] > maxPull) maxPull = trace.pull[j]; } } return maxPull; } 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); var thisInnerY = yHalf ? thisPt.yLabelMin : thisPt.yLabelMax; var thisOuterY = yHalf ? thisPt.yLabelMax : thisPt.yLabelMin; var thisSliceOuterY = thisPt.cyFinal + farthestY(thisPt.px0[1], thisPt.px1[1]); var newExtraY = prevOuterY - thisInnerY; var 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(!Lib.isArrayOrTypedArray(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 || ( (helpers.castOption(trace.pull, thisPt.pts) || 0) >= (helpers.castOption(trace.pull, otherPt.pts) || 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 layoutAreas(cdModule, plotSize) { var scaleGroups = []; // figure out the center and maximum radius for(var i = 0; i < cdModule.length; i++) { var cd0 = cdModule[i][0]; var trace = cd0.trace; var domain = trace.domain; var width = plotSize.w * (domain.x[1] - domain.x[0]); var height = plotSize.h * (domain.y[1] - domain.y[0]); // leave some space for the title, if it will be displayed outside if(trace.title.text && trace.title.position !== 'middle center') { height -= getTitleSpace(cd0, plotSize); } var rx = width / 2; var ry = height / 2; if(trace.type === 'funnelarea' && !trace.scalegroup) { ry /= trace.aspectratio; } cd0.r = Math.min(rx, ry) / (1 + getMaxPull(trace)); cd0.cx = plotSize.l + plotSize.w * (trace.domain.x[1] + trace.domain.x[0]) / 2; cd0.cy = plotSize.t + plotSize.h * (1 - trace.domain.y[0]) - height / 2; if(trace.title.text && trace.title.position.indexOf('bottom') !== -1) { cd0.cy -= getTitleSpace(cd0, plotSize); } if(trace.scalegroup && scaleGroups.indexOf(trace.scalegroup) === -1) { scaleGroups.push(trace.scalegroup); } } groupScale(cdModule, scaleGroups); } function groupScale(cdModule, scaleGroups) { var cd0, i, trace; // scale those that are grouped for(var k = 0; k < scaleGroups.length; k++) { var min = Infinity; var g = scaleGroups[k]; for(i = 0; i < cdModule.length; i++) { cd0 = cdModule[i][0]; trace = cd0.trace; if(trace.scalegroup === g) { var area; if(trace.type === 'pie') { area = cd0.r * cd0.r; } else if(trace.type === 'funnelarea') { var rx, ry; if(trace.aspectratio > 1) { rx = cd0.r; ry = rx / trace.aspectratio; } else { ry = cd0.r; rx = ry * trace.aspectratio; } rx *= (1 + trace.baseratio) / 2; area = rx * ry; } min = Math.min(min, area / cd0.vTotal); } } for(i = 0; i < cdModule.length; i++) { cd0 = cdModule[i][0]; trace = cd0.trace; if(trace.scalegroup === g) { var v = min * cd0.vTotal; if(trace.type === 'funnelarea') { v /= (1 + trace.baseratio) / 2; v /= trace.aspectratio; } cd0.r = Math.sqrt(v); } } } } function setCoords(cd) { var cd0 = cd[0]; var r = cd0.r; var trace = cd0.trace; var currentAngle = helpers.getRotationAngle(trace.rotation); var angleFactor = 2 * Math.PI / cd0.vTotal; var firstPt = 'px0'; var lastPt = 'px1'; var 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'; } currentCoords = getCoords(r, currentAngle); for(i = 0; i < cd.length; i++) { cdi = cd[i]; if(cdi.hidden) continue; cdi[firstPt] = currentCoords; cdi.startangle = currentAngle; currentAngle += angleFactor * cdi.v / 2; cdi.pxmid = getCoords(r, currentAngle); cdi.midangle = currentAngle; currentAngle += angleFactor * cdi.v / 2; currentCoords = getCoords(r, currentAngle); cdi.stopangle = currentAngle; cdi[lastPt] = currentCoords; cdi.largeArc = (cdi.v > cd0.vTotal / 2) ? 1 : 0; cdi.halfangle = Math.PI * Math.min(cdi.v / cd0.vTotal, 0.5); cdi.ring = 1 - trace.hole; cdi.rInscribed = getInscribedRadiusFraction(cdi, cd0); } } function getCoords(r, angle) { return [r * Math.sin(angle), -r * Math.cos(angle)]; } function formatSliceLabel(gd, pt, cd0) { var fullLayout = gd._fullLayout; var trace = cd0.trace; // look for textemplate var texttemplate = trace.texttemplate; // now insert text var textinfo = trace.textinfo; if(!texttemplate && textinfo && textinfo !== 'none') { var parts = textinfo.split('+'); var hasFlag = function(flag) { return parts.indexOf(flag) !== -1; }; var hasLabel = hasFlag('label'); var hasText = hasFlag('text'); var hasValue = hasFlag('value'); var hasPercent = hasFlag('percent'); var separators = fullLayout.separators; var text; text = hasLabel ? [pt.label] : []; if(hasText) { var tx = helpers.getFirstFilled(trace.text, pt.pts); if(isValidTextValue(tx)) text.push(tx); } if(hasValue) text.push(helpers.formatPieValue(pt.v, separators)); if(hasPercent) text.push(helpers.formatPiePercent(pt.v / cd0.vTotal, separators)); pt.text = text.join('
'); } function makeTemplateVariables(pt) { return { label: pt.label, value: pt.v, valueLabel: helpers.formatPieValue(pt.v, fullLayout.separators), percent: pt.v / cd0.vTotal, percentLabel: helpers.formatPiePercent(pt.v / cd0.vTotal, fullLayout.separators), color: pt.color, text: pt.text, customdata: Lib.castOption(trace, pt.i, 'customdata') }; } if(texttemplate) { var txt = Lib.castOption(trace, pt.i, 'texttemplate'); if(!txt) { pt.text = ''; } else { var obj = makeTemplateVariables(pt); var ptTx = helpers.getFirstFilled(trace.text, pt.pts); if(isValidTextValue(ptTx) || ptTx === '') obj.text = ptTx; pt.text = Lib.texttemplateString(txt, obj, gd._fullLayout._d3locale, obj, trace._meta || {}); } } } function computeTransform( transform, // inout textBB // in ) { var a = transform.rotate * Math.PI / 180; var cosA = Math.cos(a); var sinA = Math.sin(a); var midX = (textBB.left + textBB.right) / 2; var midY = (textBB.top + textBB.bottom) / 2; transform.textX = midX * cosA - midY * sinA; transform.textY = midX * sinA + midY * cosA; transform.noCenter = true; } module.exports = { plot: plot, formatSliceLabel: formatSliceLabel, transformInsideText: transformInsideText, determineInsideTextFont: determineInsideTextFont, positionTitleOutside: positionTitleOutside, prerenderTitles: prerenderTitles, layoutAreas: layoutAreas, attachFxHandlers: attachFxHandlers, computeTransform: computeTransform };