Polar Plot

A polar coordinate plot renders data in (r, θ) space — radial distance and angle — projected onto a circular canvas with a configurable grid.

By default kuva uses compass convention: θ=0 at north (top), increasing clockwise. To use math convention (θ=0 at east, increasing CCW), combine .with_theta_start(90.0) and .with_clockwise(false).

Polar plot — cardioid and reference circle

Rust API

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

// Cardioid: r = 1 + cos(θ)
let n = 72;
let theta: Vec<f64> = (0..n).map(|i| i as f64 * 360.0 / n as f64).collect();
let r: Vec<f64> = theta.iter().map(|&t| 1.0 + t.to_radians().cos()).collect();

let plot = PolarPlot::new()
    .with_series_labeled(r, theta, "Cardioid", PolarMode::Line)
    .with_r_max(2.1)
    .with_r_grid_lines(4)
    .with_theta_divisions(12)
    .with_legend(true);

let plots = vec![Plot::Polar(plot)];
let layout = Layout::auto_from_plots(&plots).with_title("Cardioid");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}

Scatter vs Line mode

#![allow(unused)]
fn main() {
// Scatter (default): each (r, θ) point is a circle
let plot = PolarPlot::new().with_series(r, theta);

// Line: points connected by a path
let plot = PolarPlot::new().with_series_line(r, theta);

// Labeled series (used for legends)
let plot = PolarPlot::new()
    .with_series_labeled(r, theta, "Wind speed", PolarMode::Scatter);
}

Conventions

#![allow(unused)]
fn main() {
// Compass convention (default): 0° = north, clockwise
let compass = PolarPlot::new()
    .with_theta_start(0.0)
    .with_clockwise(true);

// Math convention: 0° = east, CCW
let math = PolarPlot::new()
    .with_theta_start(90.0)
    .with_clockwise(false);
}

Theta tick labels

#![allow(unused)]
fn main() {
let mut theta: Vec<f64> = (0..8).map(|i| i as f64 * 45.0).collect();
theta.push(360.0);
let r_location1 = vec![4.8, 3.2, 2.8, 1.2, 0.5, 1.4, 2.8, 4.1, 4.8];
let r_location2 = vec![1.8, 2.2, 3.8, 4.2, 4.5, 3.4, 2.2, 1.1, 1.8];
let plot1 = PolarPlot::new()
    .with_series_labeled(r_location1, theta.clone(), "Location 1", PolarMode::Line)
    .with_theta_divisions(8)
    .with_r_max(5.0)
    .with_r_grid_lines(5)
    .with_color("steelblue")
    .with_legend(true);
let plot2 = PolarPlot::new()
    .with_series_labeled(r_location2, theta, "Location 2", PolarMode::Line)
    .with_theta_divisions(8)
    .with_r_max(5.0)
    .with_r_grid_lines(5)
    .with_color("orange")
    .with_legend(true);

let plots = vec![Plot::Polar(plot1), Plot::Polar(plot2)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Polar Plot with custom theta ticks")
    .with_x_tick_format(TickFormat::Custom(std::sync::Arc::new(
        |v| {
            let div = 360.0 / 8.0;
            if v < div {
                "eventful".to_string()
            } else if v < 2.0 * div {
                "exciting".to_string()
            } else if v < 3.0 * div {
                "pleasant".to_string()
            } else if v < 4.0 * div {
                "calm".to_string()
            } else if v < 5.0 * div {
                "uneventful".to_string()
            } else if v < 6.0 * div {
                "monotonous".to_string()
            } else if v < 7.0 * div {
                "unpleasant".to_string()
            } else {
                "chaotic".to_string()
            }
        }
    )));
}
Custom Tick Labels for Theta axis

Marker opacity and stroke (scatter mode)

Control fill transparency and an optional outline on scatter-mode points. Settings are per-series and must be called immediately after the series they apply to.

500 observations with two dominant directions (NE at 45° and SW at 225°). With solid markers each directional cluster collapses into an opaque wedge, hiding the internal spread. At opacity = 0.2 the denser core of each cluster is visibly darker than its fringe, and the thin 0.7 px stroke keeps individual observations readable.

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

// (populate r_vals and t_vals with 500 (r, theta_degrees) observations)
let (r_vals, t_vals): (Vec<f64>, Vec<f64>) = (vec![], vec![]);
let plot = PolarPlot::new()
    .with_series(r_vals, t_vals)
    .with_color("steelblue")
    .with_marker_opacity(0.2)
    .with_marker_stroke_width(0.7)
    .with_r_max(1.2)
    .with_theta_divisions(24);

let plots = vec![Plot::Polar(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Directional scatter — semi-transparent markers (500 pts)");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Polar scatter with two directional clusters and semi-transparent markers

These builders have no effect on PolarMode::Line series.

Grid control

#![allow(unused)]
fn main() {
let plot = PolarPlot::new()
    .with_r_grid_lines(5)        // 5 concentric rings
    .with_theta_divisions(8)     // 8 spokes (every 45°)
    .with_r_labels(true)         // show r value on each ring
    .with_grid(true);            // show grid (default)
}

Builder reference

MethodDefaultDescription
.with_series(r, theta)Add scatter series
.with_series_line(r, theta)Add line series
.with_series_labeled(r, theta, label, mode)Add labeled series
.with_r_max(f64)autoSet maximum radial extent
.with_theta_start(deg)0.0Where θ=0 appears (CW from north)
.with_clockwise(bool)trueDirection of increasing θ
.with_r_grid_lines(n)4Number of concentric grid circles
.with_theta_divisions(n)12Number of angular spokes
.with_grid(bool)trueShow/hide grid
.with_r_labels(bool)trueShow/hide r-value labels
.with_legend(bool)falseShow legend for labeled series
.with_color(s)Set fill color of the last added series
.with_marker_opacity(f)solidFill alpha for scatter markers of the last series (0.01.0)
.with_marker_stroke_width(w)noneOutline stroke for scatter markers of the last series

CLI

# Basic scatter
kuva polar data.tsv --r r --theta theta --title "Polar Plot"

# Line mode, multiple series via color-by
kuva polar data.tsv --r r --theta theta --color-by group --mode line

# Custom r-max and angular divisions
kuva polar data.tsv --r r --theta theta --r-max 5.0 --theta-divisions 8

CLI flags

FlagDefaultDescription
--r <COL>0Radial value column
--theta <COL>1Angle column (degrees)
--color-by <COL>One series per unique group value
--mode <MODE>scatterscatter or line
--r-max <F>autoMaximum radial extent
--theta-divisions <N>12Angular grid spokes
--theta-start <DEG>0.0Where θ=0 appears (CW from north)
--legendoffShow legend