Merge pull request #438 from MarekSpodBiedry/issue-428

Scatter charts of two unlabelled columns plot the first as X for one series (#428)
This commit is contained in:
Brandon Thomas
2026-10-11 18:05:11 -04:00
3 changed files with 40 additions and 1 deletions
+1
View File
@@ -421,6 +421,7 @@ fn select_data(s: &mut Session, p: &Json) -> Result<Json> {
with_chart(s, p, move |c| {
if let Some((series, source, by_rows)) = rebuilt {
c.series = series;
super::insert::scatter_x_from_first(c.kind, &mut c.series);
c.source = Some(source);
c.by_rows = by_rows;
}
+14 -1
View File
@@ -573,6 +573,17 @@ pub fn series_from_range(sh: &Sheet, r: RangeRef, by_rows: bool) -> Vec<Series>
out
}
/// A scatter plot of a block without category labels takes its first column (or row) as the X
/// values of the others, as Excel does: two plain columns make one series of points, not two (#428).
pub fn scatter_x_from_first(kind: ChartKind, series: &mut Vec<Series>) {
if matches!(kind, ChartKind::Scatter | ChartKind::ScatterLines) && series.len() > 1 && series[0].categories.is_none() {
let x = series.remove(0).values;
for se in series.iter_mut() {
se.categories = Some(x.clone());
}
}
}
/// Most cells a chart's source block may span (bounds the series it builds).
const MAX_CHART_CELLS: u64 = 1_000_000;
@@ -661,10 +672,11 @@ pub fn build_chart(s: &Session, p: &Json) -> Result<(usize, Chart)> {
let data_w = r.width();
let by_rows = data_w > data_h + 1;
let kind = chart_kind(&t, &sub);
let series = series_from_range(sh, r, by_rows);
let mut series = series_from_range(sh, r, by_rows);
if series.is_empty() {
return Err(EngineError::Other("Select data for the chart first.".into()));
}
scatter_x_from_first(kind, &mut series);
let id = d.wb.next_object_id();
let at = cell_param_on(s, p, "at", sheet)?.unwrap_or_else(|| CellRef::new(r.start.row, r.end.col + 2));
let title = str_param(p, "title").map(str::to_string).or_else(|| {
@@ -801,6 +813,7 @@ fn chart_switch(s: &mut Session, p: &Json) -> Result<Json> {
c.source.get_or_insert(src);
c.by_rows = by_rows;
c.series = series;
scatter_x_from_first(c.kind, &mut c.series);
}
Ok(Json::Null)
})
+25
View File
@@ -1554,6 +1554,31 @@ fn every_chart_kind_survives_xlsx() {
}
}
#[test]
fn scatter_of_two_columns_is_one_series_with_the_first_as_x() {
let mut s = s();
s.execute("range.setValues", json!({"range": "A1", "values": [["X", "Y"], [1, 10], [2, 20], [3, 30]]})).unwrap();
s.execute("insert.chart", json!({"type": "scatter", "range": "A1:B4"})).unwrap();
let c = s.doc().unwrap().wb.active().unwrap().charts[0].clone();
assert_eq!(c.series.len(), 1);
assert_eq!(c.series[0].values, "Sheet1!$B$2:$B$4");
assert_eq!(c.series[0].categories.as_deref(), Some("Sheet1!$A$2:$A$4"));
// Select Data on the scatter chart and Switch Row/Column rebuild the series the same way.
s.execute("chart.selectData", json!({"range": "A2:B4"})).unwrap();
let c = s.doc().unwrap().wb.active().unwrap().charts[0].clone();
assert_eq!(c.series.len(), 1);
assert_eq!(c.series[0].categories.as_deref(), Some("Sheet1!$A$2:$A$4"));
s.execute("range.setValues", json!({"range": "D1", "values": [[1, 2, 3, 4], [10, 20, 30, 40]]})).unwrap();
s.execute("chart.selectData", json!({"range": "D1:G2"})).unwrap();
s.execute("chart.switchRowColumn", json!({})).unwrap();
s.execute("chart.switchRowColumn", json!({})).unwrap();
let c = s.doc().unwrap().wb.active().unwrap().charts[0].clone();
assert!(c.by_rows);
assert_eq!(c.series.len(), 1);
assert_eq!(c.series[0].values, "Sheet1!$D$2:$G$2");
assert_eq!(c.series[0].categories.as_deref(), Some("Sheet1!$D$1:$G$1"));
}
#[test]
fn whole_sheet_selection_stays_within_the_data() {
// On an empty sheet with everything selected these filled or scanned every cell of the