From e5a535c52a788a2bc8da26adac93a4b3396e6ac8 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 8 Aug 2017 14:28:55 -0400 Subject: [PATCH 1/8] histogram autobins match each other when possible --- src/traces/histogram/calc.js | 276 ++++++++++++++++++++------- test/jasmine/tests/histogram_test.js | 53 ++++- 2 files changed, 261 insertions(+), 68 deletions(-) diff --git a/src/traces/histogram/calc.js b/src/traces/histogram/calc.js index 009c59546d5..f0ef130df33 100644 --- a/src/traces/histogram/calc.js +++ b/src/traces/histogram/calc.js @@ -19,6 +19,7 @@ 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) { @@ -27,60 +28,35 @@ module.exports = function calc(gd, trace) { // 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 = [], - size = [], - i, - pa = Axes.getFromId(gd, - 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'], - cumulativeSpec = trace.cumulative; + 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); - // prepare the raw data - var pos0 = pa.makeCalcdata(trace, maindata); + var binspec = calcAllAutoBins(gd, trace, pa, maindata); - // calculate the bins - var binAttr = maindata + 'bins'; - var autoBinAttr = 'autobin' + maindata; - var binspec = trace[binAttr]; - if((trace[autoBinAttr] !== false) || !binspec || - binspec.start === null || binspec.end === null) { - binspec = Axes.autoBin(pos0, pa, trace['nbins' + maindata], false, calendar); - - // adjust for CDF edge cases - if(cumulativeSpec.enabled && (cumulativeSpec.currentbin !== 'include')) { - if(cumulativeSpec.direction === 'decreasing') { - binspec.start = pa.c2r(pa.r2c(binspec.start) - binspec.size); - } - else { - binspec.end = pa.c2r(pa.r2c(binspec.end) + binspec.size); - } - } + // the raw data was prepared in calcAllAutoBins (during the first trace in + // this group) and stashed. Pull it out and drop the stash + var pos0 = trace._pos0; + delete trace._pos0; - // copy bin info back to the source and full data. - trace._input[binAttr] = trace[binAttr] = binspec; - // 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. - trace._input[autoBinAttr] = trace[autoBinAttr]; - } - - var nonuniformBins = typeof binspec.size === 'string', - bins = nonuniformBins ? [] : binspec, - // make the empty bin array - i2, - binend, - n, - inc = [], - counts = [], - total = 0, - norm = trace.histnorm, - func = trace.histfunc, - densitynorm = norm.indexOf('density') !== -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, @@ -89,13 +65,13 @@ module.exports = function calc(gd, trace) { densitynorm = false; } - var extremefunc = func === 'max' || func === 'min', - sizeinit = extremefunc ? null : 0, - binfunc = binFunctions.count, - normfunc = normFunctions[norm], - doavg = false, - pr2c = function(v) { return pa.r2c(v, 0, calendar); }, - rawCounterData; + 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]; @@ -104,7 +80,7 @@ module.exports = function calc(gd, trace) { } // create the bins (and any extra arrays needed) - // assume more than 5000 bins is an error, so we don't crash the browser + // 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 @@ -150,10 +126,11 @@ module.exports = function calc(gd, trace) { if(cumulativeSpec.enabled) cdf(size, cumulativeSpec.direction, cumulativeSpec.currentbin); - var serieslen = Math.min(pos.length, size.length), - cd = [], - firstNonzero = 0, - lastNonzero = serieslen - 1; + 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]) { @@ -180,10 +157,181 @@ module.exports = function calc(gd, 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'; + + // 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; + + return trace[binAttr]; + } + + // 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; + var i, tracei, calendar, firstManual; + + + 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 + var 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). + // + // TODO: there's probably a weird case here where a larger bin pushes the + // start/end out, then it gets shrunk and doesn't make sense with the smaller bin. + // Need to look for cases like this and see if the results are acceptable + // or we need to think harder about it. + 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]; + } + + return trace[binAttr]; +} + +/* + * return an array of traces that are all stacked or grouped together + * TODO: only considers histograms. Should we also harmonize with bars? + * in principle people can mix and match these, but bars always + * specify their positions explicitly... + */ +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; + var i, vi, prevSum; function firstHalfPoint(i) { prevSum = size[i]; diff --git a/test/jasmine/tests/histogram_test.js b/test/jasmine/tests/histogram_test.js index 922adf4d497..e9a2192de83 100644 --- a/test/jasmine/tests/histogram_test.js +++ b/test/jasmine/tests/histogram_test.js @@ -7,6 +7,7 @@ var calc = require('@src/traces/histogram/calc'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); +var customMatchers = require('../assets/custom_matchers'); describe('Test histogram', function() { @@ -162,10 +163,20 @@ describe('Test histogram', function() { describe('calc', function() { - function _calc(opts) { - var base = { type: 'histogram' }, - trace = Lib.extendFlat({}, base, opts), - gd = { data: [trace] }; + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + function _calc(opts, extraTraces) { + var base = { type: 'histogram' }; + var trace = Lib.extendFlat({}, base, opts); + var gd = { data: [trace] }; + + if(Array.isArray(extraTraces)) { + extraTraces.forEach(function(extraTrace) { + gd.data.push(Lib.extendFlat({}, base, extraTrace)); + }); + } Plots.supplyDefaults(gd); var fullTrace = gd._fullData[0]; @@ -263,6 +274,40 @@ describe('Test histogram', function() { expect(out.length).toEqual(9001); }); + function calcPositions(opts, extraTraces) { + return _calc(opts, extraTraces).map(function(v) { return v.p; }); + } + + it('harmonizes autobins when all traces are autobinned', function() { + var trace1 = {x: [1, 2, 3, 4]}; + var trace2 = {x: [5, 5.5, 6, 6.5]}; + + expect(calcPositions(trace1)).toEqual([0.5, 2.5, 4.5]); + + expect(calcPositions(trace2)).toEqual[5, 6, 7]; + + expect(calcPositions(trace1, [trace2])).toEqual([1, 2, 3, 4]); + expect(calcPositions(trace2, [trace1])).toEqual([5, 6, 7]); + }); + + it('harmonizes autobins with smaller manual bins', function() { + var trace1 = {x: [1, 2, 3, 4]}; + var trace2 = {x: [5, 6, 7, 8], xbins: {start: 4.3, end: 7.1, size: 0.4}}; + + expect(calcPositions(trace1, [trace2])).toBeCloseToArray([ + 0.9, 1.3, 1.7, 2.1, 2.5, 2.9, 3.3, 3.7, 4.1 + ], 5); + }); + + it('harmonizes autobins with larger manual bins', function() { + var trace1 = {x: [1, 2, 3, 4]}; + var trace2 = {x: [5, 6, 7, 8], xbins: {start: 4.3, end: 15, size: 7}}; + + expect(calcPositions(trace1, [trace2])).toBeCloseToArray([ + 0.8, 2.55, 4.3 + ], 5); + }); + describe('cumulative distribution functions', function() { var base = { x: [0, 5, 10, 15, 5, 10, 15, 10, 15, 15], From 11821c2c20af634a7d933a59e393ec183ac8d8be Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 10 Aug 2017 00:33:40 -0600 Subject: [PATCH 2/8] extra test for histograms on other axes --- test/jasmine/tests/histogram_test.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/jasmine/tests/histogram_test.js b/test/jasmine/tests/histogram_test.js index e9a2192de83..d62e8eb503d 100644 --- a/test/jasmine/tests/histogram_test.js +++ b/test/jasmine/tests/histogram_test.js @@ -308,6 +308,16 @@ describe('Test histogram', function() { ], 5); }); + it('ignores traces on other axes', function() { + var trace1 = {x: [1, 2, 3, 4]}; + var trace2 = {x: [5, 5.5, 6, 6.5]}; + var trace3 = {x: [1, 1.1, 1.2, 1.3], xaxis: 'x2'}; + var trace4 = {x: [1, 1.2, 1.4, 1.6], yaxis: 'y2'}; + + expect(calcPositions(trace1, [trace2, trace3, trace4])).toEqual([1, 2, 3, 4]); + expect(calcPositions(trace3)).toBeCloseToArray([0.9, 1.1, 1.3]); + }); + describe('cumulative distribution functions', function() { var base = { x: [0, 5, 10, 15, 5, 10, 15, 10, 15, 15], From ec12eab5e17fcdf2985d1b888b44dda99614fbf3 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 14 Aug 2017 18:32:27 -0400 Subject: [PATCH 3/8] clear - and test - TODOs we're not going to address now also fixed a typo in the test that means... we were actually already sort of supposed to be testing the TODO about start/size interactions --- src/traces/histogram/calc.js | 13 +++------ test/jasmine/tests/histogram_test.js | 41 ++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/traces/histogram/calc.js b/src/traces/histogram/calc.js index f0ef130df33..62ef774984b 100644 --- a/src/traces/histogram/calc.js +++ b/src/traces/histogram/calc.js @@ -233,11 +233,6 @@ function calcAllAutoBins(gd, trace, pa, maindata) { // 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). - // - // TODO: there's probably a weird case here where a larger bin pushes the - // start/end out, then it gets shrunk and doesn't make sense with the smaller bin. - // Need to look for cases like this and see if the results are acceptable - // or we need to think harder about it. 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)); @@ -283,10 +278,10 @@ function calcAllAutoBins(gd, trace, pa, maindata) { } /* - * return an array of traces that are all stacked or grouped together - * TODO: only considers histograms. Should we also harmonize with bars? - * in principle people can mix and match these, but bars always - * specify their positions explicitly... + * 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]; diff --git a/test/jasmine/tests/histogram_test.js b/test/jasmine/tests/histogram_test.js index d62e8eb503d..efd03c6da4b 100644 --- a/test/jasmine/tests/histogram_test.js +++ b/test/jasmine/tests/histogram_test.js @@ -282,14 +282,49 @@ describe('Test histogram', function() { var trace1 = {x: [1, 2, 3, 4]}; var trace2 = {x: [5, 5.5, 6, 6.5]}; - expect(calcPositions(trace1)).toEqual([0.5, 2.5, 4.5]); + expect(calcPositions(trace1)).toBeCloseToArray([0.5, 2.5, 4.5], 5); - expect(calcPositions(trace2)).toEqual[5, 6, 7]; + expect(calcPositions(trace2)).toBeCloseToArray([5.5, 6.5], 5); expect(calcPositions(trace1, [trace2])).toEqual([1, 2, 3, 4]); + // huh, turns out even this one is an example of "unexpected bin positions" + // (see another example below) - in this case it's because trace1 gets + // autoshifted to keep integers off the bin edges, whereas trace2 doesn't + // because there are as many integers as half-integers. + // In this case though, it's unexpected but arguably better than the + // "expected" result. expect(calcPositions(trace2, [trace1])).toEqual([5, 6, 7]); }); + it('can sometimes give unexpected bin positions', function() { + // documenting an edge case that might not be desirable but for now + // we've decided to ignore: a larger bin sets the bin start, but then it + // doesn't quite make sense with the smaller bin we end up with + // we *could* fix this by ensuring that the bin start is based on the + // same bin spec that gave the minimum bin size, but incremented down to + // include the minimum start... but that would have awkward edge cases + // involving month bins so for now we're ignoring it. + + // all integers, so all autobins should get shifted to start 0.5 lower + // than they otherwise would. + var trace1 = {x: [1, 2, 3, 4]}; + var trace2 = {x: [-2, 1, 4, 7]}; + + // as above... size: 2 + expect(calcPositions(trace1)).toBeCloseToArray([0.5, 2.5, 4.5], 5); + + // {size: 5, start: -5.5}: -5..-1, 0..4, 5..9 + expect(calcPositions(trace2)).toEqual([-3, 2, 7]); + + // unexpected behavior when we put these together, + // because 2 and 5 are mutually prime. Normally you could never get + // groupings 1&2, 3&4... you'd always get 0&1, 2&3... + expect(calcPositions(trace1, [trace2])).toBeCloseToArray([1.5, 3.5], 5); + expect(calcPositions(trace2, [trace1])).toBeCloseToArray([ + -2.5, -0.5, 1.5, 3.5, 5.5, 7.5 + ], 5); + }); + it('harmonizes autobins with smaller manual bins', function() { var trace1 = {x: [1, 2, 3, 4]}; var trace2 = {x: [5, 6, 7, 8], xbins: {start: 4.3, end: 7.1, size: 0.4}}; @@ -315,7 +350,7 @@ describe('Test histogram', function() { var trace4 = {x: [1, 1.2, 1.4, 1.6], yaxis: 'y2'}; expect(calcPositions(trace1, [trace2, trace3, trace4])).toEqual([1, 2, 3, 4]); - expect(calcPositions(trace3)).toBeCloseToArray([0.9, 1.1, 1.3]); + expect(calcPositions(trace3)).toBeCloseToArray([0.9, 1.1, 1.3], 5); }); describe('cumulative distribution functions', function() { From 3f173a447fcb09096026230501e0662916af8971 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 14 Aug 2017 18:44:35 -0400 Subject: [PATCH 4/8] test new histogram logic in images --- test/image/baselines/hist_grouped.png | Bin 7575 -> 6667 bytes test/image/baselines/hist_stacked.png | Bin 6771 -> 6564 bytes test/image/mocks/hist_grouped.json | 12 ++++++------ test/image/mocks/hist_stacked.json | 14 +++++++------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/test/image/baselines/hist_grouped.png b/test/image/baselines/hist_grouped.png index afe4759741f3f1141e6be410972be875d44de4a3..a5d9e71a7941283f715b1bf3e0d35918b5ee555f 100644 GIT binary patch literal 6667 zcmeHMc{J4jyEoJL8cN2Jvc$()_H0SEQ5X_JvYUuXvV<_QjHS$%3WGtmQlf|?TSS;> zY?W-;!;BW|Ok>H?eXHN^-upY}{&CN_f82Y{ZO)m`d7t;^c|V`$^Sqze`*}Uj=kHVI z7)}mB4i**`P7`ATD;5@568Os_{{k~})zoGd7J(iU1O3w>E_0*TF5C3wzFHEsB1an= zecms<@4+GXU$he{er0wN`-Ji(?;k{%u&PBPQ!%)UPta;bRA+QjA?ozSL;A{w#t>3H zMCvS}{?fEqD*pXh))v%nFtI^c?g?(#s8?BN^Rs}M-! zwQVspS_+LmeR-GIwg3vXye)>upTOgb2KOD?7J#7H-_J>7fmxO-$8HOt(StjFDVBx7 z^WDxNw*~O{r)<9zpN1n0UbmvQ^{~KPxwfB=EJ7d&@wdc&*$ZzL`D5T85B`b4{|QLg z2XF7!k~U3OOzY|CnRwHBOQ>VJ5Ol(-&ZH;#nq_sIGk0VQ=E>?Bz$!byX#H z<;SWne-xi;ODyYNzu|=e=#ItP`9z1WFH>r+Ou4_$HzcJ9-v$cErg*$v$xN~Y#+}xg z*;8V#N}*87Gp0`g07DUQge3Wnh<3;+|A2tkhzDRfF)vSv`E7tpT25B>SawsF*q+Gj ztYwCG^;j=O@pn zk?gZINAA_`SM~5K?DS&`;DamkL6D7?B|E`QCv8TsP`T|oT+-jeDMdy!QqaYXOF>;l?A2D z%z+5mzcEnX2C- z|Dh|B2Fi(N%=2^=5_NwRWoUfM$Xct=7!R{vUKm%Zywt~WTUE5cU6*@mu6mr5a=@Zp zXJtg<_>EocTkEf@SH>f;5kEJBamybs#ORIH1(%)7R53jNHg}Fp!XI&!ghciMCyXJ8 zdr$!|8g_GKG@BivdYhK%=;2Wiwmw(g^UzFajX8jg@m-~P7KSpv zQUc!U9I(*a3_KM1{quXspmBqquuJIh$%htsiUI-xDmeP|gv_%8d0}*(iv0ZiiR>J$ z;N4pz+P9cwUW+$vqLH)zYDNBTV|)8dK0ZDt#t%B39xdu}c;UO+l?)BvF{!qQ)lYG5 z5wCC)ucDgrBAI=*^lq&M0(%|@IR4TPeV;QVNSlDD@B&4LI=&eG&PU7m3TVcvd zx5tBqy_@p#Gy|kpf70%g6dN)JbUVF761C~Zi*0R*qUj zj3Boh~uF!K-F6iWl3o}RJCu7swbAdfW3>;0OckMDRfJEPk{IF>#?T~Q@|*1t4+^s` zm02hTQF>`!Vs}x5BM{|BS{1^eTq9pZ46-a^!HPy9;M{DI_KiIx{DX;c1e_S1(CS?3 zvi(-6zbC7tA(@(sBh;@_No2girkrS;}vXJD#eCB7caRw?Dze%rG2e` zLEzkw&DY*X5~d`1ZU%pS`8kjV?7|Lw5(nH^4vm(lF3*0HNRc)p;uth;l&RKk3`5&%4JQAg(MW#pmNkz2<$MvG#oUkg_SY7COKUL1NWyApb^(?Amt-Jyhj= z&GqpMRuvHAMCv?)4l({Q3537u6!qi4H#%uz|P2DZXy zMeLXONH2wWkp?m#gg^75uAPRv`L-h9mOBCBfhT7{9)BqS?kj-%B`mOVj1;td{v?E| zWPvR`g(Ju!Y52h` zTsMC5{S*#+Wq96GK^Mo+(3;iZD#Lu+0>u^YIf4M&6I5L$K7Zhspz*@g3Cj4c~W%~q9|Aj49fTKQr+KW!6Ax>_; zlGV|sO`WH98Y|e15rL^N@3D820d8=&auUJ-(C*6_jJVddtjpPDX63zo-e9KW2uuv( zAEuTapV}}x+&%EnVfL%K0YFN1V`Kq?kHqu`_)^d81HhS#A8rB=iRS?jaoJI>vD0Ok zeQhXha`uxE()dVWhKM0rTVDPj07DqKwi2vC21{kUP2v6xDkC)C`I|@fK#;j6JAssf zKR6x2VH(v+Fa=^(jFH+9gj?bX0B%4P4xJ-EVu4+0r%0d_4!|tmEFlg4DVTzRE}6me z?@B-%YDP{VSkvzy44#;v`2rFDA%ibcXD3A8#vgpQ?+Vxqr%ODjN@caaOm2zeG6nU~gS&UpK8>5pUgqZ5YSV{h!}}*Y zgW}}9EM-^b^L#b2wla2YAzt-pzY>Lm&=j#?^2L<$5V7JhDy5VZ)IzvE%js0x!!k7X$4^u$t$A-z( z3@~~TydV;L-{oa!F6sIQanKO60mq)CodhEvH{KBL@$Rnv-m&YnV7 z?PgvgI5-jWz!Z<32PSmJTRl_2LZS;R1l2F`xe38+U(QF=RACz<+{Q9jCt|o-zx33QP}@dW^~T9#8=MY%4@n*{&rSl=o-7T4 zO-)Oavpq3T>Re!+bM%=zg-1qCE(Pc|GK*-zj)$F7_af!wC{%h4?2cgRs~0%8vAU!Y zHvc#<+SUbUcw1CY(fLK77-Zw=N({szjfr=Cr#t>NO$cS-oqV2Mc$+)%s2Q-YQKW5J z_GqlGZw zH~$|*B9Ah+vIUd;C20B$ccx~*`9;l3nwpyBHP1mf-`*r(} zGXhr5i2wfi2-}1DdhL){^(&KPyZXP}4!48AjKSQLhUSK=xk}4Gy_GyKW{Vxs?ivKv zOvHJb39}RAKK35vf|=Z7#(SYVHS8E&S6|^_O&SksMuE5X)MxmN9Xh*J=>jmD+CTZ1OOqLH%}XBU+73tFSQyY;lh*Rd7Yd2d2C*CZH1< zxR|nOZ*MR2oagU_uOOtOgGuTa-$6%0Ks2a;BV{7z{7mDLw65tT;UB52nXYun9{jbBi?PSNRyodQW1X&p0vukx3JBl{fSJFE>J5M*1 z6psbNdFrG%!}Sr8YJ1qWqaW!c5Bi^6e>%v2dhq|(J`zm)gl#!sy#;%nior)77865r KgVJMXqyG(My%Q$@ literal 7575 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 diff --git a/test/image/baselines/hist_stacked.png b/test/image/baselines/hist_stacked.png index 6c549fc6817eae3ef5f7aa7f3d850e63f0012db9..72cb9a5ec045d9168dbe6cf6621c22b28abf6e1b 100644 GIT binary patch literal 6564 zcmeHMS5#B&woNuc0#ZU3rECE~KuWM6(xNB?L{L-^5Ksga5o17!G!sxMzk;DF)dGT4 zLFpxmRB4I`(v>PAEp$l8`SPE0k9UlF$2bq?<=!*?hdgBOm6etK&AHZ`E1}2Djrn-C z@*ofhJ`?<5O9TQ*1+QJ`P2kE@@n|&yA>n3n_~5Bq4paTU8K+%R-p-}CI)p!dbYS@0 zBXRr@M78Dt&VX35Cgpaf8e@orT;%he^NWOKVlWDgH~D9;@`xrfLo( z3WGxgJc3T6@n%?lV+43V3!(0YQ4kOsej)_J%KL&q1QLZt`?7E1a653gQ&)MnvjZ@g zC3YBvVoIT8eBE-09RS0>{>+JtK_E}$WACy9aJa7=APfO1BcW$b7trhg3gs2opNUUF zsKd2&7R#tabG(yuLgW_tY?WO)gI$#O!6lM@#1!*VbsJp09YVXnzMN)vn0`$CF)OLL7E( zOV+bYGZiI|lzNV*Uip&QUg%~mq3>5Rq77@NY1);?WF15Vm~msMkD7?PE1~T$R*NfG z{NdZNiMiaxBC@C}>uSa0gm0H2AzTz0~p)n=a5EmS$<@P@RbaU;3q(>&G0Rl18!sYjaM>^rUkpT(bt^8zSm zw7El0Q@v5`;&?1fs9cPFbG6%Yn#S7bAh&K7S8I6V`Z@JBbFIMjvw}>7ybP))(*&0t z*epk_!kHphf4y#Veiz7TS?=XtI#tv=k{0I>*=E2xYiepbl9g8#X&v<9d$w^t5ct6a1qpB=6Xb6H=SqA&Fl)&-XOJlpg( zRAT$cWae;_IxTu>s<-LFLp=o<|AiCJI#D$r{#zYHgRgFRl?uxPzB6B};hD;a zCi66%YS7-y#jdpT?}IemI(M5a;mIuWj;@jI;nOQ<#SshPFd-m*5xRlP=^L)~b1IsGLcW=SP zmaX24U##<1-wpW9A0v`S{MM(6*U8fk=Sx_<{-z5?0uBL(`@~<$7~w1x$M2#aaY9@p zjMe$}V!~jSL6C3Z+{*C7raJ=qKUm%0-rcFK*IL?jl+h0tO6MD!s^ zL)@VZPjl2&NNJPv_v(8aaU&M`tEse$%R1Caj-d^mEw$Q8Ygk^=ajzaO@CZpdSRcIe|esXhhxuA{m{i9&mVs1Ck?7{ z2}m!bRIg2>r=(tNiQl+TN*eG^2PbXVXJ?}(IsMt%LT@ozcj2QUNBnPp5PbL$Y&KwIa92L%;(X3h=7hL<8axVjsJ|I z;39Z1n8|8EOcp$khL5L0fM(ZGHOdXT5L)CV0-3KS`7b|~sy4e;$INf-er3+*O*eSo zhf(gl_rgyDnI9D{9z2MzU6PSaA@vNCquN=Qg;rM*Msbjny({D->MR}5!P(A> z|6$itMwq&}EJk!p@fh@9Zk+x8el*tZnn_I}MdY0B&7aY8uGE zlzc>3LljJXfs03SdmrtUAu~=m_{|-8tV7DB<>Zu?#;jM@CF}VrR3F3fXq2fd_m1#Nn2zhpo*I#m?2JRek%mQ!x{wEWn$u3US)O5bfw z{iKoMhX&@(N@zT9-G>jRzgv| ze&gEWB-8&$wg4n_@MFL4*M4j>XR2W^|Hei2=}p*;h~dEi#=fEJC@KDfuwDRYbvrx? z{*i_xGfk2fFkrkQd1|Hp{E{V}hZE{Pf=#KZWO&ldNXu$f9`kW8hC6HhVu!RD*IQV= zF{gFoB$=k^r;H3@ z#p!-Kv0Z0izO8#O&@)7HH5%Dit&A!U+QT742S27i3N(fF%Ow`)ON-yc?{!n~Ut2J# zNd`xFBjvlAlG5DDbiPZIm`%m4{OmLN)wEzds4{rMfE#HtsB3u1Y&ZOr;5w<#J*IE^ zYha%z;a$1!%W}JK3UgpUiIArIl7nsLy&7*lF;v8l zZf%!%WMivE5+eqKd9H-SLP1*<;in_Ji325r_ZHQQyGF9iuD$X=)s!T0hX`tP>_>9% zX!G`z$Kf8_H^DuiJgwu#@APg>kV8UFXU!ld6z^Vh+@VI#0ZE{Uhn<8la_2ILkdSHe z{1~1>*&-fA*@83-6Q&-gCu$i33Y9!U)u6XrNu=D39Iwy@bG5Ek9yWS=d>ADktvlWl zfk0x_6#iM3HX$5{ZzA^pQ7M+^uUy zDr>)AP3I_tN_~4JQP-azDKLQfu^%qp?6Zcs)LlNO8WRUGiv11ctF)?k(|qhh#OcbnJwLKC7J! z7N(l1a*3ZzWNWB0DIG5}(6^mIb7t_%2ib`U=$%|z-sK?Hbqe>yD#vz<3plaEXCDAE z$~+9<;4&@un#wgFU5)!8==ul!m0lDpYiHj2McQR3GDiw%uk3m-OPv`jbC$NY${=~g z#X!noMDw@p!H(-QybhvqcX33ZZlKAVYYVQAHO>bv{OUe=;O5T-0b^n6P|vJ&ZY)J) zyo~szn=P+8F92<)Nf^+5X_eEn*ElvV?r4@(jt7$jclTsAI`AP)I-@R?YjXlP^m}sx z-0d;PLGL_!&a!dm!N%PafID>Z=P}^_w#2b<#{qDMn@!wj&Bk3K8+VrzDX75lH>GUc zjk0lv1GvNJrndlph*eVrxZ4Ei5b#agJ(rC;ppgVMJ^~7$+FU(>)qK#zra%Co0DQ|l zL4{2Lb2bJ3b4n46^Xx5h*L3gMqol4LC#Gt9Hc1v1MDF(5f^7U6cwPGlTXgU0%{-mu zJlUD!vGmJELeFOpGW1;Rq9GdJX&o}DvWcy{W4xw%cDVPvi6|)0l+biW|Br|ZF-GB% zNZM34CXf(P<4!P!%ByA?eN-0cR!f~AipZ0y8xxVb^Ub?8{Z_8K4pvoWJ5<_w z{rb9HoyO2anz$z@kY%CB3RmSeJkIibxQ`HIiMuj0P+4&Klk8(HLb3rJETG!6tkrF* z{7O4=?Gvow6C~TZa5-4Qj+zl?h%zJlm9u2p=MaVlr#n^jL`GOq=+!6224@8&&ZR5cbscN1lxfR^YCs>Vz2E-&l7&n-XtU0jl}bg}4%!|GS-$OMwpTdP zZfo9}{gwrU^w52E=gw@KiCmFOhG3tislcXn!@E0NE@RD)>A-j4qZE!YdTqkC?)^if zaXZ*^;cf%N9FaX$%=;*}Yb?FT;a+88T1G1TL7R==U@FYO6E^v8Vq2F@Ey|3hi z*UW(Gmn`eJ&Kx^o44+d(8<^#`ZzJT4*NS=Qbg|CG1-H&@L2;~xXQMfU>KgO2$W75r%38}HPfo&u@{WgZgUOoV_Qh3ijg=K{MeP0 zJYc8~E+4m4BoTHm{rL2rh0VDyhcq!QDoGBeh2C9U>*9F4Mkd(c$o(sUB*DJ2#vrFr!v!qkml zU$OQdquV;mNd%i$=K}Uz{s@omR`flvj9t?6UBbsGpJoWB=#W~C^TDBX`Hfl>TpH~z z*ongxlqi!uj(KkmSzl( literal 6771 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 diff --git a/test/image/mocks/hist_grouped.json b/test/image/mocks/hist_grouped.json index 6c1a7a6ade1..e41f2042a2b 100644 --- a/test/image/mocks/hist_grouped.json +++ b/test/image/mocks/hist_grouped.json @@ -1,10 +1,10 @@ { "data":[{ - "x":["1","2","3","4"], - "type":"histogram" - },{ - "x":["1","2","3","4"], - "type":"histogram" + "x": [1, 1, 1, 2, 2], + "type": "histogram" + }, { + "x": [1, 2, 3, 4], + "type": "histogram" }], - "layout":{"height":300,"width":400} + "layout": {"height": 300, "width": 400} } diff --git a/test/image/mocks/hist_stacked.json b/test/image/mocks/hist_stacked.json index 59b3c32d640..5588ec4989e 100644 --- a/test/image/mocks/hist_stacked.json +++ b/test/image/mocks/hist_stacked.json @@ -1,10 +1,10 @@ { - "data":[{ - "x":["1","2","3","4"], - "type":"histogram" - },{ - "x":["1","2","3","4"], - "type":"histogram" + "data": [{ + "x": [1, 1, 1, 2, 2], + "type": "histogram" + }, { + "x": [1, 2, 3, 4], + "type": "histogram" }], - "layout":{"height":300,"width":400,"barmode":"stack"} + "layout": {"height": 300, "width": 400, "barmode": "stack"} } From 89093ad688568d229baa967b440a7a8cd92970be Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 14 Aug 2017 19:01:00 -0400 Subject: [PATCH 5/8] handle _pos0 totally inside calcAllAutoBins --- src/traces/histogram/calc.js | 192 +++++++++++++++++------------------ 1 file changed, 95 insertions(+), 97 deletions(-) diff --git a/src/traces/histogram/calc.js b/src/traces/histogram/calc.js index 62ef774984b..9a1b994dee0 100644 --- a/src/traces/histogram/calc.js +++ b/src/traces/histogram/calc.js @@ -40,12 +40,9 @@ module.exports = function calc(gd, trace) { cleanBins(trace, pa, maindata); - var binspec = calcAllAutoBins(gd, trace, pa, maindata); - - // the raw data was prepared in calcAllAutoBins (during the first trace in - // this group) and stashed. Pull it out and drop the stash - var pos0 = trace._pos0; - delete trace._pos0; + 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; @@ -166,115 +163,116 @@ module.exports = function calc(gd, trace) { */ 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; - - return trace[binAttr]; } + 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); + } + } - // 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; - var i, tracei, calendar, firstManual; - + // 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) + }; + } - for(i = 0; i < traceGroup.length; i++) { - tracei = traceGroup[i]; + // 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; + } - // stash pos0 on the trace so we don't need to duplicate this - // in the main body of calc - var pos0 = tracei._pos0 = pa.makeCalcdata(tracei, maindata); - var binspec = tracei[binAttr]; + // 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); + } - if((tracei[autoBinAttr]) || !binspec || - binspec.start === null || binspec.end === null) { + // 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']; - 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); - } - } + 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. mark this trace for followup - autoBinnedTraces.push(tracei); + // 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]; } - 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]; + return [trace[binAttr], pos0]; } /* From 2071acb34a160d8a7995b9936b3968e183ef0d95 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 14 Aug 2017 19:10:14 -0400 Subject: [PATCH 6/8] camelCase histogram.calc --- src/traces/histogram/calc.js | 116 +++++++++++++++++------------------ 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/src/traces/histogram/calc.js b/src/traces/histogram/calc.js index 9a1b994dee0..fee1099eb4c 100644 --- a/src/traces/histogram/calc.js +++ b/src/traces/histogram/calc.js @@ -32,67 +32,67 @@ module.exports = function calc(gd, trace) { 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 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); + cleanBins(trace, pa, mainData); - var binsAndPos = calcAllAutoBins(gd, trace, pa, maindata); - var binspec = binsAndPos[0]; + 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; + 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; + var densityNorm = norm.indexOf('density') !== -1; + var i2, binEnd, n; - if(cumulativeSpec.enabled && densitynorm) { + 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; + 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 extremeFunc = func === 'max' || func === 'min'; + var sizeInit = extremeFunc ? null : 0; + var binFunc = binFunctions.count; + var normFunc = normFunctions[norm]; + var isAvg = 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]; + if(Array.isArray(trace[counterData]) && func !== 'count') { + rawCounterData = trace[counterData]; + isAvg = 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); + 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; + 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); + while(i < binEnd && pos.length < 1e6) { + i2 = Axes.tickIncrement(i, binSpec.size, false, calendar); pos.push((i + i2) / 2); - size.push(sizeinit); + 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); + if(densityNorm) inc.push(1 / (i2 - i)); + if(isAvg) counts.push(0); // break to avoid infinite loops if(i2 <= i) break; i = i2; @@ -112,30 +112,30 @@ module.exports = function calc(gd, trace) { // 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); + 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); + if(isAvg) 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 seriesLen = Math.min(pos.length, size.length); var cd = []; var firstNonzero = 0; - var lastNonzero = serieslen - 1; + var lastNonzero = seriesLen - 1; // look for empty bins at the ends to remove, so autoscale omits them - for(i = 0; i < serieslen; i++) { + for(i = 0; i < seriesLen; i++) { if(size[i]) { firstNonzero = i; break; } } - for(i = serieslen - 1; i > firstNonzero; i--) { + for(i = seriesLen - 1; i > firstNonzero; i--) { if(size[i]) { lastNonzero = i; break; @@ -161,8 +161,8 @@ module.exports = function calc(gd, trace) { * smallest bins of any of the auto values for all histograms grouped/stacked * together. */ -function calcAllAutoBins(gd, trace, pa, maindata) { - var binAttr = maindata + 'bins'; +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 @@ -179,30 +179,30 @@ function calcAllAutoBins(gd, trace, pa, maindata) { var minStart = Infinity; var maxEnd = -Infinity; - var autoBinAttr = 'autobin' + maindata; + 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]; + 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']; + 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); + 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); + 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); + maxEnd = Math.max(maxEnd, pa.r2c(binSpec.end, 0, calendar) + binSpec.size); } } @@ -211,14 +211,14 @@ function calcAllAutoBins(gd, trace, pa, maindata) { autoBinnedTraces.push(tracei); } else if(!firstManual) { - // Remember the first manually set binspec. We'll try to be extra + // 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) + size: binSpec.size, + start: pa.r2c(binSpec.start, 0, calendar), + end: pa.r2c(binSpec.end, 0, calendar) }; } @@ -227,9 +227,9 @@ function calcAllAutoBins(gd, trace, pa, maindata) { // 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)); + 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; @@ -253,7 +253,7 @@ function calcAllAutoBins(gd, trace, pa, maindata) { // 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']; + calendar = tracei[mainData + 'calendar']; tracei._input[binAttr] = tracei[binAttr] = { start: pa.c2r(minStart, 0, calendar), @@ -323,7 +323,7 @@ function numericSize(size) { return Infinity; } -function cdf(size, direction, currentbin) { +function cdf(size, direction, currentBin) { var i, vi, prevSum; function firstHalfPoint(i) { @@ -337,7 +337,7 @@ function cdf(size, direction, currentbin) { prevSum += vi; } - if(currentbin === 'half') { + if(currentBin === 'half') { if(direction === 'increasing') { firstHalfPoint(0); @@ -358,7 +358,7 @@ function cdf(size, direction, currentbin) { } // 'exclude' is identical to 'include' just shifted one bin over - if(currentbin === 'exclude') { + if(currentBin === 'exclude') { size.unshift(0); size.pop(); } @@ -368,7 +368,7 @@ function cdf(size, direction, currentbin) { size[i] += size[i + 1]; } - if(currentbin === 'exclude') { + if(currentBin === 'exclude') { size.push(0); size.shift(); } From e81fcb6dc4f469b80a4b722ebc4d3ffa13adf59d Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 14 Aug 2017 19:24:36 -0400 Subject: [PATCH 7/8] cut out duplicate setPositions calls (and loops) --- src/plot_api/plot_api.js | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index f7fb3e83880..70f991d35a7 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -237,21 +237,32 @@ Plotly.plot = function(gd, data, layout, config) { return; } - var subplots = Plots.getSubplotIds(fullLayout, 'cartesian'), - modules = fullLayout._modules; + var subplots = Plots.getSubplotIds(fullLayout, 'cartesian'); + var modules = fullLayout._modules; + var setPositionsArray = []; + var hasSetPositions = false; // position and range calculations for traces that // depend on each other ie bars (stacked or grouped) // and boxes (grouped) push each other out of the way - var subplotInfo, _module; + var subplotInfo, setPositionsFunc, i, j; - for(var i = 0; i < subplots.length; i++) { - subplotInfo = fullLayout._plots[subplots[i]]; + for(j = 0; j < modules.length; j++) { + setPositionsFunc = modules[j].setPositions; + if(setPositionsFunc && setPositionsArray.indexOf(setPositionsFunc) === -1) { + setPositionsArray.push(setPositionsFunc); + hasSetPositions = true; + } + } - for(var j = 0; j < modules.length; j++) { - _module = modules[j]; - if(_module.setPositions) _module.setPositions(gd, subplotInfo); + if(hasSetPositions) { + for(i = 0; i < subplots.length; i++) { + subplotInfo = fullLayout._plots[subplots[i]]; + + for(j = 0; j < setPositionsArray.length; j++) { + setPositionsArray[j](gd, subplotInfo); + } } } From 241d9a87926515fb709fe57fa956204a78420522 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 14 Aug 2017 21:28:39 -0400 Subject: [PATCH 8/8] pushUnique setPositions --- src/plot_api/plot_api.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 70f991d35a7..721a50afb90 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -240,23 +240,18 @@ Plotly.plot = function(gd, data, layout, config) { var subplots = Plots.getSubplotIds(fullLayout, 'cartesian'); var modules = fullLayout._modules; var setPositionsArray = []; - var hasSetPositions = false; // position and range calculations for traces that // depend on each other ie bars (stacked or grouped) // and boxes (grouped) push each other out of the way - var subplotInfo, setPositionsFunc, i, j; + var subplotInfo, i, j; for(j = 0; j < modules.length; j++) { - setPositionsFunc = modules[j].setPositions; - if(setPositionsFunc && setPositionsArray.indexOf(setPositionsFunc) === -1) { - setPositionsArray.push(setPositionsFunc); - hasSetPositions = true; - } + Lib.pushUnique(setPositionsArray, modules[j].setPositions); } - if(hasSetPositions) { + if(setPositionsArray.length) { for(i = 0; i < subplots.length; i++) { subplotInfo = fullLayout._plots[subplots[i]];