Look up
The eight marks
Line, Points, Bars, Area, Cells, Range, Rule, Text. Every channel, and a plate for each.
A mark is a family of geometric primitives that draw data. There are eight, joined under the closed Mark enum. The family is declared complete. A chart type is a composition of marks. It is not a peer of them. Each mark has position channels (its constructor arguments) and constant channels (builder methods). This page shows every one with a plate.
Marks draw onto a subpixel surface — 2×4 dots per cell in braille, 2×4 blocks in octants, 2×2 in quadrants, 1×1 in ASCII — and a charset codec turns each cell into a glyph. Text shares the grid and wins over pixels. Drawing does not fail. Off the surface, it clips. A non-finite coordinate draws nothing.
Line#
Points in order, paired series, or a sampled function. Line::y(values) plots against the index. Line::xy(x, y) pairs two series. Line::function(domain, f) samples a closure once per subpixel column, so there is no resolution to choose.
Line shows the subpixel default, the one-glyph-per-column corners style, and a dash.The code that drew it
use malevich::{Dash, Line, LineStyle, Plot};
let x: Vec<f64> = (0..40).map(|i| f64::from(i) * 0.25).collect();
let wave: Vec<f64> = x.iter().map(|x| (x * 1.1).sin() * 4.0 + 5.0).collect();
let lower: Vec<f64> = wave.iter().map(|y| y - 4.5).collect();
Plot::new()
.layer(Line::xy(x.clone(), wave).label("Pixels, the default"))
.layer(Line::xy(x.clone(), lower.clone()).style(LineStyle::Corners).label("Corners, one glyph per column"))
.layer(Line::xy(x, lower.iter().map(|y| y - 4.5).collect::<Vec<_>>()).dash(Dash::Dashed).label("dashed"))
.title("line styles")LineStyle::Pixels is the subpixel default. LineStyle::Corners is the asciichart look — box-drawing corners, one glyph per column — with real axes underneath, which the original never had. dash takes Dashed or Dotted. glow thickens a line. color sets a constant color, and label puts the layer in the legend.
Line::function samples a closure once per subpixel column.The code that drew it
use malevich::{Line, Plot};
// A function samples itself once per subpixel column: no resolution to choose.
Plot::new()
.layer(Line::function(0.0..12.6, f64::sin).label("sin x"))
.layer(Line::function(0.0..12.6, |x| (x * 0.5).cos() * 0.6).label("0.6 cos x/2"))
.title("Line::function")grade colors the line by a third series through a colormap — a route by its altitude, a run by its temperature — and adds a colorbar when the plot asks for one.
Line::grade colors the line by a third series through a colormap, and glow thickens it.The code that drew it
use malevich::scale::Colormap;
use malevich::{Line, Plot};
// `grade` colors the line by a third series through a colormap: here the
// temperature of a run colors its altitude profile.
let km: Vec<f64> = (0..120).map(|i| f64::from(i) * 0.1).collect();
let altitude: Vec<f64> = km.iter().map(|k| 300.0 + 180.0 * (k * 0.6).sin() + 40.0 * (k * 2.3).cos()).collect();
let temperature: Vec<f64> = km.iter().map(|k| 12.0 + 9.0 * (k * 0.35).sin()).collect();
Plot::new()
.layer(Line::xy(km, altitude).grade(temperature, Colormap::MAGMA).glow())
.colorbar()
.title("altitude, colored by temperature")
.x_label("km")
.y_label("m")Large lines reduce automatically. Past four points per rendered column, the plot inserts M4 — first, last, minimum, and maximum per column — which is pixel-identical to drawing every point (the full draw is the oracle). A NaN breaks the line, at every reduction level. That break is path topology.
Points#
A scatter. Points::y and Points::xy mirror Line. style picks a marker. opacity fades dense clouds. density, on the pixel canvas, shades by count.
Points has five marker styles, and in colorless output color_by cycles through them so groups stay apart.The code that drew it
use malevich::{Plot, PointStyle, Points};
let styles = [
(PointStyle::Dot, "Dot"), (PointStyle::Plus, "Plus"), (PointStyle::Cross, "Cross"),
(PointStyle::Asterisk, "Asterisk"), (PointStyle::Circle, "Circle"),
];
let mut plot = Plot::new();
for (row, (style, name)) in styles.into_iter().enumerate() {
let x: Vec<f64> = (0..8).map(|i| f64::from(i) * 1.5 + row as f64 * 0.3).collect();
let y: Vec<f64> = x.iter().map(|x| row as f64 * 2.0 + (x * 0.8).sin() * 0.6).collect();
plot = plot.layer(Points::xy(x, y).style(style).label(name));
}
plot.title("point styles")The five styles are also the shapes color_by cycles through when the output has no color, so a grouped scatter piped into a log keeps its groups apart.
Points::color_by gives three species Okabe–Ito colors and a legend.The code that drew it
use malevich::{Plot, Points};
use super::penguins;
let p = penguins();
Plot::new()
.layer(Points::xy(p.flipper, p.mass).color_by(p.species))
.title("flipper length against body mass")
.x_label("mm")
.y_label("g")Bars#
Bars rise from the zero baseline, or from a per-bar base. Four placements: bands (one bar per category), contiguous spans on a numeric axis, free positions, and explicit intervals.
Bars::new draws one bar per category on a band axis.The code that drew it
use malevich::{Bars, Plot};
Plot::new()
.layer(Bars::new(["rust", "go", "python", "typescript", "zig"], vec![68.0, 41.0, 55.0, 62.0, 12.0]))
.title("Bars::new — one bar per band")Bars::spans draws contiguous bins on a numeric axis.The code that drew it
use malevich::{Bars, Plot};
// Contiguous spans on a numeric axis: bin 0 starts at 10 and each is 5 wide.
Plot::new()
.layer(Bars::spans(10.0, 5.0, vec![2.0, 9.0, 17.0, 12.0, 6.0, 3.0, 1.0]))
.title("Bars::spans — contiguous bins from 10, width 5")Bars::spans(start, width, values) is the histogram's geometry: bin i runs from start + i·width. Bars::intervals(starts, ends, values) draws each bar between its own edges — bins of unequal width, or calendar months of their true length.
Bars::intervals places each bar between its own edges.The code that drew it
use malevich::{Bars, Plot};
// Irregular bins: each bar between its own edges. Widths of 1, 2, 4, 8, 16.
let starts = vec![1.0, 2.0, 4.0, 8.0, 16.0];
let ends = vec![2.0, 4.0, 8.0, 16.0, 32.0];
Plot::new()
.layer(Bars::intervals(starts, ends, vec![14.0, 22.0, 31.0, 18.0, 6.0]))
.title("Bars::intervals — bins of unequal width")base is the y2-style channel. With it, stacked bars, grouped bars, and waterfalls are plain compositions. They are not modes. Stack by giving the second layer the first layer's values as its base. Group by placing layers side by side with Bars::at, at positions stat::dodge computes (the statistics layer).
Bars::base starts the second layer where the first ends, a stack with no stacking mode.The code that drew it
use malevich::{Bars, Plot};
// A per-bar base is the y2 channel: the second layer starts where the first ends.
let quarters = ["Q1", "Q2", "Q3", "Q4"];
let hardware = vec![12.0, 15.0, 11.0, 19.0];
let software = [8.0, 9.5, 14.0, 12.0];
Plot::new()
.layer(Bars::new(quarters, hardware.clone()).label("hardware"))
.layer(Bars::new(quarters, software.iter().zip(&hardware).map(|(s, h)| s + h).collect::<Vec<_>>()).base(hardware).label("software"))
.title("stacked through Bars::base")horizontal turns any placement sideways — the bands run down the y axis in reading order and the values along x, the barh of the catalog. Long category names then take the measured label gutter. A band's width does not hold them.
Bars::horizontal runs bands down the y axis, values along x, long names in the label gutter.The code that drew it
use malevich::{Bars, Plot, Rule, Scale};
// Long category names take the measured label gutter instead of a band's width.
let workloads = ["slice a buffer", "iterate indices", "walk an adjacency list", "memo lookup", "random access"];
Plot::new()
.y_scale(Scale::bands(workloads))
.layer(Bars::new(workloads, vec![1.02, 1.38, 2.11, 0.91, 1.63]).horizontal())
.layer(Rule::v(1.0).label("baseline"))
.title("Bars::horizontal")
.x_label("speedup, ×")color_by on bars colors each bar by a category and builds the legend, the same channel as on points and lines.
Area#
A fill. Area::y and Area::xy fill from the baseline. Area::between(x, low, high) fills a band between two series — a confidence band, a p10–p90 envelope, one layer of a stacked area. opacity is a pixel-target channel. On a sixel, kitty, or iTerm2 panel it scales the fill's coverage, so the background and the layers beneath read through. On cells the fill stays solid, so a wash under a line is a dark explicit color there.
Area is a fill from the baseline, and a band between two series.The code that drew it
use malevich::{Area, Line, Plot};
let x: Vec<f64> = (0..60).map(f64::from).collect();
let mid: Vec<f64> = x.iter().map(|x| 10.0 + 4.0 * (x * 0.15).sin()).collect();
let low: Vec<f64> = mid.iter().zip(&x).map(|(m, x)| m - 1.0 - x * 0.03).collect();
let high: Vec<f64> = mid.iter().zip(&x).map(|(m, x)| m + 1.0 + x * 0.03).collect();
let floor: Vec<f64> = x.iter().map(|x| 2.0 + (x * 0.3).cos()).collect();
Plot::new()
.layer(Area::xy(x.clone(), floor).label("Area::xy, from the baseline"))
.layer(Area::between(x.clone(), low, high).opacity(0.35).label("Area::between, a band"))
.layer(Line::xy(x, mid).label("the line inside it"))
.title("areas")Area::horizontal(y, x_low, x_high) fills along y. A violin is two of these — a density and its mirror — which is exactly how the violin preset is expanded.
Area::horizontal fills along y, and a violin is two of these.The code that drew it
use malevich::stat::kde;
use malevich::{Area, Plot};
use super::penguins;
// A horizontal area is a fill along y: the two halves of a violin are one each.
let flippers = penguins().of("Gentoo", &penguins().flipper);
let (ys, density) = kde(&flippers, 120).expect("enough data");
let left: Vec<f64> = density.iter().map(|d| -d).collect();
Plot::new()
.layer(Area::horizontal(ys.clone(), vec![0.0; ys.len()], density).label("Area::horizontal"))
.layer(Area::horizontal(ys.clone(), left, vec![0.0; ys.len()]))
.title("a violin is two horizontal areas")
.y_label("flipper, mm")Cells#
One geometry, three color readings: a value grid under a colormap, an RGB image, or categorical class regions. A heatmap is not a mark. It is Cells under a colormap, and the heatmap preset says so.
Cells::matrix puts a value grid under a colormap, with rows labeled in matrix order.The code that drew it
use malevich::{Cells, Plot, Scale};
// A value grid: row-major, rows labeled in matrix order on a band axis.
let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"];
let hours = ["00", "04", "08", "12", "16", "20"];
let mut values = Vec::new();
for row in 0..6 {
for column in 0..6 {
values.push(20.0 + 18.0 * ((row as f64 - 2.5) * 0.7).cos() * ((column as f64 - 3.0) * 0.5).cos());
}
}
Plot::new()
.layer(Cells::matrix(6, values))
.x_scale(Scale::bands(hours))
.y_scale(Scale::bands(months))
.colorbar()
.title("Cells::matrix — a value grid under a colormap")Cells::matrix(columns, values) takes a row-major grid. Put Scale::bands on both axes and the rows are labeled in matrix order — row 0 at the top — which is what a confusion matrix or an attention map needs. colormap chooses the ramp. Plot::colorbar draws its legend.
Cells::rgb uses direct colors and no colormap, and an image is one more cell grid.The code that drew it
use malevich::{Cells, Plot};
// Direct colors, no colormap: an image is one more cell grid.
let (w, h) = (48, 20);
let mut pixels = Vec::with_capacity(w * h);
for row in 0..h {
for column in 0..w {
let (x, y) = (column as f64 / w as f64, row as f64 / h as f64);
let r = (255.0 * (0.5 + 0.5 * (x * 6.3).sin())) as u8;
let g = (255.0 * (0.5 + 0.5 * (y * 6.3 + 1.0).sin())) as u8;
let b = (255.0 * (0.5 + 0.5 * ((x + y) * 4.0).cos())) as u8;
pixels.push((r, g, b));
}
}
Plot::new().layer(Cells::rgb(w, pixels)).axes(false).title("Cells::rgb")Cells::rgb(columns, pixels) takes direct colors: an image, a convolution filter bank, a color-opponency map. In a plain pipe it degrades to a luma shade ramp.
Cells::classes paints categorical regions from the palette, with legend swatches.The code that drew it
use malevich::{Cells, Plot, Points};
// Categorical regions: each cell names a class; the legend follows the palette.
let n = 40;
let mut labels = Vec::with_capacity(n * n);
for row in 0..n {
for column in 0..n {
let (x, y) = (column as f64 / n as f64 * 4.0 - 2.0, 2.0 - row as f64 / n as f64 * 4.0);
labels.push(if (x * x + y * y).sqrt() < 1.0 { "inside" } else if x + y > 1.4 { "north-east" } else { "outside" });
}
}
let ring: Vec<f64> = (0..24).map(|i| f64::from(i) * 0.26).collect();
Plot::new()
.layer(Cells::classes(n, labels).extents((-2.0, 2.0), (-2.0, 2.0)))
.layer(Points::xy(ring.iter().map(|t| t.cos() * 1.05).collect::<Vec<_>>(), ring.iter().map(|t| t.sin() * 1.05).collect::<Vec<_>>()).label("boundary points"))
.title("Cells::classes — a decision region")Cells::classes(columns, labels) colors each cell by its class through the categorical palette, keeps a stable shade per class, and puts matching swatches in the legend. A decision boundary, a waffle, a land-use map.
Cells::extents and reduce max-reduce a grid denser than the raster, so its spikes survive.The code that drew it
use malevich::scale::Colormap;
use malevich::stat::Reducer;
use malevich::{Cells, Plot};
// A 600×300 grid onto a few dozen cells: every screen bucket owns the cells
// whose centers fall inside it and shows their maximum, so the sparse
// spikes survive. Extents place the grid in data coordinates.
let (w, h) = (600, 300);
let mut unit = super::noise(11);
let mut field = Vec::with_capacity(w * h);
for row in 0..h {
for column in 0..w {
let (x, y) = (column as f64 / 100.0, row as f64 / 100.0);
let base = ((x * 2.0).sin() * (y * 3.0).cos()).abs() * 0.3;
field.push(if unit() < 0.0008 { 1.0 } else { base });
}
}
Plot::new()
.layer(Cells::matrix(w, field).extents((0.0, 6.0), (0.0, 3.0)).reduce(Reducer::Max).colormap(Colormap::CIVIDIS))
.colorbar()
.title("180,000 cells, max-reduced")
.x_label("s")
.y_label("kHz")extents places the grid in data coordinates so it can share axes with points and lines. A grid denser than the raster reduces bucket-exactly. Every screen bucket owns the cells whose centers fall inside it and shows a declared reduction over all of them — the mean box filter by default, reduce(Reducer::Max) when the sparse spikes are the point. Nothing is dropped because a sampler stepped over it. smooth interpolates on the pixel canvas.
Range#
An interval per position, with two optional channels inside it: a thick body sub-interval and a marker crossbar. Range::xy(x, low, high) is an error bar at each x. Range::y(low, high) uses the index. Range::over(categories, low, high) puts one interval per band.
Range::xy draws an interval at each x: the error bars.The code that drew it
use malevich::{Plot, Points, Range};
// An interval per measurement: error bars are a Range around each point.
let dose = vec![1.0, 2.0, 4.0, 8.0, 16.0, 32.0];
let response = vec![0.12, 0.21, 0.38, 0.55, 0.71, 0.77];
let error = vec![0.04, 0.05, 0.06, 0.05, 0.07, 0.09];
let low: Vec<f64> = response.iter().zip(&error).map(|(r, e)| r - e).collect();
let high: Vec<f64> = response.iter().zip(&error).map(|(r, e)| r + e).collect();
Plot::new()
.layer(Range::xy(dose.clone(), low, high))
.layer(Points::xy(dose, response))
.log_x()
.title("Range::xy — an interval at each x")
.x_label("dose")
.y_label("response")Whiskers plus a body from the first to the third quartile plus a marker at the median is a box plot, and that is the whole expansion of the box_plot preset — the statistics come from stat::BoxStats. Candlesticks are the same mark with color_by splitting up days from down days.
Range::over with body and marker builds a box plot from the grammar.The code that drew it
use malevich::stat::BoxStats;
use malevich::{Plot, Range};
use super::penguins;
// The box plot, spelled out: whiskers, a body from q1 to q3, a marker at the median.
let p = penguins();
let species = ["Adelie", "Chinstrap", "Gentoo"];
let stats: Vec<BoxStats> = species.iter().map(|s| BoxStats::of(&p.of(s, &p.flipper)).expect("data")).collect();
Plot::new()
.layer(
Range::over(species, stats.iter().map(|s| s.whisker_low).collect::<Vec<_>>(), stats.iter().map(|s| s.whisker_high).collect::<Vec<_>>())
.body(stats.iter().map(|s| s.q1).collect::<Vec<_>>(), stats.iter().map(|s| s.q3).collect::<Vec<_>>())
.marker(stats.iter().map(|s| s.median).collect::<Vec<_>>()),
)
.title("Range::over with body and marker")
.y_label("flipper, mm")Rule#
A reference line at one value: Rule::h(y) or Rule::v(x), optionally dashed and labeled. And a span: Rule::h_span(y0, y1) or Rule::v_span(x0, x1) washes the band between two values across the plot — a recession, a warm-up phase, a tolerance window — behind the layers drawn after it.
Rule draws horizontal and vertical lines, dashed or not, and spans that wash a band across the plot.The code that drew it
use malevich::{Dash, Line, Plot, Rule};
let x: Vec<f64> = (0..100).map(f64::from).collect();
let y: Vec<f64> = x.iter().map(|x| 60.0 + 25.0 * (x * 0.08).sin() + x * 0.2).collect();
Plot::new()
.layer(Rule::v_span(10.0, 30.0).label("warm-up"))
.layer(Rule::h_span(70.0, 80.0).label("tolerance"))
.layer(Line::xy(x, y).label("signal"))
.layer(Rule::h(60.0).dash(Dash::Dashed).label("baseline"))
.layer(Rule::v(75.0).dash(Dash::Dotted).label("deploy"))
.title("rules: lines and spans")Rules take part in the domain: a target at 0.5 is on the axis even when no data reaches it. On a log axis a span that starts at or below zero washes its visible part from the axis floor up.
Text#
A string at data coordinates. Text::at(x, y, text) starts at the anchor and extends right. align(Align::Center) sets it on the anchor. Align::Right ends it there. On a Bands x axis, the band nearest the anchor becomes the text's box, with exactly the geometry the band's own header label uses — its rounded center, its step-wide budget — so aligned text and band labels land in lockstep. Text wider than its box clips to it with a truncation .. Digits from a neighboring column are never mixed into a number.
Text::at with align puts annotations at data coordinates, centered on their bands.The code that drew it
use malevich::{Align, Bars, Plot, Text};
// Text at data coordinates; on a band axis, `align` sets it in the band's
// own box, so labels and annotations land in lockstep.
let cities = ["Oslo", "Lima", "Osaka", "Cairo"];
let values = vec![14.0, 22.0, 9.0, 31.0];
let mut plot = Plot::new().layer(Bars::new(cities, values.clone()));
for (index, value) in values.iter().enumerate() {
plot = plot.layer(Text::at(index as f64, value + 2.5, format!("{value}")).align(Align::Center));
}
plot.layer(Text::at(1.0, 34.0, "Text::at, aligned to its band")).y_max(38.0).title("annotated bars")Text is how a stat table is drawn. describe and table are Text marks on two band axes, each column formatted by its own NumberFormat. Text is also how a heatmap is annotated. A Text over a Cells keeps the cell's color as its background. It does not punch a hole in the field. It picks dark or light ink from the luminance underneath.
correlation
An annotated correlation matrix from the grammar, no preset: signed data on a diverging colormap centered at zero, every coefficient printed in its cell. Annotations keep the field they land on as their background and pick dark or light ink from the luminance underneath; in plain output the digits stand beside the shades, so the numbers survive any pipe.
feature correlation (synthetic)
age ┤ █+1.00█+0.60▓+0.23▒-0.03▒-0.15▒-0.16▒-0.12▒-0.07
len ┤ █+0.60█+1.00▒-0.06▒-0.29▒-0.32▒-0.24▒-0.13▒-0.04
dep ┤ ▓+0.23▒-0.06█+1.00░-0.65▒-0.49▒-0.27▒-0.08▓+0.04
mass ┤ ▒-0.03▒-0.29░-0.65█+1.00░-0.54▒-0.15▓+0.08▓+0.17
veg ┤ ▒-0.15▒-0.32▒-0.49░-0.54█+1.00▓+0.17▓+0.35▓+0.34
kcal ┤ ▒-0.16▒-0.24▒-0.27▒-0.15▓+0.17█+1.00█+0.69▓+0.47
spd ┤ ▒-0.12▒-0.13▒-0.08▓+0.08▓+0.35█+0.69█+1.00▓+0.46
alt ┤ ▒-0.07▒-0.04▓+0.04▓+0.17▓+0.34▓+0.47▓+0.46█+1.00
└──────────────────────────────────────────────────
age len dep mass veg kcal spd alt
cargo run --example correlation -- --svgcargo run --example correlationThe source, examples/correlation.rs
A correlation matrix, annotated — from the grammar, no preset: signed data on a diverging colormap centered at zero, feature names on band axes, and every coefficient printed in its cell. The annotations keep the field they land on — a glyph over a filled cell takes the cell's color as its background — and each one picks dark or light ink from the luminance underneath, so the numbers read at both ends of the ramp. In plain output the digits replace the shades: the numbers are the values, so nothing is lost in a pipe.
use malevich::scale::Colormap;
use malevich::{Align, Cells, Color, Frame, Plot, Scale, Text};
include!("support/svg_card.rs");
fn main() {
let features = ["age", "len", "dep", "mass", "veg", "kcal", "spd", "alt"];
let n = features.len();
let grid: Vec<f64> = (0..n * n)
.map(|i| {
let (row, column) = ((i / n) as f64, (i % n) as f64);
if row == column {
1.0
} else {
// Symmetric, decaying with distance, alternating in sign — the
// shape of a real feature-correlation matrix.
((row - column).abs() * -0.35).exp() * ((row + column) * 0.55).cos()
}
})
.collect();
<span class="k">let</span> colormap = <span class="t">Colormap</span>::<span class="t">RED_BLUE</span>.<span class="f">centered_at</span>(<span class="n">0.0</span>);
<span class="k">let</span> <span class="k">mut</span> plot = <span class="t">Plot</span>::<span class="f">new</span>()
.<span class="f">layer</span>(<span class="t">Cells</span>::<span class="f">matrix</span>(n, &grid[..]).<span class="f">colormap</span>(colormap.<span class="f">clone</span>()))
.<span class="f">x_scale</span>(<span class="t">Scale</span>::<span class="f">bands</span>(features))
.<span class="f">y_scale</span>(<span class="t">Scale</span>::<span class="f">bands</span>(features))
.<span class="f">title</span>(<span class="s">"feature correlation (synthetic)"</span>);
<span class="k">for</span> (index, &coefficient) <span class="k">in</span> grid.<span class="f">iter</span>().<span class="f">enumerate</span>() {
<span class="k">let</span> (column, row) = (index % n, index / n);
<span class="c">// Ink by the luminance under it: dark on the pale middle of the</span>
<span class="c">// ramp, light on the saturated ends.</span>
<span class="k">let</span> ink = <span class="k">match</span> colormap.<span class="f">color</span>(colormap.<span class="f">position_in</span>(coefficient, -<span class="n">1.0</span>, <span class="n">1.0</span>)) {
<span class="t">Color</span>::<span class="t">Rgb</span>(r, g, b) <span class="k">if</span> u16::<span class="f">from</span>(r) + u16::<span class="f">from</span>(g) + u16::<span class="f">from</span>(b) > <span class="n">384</span> => {
<span class="t">Color</span>::<span class="t">Rgb</span>(<span class="n">32</span>, <span class="n">32</span>, <span class="n">32</span>)
}
_ => <span class="t">Color</span>::<span class="t">Rgb</span>(<span class="n">235</span>, <span class="n">235</span>, <span class="n">230</span>),
};
plot = plot.<span class="f">layer</span>(
<span class="t">Text</span>::<span class="f">at</span>(column <span class="k">as</span> f64, row <span class="k">as</span> f64, <span class="m">format!</span>(<span class="s">"{coefficient:+.2}"</span>))
.<span class="f">align</span>(<span class="t">Align</span>::<span class="t">Center</span>)
.<span class="f">color</span>(ink),
);
}
<span class="k">let</span> frame = <span class="t">Frame</span>::<span class="f">plain</span>(<span class="n">56</span>, <span class="n">11</span>);
<span class="k">if</span> <span class="f">svg_card</span>(&plot, &frame) {
<span class="k">return</span>;
}
<span class="m">println!</span>(<span class="s">"{}"</span>, plot.<span class="f">render</span>(&frame));
}
What a mark is not#
There is no Heatmap mark beside Cells, no barh beside Bars, no Errorbar beside Range, no Annotation beside Text. Each of those would be a second name for one geometry with a different reading, and a vocabulary that large cannot be learned, only searched. The membership test is two clauses, both required. Real charts demand it. No composition of the rest reproduces its output (what earns a concept).