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
135 changes: 87 additions & 48 deletions site/components/InteractiveGraphics/InteractiveGraphics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ContextMenu } from "./ContextMenu"
import { InfiniteLine } from "./InfiniteLine"
import { InteractiveState } from "./InteractiveState"
import { Line } from "./Line"
import { limitFilteredObjects } from "./limitFilteredObjects"
import { Marker, MarkerPoint } from "./Marker"
import { Point } from "./Point"
import { Polygon } from "./Polygon"
Expand Down Expand Up @@ -421,65 +422,102 @@ 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(
() =>
filterAndLimit(graphics.lines, filterLines).sort(
(a, b) =>
(a.zIndex ?? 0) - (b.zIndex ?? 0) ||
a.originalIndex - b.originalIndex,
),
[graphics.lines, filterLines, objectLimit],
const filteredLinesBeforeLimit = useMemo(
() => filterObjects(graphics.lines, filterLines),
[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(
() => 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 {
groups: limitedFilteredObjects,
totalFilteredObjects,
isLimitReached,
} = useMemo(
() =>
limitFilteredObjects(
{
arrows: filteredArrowsBeforeLimit,
infiniteLines: filteredInfiniteLinesBeforeLimit,
lines: filteredLinesBeforeLimit,
rects: filteredRectsBeforeLimit,
polygons: filteredPolygonsBeforeLimit,
circles: filteredCirclesBeforeLimit,
texts: filteredTextsBeforeLimit,
points: filteredPointsBeforeLimit,
},
objectLimit,
),
[
filteredArrowsBeforeLimit,
filteredInfiniteLinesBeforeLimit,
filteredLinesBeforeLimit,
filteredRectsBeforeLimit,
filteredPolygonsBeforeLimit,
filteredCirclesBeforeLimit,
filteredTextsBeforeLimit,
filteredPointsBeforeLimit,
objectLimit,
],
)

const totalFilteredObjects =
filteredInfiniteLines.length +
filteredLines.length +
filteredRects.length +
filteredPolygons.length +
filteredPoints.length +
filteredCircles.length +
filteredTexts.length +
filteredArrows.length
const isLimitReached = objectLimit && totalFilteredObjects > objectLimit
const filteredLines = useMemo(
() =>
limitedFilteredObjects.lines
.slice()
.sort(
(a, b) =>
(a.zIndex ?? 0) - (b.zIndex ?? 0) ||
a.originalIndex - b.originalIndex,
),
[limitedFilteredObjects.lines],
)
const filteredInfiniteLines = limitedFilteredObjects.infiniteLines
const filteredRects = useMemo(
() => sortRectsByArea(limitedFilteredObjects.rects),
[limitedFilteredObjects.rects],
)
const filteredPolygons = limitedFilteredObjects.polygons
const filteredPoints = limitedFilteredObjects.points
const filteredCircles = limitedFilteredObjects.circles
const filteredTexts = limitedFilteredObjects.texts
const filteredArrows = limitedFilteredObjects.arrows

return (
<div>
Expand Down Expand Up @@ -552,15 +590,16 @@ export const InteractiveGraphics = ({
/>
Show last step
</label>
{isLimitReached && (
<span style={{ color: "red", fontSize: "12px" }}>
Display limited to {objectLimit} objects. Received:{" "}
{totalFilteredObjects}.
</span>
)}
</div>
)}

{isLimitReached && (
<span style={{ color: "red", fontSize: "12px" }}>
Display limited to {objectLimit} objects. Received:{" "}
{totalFilteredObjects}.
</span>
)}

<label>
<input
type="checkbox"
Expand Down
49 changes: 49 additions & 0 deletions site/components/InteractiveGraphics/limitFilteredObjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export const filteredObjectGroupOrder = [
"arrows",
"infiniteLines",
"lines",
"rects",
"polygons",
"circles",
"texts",
"points",
] as const

export type FilteredObjectGroupName = (typeof filteredObjectGroupOrder)[number]

export type FilteredObjectGroups = Record<FilteredObjectGroupName, unknown[]>

export const limitFilteredObjects = <TGroups extends FilteredObjectGroups>(
groups: TGroups,
objectLimit?: number,
): {
groups: TGroups
totalFilteredObjects: number
isLimitReached: boolean
} => {
const totalFilteredObjects = filteredObjectGroupOrder.reduce(
(total, groupName) => total + groups[groupName].length,
0,
)

if (!objectLimit || objectLimit <= 0 || totalFilteredObjects <= objectLimit) {
return { groups, totalFilteredObjects, isLimitReached: false }
}

let objectsToDrop = totalFilteredObjects - objectLimit
const limitedGroups: Partial<FilteredObjectGroups> = {}

for (const groupName of filteredObjectGroupOrder) {
const objects = groups[groupName]
const dropCount = Math.min(objectsToDrop, objects.length)
limitedGroups[groupName] =
dropCount > 0 ? objects.slice(dropCount) : objects
objectsToDrop -= dropCount
}

return {
groups: limitedGroups as TGroups,
totalFilteredObjects,
isLimitReached: true,
}
}
68 changes: 68 additions & 0 deletions tests/limitFilteredObjects.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { describe, expect, test } from "bun:test"
import {
type FilteredObjectGroupName,
filteredObjectGroupOrder,
limitFilteredObjects,
} from "site/components/InteractiveGraphics/limitFilteredObjects"

const makeObjects = (prefix: string, count: number) =>
Array.from({ length: count }, (_, originalIndex) => ({
id: `${prefix}-${originalIndex}`,
originalIndex,
}))

const makeGroups = (
groups: Partial<
Record<FilteredObjectGroupName, ReturnType<typeof makeObjects>>
>,
) =>
Object.fromEntries(
filteredObjectGroupOrder.map((groupName) => [
groupName,
groups[groupName] ?? [],
]),
) as Record<FilteredObjectGroupName, ReturnType<typeof makeObjects>>

const countObjects = (
groups: Record<FilteredObjectGroupName, ReturnType<typeof makeObjects>>,
) =>
filteredObjectGroupOrder.reduce(
(total, groupName) => total + groups[groupName].length,
0,
)

describe("limitFilteredObjects", () => {
test("enforces objectLimit across all object groups", () => {
const result = limitFilteredObjects(
makeGroups({
arrows: makeObjects("arrow", 2),
lines: makeObjects("line", 2),
points: makeObjects("point", 2),
}),
3,
)

expect(result.totalFilteredObjects).toBe(6)
expect(result.isLimitReached).toBe(true)
expect(countObjects(result.groups)).toBe(3)
expect(result.groups.arrows).toEqual([])
expect(result.groups.lines.map((object) => object.id)).toEqual(["line-1"])
expect(result.groups.points.map((object) => object.id)).toEqual([
"point-0",
"point-1",
])
})

test("leaves filtered groups unchanged when the total is within the limit", () => {
const groups = makeGroups({
rects: makeObjects("rect", 1),
circles: makeObjects("circle", 1),
texts: makeObjects("text", 1),
})
const result = limitFilteredObjects(groups, 3)

expect(result.totalFilteredObjects).toBe(3)
expect(result.isLimitReached).toBe(false)
expect(result.groups).toBe(groups)
})
})
Loading