Scatter Plot

A scatter plot renders individual (x, y) data points as markers. It supports trend lines (linear and LOESS), error bars, variable point sizes, per-point colors, and ten marker shapes.

Import path: kuva::plot::scatter::ScatterPlot


Basic usage

#![allow(unused)]
fn main() {
use kuva::plot::scatter::ScatterPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let data = vec![
    (0.5_f64, 1.2_f64),
    (1.4, 3.1),
    (2.1, 2.4),
    (3.3, 5.0),
    (4.0, 4.3),
    (5.2, 6.8),
    (6.1, 6.0),
    (7.0, 8.5),
    (8.4, 7.9),
    (9.1, 9.8),
];

let plot = ScatterPlot::new()
    .with_data(data)
    .with_color("steelblue")
    .with_size(5.0);

let plots = vec![Plot::Scatter(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Scatter Plot")
    .with_x_label("X")
    .with_y_label("Y");

let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("scatter.svg", svg).unwrap();
}
Basic scatter plot

Layout options

Layout::auto_from_plots() automatically computes axis ranges from the data. You can also set ranges manually with Layout::new((x_min, x_max), (y_min, y_max)).


Trend line

Add a linear trend line with .with_trend(TrendLine::Linear). Optionally overlay the regression equation and the signed Pearson correlation coefficient r. The fit uses ordinary least squares with an intercept and all supplied points. For this fit, the coefficient of determination is R^2 = r^2; for example, r = -1 describes a perfect descending line with R^2 = 1. The line and its annotations are omitted for fewer than two points, constant x, non-finite coordinates, or coefficients that cannot be represented as finite f64 values. Constant y keeps its flat trend line, but drops the r annotation, since the coefficient is undefined there rather than zero. An extrapolated line is omitted if either endpoint cannot be represented.

#![allow(unused)]
fn main() {
use kuva::plot::scatter::{ScatterPlot, TrendLine};
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let data = vec![
    (1.0_f64, 2.1_f64), (2.0, 3.9), (3.0, 6.2),
    (4.0, 7.8), (5.0, 10.1), (6.0, 12.3),
    (7.0, 13.9), (8.0, 16.2), (9.0, 17.8), (10.0, 19.7),
];

let plot = ScatterPlot::new()
    .with_data(data)
    .with_color("steelblue")
    .with_size(5.0)
    .with_trend(TrendLine::Linear)
    .with_trend_color("crimson")   // defaults to "black"
    .with_equation()               // show y = mx + b
    .with_correlation();           // show signed r

let plots = vec![Plot::Scatter(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Linear Trend Line")
    .with_x_label("X")
    .with_y_label("Y");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Scatter with linear trend line

Tip: .with_equation() and .with_correlation() render the fit statistics as floating text in the data area. For a cleaner presentation — particularly with dense point clouds — consider using Layout::with_stats_box() to display fit statistics in a bordered inset box instead. See Stats Box.

LOESS smoother

For data with a non-linear relationship, overlay a LOESS (locally-weighted regression) smoother instead of a straight line. .with_loess() uses a default span of 0.5; .with_loess_span(f) sets the span (the fraction of points in each local fit, 0.05–1.0): smaller spans follow local detail, larger spans give a smoother curve.

Each prediction fits a local straight line using distance weights. These weights do not downweight a point because its y value is an outlier. A fitted line can extend beyond the observed y range; the plot clips the curve at its axis limits. Log axes transform the drawn curve, rather than fitting logarithms of the observations.

#![allow(unused)]
fn main() {
use kuva::plot::scatter::ScatterPlot;
let plot = ScatterPlot::new()
    .with_data(data)
    .with_loess()                  // or .with_loess_span(0.2)
    .with_trend_color("crimson");
}

Equation and correlation annotations apply to the linear fit only, not to LOESS.

Scatter with a LOESS smoother

Confidence band

Attach a shaded uncertainty region with .with_band(y_lower, y_upper). Both slices must align with the x positions of the scatter data. The band color matches the point color.

#![allow(unused)]
fn main() {
use kuva::plot::scatter::ScatterPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let xs: Vec<f64> = (1..=10).map(|i| i as f64).collect();
let ys: Vec<f64> = xs.iter().map(|&x| x * 1.8 + 0.5).collect();
let lower: Vec<f64> = ys.iter().map(|&y| y - 1.2).collect();
let upper: Vec<f64> = ys.iter().map(|&y| y + 1.2).collect();

let data: Vec<(f64, f64)> = xs.into_iter().zip(ys).collect();

let plot = ScatterPlot::new()
    .with_data(data)
    .with_color("steelblue")
    .with_size(5.0)
    .with_band(lower, upper);

let plots = vec![Plot::Scatter(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Confidence Band")
    .with_x_label("X")
    .with_y_label("Y");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Scatter with confidence band

Error bars

Use .with_x_err() and .with_y_err() for symmetric error bars. Asymmetric variants accept (negative, positive) tuples.

#![allow(unused)]
fn main() {
use kuva::plot::scatter::ScatterPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let data = vec![
    (1.0_f64, 2.0_f64), (2.0, 4.5), (3.0, 5.8),
    (4.0, 8.2), (5.0, 10.1),
];
let x_err = vec![0.2_f64, 0.15, 0.3, 0.1, 0.25];  // symmetric
let y_err = vec![0.6_f64, 0.8, 0.4, 0.9, 0.5];     // symmetric

let plot = ScatterPlot::new()
    .with_data(data)
    .with_x_err(x_err)
    .with_y_err(y_err)
    .with_color("steelblue")
    .with_size(5.0);

let plots = vec![Plot::Scatter(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Error Bars")
    .with_x_label("X")
    .with_y_label("Y");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Scatter with error bars

Asymmetric errors

Pass (neg, pos) tuples instead of scalar values:

#![allow(unused)]
fn main() {
use kuva::plot::scatter::ScatterPlot;
let data = vec![(1.0_f64, 5.0_f64), (2.0, 6.0)];
let y_err = vec![(0.3_f64, 0.8_f64), (0.5, 1.2)];  // (neg, pos)

let plot = ScatterPlot::new()
    .with_data(data)
    .with_y_err_asymmetric(y_err);
}

Marker shapes

Ten marker shapes are available via MarkerShape. They are particularly useful when overlaying multiple series on the same axes.

#![allow(unused)]
fn main() {
use kuva::plot::scatter::{ScatterPlot, MarkerShape};
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let plots = vec![
    Plot::Scatter(ScatterPlot::new()
        .with_data(vec![(1.0_f64, 1.0_f64), (2.0, 1.0), (3.0, 1.0)])
        .with_color("steelblue").with_size(7.0)
        .with_marker(MarkerShape::Circle).with_legend("Circle")),

    Plot::Scatter(ScatterPlot::new()
        .with_data(vec![(1.0_f64, 2.0_f64), (2.0, 2.0), (3.0, 2.0)])
        .with_color("crimson").with_size(7.0)
        .with_marker(MarkerShape::Square).with_legend("Square")),

    Plot::Scatter(ScatterPlot::new()
        .with_data(vec![(1.0_f64, 3.0_f64), (2.0, 3.0), (3.0, 3.0)])
        .with_color("seagreen").with_size(7.0)
        .with_marker(MarkerShape::Triangle).with_legend("Triangle")),
];

let layout = Layout::auto_from_plots(&plots)
    .with_title("Marker Shapes")
    .with_x_label("X")
    .with_y_label("");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}

Available variants: Circle (default), Square, Triangle (up), TriangleDown, Diamond, Cross, Plus, Star, Pentagon, Hexagon.

Scatter marker shapes

Bubble plot

Encode a third dimension through point area using .with_sizes(). Values are point radii in pixels.

#![allow(unused)]
fn main() {
use kuva::plot::scatter::ScatterPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let data = vec![
    (1.0_f64, 3.0_f64), (2.5, 6.5), (4.0, 4.0),
    (5.5, 8.0), (7.0, 5.5), (8.5, 9.0),
];
let sizes = vec![5.0_f64, 14.0, 9.0, 18.0, 11.0, 7.0];

let plot = ScatterPlot::new()
    .with_data(data)
    .with_sizes(sizes)
    .with_color("steelblue");

let plots = vec![Plot::Scatter(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Bubble Plot")
    .with_x_label("X")
    .with_y_label("Y");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Bubble plot

Per-point colors

Encode a categorical grouping through color using .with_colors(). Colors are matched to points by index and fall back to the uniform .with_color() value for any point without an entry.

This is useful when your data already carries a group label and you want to avoid splitting into multiple ScatterPlot instances. The legend is not updated automatically — add .with_legend() on separate ScatterPlot instances when you need a labeled legend.

#![allow(unused)]
fn main() {
use kuva::plot::scatter::ScatterPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

// Three clusters, colors assigned per point
let data = vec![
    (1.0_f64, 1.5_f64), (1.5, 2.0), (2.0, 1.8),  // cluster A
    (4.0, 4.5),         (4.5, 5.0), (5.0, 4.8),  // cluster B
    (7.0, 2.0),         (7.5, 2.5), (8.0, 2.2),  // cluster C
];
let colors = vec![
    "steelblue", "steelblue", "steelblue",
    "crimson",   "crimson",   "crimson",
    "seagreen",  "seagreen",  "seagreen",
];

let plot = ScatterPlot::new()
    .with_data(data)
    .with_colors(colors)
    .with_size(6.0);

let plots = vec![Plot::Scatter(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Per-Point Colors")
    .with_x_label("X")
    .with_y_label("Y");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Scatter with per-point colors

Marker opacity and stroke

Two builders control the visual style of markers, enabling three distinct modes useful for dense datasets:

ModeSettingUse case
Solid (default)no calls neededSmall N or well-separated clusters
Semi-transparentopacity < 1 + strokeDense regions pool colour; individual points stay visible
Hollowopacity = 0.0 + strokeVery large N; overlapping rings reveal density without blobs

Semi-transparent markers — overlapping clusters

Three Gaussian clusters of 200 points each share a region in the centre. Solid markers at this density merge into a single opaque mass. Reducing opacity to 0.25 lets the darker overlap region show where clusters share space, while the 0.7 px stroke keeps each marker individually legible.

#![allow(unused)]
fn main() {
use kuva::plot::scatter::ScatterPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

// Three clusters, 200 points each — defined as (center_x, center_y, color, label)
let series = [
    (3.0_f64, 4.0_f64, "steelblue", "Cluster A"),
    (5.0,     5.5,     "tomato",    "Cluster B"),
    (4.0,     3.0,     "seagreen",  "Cluster C"),
];

// (populate `data` from your source — each entry is 200 (x, y) points)

let data: Vec<(f64,f64)> = vec![];
let plot = ScatterPlot::new()
    .with_data(data)
    .with_color("steelblue")
    .with_size(5.0)
    .with_marker_opacity(0.25)
    .with_marker_stroke_width(0.7)
    .with_legend("Cluster A");

let plots = vec![Plot::Scatter(plot) /* , ... */];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Overlapping Clusters — semi-transparent markers")
    .with_x_label("X")
    .with_y_label("Y");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Three overlapping Gaussian clusters with semi-transparent markers

Hollow open circles — dense annular data

800 points sampled uniformly along a noisy ring. With solid fill the ring becomes a uniform band; hollow circles (opacity = 0.0) make the denser arc sections visible through the accumulation of overlapping outlines.

#![allow(unused)]
fn main() {
use kuva::plot::scatter::ScatterPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

// 800 points sampled along a noisy annulus of radius ≈ 3
let data: Vec<(f64,f64)> = vec![];
let plot = ScatterPlot::new()
    .with_data(data)
    .with_color("steelblue")
    .with_size(4.0)
    .with_marker_opacity(0.0)       // fully hollow
    .with_marker_stroke_width(1.0); // only the outline is drawn

let plots = vec![Plot::Scatter(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Hollow open circles — 800 pts in a noisy annulus")
    .with_x_label("X")
    .with_y_label("Y");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
800 hollow open circles forming a noisy ring

Point labels

Attach a text label to each point with .with_labels(iter) (aligned to the data by index; an empty string leaves a point unlabelled). The placement is controlled by .with_label_style(LabelStyle::…), shared with the volcano and Manhattan plots:

StylePlacement
LabelStyle::ExactAt the point, no adjustment (may overlap)
LabelStyle::Nudge (default)Sorted left-to-right and nudged vertically
LabelStyle::Arrow { offset_x, offset_y }Fixed pixel offset with a leader line
LabelStyle::RepelForce-directed (ggrepel / adjustText style): labels pushed off each other and off the points in 2-D, with thin leader lines

.with_repel_labels() is shorthand for .with_label_style(LabelStyle::Repel), the best choice for dense, clustered labels.

#![allow(unused)]
fn main() {
use kuva::plot::ScatterPlot;
let plot = ScatterPlot::new()
    .with_data(data)
    .with_labels(vec!["Alpha", "Beta", "Gamma", "Delta"])
    .with_repel_labels();
}
Scatter with force-directed point labels

Multiple series

Wrap multiple ScatterPlot structs in a Vec<Plot> and pass them to render_multiple(). Legends are shown when any series has a label attached via .with_legend().

#![allow(unused)]
fn main() {
use kuva::plot::scatter::ScatterPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let series_a = ScatterPlot::new()
    .with_data(vec![(1.0_f64, 2.0_f64), (3.0, 4.0), (5.0, 3.5)])
    .with_color("steelblue")
    .with_legend("Series A");

let series_b = ScatterPlot::new()
    .with_data(vec![(1.0_f64, 5.0_f64), (3.0, 6.5), (5.0, 7.0)])
    .with_color("crimson")
    .with_legend("Series B");

let plots = vec![Plot::Scatter(series_a), Plot::Scatter(series_b)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Two Series")
    .with_x_label("X")
    .with_y_label("Y");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Multiple scatter series with legend

API reference

MethodDescription
ScatterPlot::new()Create a new scatter plot with defaults
.with_data(iter)Set (x, y) data; accepts any Into<f64> numeric type
.with_color(s)Set uniform point color (CSS color string, default "black")
.with_colors(iter)Set per-point colors; falls back to .with_color for out-of-range indices
.with_size(r)Set uniform point radius in pixels (default 3.0)
.with_sizes(iter)Set per-point radii (bubble plot); falls back to .with_size for out-of-range indices
.with_marker(MarkerShape)Set marker shape (default Circle)
.with_legend(s)Attach a legend label to this series
.with_trend(TrendLine)Overlay a trend line
.with_loess()Overlay a LOESS smoother (default span 0.5)
.with_loess_span(f)LOESS smoother with an explicit span (0.05–1.0)
.with_trend_color(s)Set trend/smoother line color
.with_trend_width(w)Set trend/smoother line stroke width
.with_equation()Annotate the plot with the regression equation
.with_correlation()Annotate the plot with signed Pearson r
.with_x_err(iter)Symmetric X error bars
.with_x_err_asymmetric(iter)Asymmetric X error bars: (neg, pos) tuples
.with_y_err(iter)Symmetric Y error bars
.with_y_err_asymmetric(iter)Asymmetric Y error bars: (neg, pos) tuples
.with_band(lower, upper)Confidence band aligned to scatter x positions
.with_marker_opacity(f)Fill alpha: 0.0 = hollow, 1.0 = solid (default: solid)
.with_marker_stroke_width(w)Outline stroke at the fill color; None = no stroke (default)
.with_labels(iter)Per-point text labels (aligned by index; "" skips a point)
.with_label_style(LabelStyle)Label placement: Exact / Nudge / Arrow / Repel
.with_repel_labels()Shorthand for .with_label_style(LabelStyle::Repel)

MarkerShape variants

Circle · Square · Triangle (up) · TriangleDown · Diamond · Cross · Plus · Star · Pentagon · Hexagon

TrendLine variants

Linear: fits y = mx + b by ordinary least squares. Loess { span }: locally-weighted regression smoother for non-linear trends.

See also: Line Plot for connected/ordered data, Hexbin Plot for large-N density, Joint Plot for scatter with marginal distributions.


CLI

Scatter plot of (x, y) point pairs. Supports multi-series coloring, trend lines, log scale, and .parquet input (like every other subcommand, not just this one).

Input: any tabular file with two numeric columns.

FlagDefaultDescription
--x <COL>0X-axis column
--y <COL>1Y-axis column
--color-by <COL>—Group by this column; each group gets a distinct color
--color <CSS>steelbluePoint color (single-series only)
--size <PX>3.0Point radius in pixels
--marker <SHAPE>circleMarker shape: circle, square, triangle, triangle-down, diamond, cross, plus, star, pentagon, hexagon
--trendoffOverlay a linear trend line
--loessoffOverlay a LOESS smoother (local regression) instead of a linear trend
--loess-span <F>0.5LOESS span, 0.05–1.0 (implies --loess); smaller = wigglier
--equationoffAnnotate with regression equation (requires --trend)
--correlationoffAnnotate with signed Pearson r (requires --trend)
--label-col <COL>—Label each point with this column (single-series mode only)
--label-style <STYLE>nudgePoint-label placement: nudge, exact, or repel
--legendoffShow legend
--x-date-format <FMT>—Parse the X column as a date/time (see Date/time X axis)
kuva scatter measurements.tsv --x time --y value --color steelblue

kuva scatter measurements.tsv --x time --y value \
    --color-by group --legend --title "Expression over time"

kuva scatter measurements.tsv --x time --y value \
    --trend --equation --correlation --log-y

# star markers with a LOESS smoother
kuva scatter measurements.tsv --x time --y value \
    --marker star --loess-span 0.3

kuva scatter prices.tsv --x date --y close --x-date-format "%Y-%m-%d"

See also: Shared flags — output, appearance, axes, log scale.