Labels
Attach content to individual datapoints: the value on a bar, the share in a slice, the series name at the end of a line.
AI assisted, human approved — novem uses AI to review and keep our documentation up to date.
Note: the labels file is part of the early-access flat config tree. You can explore it interactively in the plot playground.
Where a legend describes a whole series, a label describes one point. Labels live in one file directly under your plot's config:
One instruction per line:
[slicer [slicer]] [prop ...]
Every prop is a k:v pair, and the label text itself is the value
of the l: prop. Labels never appear uninvited: an empty file draws
nothing, and a file containing just l:%v gives correct value
labels on any chart — above columns (sign-aware), centered in
stacked segments and slices.
Props split into two namespaces. Bare keys are line-scoped
instructions: the selector, the text (l:), the lane policy (af:),
total and hide. Everything that styles the label box carries the l
prefix — le: li: la: lm: lo: lr: lc: lb: lbs: lbd: lbp: ltx: and the
size keys — one l* vocabulary shared with facet strips, axis
titles and complications, on the model of the axis t/tl prefix.
The old bare spellings (e:, m:, c:, ...) are retired and error
with a pointer to their l* replacement.
The datapoint is the unit: a cell (row, column) on bars, lines and scatter, a slice (row) on pie and donut. Selection is the shared slicer vocabulary, aimed at points:
l:%v le:c -- no slicer: every datapoint
espresso l:%v -- one slicer: a column (a series)
-1 : l:%s le:r -- two slicers: rows x cols of the raw csv
1,-1 : l:%v -- first and last data rows
One slicer addresses columns; two are rows × columns of the RAW
csv layout (the slicing contract):
row 0 is the header line, col 0 the x column — both paint nothing —
so the first data row is 1 and the first series is column 1.
Ranges are stop-inclusive, negatives count from the end, header
names substitute in the column token. Pie and donut slices are ROWS:
2 : l:%v labels the second slice. Under facets, labels resolve per
panel against the panel's own data.
The row slot also takes a value token: @x the maximum, @i
the minimum, @f the first, @l the last cell with a value.
Resolution happens server-side, per selected series, so each series
gets its own answer:
@x : l:%v lc:mark -- every series' peak, series-colored
@x -1 l:"peak %v" -- the last series' peak only
@i espresso l:%v -- the espresso series' minimum
The @ letters live in the row slot only, so they need their column
selection spelled out: @x : addresses every series, and : @x is
invalid. @e and @a (median, mean) are reserved — they name a
value between cells, not a cell — and an unknown @ token drops the
line, since a failed selector never broadens.
The bare keyword total makes the line address stack sums
instead of cells: one label per x position, over the stacked region
the selection touches. %v is the sum; every other prop rides
unchanged. A column slicer narrows which stacked series contribute,
and a row window narrows the x positions:
total l:%v -- the sum over every stack
-5: : total l:%v,.0f -- the last five stacks only
total is line-scoped, like hide, and meaningful on stacked types
(stacked bars and areas).
The label text is the value of the l: directive — one letter that
means label text across the whole platform, from axis titles and
facet strips to legend entries, and an explicit directive keeps live
editing stable: a half-typed word is never reclassified as label
text. The value is unquoted when it has no spaces (l:%v), quoted
when it does (l:"NVDA ($%v)"); quotes are ' or " and must
match — an unterminated quote recovers loudly, the text running to
the end of the line while you type, and the label stays put.
Inside the value, the legend token set, re-scoped to the point:
| token | meaning |
|---|---|
%v | this point's value |
%vl %vf %vi %vx %ve %va | value stats over the point's series: last, first, min, max, median, mean |
%l | the point's row label (category, slice name, date) |
%s | the series name |
%p | the point's share of its series total, default .0% |
Format specs append directly to a token, the table cell-format
vocabulary: %v,.1f, %p.1%, %l%b-%y (strftime for dates).
A line with no l: uses the point's natural label: %l, the
category on arcs, the row/x label on cartesian points. So -1 :
alone labels the last row of every series with its natural label.
Bare text is never a template: an unknown bare token drops the whole
line with a diagnostic, and an unknown k:v drops just that prop.
Two keywords, the facet-strip vocabulary: an unquoted l:none (or
l:0) means no label — it replaces any earlier label on those
cells — and l:auto (or l:1) spells out the natural default. A
quoted value is always literal, so l:"none" labels with the text
"none".
| is a deck break inside the value: the label stacks
vertically, and a leading < > - aligns each deck within the
block:
Every template glyph escapes with a backslash: \| is a literal pipe, and \- \< \> start a deck with that character rather than aligning it — write l:"\-5% of total" to keep the minus sign. Those four are the only escapes; a backslash anywhere else is ordinary text.
l:-%l|-%v,.0f -- centered name over centered value
Three orthogonal keys place the label; the same keys drive facet and axis labels, so you learn them once.
| key | values | meaning |
|---|---|---|
le: | t b l r, corners tl tr bl br, c | the anchor edge or corner on the mark. Default: the value end (top of a positive column, bottom of a negative one). |
li: | in, out | which side of that edge the text sits. Stacked segments and arcs default in, everything else out. |
la: | data, axis | x attachment target. data uses the selected mark; axis follows the y rail that claims its series (y2 s:… before the primary rail). Y remains at that rail's selected value. |
lm: | px or % of the mark, pair lm:x,y | the gap from the anchor edge. Default 2; unsigned is exact, while +/- adjusts the default (lm:+5 is 7px). |
lml: lmr: lmt: lmb: | px | per-side margins on the label box. Unsigned is exact; +/- adjusts the zero default. The anchored side nudges the label away from its mark, while the free side joins the plot's automatic margin. |
lmx: lmy: lma: | px | side bundles under the same absolute/relative rule; a later single side overrides its half. |
Text grows into the mark inside and away from it outside, so there is
no separate alignment key. On point marks (line vertices, scatter)
le: is the compass direction from the point. On arcs (pie and
donut) the edge set reads radially: le:t anchors at the outer rim,
le:b at the inner edge, le:c at the middle of the annulus (the
default), and a corner degrades to its radial component (tl reads
as t). The angular position is always the centroid ray. li: and
lm: keep their meanings against the chosen edge — li:in sits
inside it, li:out beyond it (past the rim for le:t), and lm:
is the radial gap.
l:%v le:tl li:in lm:2 -- inside each rect, top-left
l:%v le:c -- dead center
l:%v le:t li:out lm:2 -- above the bar (the default)
l:%l|%p li:in -- donut: name over share, in the arc
l:%l li:out lm:8 lr:ray -- outside the arc, on the radial line
-1 : l:%s le:r lm:6 lc:mark -- series names at the line ends
The last line is the pseudo-legend: the series name rides the last datapoint, in the series color, and the plot reserves the margin it needs.
| value | behavior |
|---|---|
lo:e lo:t lo:w lo:o | ellipsis, truncate, wrap, overflow |
lo:h | hide the label when it does not fit |
lo:f | flip to the other side of the edge when it does not fit |
The former alternate letter (o:a) is now the lane policy af:a;
see the Autofit section below.
The size trio bounds the label box, the CSS clamp reading: a fixed
size, a minimum and a maximum. Values are px only, greater than
zero, and when a minimum and a maximum conflict, min beats max.
The letters rhyme with the platform: the table width file and the
%vi/%vx value stats already spell min/max as i/x.
| key | meaning |
|---|---|
lw: lh: | fixed width / height in px |
lwi: lhi: | minimum — floors the box: equal-width plates, so line-end pseudo-legends align |
lwx: | maximum width — caps the text (wrap or ellipsis per lo:) and the box and margin it reserves |
lhx: | maximum height — caps the deck count; excess decks drop |
Inside a mark the fit box is the mark minus margins; outside it is
lwx: or unbounded. Rect labels default to lo:f, the best-fit
behavior; everything else defaults to lo:o. The old w: (which
meant max width) is retired; its meaning lives at lwx:.
Point labels avoid colliding with each other automatically, the way axis tick labels do: one decision per label lane, measured before anything draws. When labels would overlap, every other one mirrors across its mark (the alternate stagger); when even that cannot fit, the lane thins to the smallest uniform step that does — and the last point always keeps its label.
af: is the lane policy, and it is a bare key: it belongs to the
line's selection, not to the label box.
| key | values | meaning |
|---|---|---|
af: | absent, a, 0 (none, off) | absent runs the automatic ladder; af:a forces the alternate stagger regardless of fit; af:0 disables collision handling for the lane |
Only af:0 opts a lane out: box props such as lo:, lr: or
lwx: style the labels without touching collision handling.
| key | values | meaning |
|---|---|---|
lr: | degrees −90..90, ray | rotation; ray aligns arc labels to the centroid ray, flipping to stay readable |
lc: | color[,dark], ink, mark | text color. ink (the default) contrast-flips inside dark marks; mark inherits the element's color |
lb: | auto, mark, 0, color[,dark] | a legibility plate behind the label; mark uses the resolved mark color |
lbs: | flat, point, notch, round, cut; one value or near,far | vector shapes for the logical ends of the responsive plate. One value applies to both ends. |
lbd: | px or near,far | exact horizontal depth of shaped ends. lbd:5 makes a 5px chevron; omitted stays responsive. |
lbp: | px or x,y | horizontal and vertical plate padding. Unsigned is exact; +/- adjusts the themed default (3,2). |
ltx: | style/size/family, any order | the table text vocabulary: ltx:b, ltx:600,sm,mono, ltx:12 |
hide | bare keyword | drop the sliced points' labels |
The plate is a generated SVG path, not a glyph or emoji: its body and
ends grow with the measured text, font, wrapping and padding. near
faces the mark and far faces away, so the silhouette remains correct
when a label flips from one side to the other. For centered labels,
near falls back to the inline-start side. Text defaults to contrast ink
against the plate; an explicit lc: still wins.
An explicit depth participates in layout: lbd:5 lbp:4,2 lays out the
tip, then 5px of shaped end, then 4px of horizontal padding before the
text. The point cannot overlap or clip the first glyph.
la:data keeps a label attached to its mark and lets lm: offset that
attachment. la:axis keeps the value-derived y but places text at the
same x as the corresponding y-axis labels. A claimed series follows its
claiming rail, so a Volume badge selected by y2 s:-1 aligns with y2,
not the nearer primary y axis. Ordinary labels default to
data; an outward near-point endpoint defaults to axis for line, area,
scatter and bar marks. Side margins still apply after axis placement:
omit lml:/lmr: for exact tick-label alignment.
For that Bloomberg endpoint, the point tip lands on the domain line
while the text lands in the axis-label lane. The tip stays fixed while
padding and content reshape the box around the text. The complete rendered
SVG text block is measured and centered before padding is added, so multi-deck labels such as
l:"%v|%s|%l" receive the same space above, below and on both sides.
This example turns the final value into a mark-colored arrow label.
The pointed near end grows with ltx: and lbp: and the plot reserves
its full vector extent.
date,price
2026-01-01,101.2
2026-02-01,104.8
2026-03-01,102.9
2026-04-01,109.4
2026-05-01,114.7
2026-06-01,112.6A label occupies a slot, its resolved (e, i) position. One
label per point per slot; different slots coexist:
l:%l le:t li:out -- category above every bar
l:%v le:c -- value centered inside the same bars
Plain lines define labels, and later lines replace their slot
(later wins). To patch a previous line instead, lead with the +
attach marker: the line restates the previous one over a new
selector, merging its overrides onto what those points already have.
A leading - erases.
l:%v
+ -1 : lc:mark ltx:b -- ...and the last points get bold, series-colored
An unknown bare token drops its whole line; an unknown k:v prop
drops just that prop. A token still being typed drops alone too: a
key without its colon (lm before lm:6), an l: with no value
yet, and an unterminated quote, which runs to the end of the line so
the label holds its place while you type inside it. Drops are loud
but never fatal: the rest of the file still applies, and every drop
lands in the plot's diagnostics with its line number. The
playground shows them inline.