use crate::widget::chart_widgets::types::*;
use crate::core::{Color, Point, Rect};
use alloc::borrow::Cow;
pub struct LineChart {
title: String,
x_axis_label: String,
y_axis_label: String,
series: Vec<ChartSeries>,
x_tick_count: usize,
y_tick_count: usize,
show_grid: bool,
zoom: Option<(f64, f64, f64, f64)>,
scroll: (f64, f64),
axes: Vec<(Axis, AxisEdge)>,
}
pub struct CartesianLayout {
plot_x: f32,
plot_y: f32,
plot_w: f32,
plot_h: f32,
legend_x: f32,
legend_y: f32,
tick_label_height: f32,
}
impl CartesianLayout {
pub fn plot_x(&self) -> f32 {
self.plot_x
}
pub fn plot_y(&self) -> f32 {
self.plot_y
}
pub fn plot_w(&self) -> f32 {
self.plot_w
}
pub fn plot_h(&self) -> f32 {
self.plot_h
}
pub fn legend_x(&self) -> f32 {
self.legend_x
}
pub fn legend_y(&self) -> f32 {
self.legend_y
}
pub fn tick_label_height(&self) -> f32 {
self.tick_label_height
}
}
pub(crate) fn axis_chrome() -> (Color, Color, Color) {
axis_chrome_palette()
}
fn axis_chrome_palette() -> (Color, Color, Color) {
let (surface, ink) = crate::style::palette_shared()
.map(|colors| (colors.background, colors.foreground))
.unwrap_or((Color { r: 255, g: 255, b: 255, a: 255 }, Color { r: 0, g: 0, b: 0, a: 255 }));
(surface.blend(&ink, 0.45), surface.blend(&ink, 0.70), surface.blend(&ink, 0.16))
}
fn apply_view(
auto: (f64, f64, f64, f64),
zoom: Option<(f64, f64, f64, f64)>,
scroll: (f64, f64),
) -> (f64, f64, f64, f64) {
let Some((x, y, width, height)) = zoom else {
return auto;
};
if width <= 0.0 || height <= 0.0 || !x.is_finite() || !y.is_finite() {
return auto;
}
(x + scroll.0, x + width + scroll.0, y + scroll.1, y + height + scroll.1)
}
fn draw_attached_axes(
context: &mut dyn ChartContext,
layout: &CartesianLayout,
axes: &[(Axis, AxisEdge)],
x_range: (f64, f64),
y_range: (f64, f64),
) {
for (axis, edge) in axes {
let vertical = matches!(edge, AxisEdge::Left | AxisEdge::Right);
let (low, high) = if vertical {
(axis.minimum.unwrap_or(y_range.0), axis.maximum.unwrap_or(y_range.1))
} else {
(axis.minimum.unwrap_or(x_range.0), axis.maximum.unwrap_or(x_range.1))
};
let span = high - low;
if !span.is_finite() || span.abs() <= f64::EPSILON {
continue;
}
let color = axis_chrome_color(0.55);
for step in 0..=4 {
let value = low + span * f64::from(step) / 4.0;
let mut label = String::new();
let _ = core::fmt::Write::write_fmt(&mut label, format_args!("{value:.1}"));
match edge {
AxisEdge::Left => {
let y = layout.plot_y + layout.plot_h * (1.0 - step as f32 / 4.0);
context.draw_text(
&label,
Point::from_f32(layout.plot_x - 52.0, y + 4.0),
9.0,
color,
);
}
AxisEdge::Right => {
let y = layout.plot_y + layout.plot_h * (1.0 - step as f32 / 4.0);
context.draw_text(
&label,
Point::from_f32(layout.plot_x + layout.plot_w + 6.0, y + 4.0),
9.0,
color,
);
}
AxisEdge::Top | AxisEdge::Bottom => {
let x = layout.plot_x + layout.plot_w * (step as f32 / 4.0);
let y = if matches!(edge, AxisEdge::Top) {
layout.plot_y - 14.0
} else {
layout.plot_y + layout.plot_h + 18.0
};
context.draw_text(&label, Point::from_f32(x - 10.0, y), 9.0, color);
}
}
}
if !axis.label.is_empty() {
let (x, y) = match edge {
AxisEdge::Left => (layout.plot_x - 54.0, layout.plot_y - 14.0),
AxisEdge::Right => (layout.plot_x + layout.plot_w + 6.0, layout.plot_y - 14.0),
AxisEdge::Top => (layout.plot_x, layout.plot_y - 30.0),
AxisEdge::Bottom => (layout.plot_x, layout.plot_y + layout.plot_h + 34.0),
};
context.draw_text(&axis.label, Point::from_f32(x, y), 10.0, axis_chrome_color(0.45));
}
}
}
pub(crate) fn axis_chrome_color(strength: f32) -> Color {
let (surface, ink) = crate::style::palette_shared()
.map(|colors| (colors.background, colors.foreground))
.unwrap_or((Color { r: 255, g: 255, b: 255, a: 255 }, Color { r: 0, g: 0, b: 0, a: 255 }));
surface.blend(&ink, strength)
}
pub fn compute_cartesian_layout(
rect: Rect,
has_x_label: bool,
has_y_label: bool,
legend_items: usize,
) -> CartesianLayout {
compute_cartesian_layout_measured(rect, has_x_label, has_y_label, legend_items, 0.0)
}
pub fn compute_cartesian_layout_measured(
rect: Rect,
has_x_label: bool,
has_y_label: bool,
legend_items: usize,
widest_y_label: f32,
) -> CartesianLayout {
let measured_left = if widest_y_label > 0.0 { (widest_y_label + 16.0).max(0.0) } else { 0.0 };
let left_margin = if has_y_label { measured_left.max(64.0) } else { 48.0 };
let top_margin = 32.0;
let bottom_margin = if has_x_label { 52.0 } else { 36.0 };
let right_margin = if legend_items > 0 { 170.0 } else { 24.0 };
let plot_x = rect.x as f32 + left_margin;
let plot_y = rect.y as f32 + top_margin;
let plot_w = (rect.width as f32 - left_margin - right_margin).max(1.0);
let plot_h = (rect.height as f32 - top_margin - bottom_margin).max(1.0);
let tick_label_height = 12.0;
CartesianLayout {
plot_x,
plot_y,
plot_w,
plot_h,
legend_x: plot_x + plot_w + 16.0,
legend_y: plot_y + 8.0,
tick_label_height,
}
}
pub fn draw_cartesian_axes(context: &mut dyn ChartContext, layout: &CartesianLayout) {
let (axis_color, _label_color, _grid_color) = axis_chrome();
context.draw_line(
Point::from_f32(layout.plot_x, layout.plot_y + layout.plot_h),
Point::from_f32(layout.plot_x + layout.plot_w, layout.plot_y + layout.plot_h),
1.0,
axis_color,
);
context.draw_line(
Point::from_f32(layout.plot_x, layout.plot_y),
Point::from_f32(layout.plot_x, layout.plot_y + layout.plot_h),
1.0,
axis_color,
);
}
pub fn draw_y_ticks(
context: &mut dyn ChartContext,
layout: &CartesianLayout,
min_y: f64,
max_y: f64,
tick_count: usize,
draw_grid: bool,
) {
let tick_count = tick_count.max(2);
let (axis_color, label_color, grid_color) = axis_chrome();
let mut label = crate::compat::String::new();
let tick_spacing =
if tick_count == 0 { layout.plot_h } else { layout.plot_h / tick_count as f32 };
let label_line = layout.tick_label_height().max(1.0);
let label_stride = if tick_spacing >= label_line {
1
} else {
(label_line / tick_spacing.max(1.0)).ceil().max(1.0) as usize
};
for tick in 0..=tick_count {
let t = tick as f32 / tick_count as f32;
let y = layout.plot_y + layout.plot_h - t * layout.plot_h;
if draw_grid {
context.draw_line(
Point::from_f32(layout.plot_x, y),
Point::from_f32(layout.plot_x + layout.plot_w, y),
1.0,
grid_color,
);
}
context.draw_line(
Point::from_f32(layout.plot_x - 4.0, y),
Point::from_f32(layout.plot_x, y),
1.0,
axis_color,
);
let value = min_y + (max_y - min_y) * t as f64;
label.clear();
let _ = core::fmt::Write::write_fmt(&mut label, format_args!("{value:.1}"));
if tick % label_stride != 0 {
continue;
}
context.draw_text(
&label,
Point::from_f32(layout.plot_x - 44.0, y + 4.0),
10.0,
label_color,
);
}
}
pub fn draw_x_ticks(
context: &mut dyn ChartContext,
layout: &CartesianLayout,
min_x: f64,
max_x: f64,
tick_count: usize,
draw_grid: bool,
) {
let tick_count = tick_count.max(2);
let (axis_color, label_color, grid_color) = axis_chrome();
let mut label = crate::compat::String::new();
for tick in 0..=tick_count {
let t = tick as f32 / tick_count as f32;
let x = layout.plot_x + t * layout.plot_w;
if draw_grid {
context.draw_line(
Point::from_f32(x, layout.plot_y),
Point::from_f32(x, layout.plot_y + layout.plot_h),
1.0,
grid_color,
);
}
context.draw_line(
Point::from_f32(x, layout.plot_y + layout.plot_h),
Point::from_f32(x, layout.plot_y + layout.plot_h + 4.0),
1.0,
axis_color,
);
let value = min_x + (max_x - min_x) * t as f64;
label.clear();
let _ = core::fmt::Write::write_fmt(&mut label, format_args!("{value:.1}"));
let label_width = (label.chars().count() as f32 * 10.0 * 0.6).max(10.0);
let min_x = layout.plot_x;
let max_x = (layout.plot_x + layout.plot_w - label_width).max(min_x);
let label_x = (x - label_width / 2.0).clamp(min_x, max_x);
let label_y = (layout.plot_y + layout.plot_h + 16.0)
.min(layout.plot_y + layout.plot_h + layout.tick_label_height());
context.draw_text(&label, Point::from_f32(label_x, label_y), 10.0, label_color);
}
}
pub fn draw_legend(
context: &mut dyn ChartContext,
layout: &CartesianLayout,
series: &[&ChartSeries],
) {
draw_legend_rows(context, layout, series.iter().copied(), series.len());
}
pub fn draw_legend_entries(
context: &mut dyn ChartContext,
layout: &CartesianLayout,
entries: &[(&str, Color)],
) {
draw_legend_entries_iter(context, layout, entries.iter().copied());
}
pub fn draw_legend_entries_iter<'a, I>(
context: &mut dyn ChartContext,
layout: &CartesianLayout,
entries: I,
) where
I: IntoIterator<Item = (&'a str, Color)>,
{
let max_rows = ((layout.plot_h / 18.0).floor() as usize).max(1);
let max_items = max_rows.saturating_sub(1).max(1);
let mut cursor_y = layout.legend_y;
let mut iter = entries.into_iter();
for (name, color) in iter.by_ref().take(max_items) {
context.draw_line(
Point::from_f32(layout.legend_x, cursor_y),
Point::from_f32(layout.legend_x + 20.0, cursor_y),
3.0,
color,
);
context.draw_text(
truncate_legend_label(name, 20).as_ref(),
Point::from_f32(layout.legend_x + 26.0, cursor_y + 4.0),
11.0,
axis_chrome_color(0.45),
);
cursor_y += 18.0;
}
let hidden = iter.count();
if hidden > 0 {
use core::fmt::Write as _;
let mut summary = String::new();
let _ = write!(summary, "+{hidden} more");
context.draw_text(
&summary,
Point::from_f32(layout.legend_x + 26.0, cursor_y + 4.0),
10.0,
axis_chrome_color(0.45),
);
}
}
fn draw_legend_rows<'a>(
context: &mut dyn ChartContext,
layout: &CartesianLayout,
series: impl Iterator<Item = &'a ChartSeries>,
visible_count: usize,
) {
if visible_count == 0 {
return;
}
let max_rows = ((layout.plot_h / 18.0).floor() as usize).max(1);
let max_items = max_rows.saturating_sub(1).max(1);
let max_label_chars = 20usize;
let mut cursor_y = layout.legend_y;
for item in series.take(max_items) {
context.draw_line(
Point::from_f32(layout.legend_x, cursor_y),
Point::from_f32(layout.legend_x + 20.0, cursor_y),
3.0,
item.color,
);
context.draw_text(
truncate_legend_label(&item.name, max_label_chars).as_ref(),
Point::from_f32(layout.legend_x + 26.0, cursor_y + 4.0),
11.0,
axis_chrome_color(0.45),
);
cursor_y += 18.0;
}
let hidden = visible_count.saturating_sub(max_items);
if hidden > 0 {
use core::fmt::Write as _;
let mut summary = String::new();
let _ = write!(summary, "+{hidden} more");
context.draw_text(
&summary,
Point::from_f32(layout.legend_x + 26.0, cursor_y + 4.0),
10.0,
Color { r: 90, g: 90, b: 90, a: 255 },
);
}
}
pub fn truncate_legend_label(label: &str, max_chars: usize) -> Cow<'_, str> {
crate::widget::text_utils::elide_end(label, max_chars)
}
pub fn sector_polygon(
center: Point,
outer_radius: f32,
inner_radius: f32,
start_angle: f32,
end_angle: f32,
steps: u32,
) -> Vec<Point> {
let sweep = end_angle - start_angle;
if sweep.abs() < f32::EPSILON || outer_radius <= 0.0 {
return Vec::new();
}
let steps = steps.clamp(3, 180);
let cx = center.x as f32;
let cy = center.y as f32;
let point_at = |radius: f32, angle: f32| -> Point {
Point {
x: (cx + radius * angle.cos()).round() as i32,
y: (cy + radius * angle.sin()).round() as i32,
}
};
let mut vertices = Vec::with_capacity((steps as usize + 1) * 2);
for step in 0..=steps {
let angle = start_angle + sweep * (step as f32 / steps as f32);
vertices.push(point_at(outer_radius, angle));
}
if inner_radius > 0.0 {
for step in (0..=steps).rev() {
let angle = start_angle + sweep * (step as f32 / steps as f32);
vertices.push(point_at(inner_radius, angle));
}
} else {
vertices.push(Point { x: cx.round() as i32, y: cy.round() as i32 });
}
vertices
}
impl LineChart {
pub fn new() -> Self {
Self {
title: String::new(),
x_axis_label: String::new(),
y_axis_label: String::new(),
series: Vec::new(),
x_tick_count: 5,
y_tick_count: 5,
show_grid: false,
zoom: None,
scroll: (0.0, 0.0),
axes: Vec::new(),
}
}
pub fn set_x_tick_count(&mut self, tick_count: usize) {
self.x_tick_count = tick_count.clamp(2, 16);
}
pub fn set_y_tick_count(&mut self, tick_count: usize) {
self.y_tick_count = tick_count.clamp(2, 16);
}
pub fn set_grid_enabled(&mut self, enabled: bool) {
self.show_grid = enabled;
}
}
crate::impl_default_via_new!(LineChart);
impl Chart for LineChart {
fn add_series(&mut self, series: ChartSeries) {
self.series.push(series);
}
fn remove_series(&mut self, name: &str) {
self.series.retain(|s| s.name != name);
}
fn clear_series(&mut self) {
self.series.clear();
}
fn set_title(&mut self, title: String) {
self.title = title;
}
fn set_x_axis_label(&mut self, label: String) {
self.x_axis_label = label;
}
fn set_y_axis_label(&mut self, label: String) {
self.y_axis_label = label;
}
fn zoom_in(&mut self, rect: Rect) -> bool {
if rect.width == 0 || rect.height == 0 {
return false;
}
self.zoom = Some((
f64::from(rect.x),
f64::from(rect.y),
f64::from(rect.width),
f64::from(rect.height),
));
self.scroll = (0.0, 0.0);
true
}
fn zoom_reset(&mut self) {
self.zoom = None;
self.scroll = (0.0, 0.0);
}
fn scroll(&mut self, dx: f64, dy: f64) {
if dx.is_finite() && dy.is_finite() {
self.scroll.0 += dx;
self.scroll.1 += dy;
}
}
fn attach_axis(&mut self, axis: Axis, edge: AxisEdge) {
self.axes.retain(|(_, existing)| *existing != edge);
self.axes.push((axis, edge));
}
fn draw(&self, rect: Rect, context: &mut dyn ChartContext) {
context.draw_rect(rect, axis_chrome_color(0.0));
context.draw_text(
&self.title,
Point::new(rect.x + 8, rect.y + 16),
14.0,
axis_chrome_color(0.70),
);
let visible_count = self.series.iter().filter(|series| series.visible).count();
let layout = compute_cartesian_layout(
rect,
!self.x_axis_label.is_empty(),
!self.y_axis_label.is_empty(),
visible_count,
);
draw_cartesian_axes(context, &layout);
let mut min_x = f64::MAX;
let mut max_x = f64::MIN;
let mut min_y = f64::MAX;
let mut max_y = f64::MIN;
for series in self.series.iter().filter(|series| series.visible) {
for point in &series.data {
min_x = min_x.min(point.x);
max_x = max_x.max(point.x);
min_y = min_y.min(point.y);
max_y = max_y.max(point.y);
}
}
if min_x == f64::MAX || min_y == f64::MAX {
return;
}
let (min_x, max_x, min_y, max_y) =
apply_view((min_x, max_x, min_y, max_y), self.zoom, self.scroll);
let span_x = (max_x - min_x).max(f64::EPSILON);
let span_y = (max_y - min_y).max(f64::EPSILON);
draw_x_ticks(context, &layout, min_x, max_x, self.x_tick_count, self.show_grid);
draw_y_ticks(context, &layout, min_y, max_y, self.y_tick_count, self.show_grid);
if !self.x_axis_label.is_empty() {
context.draw_text(
&self.x_axis_label,
Point::from_f32(
layout.plot_x + layout.plot_w * 0.5 - 28.0,
layout.plot_y + layout.plot_h + 36.0,
),
11.0,
axis_chrome_color(0.45),
);
}
if !self.y_axis_label.is_empty() {
context.draw_text(
&self.y_axis_label,
Point::from_f32(layout.plot_x - 56.0, layout.plot_y - 10.0),
11.0,
axis_chrome_color(0.45),
);
}
for series in self.series.iter().filter(|series| series.visible) {
if series.data.len() < 2 {
continue;
}
for i in 1..series.data.len() {
let p1 = &series.data[i - 1];
let p2 = &series.data[i];
let x1 = layout.plot_x + (((p1.x - min_x) / span_x) as f32) * layout.plot_w;
let y1 = layout.plot_y + layout.plot_h
- (((p1.y - min_y) / span_y) as f32) * layout.plot_h;
let x2 = layout.plot_x + (((p2.x - min_x) / span_x) as f32) * layout.plot_w;
let y2 = layout.plot_y + layout.plot_h
- (((p2.y - min_y) / span_y) as f32) * layout.plot_h;
context.draw_line(
Point::from_f32(x1, y1),
Point::from_f32(x2, y2),
2.0,
series.color,
);
}
}
draw_legend_rows(
context,
&layout,
self.series.iter().filter(|series| series.visible),
visible_count,
);
draw_attached_axes(context, &layout, &self.axes, (min_x, max_x), (min_y, max_y));
}
}
pub struct BarChart {
title: String,
x_axis_label: String,
y_axis_label: String,
series: Vec<ChartSeries>,
x_tick_count: usize,
y_tick_count: usize,
show_grid: bool,
zoom: Option<(f64, f64, f64, f64)>,
scroll: (f64, f64),
axes: Vec<(Axis, AxisEdge)>,
}
impl BarChart {
pub fn new() -> Self {
Self {
title: String::new(),
x_axis_label: String::new(),
y_axis_label: String::new(),
series: Vec::new(),
x_tick_count: 5,
y_tick_count: 5,
show_grid: false,
zoom: None,
scroll: (0.0, 0.0),
axes: Vec::new(),
}
}
pub fn set_x_tick_count(&mut self, tick_count: usize) {
self.x_tick_count = tick_count.clamp(2, 16);
}
pub fn set_y_tick_count(&mut self, tick_count: usize) {
self.y_tick_count = tick_count.clamp(2, 16);
}
pub fn set_grid_enabled(&mut self, enabled: bool) {
self.show_grid = enabled;
}
}
crate::impl_default_via_new!(BarChart);
impl Chart for BarChart {
fn add_series(&mut self, series: ChartSeries) {
self.series.push(series);
}
fn remove_series(&mut self, name: &str) {
self.series.retain(|s| s.name != name);
}
fn clear_series(&mut self) {
self.series.clear();
}
fn set_title(&mut self, title: String) {
self.title = title;
}
fn set_x_axis_label(&mut self, label: String) {
self.x_axis_label = label;
}
fn set_y_axis_label(&mut self, label: String) {
self.y_axis_label = label;
}
fn zoom_in(&mut self, rect: Rect) -> bool {
if rect.width == 0 || rect.height == 0 {
return false;
}
self.zoom = Some((
f64::from(rect.x),
f64::from(rect.y),
f64::from(rect.width),
f64::from(rect.height),
));
self.scroll = (0.0, 0.0);
true
}
fn zoom_reset(&mut self) {
self.zoom = None;
self.scroll = (0.0, 0.0);
}
fn scroll(&mut self, dx: f64, dy: f64) {
if dx.is_finite() && dy.is_finite() {
self.scroll.0 += dx;
self.scroll.1 += dy;
}
}
fn attach_axis(&mut self, axis: Axis, edge: AxisEdge) {
self.axes.retain(|(_, existing)| *existing != edge);
self.axes.push((axis, edge));
}
fn draw(&self, rect: Rect, context: &mut dyn ChartContext) {
context.draw_rect(rect, axis_chrome_color(0.0));
context.draw_text(
&self.title,
Point::new(rect.x + 8, rect.y + 16),
14.0,
axis_chrome_color(0.70),
);
let visible_count = self.series.iter().filter(|series| series.visible).count();
let layout = compute_cartesian_layout(
rect,
!self.x_axis_label.is_empty(),
!self.y_axis_label.is_empty(),
visible_count,
);
draw_cartesian_axes(context, &layout);
if visible_count == 0 {
return;
}
let mut max_y = 1.0f64;
let mut min_x = f64::MAX;
let mut max_x = f64::MIN;
let mut points_per_series = 0usize;
for series in self.series.iter().filter(|series| series.visible) {
points_per_series = points_per_series.max(series.data.len());
for point in &series.data {
max_y = max_y.max(point.y.max(1.0));
min_x = min_x.min(point.x);
max_x = max_x.max(point.x);
}
}
if min_x == f64::MAX {
return;
}
let (min_x, max_x, min_y, max_y) =
apply_view((min_x, max_x, 0.0, max_y), self.zoom, self.scroll);
let span_x = (max_x - min_x).max(f64::EPSILON);
draw_x_ticks(context, &layout, min_x, max_x, self.x_tick_count, self.show_grid);
draw_y_ticks(context, &layout, min_y, max_y, self.y_tick_count, self.show_grid);
if !self.x_axis_label.is_empty() {
context.draw_text(
&self.x_axis_label,
Point::from_f32(
layout.plot_x + layout.plot_w * 0.5 - 28.0,
layout.plot_y + layout.plot_h + 36.0,
),
11.0,
axis_chrome_color(0.45),
);
}
if !self.y_axis_label.is_empty() {
context.draw_text(
&self.y_axis_label,
Point::from_f32(layout.plot_x - 56.0, layout.plot_y - 10.0),
11.0,
axis_chrome_color(0.45),
);
}
let point_slots = points_per_series.max(1) * visible_count;
let bar_width = (layout.plot_w / (point_slots as f32 + 1.0)).clamp(4.0, 24.0);
for (series_index, series) in self.series.iter().filter(|series| series.visible).enumerate()
{
for point in &series.data {
let base_x = layout.plot_x
+ (((point.x - min_x) / span_x) as f32) * (layout.plot_w - bar_width);
let x = base_x + series_index as f32 * bar_width;
let bar_h = (((point.y - min_y) / (max_y - min_y).max(f64::EPSILON))
* layout.plot_h as f64) as u32;
let bar = Rect {
x: x as i32,
y: (layout.plot_y + layout.plot_h) as i32 - bar_h as i32,
width: bar_width.max(1.0) as u32,
height: bar_h,
};
context.draw_rect(bar, series.color);
}
}
draw_legend_rows(
context,
&layout,
self.series.iter().filter(|series| series.visible),
visible_count,
);
draw_attached_axes(context, &layout, &self.axes, (min_x, max_x), (min_y, max_y));
}
}
pub struct PieChart {
title: String,
series: Vec<ChartSeries>,
}
impl PieChart {
pub fn new() -> Self {
Self { title: String::new(), series: Vec::new() }
}
fn palette_color(index: usize) -> Color {
const PALETTE: &[Color] = &[
Color { r: 255, g: 99, b: 132, a: 255 }, Color { r: 54, g: 162, b: 235, a: 255 }, Color { r: 255, g: 206, b: 86, a: 255 }, Color { r: 75, g: 192, b: 192, a: 255 }, Color { r: 153, g: 102, b: 255, a: 255 }, Color { r: 255, g: 159, b: 64, a: 255 }, Color { r: 46, g: 204, b: 113, a: 255 }, Color { r: 231, g: 76, b: 60, a: 255 }, ];
PALETTE[index % PALETTE.len()]
}
}
crate::impl_default_via_new!(PieChart);
impl Chart for PieChart {
fn add_series(&mut self, series: ChartSeries) {
self.series.push(series);
}
fn remove_series(&mut self, name: &str) {
self.series.retain(|s| s.name != name);
}
fn clear_series(&mut self) {
self.series.clear();
}
fn set_title(&mut self, title: String) {
self.title = title;
}
fn set_x_axis_label(&mut self, label: String) {
log::debug!(
"[chart] PieChart.set_x_axis_label ignored: pie charts have no x-axis (label='{label}')"
);
}
fn set_y_axis_label(&mut self, label: String) {
log::debug!(
"[chart] PieChart.set_y_axis_label ignored: pie charts have no y-axis (label='{label}')"
);
}
fn draw(&self, rect: Rect, context: &mut dyn ChartContext) {
let title_font_size = 14.0;
let title_band = Rect { x: rect.x, y: rect.y, width: rect.width, height: 28 };
let title_line = context.text_line(title_band, title_font_size);
context.draw_text(
&self.title,
Point::new(title_band.x + 8, title_line.y),
title_font_size,
axis_chrome_color(0.70),
);
let center =
Point { x: rect.x + rect.width as i32 / 2, y: rect.y + rect.height as i32 / 2 };
let radius = (rect.width.min(rect.height) as f64 / 2.5) as u32;
let total: f64 =
self.series.iter().filter(|s| s.visible).flat_map(|s| &s.data).map(|d| d.y).sum();
if total <= 0.0 {
return;
}
let mut start_angle: f64 = -90.0; let mut sector_index: usize = 0;
for series in &self.series {
if !series.visible {
continue;
}
for point in &series.data {
let sweep = (point.y / total) * 360.0;
if sweep <= 0.0 {
continue;
}
let sector_color = Self::palette_color(sector_index);
sector_index += 1;
let cx = center.x as f64;
let cy = center.y as f64;
let r = radius as f64;
let sweep_deg = sweep as f32;
let start_rad = (start_angle as f32).to_radians();
let end_rad = start_rad + sweep_deg.to_radians();
let vertices = sector_polygon(
Point { x: cx as i32, y: cy as i32 },
r as f32,
0.0,
start_rad,
end_rad,
(sweep.abs().ceil() as u32).clamp(3, 90),
);
if !vertices.is_empty() {
context.draw_polygon(&vertices, sector_color);
}
start_angle += sweep;
}
}
}
}
pub struct ScatterChart {
title: String,
x_axis_label: String,
y_axis_label: String,
series: Vec<ChartSeries>,
x_tick_count: usize,
y_tick_count: usize,
show_grid: bool,
point_radius: f32,
zoom: Option<(f64, f64, f64, f64)>,
scroll: (f64, f64),
axes: Vec<(Axis, AxisEdge)>,
}
impl ScatterChart {
pub fn new() -> Self {
Self {
title: String::new(),
x_axis_label: String::new(),
y_axis_label: String::new(),
series: Vec::new(),
x_tick_count: 5,
y_tick_count: 5,
show_grid: false,
point_radius: 4.0,
zoom: None,
scroll: (0.0, 0.0),
axes: Vec::new(),
}
}
pub fn set_x_tick_count(&mut self, tick_count: usize) {
self.x_tick_count = tick_count.clamp(2, 16);
}
pub fn set_y_tick_count(&mut self, tick_count: usize) {
self.y_tick_count = tick_count.clamp(2, 16);
}
pub fn set_grid_enabled(&mut self, enabled: bool) {
self.show_grid = enabled;
}
pub fn set_point_radius(&mut self, radius: f32) {
self.point_radius = radius.max(1.0);
}
}
crate::impl_default_via_new!(ScatterChart);
impl Chart for ScatterChart {
fn add_series(&mut self, series: ChartSeries) {
self.series.push(series);
}
fn remove_series(&mut self, name: &str) {
self.series.retain(|s| s.name != name);
}
fn clear_series(&mut self) {
self.series.clear();
}
fn set_title(&mut self, title: String) {
self.title = title;
}
fn set_x_axis_label(&mut self, label: String) {
self.x_axis_label = label;
}
fn set_y_axis_label(&mut self, label: String) {
self.y_axis_label = label;
}
fn zoom_in(&mut self, rect: Rect) -> bool {
if rect.width == 0 || rect.height == 0 {
return false;
}
self.zoom = Some((
f64::from(rect.x),
f64::from(rect.y),
f64::from(rect.width),
f64::from(rect.height),
));
self.scroll = (0.0, 0.0);
true
}
fn zoom_reset(&mut self) {
self.zoom = None;
self.scroll = (0.0, 0.0);
}
fn scroll(&mut self, dx: f64, dy: f64) {
if dx.is_finite() && dy.is_finite() {
self.scroll.0 += dx;
self.scroll.1 += dy;
}
}
fn attach_axis(&mut self, axis: Axis, edge: AxisEdge) {
self.axes.retain(|(_, existing)| *existing != edge);
self.axes.push((axis, edge));
}
fn draw(&self, rect: Rect, context: &mut dyn ChartContext) {
context.draw_rect(rect, axis_chrome_color(0.0));
context.draw_text(
&self.title,
Point::new(rect.x + 8, rect.y + 16),
14.0,
axis_chrome_color(0.70),
);
let visible_count = self.series.iter().filter(|series| series.visible).count();
let layout = compute_cartesian_layout(
rect,
!self.x_axis_label.is_empty(),
!self.y_axis_label.is_empty(),
visible_count,
);
draw_cartesian_axes(context, &layout);
let mut min_x = f64::MAX;
let mut max_x = f64::MIN;
let mut min_y = f64::MAX;
let mut max_y = f64::MIN;
for series in self.series.iter().filter(|series| series.visible) {
for point in &series.data {
min_x = min_x.min(point.x);
max_x = max_x.max(point.x);
min_y = min_y.min(point.y);
max_y = max_y.max(point.y);
}
}
if min_x == f64::MAX || min_y == f64::MAX {
return;
}
let (min_x, max_x, min_y, max_y) =
apply_view((min_x, max_x, min_y, max_y), self.zoom, self.scroll);
let span_x = (max_x - min_x).max(f64::EPSILON);
let span_y = (max_y - min_y).max(f64::EPSILON);
draw_x_ticks(context, &layout, min_x, max_x, self.x_tick_count, self.show_grid);
draw_y_ticks(context, &layout, min_y, max_y, self.y_tick_count, self.show_grid);
if !self.x_axis_label.is_empty() {
context.draw_text(
&self.x_axis_label,
Point::from_f32(
layout.plot_x + layout.plot_w * 0.5 - 28.0,
layout.plot_y + layout.plot_h + 36.0,
),
11.0,
axis_chrome_color(0.45),
);
}
if !self.y_axis_label.is_empty() {
context.draw_text(
&self.y_axis_label,
Point::from_f32(layout.plot_x - 56.0, layout.plot_y - 10.0),
11.0,
axis_chrome_color(0.45),
);
}
for series in self.series.iter().filter(|series| series.visible) {
for point in &series.data {
let x = layout.plot_x + (((point.x - min_x) / span_x) as f32) * layout.plot_w;
let y = layout.plot_y + layout.plot_h
- (((point.y - min_y) / span_y) as f32) * layout.plot_h;
context.draw_circle(
Point { x: x as i32, y: y as i32 },
self.point_radius,
series.color,
);
}
}
draw_legend_rows(
context,
&layout,
self.series.iter().filter(|series| series.visible),
visible_count,
);
draw_attached_axes(context, &layout, &self.axes, (min_x, max_x), (min_y, max_y));
}
}
pub struct AreaChart {
title: String,
x_axis_label: String,
y_axis_label: String,
series: Vec<ChartSeries>,
x_tick_count: usize,
y_tick_count: usize,
show_grid: bool,
stacked: bool,
zoom: Option<(f64, f64, f64, f64)>,
scroll: (f64, f64),
axes: Vec<(Axis, AxisEdge)>,
}
impl AreaChart {
pub fn new() -> Self {
Self {
title: String::new(),
x_axis_label: String::new(),
y_axis_label: String::new(),
series: Vec::new(),
x_tick_count: 5,
y_tick_count: 5,
show_grid: false,
stacked: false,
zoom: None,
scroll: (0.0, 0.0),
axes: Vec::new(),
}
}
pub fn set_x_tick_count(&mut self, tick_count: usize) {
self.x_tick_count = tick_count.clamp(2, 16);
}
pub fn set_y_tick_count(&mut self, tick_count: usize) {
self.y_tick_count = tick_count.clamp(2, 16);
}
pub fn set_grid_enabled(&mut self, enabled: bool) {
self.show_grid = enabled;
}
pub fn set_stacked(&mut self, stacked: bool) {
self.stacked = stacked;
}
}
crate::impl_default_via_new!(AreaChart);
impl Chart for AreaChart {
fn add_series(&mut self, series: ChartSeries) {
self.series.push(series);
}
fn remove_series(&mut self, name: &str) {
self.series.retain(|s| s.name != name);
}
fn clear_series(&mut self) {
self.series.clear();
}
fn set_title(&mut self, title: String) {
self.title = title;
}
fn set_x_axis_label(&mut self, label: String) {
self.x_axis_label = label;
}
fn set_y_axis_label(&mut self, label: String) {
self.y_axis_label = label;
}
fn zoom_in(&mut self, rect: Rect) -> bool {
if rect.width == 0 || rect.height == 0 {
return false;
}
self.zoom = Some((
f64::from(rect.x),
f64::from(rect.y),
f64::from(rect.width),
f64::from(rect.height),
));
self.scroll = (0.0, 0.0);
true
}
fn zoom_reset(&mut self) {
self.zoom = None;
self.scroll = (0.0, 0.0);
}
fn scroll(&mut self, dx: f64, dy: f64) {
if dx.is_finite() && dy.is_finite() {
self.scroll.0 += dx;
self.scroll.1 += dy;
}
}
fn attach_axis(&mut self, axis: Axis, edge: AxisEdge) {
self.axes.retain(|(_, existing)| *existing != edge);
self.axes.push((axis, edge));
}
fn draw(&self, rect: Rect, context: &mut dyn ChartContext) {
context.draw_rect(rect, axis_chrome_color(0.0));
context.draw_text(
&self.title,
Point::new(rect.x + 8, rect.y + 16),
14.0,
axis_chrome_color(0.70),
);
let visible_count = self.series.iter().filter(|series| series.visible).count();
let layout = compute_cartesian_layout(
rect,
!self.x_axis_label.is_empty(),
!self.y_axis_label.is_empty(),
visible_count,
);
draw_cartesian_axes(context, &layout);
let max_len = self
.series
.iter()
.filter(|series| series.visible)
.map(|s| s.data.len())
.max()
.unwrap_or(0);
let mut min_x = f64::MAX;
let mut max_x = f64::MIN;
let mut min_y = f64::MAX;
let mut max_y = f64::MIN;
if self.stacked {
let mut stacked_y_totals = vec![0.0f64; max_len];
for series in self.series.iter().filter(|series| series.visible) {
for (i, point) in series.data.iter().enumerate() {
if !point.x.is_finite() || !point.y.is_finite() {
continue;
}
min_x = min_x.min(point.x);
max_x = max_x.max(point.x);
stacked_y_totals[i] += point.y;
}
}
min_y = 0.0;
for &sy in &stacked_y_totals {
min_y = min_y.min(sy);
max_y = max_y.max(sy);
}
if max_y <= min_y {
max_y = min_y + 1.0;
}
} else {
for series in self.series.iter().filter(|series| series.visible) {
for point in &series.data {
if !point.x.is_finite() || !point.y.is_finite() {
continue;
}
min_x = min_x.min(point.x);
max_x = max_x.max(point.x);
min_y = min_y.min(point.y);
max_y = max_y.max(point.y);
}
}
}
if min_x == f64::MAX || min_y == f64::MAX {
return;
}
let (min_x, max_x, min_y, max_y) =
apply_view((min_x, max_x, min_y, max_y), self.zoom, self.scroll);
let span_x = (max_x - min_x).max(f64::EPSILON);
let span_y = (max_y - min_y).max(f64::EPSILON);
draw_x_ticks(context, &layout, min_x, max_x, self.x_tick_count, self.show_grid);
draw_y_ticks(context, &layout, min_y, max_y, self.y_tick_count, self.show_grid);
if !self.x_axis_label.is_empty() {
context.draw_text(
&self.x_axis_label,
Point::from_f32(
layout.plot_x + layout.plot_w * 0.5 - 28.0,
layout.plot_y + layout.plot_h + 36.0,
),
11.0,
axis_chrome_color(0.45),
);
}
if !self.y_axis_label.is_empty() {
context.draw_text(
&self.y_axis_label,
Point::from_f32(layout.plot_x - 56.0, layout.plot_y - 10.0),
11.0,
axis_chrome_color(0.45),
);
}
let baseline = layout.plot_y + layout.plot_h;
let mut accum = if self.stacked { vec![0.0_f64; max_len] } else { Vec::new() };
for series in self.series.iter().filter(|series| series.visible) {
if series.data.len() < 2 {
continue;
}
let mut pts = Vec::with_capacity(series.data.len() * 2 + 2);
let p0 = &series.data[0];
let x0 = layout.plot_x + (((p0.x - min_x) / span_x) as f32) * layout.plot_w;
let first_y = if self.stacked {
let stacked_y = accum[0] + p0.y;
accum[0] = stacked_y;
stacked_y
} else {
p0.y
};
let y0 = baseline - (((first_y - min_y) / span_y) as f32) * layout.plot_h;
pts.push(Point::from_f32(x0, baseline));
pts.push(Point::from_f32(x0, y0));
for (i, p) in series.data.iter().enumerate().skip(1) {
let effective_y = if self.stacked {
let stacked_y = accum[i] + p.y;
accum[i] = stacked_y;
stacked_y
} else {
p.y
};
let x = layout.plot_x + (((p.x - min_x) / span_x) as f32) * layout.plot_w;
let y = baseline - (((effective_y - min_y) / span_y) as f32) * layout.plot_h;
pts.push(Point::from_f32(x, y));
}
let last = &series.data[series.data.len() - 1];
let last_x = layout.plot_x + (((last.x - min_x) / span_x) as f32) * layout.plot_w;
pts.push(Point::from_f32(last_x, baseline));
context.draw_polygon(&pts, series.color.with_alpha_f32(0.3));
for pair in pts[1..pts.len() - 1].windows(2) {
context.draw_line(pair[0], pair[1], 2.0, series.color);
}
}
draw_legend_rows(
context,
&layout,
self.series.iter().filter(|series| series.visible),
visible_count,
);
draw_attached_axes(context, &layout, &self.axes, (min_x, max_x), (min_y, max_y));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::widget::chart_widgets::svg::MemoryChartContext;
#[test]
fn a_short_plot_thins_the_y_tick_labels_instead_of_stacking_them() {
let layout = compute_cartesian_layout(Rect::new(0, 0, 300, 120), true, true, 0);
let mut context = MemoryChartContext::default();
draw_y_ticks(&mut context, &layout, 0.0, 100.0, 4, true);
let mut label_ys: Vec<f32> = Vec::new();
for command in &context.commands {
let Some(rest) = command.strip_prefix("text:") else { continue };
let Some((_, position)) = rest.split_once('@') else { continue };
let Some((_, y)) = position.split_once(',') else { continue };
let Some((y, _)) = y.split_once(':') else { continue };
label_ys.push(y.parse().expect("a recorded y is a number"));
}
assert!(
label_ys.len() < 5,
"five ticks cannot each carry a label in a 36 px plot: {label_ys:?}"
);
let line = layout.tick_label_height();
let mut sorted = label_ys.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).expect("a recorded y is finite"));
for pair in sorted.windows(2) {
assert!(
pair[1] - pair[0] >= line,
"labels {pair:?} are closer than one {line} px line box"
);
}
assert!(
context.commands.iter().any(|command| command.starts_with("line:")),
"the gridlines must still be drawn at every tick"
);
}
#[test]
fn a_tall_plot_keeps_every_y_tick_label() {
let layout = compute_cartesian_layout(Rect::new(0, 0, 300, 400), true, true, 0);
let mut context = MemoryChartContext::default();
draw_y_ticks(&mut context, &layout, 0.0, 100.0, 4, true);
let labels = context.commands.iter().filter(|command| command.starts_with("text:")).count();
assert_eq!(labels, 5, "five ticks, five labels, when the plot is tall enough");
}
}