Skip to content

Commit 968f742

Browse files
committed
fix part 2 of #1978 - autobin size for single-value overlaid histograms
1 parent a9498bb commit 968f742

File tree

4 files changed

+190
-21
lines changed

4 files changed

+190
-21
lines changed

Diff for: src/plots/cartesian/axes.js

+9-5
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,8 @@ axes.autoBin = function(data, ax, nbins, is2d, calendar) {
569569
return {
570570
start: dataMin - 0.5,
571571
end: dataMax + 0.5,
572-
size: 1
572+
size: 1,
573+
_count: dataMax - dataMin + 1
573574
};
574575
}
575576

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

614615
axes.autoTicks(dummyAx, size0);
615616
var binStart = axes.tickIncrement(
616-
axes.tickFirst(dummyAx), dummyAx.dtick, 'reverse', calendar),
617-
binEnd;
617+
axes.tickFirst(dummyAx), dummyAx.dtick, 'reverse', calendar);
618+
var binEnd, bincount;
618619

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

625-
var bincount = 1 + Math.floor((dataMax - binStart) / dummyAx.dtick);
626+
bincount = 1 + Math.floor((dataMax - binStart) / dummyAx.dtick);
626627
binEnd = binStart + bincount * dummyAx.dtick;
627628
}
628629
else {
@@ -638,15 +639,18 @@ axes.autoBin = function(data, ax, nbins, is2d, calendar) {
638639
// calculate the endpoint for nonlinear ticks - you have to
639640
// just increment until you're done
640641
binEnd = binStart;
642+
bincount = 0;
641643
while(binEnd <= dataMax) {
642644
binEnd = axes.tickIncrement(binEnd, dummyAx.dtick, false, calendar);
645+
bincount++;
643646
}
644647
}
645648

646649
return {
647650
start: ax.c2r(binStart, 0, calendar),
648651
end: ax.c2r(binEnd, 0, calendar),
649-
size: dummyAx.dtick
652+
size: dummyAx.dtick,
653+
_count: bincount
650654
};
651655
};
652656

Diff for: src/traces/histogram/calc.js

+101-9
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,9 @@ module.exports = function calc(gd, trace) {
167167
* smallest bins of any of the auto values for all histograms grouped/stacked
168168
* together.
169169
*/
170-
function calcAllAutoBins(gd, trace, pa, mainData) {
170+
function calcAllAutoBins(gd, trace, pa, mainData, _overlayEdgeCase) {
171171
var binAttr = mainData + 'bins';
172+
var isOverlay = gd._fullLayout.barmode === 'overlay';
172173
var i, tracei, calendar, firstManual, pos0;
173174

174175
// all but the first trace in this group has already been marked finished
@@ -178,7 +179,9 @@ function calcAllAutoBins(gd, trace, pa, mainData) {
178179
}
179180
else {
180181
// must be the first trace in the group - do the autobinning on them all
181-
var traceGroup = getConnectedHistograms(gd, trace);
182+
183+
// find all grouped traces - in overlay mode each trace is independent
184+
var traceGroup = isOverlay ? [trace] : getConnectedHistograms(gd, trace);
182185
var autoBinnedTraces = [];
183186

184187
var minSize = Infinity;
@@ -202,6 +205,19 @@ function calcAllAutoBins(gd, trace, pa, mainData) {
202205

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

208+
// Edge case: single-valued histogram overlaying others
209+
// Use them all together to calculate the bin size for the single-valued one
210+
if(isOverlay && binSpec._count === 1 && pa.type !== 'category') {
211+
// trace[binAttr] = binSpec;
212+
213+
// Several single-valued histograms! Stop infinite recursion,
214+
// just return an extra flag that tells handleSingleValueOverlays
215+
// to sort out this trace too
216+
if(_overlayEdgeCase) return [binSpec, pos0, true];
217+
218+
binSpec = handleSingleValueOverlays(gd, trace, pa, mainData, binAttr);
219+
}
220+
205221
// adjust for CDF edge cases
206222
if(cumulativeSpec.enabled && (cumulativeSpec.currentbin !== 'include')) {
207223
if(cumulativeSpec.direction === 'decreasing') {
@@ -218,9 +234,9 @@ function calcAllAutoBins(gd, trace, pa, mainData) {
218234
}
219235
else if(!firstManual) {
220236
// Remember the first manually set binSpec. We'll try to be extra
221-
// accommodating of this one, so other bins line up with these
222-
// if there's more than one manual bin set and they're mutually inconsistent,
223-
// then there's not much we can do...
237+
// accommodating of this one, so other bins line up with these.
238+
// But if there's more than one manual bin set and they're mutually
239+
// inconsistent, then there's not much we can do...
224240
firstManual = {
225241
size: binSpec.size,
226242
start: pa.r2c(binSpec.start, 0, calendar),
@@ -282,14 +298,90 @@ function calcAllAutoBins(gd, trace, pa, mainData) {
282298
}
283299

284300
/*
285-
* Return an array of traces that are all stacked or grouped together
286-
* Only considers histograms. In principle we could include them in a
301+
* Adjust single-value histograms in overlay mode to make as good a
302+
* guess as we can at autobin values the user would like.
303+
*
304+
* Returns the binSpec for the trace that sparked all this
305+
*/
306+
function handleSingleValueOverlays(gd, trace, pa, mainData, binAttr) {
307+
var overlaidTraceGroup = getConnectedHistograms(gd, trace);
308+
var pastThisTrace = false;
309+
var minSize = Infinity;
310+
var singleValuedTraces = [trace];
311+
var i, tracei;
312+
313+
// first collect all the:
314+
// - min bin size from all multi-valued traces
315+
// - single-valued traces
316+
for(i = 0; i < overlaidTraceGroup.length; i++) {
317+
tracei = overlaidTraceGroup[i];
318+
if(tracei === trace) pastThisTrace = true;
319+
else if(!pastThisTrace) {
320+
// This trace has already had its autobins calculated
321+
// (so must not have been single-valued).
322+
minSize = Math.min(minSize, tracei[binAttr].size);
323+
}
324+
else {
325+
var resulti = calcAllAutoBins(gd, tracei, pa, mainData, true);
326+
var binSpeci = resulti[0];
327+
var isSingleValued = resulti[2];
328+
329+
// so we can use this result when we get to tracei in the normal
330+
// course of events, mark it as done and put _pos0 back
331+
tracei._autoBinFinished = 1;
332+
tracei._pos0 = resulti[1];
333+
334+
if(isSingleValued) {
335+
singleValuedTraces.push(tracei);
336+
}
337+
else {
338+
minSize = Math.min(minSize, binSpeci.size);
339+
}
340+
}
341+
}
342+
343+
// find the real data values for each single-valued trace
344+
// hunt through pos0 for the first valid value
345+
var dataVals = new Array(singleValuedTraces.length);
346+
for(i = 0; i < singleValuedTraces.length; i++) {
347+
var pos0 = singleValuedTraces[i]._pos0;
348+
for(var j = 0; j < pos0.length; j++) {
349+
if(pos0[j] !== undefined) {
350+
dataVals[i] = pos0[j];
351+
break;
352+
}
353+
}
354+
}
355+
356+
// are ALL traces are single-valued? use the min difference between
357+
// all of their values (which defaults to 1 if there's still only one)
358+
if(!isFinite(minSize)) {
359+
minSize = Lib.distinctVals(dataVals).minDiff;
360+
}
361+
362+
// now apply the min size we found to all single-valued traces
363+
for(i = 0; i < singleValuedTraces.length; i++) {
364+
tracei = singleValuedTraces[i];
365+
var calendar = tracei[mainData + 'calendar'];
366+
367+
tracei._input[binAttr] = tracei[binAttr] = {
368+
start: pa.c2r(dataVals[i] - minSize / 2, 0, calendar),
369+
end: pa.c2r(dataVals[i] + minSize / 2, 0, calendar),
370+
size: minSize
371+
};
372+
}
373+
374+
return trace[binAttr];
375+
}
376+
377+
/*
378+
* Return an array of histograms that share axes and orientation.
379+
*
380+
* Only considers histograms. In principle we could include bars in a
287381
* similar way to how we do manually binned histograms, though this
288382
* would have tons of edge cases and value judgments to make.
289383
*/
290384
function getConnectedHistograms(gd, trace) {
291-
if(gd._fullLayout.barmode === 'overlay') return [trace];
292-
293385
var xid = trace.xaxis;
294386
var yid = trace.yaxis;
295387
var orientation = trace.orientation;

Diff for: test/jasmine/tests/axes_test.js

+12-6
Original file line numberDiff line numberDiff line change
@@ -2401,7 +2401,8 @@ describe('Test axes', function() {
24012401
expect(out).toEqual({
24022402
start: -0.5,
24032403
end: 2.5,
2404-
size: 1
2404+
size: 1,
2405+
_count: 3
24052406
});
24062407
});
24072408

@@ -2414,7 +2415,8 @@ describe('Test axes', function() {
24142415
expect(out).toEqual({
24152416
start: undefined,
24162417
end: undefined,
2417-
size: 2
2418+
size: 2,
2419+
_count: NaN
24182420
});
24192421
});
24202422

@@ -2427,7 +2429,8 @@ describe('Test axes', function() {
24272429
expect(out).toEqual({
24282430
start: undefined,
24292431
end: undefined,
2430-
size: 2
2432+
size: 2,
2433+
_count: NaN
24312434
});
24322435
});
24332436

@@ -2440,7 +2443,8 @@ describe('Test axes', function() {
24402443
expect(out).toEqual({
24412444
start: undefined,
24422445
end: undefined,
2443-
size: 2
2446+
size: 2,
2447+
_count: NaN
24442448
});
24452449
});
24462450

@@ -2453,7 +2457,8 @@ describe('Test axes', function() {
24532457
expect(out).toEqual({
24542458
start: 0.5,
24552459
end: 4.5,
2456-
size: 1
2460+
size: 1,
2461+
_count: 4
24572462
});
24582463
});
24592464

@@ -2470,7 +2475,8 @@ describe('Test axes', function() {
24702475
expect(out).toEqual({
24712476
start: -0.5,
24722477
end: 5.5,
2473-
size: 2
2478+
size: 2,
2479+
_count: 3
24742480
});
24752481
});
24762482
});

Diff for: test/jasmine/tests/histogram_test.js

+68-1
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,13 @@ describe('Test histogram', function() {
162162

163163

164164
describe('calc', function() {
165-
function _calc(opts, extraTraces) {
165+
function _calc(opts, extraTraces, layout) {
166166
var base = { type: 'histogram' };
167167
var trace = Lib.extendFlat({}, base, opts);
168168
var gd = { data: [trace] };
169169

170+
if(layout) gd.layout = layout;
171+
170172
if(Array.isArray(extraTraces)) {
171173
extraTraces.forEach(function(extraTrace) {
172174
gd.data.push(Lib.extendFlat({}, base, extraTrace));
@@ -280,6 +282,71 @@ describe('Test histogram', function() {
280282
]);
281283
});
282284

285+
it('handles single-value overlaid autobinned data with other manual bins', function() {
286+
var out = _calc({x: [1.1, 1.1, 1.1]}, [
287+
{x: [1, 2, 3, 4], xbins: {start: 0.5, end: 4.5, size: 2}},
288+
{x: [10, 10.5, 11, 11.5], xbins: {start: 9.8, end: 11.8, size: 0.5}}
289+
], {
290+
barmode: 'overlay'
291+
});
292+
293+
expect(out).toEqual([
294+
{b: 0, p: 1.1, s: 3, width1: 0.5}
295+
]);
296+
});
297+
298+
it('handles single-value overlaid autobinned data with other auto bins', function() {
299+
var out = _calc({x: ['', null, 17, '', 17]}, [
300+
{x: [10, 20, 30, 40]},
301+
{x: [100, 101, 102, 103]}
302+
], {
303+
barmode: 'overlay'
304+
});
305+
306+
expect(out).toEqual([
307+
{b: 0, p: 17, s: 2, width1: 2}
308+
]);
309+
});
310+
311+
it('handles multiple single-valued overlaid autobinned traces with different values', function() {
312+
var out = _calc({x: [null, 13, '', 13]}, [
313+
{x: [5]},
314+
{x: [null, 29, 29, 29, null]}
315+
], {
316+
barmode: 'overlay'
317+
});
318+
319+
expect(out).toEqual([
320+
{b: 0, p: 13, s: 2, width1: 8}
321+
]);
322+
});
323+
324+
it('handles multiple single-date overlaid autobinned traces with different values', function() {
325+
var out = _calc({x: [null, '2011-02-03', '', '2011-02-03']}, [
326+
{x: ['2011-02-05']},
327+
{x: [null, '2015-05-05', '2015-05-05', '2015-05-05', null]}
328+
], {
329+
barmode: 'overlay'
330+
});
331+
332+
expect(out).toEqual([
333+
{b: 0, p: 1296691200000, s: 2, width1: 2 * 24 * 3600 * 1000}
334+
]);
335+
});
336+
337+
it('handles several overlaid autobinned traces with only one value total', function() {
338+
var out = _calc({x: [null, 97, '', 97]}, [
339+
{x: [97]},
340+
{x: [null, 97, 97, 97, null]}
341+
], {
342+
barmode: 'overlay'
343+
});
344+
345+
expect(out).toEqual([
346+
{b: 0, p: 97, s: 2, width1: 1}
347+
]);
348+
});
349+
283350
function calcPositions(opts, extraTraces) {
284351
return _calc(opts, extraTraces).map(function(v) { return v.p; });
285352
}

0 commit comments

Comments
 (0)