Files
plotter/include/plot/plot/legend.hpp
T
Martino Ferrari ed3a8ce645 licence: MIT LICENSE and SPDX identifiers in every first-party file
The repository had no licence at all, so every source was all-rights-reserved
by default. Add the verbatim MIT text at LICENSE (Copyright (c) 2026 Martino
Ferrari) and a two-line SPDX header to each of the 113 first-party sources --
48 include/plot, 26 src, 10 adapters, 5 examples, 21 tests, 3 tools -- so the
grant travels with each file rather than relying on the root document alone.

Scripts keep their shebang on line 1; the SPDX lines follow it. Genuinely
vendored code is untouched: third_party/stb_truetype.h carries no header of
ours. src/render/embedded_mono_font.cpp is generated, so tools/fonts/gen_atlas.py
emit() now writes the two lines into its header comment and the file was
regenerated -- gen_atlas.py --check passes and the regeneration adds only those
comment lines to the previously committed file.

README's provenance section now states MIT and links LICENSE.

No code, test or public API change.
2026-09-28 22:13:40 +02:00

116 lines
5.7 KiB
C++

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include <vector>
#include <plot/core/rect.hpp>
#include <plot/core/types.hpp>
namespace plot {
/// Which corner of `axes` the legend panel is anchored to.
///
/// There is no "outside the plot" placement: the strips around `axes` are the tick
/// labels' and the axis titles', and a legend that reserved space would resize the
/// plot the moment a caller named a trace. The panel is drawn inside the data area,
/// over the traces, which is why it spends `Legend::margin` keeping off the axes'
/// edges.
enum class LegendPlace { TopLeft, TopRight, BottomLeft, BottomRight };
/// The legend's configuration, read by `Plot::layout()` every time it runs.
///
/// All of it is deliberately coarse: the size, the row heights and the panel's
/// extent come from the text backend's metrics and the names themselves, never from
/// a caller's arithmetic, so a legend can only be *placed* and never mis-measured.
///
/// The sizes below are **logical** units, like every other metric a user
/// perceives as a size (see the unit rule on `render::Style`): `Plot::layout()`
/// converts each through `Style::device()` before it measures the panel, so a
/// legend is the same physical size on a 2x panel as on a 1x one.
struct Legend {
/// Draw the legend at all. This is on by default and costs nothing to leave on:
/// a legend with no entries -- no trace named, or no backend to measure a name
/// with -- draws nothing and lays out nothing, so the feature is free for a
/// caller who never names a trace.
bool visible = true;
LegendPlace place = LegendPlace::TopRight;
/// Paint the panel: the frame's own `Style::background` at a partial alpha, with a
/// one-pixel border. Off, the entries float on the data with nothing between them
/// and a dense trace.
bool frame = true;
/// Inset from the panel's edge to its first row, and the gutter between a row's
/// swatch and its name. One knob for both, so the panel's interior spacing is one
/// number rather than two.
f32 pad = 6.0f;
/// The swatch's width, and the height it is drawn at whatever the text line's is:
/// `max(swatch_h, line height)` is the row's height, so a small swatch is centred
/// in the line rather than stretching it.
f32 swatch_w = 14.0f;
f32 swatch_h = 8.0f;
/// Blank space between two rows.
f32 row_gap = 3.0f;
/// How far the panel is held back from the `axes` edge. It is the anchor's inset
/// *and* the budget the panel has to fit in, which is what makes "never crosses
/// `axes`" a property of one number instead of two that can disagree: a panel that
/// does not fit inside `axes` inset by this drops entries rather than overhanging
/// the plot (see `LegendLayout`).
f32 margin = 8.0f;
};
/// One legend entry: which trace it names, and the two boxes it is drawn in.
///
/// The boxes are the *layout's* output, not a renderer's input to be re-derived:
/// `Plot::layout()` places the swatch and the name's pen, and the renderer stages
/// exactly those pixels. A renderer that computed either from the panel rect would
/// be a second, independent copy of the placement rule -- which is the bug the
/// measured layout exists to prevent -- so nothing here needs arithmetic after the
/// fact.
struct LegendRow {
/// Index of the trace this row names: `Plot::trace(trace)` is what the row's
/// swatch is coloured by. The filter that chose the row (visible, named) is not
/// repeated by the renderer. Not meaningful on the truncation row.
u32 trace = 0;
/// This row is the truncation marker rather than a trace: entries did not fit and
/// the panel ends with `kLegendEllipsis` in place of the ones it dropped. Always
/// the last row, and its `swatch` is empty -- there is no trace to colour.
bool ellipsis = false;
/// The swatch's box, `Legend::swatch_w` x `Legend::swatch_h`, centred in the row.
/// Empty on the truncation row; the row's name box starts where the other rows'
/// names do either way, so the marker lines up with the names above it.
Rect swatch;
/// The name's line box: its top-left is the pen the run is staged at and its
/// height is one text line, so the box *is* the placement. Its width is what the
/// backend measured for this row's own string (the truncation row measures
/// `kLegendEllipsis`).
Rect name;
};
/// The measured legend: the panel to paint and the rows to draw in it, in the same
/// pixel space as `PlotLayout`, which `Plot::layout()` fills alongside its own
/// rects.
///
/// It is a member of `PlotLayout` rather than a second call so the numbers a test
/// asserts and the numbers the renderer draws are one set: a panel rect, and one
/// entry per row with the trace and both boxes. Everything is zero/empty when there
/// is nothing to draw -- no backend, no named visible trace, or no room for even one
/// entry and the truncation marker -- and an empty legend is what makes a plot that
/// never names a trace lay out byte-for-byte as it did before legends existed.
struct LegendLayout {
/// The panel, always inside `PlotLayout::axes`. Zero-sized when there is nothing
/// to draw.
Rect panel;
/// The rows to draw, in trace order, truncated from the end when they did not fit.
std::vector<LegendRow> rows;
bool empty() const noexcept { return rows.empty(); }
};
/// The last row of a truncated legend. ASCII, and one constant for both sides of the
/// feature, because the embedded atlas has no U+2026: the three-dot form is the one
/// that draws, and the layout measures this string while the renderer stages it, so
/// the two cannot disagree about how wide the row is.
inline constexpr const char* kLegendEllipsis = "...";
} // namespace plot