use std::{cmp::Ordering, ops::Range, sync::Arc, time::Duration};
use egui::Ui;
use instant::Instant;
use plotters::{
prelude::ChartBuilder,
series::LineSeries,
style::{
full_palette::{GREY, GREY_700, RED_900},
Color, FontDesc, RGBAColor, ShapeStyle, TextStyle, BLACK, WHITE,
},
};
use plotters_backend::{FontFamily, FontStyle};
use crate::{mult_range, Chart, MouseConfig};
const MIN_DELTA: f32 = 0.000_010;
const DEFAULT_RATIO: f32 = 1.0;
const X_MARGIN: i32 = 25;
const Y_MARGIN: i32 = 25;
const LABEL_AREA: i32 = 25;
const CAPTION_SIZE: i32 = 10;
#[derive(Clone)]
struct XyTimeConfig {
points: Arc<[(f32, f32)]>,
range: (Range<f32>, Range<f32>),
line_style: ShapeStyle,
grid_style: ShapeStyle,
subgrid_style: ShapeStyle,
axes_style: ShapeStyle,
text_color: RGBAColor,
background_color: RGBAColor,
x_unit: Arc<str>,
y_unit: Arc<str>,
ratio: f32,
caption: Arc<str>,
}
pub struct XyTimeData {
playback_start: Option<Instant>,
pause_start: Option<Instant>,
playback_speed: f32,
points: Arc<[(f32, f32)]>,
ranges: Arc<[(Range<f32>, Range<f32>)]>,
times: Arc<[f32]>,
chart: Chart<XyTimeConfig>,
}
impl XyTimeData {
pub fn new(points: &[(f32, f32, f32)], x_unit: &str, y_unit: &str, caption: &str) -> Self {
let mut points = points.to_vec();
points.sort_by(|a, b| {
let (_, _, a) = a;
let (_, _, b) = b;
a.partial_cmp(b).unwrap_or(Ordering::Equal)
});
let times: Vec<f32> = points
.iter()
.map(|point| {
let (_, _, time) = point;
*time
})
.collect();
let points: Vec<(f32, f32)> = points
.iter()
.map(|point| {
let (x, y, _) = point;
(*x, *y)
})
.collect();
let mut ranges = Vec::<(Range<f32>, Range<f32>)>::with_capacity(points.len());
let mut min_x: f32 = f32::MAX;
let mut min_y: f32 = f32::MAX;
let mut max_x: f32 = f32::MIN;
let mut max_y: f32 = f32::MIN;
for point in &points {
let (x, y) = *point;
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
let range_x = min_x..max_x;
let range_y = min_y..max_y;
ranges.push((range_x, range_y));
}
let points: Arc<[(f32, f32)]> = points.into();
let ranges: Arc<[(Range<f32>, Range<f32>)]> = ranges.into();
let times: Arc<[f32]> = times.into();
let x_unit: Arc<str> = x_unit.into();
let y_unit: Arc<str> = y_unit.into();
let caption: Arc<str> = caption.into();
let grid_style = ShapeStyle {
color: GREY.to_rgba(),
filled: false,
stroke_width: 2,
};
let subgrid_style = ShapeStyle {
color: GREY_700.to_rgba(),
filled: false,
stroke_width: 1,
};
let axes_style = ShapeStyle {
color: BLACK.to_rgba(),
filled: false,
stroke_width: 2,
};
let line_style = ShapeStyle {
color: RED_900.to_rgba(),
filled: false,
stroke_width: 2,
};
let background_color = WHITE.to_rgba();
let text_color = BLACK.to_rgba();
let config = XyTimeConfig {
points: points.clone(),
range: ranges.last().unwrap().clone(),
line_style,
grid_style,
subgrid_style,
axes_style,
text_color,
background_color,
x_unit,
y_unit,
ratio: DEFAULT_RATIO,
caption,
};
let chart = Chart::new(config)
.mouse(MouseConfig::enabled())
.builder_cb(Box::new(|area, _t, data| {
let area_ratio = {
let (x_range, y_range) = area.get_pixel_range();
let x_delta =
((x_range.end - x_range.start).abs() - (X_MARGIN * 2) - LABEL_AREA) as f32;
let y_delta = ((y_range.end - y_range.start).abs()
- (Y_MARGIN * 2)
- LABEL_AREA
- CAPTION_SIZE) as f32;
x_delta / y_delta
};
if !area_ratio.is_finite() {
return;
}
let (x_range, y_range) = data.range.clone();
let data_ratio = {
let x_delta = (x_range.end - x_range.start).abs();
let y_delta = (y_range.end - y_range.start).abs();
y_delta / x_delta
};
let display_ratio = data.ratio * data_ratio * area_ratio;
let (x_range, y_range) =
match display_ratio.partial_cmp(&1.0).unwrap_or(Ordering::Equal) {
Ordering::Equal => (x_range, y_range),
Ordering::Greater => (mult_range(x_range, display_ratio), y_range),
Ordering::Less => (x_range, mult_range(y_range, 1.0 / display_ratio)),
};
let font_style = FontStyle::Normal;
let font_family = FontFamily::Monospace;
let font_size = CAPTION_SIZE;
let font_desc = FontDesc::new(font_family, font_size as f64, font_style);
let text_style = TextStyle::from(font_desc).color(&data.text_color);
let mut chart = ChartBuilder::on(area)
.caption(data.caption.clone(), text_style.clone())
.x_label_area_size(LABEL_AREA)
.y_label_area_size(LABEL_AREA)
.margin_left(X_MARGIN)
.margin_right(X_MARGIN)
.margin_top(Y_MARGIN)
.margin_bottom(Y_MARGIN)
.build_cartesian_2d(x_range, y_range)
.unwrap();
chart
.configure_mesh()
.label_style(text_style.clone())
.bold_line_style(data.grid_style)
.light_line_style(data.subgrid_style)
.axis_style(data.axes_style)
.x_desc(&data.x_unit.to_string())
.set_all_tick_mark_size(4)
.y_desc(&data.y_unit.to_string())
.draw()
.unwrap();
chart
.draw_series(LineSeries::new(data.points.to_vec(), data.line_style))
.unwrap();
}));
Self {
playback_start: None,
pause_start: None,
playback_speed: 1.0,
points,
ranges,
times,
chart,
}
}
pub fn set_time(&mut self, time: f32) {
let start_time = Some(Instant::now() - Duration::from_secs_f32(time));
match self.playback_start {
Some(_) => {
if let Some(_) = self.pause_start {
self.pause_start = Some(Instant::now());
}
self.playback_start = start_time;
}
None => {
self.playback_start = start_time;
self.pause_start = Some(Instant::now());
}
}
}
#[inline]
pub fn time(mut self, time: f32) -> Self {
self.set_time(time);
self
}
#[inline]
pub fn set_playback_speed(&mut self, speed: f32) {
self.playback_speed = speed;
}
#[inline]
pub fn playback_speed(mut self, speed: f32) -> Self {
self.set_playback_speed(speed);
self
}
pub fn set_line_style(&mut self, line_style: ShapeStyle) {
self.chart.get_data_mut().line_style = line_style;
}
#[inline]
pub fn line_style(mut self, line_style: ShapeStyle) -> Self {
self.set_line_style(line_style);
self
}
pub fn set_grid_style(&mut self, grid_style: ShapeStyle) {
self.chart.get_data_mut().grid_style = grid_style
}
#[inline]
pub fn grid_style(mut self, grid_style: ShapeStyle) -> Self {
self.set_grid_style(grid_style);
self
}
pub fn set_subgrid_style(&mut self, subgrid_style: ShapeStyle) {
self.chart.get_data_mut().subgrid_style = subgrid_style
}
#[inline]
pub fn subgrid_style(mut self, subgrid_style: ShapeStyle) -> Self {
self.set_subgrid_style(subgrid_style);
self
}
pub fn set_axes_style(&mut self, axes_style: ShapeStyle) {
self.chart.get_data_mut().axes_style = axes_style
}
#[inline]
pub fn axes_style(mut self, axes_style: ShapeStyle) -> Self {
self.set_axes_style(axes_style);
self
}
pub fn set_text_color<T>(&mut self, color: T)
where
T: Into<RGBAColor>,
{
let color: RGBAColor = color.into();
self.chart.get_data_mut().text_color = color
}
#[inline]
pub fn text_color<T>(mut self, color: T) -> Self
where
T: Into<RGBAColor>,
{
self.set_text_color(color);
self
}
pub fn set_background_color<T>(&mut self, color: T)
where
T: Into<RGBAColor>,
{
let color: RGBAColor = color.into();
self.chart.get_data_mut().background_color = color
}
#[inline]
pub fn background_color<T>(mut self, color: T) -> Self
where
T: Into<RGBAColor>,
{
self.set_background_color(color);
self
}
#[inline]
pub fn set_ratio(&mut self, ratio: f32) {
self.chart.get_data_mut().ratio = ratio
}
#[inline]
pub fn ratio(mut self, ratio: f32) -> Self {
self.set_ratio(ratio);
self
}
pub fn draw(&mut self, ui: &Ui) {
if let Some(_) = self.playback_start {
let time = self.current_time();
let time_index = match self
.times
.binary_search_by(|probe| probe.partial_cmp(&time).unwrap_or(Ordering::Equal))
{
Ok(index) => index,
Err(index) => self.points.len().min(index),
};
let points = &self.points[..=time_index];
let range = self.ranges[time_index].clone();
let config = self.chart.get_data_mut();
config.points = points.into();
config.range = range;
}
self.chart.draw(ui);
}
#[inline]
pub fn start_playback(&mut self) {
self.playback_start = Some(Instant::now());
self.pause_start = None;
}
#[inline]
pub fn stop_playback(&mut self) {
self.playback_start = None;
self.pause_start = None;
}
pub fn toggle_playback(&mut self) {
match self.playback_start {
Some(playback_start) => match self.pause_start {
Some(pause_start) => {
let delta = Instant::now().duration_since(pause_start);
self.pause_start = None;
self.playback_start = Some(playback_start + delta);
}
None => self.pause_start = Some(Instant::now()),
},
None => {
self.start_playback();
}
}
}
#[inline]
pub fn is_playing(&self) -> bool {
self.playback_start != None && self.pause_start == None
}
#[inline]
pub fn start_time(&self) -> f32 {
let time_start = *self.times.first().unwrap();
time_start
}
pub fn current_time(&mut self) -> f32 {
if let Some(playback_start) = self.playback_start {
let now = Instant::now();
let time_start = self.start_time();
let time_end = self.end_time();
let base_delta = time_end - time_start;
let current_delta = MIN_DELTA
+ self.playback_speed
* match self.pause_start {
Some(pause_start) => {
pause_start.duration_since(playback_start).as_secs_f32()
}
None => now.duration_since(playback_start).as_secs_f32(),
};
match base_delta > current_delta {
true => current_delta + time_start,
false => {
self.playback_start = None;
time_end
}
}
} else {
self.start_time()
}
}
#[inline]
pub fn end_time(&self) -> f32 {
let time_end = *self.times.last().unwrap();
time_end
}
#[inline]
pub fn get_playback_speed(&self) -> f32 {
self.playback_speed
}
}