Skip to content

v0.16.0 — hardened SVG output, slim dependency tree, host-integration features

Choose a tag to compare

@sipemu sipemu released this 08 Oct 20:53
· 425 commits to main since this release
8c4ce73

Breaking changes

  • The library is now an rlib only (was ["cdylib", "rlib"]), which
    unblocks static / emscripten side-module consumers. The wasm-bindgen browser
    exports (ggplot_rs::wasm::*: render_geo, render_plot, render_bar,
    render_hist, geo_bounds, scatter_frame, render_scatter_rgba,
    render_scatter_xy) moved to the new, unpublished workspace crate
    crates/ggplot-rs-wasm (cdylib). Build the browser bundle with
    wasm-pack build crates/ggplot-rs-wasm --target web --out-dir ../../web/pkg --out-name ggplot_rs.
    The library no longer depends on wasm-bindgen.
  • The wasm feature is now a deprecated alias for sf (kept so
    features = ["wasm"] dependents keep compiling); it no longer adds
    wasm-bindgen/serde_json.
  • New default-on feature plotters (implies the new png feature) gates
    the plotters/image-backed APIs: GGPlot::{render_svg, render_svg_with_size, render_png, render_png_with_size, save, save_with_size, ggsave},
    ggpubr::{ggarrange_png, ggarrange_save_png} and
    render::plotters_backend. Crates using default-features = false that call
    these must add features = ["plotters"]; the self-contained
    render_svg_native* path is always available. canvas now implies png.
    cli implies plotters.
  • DataFrame::add_column no longer panics on a length mismatch: it pads the
    shorter side with Value::Na and records an issue
    (DataFrame::issues() / validate()); GGPlot::try_build and every
    render_*/save* then return GGError::ValidationError. This is reachable
    from GGPlot::new(Vec<(String, Vec<Value>)>) with ragged columns, which used
    to panic. New DataFrame::try_add_column rejects the column instead.
  • DataFrame::group_by / unique_values keep Value::Na distinct from the
    literal string "NA" (they used to merge into one group).
  • label_number(…, prefix, …) is now sign-aware: -5 with prefix "€"
    renders "-€5" (was "€-5"), and a value rounding to zero drops its sign.
  • PositionJitter gained a seed: u64 field, and width/height are now
    Option<f64> (None = 0.4 × the data's resolution, as in ggplot2). Build it
    with PositionJitter::new(w, h), PositionJitter::default(), .with_width(),
    .with_height() and .with_seed() instead of a struct literal.
    GeomJitter::{width, height} are likewise Option<f64> (default None).
  • Jitter and mean_cl_boot now draw from an internal SplitMix64 stream instead
    of rand::StdRng: output is still deterministic, but the exact jittered
    positions / bootstrap intervals differ from 0.15.

Added

  • ggplot_rs::format::format_value(&Value) -> String — the public tooltip /
    data-label value formatter (previously the crate-private geom::tip_value);
    ggplot_rs::format also re-exports the label_* formatters.

  • label_currency(prefix, suffix, big_mark, accuracy) — sign-aware currency
    labels with a configurable thousands mark ("-€1.234.567"). label_dollar
    is now implemented on top of it (output unchanged).

  • tests/native_svg.rs: smoke coverage of the plotters-free SVG path across
    geoms/coords/facets/themes, run in every feature configuration.

  • Value::group_key() -> GroupKey (injective w.r.t. missingness, borrows
    strings) and Value::key_str() -> Cow<str> (allocation-free form of
    to_group_key).

  • DataFrame::try_add_column, DataFrame::issues, DataFrame::validate.

Changed

  • Minimal dependency tree (default-features = false, features = ["sf"]):
    38 crates → 3 (indexmap, equivalent, hashbrown). No plotters, image,
    ab_glyph, chrono, rand or getrandom.
  • Removed the rand dependency (and the wasm32-only getrandom dependency)
    from the library; randomness comes from a small internal seeded PRNG.
  • Default jitter amount is resolution-based (ggplot2's 0.4 * resolution(x)):
    integer-spaced data keeps ±0.4, finely spaced continuous data is no longer
    smeared by a fixed ±0.4 data units.

Performance

  • stat_density switches to a binned estimator (linear binning onto a grid
    with spacing ≤ bw/50 + a kernel table truncated at ±8 bw) for groups with
    ≥ 2048 values: O(n + 512·window) instead of O(n·512) kernel evaluations,
    within ~2e-5 of the exact estimate relative to the peak. Smaller groups use
    the exact sum, unchanged.
  • Layers without their own data borrow the plot data instead of cloning the
    whole frame per layer.
  • Discrete scale training/lookups, stat_count, position_stack and
    position_fill use borrowed keys and hash lookups instead of allocating a
    String per value and scanning the distinct x values linearly per row
    (O(n·k) → O(n)).
  • DataFrame::group_by builds borrowed keys (no per-row String clones).

Fixed

  • Empty manual scales (scale_color_manual(vec![]), scale_fill_manual,
    scale_shape_manual, scale_linetype_manual) no longer panic with a
    division by zero; the geom falls back to its default colour/shape/linetype.

  • format_epoch_secs is total over i64 (it overflowed for i64::MIN).

  • stat_binhex / geom_hex output order is deterministic (row-major) instead
    of following HashMap iteration order, so identical data renders identical
    SVG.

  • label_number / label_currency with an accuracy that keeps decimals no
    longer inserts thousands marks into the fractional part
    (1234.5 @ 0.01 → "1,234.50", was "1,23,4.50"-style garbage).

Rendering robustness

  • An empty plot (zero-row data, no columns, or no layers) renders an empty
    panel with its title/axes instead of failing with
    ValidationError("geom_… requires aesthetic …").
  • A layer whose stat produces no rows (a density/violin of one value, loess on
    two points, …) draws nothing; the other layers still render.
  • Rows with non-finite position values (NaN, ±Inf, or a transform that is
    undefined such as log10(0)) are dropped before stats, like ggplot2's
    "Removed n rows containing non-finite values". Stat output is filtered the
    same way.
  • Build warnings are exposed: BuiltPlot::warnings() and
    GGPlot::render_svg_native_with_warnings(w, h) -> (String, Vec<String>).
  • The native SVG backend never writes NaN/inf into an attribute: marks with
    non-finite geometry are dropped and non-finite style values fall back to
    defaults.
  • Density/violin bandwidth follows R's bw.nrd0, including its fallbacks for
    zero-spread samples (previously a constant sample produced NaN).
  • A position scale's transform now applies to its whole column family
    (ymin/ymax/yend, …), not only x/y.
  • Faceted panels keep per-mark hover metadata (tooltips / data-x).

SVG host integration

  • Hoverable marks carry data-series (colour/fill/group level) and
    data-value (the raw measured value; for stacked/filled bars the segment's
    own value) next to data-x. Stacked-bar tooltips now report the segment
    value rather than the cumulative stack top.
  • The root <svg> carries the trained, expanded position domains:
    data-domain="x0 x1 y0 y1" (both axes continuous), data-xdomain,
    data-ydomain, data-xlevels/data-ylevels (JSON arrays for discrete axes)
    and data-flip="true" under coord_flip.
  • GGPlot::render_svg_native_at(x, y, w, h) renders a nested, positioned
    <svg x y width height viewBox> fragment without xmlns, for composing
    dashboards without string surgery (SvgBackend::finish_fragment).

Rect bounds

  • -Inf/Inf (f64::NEG_INFINITY/f64::INFINITY) in geom_rect /
    annotate("rect") xmin/xmax/ymin/ymax extend the rect to the panel
    edge (ggplot2) and do not train scales — also on discrete axes. New
    Geom::allows_infinite() hook; other geoms still drop infinite positions.

Theme / colour

  • Theme presets (theme_minimal(), theme_void(), …) and theme(t) keep a
    previously set primary_color (unless the new theme sets its own), so call
    order no longer matters.
  • primary_color no longer overrides colours of geoms added with
    geom_*_with(...) (Layer::explicit_style); default-configured geoms still
    take the brand colour.
  • ScaleColorDiscrete::sorted(), GGPlot::scale_fill_discrete_sorted() and
    scale_color_discrete_sorted(): discrete colour levels in sorted order, so a
    level keeps its palette colour across charts regardless of data order.

Tiles

  • geom_tile on a continuous axis is sized by the data resolution (smallest
    gap between distinct values, like ggplot2's resolution()); width/height
    are now multiples of it (default 1 = abutting tiles). The tile extents
    (xmin/xmax/ymin/ymax) train the scales, so edge tiles no longer spill
    past the axes and no with_expand(…) workaround is needed. New
    Geom::setup_data() hook (ggplot2's setup_data). Free facet scales also
    train on extent columns.

New chart types

  • geom_candlestick() / geom_ohlc() (+ _with): financial charts from the
    new open/high/low/close aesthetics (Aes::open() …,
    Aesthetic::{Open, High, Low, Close}); configurable up/down colours;
    the y scale trains on high–low; hover carries data-value = close and
    data-series = up/down (or the mapped group).
  • coord_radar() / CoordRadar: radar/spider charts — discrete x → spokes
    (first level at 12 o'clock), y → radius from 0, straight segments, rings at
    the y breaks, spokes and outside labels. geom_polygon now groups by
    group, else color/fill (no longer requires group), strokes with the
    mapped colour, and orders radar vertices by spoke; geom_line closes its
    path under radar and orders points by mapped x position. New
    Coord::is_radar() / Coord::train_scales() hooks.
  • Gauges: CoordPolar::with_span(start, end) sweeps the theta range over a
    partial arc (e.g. -PI/2..PI/2 for a half donut) and fits the arc to the
    panel; geom_rect draws annulus sectors under polar coords, so a gauge is
    geom_rect bands + a geom_segment needle.
  • Calendar heatmaps: geom_calendar() / geom_calendar_with(tile, monday_first)
    and StatCalendar lay a date x (DateTime, epoch seconds or
    YYYY-MM-DD) out as week × weekday tiles with month labels, Mon/Wed/Fri
    labels, month boundary outlines and square cells; spans over
    MAX_CALENDAR_YEARS (50) are clipped to the most recent years with a
    warning. Tiles' hover key (data-x, tooltip) is the ISO date.
  • geom_step accepts ±Inf (stat_ecdf's padding reaches the panel edges
    again).
  • Gallery: candlestick, radar, gauge, calendar examples.