use presentar_core::{
widget::{AccessibleRole, LayoutResult, TextStyle},
Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Point, Rect,
Size, TypeId, Widget,
};
use serde::{Deserialize, Serialize};
use std::any::Any;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum ChartType {
#[default]
Line,
Bar,
Scatter,
Area,
Pie,
Histogram,
Heatmap,
BoxPlot,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DataSeries {
pub name: String,
pub points: Vec<(f64, f64)>,
pub color: Color,
pub line_width: f32,
pub point_size: f32,
pub show_points: bool,
pub fill: bool,
}
impl DataSeries {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
points: Vec::new(),
color: Color::new(0.2, 0.47, 0.96, 1.0),
line_width: 2.0,
point_size: 4.0,
show_points: true,
fill: false,
}
}
#[must_use]
pub fn point(mut self, x: f64, y: f64) -> Self {
self.points.push((x, y));
self
}
#[must_use]
pub fn points(mut self, points: impl IntoIterator<Item = (f64, f64)>) -> Self {
self.points.extend(points);
self
}
#[must_use]
pub const fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
#[must_use]
pub fn line_width(mut self, width: f32) -> Self {
self.line_width = width.max(0.5);
self
}
#[must_use]
pub fn point_size(mut self, size: f32) -> Self {
self.point_size = size.max(1.0);
self
}
#[must_use]
pub const fn show_points(mut self, show: bool) -> Self {
self.show_points = show;
self
}
#[must_use]
pub const fn fill(mut self, fill: bool) -> Self {
self.fill = fill;
self
}
#[must_use]
pub fn x_range(&self) -> Option<(f64, f64)> {
if self.points.is_empty() {
return None;
}
let min = self
.points
.iter()
.map(|(x, _)| *x)
.fold(f64::INFINITY, f64::min);
let max = self
.points
.iter()
.map(|(x, _)| *x)
.fold(f64::NEG_INFINITY, f64::max);
Some((min, max))
}
#[must_use]
pub fn y_range(&self) -> Option<(f64, f64)> {
if self.points.is_empty() {
return None;
}
let min = self
.points
.iter()
.map(|(_, y)| *y)
.fold(f64::INFINITY, f64::min);
let max = self
.points
.iter()
.map(|(_, y)| *y)
.fold(f64::NEG_INFINITY, f64::max);
Some((min, max))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Axis {
pub label: Option<String>,
pub min: Option<f64>,
pub max: Option<f64>,
pub grid_lines: usize,
pub show_grid: bool,
pub color: Color,
pub grid_color: Color,
}
impl Default for Axis {
fn default() -> Self {
Self {
label: None,
min: None,
max: None,
grid_lines: 5,
show_grid: true,
color: Color::new(0.3, 0.3, 0.3, 1.0),
grid_color: Color::new(0.9, 0.9, 0.9, 1.0),
}
}
}
impl Axis {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub const fn min(mut self, min: f64) -> Self {
self.min = Some(min);
self
}
#[must_use]
pub const fn max(mut self, max: f64) -> Self {
self.max = Some(max);
self
}
#[must_use]
pub const fn range(mut self, min: f64, max: f64) -> Self {
self.min = Some(min);
self.max = Some(max);
self
}
#[must_use]
pub fn grid_lines(mut self, count: usize) -> Self {
self.grid_lines = count.max(2);
self
}
#[must_use]
pub const fn show_grid(mut self, show: bool) -> Self {
self.show_grid = show;
self
}
#[must_use]
pub const fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
#[must_use]
pub const fn grid_color(mut self, color: Color) -> Self {
self.grid_color = color;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum LegendPosition {
None,
#[default]
TopRight,
TopLeft,
BottomRight,
BottomLeft,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Chart {
kind: ChartType,
series: Vec<DataSeries>,
title: Option<String>,
x_axis: Axis,
y_axis: Axis,
legend: LegendPosition,
background: Color,
padding: f32,
width: Option<f32>,
height: Option<f32>,
accessible_name_value: Option<String>,
test_id_value: Option<String>,
#[serde(skip)]
bounds: Rect,
}
impl Default for Chart {
fn default() -> Self {
Self {
kind: ChartType::Line,
series: Vec::new(),
title: None,
x_axis: Axis::default(),
y_axis: Axis::default(),
legend: LegendPosition::TopRight,
background: Color::WHITE,
padding: 40.0,
width: None,
height: None,
accessible_name_value: None,
test_id_value: None,
bounds: Rect::default(),
}
}
}
impl Chart {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn line() -> Self {
Self::new().chart_type(ChartType::Line)
}
#[must_use]
pub fn bar() -> Self {
Self::new().chart_type(ChartType::Bar)
}
#[must_use]
pub fn scatter() -> Self {
Self::new().chart_type(ChartType::Scatter)
}
#[must_use]
pub fn area() -> Self {
Self::new().chart_type(ChartType::Area)
}
#[must_use]
pub fn pie() -> Self {
Self::new().chart_type(ChartType::Pie)
}
#[must_use]
pub fn heatmap() -> Self {
Self::new().chart_type(ChartType::Heatmap)
}
#[must_use]
pub fn boxplot() -> Self {
Self::new().chart_type(ChartType::BoxPlot)
}
#[must_use]
pub const fn chart_type(mut self, chart_type: ChartType) -> Self {
self.kind = chart_type;
self
}
#[must_use]
pub fn series(mut self, series: DataSeries) -> Self {
self.series.push(series);
self
}
#[must_use]
pub fn add_series(mut self, series: impl IntoIterator<Item = DataSeries>) -> Self {
self.series.extend(series);
self
}
#[must_use]
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
#[must_use]
pub fn x_axis(mut self, axis: Axis) -> Self {
self.x_axis = axis;
self
}
#[must_use]
pub fn y_axis(mut self, axis: Axis) -> Self {
self.y_axis = axis;
self
}
#[must_use]
pub const fn legend(mut self, position: LegendPosition) -> Self {
self.legend = position;
self
}
#[must_use]
pub const fn background(mut self, color: Color) -> Self {
self.background = color;
self
}
#[must_use]
pub fn padding(mut self, padding: f32) -> Self {
self.padding = padding.max(0.0);
self
}
#[must_use]
pub fn width(mut self, width: f32) -> Self {
self.width = Some(width.max(100.0));
self
}
#[must_use]
pub fn height(mut self, height: f32) -> Self {
self.height = Some(height.max(100.0));
self
}
#[must_use]
pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
self.accessible_name_value = Some(name.into());
self
}
#[must_use]
pub fn test_id(mut self, id: impl Into<String>) -> Self {
self.test_id_value = Some(id.into());
self
}
#[must_use]
pub const fn get_chart_type(&self) -> ChartType {
self.kind
}
#[must_use]
pub fn get_series(&self) -> &[DataSeries] {
&self.series
}
#[must_use]
pub fn series_count(&self) -> usize {
self.series.len()
}
#[must_use]
pub fn has_data(&self) -> bool {
self.series.iter().any(|s| !s.points.is_empty())
}
#[must_use]
pub fn get_title(&self) -> Option<&str> {
self.title.as_deref()
}
#[must_use]
pub fn data_bounds(&self) -> Option<(f64, f64, f64, f64)> {
if !self.has_data() {
return None;
}
let mut x_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for series in &self.series {
if let Some((min, max)) = series.x_range() {
x_min = x_min.min(min);
x_max = x_max.max(max);
}
if let Some((min, max)) = series.y_range() {
y_min = y_min.min(min);
y_max = y_max.max(max);
}
}
if let Some(min) = self.x_axis.min {
x_min = min;
}
if let Some(max) = self.x_axis.max {
x_max = max;
}
if let Some(min) = self.y_axis.min {
y_min = min;
}
if let Some(max) = self.y_axis.max {
y_max = max;
}
Some((x_min, x_max, y_min, y_max))
}
fn plot_area(&self) -> Rect {
let title_height = if self.title.is_some() { 30.0 } else { 0.0 };
Rect::new(
self.bounds.x + self.padding,
self.bounds.y + self.padding + title_height,
self.padding.mul_add(-2.0, self.bounds.width),
self.padding.mul_add(-2.0, self.bounds.height) - title_height,
)
}
fn map_point(&self, x: f64, y: f64, bounds: &(f64, f64, f64, f64), plot: &Rect) -> Point {
let (x_min, x_max, y_min, y_max) = *bounds;
let x_range = (x_max - x_min).max(1e-10);
let y_range = (y_max - y_min).max(1e-10);
let px = (((x - x_min) / x_range) as f32).mul_add(plot.width, plot.x);
let py = (((y - y_min) / y_range) as f32).mul_add(-plot.height, plot.y + plot.height);
Point::new(px, py)
}
fn paint_grid(&self, canvas: &mut dyn Canvas, plot: &Rect, bounds: &(f64, f64, f64, f64)) {
let (x_min, x_max, y_min, y_max) = *bounds;
if self.x_axis.show_grid {
for i in 0..=self.x_axis.grid_lines {
let t = i as f32 / self.x_axis.grid_lines as f32;
let x = t.mul_add(plot.width, plot.x);
canvas.draw_line(
Point::new(x, plot.y),
Point::new(x, plot.y + plot.height),
self.x_axis.grid_color,
1.0,
);
}
}
if self.y_axis.show_grid {
for i in 0..=self.y_axis.grid_lines {
let t = i as f32 / self.y_axis.grid_lines as f32;
let y = t.mul_add(plot.height, plot.y);
canvas.draw_line(
Point::new(plot.x, y),
Point::new(plot.x + plot.width, y),
self.y_axis.grid_color,
1.0,
);
}
}
let text_style = TextStyle {
size: 10.0,
color: self.x_axis.color,
..TextStyle::default()
};
for i in 0..=self.x_axis.grid_lines {
let t = i as f64 / self.x_axis.grid_lines as f64;
let value = t.mul_add(x_max - x_min, x_min);
let x = (t as f32).mul_add(plot.width, plot.x);
canvas.draw_text(
&format!("{value:.1}"),
Point::new(x, plot.y + plot.height + 15.0),
&text_style,
);
}
for i in 0..=self.y_axis.grid_lines {
let t = i as f64 / self.y_axis.grid_lines as f64;
let value = t.mul_add(-(y_max - y_min), y_max);
let y = (t as f32).mul_add(plot.height, plot.y);
canvas.draw_text(
&format!("{value:.1}"),
Point::new(plot.x - 35.0, y + 4.0),
&text_style,
);
}
}
fn paint_line(&self, canvas: &mut dyn Canvas, plot: &Rect, bounds: &(f64, f64, f64, f64)) {
for series in &self.series {
if series.points.len() < 2 {
continue;
}
let path_points: Vec<Point> = series
.points
.iter()
.map(|&(x, y)| self.map_point(x, y, bounds, plot))
.collect();
canvas.draw_path(&path_points, series.color, series.line_width);
if series.fill {
let mut fill_points = path_points.clone();
if let (Some(first), Some(last)) = (path_points.first(), path_points.last()) {
fill_points.push(Point::new(last.x, plot.y + plot.height));
fill_points.push(Point::new(first.x, plot.y + plot.height));
}
let mut fill_color = series.color;
fill_color.a = 0.3; canvas.fill_polygon(&fill_points, fill_color);
}
if series.show_points {
for &(x, y) in &series.points {
let pt = self.map_point(x, y, bounds, plot);
canvas.fill_circle(pt, series.point_size / 2.0, series.color);
}
}
}
}
fn paint_bar(&self, canvas: &mut dyn Canvas, plot: &Rect, bounds: &(f64, f64, f64, f64)) {
let (_, _, y_min, y_max) = *bounds;
let y_range = (y_max - y_min).max(1e-10);
let series_count = self.series.len();
if series_count == 0 {
return;
}
let max_points = self
.series
.iter()
.map(|s| s.points.len())
.max()
.unwrap_or(1);
let group_width = plot.width / max_points as f32;
let bar_width = (group_width * 0.8) / series_count as f32;
let bar_gap = group_width * 0.1;
for (si, series) in self.series.iter().enumerate() {
for (i, &(_, y)) in series.points.iter().enumerate() {
let bar_height = ((y - y_min) / y_range) as f32 * plot.height;
let x = (si as f32)
.mul_add(bar_width, (i as f32).mul_add(group_width, plot.x + bar_gap));
let rect = Rect::new(
x,
plot.y + plot.height - bar_height,
bar_width - 2.0,
bar_height,
);
canvas.fill_rect(rect, series.color);
}
}
}
fn paint_scatter(&self, canvas: &mut dyn Canvas, plot: &Rect, bounds: &(f64, f64, f64, f64)) {
for series in &self.series {
for &(x, y) in &series.points {
let pt = self.map_point(x, y, bounds, plot);
canvas.fill_circle(pt, series.point_size / 2.0, series.color);
}
}
}
fn paint_pie(&self, canvas: &mut dyn Canvas, plot: &Rect) {
let total: f64 = self
.series
.iter()
.flat_map(|s| s.points.iter().map(|(_, y)| *y))
.sum();
if total <= 0.0 {
return;
}
let cx = plot.x + plot.width / 2.0;
let cy = plot.y + plot.height / 2.0;
let radius = plot.width.min(plot.height) / 2.0 * 0.8;
let center = Point::new(cx, cy);
let mut start_angle: f32 = -std::f32::consts::FRAC_PI_2; for series in &self.series {
for &(_, y) in &series.points {
let fraction = (y / total) as f32;
let sweep = fraction * std::f32::consts::TAU;
let end_angle = start_angle + sweep;
canvas.fill_arc(center, radius, start_angle, end_angle, series.color);
start_angle = end_angle;
}
}
}
fn paint_heatmap(&self, canvas: &mut dyn Canvas, plot: &Rect, bounds: &(f64, f64, f64, f64)) {
let (_, _, y_min, y_max) = *bounds;
let y_range = (y_max - y_min).max(1e-10);
let row_count = self.series.len();
if row_count == 0 {
return;
}
let col_count = self
.series
.iter()
.map(|s| s.points.len())
.max()
.unwrap_or(1);
let cell_width = plot.width / col_count as f32;
let cell_height = plot.height / row_count as f32;
for (row, series) in self.series.iter().enumerate() {
for (col, &(_, value)) in series.points.iter().enumerate() {
let t = ((value - y_min) / y_range) as f32;
let color = Color::new(t, 0.2, 1.0 - t, 1.0);
let rect = Rect::new(
(col as f32).mul_add(cell_width, plot.x),
(row as f32).mul_add(cell_height, plot.y),
cell_width - 1.0,
cell_height - 1.0,
);
canvas.fill_rect(rect, color);
}
}
}
fn paint_boxplot(&self, canvas: &mut dyn Canvas, plot: &Rect, bounds: &(f64, f64, f64, f64)) {
let (_, _, y_min, y_max) = *bounds;
let y_range = (y_max - y_min).max(1e-10);
let series_count = self.series.len();
if series_count == 0 {
return;
}
let box_width = (plot.width / series_count as f32) * 0.6;
let gap = (plot.width / series_count as f32) * 0.2;
for (i, series) in self.series.iter().enumerate() {
if series.points.len() < 5 {
continue; }
let mut values: Vec<f64> = series.points.iter().map(|(_, y)| *y).collect();
values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let min_val = values[0];
let q1 = values[values.len() / 4];
let median = values[values.len() / 2];
let q3 = values[3 * values.len() / 4];
let max_val = values[values.len() - 1];
let x_center = (i as f32).mul_add(plot.width / series_count as f32, plot.x + gap);
let map_y = |v: f64| -> f32 {
let t = (v - y_min) / y_range;
(1.0 - t as f32).mul_add(plot.height, plot.y)
};
let y_min_px = map_y(min_val);
let y_q1 = map_y(q1);
let y_median = map_y(median);
let y_q3 = map_y(q3);
let y_max_px = map_y(max_val);
canvas.draw_line(
Point::new(x_center + box_width / 2.0, y_min_px),
Point::new(x_center + box_width / 2.0, y_q1),
series.color,
1.0,
);
canvas.draw_line(
Point::new(x_center + box_width / 2.0, y_q3),
Point::new(x_center + box_width / 2.0, y_max_px),
series.color,
1.0,
);
let box_rect = Rect::new(x_center, y_q3, box_width, y_q1 - y_q3);
canvas.fill_rect(box_rect, series.color);
canvas.stroke_rect(box_rect, Color::new(0.0, 0.0, 0.0, 1.0), 1.0);
canvas.draw_line(
Point::new(x_center, y_median),
Point::new(x_center + box_width, y_median),
Color::new(0.0, 0.0, 0.0, 1.0),
2.0,
);
let cap_width = box_width * 0.3;
canvas.draw_line(
Point::new(x_center + box_width / 2.0 - cap_width / 2.0, y_min_px),
Point::new(x_center + box_width / 2.0 + cap_width / 2.0, y_min_px),
series.color,
1.0,
);
canvas.draw_line(
Point::new(x_center + box_width / 2.0 - cap_width / 2.0, y_max_px),
Point::new(x_center + box_width / 2.0 + cap_width / 2.0, y_max_px),
series.color,
1.0,
);
}
}
fn paint_legend(&self, canvas: &mut dyn Canvas) {
if self.legend == LegendPosition::None || self.series.is_empty() {
return;
}
let entry_height = 20.0;
let legend_width = 100.0;
let legend_height = (self.series.len() as f32).mul_add(entry_height, 10.0);
let (lx, ly) = match self.legend {
LegendPosition::TopRight => (
self.bounds.x + self.bounds.width - legend_width - 10.0,
self.bounds.y + self.padding + 10.0,
),
LegendPosition::TopLeft => (
self.bounds.x + self.padding + 10.0,
self.bounds.y + self.padding + 10.0,
),
LegendPosition::BottomRight => (
self.bounds.x + self.bounds.width - legend_width - 10.0,
self.bounds.y + self.bounds.height - legend_height - 10.0,
),
LegendPosition::BottomLeft => (
self.bounds.x + self.padding + 10.0,
self.bounds.y + self.bounds.height - legend_height - 10.0,
),
LegendPosition::None => return,
};
canvas.fill_rect(
Rect::new(lx, ly, legend_width, legend_height),
Color::new(1.0, 1.0, 1.0, 0.9),
);
canvas.stroke_rect(
Rect::new(lx, ly, legend_width, legend_height),
Color::new(0.8, 0.8, 0.8, 1.0),
1.0,
);
let text_style = TextStyle {
size: 12.0,
color: Color::new(0.2, 0.2, 0.2, 1.0),
..TextStyle::default()
};
for (i, series) in self.series.iter().enumerate() {
let ey = (i as f32).mul_add(entry_height, ly + 5.0);
canvas.fill_rect(Rect::new(lx + 5.0, ey + 4.0, 12.0, 12.0), series.color);
canvas.draw_text(&series.name, Point::new(lx + 22.0, ey + 14.0), &text_style);
}
}
}
impl Widget for Chart {
fn type_id(&self) -> TypeId {
TypeId::of::<Self>()
}
fn measure(&self, constraints: Constraints) -> Size {
let width = self.width.unwrap_or(400.0);
let height = self.height.unwrap_or(300.0);
constraints.constrain(Size::new(width, height))
}
fn layout(&mut self, bounds: Rect) -> LayoutResult {
self.bounds = bounds;
LayoutResult {
size: bounds.size(),
}
}
fn paint(&self, canvas: &mut dyn Canvas) {
canvas.fill_rect(self.bounds, self.background);
if let Some(ref title) = self.title {
let text_style = TextStyle {
size: 16.0,
color: Color::new(0.1, 0.1, 0.1, 1.0),
..TextStyle::default()
};
canvas.draw_text(
title,
Point::new(
(title.len() as f32).mul_add(-4.0, self.bounds.x + self.bounds.width / 2.0),
self.bounds.y + 25.0,
),
&text_style,
);
}
let plot = self.plot_area();
let Some(bounds) = self.data_bounds() else {
return;
};
self.paint_grid(canvas, &plot, &bounds);
match self.kind {
ChartType::Line | ChartType::Area => self.paint_line(canvas, &plot, &bounds),
ChartType::Bar | ChartType::Histogram => self.paint_bar(canvas, &plot, &bounds),
ChartType::Scatter => self.paint_scatter(canvas, &plot, &bounds),
ChartType::Pie => self.paint_pie(canvas, &plot),
ChartType::Heatmap => self.paint_heatmap(canvas, &plot, &bounds),
ChartType::BoxPlot => self.paint_boxplot(canvas, &plot, &bounds),
}
self.paint_legend(canvas);
}
fn event(&mut self, _event: &presentar_core::Event) -> Option<Box<dyn Any + Send>> {
None
}
fn children(&self) -> &[Box<dyn Widget>] {
&[]
}
fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
&mut []
}
fn is_interactive(&self) -> bool {
false
}
fn is_focusable(&self) -> bool {
false
}
fn accessible_name(&self) -> Option<&str> {
self.accessible_name_value
.as_deref()
.or(self.title.as_deref())
}
fn accessible_role(&self) -> AccessibleRole {
AccessibleRole::Image }
fn test_id(&self) -> Option<&str> {
self.test_id_value.as_deref()
}
}
impl Brick for Chart {
fn brick_name(&self) -> &'static str {
"Chart"
}
fn assertions(&self) -> &[BrickAssertion] {
&[BrickAssertion::MaxLatencyMs(16)]
}
fn budget(&self) -> BrickBudget {
BrickBudget::uniform(16)
}
fn verify(&self) -> BrickVerification {
BrickVerification {
passed: self.assertions().to_vec(),
failed: vec![],
verification_time: Duration::from_micros(10),
}
}
fn to_html(&self) -> String {
let test_id = self.test_id_value.as_deref().unwrap_or("chart");
let title = self.title.as_deref().unwrap_or("Chart");
format!(
r#"<div class="brick-chart" data-testid="{test_id}" role="img" aria-label="{title}">{title}</div>"#
)
}
fn to_css(&self) -> String {
".brick-chart { display: block; }".into()
}
fn test_id(&self) -> Option<&str> {
self.test_id_value.as_deref()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chart_type_default() {
assert_eq!(ChartType::default(), ChartType::Line);
}
#[test]
fn test_chart_type_variants() {
let types = [
ChartType::Line,
ChartType::Bar,
ChartType::Scatter,
ChartType::Area,
ChartType::Pie,
ChartType::Histogram,
ChartType::Heatmap,
ChartType::BoxPlot,
];
assert_eq!(types.len(), 8);
}
#[test]
fn test_chart_heatmap() {
let chart = Chart::new().chart_type(ChartType::Heatmap);
assert_eq!(chart.get_chart_type(), ChartType::Heatmap);
}
#[test]
fn test_chart_boxplot() {
let chart = Chart::new().chart_type(ChartType::BoxPlot);
assert_eq!(chart.get_chart_type(), ChartType::BoxPlot);
}
#[test]
fn test_data_series_new() {
let series = DataSeries::new("Sales");
assert_eq!(series.name, "Sales");
assert!(series.points.is_empty());
assert!(series.show_points);
assert!(!series.fill);
}
#[test]
fn test_data_series_point() {
let series = DataSeries::new("Data")
.point(1.0, 10.0)
.point(2.0, 20.0)
.point(3.0, 15.0);
assert_eq!(series.points.len(), 3);
assert_eq!(series.points[0], (1.0, 10.0));
}
#[test]
fn test_data_series_points() {
let data = vec![(1.0, 5.0), (2.0, 10.0), (3.0, 7.0)];
let series = DataSeries::new("Data").points(data);
assert_eq!(series.points.len(), 3);
}
#[test]
fn test_data_series_color() {
let series = DataSeries::new("Data").color(Color::RED);
assert_eq!(series.color, Color::RED);
}
#[test]
fn test_data_series_line_width() {
let series = DataSeries::new("Data").line_width(3.0);
assert_eq!(series.line_width, 3.0);
}
#[test]
fn test_data_series_line_width_min() {
let series = DataSeries::new("Data").line_width(0.1);
assert_eq!(series.line_width, 0.5);
}
#[test]
fn test_data_series_point_size() {
let series = DataSeries::new("Data").point_size(6.0);
assert_eq!(series.point_size, 6.0);
}
#[test]
fn test_data_series_point_size_min() {
let series = DataSeries::new("Data").point_size(0.5);
assert_eq!(series.point_size, 1.0);
}
#[test]
fn test_data_series_show_points() {
let series = DataSeries::new("Data").show_points(false);
assert!(!series.show_points);
}
#[test]
fn test_data_series_fill() {
let series = DataSeries::new("Data").fill(true);
assert!(series.fill);
}
#[test]
fn test_data_series_x_range() {
let series = DataSeries::new("Data")
.point(1.0, 10.0)
.point(5.0, 20.0)
.point(3.0, 15.0);
assert_eq!(series.x_range(), Some((1.0, 5.0)));
}
#[test]
fn test_data_series_x_range_empty() {
let series = DataSeries::new("Data");
assert_eq!(series.x_range(), None);
}
#[test]
fn test_data_series_y_range() {
let series = DataSeries::new("Data")
.point(1.0, 10.0)
.point(2.0, 30.0)
.point(3.0, 5.0);
assert_eq!(series.y_range(), Some((5.0, 30.0)));
}
#[test]
fn test_data_series_y_range_empty() {
let series = DataSeries::new("Data");
assert_eq!(series.y_range(), None);
}
#[test]
fn test_axis_default() {
let axis = Axis::default();
assert!(axis.label.is_none());
assert!(axis.min.is_none());
assert!(axis.max.is_none());
assert_eq!(axis.grid_lines, 5);
assert!(axis.show_grid);
}
#[test]
fn test_axis_label() {
let axis = Axis::new().label("Time");
assert_eq!(axis.label, Some("Time".to_string()));
}
#[test]
fn test_axis_min_max() {
let axis = Axis::new().min(0.0).max(100.0);
assert_eq!(axis.min, Some(0.0));
assert_eq!(axis.max, Some(100.0));
}
#[test]
fn test_axis_range() {
let axis = Axis::new().range(10.0, 50.0);
assert_eq!(axis.min, Some(10.0));
assert_eq!(axis.max, Some(50.0));
}
#[test]
fn test_axis_grid_lines() {
let axis = Axis::new().grid_lines(10);
assert_eq!(axis.grid_lines, 10);
}
#[test]
fn test_axis_grid_lines_min() {
let axis = Axis::new().grid_lines(1);
assert_eq!(axis.grid_lines, 2);
}
#[test]
fn test_axis_show_grid() {
let axis = Axis::new().show_grid(false);
assert!(!axis.show_grid);
}
#[test]
fn test_axis_colors() {
let axis = Axis::new().color(Color::RED).grid_color(Color::BLUE);
assert_eq!(axis.color, Color::RED);
assert_eq!(axis.grid_color, Color::BLUE);
}
#[test]
fn test_legend_position_default() {
assert_eq!(LegendPosition::default(), LegendPosition::TopRight);
}
#[test]
fn test_chart_new() {
let chart = Chart::new();
assert_eq!(chart.get_chart_type(), ChartType::Line);
assert_eq!(chart.series_count(), 0);
assert!(!chart.has_data());
}
#[test]
fn test_chart_line() {
let chart = Chart::line();
assert_eq!(chart.get_chart_type(), ChartType::Line);
}
#[test]
fn test_chart_bar() {
let chart = Chart::bar();
assert_eq!(chart.get_chart_type(), ChartType::Bar);
}
#[test]
fn test_chart_scatter() {
let chart = Chart::scatter();
assert_eq!(chart.get_chart_type(), ChartType::Scatter);
}
#[test]
fn test_chart_area() {
let chart = Chart::area();
assert_eq!(chart.get_chart_type(), ChartType::Area);
}
#[test]
fn test_chart_pie() {
let chart = Chart::pie();
assert_eq!(chart.get_chart_type(), ChartType::Pie);
}
#[test]
fn test_chart_builder() {
let chart = Chart::new()
.chart_type(ChartType::Bar)
.series(DataSeries::new("Sales").point(1.0, 100.0))
.series(DataSeries::new("Expenses").point(1.0, 80.0))
.title("Revenue")
.x_axis(Axis::new().label("Month"))
.y_axis(Axis::new().label("Amount"))
.legend(LegendPosition::BottomRight)
.background(Color::WHITE)
.padding(50.0)
.width(600.0)
.height(400.0)
.accessible_name("Revenue chart")
.test_id("revenue-chart");
assert_eq!(chart.get_chart_type(), ChartType::Bar);
assert_eq!(chart.series_count(), 2);
assert!(chart.has_data());
assert_eq!(chart.get_title(), Some("Revenue"));
assert_eq!(Widget::accessible_name(&chart), Some("Revenue chart"));
assert_eq!(Widget::test_id(&chart), Some("revenue-chart"));
}
#[test]
fn test_chart_add_series() {
let series_list = vec![DataSeries::new("A"), DataSeries::new("B")];
let chart = Chart::new().add_series(series_list);
assert_eq!(chart.series_count(), 2);
}
#[test]
fn test_chart_data_bounds() {
let chart = Chart::new()
.series(DataSeries::new("S1").point(0.0, 10.0).point(5.0, 20.0))
.series(DataSeries::new("S2").point(1.0, 5.0).point(4.0, 25.0));
let bounds = chart.data_bounds().unwrap();
assert_eq!(bounds.0, 0.0); assert_eq!(bounds.1, 5.0); assert_eq!(bounds.2, 5.0); assert_eq!(bounds.3, 25.0); }
#[test]
fn test_chart_data_bounds_with_axis_override() {
let chart = Chart::new()
.series(DataSeries::new("S1").point(0.0, 10.0).point(5.0, 20.0))
.x_axis(Axis::new().min(-5.0).max(10.0))
.y_axis(Axis::new().min(0.0).max(50.0));
let bounds = chart.data_bounds().unwrap();
assert_eq!(bounds.0, -5.0); assert_eq!(bounds.1, 10.0); assert_eq!(bounds.2, 0.0); assert_eq!(bounds.3, 50.0); }
#[test]
fn test_chart_data_bounds_empty() {
let chart = Chart::new();
assert!(chart.data_bounds().is_none());
}
#[test]
fn test_chart_padding_min() {
let chart = Chart::new().padding(-10.0);
assert_eq!(chart.padding, 0.0);
}
#[test]
fn test_chart_width_min() {
let chart = Chart::new().width(50.0);
assert_eq!(chart.width, Some(100.0));
}
#[test]
fn test_chart_height_min() {
let chart = Chart::new().height(50.0);
assert_eq!(chart.height, Some(100.0));
}
#[test]
fn test_chart_type_id() {
let chart = Chart::new();
assert_eq!(Widget::type_id(&chart), TypeId::of::<Chart>());
}
#[test]
fn test_chart_measure_default() {
let chart = Chart::new();
let size = chart.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
assert_eq!(size.width, 400.0);
assert_eq!(size.height, 300.0);
}
#[test]
fn test_chart_measure_custom() {
let chart = Chart::new().width(600.0).height(400.0);
let size = chart.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
assert_eq!(size.width, 600.0);
assert_eq!(size.height, 400.0);
}
#[test]
fn test_chart_layout() {
let mut chart = Chart::new();
let bounds = Rect::new(10.0, 20.0, 500.0, 300.0);
let result = chart.layout(bounds);
assert_eq!(result.size, Size::new(500.0, 300.0));
assert_eq!(chart.bounds, bounds);
}
#[test]
fn test_chart_children() {
let chart = Chart::new();
assert!(chart.children().is_empty());
}
#[test]
fn test_chart_is_interactive() {
let chart = Chart::new();
assert!(!chart.is_interactive());
}
#[test]
fn test_chart_is_focusable() {
let chart = Chart::new();
assert!(!chart.is_focusable());
}
#[test]
fn test_chart_accessible_role() {
let chart = Chart::new();
assert_eq!(chart.accessible_role(), AccessibleRole::Image);
}
#[test]
fn test_chart_accessible_name_from_title() {
let chart = Chart::new().title("Sales Chart");
assert_eq!(Widget::accessible_name(&chart), Some("Sales Chart"));
}
#[test]
fn test_chart_accessible_name_explicit() {
let chart = Chart::new()
.title("Sales Chart")
.accessible_name("Custom name");
assert_eq!(Widget::accessible_name(&chart), Some("Custom name"));
}
#[test]
fn test_chart_test_id() {
let chart = Chart::new().test_id("my-chart");
assert_eq!(Widget::test_id(&chart), Some("my-chart"));
}
#[test]
fn test_chart_plot_area_no_title() {
let mut chart = Chart::new().padding(40.0);
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let plot = chart.plot_area();
assert_eq!(plot.x, 40.0);
assert_eq!(plot.y, 40.0);
assert_eq!(plot.width, 320.0);
assert_eq!(plot.height, 220.0);
}
#[test]
fn test_chart_plot_area_with_title() {
let mut chart = Chart::new().padding(40.0).title("Test");
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let plot = chart.plot_area();
assert_eq!(plot.y, 70.0); }
#[test]
fn test_chart_map_point() {
let chart = Chart::new();
let bounds = (0.0, 10.0, 0.0, 100.0);
let plot = Rect::new(0.0, 0.0, 100.0, 100.0);
let pt = chart.map_point(5.0, 50.0, &bounds, &plot);
assert!((pt.x - 50.0).abs() < 0.1);
assert!((pt.y - 50.0).abs() < 0.1);
}
#[test]
fn test_chart_map_point_origin() {
let chart = Chart::new();
let bounds = (0.0, 10.0, 0.0, 100.0);
let plot = Rect::new(0.0, 0.0, 100.0, 100.0);
let pt = chart.map_point(0.0, 0.0, &bounds, &plot);
assert!((pt.x - 0.0).abs() < 0.1);
assert!((pt.y - 100.0).abs() < 0.1); }
#[test]
fn test_chart_has_data_empty_series() {
let chart = Chart::new().series(DataSeries::new("Empty"));
assert!(!chart.has_data());
}
#[test]
fn test_chart_has_data_with_points() {
let chart = Chart::new().series(DataSeries::new("Data").point(1.0, 1.0));
assert!(chart.has_data());
}
#[test]
fn test_data_series_eq() {
let s1 = DataSeries::new("A").point(1.0, 2.0);
let s2 = DataSeries::new("A").point(1.0, 2.0);
assert_eq!(s1, s2);
}
#[test]
fn test_chart_type_eq() {
assert_eq!(ChartType::Line, ChartType::Line);
assert_ne!(ChartType::Line, ChartType::Bar);
}
#[test]
fn test_legend_position_all_variants() {
let positions = [
LegendPosition::None,
LegendPosition::TopRight,
LegendPosition::TopLeft,
LegendPosition::BottomRight,
LegendPosition::BottomLeft,
];
assert_eq!(positions.len(), 5);
}
#[test]
fn test_chart_children_mut() {
let mut chart = Chart::new();
assert!(chart.children_mut().is_empty());
}
#[test]
fn test_chart_event_returns_none() {
let mut chart = Chart::new();
let result = chart.event(&presentar_core::Event::KeyDown {
key: presentar_core::Key::Down,
});
assert!(result.is_none());
}
#[test]
fn test_axis_default_colors() {
let axis = Axis::default();
assert_eq!(axis.color.a, 1.0);
assert_eq!(axis.grid_color.a, 1.0);
}
#[test]
fn test_chart_get_series() {
let chart = Chart::new()
.series(DataSeries::new("A"))
.series(DataSeries::new("B"));
assert_eq!(chart.get_series().len(), 2);
assert_eq!(chart.get_series()[0].name, "A");
}
#[test]
fn test_chart_histogram() {
let chart = Chart::new().chart_type(ChartType::Histogram);
assert_eq!(chart.get_chart_type(), ChartType::Histogram);
}
#[test]
fn test_chart_data_bounds_single_point() {
let chart = Chart::new().series(DataSeries::new("S").point(5.0, 10.0));
let bounds = chart.data_bounds().unwrap();
assert_eq!(bounds.0, 5.0); assert_eq!(bounds.1, 5.0); }
#[test]
fn test_chart_legend_none() {
let chart = Chart::new().legend(LegendPosition::None);
assert_eq!(chart.legend, LegendPosition::None);
}
#[test]
fn test_chart_legend_top_left() {
let chart = Chart::new().legend(LegendPosition::TopLeft);
assert_eq!(chart.legend, LegendPosition::TopLeft);
}
#[test]
fn test_chart_legend_bottom_left() {
let chart = Chart::new().legend(LegendPosition::BottomLeft);
assert_eq!(chart.legend, LegendPosition::BottomLeft);
}
#[test]
fn test_chart_test_id_none() {
let chart = Chart::new();
assert!(Widget::test_id(&chart).is_none());
}
#[test]
fn test_chart_accessible_name_none() {
let chart = Chart::new();
assert!(Widget::accessible_name(&chart).is_none());
}
#[test]
fn test_data_series_default_values() {
let series = DataSeries::new("Test");
assert_eq!(series.line_width, 2.0);
assert_eq!(series.point_size, 4.0);
}
#[test]
fn test_chart_brick_name() {
let chart = Chart::new();
assert_eq!(chart.brick_name(), "Chart");
}
#[test]
fn test_chart_brick_assertions() {
let chart = Chart::new();
let assertions = chart.assertions();
assert!(!assertions.is_empty());
assert!(matches!(assertions[0], BrickAssertion::MaxLatencyMs(16)));
}
#[test]
fn test_chart_brick_budget() {
let chart = Chart::new();
let budget = chart.budget();
assert!(budget.layout_ms > 0);
assert!(budget.paint_ms > 0);
}
#[test]
fn test_chart_brick_verify() {
let chart = Chart::new();
let verification = chart.verify();
assert!(!verification.passed.is_empty());
assert!(verification.failed.is_empty());
}
#[test]
fn test_chart_brick_to_html() {
let chart = Chart::new().test_id("my-chart").title("Test Chart");
let html = chart.to_html();
assert!(html.contains("brick-chart"));
assert!(html.contains("my-chart"));
assert!(html.contains("Test Chart"));
}
#[test]
fn test_chart_brick_to_html_default() {
let chart = Chart::new();
let html = chart.to_html();
assert!(html.contains("data-testid=\"chart\""));
assert!(html.contains("aria-label=\"Chart\""));
}
#[test]
fn test_chart_brick_to_css() {
let chart = Chart::new();
let css = chart.to_css();
assert!(css.contains(".brick-chart"));
assert!(css.contains("display: block"));
}
#[test]
fn test_chart_brick_test_id() {
let chart = Chart::new().test_id("chart-1");
assert_eq!(Brick::test_id(&chart), Some("chart-1"));
}
#[test]
fn test_chart_brick_test_id_none() {
let chart = Chart::new();
assert!(Brick::test_id(&chart).is_none());
}
#[test]
fn test_chart_heatmap_constructor() {
let chart = Chart::heatmap();
assert_eq!(chart.get_chart_type(), ChartType::Heatmap);
}
#[test]
fn test_chart_boxplot_constructor() {
let chart = Chart::boxplot();
assert_eq!(chart.get_chart_type(), ChartType::BoxPlot);
}
#[test]
fn test_chart_data_bounds_with_partial_axis_override() {
let chart = Chart::new()
.series(DataSeries::new("S1").point(0.0, 10.0).point(5.0, 20.0))
.x_axis(Axis::new().min(-10.0));
let bounds = chart.data_bounds().unwrap();
assert_eq!(bounds.0, -10.0); assert_eq!(bounds.1, 5.0); }
#[test]
fn test_chart_data_bounds_only_y_axis_override() {
let chart = Chart::new()
.series(DataSeries::new("S1").point(0.0, 10.0).point(5.0, 20.0))
.y_axis(Axis::new().max(100.0));
let bounds = chart.data_bounds().unwrap();
assert_eq!(bounds.3, 100.0); }
#[test]
fn test_chart_map_point_with_zero_range() {
let chart = Chart::new();
let bounds = (5.0, 5.0, 10.0, 10.0); let plot = Rect::new(0.0, 0.0, 100.0, 100.0);
let pt = chart.map_point(5.0, 10.0, &bounds, &plot);
assert!(pt.x.is_finite());
assert!(pt.y.is_finite());
}
#[test]
fn test_chart_measure_constrained() {
let chart = Chart::new().width(800.0).height(600.0);
let size = chart.measure(Constraints::tight(Size::new(400.0, 300.0)));
assert_eq!(size.width, 400.0);
assert_eq!(size.height, 300.0);
}
#[test]
fn test_data_series_x_range_single_point() {
let series = DataSeries::new("Data").point(5.0, 10.0);
let range = series.x_range().unwrap();
assert_eq!(range.0, 5.0);
assert_eq!(range.1, 5.0);
}
#[test]
fn test_data_series_y_range_single_point() {
let series = DataSeries::new("Data").point(5.0, 10.0);
let range = series.y_range().unwrap();
assert_eq!(range.0, 10.0);
assert_eq!(range.1, 10.0);
}
#[test]
fn test_axis_new() {
let axis = Axis::new();
assert!(axis.label.is_none());
assert!(axis.min.is_none());
assert!(axis.max.is_none());
}
#[test]
fn test_chart_type_clone() {
let ct = ChartType::Histogram;
let cloned = ct;
assert_eq!(cloned, ChartType::Histogram);
}
#[test]
fn test_legend_position_eq() {
assert_eq!(LegendPosition::TopRight, LegendPosition::TopRight);
assert_ne!(LegendPosition::TopRight, LegendPosition::TopLeft);
}
#[test]
fn test_chart_with_empty_series() {
let chart = Chart::new()
.series(DataSeries::new("Empty1"))
.series(DataSeries::new("Empty2").point(1.0, 2.0));
assert!(chart.has_data()); assert_eq!(chart.series_count(), 2);
}
#[test]
fn test_chart_multiple_series_data_bounds() {
let chart = Chart::new()
.series(DataSeries::new("S1").point(-5.0, 0.0).point(0.0, 10.0))
.series(DataSeries::new("S2").point(0.0, -10.0).point(10.0, 50.0));
let bounds = chart.data_bounds().unwrap();
assert_eq!(bounds.0, -5.0); assert_eq!(bounds.1, 10.0); assert_eq!(bounds.2, -10.0); assert_eq!(bounds.3, 50.0); }
#[test]
fn test_chart_legend_bottom_right() {
let chart = Chart::new().legend(LegendPosition::BottomRight);
assert_eq!(chart.legend, LegendPosition::BottomRight);
}
#[test]
fn test_chart_background_setter() {
let chart = Chart::new().background(Color::BLACK);
assert_eq!(chart.background, Color::BLACK);
}
#[test]
fn test_data_series_clone() {
let series = DataSeries::new("Test").point(1.0, 2.0);
let cloned = series.clone();
assert_eq!(cloned.name, "Test");
assert_eq!(cloned.points.len(), 1);
}
#[test]
fn test_axis_clone() {
let axis = Axis::new().label("X").min(0.0).max(100.0);
let cloned = axis.clone();
assert_eq!(cloned.label, Some("X".to_string()));
assert_eq!(cloned.min, Some(0.0));
assert_eq!(cloned.max, Some(100.0));
}
#[test]
fn test_chart_clone() {
let chart = Chart::new()
.title("Test")
.series(DataSeries::new("S1").point(1.0, 2.0));
let cloned = chart.clone();
assert_eq!(cloned.get_title(), Some("Test"));
assert_eq!(cloned.series_count(), 1);
}
#[test]
fn test_chart_type_debug() {
let ct = ChartType::Pie;
let debug_str = format!("{:?}", ct);
assert!(debug_str.contains("Pie"));
}
#[test]
fn test_legend_position_debug() {
let lp = LegendPosition::TopRight;
let debug_str = format!("{:?}", lp);
assert!(debug_str.contains("TopRight"));
}
#[test]
fn test_data_series_debug() {
let series = DataSeries::new("Test");
let debug_str = format!("{:?}", series);
assert!(debug_str.contains("Test"));
}
#[test]
fn test_axis_debug() {
let axis = Axis::new().label("Time");
let debug_str = format!("{:?}", axis);
assert!(debug_str.contains("Time"));
}
#[test]
fn test_chart_debug() {
let chart = Chart::new().title("Debug Test");
let debug_str = format!("{:?}", chart);
assert!(debug_str.contains("Debug Test"));
}
use presentar_core::RecordingCanvas;
#[test]
fn test_chart_paint_empty() {
let mut chart = Chart::new();
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(!canvas.commands().is_empty());
}
#[test]
fn test_chart_paint_with_title() {
let mut chart = Chart::new().title("My Chart");
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(canvas.commands().len() >= 2);
}
#[test]
fn test_chart_paint_line_chart() {
let mut chart = Chart::line().series(
DataSeries::new("Data")
.point(0.0, 0.0)
.point(5.0, 10.0)
.point(10.0, 5.0),
);
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(canvas.commands().len() > 5);
}
#[test]
fn test_chart_paint_line_chart_no_points() {
let mut chart = Chart::line().series(
DataSeries::new("Data")
.point(0.0, 0.0)
.point(5.0, 10.0)
.show_points(false),
);
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(!canvas.commands().is_empty());
}
#[test]
fn test_chart_paint_area_chart() {
let mut chart = Chart::area().series(
DataSeries::new("Data")
.point(0.0, 0.0)
.point(5.0, 10.0)
.point(10.0, 5.0)
.fill(true),
);
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(canvas.commands().len() > 5);
}
#[test]
fn test_chart_paint_bar_chart() {
let mut chart = Chart::bar()
.series(DataSeries::new("A").point(1.0, 10.0).point(2.0, 20.0))
.series(DataSeries::new("B").point(1.0, 15.0).point(2.0, 25.0));
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(canvas.commands().len() > 5);
}
#[test]
fn test_chart_paint_bar_chart_empty_series() {
let mut chart = Chart::bar();
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(!canvas.commands().is_empty());
}
#[test]
fn test_chart_paint_scatter_chart() {
let mut chart = Chart::scatter().series(
DataSeries::new("Points")
.point(1.0, 2.0)
.point(3.0, 4.0)
.point(5.0, 6.0),
);
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(canvas.commands().len() > 5);
}
#[test]
fn test_chart_paint_pie_chart() {
let mut chart = Chart::pie()
.series(DataSeries::new("Slice1").point(0.0, 30.0))
.series(DataSeries::new("Slice2").point(0.0, 50.0))
.series(DataSeries::new("Slice3").point(0.0, 20.0));
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(canvas.commands().len() > 3);
}
#[test]
fn test_chart_paint_pie_chart_zero_total() {
let mut chart = Chart::pie().series(DataSeries::new("Zero").point(0.0, 0.0));
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(!canvas.commands().is_empty());
}
#[test]
fn test_chart_paint_histogram() {
let mut chart = Chart::new().chart_type(ChartType::Histogram).series(
DataSeries::new("Data")
.point(1.0, 5.0)
.point(2.0, 10.0)
.point(3.0, 7.0),
);
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(canvas.commands().len() > 5);
}
#[test]
fn test_chart_paint_heatmap() {
let mut chart = Chart::heatmap()
.series(
DataSeries::new("Row1")
.point(0.0, 10.0)
.point(1.0, 20.0)
.point(2.0, 30.0),
)
.series(
DataSeries::new("Row2")
.point(0.0, 15.0)
.point(1.0, 25.0)
.point(2.0, 35.0),
);
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(canvas.commands().len() > 5);
}
#[test]
fn test_chart_paint_heatmap_empty() {
let mut chart = Chart::heatmap();
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(!canvas.commands().is_empty());
}
#[test]
fn test_chart_paint_boxplot() {
let mut chart = Chart::boxplot().series(
DataSeries::new("Stats")
.point(0.0, 1.0)
.point(0.0, 2.0)
.point(0.0, 3.0)
.point(0.0, 4.0)
.point(0.0, 5.0)
.point(0.0, 6.0)
.point(0.0, 7.0),
);
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(canvas.commands().len() > 5);
}
#[test]
fn test_chart_paint_boxplot_insufficient_points() {
let mut chart = Chart::boxplot().series(
DataSeries::new("TooFew")
.point(0.0, 1.0)
.point(0.0, 2.0)
.point(0.0, 3.0),
);
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(!canvas.commands().is_empty());
}
#[test]
fn test_chart_paint_boxplot_empty_series() {
let mut chart = Chart::boxplot();
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(!canvas.commands().is_empty());
}
#[test]
fn test_chart_paint_legend_top_right() {
let mut chart = Chart::new()
.legend(LegendPosition::TopRight)
.series(DataSeries::new("Series A").point(1.0, 2.0))
.series(DataSeries::new("Series B").point(2.0, 3.0));
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(canvas.commands().len() > 5);
}
#[test]
fn test_chart_paint_legend_top_left() {
let mut chart = Chart::new()
.legend(LegendPosition::TopLeft)
.series(DataSeries::new("Data").point(1.0, 2.0));
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(canvas.commands().len() > 5);
}
#[test]
fn test_chart_paint_legend_bottom_right() {
let mut chart = Chart::new()
.legend(LegendPosition::BottomRight)
.series(DataSeries::new("Data").point(1.0, 2.0));
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(canvas.commands().len() > 5);
}
#[test]
fn test_chart_paint_legend_bottom_left() {
let mut chart = Chart::new()
.legend(LegendPosition::BottomLeft)
.series(DataSeries::new("Data").point(1.0, 2.0));
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(canvas.commands().len() > 5);
}
#[test]
fn test_chart_paint_legend_none() {
let mut chart = Chart::new()
.legend(LegendPosition::None)
.series(DataSeries::new("Data").point(1.0, 2.0));
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(!canvas.commands().is_empty());
}
#[test]
fn test_chart_paint_legend_empty_series() {
let mut chart = Chart::new().legend(LegendPosition::TopRight);
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(!canvas.commands().is_empty());
}
#[test]
fn test_chart_paint_grid_hidden() {
let mut chart = Chart::new()
.x_axis(Axis::new().show_grid(false))
.y_axis(Axis::new().show_grid(false))
.series(DataSeries::new("Data").point(0.0, 0.0).point(10.0, 10.0));
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(!canvas.commands().is_empty());
}
#[test]
fn test_chart_paint_line_single_point() {
let mut chart = Chart::line().series(DataSeries::new("Single").point(5.0, 10.0));
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(!canvas.commands().is_empty());
}
#[test]
fn test_chart_paint_multiple_series_line() {
let mut chart = Chart::line()
.series(DataSeries::new("A").point(0.0, 0.0).point(5.0, 10.0))
.series(DataSeries::new("B").point(0.0, 5.0).point(5.0, 15.0))
.series(DataSeries::new("C").point(0.0, 10.0).point(5.0, 20.0));
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(canvas.commands().len() > 10);
}
#[test]
fn test_paint_grid_labels() {
let mut chart = Chart::new()
.x_axis(Axis::new().grid_lines(3))
.y_axis(Axis::new().grid_lines(4))
.series(DataSeries::new("Data").point(0.0, 0.0).point(10.0, 100.0));
chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(canvas.commands().len() > 5);
}
#[test]
fn test_chart_paint_with_all_options() {
let mut chart = Chart::new()
.chart_type(ChartType::Line)
.title("Full Chart")
.series(
DataSeries::new("Main")
.point(0.0, 0.0)
.point(5.0, 50.0)
.point(10.0, 30.0)
.color(Color::RED)
.line_width(3.0)
.point_size(6.0)
.show_points(true)
.fill(true),
)
.x_axis(Axis::new().label("X").min(-5.0).max(15.0).grid_lines(4))
.y_axis(Axis::new().label("Y").min(-10.0).max(60.0).grid_lines(5))
.legend(LegendPosition::TopRight)
.background(Color::WHITE)
.padding(50.0);
chart.bounds = Rect::new(0.0, 0.0, 500.0, 400.0);
let mut canvas = RecordingCanvas::new();
chart.paint(&mut canvas);
assert!(canvas.commands().len() > 15);
}
}