DOCUMENTATION / PLACEMENT-QUALITY

Placement quality evaluation

On this page

This evaluation checks authored host geometry against semantic quality budgets and compares normal lean output, diagnostics: { candidates: 8 }, and diagnostics: { candidates: 'all' } at the same solver settings. It complements the performance stress benchmark: known-feasible fixtures must fail when notes overlap, overflow bounds, disappear, shrink, or occlude host content.

These are synthetic fixtures authored for this package. They are not independent adoption evidence, upstream examples, or an external competitor comparison. Consumer adoption requires independent human use and review in a real host.

Runtime quality and original inputs

Anchor validation and placement quality answer different questions. An adapter's assert: true validates its selected annotation targets; it does not certify unselected host obstacles. For a prepared adapter result, use the existing resolvePreparedAnnotationLayout(prepared, { bounds, assertQuality: true }) helper to check all included prepared and additional obstacles as well as the resolved placement.

The helper normalizes negative obstacle dimensions just as the solver does, but passes the full original list to quality instead of the solver's filtered list. Its invalid-box errors identify the original combined obstacleIndex; invalidBoxCount also covers invalid effective bounds and note boxes. Invalid obstacles make hostContentClear false: unknown geometry is not evidence that host content is unobscured. Invalid boxes are excluded from numerical overlap calculations without being hidden from the diagnostics.

Direct resolveAnnotationLayout remains permissive: it normalizes obstacles and filters malformed boxes. A report created only from that resolved layout cannot recover discarded inputs. To inspect the original input list explicitly, pass it to the evaluator:

const layout = resolveAnnotationLayout({ annotations, bounds, obstacles });
const quality = evaluateAnnotationLayout(layout, { obstacles });
assertAnnotationLayoutQuality(quality);

For the direct evaluator, supplied boxes must already have nonnegative dimensions; finite zero-size boxes remain valid. Bounds and rectangle far edges must be finite. includePreparedObstacles: false intentionally excludes prepared obstacles from both placement and inspection; any additionalObstacles are still inspected. Reports describe the geometry supplied to them, not a fresh measurement of host state or an accessibility certification.

Numerical range and saved reports

Quality measurements use JavaScript numbers. Finite box fields and far edges do not guarantee finite area products, distance differences, or accumulated totals. For example, the intersection of two identical 1e200 by 1e200 boxes is Infinity; enough individually finite intersections can also overflow a total. Finite results depend on dimensions, intermediate operations, and the number of contributions, as well as coordinate magnitude. Use values whose arithmetic and reported totals remain finite when you need finite measurements. The evaluator does not provide arbitrary precision, detect all precision loss or underflow, or repair geometry lost to rounding at extreme translations. An overflowing intermediate difference can produce Infinity even if multiplying the exact difference by a tiny dimension would yield a finite real area. A numeric diagnostic describes the computed value, not an arbitrary-precision area bound.

The evaluator retains infinite and NaN results without saturation. When an existing report field is not finite, the optional numericDiagnostics array records its path and string value ("Infinity", "-Infinity", or "NaN"). Paths use score, metrics.<field>, issues[<index>].area, issues[<index>].amount, or readability.<field>. Issue indices refer to the original report's ordered issue list. Entries follow score, metrics declaration order, issue order (area before amount), then readability declaration order. Reports with only finite numeric fields omit this property entirely. formatLayoutQualityReport includes these diagnostics separately from layout issues, even when maxIssues hides issue details. They do not affect issue counts or assertion severity policies.

Native JSON.stringify still writes null for numbers that are not finite. The diagnostics survive serialization and distinguish those values from missing or zero measurements. Keep them with the saved report; they do not automatically rehydrate null into JavaScript numbers. For example, a saved report can contain "obstacleOverlapArea": null together with { "path": "metrics.obstacleOverlapArea", "value": "Infinity" } in numericDiagnostics. Parsed JSON is encoded evidence, not a restored typed LayoutQualityReport suitable for passing directly to report helpers.

Numeric diagnostics do not add geometry issues or change invalidBoxCount, geometryValid, hostContentClear, ok, or scoring. An infinite positive overlap still reports its overlap issue and fails the applicable geometry or host-content check. An infinite total connector length can coexist with ok: true, because optional readability evidence is separate from placement validity. Its readability.complete is false. An individual route whose length cannot be represented is already excluded by readability measurement and contributes to invalidConnectorCount; its omitted length is not a numeric report field and does not receive a numeric diagnostic. A missing numericDiagnostics property therefore does not imply complete readability or numerically exact geometry.

Finite zero-size boxes remain structurally valid. The existing bounds metric sums outside edge strips: a zero-width box can still have a positive strip along its height, and a zero-height box along its width. A strip with zero area remains zero even when its distance would overflow, preventing Infinity * 0 from hiding another nonzero strip. Negative dimensions and far edges that are not finite retain their existing invalid-box diagnostics.

Reproduce

Use the repository's existing dependency and build workflow. From the checkout:

npm run build
npx vitest run test/scripts/placement-quality.test.ts
node scripts/placement-quality-corpus.mjs
node scripts/verify-placement-quality.mjs

The CLI defaults to .tmp/placement-quality. To preserve results across builds (the build's clean step removes .tmp), choose an explicit directory:

node scripts/placement-quality-corpus.mjs --output-dir /tmp/annotation-quality-review
node scripts/verify-placement-quality.mjs --output-dir /tmp/annotation-quality-review

npm run test:quality builds the package and runs this corpus. Node 22 CI retains its JSON and gallery alongside browser evidence. The existing stress benchmark remains a separate latency guard.

Open index.html in the output directory. Each reference/bounded pair includes complete host shapes, host labels, SVG annotation notes, and connectors. The dashed boundary shows the padded placement bounds. Images retain their native dimensions instead of shrinking chart labels into unreadable side-by-side thumbnails. Wide displays show comparisons side by side; smaller displays stack them in focusable scroll regions. Standalone .svg files contain their own annotation styles and can be inspected separately. results.json records every case, including failures; generation writes artifacts before asserting quality budgets, and both scripts exit nonzero on a failed verification.

The standalone verifier reads the saved JSON, compares its fixture configuration with the checked-in corpus, and recomputes diagnostics from selected geometry. It does not trust a saved passed label or saved measurement totals. It does not rerun placement or authenticate arbitrary third-party reports. For new source, rebuild and generate fresh evidence rather than only verifying old output.

Authored cases and budgets

test/fixtures/placement-quality/corpus.json owns fixed host geometry, stable note IDs, text, note sizes, obstacles, viewport bounds, and the reviewed expectation. There is no randomness or browser-dependent note measurement in the solve.

Host Notes Bounds in SVG/CSS pixels Reviewed expectation
Monthly bar chart 5 960 × 600; 480 × 980 Zero note overlap, bounds overflow, and host occlusion
Operating report 5 960 × 600; 480 × 980 Same zero budgets
Architecture graph 6 960 × 600; 480 × 980 Same zero budgets
Long-text report 5 960 × 980; 480 × 980 Same zero budgets; full multi-line text
Impossible crowded review 5 400 × 220; 240 × 220 Nonzero note-overlap errors must remain visible

All cases have 16-pixel viewport padding. The narrow chart is authored as a horizontal-bar reflow. The long-text report has rows tall enough for its 140-pixel notes. These host choices are part of the fixture, not runtime layout features. All feasible obstacle-occlusion budgets are zero. Connector hits remain reported; they are not silently treated as proof of readability.

The impossible cases retain all five 180 × 100 notes. Their total note area exceeds the padded viewport area, proving that disjoint notes cannot fit. A passing evaluation means the expected failure was detected; the gallery labels these cases EXPECTED PROBLEMS — NOT A GOOD LAYOUT. Removing a note, hiding it, reducing its box, or suppressing its overlap diagnostics is not an acceptable fix.

During initial fixture review, the wide long-text host's 105-pixel row spacing failed the zero-overlap budget for 140-pixel notes, and narrow vertical bars caused neighboring-bar occlusion. The authored host geometry was corrected to 175-pixel rows and horizontal narrow bars. Note counts and zero budgets were retained. Future fixture adjustments should preserve failed evidence and explain the host or semantic reason; do not increase budgets to hide a real quality regression.

Comparison and measurements

All three configurations use the same notes, obstacles, bounds, and enabled refinement: two passes over at most eight candidates per annotation. The normal run omits diagnostics; the other runs collect eight or all ranked alternatives. This compares diagnostic output while keeping the refinement frontier identical. The normal output contains no diagnostic candidate arrays. It does not claim that eight-candidate refinement equals unrestricted refinement over every candidate.

Selected IDs, subjects, anchor points, note boxes, connector points and paths, placement sides, alignments, offsets, and scores must match exactly, as must quality metrics and issue lists. Retained candidate lists intentionally differ: normal output retains none, bounded diagnostics retain eight, and full diagnostics retain all alternatives. No internal optimization API is required.

The JSON contains source commit, a dirty flag for evaluated source/fixture/script paths, Node/platform/architecture, full fixture inputs, resolved counts, selected geometry, candidate counts and bytes, and timing observations. Candidate bytes are the sum of UTF-8 JSON encodings of each retained per-note candidate array. They are not JavaScript heap measurements. Selected geometry is stored without candidate arrays; the gallery can be reproduced from the fixture and geometry.

Each configuration gets one warmup solve and three measured solves. Samples, warmup duration, and median are in milliseconds and cover resolution/refinement only; diagnostics, serialization, and rendering are outside that timer. All samples are retained. There are no machine-specific timing pass limits. Measured solves must produce deterministic selected geometry. Tests exclude timing fields from deterministic equality assertions.

Area metrics are in squared SVG/CSS pixels:

  • Note-overlap area sums pairwise rectangle intersections, counting a pair once.
  • Obstacle-occlusion area sums note/obstacle intersections; overlapping obstacles can count the same region more than once.
  • Bounds-overflow area measures each note's true rectangular area outside the padded viewport, counting corner overflow once. The package's original report is also retained; its overflow metric sums outside edge strips.
  • Connector-obstacle and connector-note hits count intersecting polyline segments, using the package diagnostics. Own-note connector contacts are ignored by the package's default. A connector starting inside or touching its host subject's obstacle can produce a hit; this is not necessarily an unrelated crossing.

Resize evidence narrows only the viewport's right boundary by eight pixels, keeping host geometry and note sizes fixed. Stable IDs match before/after layouts. JSON records both bounds, per-note dxPx, dyPx, Euclidean movement in pixels, mean and maximum distance, and placement-side changes. Both resized configurations must still have exact parity and satisfy their fixture expectations. Movement is an observation without a pass threshold; zero is a valid observation. This is not a responsive host reflow test or an incremental-session continuity claim.

Limits and human review

Rectangle diagnostics do not establish visual quality, accessibility, semantic anchor correctness, or consumer usefulness. Inspect the full gallery for text legibility, connector ambiguity, host content visibility, and sensible note/host relationships. Authored note sizes and character wrapping do not measure actual font glyphs, localization, zoom, device pixel ratio, or a consumer's DOM.

Connector diagnostics test polyline segments, including boundary contacts. They do not count connector/connector crossings, graph-edge crossings, stroke-width occlusion, or curved-path interior geometry. Architecture edges are rendered host content, but only authored rectangular node obstacles participate in avoidance. The static SVG gallery is not a keyboard, screen-reader, contrast, or responsive accessibility audit. Consumer adoption needs independent human use; these geometric checks and manual specimens cannot substitute for that evidence.