import * as React from 'react' import { createRoot } from 'react-dom/client' import { createGraphStore } from '@graphora/core' import { createCircularLayout, createForceLayout, createGridLayout, createHierarchicalLayout } from '@graphora/layouts' import { Graph, type GraphRef } from '@graphora/react' import { CanvasGraphRenderer, panBy, zoomAt } from '@graphora/renderer' import { sourceGraph, kinds, colors, type Kind, type ComponentData, type ConnectionData } from './data' import './style.css' import { createDirectedPathHighlight, createDirectNeighborhoodHighlight, emptyExplorerHighlight } from './highlighting' const theme = { backgroundColor: '#f4f4f1', node: { radius: 17, stroke: '#ffffff', strokeWidth: 2 }, edge: { stroke: '#93938c', strokeWidth: 1.7, arrow: { end: true, size: 8 } }, label: { fill: '#24241f', fontSize: 12, fontWeight: '600' } } const interactionStyles = { hover: { node: { stroke: '#ad5700', strokeWidth: 4 }, edge: { stroke: '#ad5700', strokeWidth: 4 } }, selection: { node: { stroke: '#ad5700', strokeWidth: 5 }, edge: { stroke: '#312c85', strokeWidth: 4 } } } const fitOptions = { padding: 65 } const rendererFactory = () => new CanvasGraphRenderer({ labels: { mode: 'automatic', minZoom: 0.65, edgeMinZoom: 1.35 } }) type Selected = { kind: 'node' | 'edge'; id: string } | null const initialPath = { source: 'storefront', target: 'postgres' } function App(): React.ReactElement { const api = React.useRef | null>(null) const [query, setQuery] = React.useState('') const [types, setTypes] = React.useState(kinds) const [layoutName, setLayoutName] = React.useState('force') const [selected, setSelected] = React.useState(null) const [error, setError] = React.useState('') const [zoom, setZoom] = React.useState(1) const [pathEndpoints, setPathEndpoints] = React.useState(initialPath) const [highlightResult, setHighlightResult] = React.useState( emptyExplorerHighlight() ) const layout = React.useMemo( () => layoutName === 'circular' ? createCircularLayout({ radius: 220 }) : layoutName === 'grid' ? createGridLayout({ columns: 4, cellWidth: 155, cellHeight: 120 }) : layoutName === 'hierarchical' ? createHierarchicalLayout({ direction: 'LR', rankSpacing: 120, nodeSpacing: 75 }) : createForceLayout({ iterations: 180, linkDistance: 125, collisionRadius: 45, seed: 7 }), [layoutName] ) const graph = React.useMemo(() => { const nodes = sourceGraph.nodes.filter((node) => types.includes(node.data!.kind) ) const ids = new Set(nodes.map((node) => node.id)) return { nodes, edges: sourceGraph.edges!.filter( (edge) => ids.has(edge.source) && ids.has(edge.target) ) } }, [types]) const selectedNode = selected?.kind === 'node' ? graph.nodes.find((node) => node.id === selected.id) : undefined const selectedEdge = selected?.kind === 'edge' ? graph.edges.find((edge) => edge.id === selected.id) : undefined const results = graph.nodes.filter((node) => `${node.label} ${node.data!.description}` .toLowerCase() .includes(query.trim().toLowerCase()) ) const label = (id: string | number) => sourceGraph.nodes.find((node) => node.id === id)?.label ?? String(id) const selection = React.useMemo( () => ({ selectedNodeIds: selectedNode ? [selectedNode.id] : [], selectedEdgeIds: selectedEdge ? [selectedEdge.id!] : [] }), [selectedNode, selectedEdge] ) React.useEffect(() => { if (selected && !selectedNode && !selectedEdge) setSelected(null) }, [selected, selectedNode, selectedEdge]) React.useEffect(() => { if (highlightResult.status === 'idle') return const visibleIds = new Set(graph.nodes.map((node) => node.id)) if (highlightResult.nodeIds.some((id) => !visibleIds.has(id))) { setHighlightResult( emptyExplorerHighlight( 'The highlight was cleared because its components are filtered.' ) ) } }, [graph, highlightResult]) const inspectNode = (id: string | number, focus = false) => { setSelected({ kind: 'node', id: String(id) }) if (focus) { const position = api.current ?.getEngine() ?.snapshot() .nodes.find((node) => node.id === id)?.position if (position) api.current?.setViewport({ center: position, zoom: 1.3 }) } } const fit = () => api.current?.fitToView(fitOptions) const reset = () => { api.current?.clearPositionMemory() setQuery('') setTypes(kinds) setSelected(null) setLayoutName('force') setPathEndpoints(initialPath) setHighlightResult(emptyExplorerHighlight()) setError('') requestAnimationFrame(() => { void api.current ?.runLayout({ fitToView: fitOptions }) .then(() => setZoom(api.current?.getViewport()?.zoom ?? 1)) .catch((cause: unknown) => setError( cause instanceof Error ? cause.message : 'Unable to reset graph' ) ) }) } const changeZoom = (factor: number) => { const viewport = api.current?.getViewport() if (viewport) api.current?.setViewport( zoomAt( viewport, { x: viewport.width / 2, y: viewport.height / 2 }, viewport.zoom * factor ) ) } const moveView = (x: number, y: number) => { const viewport = api.current?.getViewport() if (viewport) api.current?.setViewport(panBy(viewport, { x, y })) } const connections = selectedNode ? sourceGraph.edges!.filter( (edge) => edge.source === selectedNode.id || edge.target === selectedNode.id ) : [] const tracePath = () => setHighlightResult( createDirectedPathHighlight( createGraphStore(sourceGraph).snapshot(), createGraphStore(graph).snapshot(), pathEndpoints.source, pathEndpoints.target ) ) const highlightNeighborhood = () => { if (selectedNode) { setHighlightResult( createDirectNeighborhoodHighlight( createGraphStore(graph).snapshot(), selectedNode.id ) ) } } return (
Graphora

Explore a system

Dependency explorer

A fictional commerce system. Follow a request from storefront to storage.

Quickstart
Trace a path
{Math.round(zoom * 100)}%
{ if ( event.target instanceof HTMLElement && event.target.closest('button,input,select,a') ) return const directions: Record = { ArrowLeft: [50, 0], ArrowRight: [-50, 0], ArrowUp: [0, 50], ArrowDown: [0, -50] } if (directions[event.key]) { event.preventDefault() moveView(...directions[event.key]) } if (event.key === 'Escape') setSelected(null) }} > ref={api} graph={graph} graphUpdatePolicy="preserve" layout={layout} rendererFactory={rendererFactory} theme={theme} interactionStyles={interactionStyles} highlight={highlightResult.highlight} fitToViewOnLayout={fitOptions} className="graph-surface" ariaLabel="Commerce dependencies. Drag to pan, scroll to zoom, or use the component list. Arrow keys pan the view." selection={selection} onSelectionChange={(next) => setSelected( next.selectedNodeIds.length ? { kind: 'node', id: String(next.selectedNodeIds[0]) } : next.selectedEdgeIds.length ? { kind: 'edge', id: String(next.selectedEdgeIds[0]) } : null ) } onViewportChange={(viewport) => setZoom(viewport.zoom)} onError={(cause) => setError( cause instanceof Error ? cause.message : 'Unable to update graph' ) } /> {graph.nodes.length === 0 && (

All component types are hidden.

)}
{graph.nodes.length} components · {graph.edges.length} connections Drag background to pan · Scroll to zoom · Click to inspect
{highlightResult.message} {highlightResult.nodeIds.length > 0 && (
    {highlightResult.nodeIds.map((id) => (
  1. {label(id)}
  2. ))}
)} {highlightResult.status !== 'idle' && ( )}
{error && (

{error}

)}
Browse all {graph.edges.length} visible connections
    {graph.edges.map((edge) => (
  • ))}
) } createRoot(document.querySelector('#root')!).render()