# Style a graph from data Use a theme for graph-wide defaults, ordered engine rules for data-driven appearance, and `GRAPHORA_STYLE_ATTRIBUTE` for explicit item overrides. Interaction styles provide consistent feedback for hover, focus, selection, highlight, dimming, and drag. [Open the live custom styles example](../../vanilla/custom-styles/index.html) or [view its deployed source](../../source/examples/vanilla/custom-styles/src/main.ts). ## Set the base theme Pass the theme when the engine is created or through the React `Graph` prop: ```ts const theme = { backgroundColor: '#f8fafc', node: { fill: '#2563eb', stroke: '#1e3a8a', strokeWidth: 2, radius: 20 }, edge: { stroke: '#64748b', strokeWidth: 2, arrow: { end: true, size: 8 } }, label: { fill: '#0f172a', fontSize: 13, fontWeight: '500' } } const engine = createGraphEngine({ graph, renderer, layout, viewport, theme }) ``` `GRAPHORA_LIGHT_THEME` and `GRAPHORA_DARK_THEME` are immutable presets exported by `@graphora/renderer`. There is no runtime theme setter; replace the owning engine or React configuration when the theme changes. ## Apply ordered rules without changing graph data ```ts const engine = createGraphEngine<{ load: number }>({ graph, renderer, styleRules: { nodes: [ { id: 'load', appearance: (node) => ({ node: { radius: 12 + (node.data?.load ?? 0) * 3 } }) }, { id: 'busy', when: (node) => (node.data?.load ?? 0) > 4, appearance: { node: { fill: '#dc2626' }, label: { fontWeight: '700' } } } ], edges: [{ id: 'connections', appearance: { edge: { strokeWidth: 2 } } }] } }) ``` Later rule properties win, followed by explicit item attributes and the existing interaction overlays. Each rule needs an ID unique within its node/edge array. `appearance` may be a constant or pure synchronous callback; null/undefined means no contribution. Node rules return `node`/`label`, edge rules `edge`/`label`. Nested arrow settings merge per property. Rules see records from the current view. Use `engine.setStyleRules(nextRules)` to replace the complete configuration, `engine.getStyleRules()` to read its frozen snapshot, and `engine.refreshStyles()` when captured application state changes. Configuration and results are copied; functions retain their identity. Graph data and versions remain unchanged by rule installation or refresh. Update data through the store for automatic refresh. Callbacks run once per graph/view snapshot and configuration, and are reused for camera, picking and interaction-only changes. Any graph/view revision may run all rules again. Do not mutate state, read resolved styles, return promises or capture camera/interaction state. Finite nonnegative dimensions, opacity in [0,1], valid built-in shapes and correctly typed tokens are required. Validate application values or return null when data is incomplete. A failed `setStyleRules`/`refreshStyles` throws and keeps the previous accepted configuration. If later graph data makes a callback fail, rendering reports a `GraphStyleEvaluationError` through `render:error` and throws; picking returns no hit, and measurement fails. Errors identify `ruleId`, `itemType` and `itemId`. The last bitmap may remain visible. Replace invalid rules or fix data before rendering/exporting again. Low-level renderer bitmap export can still return its old bitmap and does not certify fresh engine state. Rule-derived base sizes also feed `createLayoutGeometry()`. Paint-only updates preserve installed measurements/routes. Size, shape, stroke, visibility and font changes conservatively invalidate geometry-dependent work. Remeasure and request layout explicitly; transient interaction emphasis stays outside base measurements. Caller-supplied measurements and padding remain your responsibility. [Rule example source](../../source/examples/vanilla/custom-styles/src/main.ts). ## Override individual nodes and edges ```ts import { GRAPHORA_STYLE_ATTRIBUTE } from '@graphora/renderer' const graph = { nodes: [ { id: 'gateway', label: 'Gateway', attributes: { [GRAPHORA_STYLE_ATTRIBUTE]: { shape: 'rounded-rect', fill: '#0f766e', radius: 26 } } }, { id: 'orders', label: 'Orders' } ], edges: [ { id: 'gateway-orders', source: 'gateway', target: 'orders', directed: true, attributes: { [GRAPHORA_STYLE_ATTRIBUTE]: { shape: 'curve', stroke: '#0f766e', strokeWidth: 3, arrow: { end: true, size: 10 }, label: { visible: true, fontWeight: '700' } } } } ] } ``` Built-in node shapes are `circle`, `square`, `diamond`, and `rounded-rect`. Edges support `line` and `curve`. Node size is currently radius-based; arbitrary width and height, images, icons, and custom visual callbacks are outside the current renderer surface. ## Make interaction feedback visible ```ts const interactionStyles = { hover: { node: { stroke: '#0284c7', strokeWidth: 4 } }, selection: { node: { stroke: '#7c3aed', strokeWidth: 5 }, edge: { stroke: '#7c3aed', strokeWidth: 4 } }, highlight: { node: { opacity: 1 }, edge: { stroke: '#0f766e', strokeWidth: 4 } }, dimmed: { node: { opacity: 0.2 }, edge: { opacity: 0.12 } } } const engine = createGraphEngine({ graph, renderer, layout, viewport, interactionStyles }) ``` Resolution order is built-in defaults, theme overrides, item overrides, then interaction overlays. Within interaction overlays, later active states win in this order: dimmed, highlight, hover, focus, selection, drag. Shape and curve geometry remain stable while overlays are active. The current model is a compact set of properties, not a selector or stylesheet engine. See `RenderThemeInput`, `InteractionStyleInput`, and the resolved style types in the [renderer API](../api/reference/modules/_graphora_renderer.html).