From bc7926ae8ed38f2832076b5e0c3e5ec057271562 Mon Sep 17 00:00:00 2001 From: Osamaali313 Date: Wed, 1 Jul 2026 20:22:13 +0300 Subject: [PATCH] Fix histogram bin redistribution dropping counts at the range boundary buildNormalizedHistograms redistributes input bins into binCount equal-width result bins. The last result bin's right edge was computed as resultLeft + dx == left + (binCount-1)*dx + dx, which accumulates floating-point error and can land just below range.right (e.g. for range [0, 1] with binCount 6 it is 0.9999999999999999). An input bin whose right edge sits exactly on range.right then satisfies 'bin.x + bin.dx > resultRight' in the inner loop and the loop breaks without consuming it, so its y counts are silently dropped from the normalized histogram. Clamp the last result bin's right edge to range.right (its true upper bound by construction). Add regression tests asserting count conservation when a bin's right edge sits on range.right. --- .../widgets/histogram/histogram_util.ts | 8 ++++- .../widgets/histogram/histogram_util_test.ts | 32 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/tensorboard/webapp/widgets/histogram/histogram_util.ts b/tensorboard/webapp/widgets/histogram/histogram_util.ts index fafaf61f9a7..6f5cf79d09e 100644 --- a/tensorboard/webapp/widgets/histogram/histogram_util.ts +++ b/tensorboard/webapp/widgets/histogram/histogram_util.ts @@ -119,8 +119,14 @@ function rebuildBins(bins: Bin[], range: Range, binCount: number): Bin[] { let nextBinContribution = 0; for (let i = 0; i < binCount; i++) { const resultLeft = left + i * dx; - const resultRight = resultLeft + dx; const isLastResultBin = i === binCount - 1; + // The last result bin must extend exactly to `range.right`. Computing it as + // `resultLeft + dx` accumulates floating-point error in the repeated `i * dx` + // sum and can land just below `range.right`; an input bin whose right edge + // sits exactly on `range.right` then satisfies `bin.x + bin.dx > resultRight` + // and is skipped without being consumed, dropping its counts. Clamp the last + // result bin's right edge to `range.right` so those counts are preserved. + const resultRight = isLastResultBin ? right : resultLeft + dx; let resultY = nextBinContribution; nextBinContribution = 0; diff --git a/tensorboard/webapp/widgets/histogram/histogram_util_test.ts b/tensorboard/webapp/widgets/histogram/histogram_util_test.ts index 8aee2f66dfb..7a934ed718f 100644 --- a/tensorboard/webapp/widgets/histogram/histogram_util_test.ts +++ b/tensorboard/webapp/widgets/histogram/histogram_util_test.ts @@ -54,6 +54,38 @@ describe('histogram util', () => { }); }); + describe('count conservation at the range boundary', () => { + function totalCount(bins: Bin[]): number { + return bins.reduce((sum, bin) => sum + bin.y, 0); + } + + it('preserves a bin whose right edge sits on range.right', () => { + // A zero-width bin at x=1 sits exactly on the range's right edge (the + // widest histogram spans [0, 1]). It must not be dropped when the bins + // are redistributed into `binCount` result bins. + const input: Bin[] = [ + {x: 0, dx: 1, y: 1}, + {x: 1, dx: 0, y: 5}, + ]; + const [result] = histogramsToBins( + buildNormalizedHistograms([binsToHistogram(input)], 6) + ); + expect(totalCount(result)).toBeCloseTo(totalCount(input), 6); + }); + + it('preserves counts when the final normal bin ends on range.right', () => { + const input: Bin[] = [ + {x: 0, dx: 1, y: 2}, + {x: 1, dx: 1, y: 3}, + {x: 2, dx: 1, y: 4}, + ]; + const [result] = histogramsToBins( + buildNormalizedHistograms([binsToHistogram(input)], 7) + ); + expect(totalCount(result)).toBeCloseTo(totalCount(input), 6); + }); + }); + describe('single histogram', () => { it('converts a 0 width bin into a default bin', () => { expect(