diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs index e4b5bc9d1..b922b4c25 100644 --- a/crates/engine/src/cmd/insert.rs +++ b/crates/engine/src/cmd/insert.rs @@ -654,14 +654,16 @@ fn chart_switch(s: &mut Session, p: &Json) -> Result { edit(s, |cx| { let Some(sh) = cx.wb.sheet(si) else { return Ok(Json::Null) }; let Some(c) = sh.charts.get(ci) else { return Ok(Json::Null) }; - let Some(src) = c.source.clone() else { return Ok(Json::Null) }; + // Charts read from a file have no source range: take the block their series span. + let Some((src, by_rows)) = c.source.clone().map(|s| (s, c.by_rows)).or_else(|| series_block(&c.series)) else { return Ok(Json::Null) }; let (sheet_name, body) = split_sheet(&src); let src_sheet = sheet_name.and_then(|n| cx.wb.sheet_index(&n)).unwrap_or(si); let Some(r) = RangeRef::parse(body) else { return Ok(Json::Null) }; let Some(ssh) = cx.wb.sheet(src_sheet) else { return Ok(Json::Null) }; - let by_rows = !c.by_rows; + let by_rows = !by_rows; let series = series_from_range(ssh, r, by_rows); if let Some(c) = cx.sheet_mut(si)?.charts.get_mut(ci) { + c.source.get_or_insert(src); c.by_rows = by_rows; c.series = series; } @@ -669,6 +671,29 @@ fn chart_switch(s: &mut Session, p: &Json) -> Result { }) } +/// The block a chart's series come from, and whether each series is a row of it: the bounding +/// range of the series names, categories and values, when they're all references to one sheet. +fn series_block(series: &[Series]) -> Option<(String, bool)> { + let mut sheet: Option = None; + let mut bounds: Option = None; + let mut rows = 0; + for s in series { + for (i, f) in [Some(&s.values), s.categories.as_ref(), s.name.as_ref()].into_iter().flatten().enumerate() { + let Ok(gridcraft_formula::Expr::Ref(r)) = gridcraft_formula::parse(f) else { return None }; + let gridcraft_formula::SheetSel::Named(n) = &r.sheet else { return None }; + if !sheet.get_or_insert_with(|| n.clone()).eq_ignore_ascii_case(n) { + return None; + } + let range = r.range(); + if i == 0 && range.height() == 1 && range.width() > 1 { + rows += 1; + } + bounds = Some(bounds.map_or(range, |b| b.union(&range))); + } + } + Some((format!("{}!{}", gridcraft_formula::quote_sheet(&sheet?), bounds?.a1()), rows > 0 && rows == series.len())) +} + fn chart_delete(s: &mut Session, p: &Json) -> Result { let (si, ci) = find_chart(s, p)?; edit(s, |cx| { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index f3e36cdf4..6a01542ed 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -174,6 +174,32 @@ fn parity_counts() { assert!(done > 150, "parity {done}/{total}"); } +#[test] +fn switch_row_column_on_a_chart_read_from_a_file() { + let mut s = s(); + s.execute( + "range.setValues", + json!({"range": "A1", "values": [["", "North", "South", "East", "West"], ["Mon", 10, 14, 9, 12], ["Tue", 12, 15, 11, 14], ["Wed", 14, 16, 10, 11], ["Thu", 11, 13, 8, 9], ["Fri", 9, 12, 7, 11], ["Sat", 11, 15, 10, 14]]}), + ) + .unwrap(); + s.execute("insert.chart", json!({"type": "column", "range": "A1:E7"})).unwrap(); + let r = s.execute("file.saveBytes", json!({"format": "xlsx"})).unwrap(); + let b64 = r["base64"].as_str().unwrap().to_string(); + s.execute("file.open", json!({"name": "x.xlsx", "base64": b64})).unwrap(); + let chart = |s: &Session| s.doc().unwrap().wb.active().unwrap().charts[0].clone(); + assert_eq!((chart(&s).source, chart(&s).series.len()), (None, 4)); + s.execute("chart.switchRowColumn", json!({})).unwrap(); + let c = chart(&s); + assert!(c.by_rows); + assert_eq!(c.source.as_deref(), Some("Sheet1!A1:E7")); + assert_eq!(c.series.len(), 6); + assert_eq!(c.series[0].name.as_deref(), Some("Sheet1!$A$2")); + assert_eq!(c.series[0].categories.as_deref(), Some("Sheet1!$B$1:$E$1")); + assert_eq!(c.series[0].values, "Sheet1!$B$2:$E$2"); + s.execute("chart.switchRowColumn", json!({})).unwrap(); + assert_eq!(chart(&s).series.len(), 4); +} + #[test] fn ink_strokes_and_ink_to_shape() { let mut s = s();