From c5cb42c7195d7d11f0d8c7c91d0f4451a074c221 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sun, 23 Sep 2018 15:17:19 -0400 Subject: [PATCH 01/10] fix up cleanDate error reporting no error on `undefined` but yes on non-finite numbers --- src/lib/dates.js | 4 +++- test/jasmine/tests/lib_date_test.js | 8 +++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/lib/dates.js b/src/lib/dates.js index 67828b32837..1027a797928 100644 --- a/src/lib/dates.js +++ b/src/lib/dates.js @@ -345,7 +345,9 @@ function includeTime(dateStr, h, m, s, msec10) { // a Date object or milliseconds // optional dflt is the return value if cleaning fails exports.cleanDate = function(v, dflt, calendar) { - if(exports.isJSDate(v) || typeof v === 'number') { + // let us use cleanDate to provide a missing default without an error + if(v === BADNUM) return dflt; + if(exports.isJSDate(v) || (typeof v === 'number' && isFinite(v))) { // do not allow milliseconds (old) or jsdate objects (inherently // described as gregorian dates) with world calendars if(isWorldCalendar(calendar)) { diff --git a/test/jasmine/tests/lib_date_test.js b/test/jasmine/tests/lib_date_test.js index 2a0971afd7b..6bccb8a9865 100644 --- a/test/jasmine/tests/lib_date_test.js +++ b/test/jasmine/tests/lib_date_test.js @@ -391,20 +391,22 @@ describe('dates', function() { errors.push(msg); }); - [ + var cases = [ new Date(-20000, 0, 1), new Date(20000, 0, 1), new Date('fail'), undefined, null, NaN, [], {}, [0], {1: 2}, '', '2001-02-29' // not a leap year - ].forEach(function(v) { + ]; + cases.forEach(function(v) { expect(Lib.cleanDate(v)).toBeUndefined(); if(!isNumeric(+v)) expect(Lib.cleanDate(+v)).toBeUndefined(); expect(Lib.cleanDate(v, '2000-01-01')).toBe('2000-01-01'); }); - expect(errors.length).toBe(16); + // two errors for each case except `undefined` + expect(errors.length).toBe(2 * (cases.length - 1)); }); it('should not alter valid date strings, even to truncate them', function() { From e066bb91c6bc291647604878328f2eeaadffb986 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sun, 23 Sep 2018 15:19:28 -0400 Subject: [PATCH 02/10] fix `legend.traceorder` when all traces are `legendonly` --- src/plots/plots.js | 5 ++++- test/jasmine/tests/bar_test.js | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index 4cc227fb3cb..a6907a04041 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1482,7 +1482,10 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData, trans } // trace module layout defaults - var modules = layoutOut._visibleModules; + // use _modules rather than _visibleModules so that even + // legendonly traces can include settings - eg barmode, which affects + // legend.traceorder default value. + var modules = layoutOut._modules; for(i = 0; i < modules.length; i++) { _module = modules[i]; diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index e7b3f395a1e..dc9ec698751 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -1361,6 +1361,7 @@ describe('bar visibility toggling:', function() { spyOn(gd._fullData[0]._module, 'crossTraceCalc').and.callThrough(); _assert('base', [0.5, 3.5], [-2.222, 2.222], 0); + expect(gd._fullLayout.legend.traceorder).toBe('normal'); return Plotly.restyle(gd, 'visible', false, [1]); }) .then(function() { @@ -1369,6 +1370,11 @@ describe('bar visibility toggling:', function() { }) .then(function() { _assert('both invisible', [0.5, 3.5], [0, 2.105], 0); + return Plotly.restyle(gd, 'visible', 'legendonly'); + }) + .then(function() { + _assert('both legendonly', [0.5, 3.5], [0, 2.105], 0); + expect(gd._fullLayout.legend.traceorder).toBe('normal'); return Plotly.restyle(gd, 'visible', true, [1]); }) .then(function() { @@ -1391,6 +1397,7 @@ describe('bar visibility toggling:', function() { spyOn(gd._fullData[0]._module, 'crossTraceCalc').and.callThrough(); _assert('base', [0.5, 3.5], [0, 5.263], 0); + expect(gd._fullLayout.legend.traceorder).toBe('reversed'); return Plotly.restyle(gd, 'visible', false, [1]); }) .then(function() { @@ -1399,6 +1406,11 @@ describe('bar visibility toggling:', function() { }) .then(function() { _assert('both invisible', [0.5, 3.5], [0, 2.105], 0); + return Plotly.restyle(gd, 'visible', 'legendonly'); + }) + .then(function() { + _assert('both legendonly', [0.5, 3.5], [0, 2.105], 0); + expect(gd._fullLayout.legend.traceorder).toBe('reversed'); return Plotly.restyle(gd, 'visible', true, [1]); }) .then(function() { From ab20fae9a57d7b418accb1af3d93d66f4046a195 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sun, 23 Sep 2018 21:21:45 -0400 Subject: [PATCH 03/10] pull out tick0/dtick validation logic for reuse --- src/plots/cartesian/clean_ticks.js | 87 ++++++++++++++++++++++ src/plots/cartesian/tick_value_defaults.js | 50 ++----------- 2 files changed, 93 insertions(+), 44 deletions(-) create mode 100644 src/plots/cartesian/clean_ticks.js diff --git a/src/plots/cartesian/clean_ticks.js b/src/plots/cartesian/clean_ticks.js new file mode 100644 index 00000000000..a6a51bef9a7 --- /dev/null +++ b/src/plots/cartesian/clean_ticks.js @@ -0,0 +1,87 @@ +/** +* Copyright 2012-2018, 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'; + +var isNumeric = require('fast-isnumeric'); +var Lib = require('../../lib'); +var ONEDAY = require('../../constants/numerical').ONEDAY; + +/** + * Return a validated dtick value for this axis + * + * @param {any} dtick: the candidate dtick. valid values are numbers and strings, + * and further constrained depending on the axis type. + * @param {string} axType: the axis type + */ +exports.dtick = function(dtick, axType) { + var isLog = axType === 'log'; + var isDate = axType === 'date'; + var isCat = axType === 'category'; + var dtickDflt = isDate ? ONEDAY : 1; + + if(!dtick) return dtickDflt; + + if(isNumeric(dtick)) { + dtick = Number(dtick); + if(dtick <= 0) return dtickDflt; + if(isCat) { + // category dtick must be positive integers + return Math.max(1, Math.round(dtick)); + } + if(isDate) { + // date dtick must be at least 0.1ms (our current precision) + return Math.max(0.1, dtick); + } + return dtick; + } + + if(typeof dtick !== 'string' || !(isDate || isLog)) { + return dtickDflt; + } + + var prefix = dtick.charAt(0); + var dtickNum = dtick.substr(1); + dtickNum = isNumeric(dtickNum) ? Number(dtickNum) : 0; + + if((dtickNum <= 0) || !( + // "M" gives ticks every (integer) n months + (isDate && prefix === 'M' && dtickNum === Math.round(dtickNum)) || + // "L" gives ticks linearly spaced in data (not in position) every (float) f + (isLog && prefix === 'L') || + // "D1" gives powers of 10 with all small digits between, "D2" gives only 2 and 5 + (isLog && prefix === 'D' && (dtickNum === 1 || dtickNum === 2)) + )) { + return dtickDflt; + } + + return dtick; +}; + +/** + * Return a validated tick0 for this axis + * + * @param {any} tick0: the candidate tick0. Valid values are numbers and strings, + * further constrained depending on the axis type + * @param {string} axType: the axis type + * @param {string} calendar: for date axes, the calendar to validate/convert with + * @param {any} dtick: an already valid dtick. Only used for D1 and D2 log dticks, + * which do not support tick0 at all. + */ +exports.tick0 = function(tick0, axType, calendar, dtick) { + if(axType === 'date') { + return Lib.cleanDate(tick0, Lib.dateTick0(calendar)); + } + if(dtick === 'D1' || dtick === 'D2') { + // D1 and D2 modes ignore tick0 entirely + return undefined; + } + // Aside from date axes, tick0 must be numeric + return isNumeric(tick0) ? Number(tick0) : 0; +}; diff --git a/src/plots/cartesian/tick_value_defaults.js b/src/plots/cartesian/tick_value_defaults.js index ca3e1aa356d..f5aff20aefa 100644 --- a/src/plots/cartesian/tick_value_defaults.js +++ b/src/plots/cartesian/tick_value_defaults.js @@ -9,9 +9,7 @@ 'use strict'; -var isNumeric = require('fast-isnumeric'); -var Lib = require('../../lib'); -var ONEDAY = require('../../constants/numerical').ONEDAY; +var cleanTicks = require('./clean_ticks'); module.exports = function handleTickValueDefaults(containerIn, containerOut, coerce, axType) { @@ -33,47 +31,11 @@ module.exports = function handleTickValueDefaults(containerIn, containerOut, coe else if(tickmode === 'linear') { // dtick is usually a positive number, but there are some // special strings available for log or date axes - // default is 1 day for dates, otherwise 1 - var dtickDflt = (axType === 'date') ? ONEDAY : 1; - var dtick = coerce('dtick', dtickDflt); - if(isNumeric(dtick)) { - containerOut.dtick = (dtick > 0) ? Number(dtick) : dtickDflt; - } - else if(typeof dtick !== 'string') { - containerOut.dtick = dtickDflt; - } - else { - // date and log special cases are all one character plus a number - var prefix = dtick.charAt(0), - dtickNum = dtick.substr(1); - - dtickNum = isNumeric(dtickNum) ? Number(dtickNum) : 0; - if((dtickNum <= 0) || !( - // "M" gives ticks every (integer) n months - (axType === 'date' && prefix === 'M' && dtickNum === Math.round(dtickNum)) || - // "L" gives ticks linearly spaced in data (not in position) every (float) f - (axType === 'log' && prefix === 'L') || - // "D1" gives powers of 10 with all small digits between, "D2" gives only 2 and 5 - (axType === 'log' && prefix === 'D' && (dtickNum === 1 || dtickNum === 2)) - )) { - containerOut.dtick = dtickDflt; - } - } - - // tick0 can have different valType for different axis types, so - // validate that now. Also for dates, change milliseconds to date strings - var tick0Dflt = (axType === 'date') ? Lib.dateTick0(containerOut.calendar) : 0; - var tick0 = coerce('tick0', tick0Dflt); - if(axType === 'date') { - containerOut.tick0 = Lib.cleanDate(tick0, tick0Dflt); - } - // Aside from date axes, dtick must be numeric; D1 and D2 modes ignore tick0 entirely - else if(isNumeric(tick0) && dtick !== 'D1' && dtick !== 'D2') { - containerOut.tick0 = Number(tick0); - } - else { - containerOut.tick0 = tick0Dflt; - } + // tick0 also has special logic + var dtick = containerOut.dtick = cleanTicks.dtick( + containerIn.dtick, axType); + containerOut.tick0 = cleanTicks.tick0( + containerIn.tick0, axType, containerOut.calendar, dtick); } else { var tickvals = coerce('tickvals'); From a8e48020abfc748713a6c1781518cf4edaa5cad9 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sun, 23 Sep 2018 21:30:45 -0400 Subject: [PATCH 04/10] extend Axes.autoBin to find start/end with fixed size --- src/plots/cartesian/axes.js | 82 +++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index f8d1b4e8369..c2352fcdc33 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -21,6 +21,7 @@ var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); var axAttrs = require('./layout_attributes'); +var cleanTicks = require('./clean_ticks'); var constants = require('../../constants/numerical'); var ONEAVGYEAR = constants.ONEAVGYEAR; @@ -280,43 +281,22 @@ axes.saveShowSpikeInitial = function(gd, overwrite) { return hasOneAxisChanged; }; -axes.autoBin = function(data, ax, nbins, is2d, calendar) { - var dataMin = Lib.aggNums(Math.min, null, data), - dataMax = Lib.aggNums(Math.max, null, data); - - if(!calendar) calendar = ax.calendar; +axes.autoBin = function(data, ax, nbins, is2d, calendar, size) { + var dataMin = Lib.aggNums(Math.min, null, data); + var dataMax = Lib.aggNums(Math.max, null, data); if(ax.type === 'category') { return { start: dataMin - 0.5, end: dataMax + 0.5, - size: 1, + size: Math.max(1, Math.round(size) || 1), _dataSpan: dataMax - dataMin, }; } - var size0; - if(nbins) size0 = ((dataMax - dataMin) / nbins); - else { - // totally auto: scale off std deviation so the highest bin is - // somewhat taller than the total number of bins, but don't let - // the size get smaller than the 'nice' rounded down minimum - // difference between values - var distinctData = Lib.distinctVals(data), - msexp = Math.pow(10, Math.floor( - Math.log(distinctData.minDiff) / Math.LN10)), - minSize = msexp * Lib.roundUp( - distinctData.minDiff / msexp, [0.9, 1.9, 4.9, 9.9], true); - size0 = Math.max(minSize, 2 * Lib.stdev(data) / - Math.pow(data.length, is2d ? 0.25 : 0.4)); - - // fallback if ax.d2c output BADNUMs - // e.g. when user try to plot categorical bins - // on a layout.xaxis.type: 'linear' - if(!isNumeric(size0)) size0 = 1; - } + if(!calendar) calendar = ax.calendar; - // piggyback off autotick code to make "nice" bin sizes + // piggyback off tick code to make "nice" bin sizes and edges var dummyAx; if(ax.type === 'log') { dummyAx = { @@ -333,19 +313,51 @@ axes.autoBin = function(data, ax, nbins, is2d, calendar) { } axes.setConvert(dummyAx); - axes.autoTicks(dummyAx, size0); + size = size && cleanTicks.dtick(size, dummyAx.type); + + if(size) { + dummyAx.dtick = size; + dummyAx.tick0 = cleanTicks.tick0(undefined, dummyAx.type, calendar); + } + else { + var size0; + if(nbins) size0 = ((dataMax - dataMin) / nbins); + else { + // totally auto: scale off std deviation so the highest bin is + // somewhat taller than the total number of bins, but don't let + // the size get smaller than the 'nice' rounded down minimum + // difference between values + var distinctData = Lib.distinctVals(data); + var msexp = Math.pow(10, Math.floor( + Math.log(distinctData.minDiff) / Math.LN10)); + var minSize = msexp * Lib.roundUp( + distinctData.minDiff / msexp, [0.9, 1.9, 4.9, 9.9], true); + size0 = Math.max(minSize, 2 * Lib.stdev(data) / + Math.pow(data.length, is2d ? 0.25 : 0.4)); + + // fallback if ax.d2c output BADNUMs + // e.g. when user try to plot categorical bins + // on a layout.xaxis.type: 'linear' + if(!isNumeric(size0)) size0 = 1; + } + + axes.autoTicks(dummyAx, size0); + } + + + var finalSize = dummyAx.dtick; var binStart = axes.tickIncrement( - axes.tickFirst(dummyAx), dummyAx.dtick, 'reverse', calendar); + axes.tickFirst(dummyAx), finalSize, 'reverse', calendar); var binEnd, bincount; // check for too many data points right at the edges of bins // (>50% within 1% of bin edges) or all data points integral // and offset the bins accordingly - if(typeof dummyAx.dtick === 'number') { + if(typeof finalSize === 'number') { binStart = autoShiftNumericBins(binStart, data, dummyAx, dataMin, dataMax); - bincount = 1 + Math.floor((dataMax - binStart) / dummyAx.dtick); - binEnd = binStart + bincount * dummyAx.dtick; + bincount = 1 + Math.floor((dataMax - binStart) / finalSize); + binEnd = binStart + bincount * finalSize; } else { // month ticks - should be the only nonlinear kind we have at this point. @@ -354,7 +366,7 @@ axes.autoBin = function(data, ax, nbins, is2d, calendar) { // we bin it on a linear axis (which one could argue against, but that's // a separate issue) if(dummyAx.dtick.charAt(0) === 'M') { - binStart = autoShiftMonthBins(binStart, data, dummyAx.dtick, dataMin, calendar); + binStart = autoShiftMonthBins(binStart, data, finalSize, dataMin, calendar); } // calculate the endpoint for nonlinear ticks - you have to @@ -362,7 +374,7 @@ axes.autoBin = function(data, ax, nbins, is2d, calendar) { binEnd = binStart; bincount = 0; while(binEnd <= dataMax) { - binEnd = axes.tickIncrement(binEnd, dummyAx.dtick, false, calendar); + binEnd = axes.tickIncrement(binEnd, finalSize, false, calendar); bincount++; } } @@ -370,7 +382,7 @@ axes.autoBin = function(data, ax, nbins, is2d, calendar) { return { start: ax.c2r(binStart, 0, calendar), end: ax.c2r(binEnd, 0, calendar), - size: dummyAx.dtick, + size: finalSize, _dataSpan: dataMax - dataMin }; }; From 536ce480be5802c92c8c53d386ae8ce1c6a506ea Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 24 Sep 2018 16:28:51 -0400 Subject: [PATCH 05/10] new histogram autobin algo --- src/plot_api/helpers.js | 13 + src/plot_api/plot_api.js | 33 ++- src/plots/plots.js | 21 +- src/traces/bar/layout_defaults.js | 2 +- src/traces/histogram/attributes.js | 87 +++---- src/traces/histogram/bin_defaults.js | 32 --- src/traces/histogram/calc.js | 253 ++++++++++---------- src/traces/histogram/clean_bins.js | 78 ------ src/traces/histogram/clean_data.js | 112 +++++++++ src/traces/histogram/defaults.js | 4 +- src/traces/histogram/index.js | 1 + src/traces/histogram2d/attributes.js | 8 +- src/traces/histogram2d/calc.js | 59 +++-- src/traces/histogram2d/clean_data.js | 93 +++++++ src/traces/histogram2d/index.js | 1 + src/traces/histogram2d/sample_defaults.js | 6 +- src/traces/histogram2dcontour/attributes.js | 8 +- src/traces/histogram2dcontour/index.js | 1 + test/jasmine/tests/axes_test.js | 2 +- test/jasmine/tests/histogram2d_test.js | 73 +++--- test/jasmine/tests/histogram_test.js | 194 +++++++++++---- test/jasmine/tests/plot_api_test.js | 25 +- 22 files changed, 694 insertions(+), 412 deletions(-) delete mode 100644 src/traces/histogram/bin_defaults.js delete mode 100644 src/traces/histogram/clean_bins.js create mode 100644 src/traces/histogram/clean_data.js create mode 100644 src/traces/histogram2d/clean_data.js diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js index 8a49ae911ae..6a4208e1cde 100644 --- a/src/plot_api/helpers.js +++ b/src/plot_api/helpers.js @@ -386,6 +386,19 @@ exports.cleanData = function(data) { // sanitize rgb(fractions) and rgba(fractions) that old tinycolor // supported, but new tinycolor does not because they're not valid css Color.clean(trace); + + // remove obsolete autobin(x|y) attributes, but only if true + // if false, this needs to happen in Histogram.calc because it + // can be a one-time autobin so we need to know the results before + // we can push them back into the trace. + if(trace.autobinx) { + delete trace.autobinx; + delete trace.xbins; + } + if(trace.autobiny) { + delete trace.autobiny; + delete trace.ybins; + } } }; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 9bd8007dfc2..97014e69014 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1434,6 +1434,18 @@ function _restyle(gd, aobj, traces) { } } + function allBins(binAttr) { + return function(j) { + return fullData[j][binAttr]; + }; + } + + function arrayBins(binAttr) { + return function(vij, j) { + return vij === false ? fullData[traces[j]][binAttr] : null; + }; + } + // now make the changes to gd.data (and occasionally gd.layout) // and figure out what kind of graphics update we need to do for(var ai in aobj) { @@ -1449,6 +1461,17 @@ function _restyle(gd, aobj, traces) { newVal, valObject; + // Backward compatibility shim for turning histogram autobin on, + // or freezing previous autobinned values. + // Replace obsolete `autobin(x|y): true` with `(x|y)bins: null` + // and `autobin(x|y): false` with the `(x|y)bins` in `fullData` + if(ai === 'autobinx' || ai === 'autobiny') { + ai = ai.charAt(ai.length - 1) + 'bins'; + if(Array.isArray(vi)) vi = vi.map(arrayBins(ai)); + else if(vi === false) vi = traces.map(allBins(ai)); + else vi = null; + } + redoit[ai] = vi; if(ai.substr(0, 6) === 'LAYOUT') { @@ -1609,8 +1632,12 @@ function _restyle(gd, aobj, traces) { } } - // major enough changes deserve autoscale, autobin, and + // Major enough changes deserve autoscale and // non-reversed axes so people don't get confused + // + // Note: autobin (or its new analog bin clearing) is not included here + // since we're not pushing bins back to gd.data, so if we have bin + // info it was explicitly provided by the user. if(['orientation', 'type'].indexOf(ai) !== -1) { axlist = []; for(i = 0; i < traces.length; i++) { @@ -1619,10 +1646,6 @@ function _restyle(gd, aobj, traces) { if(Registry.traceIs(trace, 'cartesian')) { addToAxlist(trace.xaxis || 'x'); addToAxlist(trace.yaxis || 'y'); - - if(ai === 'type') { - doextra(['autobinx', 'autobiny'], true, i); - } } } diff --git a/src/plots/plots.js b/src/plots/plots.js index a6907a04041..6c10a889201 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -428,13 +428,6 @@ plots.supplyDefaults = function(gd, opts) { // attach helper method to check whether a plot type is present on graph newFullLayout._has = plots._hasPlotType.bind(newFullLayout); - // special cases that introduce interactions between traces - var _modules = newFullLayout._visibleModules; - for(i = 0; i < _modules.length; i++) { - var _module = _modules[i]; - if(_module.cleanData) _module.cleanData(newFullData); - } - if(oldFullData.length === newFullData.length) { for(i = 0; i < newFullData.length; i++) { relinkPrivateKeys(newFullData[i], oldFullData[i]); @@ -444,6 +437,20 @@ plots.supplyDefaults = function(gd, opts) { // finally, fill in the pieces of layout that may need to look at data plots.supplyLayoutModuleDefaults(newLayout, newFullLayout, newFullData, gd._transitionData); + // Special cases that introduce interactions between traces. + // This is after relinkPrivateKeys so we can use those in cleanData + // and after layout module defaults, so we can use eg barmode + var _modules = newFullLayout._visibleModules; + var cleanDataFuncs = []; + for(i = 0; i < _modules.length; i++) { + var _module = _modules[i]; + // some trace types share cleanData (ie histogram2d, histogram2dcontour) + if(_module.cleanData) Lib.pushUnique(cleanDataFuncs, _module.cleanData); + } + for(i = 0; i < cleanDataFuncs.length; i++) { + cleanDataFuncs[i](newFullData, newFullLayout); + } + // turn on flag to optimize large splom-only graphs // mostly by omitting SVG layers during Cartesian.drawFramework newFullLayout._hasOnlyLargeSploms = ( diff --git a/src/traces/bar/layout_defaults.js b/src/traces/bar/layout_defaults.js index 274b0696282..4809ce1d035 100644 --- a/src/traces/bar/layout_defaults.js +++ b/src/traces/bar/layout_defaults.js @@ -28,7 +28,7 @@ module.exports = function(layoutIn, layoutOut, fullData) { for(var i = 0; i < fullData.length; i++) { var trace = fullData[i]; - if(Registry.traceIs(trace, 'bar')) hasBars = true; + if(Registry.traceIs(trace, 'bar') && trace.visible) hasBars = true; else continue; // if we have at least 2 grouped bar traces on the same subplot, diff --git a/src/traces/histogram/attributes.js b/src/traces/histogram/attributes.js index f15e85b6fc2..bd9e3df112b 100644 --- a/src/traces/histogram/attributes.js +++ b/src/traces/histogram/attributes.js @@ -125,24 +125,6 @@ module.exports = { }, editType: 'calc' }, - - autobinx: { - valType: 'boolean', - dflt: null, - role: 'style', - editType: 'calc', - impliedEdits: { - 'xbins.start': undefined, - 'xbins.end': undefined, - 'xbins.size': undefined - }, - description: [ - 'Determines whether or not the x axis bin attributes are picked', - 'by an algorithm. Note that this should be set to false if you', - 'want to manually set the number of bins using the attributes in', - 'xbins.' - ].join(' ') - }, nbinsx: { valType: 'integer', min: 0, @@ -152,28 +134,12 @@ module.exports = { description: [ 'Specifies the maximum number of desired bins. This value will be used', 'in an algorithm that will decide the optimal bin size such that the', - 'histogram best visualizes the distribution of the data.' + 'histogram best visualizes the distribution of the data.', + 'Ignored if `xbins.size` is provided.' ].join(' ') }, xbins: makeBinsAttr('x'), - autobiny: { - valType: 'boolean', - dflt: null, - role: 'style', - editType: 'calc', - impliedEdits: { - 'ybins.start': undefined, - 'ybins.end': undefined, - 'ybins.size': undefined - }, - description: [ - 'Determines whether or not the y axis bin attributes are picked', - 'by an algorithm. Note that this should be set to false if you', - 'want to manually set the number of bins using the attributes in', - 'ybins.' - ].join(' ') - }, nbinsy: { valType: 'integer', min: 0, @@ -183,7 +149,8 @@ module.exports = { description: [ 'Specifies the maximum number of desired bins. This value will be used', 'in an algorithm that will decide the optimal bin size such that the', - 'histogram best visualizes the distribution of the data.' + 'histogram best visualizes the distribution of the data.', + 'Ignored if `ybins.size` is provided.' ].join(' ') }, ybins: makeBinsAttr('y'), @@ -194,23 +161,46 @@ module.exports = { unselected: barAttrs.unselected, _deprecated: { - bardir: barAttrs._deprecated.bardir + bardir: barAttrs._deprecated.bardir, + autobinx: { + valType: 'boolean', + dflt: null, + role: 'style', + editType: 'calc', + impliedEdits: { + 'xbins.start': undefined, + 'xbins.end': undefined, + 'xbins.size': undefined + }, + description: [ + 'Obsolete: since v1.42 each bin', + 'attribute is auto-determined separately.' + ].join(' ') + }, + autobiny: { + valType: 'boolean', + dflt: null, + role: 'style', + editType: 'calc', + impliedEdits: { + 'ybins.start': undefined, + 'ybins.end': undefined, + 'ybins.size': undefined + }, + description: [ + 'Obsolete: since v1.42 each bin', + 'attribute is auto-determined separately.' + ].join(' ') + } } }; function makeBinsAttr(axLetter) { - var impliedEdits = {}; - impliedEdits['autobin' + axLetter] = false; - var impliedEditsInner = {}; - impliedEditsInner['^autobin' + axLetter] = false; - return { start: { valType: 'any', // for date axes - dflt: null, role: 'style', editType: 'calc', - impliedEdits: impliedEditsInner, description: [ 'Sets the starting value for the', axLetter, 'axis bins.' @@ -218,10 +208,8 @@ function makeBinsAttr(axLetter) { }, end: { valType: 'any', // for date axes - dflt: null, role: 'style', editType: 'calc', - impliedEdits: impliedEditsInner, description: [ 'Sets the end value for the', axLetter, 'axis bins.' @@ -229,16 +217,13 @@ function makeBinsAttr(axLetter) { }, size: { valType: 'any', // for date axes - dflt: null, role: 'style', editType: 'calc', - impliedEdits: impliedEditsInner, description: [ 'Sets the step in-between value each', axLetter, 'axis bin.' ].join(' ') }, - editType: 'calc', - impliedEdits: impliedEdits + editType: 'calc' }; } diff --git a/src/traces/histogram/bin_defaults.js b/src/traces/histogram/bin_defaults.js deleted file mode 100644 index 77259579edd..00000000000 --- a/src/traces/histogram/bin_defaults.js +++ /dev/null @@ -1,32 +0,0 @@ -/** -* Copyright 2012-2018, 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 = function handleBinDefaults(traceIn, traceOut, coerce, binDirections) { - coerce('histnorm'); - - binDirections.forEach(function(binDirection) { - /* - * Because date axes have string values for start and end, - * and string options for size, we cannot validate these attributes - * now. We will do this during calc (immediately prior to binning) - * in ./clean_bins, and push the cleaned values back to _fullData. - */ - coerce(binDirection + 'bins.start'); - coerce(binDirection + 'bins.end'); - coerce(binDirection + 'bins.size'); - - var autobin = coerce('autobin' + binDirection); - if(autobin !== false) coerce('nbins' + binDirection); - }); - - return traceOut; -}; diff --git a/src/traces/histogram/calc.js b/src/traces/histogram/calc.js index 7d4659719c2..5b66465a5f5 100644 --- a/src/traces/histogram/calc.js +++ b/src/traces/histogram/calc.js @@ -18,8 +18,6 @@ var arraysToCalcdata = require('../bar/arrays_to_calcdata'); var binFunctions = require('./bin_functions'); var normFunctions = require('./norm_functions'); var doAvg = require('./average'); -var cleanBins = require('./clean_bins'); -var oneMonth = require('../../constants/numerical').ONEAVGMONTH; var getBinSpanLabelRound = require('./bin_label_vals'); module.exports = function calc(gd, trace) { @@ -38,8 +36,6 @@ module.exports = function calc(gd, trace) { var cumulativeSpec = trace.cumulative; var i; - cleanBins(trace, pa, mainData); - var binsAndPos = calcAllAutoBins(gd, trace, pa, mainData); var binSpec = binsAndPos[0]; var pos0 = binsAndPos[1]; @@ -217,8 +213,26 @@ module.exports = function calc(gd, trace) { */ function calcAllAutoBins(gd, trace, pa, mainData, _overlayEdgeCase) { var binAttr = mainData + 'bins'; - var isOverlay = gd._fullLayout.barmode === 'overlay'; - var i, tracei, calendar, firstManual, pos0; + var fullLayout = gd._fullLayout; + var isOverlay = fullLayout.barmode === 'overlay'; + var i, traces, tracei, calendar, pos0, autoVals, cumulativeSpec; + + var cleanBound = (pa.type === 'date') ? + function(v) { return (v || v === 0) ? Lib.cleanDate(v, null, pa.calendar) : null; } : + function(v) { return isNumeric(v) ? Number(v) : null; }; + + function setBound(attr, bins, newBins) { + if(bins[attr + 'Found']) { + bins[attr] = cleanBound(bins[attr]); + if(bins[attr] === null) bins[attr] = newBins[attr]; + } + else { + autoVals[attr] = bins[attr] = newBins[attr]; + Lib.nestedProperty(traces[0], binAttr + '.' + attr).set(newBins[attr]); + } + } + + var binOpts = fullLayout._histogramBinOpts[trace._groupName]; // all but the first trace in this group has already been marked finished // clear this flag, so next time we run calc we will run autobin again @@ -226,121 +240,131 @@ function calcAllAutoBins(gd, trace, pa, mainData, _overlayEdgeCase) { delete trace._autoBinFinished; } else { - // must be the first trace in the group - do the autobinning on them all - - // find all grouped traces - in overlay mode each trace is independent - var traceGroup = isOverlay ? [trace] : getConnectedHistograms(gd, trace); - var autoBinnedTraces = []; - - var minSize = Infinity; - var minStart = Infinity; - var maxEnd = -Infinity; - - var autoBinAttr = 'autobin' + mainData; - - for(i = 0; i < traceGroup.length; i++) { - tracei = traceGroup[i]; - - // stash pos0 on the trace so we don't need to duplicate this - // in the main body of calc + traces = binOpts.traces; + var sizeFound = binOpts.sizeFound; + var allPos = []; + autoVals = traces[0]._autoBin = {}; + // Note: we're including `legendonly` traces here for autobin purposes, + // so that showing & hiding from the legend won't affect bins. + // But this complicates things a bit since those traces don't `calc`, + // hence `isFirstVisible`. + var isFirstVisible = true; + for(i = 0; i < traces.length; i++) { + tracei = traces[i]; pos0 = tracei._pos0 = pa.makeCalcdata(tracei, mainData); - var binSpec = tracei[binAttr]; - - if((tracei[autoBinAttr]) || !binSpec || - binSpec.start === null || binSpec.end === null) { - calendar = tracei[mainData + 'calendar']; - var cumulativeSpec = tracei.cumulative; - - binSpec = Axes.autoBin(pos0, pa, tracei['nbins' + mainData], false, calendar); - - // Edge case: single-valued histogram overlaying others - // Use them all together to calculate the bin size for the single-valued one - if(isOverlay && binSpec._dataSpan === 0 && pa.type !== 'category') { - // Several single-valued histograms! Stop infinite recursion, - // just return an extra flag that tells handleSingleValueOverlays - // to sort out this trace too - if(_overlayEdgeCase) return [binSpec, pos0, true]; - - binSpec = handleSingleValueOverlays(gd, trace, pa, mainData, binAttr); + allPos = allPos.concat(pos0); + delete tracei._autoBinFinished; + if(trace.visible === true) { + if(isFirstVisible) { + isFirstVisible = false; } - - // adjust for CDF edge cases - if(cumulativeSpec.enabled && (cumulativeSpec.currentbin !== 'include')) { - if(cumulativeSpec.direction === 'decreasing') { - minStart = Math.min(minStart, pa.r2c(binSpec.start, 0, calendar) - binSpec.size); - } - else { - maxEnd = Math.max(maxEnd, pa.r2c(binSpec.end, 0, calendar) + binSpec.size); - } + else { + delete tracei._autoBin; + tracei._autoBinFinished = 1; } + } + } + calendar = traces[0][mainData + 'calendar']; + var newBinSpec = Axes.autoBin( + allPos, pa, binOpts.nbins, false, calendar, sizeFound && binOpts.size); + + // Edge case: single-valued histogram overlaying others + // Use them all together to calculate the bin size for the single-valued one + if(isOverlay && newBinSpec._dataSpan === 0 && pa.type !== 'category') { + // Several single-valued histograms! Stop infinite recursion, + // just return an extra flag that tells handleSingleValueOverlays + // to sort out this trace too + if(_overlayEdgeCase) return [newBinSpec, pos0, true]; + + newBinSpec = handleSingleValueOverlays(gd, trace, pa, mainData, binAttr); + } - // note that it's possible to get here with an explicit autobin: false - // if the bins were not specified. mark this trace for followup - autoBinnedTraces.push(tracei); + // adjust for CDF edge cases + cumulativeSpec = tracei.cumulative; + if(cumulativeSpec.enabled && (cumulativeSpec.currentbin !== 'include')) { + if(cumulativeSpec.direction === 'decreasing') { + newBinSpec.start = pa.c2r(Axes.tickIncrement( + pa.r2c(newBinSpec.start, 0, calendar), + newBinSpec.size, true, calendar + )); } - else if(!firstManual) { - // Remember the first manually set binSpec. We'll try to be extra - // accommodating of this one, so other bins line up with these. - // But if there's more than one manual bin set and they're mutually - // inconsistent, then there's not much we can do... - firstManual = { - size: binSpec.size, - start: pa.r2c(binSpec.start, 0, calendar), - end: pa.r2c(binSpec.end, 0, calendar) - }; + else { + newBinSpec.end = pa.c2r(Axes.tickIncrement( + pa.r2c(newBinSpec.end, 0, calendar), + newBinSpec.size, false, calendar + )); } - - // Even non-autobinned traces get included here, so we get the greatest extent - // and minimum bin size of them all. - // But manually binned traces won't be adjusted, even if the auto values - // are inconsistent with the manual ones (or the manual ones are inconsistent - // with each other). - minSize = getMinSize(minSize, binSpec.size); - minStart = Math.min(minStart, pa.r2c(binSpec.start, 0, calendar)); - maxEnd = Math.max(maxEnd, pa.r2c(binSpec.end, 0, calendar)); - - // add the flag that lets us abort autobin on later traces - if(i) tracei._autoBinFinished = 1; } - // do what we can to match the auto bins to the first manual bins - // but only if sizes are all numeric - if(firstManual && isNumeric(firstManual.size) && isNumeric(minSize)) { - // first need to ensure the bin size is the same as or an integer fraction - // of the first manual bin - // allow the bin size to increase just under the autobin step size to match, - // (which is a factor of 2 or 2.5) otherwise shrink it - if(minSize > firstManual.size / 1.9) minSize = firstManual.size; - else minSize = firstManual.size / Math.ceil(firstManual.size / minSize); - - // now decrease minStart if needed to make the bin centers line up - var adjustedFirstStart = firstManual.start + (firstManual.size - minSize) / 2; - minStart = adjustedFirstStart - minSize * Math.ceil((adjustedFirstStart - minStart) / minSize); + binOpts.size = newBinSpec.size; + if(!sizeFound) { + autoVals.size = newBinSpec.size; + Lib.nestedProperty(traces[0], binAttr + '.size').set(newBinSpec.size); } - // now go back to the autobinned traces and update their bin specs with the final values - for(i = 0; i < autoBinnedTraces.length; i++) { - tracei = autoBinnedTraces[i]; - calendar = tracei[mainData + 'calendar']; + setBound('start', binOpts, newBinSpec); + setBound('end', binOpts, newBinSpec); + } - tracei._input[binAttr] = tracei[binAttr] = { - start: pa.c2r(minStart, 0, calendar), - end: pa.c2r(maxEnd, 0, calendar), - size: minSize - }; + pos0 = trace._pos0; + delete trace._pos0; - // 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. - tracei._input[autoBinAttr] = tracei[autoBinAttr]; + // Each trace can specify its own start/end, or if omitted + // we ensure they're beyond the bounds of this trace's data, + // and we need to make sure start is aligned with the main start + var traceInputBins = trace._input[binAttr] || {}; + var traceBinOptsCalc = Lib.extendFlat({}, binOpts); + var mainStart = binOpts.start; + var startIn = pa.r2l(traceInputBins.start); + var hasStart = startIn !== undefined; + if((binOpts.startFound || hasStart) && startIn !== pa.r2l(mainStart)) { + // We have an explicit start to reconcile across traces + // if this trace has an explicit start, shift it down to a bin edge + // if another trace had an explicit start, shift it down to a + // bin edge past our data + var traceStart = hasStart ? + startIn : + Lib.aggNums(Math.min, null, pos0); + + var dummyAx = { + type: pa.type === 'category' ? 'linear' : pa.type, + r2l: pa.r2l, + dtick: binOpts.size, + tick0: mainStart, + calendar: calendar, + range: ([traceStart, Axes.tickIncrement(traceStart, binOpts.size, false, calendar)]).map(pa.l2r) + }; + var newStart = Axes.tickFirst(dummyAx); + if(newStart > pa.r2l(traceStart)) { + newStart = Axes.tickIncrement(newStart, binOpts.size, true, calendar); } + traceBinOptsCalc.start = pa.l2r(newStart); + if(!hasStart) Lib.nestedProperty(trace, binAttr + '.start').set(traceBinOptsCalc.start); } - pos0 = trace._pos0; - delete trace._pos0; + var mainEnd = binOpts.end; + var endIn = pa.r2l(traceInputBins.end); + var hasEnd = endIn !== undefined; + if((binOpts.endFound || hasEnd) && endIn !== pa.r2l(mainEnd)) { + // Reconciling an explicit end is easier, as it doesn't need to + // match bin edges + var traceEnd = hasEnd ? + endIn : + Lib.aggNums(Math.max, null, pos0); + + traceBinOptsCalc.end = pa.l2r(traceEnd); + if(!hasEnd) Lib.nestedProperty(trace, binAttr + '.start').set(traceBinOptsCalc.end); + } - return [trace[binAttr], pos0]; + // Backward compatibility for one-time autobinning. + // autobin: true is handled in cleanData, but autobin: false + // needs to be here where we have determined the values. + if(trace._input['autobin' + mainData] === false) { + trace._input[binAttr] = Lib.extendFlat({}, trace[binAttr] || {}); + delete trace._input['autobin' + mainData]; + } + + return [traceBinOptsCalc, pos0]; } /* @@ -449,25 +473,6 @@ function getConnectedHistograms(gd, trace) { } -/* - * getMinSize: find the smallest given that size can be a string code - * ie 'M6' for 6 months. ('L' wouldn't make sense to compare with numeric sizes) - */ -function getMinSize(size1, size2) { - if(size1 === Infinity) return size2; - var sizeNumeric1 = numericSize(size1); - var sizeNumeric2 = numericSize(size2); - return sizeNumeric2 < sizeNumeric1 ? size2 : size1; -} - -function numericSize(size) { - if(isNumeric(size)) return size; - if(typeof size === 'string' && size.charAt(0) === 'M') { - return oneMonth * +(size.substr(1)); - } - return Infinity; -} - function cdf(size, direction, currentBin) { var i, vi, prevSum; diff --git a/src/traces/histogram/clean_bins.js b/src/traces/histogram/clean_bins.js deleted file mode 100644 index dc322d7401d..00000000000 --- a/src/traces/histogram/clean_bins.js +++ /dev/null @@ -1,78 +0,0 @@ -/** -* Copyright 2012-2018, 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'; -var isNumeric = require('fast-isnumeric'); -var cleanDate = require('../../lib').cleanDate; -var constants = require('../../constants/numerical'); -var ONEDAY = constants.ONEDAY; -var BADNUM = constants.BADNUM; - -/* - * cleanBins: validate attributes autobin[xy] and [xy]bins.(start, end, size) - * Mutates trace so all these attributes are valid. - * - * Normally this kind of thing would happen during supplyDefaults, but - * in this case we need to know the axis type, and axis type isn't set until - * after trace supplyDefaults are completed. So this gets called during the - * calc step, when data are inserted into bins. - */ -module.exports = function cleanBins(trace, ax, binDirection) { - var axType = ax.type, - binAttr = binDirection + 'bins', - bins = trace[binAttr]; - - if(!bins) bins = trace[binAttr] = {}; - - var cleanBound = (axType === 'date') ? - function(v) { return (v || v === 0) ? cleanDate(v, BADNUM, bins.calendar) : null; } : - function(v) { return isNumeric(v) ? Number(v) : null; }; - - bins.start = cleanBound(bins.start); - bins.end = cleanBound(bins.end); - - // logic for bin size is very similar to dtick (cartesian/tick_value_defaults) - // but without the extra string options for log axes - // ie the only strings we accept are M for months - var sizeDflt = (axType === 'date') ? ONEDAY : 1, - binSize = bins.size; - - if(isNumeric(binSize)) { - bins.size = (binSize > 0) ? Number(binSize) : sizeDflt; - } - else if(typeof binSize !== 'string') { - bins.size = sizeDflt; - } - else { - // date special case: "M" gives bins every (integer) n months - var prefix = binSize.charAt(0), - sizeNum = binSize.substr(1); - - sizeNum = isNumeric(sizeNum) ? Number(sizeNum) : 0; - if((sizeNum <= 0) || !( - axType === 'date' && prefix === 'M' && sizeNum === Math.round(sizeNum) - )) { - bins.size = sizeDflt; - } - } - - var autoBinAttr = 'autobin' + binDirection; - - if(typeof trace[autoBinAttr] !== 'boolean') { - trace[autoBinAttr] = trace._fullInput[autoBinAttr] = trace._input[autoBinAttr] = !( - (bins.start || bins.start === 0) && - (bins.end || bins.end === 0) - ); - } - - if(!trace[autoBinAttr]) { - delete trace['nbins' + binDirection]; - delete trace._fullInput['nbins' + binDirection]; - } -}; diff --git a/src/traces/histogram/clean_data.js b/src/traces/histogram/clean_data.js new file mode 100644 index 00000000000..90dfb33b47b --- /dev/null +++ b/src/traces/histogram/clean_data.js @@ -0,0 +1,112 @@ +/** +* Copyright 2012-2018, 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'; + +var Lib = require('../../lib'); +var nestedProperty = Lib.nestedProperty; + +var attributes = require('./attributes'); + +var BINATTRS = { + x: [ + {aStr: 'xbins.start', name: 'start'}, + {aStr: 'xbins.end', name: 'end'}, + {aStr: 'xbins.size', name: 'size'}, + {aStr: 'nbinsx', name: 'nbins'} + ], + y: [ + {aStr: 'ybins.start', name: 'start'}, + {aStr: 'ybins.end', name: 'end'}, + {aStr: 'ybins.size', name: 'size'}, + {aStr: 'nbinsy', name: 'nbins'} + ] +}; + +// handle bin attrs and relink auto-determined values so fullData is complete +module.exports = function cleanData(fullData, fullLayout) { + var allBinOpts = fullLayout._histogramBinOpts = {}; + var isOverlay = fullLayout.barmode === 'overlay'; + var i, j, traceOut, traceIn, binDirection, group, binOpts; + + function coerce(attr) { + return Lib.coerce(traceOut._input, traceOut, attributes, attr); + } + + for(i = 0; i < fullData.length; i++) { + traceOut = fullData[i]; + if(traceOut.type !== 'histogram') continue; + + // TODO: this shouldn't be relinked as it's only used within calc + // https://github.com/plotly/plotly.js/issues/749 + delete traceOut._autoBinFinished; + + binDirection = traceOut.orientation === 'v' ? 'x' : 'y'; + // in overlay mode make a separate group for each trace + // otherwise collect all traces of the same subplot & orientation + group = isOverlay ? traceOut.uid : (traceOut.xaxis + traceOut.yaxis + binDirection); + traceOut._groupName = group; + + binOpts = allBinOpts[group]; + + if(binOpts) { + binOpts.traces.push(traceOut); + } + else { + binOpts = allBinOpts[group] = { + traces: [traceOut], + direction: binDirection + }; + } + } + + for(group in allBinOpts) { + binOpts = allBinOpts[group]; + binDirection = binOpts.direction; + var attrs = BINATTRS[binDirection]; + for(j = 0; j < attrs.length; j++) { + var attrSpec = attrs[j]; + var attr = attrSpec.name; + + // nbins(x|y) is moot if we have a size. This depends on + // nbins coming after size in binAttrs. + if(attr === 'nbins' && binOpts.sizeFound) continue; + + var aStr = attrSpec.aStr; + for(i = 0; i < binOpts.traces.length; i++) { + traceOut = binOpts.traces[i]; + traceIn = traceOut._input; + if(nestedProperty(traceIn, aStr).get() !== undefined) { + binOpts[attr] = coerce(aStr); + binOpts[attr + 'Found'] = true; + break; + } + var autoVals = traceOut._autoBin; + if(autoVals && autoVals[attr]) { + // if this is the *first* autoval + nestedProperty(traceOut, aStr).set(autoVals[attr]); + } + } + // start and end we need to coerce anyway, after having collected the + // first of each into binOpts, in case a trace wants to restrict its + // data to a certain range + if(attr === 'start' || attr === 'end') { + for(; i < binOpts.traces.length; i++) { + traceOut = binOpts.traces[i]; + coerce(aStr, (traceOut._autoBin || {})[attr]); + } + } + + if(attr === 'nbins' && !binOpts.sizeFound && !binOpts.nbinsFound) { + traceOut = binOpts.traces[0]; + binOpts[attr] = coerce(aStr); + } + } + } +}; diff --git a/src/traces/histogram/defaults.js b/src/traces/histogram/defaults.js index b56b451311a..296f8834bb6 100644 --- a/src/traces/histogram/defaults.js +++ b/src/traces/histogram/defaults.js @@ -13,7 +13,6 @@ var Registry = require('../../registry'); var Lib = require('../../lib'); var Color = require('../../components/color'); -var handleBinDefaults = require('./bin_defaults'); var handleStyleDefaults = require('../bar/style_defaults'); var attributes = require('./attributes'); @@ -51,8 +50,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var hasAggregationData = traceOut[aggLetter]; if(hasAggregationData) coerce('histfunc'); + coerce('histnorm'); - handleBinDefaults(traceIn, traceOut, coerce, [sampleLetter]); + // Note: bin defaults are now handled in Histogram.cleanData handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout); diff --git a/src/traces/histogram/index.js b/src/traces/histogram/index.js index 25fc27a227e..e1469215b67 100644 --- a/src/traces/histogram/index.js +++ b/src/traces/histogram/index.js @@ -28,6 +28,7 @@ var Histogram = {}; Histogram.attributes = require('./attributes'); Histogram.layoutAttributes = require('../bar/layout_attributes'); Histogram.supplyDefaults = require('./defaults'); +Histogram.cleanData = require('./clean_data'); Histogram.supplyLayoutDefaults = require('../bar/layout_defaults'); Histogram.calc = require('./calc'); Histogram.crossTraceCalc = require('../bar/cross_trace_calc').crossTraceCalc; diff --git a/src/traces/histogram2d/attributes.js b/src/traces/histogram2d/attributes.js index 7d166f3daf1..c8a208ce5a6 100644 --- a/src/traces/histogram2d/attributes.js +++ b/src/traces/histogram2d/attributes.js @@ -36,17 +36,19 @@ module.exports = extendFlat( histnorm: histogramAttrs.histnorm, histfunc: histogramAttrs.histfunc, - autobinx: histogramAttrs.autobinx, nbinsx: histogramAttrs.nbinsx, xbins: histogramAttrs.xbins, - autobiny: histogramAttrs.autobiny, nbinsy: histogramAttrs.nbinsy, ybins: histogramAttrs.ybins, xgap: heatmapAttrs.xgap, ygap: heatmapAttrs.ygap, zsmooth: heatmapAttrs.zsmooth, - zhoverformat: heatmapAttrs.zhoverformat + zhoverformat: heatmapAttrs.zhoverformat, + _deprecated: { + autobinx: histogramAttrs._deprecated.autobinx, + autobiny: histogramAttrs._deprecated.autobiny + } }, colorscaleAttrs('', { cLetter: 'z', diff --git a/src/traces/histogram2d/calc.js b/src/traces/histogram2d/calc.js index bc3c91294fe..c345c904e5f 100644 --- a/src/traces/histogram2d/calc.js +++ b/src/traces/histogram2d/calc.js @@ -15,7 +15,6 @@ var Axes = require('../../plots/cartesian/axes'); var binFunctions = require('../histogram/bin_functions'); var normFunctions = require('../histogram/norm_functions'); var doAvg = require('../histogram/average'); -var cleanBins = require('../histogram/clean_bins'); var getBinSpanLabelRound = require('../histogram/bin_label_vals'); @@ -38,8 +37,8 @@ module.exports = function calc(gd, trace) { if(y.length > serieslen) y.splice(serieslen, y.length - serieslen); // calculate the bins - cleanAndAutobin(trace, 'x', x, xa, xr2c, xc2r, xcalendar); - cleanAndAutobin(trace, 'y', y, ya, yr2c, yc2r, ycalendar); + doAutoBin(trace, 'x', x, xa, xr2c, xc2r, xcalendar); + doAutoBin(trace, 'y', y, ya, yr2c, yc2r, ycalendar); // make the empty bin array & scale the map var z = []; @@ -182,31 +181,47 @@ module.exports = function calc(gd, trace) { }; }; -function cleanAndAutobin(trace, axLetter, data, ax, r2c, c2r, calendar) { - var binSpecAttr = axLetter + 'bins'; - var autoBinAttr = 'autobin' + axLetter; - var binSpec = trace[binSpecAttr]; +function doAutoBin(trace, axLetter, data, ax, r2c, c2r, calendar) { + var binAttr = axLetter + 'bins'; + var binSpec = trace[binAttr]; + if(!binSpec) binSpec = trace[binAttr] = {}; + var inputBinSpec = trace._input[binAttr] || {}; + var autoBin = trace._autoBin = {}; - cleanBins(trace, ax, axLetter); + // clear out any previously added autobin info + if(!inputBinSpec.size) delete binSpec.size; + if(inputBinSpec.start === undefined) delete binSpec.start; + if(inputBinSpec.end === undefined) delete binSpec.end; - if(trace[autoBinAttr] || !binSpec || binSpec.start === null || binSpec.end === null) { - binSpec = Axes.autoBin(data, ax, trace['nbins' + axLetter], '2d', calendar); + var autoSize = !binSpec.size; + var autoStart = binSpec.start === undefined; + var autoEnd = binSpec.end === undefined; + + if(autoSize || autoStart || autoEnd) { + var newBinSpec = Axes.autoBin(data, ax, trace['nbins' + axLetter], '2d', calendar, binSpec.size); if(trace.type === 'histogram2dcontour') { - // the "true" last argument reverses the tick direction (which we can't + // the "true" 2nd argument reverses the tick direction (which we can't // just do with a minus sign because of month bins) - binSpec.start = c2r(Axes.tickIncrement( - r2c(binSpec.start), binSpec.size, true, calendar)); - binSpec.end = c2r(Axes.tickIncrement( - r2c(binSpec.end), binSpec.size, false, calendar)); + if(autoStart) { + newBinSpec.start = c2r(Axes.tickIncrement( + r2c(newBinSpec.start), newBinSpec.size, true, calendar)); + } + if(autoEnd) { + newBinSpec.end = c2r(Axes.tickIncrement( + r2c(newBinSpec.end), newBinSpec.size, false, calendar)); + } } + if(autoSize) binSpec.size = autoBin.size = newBinSpec.size; + if(autoStart) binSpec.start = autoBin.start = newBinSpec.start; + if(autoEnd) binSpec.end = autoBin.end = newBinSpec.end; + } - // copy bin info back to the source data. - trace._input[binSpecAttr] = trace[binSpecAttr] = binSpec; - // 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]; + // Backward compatibility for one-time autobinning. + // autobin: true is handled in cleanData, but autobin: false + // needs to be here where we have determined the values. + if(trace._input['autobin' + axLetter] === false) { + trace._input[binAttr] = Lib.extendFlat({}, binSpec); + delete trace._input['autobin' + axLetter]; } } diff --git a/src/traces/histogram2d/clean_data.js b/src/traces/histogram2d/clean_data.js new file mode 100644 index 00000000000..cff8ddf781c --- /dev/null +++ b/src/traces/histogram2d/clean_data.js @@ -0,0 +1,93 @@ +/** +* Copyright 2012-2018, 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'; + + +var isNumeric = require('fast-isnumeric'); + +var BADNUM = require('../../constants/numerical').BADNUM; +var axisIds = require('../../plots/cartesian/axis_ids'); +var Lib = require('../../lib'); + +var attributes = require('./attributes'); + +var BINDIRECTIONS = ['x', 'y']; + +// Handle bin attrs and relink auto-determined values so fullData is complete +// does not have cross-trace coupling, but moved out here so we have axis types +// and relinked trace._autoBin +module.exports = function cleanData(fullData, fullLayout) { + var i, j, traceOut, binDirection; + + function coerce(attr) { + return Lib.coerce(traceOut._input, traceOut, attributes, attr); + } + + for(i = 0; i < fullData.length; i++) { + traceOut = fullData[i]; + var type = traceOut.type; + if(type !== 'histogram2d' && type !== 'histogram2dcontour') continue; + + for(j = 0; j < BINDIRECTIONS.length; j++) { + binDirection = BINDIRECTIONS[j]; + var binAttr = binDirection + 'bins'; + var autoBins = (traceOut._autoBin || {})[binDirection] || {}; + coerce(binAttr + '.start', autoBins.start); + coerce(binAttr + '.end', autoBins.end); + coerce(binAttr + '.size', autoBins.size); + + cleanBins(traceOut, binDirection, fullLayout, autoBins); + + if(!(traceOut[binAttr] || {}).size) coerce('nbins' + binDirection); + } + } +}; + +function cleanBins(trace, binDirection, fullLayout, autoBins) { + var ax = fullLayout[axisIds.id2name(trace[binDirection + 'axis'])]; + var axType = ax.type; + var binAttr = binDirection + 'bins'; + var bins = trace[binAttr]; + var calendar = trace[binDirection + 'calendar']; + + if(!bins) bins = trace[binAttr] = {}; + + var cleanBound = (axType === 'date') ? + function(v, dflt) { return (v || v === 0) ? Lib.cleanDate(v, BADNUM, calendar) : dflt; } : + function(v, dflt) { return isNumeric(v) ? Number(v) : dflt; }; + + bins.start = cleanBound(bins.start, autoBins.start); + bins.end = cleanBound(bins.end, autoBins.end); + + // logic for bin size is very similar to dtick (cartesian/tick_value_defaults) + // but without the extra string options for log axes + // ie the only strings we accept are M for months + var sizeDflt = autoBins.size; + var binSize = bins.size; + + if(isNumeric(binSize)) { + bins.size = (binSize > 0) ? Number(binSize) : sizeDflt; + } + else if(typeof binSize !== 'string') { + bins.size = sizeDflt; + } + else { + // date special case: "M" gives bins every (integer) n months + var prefix = binSize.charAt(0); + var sizeNum = binSize.substr(1); + + sizeNum = isNumeric(sizeNum) ? Number(sizeNum) : 0; + if((sizeNum <= 0) || !( + axType === 'date' && prefix === 'M' && sizeNum === Math.round(sizeNum) + )) { + bins.size = sizeDflt; + } + } +} diff --git a/src/traces/histogram2d/index.js b/src/traces/histogram2d/index.js index af2549a9c18..6f3e49860f7 100644 --- a/src/traces/histogram2d/index.js +++ b/src/traces/histogram2d/index.js @@ -13,6 +13,7 @@ var Histogram2D = {}; Histogram2D.attributes = require('./attributes'); Histogram2D.supplyDefaults = require('./defaults'); +Histogram2D.cleanData = require('./clean_data'); Histogram2D.calc = require('../heatmap/calc'); Histogram2D.plot = require('../heatmap/plot'); Histogram2D.layerName = 'heatmaplayer'; diff --git a/src/traces/histogram2d/sample_defaults.js b/src/traces/histogram2d/sample_defaults.js index 80575051d26..b1262ec82e9 100644 --- a/src/traces/histogram2d/sample_defaults.js +++ b/src/traces/histogram2d/sample_defaults.js @@ -10,8 +10,6 @@ 'use strict'; var Registry = require('../../registry'); -var handleBinDefaults = require('../histogram/bin_defaults'); - module.exports = function handleSampleDefaults(traceIn, traceOut, coerce, layout) { var x = coerce('x'); @@ -34,7 +32,7 @@ module.exports = function handleSampleDefaults(traceIn, traceOut, coerce, layout var hasAggregationData = coerce('z') || coerce('marker.color'); if(hasAggregationData) coerce('histfunc'); + coerce('histnorm'); - var binDirections = ['x', 'y']; - handleBinDefaults(traceIn, traceOut, coerce, binDirections); + // Note: bin defaults are now handled in Histogram2D.cleanData }; diff --git a/src/traces/histogram2dcontour/attributes.js b/src/traces/histogram2dcontour/attributes.js index 048a0c481d4..e8c3c402d7b 100644 --- a/src/traces/histogram2dcontour/attributes.js +++ b/src/traces/histogram2dcontour/attributes.js @@ -23,10 +23,8 @@ module.exports = extendFlat({ histnorm: histogram2dAttrs.histnorm, histfunc: histogram2dAttrs.histfunc, - autobinx: histogram2dAttrs.autobinx, nbinsx: histogram2dAttrs.nbinsx, xbins: histogram2dAttrs.xbins, - autobiny: histogram2dAttrs.autobiny, nbinsy: histogram2dAttrs.nbinsy, ybins: histogram2dAttrs.ybins, @@ -34,7 +32,11 @@ module.exports = extendFlat({ ncontours: contourAttrs.ncontours, contours: contourAttrs.contours, line: contourAttrs.line, - zhoverformat: histogram2dAttrs.zhoverformat + zhoverformat: histogram2dAttrs.zhoverformat, + _deprecated: { + autobinx: histogram2dAttrs._deprecated.autobinx, + autobiny: histogram2dAttrs._deprecated.autobiny + } }, colorscaleAttrs('', { cLetter: 'z', diff --git a/src/traces/histogram2dcontour/index.js b/src/traces/histogram2dcontour/index.js index c16206dff5e..99a3679af0e 100644 --- a/src/traces/histogram2dcontour/index.js +++ b/src/traces/histogram2dcontour/index.js @@ -13,6 +13,7 @@ var Histogram2dContour = {}; Histogram2dContour.attributes = require('./attributes'); Histogram2dContour.supplyDefaults = require('./defaults'); +Histogram2dContour.cleanData = require('../histogram2d/clean_data'); Histogram2dContour.calc = require('../contour/calc'); Histogram2dContour.plot = require('../contour/plot').plot; Histogram2dContour.layerName = 'contourlayer'; diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index bfc15effcc0..e3a2680b343 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -1220,7 +1220,7 @@ describe('Test axes', function() { axOut = {}; mockSupplyDefaults(axIn, axOut, 'log'); // tick0 gets ignored for D - expect(axOut.tick0).toBe(0); + expect(axOut.tick0).toBeUndefined(v); expect(axOut.dtick).toBe(v); }); diff --git a/test/jasmine/tests/histogram2d_test.js b/test/jasmine/tests/histogram2d_test.js index 3950324aeb5..7893e18c767 100644 --- a/test/jasmine/tests/histogram2d_test.js +++ b/test/jasmine/tests/histogram2d_test.js @@ -182,6 +182,16 @@ describe('Test histogram2d', function() { .then(done); }); + function _assert(xBinsFull, yBinsFull, xBins, yBins) { + expect(gd._fullData[0].xbins).toEqual(xBinsFull); + expect(gd._fullData[0].ybins).toEqual(yBinsFull); + expect(gd._fullData[0].autobinx).toBeUndefined(); + expect(gd._fullData[0].autobiny).toBeUndefined(); + expect(gd.data[0].xbins).toEqual(xBins); + expect(gd.data[0].ybins).toEqual(yBins); + expect(gd.data[0].autobinx).toBeUndefined(); + expect(gd.data[0].autobiny).toBeUndefined(); + } it('handles autobin correctly on restyles', function() { var x1 = [ @@ -191,65 +201,64 @@ describe('Test histogram2d', function() { 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, _dataSpan: 3}); - expect(gd._fullData[0].ybins).toEqual({start: 0.5, end: 4.5, size: 1, _dataSpan: 3}); - expect(gd._fullData[0].autobinx).toBe(true); - expect(gd._fullData[0].autobiny).toBe(true); + _assert( + {start: 0.5, end: 4.5, size: 1}, + {start: 0.5, end: 4.5, size: 1}, + undefined, undefined); // 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, _dataSpan: 3}); - expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 5.5, size: 2, _dataSpan: 3}); - expect(gd._fullData[0].autobinx).toBe(true); - expect(gd._fullData[0].autobiny).toBe(true); + _assert( + {start: -0.5, end: 5.5, size: 2}, + {start: -0.5, end: 5.5, size: 2}, + undefined, undefined); // 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, _dataSpan: 30}); - expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 59.5, size: 20, _dataSpan: 30}); - expect(gd._fullData[0].autobinx).toBe(true); - expect(gd._fullData[0].autobiny).toBe(true); + _assert( + {start: -0.5, end: 59.5, size: 20}, + {start: -0.5, end: 59.5, size: 20}, + undefined, undefined); // explicit changes to bin settings Plotly.restyle(gd, 'xbins.start', 12); - expect(gd._fullData[0].xbins).toEqual({start: 12, end: 59.5, size: 20, _dataSpan: 30}); - expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 59.5, size: 20, _dataSpan: 30}); - expect(gd._fullData[0].autobinx).toBe(false); - expect(gd._fullData[0].autobiny).toBe(true); + _assert( + {start: 12, end: 59.5, size: 20}, + {start: -0.5, end: 59.5, size: 20}, + {start: 12}, undefined); Plotly.restyle(gd, {'ybins.end': 12, 'ybins.size': 3}); - expect(gd._fullData[0].xbins).toEqual({start: 12, end: 59.5, size: 20, _dataSpan: 30}); - expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 12, size: 3, _dataSpan: 30}); - expect(gd._fullData[0].autobinx).toBe(false); - expect(gd._fullData[0].autobiny).toBe(false); + _assert( + {start: 12, end: 59.5, size: 20}, + // with the new autobin algo, start responds to autobin + {start: 8.5, end: 12, size: 3}, + {start: 12}, + {end: 12, size: 3}); // restart autobin Plotly.restyle(gd, {autobinx: true, autobiny: true}); - expect(gd._fullData[0].xbins).toEqual({start: -0.5, end: 59.5, size: 20, _dataSpan: 30}); - expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 59.5, size: 20, _dataSpan: 30}); - expect(gd._fullData[0].autobinx).toBe(true); - expect(gd._fullData[0].autobiny).toBe(true); + _assert( + {start: -0.5, end: 59.5, size: 20}, + {start: -0.5, end: 59.5, size: 20}, + undefined, undefined); }); it('respects explicit autobin: false as a one-time autobin', function() { + // patched in for backward compat, but there aren't really + // autobinx/autobiny attributes anymore 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]; + var binSpec = {start: 0.5, end: 4.5, size: 1}; 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, _dataSpan: 3}); - expect(gd._fullData[0].ybins).toEqual({start: 0.5, end: 4.5, size: 1, _dataSpan: 3}); - expect(gd._fullData[0].autobinx).toBe(false); - expect(gd._fullData[0].autobiny).toBe(false); + _assert(binSpec, binSpec, binSpec, binSpec); // 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, _dataSpan: 3}); - expect(gd._fullData[0].ybins).toEqual({start: 0.5, end: 4.5, size: 1, _dataSpan: 3}); - expect(gd._fullData[0].autobinx).toBe(false); - expect(gd._fullData[0].autobiny).toBe(false); + _assert(binSpec, binSpec, binSpec, binSpec); }); }); diff --git a/test/jasmine/tests/histogram_test.js b/test/jasmine/tests/histogram_test.js index e39096a7c94..141204b08cc 100644 --- a/test/jasmine/tests/histogram_test.js +++ b/test/jasmine/tests/histogram_test.js @@ -187,10 +187,10 @@ describe('Test histogram', function() { describe('calc', function() { - function _calc(opts, extraTraces, layout) { + function _calc(opts, extraTraces, layout, prependExtras) { var base = { type: 'histogram' }; var trace = Lib.extendFlat({}, base, opts); - var gd = { data: [trace] }; + var gd = { data: prependExtras ? [] : [trace] }; if(layout) gd.layout = layout; @@ -200,8 +200,16 @@ describe('Test histogram', function() { }); } + if(prependExtras) gd.data.push(trace); + supplyAllDefaults(gd); - var fullTrace = gd._fullData[0]; + var fullTrace = gd._fullData[prependExtras ? gd._fullData.length - 1 : 0]; + + if(prependExtras) { + for(var i = 0; i < gd._fullData.length - 1; i++) { + calc(gd, gd._fullData[i]); + } + } var out = calc(gd, fullTrace); delete out[0].trace; @@ -408,8 +416,8 @@ describe('Test histogram', function() { ]); }); - function calcPositions(opts, extraTraces) { - return _calc(opts, extraTraces).map(function(v) { return v.p; }); + function calcPositions(opts, extraTraces, prepend) { + return _calc(opts, extraTraces, {}, prepend).map(function(v) { return v.p; }); } it('harmonizes autobins when all traces are autobinned', function() { @@ -420,25 +428,11 @@ describe('Test histogram', function() { expect(calcPositions(trace2)).toBeCloseToArray([5.5, 6.5], 5); - expect(calcPositions(trace1, [trace2])).toEqual([1, 2, 3, 4]); - // huh, turns out even this one is an example of "unexpected bin positions" - // (see another example below) - in this case it's because trace1 gets - // autoshifted to keep integers off the bin edges, whereas trace2 doesn't - // because there are as many integers as half-integers. - // In this case though, it's unexpected but arguably better than the - // "expected" result. - expect(calcPositions(trace2, [trace1])).toEqual([5, 6, 7]); - }); - - it('can sometimes give unexpected bin positions', function() { - // documenting an edge case that might not be desirable but for now - // we've decided to ignore: a larger bin sets the bin start, but then it - // doesn't quite make sense with the smaller bin we end up with - // we *could* fix this by ensuring that the bin start is based on the - // same bin spec that gave the minimum bin size, but incremented down to - // include the minimum start... but that would have awkward edge cases - // involving month bins so for now we're ignoring it. + expect(calcPositions(trace1, [trace2])).toEqual([1, 3, 5]); + expect(calcPositions(trace2, [trace1])).toEqual([5, 7]); + }); + it('autobins all data as one', function() { // all integers, so all autobins should get shifted to start 0.5 lower // than they otherwise would. var trace1 = {x: [1, 2, 3, 4]}; @@ -450,19 +444,21 @@ describe('Test histogram', function() { // {size: 5, start: -5.5}: -5..-1, 0..4, 5..9 expect(calcPositions(trace2)).toEqual([-3, 2, 7]); - // unexpected behavior when we put these together, - // because 2 and 5 are mutually prime. Normally you could never get - // groupings 1&2, 3&4... you'd always get 0&1, 2&3... - expect(calcPositions(trace1, [trace2])).toBeCloseToArray([1.5, 3.5], 5); - expect(calcPositions(trace2, [trace1])).toBeCloseToArray([ - -2.5, -0.5, 1.5, 3.5, 5.5, 7.5 - ], 5); + // together bins match the wider trace + expect(calcPositions(trace1, [trace2])).toBeCloseToArray([2], 5); + expect(calcPositions(trace2, [trace1])).toEqual([-3, 2, 7]); + + // unless we add enough points to shrink the bins + expect(calcPositions(trace2, [trace1, trace1, trace1, trace1])) + .toBeCloseToArray([-1.5, 0.5, 2.5, 4.5, 6.5], 5); }); it('harmonizes autobins with smaller manual bins', function() { var trace1 = {x: [1, 2, 3, 4]}; var trace2 = {x: [5, 6, 7, 8], xbins: {start: 4.3, end: 7.1, size: 0.4}}; + // size is preserved, and start is shifted to be compatible with trace2 + // (but we can't just use start from trace2 or it would cut off all our data!) expect(calcPositions(trace1, [trace2])).toBeCloseToArray([ 0.9, 1.3, 1.7, 2.1, 2.5, 2.9, 3.3, 3.7, 4.1 ], 5); @@ -470,20 +466,73 @@ describe('Test histogram', function() { it('harmonizes autobins with larger manual bins', function() { var trace1 = {x: [1, 2, 3, 4]}; - var trace2 = {x: [5, 6, 7, 8], xbins: {start: 4.3, end: 15, size: 7}}; + var trace2 = {x: [5, 6, 7, 8], xbins: {start: 3.7, end: 15, size: 7}}; expect(calcPositions(trace1, [trace2])).toBeCloseToArray([ - 0.8, 2.55, 4.3 + 0.2, 7.2 ], 5); }); + it('ignores incompatible sizes, and harmonizes start values', function() { + var trace1 = {x: [1, 2, 3, 4], xbins: {start: 1.7, end: 3.5, size: 0.6}}; + var trace2 = {x: [5, 6, 7, 8], xbins: {start: 4.3, end: 7.1, size: 0.4}}; + + // trace1 is first: all its settings are used directly, + // and trace2 uses its size and shifts start to harmonize with it. + expect(calcPositions(trace1, [trace2])).toBeCloseToArray([ + 2.0, 2.6, 3.2 + ], 5); + expect(calcPositions(trace2, [trace1], true)).toBeCloseToArray([ + 5.0, 5.6, 6.2, 6.8 + ], 5); + + // switch the order: trace2 values win + expect(calcPositions(trace2, [trace1])).toBeCloseToArray([ + 4.9, 5.3, 5.7, 6.1, 6.5, 6.9 + ], 5); + expect(calcPositions(trace1, [trace2], true)).toBeCloseToArray([ + 2.1, 2.5, 2.9 + ], 5); + }); + + it('can take size and start from different traces in any order', function() { + var trace1 = {x: [1, 2, 3, 4], xbins: {size: 0.6}}; + var trace2 = {x: [5, 6, 7, 8], xbins: {start: 4.8}}; + + [true, false].forEach(function(prepend) { + expect(calcPositions(trace1, [trace2], prepend)).toBeCloseToArray([ + 0.9, 1.5, 2.1, 2.7, 3.3, 3.9 + ], 5); + + expect(calcPositions(trace2, [trace1], prepend)).toBeCloseToArray([ + 5.1, 5.7, 6.3, 6.9, 7.5, 8.1 + ], 5); + }); + }); + + it('works with only a size specified', function() { + // this used to not just lose the size, but actually errored out. + var trace1 = {x: [1, 2, 3, 4], xbins: {size: 0.8}}; + var trace2 = {x: [5, 6, 7, 8]}; + + [true, false].forEach(function(prepend) { + expect(calcPositions(trace1, [trace2], prepend)).toBeCloseToArray([ + 1, 1.8, 2.6, 3.4, 4.2 + ], 5); + + expect(calcPositions(trace2, [trace1], prepend)).toBeCloseToArray([ + 5, 5.8, 6.6, 7.4, 8.2 + ], 5); + }); + }); + it('ignores traces on other axes', function() { var trace1 = {x: [1, 2, 3, 4]}; var trace2 = {x: [5, 5.5, 6, 6.5]}; var trace3 = {x: [1, 1.1, 1.2, 1.3], xaxis: 'x2'}; var trace4 = {x: [1, 1.2, 1.4, 1.6], yaxis: 'y2'}; - expect(calcPositions(trace1, [trace2, trace3, trace4])).toEqual([1, 2, 3, 4]); + expect(calcPositions(trace1, [trace2, trace3, trace4])).toEqual([1, 3, 5]); expect(calcPositions(trace3)).toBeCloseToArray([1.1, 1.3], 5); }); @@ -610,43 +659,59 @@ describe('Test histogram', function() { 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); + expect(gd._fullData[0].nbinsx).toBe(0); // 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); + expect(gd._fullData[0].nbinsx).toBe(0); // 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); + expect(gd._fullData[0].nbinsx).toBe(0); - // explicit change to a bin attribute clears autobin + // explicit change to start does not update anything else 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); + expect(gd._fullData[0].nbinsx).toBe(0); // 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); + expect(gd._fullData[0].nbinsx).toBe(0); + + // explicit end does not update anything else + Plotly.restyle(gd, 'xbins.end', 43); + expect(gd._fullData[0].xbins).toEqual({start: 5, end: 43, size: 10}); + expect(gd._fullData[0].nbinsx).toBe(0); + + // nbins would update all three, but explicit end is honored + Plotly.restyle(gd, 'nbinsx', 3); + expect(gd._fullData[0].xbins).toEqual({start: 0, end: 43, size: 20}); + expect(gd._fullData[0].nbinsx).toBe(3); + + // explicit size updates auto start *and* end, and moots nbins + Plotly.restyle(gd, {'xbins.end': null, 'xbins.size': 2}); + expect(gd._fullData[0].xbins).toEqual({start: 9, end: 51, size: 2}); + expect(gd._fullData[0].nbinsx).toBeUndefined(); }); 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 + // but for backward compat. calc pushes these bins back into gd.data + // even though there's no `autobinx` attribute anymore. expect(gd._fullData[0].xbins).toEqual({start: 1, end: 6, size: 1}); - expect(gd._fullData[0].autobinx).toBe(false); + expect(gd.data[0].xbins).toEqual({start: 1, end: 6, size: 1}); // 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); }); it('allows changing axis type with new x data', function() { @@ -742,6 +807,53 @@ describe('Test histogram', function() { .catch(failTest) .then(done); }); + + it('autobins all histograms together except `visible: false`', function(done) { + function _assertBinCenters(expectedCenters) { + var centers = gd.calcdata.map(function(cd) { + return cd.map(function(cdi) { return cdi.p; }); + }); + + expect(centers).toBeCloseTo2DArray(expectedCenters); + } + + var hidden = [undefined]; + + Plotly.newPlot(gd, [ + {type: 'histogram', x: [1]}, + {type: 'histogram', x: [10, 10.1, 10.2, 10.3]}, + {type: 'histogram', x: [20, 20, 20, 20, 20, 20, 20, 20, 20, 21]} + ]) + .then(function() { + _assertBinCenters([[0], [10], [20]]); + return Plotly.restyle(gd, 'visible', 'legendonly', [1, 2]); + }) + .then(function() { + _assertBinCenters([[0], hidden, hidden]); + return Plotly.restyle(gd, 'visible', false, [1, 2]); + }) + .then(function() { + _assertBinCenters([[1], hidden, hidden]); + return Plotly.restyle(gd, 'visible', [false, false, true]); + }) + .then(function() { + _assertBinCenters([hidden, hidden, [20, 21]]); + return Plotly.restyle(gd, 'visible', [false, true, false]); + }) + .then(function() { + _assertBinCenters([hidden, [10.1, 10.3], hidden]); + // only one trace is visible, despite traces being grouped + expect(gd._fullLayout.bargap).toBe(0); + return Plotly.restyle(gd, 'visible', ['legendonly', true, 'legendonly']); + }) + .then(function() { + _assertBinCenters([hidden, [10], hidden]); + // legendonly traces still flip us back to gapped + expect(gd._fullLayout.bargap).toBe(0.2); + }) + .catch(failTest) + .then(done); + }); }); }); diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 4e38fc1e982..42ad39c79f6 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -1119,17 +1119,30 @@ describe('Test plot api', function() { }); it('turns off autobin when you edit bin specs', function(done) { + // test retained (modified) for backward compat with new autobin logic var start0 = 0.2; var end1 = 6; var size1 = 0.5; function check(auto, msg) { - expect(gd.data[0].autobinx).toBe(auto, msg); - expect(gd.data[0].xbins.start).negateIf(auto).toBe(start0, msg); - expect(gd.data[1].autobinx).toBe(auto, msg); - expect(gd.data[1].autobiny).toBe(auto, msg); - expect(gd.data[1].xbins.end).negateIf(auto).toBe(end1, msg); - expect(gd.data[1].ybins.size).negateIf(auto).toBe(size1, msg); + expect(gd.data[0].autobinx).toBeUndefined(msg); + expect(gd.data[1].autobinx).toBeUndefined(msg); + expect(gd.data[1].autobiny).toBeUndefined(msg); + + if(auto) { + expect(gd.data[0].xbins).toBeUndefined(msg); + expect(gd.data[1].xbins).toBeUndefined(msg); + expect(gd.data[1].ybins).toBeUndefined(msg); + } + else { + // we can have - and use - partial autobin now + expect(gd.data[0].xbins).toEqual({start: start0}); + expect(gd.data[1].xbins).toEqual({end: end1}); + expect(gd.data[1].ybins).toEqual({size: size1}); + expect(gd._fullData[0].xbins.start).toBe(start0, msg); + expect(gd._fullData[1].xbins.end).toBe(end1, msg); + expect(gd._fullData[1].ybins.size).toBe(size1, msg); + } } Plotly.plot(gd, [ From c0b8c6ffe4ca4273bc1b578bdeaab664570615bf Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 27 Sep 2018 15:58:35 -0400 Subject: [PATCH 06/10] test invisible bars and traceorder --- test/jasmine/tests/bar_test.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index dc9ec698751..609231fffda 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -1423,6 +1423,37 @@ describe('bar visibility toggling:', function() { .catch(failTest) .then(done); }); + + it('gets the right legend traceorder if all bars are visible: false', function(done) { + function _assert(traceorder, yRange, legendCount) { + expect(gd._fullLayout.legend.traceorder).toBe(traceorder); + expect(gd._fullLayout.yaxis.range).toBeCloseToArray(yRange, 2); + expect(d3.select(gd).selectAll('.legend .traces').size()).toBe(legendCount); + } + Plotly.newPlot(gd, [ + {type: 'bar', y: [1, 2, 3]}, + {type: 'bar', y: [3, 2, 1]}, + {y: [2, 3, 2]}, + {y: [3, 2, 3]} + ], { + barmode: 'stack', width: 400, height: 400 + }) + .then(function() { + _assert('reversed', [0, 4.211], 4); + + return Plotly.restyle(gd, {visible: false}, [0, 1]); + }) + .then(function() { + _assert('normal', [1.922, 3.077], 2); + + return Plotly.restyle(gd, {visible: 'legendonly'}, [0, 1]); + }) + .then(function() { + _assert('reversed', [1.922, 3.077], 4); + }) + .catch(failTest) + .then(done); + }); }); describe('bar hover', function() { From 76eee4b992eacbebff1f00ee025c7696eeb7f5d3 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 27 Sep 2018 19:52:03 -0400 Subject: [PATCH 07/10] flesh out bin attribute descriptions and let autobin(x|y) pass validate --- src/traces/histogram/attributes.js | 99 +++++++-------------- src/traces/histogram/bin_attributes.js | 74 +++++++++++++++ src/traces/histogram/calc.js | 6 +- src/traces/histogram/defaults.js | 2 + src/traces/histogram2d/attributes.js | 13 ++- src/traces/histogram2d/calc.js | 6 +- src/traces/histogram2d/sample_defaults.js | 3 + src/traces/histogram2dcontour/attributes.js | 8 +- test/jasmine/tests/validate_test.js | 69 ++++++++++++++ 9 files changed, 198 insertions(+), 82 deletions(-) create mode 100644 src/traces/histogram/bin_attributes.js diff --git a/src/traces/histogram/attributes.js b/src/traces/histogram/attributes.js index bd9e3df112b..80bf57e1585 100644 --- a/src/traces/histogram/attributes.js +++ b/src/traces/histogram/attributes.js @@ -9,6 +9,7 @@ 'use strict'; var barAttrs = require('../bar/attributes'); +var makeBinAttrs = require('./bin_attributes'); module.exports = { x: { @@ -138,7 +139,7 @@ module.exports = { 'Ignored if `xbins.size` is provided.' ].join(' ') }, - xbins: makeBinsAttr('x'), + xbins: makeBinAttrs('x', true), nbinsy: { valType: 'integer', @@ -153,7 +154,36 @@ module.exports = { 'Ignored if `ybins.size` is provided.' ].join(' ') }, - ybins: makeBinsAttr('y'), + ybins: makeBinAttrs('y', true), + autobinx: { + valType: 'boolean', + dflt: null, + role: 'style', + editType: 'calc', + description: [ + 'Obsolete: since v1.42 each bin attribute is auto-determined', + 'separately and `autobinx` is not needed. However, we accept', + '`autobinx: true` or `false` and will update `xbins` accordingly', + 'before deleting `autobinx` from the trace.' + ].join(' ') + }, + autobiny: { + valType: 'boolean', + dflt: null, + role: 'style', + editType: 'calc', + impliedEdits: { + 'ybins.start': undefined, + 'ybins.end': undefined, + 'ybins.size': undefined + }, + description: [ + 'Obsolete: since v1.42 each bin attribute is auto-determined', + 'separately and `autobiny` is not needed. However, we accept', + '`autobiny: true` or `false` and will update `ybins` accordingly', + 'before deleting `autobiny` from the trace.' + ].join(' ') + }, marker: barAttrs.marker, @@ -161,69 +191,6 @@ module.exports = { unselected: barAttrs.unselected, _deprecated: { - bardir: barAttrs._deprecated.bardir, - autobinx: { - valType: 'boolean', - dflt: null, - role: 'style', - editType: 'calc', - impliedEdits: { - 'xbins.start': undefined, - 'xbins.end': undefined, - 'xbins.size': undefined - }, - description: [ - 'Obsolete: since v1.42 each bin', - 'attribute is auto-determined separately.' - ].join(' ') - }, - autobiny: { - valType: 'boolean', - dflt: null, - role: 'style', - editType: 'calc', - impliedEdits: { - 'ybins.start': undefined, - 'ybins.end': undefined, - 'ybins.size': undefined - }, - description: [ - 'Obsolete: since v1.42 each bin', - 'attribute is auto-determined separately.' - ].join(' ') - } + bardir: barAttrs._deprecated.bardir } }; - -function makeBinsAttr(axLetter) { - return { - start: { - valType: 'any', // for date axes - role: 'style', - editType: 'calc', - description: [ - 'Sets the starting value for the', axLetter, - 'axis bins.' - ].join(' ') - }, - end: { - valType: 'any', // for date axes - role: 'style', - editType: 'calc', - description: [ - 'Sets the end value for the', axLetter, - 'axis bins.' - ].join(' ') - }, - size: { - valType: 'any', // for date axes - role: 'style', - editType: 'calc', - description: [ - 'Sets the step in-between value each', axLetter, - 'axis bin.' - ].join(' ') - }, - editType: 'calc' - }; -} diff --git a/src/traces/histogram/bin_attributes.js b/src/traces/histogram/bin_attributes.js new file mode 100644 index 00000000000..24c800477b8 --- /dev/null +++ b/src/traces/histogram/bin_attributes.js @@ -0,0 +1,74 @@ +/** +* Copyright 2012-2018, 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 = function makeBinAttrs(axLetter, match) { + return { + start: { + valType: 'any', // for date axes + role: 'style', + editType: 'calc', + description: [ + 'Sets the starting value for the', axLetter, + 'axis bins. Defaults to the minimum data value,', + 'shifted down if necessary to make nice round values', + 'and to remove ambiguous bin edges. For example, if most of the', + 'data is integers we shift the bin edges 0.5 down, so a `size`', + 'of 5 would have a default `start` of -0.5, so it is clear', + 'that 0-4 are in the first bin, 5-9 in the second, but', + 'continuous data gets a start of 0 and bins [0,5), [5,10) etc.', + 'Dates behave similarly, and `start` should be a date string.', + 'For category data, `start` is based on the category serial', + 'numbers, and defaults to -0.5.', + (match ? ( + 'If multiple non-overlaying histograms share a subplot, ' + + 'the first explicit `start` is used exactly and all others ' + + 'are shifted down (if necessary) to differ from that one ' + + 'by an integer number of bins.' + ) : '') + ].join(' ') + }, + end: { + valType: 'any', // for date axes + role: 'style', + editType: 'calc', + description: [ + 'Sets the end value for the', axLetter, + 'axis bins. The last bin may not end exactly at this value,', + 'we increment the bin edge by `size` from `start` until we', + 'reach or exceed `end`. Defaults to the maximum data value.', + 'Like `start`, for dates use a date string, and for category', + 'data `end` is based on the category serial numbers.' + ].join(' ') + }, + size: { + valType: 'any', // for date axes + role: 'style', + editType: 'calc', + description: [ + 'Sets the size of each', axLetter, 'axis bin.', + 'Default behavior: If `nbins' + axLetter + '` is 0 or omitted,', + 'we choose a nice round bin size such that the number of bins', + 'is about the same as the typical number of samples in each bin.', + 'If `nbins' + axLetter + '` is provided, we choose a nice round', + 'bin size giving no more than that many bins.', + 'For date data, use milliseconds or *M* for months, as in', + '`axis.dtick`. For category data, the number of categories to', + 'bin together (always defaults to 1).', + (match ? ( + 'If multiple non-overlaying histograms share a subplot, ' + + 'the first explicit `size` is used and all others discarded. ' + + 'If no `size` is provided,the sample data from all traces ' + + 'is combined to determine `size` as described above.' + ) : '') + ].join(' ') + }, + editType: 'calc' + }; +}; diff --git a/src/traces/histogram/calc.js b/src/traces/histogram/calc.js index 5b66465a5f5..1a60c9206e9 100644 --- a/src/traces/histogram/calc.js +++ b/src/traces/histogram/calc.js @@ -359,9 +359,11 @@ function calcAllAutoBins(gd, trace, pa, mainData, _overlayEdgeCase) { // Backward compatibility for one-time autobinning. // autobin: true is handled in cleanData, but autobin: false // needs to be here where we have determined the values. - if(trace._input['autobin' + mainData] === false) { + var autoBinAttr = 'autobin' + mainData; + if(trace._input[autoBinAttr] === false) { trace._input[binAttr] = Lib.extendFlat({}, trace[binAttr] || {}); - delete trace._input['autobin' + mainData]; + delete trace._input[autoBinAttr]; + delete trace[autoBinAttr]; } return [traceBinOptsCalc, pos0]; diff --git a/src/traces/histogram/defaults.js b/src/traces/histogram/defaults.js index 296f8834bb6..949adacf76f 100644 --- a/src/traces/histogram/defaults.js +++ b/src/traces/histogram/defaults.js @@ -53,6 +53,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('histnorm'); // Note: bin defaults are now handled in Histogram.cleanData + // autobin(x|y) are only included here to appease Plotly.validate + coerce('autobin' + sampleLetter); handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout); diff --git a/src/traces/histogram2d/attributes.js b/src/traces/histogram2d/attributes.js index c8a208ce5a6..eba5151eb0b 100644 --- a/src/traces/histogram2d/attributes.js +++ b/src/traces/histogram2d/attributes.js @@ -9,6 +9,7 @@ 'use strict'; var histogramAttrs = require('../histogram/attributes'); +var makeBinAttrs = require('../histogram/bin_attributes'); var heatmapAttrs = require('../heatmap/attributes'); var colorscaleAttrs = require('../../components/colorscale/attributes'); var colorbarAttrs = require('../../components/colorbar/attributes'); @@ -37,18 +38,16 @@ module.exports = extendFlat( histnorm: histogramAttrs.histnorm, histfunc: histogramAttrs.histfunc, nbinsx: histogramAttrs.nbinsx, - xbins: histogramAttrs.xbins, + xbins: makeBinAttrs('x'), nbinsy: histogramAttrs.nbinsy, - ybins: histogramAttrs.ybins, + ybins: makeBinAttrs('y'), + autobinx: histogramAttrs.autobinx, + autobiny: histogramAttrs.autobiny, xgap: heatmapAttrs.xgap, ygap: heatmapAttrs.ygap, zsmooth: heatmapAttrs.zsmooth, - zhoverformat: heatmapAttrs.zhoverformat, - _deprecated: { - autobinx: histogramAttrs._deprecated.autobinx, - autobiny: histogramAttrs._deprecated.autobiny - } + zhoverformat: heatmapAttrs.zhoverformat }, colorscaleAttrs('', { cLetter: 'z', diff --git a/src/traces/histogram2d/calc.js b/src/traces/histogram2d/calc.js index c345c904e5f..7a221a8eb91 100644 --- a/src/traces/histogram2d/calc.js +++ b/src/traces/histogram2d/calc.js @@ -219,9 +219,11 @@ function doAutoBin(trace, axLetter, data, ax, r2c, c2r, calendar) { // Backward compatibility for one-time autobinning. // autobin: true is handled in cleanData, but autobin: false // needs to be here where we have determined the values. - if(trace._input['autobin' + axLetter] === false) { + var autoBinAttr = 'autobin' + axLetter; + if(trace._input[autoBinAttr] === false) { trace._input[binAttr] = Lib.extendFlat({}, binSpec); - delete trace._input['autobin' + axLetter]; + delete trace._input[autoBinAttr]; + delete trace[autoBinAttr]; } } diff --git a/src/traces/histogram2d/sample_defaults.js b/src/traces/histogram2d/sample_defaults.js index b1262ec82e9..155f717de6f 100644 --- a/src/traces/histogram2d/sample_defaults.js +++ b/src/traces/histogram2d/sample_defaults.js @@ -35,4 +35,7 @@ module.exports = function handleSampleDefaults(traceIn, traceOut, coerce, layout coerce('histnorm'); // Note: bin defaults are now handled in Histogram2D.cleanData + // autobin(x|y) are only included here to appease Plotly.validate + coerce('autobinx'); + coerce('autobiny'); }; diff --git a/src/traces/histogram2dcontour/attributes.js b/src/traces/histogram2dcontour/attributes.js index e8c3c402d7b..ee60da32e57 100644 --- a/src/traces/histogram2dcontour/attributes.js +++ b/src/traces/histogram2dcontour/attributes.js @@ -27,16 +27,14 @@ module.exports = extendFlat({ xbins: histogram2dAttrs.xbins, nbinsy: histogram2dAttrs.nbinsy, ybins: histogram2dAttrs.ybins, + autobinx: histogram2dAttrs.autobinx, + autobiny: histogram2dAttrs.autobiny, autocontour: contourAttrs.autocontour, ncontours: contourAttrs.ncontours, contours: contourAttrs.contours, line: contourAttrs.line, - zhoverformat: histogram2dAttrs.zhoverformat, - _deprecated: { - autobinx: histogram2dAttrs._deprecated.autobinx, - autobiny: histogram2dAttrs._deprecated.autobiny - } + zhoverformat: histogram2dAttrs.zhoverformat }, colorscaleAttrs('', { cLetter: 'z', diff --git a/test/jasmine/tests/validate_test.js b/test/jasmine/tests/validate_test.js index d3123bd3a1a..0a455f2b4e5 100644 --- a/test/jasmine/tests/validate_test.js +++ b/test/jasmine/tests/validate_test.js @@ -568,4 +568,73 @@ describe('Plotly.validate', function() { 'In layout, container polar3 did not get coerced' ); }); + + it('understands histogram bin and autobin attributes', function() { + var out = Plotly.validate([{ + type: 'histogram', + x: [1, 2, 3], + // allowed by Plotly.validate, even though we get rid of it + // in a real plot call + autobinx: true, + // valid attribute, but not coerced + autobiny: false + }]); + expect(out.length).toBe(1); + assertErrorContent( + out[0], 'unused', 'data', 0, ['autobiny'], 'autobiny', + 'In data trace 0, key autobiny did not get coerced' + ); + + out = Plotly.validate([{ + type: 'histogram', + x: [1, 2, 3], + xbins: {start: 1, end: 4, size: 0.5} + }]); + expect(out).toBeUndefined(); + + out = Plotly.validate([{ + type: 'histogram', + x: [1, 2, 3], + xbins: {start: 0.8, end: 4, size: 0.5} + }, { + type: 'histogram', + x: [1, 2, 3], + // start and end still get coerced, even though start will get modified + // during calc. size will not be coerced because trace 0 already has it. + xbins: {start: 2, end: 3, size: 1} + }]); + + expect(out.length).toBe(1); + assertErrorContent( + out[0], 'unused', 'data', 1, ['xbins', 'size'], 'xbins.size', + 'In data trace 1, key xbins.size did not get coerced' + ); + }); + + it('understands histogram2d(contour) bin and autobin attributes', function() { + var out = Plotly.validate([{ + type: 'histogram2d', + x: [1, 2, 3], + y: [1, 2, 3], + autobinx: true, + autobiny: false, + xbins: {start: 5, end: 10}, + ybins: {size: 2} + }, { + type: 'histogram2d', + x: [1, 2, 3], + y: [1, 2, 3], + xbins: {start: 0, end: 7, size: 1}, + ybins: {size: 3} + }, { + type: 'histogram2dcontour', + x: [1, 2, 3], + y: [1, 2, 3], + autobinx: false, + autobiny: false, + xbins: {start: 1, end: 5, size: 2}, + ybins: {size: 4} + }]); + expect(out).toBeUndefined(); + }); }); From 5d930cee2d0df51ad6bbb99a0df17a4eb2fe64dc Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 27 Sep 2018 21:23:25 -0400 Subject: [PATCH 08/10] _module.cleanData -> crossTraceDefaults --- src/plots/plots.js | 14 +++++++------- .../{clean_data.js => cross_trace_defaults.js} | 2 +- src/traces/histogram/defaults.js | 2 +- src/traces/histogram/index.js | 2 +- .../{clean_data.js => cross_trace_defaults.js} | 2 +- src/traces/histogram2d/index.js | 2 +- src/traces/histogram2d/sample_defaults.js | 2 +- src/traces/histogram2dcontour/index.js | 2 +- .../{clean_data.js => cross_trace_defaults.js} | 2 +- src/traces/scatter/index.js | 2 +- src/traces/scattergl/index.js | 2 +- 11 files changed, 17 insertions(+), 17 deletions(-) rename src/traces/histogram/{clean_data.js => cross_trace_defaults.js} (98%) rename src/traces/histogram2d/{clean_data.js => cross_trace_defaults.js} (97%) rename src/traces/scatter/{clean_data.js => cross_trace_defaults.js} (94%) diff --git a/src/plots/plots.js b/src/plots/plots.js index 6c10a889201..246f518de3e 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -438,17 +438,17 @@ plots.supplyDefaults = function(gd, opts) { plots.supplyLayoutModuleDefaults(newLayout, newFullLayout, newFullData, gd._transitionData); // Special cases that introduce interactions between traces. - // This is after relinkPrivateKeys so we can use those in cleanData + // This is after relinkPrivateKeys so we can use those in crossTraceDefaults // and after layout module defaults, so we can use eg barmode var _modules = newFullLayout._visibleModules; - var cleanDataFuncs = []; + var crossTraceDefaultsFuncs = []; for(i = 0; i < _modules.length; i++) { - var _module = _modules[i]; - // some trace types share cleanData (ie histogram2d, histogram2dcontour) - if(_module.cleanData) Lib.pushUnique(cleanDataFuncs, _module.cleanData); + var funci = _modules[i].crossTraceDefaults; + // some trace types share crossTraceDefaults (ie histogram2d, histogram2dcontour) + if(funci) Lib.pushUnique(crossTraceDefaultsFuncs, funci); } - for(i = 0; i < cleanDataFuncs.length; i++) { - cleanDataFuncs[i](newFullData, newFullLayout); + for(i = 0; i < crossTraceDefaultsFuncs.length; i++) { + crossTraceDefaultsFuncs[i](newFullData, newFullLayout); } // turn on flag to optimize large splom-only graphs diff --git a/src/traces/histogram/clean_data.js b/src/traces/histogram/cross_trace_defaults.js similarity index 98% rename from src/traces/histogram/clean_data.js rename to src/traces/histogram/cross_trace_defaults.js index 90dfb33b47b..4bffacb4d69 100644 --- a/src/traces/histogram/clean_data.js +++ b/src/traces/histogram/cross_trace_defaults.js @@ -30,7 +30,7 @@ var BINATTRS = { }; // handle bin attrs and relink auto-determined values so fullData is complete -module.exports = function cleanData(fullData, fullLayout) { +module.exports = function crossTraceDefaults(fullData, fullLayout) { var allBinOpts = fullLayout._histogramBinOpts = {}; var isOverlay = fullLayout.barmode === 'overlay'; var i, j, traceOut, traceIn, binDirection, group, binOpts; diff --git a/src/traces/histogram/defaults.js b/src/traces/histogram/defaults.js index 949adacf76f..23ef933ba18 100644 --- a/src/traces/histogram/defaults.js +++ b/src/traces/histogram/defaults.js @@ -52,7 +52,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout if(hasAggregationData) coerce('histfunc'); coerce('histnorm'); - // Note: bin defaults are now handled in Histogram.cleanData + // Note: bin defaults are now handled in Histogram.crossTraceDefaults // autobin(x|y) are only included here to appease Plotly.validate coerce('autobin' + sampleLetter); diff --git a/src/traces/histogram/index.js b/src/traces/histogram/index.js index e1469215b67..b157d73f4be 100644 --- a/src/traces/histogram/index.js +++ b/src/traces/histogram/index.js @@ -28,7 +28,7 @@ var Histogram = {}; Histogram.attributes = require('./attributes'); Histogram.layoutAttributes = require('../bar/layout_attributes'); Histogram.supplyDefaults = require('./defaults'); -Histogram.cleanData = require('./clean_data'); +Histogram.crossTraceDefaults = require('./cross_trace_defaults'); Histogram.supplyLayoutDefaults = require('../bar/layout_defaults'); Histogram.calc = require('./calc'); Histogram.crossTraceCalc = require('../bar/cross_trace_calc').crossTraceCalc; diff --git a/src/traces/histogram2d/clean_data.js b/src/traces/histogram2d/cross_trace_defaults.js similarity index 97% rename from src/traces/histogram2d/clean_data.js rename to src/traces/histogram2d/cross_trace_defaults.js index cff8ddf781c..a32db267f94 100644 --- a/src/traces/histogram2d/clean_data.js +++ b/src/traces/histogram2d/cross_trace_defaults.js @@ -23,7 +23,7 @@ var BINDIRECTIONS = ['x', 'y']; // Handle bin attrs and relink auto-determined values so fullData is complete // does not have cross-trace coupling, but moved out here so we have axis types // and relinked trace._autoBin -module.exports = function cleanData(fullData, fullLayout) { +module.exports = function crossTraceDefaults(fullData, fullLayout) { var i, j, traceOut, binDirection; function coerce(attr) { diff --git a/src/traces/histogram2d/index.js b/src/traces/histogram2d/index.js index 6f3e49860f7..4423cfb0945 100644 --- a/src/traces/histogram2d/index.js +++ b/src/traces/histogram2d/index.js @@ -13,7 +13,7 @@ var Histogram2D = {}; Histogram2D.attributes = require('./attributes'); Histogram2D.supplyDefaults = require('./defaults'); -Histogram2D.cleanData = require('./clean_data'); +Histogram2D.crossTraceDefaults = require('./cross_trace_defaults'); Histogram2D.calc = require('../heatmap/calc'); Histogram2D.plot = require('../heatmap/plot'); Histogram2D.layerName = 'heatmaplayer'; diff --git a/src/traces/histogram2d/sample_defaults.js b/src/traces/histogram2d/sample_defaults.js index 155f717de6f..aca6acf6595 100644 --- a/src/traces/histogram2d/sample_defaults.js +++ b/src/traces/histogram2d/sample_defaults.js @@ -34,7 +34,7 @@ module.exports = function handleSampleDefaults(traceIn, traceOut, coerce, layout if(hasAggregationData) coerce('histfunc'); coerce('histnorm'); - // Note: bin defaults are now handled in Histogram2D.cleanData + // Note: bin defaults are now handled in Histogram2D.crossTraceDefaults // autobin(x|y) are only included here to appease Plotly.validate coerce('autobinx'); coerce('autobiny'); diff --git a/src/traces/histogram2dcontour/index.js b/src/traces/histogram2dcontour/index.js index 99a3679af0e..7e8400d7499 100644 --- a/src/traces/histogram2dcontour/index.js +++ b/src/traces/histogram2dcontour/index.js @@ -13,7 +13,7 @@ var Histogram2dContour = {}; Histogram2dContour.attributes = require('./attributes'); Histogram2dContour.supplyDefaults = require('./defaults'); -Histogram2dContour.cleanData = require('../histogram2d/clean_data'); +Histogram2dContour.crossTraceDefaults = require('../histogram2d/cross_trace_defaults'); Histogram2dContour.calc = require('../contour/calc'); Histogram2dContour.plot = require('../contour/plot').plot; Histogram2dContour.layerName = 'contourlayer'; diff --git a/src/traces/scatter/clean_data.js b/src/traces/scatter/cross_trace_defaults.js similarity index 94% rename from src/traces/scatter/clean_data.js rename to src/traces/scatter/cross_trace_defaults.js index b72ab26eb1a..7f079158165 100644 --- a/src/traces/scatter/clean_data.js +++ b/src/traces/scatter/cross_trace_defaults.js @@ -11,7 +11,7 @@ // remove opacity for any trace that has a fill or is filled to -module.exports = function cleanData(fullData) { +module.exports = function crossTraceDefaults(fullData) { for(var i = 0; i < fullData.length; i++) { var tracei = fullData[i]; if(tracei.type !== 'scatter') continue; diff --git a/src/traces/scatter/index.js b/src/traces/scatter/index.js index 133b54ae32e..82736acb321 100644 --- a/src/traces/scatter/index.js +++ b/src/traces/scatter/index.js @@ -19,7 +19,7 @@ Scatter.isBubble = subtypes.isBubble; Scatter.attributes = require('./attributes'); Scatter.supplyDefaults = require('./defaults'); -Scatter.cleanData = require('./clean_data'); +Scatter.crossTraceDefaults = require('./cross_trace_defaults'); Scatter.calc = require('./calc').calc; Scatter.crossTraceCalc = require('./cross_trace_calc'); Scatter.arraysToCalcdata = require('./arrays_to_calcdata'); diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index 14f8b27d021..99657b2c68a 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -952,7 +952,7 @@ module.exports = { attributes: require('./attributes'), supplyDefaults: require('./defaults'), - cleanData: require('../scatter/clean_data'), + crossTraceDefaults: require('../scatter/cross_trace_defaults'), colorbar: require('../scatter/marker_colorbar'), calc: calc, plot: plot, From 7514592f34d0a1eb290e97d5817902f0cd2d67f3 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 27 Sep 2018 21:26:58 -0400 Subject: [PATCH 09/10] updated test description --- test/jasmine/tests/histogram_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jasmine/tests/histogram_test.js b/test/jasmine/tests/histogram_test.js index 141204b08cc..1d86448b260 100644 --- a/test/jasmine/tests/histogram_test.js +++ b/test/jasmine/tests/histogram_test.js @@ -808,7 +808,7 @@ describe('Test histogram', function() { .then(done); }); - it('autobins all histograms together except `visible: false`', function(done) { + it('autobins all histograms (on the same subplot) together except `visible: false`', function(done) { function _assertBinCenters(expectedCenters) { var centers = gd.calcdata.map(function(cd) { return cd.map(function(cdi) { return cdi.p; }); From 7909f53274f20997104193e136d5f2e939184c00 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 27 Sep 2018 21:42:48 -0400 Subject: [PATCH 10/10] new histogram autobin mock --- test/image/baselines/hist_multi.png | Bin 0 -> 12999 bytes test/image/mocks/hist_multi.json | 22 ++++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 test/image/baselines/hist_multi.png create mode 100644 test/image/mocks/hist_multi.json diff --git a/test/image/baselines/hist_multi.png b/test/image/baselines/hist_multi.png new file mode 100644 index 0000000000000000000000000000000000000000..df9598daf235ac47065a55dec0caa7ed64186b4e GIT binary patch literal 12999 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 PDjdd!W{2}&=db@8kqN{5 literal 0 HcmV?d00001 diff --git a/test/image/mocks/hist_multi.json b/test/image/mocks/hist_multi.json new file mode 100644 index 00000000000..05ef17e5ad3 --- /dev/null +++ b/test/image/mocks/hist_multi.json @@ -0,0 +1,22 @@ +{ + "data":[{ + "x": [1, 1, 1, 2, 2], + "type": "histogram" + }, { + "x": [20, 20, 21, 21], + "type": "histogram" + }, { + "x": [1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 7], + "type": "histogram", + "xaxis": "x2" + }, { + "x": [6, 6.1, 6.2], + "type": "histogram", + "xaxis": "x2" + }], + "layout": { + "height": 400, "width": 500, + "barmode": "stack", + "grid": {"rows": 1, "columns": 2} + } +}