-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
/
Copy pathcalc.js
376 lines (316 loc) · 12.9 KB
/
calc.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
/**
* Copyright 2012-2017, 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 Lib = require('../../lib');
var Axes = require('../../plots/cartesian/axes');
var arraysToCalcdata = require('../bar/arrays_to_calcdata');
var binFunctions = require('./bin_functions');
var normFunctions = require('./norm_functions');
var doAvg = require('./average');
var cleanBins = require('./clean_bins');
var oneMonth = require('../../constants/numerical').ONEAVGMONTH;
module.exports = function calc(gd, trace) {
// ignore as much processing as possible (and including in autorange) if bar is not visible
if(trace.visible !== true) return;
// depending on orientation, set position and size axes and data ranges
// note: this logic for choosing orientation is duplicated in graph_obj->setstyles
var pos = [];
var size = [];
var pa = Axes.getFromId(gd, trace.orientation === 'h' ?
(trace.yaxis || 'y') : (trace.xaxis || 'x'));
var maindata = trace.orientation === 'h' ? 'y' : 'x';
var counterdata = {x: 'y', y: 'x'}[maindata];
var calendar = trace[maindata + 'calendar'];
var cumulativeSpec = trace.cumulative;
var i;
cleanBins(trace, pa, maindata);
var binsAndPos = calcAllAutoBins(gd, trace, pa, maindata);
var binspec = binsAndPos[0];
var pos0 = binsAndPos[1];
var nonuniformBins = typeof binspec.size === 'string';
var bins = nonuniformBins ? [] : binspec;
// make the empty bin array
var inc = [];
var counts = [];
var total = 0;
var norm = trace.histnorm;
var func = trace.histfunc;
var densitynorm = norm.indexOf('density') !== -1;
var i2, binend, n;
if(cumulativeSpec.enabled && densitynorm) {
// we treat "cumulative" like it means "integral" if you use a density norm,
// which in the end means it's the same as without "density"
norm = norm.replace(/ ?density$/, '');
densitynorm = false;
}
var extremefunc = func === 'max' || func === 'min';
var sizeinit = extremefunc ? null : 0;
var binfunc = binFunctions.count;
var normfunc = normFunctions[norm];
var doavg = false;
var pr2c = function(v) { return pa.r2c(v, 0, calendar); };
var rawCounterData;
if(Array.isArray(trace[counterdata]) && func !== 'count') {
rawCounterData = trace[counterdata];
doavg = func === 'avg';
binfunc = binFunctions[func];
}
// create the bins (and any extra arrays needed)
// assume more than 1e6 bins is an error, so we don't crash the browser
i = pr2c(binspec.start);
// decrease end a little in case of rounding errors
binend = pr2c(binspec.end) + (i - Axes.tickIncrement(i, binspec.size, false, calendar)) / 1e6;
while(i < binend && pos.length < 1e6) {
i2 = Axes.tickIncrement(i, binspec.size, false, calendar);
pos.push((i + i2) / 2);
size.push(sizeinit);
// nonuniform bins (like months) we need to search,
// rather than straight calculate the bin we're in
if(nonuniformBins) bins.push(i);
// nonuniform bins also need nonuniform normalization factors
if(densitynorm) inc.push(1 / (i2 - i));
if(doavg) counts.push(0);
// break to avoid infinite loops
if(i2 <= i) break;
i = i2;
}
// for date axes we need bin bounds to be calcdata. For nonuniform bins
// we already have this, but uniform with start/end/size they're still strings.
if(!nonuniformBins && pa.type === 'date') {
bins = {
start: pr2c(bins.start),
end: pr2c(bins.end),
size: bins.size
};
}
var nMax = size.length;
// bin the data
for(i = 0; i < pos0.length; i++) {
n = Lib.findBin(pos0[i], bins);
if(n >= 0 && n < nMax) total += binfunc(n, i, size, rawCounterData, counts);
}
// average and/or normalize the data, if needed
if(doavg) total = doAvg(size, counts);
if(normfunc) normfunc(size, total, inc);
// after all normalization etc, now we can accumulate if desired
if(cumulativeSpec.enabled) cdf(size, cumulativeSpec.direction, cumulativeSpec.currentbin);
var serieslen = Math.min(pos.length, size.length);
var cd = [];
var firstNonzero = 0;
var lastNonzero = serieslen - 1;
// look for empty bins at the ends to remove, so autoscale omits them
for(i = 0; i < serieslen; i++) {
if(size[i]) {
firstNonzero = i;
break;
}
}
for(i = serieslen - 1; i > firstNonzero; i--) {
if(size[i]) {
lastNonzero = i;
break;
}
}
// create the "calculated data" to plot
for(i = firstNonzero; i <= lastNonzero; i++) {
if((isNumeric(pos[i]) && isNumeric(size[i]))) {
cd.push({p: pos[i], s: size[i], b: 0});
}
}
arraysToCalcdata(cd, trace);
return cd;
};
/*
* calcAllAutoBins: we want all histograms on the same axes to share bin specs
* if they're grouped or stacked. If the user has explicitly specified differing
* bin specs, there's nothing we can do, but if possible we will try to use the
* smallest bins of any of the auto values for all histograms grouped/stacked
* together.
*/
function calcAllAutoBins(gd, trace, pa, maindata) {
var binAttr = maindata + 'bins';
var i, tracei, calendar, firstManual, pos0;
// all but the first trace in this group has already been marked finished
// clear this flag, so next time we run calc we will run autobin again
if(trace._autoBinFinished) {
delete trace._autoBinFinished;
}
else {
// must be the first trace in the group - do the autobinning on them all
var traceGroup = getConnectedHistograms(gd, trace);
var autoBinnedTraces = [];
var minSize = Infinity;
var minStart = Infinity;
var maxEnd = -Infinity;
var autoBinAttr = 'autobin' + maindata;
for(i = 0; i < traceGroup.length; i++) {
tracei = traceGroup[i];
// stash pos0 on the trace so we don't need to duplicate this
// in the main body of calc
pos0 = tracei._pos0 = pa.makeCalcdata(tracei, maindata);
var binspec = tracei[binAttr];
if((tracei[autoBinAttr]) || !binspec ||
binspec.start === null || binspec.end === null) {
calendar = tracei[maindata + 'calendar'];
var cumulativeSpec = tracei.cumulative;
binspec = Axes.autoBin(pos0, pa, tracei['nbins' + maindata], false, calendar);
// adjust for CDF edge cases
if(cumulativeSpec.enabled && (cumulativeSpec.currentbin !== 'include')) {
if(cumulativeSpec.direction === 'decreasing') {
minStart = Math.min(minStart, pa.r2c(binspec.start, 0, calendar) - binspec.size);
}
else {
maxEnd = Math.max(maxEnd, pa.r2c(binspec.end, 0, calendar) + binspec.size);
}
}
// note that it's possible to get here with an explicit autobin: false
// if the bins were not specified. mark this trace for followup
autoBinnedTraces.push(tracei);
}
else if(!firstManual) {
// Remember the first manually set binspec. We'll try to be extra
// accommodating of this one, so other bins line up with these
// if there's more than one manual bin set and they're mutually inconsistent,
// then there's not much we can do...
firstManual = {
size: binspec.size,
start: pa.r2c(binspec.start, 0, calendar),
end: pa.r2c(binspec.end, 0, calendar)
};
}
// Even non-autobinned traces get included here, so we get the greatest extent
// and minimum bin size of them all.
// But manually binned traces won't be adjusted, even if the auto values
// are inconsistent with the manual ones (or the manual ones are inconsistent
// with each other).
minSize = getMinSize(minSize, binspec.size);
minStart = Math.min(minStart, pa.r2c(binspec.start, 0, calendar));
maxEnd = Math.max(maxEnd, pa.r2c(binspec.end, 0, calendar));
// add the flag that lets us abort autobin on later traces
if(i) trace._autoBinFinished = 1;
}
// do what we can to match the auto bins to the first manual bins
// but only if sizes are all numeric
if(firstManual && isNumeric(firstManual.size) && isNumeric(minSize)) {
// first need to ensure the bin size is the same as or an integer fraction
// of the first manual bin
// allow the bin size to increase just under the autobin step size to match,
// (which is a factor of 2 or 2.5) otherwise shrink it
if(minSize > firstManual.size / 1.9) minSize = firstManual.size;
else minSize = firstManual.size / Math.ceil(firstManual.size / minSize);
// now decrease minStart if needed to make the bin centers line up
var adjustedFirstStart = firstManual.start + (firstManual.size - minSize) / 2;
minStart = adjustedFirstStart - minSize * Math.ceil((adjustedFirstStart - minStart) / minSize);
}
// now go back to the autobinned traces and update their bin specs with the final values
for(i = 0; i < autoBinnedTraces.length; i++) {
tracei = autoBinnedTraces[i];
calendar = tracei[maindata + 'calendar'];
tracei._input[binAttr] = tracei[binAttr] = {
start: pa.c2r(minStart, 0, calendar),
end: pa.c2r(maxEnd, 0, calendar),
size: minSize
};
// note that it's possible to get here with an explicit autobin: false
// if the bins were not specified.
// in that case this will remain in the trace, so that future updates
// which would change the autobinning will not do so.
tracei._input[autoBinAttr] = tracei[autoBinAttr];
}
}
pos0 = trace._pos0;
delete trace._pos0;
return [trace[binAttr], pos0];
}
/*
* Return an array of traces that are all stacked or grouped together
* Only considers histograms. In principle we could include them in a
* similar way to how we do manually binned histograms, though this
* would have tons of edge cases and value judgments to make.
*/
function getConnectedHistograms(gd, trace) {
if(gd._fullLayout.barmode === 'overlay') return [trace];
var xid = trace.xaxis;
var yid = trace.yaxis;
var orientation = trace.orientation;
var out = [];
var fullData = gd._fullData;
for(var i = 0; i < fullData.length; i++) {
var tracei = fullData[i];
if(tracei.type === 'histogram' &&
tracei.orientation === orientation &&
tracei.xaxis === xid && tracei.yaxis === yid
) {
out.push(tracei);
}
}
return out;
}
/*
* getMinSize: find the smallest given that size can be a string code
* ie 'M6' for 6 months. ('L' wouldn't make sense to compare with numeric sizes)
*/
function getMinSize(size1, size2) {
if(size1 === Infinity) return size2;
var sizeNumeric1 = numericSize(size1);
var sizeNumeric2 = numericSize(size2);
return sizeNumeric2 < sizeNumeric1 ? size2 : size1;
}
function numericSize(size) {
if(isNumeric(size)) return size;
if(typeof size === 'string' && size.charAt(0) === 'M') {
return oneMonth * +(size.substr(1));
}
return Infinity;
}
function cdf(size, direction, currentbin) {
var i, vi, prevSum;
function firstHalfPoint(i) {
prevSum = size[i];
size[i] /= 2;
}
function nextHalfPoint(i) {
vi = size[i];
size[i] = prevSum + vi / 2;
prevSum += vi;
}
if(currentbin === 'half') {
if(direction === 'increasing') {
firstHalfPoint(0);
for(i = 1; i < size.length; i++) {
nextHalfPoint(i);
}
}
else {
firstHalfPoint(size.length - 1);
for(i = size.length - 2; i >= 0; i--) {
nextHalfPoint(i);
}
}
}
else if(direction === 'increasing') {
for(i = 1; i < size.length; i++) {
size[i] += size[i - 1];
}
// 'exclude' is identical to 'include' just shifted one bin over
if(currentbin === 'exclude') {
size.unshift(0);
size.pop();
}
}
else {
for(i = size.length - 2; i >= 0; i--) {
size[i] += size[i + 1];
}
if(currentbin === 'exclude') {
size.push(0);
size.shift();
}
}
}