Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-plotters/examples/plotters_demo.rs

//! **plotters_demo** — a live gallery of the charts `egui_plot` lacks, all
//! hand-painted on the egui `Painter` and **theme-driven** (no hardcoded
//! palette). A tab per chart hosts a [`facett_core::Facet`]:
//!
//! - **Line / Bar / Scatter** — the everyday XY [`ChartView`] over named series,
//!   series-coloured from the validated categorical palette (same data, 3 tabs)
//! - **Box** — quartiles + 1.5·IQR whiskers + outliers, per group
//! - **Violin** — Gaussian-KDE silhouette + median
//! - **Heatmap** — a matrix → colour grid with a gradient legend
//! - **Candlestick** — OHLC wick+body bars
//! - **Treemap** — weighted slice-and-dice rectangles
//!
//! A theme switcher proves every fill/ramp re-skins live; Ctrl-C copies the
//! active chart's underlying data as CSV (routed the way a `FacetDeck` does).
//!
//! Run: `cargo run -p facett-plotters --example plotters_demo`

use eframe::egui;
use facett_core::{FacetDeck, Theme, set_theme};
use facett_plotters::{
    BoxPlotView, CandlestickView, ChartKind, ChartView, HeatmapView, Ohlc, Series, Tile,
    TreemapView, ViolinView,
};

/// A reproducible pseudo-random sample around `mean` with spread `sd`, plus a
/// couple of planted outliers — enough to exercise whiskers/KDE without a rng dep.
fn sample(seed: u64, mean: f64, sd: f64, n: usize) -> Vec<f64> {
    let mut s = seed.wrapping_mul(0x9E3779B97F4A7C15).max(1);
    let mut next = || {
        // xorshift64 → uniform in [0,1)
        s ^= s << 13;
        s ^= s >> 7;
        s ^= s << 17;
        (s >> 11) as f64 / (1u64 << 53) as f64
    };
    let mut v = Vec::with_capacity(n);
    for _ in 0..n {
        // Box–Muller for a rough normal.
        let (u1, u2) = (next().max(1e-9), next());
        let z = (-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos();
        v.push(mean + z * sd);
    }
    v
}

fn boxplot() -> BoxPlotView {
    let mut groups = vec![
        ("hugin".into(), sample(1, 50.0, 8.0, 60)),
        ("munin".into(), sample(2, 62.0, 12.0, 60)),
        ("oden".into(), sample(3, 45.0, 5.0, 60)),
    ];
    // plant an outlier
    groups[0].1.push(120.0);
    BoxPlotView::new("Latency by node (ms)", groups)
}

fn violin() -> ViolinView {
    ViolinView::new(
        "Score distribution",
        vec![
            ("A".into(), sample(7, 30.0, 6.0, 120)),
            ("B".into(), sample(8, 48.0, 10.0, 120)),
            ("C".into(), sample(9, 40.0, 4.0, 120)),
        ],
    )
}

fn heatmap() -> HeatmapView {
    let (rows, cols) = (6, 8);
    let mut data = Vec::with_capacity(rows * cols);
    for r in 0..rows {
        for c in 0..cols {
            // a smooth field + a hotspot
            let v = ((r as f64 / 2.0).sin() + (c as f64 / 3.0).cos()) * 5.0
                + if r == 2 && c == 5 { 12.0 } else { 0.0 };
            data.push(v);
        }
    }
    HeatmapView::new("Activity matrix", rows, cols, data).with_labels(
        (0..rows).map(|r| format!("port{r}")).collect(),
        (0..cols).map(|c| format!("h{c}")).collect(),
    )
}

fn candlestick() -> CandlestickView {
    // a synthetic price walk
    let mut price = 100.0_f64;
    let mut s = 42u64;
    let mut rnd = || {
        s ^= s << 13;
        s ^= s >> 7;
        s ^= s << 17;
        ((s >> 11) as f64 / (1u64 << 53) as f64) - 0.5
    };
    let bars = (0..20)
        .map(|i| {
            let open = price;
            let drift = rnd() * 6.0;
            let close = (open + drift).max(1.0);
            let high = open.max(close) + rnd().abs() * 4.0;
            let low = open.min(close) - rnd().abs() * 4.0;
            price = close;
            Ohlc::new(format!("d{i}"), open, high, low, close)
        })
        .collect();
    CandlestickView::new("ACME price (OHLC)", bars)
}

fn treemap() -> TreemapView {
    TreemapView::new(
        "Storage by table",
        vec![
            Tile::new("events", 420.0),
            Tile::new("vessels", 180.0),
            Tile::new("ports", 90.0),
            Tile::new("ais_gaps", 60.0),
            Tile::new("reports", 35.0),
            Tile::new("audit", 22.0),
            Tile::new("misc", 12.0),
        ],
    )
}

/// Three named series over a shared x — the same data shown three ways so the
/// line/bar/scatter tabs are directly comparable.
fn series() -> Vec<Series> {
    vec![
        Series::new("north", (0..8).map(|i| (i as f64, 20.0 + 8.0 * (i as f64 * 0.7).sin())).collect()),
        Series::new("south", (0..8).map(|i| (i as f64, 14.0 + 1.6 * i as f64)).collect()),
        Series::new("west", (0..8).map(|i| (i as f64, 30.0 - 2.0 * i as f64)).collect()),
    ]
}

struct Demo {
    theme_idx: usize,
    deck: FacetDeck,
}

impl Default for Demo {
    fn default() -> Self {
        let deck = FacetDeck::new(vec![
            Box::new(ChartView::new("Throughput — line", series())),
            Box::new(ChartView::new("Throughput — bar", series()).with_kind(ChartKind::Bar)),
            Box::new(ChartView::new("Throughput — scatter", series()).with_kind(ChartKind::Scatter)),
            Box::new(boxplot()),
            Box::new(violin()),
            Box::new(heatmap()),
            Box::new(candlestick()),
            Box::new(treemap()),
        ]);
        Self { theme_idx: 2, deck }
    }
}

impl eframe::App for Demo {
    fn ui(&mut self, root: &mut egui::Ui, _frame: &mut eframe::Frame) {
        let ctx = root.ctx().clone();
        set_theme(&ctx, Theme::ALL[self.theme_idx]());

        egui::Panel::top("controls").show(root, |ui| {
            ui.horizontal_wrapped(|ui| {
                ui.label("Theme:");
                for (i, ctor) in Theme::ALL.iter().enumerate() {
                    ui.selectable_value(&mut self.theme_idx, i, ctor().name);
                }
            });
            ui.label("Hover a chart for a readout · Ctrl-C copies the active chart's data as CSV");
        });

        // The deck draws the tab bar + capability toolbar + active chart, and
        // routes Ctrl-C to the active facet's copy() per its caps.
        egui::CentralPanel::default().show(root, |ui| {
            self.deck.ui(ui);
        });
    }
}

fn main() -> eframe::Result {
    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default().with_inner_size(egui::vec2(820.0, 640.0)),
        ..Default::default()
    };
    eframe::run_native("facett · plotters_demo", options, Box::new(|_cc| Ok(Box::<Demo>::default())))
}