diff --git a/src/traces/bar/calc.js b/src/traces/bar/calc.js index 7468cebdb48..dc8ee1e77e4 100644 --- a/src/traces/bar/calc.js +++ b/src/traces/bar/calc.js @@ -39,7 +39,13 @@ module.exports = function calc(gd, trace) { // create the "calculated data" to plot var serieslen = Math.min(pos.length, size.length), cd = []; + for(i = 0; i < serieslen; i++) { + + // add bars with non-numeric sizes to calcdata + // so that ensure that traces with gaps are + // plotted in the correct order + if(isNumeric(pos[i])) { cd.push({p: pos[i], s: size[i], b: 0}); } diff --git a/src/traces/bar/set_positions.js b/src/traces/bar/set_positions.js index 14daefccfe4..dbc68616224 100644 --- a/src/traces/bar/set_positions.js +++ b/src/traces/bar/set_positions.js @@ -142,6 +142,11 @@ module.exports = function setPositions(gd, plotinfo) { 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 if(relative && ti[j].s < 0) sv = -sv; diff --git a/test/image/baselines/bar_stack-with-gaps.png b/test/image/baselines/bar_stack-with-gaps.png new file mode 100644 index 00000000000..2d08e07c8db Binary files /dev/null and b/test/image/baselines/bar_stack-with-gaps.png differ diff --git a/test/image/mocks/bar_stack-with-gaps.json b/test/image/mocks/bar_stack-with-gaps.json new file mode 100644 index 00000000000..19f4d96f231 --- /dev/null +++ b/test/image/mocks/bar_stack-with-gaps.json @@ -0,0 +1,175 @@ +{ + "data": [ + { + "x": [ + "A", + "B", + "C", + "D", + "E", + "F", + "G" + ], + "y": [ + null, + null, + null, + null, + 7, + null, + 6 + ], + "name": "AA", + "type": "bar" + }, + { + "x": [ + "A", + "B", + "C", + "D", + "E", + "F", + "G" + ], + "y": [ + 8, + null, + null, + null, + null, + null + ], + "name": "BB", + "type": "bar" + }, + { + "x": [ + "A", + "B", + "C", + "D", + "E", + "F", + "G" + ], + "y": [ + null, + null, + null, + 1, + 3, + null + ], + "name": "CC", + "type": "bar" + }, + { + "x": [ + "A", + "B", + "C", + "D", + "E", + "F", + "G" + ], + "y": [ + null, + 4, + 4, + null, + null, + null + ], + "name": "DD", + "type": "bar" + }, + { + "x": [ + "A", + "B", + "C", + "D", + "E", + "F", + "G" + ], + "y": [ + null, + null, + null, + 8, + null, + null, + 3 + ], + "name": "EE", + "type": "bar" + }, + { + "x": [ + "A", + "B", + "C", + "D", + "E", + "F", + "G" + ], + "y": [ + null, + null, + null, + 2, + null, + null + ], + "name": "FF", + "type": "bar" + }, + { + "x": [ + "A", + "B", + "C", + "D", + "E", + "F", + "G" + ], + "y": [ + null, + null, + null, + null, + null, + 1 + ], + "name": "GG", + "type": "bar" + } + ], + "layout": { + "xaxis": { + "type": "category", + "range": [ + -0.5, + 6.5 + ], + "autorange": true + }, + "barmode": "stack", + "yaxis": { + "type": "linear", + "range": [ + 0, + 11.578947368421053 + ], + "autorange": true + }, + "height": 450, + "width": 1100, + "autosize": true + } +} diff --git a/test/jasmine/assets/custom_matchers.js b/test/jasmine/assets/custom_matchers.js index fc1e710bd19..75d88f5bdb1 100644 --- a/test/jasmine/assets/custom_matchers.js +++ b/test/jasmine/assets/custom_matchers.js @@ -1,13 +1,18 @@ +'use strict'; + +var isNumeric = require('fast-isnumeric'); + + module.exports = { // toBeCloseTo... but for arrays toBeCloseToArray: function() { return { - compare: function(actual, expected, precision) { + compare: function(actual, expected, precision, msgExtra) { precision = coercePosition(precision); var tested = actual.map(function(element, i) { - return Math.abs(expected[i] - element) < precision; + return isClose(element, expected[i], precision); }); var passed = ( @@ -15,9 +20,13 @@ module.exports = { tested.indexOf(false) < 0 ); + var message = [ + 'Expected', actual, 'to be close to', expected, msgExtra + ].join(' '); + return { pass: passed, - message: 'Expected ' + actual + ' to be close to ' + expected + '.' + message: message }; } }; @@ -26,7 +35,7 @@ module.exports = { // toBeCloseTo... but for 2D arrays toBeCloseTo2DArray: function() { return { - compare: function(actual, expected, precision) { + compare: function(actual, expected, precision, msgExtra) { precision = coercePosition(precision); var passed = true; @@ -40,9 +49,7 @@ module.exports = { } for(var j = 0; j < expected[i].length; ++j) { - var isClose = Math.abs(expected[i][j] - actual[i][j]) < precision; - - if(!isClose) { + if(!isClose(actual[i][j], expected[i][j], precision)) { passed = false; break; } @@ -54,7 +61,8 @@ module.exports = { 'Expected', arrayToStr(actual.map(arrayToStr)), 'to be close to', - arrayToStr(expected.map(arrayToStr)) + arrayToStr(expected.map(arrayToStr)), + msgExtra ].join(' '); return { @@ -66,6 +74,14 @@ module.exports = { } }; +function isClose(actual, expected, precision) { + if(isNumeric(actual) && isNumeric(expected)) { + return Math.abs(actual - expected) < precision; + } + + return actual === expected; +} + function coercePosition(precision) { if(precision !== 0) { precision = Math.pow(10, -precision) / 2 || 0.005; diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index 7645b93c2cd..365891170d9 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -1,62 +1,186 @@ +var Plots = require('@src/plots/plots'); +var Lib = require('@src/lib'); + var Bar = require('@src/traces/bar'); +var customMatchers = require('../assets/custom_matchers'); + +describe('bar supplyDefaults', function() { + 'use strict'; + + var traceIn, + traceOut; + + var defaultColor = '#444'; + + var supplyDefaults = Bar.supplyDefaults; + + beforeEach(function() { + traceOut = {}; + }); + + it('should set visible to false when x and y are empty', function() { + traceIn = {}; + supplyDefaults(traceIn, traceOut, defaultColor); + expect(traceOut.visible).toBe(false); + + traceIn = { + x: [], + y: [] + }; + supplyDefaults(traceIn, traceOut, defaultColor); + expect(traceOut.visible).toBe(false); + }); + + it('should set visible to false when x or y is empty', function() { + traceIn = { + x: [] + }; + supplyDefaults(traceIn, traceOut, defaultColor); + expect(traceOut.visible).toBe(false); + + traceIn = { + x: [], + y: [1, 2, 3] + }; + supplyDefaults(traceIn, traceOut, defaultColor); + expect(traceOut.visible).toBe(false); + + traceIn = { + y: [] + }; + supplyDefaults(traceIn, traceOut, defaultColor); + expect(traceOut.visible).toBe(false); + + traceIn = { + x: [1, 2, 3], + y: [] + }; + supplyDefaults(traceIn, traceOut, defaultColor); + expect(traceOut.visible).toBe(false); + }); +}); -describe('Test bar', function() { +describe('heatmap calc / setPositions', function() { 'use strict'; - describe('supplyDefaults', function() { - var traceIn, - traceOut; + beforeAll(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 = { + x: function() { return gd._fullLayout.xaxis; }, + y: function() { return gd._fullLayout.yaxis; } + }; + + Bar.setPositions(gd, plotinfo); + + return gd.calcdata; + } - var defaultColor = '#444'; + function assertPtField(calcData, prop, expectation) { + var values = []; - var supplyDefaults = Bar.supplyDefaults; + calcData.forEach(function(calcTrace) { + var vals = calcTrace.map(function(pt) { + return Lib.nestedProperty(pt, prop).get(); + }); - beforeEach(function() { - traceOut = {}; + values.push(vals); }); - it('should set visible to false when x and y are empty', function() { - traceIn = {}; - supplyDefaults(traceIn, traceOut, defaultColor); - expect(traceOut.visible).toBe(false); - - traceIn = { - x: [], - y: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor); - expect(traceOut.visible).toBe(false); + 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([{ + y: [2, 1, 2] + }, { + y: [3, 1, 2] + }, { + y: [null, null, 2] + }], { + 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]); + }); + + it('should fill in calc pt fields (overlay case)', function() { + var out = _calc([{ + y: [2, 1, 2] + }, { + y: [3, 1, 2] + }], { + barmode: 'overlay' }); - it('should set visible to false when x or y is empty', function() { - traceIn = { - x: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor); - expect(traceOut.visible).toBe(false); - - traceIn = { - x: [], - y: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, defaultColor); - expect(traceOut.visible).toBe(false); - - traceIn = { - y: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor); - expect(traceOut.visible).toBe(false); - - traceIn = { - x: [1, 2, 3], - y: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor); - expect(traceOut.visible).toBe(false); + 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]); + }); + + it('should fill in calc pt fields (group case)', function() { + var out = _calc([{ + y: [2, 1, 2] + }, { + y: [3, 1, 2] + }], { + barmode: 'group' }); + 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]); }); });