Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

.nornir/plot-reasoning.md

Charting (plot + plotters + pop) — reasoning

Class: reasoning. The why behind the charting subsystem and the shapes we rejected. The descriptive shape is plot-design.md; the how-to is plot-guide.md.

Why three crates, not one charting crate

The three cover different kinds of "numbers on screen" with genuinely different backends: facett-plot wraps an existing interactive widget (egui_plot) for the line/bar case it already does well; facett-plotters hand-paints the five statistical charts egui_plot has no widget for; facett-pop is a fed progress display, not a chart of a dataset at all. Fusing them would drag egui_plot into the hand-painted charts (which don't need it) and force the progress viewer to pretend to be a plot. Three small crates keep each dependency where it is actually used, and a host pulls only the one it needs.

Why guest-wrap egui_plot for line/bar (not hand-paint them too)

Line and bar charts with pan/zoom/pinch and a legend are exactly what egui_plot already does robustly. Re-implementing that would be a large surface to test for no gain. So PlotView wraps it and confines the one awkwardness — egui_plot owns the continuous gesture in its own widget memory — to a single per-frame mirror: the live drag/scroll/pinch is re-emitted as one Msg::SetViewport per frame, and an idle frame emits nothing so auto-fit stays auto-fit. That mirror is the price of reusing the widget while still keeping the viewport observable in the Model (FC-1/FC-3).

Why the statistical charts hand-paint on egui::Painter (no plotters crate)

Despite the crate name, facett-plotters depends on no chart library — each chart is a pure data → geometry function painted straight onto egui::Painter. This keeps every chart deterministic and independently unit-testable (the geometry function has no egui in its signature), lets the tooltip/hover be modelled as a real Msg::Hover under the FC contract, and avoids binding facett's look to a foreign renderer's styling. Colour comes from a theme-derived Ramp (or hash_color) so the charts recolour with the rest of the product instead of shipping a fixed palette.

Why hover is the last frame's hit-test (the one-frame lag)

Each statistical view hit-tests the current pointer against geometry computed fresh, but paints its tooltip from the persisted state.hover and emits a Hover Msg only when the hit index actually changed. Storing the hover in the Model (rather than reading it ambiently mid-paint) is what keeps view pure (FC-9) and the hover observable/replayable — the cost is that the tooltip reflects the previous frame, which at interactive frame rates is invisible and is the correct trade for determinism.

Why ProgressView is its own Model

facett-pop has no canvas gesture — it is written to, not interacted with — so its whole struct is already Serialize + Deserialize + Clone + PartialEq, and Elm::state(&self) returns self. Splitting a separate Model out would be ceremony with no payoff: there is no render-only ephemera to keep out of the serialized truth. The Msg vocabulary (Upsert/Advance/Remove/…) is deliberately a data-feed surface, so a host or a headless driver pushes progress through the same seam a test does, and is_idle() is a typed quiescence flag (FC-8) a caller polls instead of parsing the free-text phase.

Rejected: .scalable() / .selectable() on the chart caps

Even though a plot holds a zoom viewport and every statistical view holds a hovered index, none declares scalable or selectable. Zoom is the chart's own gesture (mirrored to the viewport), not the deck's uniform Ctrl-+/- scale; and a transient hover is not a persistent selection the deck should route clipboard against. Declaring those caps would invite the deck to drive a scale/selection the view doesn't actually own, so the caps stay themeable + copyable + resizable — exactly what the deck can correctly drive.