specter-viz
WASM-first egui dashboard components for research visualization.
The crate is UI-focused and works inside any egui app. A native desktop app will typically use eframe, but specter-viz itself only depends on egui/egui_plot.
MSRV: Rust 1.87.
Environment
Enter the local project shell from addenda/specter-viz/:
nix developIf you use direnv, direnv allow is equivalent. The shell auto-runs cargo fetch --locked.
Install (in this repo)
[dependencies]
specter-viz = { path = "addenda/specter-viz" }Minimal Example (Native)
use eframe::egui;
use specter_viz::prelude::*;
struct App {
loss: Series,
loss_band: IntervalSeries,
}
impl Default for App {
fn default() -> Self {
Self {
loss: Series::from_values("loss", vec![1.0, 0.82, 0.71, 0.64], colors::ACCENT_BLUE),
loss_band: IntervalSeries::from_values(
"loss 95% CI",
vec![0.92, 0.75, 0.66, 0.60],
vec![1.08, 0.89, 0.76, 0.68],
colors::ACCENT_BLUE,
)
.expect("valid interval series"),
}
}
}
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
init(ctx);
egui::CentralPanel::default().show(ctx, |ui| {
Panel::new("Overview").show(ui, |ui| {
let mut grid = MetricGrid::new().columns(3);
grid.push(MetricCard::new("samples", "12_480"));
grid.push(MetricCard::new("loss", "0.64").accent(colors::ACCENT_BLUE));
grid.push(MetricCard::new("step", "918"));
grid.show(ui);
ui.add_space(spacing::MD);
LineChart::new()
.interval(&self.loss_band)
.series(&self.loss)
.interactions(PlotInteraction::enabled())
.try_show(ui)
.expect("render overview chart");
});
});
}
}
fn main() -> eframe::Result<()> {
let opts = eframe::NativeOptions::default();
eframe::run_native("specter-viz demo", opts, Box::new(|_cc| Ok(Box::<App>::default())))
}init(ctx) is idempotent, so calling it from update() is fine.
Data Model
Series, IntervalSeries, and PointSeries all support explicit coordinates.
Series::from_values(...)uses the item index asx.Series::from_xy(...)accepts explicitx/yvectors and validates that lengths match.IntervalSeries::from_values(...)andIntervalSeries::from_xy(...)do the same for interval bands.PointSeries::new(...)creates scatter data with optional labels and metadata.
LineChart::try_show(...) and ScatterPlot::try_show(...) return RenderError when values are non-finite or incompatible with the selected axis policy instead of silently dropping points.
Responsive Layout
Use ResponsiveGrid for dashboard sections and ResponsiveColumns for local sub-layouts.
use specter_viz::prelude::*;
let grid = ResponsiveGrid::new()
.columns(1, 2, 3)
.min_column_width(560.0)
.flow(GridFlow::Masonry);
let columns = ResponsiveColumns::new()
.columns(1, 2, 2)
.min_column_width(520.0);columns(...): preferred layout targets by breakpoint.min_column_width(...): hard floor that collapses columns when space is tight.flow(GridFlow::Rows | GridFlow::Masonry): row packing or compact masonry-style packing.
Data Loading
The specter_viz::data helpers load JSON and JSONL.
- On
wasm32, they usewindow.fetch(...), sopathis typically a URL. - On native targets, they read from the filesystem, so
pathis a local file path.
Expected formats:
JSON:
{"run_id":"run-001","metrics":[["loss",0.64],["accuracy",0.91]]}JSONL:
{"step":1,"loss":1.0}
{"step":2,"loss":0.82}
{"step":3,"loss":0.71}JSONL (native)
fetch_jsonl is async, so you need an executor. For small tools, pollster is a simple option:
use serde::Deserialize;
use specter_viz::data;
#[derive(Deserialize)]
struct Row {
step: u64,
loss: f64,
}
fn load_rows(path: &str) -> Vec<Row> {
pollster::block_on(async { data::fetch_jsonl(path).await }).expect("load jsonl")
}App crate deps for the snippet above:
pollster = "0.3"
serde = { version = "1", features = ["derive"] }JSON (wasm)
In wasm builds, load once by spawning an async task and storing the result in your app state:
use std::cell::RefCell;
use std::rc::Rc;
use serde::Deserialize;
use specter_viz::data;
#[derive(Clone, Deserialize)]
struct Snapshot {
run_id: String,
metrics: Vec<(String, f64)>,
}
fn start_loading(url: String, out: Rc<RefCell<Option<Result<Snapshot, String>>>>) {
wasm_bindgen_futures::spawn_local(async move {
let result = data::fetch_json::<Snapshot>(&url).await;
*out.borrow_mut() = Some(result);
});
}App crate deps for the snippet above:
wasm-bindgen-futures = "0.4"
serde = { version = "1", features = ["derive"] }Example Project Layout
Create a tiny app crate that depends on this addendum:
cargo new --bin specter-viz-demospecter-viz-demo/Cargo.toml:
[package]
name = "specter-viz-demo"
version = "0.1.0"
edition = "2021"
[dependencies]
eframe = "0.33"
specter-viz = { path = "../addenda/specter-viz" }
pollster = "0.3"specter-viz-demo/src/main.rs: copy the native example above.
Run it:
cargo run --manifest-path specter-viz-demo/Cargo.tomlIncluded
- Charts:
LineChart,ScatterPlot,HeatMap,GraphView - Views:
TreeView - Primitives:
Panel,MetricCard,MetricGrid,ChartHeader,Label,ResponsiveColumns,ResponsiveGrid,GridFlow - Data types:
Series,IntervalSeries,IntervalDatum,PointSeries,PointMeta,Graph,TreeNode - Data loading:
data::fetch_json,data::fetch_jsonl
License
Code in this addendum is licensed under PolyForm Noncommercial 1.0.0. Documentation and other non-code content, including this README, are licensed under CC-BY-NC-4.0. See LICENSE for the local project notice.