diff --git a/site/components/InteractiveGraphics/InteractiveGraphics.tsx b/site/components/InteractiveGraphics/InteractiveGraphics.tsx index de014e1..066cd74 100644 --- a/site/components/InteractiveGraphics/InteractiveGraphics.tsx +++ b/site/components/InteractiveGraphics/InteractiveGraphics.tsx @@ -421,64 +421,100 @@ export const InteractiveGraphics = ({ filterLayerAndStep, }) - const filterAndLimit = ( + const filterObjects = ( 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 limitObjects = (objects: (T & { originalIndex: number })[]) => { + return objectLimit ? objects.slice(-objectLimit) : objects + } + + 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 allFilteredInfiniteLines = useMemo( + () => filterObjects(graphics.infiniteLines, filterLayerAndStep), + [graphics.infiniteLines, filterLayerAndStep], + ) + const allFilteredRects = useMemo( + () => sortRectsByArea(filterObjects(graphics.rects, filterRects)), + [graphics.rects, filterRects], + ) + const allFilteredPolygons = useMemo( + () => filterObjects(graphics.polygons, filterPolygons), + [graphics.polygons, filterPolygons], + ) + const allFilteredPoints = useMemo( + () => filterObjects(graphics.points, filterPoints), + [graphics.points, filterPoints], + ) + const allFilteredCircles = useMemo( + () => filterObjects(graphics.circles, filterCircles), + [graphics.circles, filterCircles], + ) + const allFilteredTexts = useMemo( + () => filterObjects(graphics.texts, filterTexts), + [graphics.texts, filterTexts], + ) + const allFilteredArrows = useMemo( + () => filterObjects(graphics.arrows, filterArrows), + [graphics.arrows, filterArrows], + ) + + const filteredLines = useMemo( + () => limitObjects(allFilteredLines), + [allFilteredLines, objectLimit], ) const filteredInfiniteLines = useMemo( - () => filterAndLimit(graphics.infiniteLines, filterLayerAndStep), - [graphics.infiniteLines, filterLayerAndStep, objectLimit], + () => limitObjects(allFilteredInfiniteLines), + [allFilteredInfiniteLines, objectLimit], ) const filteredRects = useMemo( - () => sortRectsByArea(filterAndLimit(graphics.rects, filterRects)), - [graphics.rects, filterRects, objectLimit], + () => limitObjects(allFilteredRects), + [allFilteredRects, objectLimit], ) const filteredPolygons = useMemo( - () => filterAndLimit(graphics.polygons, filterPolygons), - [graphics.polygons, filterPolygons, objectLimit], + () => limitObjects(allFilteredPolygons), + [allFilteredPolygons, objectLimit], ) const filteredPoints = useMemo( - () => filterAndLimit(graphics.points, filterPoints), - [graphics.points, filterPoints, objectLimit], + () => limitObjects(allFilteredPoints), + [allFilteredPoints, objectLimit], ) const filteredCircles = useMemo( - () => filterAndLimit(graphics.circles, filterCircles), - [graphics.circles, filterCircles, objectLimit], + () => limitObjects(allFilteredCircles), + [allFilteredCircles, objectLimit], ) const filteredTexts = useMemo( - () => filterAndLimit(graphics.texts, filterTexts), - [graphics.texts, filterTexts, objectLimit], + () => limitObjects(allFilteredTexts), + [allFilteredTexts, objectLimit], ) const filteredArrows = useMemo( - () => filterAndLimit(graphics.arrows, filterArrows), - [graphics.arrows, filterArrows, objectLimit], + () => limitObjects(allFilteredArrows), + [allFilteredArrows, objectLimit], ) const totalFilteredObjects = - filteredInfiniteLines.length + - filteredLines.length + - filteredRects.length + - filteredPolygons.length + - filteredPoints.length + - filteredCircles.length + - filteredTexts.length + - filteredArrows.length + allFilteredInfiniteLines.length + + allFilteredLines.length + + allFilteredRects.length + + allFilteredPolygons.length + + allFilteredPoints.length + + allFilteredCircles.length + + allFilteredTexts.length + + allFilteredArrows.length const isLimitReached = objectLimit && totalFilteredObjects > objectLimit return ( diff --git a/tests/InteractiveGraphics.objectLimit.test.tsx b/tests/InteractiveGraphics.objectLimit.test.tsx new file mode 100644 index 0000000..f344ed9 --- /dev/null +++ b/tests/InteractiveGraphics.objectLimit.test.tsx @@ -0,0 +1,79 @@ +import { beforeAll, describe, expect, test } from "bun:test" +import { act } from "react" +import { createRoot } from "react-dom/client" +import * as jsdom from "jsdom" +import { InteractiveGraphics } from "../site/components/InteractiveGraphics/InteractiveGraphics" + +declare global { + var IS_REACT_ACT_ENVIRONMENT: boolean +} + +beforeAll(() => { + const { JSDOM } = jsdom + const dom = new JSDOM("", { + url: "http://localhost/", + }) + + class ResizeObserverMock { + observe() {} + unobserve() {} + disconnect() {} + } + + global.document = dom.window.document + global.window = dom.window as unknown as Window & typeof globalThis + global.navigator = dom.window.navigator + global.localStorage = dom.window.localStorage + global.ResizeObserver = ResizeObserverMock as any + ;(global.window as any).ResizeObserver = ResizeObserverMock + ;(global.window as any).HTMLCanvasElement.prototype.getContext = () => ({ + clearRect() {}, + beginPath() {}, + moveTo() {}, + lineTo() {}, + stroke() {}, + strokeRect() {}, + fillText() {}, + measureText() { + return { width: 0 } + }, + setLineDash() {}, + save() {}, + restore() {}, + translate() {}, + scale() {}, + }) + globalThis.IS_REACT_ACT_ENVIRONMENT = true +}) + +describe("InteractiveGraphics objectLimit", () => { + test("shows limit message when filtered objects exceed objectLimit", async () => { + const container = document.createElement("div") + document.body.appendChild(container) + const root = createRoot(container) + + const graphics = { + points: [ + { x: 0, y: 0, label: "A", step: 1 }, + { x: 10, y: 10, label: "B", step: 1 }, + { x: 20, y: 20, label: "C", step: 1 }, + ], + } + + try { + await act(async () => { + root.render( + , + ) + }) + + expect(container.textContent).toContain("Display limited to 2 objects") + expect(container.textContent).toContain("Received: 3") + } finally { + await act(async () => { + root.unmount() + }) + document.body.removeChild(container) + } + }) +})