Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 30ed4a4

Browse filesBrowse files
authored
Merge pull request #3044 from plotly/histogram-autobin
Histogram autobin
2 parents 48209a0 + 7909f53 commit 30ed4a4
Copy full SHA for 30ed4a4
Expand file treeCollapse file tree

35 files changed

+1058
-527
lines changed

‎src/lib/dates.js

Copy file name to clipboardExpand all lines: src/lib/dates.js
+3-1Lines changed: 3 additions & 1 deletion
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

Copy file name to clipboardExpand all lines: src/plot_api/helpers.js
+13Lines changed: 13 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: src/plot_api/plot_api.js
+28-5Lines changed: 28 additions & 5 deletions
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

Copy file name to clipboardExpand all lines: src/plots/cartesian/axes.js
+47-35Lines changed: 47 additions & 35 deletions
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

Copy file name to clipboard
+87Lines changed: 87 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: src/plots/cartesian/tick_value_defaults.js
+6-44Lines changed: 6 additions & 44 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.