UNPKG

plotly.js

Version:

The open source javascript graphing library that powers plotly

926 lines (785 loc) 31.4 kB
'use strict'; const isNumeric = require('fast-isnumeric'); const { isArrayOrTypedArray } = require('../../lib'); const { BADNUM } = require('../../constants/numerical'); const Registry = require('../../registry'); const Axes = require('../../plots/cartesian/axes'); const { getAxisGroup } = require('../../plots/cartesian/constraints'); const Sieve = require('./sieve.js'); const { TEXTPAD } = require('./constants'); const { LINE_SPACING } = require('../../constants/alignment'); 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 * note that this handles histograms too * now doing this one subplot at a time */ function crossTraceCalc(gd, plotinfo) { var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; var fullLayout = gd._fullLayout; var fullTraces = gd._fullData; var calcTraces = gd.calcdata; var calcTracesHorz = []; var calcTracesVert = []; for (var i = 0; i < fullTraces.length; i++) { var fullTrace = fullTraces[i]; if ( fullTrace.visible === true && Registry.traceIs(fullTrace, 'bar') && fullTrace.xaxis === xa._id && fullTrace.yaxis === ya._id ) { if (fullTrace.orientation === 'h') { calcTracesHorz.push(calcTraces[i]); } else { calcTracesVert.push(calcTraces[i]); } 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(); } } } } var opts = { xCat: xa.type === 'category' || xa.type === 'multicategory', yCat: ya.type === 'category' || ya.type === 'multicategory', mode: fullLayout.barmode, norm: fullLayout.barnorm, gap: fullLayout.bargap, groupgap: fullLayout.bargroupgap }; setGroupPositions(gd, xa, ya, calcTracesVert, opts); setGroupPositions(gd, ya, xa, calcTracesHorz, opts); } function setGroupPositions(gd, pa, sa, calcTraces, opts) { if (!calcTraces.length) return; var excluded; var included; var i, calcTrace, fullTrace; initBase(sa, calcTraces); switch (opts.mode) { case 'overlay': setGroupPositionsInOverlayMode(gd, pa, sa, calcTraces, opts); break; case 'group': // exclude from the group those traces for which the user set an offset excluded = []; included = []; for (i = 0; i < calcTraces.length; i++) { calcTrace = calcTraces[i]; fullTrace = calcTrace[0].trace; if (fullTrace.offset === undefined) included.push(calcTrace); else excluded.push(calcTrace); } if (included.length) { setGroupPositionsInGroupMode(gd, pa, sa, included, opts); } if (excluded.length) { setGroupPositionsInOverlayMode(gd, pa, sa, excluded, opts); } break; case 'stack': case 'relative': // exclude from the stack those traces for which the user set a base excluded = []; included = []; for (i = 0; i < calcTraces.length; i++) { calcTrace = calcTraces[i]; fullTrace = calcTrace[0].trace; if (fullTrace.base === undefined) included.push(calcTrace); else excluded.push(calcTrace); } // If any trace in `included` has a cornerradius, set cornerradius of all bars // in `included` to match the first trace which has a cornerradius standardizeCornerradius(included); if (included.length) { setGroupPositionsInStackOrRelativeMode(gd, pa, sa, included, opts); } if (excluded.length) { setGroupPositionsInOverlayMode(gd, pa, sa, excluded, opts); } break; } setCornerradius(calcTraces); collectExtents(calcTraces, pa); } // Set cornerradiusvalue and cornerradiusform in calcTraces[0].t function setCornerradius(calcTraces) { var i, calcTrace, fullTrace, t, cr, crValue, crForm; for (i = 0; i < calcTraces.length; i++) { calcTrace = calcTraces[i]; fullTrace = calcTrace[0].trace; t = calcTrace[0].t; if (t.cornerradiusvalue === undefined) { cr = fullTrace.marker ? fullTrace.marker.cornerradius : undefined; if (cr !== undefined) { crValue = isNumeric(cr) ? +cr : +cr.slice(0, -1); crForm = isNumeric(cr) ? 'px' : '%'; t.cornerradiusvalue = crValue; t.cornerradiusform = crForm; } } } } // Make sure all traces in a stack use the same cornerradius function standardizeCornerradius(calcTraces) { if (calcTraces.length < 2) return; var i, calcTrace, fullTrace, t; var cr, crValue, crForm; 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 any trace has cornerradius, store first cornerradius // in calcTrace[0].t so that all traces in stack use same cornerradius if (cr !== undefined) { crValue = isNumeric(cr) ? +cr : +cr.slice(0, -1); crForm = isNumeric(cr) ? 'px' : '%'; for (i = 0; i < calcTraces.length; i++) { calcTrace = calcTraces[i]; t = calcTrace[0].t; t.cornerradiusvalue = crValue; t.cornerradiusform = crForm; } } } function initBase(sa, calcTraces) { var i, j; 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 b; // not sure if it really makes sense to have dates for bar size data... // ideally if we want to make gantt charts or something we'd treat // the actual size (trace.x or y) as time delta but base as absolute // time. But included here for completeness. 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++) { b = d2c(base[j], 0, scalendar); if (isNumeric(b)) { cd[j].b = +b; cd[j].hasB = 1; } else cd[j].b = 0; } 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++) { cd[j].b = b; if (hasBase) cd[j].hasB = 1; } } } } function setGroupPositionsInOverlayMode(gd, pa, sa, calcTraces, opts) { // update position axis and set bar offsets and widths for (var i = 0; i < calcTraces.length; i++) { var calcTrace = calcTraces[i]; var sieve = new Sieve([calcTrace], { posAxis: pa, sepNegVal: false, overlapNoMerge: !opts.norm }); // set bar offsets and widths, and update position axis setOffsetAndWidth(gd, pa, sieve, opts); // set bar bases and sizes, and update size axis // // (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) { sieveBars(sieve); normalizeBars(sa, sieve, opts); } else { setBaseAndTop(sa, sieve); } } } function setGroupPositionsInGroupMode(gd, pa, sa, calcTraces, opts) { var sieve = new Sieve(calcTraces, { posAxis: pa, sepNegVal: false, overlapNoMerge: !opts.norm }); // set bar offsets and widths, and update position axis setOffsetAndWidth(gd, pa, sieve, opts); // relative-stack bars within the same trace that would otherwise // be hidden unhideBarsWithinTrace(sieve, pa); // set bar bases and sizes, and update size axis if (opts.norm) { sieveBars(sieve); normalizeBars(sa, sieve, opts); } else { setBaseAndTop(sa, sieve); } } function setGroupPositionsInStackOrRelativeMode(gd, pa, sa, calcTraces, opts) { var sieve = new Sieve(calcTraces, { posAxis: pa, sepNegVal: opts.mode === 'relative', overlapNoMerge: !(opts.norm || opts.mode === 'stack' || opts.mode === 'relative') }); // set bar offsets and widths, and update position axis setOffsetAndWidth(gd, pa, sieve, opts); // set bar bases and sizes, and update size axis stackBars(sa, sieve, opts); // flag the outmost bar (for text display purposes) 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++) { 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; } } } // 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); } /** * Mode group: Traces should be offsetted to other traces at the same position if they have a * different offsetgroup or if no offsetgroups are specified. * If there are no other traces at the same position, the trace will not be offsetted and it * can occupy the whole width. * If two traces share an offsetgroup, they should overlap. * Mode overlay/stack/relative: Traces should be offseted to other traces at the same position if * they have a different offsetgroup. * If two traces share an offsetgroup or if no offsetgroups are specified, they should instead * overlap/stack. * Angular axes (for barpolar type) don't support group offsets. */ function setOffsetAndWidth(gd, pa, sieve, opts) { var fullLayout = gd._fullLayout; var positions = sieve.positions; var distinctPositions = sieve.distinctPositions; var minDiff = sieve.minDiff; var calcTraces = sieve.traces; var nTraces = calcTraces.length; // if there aren't any overlapping positions, // let them have full width even if mode is group var overlap = positions.length !== distinctPositions.length; var barGroupWidth = minDiff * (1 - opts.gap); var barWidthPlusGap; var barWidth; var offsetFromCenter; var alignmentGroups; if (pa._id === 'angularaxis') { barWidthPlusGap = barGroupWidth; barWidth = barWidthPlusGap * (1 - (opts.groupgap || 0)); offsetFromCenter = -barWidth / 2; } 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++) { var calcTrace = calcTraces[i]; var trace = calcTrace[0].trace; if (pa._id !== 'angularaxis') { var alignmentGroupOpts = alignmentGroups[trace.alignmentgroup] || {}; var nOffsetGroups = Object.keys(alignmentGroupOpts.offsetGroups || {}).length; if (nOffsetGroups) { barWidthPlusGap = barGroupWidth / nOffsetGroups; } else { barWidthPlusGap = overlap ? barGroupWidth / nTraces : barGroupWidth; } barWidth = barWidthPlusGap * (1 - (opts.groupgap || 0)); if (nOffsetGroups) { offsetFromCenter = ((2 * trace._offsetIndex + 1 - nOffsetGroups) * barWidthPlusGap - barWidth) / 2; } else { offsetFromCenter = overlap ? ((2 * i + 1 - nTraces) * barWidthPlusGap - barWidth) / 2 : -barWidth / 2; } } var t = calcTrace[0].t; t.barwidth = barWidth; t.offsetindex = trace._offsetIndex || 0; t.poffset = offsetFromCenter; t.bargroupwidth = barGroupWidth; t.bardelta = minDiff; } // stack bars that only differ by rounding sieve.binWidth = calcTraces[0][0].t.barwidth / 100; // if defined, apply trace width applyAttributes(sieve); // store the bar center in each calcdata item setBarCenterAndWidth(pa, sieve); // update position axes if (pa._id === 'angularaxis') { updatePositionAxis(pa, sieve); } else { updatePositionAxis(pa, sieve, overlap); } } function applyAttributes(sieve) { var calcTraces = sieve.traces; var i, j; for (i = 0; i < calcTraces.length; i++) { var calcTrace = calcTraces[i]; var calcTrace0 = calcTrace[0]; var fullTrace = calcTrace0.trace; var t = calcTrace0.t; var offset = fullTrace._offset || fullTrace.offset; var initialPoffset = t.poffset; var newPoffset; 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])) { 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++) { newPoffset.push(initialPoffset); } t.poffset = newPoffset; } else if (offset !== undefined) { t.poffset = offset; } var width = fullTrace._width || fullTrace.width; var initialBarwidth = t.barwidth; 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; } // 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++) { newBarwidth.push(initialBarwidth); } t.barwidth = newBarwidth; // if user didn't set offset, // then correct t.poffset to ensure bars remain centered if (offset === undefined) { newPoffset = []; for (j = 0; j < calcTrace.length; j++) { newPoffset.push(initialPoffset + (initialBarwidth - newBarwidth[j]) / 2); } t.poffset = newPoffset; } } 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) { t.poffset = initialPoffset + (initialBarwidth - width) / 2; } } } } function setBarCenterAndWidth(pa, sieve) { var calcTraces = sieve.traces; var pLetter = getAxisLetter(pa); for (var i = 0; i < calcTraces.length; i++) { var calcTrace = calcTraces[i]; var t = calcTrace[0].t; var poffset = t.poffset; var poffsetIsArray = isArrayOrTypedArray(poffset); var barwidth = t.barwidth; var barwidthIsArray = isArrayOrTypedArray(barwidth); 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); if (calcBar.p === undefined) { calcBar.p = calcBar[pLetter]; calcBar['orig_' + pLetter] = calcBar[pLetter]; } var delta = (poffsetIsArray ? poffset[j] : poffset) + width / 2; calcBar[pLetter] = calcBar.p + delta; } } } function updatePositionAxis(pa, sieve, allowMinDtick) { var calcTraces = sieve.traces; var minDiff = sieve.minDiff; var vpad = minDiff / 2; Axes.minDtick(pa, sieve.minDiff, sieve.distinctPositions[0], allowMinDtick); 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++) { bar = calcTrace[j]; l = bar.p - vpad; r = bar.p + vpad; pts.push(l, r); } 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++) { bar = calcTrace[j]; var calcBarOffset = poffsetIsArray ? poffset[j] : poffset; var calcBarWidth = barwidthIsArray ? barwidth[j] : barwidth; l = bar.p + calcBarOffset; r = l + calcBarWidth; pts.push(l, r); } } fullTrace._extremes[pa._id] = Axes.findExtremes(pa, pts, { padded: false }); } } // store these bar bases and tops in calcdata // and make sure the size axis includes zero, // along with the bases and tops of each bar. function setBaseAndTop(sa, sieve) { var calcTraces = sieve.traces; var sLetter = getAxisLetter(sa); for (var i = 0; i < calcTraces.length; i++) { var calcTrace = calcTraces[i]; var fullTrace = calcTrace[0].trace; var isScatter = fullTrace.type === 'scatter'; var isVertical = fullTrace.orientation === 'v'; var pts = []; var tozero = false; 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; bar[sLetter] = top; pts.push(top); if (bar.hasB) pts.push(base); if (!bar.hasB || !bar.b) { tozero = true; } } const { ppadminus, ppadplus } = estimateAxisPaddingForText(fullTrace, calcTrace); fullTrace._extremes[sa._id] = Axes.findExtremes(sa, pts, { tozero, padded: true, ppadplus, ppadminus }); } } function stackBars(sa, sieve, opts) { var sLetter = getAxisLetter(sa); var calcTraces = sieve.traces; var calcTrace; var fullTrace; var isFunnel; var i, j; var bar; var offsetIndex; for (i = 0; i < calcTraces.length; i++) { calcTrace = calcTraces[i]; fullTrace = calcTrace[0].trace; if (fullTrace.type === 'funnel') { offsetIndex = calcTrace[0].t.offsetindex; for (j = 0; j < calcTrace.length; j++) { bar = calcTrace[j]; if (bar.s !== BADNUM) { // create base of funnels sieve.put(bar.p, offsetIndex, -0.5 * bar.s); } } } } for (i = 0; i < calcTraces.length; i++) { calcTrace = calcTraces[i]; fullTrace = calcTrace[0].trace; isFunnel = fullTrace.type === 'funnel'; offsetIndex = fullTrace.type === 'barpolar' ? 0 : calcTrace[0].t.offsetindex; var pts = []; for (j = 0; j < calcTrace.length; j++) { bar = calcTrace[j]; if (bar.s !== BADNUM) { // stack current bar and get previous sum var value; if (isFunnel) { value = bar.s; } else { value = bar.s + bar.b; } var base = sieve.put(bar.p, offsetIndex, value); var top = base + value; // store the bar base and top in each calcdata item bar.b = base; bar[sLetter] = top; if (!opts.norm) { pts.push(top); if (bar.hasB) { pts.push(base); } } } } // if barnorm is set, let normalizeBars update the axis range if (!opts.norm) { const { ppadminus, ppadplus } = 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, ppadplus, ppadminus }); } } } function sieveBars(sieve) { var calcTraces = sieve.traces; 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++) { var bar = calcTrace[j]; if (bar.s !== BADNUM) { sieve.put(bar.p, offsetIndex, bar.b + bar.s); } } } } function unhideBarsWithinTrace(sieve, pa) { var calcTraces = sieve.traces; 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) { var inTraceSieve = new Sieve([calcTrace], { posAxis: pa, sepNegVal: true, overlapNoMerge: true }); for (var j = 0; j < calcTrace.length; j++) { var bar = calcTrace[j]; 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; } } } } } // Note: // // normalizeBars requires that either sieveBars or stackBars has been // previously invoked. function normalizeBars(sa, sieve, opts) { var calcTraces = sieve.traces; var sLetter = getAxisLetter(sa); var sTop = opts.norm === 'fraction' ? 1 : 100; var sTiny = sTop / 1e9; // in case of rounding error in sum var sMin = sa.l2c(sa.c2l(0)); var sMax = opts.mode === 'stack' ? sTop : sMin; function needsPadding(v) { return isNumeric(sa.c2l(v)) && (v < sMin - sTiny || v > sMax + sTiny || !isNumeric(sMin)); } for (var i = 0; i < calcTraces.length; i++) { var calcTrace = calcTraces[i]; var offsetIndex = calcTrace[0].t.offsetindex; var fullTrace = calcTrace[0].trace; var pts = []; var tozero = false; var padded = false; for (var j = 0; j < calcTrace.length; j++) { var bar = calcTrace[j]; if (bar.s !== BADNUM) { var scale = Math.abs(sTop / sieve.get(bar.p, offsetIndex, bar.s)); bar.b *= scale; bar.s *= scale; var base = bar.b; var top = base + bar.s; bar[sLetter] = top; pts.push(top); padded = padded || needsPadding(top); if (bar.hasB) { pts.push(base); padded = padded || needsPadding(base); } if (!bar.hasB || !bar.b) { tozero = true; } } } const { ppadminus, ppadplus } = estimateAxisPaddingForText(fullTrace, calcTrace); fullTrace._extremes[sa._id] = Axes.findExtremes(sa, pts, { tozero, padded, ppadplus, ppadminus }); } } /* * Returns a very lightweight estimate of extra padding (in pixels) * needed to accommodate outside text labels on bars. Only adds padding * for 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 <br> elements function countLines(text) { if (!text || typeof text !== 'string') return 0; return (text.match(BR_TAG_ALL) || []).length + 1; } var nLines; if (trace.texttemplate) { nLines = countLines(trace.texttemplate); } else { nLines = isArrayOrTypedArray(trace.text) ? Math.max(...trace.text.map((t) => countLines(t))) : countLines(trace.text); } const padAmount = trace.outsidetextfont.size * LINE_SPACING * nLines + TEXTPAD; return { // ppadplus corresponds to the negative-direction bars and // ppadminus corresponds to the positive-direction bars (for some reason) 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++) { var calcTrace = calcTraces[i]; for (var j = 0; j < calcTrace.length; j++) { var bar = calcTrace[j]; var pos = bar[pLetter]; bar._sMin = sMinByPos[pos]; bar._sMax = sMaxByPos[pos]; } } } // find the full position span of bars at each position // for use by hover, to ensure labels move in if bars are // narrower than the space they're in. // run once per trace group (subplot & direction) and // the same mapping is attached to all calcdata traces function collectExtents(calcTraces, pa) { var pLetter = getAxisLetter(pa); var extents = {}; var i, j, cd; var pMin = Infinity; var pMax = -Infinity; for (i = 0; i < calcTraces.length; i++) { cd = calcTraces[i]; for (j = 0; j < cd.length; j++) { var p = cd[j].p; if (isNumeric(p)) { pMin = Math.min(pMin, p); pMax = Math.max(pMax, p); } } } // this is just for positioning of hover labels, and nobody will care if // 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) { 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 // down to lower bars in the case of stacked bars var sMinByPos = {}; var sMaxByPos = {}; // Check whether any trace has rounded corners 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++) { 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++) { var di = cd[j]; var p0 = di[pLetter] - di.w / 2; if (isNumeric(p0)) { var p1 = di[pLetter] + di.w / 2; var pVal = round(di.p); if (extents[pVal]) { extents[pVal] = [Math.min(p0, extents[pVal][0]), Math.max(p1, extents[pVal][1])]; } else { extents[pVal] = [p0, p1]; } } di.p0 = di.p + (poffsetIsArray ? poffset[j] : poffset); di.p1 = di.p0 + di.w; di.s0 = di.b; di.s1 = di.s0 + di.s; 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; } } } if (anyTraceHasCornerradius) { setHelperValuesForRoundedCorners(calcTraces, sMinByPos, sMaxByPos, pa); } } function getAxisLetter(ax) { return ax._id.charAt(0); } module.exports = { crossTraceCalc: crossTraceCalc, setGroupPositions: setGroupPositions };