Make best legend placement content-aware

Merged
AL
2mo ago
+4804-136

Closes #485

What changed

The existing loc="best" pass only scored sampled point anchors once during payload construction. It could miss line crossings, area fills, marker extents, bar rectangles, and annotations, and the browser had no automatic-placement intent left to reconsider after layout or view changes.

This PR:

  • measures the real legend footprint and scores the nine standard in-plot candidates against bounded emitted geometry for line, scatter, area, bar/column/histogram, density, and annotations;
  • ships a concrete deterministic fallback plus auto_loc="best", so older clients and static output remain safe while the live renderer can refine placement;
  • re-scores a fixed 96×72 rendered-pixel occupancy raster after responsive resize, settled pan/zoom, legend toggles, data updates, and LOD updates—never on every gesture frame and never by rescanning source rows;
  • preserves explicit locations, anchors, and polar legend placement exactly;
  • remeasures automatic legends against final SVG/raster export dimensions; and
  • documents the placement, tie-break, wire, lifecycle, and bounded-work contracts.

Before / after

Same chart, data, 760×440 chart size, 800×500 Chrome viewport, and DPR 1. The baseline is commit 036271ac; both captures used their tree's own standalone client.

Before — legend obscures the rising lineAfter — legend moves to the empty corner
Before: upper-right legend overlaps the final rising line segmentAfter: automatic legend moved to the empty upper-left corner

Capture provenance, DOM locations, dimensions, and SHA-256 hashes

Verification

  • uv run --with pre-commit pre-commit run --all-files
  • uv run ruff check .
  • uv run ruff format --check .
  • .venv/bin/ty check
  • npm run typecheck
  • npm run build
  • full suite: 4,314 passed, 108 skipped
  • focused real-Chromium adaptive-placement suite: 12 passed

Summary by CodeRabbit

  • New Features

    • Added content-aware automatic placement for unanchored Cartesian legends using loc="best".
    • Legends avoid chart marks and annotations based on rendered geometry.
    • Placement updates after resizing, view changes, data updates, and responsive exports.
    • Added stable placement behavior with fallback when measurements are unavailable.
    • PNG and SVG exports recalculate placement when dimensions change.
    • Explicit locations, anchors, and polar legends remain unchanged.
  • Documentation

    • Added guidance and examples for automatic legend placement and its behavior.