Graphora API
    Preparing search index...

    Module @graphora/renderer

    Renderer Package

    Package name: @graphora/renderer

    Planned responsibility: renderer interfaces, viewport and camera coordination, drawing, labels, style resolution, hit testing, and picking.

    CanvasGraphRenderer is the first concrete renderer. It owns one <canvas> element inside the container passed to mount, uses Canvas 2D drawing primitives, and implements the public GraphRenderer lifecycle.

    Minimal usage:

    import { createGraphEngine } from '@graphora/engine'
    import { CanvasGraphRenderer, createViewport } from '@graphora/renderer'

    const renderer = new CanvasGraphRenderer()
    const engine = createGraphEngine({
    graph: {
    nodes: [
    { id: 'a', label: 'Alpha', position: { x: -40, y: 0 } },
    { id: 'b', label: 'Beta', position: { x: 40, y: 0 } }
    ],
    edges: [{ id: 'ab', source: 'a', target: 'b', label: 'connects' }]
    },
    renderer,
    viewport: createViewport({ width: 640, height: 360 })
    })

    renderer.resize({ width: 640, height: 360 })
    engine.mount(document.querySelector('#graph')!)

    The renderer clears and paints the snapshot background, then draws edges, basic directed arrowheads, nodes, and resolved labels in a stable order. It uses worldToScreen for placement and stores a device-pixel-ratio-adjusted canvas backing size during resize.

    Image export is available from the Canvas renderer after it has rendered at least one snapshot:

    const image = renderer.exportImage({ type: 'image/png' })

    console.log(image.width, image.height, image.backgroundColor)

    This exports the current viewport from the Canvas backing store. width and height are backing-pixel dimensions, displayWidth and displayHeight are CSS-pixel dimensions, and pixelRatio records the ratio used for the backing store. The background is the rendered snapshot theme background. Full-graph export is intentionally deferred until Graphora has a graph-fitting capture pipeline that can render offscreen without disturbing the live viewport.

    Edge, arrow, and label behavior:

    • straight edges render as one Canvas line segment
    • curved edges use a quadratic curve when edge.style.curved is true
    • parallel and reciprocal edges receive separate, stable quadratic routes
    • self-edges render as loops; additional loops expand away from the node
    • directed edges draw an end arrowhead only when edge.style.arrow.end is true
    • arrowhead tips are offset from the target node radius when the target node is present in the render snapshot
    • node labels are placed below the resolved node radius with a small font-scaled gap
    • edge labels follow the midpoint of the actual line, curve, or loop route
    • hidden or empty labels are skipped
    • a background-color halo keeps text legible where routes pass underneath it

    Drawing, labels, arrow direction, and picking share the same derived route. Derivation never modifies graph records or the renderer snapshot.

    Labels retain the compatible draw-all default. Applications can opt into zoom-aware collision suppression:

    const renderer = new CanvasGraphRenderer({
    labels: {
    mode: 'automatic',
    minZoom: 0.65,
    edgeMinZoom: 1,
    collisionPadding: 3,
    showEdgeLabels: true
    }
    })

    Automatic mode keeps dragging, selected, focused, hovered, and highlighted labels ahead of ordinary labels. Explicit label.visible: false always wins. Set showEdgeLabels: false to suppress edge labels at every zoom.

    Current limits:

    • pick(point, snapshot?) tests visible nodes first, then the rendered edge routes, and returns a typed canvas result when neither is hit.
    • exportImage(options?) is Canvas-only and exports the current viewport. It fails if called before mount or before the first render. Supported requested types are image/png, image/jpeg, and image/webp; browsers may fall back to PNG for unsupported encoders, and the returned type reports the detected data URL type.
    • resize scheduling, ResizeObserver, and animation-frame invalidation belong to TASK-033.
    • styling is limited to already-resolved snapshot fields. Broad style and theme resolution is intentionally small for V0.1.
    • label rotation, wrapping, halos, and arbitrary manual placement are deferred.

    @graphora/renderer owns the V0.1 style and theme model. The public helpers are pure and framework-agnostic:

    • resolveRenderTheme(theme?)
    • resolveNodeStyle(input)
    • resolveEdgeStyle(input)
    • resolveLabelStyle(input)
    • resolveInteractionStyles(input?)
    • mergeInteractionRenderState(input?)

    The style priority order is:

    1. built-in default theme tokens
    2. user-provided theme overrides
    3. explicit item style overrides
    4. interaction overlays

    Interaction overlays apply in this order: dimmed context, programmatic highlight, hover, focus, selection, active drag. Later overlays win for the same property only, so highlighting can reduce unrelated context without overwriting pointer or selection feedback. A hidden label stays hidden when it is highlighted.

    Graph data can opt into item-level style overrides through the project-owned attribute key:

    import { GRAPHORA_STYLE_ATTRIBUTE } from '@graphora/renderer'

    const graph = {
    nodes: [
    {
    id: 'service-api',
    label: 'API',
    attributes: {
    [GRAPHORA_STYLE_ATTRIBUTE]: {
    fill: '#16a34a',
    radius: 24,
    label: { visible: true, fontSize: 14 }
    }
    }
    }
    ],
    edges: [
    {
    source: 'service-api',
    target: 'database',
    attributes: {
    [GRAPHORA_STYLE_ATTRIBUTE]: {
    stroke: '#475569',
    strokeWidth: 3,
    arrow: { end: true, size: 10 }
    }
    }
    }
    ]
    }

    This is deliberately not a selector engine. V0.1 supports node fill, stroke, stroke width, radius, opacity, visibility; edge stroke, stroke width, opacity, visibility, curved flag, arrow end settings; and label fill, font family, font size, font weight, and visibility.

    Import GRAPHORA_LIGHT_THEME or GRAPHORA_DARK_THEME from @graphora/renderer and pass it directly as createGraphEngine({ theme }). Light is the canonical DEFAULT_RENDER_THEME; both presets and all nested objects are frozen. Resolution copies caller overrides into immutable tokens.

    import { createGraphEngine } from '@graphora/engine'
    import { GRAPHORA_DARK_THEME } from '@graphora/renderer'

    const engine = createGraphEngine({
    graph,
    renderer,
    theme: {
    ...GRAPHORA_DARK_THEME,
    node: { ...GRAPHORA_DARK_THEME.node, radius: 22 },
    edge: {
    ...GRAPHORA_DARK_THEME.edge,
    arrow: { ...GRAPHORA_DARK_THEME.edge.arrow, size: 10 }
    }
    },
    interactionStyles: {
    highlight: {
    node: { stroke: '#5eead4' },
    edge: { stroke: '#5eead4' },
    label: { fill: '#99f6e4' }
    },
    dimmed: { label: { fill: '#94a3b8' } }
    }
    })

    Spread each nested object you override to retain the remaining preset tokens. Passing only node: { radius: 22 } replaces that input object; omitted fields then fall back to built-in light defaults. Nested arrow overrides work the same way. No merge helper is needed for the existing small token surface.

    A node's explicit GRAPHORA_STYLE_ATTRIBUTE fill still wins over the preset fill. Interaction overlays remain higher priority and independent of presets: the example above supplies bright highlight labels for a dark background. Review application-specific hover, focus, selection and drag colors alongside any item colors you introduce. Presets do not automatically choose overlays or follow OS color preferences. The engine accepts theme configuration at creation; this slice adds no runtime theme setter.

    The custom-styles vanilla example compares both presets and toggles path highlighting. Themes remain renderer configuration, with no new package or React provider. A future @graphora/themes package needs multiple renderer consumers, larger collections, palette dependencies or external reuse demand; see ADR 0020 in the product workspace.

    @graphora/renderer now exposes the public GraphRenderer contract. The contract is framework-agnostic and does not import React.

    Renderer lifecycle:

    • mount(container) attaches renderer-owned resources to an HTMLElement.
    • resize(size) accepts the shared ViewportSize shape.
    • render(snapshot) receives a renderer-ready RenderSnapshot.
    • pick(point, snapshot?) returns a typed node, edge, canvas, or miss result, or null when a renderer chooses not to report a result.
    • unmount() releases mounted resources while keeping the renderer reusable.
    • destroy() is optional and releases final renderer resources.

    RenderSnapshot is resolved drawing input. It contains:

    • viewport: current ViewportState
    • nodes: render nodes with graph identity, world position, size/radius, optional labels/data/attributes, resolved node style, and needsLayout
    • edges: render edges with graph identity, endpoint identity, resolved endpoint world points, optional labels/data/attributes, direction, and style
    • labels: resolved label items owned by nodes or edges
    • theme: default resolved theme values for V0.1
    • interaction: hover, selection, focus, and drag state for draw passes
    • version: compatible optional source version for cache invalidation
    • sourceVersion: explicit canonical source snapshot version
    • view: optional visible-view ID and membership version

    Renderers should not accept raw graph input directly. Raw graph parsing, normalization, mutation, and store snapshots remain outside the renderer package. The engine is responsible for adapting core graph snapshots into renderer-ready snapshots before calling render.

    The current contract defines the API shape only. It does not implement Canvas drawing, render scheduling, hit-test algorithms, interactions, layouts, React bindings, or custom visual plugins.

    The current implementation exposes pure viewport state and coordinate utilities. These utilities do not depend on DOM, Canvas, or browser globals.

    Coordinate convention:

    • World coordinates are graph coordinates.
    • Screen coordinates are pixel coordinates relative to the top-left of the render surface.
    • Positive x moves right.
    • Positive y moves down.
    • zoom is pixels per world unit.
    • center is the world coordinate shown at the center of the viewport.

    Public viewport helpers:

    • createViewport
    • setViewport
    • worldToScreen
    • screenToWorld
    • panBy
    • zoomAt
    • fitBounds
    • clampZoom
    • clampViewportZoom

    setViewport, zoomAt, and fitBounds clamp zoom to the viewport's minZoom and maxZoom.

    panBy accepts a screen-space drag delta. A drag delta of { x: 10, y: 0 } at zoom 1 moves the viewport center to the left by 10 world units, so visible world content follows the drag direction.

    fitBounds(null) returns the current viewport normalized through the same validation and clamping path. Zero-size bounds center on the point and use the clamped maximum zoom.

    Set theme.node.shape or attributes['graphora:style'].shape to circle (default), square, diamond, or rounded-rect. Radius defines the half-size of the first three; rounded rectangles use the snapshot's size (the engine currently resolves this to diameter × diameter). Fill, stroke, opacity, labels and interaction emphasis continue to compose normally; interaction overlays cannot switch shapes.

    theme.edge.shape and edge style attributes accept line and curve. The existing curved flag remains supported; when both occur in one input, shape wins. Parallel/reciprocal routes and self-loops still route automatically.

    Use isBuiltInNodeShape(value) and isBuiltInEdgeShape(value) to validate untyped inputs. Unknown values resolve to circle/line. The immutable BUILT_IN_NODE_SHAPES and BUILT_IN_EDGE_SHAPES arrays support application controls; arbitrary Canvas callbacks are intentionally outside this registry.

    Drawing, fill picking and endpoint clipping share geometry. Stroke outside the fill is excluded from picking and clipping. Labels and layout bounds retain the current radius-based policy; independent engine dimensions, image nodes, icons and custom visual callbacks are future work. See the vanilla node-shapes example for all forms with directed, reciprocal and self-loop edges.

    Optional RenderEdge.route supplies a world-space center-anchored polyline. Canvas clips endpoint legs and shares its resolved segments across draw, pick, arrows, fallback labels and bitmap export. RenderLabel.layoutCenter overrides automatic anchor selection with centered screen-sized text; visibility and collision policy still apply. Edges without usable routes keep the existing line/curve/loop fallback. See layout artifacts.

    The engine prepares snapshots with freezeRenderSnapshot. Direct renderer users can call this helper to copy and freeze rendering geometry, styles and camera state while retaining opaque application data and attributes. Reuse the returned snapshot until rendering fields change; then prepare a new snapshot. Arbitrary mutable snapshots remain supported through the uncached reference path.

    Canvas retains one prepared scene, reuses edge geometry, lazily indexes picking candidates and skips offscreen primary shapes. Precise hit testing and original draw/tie order are preserved. Labels retain their full placement pass, including visible labels whose owners are offscreen. Camera, graph, style, interaction or view changes produce a new engine snapshot and replace the cache. Unmount and destroy release the retained snapshot and scene; implicit picking after unmount returns the canvas until a new snapshot is rendered.

    labels: { mode: 'placement' } measures actual Canvas text bounds and searches positions around visible nodes and edge paths. Priorities, deterministic check limits and a time cutoff produce explicit placement/suppression diagnostics. getLabelPlacements() exposes the latest immutable report; resize/unmount/destroy clear it. placeRenderLabels(snapshot, measure, options) exposes the same policy with an application metrics provider. See automatic label placement for defaults, fonts, candidate limits and explicit layout centers.

    Use display detail for explicit runtime modes, zoom/count hysteresis and direct renderer policy decisions.

    CanvasGraphRenderer.addLayer installs Canvas or HTML definitions with scoped mount resources and enable/disable/dispose handles. See custom layers for coordinates, ordering, input ownership, invalidation and export omissions.

    CanvasGraphRenderer
    AutomaticLabelOptions
    CanvasGraphLayer
    CanvasGraphRendererOptions
    CanvasPickResult
    DisplayDetailContext
    DisplayDetailDecision
    DisplayDetailOptions
    EdgePickResult
    EdgeStyleInput
    FitBoundsOptions
    GraphImageExport
    GraphImageExportOptions
    GraphLayerContext
    GraphLayerHandle
    GraphLayerMountContext
    GraphRenderer
    HtmlGraphLayer
    InteractionItemStyleInput
    InteractionRenderState
    InteractionRenderStateInput
    InteractionStyleInput
    LabelPlacementOptions
    LabelPlacementReport
    LabelRectangle
    MissPickResult
    NodePickResult
    PickResultBase
    RenderArrowStyle
    RenderEdge
    RenderLabel
    RenderLabelPlacement
    RenderNode
    RenderSize
    RenderSnapshot
    RenderThemeInput
    RenderViewIdentity
    ResolvedEdgeStyle
    ResolvedInteractionStyleSet
    ResolvedLabelStyle
    ResolvedNodeStyle
    ResolvedTheme
    ResolveEdgeStyleInput
    ResolveLabelStyleInput
    ResolveNodeStyleInput
    ScreenPoint
    ViewportChangedEvent
    ViewportEventBase
    ViewportFitEvent
    ViewportPadding
    ViewportPannedEvent
    ViewportSize
    ViewportState
    ViewportZoomedEvent
    WorldPoint
    BuiltInEdgeShape
    BuiltInNodeShape
    CanvasLayerOrder
    DisplayDetailMode
    EdgePathGeometry
    GraphImageExportType
    GraphLayer
    GraphLayerCoordinates
    LabelBoundsMeasurer
    LabelStyleInput
    NodeStyleInput
    PickResult
    RenderItemKind
    ViewportChangeReason
    ViewportEvent
    ViewportPaddingInput
    ViewportPartial
    BUILT_IN_EDGE_SHAPES
    BUILT_IN_NODE_SHAPES
    DEFAULT_INTERACTION_RENDER_STATE
    DEFAULT_RENDER_THEME
    DEFAULT_VIEWPORT_PADDING
    DEFAULT_VIEWPORT_STATE
    GRAPHORA_DARK_THEME
    GRAPHORA_LIGHT_THEME
    GRAPHORA_STYLE_ATTRIBUTE
    rendererPackageDependencies
    rendererPackageInfo
    rendererPackageName
    clampViewportZoom
    clampZoom
    createViewport
    exportGraphImage
    fitBounds
    freezeRenderSnapshot
    isBuiltInEdgeShape
    isBuiltInNodeShape
    mergeInteractionRenderState
    normalizeDisplayDetailOptions
    panBy
    placeRenderLabels
    resolveDisplayDetail
    resolveEdgeGeometries
    resolveEdgeStyle
    resolveInteractionStyles
    resolveLabelStyle
    resolveNodeStyle
    resolveRenderTheme
    resolveVisibleLabelIds
    screenToWorld
    setViewport
    worldToScreen
    zoomAt