`Style::marker_below_samples` (50 by default, `0` for never) draws a `marker_px` dot on every *raw* sample when the visible window holds at most that many, in the trace's own colour. Zoomed in far enough to see individual samples, the polyline between them stops being the shape a reader is looking at -- a handful of points across a whole panel ARE the picture -- and a dot makes each one a place rather than a bend in a line. Level 0 only: above it the geometry is pyramid buckets, which are not points anyone can find. Test: a_sparse_window_marks_its_samples -- the same flat series at ten samples and at a hundred. Ten gives exactly ten dots, evenly spaced (their centres are checked against the panel's own step, so "they are the samples" is asserted rather than "something was painted"); a hundred is past the threshold and gives none. The dots *group* into runs of columns (a marker is `marker_px` wide), which is how the test reads one marker per sample -- counting columns read three.
189 lines
10 KiB
Markdown
189 lines
10 KiB
Markdown
# 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 |
|
||
| `marker_below_samples` | `50` | a dot per raw sample when the window holds at most this many (`0` = never) |
|
||
| `marker_px` | `3.0` | logical size of that dot |
|
||
| `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.
|
||
|
||
## A cursor's stroke
|
||
|
||
`set_cursor_color()` gives a cursor its own colour and `set_cursor_stroke()` its own width and
|
||
dash (`Cursor::width`, `Cursor::dash`). Per cursor rather than per plot, because a host uses
|
||
cursors for two different jobs: the instruments that frame a reading — a marker at an edge, a pull
|
||
handle — and the *readout marks* a computation places on the data (a measured level, a peak, a
|
||
pulse's edges). Those want different weights. `width <= 0` means the style's default for that
|
||
cursor (`handle_width_px` while it is hovered or grabbed, `line_width_px` otherwise), and a hover
|
||
can only make the stroke *thicker*: the cue that the pointer has hold of something must not be the
|
||
thing that hides it. `dash.off <= 0` (the default) is solid — the same convention as a trace's,
|
||
the crosshair's and the grid's.
|
||
|
||
## 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.
|