diff --git a/src/traces/bar/cross_trace_calc.js b/src/traces/bar/cross_trace_calc.js index ea1757df2f1..73459c702e6 100644 --- a/src/traces/bar/cross_trace_calc.js +++ b/src/traces/bar/cross_trace_calc.js @@ -9,6 +9,10 @@ var Axes = require('../../plots/cartesian/axes'); var getAxisGroup = require('../../plots/cartesian/constraints').getAxisGroup; var Sieve = require('./sieve.js'); +var TEXTPAD = require('./constants').TEXTPAD; +const { TEXTPAD } = require('./constants'); +const { BR_TAG_ALL } = require('../../lib/svg_text_utils'); + /* * Bar chart stacking/grouping positioning and autoscaling calculations * for each direction separately calculate the ranges and positions @@ -26,25 +30,25 @@ function crossTraceCalc(gd, plotinfo) { var calcTracesHorz = []; var calcTracesVert = []; - for(var i = 0; i < fullTraces.length; i++) { + for (var i = 0; i < fullTraces.length; i++) { var fullTrace = fullTraces[i]; - if( + if ( fullTrace.visible === true && Registry.traceIs(fullTrace, 'bar') && fullTrace.xaxis === xa._id && fullTrace.yaxis === ya._id ) { - if(fullTrace.orientation === 'h') { + if (fullTrace.orientation === 'h') { calcTracesHorz.push(calcTraces[i]); } else { calcTracesVert.push(calcTraces[i]); } - if(fullTrace._computePh) { + if (fullTrace._computePh) { var cd = gd.calcdata[i]; - for(var j = 0; j < cd.length; j++) { - if(typeof cd[j].ph0 === 'function') cd[j].ph0 = cd[j].ph0(); - if(typeof cd[j].ph1 === 'function') cd[j].ph1 = cd[j].ph1(); + for (var j = 0; j < cd.length; j++) { + if (typeof cd[j].ph0 === 'function') cd[j].ph0 = cd[j].ph0(); + if (typeof cd[j].ph1 === 'function') cd[j].ph1 = cd[j].ph1(); } } } @@ -65,7 +69,7 @@ function crossTraceCalc(gd, plotinfo) { } function setGroupPositions(gd, pa, sa, calcTraces, opts) { - if(!calcTraces.length) return; + if (!calcTraces.length) return; var excluded; var included; @@ -73,7 +77,7 @@ function setGroupPositions(gd, pa, sa, calcTraces, opts) { initBase(sa, calcTraces); - switch(opts.mode) { + switch (opts.mode) { case 'overlay': setGroupPositionsInOverlayMode(gd, pa, sa, calcTraces, opts); break; @@ -82,18 +86,18 @@ function setGroupPositions(gd, pa, sa, calcTraces, opts) { // exclude from the group those traces for which the user set an offset excluded = []; included = []; - for(i = 0; i < calcTraces.length; i++) { + for (i = 0; i < calcTraces.length; i++) { calcTrace = calcTraces[i]; fullTrace = calcTrace[0].trace; - if(fullTrace.offset === undefined) included.push(calcTrace); + if (fullTrace.offset === undefined) included.push(calcTrace); else excluded.push(calcTrace); } - if(included.length) { + if (included.length) { setGroupPositionsInGroupMode(gd, pa, sa, included, opts); } - if(excluded.length) { + if (excluded.length) { setGroupPositionsInOverlayMode(gd, pa, sa, excluded, opts); } break; @@ -103,11 +107,11 @@ function setGroupPositions(gd, pa, sa, calcTraces, opts) { // exclude from the stack those traces for which the user set a base excluded = []; included = []; - for(i = 0; i < calcTraces.length; i++) { + for (i = 0; i < calcTraces.length; i++) { calcTrace = calcTraces[i]; fullTrace = calcTrace[0].trace; - if(fullTrace.base === undefined) included.push(calcTrace); + if (fullTrace.base === undefined) included.push(calcTrace); else excluded.push(calcTrace); } @@ -115,10 +119,10 @@ function setGroupPositions(gd, pa, sa, calcTraces, opts) { // in `included` to match the first trace which has a cornerradius standardizeCornerradius(included); - if(included.length) { + if (included.length) { setGroupPositionsInStackOrRelativeMode(gd, pa, sa, included, opts); } - if(excluded.length) { + if (excluded.length) { setGroupPositionsInOverlayMode(gd, pa, sa, excluded, opts); } break; @@ -131,14 +135,14 @@ function setGroupPositions(gd, pa, sa, calcTraces, opts) { function setCornerradius(calcTraces) { var i, calcTrace, fullTrace, t, cr, crValue, crForm; - for(i = 0; i < calcTraces.length; i++) { + for (i = 0; i < calcTraces.length; i++) { calcTrace = calcTraces[i]; fullTrace = calcTrace[0].trace; t = calcTrace[0].t; - if(t.cornerradiusvalue === undefined) { + if (t.cornerradiusvalue === undefined) { cr = fullTrace.marker ? fullTrace.marker.cornerradius : undefined; - if(cr !== undefined) { + if (cr !== undefined) { crValue = isNumeric(cr) ? +cr : +cr.slice(0, -1); crForm = isNumeric(cr) ? 'px' : '%'; t.cornerradiusvalue = crValue; @@ -150,21 +154,21 @@ function setCornerradius(calcTraces) { // Make sure all traces in a stack use the same cornerradius function standardizeCornerradius(calcTraces) { - if(calcTraces.length < 2) return; + if (calcTraces.length < 2) return; var i, calcTrace, fullTrace, t; var cr, crValue, crForm; - for(i = 0; i < calcTraces.length; i++) { + for (i = 0; i < calcTraces.length; i++) { calcTrace = calcTraces[i]; fullTrace = calcTrace[0].trace; cr = fullTrace.marker ? fullTrace.marker.cornerradius : undefined; - if(cr !== undefined) break; + if (cr !== undefined) break; } // If any trace has cornerradius, store first cornerradius // in calcTrace[0].t so that all traces in stack use same cornerradius - if(cr !== undefined) { + if (cr !== undefined) { crValue = isNumeric(cr) ? +cr : +cr.slice(0, -1); crForm = isNumeric(cr) ? 'px' : '%'; - for(i = 0; i < calcTraces.length; i++) { + for (i = 0; i < calcTraces.length; i++) { calcTrace = calcTraces[i]; t = calcTrace[0].t; @@ -177,10 +181,10 @@ function standardizeCornerradius(calcTraces) { function initBase(sa, calcTraces) { var i, j; - for(i = 0; i < calcTraces.length; i++) { + for (i = 0; i < calcTraces.length; i++) { var cd = calcTraces[i]; var trace = cd[0].trace; - var base = (trace.type === 'funnel') ? trace._base : trace.base; + var base = trace.type === 'funnel' ? trace._base : trace.base; var b; // not sure if it really makes sense to have dates for bar size data... @@ -190,28 +194,31 @@ function initBase(sa, calcTraces) { var scalendar = trace.orientation === 'h' ? trace.xcalendar : trace.ycalendar; // 'base' on categorical axes makes no sense - var d2c = sa.type === 'category' || sa.type === 'multicategory' ? - function() { return null; } : - sa.d2c; - - if(isArrayOrTypedArray(base)) { - for(j = 0; j < Math.min(base.length, cd.length); j++) { + var d2c = + sa.type === 'category' || sa.type === 'multicategory' + ? function () { + return null; + } + : sa.d2c; + + if (isArrayOrTypedArray(base)) { + for (j = 0; j < Math.min(base.length, cd.length); j++) { b = d2c(base[j], 0, scalendar); - if(isNumeric(b)) { + if (isNumeric(b)) { cd[j].b = +b; cd[j].hasB = 1; } else cd[j].b = 0; } - for(; j < cd.length; j++) { + for (; j < cd.length; j++) { cd[j].b = 0; } } else { b = d2c(base, 0, scalendar); var hasBase = isNumeric(b); b = hasBase ? b : 0; - for(j = 0; j < cd.length; j++) { + for (j = 0; j < cd.length; j++) { cd[j].b = b; - if(hasBase) cd[j].hasB = 1; + if (hasBase) cd[j].hasB = 1; } } } @@ -219,7 +226,7 @@ function initBase(sa, calcTraces) { function setGroupPositionsInOverlayMode(gd, pa, sa, calcTraces, opts) { // update position axis and set bar offsets and widths - for(var i = 0; i < calcTraces.length; i++) { + for (var i = 0; i < calcTraces.length; i++) { var calcTrace = calcTraces[i]; var sieve = new Sieve([calcTrace], { @@ -236,7 +243,7 @@ function setGroupPositionsInOverlayMode(gd, pa, sa, calcTraces, opts) { // (note that `setGroupPositionsInOverlayMode` handles the case barnorm // is defined, because this function is also invoked for traces that // can't be grouped or stacked) - if(opts.norm) { + if (opts.norm) { sieveBars(sieve); normalizeBars(sa, sieve, opts); } else { @@ -260,7 +267,7 @@ function setGroupPositionsInGroupMode(gd, pa, sa, calcTraces, opts) { unhideBarsWithinTrace(sieve, pa); // set bar bases and sizes, and update size axis - if(opts.norm) { + if (opts.norm) { sieveBars(sieve); normalizeBars(sa, sieve, opts); } else { @@ -282,22 +289,22 @@ function setGroupPositionsInStackOrRelativeMode(gd, pa, sa, calcTraces, opts) { stackBars(sa, sieve, opts); // flag the outmost bar (for text display purposes) - for(var i = 0; i < calcTraces.length; i++) { + for (var i = 0; i < calcTraces.length; i++) { var calcTrace = calcTraces[i]; var offsetIndex = calcTrace[0].t.offsetindex; - for(var j = 0; j < calcTrace.length; j++) { + for (var j = 0; j < calcTrace.length; j++) { var bar = calcTrace[j]; - if(bar.s !== BADNUM) { - var isOutmostBar = ((bar.b + bar.s) === sieve.get(bar.p, offsetIndex, bar.s)); - if(isOutmostBar) bar._outmost = true; + if (bar.s !== BADNUM) { + var isOutmostBar = bar.b + bar.s === sieve.get(bar.p, offsetIndex, bar.s); + if (isOutmostBar) bar._outmost = true; } } } // Note that marking the outmost bars has to be done // before `normalizeBars` changes `bar.b` and `bar.s`. - if(opts.norm) normalizeBars(sa, sieve, opts); + if (opts.norm) normalizeBars(sa, sieve, opts); } /** @@ -322,30 +329,31 @@ function setOffsetAndWidth(gd, pa, sieve, opts) { // if there aren't any overlapping positions, // let them have full width even if mode is group - var overlap = (positions.length !== distinctPositions.length); + var overlap = positions.length !== distinctPositions.length; var barGroupWidth = minDiff * (1 - opts.gap); var barWidthPlusGap; var barWidth; var offsetFromCenter; var alignmentGroups; - if(pa._id === 'angularaxis') { + if (pa._id === 'angularaxis') { barWidthPlusGap = barGroupWidth; barWidth = barWidthPlusGap * (1 - (opts.groupgap || 0)); offsetFromCenter = -barWidth / 2; - } else { // collect groups and calculate values in loop below + } else { + // collect groups and calculate values in loop below var groupId = getAxisGroup(fullLayout, pa._id) + calcTraces[0][0].trace.orientation; alignmentGroups = fullLayout._alignmentOpts[groupId] || {}; } - for(var i = 0; i < nTraces; i++) { + for (var i = 0; i < nTraces; i++) { var calcTrace = calcTraces[i]; var trace = calcTrace[0].trace; - if(pa._id !== 'angularaxis') { + if (pa._id !== 'angularaxis') { var alignmentGroupOpts = alignmentGroups[trace.alignmentgroup] || {}; var nOffsetGroups = Object.keys(alignmentGroupOpts.offsetGroups || {}).length; - if(nOffsetGroups) { + if (nOffsetGroups) { barWidthPlusGap = barGroupWidth / nOffsetGroups; } else { barWidthPlusGap = overlap ? barGroupWidth / nTraces : barGroupWidth; @@ -353,12 +361,10 @@ function setOffsetAndWidth(gd, pa, sieve, opts) { barWidth = barWidthPlusGap * (1 - (opts.groupgap || 0)); - if(nOffsetGroups) { + if (nOffsetGroups) { offsetFromCenter = ((2 * trace._offsetIndex + 1 - nOffsetGroups) * barWidthPlusGap - barWidth) / 2; } else { - offsetFromCenter = overlap ? - ((2 * i + 1 - nTraces) * barWidthPlusGap - barWidth) / 2 : - -barWidth / 2; + offsetFromCenter = overlap ? ((2 * i + 1 - nTraces) * barWidthPlusGap - barWidth) / 2 : -barWidth / 2; } } @@ -380,7 +386,7 @@ function setOffsetAndWidth(gd, pa, sieve, opts) { setBarCenterAndWidth(pa, sieve); // update position axes - if(pa._id === 'angularaxis') { + if (pa._id === 'angularaxis') { updatePositionAxis(pa, sieve); } else { updatePositionAxis(pa, sieve, overlap); @@ -391,7 +397,7 @@ function applyAttributes(sieve) { var calcTraces = sieve.traces; var i, j; - for(i = 0; i < calcTraces.length; i++) { + for (i = 0; i < calcTraces.length; i++) { var calcTrace = calcTraces[i]; var calcTrace0 = calcTrace[0]; var fullTrace = calcTrace0.trace; @@ -400,43 +406,43 @@ function applyAttributes(sieve) { var initialPoffset = t.poffset; var newPoffset; - if(isArrayOrTypedArray(offset)) { + if (isArrayOrTypedArray(offset)) { // if offset is an array, then clone it into t.poffset. newPoffset = Array.prototype.slice.call(offset, 0, calcTrace.length); // guard against non-numeric items - for(j = 0; j < newPoffset.length; j++) { - if(!isNumeric(newPoffset[j])) { + for (j = 0; j < newPoffset.length; j++) { + if (!isNumeric(newPoffset[j])) { newPoffset[j] = initialPoffset; } } // if the length of the array is too short, // then extend it with the initial value of t.poffset - for(j = newPoffset.length; j < calcTrace.length; j++) { + for (j = newPoffset.length; j < calcTrace.length; j++) { newPoffset.push(initialPoffset); } t.poffset = newPoffset; - } else if(offset !== undefined) { + } else if (offset !== undefined) { t.poffset = offset; } var width = fullTrace._width || fullTrace.width; var initialBarwidth = t.barwidth; - if(isArrayOrTypedArray(width)) { + if (isArrayOrTypedArray(width)) { // if width is an array, then clone it into t.barwidth. var newBarwidth = Array.prototype.slice.call(width, 0, calcTrace.length); // guard against non-numeric items - for(j = 0; j < newBarwidth.length; j++) { - if(!isNumeric(newBarwidth[j])) newBarwidth[j] = initialBarwidth; + for (j = 0; j < newBarwidth.length; j++) { + if (!isNumeric(newBarwidth[j])) newBarwidth[j] = initialBarwidth; } // if the length of the array is too short, // then extend it with the initial value of t.barwidth - for(j = newBarwidth.length; j < calcTrace.length; j++) { + for (j = newBarwidth.length; j < calcTrace.length; j++) { newBarwidth.push(initialBarwidth); } @@ -444,21 +450,19 @@ function applyAttributes(sieve) { // if user didn't set offset, // then correct t.poffset to ensure bars remain centered - if(offset === undefined) { + if (offset === undefined) { newPoffset = []; - for(j = 0; j < calcTrace.length; j++) { - newPoffset.push( - initialPoffset + (initialBarwidth - newBarwidth[j]) / 2 - ); + for (j = 0; j < calcTrace.length; j++) { + newPoffset.push(initialPoffset + (initialBarwidth - newBarwidth[j]) / 2); } t.poffset = newPoffset; } - } else if(width !== undefined) { + } else if (width !== undefined) { t.barwidth = width; // if user didn't set offset, // then correct t.poffset to ensure bars remain centered - if(offset === undefined) { + if (offset === undefined) { t.poffset = initialPoffset + (initialBarwidth - width) / 2; } } @@ -469,7 +473,7 @@ function setBarCenterAndWidth(pa, sieve) { var calcTraces = sieve.traces; var pLetter = getAxisLetter(pa); - for(var i = 0; i < calcTraces.length; i++) { + for (var i = 0; i < calcTraces.length; i++) { var calcTrace = calcTraces[i]; var t = calcTrace[0].t; var poffset = t.poffset; @@ -477,13 +481,13 @@ function setBarCenterAndWidth(pa, sieve) { var barwidth = t.barwidth; var barwidthIsArray = isArrayOrTypedArray(barwidth); - for(var j = 0; j < calcTrace.length; j++) { + for (var j = 0; j < calcTrace.length; j++) { var calcBar = calcTrace[j]; // store the actual bar width and position, for use by hover - var width = calcBar.w = barwidthIsArray ? barwidth[j] : barwidth; + var width = (calcBar.w = barwidthIsArray ? barwidth[j] : barwidth); - if(calcBar.p === undefined) { + if (calcBar.p === undefined) { calcBar.p = calcBar[pLetter]; calcBar['orig_' + pLetter] = calcBar[pLetter]; } @@ -501,28 +505,28 @@ function updatePositionAxis(pa, sieve, allowMinDtick) { Axes.minDtick(pa, sieve.minDiff, sieve.distinctPositions[0], allowMinDtick); - for(var i = 0; i < calcTraces.length; i++) { + for (var i = 0; i < calcTraces.length; i++) { var calcTrace = calcTraces[i]; var calcTrace0 = calcTrace[0]; var fullTrace = calcTrace0.trace; var pts = []; var bar, l, r, j; - for(j = 0; j < calcTrace.length; j++) { + for (j = 0; j < calcTrace.length; j++) { bar = calcTrace[j]; l = bar.p - vpad; r = bar.p + vpad; pts.push(l, r); } - if(fullTrace.width || fullTrace.offset) { + if (fullTrace.width || fullTrace.offset) { var t = calcTrace0.t; var poffset = t.poffset; var barwidth = t.barwidth; var poffsetIsArray = isArrayOrTypedArray(poffset); var barwidthIsArray = isArrayOrTypedArray(barwidth); - for(j = 0; j < calcTrace.length; j++) { + for (j = 0; j < calcTrace.length; j++) { bar = calcTrace[j]; var calcBarOffset = poffsetIsArray ? poffset[j] : poffset; var calcBarWidth = barwidthIsArray ? barwidth[j] : barwidth; @@ -532,7 +536,7 @@ function updatePositionAxis(pa, sieve, allowMinDtick) { } } - fullTrace._extremes[pa._id] = Axes.findExtremes(pa, pts, {padded: false}); + fullTrace._extremes[pa._id] = Axes.findExtremes(pa, pts, { padded: false }); } } @@ -543,7 +547,7 @@ function setBaseAndTop(sa, sieve) { var calcTraces = sieve.traces; var sLetter = getAxisLetter(sa); - for(var i = 0; i < calcTraces.length; i++) { + for (var i = 0; i < calcTraces.length; i++) { var calcTrace = calcTraces[i]; var fullTrace = calcTrace[0].trace; var isScatter = fullTrace.type === 'scatter'; @@ -551,25 +555,26 @@ function setBaseAndTop(sa, sieve) { var pts = []; var tozero = false; - for(var j = 0; j < calcTrace.length; j++) { + for (var j = 0; j < calcTrace.length; j++) { var bar = calcTrace[j]; var base = isScatter ? 0 : bar.b; - var top = isScatter ? ( - isVertical ? bar.y : bar.x - ) : base + bar.s; + var top = isScatter ? (isVertical ? bar.y : bar.x) : base + bar.s; bar[sLetter] = top; pts.push(top); - if(bar.hasB) pts.push(base); + if (bar.hasB) pts.push(base); - if(!bar.hasB || !bar.b) { + if (!bar.hasB || !bar.b) { tozero = true; } } + const extraPad = estimateAxisPaddingForText(fullTrace, calcTrace); fullTrace._extremes[sa._id] = Axes.findExtremes(sa, pts, { tozero: tozero, - padded: true + padded: true, + ppadplus: extraPad.ppadplus, + ppadminus: extraPad.ppadminus }); } } @@ -584,16 +589,16 @@ function stackBars(sa, sieve, opts) { var bar; var offsetIndex; - for(i = 0; i < calcTraces.length; i++) { + for (i = 0; i < calcTraces.length; i++) { calcTrace = calcTraces[i]; fullTrace = calcTrace[0].trace; - if(fullTrace.type === 'funnel') { + if (fullTrace.type === 'funnel') { offsetIndex = calcTrace[0].t.offsetindex; - for(j = 0; j < calcTrace.length; j++) { + for (j = 0; j < calcTrace.length; j++) { bar = calcTrace[j]; - if(bar.s !== BADNUM) { + if (bar.s !== BADNUM) { // create base of funnels sieve.put(bar.p, offsetIndex, -0.5 * bar.s); } @@ -601,23 +606,23 @@ function stackBars(sa, sieve, opts) { } } - for(i = 0; i < calcTraces.length; i++) { + for (i = 0; i < calcTraces.length; i++) { calcTrace = calcTraces[i]; fullTrace = calcTrace[0].trace; - isFunnel = (fullTrace.type === 'funnel'); + isFunnel = fullTrace.type === 'funnel'; offsetIndex = fullTrace.type === 'barpolar' ? 0 : calcTrace[0].t.offsetindex; var pts = []; - for(j = 0; j < calcTrace.length; j++) { + for (j = 0; j < calcTrace.length; j++) { bar = calcTrace[j]; - if(bar.s !== BADNUM) { + if (bar.s !== BADNUM) { // stack current bar and get previous sum var value; - if(isFunnel) { + if (isFunnel) { value = bar.s; } else { value = bar.s + bar.b; @@ -630,9 +635,9 @@ function stackBars(sa, sieve, opts) { bar.b = base; bar[sLetter] = top; - if(!opts.norm) { + if (!opts.norm) { pts.push(top); - if(bar.hasB) { + if (bar.hasB) { pts.push(base); } } @@ -640,12 +645,15 @@ function stackBars(sa, sieve, opts) { } // if barnorm is set, let normalizeBars update the axis range - if(!opts.norm) { + if (!opts.norm) { + const extraPad = estimateAxisPaddingForText(fullTrace, calcTrace); fullTrace._extremes[sa._id] = Axes.findExtremes(sa, pts, { // N.B. we don't stack base with 'base', // so set tozero:true always! tozero: true, - padded: true + padded: true, + ppadplus: extraPad.ppadplus, + ppadminus: extraPad.ppadminus }); } } @@ -654,13 +662,13 @@ function stackBars(sa, sieve, opts) { function sieveBars(sieve) { var calcTraces = sieve.traces; - for(var i = 0; i < calcTraces.length; i++) { + for (var i = 0; i < calcTraces.length; i++) { var calcTrace = calcTraces[i]; var offsetIndex = calcTrace[0].t.offsetindex; - for(var j = 0; j < calcTrace.length; j++) { + for (var j = 0; j < calcTrace.length; j++) { var bar = calcTrace[j]; - if(bar.s !== BADNUM) { + if (bar.s !== BADNUM) { sieve.put(bar.p, offsetIndex, bar.b + bar.s); } } @@ -670,29 +678,29 @@ function sieveBars(sieve) { function unhideBarsWithinTrace(sieve, pa) { var calcTraces = sieve.traces; - for(var i = 0; i < calcTraces.length; i++) { + for (var i = 0; i < calcTraces.length; i++) { var calcTrace = calcTraces[i]; var fullTrace = calcTrace[0].trace; var offsetIndex = calcTrace[0].t.offsetindex; - if(fullTrace.base === undefined) { + if (fullTrace.base === undefined) { var inTraceSieve = new Sieve([calcTrace], { posAxis: pa, sepNegVal: true, overlapNoMerge: true }); - for(var j = 0; j < calcTrace.length; j++) { + for (var j = 0; j < calcTrace.length; j++) { var bar = calcTrace[j]; - if(bar.p !== BADNUM) { + if (bar.p !== BADNUM) { // stack current bar and get previous sum var base = inTraceSieve.put(bar.p, offsetIndex, bar.b + bar.s); // if previous sum if non-zero, this means: // multiple bars have same starting point are potentially hidden, // shift them vertically so that all bars are visible by default - if(base) bar.b = base; + if (base) bar.b = base; } } } @@ -712,13 +720,10 @@ function normalizeBars(sa, sieve, opts) { var sMax = opts.mode === 'stack' ? sTop : sMin; function needsPadding(v) { - return ( - isNumeric(sa.c2l(v)) && - ((v < sMin - sTiny) || (v > sMax + sTiny) || !isNumeric(sMin)) - ); + return isNumeric(sa.c2l(v)) && (v < sMin - sTiny || v > sMax + sTiny || !isNumeric(sMin)); } - for(var i = 0; i < calcTraces.length; i++) { + for (var i = 0; i < calcTraces.length; i++) { var calcTrace = calcTraces[i]; var offsetIndex = calcTrace[0].t.offsetindex; var fullTrace = calcTrace[0].trace; @@ -726,10 +731,10 @@ function normalizeBars(sa, sieve, opts) { var tozero = false; var padded = false; - for(var j = 0; j < calcTrace.length; j++) { + for (var j = 0; j < calcTrace.length; j++) { var bar = calcTrace[j]; - if(bar.s !== BADNUM) { + if (bar.s !== BADNUM) { var scale = Math.abs(sTop / sieve.get(bar.p, offsetIndex, bar.s)); bar.b *= scale; bar.s *= scale; @@ -741,33 +746,78 @@ function normalizeBars(sa, sieve, opts) { pts.push(top); padded = padded || needsPadding(top); - if(bar.hasB) { + if (bar.hasB) { pts.push(base); padded = padded || needsPadding(base); } - if(!bar.hasB || !bar.b) { + if (!bar.hasB || !bar.b) { tozero = true; } } } + const extraPad = estimateAxisPaddingForText(fullTrace, calcTrace); fullTrace._extremes[sa._id] = Axes.findExtremes(sa, pts, { tozero: tozero, - padded: padded + padded: padded, + ppadplus: extraPad.ppadplus, + ppadminus: extraPad.ppadminus }); } } +// Returns a very lightweight estimate of extra padding (in pixels) +// needed to accommodate outside text labels on bars. Only adds padding +// vertical bars with textposition 'outside' and textangle 0 or 'auto' +// for now. +// +// This mitigates the most common scenario where a simple vertical +// bar chart with textposition set to 'outside' experiences text +// labels being cut off at the edge of the plot area. +// +// More complex scenarios (horizontal bars, various text angles) +// are not (yet) handled here, but could be in the future. +// Returns an object with ppadplus and ppadminus values, +// to be passed into Axes.findExtremes. +function estimateAxisPaddingForText(trace, calcTrace) { + if ( + trace.orientation === 'v' && + (trace.text || trace.texttemplate) && + trace.textposition === 'outside' && + (trace.textangle === 'auto' || trace.textangle === 0) + ) { + // count number of lines by counting
elements + function countLines(text) { + if (!text || typeof text !== 'string') return 0; + return (text.match(BR_TAG_ALL) || []).length + 1; + } + const nLines = trace.texttemplate + ? countLines(trace.texttemplate) + : isArrayOrTypedArray(trace.text) + ? Math.max(...trace.text.map((t) => countLines(t))) + : countLines(trace.text); + + const padAmount = trace.outsidetextfont.size * LINE_SPACING * nLines + TEXTPAD; + return { + // Yes, I know this looks backwards from what it should be, + // but it works like this + ppadplus: calcTrace.some((bar) => bar.s < 0) ? padAmount : 0, + ppadminus: calcTrace.some((bar) => bar.s >= 0) ? padAmount : 0 + }; + } + return { ppadplus: undefined, ppadminus: undefined }; +} + // Add an `_sMin` and `_sMax` value for each bar representing the min and max size value // across all bars sharing the same position as that bar. These values are used for rounded // bar corners, to carry rounding down to lower bars in the stack as needed. function setHelperValuesForRoundedCorners(calcTraces, sMinByPos, sMaxByPos, pa) { var pLetter = getAxisLetter(pa); // Set `_sMin` and `_sMax` value for each bar - for(var i = 0; i < calcTraces.length; i++) { + for (var i = 0; i < calcTraces.length; i++) { var calcTrace = calcTraces[i]; - for(var j = 0; j < calcTrace.length; j++) { + for (var j = 0; j < calcTrace.length; j++) { var bar = calcTrace[j]; var pos = bar[pLetter]; bar._sMin = sMinByPos[pos]; @@ -789,11 +839,11 @@ function collectExtents(calcTraces, pa) { var pMin = Infinity; var pMax = -Infinity; - for(i = 0; i < calcTraces.length; i++) { + for (i = 0; i < calcTraces.length; i++) { cd = calcTraces[i]; - for(j = 0; j < cd.length; j++) { + for (j = 0; j < cd.length; j++) { var p = cd[j].p; - if(isNumeric(p)) { + if (isNumeric(p)) { pMin = Math.min(pMin, p); pMax = Math.max(pMax, p); } @@ -804,9 +854,9 @@ function collectExtents(calcTraces, pa) { // the label is 1px too far out; so round positions to 1/10K in case // position values don't exactly match from trace to trace var roundFactor = 10000 / (pMax - pMin); - var round = extents.round = function(p) { + var round = (extents.round = function (p) { return String(Math.round(roundFactor * (p - pMin))); - }; + }); // Find min and max size axis extent for each position // This is used for rounded bar corners, to carry rounding @@ -815,26 +865,26 @@ function collectExtents(calcTraces, pa) { var sMaxByPos = {}; // Check whether any trace has rounded corners - var anyTraceHasCornerradius = calcTraces.some(function(x) { + var anyTraceHasCornerradius = calcTraces.some(function (x) { var trace = x[0].trace; return 'marker' in trace && trace.marker.cornerradius; }); - for(i = 0; i < calcTraces.length; i++) { + for (i = 0; i < calcTraces.length; i++) { cd = calcTraces[i]; cd[0].t.extents = extents; var poffset = cd[0].t.poffset; var poffsetIsArray = isArrayOrTypedArray(poffset); - for(j = 0; j < cd.length; j++) { + for (j = 0; j < cd.length; j++) { var di = cd[j]; var p0 = di[pLetter] - di.w / 2; - if(isNumeric(p0)) { + if (isNumeric(p0)) { var p1 = di[pLetter] + di.w / 2; var pVal = round(di.p); - if(extents[pVal]) { + if (extents[pVal]) { extents[pVal] = [Math.min(p0, extents[pVal][0]), Math.max(p1, extents[pVal][1])]; } else { extents[pVal] = [p0, p1]; @@ -846,16 +896,16 @@ function collectExtents(calcTraces, pa) { di.s0 = di.b; di.s1 = di.s0 + di.s; - if(anyTraceHasCornerradius) { + if (anyTraceHasCornerradius) { var sMin = Math.min(di.s0, di.s1) || 0; var sMax = Math.max(di.s0, di.s1) || 0; var pos = di[pLetter]; - sMinByPos[pos] = (pos in sMinByPos) ? Math.min(sMinByPos[pos], sMin) : sMin; - sMaxByPos[pos] = (pos in sMaxByPos) ? Math.max(sMaxByPos[pos], sMax) : sMax; + sMinByPos[pos] = pos in sMinByPos ? Math.min(sMinByPos[pos], sMin) : sMin; + sMaxByPos[pos] = pos in sMaxByPos ? Math.max(sMaxByPos[pos], sMax) : sMax; } } } - if(anyTraceHasCornerradius) { + if (anyTraceHasCornerradius) { setHelperValuesForRoundedCorners(calcTraces, sMinByPos, sMaxByPos, pa); } } diff --git a/test/image/baselines/funnel_multicategory.png b/test/image/baselines/funnel_multicategory.png index e9bc1ab0cbb..c6fd2fa5b2a 100644 Binary files a/test/image/baselines/funnel_multicategory.png and b/test/image/baselines/funnel_multicategory.png differ diff --git a/test/image/baselines/hist_stacked.png b/test/image/baselines/hist_stacked.png index b5b580fb99b..ace44eef128 100644 Binary files a/test/image/baselines/hist_stacked.png and b/test/image/baselines/hist_stacked.png differ diff --git a/test/image/baselines/waterfall_attrs.png b/test/image/baselines/waterfall_attrs.png index f83563063e6..eff442c60be 100644 Binary files a/test/image/baselines/waterfall_attrs.png and b/test/image/baselines/waterfall_attrs.png differ diff --git a/test/image/mocks/hist_stacked.json b/test/image/mocks/hist_stacked.json index 99283aab75a..687c9ca3665 100644 --- a/test/image/mocks/hist_stacked.json +++ b/test/image/mocks/hist_stacked.json @@ -11,7 +11,6 @@ "x": [1, 2, 3, 4], "text": "Orange", "textposition": "outside", - "cliponaxis": false, "texttemplate": "%{value}
%{text}", "type": "histogram" }