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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,4 +364,5 @@ export default function Demo() {
`InteractiveGraphics` accepts a `GraphicsObject` via the `graphics` prop.
You can optionally handle clicks on objects with `onObjectClicked`, set the
viewer height with `height`, or limit how many objects are drawn with
`objectLimit`.
`objectLimit`. The limit is applied globally after viewport, layer and step
filters, not separately per graphics object type.
109 changes: 73 additions & 36 deletions site/components/InteractiveGraphics/InteractiveGraphics.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import useResizeObserver from "@react-hook/resize-observer"
import { useCallback, useEffect, useMemo, useState } from "react"
import { SuperGrid } from "react-supergrid"
import { applyObjectLimit } from "site/utils/applyObjectLimit"
import { getGraphicsBounds } from "site/utils/getGraphicsBounds"
import { getMaxStep } from "site/utils/getMaxStep"
import { sortRectsByArea } from "site/utils/sortRectsByArea"
Expand Down Expand Up @@ -421,65 +422,101 @@ 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 filteredLinesWithoutLimit = 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 filteredInfiniteLinesWithoutLimit = useMemo(
() => filterObjects(graphics.infiniteLines, filterLayerAndStep),
[graphics.infiniteLines, filterLayerAndStep],
)
const filteredRects = useMemo(
() => sortRectsByArea(filterAndLimit(graphics.rects, filterRects)),
[graphics.rects, filterRects, objectLimit],
const filteredRectsWithoutLimit = useMemo(
() => sortRectsByArea(filterObjects(graphics.rects, filterRects)),
[graphics.rects, filterRects],
)
const filteredPolygons = useMemo(
() => filterAndLimit(graphics.polygons, filterPolygons),
[graphics.polygons, filterPolygons, objectLimit],
const filteredPolygonsWithoutLimit = useMemo(
() => filterObjects(graphics.polygons, filterPolygons),
[graphics.polygons, filterPolygons],
)
const filteredPoints = useMemo(
() => filterAndLimit(graphics.points, filterPoints),
[graphics.points, filterPoints, objectLimit],
const filteredPointsWithoutLimit = useMemo(
() => filterObjects(graphics.points, filterPoints),
[graphics.points, filterPoints],
)
const filteredCircles = useMemo(
() => filterAndLimit(graphics.circles, filterCircles),
[graphics.circles, filterCircles, objectLimit],
const filteredCirclesWithoutLimit = useMemo(
() => filterObjects(graphics.circles, filterCircles),
[graphics.circles, filterCircles],
)
const filteredTexts = useMemo(
() => filterAndLimit(graphics.texts, filterTexts),
[graphics.texts, filterTexts, objectLimit],
const filteredTextsWithoutLimit = useMemo(
() => filterObjects(graphics.texts, filterTexts),
[graphics.texts, filterTexts],
)
const filteredArrows = useMemo(
() => filterAndLimit(graphics.arrows, filterArrows),
[graphics.arrows, filterArrows, objectLimit],
const filteredArrowsWithoutLimit = 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
filteredInfiniteLinesWithoutLimit.length +
filteredLinesWithoutLimit.length +
filteredRectsWithoutLimit.length +
filteredPolygonsWithoutLimit.length +
filteredPointsWithoutLimit.length +
filteredCirclesWithoutLimit.length +
filteredTextsWithoutLimit.length +
filteredArrowsWithoutLimit.length

const limitedObjects = useMemo(
() =>
applyObjectLimit(
{
arrows: filteredArrowsWithoutLimit,
infiniteLines: filteredInfiniteLinesWithoutLimit,
lines: filteredLinesWithoutLimit,
rects: filteredRectsWithoutLimit,
polygons: filteredPolygonsWithoutLimit,
circles: filteredCirclesWithoutLimit,
texts: filteredTextsWithoutLimit,
points: filteredPointsWithoutLimit,
},
objectLimit,
),
[
filteredArrowsWithoutLimit,
filteredInfiniteLinesWithoutLimit,
filteredLinesWithoutLimit,
filteredRectsWithoutLimit,
filteredPolygonsWithoutLimit,
filteredCirclesWithoutLimit,
filteredTextsWithoutLimit,
filteredPointsWithoutLimit,
objectLimit,
],
)

const filteredArrows = limitedObjects.arrows
const filteredInfiniteLines = limitedObjects.infiniteLines
const filteredLines = limitedObjects.lines
const filteredRects = limitedObjects.rects
const filteredPolygons = limitedObjects.polygons
const filteredCircles = limitedObjects.circles
const filteredTexts = limitedObjects.texts
const filteredPoints = limitedObjects.points
const isLimitReached = !!objectLimit && totalFilteredObjects > objectLimit

return (
<div>
Expand Down
38 changes: 38 additions & 0 deletions site/utils/applyObjectLimit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
type ObjectBuckets = Record<string, readonly unknown[]>

type LimitedBuckets<TBuckets extends ObjectBuckets> = {
[TKey in keyof TBuckets]: TBuckets[TKey] extends readonly (infer TItem)[]
? TItem[]
: never
}

export function applyObjectLimit<TBuckets extends ObjectBuckets>(
buckets: TBuckets,
objectLimit?: number,
): LimitedBuckets<TBuckets> {
const bucketKeys = Object.keys(buckets) as Array<keyof TBuckets>
const limitedBuckets = {} as LimitedBuckets<TBuckets>

for (const key of bucketKeys) {
limitedBuckets[key] = [] as unknown as LimitedBuckets<TBuckets>[typeof key]
}

if (!objectLimit || objectLimit <= 0) {
for (const key of bucketKeys) {
limitedBuckets[key] = [
...buckets[key],
] as unknown as LimitedBuckets<TBuckets>[typeof key]
}
return limitedBuckets
}

const allObjects = bucketKeys.flatMap((key) =>
buckets[key].map((object) => ({ key, object })),
)

for (const { key, object } of allObjects.slice(-objectLimit)) {
;(limitedBuckets[key] as unknown[]).push(object)
}

return limitedBuckets
}
34 changes: 34 additions & 0 deletions tests/apply-object-limit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, test } from "bun:test"
import { applyObjectLimit } from "site/utils/applyObjectLimit"

describe("applyObjectLimit", () => {
test("caps objects globally across buckets", () => {
const limited = applyObjectLimit(
{
lines: [{ id: "line-1" }, { id: "line-2" }],
rects: [{ id: "rect-1" }, { id: "rect-2" }],
points: [{ id: "point-1" }, { id: "point-2" }],
},
3,
)

const renderedObjects = [
...limited.lines,
...limited.rects,
...limited.points,
].map((object) => object.id)

expect(renderedObjects).toEqual(["rect-2", "point-1", "point-2"])
expect(limited.lines).toHaveLength(0)
})

test("does not limit objects when objectLimit is omitted", () => {
const limited = applyObjectLimit({
lines: [{ id: "line-1" }],
points: [{ id: "point-1" }],
})

expect(limited.lines).toHaveLength(1)
expect(limited.points).toHaveLength(1)
})
})
Loading