From 790f136fdad2aa5158fdfb19297071838830e655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Sat, 2 Apr 2022 01:30:08 +0200 Subject: [PATCH] make bins faster by quantization (#220) * make bins faster by quantization ref. https://github.com/observablehq/plot/issues/454 * better fractional step * fix quantization for custom domains Co-authored-by: Mike Bostock --- src/bin.js | 29 +++++++++++++++++++++++++---- test/bin-test.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/bin.js b/src/bin.js index 2bd96850..709f7c18 100644 --- a/src/bin.js +++ b/src/bin.js @@ -18,6 +18,7 @@ export default function bin() { var i, n = data.length, x, + step, values = new Array(n); for (i = 0; i < n; ++i) { @@ -36,6 +37,11 @@ export default function bin() { if (domain === extent) [x0, x1] = nice(x0, x1, tn); tz = ticks(x0, x1, tn); + // If the domain is aligned with the first tick (which it will by + // default), then we can use quantization rather than bisection to bin + // values, which is substantially faster. + if (tz[0] <= x0) step = tickIncrement(x0, x1, tn); + // If the last threshold is coincident with the domain’s upper bound, the // last bin will be zero-width. If the default domain is used, and this // last threshold is coincident with the maximum input value, we can @@ -75,10 +81,25 @@ export default function bin() { } // Assign data to bins by value, ignoring any outside the domain. - for (i = 0; i < n; ++i) { - x = values[i]; - if (x != null && x0 <= x && x <= x1) { - bins[bisect(tz, x, 0, m)].push(data[i]); + if (isFinite(step)) { + if (step > 0) { + for (i = 0; i < n; ++i) { + if ((x = values[i]) != null && x0 <= x && x <= x1) { + bins[Math.floor((x - x0) / step)].push(data[i]); + } + } + } else if (step < 0) { + for (i = 0; i < n; ++i) { + if ((x = values[i]) != null && x0 <= x && x <= x1) { + bins[Math.floor((x0 - x) * step)].push(data[i]); + } + } + } + } else { + for (i = 0; i < n; ++i) { + if ((x = values[i]) != null && x0 <= x && x <= x1) { + bins[bisect(tz, x, 0, m)].push(data[i]); + } } } diff --git a/test/bin-test.js b/test/bin-test.js index 42e41c75..b4c33d01 100644 --- a/test/bin-test.js +++ b/test/bin-test.js @@ -164,6 +164,48 @@ it("bin()() returns bins whose rightmost bin is not too wide", () => { ]); }); +it("bin(data) handles fractional step correctly", () => { + const h = bin().thresholds(10); + assert.deepStrictEqual(h([9.8, 10, 11, 12, 13, 13.2]), [ + box([9.8], 9.5, 10), + box([10], 10, 10.5), + box([], 10.5, 11), + box([11], 11, 11.5), + box([], 11.5, 12), + box([12], 12, 12.5), + box([], 12.5, 13), + box([13, 13.2], 13, 13.5) + ]); +}); + +it("bin(data) handles fractional step correctly with a custom, non-aligned domain", () => { + const h = bin().thresholds(10).domain([9.7, 13.3]); + assert.deepStrictEqual(h([9.8, 10, 11, 12, 13, 13.2]), [ + box([9.8], 9.7, 10), + box([10], 10, 10.5), + box([], 10.5, 11), + box([11], 11, 11.5), + box([], 11.5, 12), + box([12], 12, 12.5), + box([], 12.5, 13), + box([13, 13.2], 13, 13.3) + ]); +}); + +it("bin(data) handles fractional step correctly with a custom, aligned domain", () => { + const h = bin().thresholds(10).domain([9.5, 13.5]); + assert.deepStrictEqual(h([9.8, 10, 11, 12, 13, 13.2]), [ + box([9.8], 9.5, 10), + box([10], 10, 10.5), + box([], 10.5, 11), + box([11], 11, 11.5), + box([], 11.5, 12), + box([12], 12, 12.5), + box([], 12.5, 13), + box([13, 13.2], 13, 13.5) + ]); +}); + it("bin(data) coerces values to numbers as expected", () => { const h = bin().thresholds(10); assert.deepStrictEqual(h(["1", "2", "3", "4", "5"]), [