Skip to content

Commit

Permalink
chart: add initial support for combined charts
Browse files Browse the repository at this point in the history
Feature request #19
  • Loading branch information
jmcnamara committed Apr 13, 2024
1 parent 21cc449 commit 0b3c9da
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 95 deletions.
117 changes: 73 additions & 44 deletions src/chart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@
mod tests;

use regex::Regex;
use std::fmt;
use std::{fmt, mem};

use crate::{
drawing::{DrawingObject, DrawingType},
Expand Down Expand Up @@ -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<Box<Chart>>,
grouping: ChartGrouping,
show_empty_cells_as: ChartEmptyCells,
show_empty_cells_as: Option<ChartEmptyCells>,
show_hidden_data: bool,
show_na_as_empty: bool,
default_num_format: String,
Expand All @@ -488,6 +489,7 @@ pub struct Chart {
has_drop_lines: bool,
drop_lines_format: ChartFormat,
table: Option<ChartDataTable>,
base_series_index: usize,
}

impl Chart {
Expand Down Expand Up @@ -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(),
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 <c:xxxChart> 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.
Expand Down Expand Up @@ -2917,6 +2909,45 @@ impl Chart {
self.writer.xml_end_tag("c:plotArea");
}

// Write the <c:xxxChart> 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 <c:layout> element.
fn write_layout(&mut self) {
self.writer.xml_empty_tag_only("c:layout");
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -5574,13 +5605,11 @@ impl Chart {

// Write the <c:dispBlanksAs> 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 <dispNaAsBlank> element. This is an Excel 16 extension.
Expand Down
119 changes: 71 additions & 48 deletions src/workbook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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);
}
}
}
Expand All @@ -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);
}
}
}
Expand All @@ -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(
Expand Down
Binary file added tests/input/chart_combined01.xlsx
Binary file not shown.
Binary file added tests/input/chart_combined02.xlsx
Binary file not shown.
Binary file added tests/input/chart_combined03.xlsx
Binary file not shown.
4 changes: 1 addition & 3 deletions tests/integration/chart_blank01.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// Copyright 2022-2024, John McNamara, [email protected]

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> {
Expand All @@ -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)?;
Expand Down
Loading

0 comments on commit 0b3c9da

Please sign in to comment.