Migrating to the 0.7 API
On this page
This guide moves 0.6 code to the 0.7 API, which 0.8 and 0.9 retain with additive capabilities. The confirmed stable npm release is 0.9.1 on latest:
npm install @ponchia/annotations@0.9.1
Use the 0.9.1 API reference for that package's API. Its archive and signed provenance were verified; see release readiness for the publication evidence. A future source commit or tag alone does not establish registry publication.
Why change the contract?
A renderable layout should not require retaining every discarded placement. Diagnostic retention should not change the chosen layout. A React renderer should have one source of geometry, not both a resolved layout and a second copy of its input. D3 migration helpers should be opt-in rather than occupying the main import surface.
Normal layouts are lean
The ordinary call keeps its name and basic input shape:
import { resolveAnnotationLayout, renderAnnotationsSvg } from '@ponchia/annotations';
const layout = resolveAnnotationLayout({ annotations, bounds, noteSizes });
const svg = renderAnnotationsSvg(layout);
layout.annotations contains the chosen placements. placement.candidates is
removed. layout.diagnostics is absent unless requested. This changes both
the default output and the work performed: unrefined solves need the winning
candidate, not a fully routed graph of all alternatives.
To inspect alternatives explicitly:
const inspected = resolveAnnotationLayout({
annotations,
bounds,
noteSizes,
diagnostics: { candidates: 8 }
});
for (const diagnostic of inspected.diagnostics!.annotations) {
console.log(diagnostic.id, diagnostic.candidates);
}
Use diagnostics: { candidates: 'all' } only when exhaustive alternative
inspection is needed. Numeric limits must be positive safe integers. Diagnostic
entries are an ordered array of IDs and candidates, not an object keyed by
untrusted IDs. IDs such as __proto__ remain valid.
Separate diagnostics from refinement
Remove placement.maxCandidates from global and per-note preferences. It mixed
output retention with the refinement search space. The runtime rejects it with
migration guidance rather than silently ignoring an old JavaScript option.
Choose the solver's refinement frontier independently:
const layout = resolveAnnotationLayout({
annotations,
bounds,
refinement: { passes: 2, maxCandidatesPerAnnotation: 32 },
diagnostics: { candidates: 8 }
});
Omitting diagnostics or requesting 1, 8, or all diagnostic candidates must not
change these winners. The internal refinement frontier is still 32. Diagnostics
record the ranked alternatives from the greedy pass; a refined final placement
is authoritative in layout.annotations, and may not appear in a short
retained diagnostic list.
refineAnnotationLayout(layout) is no longer a root export. Keep the host's
annotation inputs and request refinement during resolution. A render snapshot
is not a reusable solver workspace. Persist authored annotations, not diagnostics.
Low-level candidate inspection remains explicit:
allPlacementCandidates(candidateInput, { limit: 8 });
Its separate limit parameter replaces candidateInput.placement.maxCandidates.
It is an exact ranked-result limit, not a hard latency budget. With no limit,
this low-level diagnostic operation deliberately returns all alternatives.
React: inputs or a layout, never both
The convenience path continues to own resolution and optional measurement:
<AnnotationLayer annotations={annotations} bounds={bounds} measure="dom" />
For externally resolved or incremental layouts, replace resolvedLayout with
layout and remove duplicated solver inputs:
<AnnotationLayer
layout={session.layout}
editable={{ includeAnchor: true }}
previewEdits
onEditEnd={handleCommittedEdit}
/>
The renderer derives bounds, annotation sources and note sizes from that layout.
Do not also pass annotations, bounds, placement, noteSizes, refinement,
obstacles, padding, diagnostics, or measure. The public TypeScript union
and runtime checks reject mixed modes. The render-only mode does not solve or
measure; the host updates its session when it accepts an edit.
SVG and externally resolved React debug rendering can display only diagnostics already present. It never secretly resolves another layout to obtain them.
Interactive hosts enabling strict assertQuality should check a proposed
onEditEnd patch with evaluateAnnotationLayout before committing it.
The renderer's preview is intentionally provisional; an invalid drop should
leave the last valid source state intact and receive accessible feedback. See
the React edit-admission guide.
The example-only Worker protocols use full snapshots when diagnostic graphs are present, preserving aliases between candidates and winning geometry. Lean responses still use the existing safe structural-sharing/patch path. This is a consumer recipe, not a new public Worker API.
Identity, typography and quality
- Annotation IDs must now be unique during every layout resolution and
incremental session update. Duplicate IDs are rejected before state changes.
IDs may still contain arbitrary Unicode and property names like
__proto__. Equal-priority IDs are compared by fixed UTF-16 code units, not an OS-dependent locale. - Text wraps whole Unicode grapheme clusters, keeping accented characters,
joined emoji and modifiers together.
estimateNoteSizestill approximates glyph widths; prefer providednoteSizesor Reactmeasure="dom"for font-accurate layout. evaluateAnnotationLayoutnow returnsgeometryValid(finite, bounded, disjoint note boxes),hostContentClear(notes do not cover declared host obstacles), andok(both). Obstacle overlap remains a diagnostic warning but failsokand default assertions. Connector intersections remain informational. A passing report does not certify accessibility.
Metadata and Worker transport
A lean layout is not automatically serializable. Resolved annotations
retain their source annotation objects, including arbitrary application
metadata and note metadata. Cyclic data can make JSON.stringify fail;
functions can make structuredClone and Worker.postMessage fail.
Worker consumers must keep their authored inputs and metadata structured-clone
safe or explicitly project their own transport-safe representation. Do not
expect the engine to silently strip callbacks, invoke toJSON, or delete
application data. Persist only the authored input you actually own; if a Worker
needs a projection, validate the projection at the host boundary.
D3 migration has its own import
Change migration-specific imports:
import { annotationFromD3Style, createD3StyleAnnotationBuilder }
from '@ponchia/annotations/d3';
import type { D3StyleAnnotationInput } from '@ponchia/annotations/d3';
The helper behavior is unchanged. Normal pointCallout, regionCallout,
encircleCallout, thresholdAnnotation, and badgeAnnotation presets remain
at @ponchia/annotations. Importing the root no longer loads the migration
implementation through a presets barrel.
Upgrade verification
Run your typecheck against the packed preview, then exercise ordinary layout,
measured React content and committed edits. Search host code for
placement.candidates, maxCandidates, resolvedLayout, and D3-specific root
imports. maxCandidatesPerAnnotation is still a valid refinement option.
Compare chosen geometry and quality at the same refinement settings. The old
0.6.x retention option could limit refinement, so an old eight-candidate setup
should set refinement.maxCandidatesPerAnnotation: 8 explicitly when retaining
that search policy. Do not compare only serialized sizes and assume quality is
the same. The repository includes parity, negative API/type, browser and
packed-consumer tests for this migration.
Independent Vega coordinates
The coordinate helpers vegaScaleCoordinate, vegaScaleInterval and
vegaViewOrigin are additive. Hosts can remove copied per-axis band handling and
synthetic zero-scale views. Existing bulk adapter behavior is unchanged. Use
{ band: 'center', coerce: true } for JSON-authored point callouts, and the
interval helper for inclusive regions with either endpoint order.