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
158 changes: 115 additions & 43 deletions site/components/InteractiveGraphics/InteractiveGraphics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,46 @@ export type GraphicsObjectClickEvent = {
object: any
}

const normalizeObjectLimit = (objectLimit?: number) => {
if (objectLimit === undefined || !Number.isFinite(objectLimit)) return null
const normalizedLimit = Math.floor(objectLimit)
return normalizedLimit > 0 ? normalizedLimit : null
}

export const limitObjectGroups = <T extends Record<string, readonly unknown[]>>(
groups: T,
objectLimit?: number,
): {
groups: T
totalObjectCount: number
isLimitReached: boolean
} => {
const totalObjectCount = Object.values(groups).reduce(
(count, group) => count + group.length,
0,
)
const normalizedLimit = normalizeObjectLimit(objectLimit)

if (normalizedLimit === null) {
return { groups, totalObjectCount, isLimitReached: false }
}

let remainingObjects = normalizedLimit
const limitedGroups = Object.fromEntries(
Object.entries(groups).map(([groupName, group]) => {
const limitedGroup = group.slice(0, remainingObjects)
remainingObjects = Math.max(remainingObjects - limitedGroup.length, 0)
return [groupName, limitedGroup]
}),
) as unknown as T

return {
groups: limitedGroups,
totalObjectCount,
isLimitReached: totalObjectCount > normalizedLimit,
}
}

export const InteractiveGraphics = ({
graphics,
onObjectClicked,
Expand Down Expand Up @@ -421,65 +461,96 @@ 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(
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],
() => filterObjects(graphics.infiniteLines, filterLayerAndStep),
[graphics.infiniteLines, filterLayerAndStep],
)
const filteredRects = useMemo(
() => sortRectsByArea(filterAndLimit(graphics.rects, filterRects)),
[graphics.rects, filterRects, objectLimit],
() => sortRectsByArea(filterObjects(graphics.rects, filterRects)),
[graphics.rects, filterRects],
)
const filteredPolygons = useMemo(
() => filterAndLimit(graphics.polygons, filterPolygons),
[graphics.polygons, filterPolygons, objectLimit],
() => filterObjects(graphics.polygons, filterPolygons),
[graphics.polygons, filterPolygons],
)
const filteredPoints = useMemo(
() => filterAndLimit(graphics.points, filterPoints),
[graphics.points, filterPoints, objectLimit],
() => filterObjects(graphics.points, filterPoints),
[graphics.points, filterPoints],
)
const filteredCircles = useMemo(
() => filterAndLimit(graphics.circles, filterCircles),
[graphics.circles, filterCircles, objectLimit],
() => filterObjects(graphics.circles, filterCircles),
[graphics.circles, filterCircles],
)
const filteredTexts = useMemo(
() => filterAndLimit(graphics.texts, filterTexts),
[graphics.texts, filterTexts, objectLimit],
() => filterObjects(graphics.texts, filterTexts),
[graphics.texts, filterTexts],
)
const filteredArrows = useMemo(
() => filterAndLimit(graphics.arrows, filterArrows),
[graphics.arrows, filterArrows, objectLimit],
() => 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 {
groups: limitedObjectGroups,
totalObjectCount,
isLimitReached,
} = useMemo(
() =>
limitObjectGroups(
{
filteredArrows,
filteredInfiniteLines,
filteredLines,
filteredRects,
filteredPolygons,
filteredCircles,
filteredTexts,
filteredPoints,
},
objectLimit,
),
[
filteredArrows,
filteredInfiniteLines,
filteredLines,
filteredRects,
filteredPolygons,
filteredCircles,
filteredTexts,
filteredPoints,
objectLimit,
],
)

const {
filteredArrows: visibleArrows,
filteredInfiniteLines: visibleInfiniteLines,
filteredLines: visibleLines,
filteredRects: visibleRects,
filteredPolygons: visiblePolygons,
filteredCircles: visibleCircles,
filteredTexts: visibleTexts,
filteredPoints: visiblePoints,
} = limitedObjectGroups

return (
<div>
Expand Down Expand Up @@ -552,15 +623,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 {normalizeObjectLimit(objectLimit)} objects.
Received: {totalObjectCount}.
</span>
)}

<label>
<input
type="checkbox"
Expand Down Expand Up @@ -598,15 +670,15 @@ export const InteractiveGraphics = ({
onContextMenu={handleContextMenu}
>
<DimensionOverlay transform={realToScreen}>
{filteredArrows.map((arrow) => (
{visibleArrows.map((arrow) => (
<Arrow
key={arrow.originalIndex}
arrow={arrow}
index={arrow.originalIndex}
interactiveState={interactiveState}
/>
))}
{filteredInfiniteLines.map((infiniteLine) => (
{visibleInfiniteLines.map((infiniteLine) => (
<InfiniteLine
key={infiniteLine.originalIndex}
infiniteLine={infiniteLine}
Expand All @@ -615,7 +687,7 @@ export const InteractiveGraphics = ({
size={size}
/>
))}
{filteredLines.map((line) => (
{visibleLines.map((line) => (
<Line
key={line.originalIndex}
line={line}
Expand All @@ -625,39 +697,39 @@ export const InteractiveGraphics = ({
mousePosition={mousePosition}
/>
))}
{filteredRects.map((rect) => (
{visibleRects.map((rect) => (
<Rect
key={rect.originalIndex}
rect={rect}
index={rect.originalIndex}
interactiveState={interactiveState}
/>
))}
{filteredPolygons.map((polygon) => (
{visiblePolygons.map((polygon) => (
<Polygon
key={polygon.originalIndex}
polygon={polygon}
index={polygon.originalIndex}
interactiveState={interactiveState}
/>
))}
{filteredCircles.map((circle) => (
{visibleCircles.map((circle) => (
<Circle
key={circle.originalIndex}
circle={circle}
index={circle.originalIndex}
interactiveState={interactiveState}
/>
))}
{filteredTexts.map((txt) => (
{visibleTexts.map((txt) => (
<Text
key={txt.originalIndex}
textObj={txt}
index={txt.originalIndex}
interactiveState={interactiveState}
/>
))}
{filteredPoints.map((point) => (
{visiblePoints.map((point) => (
<Point
key={point.originalIndex}
point={point}
Expand Down
48 changes: 48 additions & 0 deletions tests/limit-object-groups.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, test } from "bun:test"
import { limitObjectGroups } from "../site/components/InteractiveGraphics/InteractiveGraphics"

describe("limitObjectGroups", () => {
test("limits objects globally across groups in display order", () => {
const result = limitObjectGroups(
{
lines: [{ id: "line-1" }, { id: "line-2" }],
rects: [{ id: "rect-1" }, { id: "rect-2" }],
points: [{ id: "point-1" }],
},
3,
)

expect(result.totalObjectCount).toBe(5)
expect(result.isLimitReached).toBe(true)
expect(result.groups.lines).toEqual([{ id: "line-1" }, { id: "line-2" }])
expect(result.groups.rects).toEqual([{ id: "rect-1" }])
expect(result.groups.points).toEqual([])
})

test("counts filtered objects before applying the limit", () => {
const result = limitObjectGroups(
{
arrows: [],
lines: [{ id: "visible-line" }],
rects: [{ id: "visible-rect" }],
},
2,
)

expect(result.totalObjectCount).toBe(2)
expect(result.isLimitReached).toBe(false)
expect(result.groups.lines).toEqual([{ id: "visible-line" }])
expect(result.groups.rects).toEqual([{ id: "visible-rect" }])
})

test("does not trim for missing or non-positive limits", () => {
const groups = {
lines: [{ id: "line-1" }],
points: [{ id: "point-1" }],
}

expect(limitObjectGroups(groups).groups).toEqual(groups)
expect(limitObjectGroups(groups, 0).groups).toEqual(groups)
expect(limitObjectGroups(groups, -1).groups).toEqual(groups)
})
})
Loading