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:
- Render or measure the host surface.
- Extract annotation anchors and obstacles with the matching adapter.
- Resolve prepared annotations with validation and layout-quality assertions.
- 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
viewBoxandpreserveAspectRatiowith the host SVG or graph coordinate space. - Validate generated targets before layout.
- Use
assertAnchorValidationReportorformatAnchorValidationReportto 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.manualonly for editorial callouts that must stay in a specific position. - Persist annotations and manual coordinates in host state, not in this package.
- Run
resolvePreparedAnnotationLayoutwithassertValidation,assertTargetAlignment, andassertQuality, or runevaluateAnnotationLayoutdirectly, 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.