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
103 changes: 66 additions & 37 deletions site/components/InteractiveGraphics/InteractiveGraphics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { Polygon } from "./Polygon"
import { Rect } from "./Rect"
import { Text } from "./Text"
import { Tooltip } from "./Tooltip"
import { applyObjectLimit } from "./apply-object-limit"
import {
useDoesLineIntersectViewport,
useFilterArrows,
Expand Down Expand Up @@ -421,65 +422,93 @@ export const InteractiveGraphics = ({
filterLayerAndStep,
})

const filterAndLimit = <T,>(
const filterObjects = <T,>(
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(
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 allFilteredInfiniteLines = useMemo(
() => filterObjects(graphics.infiniteLines, filterLayerAndStep),
[graphics.infiniteLines, filterLayerAndStep],
)
const filteredRects = useMemo(
() => sortRectsByArea(filterAndLimit(graphics.rects, filterRects)),
[graphics.rects, filterRects, objectLimit],
const allFilteredRects = useMemo(
() => sortRectsByArea(filterObjects(graphics.rects, filterRects)),
[graphics.rects, filterRects],
)
const filteredPolygons = useMemo(
() => filterAndLimit(graphics.polygons, filterPolygons),
[graphics.polygons, filterPolygons, objectLimit],
const allFilteredPolygons = useMemo(
() => filterObjects(graphics.polygons, filterPolygons),
[graphics.polygons, filterPolygons],
)
const filteredPoints = useMemo(
() => filterAndLimit(graphics.points, filterPoints),
[graphics.points, filterPoints, objectLimit],
const allFilteredPoints = useMemo(
() => filterObjects(graphics.points, filterPoints),
[graphics.points, filterPoints],
)
const filteredCircles = useMemo(
() => filterAndLimit(graphics.circles, filterCircles),
[graphics.circles, filterCircles, objectLimit],
const allFilteredCircles = useMemo(
() => filterObjects(graphics.circles, filterCircles),
[graphics.circles, filterCircles],
)
const filteredTexts = useMemo(
() => filterAndLimit(graphics.texts, filterTexts),
[graphics.texts, filterTexts, objectLimit],
const allFilteredTexts = useMemo(
() => filterObjects(graphics.texts, filterTexts),
[graphics.texts, filterTexts],
)
const filteredArrows = useMemo(
() => filterAndLimit(graphics.arrows, filterArrows),
[graphics.arrows, filterArrows, objectLimit],
const allFilteredArrows = 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 {
buckets: limitedObjectBuckets,
totalFilteredObjects,
isLimitReached,
} = useMemo(
() =>
applyObjectLimit(
{
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 filteredArrows = limitedObjectBuckets.arrows
const filteredInfiniteLines = limitedObjectBuckets.infiniteLines
const filteredLines = limitedObjectBuckets.lines
const filteredRects = limitedObjectBuckets.rects
const filteredPolygons = limitedObjectBuckets.polygons
const filteredCircles = limitedObjectBuckets.circles
const filteredTexts = limitedObjectBuckets.texts
const filteredPoints = limitedObjectBuckets.points

return (
<div>
Expand Down
52 changes: 52 additions & 0 deletions site/components/InteractiveGraphics/apply-object-limit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
type IndexedObject = { originalIndex: number }

export type ObjectBucketMap = Record<string, IndexedObject[]>

const getNormalizedObjectLimit = (objectLimit?: number): number | null => {
if (typeof objectLimit !== "number" || !Number.isFinite(objectLimit)) {
return null
}

return Math.max(0, Math.floor(objectLimit))
}

export const applyObjectLimit = <T extends ObjectBucketMap>(
buckets: T,
objectLimit?: number,
): {
buckets: T
totalFilteredObjects: number
isLimitReached: boolean
} => {
const entries = Object.entries(buckets)
const slots = entries.flatMap(([bucketName, objects]) =>
objects.map((object) => ({
key: `${bucketName}:${object.originalIndex}`,
})),
)
const totalFilteredObjects = slots.length
const normalizedLimit = getNormalizedObjectLimit(objectLimit)

if (normalizedLimit === null || totalFilteredObjects <= normalizedLimit) {
return { buckets, totalFilteredObjects, isLimitReached: false }
}

const firstAllowedIndex = totalFilteredObjects - normalizedLimit
const allowedKeys = new Set(
slots.slice(firstAllowedIndex).map((slot) => slot.key),
)
const limitedBuckets = Object.fromEntries(
entries.map(([bucketName, objects]) => [
bucketName,
objects.filter((object) =>
allowedKeys.has(`${bucketName}:${object.originalIndex}`),
),
]),
) as T

return {
buckets: limitedBuckets,
totalFilteredObjects,
isLimitReached: true,
}
}
50 changes: 50 additions & 0 deletions tests/apply-object-limit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { expect, test } from "bun:test"
import { applyObjectLimit } from "site/components/InteractiveGraphics/apply-object-limit"

test("object limit caps the total filtered objects across buckets", () => {
const result = applyObjectLimit(
{
lines: [{ originalIndex: 0 }, { originalIndex: 1 }],
rects: [{ originalIndex: 0 }, { originalIndex: 1 }],
points: [{ originalIndex: 0 }, { originalIndex: 1 }],
},
3,
)

expect(result.totalFilteredObjects).toBe(6)
expect(result.isLimitReached).toBe(true)
expect(result.buckets.lines).toEqual([])
expect(result.buckets.rects).toEqual([{ originalIndex: 1 }])
expect(result.buckets.points).toEqual([
{ originalIndex: 0 },
{ originalIndex: 1 },
])
})

test("object limit leaves buckets unchanged when the total is below the cap", () => {
const buckets = {
lines: [{ originalIndex: 0 }],
rects: [{ originalIndex: 0 }],
}

const result = applyObjectLimit(buckets, 3)

expect(result.totalFilteredObjects).toBe(2)
expect(result.isLimitReached).toBe(false)
expect(result.buckets).toBe(buckets)
})

test("object limit can intentionally hide every object", () => {
const result = applyObjectLimit(
{
lines: [{ originalIndex: 0 }],
rects: [{ originalIndex: 0 }],
},
0,
)

expect(result.totalFilteredObjects).toBe(2)
expect(result.isLimitReached).toBe(true)
expect(result.buckets.lines).toEqual([])
expect(result.buckets.rects).toEqual([])
})
Loading