Skip to content

Commit

Permalink
Merge pull request #1150 from plotly/date-hist-bins
Browse files Browse the repository at this point in the history
make histogram bins work with date strings
  • Loading branch information
alexcjohnson authored Nov 15, 2016
2 parents 7c9b538 + 4e0490f commit 5f517cf
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 152 deletions.
10 changes: 6 additions & 4 deletions src/plots/cartesian/axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -518,15 +518,17 @@ axes.autoBin = function(data, ax, nbins, is2d) {
if(ax.type === 'log') {
dummyax = {
type: 'linear',
range: [datamin, datamax]
range: [datamin, datamax],
r2l: Number
};
}
else {
dummyax = {
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
};
}

Expand Down Expand Up @@ -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
};
};
Expand Down
3 changes: 3 additions & 0 deletions src/plots/cartesian/set_convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 3 additions & 4 deletions src/traces/heatmap/calc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 5 additions & 5 deletions src/traces/histogram/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -132,7 +132,7 @@ module.exports = {
function makeBinsAttr(axLetter) {
return {
start: {
valType: 'number',
valType: 'any', // for date axes
dflt: null,
role: 'style',
description: [
Expand All @@ -141,7 +141,7 @@ function makeBinsAttr(axLetter) {
].join(' ')
},
end: {
valType: 'number',
valType: 'any', // for date axes
dflt: null,
role: 'style',
description: [
Expand All @@ -151,7 +151,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,
Expand Down
19 changes: 11 additions & 8 deletions src/traces/histogram/bin_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 7 additions & 3 deletions src/traces/histogram/calc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down
73 changes: 73 additions & 0 deletions src/traces/histogram/clean_bins.js
Original file line number Diff line number Diff line change
@@ -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<n> 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<n>" 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];
};
40 changes: 24 additions & 16 deletions src/traces/histogram2d/calc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
Expand All @@ -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.
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
Expand All @@ -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());
Expand All @@ -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) {
Expand Down
Binary file modified test/image/baselines/date_histogram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 5f517cf

Please sign in to comment.