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())))
}