Charts: scatter and bubble charts with text X values number the points

A scatter or bubble chart whose X values are text (a first column of
labels) drew no points, as none of its X values were numbers. Like
Excel, such X values are now ignored and the points numbered 1..n.
This commit is contained in:
nihilist-narwhal 2026-10-09 23:31:26 -05:00
parent fddd08bec3
commit bf1381253f
3 changed files with 26 additions and 2 deletions

View File

@ -153,7 +153,8 @@ pub fn resolve(wb: &Workbook, sheet: usize, chart: &Chart) -> ChartData {
s.color.as_ref().and_then(|c| c.resolve(&wb.theme)).map(|[r, g, b]| [r, g, b, 0xFF]).unwrap_or_else(|| series_color(&wb.theme, i));
let kind = effective_kind(chart.kind, s.kind, i);
let x = match (&s.categories, numeric_x || matches!(kind, ChartKind::Scatter | ChartKind::ScatterLines | ChartKind::Bubble)) {
(Some(f), true) => Some(nums(wb, sheet, f)),
// X values with text in them (labels) are ignored: the points are numbered 1..n.
(Some(f), true) => Some(eval_list(wb, sheet, f)).filter(|xs| !xs.iter().any(Value::is_text)).map(|xs| xs.iter().map(to_num).collect()),
_ => None,
};
if categories.is_none()

View File

@ -314,6 +314,29 @@ fn palette_beyond_six_series() {
assert_ne!(p[6], p[12]);
}
#[test]
fn scatter_with_text_x_values_numbers_the_points() {
let mut wb = Workbook::new();
let s = wb.sheet_mut(0).unwrap();
for (i, (label, v)) in [("North", 3.0), ("South", 5.0), ("East", 4.0)].iter().enumerate() {
s.set_value(CellRef::new(i as u32, 0), Value::from(*label));
s.set_value(CellRef::new(i as u32, 1), Value::Number(*v));
s.set_value(CellRef::new(i as u32, 2), Value::Number(i as f64 * 10.0));
}
for kind in [ChartKind::Scatter, ChartKind::ScatterLines, ChartKind::Bubble] {
let mut c = chart(kind);
c.series = vec![Series { categories: Some("Sheet1!$A$1:$A$3".into()), values: "Sheet1!$B$1:$B$3".into(), ..Default::default() }];
let d = resolve(&wb, 0, &c);
assert_eq!(d.series[0].x, None, "{kind:?}");
let prims = render(&c, &d, 400.0, 300.0, &ApproxMeasure);
let empty = render(&c, &ChartData { categories: vec![], series: vec![] }, 400.0, 300.0, &ApproxMeasure);
assert!(prims.len() > empty.len() + 2, "{kind:?} draws its points");
// Numeric X values are kept.
c.series[0].categories = Some("Sheet1!$C$1:$C$3".into());
assert_eq!(resolve(&wb, 0, &c).series[0].x, Some(vec![Some(0.0), Some(10.0), Some(20.0)]));
}
}
fn pixel(img: &[u8], w: u32, x: u32, y: u32) -> [u8; 4] {
let i = ((y * w + x) * 4) as usize;
[img[i], img[i + 1], img[i + 2], img[i + 3]]

View File

@ -340,7 +340,7 @@ pub enum LegendPos {
Right,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Series {
/// Formula text for the series name (`Sheet1!$B$1`) or a literal.
pub name: Option<String>,