Skip to content

Commit 30ed4a4

Browse files
authored
Merge pull request #3044 from plotly/histogram-autobin
Histogram autobin
2 parents 48209a0 + 7909f53 commit 30ed4a4

35 files changed

+1058
-527
lines changed

src/lib/dates.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,9 @@ function includeTime(dateStr, h, m, s, msec10) {
345345
// a Date object or milliseconds
346346
// optional dflt is the return value if cleaning fails
347347
exports.cleanDate = function(v, dflt, calendar) {
348-
if(exports.isJSDate(v) || typeof v === 'number') {
348+
// let us use cleanDate to provide a missing default without an error
349+
if(v === BADNUM) return dflt;
350+
if(exports.isJSDate(v) || (typeof v === 'number' && isFinite(v))) {
349351
// do not allow milliseconds (old) or jsdate objects (inherently
350352
// described as gregorian dates) with world calendars
351353
if(isWorldCalendar(calendar)) {

src/plot_api/helpers.js

+13
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,19 @@ exports.cleanData = function(data) {
386386
// sanitize rgb(fractions) and rgba(fractions) that old tinycolor
387387
// supported, but new tinycolor does not because they're not valid css
388388
Color.clean(trace);
389+
390+
// remove obsolete autobin(x|y) attributes, but only if true
391+
// if false, this needs to happen in Histogram.calc because it
392+
// can be a one-time autobin so we need to know the results before
393+
// we can push them back into the trace.
394+
if(trace.autobinx) {
395+
delete trace.autobinx;
396+
delete trace.xbins;
397+
}
398+
if(trace.autobiny) {
399+
delete trace.autobiny;
400+
delete trace.ybins;
401+
}
389402
}
390403
};
391404

src/plot_api/plot_api.js

+28-5
Original file line numberDiff line numberDiff line change
@@ -1434,6 +1434,18 @@ function _restyle(gd, aobj, traces) {
14341434
}
14351435
}
14361436

1437+
function allBins(binAttr) {
1438+
return function(j) {
1439+
return fullData[j][binAttr];
1440+
};
1441+
}
1442+
1443+
function arrayBins(binAttr) {
1444+
return function(vij, j) {
1445+
return vij === false ? fullData[traces[j]][binAttr] : null;
1446+
};
1447+
}
1448+
14371449
// now make the changes to gd.data (and occasionally gd.layout)
14381450
// and figure out what kind of graphics update we need to do
14391451
for(var ai in aobj) {
@@ -1449,6 +1461,17 @@ function _restyle(gd, aobj, traces) {
14491461
newVal,
14501462
valObject;
14511463

1464+
// Backward compatibility shim for turning histogram autobin on,
1465+
// or freezing previous autobinned values.
1466+
// Replace obsolete `autobin(x|y): true` with `(x|y)bins: null`
1467+
// and `autobin(x|y): false` with the `(x|y)bins` in `fullData`
1468+
if(ai === 'autobinx' || ai === 'autobiny') {
1469+
ai = ai.charAt(ai.length - 1) + 'bins';
1470+
if(Array.isArray(vi)) vi = vi.map(arrayBins(ai));
1471+
else if(vi === false) vi = traces.map(allBins(ai));
1472+
else vi = null;
1473+
}
1474+
14521475
redoit[ai] = vi;
14531476

14541477
if(ai.substr(0, 6) === 'LAYOUT') {
@@ -1609,8 +1632,12 @@ function _restyle(gd, aobj, traces) {
16091632
}
16101633
}
16111634

1612-
// major enough changes deserve autoscale, autobin, and
1635+
// Major enough changes deserve autoscale and
16131636
// non-reversed axes so people don't get confused
1637+
//
1638+
// Note: autobin (or its new analog bin clearing) is not included here
1639+
// since we're not pushing bins back to gd.data, so if we have bin
1640+
// info it was explicitly provided by the user.
16141641
if(['orientation', 'type'].indexOf(ai) !== -1) {
16151642
axlist = [];
16161643
for(i = 0; i < traces.length; i++) {
@@ -1619,10 +1646,6 @@ function _restyle(gd, aobj, traces) {
16191646
if(Registry.traceIs(trace, 'cartesian')) {
16201647
addToAxlist(trace.xaxis || 'x');
16211648
addToAxlist(trace.yaxis || 'y');
1622-
1623-
if(ai === 'type') {
1624-
doextra(['autobinx', 'autobiny'], true, i);
1625-
}
16261649
}
16271650
}
16281651

src/plots/cartesian/axes.js

+47-35
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ var Color = require('../../components/color');
2121
var Drawing = require('../../components/drawing');
2222

2323
var axAttrs = require('./layout_attributes');
24+
var cleanTicks = require('./clean_ticks');
2425

2526
var constants = require('../../constants/numerical');
2627
var ONEAVGYEAR = constants.ONEAVGYEAR;
@@ -280,43 +281,22 @@ axes.saveShowSpikeInitial = function(gd, overwrite) {
280281
return hasOneAxisChanged;
281282
};
282283

283-
axes.autoBin = function(data, ax, nbins, is2d, calendar) {
284-
var dataMin = Lib.aggNums(Math.min, null, data),
285-
dataMax = Lib.aggNums(Math.max, null, data);
286-
287-
if(!calendar) calendar = ax.calendar;
284+
axes.autoBin = function(data, ax, nbins, is2d, calendar, size) {
285+
var dataMin = Lib.aggNums(Math.min, null, data);
286+
var dataMax = Lib.aggNums(Math.max, null, data);
288287

289288
if(ax.type === 'category') {
290289
return {
291290
start: dataMin - 0.5,
292291
end: dataMax + 0.5,
293-
size: 1,
292+
size: Math.max(1, Math.round(size) || 1),
294293
_dataSpan: dataMax - dataMin,
295294
};
296295
}
297296

298-
var size0;
299-
if(nbins) size0 = ((dataMax - dataMin) / nbins);
300-
else {
301-
// totally auto: scale off std deviation so the highest bin is
302-
// somewhat taller than the total number of bins, but don't let
303-
// the size get smaller than the 'nice' rounded down minimum
304-
// difference between values
305-
var distinctData = Lib.distinctVals(data),
306-
msexp = Math.pow(10, Math.floor(
307-
Math.log(distinctData.minDiff) / Math.LN10)),
308-
minSize = msexp * Lib.roundUp(
309-
distinctData.minDiff / msexp, [0.9, 1.9, 4.9, 9.9], true);
310-
size0 = Math.max(minSize, 2 * Lib.stdev(data) /
311-
Math.pow(data.length, is2d ? 0.25 : 0.4));
312-
313-
// fallback if ax.d2c output BADNUMs
314-
// e.g. when user try to plot categorical bins
315-
// on a layout.xaxis.type: 'linear'
316-
if(!isNumeric(size0)) size0 = 1;
317-
}
297+
if(!calendar) calendar = ax.calendar;
318298

319-
// piggyback off autotick code to make "nice" bin sizes
299+
// piggyback off tick code to make "nice" bin sizes and edges
320300
var dummyAx;
321301
if(ax.type === 'log') {
322302
dummyAx = {
@@ -333,19 +313,51 @@ axes.autoBin = function(data, ax, nbins, is2d, calendar) {
333313
}
334314
axes.setConvert(dummyAx);
335315

336-
axes.autoTicks(dummyAx, size0);
316+
size = size && cleanTicks.dtick(size, dummyAx.type);
317+
318+
if(size) {
319+
dummyAx.dtick = size;
320+
dummyAx.tick0 = cleanTicks.tick0(undefined, dummyAx.type, calendar);
321+
}
322+
else {
323+
var size0;
324+
if(nbins) size0 = ((dataMax - dataMin) / nbins);
325+
else {
326+
// totally auto: scale off std deviation so the highest bin is
327+
// somewhat taller than the total number of bins, but don't let
328+
// the size get smaller than the 'nice' rounded down minimum
329+
// difference between values
330+
var distinctData = Lib.distinctVals(data);
331+
var msexp = Math.pow(10, Math.floor(
332+
Math.log(distinctData.minDiff) / Math.LN10));
333+
var minSize = msexp * Lib.roundUp(
334+
distinctData.minDiff / msexp, [0.9, 1.9, 4.9, 9.9], true);
335+
size0 = Math.max(minSize, 2 * Lib.stdev(data) /
336+
Math.pow(data.length, is2d ? 0.25 : 0.4));
337+
338+
// fallback if ax.d2c output BADNUMs
339+
// e.g. when user try to plot categorical bins
340+
// on a layout.xaxis.type: 'linear'
341+
if(!isNumeric(size0)) size0 = 1;
342+
}
343+
344+
axes.autoTicks(dummyAx, size0);
345+
}
346+
347+
348+
var finalSize = dummyAx.dtick;
337349
var binStart = axes.tickIncrement(
338-
axes.tickFirst(dummyAx), dummyAx.dtick, 'reverse', calendar);
350+
axes.tickFirst(dummyAx), finalSize, 'reverse', calendar);
339351
var binEnd, bincount;
340352

341353
// check for too many data points right at the edges of bins
342354
// (>50% within 1% of bin edges) or all data points integral
343355
// and offset the bins accordingly
344-
if(typeof dummyAx.dtick === 'number') {
356+
if(typeof finalSize === 'number') {
345357
binStart = autoShiftNumericBins(binStart, data, dummyAx, dataMin, dataMax);
346358

347-
bincount = 1 + Math.floor((dataMax - binStart) / dummyAx.dtick);
348-
binEnd = binStart + bincount * dummyAx.dtick;
359+
bincount = 1 + Math.floor((dataMax - binStart) / finalSize);
360+
binEnd = binStart + bincount * finalSize;
349361
}
350362
else {
351363
// month ticks - should be the only nonlinear kind we have at this point.
@@ -354,23 +366,23 @@ axes.autoBin = function(data, ax, nbins, is2d, calendar) {
354366
// we bin it on a linear axis (which one could argue against, but that's
355367
// a separate issue)
356368
if(dummyAx.dtick.charAt(0) === 'M') {
357-
binStart = autoShiftMonthBins(binStart, data, dummyAx.dtick, dataMin, calendar);
369+
binStart = autoShiftMonthBins(binStart, data, finalSize, dataMin, calendar);
358370
}
359371

360372
// calculate the endpoint for nonlinear ticks - you have to
361373
// just increment until you're done
362374
binEnd = binStart;
363375
bincount = 0;
364376
while(binEnd <= dataMax) {
365-
binEnd = axes.tickIncrement(binEnd, dummyAx.dtick, false, calendar);
377+
binEnd = axes.tickIncrement(binEnd, finalSize, false, calendar);
366378
bincount++;
367379
}
368380
}
369381

370382
return {
371383
start: ax.c2r(binStart, 0, calendar),
372384
end: ax.c2r(binEnd, 0, calendar),
373-
size: dummyAx.dtick,
385+
size: finalSize,
374386
_dataSpan: dataMax - dataMin
375387
};
376388
};

src/plots/cartesian/clean_ticks.js

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Copyright 2012-2018, 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+
12+
var isNumeric = require('fast-isnumeric');
13+
var Lib = require('../../lib');
14+
var ONEDAY = require('../../constants/numerical').ONEDAY;
15+
16+
/**
17+
* Return a validated dtick value for this axis
18+
*
19+
* @param {any} dtick: the candidate dtick. valid values are numbers and strings,
20+
* and further constrained depending on the axis type.
21+
* @param {string} axType: the axis type
22+
*/
23+
exports.dtick = function(dtick, axType) {
24+
var isLog = axType === 'log';
25+
var isDate = axType === 'date';
26+
var isCat = axType === 'category';
27+
var dtickDflt = isDate ? ONEDAY : 1;
28+
29+
if(!dtick) return dtickDflt;
30+
31+
if(isNumeric(dtick)) {
32+
dtick = Number(dtick);
33+
if(dtick <= 0) return dtickDflt;
34+
if(isCat) {
35+
// category dtick must be positive integers
36+
return Math.max(1, Math.round(dtick));
37+
}
38+
if(isDate) {
39+
// date dtick must be at least 0.1ms (our current precision)
40+
return Math.max(0.1, dtick);
41+
}
42+
return dtick;
43+
}
44+
45+
if(typeof dtick !== 'string' || !(isDate || isLog)) {
46+
return dtickDflt;
47+
}
48+
49+
var prefix = dtick.charAt(0);
50+
var dtickNum = dtick.substr(1);
51+
dtickNum = isNumeric(dtickNum) ? Number(dtickNum) : 0;
52+
53+
if((dtickNum <= 0) || !(
54+
// "M<n>" gives ticks every (integer) n months
55+
(isDate && prefix === 'M' && dtickNum === Math.round(dtickNum)) ||
56+
// "L<f>" gives ticks linearly spaced in data (not in position) every (float) f
57+
(isLog && prefix === 'L') ||
58+
// "D1" gives powers of 10 with all small digits between, "D2" gives only 2 and 5
59+
(isLog && prefix === 'D' && (dtickNum === 1 || dtickNum === 2))
60+
)) {
61+
return dtickDflt;
62+
}
63+
64+
return dtick;
65+
};
66+
67+
/**
68+
* Return a validated tick0 for this axis
69+
*
70+
* @param {any} tick0: the candidate tick0. Valid values are numbers and strings,
71+
* further constrained depending on the axis type
72+
* @param {string} axType: the axis type
73+
* @param {string} calendar: for date axes, the calendar to validate/convert with
74+
* @param {any} dtick: an already valid dtick. Only used for D1 and D2 log dticks,
75+
* which do not support tick0 at all.
76+
*/
77+
exports.tick0 = function(tick0, axType, calendar, dtick) {
78+
if(axType === 'date') {
79+
return Lib.cleanDate(tick0, Lib.dateTick0(calendar));
80+
}
81+
if(dtick === 'D1' || dtick === 'D2') {
82+
// D1 and D2 modes ignore tick0 entirely
83+
return undefined;
84+
}
85+
// Aside from date axes, tick0 must be numeric
86+
return isNumeric(tick0) ? Number(tick0) : 0;
87+
};

src/plots/cartesian/tick_value_defaults.js

+6-44
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@
99

1010
'use strict';
1111

12-
var isNumeric = require('fast-isnumeric');
13-
var Lib = require('../../lib');
14-
var ONEDAY = require('../../constants/numerical').ONEDAY;
12+
var cleanTicks = require('./clean_ticks');
1513

1614

1715
module.exports = function handleTickValueDefaults(containerIn, containerOut, coerce, axType) {
@@ -33,47 +31,11 @@ module.exports = function handleTickValueDefaults(containerIn, containerOut, coe
3331
else if(tickmode === 'linear') {
3432
// dtick is usually a positive number, but there are some
3533
// special strings available for log or date axes
36-
// default is 1 day for dates, otherwise 1
37-
var dtickDflt = (axType === 'date') ? ONEDAY : 1;
38-
var dtick = coerce('dtick', dtickDflt);
39-
if(isNumeric(dtick)) {
40-
containerOut.dtick = (dtick > 0) ? Number(dtick) : dtickDflt;
41-
}
42-
else if(typeof dtick !== 'string') {
43-
containerOut.dtick = dtickDflt;
44-
}
45-
else {
46-
// date and log special cases are all one character plus a number
47-
var prefix = dtick.charAt(0),
48-
dtickNum = dtick.substr(1);
49-
50-
dtickNum = isNumeric(dtickNum) ? Number(dtickNum) : 0;
51-
if((dtickNum <= 0) || !(
52-
// "M<n>" gives ticks every (integer) n months
53-
(axType === 'date' && prefix === 'M' && dtickNum === Math.round(dtickNum)) ||
54-
// "L<f>" gives ticks linearly spaced in data (not in position) every (float) f
55-
(axType === 'log' && prefix === 'L') ||
56-
// "D1" gives powers of 10 with all small digits between, "D2" gives only 2 and 5
57-
(axType === 'log' && prefix === 'D' && (dtickNum === 1 || dtickNum === 2))
58-
)) {
59-
containerOut.dtick = dtickDflt;
60-
}
61-
}
62-
63-
// tick0 can have different valType for different axis types, so
64-
// validate that now. Also for dates, change milliseconds to date strings
65-
var tick0Dflt = (axType === 'date') ? Lib.dateTick0(containerOut.calendar) : 0;
66-
var tick0 = coerce('tick0', tick0Dflt);
67-
if(axType === 'date') {
68-
containerOut.tick0 = Lib.cleanDate(tick0, tick0Dflt);
69-
}
70-
// Aside from date axes, dtick must be numeric; D1 and D2 modes ignore tick0 entirely
71-
else if(isNumeric(tick0) && dtick !== 'D1' && dtick !== 'D2') {
72-
containerOut.tick0 = Number(tick0);
73-
}
74-
else {
75-
containerOut.tick0 = tick0Dflt;
76-
}
34+
// tick0 also has special logic
35+
var dtick = containerOut.dtick = cleanTicks.dtick(
36+
containerIn.dtick, axType);
37+
containerOut.tick0 = cleanTicks.tick0(
38+
containerIn.tick0, axType, containerOut.calendar, dtick);
7739
}
7840
else {
7941
var tickvals = coerce('tickvals');

0 commit comments

Comments
 (0)