Skip to content

Commit 5f517cf

Browse files
authored
Merge pull request #1150 from plotly/date-hist-bins
make histogram bins work with date strings
2 parents 7c9b538 + 4e0490f commit 5f517cf

File tree

11 files changed

+148
-152
lines changed

11 files changed

+148
-152
lines changed

src/plots/cartesian/axes.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -518,15 +518,17 @@ axes.autoBin = function(data, ax, nbins, is2d) {
518518
if(ax.type === 'log') {
519519
dummyax = {
520520
type: 'linear',
521-
range: [datamin, datamax]
521+
range: [datamin, datamax],
522+
r2l: Number
522523
};
523524
}
524525
else {
525526
dummyax = {
526527
type: ax.type,
527528
// conversion below would be ax.c2r but that's only different from l2r
528529
// for log, and this is the only place (so far?) we would want c2r.
529-
range: [datamin, datamax].map(ax.l2r)
530+
range: [datamin, datamax].map(ax.l2r),
531+
r2l: ax.r2l
530532
};
531533
}
532534

@@ -593,8 +595,8 @@ axes.autoBin = function(data, ax, nbins, is2d) {
593595
}
594596

595597
return {
596-
start: binstart,
597-
end: binend,
598+
start: ax.c2r(binstart),
599+
end: ax.c2r(binend),
598600
size: dummyax.dtick
599601
};
600602
};

src/plots/cartesian/set_convert.js

+3
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,9 @@ module.exports = function setConvert(ax) {
237237
ax.r2p = function(v, clip) { return ax.l2p(ax.r2l(v, clip)); };
238238
ax.p2r = function(px) { return ax.l2r(ax.p2l(px)); };
239239

240+
ax.r2c = function(v) { return ax.l2c(ax.r2l(v)); };
241+
ax.c2r = function(v) { return ax.l2r(ax.c2l(v)); };
242+
240243
if(['linear', 'log', '-'].indexOf(ax.type) !== -1) {
241244
ax.c2d = num;
242245
ax.d2c = Lib.cleanNumber;

src/traces/heatmap/calc.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,9 @@ module.exports = function calc(gd, trace) {
108108

109109
// create arrays of brick boundaries, to be used by autorange and heatmap.plot
110110
var xlen = maxRowLength(z),
111-
xIn = trace.xtype === 'scaled' ? '' : trace.x,
111+
xIn = trace.xtype === 'scaled' ? '' : x,
112112
xArray = makeBoundArray(trace, xIn, x0, dx, xlen, xa),
113-
yIn = trace.ytype === 'scaled' ? '' : trace.y,
113+
yIn = trace.ytype === 'scaled' ? '' : y,
114114
yArray = makeBoundArray(trace, yIn, y0, dy, z.length, ya);
115115

116116
// handled in gl2d convert step
@@ -180,7 +180,6 @@ function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, ax) {
180180
var isArrayOfTwoItemsOrMore = Array.isArray(arrayIn) && arrayIn.length > 1;
181181

182182
if(isArrayOfTwoItemsOrMore && !isHist && (ax.type !== 'category')) {
183-
arrayIn = arrayIn.map(ax.d2c);
184183
var len = arrayIn.length;
185184

186185
// given vals are brick centers
@@ -223,7 +222,7 @@ function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, ax) {
223222
else {
224223
dv = dvIn || 1;
225224

226-
if(isHist || ax.type === 'category') v0 = v0In || 0;
225+
if(isHist || ax.type === 'category') v0 = ax.r2c(v0In) || 0;
227226
else if(Array.isArray(arrayIn) && arrayIn.length === 1) v0 = arrayIn[0];
228227
else if(v0In === undefined) v0 = 0;
229228
else v0 = ax.d2c(v0In);

src/traces/histogram/attributes.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ module.exports = {
7373

7474
autobinx: {
7575
valType: 'boolean',
76-
dflt: true,
76+
dflt: null,
7777
role: 'style',
7878
description: [
7979
'Determines whether or not the x axis bin attributes are picked',
@@ -97,7 +97,7 @@ module.exports = {
9797

9898
autobiny: {
9999
valType: 'boolean',
100-
dflt: true,
100+
dflt: null,
101101
role: 'style',
102102
description: [
103103
'Determines whether or not the y axis bin attributes are picked',
@@ -132,7 +132,7 @@ module.exports = {
132132
function makeBinsAttr(axLetter) {
133133
return {
134134
start: {
135-
valType: 'number',
135+
valType: 'any', // for date axes
136136
dflt: null,
137137
role: 'style',
138138
description: [
@@ -141,7 +141,7 @@ function makeBinsAttr(axLetter) {
141141
].join(' ')
142142
},
143143
end: {
144-
valType: 'number',
144+
valType: 'any', // for date axes
145145
dflt: null,
146146
role: 'style',
147147
description: [
@@ -151,7 +151,7 @@ function makeBinsAttr(axLetter) {
151151
},
152152
size: {
153153
valType: 'any', // for date axes
154-
dflt: 1,
154+
dflt: null,
155155
role: 'style',
156156
description: [
157157
'Sets the step in-between value each', axLetter,

src/traces/histogram/bin_defaults.js

+11-8
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@ module.exports = function handleBinDefaults(traceIn, traceOut, coerce, binDirect
1414
coerce('histnorm');
1515

1616
binDirections.forEach(function(binDirection) {
17-
// data being binned - note that even though it's a little weird,
18-
// it's possible to have bins without data, if there's inferred data
19-
var binstrt = coerce(binDirection + 'bins.start'),
20-
binend = coerce(binDirection + 'bins.end'),
21-
autobin = coerce('autobin' + binDirection, !(binstrt && binend));
22-
23-
if(autobin) coerce('nbins' + binDirection);
24-
else coerce(binDirection + 'bins.size');
17+
/*
18+
* Because date axes have string values for start and end,
19+
* and string options for size, we cannot validate these attributes
20+
* now. We will do this during calc (immediately prior to binning)
21+
* in ./clean_bins, and push the cleaned values back to _fullData.
22+
*/
23+
coerce(binDirection + 'bins.start');
24+
coerce(binDirection + 'bins.end');
25+
coerce(binDirection + 'bins.size');
26+
coerce('autobin' + binDirection);
27+
coerce('nbins' + binDirection);
2528
});
2629

2730
return traceOut;

src/traces/histogram/calc.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ var Axes = require('../../plots/cartesian/axes');
1717
var binFunctions = require('./bin_functions');
1818
var normFunctions = require('./norm_functions');
1919
var doAvg = require('./average');
20+
var cleanBins = require('./clean_bins');
2021

2122

2223
module.exports = function calc(gd, trace) {
@@ -33,6 +34,8 @@ module.exports = function calc(gd, trace) {
3334
maindata = trace.orientation === 'h' ? 'y' : 'x',
3435
counterdata = {x: 'y', y: 'x'}[maindata];
3536

37+
cleanBins(trace, pa, maindata);
38+
3639
// prepare the raw data
3740
var pos0 = pa.makeCalcdata(trace, maindata);
3841
// calculate the bins
@@ -71,10 +74,11 @@ module.exports = function calc(gd, trace) {
7174

7275
// create the bins (and any extra arrays needed)
7376
// assume more than 5000 bins is an error, so we don't crash the browser
74-
i = binspec.start;
77+
i = pa.r2c(binspec.start);
78+
7579
// decrease end a little in case of rounding errors
76-
binend = binspec.end +
77-
(binspec.start - Axes.tickIncrement(binspec.start, binspec.size)) / 1e6;
80+
binend = pa.r2c(binspec.end) + (i - Axes.tickIncrement(i, binspec.size)) / 1e6;
81+
7882
while(i < binend && pos.length < 5000) {
7983
i2 = Axes.tickIncrement(i, binspec.size);
8084
pos.push((i + i2) / 2);

src/traces/histogram/clean_bins.js

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* Copyright 2012-2016, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
10+
'use strict';
11+
var isNumeric = require('fast-isnumeric');
12+
var cleanDate = require('../../lib').cleanDate;
13+
var ONEDAY = require('../../constants/numerical').ONEDAY;
14+
15+
/*
16+
* cleanBins: validate attributes autobin[xy] and [xy]bins.(start, end, size)
17+
* Mutates trace so all these attributes are valid.
18+
*
19+
* Normally this kind of thing would happen during supplyDefaults, but
20+
* in this case we need to know the axis type, and axis type isn't set until
21+
* after trace supplyDefaults are completed. So this gets called during the
22+
* calc step, when data are inserted into bins.
23+
*/
24+
module.exports = function cleanBins(trace, ax, binDirection) {
25+
var axType = ax.type,
26+
binAttr = binDirection + 'bins',
27+
bins = trace[binAttr];
28+
29+
if(!bins) bins = trace[binAttr] = {};
30+
31+
var cleanBound = (axType === 'date') ?
32+
function(v) { return (v || v === 0) ? cleanDate(v) : null; } :
33+
function(v) { return isNumeric(v) ? Number(v) : null; };
34+
35+
bins.start = cleanBound(bins.start);
36+
bins.end = cleanBound(bins.end);
37+
38+
// logic for bin size is very similar to dtick (cartesian/tick_value_defaults)
39+
// but without the extra string options for log axes
40+
// ie the only strings we accept are M<n> for months
41+
var sizeDflt = (axType === 'date') ? ONEDAY : 1,
42+
binSize = bins.size;
43+
44+
if(isNumeric(binSize)) {
45+
bins.size = (binSize > 0) ? Number(binSize) : sizeDflt;
46+
}
47+
else if(typeof binSize !== 'string') {
48+
bins.size = sizeDflt;
49+
}
50+
else {
51+
// date special case: "M<n>" gives bins every (integer) n months
52+
var prefix = binSize.charAt(0),
53+
sizeNum = binSize.substr(1);
54+
55+
sizeNum = isNumeric(sizeNum) ? Number(sizeNum) : 0;
56+
if((sizeNum <= 0) || !(
57+
axType === 'date' && prefix === 'M' && sizeNum === Math.round(sizeNum)
58+
)) {
59+
bins.size = sizeDflt;
60+
}
61+
}
62+
63+
var autoBinAttr = 'autobin' + binDirection;
64+
65+
if(typeof trace[autoBinAttr] !== 'boolean') {
66+
trace[autoBinAttr] = !(
67+
(bins.start || bins.start === 0) &&
68+
(bins.end || bins.end === 0)
69+
);
70+
}
71+
72+
if(!trace[autoBinAttr]) delete trace['nbins' + binDirection];
73+
};

src/traces/histogram2d/calc.js

+24-16
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ var Axes = require('../../plots/cartesian/axes');
1515
var binFunctions = require('../histogram/bin_functions');
1616
var normFunctions = require('../histogram/norm_functions');
1717
var doAvg = require('../histogram/average');
18+
var cleanBins = require('../histogram/clean_bins');
1819

1920

2021
module.exports = function calc(gd, trace) {
@@ -29,6 +30,9 @@ module.exports = function calc(gd, trace) {
2930
z,
3031
i;
3132

33+
cleanBins(trace, xa, 'x');
34+
cleanBins(trace, ya, 'y');
35+
3236
var serieslen = Math.min(x.length, y.length);
3337
if(x.length > serieslen) x.splice(serieslen, x.length - serieslen);
3438
if(y.length > serieslen) y.splice(serieslen, y.length - serieslen);
@@ -38,8 +42,10 @@ module.exports = function calc(gd, trace) {
3842
if(trace.autobinx || !('xbins' in trace)) {
3943
trace.xbins = Axes.autoBin(x, xa, trace.nbinsx, '2d');
4044
if(trace.type === 'histogram2dcontour') {
41-
trace.xbins.start -= trace.xbins.size;
42-
trace.xbins.end += trace.xbins.size;
45+
// the "true" last argument reverses the tick direction (which we can't
46+
// just do with a minus sign because of month bins)
47+
trace.xbins.start = xa.c2r(Axes.tickIncrement(xa.r2c(trace.xbins.start), trace.xbins.size, true));
48+
trace.xbins.end = xa.c2r(Axes.tickIncrement(xa.r2c(trace.xbins.end), trace.xbins.size));
4349
}
4450

4551
// copy bin info back to the source data.
@@ -48,8 +54,8 @@ module.exports = function calc(gd, trace) {
4854
if(trace.autobiny || !('ybins' in trace)) {
4955
trace.ybins = Axes.autoBin(y, ya, trace.nbinsy, '2d');
5056
if(trace.type === 'histogram2dcontour') {
51-
trace.ybins.start -= trace.ybins.size;
52-
trace.ybins.end += trace.ybins.size;
57+
trace.ybins.start = ya.c2r(Axes.tickIncrement(ya.r2c(trace.ybins.start), trace.ybins.size, true));
58+
trace.ybins.end = ya.c2r(Axes.tickIncrement(ya.r2c(trace.ybins.end), trace.ybins.size));
5359
}
5460
trace._input.ybins = trace.ybins;
5561
}
@@ -91,11 +97,11 @@ module.exports = function calc(gd, trace) {
9197

9298
// decrease end a little in case of rounding errors
9399
var binspec = trace.xbins,
94-
binend = binspec.end +
95-
(binspec.start - Axes.tickIncrement(binspec.start, binspec.size)) / 1e6;
100+
binStart = xa.r2c(binspec.start),
101+
binEnd = xa.r2c(binspec.end) +
102+
(binStart - Axes.tickIncrement(binStart, binspec.size)) / 1e6;
96103

97-
for(i = binspec.start; i < binend;
98-
i = Axes.tickIncrement(i, binspec.size)) {
104+
for(i = binStart; i < binEnd; i = Axes.tickIncrement(i, binspec.size)) {
99105
onecol.push(sizeinit);
100106
if(Array.isArray(xbins)) xbins.push(i);
101107
if(doavg) zerocol.push(0);
@@ -104,15 +110,16 @@ module.exports = function calc(gd, trace) {
104110

105111
var nx = onecol.length;
106112
x0 = trace.xbins.start;
107-
dx = (i - x0) / nx;
108-
x0 += dx / 2;
113+
var x0c = xa.r2c(x0);
114+
dx = (i - x0c) / nx;
115+
x0 = xa.c2r(x0c + dx / 2);
109116

110117
binspec = trace.ybins;
111-
binend = binspec.end +
112-
(binspec.start - Axes.tickIncrement(binspec.start, binspec.size)) / 1e6;
118+
binStart = ya.r2c(binspec.start);
119+
binEnd = ya.r2c(binspec.end) +
120+
(binStart - Axes.tickIncrement(binStart, binspec.size)) / 1e6;
113121

114-
for(i = binspec.start; i < binend;
115-
i = Axes.tickIncrement(i, binspec.size)) {
122+
for(i = binStart; i < binEnd; i = Axes.tickIncrement(i, binspec.size)) {
116123
z.push(onecol.concat());
117124
if(Array.isArray(ybins)) ybins.push(i);
118125
if(doavg) counts.push(zerocol.concat());
@@ -121,8 +128,9 @@ module.exports = function calc(gd, trace) {
121128

122129
var ny = z.length;
123130
y0 = trace.ybins.start;
124-
dy = (i - y0) / ny;
125-
y0 += dy / 2;
131+
var y0c = ya.r2c(y0);
132+
dy = (i - y0c) / ny;
133+
y0 = ya.c2r(y0c + dy / 2);
126134

127135
if(densitynorm) {
128136
xinc = onecol.map(function(v, i) {
-5.12 KB
Loading

0 commit comments

Comments
 (0)