Charts: Switch Row/Column on charts read from a file

chart.switchRowColumn did nothing on a chart read from a file: only
charts made by insert.chart keep their source range. Without one, the
command now takes the block the series' names, categories and values
span (when they're all on one sheet), and keeps it.
This commit is contained in:
nihilist-narwhal 2026-10-09 23:31:26 -05:00
parent 13664fe66b
commit fddd08bec3
2 changed files with 53 additions and 2 deletions

View File

@ -654,14 +654,16 @@ fn chart_switch(s: &mut Session, p: &Json) -> Result<Json> {
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<Json> {
})
}
/// 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<String> = None;
let mut bounds: Option<RangeRef> = 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<Json> {
let (si, ci) = find_chart(s, p)?;
edit(s, |cx| {

View File

@ -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();