Skip to content

Commit

Permalink
Merge pull request #2028 from plotly/histogram-edge-cases
Browse files Browse the repository at this point in the history
Histogram edge cases
  • Loading branch information
alexcjohnson committed Sep 22, 2017
2 parents 60b445d + c057d25 commit 549ee38
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 40 deletions.
14 changes: 9 additions & 5 deletions src/plots/cartesian/axes.js
Expand Up @@ -569,7 +569,8 @@ axes.autoBin = function(data, ax, nbins, is2d, calendar) {
return {
start: dataMin - 0.5,
end: dataMax + 0.5,
size: 1
size: 1,
_count: dataMax - dataMin + 1
};
}

Expand Down Expand Up @@ -613,16 +614,16 @@ axes.autoBin = function(data, ax, nbins, is2d, calendar) {

axes.autoTicks(dummyAx, size0);
var binStart = axes.tickIncrement(
axes.tickFirst(dummyAx), dummyAx.dtick, 'reverse', calendar),
binEnd;
axes.tickFirst(dummyAx), dummyAx.dtick, 'reverse', calendar);
var binEnd, bincount;

// check for too many data points right at the edges of bins
// (>50% within 1% of bin edges) or all data points integral
// and offset the bins accordingly
if(typeof dummyAx.dtick === 'number') {
binStart = autoShiftNumericBins(binStart, data, dummyAx, dataMin, dataMax);

var bincount = 1 + Math.floor((dataMax - binStart) / dummyAx.dtick);
bincount = 1 + Math.floor((dataMax - binStart) / dummyAx.dtick);
binEnd = binStart + bincount * dummyAx.dtick;
}
else {
Expand All @@ -638,15 +639,18 @@ axes.autoBin = function(data, ax, nbins, is2d, calendar) {
// calculate the endpoint for nonlinear ticks - you have to
// just increment until you're done
binEnd = binStart;
bincount = 0;
while(binEnd <= dataMax) {
binEnd = axes.tickIncrement(binEnd, dummyAx.dtick, false, calendar);
bincount++;
}
}

return {
start: ax.c2r(binStart, 0, calendar),
end: ax.c2r(binEnd, 0, calendar),
size: dummyAx.dtick
size: dummyAx.dtick,
_count: bincount
};
};

Expand Down
11 changes: 9 additions & 2 deletions src/traces/bar/sieve.js
Expand Up @@ -30,19 +30,26 @@ function Sieve(traces, separateNegativeValues, dontMergeOverlappingData) {
this.separateNegativeValues = separateNegativeValues;
this.dontMergeOverlappingData = dontMergeOverlappingData;

// for single-bin histograms - see histogram/calc
var width1 = Infinity;

var positions = [];
for(var i = 0; i < traces.length; i++) {
var trace = traces[i];
for(var j = 0; j < trace.length; j++) {
var bar = trace[j];
if(bar.p !== BADNUM) positions.push(bar.p);
}
if(trace[0] && trace[0].width1) {
width1 = Math.min(trace[0].width1, width1);
}
}
this.positions = positions;

var dv = Lib.distinctVals(this.positions);
var dv = Lib.distinctVals(positions);
this.distinctPositions = dv.vals;
this.minDiff = dv.minDiff;
if(dv.vals.length === 1 && width1 !== Infinity) this.minDiff = width1;
else this.minDiff = Math.min(dv.minDiff, width1);

this.binWidth = this.minDiff;

Expand Down
116 changes: 106 additions & 10 deletions src/traces/histogram/calc.js
Expand Up @@ -135,7 +135,7 @@ module.exports = function calc(gd, trace) {
break;
}
}
for(i = seriesLen - 1; i > firstNonzero; i--) {
for(i = seriesLen - 1; i >= firstNonzero; i--) {
if(size[i]) {
lastNonzero = i;
break;
Expand All @@ -149,6 +149,12 @@ module.exports = function calc(gd, trace) {
}
}

if(cd.length === 1) {
// when we collapse to a single bin, calcdata no longer describes bin size
// so we need to explicitly specify it
cd[0].width1 = Axes.tickIncrement(cd[0].p, binSpec.size, false, calendar) - cd[0].p;
}

arraysToCalcdata(cd, trace);

return cd;
Expand All @@ -161,8 +167,9 @@ 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) {
function calcAllAutoBins(gd, trace, pa, mainData, _overlayEdgeCase) {
var binAttr = mainData + 'bins';
var isOverlay = gd._fullLayout.barmode === 'overlay';
var i, tracei, calendar, firstManual, pos0;

// all but the first trace in this group has already been marked finished
Expand All @@ -172,7 +179,9 @@ function calcAllAutoBins(gd, trace, pa, mainData) {
}
else {
// must be the first trace in the group - do the autobinning on them all
var traceGroup = getConnectedHistograms(gd, trace);

// find all grouped traces - in overlay mode each trace is independent
var traceGroup = isOverlay ? [trace] : getConnectedHistograms(gd, trace);
var autoBinnedTraces = [];

var minSize = Infinity;
Expand All @@ -196,6 +205,17 @@ function calcAllAutoBins(gd, trace, pa, mainData) {

binSpec = Axes.autoBin(pos0, pa, tracei['nbins' + mainData], false, calendar);

// Edge case: single-valued histogram overlaying others
// Use them all together to calculate the bin size for the single-valued one
if(isOverlay && binSpec._count === 1 && pa.type !== 'category') {
// Several single-valued histograms! Stop infinite recursion,
// just return an extra flag that tells handleSingleValueOverlays
// to sort out this trace too
if(_overlayEdgeCase) return [binSpec, pos0, true];

binSpec = handleSingleValueOverlays(gd, trace, pa, mainData, binAttr);
}

// adjust for CDF edge cases
if(cumulativeSpec.enabled && (cumulativeSpec.currentbin !== 'include')) {
if(cumulativeSpec.direction === 'decreasing') {
Expand All @@ -212,9 +232,9 @@ function calcAllAutoBins(gd, trace, pa, mainData) {
}
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...
// accommodating of this one, so other bins line up with these.
// But 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),
Expand Down Expand Up @@ -276,14 +296,90 @@ function calcAllAutoBins(gd, trace, pa, mainData) {
}

/*
* Return an array of traces that are all stacked or grouped together
* Only considers histograms. In principle we could include them in a
* Adjust single-value histograms in overlay mode to make as good a
* guess as we can at autobin values the user would like.
*
* Returns the binSpec for the trace that sparked all this
*/
function handleSingleValueOverlays(gd, trace, pa, mainData, binAttr) {
var overlaidTraceGroup = getConnectedHistograms(gd, trace);
var pastThisTrace = false;
var minSize = Infinity;
var singleValuedTraces = [trace];
var i, tracei;

// first collect all the:
// - min bin size from all multi-valued traces
// - single-valued traces
for(i = 0; i < overlaidTraceGroup.length; i++) {
tracei = overlaidTraceGroup[i];
if(tracei === trace) pastThisTrace = true;
else if(!pastThisTrace) {
// This trace has already had its autobins calculated
// (so must not have been single-valued).
minSize = Math.min(minSize, tracei[binAttr].size);
}
else {
var resulti = calcAllAutoBins(gd, tracei, pa, mainData, true);
var binSpeci = resulti[0];
var isSingleValued = resulti[2];

// so we can use this result when we get to tracei in the normal
// course of events, mark it as done and put _pos0 back
tracei._autoBinFinished = 1;
tracei._pos0 = resulti[1];

if(isSingleValued) {
singleValuedTraces.push(tracei);
}
else {
minSize = Math.min(minSize, binSpeci.size);
}
}
}

// find the real data values for each single-valued trace
// hunt through pos0 for the first valid value
var dataVals = new Array(singleValuedTraces.length);
for(i = 0; i < singleValuedTraces.length; i++) {
var pos0 = singleValuedTraces[i]._pos0;
for(var j = 0; j < pos0.length; j++) {
if(pos0[j] !== undefined) {
dataVals[i] = pos0[j];
break;
}
}
}

// are ALL traces are single-valued? use the min difference between
// all of their values (which defaults to 1 if there's still only one)
if(!isFinite(minSize)) {
minSize = Lib.distinctVals(dataVals).minDiff;
}

// now apply the min size we found to all single-valued traces
for(i = 0; i < singleValuedTraces.length; i++) {
tracei = singleValuedTraces[i];
var calendar = tracei[mainData + 'calendar'];

tracei._input[binAttr] = tracei[binAttr] = {
start: pa.c2r(dataVals[i] - minSize / 2, 0, calendar),
end: pa.c2r(dataVals[i] + minSize / 2, 0, calendar),
size: minSize
};
}

return trace[binAttr];
}

/*
* Return an array of histograms that share axes and orientation.
*
* Only considers histograms. In principle we could include bars 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;
Expand Down
18 changes: 12 additions & 6 deletions test/jasmine/tests/axes_test.js
Expand Up @@ -2401,7 +2401,8 @@ describe('Test axes', function() {
expect(out).toEqual({
start: -0.5,
end: 2.5,
size: 1
size: 1,
_count: 3
});
});

Expand All @@ -2414,7 +2415,8 @@ describe('Test axes', function() {
expect(out).toEqual({
start: undefined,
end: undefined,
size: 2
size: 2,
_count: NaN
});
});

Expand All @@ -2427,7 +2429,8 @@ describe('Test axes', function() {
expect(out).toEqual({
start: undefined,
end: undefined,
size: 2
size: 2,
_count: NaN
});
});

Expand All @@ -2440,7 +2443,8 @@ describe('Test axes', function() {
expect(out).toEqual({
start: undefined,
end: undefined,
size: 2
size: 2,
_count: NaN
});
});

Expand All @@ -2453,7 +2457,8 @@ describe('Test axes', function() {
expect(out).toEqual({
start: 0.5,
end: 4.5,
size: 1
size: 1,
_count: 4
});
});

Expand All @@ -2470,7 +2475,8 @@ describe('Test axes', function() {
expect(out).toEqual({
start: -0.5,
end: 5.5,
size: 2
size: 2,
_count: 3
});
});
});
Expand Down
32 changes: 16 additions & 16 deletions test/jasmine/tests/histogram2d_test.js
Expand Up @@ -169,42 +169,42 @@ describe('Test histogram2d', function() {
1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4,
1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4];
Plotly.newPlot(gd, [{type: 'histogram2d', x: x1, y: y1}]);
expect(gd._fullData[0].xbins).toEqual({start: 0.5, end: 4.5, size: 1});
expect(gd._fullData[0].ybins).toEqual({start: 0.5, end: 4.5, size: 1});
expect(gd._fullData[0].xbins).toEqual({start: 0.5, end: 4.5, size: 1, _count: 4});
expect(gd._fullData[0].ybins).toEqual({start: 0.5, end: 4.5, size: 1, _count: 4});
expect(gd._fullData[0].autobinx).toBe(true);
expect(gd._fullData[0].autobiny).toBe(true);

// same range but fewer samples increases sizes
Plotly.restyle(gd, {x: [[1, 3, 4]], y: [[1, 2, 4]]});
expect(gd._fullData[0].xbins).toEqual({start: -0.5, end: 5.5, size: 2});
expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 5.5, size: 2});
expect(gd._fullData[0].xbins).toEqual({start: -0.5, end: 5.5, size: 2, _count: 3});
expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 5.5, size: 2, _count: 3});
expect(gd._fullData[0].autobinx).toBe(true);
expect(gd._fullData[0].autobiny).toBe(true);

// larger range
Plotly.restyle(gd, {x: [[10, 30, 40]], y: [[10, 20, 40]]});
expect(gd._fullData[0].xbins).toEqual({start: -0.5, end: 59.5, size: 20});
expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 59.5, size: 20});
expect(gd._fullData[0].xbins).toEqual({start: -0.5, end: 59.5, size: 20, _count: 3});
expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 59.5, size: 20, _count: 3});
expect(gd._fullData[0].autobinx).toBe(true);
expect(gd._fullData[0].autobiny).toBe(true);

// explicit changes to bin settings
Plotly.restyle(gd, 'xbins.start', 12);
expect(gd._fullData[0].xbins).toEqual({start: 12, end: 59.5, size: 20});
expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 59.5, size: 20});
expect(gd._fullData[0].xbins).toEqual({start: 12, end: 59.5, size: 20, _count: 3});
expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 59.5, size: 20, _count: 3});
expect(gd._fullData[0].autobinx).toBe(false);
expect(gd._fullData[0].autobiny).toBe(true);

Plotly.restyle(gd, {'ybins.end': 12, 'ybins.size': 3});
expect(gd._fullData[0].xbins).toEqual({start: 12, end: 59.5, size: 20});
expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 12, size: 3});
expect(gd._fullData[0].xbins).toEqual({start: 12, end: 59.5, size: 20, _count: 3});
expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 12, size: 3, _count: 3});
expect(gd._fullData[0].autobinx).toBe(false);
expect(gd._fullData[0].autobiny).toBe(false);

// restart autobin
Plotly.restyle(gd, {autobinx: true, autobiny: true});
expect(gd._fullData[0].xbins).toEqual({start: -0.5, end: 59.5, size: 20});
expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 59.5, size: 20});
expect(gd._fullData[0].xbins).toEqual({start: -0.5, end: 59.5, size: 20, _count: 3});
expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 59.5, size: 20, _count: 3});
expect(gd._fullData[0].autobinx).toBe(true);
expect(gd._fullData[0].autobiny).toBe(true);
});
Expand All @@ -217,15 +217,15 @@ describe('Test histogram2d', function() {
1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4,
1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4];
Plotly.newPlot(gd, [{type: 'histogram2d', x: x1, y: y1, autobinx: false, autobiny: false}]);
expect(gd._fullData[0].xbins).toEqual({start: 0.5, end: 4.5, size: 1});
expect(gd._fullData[0].ybins).toEqual({start: 0.5, end: 4.5, size: 1});
expect(gd._fullData[0].xbins).toEqual({start: 0.5, end: 4.5, size: 1, _count: 4});
expect(gd._fullData[0].ybins).toEqual({start: 0.5, end: 4.5, size: 1, _count: 4});
expect(gd._fullData[0].autobinx).toBe(false);
expect(gd._fullData[0].autobiny).toBe(false);

// with autobin false this will no longer update the bins.
Plotly.restyle(gd, {x: [[1, 3, 4]], y: [[1, 2, 4]]});
expect(gd._fullData[0].xbins).toEqual({start: 0.5, end: 4.5, size: 1});
expect(gd._fullData[0].ybins).toEqual({start: 0.5, end: 4.5, size: 1});
expect(gd._fullData[0].xbins).toEqual({start: 0.5, end: 4.5, size: 1, _count: 4});
expect(gd._fullData[0].ybins).toEqual({start: 0.5, end: 4.5, size: 1, _count: 4});
expect(gd._fullData[0].autobinx).toBe(false);
expect(gd._fullData[0].autobiny).toBe(false);
});
Expand Down

0 comments on commit 549ee38

Please sign in to comment.