From 62aeefb107c64bf833fd480eee03655c1c277fad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 22 Nov 2016 15:21:22 -0500 Subject: [PATCH 1/7] [pof] add 'cumulative' histogram 'mode' for CDF --- src/traces/histogram/attributes.js | 10 ++++++++++ src/traces/histogram/calc.js | 9 +++++++++ src/traces/histogram/defaults.js | 1 + 3 files changed, 20 insertions(+) diff --git a/src/traces/histogram/attributes.js b/src/traces/histogram/attributes.js index b455fd4b98a..70878d80500 100644 --- a/src/traces/histogram/attributes.js +++ b/src/traces/histogram/attributes.js @@ -71,6 +71,16 @@ module.exports = { ].join(' ') }, + mode: { + valType: 'enumerated', + values: ['density', 'cumulative'], + dflt: 'density', + role: 'info', + description: [ + '' + ].join('') + }, + autobinx: { valType: 'boolean', dflt: null, diff --git a/src/traces/histogram/calc.js b/src/traces/histogram/calc.js index 5248979368a..2c32d5a0b94 100644 --- a/src/traces/histogram/calc.js +++ b/src/traces/histogram/calc.js @@ -38,6 +38,7 @@ module.exports = function calc(gd, trace) { // prepare the raw data var pos0 = pa.makeCalcdata(trace, maindata); + // calculate the bins if((trace['autobin' + maindata] !== false) || !(maindata + 'bins' in trace)) { trace[maindata + 'bins'] = Axes.autoBin(pos0, pa, trace['nbins' + maindata]); @@ -112,6 +113,8 @@ module.exports = function calc(gd, trace) { // average and/or normalize the data, if needed if(doavg) total = doAvg(size, counts); if(normfunc) normfunc(size, total, inc); + if(trace.mode === 'cumulative') cdf(size); + var serieslen = Math.min(pos.length, size.length), cd = [], @@ -140,3 +143,9 @@ module.exports = function calc(gd, trace) { return cd; }; + +function cdf(size) { + for(var i = 1; i < size.length; i++) { + size[i] += size[i - 1]; + } +} diff --git a/src/traces/histogram/defaults.js b/src/traces/histogram/defaults.js index a415ff00797..e0d233edd32 100644 --- a/src/traces/histogram/defaults.js +++ b/src/traces/histogram/defaults.js @@ -26,6 +26,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var x = coerce('x'), y = coerce('y'); + coerce('mode'); coerce('text'); var orientation = coerce('orientation', (y && !x) ? 'h' : 'v'), From fd7526b842f3520c8ff46b8fca81a6d80afdbe10 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 14 Jan 2017 01:04:06 -0500 Subject: [PATCH 2/7] edge case in autoShiftNumericBins avoid an empty bin at the start. Tested via histogram_test --- src/plots/cartesian/axes.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 64dc21e4b71..612f8ad4b8a 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -609,7 +609,9 @@ function autoShiftNumericBins(binStart, data, ax, dataMin, dataMax) { // otherwise start half an integer down regardless of // the bin size, just enough to clear up endpoint // ambiguity about which integers are in which bins. - else binStart -= 0.5; + else { + binStart += (binStart + 0.5 < dataMin) ? 0.5 : -0.5; + } } else if(midcount < dataCount * 0.1) { if(edgecount > dataCount * 0.3 || From 17c04c511cfaf15c42881a1c8b1e22a7964f3fb2 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 14 Jan 2017 01:08:01 -0500 Subject: [PATCH 3/7] flesh out CDFs --- src/plot_api/plot_api.js | 2 +- src/traces/histogram/attributes.js | 40 +++++++++++-- src/traces/histogram/calc.js | 84 ++++++++++++++++++++++++---- src/traces/histogram/defaults.js | 7 ++- test/jasmine/tests/histogram_test.js | 71 +++++++++++++++++++++++ 5 files changed, 187 insertions(+), 17 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 0e26c4b8aa9..eac342fd9a8 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1296,7 +1296,7 @@ function _restyle(gd, aobj, _traces) { 'tilt', 'tiltaxis', 'depth', 'direction', 'rotation', 'pull', 'line.showscale', 'line.cauto', 'line.autocolorscale', 'line.reversescale', 'marker.line.showscale', 'marker.line.cauto', 'marker.line.autocolorscale', 'marker.line.reversescale', - 'xcalendar', 'ycalendar' + 'xcalendar', 'ycalendar', 'cumulative', 'currentbin' ]; for(i = 0; i < traces.length; i++) { diff --git a/src/traces/histogram/attributes.js b/src/traces/histogram/attributes.js index fee1a5ef3dd..32329a80fd1 100644 --- a/src/traces/histogram/attributes.js +++ b/src/traces/histogram/attributes.js @@ -71,14 +71,44 @@ module.exports = { ].join(' ') }, - mode: { + cumulative: { + valType: 'boolean', + dflt: false, + role: 'info', + description: [ + 'If true, display the cumulative distribution by summing the', + 'binned values. Use the `direction` and `centralbin` attributes', + 'to tune the accumulation method.' + ].join(' ') + }, + + direction: { valType: 'enumerated', - values: ['density', 'cumulative'], - dflt: 'density', + values: ['increasing', 'decreasing'], + dflt: 'increasing', role: 'info', description: [ - '' - ].join('') + 'Only applies if `cumulative=true.', + 'If *increasing* (default) we sum all prior bins, so the result', + 'increases from left to right. If *decreasing* we sum later bins', + 'so the fresult decreases from left to right.' + ].join(' ') + }, + + currentbin: { + valType: 'enumerated', + values: ['include', 'exclude', 'half'], + dflt: 'include', + role: 'info', + description: [ + 'Only applies if `cumulative=true.', + 'Sets whether the current bin is included, excluded, or has half', + 'of its value included in the current cumulative value.', + '*include* is the default for compatibility with various other', + 'tools, however it introduces a half-bin bias to the results.', + '*exclude* makes the opposite half-bin bias, and *half* removes', + 'it.' + ].join(' ') }, autobinx: { diff --git a/src/traces/histogram/calc.js b/src/traces/histogram/calc.js index 0a9134de997..a88953c6815 100644 --- a/src/traces/histogram/calc.js +++ b/src/traces/histogram/calc.js @@ -41,15 +41,29 @@ module.exports = function calc(gd, trace) { var pos0 = pa.makeCalcdata(trace, maindata); // calculate the bins - if((trace['autobin' + maindata] !== false) || !(maindata + 'bins' in trace)) { - trace[maindata + 'bins'] = Axes.autoBin(pos0, pa, trace['nbins' + maindata], false, calendar); + var binAttr = maindata + 'bins', + binspec; + if((trace['autobin' + maindata] !== false) || !(binAttr in trace)) { + binspec = Axes.autoBin(pos0, pa, trace['nbins' + maindata], false, calendar); + + // adjust for CDF edge cases + if(trace.cumulative && (trace.currentbin !== 'include')) { + if(trace.direction === 'decreasing') { + binspec.start = pa.c2r(pa.r2c(binspec.start) - binspec.size); + } + else { + binspec.end = pa.c2r(pa.r2c(binspec.end) + binspec.size); + } + } - // copy bin info back to the source data. - trace._input[maindata + 'bins'] = trace[maindata + 'bins']; + // copy bin info back to the source and full data. + trace._input[binAttr] = trace[binAttr] = binspec; + } + else { + binspec = trace[binAttr]; } - var binspec = trace[maindata + 'bins'], - nonuniformBins = typeof binspec.size === 'string', + var nonuniformBins = typeof binspec.size === 'string', bins = nonuniformBins ? [] : binspec, // make the empty bin array i2, @@ -115,7 +129,9 @@ module.exports = function calc(gd, trace) { // average and/or normalize the data, if needed if(doavg) total = doAvg(size, counts); if(normfunc) normfunc(size, total, inc); - if(trace.mode === 'cumulative') cdf(size); + + // after all normalization etc, now we can accumulate if desired + if(trace.cumulative) cdf(size, trace.direction, trace.currentbin); var serieslen = Math.min(pos.length, size.length), @@ -146,8 +162,56 @@ module.exports = function calc(gd, trace) { return cd; }; -function cdf(size) { - for(var i = 1; i < size.length; i++) { - size[i] += size[i - 1]; +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(); + } } } diff --git a/src/traces/histogram/defaults.js b/src/traces/histogram/defaults.js index c6757d51278..56f8c68e53f 100644 --- a/src/traces/histogram/defaults.js +++ b/src/traces/histogram/defaults.js @@ -27,7 +27,12 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var x = coerce('x'), y = coerce('y'); - coerce('mode'); + var cumulative = coerce('cumulative'); + if(cumulative) { + coerce('direction'); + coerce('currentbin'); + } + coerce('text'); var orientation = coerce('orientation', (y && !x) ? 'h' : 'v'), diff --git a/test/jasmine/tests/histogram_test.js b/test/jasmine/tests/histogram_test.js index 684d2320939..daf48e17719 100644 --- a/test/jasmine/tests/histogram_test.js +++ b/test/jasmine/tests/histogram_test.js @@ -246,5 +246,76 @@ describe('Test histogram', function() { ]); }); + describe('cumulative distribution functions', function() { + var base = {x: [1, 2, 3, 4, 2, 3, 4, 3, 4, 4]}; + + it('makes the right base histogram', function() { + var baseOut = _calc(base); + expect(baseOut).toEqual([ + {b: 0, p: 1, s: 1}, + {b: 0, p: 2, s: 2}, + {b: 0, p: 3, s: 3}, + {b: 0, p: 4, s: 4}, + ]); + }); + + var CDFs = [ + {p: [1, 2, 3, 4], s: [1, 3, 6, 10]}, + { + direction: 'decreasing', + p: [1, 2, 3, 4], s: [10, 9, 7, 4] + }, + { + currentbin: 'exclude', + p: [2, 3, 4, 5], s: [1, 3, 6, 10] + }, + { + direction: 'decreasing', currentbin: 'exclude', + p: [0, 1, 2, 3], s: [10, 9, 7, 4] + }, + { + currentbin: 'half', + p: [1, 2, 3, 4, 5], s: [0.5, 2, 4.5, 8, 10] + }, + { + direction: 'decreasing', currentbin: 'half', + p: [0, 1, 2, 3, 4], s: [10, 9.5, 8, 5.5, 2] + }, + { + direction: 'decreasing', currentbin: 'half', histnorm: 'percent', + p: [0, 1, 2, 3, 4], s: [100, 95, 80, 55, 20] + }, + { + currentbin: 'exclude', histnorm: 'probability', + p: [2, 3, 4, 5], s: [0.1, 0.3, 0.6, 1] + } + ]; + + CDFs.forEach(function(CDF) { + var direction = CDF.direction, + currentbin = CDF.currentbin, + histnorm = CDF.histnorm, + p = CDF.p, + s = CDF.s; + + it('handles direction=' + direction + ', currentbin=' + currentbin + ', histnorm=' + histnorm, function() { + var traceIn = Lib.extendFlat({}, base, { + cumulative: true, + direction: direction, + currentbin: currentbin, + histnorm: histnorm + }); + var out = _calc(traceIn); + + expect(out.length).toBe(p.length); + out.forEach(function(outi, i) { + expect(outi.p).toBe(p[i]); + expect(outi.s).toBeCloseTo(s[i], 6); + expect(outi.b).toBe(0); + }); + }); + }); + }); + }); }); From 4bc28dd82d24f55f10e5e22af390814ca2ebed1f Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 14 Jan 2017 01:45:55 -0500 Subject: [PATCH 4/7] further tweak of autoShiftNumericBins and fix its tests for the improved behavior --- src/plots/cartesian/axes.js | 3 ++- test/jasmine/tests/axes_test.js | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 612f8ad4b8a..14317b07cf8 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -610,7 +610,8 @@ function autoShiftNumericBins(binStart, data, ax, dataMin, dataMax) { // the bin size, just enough to clear up endpoint // ambiguity about which integers are in which bins. else { - binStart += (binStart + 0.5 < dataMin) ? 0.5 : -0.5; + binStart -= 0.5; + if(binStart + ax.dtick < dataMin) binStart += ax.dtick; } } else if(midcount < dataCount * 0.1) { diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index fe98a9e13c1..3883cedb617 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -1809,7 +1809,7 @@ describe('Test axes', function() { ); expect(out).toEqual({ - start: -0.5, + start: 0.5, end: 4.5, size: 1 }); @@ -1822,6 +1822,9 @@ describe('Test axes', function() { 2 ); + // when size > 1 with all integers, we want the starting point to be + // a half integer below the round number a tick would be at (in this case 0) + // to approximate the half-open interval [) that's commonly used. expect(out).toEqual({ start: -0.5, end: 5.5, From c12b7cfb7120d80bd673c2b0df9e66dea1fa7993 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 18 Jan 2017 15:13:08 -0500 Subject: [PATCH 5/7] fix bug in histogram min/max aggregation with normalization --- src/traces/histogram/bin_functions.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/traces/histogram/bin_functions.js b/src/traces/histogram/bin_functions.js index db631f00c73..d4219667903 100644 --- a/src/traces/histogram/bin_functions.js +++ b/src/traces/histogram/bin_functions.js @@ -47,8 +47,9 @@ module.exports = { return v; } else if(size[n] > v) { + var delta = v - size[n]; size[n] = v; - return v - size[n]; + return delta; } } return 0; @@ -63,8 +64,9 @@ module.exports = { return v; } else if(size[n] < v) { + var delta = v - size[n]; size[n] = v; - return v - size[n]; + return delta; } } return 0; From 4d02af75617fdf6361abc9007913d792dcd2197a Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 18 Jan 2017 15:15:42 -0500 Subject: [PATCH 6/7] nest histogram cumulative attributes and test histnorm/histfunc more --- src/plot_api/plot_api.js | 3 +- src/traces/histogram/attributes.js | 90 +++++++++++++++------------- src/traces/histogram/calc.js | 21 +++++-- src/traces/histogram/defaults.js | 6 +- test/jasmine/tests/histogram_test.js | 78 +++++++++++++++++------- 5 files changed, 125 insertions(+), 73 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index eac342fd9a8..745e50ce7a3 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1296,7 +1296,8 @@ function _restyle(gd, aobj, _traces) { 'tilt', 'tiltaxis', 'depth', 'direction', 'rotation', 'pull', 'line.showscale', 'line.cauto', 'line.autocolorscale', 'line.reversescale', 'marker.line.showscale', 'marker.line.cauto', 'marker.line.autocolorscale', 'marker.line.reversescale', - 'xcalendar', 'ycalendar', 'cumulative', 'currentbin' + 'xcalendar', 'ycalendar', + 'cumulative', 'cumulative.enabled', 'cumulative.direction', 'cumulative.currentbin' ]; for(i = 0; i < traces.length; i++) { diff --git a/src/traces/histogram/attributes.js b/src/traces/histogram/attributes.js index 32329a80fd1..889dda231fc 100644 --- a/src/traces/histogram/attributes.js +++ b/src/traces/histogram/attributes.js @@ -56,59 +56,67 @@ module.exports = { 'If **, the span of each bar corresponds to the number of', 'occurrences (i.e. the number of data points lying inside the bins).', - 'If *percent*, the span of each bar corresponds to the percentage', - 'of occurrences with respect to the total number of sample points', - '(here, the sum of all bin area equals 100%).', + 'If *percent* / *probability*, the span of each bar corresponds to', + 'the percentage / fraction of occurrences with respect to the total', + 'number of sample points', + '(here, the sum of all bin HEIGHTS equals 100% / 1).', 'If *density*, the span of each bar corresponds to the number of', 'occurrences in a bin divided by the size of the bin interval', - '(here, the sum of all bin area equals the', + '(here, the sum of all bin AREAS equals the', 'total number of sample points).', - 'If *probability density*, the span of each bar corresponds to the', + 'If *probability density*, the area of each bar corresponds to the', 'probability that an event will fall into the corresponding bin', - '(here, the sum of all bin area equals 1).' + '(here, the sum of all bin AREAS equals 1).' ].join(' ') }, cumulative: { - valType: 'boolean', - dflt: false, - role: 'info', - description: [ - 'If true, display the cumulative distribution by summing the', - 'binned values. Use the `direction` and `centralbin` attributes', - 'to tune the accumulation method.' - ].join(' ') - }, + enabled: { + valType: 'boolean', + dflt: false, + role: 'info', + description: [ + 'If true, display the cumulative distribution by summing the', + 'binned values. Use the `direction` and `centralbin` attributes', + 'to tune the accumulation method.', + 'Note: in this mode, the *density* `histnorm` settings behave', + 'the same as their equivalents without *density*:', + '** and *density* both rise to the number of data points, and', + '*probability* and *probability density* both rise to the', + 'number of sample points.' + ].join(' ') + }, - direction: { - valType: 'enumerated', - values: ['increasing', 'decreasing'], - dflt: 'increasing', - role: 'info', - description: [ - 'Only applies if `cumulative=true.', - 'If *increasing* (default) we sum all prior bins, so the result', - 'increases from left to right. If *decreasing* we sum later bins', - 'so the fresult decreases from left to right.' - ].join(' ') - }, + direction: { + valType: 'enumerated', + values: ['increasing', 'decreasing'], + dflt: 'increasing', + role: 'info', + description: [ + 'Only applies if cumulative is enabled.', + 'If *increasing* (default) we sum all prior bins, so the result', + 'increases from left to right. If *decreasing* we sum later bins', + 'so the result decreases from left to right.' + ].join(' ') + }, - currentbin: { - valType: 'enumerated', - values: ['include', 'exclude', 'half'], - dflt: 'include', - role: 'info', - description: [ - 'Only applies if `cumulative=true.', - 'Sets whether the current bin is included, excluded, or has half', - 'of its value included in the current cumulative value.', - '*include* is the default for compatibility with various other', - 'tools, however it introduces a half-bin bias to the results.', - '*exclude* makes the opposite half-bin bias, and *half* removes', - 'it.' - ].join(' ') + currentbin: { + valType: 'enumerated', + values: ['include', 'exclude', 'half'], + dflt: 'include', + role: 'info', + description: [ + 'Only applies if cumulative is enabled.', + 'Sets whether the current bin is included, excluded, or has half', + 'of its value included in the current cumulative value.', + '*include* is the default for compatibility with various other', + 'tools, however it introduces a half-bin bias to the results.', + '*exclude* makes the opposite half-bin bias, and *half* removes', + 'it.' + ].join(' ') + } }, autobinx: { diff --git a/src/traces/histogram/calc.js b/src/traces/histogram/calc.js index a88953c6815..9b079438b98 100644 --- a/src/traces/histogram/calc.js +++ b/src/traces/histogram/calc.js @@ -33,7 +33,8 @@ module.exports = function calc(gd, trace) { trace.orientation === 'h' ? (trace.yaxis || 'y') : (trace.xaxis || 'x')), maindata = trace.orientation === 'h' ? 'y' : 'x', counterdata = {x: 'y', y: 'x'}[maindata], - calendar = trace[maindata + 'calendar']; + calendar = trace[maindata + 'calendar'], + cumulativeSpec = trace.cumulative; cleanBins(trace, pa, maindata); @@ -47,8 +48,8 @@ module.exports = function calc(gd, trace) { binspec = Axes.autoBin(pos0, pa, trace['nbins' + maindata], false, calendar); // adjust for CDF edge cases - if(trace.cumulative && (trace.currentbin !== 'include')) { - if(trace.direction === 'decreasing') { + if(cumulativeSpec.enabled && (cumulativeSpec.currentbin !== 'include')) { + if(cumulativeSpec.direction === 'decreasing') { binspec.start = pa.c2r(pa.r2c(binspec.start) - binspec.size); } else { @@ -74,8 +75,16 @@ module.exports = function calc(gd, trace) { total = 0, norm = trace.histnorm, func = trace.histfunc, - densitynorm = norm.indexOf('density') !== -1, - extremefunc = func === 'max' || func === 'min', + densitynorm = norm.indexOf('density') !== -1; + + 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', sizeinit = extremefunc ? null : 0, binfunc = binFunctions.count, normfunc = normFunctions[norm], @@ -131,7 +140,7 @@ module.exports = function calc(gd, trace) { if(normfunc) normfunc(size, total, inc); // after all normalization etc, now we can accumulate if desired - if(trace.cumulative) cdf(size, trace.direction, trace.currentbin); + if(cumulativeSpec.enabled) cdf(size, cumulativeSpec.direction, cumulativeSpec.currentbin); var serieslen = Math.min(pos.length, size.length), diff --git a/src/traces/histogram/defaults.js b/src/traces/histogram/defaults.js index 56f8c68e53f..534bddf3f8f 100644 --- a/src/traces/histogram/defaults.js +++ b/src/traces/histogram/defaults.js @@ -27,10 +27,10 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var x = coerce('x'), y = coerce('y'); - var cumulative = coerce('cumulative'); + var cumulative = coerce('cumulative.enabled'); if(cumulative) { - coerce('direction'); - coerce('currentbin'); + coerce('cumulative.direction'); + coerce('cumulative.currentbin'); } coerce('text'); diff --git a/test/jasmine/tests/histogram_test.js b/test/jasmine/tests/histogram_test.js index daf48e17719..a9fed476675 100644 --- a/test/jasmine/tests/histogram_test.js +++ b/test/jasmine/tests/histogram_test.js @@ -247,63 +247,97 @@ describe('Test histogram', function() { }); describe('cumulative distribution functions', function() { - var base = {x: [1, 2, 3, 4, 2, 3, 4, 3, 4, 4]}; + var base = { + x: [0, 5, 10, 15, 5, 10, 15, 10, 15, 15], + y: [2, 2, 2, 14, 6, 6, 6, 10, 10, 2] + }; it('makes the right base histogram', function() { var baseOut = _calc(base); expect(baseOut).toEqual([ - {b: 0, p: 1, s: 1}, - {b: 0, p: 2, s: 2}, - {b: 0, p: 3, s: 3}, - {b: 0, p: 4, s: 4}, + {b: 0, p: 2, s: 1}, + {b: 0, p: 7, s: 2}, + {b: 0, p: 12, s: 3}, + {b: 0, p: 17, s: 4}, ]); }); var CDFs = [ - {p: [1, 2, 3, 4], s: [1, 3, 6, 10]}, + {p: [2, 7, 12, 17], s: [1, 3, 6, 10]}, { direction: 'decreasing', - p: [1, 2, 3, 4], s: [10, 9, 7, 4] + p: [2, 7, 12, 17], s: [10, 9, 7, 4] }, { currentbin: 'exclude', - p: [2, 3, 4, 5], s: [1, 3, 6, 10] + p: [7, 12, 17, 22], s: [1, 3, 6, 10] }, { direction: 'decreasing', currentbin: 'exclude', - p: [0, 1, 2, 3], s: [10, 9, 7, 4] + p: [-3, 2, 7, 12], s: [10, 9, 7, 4] }, { currentbin: 'half', - p: [1, 2, 3, 4, 5], s: [0.5, 2, 4.5, 8, 10] + p: [2, 7, 12, 17, 22], s: [0.5, 2, 4.5, 8, 10] }, { direction: 'decreasing', currentbin: 'half', - p: [0, 1, 2, 3, 4], s: [10, 9.5, 8, 5.5, 2] + p: [-3, 2, 7, 12, 17], s: [10, 9.5, 8, 5.5, 2] }, { direction: 'decreasing', currentbin: 'half', histnorm: 'percent', - p: [0, 1, 2, 3, 4], s: [100, 95, 80, 55, 20] + p: [-3, 2, 7, 12, 17], s: [100, 95, 80, 55, 20] }, { currentbin: 'exclude', histnorm: 'probability', - p: [2, 3, 4, 5], s: [0.1, 0.3, 0.6, 1] + p: [7, 12, 17, 22], s: [0.1, 0.3, 0.6, 1] + }, + { + // behaves the same as without *density* + direction: 'decreasing', currentbin: 'half', histnorm: 'density', + p: [-3, 2, 7, 12, 17], s: [10, 9.5, 8, 5.5, 2] + }, + { + // behaves the same as without *density*, only *probability* + direction: 'decreasing', currentbin: 'half', histnorm: 'probability density', + p: [-3, 2, 7, 12, 17], s: [1, 0.95, 0.8, 0.55, 0.2] + }, + { + currentbin: 'half', histfunc: 'sum', + p: [2, 7, 12, 17, 22], s: [1, 6, 19, 44, 60] + }, + { + currentbin: 'half', histfunc: 'sum', histnorm: 'probability', + p: [2, 7, 12, 17, 22], s: [0.5 / 30, 0.1, 9.5 / 30, 22 / 30, 1] + }, + { + direction: 'decreasing', currentbin: 'half', histfunc: 'max', histnorm: 'percent', + p: [-3, 2, 7, 12, 17], s: [100, 3100 / 32, 2700 / 32, 1900 / 32, 700 / 32] + }, + { + direction: 'decreasing', currentbin: 'half', histfunc: 'min', histnorm: 'density', + p: [-3, 2, 7, 12, 17], s: [8, 7, 5, 3, 1] + }, + { + currentbin: 'exclude', histfunc: 'avg', histnorm: 'probability density', + p: [7, 12, 17, 22], s: [0.1, 0.3, 0.6, 1] } ]; CDFs.forEach(function(CDF) { - var direction = CDF.direction, - currentbin = CDF.currentbin, - histnorm = CDF.histnorm, - p = CDF.p, + var p = CDF.p, s = CDF.s; - it('handles direction=' + direction + ', currentbin=' + currentbin + ', histnorm=' + histnorm, function() { + it('handles direction=' + CDF.direction + ', currentbin=' + CDF.currentbin + + ', histnorm=' + CDF.histnorm + ', histfunc=' + CDF.histfunc, function() { var traceIn = Lib.extendFlat({}, base, { - cumulative: true, - direction: direction, - currentbin: currentbin, - histnorm: histnorm + cumulative: { + enabled: true, + direction: CDF.direction, + currentbin: CDF.currentbin + }, + histnorm: CDF.histnorm, + histfunc: CDF.histfunc }); var out = _calc(traceIn); From 53b61aa796465534c46834a81e60f9ff19b2f781 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 18 Jan 2017 15:41:51 -0500 Subject: [PATCH 7/7] CDF test image --- test/image/baselines/hist_cum_stacked.png | Bin 0 -> 6776 bytes test/image/mocks/hist_cum_stacked.json | 41 ++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 test/image/baselines/hist_cum_stacked.png create mode 100644 test/image/mocks/hist_cum_stacked.json diff --git a/test/image/baselines/hist_cum_stacked.png b/test/image/baselines/hist_cum_stacked.png new file mode 100644 index 0000000000000000000000000000000000000000..a12f24cc4bfde198144d5227bdf0e267e5a73c5d GIT binary patch literal 6776 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 CnBbiN literal 0 HcmV?d00001 diff --git a/test/image/mocks/hist_cum_stacked.json b/test/image/mocks/hist_cum_stacked.json new file mode 100644 index 00000000000..6cfedaefe05 --- /dev/null +++ b/test/image/mocks/hist_cum_stacked.json @@ -0,0 +1,41 @@ +{ + "data": [{ + "x": [1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 8, 8, 9, 9, 10], + "type": "histogram", + "cumulative": {"enabled": true}, + "xbins": {"start": 0.5, "end": 10.5, "size": 1}, + "marker": {"color": "blue", "line": {"width": 2, "color": "#000"}}, + "name": "A" + }, + { + "x": [3, 3, 4, 5, 6, 7, 7], + "type": "histogram", + "cumulative": {"enabled": true, "currentbin": "exclude"}, + "xbins": {"start": 0.5, "end": 10.5, "size": 1}, + "marker": {"color": "red", "line": {"width": 2, "color": "#000"}}, + "name": "B" + }, + { + "x": [1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 8, 8, 9, 9, 10], + "type": "box", + "orientation": "h", + "yaxis": "y2", + "line": {"color": "blue"}, + "showlegend": false + }, + { + "x": [3, 3, 4, 5, 6, 7, 7], + "type": "box", + "orientation": "h", + "yaxis": "y2", + "line": {"color": "red"}, + "showlegend": false + }], + "layout": { + "yaxis": {"domain": [0, 0.8]}, + "yaxis2": {"domain": [0.8, 1], "showline": false, "showticklabels": false}, + "height": 300, + "width": 400, + "barmode": "stack" + } +}