use bevy::input::mouse::{MouseMotion, MouseWheel};
use bevy::prelude::*; use nalgebra::{DMatrix, DVector};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy)]
pub enum PlotColor {
Black,
White,
Red,
Green,
Blue,
Yellow,
Cyan,
Magenta,
DarkRed,
DarkGreen,
DarkBlue,
LightRed,
LightGreen,
LightBlue,
Orange,
Purple,
Brown,
Pink,
Gray,
DarkGray,
LightGray,
}
impl PlotColor {
pub fn to_bevy_color(self) -> Color {
match self {
PlotColor::Black => Color::srgb(0.0, 0.0, 0.0),
PlotColor::White => Color::srgb(1.0, 1.0, 1.0),
PlotColor::Red => Color::srgb(1.0, 0.0, 0.0),
PlotColor::Green => Color::srgb(0.0, 0.8, 0.0),
PlotColor::Blue => Color::srgb(0.0, 0.0, 1.0),
PlotColor::Yellow => Color::srgb(1.0, 1.0, 0.0),
PlotColor::Cyan => Color::srgb(0.0, 1.0, 1.0),
PlotColor::Magenta => Color::srgb(1.0, 0.0, 1.0),
PlotColor::DarkRed => Color::srgb(0.5, 0.0, 0.0),
PlotColor::DarkGreen => Color::srgb(0.0, 0.5, 0.0),
PlotColor::DarkBlue => Color::srgb(0.0, 0.0, 0.5),
PlotColor::LightRed => Color::srgb(1.0, 0.5, 0.5),
PlotColor::LightGreen => Color::srgb(0.5, 1.0, 0.5),
PlotColor::LightBlue => Color::srgb(0.5, 0.5, 1.0),
PlotColor::Orange => Color::srgb(1.0, 0.5, 0.0),
PlotColor::Purple => Color::srgb(0.5, 0.0, 0.5),
PlotColor::Brown => Color::srgb(0.6, 0.3, 0.1),
PlotColor::Pink => Color::srgb(1.0, 0.7, 0.8),
PlotColor::Gray => Color::srgb(0.5, 0.5, 0.5),
PlotColor::DarkGray => Color::srgb(0.3, 0.3, 0.3),
PlotColor::LightGray => Color::srgb(0.8, 0.8, 0.8),
}
}
pub fn default_sequence() -> Vec<PlotColor> {
vec![
PlotColor::Red,
PlotColor::Green,
PlotColor::Blue,
PlotColor::Orange,
PlotColor::Purple,
PlotColor::Cyan,
PlotColor::Magenta,
PlotColor::Brown,
PlotColor::Pink,
PlotColor::DarkRed,
PlotColor::DarkGreen,
PlotColor::DarkBlue,
]
}
}
pub struct MultiPlotter {
pub subplot_height: f32,
pub subplot_spacing: f32,
pub nticks_x: usize,
pub nticks_y: usize,
pub axis_font_size: f32,
pub line_width: f32,
pub colors: Option<HashMap<String, PlotColor>>,
pub x_axis_label: Option<String>,
}
impl Default for MultiPlotter {
fn default() -> Self {
Self {
subplot_height: 2.0,
subplot_spacing: 0.1,
nticks_x: 6,
nticks_y: 5,
axis_font_size: 12.0,
line_width: 1.0,
colors: None,
x_axis_label: None,
}
}
}
#[derive(Resource)]
struct MultiPlotData {
x: DVector<f64>,
ys: DMatrix<f64>,
labels: Vec<String>,
config: MultiPlotter,
}
#[derive(Component)]
struct CameraController {
zoom: f32,
offset: Vec2,
}
impl MultiPlotter {
pub fn new() -> Self {
Self::default()
}
pub fn plot_static_multiplot(self, x: DVector<f64>, ys: DMatrix<f64>, labels: Vec<String>) {
App::new() .add_plugins(DefaultPlugins) .insert_resource(ClearColor(Color::srgb(1.0, 1.0, 1.0))) .insert_resource(MultiPlotData {
x,
ys,
labels,
config: self,
}) .add_systems(Startup, (setup_camera, setup_axis_labels)) .add_systems(Update, (draw_static_multiplot, camera_controller_system)) .run(); }
}
pub fn plot_static_multiplot(x: DVector<f64>, ys: DMatrix<f64>, labels: Vec<String>) {
MultiPlotter::default().plot_static_multiplot(x, ys, labels);
}
fn setup_camera(mut commands: Commands, plot_data: Res<MultiPlotData>) {
let x = &plot_data.x;
let ys = &plot_data.ys;
let nplots = ys.nrows();
let x_min = x.min();
let x_max = x.max();
let x_range = x_max - x_min;
let subplot_height = plot_data.config.subplot_height;
let subplot_spacing = plot_data.config.subplot_spacing;
let total_height = nplots as f64 * (subplot_height + subplot_spacing) as f64;
let padding = 0.1;
let width_ratio = x_range;
let height_ratio = total_height;
let zoom = 10.0 / (width_ratio.max(height_ratio) * (1.0 + padding)) as f32;
commands.spawn((
Camera2d, Transform::from_xyz(0.0, 0.0, 1.0), CameraController {
zoom,
offset: Vec2::ZERO,
},
));
}
fn camera_controller_system(
mut mouse_motion: MessageReader<MouseMotion>, mut scroll_events: MessageReader<MouseWheel>, mouse_input: Res<ButtonInput<MouseButton>>, mut camera_query: Query<(&mut Transform, &mut CameraController), With<Camera2d>>, ) {
for (mut transform, mut controller) in camera_query.iter_mut() {
if mouse_input.pressed(MouseButton::Left) {
for motion in mouse_motion.read() {
let pan_speed = 2.0;
controller.offset.x -= motion.delta.x * pan_speed / controller.zoom;
controller.offset.y += motion.delta.y * pan_speed / controller.zoom;
}
}
for scroll in scroll_events.read() {
controller.zoom *= 1.0 - scroll.y * 0.1; controller.zoom = controller.zoom.clamp(0.001, 10000.0); }
transform.translation.x = controller.offset.x;
transform.translation.y = controller.offset.y;
transform.scale = Vec3::splat(1.0 / controller.zoom); }
if !mouse_input.pressed(MouseButton::Left) {
mouse_motion.clear();
}
}
fn draw_static_multiplot(mut gizmos: Gizmos, mut commands: Commands, plot: Res<MultiPlotData>) {
let x = &plot.x;
let ys = &plot.ys;
let nplots = ys.nrows(); let npoints = x.len();
let subplot_height = plot.config.subplot_height;
let subplot_spacing = plot.config.subplot_spacing;
let total_height = nplots as f32 * (subplot_height + subplot_spacing);
let top_offset = total_height / 2.0 - subplot_spacing / 2.0;
let nticks_x = plot.config.nticks_x;
let nticks_y = plot.config.nticks_y;
for r in 0..nplots {
let y = ys.row(r);
let min_x = x.min();
let max_x = x.max();
let min_y = y.min();
let max_y = y.max();
let xrange = (max_x - min_x).max(1e-9); let yrange = (max_y - min_y).max(1e-9);
let vshift = -(r as f32) * (subplot_height + subplot_spacing) + top_offset;
let axis_y_world = vshift * 80.0;
let map = |xi: f64, yi: f64| -> Vec2 {
let xn = ((xi - min_x) / xrange * 2.0 - 1.0) as f32;
let yn = ((yi - min_y) / yrange * 2.0 - 1.0) as f32;
Vec2::new(xn * 200.0, yn * 100.0 + axis_y_world)
};
let axis_color = Color::srgb(0.0, 0.0, 0.0); gizmos.line_2d(
Vec2::new(-220.0, axis_y_world),
Vec2::new(220.0, axis_y_world),
axis_color,
);
gizmos.line_2d(
Vec2::new(0.0, axis_y_world - 120.0),
Vec2::new(0.0, axis_y_world + 120.0),
axis_color,
);
for i in 0..=nticks_x {
let t = i as f64 / nticks_x as f64; let xv = min_x + xrange * t; let wx = (((xv - min_x) / xrange * 2.0 - 1.0) as f32) * 200.0; let wy = axis_y_world;
gizmos.line_2d(Vec2::new(wx, wy - 6.0), Vec2::new(wx, wy + 6.0), axis_color);
commands.spawn((
Text2d(format!("{:.2}", xv)), Transform::from_xyz(wx, wy - 18.0, 0.1), TextFont {
font_size: plot.config.axis_font_size,
..default() },
TextColor(Color::srgb(0.1, 0.1, 0.1)), ));
}
for i in 0..=nticks_y {
let t = i as f64 / nticks_y as f64; let yv = min_y + yrange * t; let wy = (((yv - min_y) / yrange * 2.0 - 1.0) as f32) * 100.0 + axis_y_world; let wx_left = -6.0;
let wx_right = 6.0;
gizmos.line_2d(Vec2::new(wx_left, wy), Vec2::new(wx_right, wy), axis_color);
commands.spawn((
Text2d(format!("{:.2}", yv)),
Transform::from_xyz(-240.0, wy - 6.0, 0.1), TextFont {
font_size: plot.config.axis_font_size,
..default()
},
TextColor(Color::srgb(0.1, 0.1, 0.1)),
));
}
let color = if let Some(ref color_map) = plot.config.colors {
color_map
.get(&plot.labels[r])
.map(|c| c.to_bevy_color())
.unwrap_or_else(|| {
let default_colors = PlotColor::default_sequence();
default_colors[r % default_colors.len()].to_bevy_color()
})
} else {
let default_colors = PlotColor::default_sequence();
default_colors[r % default_colors.len()].to_bevy_color()
};
for i in 0..npoints - 1 {
let p1 = map(x[i], y[i]);
let p2 = map(x[i + 1], y[i + 1]);
for offset in 0..(plot.config.line_width as i32) {
let offset_f = offset as f32 - plot.config.line_width * 0.5;
gizmos.line_2d(
Vec2::new(p1.x + offset_f, p1.y + offset_f * 0.5),
Vec2::new(p2.x + offset_f, p2.y + offset_f * 0.5),
color,
);
}
}
}
}
fn setup_axis_labels(mut commands: Commands, plot_data: Res<MultiPlotData>) {
let nplots = plot_data.ys.nrows();
let subplot_height = plot_data.config.subplot_height;
let subplot_spacing = plot_data.config.subplot_spacing;
let total_height = nplots as f32 * (subplot_height + subplot_spacing);
let top_offset = total_height / 2.0 - subplot_spacing / 2.0;
let label_color = Color::srgb(0.1, 0.1, 0.1);
for (r, label) in plot_data.labels.iter().enumerate() {
let vshift = -(r as f32) * (subplot_height + subplot_spacing) + top_offset;
let vshift_world = vshift * 80.0;
let subplot_half_world = subplot_height * 80.0 * 0.5;
let x_label = plot_data.config.x_axis_label.as_deref().unwrap_or("x");
commands.spawn((
Text2d(x_label.to_string()),
Transform::from_xyz(0.0, vshift_world - subplot_half_world - 10.0, 0.1),
TextFont {
font_size: 18.0, ..default()
},
TextColor(label_color),
));
let label_length = label.len() as f32;
let base_offset = -280.0; let char_width = 8.0; let label_offset = base_offset - (label_length * char_width);
commands.spawn((
Text2d(format!("{}", label)),
Transform::from_xyz(label_offset, vshift_world, 0.1),
TextFont {
font_size: plot_data.config.axis_font_size,
..default()
},
TextColor(label_color),
));
}
}