use crate::prelude::components::PlotId;
use bevy_math::{UVec2, Vec2, Vec3};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct PlotMeta {
pub title: Option<String>,
pub description: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Color {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl Color {
pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
Self { r, g, b, a }
}
pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
Self { r, g, b, a: 1.0 }
}
pub const fn with_a(self, a: f32) -> Self {
Self { a, ..self }
}
pub const BLACK: Self = Self::rgb(0.0, 0.0, 0.0);
pub const WHITE: Self = Self::rgb(1.0, 1.0, 1.0);
pub const RED: Self = Self::rgb(1.0, 0.0, 0.0);
pub const GREEN: Self = Self::rgb(0.0, 1.0, 0.0);
pub const BLUE: Self = Self::rgb(0.0, 0.0, 1.0);
pub const YELLOW: Self = Self::rgb(1.0, 1.0, 0.0);
}
impl From<Color> for bevy::prelude::Color {
#[inline]
fn from(c: Color) -> Self {
bevy::prelude::Color::linear_rgba(c.r, c.g, c.b, c.a)
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Style {
pub color: Color,
pub size: f32, pub opacity: f32, }
impl Default for Style {
fn default() -> Self {
Self {
color: Color::BLACK,
size: 2.0,
opacity: 1.0,
}
}
}
impl Style {
#[inline]
pub const fn color(mut self, c: Color) -> Self {
self.color = c;
self
}
#[inline]
pub const fn rgb(self, r: f32, g: f32, b: f32) -> Self {
self.color(Color::rgb(r, g, b))
}
#[inline]
pub const fn rgba(self, r: f32, g: f32, b: f32, a: f32) -> Self {
self.color(Color::rgba(r, g, b, a))
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum Scale {
Linear,
Log10,
Symlog { lin_thresh: f64 },
Power { exponent: f64 },
Time,
Categorical,
}
impl Default for Scale {
fn default() -> Self {
Self::Linear
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Interaction {
pub pan: bool,
pub zoom: bool,
pub rotate: bool,
}
impl Default for Interaction {
fn default() -> Self {
Self {
pan: true,
zoom: true,
rotate: true,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Plot {
Graph2D(Graph2D),
Graph3D(Graph3D),
Distribution(Distribution),
Field(Field),
Radial(Radial),
Candlestick(Candlestick),
Heatmap(Heatmap),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Graph2D {
pub id: PlotId,
pub meta: PlotMeta,
pub layers: Vec<Layer2D>,
pub x_scale: Scale,
pub y_scale: Scale,
pub interaction: Interaction,
pub x_label: Option<String>,
pub y_label: Option<String>,
}
impl Default for Graph2D {
fn default() -> Self {
Self {
id: PlotId::new(),
meta: PlotMeta::default(),
layers: vec![],
x_scale: Scale::default(),
y_scale: Scale::default(),
interaction: Interaction::default(),
x_label: None,
y_label: None,
}
}
}
impl Graph2D {
pub fn new() -> Self {
Self::default()
}
pub fn with_layer(mut self, layer: Layer2D) -> Self {
self.layers.push(layer);
self
}
pub fn bounds(&self) -> Option<([f32; 2], [f32; 2])> {
let mut min = [f32::INFINITY; 2];
let mut max = [f32::NEG_INFINITY; 2];
let mut any = false;
for l in &self.layers {
for p in &l.xy {
if !p.x.is_finite() || !p.y.is_finite() {
continue;
}
min[0] = min[0].min(p.x);
min[1] = min[1].min(p.y);
max[0] = max[0].max(p.x);
max[1] = max[1].max(p.y);
any = true;
}
if let Some(lower) = &l.lower_line {
for p in lower {
if !p.x.is_finite() || !p.y.is_finite() {
continue;
}
min[0] = min[0].min(p.x);
min[1] = min[1].min(p.y);
max[0] = max[0].max(p.x);
max[1] = max[1].max(p.y);
any = true;
}
}
}
any.then_some((min, max))
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum Geometry2D {
Line,
Points,
Area,
Bars, Stems, FillBetween, }
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Layer2D {
pub geometry: Geometry2D,
pub xy: Vec<Vec2>,
pub style: Style,
pub lower_line: Option<Vec<Vec2>>,
pub sizes: Option<Vec<f32>>,
}
impl Layer2D {
pub fn new(geometry: Geometry2D, xy: Vec<Vec2>) -> Self {
Self {
geometry,
xy,
style: Style::default(),
lower_line: None,
sizes: None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Graph3D {
pub id: PlotId,
pub meta: PlotMeta,
pub layers: Vec<Layer3D>,
pub interaction: Interaction,
pub x_label: Option<String>,
pub y_label: Option<String>,
pub z_label: Option<String>,
}
impl Default for Graph3D {
fn default() -> Self {
Self {
id: PlotId::new(),
meta: PlotMeta::default(),
layers: vec![],
interaction: Interaction {
rotate: true,
..Interaction::default()
},
x_label: None,
y_label: None,
z_label: None,
}
}
}
impl Graph3D {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum Geometry3D {
Points,
Surface { grid: UVec2 }, }
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Layer3D {
pub geometry: Geometry3D,
pub xyz: Vec<Vec3>,
pub style: Style,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Distribution {
Histogram {
meta: PlotMeta,
values: Vec<f32>,
bins: usize,
style: Style,
x_label: Option<String>,
y_label: Option<String>,
},
Pdf {
meta: PlotMeta,
values: Vec<f32>,
style: Style,
x_label: Option<String>,
y_label: Option<String>,
},
BoxPlot {
meta: PlotMeta,
groups: Vec<(String, Vec<f32>)>,
style: Style,
x_label: Option<String>,
y_label: Option<String>,
},
ECDF {
meta: PlotMeta,
values: Vec<f32>,
style: Style,
x_label: Option<String>,
y_label: Option<String>,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Field {
pub meta: PlotMeta,
pub dims: UVec2, pub values: Vec<f32>, pub vmin: f32,
pub vmax: f32,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Radial {
Pie {
meta: PlotMeta,
slices: Vec<(String, f32)>, },
Radar {
meta: PlotMeta,
axes: Vec<String>, values: Vec<f32>, style: Style,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Candlestick {
pub meta: PlotMeta,
pub candles: Vec<OHLC>,
pub up_color: Color,
pub down_color: Color,
pub x_label: Option<String>,
pub y_label: Option<String>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct OHLC {
pub x: f32, pub open: f32,
pub high: f32,
pub low: f32,
pub close: f32,
}
impl OHLC {
pub fn new(x: f32, open: f32, high: f32, low: f32, close: f32) -> Self {
Self {
x,
open,
high,
low,
close,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Heatmap {
pub meta: PlotMeta,
pub dims: UVec2, pub values: Vec<f32>, pub vmin: Option<f32>, pub vmax: Option<f32>, pub row_labels: Option<Vec<String>>,
pub col_labels: Option<Vec<String>>,
pub show_values: bool, pub colormap: Colormap,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Default)]
pub enum Colormap {
#[default]
Viridis,
Plasma,
Inferno,
Magma,
Coolwarm, RdBu, Blues,
Reds,
Greens,
}
impl Colormap {
pub fn sample(&self, t: f32) -> Color {
let t = t.clamp(0.0, 1.0);
match self {
Colormap::Viridis => Self::viridis(t),
Colormap::Plasma => Self::plasma(t),
Colormap::Inferno => Self::inferno(t),
Colormap::Magma => Self::magma(t),
Colormap::Coolwarm => Self::coolwarm(t),
Colormap::RdBu => Self::rdbu(t),
Colormap::Blues => Self::blues(t),
Colormap::Reds => Self::reds(t),
Colormap::Greens => Self::greens(t),
}
}
fn viridis(t: f32) -> Color {
let opacity = 0.3 + t * 0.7;
Color::rgba(0.2, 0.4, 0.9, opacity)
}
fn plasma(t: f32) -> Color {
let opacity = 0.3 + t * 0.7;
Color::rgba(0.9, 0.2, 0.2, opacity)
}
fn inferno(t: f32) -> Color {
let opacity = 0.3 + t * 0.7;
Color::rgba(0.2, 0.8, 0.3, opacity)
}
fn magma(t: f32) -> Color {
let r = 0.2;
let g = 0.3 + t * 0.5;
let b = 0.9 - t * 0.6;
Color::rgba(r, g, b, 0.5 + t * 0.4)
}
fn coolwarm(t: f32) -> Color {
if t < 0.5 {
let s = (0.5 - t) * 2.0; Color::rgba(0.2, 0.4, 0.9, 0.3 + s * 0.6) } else {
let s = (t - 0.5) * 2.0; Color::rgba(0.9, 0.2, 0.2, 0.3 + s * 0.6) }
}
fn rdbu(t: f32) -> Color {
if t < 0.5 {
let s = (0.5 - t) * 2.0;
Color::rgba(0.9, 0.2, 0.2, 0.3 + s * 0.6) } else {
let s = (t - 0.5) * 2.0;
Color::rgba(0.2, 0.4, 0.9, 0.3 + s * 0.6) }
}
fn blues(t: f32) -> Color {
Color::rgba(0.2, 0.5, 1.0, 0.2 + t * 0.7)
}
fn reds(t: f32) -> Color {
Color::rgba(1.0, 0.25, 0.2, 0.2 + t * 0.7)
}
fn greens(t: f32) -> Color {
Color::rgba(0.2, 0.85, 0.35, 0.2 + t * 0.7)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Tab {
pub name: String,
pub plots: Vec<Plot>,
pub columns: Option<usize>,
}
impl Tab {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
plots: vec![],
columns: None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Dashboard {
pub background: Color,
pub plots: Vec<Plot>,
pub columns: Option<usize>,
pub tabs: Vec<Tab>,
pub active_tab: usize,
}
impl Default for Dashboard {
fn default() -> Self {
Self {
background: Color::rgba(0.05, 0.05, 0.09, 1.0),
plots: vec![],
columns: None,
tabs: vec![],
active_tab: 0,
}
}
}
impl Dashboard {
pub fn has_tabs(&self) -> bool {
!self.tabs.is_empty()
}
pub fn active_plots(&self) -> &[Plot] {
if self.has_tabs() {
self.tabs
.get(self.active_tab)
.map(|t| t.plots.as_slice())
.unwrap_or(&[])
} else {
&self.plots
}
}
pub fn active_columns(&self) -> Option<usize> {
if self.has_tabs() {
self.tabs
.get(self.active_tab)
.and_then(|t| t.columns)
.or(self.columns)
} else {
self.columns
}
}
pub fn tab_names(&self) -> Vec<&str> {
self.tabs.iter().map(|t| t.name.as_str()).collect()
}
}