docs: a user guide covering getting started, architecture, embedding, data, interaction, styling and testing

This commit is contained in:
Martino Ferrari committed 2026-09-28 21:50:19 +02:00
1 parent d03f9ce51a
commit b7d0f901cc
7 files changed
+1082

No files matched your search

+161
View File
@@ -0,0 +1,161 @@
# Styling
Every colour and metric the renderer draws with lives in `render::Style`
(`include/plot/render/style.hpp`). A caller sets fields on `Renderer::style()`; the next frame
picks them up, because nothing here is baked into a shader.
Set colours and metrics on the **renderer**, not on the plot: `Renderer::draw()` copies its
`Style` into the plot and hands it the `ui_scale` *before* it lays out, so the strips, tick
densities and strokes the plot measures with are the values the renderer draws with. A plot
drawn without a renderer lays out with whatever `Plot::set_style()` was given.
## The unit rule: logical versus device
A metric a user perceives as a **size** — a font, a stroke width, a padding, a marker, a
swatch, the minimum spacing between ticks — is written in **logical** units, so a 12 px label
is the same physical size on a 1x and a 2x panel. The host states its display with `ui_scale`
(device pixels per logical unit); every consumer converts exactly once, through `device()`:
```cpp
f32 device(f32 logical) const noexcept {
return ui_scale > 0.0f ? logical * ui_scale : logical; // non-positive scale converts 1:1
}
```
`Plot::layout()` uses it to size the label strips, title strips and legend chrome; the renderer
uses it to convert widths and font px (the font is reset at `device(axis_font_px)`).
The one exception is `dense_threshold_spp`, which is a **density** (samples per *rasterised*
pixel), compared against a ratio computed from the framebuffer's own width. Scaling it would
make the chosen LOD level depend on the display rather than on the data, so it is used raw.
## `Style` fields
Colours (neither logical nor device — nothing to scale):
| field | default | meaning |
|---|---|---|
| `background` | `0x101418` | frame clear colour and legend/readout panel base |
| `grid_major` | `0x2A3038` | major (labelled-tick) grid lines |
| `grid_minor` | `0x1C2126` | minor (subdivision) grid lines |
| `axis_line` | `0x8A94A6` | axis lines |
| `axis_text` | `0xC8D0DC` | tick labels and axis titles |
| `cursor` | `0xFFB020` | cursor markers; also the hovered/grab highlight |
| `measure_fill` | `0xFFB020` at α 0.15 | the measurement band |
| `crosshair` | `0xE6EBF2` | crosshair lines and the readout panel's border (hue-neutral by design; trace markers keep each trace's own colour) |
The scale:
| field | default | unit |
|---|---|---|
| `ui_scale` | `1.0` | device pixels per logical unit; supplied by the host |
Logical metrics (scaled by `device()`):
| field | default | meaning |
|---|---|---|
| `axis_font_px` | `12.0` | logical em the axis text is drawn at |
| `grid_label_pad` | `4.0` | gap between a tick label and the axes, and the inset inside a title strip |
| `grid_major_width` | `1.0` | width of a major grid line |
| `grid_minor_width` | `1.0` | width of a minor grid line |
| `grid_major_visible` | `true` | stage the major grid at all |
| `grid_minor_visible` | `true` | stage the minor grid at all |
| `line_width_px` | `1.5` | width of the strokes framing the data (axis lines, cursor markers) |
| `handle_width_px` | `5.0` | width of a highlighted draggable handle (a hovered/grabbed cursor tab, a region's hot bound) |
| `tick_min_px` | `60.0` | minimum spacing between major ticks |
Crosshair stroke and dash (logical):
| field | default | meaning |
|---|---|---|
| `crosshair_width` | `0.75` | the crosshair's stroke width (deliberately thinner than a trace's `1.5`) |
| `crosshair_dash_on` | `4.0` | ink length of the dash |
| `crosshair_dash_off` | `4.0` | gap length of the dash; **`0` means solid** |
Device metric (never scaled):
| field | default | meaning |
|---|---|---|
| `dense_threshold_spp` | `1.5` | samples per device pixel at or above which a trace is gathered from the LOD pyramid instead of from raw samples |
## The grid
The grid's **positions** come from the tick set alone: `Grid::build()` emits one `GridLine`
per tick, spanning the perpendicular axis' full range, tagged `major` (a labelled tick) or
not. `Style` decides only how each class is drawn — its width and visibility — and the two
colours above.
A class switched off stages **no geometry at all**: the renderer skips it before the first
segment is staged, so a dense plot's minor grid costs nothing rather than being drawn
transparent. `Grid` therefore carries both classes' geometry whatever the style says; a
caller that draws the grid itself applies the same two switches. `Grid::vertical` and
`Grid::horizontal` (both `true`) control which directions emit lines at all.
## The crosshair's dash
`crosshair_dash_on`/`crosshair_dash_off` are the fragment shader's discard pattern, so no
geometry is added. `dash_off == 0` draws a solid line — the value every other line in the
library uses, so the one pipeline serves them unchanged. A single plot may override all three
knobs without touching the global `Style` through `CrosshairStyle` (below).
## `Legend`
`Legend` (`include/plot/legend.hpp`), set with `Plot::set_legend()`. All sizes are **logical**,
converted through `Style::device()` when `Plot::layout()` measures the panel.
| field | default | meaning |
|---|---|---|
| `visible` | `true` | draw the legend at all (free when nothing is named) |
| `place` | `LegendPlace::TopRight` | corner of `axes` the panel is anchored to |
| `frame` | `true` | paint the panel (background at partial alpha plus a 1-px border) |
| `pad` | `6.0` | inset from the panel edge to its first row, and the swatch↔name gutter (one knob for both) |
| `swatch_w` | `14.0` | swatch width |
| `swatch_h` | `8.0` | swatch height; the row height is `max(swatch_h, line height)` |
| `row_gap` | `3.0` | blank space between rows |
| `margin` | `8.0` | inset from the axes edge, *and* the budget the panel must fit in (so it drops entries rather than overhanging) |
There is no "outside the plot" placement: the strips around `axes` belong to the tick labels
and axis titles, and a legend that reserved space would resize the plot the moment a trace was
named. A truncated legend ends with the constant `kLegendEllipsis` (`"..."`).
## `CrosshairStyle`
A per-`Crosshair` override of the three crosshair knobs, in logical pixels
(`include/plot/crosshair.hpp`), set with `Plot::set_crosshair_style()`. Every field defaults to
`-1.0`, which means "not overridden": the field falls back to the matching `Style::crosshair_*`
metric, so a default-constructed `CrosshairStyle` is exactly the global style.
| field | default | meaning |
|---|---|---|
| `width` | `-1.0` | logical stroke width; `< 0` takes `Style::crosshair_width` |
| `dash_on` | `-1.0` | logical ink length; `< 0` takes `Style::crosshair_dash_on` |
| `dash_off` | `-1.0` | logical gap; `< 0` takes `Style::crosshair_dash_off`, and `0` (once resolved) means solid |
## Traces
A trace's own `width` (`TraceDesc::width`, `Trace::width`) is **logical** too — a stroke like
`line_width_px`, converted by the same `device()` call. Its default colour is `0x4FA3FF` and
its default width `1.5`. Region fill/edge colours are the region's own (`Region::fill`
`0xFFB020` at α 0.15, `Region::edge` `0xFFB020`); a cursor's colour is per-cursor
(`0xFFB020` by default).
## Installing a text backend
The font a plot measures with and the renderer draws with is a `render::TextBackend`
(`include/plot/render/text.hpp`): `measure()`, `height()`, `ascent()`, `em_px()`, `glyph()`,
`cell_uv()`, `atlas_pixels()`, `atlas_width()`, `atlas_height()` and `revision()`.
- `Plot::set_text_backend(const render::TextBackend*)` sets the backend a plot lays its labels
out with. It is **borrowed**, not owned, and may be null (then the label areas fall back to
`PlotLayout`'s defaults and no string is measured).
- `Renderer::set_text_backend(const render::TextBackend*)` sets the face the renderer draws
with. It is also borrowed and must outlive the frames.
**Note the interaction:** `Renderer::init()` binds `render::EmbeddedMonoFont`, and every
`Renderer::draw()` installs that binding on the plot it is given — replacing whatever a host
set with `Plot::set_text_backend()` (a debug build names the one it dropped). So installing a
backend on the *plot* only takes effect for a plot no renderer draws. To draw with your own
face (say a `plot::text::TrueTypeFont` from the optional `plot::font_ttf` target), install it
with `Renderer::set_text_backend()`; the swap takes effect on the next frame, which re-reads
the backend's metrics at `style().device(axis_font_px)` and re-uploads its atlas when the
backend's `revision()` moved. Passing `nullptr` restores the embedded face.