mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-03 00:41:56 -06:00
Add command and UI controls for chart data tables, shared chart and PDF rendering, and native XLSX persistence. Cover live values, blanks, formatting, alignment, undo/redo, bounds and table flags with regression tests. Co-Authored-By: Codex <noreply@openai.com>
122 lines
4.9 KiB
Rust
122 lines
4.9 KiB
Rust
use super::*;
|
|
use gridcraft_model::ChartDataTable;
|
|
|
|
#[test]
|
|
fn tables_draw_source_values_for_every_supported_chart_kind() {
|
|
for kind in ALL_KINDS {
|
|
let mut c = chart(kind);
|
|
c.legend = LegendPos::None;
|
|
c.data_labels = false;
|
|
c.data_table = Some(ChartDataTable::default());
|
|
let supported = c.supports_data_table();
|
|
let d = sample(kind);
|
|
let prims = render(&c, &d, 900.0, 500.0, &ApproxMeasure);
|
|
assert_sane(&prims, 900.0, 500.0);
|
|
let labels = texts(&prims);
|
|
// Source amounts, including on 100% stacked charts, not shares or running totals.
|
|
if supported {
|
|
for value in ["$12,000", "-$4,000", "$38,000", "$61,870"] {
|
|
assert!(labels.contains(&value), "{kind:?}: missing {value}");
|
|
}
|
|
} else {
|
|
c.data_table = None;
|
|
assert_eq!(prims, render(&c, &d, 900.0, 500.0, &ApproxMeasure));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn table_columns_align_with_plot_and_keep_missing_values_blank() {
|
|
let mut c = chart(ChartKind::ColumnClustered);
|
|
c.title = Some(String::new());
|
|
c.x_title = None;
|
|
c.y_title = None;
|
|
c.legend = LegendPos::None;
|
|
c.data_labels = false;
|
|
c.data_table = Some(ChartDataTable::default());
|
|
let color = [1, 2, 3, 255];
|
|
let mut s = series("Rate", &[0.25, 0.0, 0.75], color, c.kind);
|
|
s.values[1] = None;
|
|
s.number_format = "0.0%".into();
|
|
let d = ChartData { categories: ["Jan", "Feb", "Mar"].map(String::from).to_vec(), series: vec![s], ..Default::default() };
|
|
let p = render(&c, &d, 600.0, 400.0, &ApproxMeasure);
|
|
let at = |text: &str| -> (f32, f32) {
|
|
p.iter()
|
|
.find_map(|p| match p {
|
|
Prim::Text { text: t, x, y, .. } if t == text => Some((*x, *y)),
|
|
_ => None,
|
|
})
|
|
.unwrap()
|
|
};
|
|
let (x1, y1) = at("25.0%");
|
|
let (x3, y3) = at("75.0%");
|
|
let header_y = at("Jan").1;
|
|
assert!(y1 > header_y && (y1 - y3).abs() < 0.01);
|
|
assert_eq!(at("Jan").0, x1);
|
|
assert_eq!(at("Mar").0, x3);
|
|
assert_eq!(texts(&p).iter().filter(|t| **t == "Jan").count(), 1, "the table replaces category-axis labels");
|
|
let centers: Vec<f32> = p
|
|
.iter()
|
|
.filter_map(|p| match p {
|
|
Prim::Rect { x, y, w, fill: Some(f), .. } if *f == color && *y < header_y => Some(x + w / 2.0),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
assert_eq!(centers.len(), 2);
|
|
assert!((centers[0] - x1).abs() < 0.01 && (centers[1] - x3).abs() < 0.01);
|
|
let cells: Vec<&str> = p
|
|
.iter()
|
|
.filter_map(|p| match p {
|
|
Prim::Text { y, text, .. } if (*y - y1).abs() < 0.01 => Some(text.as_str()),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
assert_eq!(cells, ["Rate", "25.0%", "75.0%"], "blank cells are not zeroes");
|
|
assert!(
|
|
p.iter().any(|p| matches!(p, Prim::Rect { y, fill: Some(f), .. } if *f == color && *y > header_y)),
|
|
"legend key independent of hidden chart legend"
|
|
);
|
|
c.data_table = Some(ChartDataTable { show_keys: false, ..Default::default() });
|
|
let off = render(&c, &d, 600.0, 400.0, &ApproxMeasure);
|
|
assert!(!off.iter().any(|p| matches!(p, Prim::Rect { y, fill: Some(f), .. } if *f == color && *y > header_y)));
|
|
}
|
|
|
|
#[test]
|
|
fn table_borders_are_independent_and_multilevel_headers_are_kept() {
|
|
let mut c = chart(ChartKind::Line);
|
|
c.legend = LegendPos::None;
|
|
c.data_labels = false;
|
|
let mut d = sample(c.kind);
|
|
d.category_levels = vec![vec!["2025".into(), "".into(), "".into(), "2026".into(), "".into(), "".into()]];
|
|
for (h, v, outline, want) in [(false, false, false, 0), (true, false, false, 4), (false, true, false, 6), (false, false, true, 4)] {
|
|
c.data_table = Some(ChartDataTable { horizontal_border: h, vertical_border: v, outline, show_keys: false });
|
|
let p = render(&c, &d, 900.0, 600.0, &ApproxMeasure);
|
|
let year_y = p
|
|
.iter()
|
|
.find_map(|p| match p {
|
|
Prim::Text { text, y, .. } if text == "2025" => Some(*y),
|
|
_ => None,
|
|
})
|
|
.unwrap();
|
|
assert!(texts(&p).contains(&"2026"));
|
|
let lines = p.iter().filter(|p| matches!(p, Prim::Line { pts, .. } if pts.iter().all(|p| p[1] > year_y - 9.0))).count();
|
|
assert_eq!(lines, want, "{h} {v} {outline}");
|
|
assert_sane(&p, 900.0, 600.0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn dense_and_tiny_tables_are_bounded_and_explicit() {
|
|
let mut c = chart(ChartKind::ColumnClustered);
|
|
c.data_table = Some(ChartDataTable::default());
|
|
let mut d = sample(c.kind);
|
|
d.categories = (0..20_000).map(|i| i.to_string()).collect();
|
|
let p = render(&c, &d, 600.0, 400.0, &ApproxMeasure);
|
|
assert!(texts(&p).contains(&"Data table needs more space"));
|
|
assert_sane(&p, 600.0, 400.0);
|
|
d.categories.clear();
|
|
for (w, h) in [(1.0, 1.0), (10.0, 10.0), (40.0, 35.0), (200.0, 100.0)] {
|
|
assert_sane(&render(&c, &d, w, h, &ApproxMeasure), w, h);
|
|
}
|
|
}
|