Skip to content

Commit

Permalink
fix part 2 of #1978 - autobin size for single-value overlaid histograms
Browse files Browse the repository at this point in the history
  • Loading branch information
alexcjohnson committed Sep 21, 2017
1 parent a9498bb commit 968f742
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 21 deletions.
14 changes: 9 additions & 5 deletions src/plots/cartesian/axes.js
Original file line number Diff line number Diff line change
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
110 changes: 101 additions & 9 deletions src/traces/histogram/calc.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,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 @@ -178,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 @@ -202,6 +205,19 @@ 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') {
// trace[binAttr] = binSpec;

// 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 @@ -218,9 +234,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 @@ -282,14 +298,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
Original file line number Diff line number Diff line change
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
69 changes: 68 additions & 1 deletion test/jasmine/tests/histogram_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,13 @@ describe('Test histogram', function() {


describe('calc', function() {
function _calc(opts, extraTraces) {
function _calc(opts, extraTraces, layout) {
var base = { type: 'histogram' };
var trace = Lib.extendFlat({}, base, opts);
var gd = { data: [trace] };

if(layout) gd.layout = layout;

if(Array.isArray(extraTraces)) {
extraTraces.forEach(function(extraTrace) {
gd.data.push(Lib.extendFlat({}, base, extraTrace));
Expand Down Expand Up @@ -280,6 +282,71 @@ describe('Test histogram', function() {
]);
});

it('handles single-value overlaid autobinned data with other manual bins', function() {
var out = _calc({x: [1.1, 1.1, 1.1]}, [
{x: [1, 2, 3, 4], xbins: {start: 0.5, end: 4.5, size: 2}},
{x: [10, 10.5, 11, 11.5], xbins: {start: 9.8, end: 11.8, size: 0.5}}
], {
barmode: 'overlay'
});

expect(out).toEqual([
{b: 0, p: 1.1, s: 3, width1: 0.5}
]);
});

it('handles single-value overlaid autobinned data with other auto bins', function() {
var out = _calc({x: ['', null, 17, '', 17]}, [
{x: [10, 20, 30, 40]},
{x: [100, 101, 102, 103]}
], {
barmode: 'overlay'
});

expect(out).toEqual([
{b: 0, p: 17, s: 2, width1: 2}
]);
});

it('handles multiple single-valued overlaid autobinned traces with different values', function() {
var out = _calc({x: [null, 13, '', 13]}, [
{x: [5]},
{x: [null, 29, 29, 29, null]}
], {
barmode: 'overlay'
});

expect(out).toEqual([
{b: 0, p: 13, s: 2, width1: 8}
]);
});

it('handles multiple single-date overlaid autobinned traces with different values', function() {
var out = _calc({x: [null, '2011-02-03', '', '2011-02-03']}, [
{x: ['2011-02-05']},
{x: [null, '2015-05-05', '2015-05-05', '2015-05-05', null]}
], {
barmode: 'overlay'
});

expect(out).toEqual([
{b: 0, p: 1296691200000, s: 2, width1: 2 * 24 * 3600 * 1000}
]);
});

it('handles several overlaid autobinned traces with only one value total', function() {
var out = _calc({x: [null, 97, '', 97]}, [
{x: [97]},
{x: [null, 97, 97, 97, null]}
], {
barmode: 'overlay'
});

expect(out).toEqual([
{b: 0, p: 97, s: 2, width1: 1}
]);
});

function calcPositions(opts, extraTraces) {
return _calc(opts, extraTraces).map(function(v) { return v.p; });
}
Expand Down

0 comments on commit 968f742

Please sign in to comment.