From a859f63838c098fadab6afdfa4cbc4c7447ec782 Mon Sep 17 00:00:00 2001 From: Kris McGinnes Date: Fri, 14 Aug 2026 15:09:34 -0500 Subject: [PATCH] Resolve canvas vertices, ids, and style scope in one pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three passes over the canvas vertices became one. The visible-ids atom iterated every vertex, the style-scope atom iterated them again re-testing membership, and the render hook iterated a third time re-testing it once more — two passes existed only to re-discover a decision the first had already made. `canvasVerticesAtom` returns the drawn vertices, their ids, and the styles of the types they draw from a single loop, so the style scope and the drawn set cannot disagree: a drawn vertex's `primaryType` is in `stylesByType` by construction. That also collapses the reason a missing icon used to be silent. Style data was assembled from two inputs of different totality — scalar fields from a lookup that answers for any type, the icon from a partial scope list — so a scope miss was indistinguishable from a type that genuinely has no icon. The style data now derives entirely from the scoped style list, and a type absent from it is a loud throw rather than a node quietly drawn without its icon. The memoizing resolver layer is gone with it: five exported symbols, two factories and two cache closures replaced by one hook returning a plain per-type map. Edge style data is resolved on first sight of a type inside the existing filter loop, since the drawn edge types are only known while filtering. --- .../src/core/StateProvider/graphStyles.ts | 2 +- .../StateProvider/renderedEntities.test.ts | 60 +++++++--- .../core/StateProvider/renderedEntities.ts | 108 +++++++++-------- .../StateProvider/styleDataResolvers.test.ts | 110 +++++------------- .../core/StateProvider/styleDataResolvers.ts | 97 ++++----------- .../modules/SchemaGraph/useSchemaGraphData.ts | 32 ++++- 6 files changed, 177 insertions(+), 232 deletions(-) diff --git a/packages/graph-explorer/src/core/StateProvider/graphStyles.ts b/packages/graph-explorer/src/core/StateProvider/graphStyles.ts index 6c559d5cf..ac2556c12 100644 --- a/packages/graph-explorer/src/core/StateProvider/graphStyles.ts +++ b/packages/graph-explorer/src/core/StateProvider/graphStyles.ts @@ -270,7 +270,7 @@ export function resolveEdgeStyle( /** Returns an array of vertex styles based on the known vertex types in the schema. * For the schema view, which draws every type; the canvas scopes itself to the - * types it draws via `canvasVertexStylesAtom`. Always includes an entry for + * types it draws via `canvasVerticesAtom`. Always includes an entry for * `LABELS.MISSING_TYPE` so blank nodes (assigned that synthetic type at runtime) * are styled rather than skipped. */ diff --git a/packages/graph-explorer/src/core/StateProvider/renderedEntities.test.ts b/packages/graph-explorer/src/core/StateProvider/renderedEntities.test.ts index 9221f32cb..facb40d62 100644 --- a/packages/graph-explorer/src/core/StateProvider/renderedEntities.test.ts +++ b/packages/graph-explorer/src/core/StateProvider/renderedEntities.test.ts @@ -19,7 +19,7 @@ import { } from "@/utils/testing"; import { - canvasVertexStylesAtom, + canvasVerticesAtom, createRenderedEdgeId, createRenderedVertexId, getEdgeIdFromRenderedEdgeId, @@ -27,7 +27,6 @@ import { type RenderedEdgeId, type RenderedVertexId, useRenderedEntities, - visibleVertexIdsAtom, } from "./renderedEntities"; describe("createRenderedVertexId", () => { @@ -96,7 +95,7 @@ describe("getEdgeIdFromRenderedEdgeId", () => { }); }); -describe("visibleVertexIdsAtom", () => { +describe("canvasVerticesAtom", () => { it("should exclude vertices filtered by ID and by type", () => { const dbState = new DbState(); const kept = createTestableVertex(); @@ -112,12 +111,12 @@ describe("visibleVertexIdsAtom", () => { const store = createStore(); dbState.applyTo(store); - expect([...store.get(visibleVertexIdsAtom)]).toStrictEqual([kept.id]); + const { vertices, ids } = store.get(canvasVerticesAtom); + expect(vertices.map(v => v.id)).toStrictEqual([kept.id]); + expect([...ids]).toStrictEqual([kept.id]); }); -}); -describe("canvasVertexStylesAtom", () => { - // The point of the atom: a schema can carry far more types than the canvas + // The point of the scoping: a schema can carry far more types than the canvas // draws, and resolving an icon for every one of them dominated render cost. it("should cover only the types drawn on the canvas", () => { const dbState = new DbState(); @@ -130,9 +129,9 @@ describe("canvasVertexStylesAtom", () => { const store = createStore(); dbState.applyTo(store); - expect(store.get(canvasVertexStylesAtom).map(s => s.type)).toStrictEqual([ - createVertexType(onCanvas.types[0]), - ]); + expect([ + ...store.get(canvasVerticesAtom).stylesByType.keys(), + ]).toStrictEqual([createVertexType(onCanvas.types[0])]); }); it("should exclude the types of filtered-out vertices", () => { @@ -146,9 +145,9 @@ describe("canvasVertexStylesAtom", () => { const store = createStore(); dbState.applyTo(store); - expect(store.get(canvasVertexStylesAtom).map(s => s.type)).toStrictEqual([ - createVertexType(kept.types[0]), - ]); + expect([ + ...store.get(canvasVerticesAtom).stylesByType.keys(), + ]).toStrictEqual([createVertexType(kept.types[0])]); }); // `useAllVertexStyles` states this guarantee explicitly; here it has to hold @@ -163,9 +162,9 @@ describe("canvasVertexStylesAtom", () => { const store = createStore(); dbState.applyTo(store); - expect(store.get(canvasVertexStylesAtom).map(s => s.type)).toStrictEqual([ - LABELS.MISSING_TYPE, - ]); + expect([ + ...store.get(canvasVerticesAtom).stylesByType.keys(), + ]).toStrictEqual([LABELS.MISSING_TYPE]); }); it("should list a shared type once", () => { @@ -178,7 +177,34 @@ describe("canvasVertexStylesAtom", () => { const store = createStore(); dbState.applyTo(store); - expect(store.get(canvasVertexStylesAtom)).toHaveLength(1); + expect(store.get(canvasVerticesAtom).stylesByType.size).toBe(1); + }); + + // The invariant `useRenderedVertices` throws on: every drawn vertex has a + // style, because the same loop produced both. + it("should cover the primary type of every vertex it returns", () => { + const dbState = new DbState(); + const first = createTestableVertex(); + const shared = createTestableVertex().with({ types: first.types }); + const filtered = createTestableVertex(); + dbState.addTestableVertexToGraph(first); + dbState.addTestableVertexToGraph(shared); + dbState.addTestableVertexToGraph(filtered); + dbState.addVertexToGraph( + createVertex({ id: "blank", isBlankNode: true, types: [] }), + ); + dbState.filterVertex(filtered.id); + + const store = createStore(); + dbState.applyTo(store); + + const { vertices, stylesByType } = store.get(canvasVerticesAtom); + expect(vertices).not.toHaveLength(0); + for (const vertex of vertices) { + expect(stylesByType.get(vertex.primaryType)?.type).toBe( + vertex.primaryType, + ); + } }); }); diff --git a/packages/graph-explorer/src/core/StateProvider/renderedEntities.ts b/packages/graph-explorer/src/core/StateProvider/renderedEntities.ts index d76402a19..aefb86d1d 100644 --- a/packages/graph-explorer/src/core/StateProvider/renderedEntities.ts +++ b/packages/graph-explorer/src/core/StateProvider/renderedEntities.ts @@ -6,14 +6,15 @@ import { type DisplayEdge, type DisplayVertex, displayVerticesInCanvasSelector, + type EdgeType, edgesFilteredIdsAtom, + edgeStyleAtom, edgesTypesFilteredAtom, type EntityRawId, nodesFilteredIdsAtom, nodesTypesFilteredAtom, useAllNeighbors, useDisplayEdgesInCanvas, - useDisplayVerticesInCanvas, type VertexId, type VertexStyle, vertexStyleAtom, @@ -21,12 +22,13 @@ import { } from "@/core"; import type { EdgeId } from "../entities/edge"; -import type { EdgeStyleData, VertexStyleData } from "./graphElementStyleData"; import { - useEdgeStyleDataResolver, - useVertexStyleDataResolver, -} from "./styleDataResolvers"; + type EdgeStyleData, + edgeStyleData, + type VertexStyleData, +} from "./graphElementStyleData"; +import { useVertexStyleDataByType } from "./styleDataResolvers"; /** A string representation of a vertex ID that encodes the original type. Cytoscape requires IDs to be strings. */ export type RenderedVertexId = Branded; @@ -41,84 +43,69 @@ export type RenderedVertex = ReturnType; export type RenderedEdge = ReturnType; /** - * The IDs of the canvas vertices that survive filtering. + * The canvas vertices that survive filtering, in canvas insertion order, plus + * their IDs for membership tests and the styles of the types they draw. + * + * One loop, so the style scope and the drawn set cannot disagree: a drawn + * vertex's `primaryType` is in `stylesByType` by construction. The schema can + * carry tens of thousands of vertex types while the canvas shows a handful, and + * resolving a style plus an icon for every type in the schema on every render is + * the dominant render cost otherwise. The schema view, which genuinely draws + * every type, uses `useAllVertexStyles` instead. * * An atom rather than a hook so both the vertex and edge pipelines share one * computation per store — as a hook it ran once per call site. * * Note this still recomputes when a vertex style changes, because * `displayVerticesInCanvasSelector` resolves display labels through - * `vertexStyleByTypeAtom`. Only `id` and `types` are actually needed, so - * sourcing the predicate from `nodesAtom` would decouple it. + * `vertexStyleByTypeAtom`. */ -export const visibleVertexIdsAtom = atom(get => { +export const canvasVerticesAtom = atom(get => { const filteredIds = get(nodesFilteredIdsAtom); const filteredTypes = get(nodesTypesFilteredAtom); - const displayVerticesInGraph = get(displayVerticesInCanvasSelector); + const displayVertices = get(displayVerticesInCanvasSelector); + const styles = get(vertexStyleAtom); - const result = new Set(); + const vertices: DisplayVertex[] = []; + const ids = new Set(); + const stylesByType = new Map(); - for (const vertex of displayVerticesInGraph.values()) { + for (const vertex of displayVertices.values()) { // Filters the nodes added to the graph by: // - Individual nodes hidden using the table view // - Vertex types unselected in the filter sidebar if (filteredIds.has(vertex.id)) continue; if (vertex.types.some(type => filteredTypes.has(type))) continue; - result.add(vertex.id); - } - - return result; -}); - -/** - * Vertex styles for only the types drawn on the canvas. - * - * The schema can carry tens of thousands of vertex types while the canvas shows - * a handful, and resolving a style plus an icon for every type in the schema on - * every render is the dominant render cost otherwise. The schema view, which - * genuinely draws every type, uses `useAllVertexStyles` instead. - */ -export const canvasVertexStylesAtom = atom(get => { - const styles = get(vertexStyleAtom); - const displayVerticesInGraph = get(displayVerticesInCanvasSelector); - const visibleIds = get(visibleVertexIdsAtom); - - const types = new Set(); - for (const vertex of displayVerticesInGraph.values()) { - if (visibleIds.has(vertex.id)) { - types.add(vertex.primaryType); + vertices.push(vertex); + ids.add(vertex.id); + if (!stylesByType.has(vertex.primaryType)) { + stylesByType.set(vertex.primaryType, styles.get(vertex.primaryType)); } } - const result: VertexStyle[] = []; - for (const type of types) { - result.push(styles.get(type)); - } - return result; + return { vertices, ids, stylesByType }; }); /** Returns the filtered array of `RenderedVertex` instances for use by Cytoscape. */ export function useRenderedVertices(): RenderedVertex[] { - const displayVerticesInGraph = useDisplayVerticesInCanvas(); - const visibleIds = useAtomValue(visibleVertexIdsAtom); + const { vertices, stylesByType } = useAtomValue(canvasVerticesAtom); const neighborCounts = useAllNeighbors(); - const canvasVertexStyles = useAtomValue(canvasVertexStylesAtom); - const resolveStyleData = useVertexStyleDataResolver(canvasVertexStyles); + const styleDataByType = useVertexStyleDataByType(stylesByType.values()); const result: RenderedVertex[] = []; - for (const vertex of displayVerticesInGraph.values()) { - if (!visibleIds.has(vertex.id)) continue; + for (const vertex of vertices) { + const styleData = styleDataByType.get(vertex.primaryType); + // `canvasVerticesAtom` scopes the styles to the types it drew. + if (styleData === undefined) { + throw new Error( + `No style data resolved for drawn vertex type "${vertex.primaryType}"`, + ); + } const neighborCount = neighborCounts.get(vertex.id)?.unfetched ?? 0; - result.push( - createRenderedVertex( - vertex, - neighborCount, - resolveStyleData(vertex.primaryType), - ), - ); + result.push(createRenderedVertex(vertex, neighborCount, styleData)); } return result; @@ -129,9 +116,12 @@ export function useRenderedEdges(): RenderedEdge[] { const edges = useDisplayEdgesInCanvas(); const filteredEdgeIds = useAtomValue(edgesFilteredIdsAtom); const filteredEdgeTypes = useAtomValue(edgesTypesFilteredAtom); - const visibleVertexIds = useAtomValue(visibleVertexIdsAtom); - const resolveStyleData = useEdgeStyleDataResolver(); + const { ids: visibleVertexIds } = useAtomValue(canvasVerticesAtom); + const styles = useAtomValue(edgeStyleAtom); + // The drawn edge types are only known while filtering, so style data is + // resolved on first sight of a type — one `Color` parse per type, not per edge. + const styleDataByType = new Map(); const result: RenderedEdge[] = []; for (const edge of edges.values()) { @@ -144,7 +134,13 @@ export function useRenderedEdges(): RenderedEdge[] { if (!visibleVertexIds.has(edge.sourceId)) continue; if (!visibleVertexIds.has(edge.targetId)) continue; - result.push(createRenderedEdge(edge, resolveStyleData(edge.type))); + let styleData = styleDataByType.get(edge.type); + if (styleData === undefined) { + styleData = edgeStyleData(styles.get(edge.type)); + styleDataByType.set(edge.type, styleData); + } + + result.push(createRenderedEdge(edge, styleData)); } return result; diff --git a/packages/graph-explorer/src/core/StateProvider/styleDataResolvers.test.ts b/packages/graph-explorer/src/core/StateProvider/styleDataResolvers.test.ts index 64b7476b2..5e0fda7ad 100644 --- a/packages/graph-explorer/src/core/StateProvider/styleDataResolvers.test.ts +++ b/packages/graph-explorer/src/core/StateProvider/styleDataResolvers.test.ts @@ -1,43 +1,37 @@ // @vitest-environment happy-dom import { act, waitFor } from "@testing-library/react"; +import { useAtomValue } from "jotai"; import type { AppStore } from "@/core"; -import { createEdgeType, createVertexType } from "@/core/entities"; +import { createVertexType, type VertexType } from "@/core/entities"; import { DbState, renderHookWithJotai } from "@/utils/testing"; -import { userEdgeStylesAtom, userVertexStylesAtom } from "./storageAtoms"; -import { - useEdgeStyleDataResolver, - useVertexStyleDataResolver, -} from "./styleDataResolvers"; +import { vertexStyleAtom } from "./graphStyles"; +import { userVertexStylesAtom } from "./storageAtoms"; +import { useVertexStyleDataByType } from "./styleDataResolvers"; -describe("useVertexStyleDataResolver", () => { - it("should return the same object for repeated lookups of a type", () => { - const type = createVertexType("Person"); - const { result } = renderHookWithJotai(() => - useVertexStyleDataResolver([]), - ); - - expect(result.current(type)).toBe(result.current(type)); - }); +/** Mirrors a caller: resolve the styles for a scope, then build the style data. */ +function useStyleDataForTypes(types: VertexType[]) { + const styles = useAtomValue(vertexStyleAtom); + return useVertexStyleDataByType(types.map(type => styles.get(type))); +} - // The cache is keyed only by type, so a style edit has to replace the whole - // resolver. If it doesn't, the canvas keeps the old colors. - it("should reflect a style edited after the first lookup", async () => { +describe("useVertexStyleDataByType", () => { + it("should reflect a style edited after the first render", async () => { const type = createVertexType("Person"); const dbState = new DbState(); dbState.addVertexStyle(type, { color: "#111111" }); let store!: AppStore; const { result } = renderHookWithJotai( - () => useVertexStyleDataResolver([]), + () => useStyleDataForTypes([type]), s => { store = s; dbState.applyTo(s); }, ); - expect(result.current(type).ge_color).toBe("#111111"); + expect(result.current.get(type)?.ge_color).toBe("#111111"); act(() => store.set(userVertexStylesAtom, prev => @@ -46,7 +40,7 @@ describe("useVertexStyleDataResolver", () => { ); await waitFor(() => { - expect(result.current(type).ge_color).toBe("#222222"); + expect(result.current.get(type)?.ge_color).toBe("#222222"); }); }); @@ -56,72 +50,32 @@ describe("useVertexStyleDataResolver", () => { dbState.addVertexStyle(createVertexType("City"), { color: "#222222" }); const { result } = renderHookWithJotai( - () => useVertexStyleDataResolver([]), + () => + useStyleDataForTypes([ + createVertexType("Person"), + createVertexType("City"), + ]), store => dbState.applyTo(store), ); - expect(result.current(createVertexType("Person")).ge_color).toBe("#111111"); - expect(result.current(createVertexType("City")).ge_color).toBe("#222222"); - }); - - // The canvas passes only the types it draws, so a type outside that scope must - // still resolve — without an icon. - it("should resolve a type outside the icon scope without an icon", () => { - const { result } = renderHookWithJotai(() => - useVertexStyleDataResolver([]), - ); - - expect(result.current(createVertexType("Person")).ge_iconUrl).toBe("none"); - }); -}); - -describe("useEdgeStyleDataResolver", () => { - it("should reflect a style edited after the first lookup", async () => { - const type = createEdgeType("route"); - const dbState = new DbState(); - dbState.addEdgeStyle(type, { lineColor: "#111111" }); - - let store!: AppStore; - const { result } = renderHookWithJotai( - () => useEdgeStyleDataResolver(), - s => { - store = s; - dbState.applyTo(s); - }, + expect(result.current.get(createVertexType("Person"))?.ge_color).toBe( + "#111111", ); - expect(result.current(type).ge_lineColor).toBe("#111111"); - - act(() => - store.set(userEdgeStylesAtom, prev => - new Map(prev).set(type, { type, lineColor: "#222222" }), - ), + expect(result.current.get(createVertexType("City"))?.ge_color).toBe( + "#222222", ); - - await waitFor(() => { - expect(result.current(type).ge_lineColor).toBe("#222222"); - }); }); - it("should return the same object for repeated lookups of a type", () => { - const type = createEdgeType("route"); - const { result } = renderHookWithJotai(() => useEdgeStyleDataResolver()); - - expect(result.current(type)).toBe(result.current(type)); - }); - - it("should resolve distinct style data per type", () => { - const dbState = new DbState(); - dbState.addEdgeStyle(createEdgeType("route"), { lineColor: "#111111" }); - dbState.addEdgeStyle(createEdgeType("owns"), { lineColor: "#222222" }); - - const { result } = renderHookWithJotai( - () => useEdgeStyleDataResolver(), - store => dbState.applyTo(store), + // Scalar fields and icon come from the same list, so a type outside the scope + // is absent rather than present-but-icon-less. + it("should omit a type outside the given styles", () => { + const { result } = renderHookWithJotai(() => + useStyleDataForTypes([createVertexType("Person")]), ); - expect(result.current(createEdgeType("route")).ge_lineColor).toBe( - "#111111", + expect(result.current.has(createVertexType("City"))).toBe(false); + expect(result.current.get(createVertexType("Person"))?.ge_iconUrl).toBe( + "none", ); - expect(result.current(createEdgeType("owns")).ge_lineColor).toBe("#222222"); }); }); diff --git a/packages/graph-explorer/src/core/StateProvider/styleDataResolvers.ts b/packages/graph-explorer/src/core/StateProvider/styleDataResolvers.ts index 4a60444ad..cfa7c57c9 100644 --- a/packages/graph-explorer/src/core/StateProvider/styleDataResolvers.ts +++ b/packages/graph-explorer/src/core/StateProvider/styleDataResolvers.ts @@ -1,82 +1,31 @@ -import { useAtomValue } from "jotai"; - import { useBackgroundImageMap } from "@/core/icons"; -import type { EdgeType, VertexType } from "../entities"; +import type { VertexType } from "../entities"; +import type { VertexStyle } from "./graphStyles"; -import { - type EdgeStyleData, - edgeStyleData, - type VertexStyleData, - vertexStyleData, -} from "./graphElementStyleData"; -import { - type EdgeStyleLookup, - edgeStyleAtom, - type VertexStyle, - type VertexStyleLookup, - vertexStyleAtom, -} from "./graphStyles"; +import { type VertexStyleData, vertexStyleData } from "./graphElementStyleData"; /** - * Style data varies only by type, so within one render pass the graph surfaces - * resolve it once per type rather than once per element — N nodes of a type cost - * one `vertexStyleData` call. + * Style data per vertex type. Both the scalar fields and the icon come from the + * same style list, so a type in the result always has both. * - * The cache is keyed only by type; the styles and icons it was built from are - * pinned by resolver identity alone. It is discarded whenever those inputs - * change identity, which includes every node add or remove, so treat it as a - * per-render dedupe and not a cross-render cache. - */ - -/** Resolves the cytoscape data-mapper fields for a vertex type, memoized per type. */ -export type VertexStyleDataResolver = (type: VertexType) => VertexStyleData; - -/** Resolves the cytoscape data-mapper fields for an edge type, memoized per type. */ -export type EdgeStyleDataResolver = (type: EdgeType) => EdgeStyleData; - -/** - * @param iconStyles The styles whose icons should be resolved — the caller's - * scope, since the canvas needs only the types it draws while the schema view - * needs every type. A type outside this set resolves without an icon. + * Resolving per type rather than per element is what keeps the cost bounded by + * the types drawn: N nodes of a type cost one `vertexStyleData` call, and the + * caller decides the scope — the canvas passes the handful of types it draws, + * the schema view every type. */ -export function useVertexStyleDataResolver( - iconStyles: VertexStyle[], -): VertexStyleDataResolver { - const styles = useAtomValue(vertexStyleAtom); - const backgroundImages = useBackgroundImageMap(iconStyles); - return createVertexStyleDataResolver(styles, backgroundImages); -} - -export function useEdgeStyleDataResolver(): EdgeStyleDataResolver { - return createEdgeStyleDataResolver(useAtomValue(edgeStyleAtom)); -} - -function createVertexStyleDataResolver( - styles: VertexStyleLookup, - backgroundImages: Map, -): VertexStyleDataResolver { - const cache = new Map(); - return type => { - let data = cache.get(type); - if (data === undefined) { - data = vertexStyleData(styles.get(type), backgroundImages.get(type)); - cache.set(type, data); - } - return data; - }; -} - -function createEdgeStyleDataResolver( - styles: EdgeStyleLookup, -): EdgeStyleDataResolver { - const cache = new Map(); - return type => { - let data = cache.get(type); - if (data === undefined) { - data = edgeStyleData(styles.get(type)); - cache.set(type, data); - } - return data; - }; +export function useVertexStyleDataByType( + styles: Iterable, +): Map { + const styleList = [...styles]; + const backgroundImages = useBackgroundImageMap(styleList); + + const result = new Map(); + for (const style of styleList) { + result.set( + style.type, + vertexStyleData(style, backgroundImages.get(style.type)), + ); + } + return result; } diff --git a/packages/graph-explorer/src/modules/SchemaGraph/useSchemaGraphData.ts b/packages/graph-explorer/src/modules/SchemaGraph/useSchemaGraphData.ts index 1ce3cd675..63105143b 100644 --- a/packages/graph-explorer/src/modules/SchemaGraph/useSchemaGraphData.ts +++ b/packages/graph-explorer/src/modules/SchemaGraph/useSchemaGraphData.ts @@ -1,16 +1,19 @@ +import { useAtomValue } from "jotai"; + import type { GraphEdge, GraphNode } from "@/components/Graph"; import { createEdgeConnectionId, type EdgeConnectionId, type EdgeStyleData, + edgeStyleAtom, + edgeStyleData, type EdgeType, useActiveSchema, useAllVertexStyles, useDisplayEdgeTypeConfigs, useDisplayVertexTypeConfigs, - useEdgeStyleDataResolver, - useVertexStyleDataResolver, + useVertexStyleDataByType, type VertexStyleData, type VertexType, } from "@/core"; @@ -50,17 +53,25 @@ export function useSchemaGraphData() { function useSchemaGraphNodes(): SchemaGraphNode[] { const vtConfigs = useDisplayVertexTypeConfigs(); // The schema view draws every type, so every type's icon is in scope here. - const resolveStyleData = useVertexStyleDataResolver(useAllVertexStyles()); + const styleDataByType = useVertexStyleDataByType(useAllVertexStyles()); const nodes: SchemaGraphNode[] = []; for (const config of vtConfigs.values()) { + const styleData = styleDataByType.get(config.type); + // Both the configs and the styles come from the active schema's vertices. + if (styleData === undefined) { + throw new Error( + `No style data resolved for schema vertex type "${config.type}"`, + ); + } + nodes.push({ data: { id: config.type, type: config.type, displayLabel: config.displayLabel, - ...resolveStyleData(config.type), + ...styleData, }, }); } @@ -75,8 +86,11 @@ function useSchemaGraphEdges( const schema = useActiveSchema(); const edgeConnections = schema.edgeConnections ?? []; const etConfigs = useDisplayEdgeTypeConfigs(); - const resolveStyleData = useEdgeStyleDataResolver(); + const styles = useAtomValue(edgeStyleAtom); + // Many connections share an edge type, so style data is resolved on first + // sight of a type — one `Color` parse per type, not per connection. + const styleDataByType = new Map(); const edges: SchemaGraphEdge[] = []; for (const connection of edgeConnections) { @@ -87,6 +101,12 @@ function useSchemaGraphEdges( const edgeConfig = etConfigs.get(connection.edgeType); const displayLabel = edgeConfig?.displayLabel ?? connection.edgeType; + let styleData = styleDataByType.get(connection.edgeType); + if (styleData === undefined) { + styleData = edgeStyleData(styles.get(connection.edgeType)); + styleDataByType.set(connection.edgeType, styleData); + } + edges.push({ data: { id: createEdgeConnectionId(connection), @@ -94,7 +114,7 @@ function useSchemaGraphEdges( target: connection.targetVertexType, type: connection.edgeType, displayLabel, - ...resolveStyleData(connection.edgeType), + ...styleData, }, }); }