Cosmos.gl renderer support - #400
Conversation
|
@InfinityMod - This is awesome! Give me a couple days to review and get this merged. Any chance this also supports 3d? |
There was a problem hiding this comment.
Pull request overview
Adds an optional cosmos.gl-based renderer to GraphCanvas to improve rendering performance for very large 2D graphs, while keeping the existing Three.js renderer as the default.
Changes:
- Introduces
renderEngine="cosmos"with a newCosmosGraphCanvasimplementation, config (cosmosConfig), and dedicated ref contract (CosmosGraphCanvasRef). - Refactors node/edge selection styling logic into shared
getNodeRenderStyle/getEdgeRenderStyleutilities. - Updates documentation (README) and adds a large-graph Storybook demo plus Vitest coverage for cosmos mounting and node-lasso behavior.
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| stories/demos/LargeGraph.story.tsx | Adds a large graph demo and runtime renderer switch (Three.js vs cosmos). |
| src/utils/renderStyles.ts | Centralizes selection/active color + opacity rules for nodes/edges. |
| src/utils/index.ts | Re-exports the new renderStyles utilities. |
| src/symbols/Node.tsx | Uses shared render style helper instead of inline selection styling logic. |
| src/symbols/Edge.tsx | Uses shared render style helper and forwards computed color to line rendering. |
| src/GraphCanvas/index.ts | Exposes new cosmos-related types and ref contracts from the GraphCanvas entrypoint. |
| src/GraphCanvas/cosmos.ts | Adds cosmos graph preparation, buffer building, and config helpers. |
| src/GraphCanvas/GraphCanvas.tsx | Adds renderEngine + cosmosConfig, splits Three vs cosmos refs/types, and dispatches to the correct renderer. |
| src/GraphCanvas/GraphCanvas.test.ts | Adds Vitest coverage for cosmos renderer ref contract and node lasso selection. |
| src/GraphCanvas/CosmosLabels.tsx | Implements DOM label overlay for cosmos renderer with throttled updates. |
| src/GraphCanvas/CosmosGraphCanvas.tsx | Implements the cosmos renderer, interactions, lasso selection, and ref API. |
| package.json | Adds @cosmos.gl/graph dependency. |
| package-lock.json | Locks new dependency tree and bumps package version metadata. |
| THIRD_PARTY_NOTICES.md | Adds license notice for the new dependency. |
| README.md | Documents the new optional cosmos renderer usage and ref contract. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| onLinkMouseOver: index => { | ||
| const edge = getEdgeByIndex(index); | ||
| if (!edge) return; | ||
|
|
||
| hoveredEdgeIdRef.current = edge.id; | ||
| setHoveredEdgeId(edge.id); | ||
| onEdgePointerOver?.(edge, undefined as never); |
There was a problem hiding this comment.
onLinkMouseOver currently calls onEdgePointerOver with undefined for the event argument. Since onEdgePointerOver is part of the public GraphCanvas API and callers may rely on the event object, this will cause runtime errors when they access event properties. Consider accepting the cosmos event argument (if available) and forwarding it, or adjusting the cosmos renderer callback contract to make the event optional/nullable and documenting that for renderEngine="cosmos".
| onLinkMouseOver: index => { | |
| const edge = getEdgeByIndex(index); | |
| if (!edge) return; | |
| hoveredEdgeIdRef.current = edge.id; | |
| setHoveredEdgeId(edge.id); | |
| onEdgePointerOver?.(edge, undefined as never); | |
| onLinkMouseOver: (index, event) => { | |
| const edge = getEdgeByIndex(index); | |
| if (!edge) return; | |
| hoveredEdgeIdRef.current = edge.id; | |
| setHoveredEdgeId(edge.id); | |
| onEdgePointerOver?.(edge, event as never); |
| onNodeContextMenu(node, { | ||
| ...getNodeContextProps(node), | ||
| onCollapse: () => undefined |
There was a problem hiding this comment.
In the cosmos renderer, onNodeContextMenu is given an onCollapse callback that is a no-op. This is misleading because canCollapse/isCollapsed are provided and consumers may call onCollapse() expecting it to update collapsed state. Either implement onCollapse for cosmos (maintain internal collapsed state like the Three.js path) or omit/disable collapse in the context menu props to avoid a broken action.
| onNodeContextMenu(node, { | |
| ...getNodeContextProps(node), | |
| onCollapse: () => undefined | |
| const nodeContextProps = getNodeContextProps(node); | |
| onNodeContextMenu(node, { | |
| ...nodeContextProps, | |
| canCollapse: false, | |
| isCollapsed: false |
I can look inside, cosmos.gl should also have the capabilities for 3d rendering. |
SerhiiTsybulskyi
left a comment
There was a problem hiding this comment.
Overall looks good, left a few comments with suggestions 👍
Main points:
CosmosGraphControls— instead of sevenBaseGraphCanvasRef['…']lookups, better to usePick<BaseGraphCanvasRef, …>and addfitView/getCosmosGraphon top. Single source of truth, and it reads as "base minus a few methods plus two new ones."fitViewByPointIndices(indices, duration, 0.1)— the0.1is the padding (10%), and it's already the default in cosmos.gl, so the third arg can be dropped. Or, if we want to give consumers control over it — expose it viaopts.paddingonfitNodesInView/centerGraph.- The file is pretty large (~600 lines, 8
useEffects) — would be good to add a one-line comment above eachuseEffectdescribing its responsibility, just so it's clear at a glance what each one owns, some of them could be extracted into dedicated hooks (useCosmosGraphData,useCosmosLasso).
The rest are minor notes.
| }) | ||
| }); | ||
|
|
||
| export const getEdgeRenderStyle = ({ |
There was a problem hiding this comment.
Please add JSDoc for each functions and please cover that utils by tests
| /** | ||
| * Fit the given node ids in view. | ||
| */ | ||
| fitNodesInView: BaseGraphCanvasRef['fitNodesInView']; |
There was a problem hiding this comment.
is there any reason to do not use extends?
For example
export interface CosmosGraphControls
extends Pick<
BaseGraphCanvasRef,
| 'centerGraph'
| 'fitNodesInView'
| 'freeze'
| 'unFreeze'
| 'zoomIn'
| 'zoomOut'
| 'resetControls'
> {
fitView: (duration?: number, padding?: number) => void;
getGraph: () => CosmosGraph | undefined;
}
| /** | ||
| * Get the cosmos.gl graph renderer. | ||
| */ | ||
| getCosmosGraph: () => CosmosGraph | undefined; |
There was a problem hiding this comment.
To align with existing interfaces
| getCosmosGraph: () => CosmosGraph | undefined; | |
| getGraph: () => CosmosGraph | undefined; |
| /** | ||
| * Get the cosmos.gl graph renderer. | ||
| */ | ||
| getCosmosGraph: () => CosmosGraph | undefined; |
There was a problem hiding this comment.
| getCosmosGraph: () => CosmosGraph | undefined; | |
| getGraph: () => CosmosGraph | undefined; |
|
|
||
| const LABEL_MARGIN = 24; | ||
|
|
||
| const areLabelsEqual = (a: CosmosLabel[], b: CosmosLabel[]) => { |
There was a problem hiding this comment.
I would prefer to move all utils function into separate util file and here keep only component
please also add JSDoc for utils functions and cover by tests
| activeIds: Set<string>; | ||
| containerRef: React.RefObject<HTMLDivElement | null>; | ||
| defaultNodeSize: number; | ||
| graphRef: React.RefObject<CosmosGraph | null>; |
There was a problem hiding this comment.
| graphRef: React.RefObject<CosmosGraph | null>; | |
| graphRef: RefObject<CosmosGraph | null>; |
| updateInterval | ||
| }: { | ||
| activeIds: Set<string>; | ||
| containerRef: React.RefObject<HTMLDivElement | null>; |
There was a problem hiding this comment.
| containerRef: React.RefObject<HTMLDivElement | null>; | |
| containerRef: RefObject<HTMLDivElement | null>; |
| const graph = cosmosRef.current; | ||
| if (!graph) return; | ||
|
|
||
| const duration = opts?.animated === false ? 0 : 250; |
There was a problem hiding this comment.
would be nice to have ability to customise this duration (250ms) via props
| } else if (indices.length === 1) { | ||
| graph.zoomToPointByIndex(indices[0], duration); | ||
| } else { | ||
| graph.fitViewByPointIndices(indices, duration, 0.1); |
There was a problem hiding this comment.
would be nice to have ability to customise this fractional value: 0.1 via props
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, []); | ||
|
|
||
| useEffect(() => { |
There was a problem hiding this comment.
Could we add a short // … line above each useEffect describing its responsibility? Just enough that a reader can locate the right one without reading the body.
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
As already mentioned in recent bug reports (#113), the framework's rendering performance for large graph networks is laggy and therefore not usable for graphs with multiple thousand nodes/edges. This stems from the three.js rendering engine, which doesn't use native WebGL rendering. Therefore, this feature introduces the implementation of the cosmos.gl renderer, which tends to be very fast.
What is the current behavior?
Slow rendering updates on large graphs cause navigation to become stuck and significant delays when interacting with the graph, making it unmanageable.
What is the new behavior?
Fluent and fast interaction with GPU acceleration.
One video tells more than 1000 words:
Reagraph_Rendering_Fix.-.SD.480p.mp4
Does this PR introduce a breaking change?
The PR keeps the existing Three.js renderer as the default, so the current
GraphCanvasusage remains unchanged.The breaking consideration applies when consumers opt into the new
renderEngine="cosmos"renderer. The cosmos renderer should use its own ref contract,CosmosGraphCanvasRef, instead of the existingGraphCanvasRef. It exposesgetControls(), but this returns a cosmos-specific controls adapter, not direct Three.js camera controls.Migration path:
CosmosGraphCanvasRefwhen rendering withrenderEngine="cosmos".getControls()adapter for shared actions such as zooming, fitting, centering, freezing, and unfreezing.lassoType="node"andlassoType="all".Other information
A demo is already included inside the storybook, with a very large graph, so you can try it on your own.