Browse / github.com/storytold/gridcraft / patch
Charts plot visible cells only; Show data in hidden rows and columns round-trips (#2523)
Charts plot visible cells only; Show data in hidden rows and columns round-trips (#2523) Charts now plot visible cells only, as Excel does by default, and the "Show data in hidden rows and columns" option is read from files and written back (#2523). ## What changed - `Chart` has a `show_hidden_data` field (default off, which is Excel's `plotVisOnly` on). It is the only model change, so it should merge cleanly with the open chart-format PRs. - `chart::resolve` leaves out cells in hidden rows (hand-hidden or filtered) and hidden columns for values, categories, scatter X values and bubble sizes. A series whose cells are all hidden leaves the chart. Multi-level category labels keep t
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Harrison-Booth (imported from pull request #2671, open)
- Base
11bb67386c9bb19b0717e21e98adc608162ae2e5- Size
- +201 −31 in 20 files
- Review
- pending
- Shared
- 2026-10-28
- Id
153bcde085bcdd0e907cf471
How it went
No other operator has reported using it yet.
Diff
raw · crates/chart/src/resolve.rs crates/chart/src/tests.rs crates/engine/src/cmd/extra.rs crates/engine/src/cmd/extra/chart.rs crates/engine/src/cmd/forecast.rs crates/engine/src/cmd/insert/chart.rs crates/engine/src/cmd/pivot/charts.rs crates/engine/src/tests.rs crates/model/src/features.rs crates/ui-egui/src/dialogs.rs crates/ui-egui/src/dialogs/charts.rs crates/xlsx/src/chart.rs crates/xlsx/src/chartex.rs crates/xlsx/src/tests/chart_book_names.rs crates/xlsx/src/tests/fixtures.rs crates/xlsx/src/tests/literal_category_roundtrip.rs crates/xlsx/src/tests/multilevel_category_roundtrip.rs crates/xlsx/src/tests/roundtrip.rs crates/xlsx/src/tests/smooth_roundtrip.rs crates/xlsx/src/tests/trendline_roundtrip.rs
@@ -77,10 +77,22 @@ fn union_parts(f: &str) -> Option<Vec<&str>> { /// Evaluates a series formula to a flat list of values (row-major), capped. A union of /// references (`(A1:A3,A5)`, as PivotCharts use) is read part by part. fn eval_list(wb: &Workbook, sheet: usize, formula: &str) -> Vec<Value> {- eval_list_in(wb, sheet, formula, 0)+ eval_list_in(wb, sheet, formula, 0, false) } -fn eval_list_in(wb: &Workbook, sheet: usize, formula: &str, depth: u32) -> Vec<Value> {+/// Whether the cell at flat (row-major) position `i` of `r` on `sh` is plotted: its row and+/// column are not hidden (by hand or by a filter).+fn is_visible(wb: &Workbook, sh: usize, r: &RangeRef, i: usize) -> bool {+ let w = r.width().max(1) as usize;+ let Some(sheet) = wb.sheet(sh) else { return true };+ let at = r.start.offset((i / w) as i64, (i % w) as i64);+ at.is_none_or(|c| !sheet.is_row_hidden(c.row) && !sheet.is_col_hidden(c.col))+}++/// `eval_list`, leaving out the cells of hidden rows and columns when `hide` is set (Excel's+/// "Show data in hidden rows and columns" off). Only plain references are filtered: arrays,+/// names and other formulas have no cells to hide.+fn eval_list_in(wb: &Workbook, sheet: usize, formula: &str, depth: u32, hide: bool) -> Vec<Value> { let f = formula.trim(); if f.is_empty() { return vec![];@@ -94,13 +106,23 @@ fn eval_list_in(wb: &Workbook, sheet: usize, formula: &str, depth: u32) -> Vec<V break; } let rest = MAX_POINTS - out.len();- out.extend(eval_list_in(wb, sheet, p, depth + 1).into_iter().take(rest));+ out.extend(eval_list_in(wb, sheet, p, depth + 1, hide).into_iter().take(rest)); } return out; }+ let cells = if hide { parse_ref(wb, sheet, f) } else { None }; match evaluate(wb, sheet, CellRef::default(), f) {- Value::Array(a) => a.iter().take(MAX_POINTS).cloned().collect(),- v => vec![v],+ Value::Array(a) => match cells {+ Some((sh, r)) if a.data.len() == (r.width() as usize).saturating_mul(r.height() as usize) => {+ a.iter().enumerate().filter(|(i, _)| is_visible(wb, sh, &r, *i)).map(|(_, v)| v.clone()).take(MAX_POINTS).collect()+ }+ _ => a.iter().take(MAX_POINTS).cloned().collect(),+ },+ // A single cell: hidden, it is not plotted.+ v => match cells {+ Some((sh, r)) if !is_visible(wb, sh, &r, 0) => vec![],+ _ => vec![v],+ }, } } @@ -111,8 +133,8 @@ fn to_num(v: &Value) -> Option<f64> { } } -fn nums(wb: &Workbook, sheet: usize, formula: &str) -> Vec<Option<f64>> {- eval_list(wb, sheet, formula).iter().map(to_num).collect()+fn nums(wb: &Workbook, sheet: usize, formula: &str, hide: bool) -> Vec<Option<f64>> {+ eval_list_in(wb, sheet, formula, 0, hide).iter().map(to_num).collect() } /// Number format code of the first cell of a reference ("General" when not a reference).@@ -137,9 +159,11 @@ fn value_text(v: &Value) -> String { /// the outer levels of a multi-level axis: a category range wider than a column of values (or /// taller than a row of values) groups its innermost labels, year beside month. Each outer level /// has one entry per category, non-empty where a group starts, as the cells are.-fn category_table(wb: &Workbook, sheet: usize, formula: &str, values: &str) -> (Vec<String>, Vec<Vec<String>>) {- let vals = eval_list(wb, sheet, formula);+fn category_table(wb: &Workbook, sheet: usize, formula: &str, values: &str, hide: bool) -> (Vec<String>, Vec<Vec<String>>) { let cells: Option<(usize, RangeRef)> = parse_ref(wb, sheet, formula);+ // A plain range is read whole and its hidden rows and columns dropped at the end, so the+ // number formats and multi-level groups still line up with the cells.+ let vals = eval_list_in(wb, sheet, formula, 0, hide && cells.is_none()); let src = cells.and_then(|(sh, r)| wb.sheet(sh).map(|s| (s, r))); let texts: Vec<String> = vals .iter()@@ -158,22 +182,32 @@ fn category_table(wb: &Workbook, sheet: usize, formula: &str, values: &str) -> ( value_text(v) }) .collect();- let Some((_, r)) = src else { return (texts, vec![]) };+ let Some((sheet_ref, r)) = src else { return (texts, vec![]) };+ let visible_row = |row: usize| !(hide && sheet_ref.is_row_hidden(r.start.row.saturating_add(row as u32)));+ let visible_col = |col: usize| !(hide && sheet_ref.is_col_hidden(r.start.col.saturating_add(col as u32)));+ let w = r.width().max(1) as usize;+ let flat = |texts: Vec<String>| -> Vec<String> {+ texts.into_iter().enumerate().filter(|(i, _)| visible_row(i / w) && visible_col(i % w)).map(|(_, t)| t).collect()+ }; let value_range = parse_ref(wb, sheet, values).map(|(_, r)| r);- let Some(levels) = category_levels(&r, value_range.as_ref()) else { return (texts, vec![]) };- let (w, h) = (r.width().max(1) as usize, r.height().max(1) as usize);+ let Some(levels) = category_levels(&r, value_range.as_ref()) else { return (flat(texts), vec![]) };+ let h = r.height().max(1) as usize; if texts.len() != w.saturating_mul(h) {- return (texts, vec![]); // truncated or not a plain block: a flat list+ return (flat(texts), vec![]); // truncated or not a plain block: a flat list } let at = |row: usize, col: usize| texts.get(row * w + col).cloned().unwrap_or_default();- let column = |c: usize| (0..h).map(|row| at(row, c)).collect::<Vec<String>>();- let row = |rr: usize| (0..w).map(|c| at(rr, c)).collect::<Vec<String>>();+ let column = |c: usize| (0..h).filter(|&row| visible_row(row)).map(|row| at(row, c)).collect::<Vec<String>>();+ let row = |rr: usize| (0..w).filter(|&c| visible_col(c)).map(|c| at(rr, c)).collect::<Vec<String>>(); let outer = levels.outer as usize;- if levels.by_columns {- (column(w - 1), (1..=outer).filter_map(|k| (w - 1).checked_sub(k)).map(column).collect())+ // The innermost level is the last visible column (row), the outer levels the ones before it.+ let (lines, list): (Vec<usize>, &dyn Fn(usize) -> Vec<String>) = if levels.by_columns {+ ((0..w).filter(|&c| visible_col(c)).collect(), &column) } else {- (row(h - 1), (1..=outer).filter_map(|k| (h - 1).checked_sub(k)).map(row).collect())- }+ ((0..h).filter(|&r| visible_row(r)).collect(), &row)+ };+ let Some(&inner) = lines.last() else { return (vec![], vec![]) };+ let outers = (1..=outer).filter_map(|k| lines.len().checked_sub(k + 1)).filter_map(|i| lines.get(i)).map(|&l| list(l)).collect();+ (list(inner), outers) } /// Whether `t` names a defined name seen from `sheet` (`Name`, or `Sheet1!Name` looked up on@@ -228,27 +262,34 @@ pub fn resolve(wb: &Workbook, sheet: usize, chart: &Chart) -> ChartData { let mut series = Vec::new(); let mut categories: Option<Vec<String>> = None; let mut category_levels = Vec::new();+ let hide = !chart.show_hidden_data; for (i, s) in chart.series.iter().take(MAX_SERIES).enumerate() {- let values = nums(wb, sheet, &s.values);+ let values = nums(wb, sheet, &s.values, hide);+ // A series whose cells are all hidden (its column hidden) leaves the chart, legend included.+ if hide && values.is_empty() && !nums(wb, sheet, &s.values, false).is_empty() {+ continue;+ } let name = s.name.as_deref().map(|n| series_name(wb, sheet, n)).filter(|n| !n.is_empty()).unwrap_or_else(|| format!("Series{}", i + 1)); let color = 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)) { // 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()),+ (Some(f), true) => {+ Some(eval_list_in(wb, sheet, f, 0, hide)).filter(|xs| !xs.iter().any(Value::is_text)).map(|xs| xs.iter().map(to_num).collect())+ } _ => None, }; if categories.is_none() && let Some(f) = &s.categories {- let (c, outer) = category_table(wb, sheet, f, &s.values);+ let (c, outer) = category_table(wb, sheet, f, &s.values, hide); if !c.is_empty() { categories = Some(c); category_levels = outer; } }- let sizes = s.bubble_sizes.as_deref().map(|f| nums(wb, sheet, f));+ let sizes = s.bubble_sizes.as_deref().map(|f| nums(wb, sheet, f, hide)); let trendlines = s .trendlines .iter()@@ -50,6 +50,7 @@ fn chart(kind: ChartKind) -> Chart { source: None, by_rows: false, pivot: None,+ show_hidden_data: false, } } @@ -955,6 +956,64 @@ fn histogram_boundary_values_match_interval_labels() { assert!((h[0] / h[1] - 3.0).abs() < 0.01 && (h[1] - h[2]).abs() < 0.01, "{h:?}"); } +#[test]+fn hidden_rows_and_columns_are_left_out_unless_shown() {+ use gridcraft_model::LineInfo;+ let mut wb = Workbook::new();+ {+ let s = wb.sheet_mut(0).unwrap();+ for r in 0..4u32 {+ s.set_value(CellRef::new(r, 0), Value::from(["a", "b", "c", "d"][r as usize]));+ s.set_value(CellRef::new(r, 1), Value::Number(f64::from(r) + 1.0));+ s.set_value(CellRef::new(r, 2), Value::Number(f64::from(r) * 10.0));+ }+ s.rows.insert(1, LineInfo { hidden: true, ..Default::default() });+ }+ let mut c = chart(ChartKind::ColumnClustered);+ c.series = vec![+ Series { categories: Some("Sheet1!$A$1:$A$4".into()), values: "Sheet1!$B$1:$B$4".into(), ..Series::default() },+ Series { categories: Some("Sheet1!$A$1:$A$4".into()), values: "Sheet1!$C$1:$C$4".into(), ..Series::default() },+ ];+ let d = resolve(&wb, 0, &c);+ assert_eq!(d.series[0].values, vec![Some(1.0), Some(3.0), Some(4.0)]);+ assert_eq!(d.categories, vec!["a", "c", "d"]);+ // A hidden column removes its series, legend entry included.+ wb.sheet_mut(0).unwrap().cols.insert(2, LineInfo { hidden: true, ..Default::default() });+ assert_eq!(resolve(&wb, 0, &c).series.len(), 1);+ c.show_hidden_data = true;+ let d = resolve(&wb, 0, &c);+ assert_eq!(d.series.len(), 2);+ assert_eq!(d.series[0].values.len(), 4);+ assert_eq!(d.categories.len(), 4);+}++#[test]+fn hidden_cells_leave_multi_level_categories() {+ use gridcraft_model::LineInfo;+ let mut wb = Workbook::new();+ {+ let s = wb.sheet_mut(0).unwrap();+ for (r, (y, m)) in [("2023", "jan"), ("", "feb"), ("2024", "jan"), ("", "feb")].iter().enumerate() {+ if !y.is_empty() {+ s.set_value(CellRef::new(r as u32, 0), Value::from(*y));+ }+ s.set_value(CellRef::new(r as u32, 1), Value::from(*m));+ s.set_value(CellRef::new(r as u32, 2), Value::Number(r as f64));+ }+ s.rows.insert(1, LineInfo { hidden: true, ..Default::default() });+ }+ let mut c = chart(ChartKind::Line);+ c.series = vec![Series { categories: Some("Sheet1!$A$1:$B$4".into()), values: "Sheet1!$C$1:$C$4".into(), ..Series::default() }];+ let d = resolve(&wb, 0, &c);+ assert_eq!(d.categories, vec!["jan", "jan", "feb"]);+ assert_eq!(d.category_levels, vec![vec!["2023".to_string(), "2024".into(), String::new()]]);+ // The innermost level is the last visible column.+ wb.sheet_mut(0).unwrap().cols.insert(1, LineInfo { hidden: true, ..Default::default() });+ let d = resolve(&wb, 0, &c);+ assert_eq!(d.categories, vec!["2023", "2024", ""]);+ assert!(d.category_levels.is_empty());+}+ #[test] fn union_series_references() { let mut wb = Workbook::new();@@ -51,7 +51,7 @@ pub fn specs() -> Vec<CommandSpec> { "Select Data…", ["Chart Design", "Data"], None,- "{chart?, range?: \"Sheet1!A1:D7\", byRows?: bool, categories?: \"Sheet1!A2:A7\" (Horizontal (Category) Axis Labels for every series; \"\" clears), removeSeries?: [index…]}",+ "{chart?, range?: \"Sheet1!A1:D7\", byRows?: bool, categories?: \"Sheet1!A2:A7\" (Horizontal (Category) Axis Labels for every series; \"\" clears), removeSeries?: [index…], showHiddenData?: bool (Hidden and Empty Cells ▸ Show data in hidden rows and columns; default off: visible cells only)}", has_doc, select_data ),@@ -205,8 +205,9 @@ pub(super) fn select_data(s: &mut Session, p: &Json) -> Result<Json> { .unwrap_or_default(); remove.sort_unstable(); remove.dedup();- if rebuilt.is_none() && categories.is_none() && remove.is_empty() {- return Err(bad("chart.selectData", "missing `range`, `categories` or `removeSeries`"));+ let show_hidden = bool_param(p, "showHiddenData");+ if rebuilt.is_none() && categories.is_none() && remove.is_empty() && show_hidden.is_none() {+ return Err(bad("chart.selectData", "missing `range`, `categories`, `removeSeries` or `showHiddenData`")); } with_chart(s, p, move |c| { if let Some((series, source, by_rows)) = rebuilt {@@ -220,6 +221,9 @@ pub(super) fn select_data(s: &mut Session, p: &Json) -> Result<Json> { c.series.remove(*i); } }+ if let Some(v) = show_hidden {+ c.show_hidden_data = v;+ } if let Some(cats) = categories { for se in &mut c.series { se.categories = cats.clone();@@ -431,6 +431,7 @@ fn forecast_sheet(s: &mut Session, p: &Json) -> Result<Json> { source: None, by_rows: false, pivot: None,+ show_hidden_data: false, }); cx.wb.sheets.insert(at, Arc::new(sheet)); for nm in cx.wb.names.iter_mut() {@@ -264,6 +264,7 @@ pub fn build_chart(s: &Session, p: &Json) -> Result<(usize, Chart)> { source: (areas.len() == 1).then(|| format!("{}!{}", gridcraft_formula::quote_sheet(&sh.name), bounds.a1())), by_rows, pivot: None,+ show_hidden_data: false, }; Ok((sheet, chart)) }@@ -69,6 +69,7 @@ pub(super) fn insert_pivot_chart(s: &mut Session, p: &Json) -> Result<Json> { source: None, by_rows: false, pivot: Some(link),+ show_hidden_data: false, }; edit(s, |cx| { cx.sheet_mut(si)?.charts.push(chart);@@ -2974,6 +2974,44 @@ fn chart_data(s: &mut Session) { .unwrap(); } +/// The values and category labels a chart on the active sheet plots.+fn plotted(s: &Session) -> (Vec<Option<f64>>, Vec<String>) {+ let d = s.doc().unwrap();+ let data = gridcraft_chart::resolve(&d.wb, d.wb.active_sheet(), &d.wb.active().unwrap().charts[0]);+ (data.series[0].values.clone(), data.categories)+}++/// Excel plots visible cells only: filtered and hand-hidden rows leave the chart, and "Show data+/// in hidden rows and columns" (`c:plotVisOnly` off) brings them back, through a file round trip too.+#[test]+fn chart_plots_visible_cells_only() {+ let mut s = s();+ s.execute("range.setValues", json!({"range": "A1", "values": [["Name", "Score"], ["a", 1], ["b", 2], ["c", 3], ["d", 4], ["e", 5]]})).unwrap();+ s.execute("insert.chart", json!({"type": "column", "range": "A1:B6", "at": "D2"})).unwrap();+ let all = |v: &[f64]| v.iter().map(|n| Some(*n)).collect::<Vec<_>>();+ assert_eq!(plotted(&s).0, all(&[1.0, 2.0, 3.0, 4.0, 5.0]));+ // Rows hidden by a filter.+ s.execute("selection.set", json!({"cell": "B2"})).unwrap();+ s.execute("data.filter", json!({})).unwrap();+ s.execute("data.filterBy", json!({"column": "B", "custom": {"op": ">", "value": "2"}})).unwrap();+ assert_eq!(plotted(&s), (all(&[3.0, 4.0, 5.0]), vec!["c".to_string(), "d".into(), "e".into()]));+ // The option brings them back, and survives the file.+ let id = s.doc().unwrap().wb.active().unwrap().charts[0].id;+ s.execute("chart.selectData", json!({"chart": id, "showHiddenData": true})).unwrap();+ assert_eq!(plotted(&s).0, all(&[1.0, 2.0, 3.0, 4.0, 5.0]));+ reopen(&mut s);+ assert!(s.doc().unwrap().wb.active().unwrap().charts[0].show_hidden_data);+ assert_eq!(plotted(&s).0.len(), 5);+ s.execute("chart.selectData", json!({"chart": id, "showHiddenData": false})).unwrap();+ reopen(&mut s);+ assert!(!s.doc().unwrap().wb.active().unwrap().charts[0].show_hidden_data);+ assert_eq!(plotted(&s).0, all(&[3.0, 4.0, 5.0]));+ // A row hidden by hand, with the filter cleared.+ s.execute("data.filter", json!({})).unwrap();+ s.execute("home.hideRows", json!({"rows": "4:4"})).unwrap();+ assert_eq!(plotted(&s).1, ["a", "b", "d", "e"]);+}+ #[test] fn every_chart_kind_survives_xlsx() { use gridcraft_model::ChartKind;@@ -560,6 +560,11 @@ pub struct Chart { /// whenever the PivotTable is refreshed or its layout changes. #[serde(default)] pub pivot: Option<String>,+ /// Select Data ▸ Hidden and Empty Cells ▸ Show data in hidden rows and columns (`c:plotVisOnly`+ /// off). Off (the default, as in Excel) plots visible cells only: rows hidden by hand or by a+ /// filter, and hidden columns, leave the chart.+ #[serde(default)]+ pub show_hidden_data: bool, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]@@ -929,12 +929,13 @@ impl Dialog { let si = doc.wb.active_sheet(); let sh = doc.wb.sheet(si)?; let ch = sh.charts.iter().find(|c| Some(u64::from(c.id)) == id)?;- let data = gridcraft_chart::resolve(&doc.wb, si, ch);+ // Hidden series included, so the names line up with `Chart.series` (`removeSeries` indices).+ let data = gridcraft_chart::resolve(&doc.wb, si, &gridcraft_engine::model::Chart { show_hidden_data: true, ..ch.clone() }); let names: Vec<String> = data.series.iter().map(|x| x.name.clone()).collect(); let cats = ch.series.first().and_then(|x| x.categories.clone()).unwrap_or_default();- Some((ch.source.clone().unwrap_or_default(), cats, names))+ Some((ch.source.clone().unwrap_or_default(), cats, names, ch.show_hidden_data)) });- let Some((range, cats, names)) = found else {+ let Some((range, cats, names, hidden)) = found else { app.message = Some(("Select Data".into(), "Select a chart first.".into())); return None; };@@ -942,7 +943,7 @@ impl Dialog { Dialog::custom( "selectData", "Select Data Source",- json!({"chart": id, "range": eq(&range), "range0": eq(&range), "categories": eq(&cats), "categories0": eq(&cats), "names": names, "remove": []}),+ json!({"chart": id, "range": eq(&range), "range0": eq(&range), "categories": eq(&cats), "categories0": eq(&cats), "names": names, "remove": [], "hidden": hidden, "hidden0": hidden}), ) } "journal" => Dialog::custom("journal", "Action Journal", json!({})),@@ -160,6 +160,12 @@ pub(super) fn select_data(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, d.values.insert("remove".into(), json!(remove)); ui.add_space(6.0); edit(ui, d, "categories", "Horizontal (Category) Axis Labels:");+ ui.add_space(6.0);+ let flag = |d: &Dialog, k: &str| d.values.get(k).and_then(Json::as_bool).unwrap_or(false);+ let mut hidden = flag(d, "hidden");+ if ui.checkbox(&mut hidden, "Show data in hidden rows and columns").changed() {+ d.values.insert("hidden".into(), json!(hidden));+ } ok_cancel(ui, confirm, open); if *confirm { *confirm = false;@@ -175,6 +181,9 @@ pub(super) fn select_data(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, if text(d, "categories") != text(d, "categories0") || (range_changed && !text(d, "categories").is_empty()) { params.insert("categories".into(), json!(text(d, "categories"))); }+ if flag(d, "hidden") != flag(d, "hidden0") {+ params.insert("showHiddenData".into(), json!(flag(d, "hidden")));+ } if params.len() == 1 { *open = false; return;@@ -275,6 +275,7 @@ pub fn read_chart(root: &El) -> Option<Chart> { source: None, by_rows: false, pivot: root.path(&["pivotSource", "name"]).map(|n| strip_book(n.text.trim()).to_string()).filter(|n| !n.is_empty()),+ show_hidden_data: chart.child("plotVisOnly").is_some_and(|e| !e.flag("val", true)), }) } @@ -877,6 +878,6 @@ pub fn write_chart(wb: &Workbook, si: usize, ch: &Chart, book_name: &str) -> Str if let Some(p) = pos { let _ = write!(s, "<c:legend><c:legendPos val=\"{p}\"/><c:overlay val=\"0\"/></c:legend>"); }- s.push_str("<c:plotVisOnly val=\"1\"/><c:dispBlanksAs val=\"gap\"/></c:chart></c:chartSpace>");+ let _ = write!(s, "<c:plotVisOnly val=\"{}\"/><c:dispBlanksAs val=\"gap\"/></c:chart></c:chartSpace>", u8::from(!ch.show_hidden_data)); s }@@ -137,6 +137,7 @@ pub fn read_chartex(root: &El) -> Option<Chart> { source: None, by_rows: false, pivot: None,+ show_hidden_data: false, }) } @@ -82,6 +82,7 @@ fn chart_names_are_written_qualified() { source: None, by_rows: false, pivot: None,+ show_hidden_data: false, }); wb.sheets = vec![Arc::new(s), Arc::new(Sheet::new("My Data"))]; wb.names = vec![@@ -543,6 +543,7 @@ fn chartex_parts_as_excel_expects_them() { source: None, by_rows: false, pivot: None,+ show_hidden_data: false, }); wb.sheets = vec![std::sync::Arc::new(sheet)]; let bytes = crate::write_xlsx(&wb).unwrap();@@ -48,6 +48,7 @@ fn literal_category_with_separators_survives_save() { source: None, by_rows: false, pivot: None,+ show_hidden_data: false, }); wb.sheets = vec![Arc::new(s)]; let bytes = write_xlsx(&wb).unwrap();@@ -48,6 +48,7 @@ fn workbook(categories: &str, values: &str) -> Workbook { source: None, by_rows: false, pivot: None,+ show_hidden_data: false, }); Workbook { sheets: vec![Arc::new(s)], ..Workbook::default() } }@@ -414,6 +414,7 @@ fn sample() -> Workbook { source: None, by_rows: false, pivot: None,+ show_hidden_data: false, }; for kind in [ ChartKind::ColumnClustered,@@ -36,6 +36,7 @@ fn smooth_line_setting_round_trips() { source: None, by_rows: false, pivot: None,+ show_hidden_data: false, }); let mut smooth_series = base_series.clone(); smooth_series.smooth = true;@@ -54,6 +55,7 @@ fn smooth_line_setting_round_trips() { source: None, by_rows: false, pivot: None,+ show_hidden_data: false, }); wb.sheets = vec![Arc::new(s)]; let bytes = write_xlsx(&wb).unwrap();@@ -60,6 +60,7 @@ fn chart(id: u32, kind: ChartKind, trendlines: Vec<Trendline>) -> Chart { source: None, by_rows: false, pivot: None,+ show_hidden_data: false, } }