Skip to content

Commit 17c04c5

Browse files
committed
flesh out CDFs
1 parent fd7526b commit 17c04c5

File tree

5 files changed

+187
-17
lines changed

5 files changed

+187
-17
lines changed

src/plot_api/plot_api.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1296,7 +1296,7 @@ function _restyle(gd, aobj, _traces) {
12961296
'tilt', 'tiltaxis', 'depth', 'direction', 'rotation', 'pull',
12971297
'line.showscale', 'line.cauto', 'line.autocolorscale', 'line.reversescale',
12981298
'marker.line.showscale', 'marker.line.cauto', 'marker.line.autocolorscale', 'marker.line.reversescale',
1299-
'xcalendar', 'ycalendar'
1299+
'xcalendar', 'ycalendar', 'cumulative', 'currentbin'
13001300
];
13011301

13021302
for(i = 0; i < traces.length; i++) {

src/traces/histogram/attributes.js

+35-5
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,44 @@ module.exports = {
7171
].join(' ')
7272
},
7373

74-
mode: {
74+
cumulative: {
75+
valType: 'boolean',
76+
dflt: false,
77+
role: 'info',
78+
description: [
79+
'If true, display the cumulative distribution by summing the',
80+
'binned values. Use the `direction` and `centralbin` attributes',
81+
'to tune the accumulation method.'
82+
].join(' ')
83+
},
84+
85+
direction: {
7586
valType: 'enumerated',
76-
values: ['density', 'cumulative'],
77-
dflt: 'density',
87+
values: ['increasing', 'decreasing'],
88+
dflt: 'increasing',
7889
role: 'info',
7990
description: [
80-
''
81-
].join('')
91+
'Only applies if `cumulative=true.',
92+
'If *increasing* (default) we sum all prior bins, so the result',
93+
'increases from left to right. If *decreasing* we sum later bins',
94+
'so the fresult decreases from left to right.'
95+
].join(' ')
96+
},
97+
98+
currentbin: {
99+
valType: 'enumerated',
100+
values: ['include', 'exclude', 'half'],
101+
dflt: 'include',
102+
role: 'info',
103+
description: [
104+
'Only applies if `cumulative=true.',
105+
'Sets whether the current bin is included, excluded, or has half',
106+
'of its value included in the current cumulative value.',
107+
'*include* is the default for compatibility with various other',
108+
'tools, however it introduces a half-bin bias to the results.',
109+
'*exclude* makes the opposite half-bin bias, and *half* removes',
110+
'it.'
111+
].join(' ')
82112
},
83113

84114
autobinx: {

src/traces/histogram/calc.js

+74-10
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,29 @@ module.exports = function calc(gd, trace) {
4141
var pos0 = pa.makeCalcdata(trace, maindata);
4242

4343
// calculate the bins
44-
if((trace['autobin' + maindata] !== false) || !(maindata + 'bins' in trace)) {
45-
trace[maindata + 'bins'] = Axes.autoBin(pos0, pa, trace['nbins' + maindata], false, calendar);
44+
var binAttr = maindata + 'bins',
45+
binspec;
46+
if((trace['autobin' + maindata] !== false) || !(binAttr in trace)) {
47+
binspec = Axes.autoBin(pos0, pa, trace['nbins' + maindata], false, calendar);
48+
49+
// adjust for CDF edge cases
50+
if(trace.cumulative && (trace.currentbin !== 'include')) {
51+
if(trace.direction === 'decreasing') {
52+
binspec.start = pa.c2r(pa.r2c(binspec.start) - binspec.size);
53+
}
54+
else {
55+
binspec.end = pa.c2r(pa.r2c(binspec.end) + binspec.size);
56+
}
57+
}
4658

47-
// copy bin info back to the source data.
48-
trace._input[maindata + 'bins'] = trace[maindata + 'bins'];
59+
// copy bin info back to the source and full data.
60+
trace._input[binAttr] = trace[binAttr] = binspec;
61+
}
62+
else {
63+
binspec = trace[binAttr];
4964
}
5065

51-
var binspec = trace[maindata + 'bins'],
52-
nonuniformBins = typeof binspec.size === 'string',
66+
var nonuniformBins = typeof binspec.size === 'string',
5367
bins = nonuniformBins ? [] : binspec,
5468
// make the empty bin array
5569
i2,
@@ -115,7 +129,9 @@ module.exports = function calc(gd, trace) {
115129
// average and/or normalize the data, if needed
116130
if(doavg) total = doAvg(size, counts);
117131
if(normfunc) normfunc(size, total, inc);
118-
if(trace.mode === 'cumulative') cdf(size);
132+
133+
// after all normalization etc, now we can accumulate if desired
134+
if(trace.cumulative) cdf(size, trace.direction, trace.currentbin);
119135

120136

121137
var serieslen = Math.min(pos.length, size.length),
@@ -146,8 +162,56 @@ module.exports = function calc(gd, trace) {
146162
return cd;
147163
};
148164

149-
function cdf(size) {
150-
for(var i = 1; i < size.length; i++) {
151-
size[i] += size[i - 1];
165+
function cdf(size, direction, currentbin) {
166+
var i,
167+
vi,
168+
prevSum;
169+
170+
function firstHalfPoint(i) {
171+
prevSum = size[i];
172+
size[i] /= 2;
173+
}
174+
175+
function nextHalfPoint(i) {
176+
vi = size[i];
177+
size[i] = prevSum + vi / 2;
178+
prevSum += vi;
179+
}
180+
181+
if(currentbin === 'half') {
182+
183+
if(direction === 'increasing') {
184+
firstHalfPoint(0);
185+
for(i = 1; i < size.length; i++) {
186+
nextHalfPoint(i);
187+
}
188+
}
189+
else {
190+
firstHalfPoint(size.length - 1);
191+
for(i = size.length - 2; i >= 0; i--) {
192+
nextHalfPoint(i);
193+
}
194+
}
195+
}
196+
else if(direction === 'increasing') {
197+
for(i = 1; i < size.length; i++) {
198+
size[i] += size[i - 1];
199+
}
200+
201+
// 'exclude' is identical to 'include' just shifted one bin over
202+
if(currentbin === 'exclude') {
203+
size.unshift(0);
204+
size.pop();
205+
}
206+
}
207+
else {
208+
for(i = size.length - 2; i >= 0; i--) {
209+
size[i] += size[i + 1];
210+
}
211+
212+
if(currentbin === 'exclude') {
213+
size.push(0);
214+
size.shift();
215+
}
152216
}
153217
}

src/traces/histogram/defaults.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
2727
var x = coerce('x'),
2828
y = coerce('y');
2929

30-
coerce('mode');
30+
var cumulative = coerce('cumulative');
31+
if(cumulative) {
32+
coerce('direction');
33+
coerce('currentbin');
34+
}
35+
3136
coerce('text');
3237

3338
var orientation = coerce('orientation', (y && !x) ? 'h' : 'v'),

test/jasmine/tests/histogram_test.js

+71
Original file line numberDiff line numberDiff line change
@@ -246,5 +246,76 @@ describe('Test histogram', function() {
246246
]);
247247
});
248248

249+
describe('cumulative distribution functions', function() {
250+
var base = {x: [1, 2, 3, 4, 2, 3, 4, 3, 4, 4]};
251+
252+
it('makes the right base histogram', function() {
253+
var baseOut = _calc(base);
254+
expect(baseOut).toEqual([
255+
{b: 0, p: 1, s: 1},
256+
{b: 0, p: 2, s: 2},
257+
{b: 0, p: 3, s: 3},
258+
{b: 0, p: 4, s: 4},
259+
]);
260+
});
261+
262+
var CDFs = [
263+
{p: [1, 2, 3, 4], s: [1, 3, 6, 10]},
264+
{
265+
direction: 'decreasing',
266+
p: [1, 2, 3, 4], s: [10, 9, 7, 4]
267+
},
268+
{
269+
currentbin: 'exclude',
270+
p: [2, 3, 4, 5], s: [1, 3, 6, 10]
271+
},
272+
{
273+
direction: 'decreasing', currentbin: 'exclude',
274+
p: [0, 1, 2, 3], s: [10, 9, 7, 4]
275+
},
276+
{
277+
currentbin: 'half',
278+
p: [1, 2, 3, 4, 5], s: [0.5, 2, 4.5, 8, 10]
279+
},
280+
{
281+
direction: 'decreasing', currentbin: 'half',
282+
p: [0, 1, 2, 3, 4], s: [10, 9.5, 8, 5.5, 2]
283+
},
284+
{
285+
direction: 'decreasing', currentbin: 'half', histnorm: 'percent',
286+
p: [0, 1, 2, 3, 4], s: [100, 95, 80, 55, 20]
287+
},
288+
{
289+
currentbin: 'exclude', histnorm: 'probability',
290+
p: [2, 3, 4, 5], s: [0.1, 0.3, 0.6, 1]
291+
}
292+
];
293+
294+
CDFs.forEach(function(CDF) {
295+
var direction = CDF.direction,
296+
currentbin = CDF.currentbin,
297+
histnorm = CDF.histnorm,
298+
p = CDF.p,
299+
s = CDF.s;
300+
301+
it('handles direction=' + direction + ', currentbin=' + currentbin + ', histnorm=' + histnorm, function() {
302+
var traceIn = Lib.extendFlat({}, base, {
303+
cumulative: true,
304+
direction: direction,
305+
currentbin: currentbin,
306+
histnorm: histnorm
307+
});
308+
var out = _calc(traceIn);
309+
310+
expect(out.length).toBe(p.length);
311+
out.forEach(function(outi, i) {
312+
expect(outi.p).toBe(p[i]);
313+
expect(outi.s).toBeCloseTo(s[i], 6);
314+
expect(outi.b).toBe(0);
315+
});
316+
});
317+
});
318+
});
319+
249320
});
250321
});

0 commit comments

Comments
 (0)