diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index b3dbcc63626..27359e06df9 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -518,7 +518,8 @@ axes.autoBin = function(data, ax, nbins, is2d) { if(ax.type === 'log') { dummyax = { type: 'linear', - range: [datamin, datamax] + range: [datamin, datamax], + r2l: Number }; } else { @@ -526,7 +527,8 @@ axes.autoBin = function(data, ax, nbins, is2d) { type: ax.type, // conversion below would be ax.c2r but that's only different from l2r // for log, and this is the only place (so far?) we would want c2r. - range: [datamin, datamax].map(ax.l2r) + range: [datamin, datamax].map(ax.l2r), + r2l: ax.r2l }; } @@ -593,8 +595,8 @@ axes.autoBin = function(data, ax, nbins, is2d) { } return { - start: binstart, - end: binend, + start: ax.c2r(binstart), + end: ax.c2r(binend), size: dummyax.dtick }; }; diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index 534e380c7a2..e4fae71290f 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -237,6 +237,9 @@ module.exports = function setConvert(ax) { ax.r2p = function(v, clip) { return ax.l2p(ax.r2l(v, clip)); }; ax.p2r = function(px) { return ax.l2r(ax.p2l(px)); }; + ax.r2c = function(v) { return ax.l2c(ax.r2l(v)); }; + ax.c2r = function(v) { return ax.l2r(ax.c2l(v)); }; + if(['linear', 'log', '-'].indexOf(ax.type) !== -1) { ax.c2d = num; ax.d2c = Lib.cleanNumber; diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js index ba9aeee2c0a..29d878f5aee 100644 --- a/src/traces/heatmap/calc.js +++ b/src/traces/heatmap/calc.js @@ -108,9 +108,9 @@ module.exports = function calc(gd, trace) { // create arrays of brick boundaries, to be used by autorange and heatmap.plot var xlen = maxRowLength(z), - xIn = trace.xtype === 'scaled' ? '' : trace.x, + xIn = trace.xtype === 'scaled' ? '' : x, xArray = makeBoundArray(trace, xIn, x0, dx, xlen, xa), - yIn = trace.ytype === 'scaled' ? '' : trace.y, + yIn = trace.ytype === 'scaled' ? '' : y, yArray = makeBoundArray(trace, yIn, y0, dy, z.length, ya); // handled in gl2d convert step @@ -180,7 +180,6 @@ function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, ax) { var isArrayOfTwoItemsOrMore = Array.isArray(arrayIn) && arrayIn.length > 1; if(isArrayOfTwoItemsOrMore && !isHist && (ax.type !== 'category')) { - arrayIn = arrayIn.map(ax.d2c); var len = arrayIn.length; // given vals are brick centers @@ -223,7 +222,7 @@ function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, ax) { else { dv = dvIn || 1; - if(isHist || ax.type === 'category') v0 = v0In || 0; + if(isHist || ax.type === 'category') v0 = ax.r2c(v0In) || 0; else if(Array.isArray(arrayIn) && arrayIn.length === 1) v0 = arrayIn[0]; else if(v0In === undefined) v0 = 0; else v0 = ax.d2c(v0In); diff --git a/src/traces/histogram/attributes.js b/src/traces/histogram/attributes.js index 757c923c190..1cce4a02457 100644 --- a/src/traces/histogram/attributes.js +++ b/src/traces/histogram/attributes.js @@ -73,7 +73,7 @@ module.exports = { autobinx: { valType: 'boolean', - dflt: true, + dflt: null, role: 'style', description: [ 'Determines whether or not the x axis bin attributes are picked', @@ -97,7 +97,7 @@ module.exports = { autobiny: { valType: 'boolean', - dflt: true, + dflt: null, role: 'style', description: [ 'Determines whether or not the y axis bin attributes are picked', @@ -135,7 +135,7 @@ module.exports = { function makeBinsAttr(axLetter) { return { start: { - valType: 'number', + valType: 'any', // for date axes dflt: null, role: 'style', description: [ @@ -144,7 +144,7 @@ function makeBinsAttr(axLetter) { ].join(' ') }, end: { - valType: 'number', + valType: 'any', // for date axes dflt: null, role: 'style', description: [ @@ -154,7 +154,7 @@ function makeBinsAttr(axLetter) { }, size: { valType: 'any', // for date axes - dflt: 1, + dflt: null, role: 'style', description: [ 'Sets the step in-between value each', axLetter, diff --git a/src/traces/histogram/bin_defaults.js b/src/traces/histogram/bin_defaults.js index 3c588989b9a..cbc53f03377 100644 --- a/src/traces/histogram/bin_defaults.js +++ b/src/traces/histogram/bin_defaults.js @@ -14,14 +14,17 @@ module.exports = function handleBinDefaults(traceIn, traceOut, coerce, binDirect coerce('histnorm'); binDirections.forEach(function(binDirection) { - // data being binned - note that even though it's a little weird, - // it's possible to have bins without data, if there's inferred data - var binstrt = coerce(binDirection + 'bins.start'), - binend = coerce(binDirection + 'bins.end'), - autobin = coerce('autobin' + binDirection, !(binstrt && binend)); - - if(autobin) coerce('nbins' + binDirection); - else coerce(binDirection + 'bins.size'); + /* + * 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'); + coerce('autobin' + binDirection); + coerce('nbins' + binDirection); }); return traceOut; diff --git a/src/traces/histogram/calc.js b/src/traces/histogram/calc.js index 2053dc36321..5b43a8ad36c 100644 --- a/src/traces/histogram/calc.js +++ b/src/traces/histogram/calc.js @@ -17,6 +17,7 @@ var Axes = require('../../plots/cartesian/axes'); var binFunctions = require('./bin_functions'); var normFunctions = require('./norm_functions'); var doAvg = require('./average'); +var cleanBins = require('./clean_bins'); module.exports = function calc(gd, trace) { @@ -33,6 +34,8 @@ module.exports = function calc(gd, trace) { maindata = trace.orientation === 'h' ? 'y' : 'x', counterdata = {x: 'y', y: 'x'}[maindata]; + cleanBins(trace, pa, maindata); + // prepare the raw data var pos0 = pa.makeCalcdata(trace, maindata); // calculate the bins @@ -71,10 +74,11 @@ module.exports = function calc(gd, trace) { // create the bins (and any extra arrays needed) // assume more than 5000 bins is an error, so we don't crash the browser - i = binspec.start; + i = pa.r2c(binspec.start); + // decrease end a little in case of rounding errors - binend = binspec.end + - (binspec.start - Axes.tickIncrement(binspec.start, binspec.size)) / 1e6; + binend = pa.r2c(binspec.end) + (i - Axes.tickIncrement(i, binspec.size)) / 1e6; + while(i < binend && pos.length < 5000) { i2 = Axes.tickIncrement(i, binspec.size); pos.push((i + i2) / 2); diff --git a/src/traces/histogram/clean_bins.js b/src/traces/histogram/clean_bins.js new file mode 100644 index 00000000000..d6c4d00e5d7 --- /dev/null +++ b/src/traces/histogram/clean_bins.js @@ -0,0 +1,73 @@ +/** +* Copyright 2012-2016, 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 ONEDAY = require('../../constants/numerical').ONEDAY; + +/* + * 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) : 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] = !( + (bins.start || bins.start === 0) && + (bins.end || bins.end === 0) + ); + } + + if(!trace[autoBinAttr]) delete trace['nbins' + binDirection]; +}; diff --git a/src/traces/histogram2d/calc.js b/src/traces/histogram2d/calc.js index b6d24535930..79a42406f61 100644 --- a/src/traces/histogram2d/calc.js +++ b/src/traces/histogram2d/calc.js @@ -15,6 +15,7 @@ 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'); module.exports = function calc(gd, trace) { @@ -29,6 +30,9 @@ module.exports = function calc(gd, trace) { z, i; + cleanBins(trace, xa, 'x'); + cleanBins(trace, ya, 'y'); + var serieslen = Math.min(x.length, y.length); if(x.length > serieslen) x.splice(serieslen, x.length - serieslen); if(y.length > serieslen) y.splice(serieslen, y.length - serieslen); @@ -38,8 +42,10 @@ module.exports = function calc(gd, trace) { if(trace.autobinx || !('xbins' in trace)) { trace.xbins = Axes.autoBin(x, xa, trace.nbinsx, '2d'); if(trace.type === 'histogram2dcontour') { - trace.xbins.start -= trace.xbins.size; - trace.xbins.end += trace.xbins.size; + // the "true" last argument reverses the tick direction (which we can't + // just do with a minus sign because of month bins) + trace.xbins.start = xa.c2r(Axes.tickIncrement(xa.r2c(trace.xbins.start), trace.xbins.size, true)); + trace.xbins.end = xa.c2r(Axes.tickIncrement(xa.r2c(trace.xbins.end), trace.xbins.size)); } // copy bin info back to the source data. @@ -48,8 +54,8 @@ module.exports = function calc(gd, trace) { if(trace.autobiny || !('ybins' in trace)) { trace.ybins = Axes.autoBin(y, ya, trace.nbinsy, '2d'); if(trace.type === 'histogram2dcontour') { - trace.ybins.start -= trace.ybins.size; - trace.ybins.end += trace.ybins.size; + trace.ybins.start = ya.c2r(Axes.tickIncrement(ya.r2c(trace.ybins.start), trace.ybins.size, true)); + trace.ybins.end = ya.c2r(Axes.tickIncrement(ya.r2c(trace.ybins.end), trace.ybins.size)); } trace._input.ybins = trace.ybins; } @@ -91,11 +97,11 @@ module.exports = function calc(gd, trace) { // decrease end a little in case of rounding errors var binspec = trace.xbins, - binend = binspec.end + - (binspec.start - Axes.tickIncrement(binspec.start, binspec.size)) / 1e6; + binStart = xa.r2c(binspec.start), + binEnd = xa.r2c(binspec.end) + + (binStart - Axes.tickIncrement(binStart, binspec.size)) / 1e6; - for(i = binspec.start; i < binend; - i = Axes.tickIncrement(i, binspec.size)) { + for(i = binStart; i < binEnd; i = Axes.tickIncrement(i, binspec.size)) { onecol.push(sizeinit); if(Array.isArray(xbins)) xbins.push(i); if(doavg) zerocol.push(0); @@ -104,15 +110,16 @@ module.exports = function calc(gd, trace) { var nx = onecol.length; x0 = trace.xbins.start; - dx = (i - x0) / nx; - x0 += dx / 2; + var x0c = xa.r2c(x0); + dx = (i - x0c) / nx; + x0 = xa.c2r(x0c + dx / 2); binspec = trace.ybins; - binend = binspec.end + - (binspec.start - Axes.tickIncrement(binspec.start, binspec.size)) / 1e6; + binStart = ya.r2c(binspec.start); + binEnd = ya.r2c(binspec.end) + + (binStart - Axes.tickIncrement(binStart, binspec.size)) / 1e6; - for(i = binspec.start; i < binend; - i = Axes.tickIncrement(i, binspec.size)) { + for(i = binStart; i < binEnd; i = Axes.tickIncrement(i, binspec.size)) { z.push(onecol.concat()); if(Array.isArray(ybins)) ybins.push(i); if(doavg) counts.push(zerocol.concat()); @@ -121,8 +128,9 @@ module.exports = function calc(gd, trace) { var ny = z.length; y0 = trace.ybins.start; - dy = (i - y0) / ny; - y0 += dy / 2; + var y0c = ya.r2c(y0); + dy = (i - y0c) / ny; + y0 = ya.c2r(y0c + dy / 2); if(densitynorm) { xinc = onecol.map(function(v, i) { diff --git a/test/image/baselines/date_histogram.png b/test/image/baselines/date_histogram.png index 9d38b322d04..e6b6bed8a9a 100644 Binary files a/test/image/baselines/date_histogram.png and b/test/image/baselines/date_histogram.png differ diff --git a/test/image/mocks/date_histogram.json b/test/image/mocks/date_histogram.json index 90d4daf6a3d..d7131da10da 100644 --- a/test/image/mocks/date_histogram.json +++ b/test/image/mocks/date_histogram.json @@ -13,8 +13,8 @@ "autobinx": false, "nbinsx": 3, "xbins": { - "start": 1325394000000, - "end": 1333252800000, + "start": "2011-12-16", + "end": "2012-03-16", "size": "M1" }, "autobiny": true, @@ -26,117 +26,32 @@ ], "layout": { "title": "Click to enter Plot title", - "titlefont": { - "family": "", - "size": 0, - "color": "" - }, "font": { "family": "\"Open sans\", verdana, arial, sans-serif", "size": 12, "color": "#444" }, "showlegend": true, - "autosize": true, - "width": 841, - "height": 662, + "width": 600, + "height": 400, "xaxis": { "title": "month", - "titlefont": { - "family": "", - "size": 0, - "color": "" - }, - "range": [ - 1325438100000, - 1333210500000 - ], - "domain": [ - 0, - 1 - ], - "type": "date", - "rangemode": "normal", - "autorange": true, "showgrid": false, "zeroline": false, "showline": false, - "autotick": true, - "nticks": 41, "ticks": "", "showticklabels": true, - "tick0": 946789200000, - "dtick": 259200000, - "ticklen": 5, - "tickwidth": 1, "tickcolor": "rgb(127,127,127)", - "tickangle": "auto", - "tickfont": { - "family": "", - "size": 0, - "color": "" - }, - "exponentformat": "B", - "showexponent": "all", - "mirror": false, - "gridcolor": "rgb(255,255,255)", - "gridwidth": 1, - "zerolinecolor": "#444", - "zerolinewidth": 1, - "linecolor": "#444", - "linewidth": 1, - "anchor": "y", - "overlaying": false, - "position": 0 + "gridcolor": "rgb(255,255,255)" }, "yaxis": { "title": "count", - "titlefont": { - "family": "", - "size": 0, - "color": "" - }, - "range": [ - 0, - 3.1578947368421053 - ], - "domain": [ - 0, - 1 - ], - "type": "linear", - "rangemode": "normal", - "autorange": true, "showgrid": true, "zeroline": true, "showline": false, - "autotick": true, - "nticks": 0, "ticks": "", - "showticklabels": true, - "tick0": 0, - "dtick": 0.5, - "ticklen": 5, - "tickwidth": 1, "tickcolor": "rgb(127,127,127)", - "tickangle": "auto", - "tickfont": { - "family": "", - "size": 0, - "color": "" - }, - "exponentformat": "B", - "showexponent": "all", - "mirror": false, - "gridcolor": "rgb(255,255,255)", - "gridwidth": 1, - "zerolinecolor": "#444", - "zerolinewidth": 1, - "linecolor": "#444", - "linewidth": 1, - "anchor": "x", - "overlaying": false, - "position": 0 + "gridcolor": "rgb(255,255,255)" }, "legend": { "x": 100, @@ -151,23 +66,9 @@ "bordercolor": "transparent", "borderwidth": 0 }, - "margin": { - "l": 80, - "r": 10, - "b": 80, - "t": 100, - "pad": 0, - "autoexpand": true - }, - "paper_bgcolor": "#fff", "plot_bgcolor": "rgb(229,229,229)", - "hovermode": "x", - "dragmode": "zoom", - "separators": ".,", "barmode": "stack", "bargap": 0.2, - "bargroupgap": 0, - "boxmode": "overlay", - "hidesources": false + "bargroupgap": 0 } } diff --git a/test/jasmine/tests/histogram_test.js b/test/jasmine/tests/histogram_test.js index 7dea7663ba5..3a441fbb7a0 100644 --- a/test/jasmine/tests/histogram_test.js +++ b/test/jasmine/tests/histogram_test.js @@ -84,7 +84,10 @@ describe('Test histogram', function() { }); - it('should set autobinx to false if xbins is supplied and true if not', function() { + // coercing bin attributes got moved to calc because it needs + // axis type - so here we just test that it's NOT happening + + it('should not coerce autobinx regardless of xbins', function() { traceIn = { x: [1, 2, 2], xbins: { @@ -94,16 +97,16 @@ describe('Test histogram', function() { } }; supplyDefaults(traceIn, traceOut); - expect(traceOut.autobinx).toBe(false); + expect(traceOut.autobinx).toBeUndefined(); traceIn = { x: [1, 2, 2] }; supplyDefaults(traceIn, traceOut); - expect(traceOut.autobinx).toBe(true); + expect(traceOut.autobinx).toBeUndefined(); }); - it('should set autobiny to false if ybins is supplied and true if not', function() { + it('should not coerce autobiny regardless of ybins', function() { traceIn = { y: [1, 2, 2], ybins: { @@ -113,13 +116,13 @@ describe('Test histogram', function() { } }; supplyDefaults(traceIn, traceOut); - expect(traceOut.autobiny).toBe(false); + expect(traceOut.autobiny).toBeUndefined(); traceIn = { y: [1, 2, 2] }; supplyDefaults(traceIn, traceOut); - expect(traceOut.autobiny).toBe(true); + expect(traceOut.autobiny).toBeUndefined(); }); });