'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('
'); } module.exports = { plot: plot, toMoveInsideBar: toMoveInsideBar };