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/layout_attributes.js b/src/traces/bar/layout_attributes.js index e2cca32a822..827f0ebccf8 100644 --- a/src/traces/bar/layout_attributes.js +++ b/src/traces/bar/layout_attributes.js @@ -10,6 +10,12 @@ module.exports = { + barbase: { + valType: 'number', + dflt: 0, + role: 'style', + description: 'Sets where the bar base is drawn (in axis units).' + }, barmode: { valType: 'enumerated', values: ['stack', 'group', 'overlay', 'relative'], diff --git a/src/traces/bar/layout_defaults.js b/src/traces/bar/layout_defaults.js index 8c6d6c3ff23..dc8f4f05619 100644 --- a/src/traces/bar/layout_defaults.js +++ b/src/traces/bar/layout_defaults.js @@ -48,6 +48,8 @@ module.exports = function(layoutIn, layoutOut, fullData) { if(!hasBars) return; + coerce('barbase'); + var mode = coerce('barmode'); if(mode !== 'overlay') coerce('barnorm'); diff --git a/src/traces/bar/set_positions.js b/src/traces/bar/set_positions.js index 3fea9a1d58c..4c40e7daebe 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,344 @@ var Lib = require('../../lib'); */ module.exports = function setPositions(gd, plotinfo) { + var xa = plotinfo.xaxis, + ya = plotinfo.yaxis; + + var traces = gd._fullData, + tracesCalc = gd.calcdata, + tracesHorizontal = [], + tracesVertical = [], + i; + for(i = 0; i < traces.length; i++) { + var trace = traces[i]; + if( + trace.visible === true && + Registry.traceIs(trace, 'bar') && + trace.xaxis === xa._id && + trace.yaxis === ya._id + ) { + if(trace.orientation === 'h') tracesHorizontal.push(tracesCalc[i]); + else tracesVertical.push(tracesCalc[i]); + } + } + + setGroupPositions(gd, xa, ya, tracesVertical); + setGroupPositions(gd, ya, xa, tracesHorizontal); +}; + + +function setGroupPositions(gd, pa, sa, traces) { + if(!traces.length) return; + + var barmode = gd._fullLayout.barmode, + overlay = (barmode === 'overlay'), + group = (barmode === 'group'); + + if(overlay) { + setGroupPositionsInOverlayMode(gd, pa, sa, traces); + } + else if(group) { + setGroupPositionsInGroupMode(gd, pa, sa, traces); + } + else { + setGroupPositionsInStackOrRelativeMode(gd, pa, sa, traces); + } +} + + +function setGroupPositionsInOverlayMode(gd, pa, sa, traces) { + var barnorm = gd._fullLayout.barnorm, + separateNegativeValues = false, + dontMergeOverlappingData = !barnorm; + + // update position axis and set bar offsets and widths + traces.forEach(function(trace) { + var sieve = new Sieve( + [trace], separateNegativeValues, dontMergeOverlappingData + ); + + // set bar offsets and widths and update position axis + setOffsetAndWidth(gd, pa, sieve); + + // update size axis and set bar bases and sizes + if(barnorm) { + stackBars(gd, sa, sieve); + } + else { + // make sure the size axis includes zero, + // along with the tops of each bar, + // and store these bar tops in calcdata + var sLetter = getAxisLetter(sa), + fs = function(v) { v[sLetter] = v.s; return v.s; }; + + Axes.expand(sa, trace.map(fs), {tozero: true, padded: true}); + } + }); + + applyBarbase(gd, sa, traces); +} + + +function setGroupPositionsInGroupMode(gd, pa, sa, traces) { 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); - } - }); - - 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); - }); - }); - } + barnorm = fullLayout.barnorm, + separateNegativeValues = false, + dontMergeOverlappingData = !barnorm, + sieve = new Sieve( + traces, separateNegativeValues, dontMergeOverlappingData + ); + + // set bar offsets and widths and update position axis + setOffsetAndWidthInGroupMode(gd, pa, sieve); + + // update size axis and set bar bases and sizes + if(barnorm) { + stackBars(gd, sa, sieve); + } + else { + // make sure the size axis includes zero, + // along with the tops of each bar, + // and store these bar tops in calcdata + var sLetter = getAxisLetter(sa), + fs = function(v) { v[sLetter] = v.s; return v.s; }; + + for(var i = 0; i < traces.length; i++) { + Axes.expand(sa, traces[i].map(fs), {tozero: true, padded: true}); + } + } - // check forced minimum dtick - Axes.minDtick(pa, barDiff, pv2[0], overlap); + applyBarbase(gd, sa, traces); +} - // position axis autorange - always tight fitting - Axes.expand(pa, pv2, {vpad: barDiff / 2}); - // bar widths and position offsets - barDiff *= 1 - fullLayout.bargap; - if(overlap) barDiff /= bl.length; +function setGroupPositionsInStackOrRelativeMode(gd, pa, sa, traces) { + 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( + traces, separateNegativeValues, dontMergeOverlappingData + ); - var barCenter; - function setBarCenter(v) { v[pLetter] = v.p + barCenter; } + // set bar offsets and widths and update position axis + setOffsetAndWidth(gd, pa, sieve); - 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; + // set bar bases and sizes and update size axis + stackBars(gd, sa, sieve); +} - // store the bar center in each calcdata item - barCenter = t.poffset + t.barwidth / 2; - gd.calcdata[bl1[i]].forEach(setBarCenter); - } + +function setOffsetAndWidth(gd, pa, sieve) { + var fullLayout = gd._fullLayout, + pLetter = getAxisLetter(pa), + traces = sieve.traces, + bargap = fullLayout.bargap, + bargroupgap = fullLayout.bargroupgap, + minDiff = sieve.minDiff; + + // set bar offsets and widths + var barGroupWidth = minDiff * (1 - bargap), + barWidthPlusGap = barGroupWidth, + barWidth = barWidthPlusGap * (1 - bargroupgap); + + // computer bar group center and bar offset + var offsetFromCenter = -barWidth / 2, + barCenter = 0; + + for(var i = 0; i < traces.length; i++) { + var trace = traces[i]; + + // store bar width and offset for this trace + var t = trace[0].t; + t.barwidth = barWidth; + t.poffset = offsetFromCenter; + t.bargroupwidth = barGroupWidth; + + // store the bar center in each calcdata item + for(var j = 0; j < trace.length; j++) { + var bar = trace[j]; + bar[pLetter] = bar.p + barCenter; } + } + + // update position axis + var distinctPositions = sieve.distinctPositions; + Axes.minDtick(pa, minDiff, distinctPositions[0]); + Axes.expand(pa, distinctPositions, {vpad: minDiff / 2}); +} - if(fullLayout.barmode === 'overlay') { - bl.forEach(function(bli) { barposition([bli]); }); + +function setOffsetAndWidthInGroupMode(gd, pa, sieve) { + var fullLayout = gd._fullLayout, + pLetter = getAxisLetter(pa), + traces = sieve.traces, + bargap = fullLayout.bargap, + bargroupgap = fullLayout.bargroupgap, + positions = sieve.positions, + distinctPositions = sieve.distinctPositions, + minDiff = sieve.minDiff; + + // 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 - bargap), + barWidthPlusGap = (overlap) ? + barGroupWidth / traces.length : + barGroupWidth, + barWidth = barWidthPlusGap * (1 - bargroupgap); + + for(var i = 0; i < traces.length; i++) { + var trace = traces[i]; + + // computer bar group center and bar offset + var offsetFromCenter = (overlap) ? + ((2 * i + 1 - traces.length) * barWidthPlusGap - barWidth) / 2 : + -barWidth / 2, + barCenter = offsetFromCenter + barWidth / 2; + + // store bar width and offset for this trace + var t = trace[0].t; + t.barwidth = barWidth; + t.poffset = offsetFromCenter; + t.bargroupwidth = barGroupWidth; + + // store the bar center in each calcdata item + for(var j = 0; j < trace.length; j++) { + var bar = trace[j]; + bar[pLetter] = bar.p + barCenter; } - 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); - } - } + } + + // update position axis + Axes.minDtick(pa, minDiff, distinctPositions[0], overlap); + Axes.expand(pa, distinctPositions, {vpad: minDiff / 2}); +} + + +function stackBars(gd, sa, sieve) { + var fullLayout = gd._fullLayout, + sLetter = getAxisLetter(sa), + traces = sieve.traces, + i, trace, + j, bar; + + var stack = (fullLayout.barmode === 'stack'), + relative = (fullLayout.barmode === 'relative'), + norm = fullLayout.barnorm; + + // bar size range and stacking calculation + // 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; + + // stack bars that only differ by rounding + sieve.binWidth = traces[0][0].t.barwidth / 100; + + for(i = 0; i < traces.length; i++) { + trace = traces[i]; + + for(j = 0; j < trace.length; j++) { + bar = trace[j]; + + // skip over bars with no size, + // so that we don't try to stack them + if(!isNumeric(bar.s)) continue; + + // stack current bar and get previous sum + var previousSum = sieve.put(bar.p, bar.s); + + if(stack || relative) bar.b = previousSum; + + // store the bar top in each calcdata item + if(stack || relative) { + var barEnd = bar.b + bar.s; + bar[sLetter] = barEnd; + if(!norm && isNumeric(sa.c2l(barEnd))) { + sMax = Math.max(sMax, barEnd); + sMin = Math.min(sMin, barEnd); } } - - 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(norm) { + normalizeBars(gd, sa, sieve); + } + else { + Axes.expand(sa, [sMin, sMax], {tozero: true, padded: true}); + } +} + + +function normalizeBars(gd, sa, sieve) { + 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 barEnd = bar.b + bar.s; + bar[sLetter] = barEnd; + + if(isNumeric(sa.c2l(barEnd))) { + if(barEnd < sMin - sTiny) { + padded = true; + sMin = barEnd; + } + if(barEnd > sMax + sTiny) { + padded = true; + sMax = barEnd; } } - - Axes.expand(sa, [sMin, sMax], {tozero: true, padded: padded}); } - 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}); + } + + Axes.expand(sa, [sMin, sMax], {tozero: true, padded: padded}); +} + + +function applyBarbase(gd, sa, traces) { + var barbase = gd._fullLayout.barbase; + if(!barbase || !isNumeric(sa.c2l(barbase))) return; + + for(var i = 0; i < traces.length; i++) { + var trace = traces[i]; + + for(var j = 0; j < trace.length; j++) { + var bar = trace[j], + bartop = bar.b + bar.s; + if(isNumeric(sa.c2l(bartop))) { + bar.b = barbase; + bar.s = bartop - barbase; } } - }); -}; + } + + Axes.expand(sa, [barbase], {tozero: true, padded: true}); +} + + +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_group_barbase.png b/test/image/baselines/bar_group_barbase.png new file mode 100644 index 00000000000..5e407d216a1 Binary files /dev/null and b/test/image/baselines/bar_group_barbase.png differ diff --git a/test/image/mocks/bar_group_barbase.json b/test/image/mocks/bar_group_barbase.json new file mode 100644 index 00000000000..d9937a8943f --- /dev/null +++ b/test/image/mocks/bar_group_barbase.json @@ -0,0 +1,44 @@ +{ + "data": [ + { + "x": [ + "giraffes", + "orangutans", + "monkeys" + ], + "y": [ + 20, + 14, + 23 + ], + "name": "SF Zoo", + "type": "bar" + }, + { + "x": [ + "giraffes", + "orangutans", + "monkeys" + ], + "y": [ + 12, + 18, + 29 + ], + "name": "LA Zoo", + "type": "bar" + } + ], + "layout": { + "xaxis": { + "type": "category" + }, + "barbase": 10, + "barmode": "group", + "categories": [ + "giraffes", + "orangutans", + "monkeys" + ] + } +} diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index b1363b111ad..fcee4a2de83 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -61,46 +61,134 @@ describe('bar supplyDefaults', function() { }); }); -describe('heatmap calc / setPositions', function() { +describe('bar supplyLayoutDefaults', function() { + 'use strict'; + + var gd; + + it('should set barbase to 0', function() { + gd = { + data: [{ + type: 'bar', + x: [], + y: [], + }], + layout: {} + }; + + Plots.supplyDefaults(gd); + + expect(gd._fullLayout.barbase).toBe(0); + }); +}); + +describe('barbase', function() { 'use strict'; beforeAll(function() { jasmine.addMatchers(customMatchers); }); - function _calc(dataOpts, layout) { - var baseData = { type: 'bar' }; - - var data = dataOpts.map(function(traceOpts) { - return Lib.extendFlat({}, baseData, traceOpts); - }); + function assertPointField(calcData, prop, expectation) { + var values = []; - var gd = { - data: data, - layout: layout, - calcdata: [] - }; + calcData.forEach(function(calcTrace) { + var vals = calcTrace.map(function(pt) { + return Lib.nestedProperty(pt, prop).get(); + }); - Plots.supplyDefaults(gd); + values.push(vals); + }); - gd._fullData.forEach(function(fullTrace) { - var cd = Bar.calc(gd, fullTrace); + expect(values).toBeCloseTo2DArray( + expectation, undefined, '- field ' + prop); + } - cd[0].t = {}; - cd[0].trace = fullTrace; + it('should be honored in overlay mode', function() { + var data = [{ + y: [10, 20, 30] + }, { + y: [-10, 20, -30] + }, { + y: [null, null, -30] + }], + layout = { + barbase: 10, + barmode: 'overlay' + }, + cd = getCalcdata(data, layout); + + // Note: + // The base of null bars is set to zero and the size is left undefined + assertPointField(cd, 'b', [[10, 10, 10], [10, 10, 10], [0, 0, 10]]); + assertPointField(cd, 's', + [[0, 10, 20], [-20, 10, -40], [undefined, undefined, -40]]); + assertPointField(cd, 'p', [[0, 1, 2], [0, 1, 2], [0, 1, 2]]); + assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2], [0, 1, 2]]); + assertPointField(cd, 'y', + [[10, 20, 30], [-10, 20, -30], [undefined, undefined, -30]]); + }); - gd.calcdata.push(cd); - }); + it('should be honored in group mode', function() { + var data = [{ + y: [10, 20, 30] + }, { + y: [-10, 20, -30] + }, { + y: [null, null, -30] + }], + layout = { + barbase: 10, + barmode: 'group' + }, + cd = getCalcdata(data, layout); + + // Note: + // The base of null bars is set to zero and the size is left undefined + assertPointField(cd, 'b', [[10, 10, 10], [10, 10, 10], [0, 0, 10]]); + assertPointField(cd, 's', + [[0, 10, 20], [-20, 10, -40], [undefined, undefined, -40]]); + assertPointField(cd, 'p', [[0, 1, 2], [0, 1, 2], [0, 1, 2]]); + assertPointField(cd, 'x', + [[-0.27, 0.73, 1.73], [0, 1, 2], [0.27, 1.27, 2.27]]); + assertPointField(cd, 'y', + [[10, 20, 30], [-10, 20, -30], [undefined, undefined, -30]]); + }); - var plotinfo = { - xaxis: gd._fullLayout.xaxis, - yaxis: gd._fullLayout.yaxis - }; + it('should be honored in group mode when barnorm is set', function() { + var data = [{ + y: [30, 70, 20] + }, { + y: [70, 30, 30] + }, { + y: [null, null, 50] + }], + layout = { + barbase: 1, + barmode: 'group', + barnorm: 'fraction' + }, + cd = getCalcdata(data, layout); + + // Note: + // The base of null bars is set to zero and the size is left undefined + assertPointField(cd, 'b', [[1, 1, 1], [1, 1, 1], [0, 0, 1]]); + assertPointField(cd, 's', + [[-0.7, -0.3, -0.8], [-0.3, -0.7, -0.7], [undefined, undefined, -0.5]]); + assertPointField(cd, 'p', [[0, 1, 2], [0, 1, 2], [0, 1, 2]]); + assertPointField(cd, 'x', + [[-0.27, 0.73, 1.73], [0, 1, 2], [0.27, 1.27, 2.27]]); + assertPointField(cd, 'y', + [[0.3, 0.7, 0.2], [0.7, 0.3, 0.3], [undefined, undefined, 0.5]]); + }); +}); - Bar.setPositions(gd, plotinfo); +describe('heatmap calc / setPositions', function() { + 'use strict'; - return gd.calcdata; - } + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); function assertPtField(calcData, prop, expectation) { var values = []; @@ -125,7 +213,7 @@ describe('heatmap calc / setPositions', function() { } it('should fill in calc pt fields (stack case)', function() { - var out = _calc([{ + var out = getCalcdata([{ y: [2, 1, 2] }, { y: [3, 1, 2] @@ -142,11 +230,11 @@ describe('heatmap calc / setPositions', function() { 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]); + assertTraceField(out, 't.bargroupwidth', [0.8, 0.8, 0.8]); }); it('should fill in calc pt fields (overlay case)', function() { - var out = _calc([{ + var out = getCalcdata([{ y: [2, 1, 2] }, { y: [3, 1, 2] @@ -161,16 +249,18 @@ describe('heatmap calc / setPositions', function() { 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]); + assertTraceField(out, 't.bargroupwidth', [0.8, 0.8]); }); it('should fill in calc pt fields (group case)', function() { - var out = _calc([{ + var out = getCalcdata([{ 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]]); @@ -178,13 +268,13 @@ describe('heatmap calc / setPositions', function() { 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]); + assertTraceField(out, 't.barwidth', [0.36, 0.36]); + assertTraceField(out, 't.poffset', [-0.38, 0.02]); + assertTraceField(out, 't.bargroupwidth', [0.8, 0.8]); }); it('should fill in calc pt fields (relative case)', function() { - var out = _calc([{ + var out = getCalcdata([{ y: [20, 14, -23] }, { y: [-12, -18, -29] @@ -199,11 +289,11 @@ describe('heatmap calc / setPositions', function() { 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]); + assertTraceField(out, 't.bargroupwidth', [0.8, 0.8]); }); it('should fill in calc pt fields (relative / percent case)', function() { - var out = _calc([{ + var out = getCalcdata([{ x: ['A', 'B', 'C', 'D'], y: [20, 14, 40, -60] }, { @@ -221,6 +311,40 @@ describe('heatmap calc / setPositions', function() { 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]); + assertTraceField(out, 't.bargroupwidth', [0.8, 0.8]); }); }); + +function getCalcdata(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; +}