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
95 changes: 46 additions & 49 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 { getFilteredAndLimitedGraphics } from "./object-limit"
import {
useDoesLineIntersectViewport,
useFilterArrows,
Expand Down Expand Up @@ -421,65 +422,61 @@ export const InteractiveGraphics = ({
filterLayerAndStep,
})

const filterAndLimit = <T,>(
objects: T[] | undefined,
filterFn: (obj: T) => boolean,
): (T & { originalIndex: number })[] => {
if (!objects) return []
const filtered = objects
.map((obj, index) => ({ ...obj, originalIndex: index }))
.filter(filterFn)
return objectLimit ? filtered.slice(-objectLimit) : filtered
}
const {
lines: filteredLinesUnsorted,
infiniteLines: filteredInfiniteLines,
rects: filteredRectsUnsorted,
polygons: filteredPolygons,
points: filteredPoints,
circles: filteredCircles,
texts: filteredTexts,
arrows: filteredArrows,
totalFilteredObjects,
isLimitReached,
} = useMemo(
() =>
getFilteredAndLimitedGraphics(
graphics,
{
lines: filterLines,
infiniteLines: filterLayerAndStep,
rects: filterRects,
polygons: filterPolygons,
points: filterPoints,
circles: filterCircles,
texts: filterTexts,
arrows: filterArrows,
},
objectLimit,
),
[
graphics,
filterLines,
filterLayerAndStep,
filterRects,
filterPolygons,
filterPoints,
filterCircles,
filterTexts,
filterArrows,
objectLimit,
],
)

const filteredLines = useMemo(
() =>
filterAndLimit(graphics.lines, filterLines).sort(
filteredLinesUnsorted.sort(
(a, b) =>
(a.zIndex ?? 0) - (b.zIndex ?? 0) ||
a.originalIndex - b.originalIndex,
),
[graphics.lines, filterLines, objectLimit],
)
const filteredInfiniteLines = useMemo(
() => filterAndLimit(graphics.infiniteLines, filterLayerAndStep),
[graphics.infiniteLines, filterLayerAndStep, objectLimit],
[filteredLinesUnsorted],
)

const filteredRects = useMemo(
() => sortRectsByArea(filterAndLimit(graphics.rects, filterRects)),
[graphics.rects, filterRects, objectLimit],
() => sortRectsByArea(filteredRectsUnsorted),
[filteredRectsUnsorted],
)
const filteredPolygons = useMemo(
() => filterAndLimit(graphics.polygons, filterPolygons),
[graphics.polygons, filterPolygons, objectLimit],
)
const filteredPoints = useMemo(
() => filterAndLimit(graphics.points, filterPoints),
[graphics.points, filterPoints, objectLimit],
)
const filteredCircles = useMemo(
() => filterAndLimit(graphics.circles, filterCircles),
[graphics.circles, filterCircles, objectLimit],
)
const filteredTexts = useMemo(
() => filterAndLimit(graphics.texts, filterTexts),
[graphics.texts, filterTexts, objectLimit],
)
const filteredArrows = useMemo(
() => filterAndLimit(graphics.arrows, filterArrows),
[graphics.arrows, filterArrows, objectLimit],
)

const totalFilteredObjects =
filteredInfiniteLines.length +
filteredLines.length +
filteredRects.length +
filteredPolygons.length +
filteredPoints.length +
filteredCircles.length +
filteredTexts.length +
filteredArrows.length
const isLimitReached = objectLimit && totalFilteredObjects > objectLimit

return (
<div>
Expand Down
152 changes: 152 additions & 0 deletions site/components/InteractiveGraphics/object-limit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import type {
Arrow,
Circle,
GraphicsObject,
InfiniteLine,
Line,
Point,
Polygon,
Rect,
Text,
} from "../../../lib/types"

export type GraphicsObjectType =
| "lines"
| "infiniteLines"
| "rects"
| "polygons"
| "points"
| "circles"
| "texts"
| "arrows"

export type IndexedGraphicsObject<T> = T & {
originalIndex: number
}

export type LimitedGraphicsCollections = {
lines: IndexedGraphicsObject<Line>[]
infiniteLines: IndexedGraphicsObject<InfiniteLine>[]
rects: IndexedGraphicsObject<Rect>[]
polygons: IndexedGraphicsObject<Polygon>[]
points: IndexedGraphicsObject<Point>[]
circles: IndexedGraphicsObject<Circle>[]
texts: IndexedGraphicsObject<Text>[]
arrows: IndexedGraphicsObject<Arrow>[]
totalFilteredObjects: number
totalDisplayedObjects: number
isLimitReached: boolean
}

const GRAPHICS_OBJECT_TYPES: GraphicsObjectType[] = [
"lines",
"infiniteLines",
"rects",
"polygons",
"points",
"circles",
"texts",
"arrows",
]

type FilterMap = {
lines: (obj: Line) => boolean
infiniteLines: (obj: InfiniteLine) => boolean
rects: (obj: Rect) => boolean
polygons: (obj: Polygon) => boolean
points: (obj: Point) => boolean
circles: (obj: Circle) => boolean
texts: (obj: Text) => boolean
arrows: (obj: Arrow) => boolean
}

export const getFilteredAndLimitedGraphics = (
graphics: GraphicsObject,
filters: FilterMap,
objectLimit?: number,
): LimitedGraphicsCollections => {
const filteredCollections = {
lines: (graphics.lines ?? [])
.map((obj, originalIndex) => ({ ...obj, originalIndex }))
.filter(filters.lines),
infiniteLines: (graphics.infiniteLines ?? [])
.map((obj, originalIndex) => ({ ...obj, originalIndex }))
.filter(filters.infiniteLines),
rects: (graphics.rects ?? [])
.map((obj, originalIndex) => ({ ...obj, originalIndex }))
.filter(filters.rects),
polygons: (graphics.polygons ?? [])
.map((obj, originalIndex) => ({ ...obj, originalIndex }))
.filter(filters.polygons),
points: (graphics.points ?? [])
.map((obj, originalIndex) => ({ ...obj, originalIndex }))
.filter(filters.points),
circles: (graphics.circles ?? [])
.map((obj, originalIndex) => ({ ...obj, originalIndex }))
.filter(filters.circles),
texts: (graphics.texts ?? [])
.map((obj, originalIndex) => ({ ...obj, originalIndex }))
.filter(filters.texts),
arrows: (graphics.arrows ?? [])
.map((obj, originalIndex) => ({ ...obj, originalIndex }))
.filter(filters.arrows),
}

const totalFilteredObjects = Object.values(filteredCollections).reduce(
(sum, objects) => sum + objects.length,
0,
)

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

const keptKeys = new Set<string>()
const flattened = GRAPHICS_OBJECT_TYPES.flatMap((type) =>
filteredCollections[type].map((obj) => ({
type,
key: `${type}:${obj.originalIndex}`,
})),
)
const limitedObjects = flattened.slice(-objectLimit)
for (const entry of limitedObjects) keptKeys.add(entry.key)

const limitedCollections = {
lines: filteredCollections.lines.filter((obj) =>
keptKeys.has(`lines:${obj.originalIndex}`),
),
infiniteLines: filteredCollections.infiniteLines.filter((obj) =>
keptKeys.has(`infiniteLines:${obj.originalIndex}`),
),
rects: filteredCollections.rects.filter((obj) =>
keptKeys.has(`rects:${obj.originalIndex}`),
),
polygons: filteredCollections.polygons.filter((obj) =>
keptKeys.has(`polygons:${obj.originalIndex}`),
),
points: filteredCollections.points.filter((obj) =>
keptKeys.has(`points:${obj.originalIndex}`),
),
circles: filteredCollections.circles.filter((obj) =>
keptKeys.has(`circles:${obj.originalIndex}`),
),
texts: filteredCollections.texts.filter((obj) =>
keptKeys.has(`texts:${obj.originalIndex}`),
),
arrows: filteredCollections.arrows.filter((obj) =>
keptKeys.has(`arrows:${obj.originalIndex}`),
),
}

return {
...limitedCollections,
totalFilteredObjects,
totalDisplayedObjects: objectLimit,
isLimitReached: true,
}
}
109 changes: 109 additions & 0 deletions tests/objectLimit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { describe, expect, test } from "bun:test"
import type { GraphicsObject } from "../lib"
import { getFilteredAndLimitedGraphics } from "../site/components/InteractiveGraphics/object-limit"

const createGraphics = (): GraphicsObject => ({
rects: [
{
center: { x: 200, y: 100 },
width: 50,
height: 100,
layer: "layer1",
step: 0,
},
{
center: { x: 250, y: 100 },
width: 50,
height: 100,
layer: "layer1",
step: 1,
},
{
center: { x: 300, y: 100 },
width: 50,
height: 100,
layer: "layer1",
step: 2,
},
],
points: [
{
x: 0,
y: 0,
layer: "layer1",
},
{
x: 50,
y: 0,
layer: "layer2",
},
{
x: 100,
y: 0,
layer: "layer3",
},
],
circles: [
{
center: { x: 400, y: 100 },
radius: 25,
fill: "blue",
stroke: "black",
layer: "layer1",
step: 0,
label: "Circle 1",
},
],
})

describe("getFilteredAndLimitedGraphics", () => {
test("limits across all object types after filters are applied", () => {
const graphics = createGraphics()

const result = getFilteredAndLimitedGraphics(
graphics,
{
lines: () => true,
infiniteLines: () => true,
rects: (rect) => rect.layer === "layer1" && rect.step === 0,
polygons: () => true,
points: (point) => point.layer === "layer1",
circles: (circle) => circle.layer === "layer1" && circle.step === 0,
texts: () => true,
arrows: () => true,
},
3,
)

expect(result.totalFilteredObjects).toBe(3)
expect(result.isLimitReached).toBe(false)
expect(result.rects).toHaveLength(1)
expect(result.points).toHaveLength(1)
expect(result.circles).toHaveLength(1)
})

test("enforces a single shared limit across categories using post-filter ordering", () => {
const graphics = createGraphics()

const result = getFilteredAndLimitedGraphics(
graphics,
{
lines: () => true,
infiniteLines: () => true,
rects: () => true,
polygons: () => true,
points: () => true,
circles: () => true,
texts: () => true,
arrows: () => true,
},
3,
)

expect(result.totalFilteredObjects).toBe(7)
expect(result.isLimitReached).toBe(true)
expect(result.rects).toHaveLength(0)
expect(result.points.map((point) => point.originalIndex)).toEqual([1, 2])
expect(result.circles.map((circle) => circle.originalIndex)).toEqual([0])
})
})
Loading