diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index aed04eb5dc4..11b72020351 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -114,6 +114,7 @@ function plot(gd, plotinfo, cdModule, traceLayer, opts, makeOnCompleteCallback) } var isHorizontal = (trace.orientation === 'h'); + var withTransition = hasTransition(opts); var pointGroup = Lib.ensureSingle(plotGroup, 'g', 'points'); @@ -139,26 +140,35 @@ function plot(gd, plotinfo, cdModule, traceLayer, opts, makeOnCompleteCallback) var y0 = xy[1][0]; var y1 = xy[1][1]; - var isBlank = ( - x0 === x1 || - y0 === y1 || - !isNumeric(x0) || - !isNumeric(x1) || - !isNumeric(y0) || - !isNumeric(y1) - ); + // empty bars + var isBlank = (isHorizontal ? x1 - x0 : y1 - y0) === 0; // display zeros if line.width > 0 - if(isBlank && shouldDisplayZeros && helpers.getLineWidth(trace, di) && (isHorizontal ? x1 - x0 === 0 : y1 - y0 === 0)) { + if(isBlank && shouldDisplayZeros && helpers.getLineWidth(trace, di)) { isBlank = false; } - di.isBlank = isBlank; - if(isBlank && isHorizontal) x1 = x0; - if(isBlank && !isHorizontal) y1 = y0; + // skip nulls + if(!isBlank) { + isBlank = ( + !isNumeric(x0) || + !isNumeric(x1) || + !isNumeric(y0) || + !isNumeric(y1) + ); + } + + // record isBlank + di.isBlank = isBlank; - var spansHorizontal = isHorizontal && (x0 !== x1); - var spansVertical = !isHorizontal && (y0 !== y1); + // for blank bars, ensure start and end positions are equal - important for smooth transitions + if(isBlank) { + if(isHorizontal) { + x1 = x0; + } else { + y1 = y0; + } + } // in waterfall mode `between` we need to adjust bar end points to match the connector width if(adjustPixel && !isBlank) { @@ -185,16 +195,24 @@ function plot(gd, plotinfo, cdModule, traceLayer, opts, makeOnCompleteCallback) mc = di.mc || trace.marker.color; } - var offset = d3.round((lw / 2) % 1, 2); - function roundWithLine(v) { + var offset = d3.round((lw / 2) % 1, 2); + // if there are explicit gaps, don't round, // it can make the gaps look crappy return (opts.gap === 0 && opts.groupgap === 0) ? d3.round(Math.round(v) - offset, 2) : v; } - function expandToVisible(v, vc) { + function expandToVisible(v, vc, hideZeroSpan) { + if(hideZeroSpan && v === vc) { + // should not expand zero span bars + // when start and end positions are identical + // i.e. for vertical when y0 === y1 + // and for horizontal when x0 === x1 + return v; + } + // if it's not in danger of disappearing entirely, // round more precisely return Math.abs(v - vc) >= 2 ? roundWithLine(v) : @@ -215,14 +233,10 @@ function plot(gd, plotinfo, cdModule, traceLayer, opts, makeOnCompleteCallback) var op = Color.opacity(mc); var fixpx = (op < 1 || lw > 0.01) ? roundWithLine : expandToVisible; - if(spansHorizontal) { - x0 = fixpx(x0, x1); - x1 = fixpx(x1, x0); - } - if(spansVertical) { - y0 = fixpx(y0, y1); - y1 = fixpx(y1, y0); - } + x0 = fixpx(x0, x1, isHorizontal); + x1 = fixpx(x1, x0, isHorizontal); + y0 = fixpx(y0, y1, !isHorizontal); + y1 = fixpx(y1, y0, !isHorizontal); } var sel = transition(Lib.ensureSingle(bar, 'path'), fullLayout, opts, makeOnCompleteCallback); @@ -231,7 +245,7 @@ function plot(gd, plotinfo, cdModule, traceLayer, opts, makeOnCompleteCallback) .attr('d', 'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z') .call(Drawing.setClipUrl, plotinfo.layerClipId, gd); - if(!fullLayout.uniformtext.mode && hasTransition(opts)) { + if(!fullLayout.uniformtext.mode && withTransition) { var styleFns = Drawing.makePointStyleFns(trace); Drawing.singlePointStyle(di, sel, trace, styleFns, gd); } diff --git a/test/image/baselines/bar_show_narrow.png b/test/image/baselines/bar_show_narrow.png new file mode 100644 index 00000000000..ff641a371b1 Binary files /dev/null and b/test/image/baselines/bar_show_narrow.png differ diff --git a/test/image/mocks/bar_show_narrow.json b/test/image/mocks/bar_show_narrow.json new file mode 100644 index 00000000000..db168bb48ab --- /dev/null +++ b/test/image/mocks/bar_show_narrow.json @@ -0,0 +1,166 @@ +{ + "data": [ + { + "type": "bar", + "width": 0.001, + "x": [ + "A", + "B", + "C", + "D" + ], + "y": [ + 0.001, + 0.01, + 0.1, + 1 + ] + }, + { + "type": "bar", + "width": 0.001, + "x": [ + "A", + "B", + "C", + "D" + ], + "y": [ + 0.001, + 0.01, + 0.1, + 1 + ], + "xaxis": "x2", + "yaxis": "y2" + }, + { + "type": "bar", + "width": 0.001, + "orientation": "h", + "x": [ + 0.001, + 0.01, + 0.1, + 1 + ], + "y": [ + "A", + "B", + "C", + "D" + ], + "text": [ + 0, + null, + 0.001, + 1 + ], + "xaxis": "x3", + "yaxis": "y3" + }, + { + "type": "bar", + "width": 0.001, + "orientation": "h", + "x": [ + 0.001, + 0.01, + 0.1, + 1 + ], + "y": [ + "A", + "B", + "C", + "D" + ], + "xaxis": "x4", + "yaxis": "y4" + } + ], + "layout": { + "showlegend": false, + "width": 800, + "height": 800, + "dragmode": "pan", + "xaxis": { + "domain": [ + 0, + 0.48 + ] + }, + "xaxis2": { + "autorange": "reversed", + "anchor": "y2", + "domain": [ + 0.52, + 1 + ] + }, + "xaxis3": { + "range": [ + -0.01, + 1 + ], + "zeroline": false, + "anchor": "y3", + "domain": [ + 0, + 0.48 + ] + }, + "xaxis4": { + "range": [ + -0.01, + 1 + ], + "zeroline": false, + "autorange": "reversed", + "anchor": "y4", + "domain": [ + 0.52, + 1 + ] + }, + "yaxis": { + "range": [ + -0.01, + 1 + ], + "zeroline": false, + "domain": [ + 0, + 0.48 + ] + }, + "yaxis2": { + "range": [ + -0.01, + 1 + ], + "zeroline": false, + "autorange": "reversed", + "anchor": "x2", + "domain": [ + 0.52, + 1 + ] + }, + "yaxis3": { + "anchor": "x3", + "domain": [ + 0.52, + 1 + ] + }, + "yaxis4": { + "autorange": "reversed", + "anchor": "x4", + "domain": [ + 0, + 0.48 + ] + } + } +} diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index 171d7b8e3c6..f9951e2ddd8 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -2026,24 +2026,24 @@ describe('A bar plot', function() { .then(done); }); + function getArea(path) { + var pos = path + .substr(1, path.length - 2) + .replace('V', ',') + .replace('H', ',') + .replace('V', ',') + .split(','); + var dx = +pos[0]; + var dy = +pos[1]; + dy -= +pos[2]; + dx -= +pos[3]; + + return Math.abs(dx * dy); + } + it('should not show up null and zero bars as thin bars', function(done) { var mock = Lib.extendDeep({}, require('@mocks/bar_hide_nulls.json')); - function getArea(path) { - var pos = path - .substr(1, path.length - 2) - .replace('V', ',') - .replace('H', ',') - .replace('V', ',') - .split(','); - var dx = +pos[0]; - var dy = +pos[1]; - dy -= +pos[2]; - dx -= +pos[3]; - - return Math.abs(dx * dy); - } - Plotly.plot(gd, mock) .then(function() { var nodes = gd.querySelectorAll('g.point > path'); @@ -2076,6 +2076,39 @@ describe('A bar plot', function() { .catch(failTest) .then(done); }); + + describe('show narrow bars', function() { + ['initial zoom', 'after zoom out'].forEach(function(zoomStr) { + it(zoomStr, function(done) { + var mock = Lib.extendDeep({}, require('@mocks/bar_show_narrow.json')); + + if(zoomStr === 'after zoom out') { + mock.layout.xaxis.range = [-14.9, 17.9]; + mock.layout.xaxis2.range = [17.9, -14.9]; + mock.layout.xaxis3.range = [-3.9, 4.9]; + mock.layout.xaxis4.range = [4.9, -3.9]; + + mock.layout.yaxis.range = [-3.9, 4.9]; + mock.layout.yaxis2.range = [4.9, -3.9]; + mock.layout.yaxis3.range = [-14.9, 17.9]; + mock.layout.yaxis4.range = [17.9, -14.9]; + } + + Plotly.plot(gd, mock) + .then(function() { + var nodes = gd.querySelectorAll('g.point > path'); + expect(nodes.length).toBe(16, '# of bars'); + + for(var i = 0; i < 16; i++) { + var d = nodes[i].getAttribute('d'); + expect(getArea(d) > 0).toBe(true, 'item:' + i); + } + }) + .catch(failTest) + .then(done); + }); + }); + }); }); describe('bar visibility toggling:', function() { @@ -2837,6 +2870,67 @@ describe('bar tweening', function() { .catch(failTest) .then(done); }); + + it('blank vertical bars', function(done) { + var mockCopy = { + data: [{ + type: 'bar', + x: ['A', 'B', 'C'], + y: [null, 5, 3], + marker: { + line: { + width: 10 + } + } + }], + layout: { + width: 400, + height: 300 + } + }; + + var tests = [ + [0, '.point path', 'attr', 'd', ['M8,120V120H72V120Z', 'M88,120V6H152V120Z', 'M168,120V52H232V120Z']], + [300, '.point path', 'attr', 'd', ['M8,120V52H72V120Z', 'M88,120V74H152V120Z', 'M168,120V65H232V120Z']], + [600, '.point path', 'attr', 'd', ['M8,120V6H72V120Z', 'M88,120V120H152V120Z', 'M168,120V74H232V120Z']] + ]; + var animateOpts = {data: [{y: [5, null, 2]}]}; + + checkTransition(gd, mockCopy, animateOpts, transitionOpts, tests) + .catch(failTest) + .then(done); + }); + + it('blank horizontal bars', function(done) { + var mockCopy = { + data: [{ + type: 'bar', + orientation: 'h', + y: ['A', 'B', 'C'], + x: [null, 5, 3], + marker: { + line: { + width: 10 + } + } + }], + layout: { + width: 400, + height: 300 + } + }; + + var tests = [ + [0, '.point path', 'attr', 'd', ['M0,116V84H0V116Z', 'M0,76V44H228V76Z', 'M0,36V4H137V36Z']], + [300, '.point path', 'attr', 'd', ['M0,116V84H137V116Z', 'M0,76V44H91V76Z', 'M0,36V4H109V36Z']], + [600, '.point path', 'attr', 'd', ['M0,116V84H228V116Z', 'M0,76V44H0V76Z', 'M0,36V4H91V36Z']] + ]; + var animateOpts = {data: [{x: [5, null, 2]}]}; + + checkTransition(gd, mockCopy, animateOpts, transitionOpts, tests) + .catch(failTest) + .then(done); + }); }); describe('bar uniformtext', function() {