'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
};