diff --git a/src/lib/coerce.js b/src/lib/coerce.js index 989daa6a331..1b9a60095db 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -228,7 +228,7 @@ exports.valObjects = { any: { description: 'Any type.', requiredOpts: [], - otherOpts: ['dflt', 'values'], + otherOpts: ['dflt', 'values', 'arrayOk'], coerceFunction: function(v, propOut, dflt) { if(v === undefined) propOut.set(dflt); else propOut.set(v); diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index af891ba692d..2a6c0926033 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1267,6 +1267,7 @@ function _restyle(gd, aobj, _traces) { 'x', 'y', 'z', 'a', 'b', 'c', 'open', 'high', 'low', 'close', + 'base', 'width', 'offset', 'xtype', 'x0', 'dx', 'ytype', 'y0', 'dy', 'xaxis', 'yaxis', 'line.width', 'connectgaps', 'transpose', 'zsmooth', diff --git a/src/traces/bar/attributes.js b/src/traces/bar/attributes.js index 25b7568bd13..c5659f56611 100644 --- a/src/traces/bar/attributes.js +++ b/src/traces/bar/attributes.js @@ -48,6 +48,44 @@ module.exports = { ].join(' ') }, + base: { + valType: 'any', + dflt: null, + arrayOk: true, + role: 'info', + description: [ + 'Sets where the bar base is drawn (in position axis units).', + 'In *stack* or *relative* barmode,', + 'traces that set *base* will be excluded', + 'and drawn in *overlay* mode instead.' + ].join(' ') + }, + + offset: { + valType: 'number', + dflt: null, + arrayOk: true, + role: 'info', + description: [ + 'Shifts the position where the bar is drawn', + '(in position axis units).', + 'In *group* barmode,', + 'traces that set *offset* will be excluded', + 'and drawn in *overlay* mode instead.' + ].join(' ') + }, + + width: { + valType: 'number', + dflt: null, + min: 0, + arrayOk: true, + role: 'info', + description: [ + 'Sets the bar width (in position axis units).' + ].join(' ') + }, + marker: marker, r: scatterAttrs.r, diff --git a/src/traces/bar/calc.js b/src/traces/bar/calc.js index dc8ee1e77e4..22c2a1a9871 100644 --- a/src/traces/bar/calc.js +++ b/src/traces/bar/calc.js @@ -25,13 +25,15 @@ module.exports = function calc(gd, trace) { var xa = Axes.getFromId(gd, trace.xaxis || 'x'), ya = Axes.getFromId(gd, trace.yaxis || 'y'), orientation = trace.orientation || ((trace.x && !trace.y) ? 'h' : 'v'), - pos, size, i; + sa, pos, size, i; if(orientation === 'h') { + sa = xa; size = xa.makeCalcdata(trace, 'x'); pos = ya.makeCalcdata(trace, 'y'); } else { + sa = ya; size = ya.makeCalcdata(trace, 'y'); pos = xa.makeCalcdata(trace, 'x'); } @@ -40,6 +42,7 @@ module.exports = function calc(gd, trace) { var serieslen = Math.min(pos.length, size.length), cd = []; + // set position for(i = 0; i < serieslen; i++) { // add bars with non-numeric sizes to calcdata @@ -47,7 +50,35 @@ module.exports = function calc(gd, trace) { // plotted in the correct order if(isNumeric(pos[i])) { - cd.push({p: pos[i], s: size[i], b: 0}); + cd.push({p: pos[i]}); + } + } + + // set base + var base = trace.base, + b; + + if(Array.isArray(base)) { + for(i = 0; i < Math.min(base.length, cd.length); i++) { + b = sa.d2c(base[i]); + cd[i].b = (isNumeric(b)) ? b : 0; + } + for(; i < cd.length; i++) { + cd[i].b = 0; + } + } + else { + b = sa.d2c(base); + b = (isNumeric(b)) ? b : 0; + for(i = 0; i < cd.length; i++) { + cd[i].b = b; + } + } + + // set size + for(i = 0; i < cd.length; i++) { + if(isNumeric(size[i])) { + cd[i].s = size[i]; } } diff --git a/src/traces/bar/defaults.js b/src/traces/bar/defaults.js index 89290bf8cee..6e094021140 100644 --- a/src/traces/bar/defaults.js +++ b/src/traces/bar/defaults.js @@ -30,6 +30,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } coerce('orientation', (traceOut.x && !traceOut.y) ? 'h' : 'v'); + coerce('base'); + coerce('offset'); + coerce('width'); coerce('text'); handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout); diff --git a/src/traces/bar/hover.js b/src/traces/bar/hover.js index f93aedff94a..2573452209f 100644 --- a/src/traces/bar/hover.js +++ b/src/traces/bar/hover.js @@ -21,7 +21,8 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { xa = pointData.xa, ya = pointData.ya, barDelta = (hovermode === 'closest') ? - t.barwidth / 2 : t.dbar * (1 - xa._gd._fullLayout.bargap) / 2, + t.barwidth / 2 : + t.bargroupwidth, barPos; if(hovermode !== 'closest') barPos = function(di) { return di.p; }; diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index 0ea14bf3ee8..4e0cb53ac29 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -34,30 +34,39 @@ module.exports = function plot(gd, plotinfo, cdbar) { .attr('class', 'points') .each(function(d) { var t = d[0].t, - trace = d[0].trace; + trace = d[0].trace, + poffset = t.poffset, + poffsetIsArray = Array.isArray(poffset), + barwidth = t.barwidth, + barwidthIsArray = Array.isArray(barwidth); arraysToCalcdata(d); d3.select(this).selectAll('path') .data(Lib.identity) .enter().append('path') - .each(function(di) { + .each(function(di, i) { // 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 p0 = di.p + ((poffsetIsArray) ? poffset[i] : poffset), + p1 = p0 + ((barwidthIsArray) ? barwidth[i] : barwidth), + s0 = di.b, + s1 = s0 + di.s; + var x0, x1, y0, y1; if(trace.orientation === 'h') { - y0 = ya.c2p(t.poffset + di.p, true); - y1 = ya.c2p(t.poffset + di.p + t.barwidth, true); - x0 = xa.c2p(di.b, true); - x1 = xa.c2p(di.s + di.b, true); + y0 = ya.c2p(p0, true); + y1 = ya.c2p(p1, true); + x0 = xa.c2p(s0, true); + x1 = xa.c2p(s1, true); } else { - x0 = xa.c2p(t.poffset + di.p, true); - x1 = xa.c2p(t.poffset + di.p + t.barwidth, true); - y1 = ya.c2p(di.s + di.b, true); - y0 = ya.c2p(di.b, true); + x0 = xa.c2p(p0, true); + x1 = xa.c2p(p1, true); + y0 = ya.c2p(s0, true); + y1 = ya.c2p(s1, true); } if(!isNumeric(x0) || !isNumeric(x1) || diff --git a/src/traces/bar/set_positions.js b/src/traces/bar/set_positions.js index 3fea9a1d58c..fd5edd0487f 100644 --- a/src/traces/bar/set_positions.js +++ b/src/traces/bar/set_positions.js @@ -13,7 +13,7 @@ var isNumeric = require('fast-isnumeric'); var Registry = require('../../registry'); var Axes = require('../../plots/cartesian/axes'); -var Lib = require('../../lib'); +var Sieve = require('./sieve.js'); /* * Bar chart stacking/grouping positioning and autoscaling calculations @@ -23,207 +23,572 @@ var Lib = require('../../lib'); */ module.exports = function setPositions(gd, plotinfo) { - var fullLayout = gd._fullLayout, - xa = plotinfo.xaxis, - ya = plotinfo.yaxis, - i, j; - - ['v', 'h'].forEach(function(dir) { - var bl = [], - pLetter = {v: 'x', h: 'y'}[dir], - sLetter = {v: 'y', h: 'x'}[dir], - pa = plotinfo[pLetter + 'axis'], - sa = plotinfo[sLetter + 'axis']; - - gd._fullData.forEach(function(trace, i) { - if(trace.visible === true && - Registry.traceIs(trace, 'bar') && - trace.orientation === dir && - trace.xaxis === xa._id && - trace.yaxis === ya._id) { - bl.push(i); + var xa = plotinfo.xaxis, + ya = plotinfo.yaxis; + + var fullTraces = gd._fullData, + calcTraces = gd.calcdata, + calcTracesHorizontal = [], + calcTracesVertical = [], + i; + for(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') { + calcTracesHorizontal.push(calcTraces[i]); } - }); - - if(!bl.length) return; - - // bar position offset and width calculation - // bl1 is a list of traces (in calcdata) to look at together - // to find the maximum size bars that won't overlap - // for stacked or grouped bars, this is all vertical or horizontal - // bars for overlaid bars, call this individually on each trace. - function barposition(bl1) { - // find the min. difference between any points - // in any traces in bl1 - var pvals = []; - bl1.forEach(function(i) { - gd.calcdata[i].forEach(function(v) { pvals.push(v.p); }); - }); - var dv = Lib.distinctVals(pvals), - pv2 = dv.vals, - barDiff = dv.minDiff; - - // check if all the traces have only independent positions - // if so, let them have full width even if mode is group - var overlap = false, - comparelist = []; - - if(fullLayout.barmode === 'group') { - bl1.forEach(function(i) { - if(overlap) return; - gd.calcdata[i].forEach(function(v) { - if(overlap) return; - comparelist.forEach(function(cp) { - if(Math.abs(v.p - cp) < barDiff) overlap = true; - }); - }); - if(overlap) return; - gd.calcdata[i].forEach(function(v) { - comparelist.push(v.p); - }); - }); + else { + calcTracesVertical.push(calcTraces[i]); } + } + } + + setGroupPositions(gd, xa, ya, calcTracesVertical); + setGroupPositions(gd, ya, xa, calcTracesHorizontal); +}; + + +function setGroupPositions(gd, pa, sa, calcTraces) { + if(!calcTraces.length) return; + + var barmode = gd._fullLayout.barmode, + overlay = (barmode === 'overlay'), + group = (barmode === 'group'), + excluded, + included, + i, calcTrace, fullTrace; + + if(overlay) { + setGroupPositionsInOverlayMode(gd, pa, sa, calcTraces); + } + else if(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); + } + if(excluded.length) { + setGroupPositionsInOverlayMode(gd, pa, sa, excluded); + } + } + else { + // 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(included.length) { + setGroupPositionsInStackOrRelativeMode(gd, pa, sa, included); + } + if(excluded.length) { + setGroupPositionsInOverlayMode(gd, pa, sa, excluded); + } + } +} + + +function setGroupPositionsInOverlayMode(gd, pa, sa, calcTraces) { + var barnorm = gd._fullLayout.barnorm, + separateNegativeValues = false, + dontMergeOverlappingData = !barnorm; + + // 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], separateNegativeValues, dontMergeOverlappingData + ); + + // set bar offsets and widths, and update position axis + setOffsetAndWidth(gd, pa, sieve); + + // 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(barnorm) { + sieveBars(gd, sa, sieve); + normalizeBars(gd, sa, sieve); + } + else { + setBaseAndTop(gd, sa, sieve); + } + } +} + + +function setGroupPositionsInGroupMode(gd, pa, sa, calcTraces) { + var fullLayout = gd._fullLayout, + barnorm = fullLayout.barnorm, + separateNegativeValues = false, + dontMergeOverlappingData = !barnorm, + sieve = new Sieve( + calcTraces, separateNegativeValues, dontMergeOverlappingData + ); + + // set bar offsets and widths, and update position axis + setOffsetAndWidthInGroupMode(gd, pa, sieve); + + // set bar bases and sizes, and update size axis + if(barnorm) { + sieveBars(gd, sa, sieve); + normalizeBars(gd, sa, sieve); + } + else { + setBaseAndTop(gd, sa, sieve); + } +} + + +function setGroupPositionsInStackOrRelativeMode(gd, pa, sa, calcTraces) { + var fullLayout = gd._fullLayout, + barmode = fullLayout.barmode, + stack = (barmode === 'stack'), + relative = (barmode === 'relative'), + barnorm = gd._fullLayout.barnorm, + separateNegativeValues = relative, + dontMergeOverlappingData = !(barnorm || stack || relative), + sieve = new Sieve( + calcTraces, separateNegativeValues, dontMergeOverlappingData + ); + + // set bar offsets and widths, and update position axis + setOffsetAndWidth(gd, pa, sieve); + + // set bar bases and sizes, and update size axis + stackBars(gd, sa, sieve); +} + + +function setOffsetAndWidth(gd, pa, sieve) { + var fullLayout = gd._fullLayout, + bargap = fullLayout.bargap, + bargroupgap = fullLayout.bargroupgap, + minDiff = sieve.minDiff, + calcTraces = sieve.traces, + i, calcTrace, calcTrace0, + t; - // check forced minimum dtick - Axes.minDtick(pa, barDiff, pv2[0], overlap); + // set bar offsets and widths + var barGroupWidth = minDiff * (1 - bargap), + barWidthPlusGap = barGroupWidth, + barWidth = barWidthPlusGap * (1 - bargroupgap); - // position axis autorange - always tight fitting - Axes.expand(pa, pv2, {vpad: barDiff / 2}); + // computer bar group center and bar offset + var offsetFromCenter = -barWidth / 2; - // bar widths and position offsets - barDiff *= 1 - fullLayout.bargap; - if(overlap) barDiff /= bl.length; + for(i = 0; i < calcTraces.length; i++) { + calcTrace = calcTraces[i]; + calcTrace0 = calcTrace[0]; - var barCenter; - function setBarCenter(v) { v[pLetter] = v.p + barCenter; } + // store bar width and offset for this trace + t = calcTrace0.t; + t.barwidth = barWidth; + t.poffset = offsetFromCenter; + t.bargroupwidth = barGroupWidth; + } - for(var i = 0; i < bl1.length; i++) { - var t = gd.calcdata[bl1[i]][0].t; - t.barwidth = barDiff * (1 - fullLayout.bargroupgap); - t.poffset = ((overlap ? (2 * i + 1 - bl1.length) * barDiff : 0) - - t.barwidth) / 2; - t.dbar = dv.minDiff; + // stack bars that only differ by rounding + sieve.binWidth = calcTraces[0][0].t.barwidth / 100; - // store the bar center in each calcdata item - barCenter = t.poffset + t.barwidth / 2; - gd.calcdata[bl1[i]].forEach(setBarCenter); + // if defined, apply trace offset and width + applyAttributes(sieve); + + // store the bar center in each calcdata item + setBarCenter(gd, pa, sieve); + + // update position axes + updatePositionAxis(gd, pa, sieve); +} + + +function setOffsetAndWidthInGroupMode(gd, pa, sieve) { + var fullLayout = gd._fullLayout, + bargap = fullLayout.bargap, + bargroupgap = fullLayout.bargroupgap, + positions = sieve.positions, + distinctPositions = sieve.distinctPositions, + minDiff = sieve.minDiff, + calcTraces = sieve.traces, + i, calcTrace, calcTrace0, + t; + + // if there aren't any overlapping positions, + // let them have full width even if mode is group + var overlap = (positions.length !== distinctPositions.length); + + var nTraces = calcTraces.length, + barGroupWidth = minDiff * (1 - bargap), + barWidthPlusGap = (overlap) ? barGroupWidth / nTraces : barGroupWidth, + barWidth = barWidthPlusGap * (1 - bargroupgap); + + for(i = 0; i < nTraces; i++) { + calcTrace = calcTraces[i]; + calcTrace0 = calcTrace[0]; + + // computer bar group center and bar offset + var offsetFromCenter = (overlap) ? + ((2 * i + 1 - nTraces) * barWidthPlusGap - barWidth) / 2 : + -barWidth / 2; + + // store bar width and offset for this trace + t = calcTrace0.t; + t.barwidth = barWidth; + t.poffset = offsetFromCenter; + t.bargroupwidth = barGroupWidth; + } + + // 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 + setBarCenter(gd, pa, sieve); + + // update position axes + updatePositionAxis(gd, pa, sieve, overlap); +} + + +function applyAttributes(sieve) { + var calcTraces = sieve.traces, + i, calcTrace, calcTrace0, fullTrace, + j, + t; + + for(i = 0; i < calcTraces.length; i++) { + calcTrace = calcTraces[i]; + calcTrace0 = calcTrace[0]; + fullTrace = calcTrace0.trace; + t = calcTrace0.t; + + var offset = fullTrace.offset, + initialPoffset = t.poffset, + newPoffset; + + if(Array.isArray(offset)) { + // if offset is an array, then clone it into t.poffset. + newPoffset = offset.slice(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; } - if(fullLayout.barmode === 'overlay') { - bl.forEach(function(bli) { barposition([bli]); }); - } - else barposition(bl); - - var stack = (fullLayout.barmode === 'stack'), - relative = (fullLayout.barmode === 'relative'), - norm = fullLayout.barnorm; - - // bar size range and stacking calculation - if(stack || relative || norm) { - // for stacked bars, we need to evaluate every step in every - // stack, because negative bars mean the extremes could be - // anywhere - // also stores the base (b) of each bar in calcdata - // so we don't have to redo this later - var sMax = sa.l2c(sa.c2l(0)), - sMin = sMax, - sums = {}, - - // make sure if p is different only by rounding, - // we still stack - sumround = gd.calcdata[bl[0]][0].t.barwidth / 100, - sv = 0, - padded = true, - barEnd, - ti, - scale; - - for(i = 0; i < bl.length; i++) { // trace index - ti = gd.calcdata[bl[i]]; - for(j = 0; j < ti.length; j++) { - - // skip over bars with no size, - // so that we don't try to stack them - if(!isNumeric(ti[j].s)) continue; - - sv = Math.round(ti[j].p / sumround); - - // store the negative sum value for p at the same key, - // with sign flipped using string to ensure -0 !== 0. - if(relative && ti[j].s < 0) sv = '-' + sv; - - var previousSum = sums[sv] || 0; - if(stack || relative) ti[j].b = previousSum; - barEnd = ti[j].b + ti[j].s; - sums[sv] = previousSum + ti[j].s; - - // store the bar top in each calcdata item - if(stack || relative) { - ti[j][sLetter] = barEnd; - if(!norm && isNumeric(sa.c2l(barEnd))) { - sMax = Math.max(sMax, barEnd); - sMin = Math.min(sMin, barEnd); - } - } + var width = fullTrace.width, + initialBarwidth = t.barwidth; + + if(Array.isArray(width)) { + // if width is an array, then clone it into t.barwidth. + var newBarwidth = width.slice(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(norm) { - var top = norm === 'fraction' ? 1 : 100, - relAndNegative = false, - tiny = top / 1e9; // in case of rounding error in sum - - padded = false; - sMin = 0; - sMax = stack ? top : 0; - - for(i = 0; i < bl.length; i++) { // trace index - ti = gd.calcdata[bl[i]]; - - for(j = 0; j < ti.length; j++) { - relAndNegative = (relative && ti[j].s < 0); - - sv = Math.round(ti[j].p / sumround); - - // locate negative sum amount for this p val - if(relAndNegative) sv = '-' + sv; - - scale = top / sums[sv]; - - // preserve sign if negative - if(relAndNegative) scale *= -1; - ti[j].b *= scale; - ti[j].s *= scale; - barEnd = ti[j].b + ti[j].s; - ti[j][sLetter] = barEnd; - - if(isNumeric(sa.c2l(barEnd))) { - if(barEnd < sMin - tiny) { - padded = true; - sMin = barEnd; - } - if(barEnd > sMax + tiny) { - padded = true; - sMax = barEnd; - } - } - } + // 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 setBarCenter(gd, pa, sieve) { + var calcTraces = sieve.traces, + pLetter = getAxisLetter(pa); + + for(var i = 0; i < calcTraces.length; i++) { + var calcTrace = calcTraces[i], + t = calcTrace[0].t, + poffset = t.poffset, + poffsetIsArray = Array.isArray(poffset), + barwidth = t.barwidth, + barwidthIsArray = Array.isArray(barwidth); + + for(var j = 0; j < calcTrace.length; j++) { + var calcBar = calcTrace[j]; + + calcBar[pLetter] = calcBar.p + + ((poffsetIsArray) ? poffset[j] : poffset) + + ((barwidthIsArray) ? barwidth[j] : barwidth) / 2; + } + } +} + + +function updatePositionAxis(gd, pa, sieve, allowMinDtick) { + var calcTraces = sieve.traces, + distinctPositions = sieve.distinctPositions, + distinctPositions0 = distinctPositions[0], + minDiff = sieve.minDiff, + vpad = minDiff / 2; + + Axes.minDtick(pa, minDiff, distinctPositions0, allowMinDtick); + + // If the user set the bar width or the offset, + // then bars can be shifted away from their positions + // and widths can be larger than minDiff. + // + // Here, we compute pMin and pMax to expand the position axis, + // so that all bars are fully within the axis range. + var pMin = Math.min.apply(Math, distinctPositions) - vpad, + pMax = Math.max.apply(Math, distinctPositions) + vpad; + + for(var i = 0; i < calcTraces.length; i++) { + var calcTrace = calcTraces[i], + calcTrace0 = calcTrace[0], + fullTrace = calcTrace0.trace; + + if(fullTrace.width === undefined && fullTrace.offset === undefined) { + continue; + } + + var t = calcTrace0.t, + poffset = t.poffset, + barwidth = t.barwidth, + poffsetIsArray = Array.isArray(poffset), + barwidthIsArray = Array.isArray(barwidth); + + for(var j = 0; j < calcTrace.length; j++) { + var calcBar = calcTrace[j], + calcBarOffset = (poffsetIsArray) ? poffset[j] : poffset, + calcBarWidth = (barwidthIsArray) ? barwidth[j] : barwidth, + p = calcBar.p, + l = p + calcBarOffset, + r = l + calcBarWidth; + + pMin = Math.min(pMin, l); + pMax = Math.max(pMax, r); + } + } + + Axes.expand(pa, [pMin, pMax], {padded: false}); +} + + +function setBaseAndTop(gd, sa, sieve) { + // 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. + var traces = sieve.traces, + sLetter = getAxisLetter(sa), + sMax = sa.l2c(sa.c2l(0)), + sMin = sMax; + + for(var i = 0; i < traces.length; i++) { + var trace = traces[i]; + + for(var j = 0; j < trace.length; j++) { + var bar = trace[j], + barBase = bar.b, + barTop = barBase + bar.s; + + bar[sLetter] = barTop; + + if(isNumeric(sa.c2l(barTop))) { + sMax = Math.max(sMax, barTop); + sMin = Math.min(sMin, barTop); + } + if(isNumeric(sa.c2l(barBase))) { + sMax = Math.max(sMax, barBase); + sMin = Math.min(sMin, barBase); + } + } + } + + Axes.expand(sa, [sMin, sMax], {tozero: true, padded: true}); +} + + +function stackBars(gd, sa, sieve) { + var fullLayout = gd._fullLayout, + barnorm = fullLayout.barnorm, + sLetter = getAxisLetter(sa), + traces = sieve.traces, + i, trace, + j, bar; + + var sMax = sa.l2c(sa.c2l(0)), + sMin = sMax; + + for(i = 0; i < traces.length; i++) { + trace = traces[i]; + + for(j = 0; j < trace.length; j++) { + bar = trace[j]; + + if(!isNumeric(bar.s)) continue; + + // stack current bar and get previous sum + var barBase = sieve.put(bar.p, bar.s), + barTop = barBase + bar.s; + + // store the bar base and top in each calcdata item + bar.b = barBase; + bar[sLetter] = barTop; + + if(!barnorm) { + if(isNumeric(sa.c2l(barTop))) { + sMax = Math.max(sMax, barTop); + sMin = Math.min(sMin, barTop); + } + if(isNumeric(sa.c2l(barBase))) { + sMax = Math.max(sMax, barBase); + sMin = Math.min(sMin, barBase); } } + } + } - Axes.expand(sa, [sMin, sMax], {tozero: true, padded: padded}); + // if barnorm is set, let normalizeBars update the axis range + if(barnorm) { + normalizeBars(gd, sa, sieve); + } + else { + Axes.expand(sa, [sMin, sMax], {tozero: true, padded: true}); + } +} + + +function sieveBars(gd, sa, sieve) { + var traces = sieve.traces; + + for(var i = 0; i < traces.length; i++) { + var trace = traces[i]; + + for(var j = 0; j < trace.length; j++) { + var bar = trace[j]; + + if(isNumeric(bar.s)) sieve.put(bar.p, bar.s); } - else { - // for grouped or overlaid bars, just make sure zero is - // included, along with the tops of each bar, and store - // these bar tops in calcdata - var fs = function(v) { v[sLetter] = v.s; return v.s; }; - - for(i = 0; i < bl.length; i++) { - Axes.expand(sa, gd.calcdata[bl[i]].map(fs), - {tozero: true, padded: true}); + } +} + + +function normalizeBars(gd, sa, sieve) { + // Note: + // + // normalizeBars requires that either sieveBars or stackBars has been + // previously invoked. + + var traces = sieve.traces, + sLetter = getAxisLetter(sa), + sTop = (gd._fullLayout.barnorm === 'fraction') ? 1 : 100, + sTiny = sTop / 1e9, // in case of rounding error in sum + sMin = 0, + sMax = (gd._fullLayout.barmode === 'stack') ? sTop : 0, + padded = false; + + for(var i = 0; i < traces.length; i++) { + var trace = traces[i]; + + for(var j = 0; j < trace.length; j++) { + var bar = trace[j]; + + if(!isNumeric(bar.s)) continue; + + var scale = Math.abs(sTop / sieve.get(bar.p, bar.s)); + bar.b *= scale; + bar.s *= scale; + + var barBase = bar.b, + barTop = barBase + bar.s; + bar[sLetter] = barTop; + + if(isNumeric(sa.c2l(barTop))) { + if(barTop < sMin - sTiny) { + padded = true; + sMin = barTop; + } + if(barTop > sMax + sTiny) { + padded = true; + sMax = barTop; + } + } + + if(isNumeric(sa.c2l(barBase))) { + if(barBase < sMin - sTiny) { + padded = true; + sMin = barBase; + } + if(barBase > sMax + sTiny) { + padded = true; + sMax = barBase; + } } } - }); -}; + } + + // update range of size axis + Axes.expand(sa, [sMin, sMax], {tozero: true, padded: padded}); +} + + +function getAxisLetter(ax) { + return ax._id.charAt(0); +} diff --git a/src/traces/bar/sieve.js b/src/traces/bar/sieve.js new file mode 100644 index 00000000000..481b619b521 --- /dev/null +++ b/src/traces/bar/sieve.js @@ -0,0 +1,99 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +module.exports = Sieve; + +var Lib = require('../../lib'); + +/** + * Helper class to sieve data from traces into bins + * + * @class + * @param {Array} traces + * Array of calculated traces + * @param {boolean} [separateNegativeValues] + * If true, then split data at the same position into a bar + * for positive values and another for negative values + * @param {boolean} [dontMergeOverlappingData] + * If true, then don't merge overlapping bars into a single bar + */ +function Sieve(traces, separateNegativeValues, dontMergeOverlappingData) { + this.traces = traces; + this.separateNegativeValues = separateNegativeValues; + this.dontMergeOverlappingData = dontMergeOverlappingData; + + var positions = []; + for(var i = 0; i < traces.length; i++) { + var trace = traces[i]; + for(var j = 0; j < trace.length; j++) { + var bar = trace[j]; + positions.push(bar.p); + } + } + this.positions = positions; + + var dv = Lib.distinctVals(this.positions); + this.distinctPositions = dv.vals; + this.minDiff = dv.minDiff; + + this.binWidth = this.minDiff; + + this.bins = {}; +} + +/** + * Sieve datum + * + * @method + * @param {number} position + * @param {number} value + * @returns {number} Previous bin value + */ +Sieve.prototype.put = function put(position, value) { + var label = this.getLabel(position, value), + oldValue = this.bins[label] || 0; + + this.bins[label] = oldValue + value; + + return oldValue; +}; + +/** + * Get current bin value for a given datum + * + * @method + * @param {number} position Position of datum + * @param {number} [value] Value of datum + * (required if this.separateNegativeValues is true) + * @returns {number} Current bin value + */ +Sieve.prototype.get = function put(position, value) { + var label = this.getLabel(position, value); + return this.bins[label] || 0; +}; + +/** + * Get bin label for a given datum + * + * @method + * @param {number} position Position of datum + * @param {number} [value] Value of datum + * (required if this.separateNegativeValues is true) + * @returns {string} Bin label + * (prefixed with a 'v' if value is negative and this.separateNegativeValues is + * true; otherwise prefixed with '^') + */ +Sieve.prototype.getLabel = function getLabel(position, value) { + var prefix = (value < 0 && this.separateNegativeValues) ? 'v' : '^', + label = (this.dontMergeOverlappingData) ? + position : + Math.round(position / this.binWidth); + return prefix + label; +}; diff --git a/test/image/baselines/bar_attrs_group.png b/test/image/baselines/bar_attrs_group.png new file mode 100644 index 00000000000..1b965bb7893 Binary files /dev/null and b/test/image/baselines/bar_attrs_group.png differ diff --git a/test/image/baselines/bar_attrs_group_norm.png b/test/image/baselines/bar_attrs_group_norm.png new file mode 100644 index 00000000000..f209c383e66 Binary files /dev/null and b/test/image/baselines/bar_attrs_group_norm.png differ diff --git a/test/image/baselines/bar_attrs_overlay.png b/test/image/baselines/bar_attrs_overlay.png new file mode 100644 index 00000000000..2c0894ab69f Binary files /dev/null and b/test/image/baselines/bar_attrs_overlay.png differ diff --git a/test/image/baselines/bar_attrs_relative.png b/test/image/baselines/bar_attrs_relative.png new file mode 100644 index 00000000000..fe44f4c1eb7 Binary files /dev/null and b/test/image/baselines/bar_attrs_relative.png differ diff --git a/test/image/mocks/bar_attrs_group.json b/test/image/mocks/bar_attrs_group.json new file mode 100644 index 00000000000..d8f26b1d53a --- /dev/null +++ b/test/image/mocks/bar_attrs_group.json @@ -0,0 +1,33 @@ +{ + "data":[ + { + "base":[0,1,2,3], + "width":0.2, + "y":[1,1,1,1], + "x":[1,2,3,4], + "type":"bar" + }, { + "base":[4,3,2,1], + "y":[-1,-1,-1,-1], + "x":[1,2,3,4], + "type":"bar" + }, { + "base":[0,1,3,2], + "width":0.125, + "y":[1,2,-1,2], + "x":[1,2,3,4], + "type":"bar" + }, { + "base":[1,3,2,4], + "y":[-1,-2,1,-2], + "x":[1,2,3,4], + "type":"bar" + } + ], + "layout":{ + "height":400, + "width":400, + "barmode":"group", + "barnorm":false + } +} diff --git a/test/image/mocks/bar_attrs_group_norm.json b/test/image/mocks/bar_attrs_group_norm.json new file mode 100644 index 00000000000..f08d1d8e8b5 --- /dev/null +++ b/test/image/mocks/bar_attrs_group_norm.json @@ -0,0 +1,19 @@ +{ + "data":[ + { + "base":4, + "x":[3,2,1,0], + "type":"bar" + }, { + "base":[7,6,5,4], + "x":[1,2,3,4], + "type":"bar" + } + ], + "layout":{ + "height":400, + "width":400, + "barmode":"group", + "barnorm":"percent" + } +} diff --git a/test/image/mocks/bar_attrs_overlay.json b/test/image/mocks/bar_attrs_overlay.json new file mode 100644 index 00000000000..5160c620bfb --- /dev/null +++ b/test/image/mocks/bar_attrs_overlay.json @@ -0,0 +1,38 @@ +{ + "data":[ + { + "base":[0,1,2,3], + "width":[1,0.8,0.6,0.4], + "offset":[0,0,-0.2,-0.6], + "y":[1,1,1,1], + "x":[1,2,3,4], + "type":"bar" + }, { + "base":[4,3,2,1], + "width":[0.4,0.6,0.8,1], + "offset":[-0.2,-0.8,-1.2,-1.4], + "y":[-1,-1,-1,-1], + "x":[5,6,7,8], + "type":"bar" + }, { + "base":[0,1,3,2], + "width":1, + "offset":[1.5], + "y":[1,2,-1,2], + "x":[9,10,11,12], + "type":"bar" + }, { + "base":[1,3,2,4], + "y":[-1,-2,1,-2], + "x":[13,14,15,16], + "type":"bar" + } + ], + "layout":{ + "xaxis": {"showgrid":true}, + "height":400, + "width":400, + "barmode":"overlay", + "barnorm":false + } +} diff --git a/test/image/mocks/bar_attrs_relative.json b/test/image/mocks/bar_attrs_relative.json new file mode 100644 index 00000000000..a7e15e0ff96 --- /dev/null +++ b/test/image/mocks/bar_attrs_relative.json @@ -0,0 +1,31 @@ +{ + "data":[ + { + "width":[1,0.8,0.6,0.4], + "y":[1,2,3,4], + "x":[1,2,3,4], + "type":"bar" + }, { + "width":[0.4,0.6,0.8,1], + "y":[3,2,1,0], + "x":[1,2,3,4], + "type":"bar" + }, { + "width":1, + "y":[-1,-3,-2,-4], + "x":[1,2,3,4], + "type":"bar" + }, { + "y":[0,-1,-3,-2], + "x":[1,2,3,4], + "type":"bar" + } + ], + "layout":{ + "xaxis": {"showgrid":true}, + "height":400, + "width":400, + "barmode":"relative", + "barnorm":false + } +} diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index b1363b111ad..533cdddfdde 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -1,8 +1,14 @@ +var Plotly = require('@lib/index'); var Plots = require('@src/plots/plots'); var Lib = require('@src/lib'); +var PlotlyInternal = require('@src/plotly'); +var Axes = PlotlyInternal.Axes; + var Bar = require('@src/traces/bar'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); var customMatchers = require('../assets/custom_matchers'); describe('bar supplyDefaults', function() { @@ -59,6 +65,25 @@ describe('bar supplyDefaults', function() { supplyDefaults(traceIn, traceOut, defaultColor); expect(traceOut.visible).toBe(false); }); + + it('should not set base, offset or width', function() { + traceIn = { + y: [1, 2, 3] + }; + supplyDefaults(traceIn, traceOut, defaultColor); + expect(traceOut.base).toBeUndefined(); + expect(traceOut.offset).toBeUndefined(); + expect(traceOut.width).toBeUndefined(); + }); + + it('should coerce a non-negative width', function() { + traceIn = { + width: -1, + y: [1, 2, 3] + }; + supplyDefaults(traceIn, traceOut, defaultColor); + expect(traceOut.width).toBeUndefined(); + }); }); describe('heatmap calc / setPositions', function() { @@ -68,64 +93,8 @@ describe('heatmap calc / setPositions', function() { jasmine.addMatchers(customMatchers); }); - function _calc(dataOpts, layout) { - var baseData = { type: 'bar' }; - - var data = dataOpts.map(function(traceOpts) { - return Lib.extendFlat({}, baseData, traceOpts); - }); - - var gd = { - data: data, - layout: layout, - calcdata: [] - }; - - Plots.supplyDefaults(gd); - - gd._fullData.forEach(function(fullTrace) { - var cd = Bar.calc(gd, fullTrace); - - cd[0].t = {}; - cd[0].trace = fullTrace; - - gd.calcdata.push(cd); - }); - - var plotinfo = { - xaxis: gd._fullLayout.xaxis, - yaxis: gd._fullLayout.yaxis - }; - - Bar.setPositions(gd, plotinfo); - - return gd.calcdata; - } - - function assertPtField(calcData, prop, expectation) { - var values = []; - - calcData.forEach(function(calcTrace) { - var vals = calcTrace.map(function(pt) { - return Lib.nestedProperty(pt, prop).get(); - }); - - values.push(vals); - }); - - expect(values).toBeCloseTo2DArray(expectation, undefined, '- field ' + prop); - } - - function assertTraceField(calcData, prop, expectation) { - var values = calcData.map(function(calcTrace) { - return Lib.nestedProperty(calcTrace[0], prop).get(); - }); - - expect(values).toBeCloseToArray(expectation, undefined, '- field ' + prop); - } - it('should fill in calc pt fields (stack case)', function() { - var out = _calc([{ + var gd = mockBarPlot([{ y: [2, 1, 2] }, { y: [3, 1, 2] @@ -135,18 +104,19 @@ describe('heatmap calc / setPositions', function() { barmode: 'stack' }); - assertPtField(out, 'x', [[0, 1, 2], [0, 1, 2], [0, 1, 2]]); - assertPtField(out, 'y', [[2, 1, 2], [5, 2, 4], [undefined, undefined, 6]]); - assertPtField(out, 'b', [[0, 0, 0], [2, 1, 2], [0, 0, 4]]); - assertPtField(out, 's', [[2, 1, 2], [3, 1, 2], [undefined, undefined, 2]]); - assertPtField(out, 'p', [[0, 1, 2], [0, 1, 2], [0, 1, 2]]); - assertTraceField(out, 't.barwidth', [0.8, 0.8, 0.8]); - assertTraceField(out, 't.poffset', [-0.4, -0.4, -0.4]); - assertTraceField(out, 't.dbar', [1, 1, 1]); + var cd = gd.calcdata; + assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2], [0, 1, 2]]); + assertPointField(cd, 'y', [[2, 1, 2], [5, 2, 4], [undefined, undefined, 6]]); + assertPointField(cd, 'b', [[0, 0, 0], [2, 1, 2], [0, 0, 4]]); + assertPointField(cd, 's', [[2, 1, 2], [3, 1, 2], [undefined, undefined, 2]]); + assertPointField(cd, 'p', [[0, 1, 2], [0, 1, 2], [0, 1, 2]]); + assertTraceField(cd, 't.barwidth', [0.8, 0.8, 0.8]); + assertTraceField(cd, 't.poffset', [-0.4, -0.4, -0.4]); + assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8, 0.8]); }); it('should fill in calc pt fields (overlay case)', function() { - var out = _calc([{ + var gd = mockBarPlot([{ y: [2, 1, 2] }, { y: [3, 1, 2] @@ -154,37 +124,41 @@ describe('heatmap calc / setPositions', function() { barmode: 'overlay' }); - assertPtField(out, 'x', [[0, 1, 2], [0, 1, 2]]); - assertPtField(out, 'y', [[2, 1, 2], [3, 1, 2]]); - assertPtField(out, 'b', [[0, 0, 0], [0, 0, 0]]); - assertPtField(out, 's', [[2, 1, 2], [3, 1, 2]]); - assertPtField(out, 'p', [[0, 1, 2], [0, 1, 2]]); - assertTraceField(out, 't.barwidth', [0.8, 0.8]); - assertTraceField(out, 't.poffset', [-0.4, -0.4]); - assertTraceField(out, 't.dbar', [1, 1]); + var cd = gd.calcdata; + assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2]]); + assertPointField(cd, 'y', [[2, 1, 2], [3, 1, 2]]); + assertPointField(cd, 'b', [[0, 0, 0], [0, 0, 0]]); + assertPointField(cd, 's', [[2, 1, 2], [3, 1, 2]]); + assertPointField(cd, 'p', [[0, 1, 2], [0, 1, 2]]); + assertTraceField(cd, 't.barwidth', [0.8, 0.8]); + assertTraceField(cd, 't.poffset', [-0.4, -0.4]); + assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8]); }); it('should fill in calc pt fields (group case)', function() { - var out = _calc([{ + var gd = mockBarPlot([{ y: [2, 1, 2] }, { y: [3, 1, 2] }], { - barmode: 'group' + barmode: 'group', + // asumming default bargap is 0.2 + bargroupgap: 0.1 }); - assertPtField(out, 'x', [[-0.2, 0.8, 1.8], [0.2, 1.2, 2.2]]); - assertPtField(out, 'y', [[2, 1, 2], [3, 1, 2]]); - assertPtField(out, 'b', [[0, 0, 0], [0, 0, 0]]); - assertPtField(out, 's', [[2, 1, 2], [3, 1, 2]]); - assertPtField(out, 'p', [[0, 1, 2], [0, 1, 2]]); - assertTraceField(out, 't.barwidth', [0.4, 0.4]); - assertTraceField(out, 't.poffset', [-0.4, 0]); - assertTraceField(out, 't.dbar', [1, 1]); + var cd = gd.calcdata; + assertPointField(cd, 'x', [[-0.2, 0.8, 1.8], [0.2, 1.2, 2.2]]); + assertPointField(cd, 'y', [[2, 1, 2], [3, 1, 2]]); + assertPointField(cd, 'b', [[0, 0, 0], [0, 0, 0]]); + assertPointField(cd, 's', [[2, 1, 2], [3, 1, 2]]); + assertPointField(cd, 'p', [[0, 1, 2], [0, 1, 2]]); + assertTraceField(cd, 't.barwidth', [0.36, 0.36]); + assertTraceField(cd, 't.poffset', [-0.38, 0.02]); + assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8]); }); it('should fill in calc pt fields (relative case)', function() { - var out = _calc([{ + var gd = mockBarPlot([{ y: [20, 14, -23] }, { y: [-12, -18, -29] @@ -192,18 +166,19 @@ describe('heatmap calc / setPositions', function() { barmode: 'relative' }); - assertPtField(out, 'x', [[0, 1, 2], [0, 1, 2]]); - assertPtField(out, 'y', [[20, 14, -23], [-12, -18, -52]]); - assertPtField(out, 'b', [[0, 0, 0], [0, 0, -23]]); - assertPtField(out, 's', [[20, 14, -23], [-12, -18, -29]]); - assertPtField(out, 'p', [[0, 1, 2], [0, 1, 2]]); - assertTraceField(out, 't.barwidth', [0.8, 0.8]); - assertTraceField(out, 't.poffset', [-0.4, -0.4]); - assertTraceField(out, 't.dbar', [1, 1]); + var cd = gd.calcdata; + assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2]]); + assertPointField(cd, 'y', [[20, 14, -23], [-12, -18, -52]]); + assertPointField(cd, 'b', [[0, 0, 0], [0, 0, -23]]); + assertPointField(cd, 's', [[20, 14, -23], [-12, -18, -29]]); + assertPointField(cd, 'p', [[0, 1, 2], [0, 1, 2]]); + assertTraceField(cd, 't.barwidth', [0.8, 0.8]); + assertTraceField(cd, 't.poffset', [-0.4, -0.4]); + assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8]); }); it('should fill in calc pt fields (relative / percent case)', function() { - var out = _calc([{ + var gd = mockBarPlot([{ x: ['A', 'B', 'C', 'D'], y: [20, 14, 40, -60] }, { @@ -214,13 +189,562 @@ describe('heatmap calc / setPositions', function() { barnorm: 'percent' }); - assertPtField(out, 'x', [[0, 1, 2, 3], [0, 1, 2, 3]]); - assertPtField(out, 'y', [[100, 100, 40, -60], [-100, -100, 100, -100]]); - assertPtField(out, 'b', [[0, 0, 0, 0], [0, 0, 40, -60]]); - assertPtField(out, 's', [[100, 100, 40, -60], [-100, -100, 60, -40]]); - assertPtField(out, 'p', [[0, 1, 2, 3], [0, 1, 2, 3]]); - assertTraceField(out, 't.barwidth', [0.8, 0.8]); - assertTraceField(out, 't.poffset', [-0.4, -0.4]); - assertTraceField(out, 't.dbar', [1, 1]); + var cd = gd.calcdata; + assertPointField(cd, 'x', [[0, 1, 2, 3], [0, 1, 2, 3]]); + assertPointField(cd, 'y', [[100, 100, 40, -60], [-100, -100, 100, -100]]); + assertPointField(cd, 'b', [[0, 0, 0, 0], [0, 0, 40, -60]]); + assertPointField(cd, 's', [[100, 100, 40, -60], [-100, -100, 60, -40]]); + assertPointField(cd, 'p', [[0, 1, 2, 3], [0, 1, 2, 3]]); + assertTraceField(cd, 't.barwidth', [0.8, 0.8]); + assertTraceField(cd, 't.poffset', [-0.4, -0.4]); + assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8]); }); }); + +describe('Bar.calc', function() { + 'use strict'; + + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + it('should guard against invalid base items', function() { + var gd = mockBarPlot([{ + base: [null, 1, 2], + y: [1, 2, 3] + }, { + base: [null, 1], + y: [1, 2, 3] + }, { + base: null, + y: [1, 2] + }], { + barmode: 'overlay' + }); + + var cd = gd.calcdata; + assertPointField(cd, 'b', [[0, 1, 2], [0, 1, 0], [0, 0]]); + }); +}); + +describe('Bar.setPositions', function() { + 'use strict'; + + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + it('should guard against invalid offset items', function() { + var gd = mockBarPlot([{ + offset: [null, 0, 1], + y: [1, 2, 3] + }, { + offset: [null, 1], + y: [1, 2, 3] + }, { + offset: null, + y: [1] + }], { + bargap: 0.2, + barmode: 'overlay' + }); + + var cd = gd.calcdata; + assertArrayField(cd[0][0], 't.poffset', [-0.4, 0, 1]); + assertArrayField(cd[1][0], 't.poffset', [-0.4, 1, -0.4]); + assertArrayField(cd[2][0], 't.poffset', [-0.4]); + }); + + it('should guard against invalid width items', function() { + var gd = mockBarPlot([{ + width: [null, 1, 0.8], + y: [1, 2, 3] + }, { + width: [null, 1], + y: [1, 2, 3] + }, { + width: null, + y: [1] + }], { + bargap: 0.2, + barmode: 'overlay' + }); + + var cd = gd.calcdata; + assertArrayField(cd[0][0], 't.barwidth', [0.8, 1, 0.8]); + assertArrayField(cd[1][0], 't.barwidth', [0.8, 1, 0.8]); + assertArrayField(cd[2][0], 't.barwidth', [0.8]); + }); + + it('should guard against invalid width items (group case)', function() { + var gd = mockBarPlot([{ + width: [null, 0.1, 0.2], + y: [1, 2, 3] + }, { + width: [null, 0.1], + y: [1, 2, 3] + }, { + width: null, + y: [1] + }], { + bargap: 0, + barmode: 'group' + }); + + var cd = gd.calcdata; + assertArrayField(cd[0][0], 't.barwidth', [0.33, 0.1, 0.2]); + assertArrayField(cd[1][0], 't.barwidth', [0.33, 0.1, 0.33]); + assertArrayField(cd[2][0], 't.barwidth', [0.33]); + }); + + it('should stack vertical and horizontal traces separately', function() { + var gd = mockBarPlot([{ + y: [1, 2, 3] + }, { + y: [10, 20, 30] + }, { + x: [-1, -2, -3] + }, { + x: [-10, -20, -30] + }], { + barmode: 'stack' + }); + + var cd = gd.calcdata; + assertPointField(cd, 'b', [[0, 0, 0], [1, 2, 3], [0, 0, 0], [-1, -2, -3]]); + assertPointField(cd, 's', [[1, 2, 3], [10, 20, 30], [-1, -2, -3], [-10, -20, -30]]); + assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2], [-1, -2, -3], [-11, -22, -33]]); + assertPointField(cd, 'y', [[1, 2, 3], [11, 22, 33], [0, 1, 2], [0, 1, 2]]); + }); + + it('should not group traces that set offset', function() { + var gd = mockBarPlot([{ + y: [1, 2, 3] + }, { + y: [10, 20, 30] + }, { + offset: -1, + y: [-1, -2, -3] + }], { + bargap: 0, + barmode: 'group' + }); + + var cd = gd.calcdata; + assertPointField(cd, 'b', [[0, 0, 0], [0, 0, 0], [0, 0, 0]]); + assertPointField(cd, 's', [[1, 2, 3], [10, 20, 30], [-1, -2, -3]]); + assertPointField(cd, 'x', [[-0.25, 0.75, 1.75], [0.25, 1.25, 2.25], [-0.5, 0.5, 1.5]]); + assertPointField(cd, 'y', [[1, 2, 3], [10, 20, 30], [-1, -2, -3]]); + }); + + it('should not stack traces that set base', function() { + var gd = mockBarPlot([{ + y: [1, 2, 3] + }, { + y: [10, 20, 30] + }, { + base: -1, + y: [-1, -2, -3] + }], { + bargap: 0, + barmode: 'stack' + }); + + var cd = gd.calcdata; + assertPointField(cd, 'b', [[0, 0, 0], [1, 2, 3], [-1, -1, -1]]); + assertPointField(cd, 's', [[1, 2, 3], [10, 20, 30], [-1, -2, -3]]); + assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2], [0, 1, 2]]); + assertPointField(cd, 'y', [[1, 2, 3], [11, 22, 33], [-2, -3, -4]]); + }); + + it('should draw traces separately in overlay mode', function() { + var gd = mockBarPlot([{ + y: [1, 2, 3] + }, { + y: [10, 20, 30] + }], { + bargap: 0, + barmode: 'overlay', + barnorm: false + }); + + var cd = gd.calcdata; + assertPointField(cd, 'b', [[0, 0, 0], [0, 0, 0]]); + assertPointField(cd, 's', [[1, 2, 3], [10, 20, 30]]); + assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2]]); + assertPointField(cd, 'y', [[1, 2, 3], [10, 20, 30]]); + }); + + it('should ignore barnorm in overlay mode', function() { + var gd = mockBarPlot([{ + y: [1, 2, 3] + }, { + y: [10, 20, 30] + }], { + bargap: 0, + barmode: 'overlay', + barnorm: 'percent' + }); + + expect(gd._fullLayout.barnorm).toBeUndefined(); + + var cd = gd.calcdata; + assertPointField(cd, 'b', [[0, 0, 0], [0, 0, 0]]); + assertPointField(cd, 's', [[1, 2, 3], [10, 20, 30]]); + assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2]]); + assertPointField(cd, 'y', [[1, 2, 3], [10, 20, 30]]); + }); + + it('should honor barnorm for traces that cannot be grouped', function() { + var gd = mockBarPlot([{ + offset: 0, + y: [1, 2, 3] + }], { + bargap: 0, + barmode: 'group', + barnorm: 'percent' + }); + + expect(gd._fullLayout.barnorm).toBe('percent'); + + var cd = gd.calcdata; + assertPointField(cd, 'b', [[0, 0, 0]]); + assertPointField(cd, 's', [[100, 100, 100]]); + assertPointField(cd, 'x', [[0.5, 1.5, 2.5]]); + assertPointField(cd, 'y', [[100, 100, 100]]); + }); + + it('should honor barnorm for traces that cannot be stacked', function() { + var gd = mockBarPlot([{ + offset: 0, + y: [1, 2, 3] + }], { + bargap: 0, + barmode: 'stack', + barnorm: 'percent' + }); + + expect(gd._fullLayout.barnorm).toBe('percent'); + + var cd = gd.calcdata; + assertPointField(cd, 'b', [[0, 0, 0]]); + assertPointField(cd, 's', [[100, 100, 100]]); + assertPointField(cd, 'x', [[0.5, 1.5, 2.5]]); + assertPointField(cd, 'y', [[100, 100, 100]]); + }); + + it('should honor barnorm (group case)', function() { + var gd = mockBarPlot([{ + y: [3, 2, 1] + }, { + y: [1, 2, 3] + }], { + bargap: 0, + barmode: 'group', + barnorm: 'fraction' + }); + + expect(gd._fullLayout.barnorm).toBe('fraction'); + + var cd = gd.calcdata; + assertPointField(cd, 'b', [[0, 0, 0], [0, 0, 0]]); + assertPointField(cd, 's', [[0.75, 0.50, 0.25], [0.25, 0.50, 0.75]]); + assertPointField(cd, 'x', [[-0.25, 0.75, 1.75], [0.25, 1.25, 2.25]]); + assertPointField(cd, 'y', [[0.75, 0.50, 0.25], [0.25, 0.50, 0.75]]); + }); + + it('should honor barnorm (stack case)', function() { + var gd = mockBarPlot([{ + y: [3, 2, 1] + }, { + y: [1, 2, 3] + }], { + bargap: 0, + barmode: 'stack', + barnorm: 'fraction' + }); + + expect(gd._fullLayout.barnorm).toBe('fraction'); + + var cd = gd.calcdata; + assertPointField(cd, 'b', [[0, 0, 0], [0.75, 0.50, 0.25]]); + assertPointField(cd, 's', [[0.75, 0.50, 0.25], [0.25, 0.50, 0.75]]); + assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2]]); + assertPointField(cd, 'y', [[0.75, 0.50, 0.25], [1, 1, 1]]); + }); + + it('should honor barnorm (relative case)', function() { + var gd = mockBarPlot([{ + y: [3, 2, 1] + }, { + y: [1, 2, 3] + }, { + y: [-3, -2, -1] + }, { + y: [-1, -2, -3] + }], { + bargap: 0, + barmode: 'relative', + barnorm: 'fraction' + }); + + expect(gd._fullLayout.barnorm).toBe('fraction'); + + var cd = gd.calcdata; + assertPointField(cd, 'b', [ + [0, 0, 0], [0.75, 0.50, 0.25], + [0, 0, 0], [-0.75, -0.50, -0.25] + ]); + assertPointField(cd, 's', [ + [0.75, 0.50, 0.25], [0.25, 0.50, 0.75], + [-0.75, -0.50, -0.25], [-0.25, -0.50, -0.75], + ]); + assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2], [0, 1, 2], [0, 1, 2]]); + assertPointField(cd, 'y', [ + [0.75, 0.50, 0.25], [1, 1, 1], + [-0.75, -0.50, -0.25], [-1, -1, -1], + ]); + }); + + it('should expand position axis', function() { + var gd = mockBarPlot([{ + offset: 10, + width: 2, + y: [3, 2, 1] + }, { + offset: -5, + width: 2, + y: [-1, -2, -3] + }], { + bargap: 0, + barmode: 'overlay', + barnorm: false + }); + + expect(gd._fullLayout.barnorm).toBeUndefined(); + + var xa = gd._fullLayout.xaxis, + ya = gd._fullLayout.yaxis; + expect(Axes.getAutoRange(xa)).toBeCloseToArray([-5, 14], undefined, '(xa.range)'); + expect(Axes.getAutoRange(ya)).toBeCloseToArray([-3.33, 3.33], undefined, '(ya.range)'); + }); + + it('should expand size axis (overlay case)', function() { + var gd = mockBarPlot([{ + base: 7, + y: [3, 2, 1] + }, { + base: 2, + y: [1, 2, 3] + }, { + base: -2, + y: [-3, -2, -1] + }, { + base: -7, + y: [-1, -2, -3] + }], { + bargap: 0, + barmode: 'overlay', + barnorm: false + }); + + expect(gd._fullLayout.barnorm).toBeUndefined(); + + var xa = gd._fullLayout.xaxis, + ya = gd._fullLayout.yaxis; + expect(Axes.getAutoRange(xa)).toBeCloseToArray([-0.5, 2.5], undefined, '(xa.range)'); + expect(Axes.getAutoRange(ya)).toBeCloseToArray([-11.11, 11.11], undefined, '(ya.range)'); + }); + + it('should expand size axis (relative case)', function() { + var gd = mockBarPlot([{ + y: [3, 2, 1] + }, { + y: [1, 2, 3] + }, { + y: [-3, -2, -1] + }, { + y: [-1, -2, -3] + }], { + bargap: 0, + barmode: 'relative', + barnorm: false + }); + + expect(gd._fullLayout.barnorm).toBe(''); + + var xa = gd._fullLayout.xaxis, + ya = gd._fullLayout.yaxis; + expect(Axes.getAutoRange(xa)).toBeCloseToArray([-0.5, 2.5], undefined, '(xa.range)'); + expect(Axes.getAutoRange(ya)).toBeCloseToArray([-4.44, 4.44], undefined, '(ya.range)'); + }); + + it('should expand size axis (barnorm case)', function() { + var gd = mockBarPlot([{ + y: [3, 2, 1] + }, { + y: [1, 2, 3] + }, { + y: [-3, -2, -1] + }, { + y: [-1, -2, -3] + }], { + bargap: 0, + barmode: 'relative', + barnorm: 'fraction' + }); + + expect(gd._fullLayout.barnorm).toBe('fraction'); + + var xa = gd._fullLayout.xaxis, + ya = gd._fullLayout.yaxis; + expect(Axes.getAutoRange(xa)).toBeCloseToArray([-0.5, 2.5], undefined, '(xa.range)'); + expect(Axes.getAutoRange(ya)).toBeCloseToArray([-1.11, 1.11], undefined, '(ya.range)'); + }); +}); + +describe('A bar plot', function() { + 'use strict'; + + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + afterEach(destroyGraphDiv); + + it('should be able to restyle', function(done) { + var gd = createGraphDiv(), + mock = Lib.extendDeep({}, require('@mocks/bar_attrs_relative')); + + Plotly.plot(gd, mock.data, mock.layout).then(function() { + var cd = gd.calcdata; + assertPointField(cd, 'x', [ + [1, 2, 3, 4], [1, 2, 3, 4], + [1, 2, 3, 4], [1, 2, 3, 4]]); + assertPointField(cd, 'y', [ + [1, 2, 3, 4], [4, 4, 4, 4], + [-1, -3, -2, -4], [4, -4, -5, -6]]); + assertPointField(cd, 'b', [ + [0, 0, 0, 0], [1, 2, 3, 4], + [0, 0, 0, 0], [4, -3, -2, -4]]); + assertPointField(cd, 's', [ + [1, 2, 3, 4], [3, 2, 1, 0], + [-1, -3, -2, -4], [0, -1, -3, -2]]); + assertPointField(cd, 'p', [ + [1, 2, 3, 4], [1, 2, 3, 4], + [1, 2, 3, 4], [1, 2, 3, 4]]); + assertArrayField(cd[0][0], 't.barwidth', [1, 0.8, 0.6, 0.4]); + assertArrayField(cd[1][0], 't.barwidth', [0.4, 0.6, 0.8, 1]); + expect(cd[2][0].t.barwidth).toBe(1); + expect(cd[3][0].t.barwidth).toBe(0.8); + assertArrayField(cd[0][0], 't.poffset', [-0.5, -0.4, -0.3, -0.2]); + assertArrayField(cd[1][0], 't.poffset', [-0.2, -0.3, -0.4, -0.5]); + expect(cd[2][0].t.poffset).toBe(-0.5); + expect(cd[3][0].t.poffset).toBe(-0.4); + assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8, 0.8, 0.8]); + + return Plotly.restyle(gd, 'offset', 0); + }).then(function() { + var cd = gd.calcdata; + assertPointField(cd, 'x', [ + [1.5, 2.4, 3.3, 4.2], [1.2, 2.3, 3.4, 4.5], + [1.5, 2.5, 3.5, 4.5], [1.4, 2.4, 3.4, 4.4]]); + assertPointField(cd, 'y', [ + [1, 2, 3, 4], [4, 4, 4, 4], + [-1, -3, -2, -4], [4, -4, -5, -6]]); + assertPointField(cd, 'b', [ + [0, 0, 0, 0], [1, 2, 3, 4], + [0, 0, 0, 0], [4, -3, -2, -4]]); + assertPointField(cd, 's', [ + [1, 2, 3, 4], [3, 2, 1, 0], + [-1, -3, -2, -4], [0, -1, -3, -2]]); + assertPointField(cd, 'p', [ + [1, 2, 3, 4], [1, 2, 3, 4], + [1, 2, 3, 4], [1, 2, 3, 4]]); + assertArrayField(cd[0][0], 't.barwidth', [1, 0.8, 0.6, 0.4]); + assertArrayField(cd[1][0], 't.barwidth', [0.4, 0.6, 0.8, 1]); + expect(cd[2][0].t.barwidth).toBe(1); + expect(cd[3][0].t.barwidth).toBe(0.8); + expect(cd[0][0].t.poffset).toBe(0); + expect(cd[1][0].t.poffset).toBe(0); + expect(cd[2][0].t.poffset).toBe(0); + expect(cd[3][0].t.poffset).toBe(0); + assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8, 0.8, 0.8]); + + done(); + }); + }); +}); + + +function mockBarPlot(dataWithoutTraceType, layout) { + var traceTemplate = { type: 'bar' }; + + var dataWithTraceType = dataWithoutTraceType.map(function(trace) { + return Lib.extendFlat({}, traceTemplate, trace); + }); + + var gd = { + data: dataWithTraceType, + layout: layout, + calcdata: [] + }; + + // call Bar.supplyDefaults + Plots.supplyDefaults(gd); + + // call Bar.calc + gd._fullData.forEach(function(fullTrace) { + var cd = Bar.calc(gd, fullTrace); + + cd[0].t = {}; + cd[0].trace = fullTrace; + + gd.calcdata.push(cd); + }); + + var plotinfo = { + xaxis: gd._fullLayout.xaxis, + yaxis: gd._fullLayout.yaxis + }; + + // call Bar.setPositions + Bar.setPositions(gd, plotinfo); + + return gd; +} + +function assertArrayField(calcData, prop, expectation) { + // Note that this functions requires to add `customMatchers` to jasmine + // matchers; i.e: `jasmine.addMatchers(customMatchers);`. + var values = Lib.nestedProperty(calcData, prop).get(); + if(!Array.isArray(values)) values = [values]; + + expect(values).toBeCloseToArray(expectation, undefined, '(field ' + prop + ')'); +} + +function assertPointField(calcData, prop, expectation) { + // Note that this functions requires to add `customMatchers` to jasmine + // matchers; i.e: `jasmine.addMatchers(customMatchers);`. + var values = []; + + calcData.forEach(function(calcTrace) { + var vals = calcTrace.map(function(pt) { + return Lib.nestedProperty(pt, prop).get(); + }); + + values.push(vals); + }); + + expect(values).toBeCloseTo2DArray(expectation, undefined, '(field ' + prop + ')'); +} + +function assertTraceField(calcData, prop, expectation) { + // Note that this functions requires to add `customMatchers` to jasmine + // matchers; i.e: `jasmine.addMatchers(customMatchers);`. + var values = calcData.map(function(calcTrace) { + return Lib.nestedProperty(calcTrace[0], prop).get(); + }); + + expect(values).toBeCloseToArray(expectation, undefined, '(field ' + prop + ')'); +}