Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

std.plot

std.plot draws layered charts as deterministic SVG files or byte streams. It is implemented in Plenty and needs no plotting runtime, R installation, display server, or external shared library. Import it with import std.plot as gg.

This is the core/SVG portion of the plotting proposal, not the complete first milestone or a drop-in replacement for ggplot2. See the plotting tutorial for runnable examples.

import std.plot as gg
from std.plot import Column, PlotError

def main() -> Result[(), AllocError | IoError | PlotError]:
    data = gg.table([
        ("day", Column.Num([1.0, 2.0, 3.0, 4.0]?)),
        ("visits", Column.Num([120.0, 150.0, 90.0, 180.0]?)),
    ]?)?
    chart = gg.ggplot(data, gg.aes("day", "visits"), [
        gg.geom_line(),
        gg.geom_point(),
        gg.labs(title="Daily visits"),
    ]?)?
    notes = chart.save("visits.svg", width=6.0, height=4.0)?
    print(notes)?
    svg = read_text("visits.svg")?
    print(svg.startswith("<svg"), "Daily visits" in svg)?
    Ok(())
[]
True True

Data and ownership

table(list[(str, Column)]) consumes named columns without copying their lists. Columns have equal lengths and unique nonempty names; an empty table has height zero. Column.Num, Int, Text, and Bool contain lists of f64, i64, str, and bool. MaybeNum and MaybeText contain lists of Option[f64] and Option[str]. Table.height() and Table.names() inspect the table.

Table.share() gives another immutable owner of the same columns without allocating. ggplot consumes a table. Plot.plus(components) borrows the original plot and returns an independent specification, sharing its table and copying its mappings and components. Both plots remain usable.

columns(&dictionary) copies a dict[str, list[f64]], retaining its key order. rows(&records, fields) evaluates typed accessors and builds columns. Construct fields with num, integer, text, and flag; each takes a name and an owned callback such as def(row: &Reading) -> f64: row.temperature. These constructors allocate a boxed callback. rows consumes the field list and retains no callbacks or borrowed records. This also supports lists of tuples and derived columns.

Mappings and components

aes(x, y, colour=, color=, fill=, group=, ymin=, ymax=) maps column names, not expressions. colour and color are aliases; supplying both nonempty names is an error. An empty name leaves that mapping unset. Numeric columns are continuous; text and bool columns are discrete. factor("name") makes a numeric column discrete. Numeric factors sort by numeric value; other levels sort by their string value. Int positions and continuous colours convert to f64, with its usual precision limit; factor labels preserve exact integers.

ggplot(table, mapping, components) eagerly checks names, required aesthetics, column kinds, and component settings. Constants belong in a geom, such as geom_point(colour="red"); a colour name inside aes would instead be looked up as a column.

Every geom accepts data=, mapping=, and inherit_aes=True. Omitted data uses the plot table; supplied data is an owned table and can have different columns and a different row count. A layer mapping overrides only its nonempty aesthetics; other aesthetics inherit the plot mapping. Use inherit_aes=False to start with an empty mapping. Empty names do not clear individual inherited aesthetics. Inherited names are looked up in the layer’s table, so a summary table without the plot’s grouping column normally needs inherit_aes=False. Only effective layer mappings are validated.

Position and colour scales train across all layers, so a category has the same position and colour wherever it appears. Mixing discrete and continuous mappings on one scale is an error. plus preserves the original specification and shares the immutable plot and layer tables.

ConstructorBehavior
geom_pointPoints; requires x and y; size is a diameter in millimetres
geom_lineConnects points sorted by x within each group
geom_pathConnects points in input order within each group
geom_colBars with supplied continuous y heights
geom_barCounts rows at each x, per group; omit y
geom_histogramCounts continuous x in equal-width bins; omit y
geom_errorbarSupplied ymin/ymax intervals with caps
geom_linerangeSupplied ymin/ymax intervals without caps
geom_ribbonFilled band between ymin and ymax, sorted by x within groups

See uncertainty and intervals for bound validation, missing-value gaps, grouping, and transformations. These geoms draw supplied bounds; they do not estimate confidence intervals.

Each geom accepts colour/color, alpha, and na_rm; bars also accept fill and position. Lines and interval geoms use linewidth; ribbons also accept fill. Numeric settings accept integer or float arguments. Supported constant colours are black, white, red, green, blue, grey, gray, orange, purple, yellow, pink, brown, navy, transparent, and #RRGGBB or #RRGGBBAA. More R colour names are not yet supported.

Discrete colour/fill mappings default to an eight-colour palette, repeated for additional levels. Continuous mappings default to an RGB gradient from #132B43 to #56B1F7. Legends are automatic. This is not yet ggplot2’s variable-size HCL palette or its continuous colour-bar guide.

Manual palettes and gradients

scale_colour_manual(values) and scale_fill_manual(values) consume a dict[str, str] mapping level labels to colours. For example, gg.scale_fill_manual({"North": "navy", "South": "#D55E00"}?) keeps each region’s colour stable when rows are reordered or a region is absent. Dictionary order does not change the sorted legend order. Every observed level must have an entry; extra entries are allowed. Numeric categories need factor and keys matching their displayed labels. Invalid colours, missing entries, and continuous mappings return errors from ggplot or plus.

scale_colour_gradient(low="#132B43", high="#56B1F7") and scale_fill_gradient(...) set continuous endpoints. Both endpoints must be six-digit #RRGGBB colours, with either hex letter case. Interpolation is in encoded RGB channels, truncating to integer channels; it is not ggplot2’s Lab interpolation. The finite data minimum and maximum map to the endpoints; a constant column maps to the midpoint. Legend swatches use the same mapping. Discrete data with a gradient returns WrongKind.

The scale_color_manual and scale_color_gradient spellings are aliases. The last scale component for each aesthetic wins, regardless of its position relative to geoms. plus can replace a scale without changing the original plot. Colour and fill are independent; different palettes get separate legends even when they map the same column. Constant geom colours override mappings. Missing values are still omitted, rather than given a palette colour. Scale limits, custom breaks, na_value, and palette functions are not yet exposed.

Default groups follow the interaction of discrete mappings; an explicit group replaces this rule. Bar positions use colour/fill groups independently of x. position_identity, position_stack, position_fill, and position_dodge select unadjusted, stacked, proportion, and side-by-side bars. Positive and negative stacks accumulate separately. Stacks follow reverse group order, matching the top-to-bottom discrete legend. Dodge reserves one slot for every group, including when a group is absent at a particular x. Bar width is in data units (default 0.9); automatic numeric resolution-based widths are deferred.

Histogram bins defaults to 30 and may be 1–10,000. Bins span the finite x range; they are left-closed/right-open, except that the final bin includes the maximum. An identical-value input uses a unit-width bin centred on that value. Empty bins do not produce rectangles. This deliberately small binning API does not yet implement ggplot2’s boundary/centre/binwidth alignment or computed statistics. Count/bin layers require discrete colour/fill mappings. If an explicit group combines different mapped colours in one output bar, rendering reports a conflict instead of selecting one arbitrarily.

Rows with missing or non-finite mapped values are omitted and counted in returned notes; na_rm=True suppresses the note. These observation-only diagnostic counts saturate at i64.MAX; semantic row counts, groups, and numerical aggregates do not saturate. A missing y interrupts a line/path within its group. Numeric data magnitudes above 1e100 are rejected during construction.

Labels, coordinates, and themes

facet_wrap and facet_grid split layers into panels, with fixed or free position scales and shared colour legends. See facets for panel ordering, independent statistics and repeated reference layers.

labs(title=, x=, y=, colour=, fill=) overrides nonempty labels; default axis and legend labels come from column names. Later nonempty label settings win.

theme_grey, theme_minimal, and theme_classic select grey-panel, white-grid, and plain-axis styles. They accept base_size in points and legend_position (Legend.Right or Legend.Hidden). A later theme replaces the earlier theme. These are compact styles, not the proposal’s complete theme-element tree.

coord_flip() exchanges the displayed axes. coord_cartesian(xlim=(low, high), ylim=(low, high)) clips and zooms after counting and stacking, preserving statistics. Omit either pair to use its automatic range. Only one coordinate component is allowed. Coordinate limits are expressed in original data units, including on transformed axes. See position scales for transforms, category order, breaks, labels, and the distinction between scale limits and coordinate zoom.

Layout plans text boxes before writing any SVG. Margins adapt to axis labels, titles and a right-hand legend. Titles, tick labels and legend labels wrap at word boundaries; an overlong word splits between Unicode scalars. Explicit newlines start new lines, and other whitespace is normalised. Tick boxes must not overlap. A chart that cannot fit returns InvalidSetting before writing to the supplied writer; increase dimensions, shorten labels, use fewer breaks, or hide the legend. No ticks or legend entries are silently removed.

SVG text names Liberation Sans, Arial, Helvetica, sans-serif; no fonts or font metrics ship. Deterministic character advances define each text box, and SVG textLength with lengthAdjust="spacingAndGlyphs" constrains the displayed width to that box. This may stretch/compress glyphs with a fallback font. The guarantee concerns the planned boxes in conforming SVG viewers, not identical glyph outlines across fonts. Complex-script shaping, font-specific vertical metrics, rotated ticks, and multi-column legends remain unsupported.

Output and failures

plot.save("chart.svg", width=6.0, height=4.0, units=Units.In, dpi=300.0) writes SVG and returns Result[list[str], PlotError]. write_svg(&mut writer, ...) accepts any std.io.Writer and also returns notes. Both borrow the plot, so it can be rendered repeatedly. Units.Cm, Mm, and Px are also supported; dpi converts pixel dimensions to physical dimensions and never rasterises SVG. Dimensions must be between 2 and 100 inches on each axis.

Rendering prepares per-layer rows, writes SVG fragments directly to the writer, and flushes it. save uses a 16 KiB file buffer. It does not collect the complete SVG unless the caller selects a memory cursor. Output uses two-decimal coordinates, stable element order and identifiers, and no timestamps or machine-specific font discovery. Identical inputs with the same toolchain produce identical bytes.

save truncates an existing destination, including before layout preflight. A rendering, allocation, or I/O failure can leave partial output; it is not an atomic file replacement. For transactional publication, write a temporary file and rename it after success. A stream failure is PlotError.Write(completed, cause), where completed counts bytes in the failing fragment, not the entire plot. Flush/close failures use Io.

Other errors are UnknownColumn, DuplicateColumn, ShapeMismatch, MissingAesthetic, WrongKind, Conflict, InvalidSetting, Allocation, and Bytes. No library messages are printed. ggplot catches mapping and setting mistakes; rendering can additionally reject unsupported numeric magnification, XML-forbidden text, or insufficient layout space. Allocation failure is always returned; no infallible native plotting allocator is involved.

Current scope

Not yet implemented: after_stat, statistical smoothing, date scales and other colour scales, full themes and font metrics, PNG/PDF, dataframe input, or record reflection. These remain part of the accepted proposal; priorities are tracked in B80.

Acknowledgements

  • ggplot2 supplies the model for the public names, layered composition, aesthetic mappings, grouping, and stacked-bar semantics. Its source was consulted during implementation; no ggplot2 code or data ships.
  • Hadley Wickham’s A Layered Grammar of Graphics describes the data/mapping/layer/scale/coordinate separation used by the module.