diff --git a/src/traces/histogram/calc.js b/src/traces/histogram/calc.js index f1bca0555e7..009c59546d5 100644 --- a/src/traces/histogram/calc.js +++ b/src/traces/histogram/calc.js @@ -43,9 +43,11 @@ module.exports = function calc(gd, trace) { var pos0 = pa.makeCalcdata(trace, maindata); // calculate the bins - var binAttr = maindata + 'bins', - binspec; - if((trace['autobin' + maindata] !== false) || !(binAttr in trace)) { + var binAttr = maindata + 'bins'; + var autoBinAttr = 'autobin' + maindata; + var binspec = trace[binAttr]; + if((trace[autoBinAttr] !== false) || !binspec || + binspec.start === null || binspec.end === null) { binspec = Axes.autoBin(pos0, pa, trace['nbins' + maindata], false, calendar); // adjust for CDF edge cases @@ -60,9 +62,11 @@ module.exports = function calc(gd, trace) { // copy bin info back to the source and full data. trace._input[binAttr] = trace[binAttr] = binspec; - } - else { - binspec = trace[binAttr]; + // note that it's possible to get here with an explicit autobin: false + // if the bins were not specified. + // in that case this will remain in the trace, so that future updates + // which would change the autobinning will not do so. + trace._input[autoBinAttr] = trace[autoBinAttr]; } var nonuniformBins = typeof binspec.size === 'string', diff --git a/src/traces/histogram2d/calc.js b/src/traces/histogram2d/calc.js index 602c5d9a545..4483b938fad 100644 --- a/src/traces/histogram2d/calc.js +++ b/src/traces/histogram2d/calc.js @@ -45,7 +45,8 @@ module.exports = function calc(gd, trace) { // calculate the bins - if(trace.autobinx || !('xbins' in trace)) { + if(trace.autobinx || !trace.xbins || + trace.xbins.start === null || trace.xbins.end === null) { trace.xbins = Axes.autoBin(x, xa, trace.nbinsx, '2d', xcalendar); if(trace.type === 'histogram2dcontour') { // the "true" last argument reverses the tick direction (which we can't @@ -58,8 +59,14 @@ module.exports = function calc(gd, trace) { // copy bin info back to the source data. trace._input.xbins = trace.xbins; + // note that it's possible to get here with an explicit autobin: false + // if the bins were not specified. + // in that case this will remain in the trace, so that future updates + // which would change the autobinning will not do so. + trace._input.autobinx = trace.autobinx; } - if(trace.autobiny || !('ybins' in trace)) { + if(trace.autobiny || !trace.ybins || + trace.ybins.start === null || trace.ybins.end === null) { trace.ybins = Axes.autoBin(y, ya, trace.nbinsy, '2d', ycalendar); if(trace.type === 'histogram2dcontour') { trace.ybins.start = yc2r(Axes.tickIncrement( @@ -68,6 +75,7 @@ module.exports = function calc(gd, trace) { yr2c(trace.ybins.end), trace.ybins.size, false, ycalendar)); } trace._input.ybins = trace.ybins; + trace._input.autobiny = trace.autobiny; } // make the empty bin array & scale the map diff --git a/test/jasmine/tests/histogram2d_test.js b/test/jasmine/tests/histogram2d_test.js index e9d43fac05b..cc00183b12d 100644 --- a/test/jasmine/tests/histogram2d_test.js +++ b/test/jasmine/tests/histogram2d_test.js @@ -137,13 +137,17 @@ describe('Test histogram2d', function() { }); }); - describe('relayout interaction', function() { + describe('restyle / relayout interaction', function() { + + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); afterEach(destroyGraphDiv); it('should update paths on zooms', function(done) { - var gd = createGraphDiv(); - Plotly.newPlot(gd, [{ type: 'histogram2dcontour', x: [1, 1, 2, 2, 3], @@ -156,6 +160,75 @@ describe('Test histogram2d', function() { .then(done); }); + + it('handles autobin correctly on restyles', function() { + var x1 = [ + 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, + 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4]; + var y1 = [ + 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, + 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4]; + Plotly.newPlot(gd, [{type: 'histogram2d', x: x1, y: y1}]); + expect(gd._fullData[0].xbins).toEqual({start: 0.5, end: 4.5, size: 1}); + expect(gd._fullData[0].ybins).toEqual({start: 0.5, end: 4.5, size: 1}); + expect(gd._fullData[0].autobinx).toBe(true); + expect(gd._fullData[0].autobiny).toBe(true); + + // same range but fewer samples increases sizes + Plotly.restyle(gd, {x: [[1, 3, 4]], y: [[1, 2, 4]]}); + expect(gd._fullData[0].xbins).toEqual({start: -0.5, end: 5.5, size: 2}); + expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 5.5, size: 2}); + expect(gd._fullData[0].autobinx).toBe(true); + expect(gd._fullData[0].autobiny).toBe(true); + + // larger range + Plotly.restyle(gd, {x: [[10, 30, 40]], y: [[10, 20, 40]]}); + expect(gd._fullData[0].xbins).toEqual({start: -0.5, end: 59.5, size: 20}); + expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 59.5, size: 20}); + expect(gd._fullData[0].autobinx).toBe(true); + expect(gd._fullData[0].autobiny).toBe(true); + + // explicit changes to bin settings + Plotly.restyle(gd, 'xbins.start', 12); + expect(gd._fullData[0].xbins).toEqual({start: 12, end: 59.5, size: 20}); + expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 59.5, size: 20}); + expect(gd._fullData[0].autobinx).toBe(false); + expect(gd._fullData[0].autobiny).toBe(true); + + Plotly.restyle(gd, {'ybins.end': 12, 'ybins.size': 3}); + expect(gd._fullData[0].xbins).toEqual({start: 12, end: 59.5, size: 20}); + expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 12, size: 3}); + expect(gd._fullData[0].autobinx).toBe(false); + expect(gd._fullData[0].autobiny).toBe(false); + + // restart autobin + Plotly.restyle(gd, {autobinx: true, autobiny: true}); + expect(gd._fullData[0].xbins).toEqual({start: -0.5, end: 59.5, size: 20}); + expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 59.5, size: 20}); + expect(gd._fullData[0].autobinx).toBe(true); + expect(gd._fullData[0].autobiny).toBe(true); + }); + + it('respects explicit autobin: false as a one-time autobin', function() { + var x1 = [ + 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, + 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4]; + var y1 = [ + 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, + 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4]; + Plotly.newPlot(gd, [{type: 'histogram2d', x: x1, y: y1, autobinx: false, autobiny: false}]); + expect(gd._fullData[0].xbins).toEqual({start: 0.5, end: 4.5, size: 1}); + expect(gd._fullData[0].ybins).toEqual({start: 0.5, end: 4.5, size: 1}); + expect(gd._fullData[0].autobinx).toBe(false); + expect(gd._fullData[0].autobiny).toBe(false); + + // with autobin false this will no longer update the bins. + Plotly.restyle(gd, {x: [[1, 3, 4]], y: [[1, 2, 4]]}); + expect(gd._fullData[0].xbins).toEqual({start: 0.5, end: 4.5, size: 1}); + expect(gd._fullData[0].ybins).toEqual({start: 0.5, end: 4.5, size: 1}); + expect(gd._fullData[0].autobinx).toBe(false); + expect(gd._fullData[0].autobiny).toBe(false); + }); }); }); diff --git a/test/jasmine/tests/histogram_test.js b/test/jasmine/tests/histogram_test.js index 8c257a9b12d..922adf4d497 100644 --- a/test/jasmine/tests/histogram_test.js +++ b/test/jasmine/tests/histogram_test.js @@ -1,9 +1,13 @@ +var Plotly = require('@lib/index'); var Plots = require('@src/plots/plots'); var Lib = require('@src/lib'); var supplyDefaults = require('@src/traces/histogram/defaults'); var calc = require('@src/traces/histogram/calc'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); + describe('Test histogram', function() { 'use strict'; @@ -365,4 +369,59 @@ describe('Test histogram', function() { }); }); + + describe('plot / restyle', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + it('should update autobins correctly when restyling', function() { + // note: I'm *not* testing what this does to gd.data, as that's + // really a matter of convenience and will perhaps change later (v2?) + var data1 = [1.5, 2, 2, 3, 3, 3, 4, 4, 5]; + Plotly.plot(gd, [{x: data1, type: 'histogram' }]); + expect(gd._fullData[0].xbins).toEqual({start: 1, end: 6, size: 1}); + expect(gd._fullData[0].autobinx).toBe(true); + + // same range but fewer samples changes autobin size + var data2 = [1.5, 5]; + Plotly.restyle(gd, 'x', [data2]); + expect(gd._fullData[0].xbins).toEqual({start: -2.5, end: 7.5, size: 5}); + expect(gd._fullData[0].autobinx).toBe(true); + + // different range + var data3 = [10, 20.2, 20, 30, 30, 30, 40, 40, 50]; + Plotly.restyle(gd, 'x', [data3]); + expect(gd._fullData[0].xbins).toEqual({start: 5, end: 55, size: 10}); + expect(gd._fullData[0].autobinx).toBe(true); + + // explicit change to a bin attribute clears autobin + Plotly.restyle(gd, 'xbins.start', 3); + expect(gd._fullData[0].xbins).toEqual({start: 3, end: 55, size: 10}); + expect(gd._fullData[0].autobinx).toBe(false); + + // restart autobin + Plotly.restyle(gd, 'autobinx', true); + expect(gd._fullData[0].xbins).toEqual({start: 5, end: 55, size: 10}); + expect(gd._fullData[0].autobinx).toBe(true); + }); + + it('respects explicit autobin: false as a one-time autobin', function() { + var data1 = [1.5, 2, 2, 3, 3, 3, 4, 4, 5]; + Plotly.plot(gd, [{x: data1, type: 'histogram', autobinx: false }]); + // we have no bins, so even though autobin is false we have to autobin once + expect(gd._fullData[0].xbins).toEqual({start: 1, end: 6, size: 1}); + expect(gd._fullData[0].autobinx).toBe(false); + + // since autobin is false, this will not change the bins + var data2 = [1.5, 5]; + Plotly.restyle(gd, 'x', [data2]); + expect(gd._fullData[0].xbins).toEqual({start: 1, end: 6, size: 1}); + expect(gd._fullData[0].autobinx).toBe(false); + }); + }); });