#![cfg(not(target_arch = "wasm32"))]
use chartml_core::data::{DataTable, Row};
use chartml_core::element::{ChartElement, ViewBox};
use chartml_core::error::ChartError;
use chartml_core::plugin::{ChartConfig, ChartRenderer};
use chartml_core::ChartML;
use chartml_datafusion::DataFusionTransform;
use serde_json::json;
use std::sync::{Arc, Mutex};
fn make_row(pairs: Vec<(&str, serde_json::Value)>) -> Row {
pairs
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect()
}
fn visitors_table() -> DataTable {
let rows = vec![
make_row(vec![("date", json!("2024-01-01")), ("n", json!(100.0))]),
make_row(vec![("date", json!("2024-01-02")), ("n", json!(150.0))]),
make_row(vec![("date", json!("2024-01-03")), ("n", json!(200.0))]),
];
DataTable::from_rows(&rows).unwrap()
}
fn sessions_table() -> DataTable {
let rows = vec![
make_row(vec![("date", json!("2024-01-01")), ("n", json!(10.0))]),
make_row(vec![("date", json!("2024-01-02")), ("n", json!(15.0))]),
make_row(vec![("date", json!("2024-01-03")), ("n", json!(20.0))]),
];
DataTable::from_rows(&rows).unwrap()
}
const JOIN_YAML: &str = r#"
type: chart
version: 1
title: Visitors and Sessions
data:
visitors:
rows: []
sessions:
rows: []
transform:
sql: |
SELECT v.date, v.n AS visitors, s.n AS sessions
FROM visitors v JOIN sessions s USING (date)
ORDER BY v.date
visualize:
type: bar
columns: date
rows: visitors
"#;
struct CapturingRenderer {
captured: Arc<Mutex<Option<DataTable>>>,
}
impl ChartRenderer for CapturingRenderer {
fn render(&self, data: &DataTable, _config: &ChartConfig) -> Result<ChartElement, ChartError> {
*self.captured.lock().unwrap() = Some(data.clone());
Ok(ChartElement::Svg {
viewbox: ViewBox::new(0.0, 0.0, 800.0, 400.0),
width: Some(800.0),
height: Some(400.0),
class: "captured".to_string(),
children: vec![],
})
}
}
fn assert_join_result(captured: &Mutex<Option<DataTable>>) {
let captured = captured.lock().unwrap();
let table = captured
.as_ref()
.expect("Renderer must have been called with the joined data");
assert_eq!(table.num_rows(), 3, "Joined output should have 3 rows");
let rows = table.to_rows();
assert_eq!(
rows[0].get("visitors").and_then(|v| v.as_f64()),
Some(100.0),
);
assert_eq!(
rows[0].get("sessions").and_then(|v| v.as_f64()),
Some(10.0),
);
assert_eq!(
rows[2].get("visitors").and_then(|v| v.as_f64()),
Some(200.0),
);
assert_eq!(
rows[2].get("sessions").and_then(|v| v.as_f64()),
Some(20.0),
);
}
fn build_chartml(captured: Arc<Mutex<Option<DataTable>>>) -> ChartML {
let mut chartml = ChartML::new();
chartml.register_renderer("bar", CapturingRenderer { captured });
chartml.register_transform(DataFusionTransform);
chartml.register_source("visitors", visitors_table());
chartml.register_source("sessions", sessions_table());
chartml
}
#[tokio::test]
async fn named_map_sources_join_via_async_render() {
let captured: Arc<Mutex<Option<DataTable>>> = Arc::new(Mutex::new(None));
let chartml = build_chartml(captured.clone());
let result = chartml
.render_from_yaml_with_params_async(JOIN_YAML, None, None, None)
.await;
assert!(
result.is_ok(),
"NamedMap multi-source render (async) should succeed; got: {:?}",
result.err(),
);
assert_join_result(&captured);
}
#[test]
fn named_map_sources_join_via_sync_render() {
let captured: Arc<Mutex<Option<DataTable>>> = Arc::new(Mutex::new(None));
let chartml = build_chartml(captured.clone());
let result = chartml.render_from_yaml(JOIN_YAML);
assert!(
result.is_ok(),
"NamedMap multi-source render (sync) should succeed; got: {:?}",
result.err(),
);
assert_join_result(&captured);
}
#[tokio::test]
async fn named_map_multi_no_transform_errors() {
let captured: Arc<Mutex<Option<DataTable>>> = Arc::new(Mutex::new(None));
let chartml = build_chartml(captured);
let yaml = r#"
type: chart
version: 1
title: No transform
data:
visitors:
rows: []
sessions:
rows: []
visualize:
type: bar
columns: date
rows: n
"#;
let result = chartml
.render_from_yaml_with_params_async(yaml, None, None, None)
.await;
assert!(result.is_err(), "Multi-source without transform must error");
let err = result.unwrap_err().to_string();
assert!(
err.contains("transform") || err.contains("multiple sources"),
"Error should explain that a transform is required; got: {}",
err,
);
}