DOCUMENTATION / INTEGRATION-RECIPES

Integration Recipes

This guide is for first-use integration in a host app. The package expects the host to render the chart, diagram, graph, report, or SVG first, then pass measurable geometry into the annotation layer.

If you are still choosing the right subpath, start with docs/context-quickstart.md.

Common Flow

Use the same flow for generated surfaces and static reports:

  1. Render or measure the host surface.
  2. Extract annotation anchors and obstacles with the matching adapter.
  3. Resolve prepared annotations with validation and layout-quality assertions.
  4. Render the annotation SVG or React layer over the same coordinate space.
import {
  generatedSurfaceLayoutDefaults,
  renderAnnotationsSvg,
  resolvePreparedAnnotationLayout
} from '@ponchia/annotations';
import '@ponchia/annotations/bronto.css';

const resolved = resolvePreparedAnnotationLayout(prepared, {
  ...generatedSurfaceLayoutDefaults({
    anchorLabel: 'Generated anchors',
    includeInfo: true,
    layoutLabel: 'Surface annotations'
  }),
  bounds: { x: 0, y: 0, width: 640, height: 360 },
  padding: 16,
  targetAlignmentTargets: [{
    id: 'api-note',
    expected: 'rendered API node',
    box: { x: 96, y: 48, width: 86, height: 44 }
  }],
  assertTargetAlignment: {
    label: 'Generated target alignment',
    failOnWarnings: true
  }
});

if (!resolved.validation.ok || !resolved.targetAlignment?.ok || !resolved.quality.ok) {
  console.info(resolved.validationSummary);
  console.info(resolved.targetAlignmentSummary);
  console.info(resolved.qualitySummary);
}

hostOverlay.innerHTML = renderAnnotationsSvg(resolved.layout, {
  title: 'Annotations',
  includeEditHandles: true,
  editHandleTabIndex: 0,
  markerIdPrefix: 'host-annotations',
  noteTabIndex: 0,
  preserveAspectRatio: 'xMidYMid meet'
});

Use editHandleTabIndex only when the host app wires static SVG edit behavior with pointer or keyboard event delegation. React edit handles include their own pointer and keyboard callbacks through AnnotationLayer.

For first integrations, prefer resolvePreparedAnnotationLayout with generatedSurfaceLayoutDefaults, assertValidation, assertTargetAlignment, and assertQuality so missing anchors, anchors that no longer match generated targets, and poor note placement fail in the same place. Add failOnWarnings: true to generatedSurfaceLayoutDefaults when fallback anchors, such as unmeasured React Flow handles, should fail the run instead of using a node-side midpoint. The lower-level prepare*Annotations helpers can still accept assert when a host wants to validate anchors before choosing layout bounds.

When the host can provide expected generated geometry, pass targetAlignmentTargets and assertTargetAlignment to the prepared-layout helper, or call the lower-level target-alignment diagnostics directly in tests. This proves prepared anchors still land on the generated mark, node, handle, edge, or route the host meant to annotate:

import {
  assertAnchorAlignmentReport,
  evaluateAnchorAlignment
} from '@ponchia/annotations';

const alignment = evaluateAnchorAlignment(prepared.annotations, [{
  id: 'api-note',
  expected: 'rendered API node',
  box: { x: 96, y: 48, width: 86, height: 44 }
}]);

assertAnchorAlignmentReport(alignment, { label: 'Generated target alignment' });

Automatic placement comes from placement.side, placement.align, offset, crossOffset, priorities, note sizes, bounds, existing placed notes, and obstacles. Manual placement is explicit viewBox geometry and works through the same renderer and connector path:

const manualPlacement = {
  placement: {
    manual: { x: 420, y: 48, side: 'left' }
  }
};

Store manual coordinates in the same coordinate system as the overlay viewBox. When the host surface resizes, remeasure the host geometry and rerun layout.

SVG Figures

For a plain SVG figure, put the annotation layer in the same viewBox and use the same preserveAspectRatio as the host SVG. Host apps can supply anchors directly, or use the DOM/SVG utilities when targets already exist in the SVG.

import { resolveAnnotationLayout, renderAnnotationsSvg } from '@ponchia/annotations';
import { annotationFrameFromSvg } from '@ponchia/annotations/dom';

const frame = annotationFrameFromSvg(hostSvg, { padding: 24 });
const layout = resolveAnnotationLayout({
  annotations: [{
    id: 'threshold-note',
    anchor: { type: 'box', box: { x: 140, y: 120, width: 180, height: 64 } },
    note: { title: 'Threshold band' },
    subject: { geometry: { type: 'band', width: 180, height: 64 } }
  }],
  bounds: frame.bounds
});

overlay.innerHTML = renderAnnotationsSvg(layout, {
  preserveAspectRatio: frame.preserveAspectRatio
});

DOM And Reports

Use prepareDomAnnotations when annotations target rendered DOM boxes or report regions. The adapter measures getBoundingClientRect() relative to a coordinate-space element.

import { prepareDomAnnotations } from '@ponchia/annotations/dom';

const surface = document.querySelector('[data-report-surface]')!;
const prepared = prepareDomAnnotations(surface, [
  {
    id: 'risk-note',
    selector: '[data-region="risk"]',
    coordinateSpace: surface,
    note: { title: 'Risk region' }
  },
  {
    id: 'manual-report-note',
    selector: '[data-region="evidence"]',
    coordinateSpace: surface,
    note: { title: 'Evidence' },
    placement: { manual: { x: 460, y: 72, side: 'left' } }
  }
], {
  obstacles: [{ selector: '[data-region]', coordinateSpace: surface, inflate: 4 }]
});

Mixed DOM/SVG Report Surface

A report can use one prepareDomAnnotations call for both DOM regions and SVG marks when every selector lives under the same root. Keep the root broad enough to find all targets, and set coordinateSpace per spec and obstacle.

import { prepareDomAnnotations } from '@ponchia/annotations/dom';

const section = document.querySelector('[data-report-section]')!;
const chart = section.querySelector('svg[data-chart]')!;

const prepared = prepareDomAnnotations(section, [
  {
    id: 'chart-bar-note',
    selector: '[data-series="annotations"]',
    coordinateSpace: chart,
    note: { title: 'SVG chart mark' },
    subject: { shape: 'rect', padding: 4 }
  },
  {
    id: 'table-row-note',
    selector: '[data-import-row="annotations"]',
    coordinateSpace: section,
    note: { title: 'DOM table row' },
    placement: { manual: { x: 498, y: 348, side: 'top' } }
  }
], {
  obstacles: [
    { selector: '[data-series]', coordinateSpace: chart, inflate: 3 },
    { selector: '[data-import-row]', coordinateSpace: section, inflate: 4 }
  ]
});

React

AnnotationLayer runs the same layout engine and adds DOM note measurement, custom note rendering, edit handles, and SSR-safe effects. Keep annotation data in host state; the package emits edit suggestions but does not persist them.

import { useState } from 'react';
import { applyAnnotationEdits, type Annotation } from '@ponchia/annotations';
import { AnnotationLayer } from '@ponchia/annotations/react';

export function AnnotatedSurface({ initial }: { initial: Annotation[] }) {
  const [annotations, setAnnotations] = useState(initial);

  return (
    <AnnotationLayer
      annotations={annotations}
      bounds={{ x: 0, y: 0, width: 640, height: 360 }}
      obstacles={[{ x: 260, y: 80, width: 120, height: 80 }]}
      measure="dom"
      noteTabIndex={0}
      editable={{ includeAnchor: true }}
      onEditEnd={(event) => {
        setAnnotations((current) => applyAnnotationEdits(current, event));
      }}
    />
  );
}

For a non-React authoring surface, keep the active handle in host state and use the root helpers to build the same commit-ready edit suggestion:

import {
  annotationEditHandles,
  applyAnnotationEdits,
  createAnnotationEditSession,
  resolveAnnotationLayout
} from '@ponchia/annotations';

const layout = resolveAnnotationLayout({ annotations, bounds, noteSizes });
const handle = annotationEditHandles(layout, { includeAnchor: true })[0]!;
const edit = createAnnotationEditSession({ layout, handle });

const dragEnd = edit.end({ x: edit.origin.x + 18, y: edit.origin.y + 10 });
const keyboardNudge = edit.delta({ x: 2, y: 0 }, 'end');

const afterDrag = applyAnnotationEdits(annotations, dragEnd);
const afterNudge = applyAnnotationEdits(afterDrag, keyboardNudge);

Vega And Vega-Lite

For Vega, prefer scenegraph anchors after the view has run, because they reflect generated marks, scales, padding, and layout. Vega-Lite consumers compile or render through public Vega-Lite/Vega APIs first, then pass the resulting Vega View or rendered SVG to this adapter.

import { assertAnchorValidationReport } from '@ponchia/annotations';
import { prepareVegaScenegraphAnnotations } from '@ponchia/annotations/vega';

const prepared = prepareVegaScenegraphAnnotations(view, [
  {
    id: 'peak-note',
    markName: 'points',
    datum: (datum) => datum?.id === 'peak',
    note: { title: 'Generated Vega mark' },
    placement: { side: ['right', 'top'] }
  },
  {
    id: 'manual-bar-note',
    markName: 'bars',
    datum: (datum) => datum?.id === 'target-bar',
    note: { title: 'Manual callout' },
    placement: { manual: { x: 420, y: 52, side: 'left' } }
  }
], {
  obstacles: { padding: 4 }
});

assertAnchorValidationReport(prepared.validation, { label: 'Vega anchors' });

Use prepareVegaSvgAnnotations when the host only has exported/rendered SVG. Match marks through selectors or public Vega SVG metadata such as mark name, mark type, and role. Metadata-matched anchors measure the concrete child mark when the wrapper contains exactly one mark element; use a selector or scenegraph datum predicate for multi-mark layers.

Mermaid

Mermaid annotations run after Mermaid renders SVG. Target stable rendered labels, generated node or edge ids, clusters, classes, data attributes, or explicit selectors. For rendered edge callouts, use edgeId when Mermaid's generated edge id is stable; otherwise use edgeSourceId and edgeTargetId to match the rendered edge by connected node ids. The adapter does not parse Mermaid source.

import { prepareMermaidAnnotations } from '@ponchia/annotations/mermaid';

const svg = document.querySelector('#diagram svg')!;
const prepared = prepareMermaidAnnotations(svg, [
  {
    id: 'api-note',
    label: 'API',
    coordinateSpace: svg,
    note: { title: 'Mermaid node' },
    placement: { side: ['right', 'bottom'] }
  },
  {
    id: 'manual-edge-note',
    edgeSourceId: 'api',
    edgeTargetId: 'report',
    kind: 'path',
    coordinateSpace: svg,
    note: { title: 'Important edge' },
    placement: { manual: { x: 360, y: 120, side: 'left' } }
  }
], {
  obstacles: { coordinateSpace: svg, inflate: 4 }
});

If labels are translated or user-authored, prefer node ids, edge ids, classes, or data attributes over label text. examples/mermaid-basic browser-verifies this with a localized Ingresso dati node annotated through [data-node-id="flow-intake"] instead of its rendered label.

For sequence diagrams, use rendered SVG hooks after Mermaid has produced the diagram: participant labels or ids for actors, data-message-id or selectors for message labels, and data-edge-id or explicit path selectors for message routes. Mermaid 11 sequence routes may render as line.messageLine0 or line.messageLine1, while older/exported SVG can use path equivalents; the adapter handles both. The default Mermaid obstacle extraction includes common sequence participant and message classes.

D2

D2 has two good integration paths. Use compiled diagram geometry when the host has access to the D2 compile result; use rendered SVG extraction when only the SVG is available. The adapter does not parse D2 source or execute D2.

import { prepareD2DiagramAnnotations } from '@ponchia/annotations/d2';

const prepared = prepareD2DiagramAnnotations(compiledDiagram, [
  {
    id: 'process-note',
    shapeId: 'process',
    note: { title: 'Compiled D2 shape' },
    placement: { side: ['right', 'top'] }
  },
  {
    id: 'manual-route-note',
    connectionId: 'api-report',
    note: { title: 'Routed connection' },
    placement: { manual: { x: 420, y: 148, side: 'left' } }
  }
], {
  obstacles: { includeConnections: true, padding: 4 }
});

For SVG-only D2 output, use prepareD2SvgAnnotations with selectors, shapeId, connectionId, classes, labels, or data-* hooks. Include connection obstacles when route labels or edge paths should affect placement. For route callouts, set kind: 'path' on a connectionId spec; when D2 wraps a route in a group, the adapter uses the child route <path> for the anchor.

React Flow

React Flow annotations should read public node, handle, edge, and viewport state from the host. The adapter maps graph coordinates through the viewport and uses measured handles when the host exposes them. Default edge anchors use measured sourceHandle and targetHandle centers when they are available; provide edgePoints when the host wants annotations to follow a custom routed edge path.

import {
  annotationEditPatch,
  type AnnotationEditEvent,
  type AnnotationEditPatch
} from '@ponchia/annotations';
import { prepareReactFlowAnnotations } from '@ponchia/annotations/react-flow';

const editPatches: Record<string, AnnotationEditPatch> = {};

const prepared = prepareReactFlowAnnotations({
  nodes,
  edges,
  viewport
}, [
  {
    id: 'review-node-note',
    nodeId: 'review',
    note: { title: 'React Flow node' },
    placement: { side: ['right', 'bottom'] }
  },
  {
    id: 'manual-handle-note',
    handle: { nodeId: 'review', id: 'approved', type: 'source', side: 'bottom' },
    note: { title: 'Approved path' },
    placement: editPatches['manual-handle-note']?.placement ?? {
      manual: { x: 380, y: 184, side: 'left' }
    }
  }
], {
  obstacles: { includeEdges: true, padding: 4 }
});

function onAnnotationEditEnd(event: AnnotationEditEvent) {
  editPatches[event.annotationId] = annotationEditPatch(event);
}

Handle specs can fall back to the requested node side before React Flow has measured handle bounds. Treat fallback diagnostics as warnings during initial render and rerun after the graph has measured. For zoom/pan, pass React Flow's public viewport state into the adapter on each layout. For editing, persist the suggested patch in host state and merge the placement back into the annotation spec; keep graph node/edge state in React Flow.

First-Use Checklist

  • Use public package imports only.
  • Align overlay viewBox and preserveAspectRatio with the host SVG or graph coordinate space.
  • Validate generated targets before layout.
  • Use assertAnchorValidationReport or formatAnchorValidationReport to turn adapter diagnostics into host-app errors or user-visible messages.
  • Pass host marks, nodes, regions, axes, shapes, and edges as obstacles when notes should avoid them.
  • Use automatic placement first; add placement.manual only for editorial callouts that must stay in a specific position.
  • Persist annotations and manual coordinates in host state, not in this package.
  • Run resolvePreparedAnnotationLayout with assertValidation, assertTargetAlignment, and assertQuality, or run evaluateAnnotationLayout directly, in tests or report builds for important output.

Passage comments in a host editor

Keep the document selector and thread state in the host. Measure the current selection only when it is visible, then put a pin in the same coordinate space:

import { measureRangeAnchor } from '@ponchia/annotations/dom';
import { AnnotationPin } from '@ponchia/annotations/react';
import '@ponchia/annotations/bronto.css';

function PassagePin({ range, openDiscussion }: { range: Range; openDiscussion: () => void }) {
const measured = measureRangeAnchor(range, { maxRects: 32 });
const firstLine = measured.rects[0];

return firstLine && measured.status === 'resolved' ? (
  <div style={{ position: 'fixed', inset: 0, pointerEvents: 'none' }}>
    <AnnotationPin
      point={{ x: firstLine.x + firstLine.width, y: firstLine.y }}
      label="Open discussion about this passage"
      style={{ pointerEvents: 'auto' }}
      onClick={openDiscussion}
    />
  </div>
) : null;
}

The host clips highlights to its scroll viewport and keeps pins inside the viewport. On a transformed canvas, use a fixed viewport overlay as above, or pass coordinateSpace and position the overlay in that element's local padding space. Keep pin targets unscaled when touch access matters. If several pins collide, group them or show a bounded subset and offer the complete thread list.

Do not retain a Range after an editor replaces its DOM. Reconstruct it from validated document offsets, resolve ambiguous or missing text explicitly, and measure again. The package never decides whether a discussion is resolved or whether a quotation still names the intended passage. Native button semantics provide keyboard activation; the host supplies panel focus return and any canvas-specific drag exclusion classes.