use hephaestus::backend::vello::MAX_DRAW_INFO_WORDS;
use hephaestus::color::{rgb8, Color};
use hephaestus::composition::{Composition, Patch, Span};
use hephaestus::plot::chrome::axis::{Axis, AxisPlacement};
use hephaestus::plot::{scale, Plot, PlotComposition, PointGeom};
use hephaestus::scales::chrome::AxisSide;
use hephaestus::window::{run, Backend, Event, EventCtx, Frame, WindowApp, WindowConfig};
use std::time::Duration;
const BASE_SIZE: f64 = 1.0;
const HOVER_SIZE: f64 = 7.0;
const DEFAULT_POINTS: usize = 40;
const X_RANGE: (f64, f64) = (0.0, 100.0);
const Y_RANGE: (f64, f64) = (40.0, 90.0);
struct Xorshift(u64);
impl Xorshift {
fn new() -> Self {
Self(0x2545_f491_4f6c_dd1d)
}
fn unit(&mut self) -> f64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
(self.0 >> 11) as f64 / (1u64 << 53) as f64
}
}
struct Demo {
view: PlotComposition,
xs: Vec<f64>,
ys: Vec<f64>,
hovered: Option<u32>,
}
impl WindowApp for Demo {
fn draw(&mut self, frame: &mut Frame<'_>) {
let (scene, size, dpi) = frame.parts();
self.view.render(scene, size, dpi);
}
fn event(&mut self, ctx: &mut EventCtx<'_>, event: Event) {
match event {
Event::CursorMoved { position } => {
let hit = ctx.pick_at(position.x.max(0.0) as u32, position.y.max(0.0) as u32);
if hit != self.hovered {
self.hovered = hit;
self.rebuild_geom();
ctx.request_redraw();
}
}
Event::CursorLeft if self.hovered.take().is_some() => {
self.rebuild_geom();
ctx.request_redraw();
}
Event::MouseDown { button } => {
println!("{button:?} press over {:?}", self.hovered);
}
Event::CloseRequested => ctx.exit(),
_ => {}
}
}
}
impl Demo {
fn new(n: usize) -> Self {
let mut rng = Xorshift::new();
let xs: Vec<f64> = (0..n)
.map(|_| X_RANGE.0 + rng.unit() * (X_RANGE.1 - X_RANGE.0))
.collect();
let ys: Vec<f64> = (0..n)
.map(|_| Y_RANGE.0 + rng.unit() * (Y_RANGE.1 - Y_RANGE.0))
.collect();
let comp = || Composition::empty(1, 1).place(1, 1, Span::cell(), Patch::new("panel"));
let mut plot = Plot::new(&comp(), "panel")
.bind("x", "time")
.bind("y", "price");
plot.set_title("Hover a point");
plot.add_axis(Axis::rail(
"time",
AxisPlacement::Cartesian(AxisSide::Bottom),
));
plot.add_axis(Axis::rail(
"price",
AxisPlacement::Cartesian(AxisSide::Left),
));
let view = PlotComposition::new(&comp())
.add_scale("time", scale::continuous(X_RANGE.0..=X_RANGE.1))
.add_scale("price", scale::continuous(Y_RANGE.0..=Y_RANGE.1))
.with_plot(plot);
let mut demo = Self {
view,
xs,
ys,
hovered: None,
};
demo.rebuild_geom();
demo
}
fn rebuild_geom(&mut self) {
let ids: Vec<f64> = (1..=self.xs.len()).map(|i| i as f64).collect();
let sizes: Vec<f64> = ids
.iter()
.map(|id| {
if Some(*id as u32) == self.hovered {
HOVER_SIZE
} else {
BASE_SIZE
}
})
.collect();
let fills: Vec<Color> = ids
.iter()
.map(|id| {
if Some(*id as u32) == self.hovered {
rgb8(220, 90, 70)
} else {
rgb8(70, 120, 220)
}
})
.collect();
let xs = self.xs.clone();
let ys = self.ys.clone();
self.view.update_plot("panel", |plot| {
let existing: Vec<_> = plot.geom_ids().collect();
for id in existing {
plot.remove_geom(id);
}
plot.add_geom(
PointGeom::builder()
.set("x", xs)
.set("y", ys)
.set("fill", fills)
.set("size", sizes)
.set("pick_id", ids)
.build(),
);
});
}
}
fn main() {
let points = match std::env::args().nth(1) {
Some(arg) => arg.parse().expect("point count must be a positive integer"),
None => DEFAULT_POINTS,
};
let backend = pick_backend(std::env::args().nth(2).as_deref());
println!(
"drawing {points} points on {backend:?}; \
the compute-shader cap is {MAX_DRAW_INFO_WORDS} draws per scene"
);
let config = WindowConfig::new("hephaestus — live plot")
.size(900, 560)
.background(rgb8(248, 248, 252))
.picking(true)
.backend(backend)
.pick_interval(Duration::from_millis(30));
if let Err(err) = run(config, Demo::new(points)) {
eprintln!("window: {err}");
std::process::exit(1);
}
}
fn pick_backend(arg: Option<&str>) -> Backend {
match arg {
#[cfg(feature = "vello-hybrid")]
Some("hybrid") => Backend::Hybrid,
Some(other) if other != "vello" => {
panic!("unknown backend {other:?}; expected `vello` or `hybrid`")
}
_ => Backend::default(),
}
}