diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index 9904121f5b0..bf185066f85 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -100,9 +100,11 @@ drawing.hideOutsideRangePoint = function(d, sel, xa, ya, xcalendar, ycalendar) { ); }; -drawing.hideOutsideRangePoints = function(traceGroups, subplot) { +drawing.hideOutsideRangePoints = function(traceGroups, subplot, selector) { if(!subplot._hasClipOnAxisFalse) return; + selector = selector || '.point,.textpoint'; + var xa = subplot.xaxis; var ya = subplot.yaxis; @@ -111,7 +113,7 @@ drawing.hideOutsideRangePoints = function(traceGroups, subplot) { var xcalendar = trace.xcalendar; var ycalendar = trace.ycalendar; - traceGroups.selectAll('.point,.textpoint').each(function(d) { + traceGroups.selectAll(selector).each(function(d) { drawing.hideOutsideRangePoint(d, d3.select(this), xa, ya, xcalendar, ycalendar); }); }); diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 18a5a807959..2df2985554d 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -216,7 +216,7 @@ exports.lsInner = function(gd) { for(i = 0; i < cartesianConstants.traceLayerClasses.length; i++) { var layer = cartesianConstants.traceLayerClasses[i]; - if(layer !== 'scatterlayer') { + if(layer !== 'scatterlayer' && layer !== 'barlayer') { plotinfo.plot.selectAll('g.' + layer).call(Drawing.setClipUrl, layerClipId); } } diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 3ded8e18b47..90c32556a7b 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -793,6 +793,9 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { .call(Drawing.setTextPointsScale, xScaleFactor2, yScaleFactor2); traceGroups .call(Drawing.hideOutsideRangePoints, subplot); + + subplot.plot.selectAll('.barlayer .trace') + .call(Drawing.hideOutsideRangePoints, subplot, '.bartext'); } } diff --git a/src/traces/bar/attributes.js b/src/traces/bar/attributes.js index fcb644e77ea..0d3d131224e 100644 --- a/src/traces/bar/attributes.js +++ b/src/traces/bar/attributes.js @@ -104,6 +104,15 @@ module.exports = { ].join(' ') }, + cliponaxis: extendFlat({}, scatterAttrs.cliponaxis, { + description: [ + 'Determines whether the text nodes', + 'are clipped about the subplot axes.', + 'To show the text nodes above axis lines and tick labels,', + 'make sure to set `xaxis.layer` and `yaxis.layer` to *below traces*.' + ].join(' ') + }), + orientation: { valType: 'enumerated', role: 'info', diff --git a/src/traces/bar/defaults.js b/src/traces/bar/defaults.js index b1d6767a1ac..04096b33ff4 100644 --- a/src/traces/bar/defaults.js +++ b/src/traces/bar/defaults.js @@ -51,6 +51,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('constraintext'); coerce('selected.textfont.color'); coerce('unselected.textfont.color'); + coerce('cliponaxis'); } handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout); diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index 766b27cfd8e..c18d036c0d0 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -138,15 +138,26 @@ module.exports = function plot(gd, plotinfo, cdbar) { bar.append('path') .style('vector-effect', 'non-scaling-stroke') .attr('d', - 'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z'); + 'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z') + .call(Drawing.setClipUrl, plotinfo.layerClipId); appendBarText(gd, bar, d, i, x0, x1, y0, y1); + + if(plotinfo.layerClipId) { + Drawing.hideOutsideRangePoint(d[i], bar.select('text'), xa, ya, trace.xcalendar, trace.ycalendar); + } }); }); // error bars are on the top bartraces.call(ErrorBars.plot, plotinfo); + // lastly, clip points groups of `cliponaxis !== false` traces + // on `plotinfo._hasClipOnAxisFalse === true` subplots + bartraces.each(function(d) { + var hasClipOnAxisFalse = d[0].trace.cliponaxis === false; + Drawing.setClipUrl(d3.select(this), hasClipOnAxisFalse ? null : plotinfo.layerClipId); + }); }; function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { diff --git a/test/image/baselines/bar_cliponaxis-false.png b/test/image/baselines/bar_cliponaxis-false.png new file mode 100644 index 00000000000..1fbbcb499d7 Binary files /dev/null and b/test/image/baselines/bar_cliponaxis-false.png differ diff --git a/test/image/mocks/bar_cliponaxis-false.json b/test/image/mocks/bar_cliponaxis-false.json new file mode 100644 index 00000000000..374094e2647 --- /dev/null +++ b/test/image/mocks/bar_cliponaxis-false.json @@ -0,0 +1,82 @@ +{ + "data": [ + { + "type": "bar", + "name": "not clipped", + "x": ["apple", "banana", "clementine"], + "y": [1.9, 2, 2.1], + "text": ["apple", "banana", "x"], + "textposition": "outside", + "cliponaxis": false, + "textfont": { + "size": [60, 40, 20] + } + }, + { + "type": "bar", + "name": "same but clipped", + "x": ["apple", "banana", "clementine"], + "y": [1.9, 2, 2.1], + "text": ["apple", "banana", "clementine"], + "textposition": "outside", + "cliponaxis": true, + "textfont": { + "size": [60, 40, 20] + }, + "xaxis": "x2", + "yaxis": "y2" + }, + { + "type": "bar", + "name": "should not see text", + "x": ["banana"], + "y": [2], + "text": ["X"], + "textposition": "outside", + "cliponaxis": true, + "textfont": {"size": [20]}, + "marker": {"opacity": 0.3} + } + ], + "layout": { + "barmode": "overlay", + "legend": { + "x": 0.5, + "xanchor": "center", + "y": -0.05, + "yanchor": "top" + }, + "xaxis": { + "showline": true, + "showticklabels": false, + "mirror": true, + "layer": "below traces", + "domain": [0, 0.48] + }, + "xaxis2": { + "anchor": "y2", + "showline": true, + "showticklabels": false, + "mirror": true, + "layer": "below traces", + "domain": [0.52, 1] + }, + "yaxis": { + "showline": true, + "mirror": true, + "layer": "below traces", + "range": [0, 2] + }, + "yaxis2": { + "anchor": "x2", + "showline": true, + "mirror": true, + "layer": "below traces", + "range": [0, 2] + }, + "width": 700, + "height": 400, + "margin": {"t": 40}, + "dragmode": "pan" + } +} diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index 756f678dcca..1dea938d0d7 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -13,6 +13,10 @@ var fail = require('../assets/fail_test'); var checkTicks = require('../assets/custom_assertions').checkTicks; var supplyAllDefaults = require('../assets/supply_defaults'); +var customAssertions = require('../assets/custom_assertions'); +var assertClip = customAssertions.assertClip; +var assertNodeDisplay = customAssertions.assertNodeDisplay; + var d3 = require('d3'); describe('Bar.supplyDefaults', function() { @@ -1509,6 +1513,96 @@ describe('bar hover', function() { .then(done); }); }); + + it('should show/hide text in clipped and non-clipped layers', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/bar_cliponaxis-false.json')); + gd = createGraphDiv(); + + // only show one trace + fig.data = [fig.data[0]]; + + function _assert(layerClips, barDisplays, barTextDisplays, barClips) { + var subplotLayer = d3.select('.plot'); + var barLayer = subplotLayer.select('.barlayer'); + + assertClip(subplotLayer, layerClips[0], 1, 'subplot layer'); + assertClip(subplotLayer.select('.maplayer'), layerClips[1], 1, 'some other trace layer'); + assertClip(barLayer, layerClips[2], 1, 'bar layer'); + + assertNodeDisplay( + barLayer.selectAll('.point'), + barDisplays, + 'bar points (never hidden by display attr)' + ); + assertNodeDisplay( + barLayer.selectAll('.bartext'), + barTextDisplays, + 'bar text' + ); + + assertClip( + barLayer.selectAll('.point > path'), + barClips[0], barClips[1], + 'bar clips' + ); + } + + Plotly.newPlot(gd, fig).then(function() { + _assert( + [false, true, false], + [null, null, null], + [null, null, 'none'], + [true, 3] + ); + return Plotly.restyle(gd, 'visible', false); + }) + .then(function() { + _assert( + [true, false, false], + [], + [], + [false, 0] + ); + return Plotly.restyle(gd, {visible: true, cliponaxis: null}); + }) + .then(function() { + _assert( + [true, false, false], + [null, null, null], + [null, null, null], + [false, 3] + ); + return Plotly.restyle(gd, 'cliponaxis', false); + }) + .then(function() { + _assert( + [false, true, false], + [null, null, null], + [null, null, 'none'], + [true, 3] + ); + return Plotly.relayout(gd, 'yaxis.range', [0, 1]); + }) + .then(function() { + _assert( + [false, true, false], + [null, null, null], + ['none', 'none', 'none'], + [true, 3] + ); + return Plotly.relayout(gd, 'yaxis.range', [0, 4]); + }) + .then(function() { + _assert( + [false, true, false], + [null, null, null], + [null, null, null], + [true, 3] + ); + }) + .catch(fail) + .then(done); + }); }); function mockBarPlot(dataWithoutTraceType, layout) { diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js index 72bf3ce45bc..bb9d6c3073b 100644 --- a/test/jasmine/tests/cartesian_interact_test.js +++ b/test/jasmine/tests/cartesian_interact_test.js @@ -14,6 +14,9 @@ var doubleClick = require('../assets/double_click'); var getNodeCoords = require('../assets/get_node_coords'); var delay = require('../assets/delay'); +var customAssertions = require('../assets/custom_assertions'); +var assertNodeDisplay = customAssertions.assertNodeDisplay; + var MODEBAR_DELAY = 500; describe('zoom box element', function() { @@ -62,50 +65,24 @@ describe('zoom box element', function() { describe('main plot pan', function() { - - var mock = require('@mocks/10.json'); var gd, modeBar, relayoutCallback; - beforeEach(function(done) { + beforeEach(function() { gd = createGraphDiv(); - - Plotly.plot(gd, mock.data, mock.layout).then(function() { - - modeBar = gd._fullLayout._modeBar; - relayoutCallback = jasmine.createSpy('relayoutCallback'); - - gd.on('plotly_relayout', relayoutCallback); - }) - .catch(failTest) - .then(done); }); afterEach(destroyGraphDiv); it('should respond to pan interactions', function(done) { - + var mock = require('@mocks/10.json'); var precision = 5; - var buttonPan = selectButton(modeBar, 'pan2d'); - var originalX = [-0.6225, 5.5]; var originalY = [-1.6340975059013805, 7.166241526218911]; var newX = [-2.0255729166666665, 4.096927083333333]; var newY = [-0.3769062155984817, 8.42343281652181]; - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); - - // Switch to pan mode - expect(buttonPan.isActive()).toBe(false); // initially, zoom is active - buttonPan.click(); - expect(buttonPan.isActive()).toBe(true); // switched on dragmode - - // Switching mode must not change visible range - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); - function _drag(x0, y0, x1, y1) { mouseEvent('mousedown', x0, y0); mouseEvent('mousemove', x1, y1); @@ -143,9 +120,27 @@ describe('main plot pan', function() { _checkAxes(xr0, yr0); } - delay(MODEBAR_DELAY)() - .then(function() { + Plotly.plot(gd, mock.data, mock.layout).then(function() { + modeBar = gd._fullLayout._modeBar; + relayoutCallback = jasmine.createSpy('relayoutCallback'); + gd.on('plotly_relayout', relayoutCallback); + + var buttonPan = selectButton(modeBar, 'pan2d'); + + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + // Switch to pan mode + expect(buttonPan.isActive()).toBe(false); // initially, zoom is active + buttonPan.click(); + expect(buttonPan.isActive()).toBe(true); // switched on dragmode + + // Switching mode must not change visible range + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + }) + .then(delay(MODEBAR_DELAY)) + .then(function() { expect(relayoutCallback).toHaveBeenCalledTimes(1); relayoutCallback.calls.reset(); _runDrag(originalX, newX, originalY, newY); @@ -190,6 +185,93 @@ describe('main plot pan', function() { .catch(failTest) .then(done); }); + + it('should show/hide `cliponaxis: false` pts according to range', function(done) { + function _assert(markerDisplay, textDisplay, barTextDisplay) { + var gd3 = d3.select(gd); + + assertNodeDisplay( + gd3.select('.scatterlayer').selectAll('.point'), + markerDisplay, + 'marker pts' + ); + assertNodeDisplay( + gd3.select('.scatterlayer').selectAll('.textpoint'), + textDisplay, + 'text pts' + ); + assertNodeDisplay( + gd3.select('.barlayer').selectAll('.bartext'), + barTextDisplay, + 'bar text' + ); + } + + function _run(p0, p1, markerDisplay, textDisplay, barTextDisplay) { + mouseEvent('mousedown', p0[0], p0[1]); + mouseEvent('mousemove', p1[0], p1[1]); + + _assert(markerDisplay, textDisplay, barTextDisplay); + + mouseEvent('mouseup', p1[0], p1[1]); + } + + Plotly.newPlot(gd, [{ + mode: 'markers+text', + x: [1, 2, 3], + y: [1, 2, 3], + text: ['a', 'b', 'c'], + cliponaxis: false + }, { + type: 'bar', + x: [1, 2, 3], + y: [1, 2, 3], + text: ['a', 'b', 'c'], + textposition: 'outside', + cliponaxis: false + }], { + xaxis: {range: [0, 4]}, + yaxis: {range: [0, 4]}, + width: 500, + height: 500, + dragmode: 'pan' + }) + .then(function() { + _assert( + [null, null, null], + [null, null, null], + [null, null, null] + ); + }) + .then(function() { + _run( + [250, 250], [250, 150], + [null, null, 'none'], + [null, null, 'none'], + [null, null, 'none'] + ); + expect(gd._fullLayout.yaxis.range[1]).toBeLessThan(3); + }) + .then(function() { + _run( + [250, 250], [150, 250], + ['none', null, 'none'], + ['none', null, 'none'], + ['none', null, 'none'] + ); + expect(gd._fullLayout.xaxis.range[0]).toBeGreaterThan(1); + }) + .then(function() { + _run( + [250, 250], [350, 350], + [null, null, null], + [null, null, null], + [null, null, null] + ); + }) + .catch(failTest) + .then(done); + }); }); describe('axis zoom/pan and main plot zoom', function() { diff --git a/test/jasmine/tests/scatter_test.js b/test/jasmine/tests/scatter_test.js index 063b65ea6a4..1f6fdb97b3f 100644 --- a/test/jasmine/tests/scatter_test.js +++ b/test/jasmine/tests/scatter_test.js @@ -1189,7 +1189,7 @@ describe('Test scatter *clipnaxis*:', function() { var scatterLayer = subplotLayer.select('.scatterlayer'); assertClip(subplotLayer, layerClips[0], 1, 'subplot layer'); - assertClip(subplotLayer.select('.barlayer'), layerClips[1], 1, 'bar layer'); + assertClip(subplotLayer.select('.maplayer'), layerClips[1], 1, 'some other trace layer'); assertClip(scatterLayer, layerClips[2], 1, 'scatter layer'); assertNodeDisplay( diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index eb92824a06a..644a7d6a4c6 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -554,7 +554,7 @@ describe('@flaky Test select box and lasso in general:', function() { }); }); -describe('Test select box and lasso per trace:', function() { +describe('@flaky Test select box and lasso per trace:', function() { var gd; beforeEach(function() { diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js index fb9e1247dd3..d68230c145d 100644 --- a/test/jasmine/tests/shapes_test.js +++ b/test/jasmine/tests/shapes_test.js @@ -629,7 +629,7 @@ describe('Test shapes: a plot with shapes and an overlaid axis', function() { }); }); -describe('Test shapes', function() { +describe('@flaky Test shapes', function() { 'use strict'; var gd, data, layout, config;