plotly.js
Version:
The open source javascript graphing library that powers plotly
1,018 lines (858 loc) • 36.5 kB
JavaScript
'use strict';
var d3 = require('@plotly/d3');
var isNumeric = require('fast-isnumeric');
var Lib = require('../../lib');
var svgTextUtils = require('../../lib/svg_text_utils');
var Color = require('../../components/color');
var Drawing = require('../../components/drawing');
var Registry = require('../../registry');
var tickText = require('../../plots/cartesian/axes').tickText;
var uniformText = require('./uniform_text');
var recordMinTextSize = uniformText.recordMinTextSize;
var clearMinTextSize = uniformText.clearMinTextSize;
var style = require('./style');
var helpers = require('./helpers');
var constants = require('./constants');
var attributes = require('./attributes');
var attributeText = attributes.text;
var attributeTextPosition = attributes.textposition;
var appendArrayPointValue = require('../../components/fx/helpers').appendArrayPointValue;
var TEXTPAD = constants.TEXTPAD;
function keyFunc(d) {return d.id;}
function getKeyFunc(trace) {
if(trace.ids) {
return keyFunc;
}
}
// Returns -1 if v < 0, 1 if v > 0, and 0 if v == 0
function sign(v) {
return (v > 0) - (v < 0);
}
// Returns 1 if a < b and -1 otherwise
// (For the purposes of this module we don't care about the case where a == b)
function dirSign(a, b) {
return (a < b) ? 1 : -1;
}
function getXY(di, xa, ya, isHorizontal) {
var s = [];
var p = [];
var sAxis = isHorizontal ? xa : ya;
var pAxis = isHorizontal ? ya : xa;
s[0] = sAxis.c2p(di.s0, true);
p[0] = pAxis.c2p(di.p0, true);
s[1] = sAxis.c2p(di.s1, true);
p[1] = pAxis.c2p(di.p1, true);
return isHorizontal ? [s, p] : [p, s];
}
function transition(selection, fullLayout, opts, makeOnCompleteCallback) {
if(!fullLayout.uniformtext.mode && hasTransition(opts)) {
var onComplete;
if(makeOnCompleteCallback) {
onComplete = makeOnCompleteCallback();
}
return selection
.transition()
.duration(opts.duration)
.ease(opts.easing)
.each('end', function() { onComplete && onComplete(); })
.each('interrupt', function() { onComplete && onComplete(); });
} else {
return selection;
}
}
function hasTransition(transitionOpts) {
return transitionOpts && transitionOpts.duration > 0;
}
function plot(gd, plotinfo, cdModule, traceLayer, opts, makeOnCompleteCallback) {
var xa = plotinfo.xaxis;
var ya = plotinfo.yaxis;
var fullLayout = gd._fullLayout;
var isStatic = gd._context.staticPlot;
if(!opts) {
opts = {
mode: fullLayout.barmode,
norm: fullLayout.barmode,
gap: fullLayout.bargap,
groupgap: fullLayout.bargroupgap
};
// don't clear bar when this is called from waterfall or funnel
clearMinTextSize('bar', fullLayout);
}
var bartraces = Lib.makeTraceGroups(traceLayer, cdModule, 'trace bars').each(function(cd) {
var plotGroup = d3.select(this);
var trace = cd[0].trace;
var t = cd[0].t;
var isWaterfall = (trace.type === 'waterfall');
var isFunnel = (trace.type === 'funnel');
var isHistogram = (trace.type === 'histogram');
var isBar = (trace.type === 'bar');
var shouldDisplayZeros = (isBar || isFunnel);
var adjustPixel = 0;
if(isWaterfall && trace.connector.visible && trace.connector.mode === 'between') {
adjustPixel = trace.connector.line.width / 2;
}
var isHorizontal = (trace.orientation === 'h');
var withTransition = hasTransition(opts);
var pointGroup = Lib.ensureSingle(plotGroup, 'g', 'points');
var keyFunc = getKeyFunc(trace);
var bars = pointGroup.selectAll('g.point').data(Lib.identity, keyFunc);
bars.enter().append('g')
.classed('point', true);
bars.exit().remove();
bars.each(function(di, i) {
var bar = d3.select(this);
// now display the bar
// clipped xf/yf (2nd arg true): non-positive
// log values go off-screen by plotwidth
// so you see them continue if you drag the plot
var xy = getXY(di, xa, ya, isHorizontal);
var x0 = xy[0][0];
var x1 = xy[0][1];
var y0 = xy[1][0];
var y1 = xy[1][1];
// empty bars
var isBlank = (isHorizontal ? x1 - x0 : y1 - y0) === 0;
// display zeros if line.width > 0
if(isBlank && shouldDisplayZeros && helpers.getLineWidth(trace, di)) {
isBlank = false;
}
// skip nulls
if(!isBlank) {
isBlank = (
!isNumeric(x0) ||
!isNumeric(x1) ||
!isNumeric(y0) ||
!isNumeric(y1)
);
}
// record isBlank
di.isBlank = isBlank;
// for blank bars, ensure start and end positions are equal - important for smooth transitions
if(isBlank) {
if(isHorizontal) {
x1 = x0;
} else {
y1 = y0;
}
}
// in waterfall mode `between` we need to adjust bar end points to match the connector width
if(adjustPixel && !isBlank) {
if(isHorizontal) {
x0 -= dirSign(x0, x1) * adjustPixel;
x1 += dirSign(x0, x1) * adjustPixel;
} else {
y0 -= dirSign(y0, y1) * adjustPixel;
y1 += dirSign(y0, y1) * adjustPixel;
}
}
var lw;
var mc;
if(trace.type === 'waterfall') {
if(!isBlank) {
var cont = trace[di.dir].marker;
lw = cont.line.width;
mc = cont.color;
}
} else {
lw = helpers.getLineWidth(trace, di);
mc = di.mc || trace.marker.color;
}
function roundWithLine(v) {
var offset = d3.round((lw / 2) % 1, 2);
// if there are explicit gaps, don't round,
// it can make the gaps look crappy
return (opts.gap === 0 && opts.groupgap === 0) ?
d3.round(Math.round(v) - offset, 2) : v;
}
function expandToVisible(v, vc, hideZeroSpan) {
if(hideZeroSpan && v === vc) {
// should not expand zero span bars
// when start and end positions are identical
// i.e. for vertical when y0 === y1
// and for horizontal when x0 === x1
return v;
}
// if it's not in danger of disappearing entirely,
// round more precisely
return Math.abs(v - vc) >= 2 ? roundWithLine(v) :
// but if it's very thin, expand it so it's
// necessarily visible, even if it might overlap
// its neighbor
(v > vc ? Math.ceil(v) : Math.floor(v));
}
var op = Color.opacity(mc);
var fixpx = (op < 1 || lw > 0.01) ? roundWithLine : expandToVisible;
if(!gd._context.staticPlot) {
// if bars are not fully opaque or they have a line
// around them, round to integer pixels, mainly for
// safari so we prevent overlaps from its expansive
// pixelation. if the bars ARE fully opaque and have
// no line, expand to a full pixel to make sure we
// can see them
x0 = fixpx(x0, x1, isHorizontal);
x1 = fixpx(x1, x0, isHorizontal);
y0 = fixpx(y0, y1, !isHorizontal);
y1 = fixpx(y1, y0, !isHorizontal);
}
// Function to convert from size axis values to pixels
var c2p = isHorizontal ? xa.c2p : ya.c2p;
// Decide whether to use upper or lower bound of current bar stack
// as reference point for rounding
var outerBound;
if(di.s0 > 0) {
outerBound = di._sMax;
} else if(di.s0 < 0) {
outerBound = di._sMin;
} else {
outerBound = di.s1 > 0 ? di._sMax : di._sMin;
}
// Calculate corner radius of bar in pixels
function calcCornerRadius(crValue, crForm) {
if(!crValue) return 0;
var barWidth = isHorizontal ? Math.abs(y1 - y0) : Math.abs(x1 - x0);
var barLength = isHorizontal ? Math.abs(x1 - x0) : Math.abs(y1 - y0);
var stackedBarTotalLength = fixpx(Math.abs(c2p(outerBound, true) - c2p(0, true)));
var maxRadius = di.hasB ? Math.min(barWidth / 2, barLength / 2) : Math.min(barWidth / 2, stackedBarTotalLength);
var crPx;
if(crForm === '%') {
// If radius is given as a % string, convert to number of pixels
var crPercent = Math.min(50, crValue);
crPx = barWidth * (crPercent / 100);
} else {
// Otherwise, it's already a number of pixels, use the given value
crPx = crValue;
}
return fixpx(Math.max(Math.min(crPx, maxRadius), 0));
}
// Exclude anything which is not explicitly a bar or histogram chart from rounding
var r = (isBar || isHistogram) ? calcCornerRadius(t.cornerradiusvalue, t.cornerradiusform) : 0;
// Construct path string for bar
var path, h;
// Default rectangular path (used if no rounding)
var rectanglePath = 'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z';
var overhead = 0;
if(r && di.s) {
// Bar has cornerradius, and nonzero size
// Check amount of 'overhead' (bars stacked above this one)
// to see whether we need to round or not
var refPoint = sign(di.s0) === 0 || sign(di.s) === sign(di.s0) ? di.s1 : di.s0;
overhead = fixpx(!di.hasB ? Math.abs(c2p(outerBound, true) - c2p(refPoint, true)) : 0);
if(overhead < r) {
// Calculate parameters for rounded corners
var xdir = dirSign(x0, x1);
var ydir = dirSign(y0, y1);
// Sweep direction for rounded corner arcs
var cornersweep = (xdir === -ydir) ? 1 : 0;
if(isHorizontal) {
// Horizontal bars
if(di.hasB) {
// Floating base: Round 1st & 2nd, and 3rd & 4th corners
path = 'M' + (x0 + r * xdir) + ',' + y0 +
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + x0 + ',' + (y0 + r * ydir) +
'V' + (y1 - r * ydir) +
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + (x0 + r * xdir) + ',' + y1 +
'H' + (x1 - r * xdir) +
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + x1 + ',' + (y1 - r * ydir) +
'V' + (y0 + r * ydir) +
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + (x1 - r * xdir) + ',' + y0 +
'Z';
} else {
// Base on axis: Round 3rd and 4th corners
// Helper variables to help with extending rounding down to lower bars
h = Math.abs(x1 - x0) + overhead;
var dy1 = (h < r) ? r - Math.sqrt(h * (2 * r - h)) : 0;
var dy2 = (overhead > 0) ? Math.sqrt(overhead * (2 * r - overhead)) : 0;
var xminfunc = xdir > 0 ? Math.max : Math.min;
path = 'M' + x0 + ',' + y0 +
'V' + (y1 - dy1 * ydir) +
'H' + xminfunc(x1 - (r - overhead) * xdir, x0) +
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + x1 + ',' + (y1 - r * ydir - dy2) +
'V' + (y0 + r * ydir + dy2) +
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + xminfunc(x1 - (r - overhead) * xdir, x0) + ',' + (y0 + dy1 * ydir) +
'Z';
}
} else {
// Vertical bars
if(di.hasB) {
// Floating base: Round 1st & 4th, and 2nd & 3rd corners
path = 'M' + (x0 + r * xdir) + ',' + y0 +
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + x0 + ',' + (y0 + r * ydir) +
'V' + (y1 - r * ydir) +
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + (x0 + r * xdir) + ',' + y1 +
'H' + (x1 - r * xdir) +
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + x1 + ',' + (y1 - r * ydir) +
'V' + (y0 + r * ydir) +
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + (x1 - r * xdir) + ',' + y0 +
'Z';
} else {
// Base on axis: Round 2nd and 3rd corners
// Helper variables to help with extending rounding down to lower bars
h = Math.abs(y1 - y0) + overhead;
var dx1 = (h < r) ? r - Math.sqrt(h * (2 * r - h)) : 0;
var dx2 = (overhead > 0) ? Math.sqrt(overhead * (2 * r - overhead)) : 0;
var yminfunc = ydir > 0 ? Math.max : Math.min;
path = 'M' + (x0 + dx1 * xdir) + ',' + y0 +
'V' + yminfunc(y1 - (r - overhead) * ydir, y0) +
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + (x0 + r * xdir - dx2) + ',' + y1 +
'H' + (x1 - r * xdir + dx2) +
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + (x1 - dx1 * xdir) + ',' + yminfunc(y1 - (r - overhead) * ydir, y0) +
'V' + y0 + 'Z';
}
}
} else {
// There is a cornerradius, but bar is too far down the stack to be rounded; just draw a rectangle
path = rectanglePath;
}
} else {
// No cornerradius, just draw a rectangle
path = rectanglePath;
}
var sel = transition(Lib.ensureSingle(bar, 'path'), fullLayout, opts, makeOnCompleteCallback);
sel
.style('vector-effect', isStatic ? 'none' : 'non-scaling-stroke')
.attr('d', (isNaN((x1 - x0) * (y1 - y0)) || (isBlank && gd._context.staticPlot)) ? 'M0,0Z' : path)
.call(Drawing.setClipUrl, plotinfo.layerClipId, gd);
if(!fullLayout.uniformtext.mode && withTransition) {
var styleFns = Drawing.makePointStyleFns(trace);
Drawing.singlePointStyle(di, sel, trace, styleFns, gd);
}
appendBarText(gd, plotinfo, bar, cd, i, x0, x1, y0, y1, r, overhead, opts, makeOnCompleteCallback);
if(plotinfo.layerClipId) {
Drawing.hideOutsideRangePoint(di, bar.select('text'), xa, ya, trace.xcalendar, trace.ycalendar);
}
});
// lastly, clip points groups of `cliponaxis !== false` traces
// on `plotinfo._hasClipOnAxisFalse === true` subplots
var hasClipOnAxisFalse = trace.cliponaxis === false;
Drawing.setClipUrl(plotGroup, hasClipOnAxisFalse ? null : plotinfo.layerClipId, gd);
});
// error bars are on the top
Registry.getComponentMethod('errorbars', 'plot')(gd, bartraces, plotinfo, opts);
}
function appendBarText(gd, plotinfo, bar, cd, i, x0, x1, y0, y1, r, overhead, opts, makeOnCompleteCallback) {
var xa = plotinfo.xaxis;
var ya = plotinfo.yaxis;
var fullLayout = gd._fullLayout;
var textPosition;
function appendTextNode(bar, text, font) {
var textSelection = Lib.ensureSingle(bar, 'text')
.text(text)
.attr({
class: 'bartext bartext-' + textPosition,
'text-anchor': 'middle',
// prohibit tex interpretation until we can handle
// tex and regular text together
'data-notex': 1
})
.call(Drawing.font, font)
.call(svgTextUtils.convertToTspans, gd);
return textSelection;
}
// get trace attributes
var trace = cd[0].trace;
var isHorizontal = (trace.orientation === 'h');
var text = getText(fullLayout, cd, i, xa, ya);
textPosition = getTextPosition(trace, i);
// compute text position
var inStackOrRelativeMode =
opts.mode === 'stack' ||
opts.mode === 'relative';
var calcBar = cd[i];
var isOutmostBar = !inStackOrRelativeMode || calcBar._outmost;
var hasB = calcBar.hasB;
var barIsRounded = r && (r - overhead) > TEXTPAD;
if(!text ||
textPosition === 'none' ||
((calcBar.isBlank || x0 === x1 || y0 === y1) && (
textPosition === 'auto' ||
textPosition === 'inside'))) {
bar.select('text').remove();
return;
}
var layoutFont = fullLayout.font;
var barColor = style.getBarColor(cd[i], trace);
var insideTextFont = style.getInsideTextFont(trace, i, layoutFont, barColor);
var outsideTextFont = style.getOutsideTextFont(trace, i, layoutFont);
var insidetextanchor = trace.insidetextanchor || 'end';
// Special case: don't use the c2p(v, true) value on log size axes,
// so that we can get correctly inside text scaling
var di = bar.datum();
if(isHorizontal) {
if(xa.type === 'log' && di.s0 <= 0) {
if(xa.range[0] < xa.range[1]) {
x0 = 0;
} else {
x0 = xa._length;
}
}
} else {
if(ya.type === 'log' && di.s0 <= 0) {
if(ya.range[0] < ya.range[1]) {
y0 = ya._length;
} else {
y0 = 0;
}
}
}
// Compute width and height of bar
var lx = Math.abs(x1 - x0);
var ly = Math.abs(y1 - y0);
// padding excluded
var barWidth = lx - 2 * TEXTPAD;
var barHeight = ly - 2 * TEXTPAD;
var textSelection;
var textBB;
var textWidth;
var textHeight;
var font;
if(textPosition === 'outside') {
if(!isOutmostBar && !calcBar.hasB) textPosition = 'inside';
}
if(textPosition === 'auto') {
if(isOutmostBar) {
// draw text using insideTextFont and check if it fits inside bar
textPosition = 'inside';
font = Lib.ensureUniformFontSize(gd, insideTextFont);
textSelection = appendTextNode(bar, text, font);
textBB = Drawing.bBox(textSelection.node());
textWidth = textBB.width;
textHeight = textBB.height;
var textHasSize = (textWidth > 0 && textHeight > 0);
var fitsInside;
if(barIsRounded) {
// If bar is rounded, check if text fits between rounded corners
if(hasB) {
fitsInside = (
textfitsInsideBar(barWidth - 2 * r, barHeight, textWidth, textHeight, isHorizontal) ||
textfitsInsideBar(barWidth, barHeight - 2 * r, textWidth, textHeight, isHorizontal)
);
} else if(isHorizontal) {
fitsInside = (
textfitsInsideBar(barWidth - (r - overhead), barHeight, textWidth, textHeight, isHorizontal) ||
textfitsInsideBar(barWidth, barHeight - 2 * (r - overhead), textWidth, textHeight, isHorizontal)
);
} else {
fitsInside = (
textfitsInsideBar(barWidth, barHeight - (r - overhead), textWidth, textHeight, isHorizontal) ||
textfitsInsideBar(barWidth - 2 * (r - overhead), barHeight, textWidth, textHeight, isHorizontal)
);
}
} else {
fitsInside = textfitsInsideBar(barWidth, barHeight, textWidth, textHeight, isHorizontal);
}
if(textHasSize && fitsInside) {
textPosition = 'inside';
} else {
textPosition = 'outside';
textSelection.remove();
textSelection = null;
}
} else {
textPosition = 'inside';
}
}
if(!textSelection) {
font = Lib.ensureUniformFontSize(gd, (textPosition === 'outside') ? outsideTextFont : insideTextFont);
textSelection = appendTextNode(bar, text, font);
var currentTransform = textSelection.attr('transform');
textSelection.attr('transform', '');
textBB = Drawing.bBox(textSelection.node()),
textWidth = textBB.width,
textHeight = textBB.height;
textSelection.attr('transform', currentTransform);
if(textWidth <= 0 || textHeight <= 0) {
textSelection.remove();
return;
}
}
var angle = trace.textangle;
// compute text transform
var transform, constrained;
if(textPosition === 'outside') {
constrained =
trace.constraintext === 'both' ||
trace.constraintext === 'outside';
transform = toMoveOutsideBar(x0, x1, y0, y1, textBB, {
isHorizontal: isHorizontal,
constrained: constrained,
angle: angle
});
} else {
constrained =
trace.constraintext === 'both' ||
trace.constraintext === 'inside';
transform = toMoveInsideBar(x0, x1, y0, y1, textBB, {
isHorizontal: isHorizontal,
constrained: constrained,
angle: angle,
anchor: insidetextanchor,
hasB: hasB,
r: r,
overhead: overhead,
});
}
transform.fontSize = font.size;
recordMinTextSize(trace.type === 'histogram' ? 'bar' : trace.type, transform, fullLayout);
calcBar.transform = transform;
var s = transition(textSelection, fullLayout, opts, makeOnCompleteCallback);
Lib.setTransormAndDisplay(s, transform);
}
function textfitsInsideBar(barWidth, barHeight, textWidth, textHeight, isHorizontal) {
if(barWidth < 0 || barHeight < 0) return false;
var fitsInside = (textWidth <= barWidth && textHeight <= barHeight);
var fitsInsideIfRotated = (textWidth <= barHeight && textHeight <= barWidth);
var fitsInsideIfShrunk = (isHorizontal) ?
(barWidth >= textWidth * (barHeight / textHeight)) :
(barHeight >= textHeight * (barWidth / textWidth));
return fitsInside || fitsInsideIfRotated || fitsInsideIfShrunk;
}
function getRotateFromAngle(angle) {
return (angle === 'auto') ? 0 : angle;
}
function getRotatedTextSize(textBB, rotate) {
var a = Math.PI / 180 * rotate;
var absSin = Math.abs(Math.sin(a));
var absCos = Math.abs(Math.cos(a));
return {
x: textBB.width * absCos + textBB.height * absSin,
y: textBB.width * absSin + textBB.height * absCos
};
}
function toMoveInsideBar(x0, x1, y0, y1, textBB, opts) {
var isHorizontal = !!opts.isHorizontal;
var constrained = !!opts.constrained;
var angle = opts.angle || 0;
var anchor = opts.anchor;
var isEnd = anchor === 'end';
var isStart = anchor === 'start';
var leftToRight = opts.leftToRight || 0; // left: -1, center: 0, right: 1
var toRight = (leftToRight + 1) / 2;
var toLeft = 1 - toRight;
var hasB = opts.hasB;
var r = opts.r;
var overhead = opts.overhead;
var textWidth = textBB.width;
var textHeight = textBB.height;
var lx = Math.abs(x1 - x0);
var ly = Math.abs(y1 - y0);
// compute remaining space
var textpad = (
lx > (2 * TEXTPAD) &&
ly > (2 * TEXTPAD)
) ? TEXTPAD : 0;
lx -= 2 * textpad;
ly -= 2 * textpad;
var rotate = getRotateFromAngle(angle);
if((angle === 'auto') &&
!(textWidth <= lx && textHeight <= ly) &&
(textWidth > lx || textHeight > ly) && (
!(textWidth > ly || textHeight > lx) ||
((textWidth < textHeight) !== (lx < ly))
)) {
rotate += 90;
}
var t = getRotatedTextSize(textBB, rotate);
var scale, padForRounding;
// Scale text for rounded bars
if(r && (r - overhead) > TEXTPAD) {
var scaleAndPad = scaleTextForRoundedBar(x0, x1, y0, y1, t, r, overhead, isHorizontal, hasB);
scale = scaleAndPad.scale;
padForRounding = scaleAndPad.pad;
// Scale text for non-rounded bars
} else {
scale = 1;
if(constrained) {
scale = Math.min(
1,
lx / t.x,
ly / t.y
);
}
padForRounding = 0;
}
// compute text and target positions
var textX = (
textBB.left * toLeft +
textBB.right * toRight
);
var textY = (textBB.top + textBB.bottom) / 2;
var targetX = (
(x0 + TEXTPAD) * toLeft +
(x1 - TEXTPAD) * toRight
);
var targetY = (y0 + y1) / 2;
var anchorX = 0;
var anchorY = 0;
if(isStart || isEnd) {
var extrapad = (isHorizontal ? t.x : t.y) / 2;
if(r && (isEnd || hasB)) {
textpad += padForRounding;
}
var dir = isHorizontal ? dirSign(x0, x1) : dirSign(y0, y1);
if(isHorizontal) {
if(isStart) {
targetX = x0 + dir * textpad;
anchorX = -dir * extrapad;
} else {
targetX = x1 - dir * textpad;
anchorX = dir * extrapad;
}
} else {
if(isStart) {
targetY = y0 + dir * textpad;
anchorY = -dir * extrapad;
} else {
targetY = y1 - dir * textpad;
anchorY = dir * extrapad;
}
}
}
return {
textX: textX,
textY: textY,
targetX: targetX,
targetY: targetY,
anchorX: anchorX,
anchorY: anchorY,
scale: scale,
rotate: rotate
};
}
function scaleTextForRoundedBar(x0, x1, y0, y1, t, r, overhead, isHorizontal, hasB) {
var barWidth = Math.max(0, Math.abs(x1 - x0) - 2 * TEXTPAD);
var barHeight = Math.max(0, Math.abs(y1 - y0) - 2 * TEXTPAD);
var R = r - TEXTPAD;
var clippedR = overhead ? R - Math.sqrt(R * R - (R - overhead) * (R - overhead)) : R;
var rX = hasB ? R * 2 : (isHorizontal ? R - overhead : 2 * clippedR);
var rY = hasB ? R * 2 : (isHorizontal ? 2 * clippedR : R - overhead);
var a, b, c;
var scale, pad;
if(t.y / t.x >= barHeight / (barWidth - rX)) {
// Case 1 (Tall text)
scale = barHeight / t.y;
} else if(t.y / t.x <= (barHeight - rY) / barWidth) {
// Case 2 (Wide text)
scale = barWidth / t.x;
} else if(!hasB && isHorizontal) {
// Case 3a (Quadratic case, two side corners are rounded)
a = t.x * t.x + t.y * t.y / 4;
b = -2 * t.x * (barWidth - R) - t.y * (barHeight / 2 - R);
c = (barWidth - R) * (barWidth - R) + (barHeight / 2 - R) * (barHeight / 2 - R) - R * R;
scale = (-b + Math.sqrt(b * b - 4 * a * c)) / (2 * a);
} else if(!hasB) {
// Case 3b (Quadratic case, two top/bottom corners are rounded)
a = t.x * t.x / 4 + t.y * t.y;
b = -t.x * (barWidth / 2 - R) - 2 * t.y * (barHeight - R);
c = (barWidth / 2 - R) * (barWidth / 2 - R) + (barHeight - R) * (barHeight - R) - R * R;
scale = (-b + Math.sqrt(b * b - 4 * a * c)) / (2 * a);
} else {
// Case 4 (Quadratic case, all four corners are rounded)
a = (t.x * t.x + t.y * t.y) / 4;
b = -t.x * (barWidth / 2 - R) - t.y * (barHeight / 2 - R);
c = (barWidth / 2 - R) * (barWidth / 2 - R) + (barHeight / 2 - R) * (barHeight / 2 - R) - R * R;
scale = (-b + Math.sqrt(b * b - 4 * a * c)) / (2 * a);
}
// Scale should not be larger than 1
scale = Math.min(1, scale);
if(isHorizontal) {
pad = Math.max(0, R - Math.sqrt(Math.max(0, R * R - (R - (barHeight - t.y * scale) / 2) * (R - (barHeight - t.y * scale) / 2))) - overhead);
} else {
pad = Math.max(0, R - Math.sqrt(Math.max(0, R * R - (R - (barWidth - t.x * scale) / 2) * (R - (barWidth - t.x * scale) / 2))) - overhead);
}
return { scale: scale, pad: pad };
}
function toMoveOutsideBar(x0, x1, y0, y1, textBB, opts) {
var isHorizontal = !!opts.isHorizontal;
var constrained = !!opts.constrained;
var angle = opts.angle || 0;
var textWidth = textBB.width;
var textHeight = textBB.height;
var lx = Math.abs(x1 - x0);
var ly = Math.abs(y1 - y0);
var textpad;
// Keep the padding so the text doesn't sit right against
// the bars, but don't factor it into barWidth
if(isHorizontal) {
textpad = (ly > 2 * TEXTPAD) ? TEXTPAD : 0;
} else {
textpad = (lx > 2 * TEXTPAD) ? TEXTPAD : 0;
}
// compute rotate and scale
var scale = 1;
if(constrained) {
scale = (isHorizontal) ?
Math.min(1, ly / textHeight) :
Math.min(1, lx / textWidth);
}
var rotate = getRotateFromAngle(angle);
var t = getRotatedTextSize(textBB, rotate);
// compute text and target positions
var extrapad = (isHorizontal ? t.x : t.y) / 2;
var textX = (textBB.left + textBB.right) / 2;
var textY = (textBB.top + textBB.bottom) / 2;
var targetX = (x0 + x1) / 2;
var targetY = (y0 + y1) / 2;
var anchorX = 0;
var anchorY = 0;
var dir = isHorizontal ? dirSign(x1, x0) : dirSign(y0, y1);
if(isHorizontal) {
targetX = x1 - dir * textpad;
anchorX = dir * extrapad;
} else {
targetY = y1 + dir * textpad;
anchorY = -dir * extrapad;
}
return {
textX: textX,
textY: textY,
targetX: targetX,
targetY: targetY,
anchorX: anchorX,
anchorY: anchorY,
scale: scale,
rotate: rotate
};
}
function getText(fullLayout, cd, index, xa, ya) {
var trace = cd[0].trace;
var texttemplate = trace.texttemplate;
var value;
if(texttemplate) {
value = calcTexttemplate(fullLayout, cd, index, xa, ya);
} else if(trace.textinfo) {
value = calcTextinfo(cd, index, xa, ya);
} else {
value = helpers.getValue(trace.text, index);
}
return helpers.coerceString(attributeText, value);
}
function getTextPosition(trace, index) {
var value = helpers.getValue(trace.textposition, index);
return helpers.coerceEnumerated(attributeTextPosition, value);
}
function calcTexttemplate(fullLayout, cd, index, xa, ya) {
var trace = cd[0].trace;
var texttemplate = Lib.castOption(trace, index, 'texttemplate');
if(!texttemplate) return '';
var isHistogram = (trace.type === 'histogram');
var isWaterfall = (trace.type === 'waterfall');
var isFunnel = (trace.type === 'funnel');
var isHorizontal = trace.orientation === 'h';
var pLetter, pAxis;
var vLetter, vAxis;
if(isHorizontal) {
pLetter = 'y';
pAxis = ya;
vLetter = 'x';
vAxis = xa;
} else {
pLetter = 'x';
pAxis = xa;
vLetter = 'y';
vAxis = ya;
}
function formatLabel(u) {
return tickText(pAxis, pAxis.c2l(u), true).text;
}
function formatNumber(v) {
return tickText(vAxis, vAxis.c2l(v), true).text;
}
var cdi = cd[index];
var obj = {};
obj.label = cdi.p;
obj.labelLabel = obj[pLetter + 'Label'] = formatLabel(cdi.p);
var tx = Lib.castOption(trace, cdi.i, 'text');
if(tx === 0 || tx) obj.text = tx;
obj.value = cdi.s;
obj.valueLabel = obj[vLetter + 'Label'] = formatNumber(cdi.s);
var pt = {};
appendArrayPointValue(pt, trace, cdi.i);
if(isHistogram || pt.x === undefined) pt.x = isHorizontal ? obj.value : obj.label;
if(isHistogram || pt.y === undefined) pt.y = isHorizontal ? obj.label : obj.value;
if(isHistogram || pt.xLabel === undefined) pt.xLabel = isHorizontal ? obj.valueLabel : obj.labelLabel;
if(isHistogram || pt.yLabel === undefined) pt.yLabel = isHorizontal ? obj.labelLabel : obj.valueLabel;
if(isWaterfall) {
obj.delta = +cdi.rawS || cdi.s;
obj.deltaLabel = formatNumber(obj.delta);
obj.final = cdi.v;
obj.finalLabel = formatNumber(obj.final);
obj.initial = obj.final - obj.delta;
obj.initialLabel = formatNumber(obj.initial);
}
if(isFunnel) {
obj.value = cdi.s;
obj.valueLabel = formatNumber(obj.value);
obj.percentInitial = cdi.begR;
obj.percentInitialLabel = Lib.formatPercent(cdi.begR);
obj.percentPrevious = cdi.difR;
obj.percentPreviousLabel = Lib.formatPercent(cdi.difR);
obj.percentTotal = cdi.sumR;
obj.percenTotalLabel = Lib.formatPercent(cdi.sumR);
}
var customdata = Lib.castOption(trace, cdi.i, 'customdata');
if(customdata) obj.customdata = customdata;
return Lib.texttemplateString(texttemplate, obj, fullLayout._d3locale, pt, obj, trace._meta || {});
}
function calcTextinfo(cd, index, xa, ya) {
var trace = cd[0].trace;
var isHorizontal = (trace.orientation === 'h');
var isWaterfall = (trace.type === 'waterfall');
var isFunnel = (trace.type === 'funnel');
function formatLabel(u) {
var pAxis = isHorizontal ? ya : xa;
return tickText(pAxis, u, true).text;
}
function formatNumber(v) {
var sAxis = isHorizontal ? xa : ya;
return tickText(sAxis, +v, true).text;
}
var textinfo = trace.textinfo;
var cdi = cd[index];
var parts = textinfo.split('+');
var text = [];
var tx;
var hasFlag = function(flag) { return parts.indexOf(flag) !== -1; };
if(hasFlag('label')) {
text.push(formatLabel(cd[index].p));
}
if(hasFlag('text')) {
tx = Lib.castOption(trace, cdi.i, 'text');
if(tx === 0 || tx) text.push(tx);
}
if(isWaterfall) {
var delta = +cdi.rawS || cdi.s;
var final = cdi.v;
var initial = final - delta;
if(hasFlag('initial')) text.push(formatNumber(initial));
if(hasFlag('delta')) text.push(formatNumber(delta));
if(hasFlag('final')) text.push(formatNumber(final));
}
if(isFunnel) {
if(hasFlag('value')) text.push(formatNumber(cdi.s));
var nPercent = 0;
if(hasFlag('percent initial')) nPercent++;
if(hasFlag('percent previous')) nPercent++;
if(hasFlag('percent total')) nPercent++;
var hasMultiplePercents = nPercent > 1;
if(hasFlag('percent initial')) {
tx = Lib.formatPercent(cdi.begR);
if(hasMultiplePercents) tx += ' of initial';
text.push(tx);
}
if(hasFlag('percent previous')) {
tx = Lib.formatPercent(cdi.difR);
if(hasMultiplePercents) tx += ' of previous';
text.push(tx);
}
if(hasFlag('percent total')) {
tx = Lib.formatPercent(cdi.sumR);
if(hasMultiplePercents) tx += ' of total';
text.push(tx);
}
}
return text.join('<br>');
}
module.exports = {
plot: plot,
toMoveInsideBar: toMoveInsideBar
};