From 72f3fe5c6e8d3fb2f57564b2b945cfa47313d42a Mon Sep 17 00:00:00 2001 From: SkinnyFatBoy05 Date: Fri, 15 May 2026 14:22:26 +1000 Subject: [PATCH] Fix global object limit in interactive graphics --- .../InteractiveGraphics.tsx | 106 ++++++++++++------ .../applyObjectLimitToBuckets.ts | 53 +++++++++ tests/applyObjectLimitToBuckets.test.ts | 35 ++++++ 3 files changed, 157 insertions(+), 37 deletions(-) create mode 100644 site/components/InteractiveGraphics/applyObjectLimitToBuckets.ts create mode 100644 tests/applyObjectLimitToBuckets.test.ts diff --git a/site/components/InteractiveGraphics/InteractiveGraphics.tsx b/site/components/InteractiveGraphics/InteractiveGraphics.tsx index de014e1..8062023 100644 --- a/site/components/InteractiveGraphics/InteractiveGraphics.tsx +++ b/site/components/InteractiveGraphics/InteractiveGraphics.tsx @@ -38,6 +38,7 @@ import { useFilterTexts, useIsPointOnScreen, } from "./hooks" +import { applyObjectLimitToBuckets } from "./applyObjectLimitToBuckets" import { tooltipLayerZIndex } from "./tooltipLayer" export type GraphicsObjectClickEvent = { @@ -421,65 +422,96 @@ export const InteractiveGraphics = ({ filterLayerAndStep, }) - const filterAndLimit = ( + const filterAndIndex = ( objects: T[] | undefined, filterFn: (obj: T) => boolean, ): (T & { originalIndex: number })[] => { if (!objects) return [] - const filtered = objects + return objects .map((obj, index) => ({ ...obj, originalIndex: index })) .filter(filterFn) - return objectLimit ? filtered.slice(-objectLimit) : filtered } - const filteredLines = useMemo( + const allFilteredLines = useMemo( () => - filterAndLimit(graphics.lines, filterLines).sort( + filterAndIndex(graphics.lines, filterLines).sort( (a, b) => (a.zIndex ?? 0) - (b.zIndex ?? 0) || a.originalIndex - b.originalIndex, ), - [graphics.lines, filterLines, objectLimit], + [graphics.lines, filterLines], ) - const filteredInfiniteLines = useMemo( - () => filterAndLimit(graphics.infiniteLines, filterLayerAndStep), - [graphics.infiniteLines, filterLayerAndStep, objectLimit], + const allFilteredInfiniteLines = useMemo( + () => filterAndIndex(graphics.infiniteLines, filterLayerAndStep), + [graphics.infiniteLines, filterLayerAndStep], ) - const filteredRects = useMemo( - () => sortRectsByArea(filterAndLimit(graphics.rects, filterRects)), - [graphics.rects, filterRects, objectLimit], + const allFilteredRects = useMemo( + () => sortRectsByArea(filterAndIndex(graphics.rects, filterRects)), + [graphics.rects, filterRects], ) - const filteredPolygons = useMemo( - () => filterAndLimit(graphics.polygons, filterPolygons), - [graphics.polygons, filterPolygons, objectLimit], + const allFilteredPolygons = useMemo( + () => filterAndIndex(graphics.polygons, filterPolygons), + [graphics.polygons, filterPolygons], ) - const filteredPoints = useMemo( - () => filterAndLimit(graphics.points, filterPoints), - [graphics.points, filterPoints, objectLimit], + const allFilteredPoints = useMemo( + () => filterAndIndex(graphics.points, filterPoints), + [graphics.points, filterPoints], ) - const filteredCircles = useMemo( - () => filterAndLimit(graphics.circles, filterCircles), - [graphics.circles, filterCircles, objectLimit], + const allFilteredCircles = useMemo( + () => filterAndIndex(graphics.circles, filterCircles), + [graphics.circles, filterCircles], ) - const filteredTexts = useMemo( - () => filterAndLimit(graphics.texts, filterTexts), - [graphics.texts, filterTexts, objectLimit], + const allFilteredTexts = useMemo( + () => filterAndIndex(graphics.texts, filterTexts), + [graphics.texts, filterTexts], ) - const filteredArrows = useMemo( - () => filterAndLimit(graphics.arrows, filterArrows), - [graphics.arrows, filterArrows, objectLimit], + const allFilteredArrows = useMemo( + () => filterAndIndex(graphics.arrows, filterArrows), + [graphics.arrows, filterArrows], ) - const totalFilteredObjects = - filteredInfiniteLines.length + - filteredLines.length + - filteredRects.length + - filteredPolygons.length + - filteredPoints.length + - filteredCircles.length + - filteredTexts.length + - filteredArrows.length - const isLimitReached = objectLimit && totalFilteredObjects > objectLimit + const { + buckets: filteredObjects, + totalFilteredObjects, + isLimitReached, + } = useMemo( + () => + applyObjectLimitToBuckets( + { + arrows: allFilteredArrows, + infiniteLines: allFilteredInfiniteLines, + lines: allFilteredLines, + rects: allFilteredRects, + polygons: allFilteredPolygons, + circles: allFilteredCircles, + texts: allFilteredTexts, + points: allFilteredPoints, + }, + objectLimit, + ), + [ + allFilteredArrows, + allFilteredInfiniteLines, + allFilteredLines, + allFilteredRects, + allFilteredPolygons, + allFilteredCircles, + allFilteredTexts, + allFilteredPoints, + objectLimit, + ], + ) + + const { + arrows: filteredArrows, + infiniteLines: filteredInfiniteLines, + lines: filteredLines, + rects: filteredRects, + polygons: filteredPolygons, + circles: filteredCircles, + texts: filteredTexts, + points: filteredPoints, + } = filteredObjects return (
diff --git a/site/components/InteractiveGraphics/applyObjectLimitToBuckets.ts b/site/components/InteractiveGraphics/applyObjectLimitToBuckets.ts new file mode 100644 index 0000000..2331de1 --- /dev/null +++ b/site/components/InteractiveGraphics/applyObjectLimitToBuckets.ts @@ -0,0 +1,53 @@ +export function applyObjectLimitToBuckets< + T extends Record, +>( + buckets: T, + objectLimit?: number, +): { + buckets: { [K in keyof T]: Array } + totalFilteredObjects: number + isLimitReached: boolean +} { + const entries = Object.entries(buckets) as Array<[keyof T, readonly unknown[]]> + const totalFilteredObjects = entries.reduce( + (total, [, objects]) => total + objects.length, + 0, + ) + const copyBuckets = () => { + const copiedBuckets = {} as { [K in keyof T]: Array } + for (const [key, objects] of entries) { + copiedBuckets[key] = [...objects] as Array + } + return copiedBuckets + } + + const limit = + typeof objectLimit === "number" && Number.isFinite(objectLimit) + ? Math.max(0, Math.floor(objectLimit)) + : undefined + + if (limit === undefined || totalFilteredObjects <= limit) { + return { + buckets: copyBuckets(), + totalFilteredObjects, + isLimitReached: false, + } + } + + const limitedBuckets = {} as { [K in keyof T]: Array } + let remaining = limit + + for (const [key, objects] of entries) { + const count = Math.min(objects.length, remaining) + limitedBuckets[key] = objects.slice(0, count) as Array< + T[typeof key][number] + > + remaining -= count + } + + return { + buckets: limitedBuckets, + totalFilteredObjects, + isLimitReached: true, + } +} diff --git a/tests/applyObjectLimitToBuckets.test.ts b/tests/applyObjectLimitToBuckets.test.ts new file mode 100644 index 0000000..c59dce7 --- /dev/null +++ b/tests/applyObjectLimitToBuckets.test.ts @@ -0,0 +1,35 @@ +import { expect, test } from "bun:test" +import { applyObjectLimitToBuckets } from "site/components/InteractiveGraphics/applyObjectLimitToBuckets" + +test("applies one global object limit across buckets", () => { + const result = applyObjectLimitToBuckets( + { + lines: ["line-1", "line-2", "line-3"], + rects: ["rect-1", "rect-2"], + points: ["point-1"], + }, + 4, + ) + + expect(result.totalFilteredObjects).toBe(6) + expect(result.isLimitReached).toBe(true) + expect(result.buckets.lines).toEqual(["line-1", "line-2", "line-3"]) + expect(result.buckets.rects).toEqual(["rect-1"]) + expect(result.buckets.points).toEqual([]) +}) + +test("reports pre-limit filtered count without limiting when under the cap", () => { + const result = applyObjectLimitToBuckets( + { + lines: ["line-1"], + rects: ["rect-1"], + points: [], + }, + 3, + ) + + expect(result.totalFilteredObjects).toBe(2) + expect(result.isLimitReached).toBe(false) + expect(result.buckets.lines).toEqual(["line-1"]) + expect(result.buckets.rects).toEqual(["rect-1"]) +})