diff --git a/Cargo.toml b/Cargo.toml index 1ede2d39..93201808 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,14 @@ criterion = { version = "0.7", features = ["html_reports"] } name = "basic" harness = false +[[bench]] +name = "style" +harness = false + +[[bench]] +name = "conditional_formatting" +harness = false + # Example that requires the `picture` feature. [[example]] name = "read_picture_data" diff --git a/benches/conditional_formatting.rs b/benches/conditional_formatting.rs new file mode 100644 index 00000000..95490750 --- /dev/null +++ b/benches/conditional_formatting.rs @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: MIT +// +// Copyright 2016-2025, Johann Tuffe. + +//! Benchmarks for conditional formatting parsing. +//! +//! Run with: +//! ```bash +//! cargo bench --bench conditional_formatting +//! ``` + +use calamine::{open_workbook, open_workbook_from_rs, Xlsx}; +use criterion::{criterion_group, criterion_main, Criterion}; +use std::fs::File; +use std::hint::black_box; +use std::io::{BufReader, Cursor, Write}; +use zip::write::SimpleFileOptions; +use zip::ZipWriter; + +const SMALL_FILE: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/conditional_formatting.xlsx" +); + +fn build_large_cf_xlsx() -> Vec { + let mut buf = Vec::new(); + let mut zip = ZipWriter::new(Cursor::new(&mut buf)); + let opts = SimpleFileOptions::default(); + + zip.start_file("[Content_Types].xml", opts).unwrap(); + write!( + zip, + r#" + + + + + + +"# + ) + .unwrap(); + + zip.start_file("_rels/.rels", opts).unwrap(); + write!( + zip, + r#" + + +"# + ) + .unwrap(); + + zip.start_file("xl/_rels/workbook.xml.rels", opts).unwrap(); + write!( + zip, + r#" + + + +"# + ) + .unwrap(); + + zip.start_file("xl/workbook.xml", opts).unwrap(); + write!( + zip, + r#" + + +"# + ) + .unwrap(); + + zip.start_file("xl/styles.xml", opts).unwrap(); + let mut styles = String::from( + r#" + + + + + + "#, + ); + for i in 0u8..100 { + let r = i.wrapping_mul(37); + let g = i.wrapping_mul(73); + let b = i.wrapping_mul(131); + styles.push_str(&format!( + r#""# + )); + } + styles.push_str(""); + write!(zip, "{styles}").unwrap(); + + zip.start_file("xl/worksheets/sheet1.xml", opts).unwrap(); + let mut sheet = String::from( + r#" + +1"#, + ); + + // 200 CF blocks, each with 5 rules -> 1000 rules total + for block in 0u32..200 { + let row_start = block * 10 + 1; + let row_end = row_start + 9; + let priority_base = block * 5 + 1; + let dxf = (block % 100) as u8; + sheet.push_str(&format!( + r#""# + )); + + sheet.push_str(&format!( + r#"50"#, + priority_base + )); + sheet.push_str(&format!( + r#""#, + priority_base + 1 + )); + sheet.push_str(&format!( + r#""#, + priority_base + 2 + )); + sheet.push_str(&format!( + r#""#, + priority_base + 3 + )); + sheet.push_str(&format!( + r#"NOT(ISERROR(SEARCH("test",A{row_start})))"#, + (dxf + 1) % 100, + priority_base + 4 + )); + + sheet.push_str(""); + } + + sheet.push_str(""); + write!(zip, "{sheet}").unwrap(); + + zip.finish().unwrap(); + buf +} + +fn bench_cf_small(c: &mut Criterion) { + c.bench_function("cf_small_25_blocks", |b| { + b.iter(|| { + let mut wb: Xlsx> = + open_workbook(SMALL_FILE).expect("cannot open file"); + black_box(wb.worksheet_conditional_formatting("Sheet1").unwrap()) + }) + }); +} + +fn bench_cf_large(c: &mut Criterion) { + let bytes = build_large_cf_xlsx(); + + c.bench_function("cf_large_200_blocks_1000_rules", |b| { + b.iter(|| { + let cursor = Cursor::new(&bytes); + let mut wb: Xlsx>> = + open_workbook_from_rs(cursor).expect("cannot open file"); + black_box(wb.worksheet_conditional_formatting("Sheet1").unwrap()) + }) + }); +} + +fn bench_cf_dxf_resolution(c: &mut Criterion) { + let bytes = build_large_cf_xlsx(); + + c.bench_function("cf_large_with_dxf_access", |b| { + b.iter(|| { + let cursor = Cursor::new(&bytes); + let mut wb: Xlsx>> = + open_workbook_from_rs(cursor).expect("cannot open file"); + let cfs = wb.worksheet_conditional_formatting("Sheet1").unwrap(); + let mut format_count = 0usize; + for cf in &cfs { + for rule in &cf.rules { + if rule.format.is_some() { + format_count += 1; + } + } + } + black_box(format_count) + }) + }); +} + +criterion_group!( + benches, + bench_cf_small, + bench_cf_large, + bench_cf_dxf_resolution +); +criterion_main!(benches); diff --git a/benches/style.rs b/benches/style.rs new file mode 100644 index 00000000..523c616a --- /dev/null +++ b/benches/style.rs @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +// +// Copyright 2016-2025, Johann Tuffe. + +//! Benchmarks for style parsing and extraction features. +//! +//! Uses `tests/styles_1M.xlsx` (1M styled cells) for realistic performance +//! measurement. +//! +//! ## Run benchmarks +//! +//! ```bash +//! cargo bench --bench style +//! ``` +//! +//! ## Profiling (identify bottlenecks) +//! +//! Install samply (cross-platform, works on macOS and Linux): +//! ```bash +//! cargo install samply +//! ``` +//! +//! Profile a specific benchmark: +//! ```bash +//! samply record cargo bench --bench style -- "style/worksheet_style" --profile-time 5 +//! ``` +//! +//! This opens Firefox Profiler with an interactive flamegraph showing where time is spent. + +use calamine::{open_workbook, Reader, Xlsx}; +use criterion::{criterion_group, criterion_main, Criterion, SamplingMode}; +use std::fs::File; +use std::hint::black_box; +use std::io::BufReader; +use std::time::Duration; + +const LARGE_FILE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/styles_1M.xlsx"); + +fn configure(c: &mut Criterion) -> criterion::BenchmarkGroup<'_, criterion::measurement::WallTime> { + let mut group = c.benchmark_group("style"); + group.sample_size(10); + group.warm_up_time(Duration::from_millis(100)); + group.measurement_time(Duration::from_secs(15)); // Accommodate slowest benchmark (~1.2s × 10) + group.sampling_mode(SamplingMode::Flat); // 1 iteration per sample for slow benchmarks + group +} + +fn bench_style_parsing(c: &mut Criterion) { + if !std::path::Path::new(LARGE_FILE).exists() { + eprintln!("ERROR: tests/styles_1M.xlsx not found."); + return; + } + + let mut group = configure(c); + + // Core style parsing + group.bench_function("worksheet_style", |b| { + b.iter(|| { + let mut excel: Xlsx> = + open_workbook(LARGE_FILE).expect("cannot open file"); + black_box(excel.worksheet_style("Sheet 1").unwrap()) + }) + }); + + // Layout parsing (column widths, row heights) + group.bench_function("worksheet_layout", |b| { + b.iter(|| { + let mut excel: Xlsx> = + open_workbook(LARGE_FILE).expect("cannot open file"); + black_box(excel.worksheet_layout("Sheet 1").unwrap()) + }) + }); + + // Range parsing (cell values only, no styles) + group.bench_function("worksheet_range", |b| { + b.iter(|| { + let mut excel: Xlsx> = + open_workbook(LARGE_FILE).expect("cannot open file"); + black_box(excel.worksheet_range("Sheet 1").unwrap()) + }) + }); + + // Combined range + style (common real-world usage) + group.bench_function("range_and_style", |b| { + b.iter(|| { + let mut excel: Xlsx> = + open_workbook(LARGE_FILE).expect("cannot open file"); + let range = excel.worksheet_range("Sheet 1").unwrap(); + let style = excel.worksheet_style("Sheet 1").unwrap(); + black_box((range.cells().count(), style.cells().count())) + }) + }); + + // Cell-by-cell iteration via cells_reader + group.bench_function("cells_reader", |b| { + b.iter(|| { + let mut excel: Xlsx> = + open_workbook(LARGE_FILE).expect("cannot open file"); + let mut reader = excel.worksheet_cells_reader("Sheet 1").unwrap(); + let mut count = 0usize; + while let Ok(Some(_)) = reader.next_cell() { + count += 1; + } + black_box(count) + }) + }); + + // Iterate and access ALL style properties + group.bench_function("iterate_all_properties", |b| { + b.iter(|| { + let mut excel: Xlsx> = + open_workbook(LARGE_FILE).expect("cannot open file"); + let styles = excel.worksheet_style("Sheet 1").unwrap(); + let mut count = 0usize; + for (_, _, style) in styles.cells() { + if style.get_font().is_some() { + count += 1; + } + if style.get_fill().is_some() { + count += 1; + } + if style.borders.is_some() { + count += 1; + } + if style.get_alignment().is_some() { + count += 1; + } + if style.get_number_format().is_some() { + count += 1; + } + } + black_box(count) + }) + }); + + group.finish(); +} + +criterion_group!(benches, bench_style_parsing); +criterion_main!(benches); diff --git a/examples/README.md b/examples/README.md index 89b109f8..6f03b3b9 100644 --- a/examples/README.md +++ b/examples/README.md @@ -16,6 +16,12 @@ This directory contains some example of Calamine usage. - `read_hyperlinks.rs`: Reads the hyperlinks defined in an XLSX worksheet, either by sheet name or by sheet index. - `read_picture_data.rs`: Reads pictures and their metadata from an XLSX file. +- `read_row_and_column_dimensions.rs`: Reads the column widths and row heights + (the worksheet layout) from an XLSX file. +- `read_charts.rs`: Reads the charts embedded in an XLSX worksheet (chart + types, titles, series data, formatting and 3D view settings). +- `conditional_formatting.rs`: Reads the conditional formatting rules defined + in an XLSX worksheet. ### Serialization examples diff --git a/examples/conditional_formatting.rs b/examples/conditional_formatting.rs new file mode 100644 index 00000000..43bf349c --- /dev/null +++ b/examples/conditional_formatting.rs @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: MIT +// +// Copyright 2016-2025, Johann Tuffe. + +use calamine::{open_workbook, ConditionalFormatRuleType, Reader, Xlsx}; + +/// Example demonstrating how to read conditional formatting rules from Excel files. +fn main() -> Result<(), Box> { + let path = format!( + "{}/tests/conditional_formatting.xlsx", + env!("CARGO_MANIFEST_DIR") + ); + let mut workbook: Xlsx<_> = open_workbook(path)?; + + let sheet_names = workbook.sheet_names(); + let Some(sheet_name) = sheet_names.first() else { + println!("No sheets found"); + return Ok(()); + }; + + println!("Conditional formatting for sheet: {sheet_name}\n"); + + let cfs = workbook.worksheet_conditional_formatting(sheet_name)?; + + for (i, cf) in cfs.iter().enumerate() { + println!( + "Block {}: range \"{}\" ({} rule(s))", + i + 1, + cf.sqref, + cf.rules.len() + ); + + for rule in &cf.rules { + print!(" [priority={}] ", rule.priority); + + match &rule.rule_type { + ConditionalFormatRuleType::CellIs { operator, formulas } => { + println!("CellIs {:?} {:?}", operator, formulas); + } + ConditionalFormatRuleType::ColorScale2 { + min, + max, + min_color, + max_color, + } => { + println!( + "2-Color Scale: {:?}({:?}) {} -> {:?}({:?}) {}", + min.value_type, min.value, min_color, max.value_type, max.value, max_color, + ); + } + ConditionalFormatRuleType::ColorScale3 { + min, + mid, + max, + min_color, + mid_color, + max_color, + } => { + println!( + "3-Color Scale: {:?}({:?}) {} -> {:?}({:?}) {} -> {:?}({:?}) {}", + min.value_type, + min.value, + min_color, + mid.value_type, + mid.value, + mid_color, + max.value_type, + max.value, + max_color, + ); + } + ConditionalFormatRuleType::DataBar { + min, + max, + fill_color, + .. + } => { + println!( + "DataBar: {:?}({:?}) -> {:?}({:?}), color={:?}", + min.value_type, min.value, max.value_type, max.value, fill_color, + ); + } + ConditionalFormatRuleType::IconSet { + icon_type, + thresholds, + reversed, + show_value, + } => { + println!( + "IconSet {:?} ({} thresholds, reversed={}, show_value={})", + icon_type, + thresholds.len(), + reversed, + show_value, + ); + } + ConditionalFormatRuleType::Top10 { + rank, + percent, + bottom, + } => { + let direction = if *bottom { "Bottom" } else { "Top" }; + let unit = if *percent { "%" } else { "" }; + println!("{direction} {rank}{unit}"); + } + ConditionalFormatRuleType::AboveAverage { + above_average, + equal_average, + std_dev, + } => { + let dir = if *above_average { "Above" } else { "Below" }; + let eq = if *equal_average { " or equal to" } else { "" }; + print!("{dir}{eq} average"); + if *std_dev > 0 { + print!(" (std_dev={})", std_dev); + } + println!(); + } + ConditionalFormatRuleType::Text { operator, text, .. } => { + println!("Text {:?} \"{}\"", operator, text); + } + ConditionalFormatRuleType::TimePeriod { period, .. } => { + println!("TimePeriod {:?}", period); + } + ConditionalFormatRuleType::Expression { formula } => { + println!("Expression: {formula}"); + } + ConditionalFormatRuleType::DuplicateValues => println!("DuplicateValues"), + ConditionalFormatRuleType::UniqueValues => println!("UniqueValues"), + ConditionalFormatRuleType::ContainsBlanks { .. } => println!("ContainsBlanks"), + ConditionalFormatRuleType::NotContainsBlanks { .. } => { + println!("NotContainsBlanks") + } + ConditionalFormatRuleType::ContainsErrors { .. } => println!("ContainsErrors"), + ConditionalFormatRuleType::NotContainsErrors { .. } => { + println!("NotContainsErrors") + } + ConditionalFormatRuleType::Unknown { raw_type } => { + println!("Unknown type: {raw_type}"); + } + } + + if let Some(fmt) = &rule.format { + if let Some(font) = &fmt.font { + if font.is_bold() { + print!(" format: bold"); + } + if let Some(color) = font.color { + print!(" font_color: {color}"); + } + println!(); + } + if let Some(fill) = &fmt.fill { + if let Some(color) = fill.get_color() { + println!(" fill_color: {color}"); + } + } + } + } + println!(); + } + + println!("Total: {} conditional formatting blocks", cfs.len()); + Ok(()) +} diff --git a/examples/excel_to_csv.rs b/examples/excel_to_csv.rs index b2d41f49..96f4897a 100644 --- a/examples/excel_to_csv.rs +++ b/examples/excel_to_csv.rs @@ -68,6 +68,7 @@ fn write_to_csv(output_file: &mut W, range: &Range) -> std::io:: Data::String(s) | Data::DateTimeIso(s) | Data::DurationIso(s) => { write!(output_file, "{s}") } + Data::RichText(r) => write!(output_file, "{}", r.plain_text()), }?; // Write the field separator except for the last column. diff --git a/examples/read_charts.rs b/examples/read_charts.rs new file mode 100644 index 00000000..03c917e0 --- /dev/null +++ b/examples/read_charts.rs @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +// +// Copyright 2016-2026, Johann Tuffe. + +//! Example of reading the charts embedded in an XLSX worksheet, including +//! 3D charts, using the `calamine` crate. + +use calamine::{Error, Xlsx}; + +fn main() -> Result<(), Error> { + let path = "tests/charts.xlsx"; + + let mut workbook: Xlsx<_> = calamine::open_workbook(path)?; + + let charts = workbook.worksheet_charts("Sheet1")?; + + for chart in &charts { + println!( + "{}: {:?}", + chart.name.as_deref().unwrap_or("(unnamed)"), + chart.chart_type() + ); + + if let Some(title) = &chart.title { + if let Some(text) = title.text() { + println!(" title: {text}"); + } + } + + if let Some(view) = &chart.view_3d { + println!( + " 3D view: rotX={:?} rotY={:?} perspective={:?}", + view.rot_x, view.rot_y, view.perspective + ); + } + + for series in chart.series() { + let name = series.name_text().unwrap_or("(unnamed)"); + let values = series.values.as_ref(); + let formula = values.and_then(|v| v.formula.as_deref()).unwrap_or("-"); + let count = values.map_or(0, |v| v.values.len()); + println!(" series '{name}': {formula} ({count} cached points)"); + } + } + + Ok(()) +} diff --git a/examples/read_row_and_column_dimensions.rs b/examples/read_row_and_column_dimensions.rs new file mode 100644 index 00000000..73dce696 --- /dev/null +++ b/examples/read_row_and_column_dimensions.rs @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +// +// Copyright 2016-2025, Johann Tuffe. + +use calamine::{open_workbook, Reader, Xlsx}; + +/// Example demonstrating how to capture column widths and row heights from Excel files +fn main() -> Result<(), Box> { + // Open an Excel file + let path = "tests/styles.xlsx"; + let mut workbook: Xlsx<_> = open_workbook(path)?; + + // Get the first sheet name + let sheet_names = workbook.sheet_names(); + if let Some(sheet_name) = sheet_names.first() { + println!("Getting layout information for sheet: {}", sheet_name); + + // Get the worksheet layout information (column widths and row heights) + let layout = workbook.worksheet_layout(sheet_name)?; + + // Display default dimensions + if let Some(default_col_width) = layout.default_column_width { + println!("Default column width: {} characters", default_col_width); + } + if let Some(default_row_height) = layout.default_row_height { + println!("Default row height: {} points", default_row_height); + } + + // Display custom column widths + if !layout.column_widths.is_empty() { + println!("\nCustom column widths:"); + for col_width in layout.column_widths.values() { + println!( + " Column {}: {} characters (custom: {}, hidden: {}, best_fit: {})", + col_width.column, + col_width.width, + col_width.custom_width, + col_width.hidden, + col_width.best_fit + ); + } + } + + // Display custom row heights + if !layout.row_heights.is_empty() { + println!("\nCustom row heights:"); + for row_height in layout.row_heights.values() { + println!( + " Row {}: {} points (custom: {}, hidden: {})", + row_height.row, row_height.height, row_height.custom_height, row_height.hidden + ); + } + } + + // Example of using the helper methods + println!("\nExample queries:"); + let effective_width_0 = layout.get_effective_column_width(0); + let effective_height_0 = layout.get_effective_row_height(0); + println!( + "Effective width of column 0: {} characters", + effective_width_0 + ); + println!("Effective height of row 0: {} points", effective_height_0); + + // Check if a specific column has custom width + if let Some(col_width) = layout.get_column_width(0) { + println!("Column 0 has custom width: {}", col_width.width); + } else { + println!("Column 0 uses default width"); + } + + // Check if layout has any custom dimensions + if layout.has_custom_dimensions() { + println!("This worksheet has custom column widths or row heights"); + } else { + println!("This worksheet uses all default dimensions"); + } + } + + Ok(()) +} diff --git a/examples/style.rs b/examples/style.rs new file mode 100644 index 00000000..3615128c --- /dev/null +++ b/examples/style.rs @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +// +// Copyright 2016-2025, Johann Tuffe. + +use calamine::{Cell, Color, Data, Font, FontWeight, Style}; + +fn main() -> Result<(), Box> { + // Example of creating a cell with style + let style = Style::new().with_font( + Font::new() + .with_name("Arial".to_string()) + .with_size(12.0) + .with_weight(FontWeight::Bold) + .with_color(Color::rgb(255, 0, 0)), + ); + + let cell = Cell::with_style((0, 0), Data::String("Hello World".to_string()), style); + + println!("Created cell with style:"); + if let Some(cell_style) = cell.get_style() { + if let Some(font) = cell_style.get_font() { + println!( + " Font: {} (size: {})", + font.name.as_deref().unwrap_or("Unknown"), + font.size.unwrap_or(0.0) + ); + println!(" Bold: {}", font.is_bold()); + if let Some(color) = font.color { + println!(" Color: {}", color); + } + } + } + + // Example of creating CellData with style + use calamine::CellData; + + let cell_data = CellData::with_style( + Data::Int(42), + Style::new().with_font(Font::new().with_weight(FontWeight::Bold)), + ); + + println!("\nCreated CellData with style:"); + if cell_data.has_style() { + if let Some(style) = cell_data.get_style() { + if let Some(font) = style.get_font() { + println!(" Bold: {}", font.is_bold()); + } + } + } + + // Example of creating a more complex style + let complex_style = Style::new() + .with_font( + Font::new() + .with_name("Times New Roman".to_string()) + .with_size(14.0) + .with_weight(FontWeight::Bold) + .with_color(Color::rgb(0, 0, 255)), + ) + .with_fill(calamine::Fill::solid(Color::rgb(255, 255, 0))) + .with_borders(calamine::Borders::new()); + + let styled_cell = Cell::with_style((1, 1), Data::Float(42.0), complex_style); + + println!("\nCreated cell with complex style:"); + if let Some(style) = styled_cell.get_style() { + if let Some(font) = style.get_font() { + println!( + " Font: {} (size: {})", + font.name.as_deref().unwrap_or("Unknown"), + font.size.unwrap_or(0.0) + ); + println!(" Bold: {}", font.is_bold()); + if let Some(color) = font.color { + println!(" Font color: {}", color); + } + } + + if let Some(fill) = style.get_fill() { + if fill.is_visible() { + println!(" Has fill"); + if let Some(color) = fill.get_color() { + println!(" Fill color: {}", color); + } + } + } + } + + println!("\nStyle system is working correctly!"); + + Ok(()) +} diff --git a/src/auto.rs b/src/auto.rs index a380b26f..b8a8594c 100644 --- a/src/auto.rs +++ b/src/auto.rs @@ -10,7 +10,7 @@ use crate::vba::VbaProject; use crate::Picture; use crate::{ open_workbook, open_workbook_from_rs, Data, DataRef, HeaderRow, Metadata, Ods, Range, Reader, - ReaderRef, Xls, Xlsb, Xlsx, + ReaderRef, StyleRange, WorksheetLayout, Xls, Xlsb, Xlsx, }; use std::fs::File; @@ -147,6 +147,24 @@ where } } + fn worksheet_style(&mut self, name: &str) -> Result { + match self { + Sheets::Xls(ref mut e) => e.worksheet_style(name).map_err(Error::Xls), + Sheets::Xlsx(ref mut e) => e.worksheet_style(name).map_err(Error::Xlsx), + Sheets::Xlsb(ref mut e) => e.worksheet_style(name).map_err(Error::Xlsb), + Sheets::Ods(ref mut e) => e.worksheet_style(name).map_err(Error::Ods), + } + } + + fn worksheet_layout(&mut self, name: &str) -> Result { + match self { + Sheets::Xls(ref mut e) => e.worksheet_layout(name).map_err(Error::Xls), + Sheets::Xlsx(ref mut e) => e.worksheet_layout(name).map_err(Error::Xlsx), + Sheets::Xlsb(ref mut e) => e.worksheet_layout(name).map_err(Error::Xlsb), + Sheets::Ods(ref mut e) => e.worksheet_layout(name).map_err(Error::Ods), + } + } + fn worksheets(&mut self) -> Vec<(String, Range)> { match self { Sheets::Xls(e) => e.worksheets(), diff --git a/src/chart.rs b/src/chart.rs new file mode 100644 index 00000000..727bb6e2 --- /dev/null +++ b/src/chart.rs @@ -0,0 +1,1244 @@ +// SPDX-License-Identifier: MIT +// +// Copyright 2016-2026, Johann Tuffe. + +//! Chart types for charts read from XLSX files. +//! +//! Charts are stored in an XLSX package as DrawingML "chartSpace" parts +//! (`xl/charts/chartN.xml`) referenced from a worksheet (or chartsheet) +//! drawing. [`crate::Xlsx::worksheet_charts`] walks those relationships and +//! returns one [`Chart`] per embedded chart, including its plot groups, +//! series (with cached values), axes, title, legend, 3D view settings and +//! shape/line formatting. +//! +//! All classic ECMA-376 chart families are supported (bar, column, line, +//! pie, doughnut, pie-of-pie, area, scatter, radar, stock, bubble and +//! surface, including their stacked and 3D variants), along with data +//! labels (including per-point overrides), trendlines, error bars, +//! up/down bars, drop/high-low/series lines, data tables and the common +//! axis and series options. The Excel 2016+ "chart-ex" families +//! (`xl/charts/chartExN.xml`: funnel, treemap, sunburst, histogram, +//! pareto, box & whisker, waterfall and filled map) are read with their +//! type, literal series data (including hierarchical category levels), +//! layout options ([`ChartExLayout`]), title and legend. +//! +//! Not read: pivot chart sources, manual plot-area layouts and surface +//! band formats. + +use crate::datatype::Data; +use crate::style::{Color, Font, RichText}; + +/// The type of a chart or of one of its plot groups. +/// +/// The variants mirror the Excel chart families, including the 3D families. +/// Combo charts contain multiple [`ChartGroup`]s with different types. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[non_exhaustive] +pub enum ChartType { + /// A 2D area chart. + Area, + /// A stacked 2D area chart. + AreaStacked, + /// A percent-stacked 2D area chart. + AreaPercentStacked, + /// A horizontal bar chart. + Bar, + /// A stacked horizontal bar chart. + BarStacked, + /// A percent-stacked horizontal bar chart. + BarPercentStacked, + /// A vertical column chart. + Column, + /// A stacked vertical column chart. + ColumnStacked, + /// A percent-stacked vertical column chart. + ColumnPercentStacked, + /// A doughnut chart. + Doughnut, + /// A line chart. + Line, + /// A stacked line chart. + LineStacked, + /// A percent-stacked line chart. + LinePercentStacked, + /// A pie chart. + Pie, + /// A pie-of-pie chart. + PieOfPie, + /// A bar-of-pie chart. + BarOfPie, + /// A radar chart. + Radar, + /// A radar chart with markers. + RadarWithMarkers, + /// A filled radar chart. + RadarFilled, + /// A scatter chart with markers only. + Scatter, + /// A scatter chart with straight connecting lines and no markers. + ScatterStraight, + /// A scatter chart with straight connecting lines and markers. + ScatterStraightWithMarkers, + /// A scatter chart with smoothed connecting lines and no markers. + ScatterSmooth, + /// A scatter chart with smoothed connecting lines and markers. + ScatterSmoothWithMarkers, + /// A stock (high-low-close) chart. + Stock, + /// A bubble chart. + Bubble, + /// A 3D area chart. + Area3D, + /// A stacked 3D area chart. + Area3DStacked, + /// A percent-stacked 3D area chart. + Area3DPercentStacked, + /// A 3D horizontal bar chart. + Bar3D, + /// A stacked 3D horizontal bar chart. + Bar3DStacked, + /// A percent-stacked 3D horizontal bar chart. + Bar3DPercentStacked, + /// A true 3D horizontal bar chart with series along the depth axis + /// (`c:grouping` `standard`). + Bar3DStandard, + /// A 3D vertical column chart. + Column3D, + /// A stacked 3D vertical column chart. + Column3DStacked, + /// A percent-stacked 3D vertical column chart. + Column3DPercentStacked, + /// A true 3D vertical column chart with series along the depth axis + /// (`c:grouping` `standard`). + Column3DStandard, + /// A 3D line chart. + Line3D, + /// A 3D pie chart. + Pie3D, + /// A 3D surface chart. + Surface3D, + /// A wireframe 3D surface chart. + Surface3DWireframe, + /// A contour chart (top view of a surface chart). + Contour, + /// A wireframe contour chart. + ContourWireframe, + /// A funnel chart (Excel 2016+ "chart-ex" chart). + Funnel, + /// A treemap chart (Excel 2016+ "chart-ex" chart). + Treemap, + /// A sunburst chart (Excel 2016+ "chart-ex" chart). + Sunburst, + /// A histogram chart (Excel 2016+ "chart-ex" chart). + Histogram, + /// A pareto chart (Excel 2016+ "chart-ex" chart). + Pareto, + /// A box & whisker chart (Excel 2016+ "chart-ex" chart). + BoxWhisker, + /// A waterfall chart (Excel 2016+ "chart-ex" chart). + Waterfall, + /// A filled map chart (Excel 2016+ "chart-ex" chart). + RegionMap, + /// An unrecognized chart type. + #[default] + Unknown, +} + +impl ChartType { + /// Returns `true` for the 3D chart families. + pub fn is_3d(self) -> bool { + matches!( + self, + ChartType::Area3D + | ChartType::Area3DStacked + | ChartType::Area3DPercentStacked + | ChartType::Bar3D + | ChartType::Bar3DStacked + | ChartType::Bar3DPercentStacked + | ChartType::Bar3DStandard + | ChartType::Column3D + | ChartType::Column3DStacked + | ChartType::Column3DPercentStacked + | ChartType::Column3DStandard + | ChartType::Line3D + | ChartType::Pie3D + | ChartType::Surface3D + | ChartType::Surface3DWireframe + | ChartType::Contour + | ChartType::ContourWireframe + ) + } + + /// Returns `true` for the Excel 2016+ "chart-ex" chart families + /// (funnel, treemap, sunburst, histogram, pareto, box & whisker, + /// waterfall and filled map). + pub fn is_chart_ex(self) -> bool { + matches!( + self, + ChartType::Funnel + | ChartType::Treemap + | ChartType::Sunburst + | ChartType::Histogram + | ChartType::Pareto + | ChartType::BoxWhisker + | ChartType::Waterfall + | ChartType::RegionMap + ) + } +} + +/// A chart read from an XLSX file. +/// +/// Returned by [`crate::Xlsx::worksheet_charts`]. A chart contains one or +/// more plot [`ChartGroup`]s (more than one for combo charts), the axes +/// declared in the plot area, and the chart-level title, legend, 3D view and +/// formatting. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Chart { + /// The drawing object name, e.g. `Chart 1`. + pub name: Option, + /// Where the chart is anchored on the worksheet. + pub position: Option, + /// The chart title. + pub title: Option, + /// The chart legend, if shown. + pub legend: Option, + /// The 3D view settings (`c:view3D`), present for 3D charts. + pub view_3d: Option, + /// The axes declared in the plot area, in document order. + pub axes: Vec, + /// The plot groups. Each group has a chart type and its own series. + pub groups: Vec, + /// The chart style number (`c:style`), 1-48. + pub style: Option, + /// Chart area (chart space) shape formatting. + pub format: Option, + /// Plot area shape formatting. + pub plot_area_format: Option, + /// How empty cells are plotted (`c:dispBlanksAs`). + pub display_blanks_as: Option, + /// Whether the chart area has rounded corners (`c:roundedCorners`). + pub rounded_corners: Option, + /// The data table shown under the chart (`c:dTable`), if any. + pub data_table: Option, + /// Whether the automatic title was deleted (`c:autoTitleDeleted`). + pub auto_title_deleted: Option, + /// Whether only visible cells are plotted (`c:plotVisOnly`). + pub plot_visible_only: Option, + /// Whether data labels over the value axis maximum are shown + /// (`c:showDLblsOverMax`). + pub show_data_labels_over_max: Option, + /// Whether the chart uses the 1904 date system (`c:date1904`). + pub date_1904: Option, +} + +impl Chart { + /// The primary chart type: the type of the first plot group. + pub fn chart_type(&self) -> ChartType { + self.groups + .first() + .map(|g| g.chart_type) + .unwrap_or(ChartType::Unknown) + } + + /// Iterate over all series across all plot groups. + pub fn series(&self) -> impl Iterator { + self.groups.iter().flat_map(|g| g.series.iter()) + } + + /// The X (category) axis. + /// + /// This is the first category or date axis, or for scatter/bubble charts + /// (which use two value axes) the first value axis. + pub fn x_axis(&self) -> Option<&ChartAxis> { + self.axes + .iter() + .find(|a| { + matches!( + a.axis_type, + ChartAxisType::Category | ChartAxisType::Date + ) + }) + .or_else(|| { + self.axes + .iter() + .find(|a| a.axis_type == ChartAxisType::Value) + }) + } + + /// The Y (value) axis. + /// + /// This is the first value axis, except for scatter/bubble charts where + /// it is the second value axis. + pub fn y_axis(&self) -> Option<&ChartAxis> { + let mut values = self + .axes + .iter() + .filter(|a| a.axis_type == ChartAxisType::Value); + let first = values.next(); + let has_cat = self.axes.iter().any(|a| { + matches!( + a.axis_type, + ChartAxisType::Category | ChartAxisType::Date + ) + }); + if has_cat { + first + } else { + values.next().or(first) + } + } + + /// The series (depth) axis of a 3D chart, if present. + pub fn series_axis(&self) -> Option<&ChartAxis> { + self.axes + .iter() + .find(|a| a.axis_type == ChartAxisType::Series) + } +} + +/// One plot group inside a chart: a chart type plus the series plotted with +/// that type and the group-level layout options. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ChartGroup { + /// The chart type of this group. + pub chart_type: ChartType, + /// The series in this group, in document order. + pub series: Vec, + /// Gap between bar/column clusters, as a percentage (`c:gapWidth`). + pub gap_width: Option, + /// Depth gap for 3D bar/column charts, as a percentage (`c:gapDepth`). + pub gap_depth: Option, + /// Overlap between bars/columns, -100 to 100 (`c:overlap`). + pub overlap: Option, + /// Doughnut hole size, as a percentage (`c:holeSize`). + pub hole_size: Option, + /// Rotation of the first pie/doughnut slice, in degrees + /// (`c:firstSliceAng`). + pub first_slice_angle: Option, + /// Bubble size scale, as a percentage (`c:bubbleScale`). + pub bubble_scale: Option, + /// Whether data points vary in color (`c:varyColors`). + pub vary_colors: Option, + /// The ids of the axes this group plots against (`c:axId`). + pub axis_ids: Vec, + /// Whether markers are shown for line charts (`c:marker`). + pub show_marker: Option, + /// Default data labels for the group (`c:dLbls`). + pub data_labels: Option, + /// The 3D shape of bars/columns in a 3D chart (`c:shape`). + pub shape: Option, + /// How bubble sizes map to bubble data (`c:sizeRepresents`). + pub size_represents: Option, + /// Whether negative-value bubbles are shown (`c:showNegBubbles`). + pub show_negative_bubbles: Option, + /// How the second plot of a pie-of-pie/bar-of-pie chart is split + /// (`c:splitType`). + pub split_type: Option, + /// The split threshold used with [`split_type`](Self::split_type) + /// (`c:splitPos`). + pub split_position: Option, + /// The zero-based point indices assigned to the second plot when + /// [`split_type`](Self::split_type) is + /// [`ChartOfPieSplitType::Custom`] (`c:custSplit`). + pub custom_split: Vec, + /// The size of the second pie/bar plot, as a percentage + /// (`c:secondPieSize`). + pub second_pie_size: Option, + /// Drop lines (`c:dropLines`), for line and area charts. + pub drop_lines: Option, + /// High-low lines (`c:hiLowLines`), for line and stock charts. + pub hi_low_lines: Option, + /// Series connector lines (`c:serLines`), for stacked bar and + /// pie-of-pie charts. + pub series_lines: Option, + /// Up/down bars (`c:upDownBars`), for line and stock charts. + pub up_down_bars: Option, +} + +/// A single data series in a chart. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ChartSeries { + /// The series index (`c:idx`). + pub index: Option, + /// The plot order of the series (`c:order`). + pub order: Option, + /// The series name (`c:tx`), with its formula and/or cached string. + pub name: Option, + /// The category (X) data (`c:cat` or `c:xVal`). + pub categories: Option, + /// The value (Y) data (`c:val` or `c:yVal`). + pub values: Option, + /// The bubble size data of a bubble chart (`c:bubbleSize`). + pub bubble_sizes: Option, + /// The series shape formatting (fill and line). + pub format: Option, + /// The series marker for line/scatter/radar charts. + pub marker: Option, + /// Whether the series line is smoothed (`c:smooth`). + pub smooth: Option, + /// Whether negative values invert the fill color + /// (`c:invertIfNegative`). + pub invert_if_negative: Option, + /// Per-data-point formatting overrides (`c:dPt`). + pub points: Vec, + /// The data labels of the series (`c:dLbls`). + pub data_labels: Option, + /// The trendlines of the series (`c:trendline`). + pub trendlines: Vec, + /// The error bars of the series (`c:errBars`), up to one per + /// direction. + pub error_bars: Vec, + /// Pie/doughnut slice offset from center, as a percentage + /// (`c:explosion`). + pub explosion: Option, + /// Whether the bubbles of a bubble chart series are drawn in 3D + /// (`c:bubble3D`). + pub bubble_3d: Option, + /// Chart-ex series layout options (`cx:layoutPr`): binning, + /// subtotals, statistics, element visibility and parent label + /// layout. `None` for classic charts. + pub chart_ex: Option, +} + +impl ChartSeries { + /// The series name as plain text, from the cached value of `c:tx`. + pub fn name_text(&self) -> Option<&str> { + let name = self.name.as_ref()?; + name.values.iter().find_map(|v| match v { + Data::String(s) => Some(s.as_str()), + _ => None, + }) + } +} + +/// A data reference used by a chart series: the source formula plus the +/// values cached in the chart part. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ChartDataSource { + /// The source range formula, e.g. `Sheet1!$B$2:$B$7` (`c:f`). `None` for + /// literal (embedded) data. + pub formula: Option, + /// The cached data points, indexed by point index. Gaps are + /// [`Data::Empty`]. + /// + /// For multi-level (hierarchical) sources this is the innermost + /// (leaf) level; see [`levels`](Self::levels) for the full + /// hierarchy. + pub values: Vec, + /// All label levels of a multi-level (hierarchical) category source + /// (`c:multiLvlStrCache` levels, or the `cx:lvl` levels of a + /// chart-ex dimension), innermost level first. Empty for + /// single-level sources. + pub levels: Vec>, + /// The cached number format code (`c:formatCode`). + pub number_format: Option, +} + +/// A per-data-point override inside a series (`c:dPt`). +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ChartDataPoint { + /// The zero-based index of the data point this override applies to. + pub index: u32, + /// The shape formatting of this data point. + pub format: Option, +} + +/// The type of a chart axis. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ChartAxisType { + /// A category axis (`c:catAx`). + Category, + /// A value axis (`c:valAx`). + Value, + /// A date axis (`c:dateAx`). + Date, + /// A series (depth) axis of a 3D chart (`c:serAx`). + Series, +} + +/// The side of the plot area an axis is drawn on (`c:axPos`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ChartAxisPosition { + /// Bottom of the plot area. + Bottom, + /// Left of the plot area. + Left, + /// Right of the plot area. + Right, + /// Top of the plot area. + Top, +} + +/// A chart axis. +#[derive(Debug, Clone, PartialEq)] +pub struct ChartAxis { + /// The axis type. + pub axis_type: ChartAxisType, + /// The axis id (`c:axId`), matched by [`ChartGroup::axis_ids`]. + pub id: Option, + /// The side of the plot area the axis is drawn on. + pub position: Option, + /// The axis title. + pub title: Option, + /// The axis number format code (`c:numFmt`). + pub number_format: Option, + /// The minimum axis bound (`c:min`). + pub min: Option, + /// The maximum axis bound (`c:max`). + pub max: Option, + /// The major unit (tick interval) of the axis (`c:majorUnit`). + pub major_unit: Option, + /// The minor unit of the axis (`c:minorUnit`). + pub minor_unit: Option, + /// The logarithmic base, if the axis uses a log scale (`c:logBase`). + pub log_base: Option, + /// Whether the axis direction is reversed (`c:orientation + /// val="maxMin"`). + pub reverse: bool, + /// Whether the axis is hidden (`c:delete val="1"`). + pub hidden: bool, + /// Whether major gridlines are shown. + pub major_gridlines: bool, + /// Whether minor gridlines are shown. + pub minor_gridlines: bool, + /// The major tick mark type (`c:majorTickMark`). + pub major_tick_mark: Option, + /// The minor tick mark type (`c:minorTickMark`). + pub minor_tick_mark: Option, + /// Where the tick labels are drawn (`c:tickLblPos`). + pub tick_label_position: Option, + /// Where the perpendicular axis crosses this axis (`c:crosses`). + pub crosses: Option, + /// The crossing value when [`crosses`](Self::crosses) is + /// [`ChartAxisCrosses::At`] (`c:crossesAt`). + pub crosses_at: Option, + /// The [`id`](Self::id) of the perpendicular axis this axis crosses + /// (`c:crossAx`). + pub crosses_axis_id: Option, + /// Whether the value axis crosses between or on category ticks + /// (`c:crossBetween`). + pub cross_between: Option, + /// The display units of a value axis (`c:dispUnits`). + pub display_units: Option, + /// Whether the display units label is shown (`c:dispUnitsLbl`). + pub display_units_label: bool, + /// The interval between tick labels on a category axis + /// (`c:tickLblSkip`). + pub tick_label_skip: Option, + /// The interval between tick marks on a category axis + /// (`c:tickMarkSkip`). + pub tick_mark_skip: Option, + /// The label offset of a category axis, as a percentage + /// (`c:lblOffset`). + pub label_offset: Option, + /// The axis line and area formatting. + pub format: Option, + /// The font of the axis labels. + pub font: Option, + /// The rotation of the axis label text in degrees + /// (`c:txPr/a:bodyPr@rot`, converted from 1/60000ths of a degree). + pub text_rotation: Option, +} + +impl ChartAxis { + pub(crate) fn new(axis_type: ChartAxisType) -> Self { + Self { + axis_type, + id: None, + position: None, + title: None, + number_format: None, + min: None, + max: None, + major_unit: None, + minor_unit: None, + log_base: None, + reverse: false, + hidden: false, + major_gridlines: false, + minor_gridlines: false, + major_tick_mark: None, + minor_tick_mark: None, + tick_label_position: None, + crosses: None, + crosses_at: None, + cross_between: None, + display_units: None, + display_units_label: false, + tick_label_skip: None, + tick_mark_skip: None, + label_offset: None, + crosses_axis_id: None, + format: None, + font: None, + text_rotation: None, + } + } +} + +/// A chart or axis title. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ChartTitle { + /// The title text with per-run formatting, for rich-text titles. + pub rich: Option, + /// The source formula, when the title comes from a cell reference. + pub formula: Option, + /// The cached title string, when the title comes from a cell reference. + pub cached: Option, + /// Whether the title overlays the plot area (`c:overlay`). + pub overlay: bool, + /// The default title font. + pub font: Option, + /// The rotation of the title text in degrees + /// (`a:bodyPr@rot`, converted from 1/60000ths of a degree). + pub text_rotation: Option, +} + +impl ChartTitle { + /// The title as plain text, from either the rich text or the cached + /// string. + pub fn text(&self) -> Option { + if let Some(rich) = &self.rich { + let text = rich.plain_text(); + if !text.is_empty() { + return Some(text); + } + } + self.cached.clone() + } +} + +/// The position of a chart legend (`c:legendPos`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ChartLegendPosition { + /// Right of the plot area (Excel's default). + #[default] + Right, + /// Left of the plot area. + Left, + /// Above the plot area. + Top, + /// Below the plot area. + Bottom, + /// In the top-right corner of the plot area. + TopRight, +} + +/// A chart legend. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ChartLegend { + /// Where the legend is positioned. + pub position: ChartLegendPosition, + /// Whether the legend overlays the plot area. + pub overlay: bool, + /// The legend font. + pub font: Option, +} + +/// The 3D view settings of a 3D chart (`c:view3D`). +/// +/// These correspond to the options in Excel's "3-D Rotation" dialog. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ChartView3d { + /// Rotation around the X axis, in degrees (`c:rotX`). + pub rot_x: Option, + /// Rotation around the Y axis, in degrees (`c:rotY`). + pub rot_y: Option, + /// Perspective, in degrees; used when right-angle axes are off + /// (`c:perspective`, stored as half-degrees in the file and converted). + pub perspective: Option, + /// Depth of the chart as a percentage of its width (`c:depthPercent`). + pub depth_percent: Option, + /// Height of the chart as a percentage of its width (`c:hPercent`). + pub height_percent: Option, + /// Whether the chart axes are drawn at right angles (`c:rAngAx`). + pub right_angle_axes: Option, +} + +/// Shape formatting of a chart element: area fill and line/border. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ChartFormat { + /// The area fill. + pub fill: Option, + /// The line (or border) formatting. + pub line: Option, + /// The linear gradient angle in degrees (`a:gradFill/a:lin@ang`, + /// converted from 1/60000ths of a degree), when + /// [`fill`](Self::fill) is [`ChartFill::Gradient`]. + pub gradient_angle: Option, +} + +/// The fill of a chart element. +#[derive(Debug, Clone, PartialEq)] +pub enum ChartFill { + /// No fill (transparent). + None, + /// A solid color fill. + Solid(Color), + /// A gradient fill with its color stops. + Gradient(Vec), + /// A pattern fill. + Pattern { + /// The pattern preset name, e.g. `pct50` (`a:pattFill@prst`). + pattern: String, + /// The foreground color. + foreground: Option, + /// The background color. + background: Option, + }, +} + +/// One color stop in a gradient fill. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct ChartGradientStop { + /// The stop position, 0.0 to 100.0 percent. + pub position: f64, + /// The stop color. + pub color: Color, +} + +/// The dash type of a chart line (`a:prstDash`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[non_exhaustive] +pub enum ChartLineDashType { + /// A solid line. + #[default] + Solid, + /// A dotted line. + Dot, + /// A dashed line. + Dash, + /// A dash-dot line. + DashDot, + /// A long dash line. + LongDash, + /// A long dash-dot line. + LongDashDot, + /// A long dash-dot-dot line. + LongDashDotDot, + /// A system dashed line. + SystemDash, + /// A system dotted line. + SystemDot, + /// A system dash-dot line. + SystemDashDot, + /// A system dash-dot-dot line. + SystemDashDotDot, +} + +/// The line (or border) formatting of a chart element. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ChartLine { + /// The line color. + pub color: Option, + /// The line width in points. + pub width: Option, + /// The line dash type. + pub dash_type: Option, + /// Whether the line is explicitly hidden (`a:noFill` inside `a:ln`). + pub hidden: bool, +} + +/// The marker symbol of a line/scatter/radar series (`c:symbol`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[non_exhaustive] +pub enum ChartMarkerType { + /// An automatically assigned marker. + #[default] + Automatic, + /// A circle marker. + Circle, + /// A dash marker. + Dash, + /// A diamond marker. + Diamond, + /// A dot marker. + Dot, + /// A plus marker. + Plus, + /// A square marker. + Square, + /// A star marker. + Star, + /// A triangle marker. + Triangle, + /// An X marker. + X, + /// No marker. + None, +} + +/// The marker of a line/scatter/radar series. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ChartMarker { + /// The marker symbol. + pub marker_type: ChartMarkerType, + /// The marker size in points (2-72). + pub size: Option, + /// The marker fill and outline formatting. + pub format: Option, +} + +/// One corner of a chart anchor: a cell plus an EMU offset into it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct ChartCellAnchor { + /// Zero-based column index. + pub col: u32, + /// Zero-based row index. + pub row: u32, + /// Horizontal offset into the cell, in EMUs (914400 per inch). + pub col_offset: i64, + /// Vertical offset into the cell, in EMUs. + pub row_offset: i64, +} + +/// Where a chart is anchored on the worksheet. +/// +/// Charts anchored with a `twoCellAnchor` have both [`from`](Self::from) and +/// [`to`](Self::to). Charts anchored with a `oneCellAnchor` have +/// [`from`](Self::from) plus a size, and `absoluteAnchor` charts (typical +/// for chartsheets) only have a size. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct ChartPosition { + /// The top-left anchor cell. + pub from: Option, + /// The bottom-right anchor cell. + pub to: Option, + /// The chart width in EMUs, for one-cell and absolute anchors. + pub width: Option, + /// The chart height in EMUs, for one-cell and absolute anchors. + pub height: Option, + /// How the chart moves/resizes with the grid + /// (`xdr:twoCellAnchor@editAs`). `Some(ChartEditAs::TwoCell)` for + /// two-cell anchors without an explicit attribute (the spec + /// default); `None` for one-cell and absolute anchors. + pub edit_as: Option, + /// The absolute X position in EMUs (`xdr:absoluteAnchor/xdr:pos@x`). + pub x: Option, + /// The absolute Y position in EMUs (`xdr:absoluteAnchor/xdr:pos@y`). + pub y: Option, +} + +/// How a two-cell anchored chart behaves when the grid changes +/// (`xdr:twoCellAnchor@editAs`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ChartEditAs { + /// The chart keeps its absolute position and size. + Absolute, + /// The chart moves with its top-left cell but keeps its size. + OneCell, + /// The chart moves and resizes with its anchor cells. + TwoCell, +} + +/// How empty cells are plotted (`c:dispBlanksAs`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ChartDisplayBlanksAs { + /// Empty cells leave a gap. + Gap, + /// Lines span across empty cells. + Span, + /// Empty cells are plotted as zero. + Zero, +} + +/// The data table shown under a chart (`c:dTable`). +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ChartDataTable { + /// Whether horizontal borders are shown. + pub show_horizontal_border: bool, + /// Whether vertical borders are shown. + pub show_vertical_border: bool, + /// Whether the table outline is shown. + pub show_outline: bool, + /// Whether legend keys are shown next to the series names. + pub show_legend_keys: bool, + /// The table font. + pub font: Option, +} + +/// The position of data labels relative to their data points +/// (`c:dLblPos`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum ChartDataLabelPosition { + /// Centered on the data point. + Center, + /// Inside the end of the data point. + InsideEnd, + /// Inside the base of the data point. + InsideBase, + /// Outside the end of the data point. + OutsideEnd, + /// Left of the data point. + Left, + /// Right of the data point. + Right, + /// Above the data point. + Above, + /// Below the data point. + Below, + /// Positioned automatically (pie charts). + BestFit, +} + +/// The data labels of a series or plot group (`c:dLbls`). +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ChartDataLabels { + /// Whether the point values are shown. + pub show_value: bool, + /// Whether the category names are shown. + pub show_category_name: bool, + /// Whether the series name is shown. + pub show_series_name: bool, + /// Whether the legend key is shown next to each label. + pub show_legend_key: bool, + /// Whether percentages are shown (pie/doughnut charts). + pub show_percent: bool, + /// Whether bubble sizes are shown (bubble charts). + pub show_bubble_size: bool, + /// The label position. + pub position: Option, + /// The label number format code (`c:numFmt`). + pub number_format: Option, + /// The label font. + pub font: Option, + /// The label area/border formatting. + pub format: Option, + /// The rotation of the label text in degrees + /// (`c:txPr/a:bodyPr@rot`, converted from 1/60000ths of a degree). + pub text_rotation: Option, + /// Per-point label overrides (`c:dLbl`). + pub point_labels: Vec, +} + +/// A per-point data label override (`c:dLbl`). +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ChartDataLabel { + /// The zero-based index of the data point this label belongs to + /// (`c:idx`). + pub index: u32, + /// Whether the label is deleted (hidden) for this point + /// (`c:delete`). + pub delete: bool, + /// Custom label text (`c:tx/c:rich`), flattened to plain text. + pub text: Option, + /// The label position, when overridden for this point. + pub position: Option, + /// The label number format code, when overridden (`c:numFmt`). + pub number_format: Option, + /// The label font, when overridden. + pub font: Option, + /// The label area/border formatting, when overridden. + pub format: Option, +} + +/// The type of a series trendline (`c:trendlineType`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ChartTrendlineType { + /// An exponential trendline. + Exponential, + /// A linear trendline. + #[default] + Linear, + /// A logarithmic trendline. + Logarithmic, + /// A moving-average trendline. + MovingAverage, + /// A polynomial trendline. + Polynomial, + /// A power trendline. + Power, +} + +/// A trendline attached to a chart series (`c:trendline`). +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ChartTrendline { + /// The trendline type. + pub trendline_type: ChartTrendlineType, + /// The custom trendline name (`c:name`). + pub name: Option, + /// The polynomial order, 2-6 (`c:order`). + pub order: Option, + /// The moving-average period (`c:period`). + pub period: Option, + /// Forecast periods forward (`c:forward`). + pub forward: Option, + /// Forecast periods backward (`c:backward`). + pub backward: Option, + /// The forced Y-axis intercept (`c:intercept`). + pub intercept: Option, + /// Whether the trendline equation is displayed (`c:dispEq`). + pub display_equation: bool, + /// Whether the R-squared value is displayed (`c:dispRSqr`). + pub display_r_squared: bool, + /// The trendline formatting. + pub format: Option, +} + +/// The direction of series error bars (`c:errDir`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ChartErrorBarsDirection { + /// Horizontal (X) error bars. + X, + /// Vertical (Y) error bars. + Y, +} + +/// Which directions the error bars extend in (`c:errBarType`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ChartErrorBarsType { + /// Both plus and minus. + #[default] + Both, + /// Minus only. + Minus, + /// Plus only. + Plus, +} + +/// How the error amount is determined (`c:errValType`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ChartErrorBarsValueType { + /// Custom plus/minus data ranges. + Custom, + /// A fixed value. + #[default] + FixedValue, + /// A percentage of each value. + Percentage, + /// A number of standard deviations. + StandardDeviation, + /// The standard error. + StandardError, +} + +/// Error bars attached to a chart series (`c:errBars`). +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ChartErrorBars { + /// The direction of the error bars. + pub direction: Option, + /// Which directions the bars extend in. + pub error_type: ChartErrorBarsType, + /// How the error amount is determined. + pub value_type: ChartErrorBarsValueType, + /// The fixed/percentage/standard-deviation amount (`c:val`). + pub value: Option, + /// Whether the bars are drawn without end caps (`c:noEndCap`). + pub no_end_cap: bool, + /// Custom plus values (`c:plus`). + pub plus_values: Option, + /// Custom minus values (`c:minus`). + pub minus_values: Option, + /// The error bar formatting. + pub format: Option, +} + +/// The tick mark type of an axis (`c:majorTickMark` / +/// `c:minorTickMark`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ChartTickMark { + /// No tick marks. + None, + /// Tick marks inside the plot area. + Inside, + /// Tick marks outside the plot area. + Outside, + /// Tick marks crossing the axis. + Cross, +} + +/// Where axis tick labels are drawn (`c:tickLblPos`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ChartTickLabelPosition { + /// Next to the axis (the default). + NextTo, + /// At the high end of the perpendicular axis. + High, + /// At the low end of the perpendicular axis. + Low, + /// Not shown. + None, +} + +/// Where the perpendicular axis crosses an axis (`c:crosses`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ChartAxisCrosses { + /// At zero (or the automatic position). + AutoZero, + /// At the minimum value. + Min, + /// At the maximum value. + Max, + /// At the value given by [`ChartAxis::crosses_at`]. + At, +} + +/// Whether a value axis crosses between or on category ticks +/// (`c:crossBetween`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ChartCrossBetween { + /// The axis crosses between categories. + Between, + /// The axis crosses at the category midpoints. + MidCat, +} + +/// The display units of a value axis (`c:builtInUnit` / +/// `c:custUnit`). +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ChartDisplayUnits { + /// Hundreds. + Hundreds, + /// Thousands. + Thousands, + /// Tens of thousands. + TenThousands, + /// Hundreds of thousands. + HundredThousands, + /// Millions. + Millions, + /// Tens of millions. + TenMillions, + /// Hundreds of millions. + HundredMillions, + /// Billions. + Billions, + /// Trillions. + Trillions, + /// A custom unit divisor. + Custom(f64), +} + +/// The 3D shape of the bars/columns of a 3D bar or column chart +/// (`c:shape`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ChartBar3dShape { + /// A box (the default). + #[default] + Box, + /// A cone tapering to a point. + Cone, + /// A cone truncated at the maximum value. + ConeToMax, + /// A cylinder. + Cylinder, + /// A pyramid tapering to a point. + Pyramid, + /// A pyramid truncated at the maximum value. + PyramidToMax, +} + +/// How the bubble size data maps to the drawn bubbles +/// (`c:sizeRepresents`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ChartSizeRepresents { + /// Bubble size data sets the bubble area (the default). + #[default] + Area, + /// Bubble size data sets the bubble width (diameter). + Width, +} + +/// How the second plot of a pie-of-pie or bar-of-pie chart is populated +/// (`c:splitType`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ChartOfPieSplitType { + /// Split determined automatically. + Auto, + /// Split by custom point assignment. + Custom, + /// Split by percentage threshold. + Percent, + /// Split by position (the last N points). + Position, + /// Split by value threshold. + Value, +} + +/// Auxiliary lines of a plot group: drop lines, high-low lines or series +/// lines. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ChartLines { + /// The line formatting, if specified. + pub format: Option, +} + +/// The up/down bars of a line or stock chart (`c:upDownBars`). +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ChartUpDownBars { + /// The gap between the bars, as a percentage (`c:gapWidth`). + pub gap_width: Option, + /// The formatting of the up bars. + pub up_format: Option, + /// The formatting of the down bars. + pub down_format: Option, +} + +/// The layout options of a chart-ex series (`cx:layoutPr`). +/// +/// Which fields are populated depends on the chart type: binning for +/// histogram/pareto, subtotals and connector lines for waterfall, +/// statistics and mean/outlier visibility for box & whisker, and parent +/// label layout for treemap. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ChartExLayout { + /// How parent labels are laid out in a treemap + /// (`cx:parentLabelLayout`). + pub parent_label_layout: Option, + /// The histogram bin size (`cx:binning/cx:binSize`). + pub bin_size: Option, + /// The histogram bin count (`cx:binning/cx:binCount`). + pub bin_count: Option, + /// The histogram overflow bin threshold (`cx:binning@overflow`). + /// `None` when automatic. + pub overflow: Option, + /// The histogram underflow bin threshold (`cx:binning@underflow`). + /// `None` when automatic. + pub underflow: Option, + /// The box & whisker quartile method + /// (`cx:statistics@quartileMethod`). + pub quartile_method: Option, + /// Whether the box & whisker mean marker is shown + /// (`cx:visibility@meanMarker`). + pub mean_marker: Option, + /// Whether the box & whisker mean line is shown + /// (`cx:visibility@meanLine`). + pub mean_line: Option, + /// Whether box & whisker outlier points are shown + /// (`cx:visibility@outliers`). + pub outliers: Option, + /// Whether box & whisker non-outlier points are shown + /// (`cx:visibility@nonoutliers`). + pub non_outliers: Option, + /// Whether waterfall connector lines are shown + /// (`cx:visibility@connectorLines`). + pub connector_lines: Option, + /// The zero-based point indices treated as waterfall subtotals + /// (`cx:subtotals`). + pub subtotals: Vec, +} + +/// How parent labels are laid out in a treemap chart +/// (`cx:parentLabelLayout@val`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum ChartExParentLabelLayout { + /// Parent labels are not shown. + None, + /// Parent labels are shown as banners above their group. + Banner, + /// Parent labels overlap their group. + Overlapping, +} + +/// The quartile calculation method of a box & whisker chart +/// (`cx:statistics@quartileMethod`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ChartExQuartileMethod { + /// Inclusive median quartile calculation. + Inclusive, + /// Exclusive median quartile calculation. + Exclusive, +} diff --git a/src/conditional_format.rs b/src/conditional_format.rs new file mode 100644 index 00000000..6c1aa641 --- /dev/null +++ b/src/conditional_format.rs @@ -0,0 +1,381 @@ +// SPDX-License-Identifier: MIT + +use crate::style::{Color, Style}; + +/// A `` element from a worksheet, containing one or more rules +/// that apply to a cell range. +#[derive(Debug, Clone, PartialEq)] +pub struct ConditionalFormatting { + /// The cell range this formatting applies to (e.g. "A1:B10"). + pub sqref: String, + /// The rules within this conditional formatting block, ordered by priority. + pub rules: Vec, +} + +/// A single `` element within a conditional formatting block. +#[derive(Debug, Clone, PartialEq)] +pub struct ConditionalFormatRule { + /// Evaluation priority (lower = higher priority). + pub priority: u32, + /// If true, stop evaluating lower-priority rules when this rule matches. + pub stop_if_true: bool, + /// The differential format to apply when the rule matches (resolved from dxfId). + pub format: Option