Skip to content

Commit 549ee38

Browse files
authored
Merge pull request #2028 from plotly/histogram-edge-cases
Histogram edge cases
2 parents 60b445d + c057d25 commit 549ee38

File tree

6 files changed

+244
-40
lines changed

6 files changed

+244
-40
lines changed

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

src/traces/bar/sieve.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,26 @@ function Sieve(traces, separateNegativeValues, dontMergeOverlappingData) {
3030
this.separateNegativeValues = separateNegativeValues;
3131
this.dontMergeOverlappingData = dontMergeOverlappingData;
3232

33+
// for single-bin histograms - see histogram/calc
34+
var width1 = Infinity;
35+
3336
var positions = [];
3437
for(var i = 0; i < traces.length; i++) {
3538
var trace = traces[i];
3639
for(var j = 0; j < trace.length; j++) {
3740
var bar = trace[j];
3841
if(bar.p !== BADNUM) positions.push(bar.p);
3942
}
43+
if(trace[0] && trace[0].width1) {
44+
width1 = Math.min(trace[0].width1, width1);
45+
}
4046
}
4147
this.positions = positions;
4248

43-
var dv = Lib.distinctVals(this.positions);
49+
var dv = Lib.distinctVals(positions);
4450
this.distinctPositions = dv.vals;
45-
this.minDiff = dv.minDiff;
51+
if(dv.vals.length === 1 && width1 !== Infinity) this.minDiff = width1;
52+
else this.minDiff = Math.min(dv.minDiff, width1);
4653

4754
this.binWidth = this.minDiff;
4855

src/traces/histogram/calc.js

+106-10
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ module.exports = function calc(gd, trace) {
135135
break;
136136
}
137137
}
138-
for(i = seriesLen - 1; i > firstNonzero; i--) {
138+
for(i = seriesLen - 1; i >= firstNonzero; i--) {
139139
if(size[i]) {
140140
lastNonzero = i;
141141
break;
@@ -149,6 +149,12 @@ module.exports = function calc(gd, trace) {
149149
}
150150
}
151151

152+
if(cd.length === 1) {
153+
// when we collapse to a single bin, calcdata no longer describes bin size
154+
// so we need to explicitly specify it
155+
cd[0].width1 = Axes.tickIncrement(cd[0].p, binSpec.size, false, calendar) - cd[0].p;
156+
}
157+
152158
arraysToCalcdata(cd, trace);
153159

154160
return cd;
@@ -161,8 +167,9 @@ module.exports = function calc(gd, trace) {
161167
* smallest bins of any of the auto values for all histograms grouped/stacked
162168
* together.
163169
*/
164-
function calcAllAutoBins(gd, trace, pa, mainData) {
170+
function calcAllAutoBins(gd, trace, pa, mainData, _overlayEdgeCase) {
165171
var binAttr = mainData + 'bins';
172+
var isOverlay = gd._fullLayout.barmode === 'overlay';
166173
var i, tracei, calendar, firstManual, pos0;
167174

168175
// all but the first trace in this group has already been marked finished
@@ -172,7 +179,9 @@ function calcAllAutoBins(gd, trace, pa, mainData) {
172179
}
173180
else {
174181
// must be the first trace in the group - do the autobinning on them all
175-
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);
176185
var autoBinnedTraces = [];
177186

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

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

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+
// Several single-valued histograms! Stop infinite recursion,
212+
// just return an extra flag that tells handleSingleValueOverlays
213+
// to sort out this trace too
214+
if(_overlayEdgeCase) return [binSpec, pos0, true];
215+
216+
binSpec = handleSingleValueOverlays(gd, trace, pa, mainData, binAttr);
217+
}
218+
199219
// adjust for CDF edge cases
200220
if(cumulativeSpec.enabled && (cumulativeSpec.currentbin !== 'include')) {
201221
if(cumulativeSpec.direction === 'decreasing') {
@@ -212,9 +232,9 @@ function calcAllAutoBins(gd, trace, pa, mainData) {
212232
}
213233
else if(!firstManual) {
214234
// Remember the first manually set binSpec. We'll try to be extra
215-
// accommodating of this one, so other bins line up with these
216-
// if there's more than one manual bin set and they're mutually inconsistent,
217-
// then there's not much we can do...
235+
// accommodating of this one, so other bins line up with these.
236+
// But if there's more than one manual bin set and they're mutually
237+
// inconsistent, then there's not much we can do...
218238
firstManual = {
219239
size: binSpec.size,
220240
start: pa.r2c(binSpec.start, 0, calendar),
@@ -276,14 +296,90 @@ function calcAllAutoBins(gd, trace, pa, mainData) {
276296
}
277297

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

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
});

test/jasmine/tests/histogram2d_test.js

+16-16
Original file line numberDiff line numberDiff line change
@@ -169,42 +169,42 @@ describe('Test histogram2d', function() {
169169
1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4,
170170
1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4];
171171
Plotly.newPlot(gd, [{type: 'histogram2d', x: x1, y: y1}]);
172-
expect(gd._fullData[0].xbins).toEqual({start: 0.5, end: 4.5, size: 1});
173-
expect(gd._fullData[0].ybins).toEqual({start: 0.5, end: 4.5, size: 1});
172+
expect(gd._fullData[0].xbins).toEqual({start: 0.5, end: 4.5, size: 1, _count: 4});
173+
expect(gd._fullData[0].ybins).toEqual({start: 0.5, end: 4.5, size: 1, _count: 4});
174174
expect(gd._fullData[0].autobinx).toBe(true);
175175
expect(gd._fullData[0].autobiny).toBe(true);
176176

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

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

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

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

204204
// restart autobin
205205
Plotly.restyle(gd, {autobinx: true, autobiny: true});
206-
expect(gd._fullData[0].xbins).toEqual({start: -0.5, end: 59.5, size: 20});
207-
expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 59.5, size: 20});
206+
expect(gd._fullData[0].xbins).toEqual({start: -0.5, end: 59.5, size: 20, _count: 3});
207+
expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 59.5, size: 20, _count: 3});
208208
expect(gd._fullData[0].autobinx).toBe(true);
209209
expect(gd._fullData[0].autobiny).toBe(true);
210210
});
@@ -217,15 +217,15 @@ describe('Test histogram2d', function() {
217217
1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4,
218218
1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4];
219219
Plotly.newPlot(gd, [{type: 'histogram2d', x: x1, y: y1, autobinx: false, autobiny: false}]);
220-
expect(gd._fullData[0].xbins).toEqual({start: 0.5, end: 4.5, size: 1});
221-
expect(gd._fullData[0].ybins).toEqual({start: 0.5, end: 4.5, size: 1});
220+
expect(gd._fullData[0].xbins).toEqual({start: 0.5, end: 4.5, size: 1, _count: 4});
221+
expect(gd._fullData[0].ybins).toEqual({start: 0.5, end: 4.5, size: 1, _count: 4});
222222
expect(gd._fullData[0].autobinx).toBe(false);
223223
expect(gd._fullData[0].autobiny).toBe(false);
224224

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

0 commit comments

Comments
 (0)