A React TypeScript component library for interactive knowledge graph visualization, designed for use with the NIH's Biomedical Data Translator. Built on ReactFlow (via @xyflow/react) and ELKjs.
Renders Biolink Model knowledge graphs with automatic layout, type-based color coding and icons, interactive selection, and hover geometry for tooltip positioning.
- Interactive graph visualization — pan, zoom, box-select, and click interactions powered by ReactFlow
- Five automatic layout algorithms — hierarchical (top-down and left-right), force-directed, grid, and radial, all computed off-thread via an ELK web worker
- Biolink type awareness — nodes are color-coded by their Biolink type and display dedicated SVG icons for 13 common entity types (Drug, Gene, Disease, Protein, ChemicalEntity, SmallMolecule, AnatomicalEntity, BiologicalEntity, PhenotypicFeature, PathologicalProcess, PhysiologicalProcess, Polypeptide, and a default fallback)
- Controlled and uncontrolled selection — click or box-drag to select; optionally drive selection from outside via
selectedIds - Controlled and uncontrolled hover — built-in hover styles plus bidirectional controlled hover for syncing graph highlights with external UI (sidebars, lists, detail panels)
- Hover geometry with anchor points — callbacks receive bounding rects and pre-computed anchor positions in viewport coordinates, re-measured on pan/zoom via
requestAnimationFrame, ideal for tooltip positioning - Multi-edge rendering — parallel edges between the same node pair automatically spread into distinct quadratic bezier curves with configurable spacing
- Inferred edge styling — edges marked
inferred: truerender with a dashed stroke - Predicate labels — edge labels extracted from Biolink predicates with optional show/hide
- Edge path styles — bezier, straight, step, and smoothstep path options
- MiniMap and zoom controls — zoomable/pannable minimap with neutral node dots
- Draggable graph annotations — parent-controlled text notes with editable content, clickable links, hover delete controls, configurable styling, and savable graph-space positions
- Node chrome — optional client-rendered hover controls at the node corners, with
onNodeRemove/onNodeMenucallbacks - Optional connection handles — hide and disable node connection handles via
showHandles - Smart text formatting — gene/protein names uppercased, other names title-cased, Roman numerals detected and preserved
- Full TypeScript type definitions — complete generics for nodes, edges, selections, geometry, and all props
- Dual module output — ESM and CommonJS builds with a separate CSS stylesheet
The library exports a single primary component (GraphView) that wraps ReactFlow in a ReactFlowProvider, a settings context, and a node-chrome context. Internally it:
-
Transforms input data — converts the Biolink-model
GraphData(a record-based format withnodesandedgeskeyed by ID) into ReactFlow's flat node/edge arrays. During transformation, each node is assigned a deterministic color via a hash of its primary Biolink type, a display label (first name), a simplified type string, and an SVG icon. Edges sharing the same node pair are indexed so the renderer can offset them. -
Computes layout via web worker — the
useGraphLayouthook instantiates an ELK instance pointing at a user-provided worker URL. It builds an ELK graph descriptor with the chosen layout algorithm's options, sends it to the worker, and applies the returned positions to the ReactFlow nodes. Layout is recomputed whenever the data or layout type changes, with stale-cancellation to avoid race conditions. -
Syncs layout to ReactFlow state — the
useLayoutSynchook pushes layouted nodes/edges into ReactFlow's controlled state and triggersfitViewwith a short delay so the viewport frames the graph. -
Handles selection — the
useSelectionhook translates ReactFlow'sOnSelectionChangeParamsback into domain-levelGraphNode[]andGraphEdge[]objects. The optionaluseControlledSelectionhook allows the parent to drive selection from the outside by togglingselectedflags on the flow nodes/edges. -
Handles hover with geometry — the
useHoverGeometryhook tracks which node or edge the pointer is over, queries the DOM for its bounding rect (scoped to the currentGraphViewinstance to avoid cross-graph collisions), computes a named anchor point, and invokes the caller'sonNodeHover/onEdgeHoverwith the geometry. On viewport pan/zoom, geometry is re-measured viarequestAnimationFrameso tooltip positions stay accurate. -
Renders custom node and edge components —
GraphNodedisplays the type icon, a formatted label, top/bottom handles (optionally hidden), and client-provided hover chrome.GraphEdgesupports four path algorithms plus a multi-edge quadratic bezier mode, dashed stroke for inferred edges, and a floating label rendered via ReactFlow'sEdgeLabelRenderer.
All layout is performed by ELK in a web worker (~1.5 MB engine kept out of the main bundle):
| Layout | ELK Algorithm | Description |
|---|---|---|
hierarchical |
layered (DOWN) |
Layered top-to-bottom with 80px node spacing and 100px inter-layer spacing |
hierarchicalLR |
layered (RIGHT) |
Layered left-to-right |
force |
force (Eades model) |
Force-directed with 400 iterations |
grid |
box |
Aspect-ratio-aware box packing |
radial |
stress |
Stress-minimization producing radial/circular layouts |
GraphData (Biolink model)
│
├─ transformNodesToFlow() → FlowNode[] (with color, label, icon, type)
├─ transformEdgesToFlow() → FlowEdge[] (with pair indexing, labels)
│
└─ useGraphLayout() → ELK worker → positioned FlowNode[]
│
└─ useLayoutSync() → ReactFlow state → rendered graph
npm install translator-graph-view elkjsReact 18+ (or 19+) and elkjs 0.9+ are required as peer dependencies.
import { GraphView } from 'translator-graph-view';
import 'translator-graph-view/styles.css';
const data = {
nodes: {
'n1': { id: 'n1', names: ['Aspirin'], types: ['biolink:Drug'] },
'n2': { id: 'n2', names: ['Headache'], types: ['biolink:Disease'] },
},
edges: {
'e1': { id: 'e1', subject: 'n1', object: 'n2', predicate: 'biolink:treats' },
},
};
const elkWorkerUrl = new URL('elkjs/lib/elk-worker.min.js', import.meta.url).href;
function App() {
return (
<div style={{ width: '100%', height: '600px' }}>
<GraphView
data={data}
layout="hierarchical"
elkWorkerUrl={elkWorkerUrl}
onNodeClick={(node) => console.log('Clicked:', node)}
onSelectionChange={(selection) => console.log('Selected:', selection)}
/>
</div>
);
}The GraphView container must have a defined width and height.
| Prop | Type | Default | Description |
|---|---|---|---|
data |
GraphData |
required | Graph nodes and edges |
elkWorkerUrl |
string |
required | URL to the ELKjs web worker script (see below) |
layout |
LayoutType |
'hierarchical' |
Layout algorithm |
onSelectionChange |
(selection: Selection) => void |
- | Fires when selection changes |
onSelectionDelete |
(selection: DeleteSelection) => void |
- | Fires on Delete/Backspace with a selection; the view never removes anything itself (see below) |
onNodeClick |
(node: GraphNode) => void |
- | Fires when a node is clicked |
onEdgeClick |
(edge: GraphEdge) => void |
- | Fires when an edge is clicked |
onNodeHover |
(node: GraphNode | null, geometry: HoverGeometry | null) => void |
- | Fires when a node is hovered or unhovered |
onEdgeHover |
(edge: GraphEdge | null, geometry: HoverGeometry | null) => void |
- | Fires when an edge is hovered or unhovered |
onAnnotationHover |
(annotationId: string | null) => void |
- | Fires when an annotation is hovered or unhovered |
hoveredNodeId |
string | null |
- | Controlled hover: focuses the given node and dims the rest |
hoveredEdgeId |
string | null |
- | Controlled hover: focuses the given edge + endpoints and dims the rest |
hoveredAnnotationId |
string | null |
- | Controlled hover: shows annotation hovered state (no dimming) |
nodeHoverAnchor |
HoverAnchorPosition |
'topCenter' |
Anchor point returned in HoverGeometry for node hovers |
edgeHoverAnchor |
HoverAnchorPosition |
'midpoint' |
Anchor point returned in HoverGeometry for edge hovers |
clearHoverOnViewportChange |
boolean |
false |
Clear the hover when the viewport pans/zooms instead of re-measuring it every frame |
selectedIds |
string[] |
- | Controlled selection by node/edge ID |
edgeType |
EdgeType |
'straight' |
Edge path style: 'bezier', 'straight', 'step', or 'smoothstep' |
showEdgeLabels |
boolean |
true |
Show predicate labels on edges |
showMiniMap |
boolean |
true |
Show the zoomable/pannable minimap |
showHandles |
boolean |
true |
Show connection handles on hover and allow connections |
nodeChrome |
GraphNodeChrome |
- | Client-rendered chrome at the top-left and bottom-right of each graph node |
getNodeIcon |
GraphNodeIconRenderer |
- | Client icon lookup; null/undefined uses the library default, false hides the icon |
getNodeColor |
GraphNodeColorRenderer |
- | Client background-color lookup; null/undefined keeps the default node background |
onNodeRemove |
(nodeId: string) => void |
- | Fires from node chrome onRemove |
onNodeMenu |
(nodeId: string, position: { x: number; y: number }) => void |
- | Fires from node chrome onMenu with a viewport position |
multiEdgeSpacing |
number |
60 |
Pixel spacing between parallel edges sharing the same node pair |
annotations |
GraphAnnotation[] |
- | Controlled annotation overlays (positions in graph coordinates) |
onAnnotationsChange |
(annotations: GraphAnnotation[]) => void |
- | Fires when an annotation is dragged, edited, or deleted |
annotationStyles |
GraphAnnotationStyles |
- | Client-configurable annotation appearance (background, delete button, icons, link rendering) |
hoverStyles |
GraphHoverStyles |
- | Client-configurable hover / dim appearance (opacity, classNames) |
className |
string |
- | Additional CSS class for the container |
Layout computation is offloaded to a web worker, keeping the ~1.5 MB ELK engine out of your main bundle. You must provide a URL pointing to the ELK worker script. In Vite-based apps:
const elkWorkerUrl = new URL('elkjs/lib/elk-worker.min.js', import.meta.url).href;For webpack or other bundlers, serve node_modules/elkjs/lib/elk-worker.min.js as a static asset and pass its URL.
Annotations are draggable text notes rendered on top of the graph. They are parent-controlled: the host application creates, persists, and restores them via the annotations prop. Positions are in graph coordinates, so they stay anchored as the user pans and zooms.
import { useState } from 'react';
import { GraphView, type GraphAnnotation } from 'translator-graph-view';
function App() {
const [annotations, setAnnotations] = useState<GraphAnnotation[]>([]);
const addAnnotation = () =>
setAnnotations((prev) => [
...prev,
{ id: crypto.randomUUID(), text: '', position: { x: 100, y: 100 } },
]);
return (
<>
<button type="button" onClick={addAnnotation}>Add annotation</button>
<GraphView
data={data}
elkWorkerUrl={elkWorkerUrl}
annotations={annotations}
onAnnotationsChange={setAnnotations}
annotationStyles={{
backgroundColor: '#FFF9C4',
deleteButton: { backgroundColor: '#fff' },
}}
/>
</>
);
}Each annotation shows as a text note with a pale yellow background by default. On hover, a delete button appears in the top-left corner (white circle with an X icon by default). Override colors, CSS classes, and the delete icon via annotationStyles.
Annotations have two faces. At rest a note renders as static text with its links clickable; clicking the note (anywhere but a link) swaps in a textarea with the caret where you clicked, and blurring commits. Escape leaves edit mode and discards the draft. Read-only annotations never enter edit mode, but their links still work.
Annotation text is plain text; the display view turns three forms into anchors:
| Typed | Renders as |
|---|---|
https://example.com/x |
the URL, linked (trailing sentence punctuation stays outside the link) |
www.example.com |
the text, linked to https://www.example.com |
curator@example.org |
the address, linked to mailto: |
[the paper](https://example.com/p) |
the paper, linked |
Links open in a new tab with rel="noopener noreferrer". Only http, https, and mailto targets are linked — anything else (javascript:, data:, and so on) renders as the literal text the user typed. Nothing is ever parsed as HTML: anchors are built from a token stream, so markup in annotation text stays inert text.
Set annotationStyles.linkify: false to render annotation text verbatim, and annotationStyles.linkClassName to style the generated anchors.
onAnnotationsChange fires when:
- the user finishes dragging an annotation (
onNodeDragStop) - the user edits text and blurs the textarea
- the user clicks the delete button
Persist the returned array (localStorage, API, etc.) and pass it back through annotations on reload.
Pass nodeChrome to render hover controls at the top-left and bottom-right corners of each graph node. Chrome is hidden until the node is hovered or contains focus (keyboard users can tab to chrome buttons). Memoize the nodeChrome object (or hoist slot renderers) so GraphView does not rebuild it every render.
onRemove and onMenu are only present when you pass onNodeRemove / onNodeMenu. Pointer-triggered menus use clientX / clientY; keyboard activation (or calling onMenu() with no event) falls back to the node's bounding-rect center.
const nodeChrome = {
topLeft: ({ onRemove }) =>
onRemove ? <button type="button" onClick={onRemove}>Remove</button> : null,
bottomRight: ({ onMenu }) =>
onMenu ? <button type="button" onClick={onMenu}>Menu</button> : null,
};
<GraphView
data={data}
elkWorkerUrl={elkWorkerUrl}
showHandles={false}
nodeChrome={nodeChrome}
onNodeRemove={(nodeId) => console.log('remove', nodeId)}
onNodeMenu={(nodeId, position) => console.log('menu', nodeId, position)}
/>Pass getNodeIcon to replace the built-in type icon on each graph node. The first argument is the simplified primary type (for example "Drug"), not a Biolink CURIE. Full types are on node.types.
- Return
nullorundefinedto keep the library icon for that node. - Return
falseto hide the icon. - Icons are wrapped in a 24×24 slot that sizes both
svgandimg.
The library keeps the callback identity stable, so an inline getNodeIcon does not re-render edges or annotations. Hoist or wrap in useCallback if you also want to avoid extra work inside the renderer itself. Compose with getNodeTypeIcon when you only want to override some types.
import { GraphView, getNodeTypeIcon } from 'translator-graph-view';
<GraphView
data={data}
elkWorkerUrl={elkWorkerUrl}
getNodeIcon={(type, node) => {
if (node.id === 'special') return <img alt="" src="/special.svg" />;
if (type === 'Drug') return <MyDrugIcon />;
return getNodeTypeIcon(type);
}}
/>Layouts space nodes by their bounding boxes, so the boxes handed to ELK track
what the stylesheet actually paints: NODE_HEIGHT is the real 32px node height,
and each node's width comes from estimateNodeWidth, clamped to the stylesheet's
own 60–200px bounds. A box larger than the painted node would reappear as gap the
configured spacing never asked for.
layoutConfigs therefore expresses spacing as the gap you actually see. Adjust
those values to trade density against breathing room; note that the radial
(stress) algorithm positions mainly by elk.stress.desiredEdgeLength and largely
ignores elk.spacing.nodeNode.
Pass getNodeColor to set a node's background from its type. Like getNodeIcon,
the first argument is the simplified primary type (for example "Drug"), not
a Biolink CURIE; full types are on node.types.
- Return
{ background, hoverBackground }to color the node. - Omit
hoverBackgroundto reusebackgroundon hover. - Return
nullorundefinedto leave that node on the default background.
With no renderer supplied, every node keeps the default #DCDCE6 background, so
adding the prop is opt-in per node. Colors are applied as the --tgv-node-bg and
--tgv-node-bg-hover custom properties, and the hover background also applies
when a node is hovered externally via hoveredNodeId.
const typeColors: Record<string, { background: string; hoverBackground: string }> = {
Drug: { background: '#DCE9FF', hoverBackground: '#C4DAFF' },
Disease: { background: '#FFE0E0', hoverBackground: '#FFC9C9' },
};
<GraphView
data={data}
elkWorkerUrl={elkWorkerUrl}
getNodeColor={(type) => typeColors[type] ?? null}
/>The library keeps the callback identity stable, so an inline getNodeColor does
not re-render edges or annotations.
The graph supports the following built-in interactions:
| Action | Behavior |
|---|---|
| Left-click node/edge | Select it |
| Shift+Click | Add to selection |
| Drag from empty area | Box selection (partial overlap) |
| Delete / Backspace | Report the selection through onSelectionDelete |
| Middle/right-button drag | Pan the viewport |
| Scroll | Zoom in/out (0.15x to 3x) |
| MiniMap drag | Pan the viewport |
| MiniMap scroll | Zoom |
Pressing Delete or Backspace with elements selected calls
onSelectionDelete and nothing else — the view never drops the elements from its own
state. Its nodes and edges mirror the data prop, so an internal delete would leave the
two out of step until the next data change put the element back. Remove the reported
ids from data to make the deletion stick, which also leaves undo entirely in the
client's hands. Delete keys are only armed when onSelectionDelete is provided.
<GraphView
data={data}
elkWorkerUrl="/elk-worker.min.js"
onSelectionDelete={({ nodes, edges }) => removeFromMyStore(nodes, edges)}
/>edges includes every edge incident to a deleted node, not only the edges the user
selected. That differs from node-chrome onNodeRemove, which reports a single node id
and does not include incident edges — wire both if your UI exposes chrome remove and
keyboard delete, and expand edges yourself for the chrome path if you need parity.
Keystrokes typed inside an input, textarea, or contenteditable — annotation text editing included — never trigger it.
The component supports both uncontrolled hover (internal styling only) and controlled hover (you drive the highlight state from outside). Use controlled hover to synchronize highlights between the graph and an external UI like a sidebar or detail panel.
When a controlled hover id is set, the graph applies neighborhood focus:
- Node: the hovered node, its incident edges, and neighbor nodes stay full opacity; everything else is dimmed.
- Edge: that edge and its two endpoints stay full opacity; everything else is dimmed.
- Annotation: the annotation shows its hovered style; the rest of the graph is not dimmed.
Uncontrolled — nodes and edges show hover styles on mouseover with no props needed.
Outbound events — use onNodeHover / onEdgeHover / onAnnotationHover to react to hover changes:
const [hoveredNode, setHoveredNode] = useState<GraphNode | null>(null);
<GraphView
data={data}
elkWorkerUrl={elkWorkerUrl}
onNodeHover={(node) => setHoveredNode(node)}
/>
{hoveredNode && <div>Hovering: {hoveredNode.names[0]}</div>}Controlled (bidirectional) — pass hoveredNodeId / hoveredEdgeId / hoveredAnnotationId to drive highlights from external UI (e.g. a sidebar list), and use the outbound hover callbacks to update that state when the user hovers inside the graph:
const [hoveredNodeId, setHoveredNodeId] = useState<string | null>(null);
// Sidebar list item triggers graph highlight
<li
onMouseEnter={() => setHoveredNodeId(node.id)}
onMouseLeave={() => setHoveredNodeId(null)}
>
{node.names[0]}
</li>
// Graph triggers sidebar highlight
<GraphView
data={data}
elkWorkerUrl={elkWorkerUrl}
hoveredNodeId={hoveredNodeId}
onNodeHover={(node) => setHoveredNodeId(node?.id ?? null)}
/>Override dimming / hovered appearance via hoverStyles (sensible defaults apply when omitted):
<GraphView
data={data}
elkWorkerUrl={elkWorkerUrl}
hoverStyles={{
dimmedOpacity: 0.25,
dimmedNodeClassName: 'my-dimmed-node',
hoveredAnnotationClassName: 'my-hovered-annotation',
}}
/>| Field | Default | Description |
|---|---|---|
dimmedOpacity |
0.3 |
Opacity for dimmed elements (--tgv-dimmed-opacity) |
dimmedNodeClassName |
- | Extra class on dimmed nodes |
dimmedEdgeClassName |
- | Extra class on dimmed edges |
dimmedAnnotationClassName |
- | Extra class on dimmed annotations |
hoveredNodeClassName |
- | Extra class on hovered nodes |
hoveredEdgeClassName |
- | Extra class on hovered edges |
hoveredAnnotationClassName |
- | Extra class on hovered annotations |
The onNodeHover and onEdgeHover callbacks receive a second HoverGeometry argument containing the hovered element's bounding rect and a pre-computed anchor point in viewport coordinates — useful for positioning tooltips without querying the DOM yourself.
import type { GraphNodeType, HoverGeometry } from 'translator-graph-view';
<GraphView
data={data}
elkWorkerUrl={elkWorkerUrl}
nodeHoverAnchor="topCenter"
onNodeHover={(node, geometry) => {
if (node && geometry) {
showTooltip({ x: geometry.anchor.x, y: geometry.anchor.y });
} else {
hideTooltip();
}
}}
/>HoverAnchorPosition can be one of: 'topLeft', 'topCenter', 'topRight', 'centerLeft', 'center', 'centerRight', 'bottomLeft', 'bottomCenter', 'bottomRight', or 'midpoint' (edges only — computes the true midpoint of the SVG path's visible segment).
When the hover callback fires with null (mouse leaves), geometry is also null. If DOM measurement fails (e.g. SSR), geometry is null.
While the pointer stays over the same node or edge, geometry is re-measured on pan and zoom (throttled with requestAnimationFrame) so anchors stay aligned with the viewport.
Re-measuring calls your hover callback once per frame for as long as the gesture lasts. If your consumer stores the geometry in state, that is a re-render per frame, and any hover-driven dimming stays on — forcing the compositor to blend every dimmed element on every frame of the pan. On large graphs this makes a pan that starts over a node noticeably heavier than one that starts over empty space.
Set clearHoverOnViewportChange to drop the hover instead. The callback fires once with null on the first frame of the gesture, tooltips and dimming clear, and nothing further fires until the pointer enters another element:
<GraphView
data={data}
elkWorkerUrl="/elk-worker.min.js"
clearHoverOnViewportChange
onNodeHover={(node, geometry) => setHover(node ? { node, geometry } : null)}
/>Keep the default (false) if your tooltips are meant to track their element while the user pans.
DOM queries are scoped to this GraphView instance, so multiple graphs on one page do not pick each other's elements. Element ids are escaped for attribute selectors (CSS.escape when available).
When multiple edges connect the same pair of nodes (in either direction), they automatically spread into distinct quadratic bezier curves offset perpendicular to the straight-line path between the nodes. The label for each edge is positioned at the bezier's midpoint (t=0.5). Control the spacing with multiEdgeSpacing (default 60px).
Edges with inferred: true in the data render with a dashed stroke to visually distinguish them from direct evidence edges.
'hierarchical'— Layered top-to-bottom (default)'hierarchicalLR'— Layered left-to-right'force'— Force-directed (Eades model, 400 iterations)'grid'— Aspect-ratio-aware box packing'radial'— Stress-minimization (radial/circular)
Each node displays:
- An SVG icon based on its primary Biolink type (13 dedicated icons plus a default)
- A formatted label — gene/protein names are uppercased; other names are title-cased with Roman numeral detection
- A colored left border derived deterministically from the type via a hash into an 18-color palette
- Selection and hover visual states (border/shadow changes)
interface GraphData {
nodes: Record<string, GraphNode>;
edges: Record<string, GraphEdge>;
results?: Result[];
paths?: Record<string, Path>;
publications?: Record<string, Publication>;
trials?: Record<string, Trial>;
}
interface GraphNode {
id: string;
names: string[];
types: string[]; // e.g. ['biolink:Drug']
curies?: string[];
}
interface GraphEdge {
id: string;
subject: string; // source node ID
object: string; // target node ID
predicate: string; // e.g. 'biolink:treats'
predicate_url?: string;
knowledge_level?: string;
inferred?: boolean; // renders with dashed stroke when true
provenance?: Provenance[];
publications?: { inferred?: Publication[] };
trials?: Trial[];
support?: unknown[];
aras?: string[];
description?: string;
}
interface Result {
id: string;
drug_name?: string;
subject: string;
object: string;
paths?: string[];
}
interface Path {
id: string;
edges: string[];
}
interface Publication {
id: string;
title?: string;
authors?: string[];
year?: number;
journal?: string;
pmid?: string;
doi?: string;
}
interface Trial {
id: string;
title?: string;
status?: string;
phase?: string;
nctid?: string;
}
interface Provenance {
source?: string;
url?: string;
evidence_type?: string;
}
interface GraphAnnotation {
id: string;
text: string;
position: { x: number; y: number };
}
interface GraphAnnotationStyles {
backgroundColor?: string;
className?: string;
deleteButton?: {
backgroundColor?: string;
className?: string;
icon?: React.ReactNode;
};
linkify?: boolean; // default true
linkClassName?: string;
}
interface GraphNodeChromeContext {
node: GraphNode;
selected: boolean;
onRemove?: () => void;
onMenu?: (event?: React.MouseEvent) => void;
}
interface GraphNodeChrome {
topLeft?: (ctx: GraphNodeChromeContext) => React.ReactNode;
bottomRight?: (ctx: GraphNodeChromeContext) => React.ReactNode;
}
type GraphNodeIconRenderer = (
type: string,
node: GraphNode,
) => React.ReactNode | null | undefined;useGraphLayout({ nodes, edges, layout, elkWorkerUrl })— Computes ELK layout positions for ReactFlow nodes/edges via a web worker. Returns{ nodes, edges, isLayouting }.useSelection({ data, onSelectionChange })— Manages node/edge selection state, translating ReactFlow selection events back into domain-level objects.useGraphSettings()— Access theGraphSettingscontext (multiEdgeSpacing,annotationStyles,hoverStyles).useNodeChrome()— Access the node-chrome context (nodeChrome,onNodeRemove,onNodeMenu,getNodeIcon) when renderingGraphNodeoutsideGraphView.
| Function | Description |
|---|---|
transformNodesToFlow(data) |
Convert GraphData to ReactFlow node array with colors, labels, and types |
transformEdgesToFlow(data, edgeType?, showLabels?) |
Convert GraphData to ReactFlow edge array with pair indexing |
transformAnnotationsToFlow(annotations) |
Convert GraphAnnotation[] to ReactFlow annotation nodes |
extractAnnotationsFromFlow(nodes) |
Extract GraphAnnotation[] from a ReactFlow node array |
isAnnotationNode(node) |
Type guard for annotation flow nodes |
getColorForType(type) |
Get a deterministic color for a Biolink type string (18-color palette) |
getNodeTypeIcon(type) |
Return the library SVG icon for a simplified (or biolink:) type |
simplifyTypeName(type) |
Extract a readable name from a prefixed type URI ("biolink:Drug" → "Drug") |
getPrimaryType(types) |
Return the first type from a types array |
formatPredicate(predicate) |
Format a predicate for display ("biolink:treats" → "treats") |
getNodesById(data, ids) |
Look up nodes by ID array |
getEdgesById(data, ids) |
Look up edges by ID array |
NODE_WIDTH / NODE_HEIGHT |
Fallback node dimensions used by the layout engine (120×32) |
NODE_MIN_WIDTH / NODE_MAX_WIDTH |
Width bounds the node stylesheet enforces (60 / 200) |
getNodeDisplayLabel(label, type) |
The label text as rendered (all caps for Gene/Protein, otherwise title case) |
estimateNodeWidth(label, type) |
Estimated rendered node width, used as the node's layout box |
All TypeScript types are exported for consumer use:
GraphData, GraphNodeType, GraphEdgeType, GraphViewProps, LayoutType, EdgeType, Selection, DeleteSelection, Result, Path, Publication, Trial, Provenance, GraphNodeData, GraphEdgeData, FlowNode, FlowGraphNode, FlowAnnotationNode, FlowEdge, HoverAnchorPosition, HoverGeometry, GraphAnnotation, GraphAnnotationStyles, GraphHoverStyles, GraphNodeChrome, GraphNodeChromeContext, GraphNodeColors, GraphNodeColorRenderer
npm install
npm run dev # Start Vite dev server with example app
npm run build # Build the library (ESM + CJS + types + CSS)
npm run typecheck # Type-check without emitting
npm run lint # Run ESLint
npm run test # Run unit tests (Vitest)
npm run test:watch # Run tests in watch mode
npm run test:e2e # Run Playwright end-to-end tests
npm run test:e2e:ui # Run Playwright tests with UIThe example/ directory contains a full demo application that showcases all library features:
- Dataset switching (small, medium, large graphs)
- All five layout algorithms
- Selection panel with node/edge lists
- Bidirectional controlled hover between sidebar and graph
- Tooltip positioning using hover geometry anchors
- Node chrome (hover remove/menu controls) and optional connection handles
- Graph statistics display
src/
├── components/
│ ├── GraphView/ # Main component, layout sync, controlled selection/hover hooks
│ ├── nodes/ # Custom ReactFlow node component with icons and formatting
│ └── edges/ # Custom ReactFlow edge component with multi-edge and path types
├── hooks/ # useGraphLayout, useSelection, useGraphSettings
├── layouts/ # ELK layout configurations for each algorithm
├── utils/ # Data transforms, color generation, hover geometry measurement
├── assets/icons/ # SVG icons for Biolink entity types
└── types/ # TypeScript type definitions
example/ # Demo application
e2e/ # Playwright end-to-end tests
MIT