diff --git a/src/chart.rs b/src/chart.rs index edc61640..57cbb9ba 100644 --- a/src/chart.rs +++ b/src/chart.rs @@ -382,7 +382,7 @@ mod tests; use regex::Regex; -use std::fmt; +use std::{fmt, mem}; use crate::{ drawing::{DrawingObject, DrawingType}, @@ -469,8 +469,9 @@ pub struct Chart { pub(crate) legend: ChartLegend, pub(crate) chart_area_format: ChartFormat, pub(crate) plot_area_format: ChartFormat, + pub(crate) combined_chart: Option>, grouping: ChartGrouping, - show_empty_cells_as: ChartEmptyCells, + show_empty_cells_as: Option, show_hidden_data: bool, show_na_as_empty: bool, default_num_format: String, @@ -488,6 +489,7 @@ pub struct Chart { has_drop_lines: bool, drop_lines_format: ChartFormat, table: Option, + base_series_index: usize, } impl Chart { @@ -582,7 +584,7 @@ impl Chart { chart_area_format: ChartFormat::default(), plot_area_format: ChartFormat::default(), grouping: ChartGrouping::Standard, - show_empty_cells_as: ChartEmptyCells::Gaps, + show_empty_cells_as: None, show_hidden_data: false, show_na_as_empty: false, default_num_format: "General".to_string(), @@ -601,6 +603,8 @@ impl Chart { has_drop_lines: false, drop_lines_format: ChartFormat::default(), table: None, + combined_chart: None, + base_series_index: 0, }; match chart_type { @@ -1064,6 +1068,17 @@ impl Chart { &mut self.legend } + /// Create a combination chart with a secondary chart. + /// + /// TODO explain chart `combine()`. + /// + /// + pub fn combine(&mut self, chart: &Chart) -> &mut Chart { + self.combined_chart = Some(Box::new(chart.clone())); + + self + } + /// Set the chart style type. /// /// The `set_style()` method is used to set the style of the chart to one of @@ -2230,7 +2245,7 @@ impl Chart { /// `option` - A [`ChartEmptyCells`] enum value. /// pub fn show_empty_cells_as(&mut self, option: ChartEmptyCells) -> &mut Chart { - self.show_empty_cells_as = option; + self.show_empty_cells_as = Some(option); self } @@ -2836,40 +2851,17 @@ impl Chart { // Write the c:layout element. self.write_layout(); - match self.chart_type { - ChartType::Area | ChartType::AreaStacked | ChartType::AreaPercentStacked => { - self.write_area_chart(); - } + // Write the element for each chart type. + self.write_chart_type(); - ChartType::Bar | ChartType::BarStacked | ChartType::BarPercentStacked => { - self.write_bar_chart(); - } - - ChartType::Column | ChartType::ColumnStacked | ChartType::ColumnPercentStacked => { - self.write_column_chart(); - } + // Write the combined chart. + if let Some(combined_chart) = &mut self.combined_chart { + combined_chart.axis_ids = self.axis_ids; + combined_chart.base_series_index = self.series.len(); - ChartType::Doughnut => self.write_doughnut_chart(), - - ChartType::Line | ChartType::LineStacked | ChartType::LinePercentStacked => { - self.write_line_chart(); - } - - ChartType::Pie => self.write_pie_chart(), - - ChartType::Radar | ChartType::RadarWithMarkers | ChartType::RadarFilled => { - self.write_radar_chart(); - } - - ChartType::Scatter - | ChartType::ScatterStraight - | ChartType::ScatterStraightWithMarkers - | ChartType::ScatterSmooth - | ChartType::ScatterSmoothWithMarkers => self.write_scatter_chart(), - - ChartType::Stock => { - self.write_stock_chart(); - } + mem::swap(&mut combined_chart.writer, &mut self.writer); + combined_chart.write_chart_type(); + mem::swap(&mut combined_chart.writer, &mut self.writer); } // Reverse the X and Y axes for Bar charts. @@ -2917,6 +2909,45 @@ impl Chart { self.writer.xml_end_tag("c:plotArea"); } + // Write the element. + fn write_chart_type(&mut self) { + match self.chart_type { + ChartType::Area | ChartType::AreaStacked | ChartType::AreaPercentStacked => { + self.write_area_chart(); + } + + ChartType::Bar | ChartType::BarStacked | ChartType::BarPercentStacked => { + self.write_bar_chart(); + } + + ChartType::Column | ChartType::ColumnStacked | ChartType::ColumnPercentStacked => { + self.write_column_chart(); + } + + ChartType::Doughnut => self.write_doughnut_chart(), + + ChartType::Line | ChartType::LineStacked | ChartType::LinePercentStacked => { + self.write_line_chart(); + } + + ChartType::Pie => self.write_pie_chart(), + + ChartType::Radar | ChartType::RadarWithMarkers | ChartType::RadarFilled => { + self.write_radar_chart(); + } + + ChartType::Scatter + | ChartType::ScatterStraight + | ChartType::ScatterStraightWithMarkers + | ChartType::ScatterSmooth + | ChartType::ScatterSmoothWithMarkers => self.write_scatter_chart(), + + ChartType::Stock => { + self.write_stock_chart(); + } + } + } + // Write the element. fn write_layout(&mut self) { self.writer.xml_empty_tag_only("c:layout"); @@ -2969,10 +3000,10 @@ impl Chart { } // Write the c:idx element. - self.write_idx(index); + self.write_idx(self.base_series_index + index); // Write the c:order element. - self.write_order(index); + self.write_order(self.base_series_index + index); self.write_series_title(&series.title); @@ -5574,13 +5605,11 @@ impl Chart { // Write the element. fn write_disp_blanks_as(&mut self) { - if self.show_empty_cells_as == ChartEmptyCells::Gaps { - return; - } + if let Some(show_empty_cells) = self.show_empty_cells_as { + let attributes = [("val", show_empty_cells.to_string())]; - let attributes = [("val", self.show_empty_cells_as.to_string())]; - - self.writer.xml_empty_tag("c:dispBlanksAs", &attributes); + self.writer.xml_empty_tag("c:dispBlanksAs", &attributes); + } } // Write the element. This is an Excel 16 extension. diff --git a/src/workbook.rs b/src/workbook.rs index e6daba06..4d0e5574 100644 --- a/src/workbook.rs +++ b/src/workbook.rs @@ -229,7 +229,7 @@ use crate::packager::PackagerOptions; use crate::worksheet::Worksheet; use crate::xmlwriter::XMLWriter; use crate::{ - utility, Border, ChartRange, ChartRangeCacheData, ColNum, DefinedName, DefinedNameType, + utility, Border, Chart, ChartRange, ChartRangeCacheData, ColNum, DefinedName, DefinedNameType, DocProperties, Fill, Font, Image, RowNum, Visible, NUM_IMAGE_FORMATS, }; use crate::{Color, FormatPattern}; @@ -1561,23 +1561,10 @@ impl Workbook { for worksheet in &self.worksheets { if !worksheet.charts.is_empty() { for chart in worksheet.charts.values() { - Self::insert_to_chart_cache(&chart.title.range, &mut chart_caches); - Self::insert_to_chart_cache(&chart.x_axis.title.range, &mut chart_caches); - Self::insert_to_chart_cache(&chart.y_axis.title.range, &mut chart_caches); - - for series in &chart.series { - Self::insert_to_chart_cache(&series.title.range, &mut chart_caches); - Self::insert_to_chart_cache(&series.value_range, &mut chart_caches); - Self::insert_to_chart_cache(&series.category_range, &mut chart_caches); - - for data_label in &series.custom_data_labels { - Self::insert_to_chart_cache(&data_label.title.range, &mut chart_caches); - } - - if let Some(error_bars) = &series.y_error_bars { - Self::insert_to_chart_cache(&error_bars.plus_range, &mut chart_caches); - Self::insert_to_chart_cache(&error_bars.minus_range, &mut chart_caches); - } + Self::insert_chart_ranges_to_cache(chart, &mut chart_caches); + + if let Some(chart) = &chart.combined_chart { + Self::insert_chart_ranges_to_cache(chart, &mut chart_caches); } } } @@ -1601,36 +1588,10 @@ impl Workbook { for worksheet in &mut self.worksheets { if !worksheet.charts.is_empty() { for chart in worksheet.charts.values_mut() { - Self::update_range_cache(&mut chart.title.range, &mut chart_caches); - Self::update_range_cache(&mut chart.x_axis.title.range, &mut chart_caches); - Self::update_range_cache(&mut chart.y_axis.title.range, &mut chart_caches); - - for series in &mut chart.series { - Self::update_range_cache(&mut series.title.range, &mut chart_caches); - Self::update_range_cache(&mut series.value_range, &mut chart_caches); - Self::update_range_cache(&mut series.category_range, &mut chart_caches); - - for data_label in &mut series.custom_data_labels { - if let Some(cache) = chart_caches.get(&data_label.title.range.key()) { - data_label.title.range.cache = cache.clone(); - } - } - - if let Some(error_bars) = &mut series.y_error_bars { - Self::update_range_cache(&mut error_bars.plus_range, &mut chart_caches); - Self::update_range_cache( - &mut error_bars.minus_range, - &mut chart_caches, - ); - } - - if let Some(error_bars) = &mut series.x_error_bars { - Self::update_range_cache(&mut error_bars.plus_range, &mut chart_caches); - Self::update_range_cache( - &mut error_bars.minus_range, - &mut chart_caches, - ); - } + Self::update_chart_ranges_from_cache(chart, &mut chart_caches); + + if let Some(chart) = &mut chart.combined_chart { + Self::update_chart_ranges_from_cache(chart, &mut chart_caches); } } } @@ -1639,6 +1600,68 @@ impl Workbook { Ok(()) } + // Insert all the various chart ranges into the lookup range cache. + fn insert_chart_ranges_to_cache( + chart: &Chart, + chart_caches: &mut HashMap<(String, RowNum, ColNum, RowNum, ColNum), ChartRangeCacheData>, + ) { + Self::insert_to_chart_cache(&chart.title.range, chart_caches); + Self::insert_to_chart_cache(&chart.x_axis.title.range, chart_caches); + Self::insert_to_chart_cache(&chart.y_axis.title.range, chart_caches); + + for series in &chart.series { + Self::insert_to_chart_cache(&series.title.range, chart_caches); + Self::insert_to_chart_cache(&series.value_range, chart_caches); + Self::insert_to_chart_cache(&series.category_range, chart_caches); + + for data_label in &series.custom_data_labels { + Self::insert_to_chart_cache(&data_label.title.range, chart_caches); + } + + if let Some(error_bars) = &series.y_error_bars { + Self::insert_to_chart_cache(&error_bars.plus_range, chart_caches); + Self::insert_to_chart_cache(&error_bars.minus_range, chart_caches); + } + + if let Some(error_bars) = &series.x_error_bars { + Self::insert_to_chart_cache(&error_bars.plus_range, chart_caches); + Self::insert_to_chart_cache(&error_bars.minus_range, chart_caches); + } + } + } + + // Update all the various chart ranges from the lookup range cache. + fn update_chart_ranges_from_cache( + chart: &mut Chart, + chart_caches: &mut HashMap<(String, RowNum, ColNum, RowNum, ColNum), ChartRangeCacheData>, + ) { + Self::update_range_cache(&mut chart.title.range, chart_caches); + Self::update_range_cache(&mut chart.x_axis.title.range, chart_caches); + Self::update_range_cache(&mut chart.y_axis.title.range, chart_caches); + + for series in &mut chart.series { + Self::update_range_cache(&mut series.title.range, chart_caches); + Self::update_range_cache(&mut series.value_range, chart_caches); + Self::update_range_cache(&mut series.category_range, chart_caches); + + for data_label in &mut series.custom_data_labels { + if let Some(cache) = chart_caches.get(&data_label.title.range.key()) { + data_label.title.range.cache = cache.clone(); + } + } + + if let Some(error_bars) = &mut series.y_error_bars { + Self::update_range_cache(&mut error_bars.plus_range, chart_caches); + Self::update_range_cache(&mut error_bars.minus_range, chart_caches); + } + + if let Some(error_bars) = &mut series.x_error_bars { + Self::update_range_cache(&mut error_bars.plus_range, chart_caches); + Self::update_range_cache(&mut error_bars.minus_range, chart_caches); + } + } + } + // Insert a chart range (expressed as a hash/key value) into the chart cache // for lookup later. fn insert_to_chart_cache( diff --git a/tests/input/chart_combined01.xlsx b/tests/input/chart_combined01.xlsx new file mode 100644 index 00000000..778f5524 Binary files /dev/null and b/tests/input/chart_combined01.xlsx differ diff --git a/tests/input/chart_combined02.xlsx b/tests/input/chart_combined02.xlsx new file mode 100644 index 00000000..739aa518 Binary files /dev/null and b/tests/input/chart_combined02.xlsx differ diff --git a/tests/input/chart_combined03.xlsx b/tests/input/chart_combined03.xlsx new file mode 100644 index 00000000..5c5b2f1f Binary files /dev/null and b/tests/input/chart_combined03.xlsx differ diff --git a/tests/integration/chart_blank01.rs b/tests/integration/chart_blank01.rs index ccc30797..5d65918a 100644 --- a/tests/integration/chart_blank01.rs +++ b/tests/integration/chart_blank01.rs @@ -6,7 +6,7 @@ // Copyright 2022-2024, John McNamara, jmcnamara@cpan.org use crate::common; -use rust_xlsxwriter::{Chart, ChartEmptyCells, ChartSeries, ChartType, Workbook, XlsxError}; +use rust_xlsxwriter::{Chart, ChartSeries, ChartType, Workbook, XlsxError}; // Create rust_xlsxwriter file to compare against Excel file. fn create_new_xlsx_file(filename: &str) -> Result<(), XlsxError> { @@ -31,8 +31,6 @@ fn create_new_xlsx_file(filename: &str) -> Result<(), XlsxError> { // Set the chart axis ids to match the random values in the Excel file. chart.set_axis_ids(45705856, 45843584); - chart.show_empty_cells_as(ChartEmptyCells::Gaps); - worksheet.insert_chart(8, 4, &chart)?; workbook.save(filename)?; diff --git a/tests/integration/chart_combined01.rs b/tests/integration/chart_combined01.rs new file mode 100644 index 00000000..79a4865c --- /dev/null +++ b/tests/integration/chart_combined01.rs @@ -0,0 +1,43 @@ +// Test case that compares a file generated by rust_xlsxwriter with a file +// created by Excel. +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +// +// Copyright 2022-2024, John McNamara, jmcnamara@cpan.org + +use crate::common; +use rust_xlsxwriter::{Chart, ChartType, Workbook, XlsxError}; + +// Create rust_xlsxwriter file to compare against Excel file. +fn create_new_xlsx_file(filename: &str) -> Result<(), XlsxError> { + let mut workbook = Workbook::new(); + + let worksheet = workbook.add_worksheet(); + + // Add some test data for the chart(s). + worksheet.write_column(0, 0, [2, 7, 3, 6, 2])?; + worksheet.write_column(0, 1, [20, 25, 10, 10, 20])?; + + let mut chart = Chart::new(ChartType::Column); + chart.set_axis_ids(84882560, 84884096); + chart.add_series().set_values(("Sheet1", 0, 0, 4, 0)); + + chart.add_series().set_values(("Sheet1", 0, 1, 4, 1)); + + worksheet.insert_chart(8, 4, &chart)?; + + workbook.save(filename)?; + + Ok(()) +} + +#[test] +fn test_chart_combined01() { + let test_runner = common::TestRunner::new() + .set_name("chart_combined01") + .set_function(create_new_xlsx_file) + .initialize(); + + test_runner.assert_eq(); + test_runner.cleanup(); +} diff --git a/tests/integration/chart_combined02.rs b/tests/integration/chart_combined02.rs new file mode 100644 index 00000000..cb33c363 --- /dev/null +++ b/tests/integration/chart_combined02.rs @@ -0,0 +1,47 @@ +// Test case that compares a file generated by rust_xlsxwriter with a file +// created by Excel. +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +// +// Copyright 2022-2024, John McNamara, jmcnamara@cpan.org + +use crate::common; +use rust_xlsxwriter::{Chart, ChartEmptyCells, ChartType, Workbook, XlsxError}; + +// Create rust_xlsxwriter file to compare against Excel file. +fn create_new_xlsx_file(filename: &str) -> Result<(), XlsxError> { + let mut workbook = Workbook::new(); + + let worksheet = workbook.add_worksheet(); + + // Add some test data for the chart(s). + worksheet.write_column(0, 0, [2, 7, 3, 6, 2])?; + worksheet.write_column(0, 1, [20, 25, 10, 10, 20])?; + + let mut chart1 = Chart::new(ChartType::Column); + chart1.add_series().set_values(("Sheet1", 0, 0, 4, 0)); + + let mut chart2 = Chart::new(ChartType::Line); + chart2.add_series().set_values(("Sheet1", 0, 1, 4, 1)); + + chart1.show_empty_cells_as(ChartEmptyCells::Gaps); + + chart1.combine(&chart2); + + worksheet.insert_chart(8, 4, &chart1)?; + + workbook.save(filename)?; + + Ok(()) +} + +#[test] +fn test_chart_combined02() { + let test_runner = common::TestRunner::new() + .set_name("chart_combined02") + .set_function(create_new_xlsx_file) + .initialize(); + + test_runner.assert_eq(); + test_runner.cleanup(); +} diff --git a/tests/integration/chart_combined03.rs b/tests/integration/chart_combined03.rs new file mode 100644 index 00000000..2343feee --- /dev/null +++ b/tests/integration/chart_combined03.rs @@ -0,0 +1,49 @@ +// Test case that compares a file generated by rust_xlsxwriter with a file +// created by Excel. +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +// +// Copyright 2022-2024, John McNamara, jmcnamara@cpan.org + +use crate::common; +use rust_xlsxwriter::{Chart, ChartEmptyCells, ChartType, Workbook, XlsxError}; + +// Create rust_xlsxwriter file to compare against Excel file. +fn create_new_xlsx_file(filename: &str) -> Result<(), XlsxError> { + let mut workbook = Workbook::new(); + + let worksheet = workbook.add_worksheet(); + + // Add some test data for the chart(s). + worksheet.write_column(0, 0, [2, 7, 3, 6, 2])?; + worksheet.write_column(0, 1, [20, 25, 10, 10, 20])?; + worksheet.write_column(0, 2, [4, 2, 5, 2, 1])?; + + let mut chart1 = Chart::new(ChartType::Column); + chart1.add_series().set_values(("Sheet1", 0, 0, 4, 0)); + chart1.add_series().set_values(("Sheet1", 0, 1, 4, 1)); + + let mut chart2 = Chart::new(ChartType::Line); + chart2.add_series().set_values(("Sheet1", 0, 2, 4, 2)); + + chart1.show_empty_cells_as(ChartEmptyCells::Gaps); + + chart1.combine(&chart2); + + worksheet.insert_chart(8, 4, &chart1)?; + + workbook.save(filename)?; + + Ok(()) +} + +#[test] +fn test_chart_combined03() { + let test_runner = common::TestRunner::new() + .set_name("chart_combined03") + .set_function(create_new_xlsx_file) + .initialize(); + + test_runner.assert_eq(); + test_runner.cleanup(); +} diff --git a/tests/integration/main.rs b/tests/integration/main.rs index 79258593..a238e08c 100644 --- a/tests/integration/main.rs +++ b/tests/integration/main.rs @@ -197,6 +197,9 @@ mod chart_column10; mod chart_column11; mod chart_column12; mod chart_column13; +mod chart_combined01; +mod chart_combined02; +mod chart_combined03; mod chart_crossing01; mod chart_crossing02; mod chart_crossing03;