'use strict'; var isNumeric = require('fast-isnumeric'); var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray; var BADNUM = require('../../constants/numerical').BADNUM; var Registry = require('../../registry'); var Axes = require('../../plots/cartesian/axes'); var getAxisGroup = require('../../plots/cartesian/constraints').getAxisGroup; var Sieve = require('./sieve.js'); /* * 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(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(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(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(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(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 setOffsetAndWidthInGroupMode(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(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]; 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, 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); } function setOffsetAndWidth(pa, sieve, opts) { var minDiff = sieve.minDiff; var calcTraces = sieve.traces; // set bar offsets and widths var barGroupWidth = minDiff * (1 - opts.gap); var barWidthPlusGap = barGroupWidth; var barWidth = barWidthPlusGap * (1 - (opts.groupgap || 0)); // computer bar group center and bar offset var offsetFromCenter = -barWidth / 2; for(var i = 0; i < calcTraces.length; i++) { var calcTrace = calcTraces[i]; var t = calcTrace[0].t; // store bar width and offset for this trace t.barwidth = barWidth; 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 offset and width applyAttributes(sieve); // store the bar center in each calcdata item setBarCenterAndWidth(pa, sieve); // update position axes updatePositionAxis(pa, sieve); } function setOffsetAndWidthInGroupMode(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 groupId = getAxisGroup(fullLayout, pa._id) + calcTraces[0][0].trace.orientation; var alignmentGroups = fullLayout._alignmentOpts[groupId] || {}; for(var i = 0; i < nTraces; i++) { var calcTrace = calcTraces[i]; var trace = calcTrace[0].trace; var alignmentGroupOpts = alignmentGroups[trace.alignmentgroup] || {}; var nOffsetGroups = Object.keys(alignmentGroupOpts.offsetGroups || {}).length; var barWidthPlusGap; if(nOffsetGroups) { barWidthPlusGap = barGroupWidth / nOffsetGroups; } else { barWidthPlusGap = overlap ? barGroupWidth / nTraces : barGroupWidth; } var barWidth = barWidthPlusGap * (1 - (opts.groupgap || 0)); var offsetFromCenter; 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.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 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; } } fullTrace._extremes[sa._id] = Axes.findExtremes(sa, pts, { tozero: tozero, padded: true }); } } function stackBars(sa, sieve, opts) { var sLetter = getAxisLetter(sa); var calcTraces = sieve.traces; var calcTrace; var fullTrace; var isFunnel; var i, j; var bar; for(i = 0; i < calcTraces.length; i++) { calcTrace = calcTraces[i]; fullTrace = calcTrace[0].trace; if(fullTrace.type === 'funnel') { for(j = 0; j < calcTrace.length; j++) { bar = calcTrace[j]; if(bar.s !== BADNUM) { // create base of funnels sieve.put(bar.p, -0.5 * bar.s); } } } } for(i = 0; i < calcTraces.length; i++) { calcTrace = calcTraces[i]; fullTrace = calcTrace[0].trace; isFunnel = (fullTrace.type === 'funnel'); 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, 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) { 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 }); } } } function sieveBars(sieve) { var calcTraces = sieve.traces; for(var i = 0; i < calcTraces.length; i++) { var calcTrace = calcTraces[i]; for(var j = 0; j < calcTrace.length; j++) { var bar = calcTrace[j]; if(bar.s !== BADNUM) { sieve.put(bar.p, 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; 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, 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 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, 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; } } } fullTrace._extremes[sa._id] = Axes.findExtremes(sa, pts, { tozero: tozero, padded: padded }); } } // 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 };