# 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` | `0x39414C` | major (labelled-tick) grid lines | | `grid_minor` | `0x1C2126` at 50% alpha | 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` | `0.75` | width of a minor grid line (sub-pixel on purpose) | | `grid_minor_dash_on` | `3.0` | minor grid dash: ink length in logical pixels | | `grid_minor_dash_off` | `3.0` | minor grid dash: gap; `0` draws the minor grid solid | | `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 labelled class is the **structure** and the subdivisions are **texture**, and the minor grid's three defaults are what makes that read: it is thinner than the major hairline (`grid_minor_width`, a sub-pixel width that at 1x is expressed as coverage and on a HiDPI display as a narrower line), its colour carries an **alpha** so it blends into the background instead of sitting on it, and it is **dashed** (`grid_minor_dash_on`/`_off`) so an unlabelled line at an unlabelled position is never mistaken for a labelled one. Together those put a minor line at an order of magnitude less ink than a major one — at full opacity and the same width a dense minor grid reads as a second set of ticks. The dash is a fragment-shader discard, not extra geometry: the vertices are the line either way. Set `grid_minor_dash_off = 0` for the old solid subdivision. ## 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.