use alloc::vec;
use alloc::vec::Vec;
use rlvgl_core::draw::draw_widget_bg;
use rlvgl_core::event::Event;
use rlvgl_core::renderer::Renderer;
use rlvgl_core::style::Style;
use rlvgl_core::widget::{Color, Rect, Widget};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChartType {
Line,
Bar,
Scatter,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChartAxis {
Primary,
Secondary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SeriesId(pub u8);
pub const SERIES_NONE: SeriesId = SeriesId(u8::MAX);
struct SeriesDescriptor {
color: Color,
#[allow(dead_code)]
axis: ChartAxis,
points: Vec<i32>,
}
pub struct Chart {
bounds: Rect,
chart_type: ChartType,
y_min: i32,
y_max: i32,
point_count: usize,
h_div: u8,
v_div: u8,
series: Vec<SeriesDescriptor>,
cursor_index: Option<usize>,
pub style: Style,
pub grid_color: Color,
}
impl Chart {
pub fn new(bounds: Rect) -> Self {
Self {
bounds,
chart_type: ChartType::Line,
y_min: 0,
y_max: 100,
point_count: 10,
h_div: 5,
v_div: 5,
series: Vec::new(),
cursor_index: None,
style: Style::default(),
grid_color: Color(180, 180, 180, 180),
}
}
pub fn set_type(&mut self, t: ChartType) {
self.chart_type = t;
}
pub fn chart_type(&self) -> ChartType {
self.chart_type
}
pub fn add_series(&mut self, color: Color, axis: ChartAxis) -> SeriesId {
if self.series.len() >= u8::MAX as usize {
return SERIES_NONE;
}
let id = SeriesId(self.series.len() as u8);
self.series.push(SeriesDescriptor {
color,
axis,
points: vec![0; self.point_count],
});
id
}
pub fn set_point_count(&mut self, count: usize) {
self.point_count = count;
for s in &mut self.series {
s.points.resize(count, 0);
}
if let Some(idx) = self.cursor_index {
if count == 0 {
self.cursor_index = None;
} else if idx >= count {
self.cursor_index = Some(count - 1);
}
}
}
pub fn point_count(&self) -> usize {
self.point_count
}
pub fn set_point(&mut self, series: SeriesId, idx: usize, value: i32) {
if let Some(s) = self.series.get_mut(series.0 as usize)
&& let Some(slot) = s.points.get_mut(idx)
{
*slot = value;
}
}
pub fn get_point(&self, series: SeriesId, idx: usize) -> Option<i32> {
self.series
.get(series.0 as usize)
.and_then(|s| s.points.get(idx).copied())
}
pub fn set_points(&mut self, series: SeriesId, values: &[i32]) {
if let Some(s) = self.series.get_mut(series.0 as usize) {
let n = values.len().min(self.point_count);
s.points[..n].copy_from_slice(&values[..n]);
}
}
pub fn set_series_color(&mut self, series: SeriesId, color: Color) {
if let Some(s) = self.series.get_mut(series.0 as usize) {
s.color = color;
}
}
pub fn set_axis_range(&mut self, _axis: ChartAxis, min: i32, max: i32) {
self.y_min = min;
self.y_max = max;
}
pub fn set_div_line_count(&mut self, h_div: u8, v_div: u8) {
self.h_div = h_div;
self.v_div = v_div;
}
pub fn h_div_line_count(&self) -> u8 {
self.h_div
}
pub fn v_div_line_count(&self) -> u8 {
self.v_div
}
pub fn cursor_index(&self) -> Option<usize> {
self.cursor_index
}
pub fn activate_cursor(&mut self) {
if self.point_count > 0 {
self.cursor_index = Some(0);
}
}
pub fn clear_cursor(&mut self) {
self.cursor_index = None;
}
pub fn navigate_cursor_left(&mut self) {
if self.point_count == 0 {
return;
}
let idx = self.cursor_index.unwrap_or(0);
self.cursor_index = Some(if idx == 0 {
self.point_count - 1
} else {
idx - 1
});
}
pub fn navigate_cursor_right(&mut self) {
if self.point_count == 0 {
return;
}
let idx = self.cursor_index.unwrap_or(self.point_count - 1);
self.cursor_index = Some((idx + 1) % self.point_count);
}
fn value_to_y(&self, value: i32) -> i32 {
let range = self.y_max - self.y_min;
if range == 0 {
return self.bounds.y + self.bounds.height / 2;
}
let clamped = value.clamp(self.y_min, self.y_max);
let frac_num = (clamped - self.y_min) as i64;
let frac_den = range as i64;
let h = self.bounds.height as i64;
let offset = (frac_num * h / frac_den) as i32;
self.bounds.y + self.bounds.height - offset
}
fn point_to_x(&self, idx: usize) -> i32 {
if self.point_count == 0 {
return self.bounds.x;
}
let slot_w = self.bounds.width / self.point_count as i32;
let slot_w = slot_w.max(1);
self.bounds.x + idx as i32 * slot_w + slot_w / 2
}
fn draw_grid(&self, renderer: &mut dyn Renderer) {
let color = self.grid_color.with_alpha(self.style.alpha);
if self.h_div > 0 {
let step = self.bounds.height / (self.h_div as i32 + 1);
for i in 1..=self.h_div as i32 {
let y = self.bounds.y + i * step;
renderer.fill_rect(
Rect {
x: self.bounds.x,
y,
width: self.bounds.width,
height: 1,
},
color,
);
}
}
if self.v_div > 0 {
let step = self.bounds.width / (self.v_div as i32 + 1);
for i in 1..=self.v_div as i32 {
let x = self.bounds.x + i * step;
renderer.fill_rect(
Rect {
x,
y: self.bounds.y,
width: 1,
height: self.bounds.height,
},
color,
);
}
}
}
fn draw_series(&self, renderer: &mut dyn Renderer) {
for s in &self.series {
let color = s.color.with_alpha(self.style.alpha);
match self.chart_type {
ChartType::Line => self.draw_line_series(renderer, s, color),
ChartType::Bar => self.draw_bar_series(renderer, s, color),
ChartType::Scatter => self.draw_scatter_series(renderer, s, color),
}
}
}
fn draw_line_series(&self, renderer: &mut dyn Renderer, s: &SeriesDescriptor, color: Color) {
if s.points.len() < 2 {
return;
}
for i in 0..s.points.len() - 1 {
let x0 = self.point_to_x(i);
let y0 = self.value_to_y(s.points[i]);
let x1 = self.point_to_x(i + 1);
let y1 = self.value_to_y(s.points[i + 1]);
renderer.stroke_line_aa(
rlvgl_core::raster::PointF {
x: x0 as f32,
y: y0 as f32,
},
rlvgl_core::raster::PointF {
x: x1 as f32,
y: y1 as f32,
},
1.0,
color,
);
}
}
fn draw_bar_series(&self, renderer: &mut dyn Renderer, s: &SeriesDescriptor, color: Color) {
if self.point_count == 0 {
return;
}
let slot_w = (self.bounds.width / self.point_count as i32).max(1);
let bar_w = (slot_w * 3 / 5).max(1);
let baseline_y = self.value_to_y(self.y_min.max(0).min(self.y_max));
for (i, &v) in s.points.iter().enumerate() {
let x = self.bounds.x + i as i32 * slot_w + (slot_w - bar_w) / 2;
let y = self.value_to_y(v);
let (top, height) = if y <= baseline_y {
(y, baseline_y - y)
} else {
(baseline_y, y - baseline_y)
};
if height > 0 {
renderer.fill_rect(
Rect {
x,
y: top,
width: bar_w,
height,
},
color,
);
}
}
}
fn draw_scatter_series(&self, renderer: &mut dyn Renderer, s: &SeriesDescriptor, color: Color) {
for (i, &v) in s.points.iter().enumerate() {
let cx = self.point_to_x(i);
let cy = self.value_to_y(v);
renderer.fill_rect(
Rect {
x: cx - 2,
y: cy - 2,
width: 5,
height: 5,
},
color,
);
}
}
fn draw_cursor(&self, renderer: &mut dyn Renderer) {
let Some(idx) = self.cursor_index else {
return;
};
let cx = self.point_to_x(idx);
let cursor_color = Color(255, 80, 80, 200).with_alpha(self.style.alpha);
renderer.fill_rect(
Rect {
x: cx,
y: self.bounds.y,
width: 1,
height: self.bounds.height,
},
cursor_color,
);
}
}
impl Widget for Chart {
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, bounds: Rect) {
self.bounds = bounds;
}
fn draw(&self, renderer: &mut dyn Renderer) {
draw_widget_bg(renderer, self.bounds, &self.style);
self.draw_grid(renderer);
self.draw_series(renderer);
self.draw_cursor(renderer);
}
fn handle_event(&mut self, _event: &Event) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
fn r(x: i32, y: i32, w: i32, h: i32) -> Rect {
Rect {
x,
y,
width: w,
height: h,
}
}
#[test]
fn add_series_returns_sequential_ids() {
let mut chart = Chart::new(r(0, 0, 100, 100));
let a = chart.add_series(Color(255, 0, 0, 255), ChartAxis::Primary);
let b = chart.add_series(Color(0, 255, 0, 255), ChartAxis::Primary);
assert_eq!(a, SeriesId(0));
assert_eq!(b, SeriesId(1));
}
#[test]
fn set_and_get_point() {
let mut chart = Chart::new(r(0, 0, 200, 100));
let s = chart.add_series(Color(0, 0, 255, 255), ChartAxis::Primary);
chart.set_point(s, 3, 42);
assert_eq!(chart.get_point(s, 3), Some(42));
assert_eq!(chart.get_point(s, 9), Some(0));
}
#[test]
fn set_points_bulk() {
let mut chart = Chart::new(r(0, 0, 200, 100));
let s = chart.add_series(Color(0, 0, 0, 255), ChartAxis::Primary);
chart.set_points(s, &[1, 2, 3, 4, 5]);
assert_eq!(chart.get_point(s, 0), Some(1));
assert_eq!(chart.get_point(s, 4), Some(5));
}
#[test]
fn value_to_y_mapping() {
let chart = Chart::new(r(0, 0, 100, 100));
assert_eq!(chart.value_to_y(0), 100);
assert_eq!(chart.value_to_y(100), 0);
assert_eq!(chart.value_to_y(50), 50);
}
#[test]
fn chart_type_dispatch() {
let mut chart = Chart::new(r(0, 0, 100, 100));
chart.set_type(ChartType::Bar);
assert_eq!(chart.chart_type(), ChartType::Bar);
chart.set_type(ChartType::Scatter);
assert_eq!(chart.chart_type(), ChartType::Scatter);
}
#[test]
fn cursor_navigation_wraps() {
let mut chart = Chart::new(r(0, 0, 100, 100));
chart.set_point_count(5);
chart.activate_cursor();
assert_eq!(chart.cursor_index(), Some(0));
chart.navigate_cursor_left();
assert_eq!(chart.cursor_index(), Some(4)); chart.navigate_cursor_right();
assert_eq!(chart.cursor_index(), Some(0)); }
#[test]
fn set_point_count_clamps_cursor() {
let mut chart = Chart::new(r(0, 0, 100, 100));
chart.activate_cursor();
chart.navigate_cursor_right();
chart.navigate_cursor_right();
chart.set_point_count(2);
assert_eq!(chart.cursor_index(), Some(1));
}
#[test]
fn resize_via_set_bounds() {
let mut chart = Chart::new(r(0, 0, 100, 100));
chart.set_bounds(r(10, 20, 200, 150));
assert_eq!(chart.bounds(), r(10, 20, 200, 150));
}
#[test]
fn div_line_counts_stored() {
let mut chart = Chart::new(r(0, 0, 100, 100));
chart.set_div_line_count(3, 7);
assert_eq!(chart.h_div_line_count(), 3);
assert_eq!(chart.v_div_line_count(), 7);
}
}