Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 80 additions & 39 deletions site/components/InteractiveGraphics/InteractiveGraphics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
useFilterTexts,
useIsPointOnScreen,
} from "./hooks"
import { normalizeObjectLimit, takeObjectLimit } from "./object-limit"
import { tooltipLayerZIndex } from "./tooltipLayer"

export type GraphicsObjectClickEvent = {
Expand Down Expand Up @@ -421,65 +422,105 @@ export const InteractiveGraphics = ({
filterLayerAndStep,
})

const filterAndLimit = <T,>(
const filterObjects = <T extends object>(
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 filteredLinesBeforeLimit = useMemo(
() =>
filterAndLimit(graphics.lines, filterLines).sort(
filterObjects(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 filteredInfiniteLinesBeforeLimit = useMemo(
() => filterObjects(graphics.infiniteLines, filterLayerAndStep),
[graphics.infiniteLines, filterLayerAndStep],
)
const filteredRects = useMemo(
() => sortRectsByArea(filterAndLimit(graphics.rects, filterRects)),
[graphics.rects, filterRects, objectLimit],
const filteredRectsBeforeLimit = useMemo(
() => sortRectsByArea(filterObjects(graphics.rects, filterRects)),
[graphics.rects, filterRects],
)
const filteredPolygons = useMemo(
() => filterAndLimit(graphics.polygons, filterPolygons),
[graphics.polygons, filterPolygons, objectLimit],
const filteredPolygonsBeforeLimit = useMemo(
() => filterObjects(graphics.polygons, filterPolygons),
[graphics.polygons, filterPolygons],
)
const filteredPoints = useMemo(
() => filterAndLimit(graphics.points, filterPoints),
[graphics.points, filterPoints, objectLimit],
const filteredPointsBeforeLimit = useMemo(
() => filterObjects(graphics.points, filterPoints),
[graphics.points, filterPoints],
)
const filteredCircles = useMemo(
() => filterAndLimit(graphics.circles, filterCircles),
[graphics.circles, filterCircles, objectLimit],
const filteredCirclesBeforeLimit = useMemo(
() => filterObjects(graphics.circles, filterCircles),
[graphics.circles, filterCircles],
)
const filteredTexts = useMemo(
() => filterAndLimit(graphics.texts, filterTexts),
[graphics.texts, filterTexts, objectLimit],
const filteredTextsBeforeLimit = useMemo(
() => filterObjects(graphics.texts, filterTexts),
[graphics.texts, filterTexts],
)
const filteredArrows = useMemo(
() => filterAndLimit(graphics.arrows, filterArrows),
[graphics.arrows, filterArrows, objectLimit],
const filteredArrowsBeforeLimit = useMemo(
() => filterObjects(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 normalizedObjectLimit = normalizeObjectLimit(objectLimit)

const [
filteredArrows,
filteredInfiniteLines,
filteredLines,
filteredRects,
filteredPolygons,
filteredCircles,
filteredTexts,
filteredPoints,
] = useMemo(
() =>
takeObjectLimit(
[
filteredArrowsBeforeLimit,
filteredInfiniteLinesBeforeLimit,
filteredLinesBeforeLimit,
filteredRectsBeforeLimit,
filteredPolygonsBeforeLimit,
filteredCirclesBeforeLimit,
filteredTextsBeforeLimit,
filteredPointsBeforeLimit,
] as const,
normalizedObjectLimit,
),
[
filteredArrowsBeforeLimit,
filteredInfiniteLinesBeforeLimit,
filteredLinesBeforeLimit,
filteredRectsBeforeLimit,
filteredPolygonsBeforeLimit,
filteredCirclesBeforeLimit,
filteredTextsBeforeLimit,
filteredPointsBeforeLimit,
normalizedObjectLimit,
],
)

const totalFilteredObjectsBeforeLimit =
filteredInfiniteLinesBeforeLimit.length +
filteredLinesBeforeLimit.length +
filteredRectsBeforeLimit.length +
filteredPolygonsBeforeLimit.length +
filteredPointsBeforeLimit.length +
filteredCirclesBeforeLimit.length +
filteredTextsBeforeLimit.length +
filteredArrowsBeforeLimit.length
const isLimitReached =
normalizedObjectLimit !== null &&
totalFilteredObjectsBeforeLimit > normalizedObjectLimit

return (
<div>
Expand Down Expand Up @@ -554,8 +595,8 @@ export const InteractiveGraphics = ({
</label>
{isLimitReached && (
<span style={{ color: "red", fontSize: "12px" }}>
Display limited to {objectLimit} objects. Received:{" "}
{totalFilteredObjects}.
Display limited to {normalizedObjectLimit} objects. Received:{" "}
{totalFilteredObjectsBeforeLimit}.
</span>
)}
</div>
Expand Down
26 changes: 26 additions & 0 deletions site/components/InteractiveGraphics/object-limit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const normalizeObjectLimit = (objectLimit: number | undefined) => {
if (objectLimit === undefined || !Number.isFinite(objectLimit)) return null
return Math.max(0, Math.floor(objectLimit))
}

export const takeObjectLimit = <T extends readonly unknown[][]>(
objectGroups: T,
objectLimit: number | null,
): { [K in keyof T]: T[K][number][] } => {
if (objectLimit === null) {
return objectGroups.map((group) => [...group]) as {
[K in keyof T]: T[K][number][]
}
}

let remainingObjects = objectLimit

return objectGroups.map((group) => {
if (remainingObjects <= 0) return []

const limitedGroup = group.slice(0, remainingObjects)
remainingObjects -= limitedGroup.length

return limitedGroup
}) as { [K in keyof T]: T[K][number][] }
}
41 changes: 41 additions & 0 deletions tests/interactive-object-limit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, expect, test } from "bun:test"
import {
normalizeObjectLimit,
takeObjectLimit,
} from "site/components/InteractiveGraphics/object-limit"

describe("InteractiveGraphics objectLimit", () => {
test("applies one object budget across render groups", () => {
const [arrows, lines, rects] = takeObjectLimit(
[["arrow-1", "arrow-2"], ["line-1", "line-2"], ["rect-1"]] as const,
3,
)

expect(arrows).toEqual(["arrow-1", "arrow-2"])
expect(lines).toEqual(["line-1"])
expect(rects).toEqual([])
})

test("counts zero as a real limit", () => {
const [points] = takeObjectLimit([["point-1"]] as const, 0)

expect(points).toEqual([])
})

test("leaves all groups unchanged when no limit is provided", () => {
const [points, circles] = takeObjectLimit(
[["point-1"], ["circle-1"]] as const,
null,
)

expect(points).toEqual(["point-1"])
expect(circles).toEqual(["circle-1"])
})

test("normalizes fractional and invalid limits", () => {
expect(normalizeObjectLimit(2.9)).toBe(2)
expect(normalizeObjectLimit(-1)).toBe(0)
expect(normalizeObjectLimit(Number.NaN)).toBe(null)
expect(normalizeObjectLimit(undefined)).toBe(null)
})
})
Loading