From a1d967c6dec8beab6c1491cb1f665612dd60025a Mon Sep 17 00:00:00 2001 From: John McNamara Date: Tue, 28 May 2024 00:15:44 +0100 Subject: [PATCH] chart: add intial support for secondary axes --- src/chart.rs | 677 +++++++++++++++++---------- src/worksheet.rs | 2 +- tests/input/bootstrap71.xlsx | Bin 0 -> 7030 bytes tests/input/chart_area04.xlsx | Bin 0 -> 9274 bytes tests/input/chart_bar24.xlsx | Bin 0 -> 9306 bytes tests/input/chart_column04.xlsx | Bin 0 -> 9282 bytes tests/input/chart_line02.xlsx | Bin 0 -> 9295 bytes tests/input/chart_scatter07.xlsx | Bin 0 -> 9400 bytes tests/input/chart_stock02.xlsx | Bin 0 -> 9723 bytes tests/integration/bootstrap71.rs | 48 ++ tests/integration/chart_area04.rs | 49 ++ tests/integration/chart_bar24.rs | 49 ++ tests/integration/chart_column04.rs | 49 ++ tests/integration/chart_line02.rs | 49 ++ tests/integration/chart_scatter07.rs | 55 +++ tests/integration/chart_stock02.rs | 94 ++++ tests/integration/main.rs | 7 + 17 files changed, 838 insertions(+), 241 deletions(-) create mode 100644 tests/input/bootstrap71.xlsx create mode 100644 tests/input/chart_area04.xlsx create mode 100644 tests/input/chart_bar24.xlsx create mode 100644 tests/input/chart_column04.xlsx create mode 100644 tests/input/chart_line02.xlsx create mode 100644 tests/input/chart_scatter07.xlsx create mode 100644 tests/input/chart_stock02.xlsx create mode 100644 tests/integration/bootstrap71.rs create mode 100644 tests/integration/chart_area04.rs create mode 100644 tests/integration/chart_bar24.rs create mode 100644 tests/integration/chart_column04.rs create mode 100644 tests/integration/chart_line02.rs create mode 100644 tests/integration/chart_scatter07.rs create mode 100644 tests/integration/chart_stock02.rs diff --git a/src/chart.rs b/src/chart.rs index 57cbb9ba..4a0eccca 100644 --- a/src/chart.rs +++ b/src/chart.rs @@ -460,12 +460,15 @@ pub struct Chart { scale_width: f64, scale_height: f64, axis_ids: (u32, u32), + axis2_ids: (u32, u32), category_has_num_format: bool, chart_type: ChartType, chart_group_type: ChartType, pub(crate) title: ChartTitle, pub(crate) x_axis: ChartAxis, pub(crate) y_axis: ChartAxis, + pub(crate) x2_axis: ChartAxis, + pub(crate) y2_axis: ChartAxis, pub(crate) legend: ChartLegend, pub(crate) chart_area_format: ChartFormat, pub(crate) plot_area_format: ChartFormat, @@ -489,7 +492,9 @@ pub struct Chart { has_drop_lines: bool, drop_lines_format: ChartFormat, table: Option, - base_series_index: usize, + series_index: usize, + has_secondary_axis: bool, + has_crosses: bool, } impl Chart { @@ -573,6 +578,7 @@ impl Chart { drawing_type: DrawingType::Chart, axis_ids: (0, 0), + axis2_ids: (0, 0), series: vec![], category_has_num_format: false, chart_type, @@ -580,6 +586,8 @@ impl Chart { title: ChartTitle::new(), x_axis: ChartAxis::new(), y_axis: ChartAxis::new(), + x2_axis: ChartAxis::new(), + y2_axis: ChartAxis::new(), legend: ChartLegend::new(), chart_area_format: ChartFormat::default(), plot_area_format: ChartFormat::default(), @@ -604,7 +612,9 @@ impl Chart { drop_lines_format: ChartFormat::default(), table: None, combined_chart: None, - base_series_index: 0, + series_index: 0, + has_secondary_axis: false, + has_crosses: true, }; match chart_type { @@ -2266,35 +2276,49 @@ impl Chart { self } - /// Set default values for the chart axis ids. + /// Set default values for the primary chart axis ids. /// - /// This is mainly used to ensure that the axis ids used in testing match - /// the semi-randomized values in the target Excel file. + /// This is mainly used to ensure that the primary axis ids used in testing + /// match the semi-randomized values in the target Excel file. /// /// # Parameters /// - /// `axis_id1` - X-axis id. - /// `axis_id2` - Y-axis id. + /// - `axis_id1`: X-axis id. + /// - `axis_id2`: Y-axis id. /// #[doc(hidden)] pub fn set_axis_ids(&mut self, axis_id1: u32, axis_id2: u32) { self.axis_ids = (axis_id1, axis_id2); } + /// Set default values for the secondary chart axis ids. + /// + /// This is mainly used to ensure that the secondary axis ids used in + /// testing match the semi-randomized values in the target Excel file. + /// + /// # Parameters + /// + /// - `axis_id1`: X-axis id. + /// - `axis_id2`: Y-axis id. + /// + #[doc(hidden)] + pub fn set_axis2_ids(&mut self, axis_id1: u32, axis_id2: u32) { + self.axis2_ids = (axis_id1, axis_id2); + } + // ----------------------------------------------------------------------- // Crate level helper methods. // ----------------------------------------------------------------------- // Set chart unique axis ids. - pub(crate) fn add_axis_ids(&mut self) { + pub(crate) fn add_axis_ids(&mut self, chart_id: u32) { if self.axis_ids.0 != 0 { return; } - let axis_id_1 = (5000 + self.id) * 10000 + 1; - let axis_id_2 = axis_id_1 + 1; - - self.axis_ids = (axis_id_1, axis_id_2); + let axis_id = (5000 + chart_id) * 10000 + 1; + self.axis_ids = (axis_id, axis_id + 1); + self.axis2_ids = (axis_id + 2, axis_id + 3); } // Check for any legend entries that have been hidden/deleted via the @@ -2335,12 +2359,25 @@ impl Chart { deleted_entries } + // todo + fn check_for_secondary_axis(&mut self) { + // TODO + for series in &self.series { + if series.y2_axis { + self.has_secondary_axis = true; + return; + } + } + } + // ----------------------------------------------------------------------- // Chart specific methods. // ----------------------------------------------------------------------- // Initialize area charts. fn initialize_area_chart(mut self) -> Chart { + self.has_crosses = false; + self.x_axis.axis_type = ChartAxisType::Category; self.x_axis.axis_position = ChartAxisPosition::Bottom; self.x_axis.position_between_ticks = false; @@ -2350,6 +2387,15 @@ impl Chart { self.y_axis.title.is_horizontal = true; self.y_axis.major_gridlines = true; + self.x2_axis.axis_type = ChartAxisType::Category; + self.x2_axis.position_between_ticks = false; + self.x2_axis.crossing = ChartAxisCrossing::Max; + self.x2_axis.is_hidden = true; + self.x2_axis.label_position = ChartAxisLabelPosition::None; + + self.y2_axis.axis_type = ChartAxisType::Value; + self.y2_axis.axis_position = ChartAxisPosition::Left; + self.chart_group_type = ChartType::Area; if self.chart_type == ChartType::Area { @@ -2369,6 +2415,8 @@ impl Chart { // Initialize bar charts. Bar chart category/value axes are reversed in // comparison to other charts. Some of the defaults reflect this. fn initialize_bar_chart(mut self) -> Chart { + self.has_crosses = false; + self.x_axis.axis_type = ChartAxisType::Value; self.x_axis.axis_position = ChartAxisPosition::Bottom; self.x_axis.major_gridlines = true; @@ -2377,6 +2425,16 @@ impl Chart { self.y_axis.axis_position = ChartAxisPosition::Left; self.y_axis.title.is_horizontal = true; + self.x2_axis.axis_type = ChartAxisType::Category; + self.x2_axis.axis_position = ChartAxisPosition::Bottom; + self.x2_axis.crossing = ChartAxisCrossing::Automatic; + + self.y2_axis.axis_type = ChartAxisType::Value; + self.y2_axis.axis_position = ChartAxisPosition::Left; + self.y2_axis.crossing = ChartAxisCrossing::Max; + self.y2_axis.is_hidden = true; + self.y2_axis.label_position = ChartAxisLabelPosition::None; + self.chart_group_type = ChartType::Bar; if self.chart_type == ChartType::Bar { @@ -2406,6 +2464,14 @@ impl Chart { self.y_axis.axis_position = ChartAxisPosition::Left; self.y_axis.major_gridlines = true; + self.x2_axis.axis_type = ChartAxisType::Category; + self.x2_axis.crossing = ChartAxisCrossing::Max; + self.x2_axis.is_hidden = true; + self.x2_axis.label_position = ChartAxisLabelPosition::None; + + self.y2_axis.axis_type = ChartAxisType::Value; + self.y2_axis.axis_position = ChartAxisPosition::Left; + self.chart_group_type = ChartType::Column; if self.chart_type == ChartType::Column { @@ -2445,6 +2511,14 @@ impl Chart { self.y_axis.title.is_horizontal = true; self.y_axis.major_gridlines = true; + self.x2_axis.axis_type = ChartAxisType::Category; + self.x2_axis.crossing = ChartAxisCrossing::Max; + self.x2_axis.is_hidden = true; + self.x2_axis.label_position = ChartAxisLabelPosition::None; + + self.y2_axis.axis_type = ChartAxisType::Value; + self.y2_axis.axis_position = ChartAxisPosition::Left; + self.chart_group_type = ChartType::Line; if self.chart_type == ChartType::Line { @@ -2500,6 +2574,16 @@ impl Chart { self.y_axis.title.is_horizontal = true; self.y_axis.major_gridlines = true; + self.x2_axis.axis_type = ChartAxisType::Value; + self.x2_axis.position_between_ticks = false; + self.x2_axis.crossing = ChartAxisCrossing::Max; + self.x2_axis.is_hidden = true; + self.x2_axis.label_position = ChartAxisLabelPosition::None; + + self.y2_axis.axis_type = ChartAxisType::Value; + self.y2_axis.axis_position = ChartAxisPosition::Left; + self.y2_axis.position_between_ticks = false; + self.chart_group_type = ChartType::Scatter; self.default_label_position = ChartDataLabelPosition::Right; @@ -2509,6 +2593,8 @@ impl Chart { // Initialize stock charts. fn initialize_stock_chart(mut self) -> Chart { + self.has_crosses = false; + self.x_axis.axis_type = ChartAxisType::Date; self.x_axis.axis_position = ChartAxisPosition::Bottom; self.x_axis.automatic = true; @@ -2518,6 +2604,15 @@ impl Chart { self.y_axis.title.is_horizontal = true; self.y_axis.major_gridlines = true; + self.x2_axis.axis_type = ChartAxisType::Date; + self.x2_axis.crossing = ChartAxisCrossing::Max; + self.x2_axis.is_hidden = true; + self.x2_axis.label_position = ChartAxisLabelPosition::None; + self.x2_axis.automatic = true; + + self.y2_axis.axis_type = ChartAxisType::Value; + self.y2_axis.axis_position = ChartAxisPosition::Left; + self.chart_group_type = ChartType::Stock; self.default_label_position = ChartDataLabelPosition::Right; @@ -2525,14 +2620,20 @@ impl Chart { } // Write the element for Column charts. - fn write_area_chart(&mut self) { + fn write_area_chart(&mut self, primary_axis: bool) { + let series = self.get_series(primary_axis); + + if series.is_empty() { + return; + } + self.writer.xml_start_tag_only("c:areaChart"); // Write the c:grouping element. self.write_grouping(); // Write the c:ser elements. - self.write_series(); + self.write_series(&series); if self.has_drop_lines { // Write the c:dropLines element. @@ -2540,13 +2641,19 @@ impl Chart { } // Write the c:axId elements. - self.write_ax_ids(); + self.write_ax_ids(primary_axis); self.writer.xml_end_tag("c:areaChart"); } // Write the element for Bar charts. - fn write_bar_chart(&mut self) { + fn write_bar_chart(&mut self, primary_axis: bool) { + let series = self.get_series(primary_axis); + + if series.is_empty() { + return; + } + self.writer.xml_start_tag_only("c:barChart"); // Write the c:barDir element. @@ -2556,7 +2663,7 @@ impl Chart { self.write_grouping(); // Write the c:ser elements. - self.write_series(); + self.write_series(&series); if self.gap != 150 { // Write the c:gapWidth element. @@ -2569,13 +2676,19 @@ impl Chart { } // Write the c:axId elements. - self.write_ax_ids(); + self.write_ax_ids(primary_axis); self.writer.xml_end_tag("c:barChart"); } // Write the element for Column charts. - fn write_column_chart(&mut self) { + fn write_column_chart(&mut self, primary_axis: bool) { + let series = self.get_series(primary_axis); + + if series.is_empty() { + return; + } + self.writer.xml_start_tag_only("c:barChart"); // Write the c:barDir element. @@ -2585,7 +2698,7 @@ impl Chart { self.write_grouping(); // Write the c:ser elements. - self.write_series(); + self.write_series(&series); if self.gap != 150 { // Write the c:gapWidth element. @@ -2598,20 +2711,26 @@ impl Chart { } // Write the c:axId elements. - self.write_ax_ids(); + self.write_ax_ids(primary_axis); self.writer.xml_end_tag("c:barChart"); } // Write the element for Column charts. - fn write_doughnut_chart(&mut self) { + fn write_doughnut_chart(&mut self, primary_axis: bool) { + let series = self.get_series(primary_axis); + + if series.is_empty() { + return; + } + self.writer.xml_start_tag_only("c:doughnutChart"); // Write the c:varyColors element. self.write_vary_colors(); // Write the c:ser elements. - self.write_series(); + self.write_series(&series); // Write the c:firstSliceAng element. self.write_first_slice_ang(); @@ -2623,14 +2742,20 @@ impl Chart { } // Write the element. - fn write_line_chart(&mut self) { + fn write_line_chart(&mut self, primary_axis: bool) { + let series = self.get_series(primary_axis); + + if series.is_empty() { + return; + } + self.writer.xml_start_tag_only("c:lineChart"); // Write the c:grouping element. self.write_grouping(); // Write the c:ser elements. - self.write_series(); + self.write_series(&series); if self.has_drop_lines { // Write the c:dropLines element. @@ -2651,20 +2776,26 @@ impl Chart { self.write_marker_value(); // Write the c:axId elements. - self.write_ax_ids(); + self.write_ax_ids(primary_axis); self.writer.xml_end_tag("c:lineChart"); } // Write the element for Column charts. - fn write_pie_chart(&mut self) { + fn write_pie_chart(&mut self, primary_axis: bool) { + let series = self.get_series(primary_axis); + + if series.is_empty() { + return; + } + self.writer.xml_start_tag_only("c:pieChart"); // Write the c:varyColors element. self.write_vary_colors(); // Write the c:ser elements. - self.write_series(); + self.write_series(&series); // Write the c:firstSliceAng element. self.write_first_slice_ang(); @@ -2673,50 +2804,68 @@ impl Chart { } // Write the element. - fn write_radar_chart(&mut self) { + fn write_radar_chart(&mut self, primary_axis: bool) { + let series = self.get_series(primary_axis); + + if series.is_empty() { + return; + } + self.writer.xml_start_tag_only("c:radarChart"); // Write the c:radarStyle element. self.write_radar_style(); // Write the c:ser elements. - self.write_series(); + self.write_series(&series); // Write the c:axId elements. - self.write_ax_ids(); + self.write_ax_ids(primary_axis); self.writer.xml_end_tag("c:radarChart"); } // Write the element. - fn write_scatter_chart(&mut self) { + fn write_scatter_chart(&mut self, primary_axis: bool) { + let mut series = self.get_series(primary_axis); + + if series.is_empty() { + return; + } + self.writer.xml_start_tag_only("c:scatterChart"); // Write the c:scatterStyle element. self.write_scatter_style(); // Write the c:ser elements. - self.write_scatter_series(); + self.write_scatter_series(&mut series); // Write the c:axId elements. - self.write_ax_ids(); + self.write_ax_ids(primary_axis); self.writer.xml_end_tag("c:scatterChart"); } // Write the element. - fn write_stock_chart(&mut self) { + fn write_stock_chart(&mut self, primary_axis: bool) { + let series = self.get_series(primary_axis); + + if series.is_empty() { + return; + } + self.writer.xml_start_tag_only("c:stockChart"); // Write the c:ser elements. - self.write_series(); + self.write_series(&series); if self.has_drop_lines { // Write the c:dropLines element. self.write_drop_lines(); } - if self.has_high_low_lines { + if primary_axis && self.has_high_low_lines { // Write the c:hiLowLines element. self.write_hi_low_lines(); } @@ -2727,7 +2876,7 @@ impl Chart { } // Write the c:axId elements. - self.write_ax_ids(); + self.write_ax_ids(primary_axis); self.writer.xml_end_tag("c:stockChart"); } @@ -2846,6 +2995,7 @@ impl Chart { // Write the element. fn write_plot_area(&mut self) { + self.series_index = 0; self.writer.xml_start_tag_only("c:plotArea"); // Write the c:layout element. @@ -2857,16 +3007,19 @@ impl 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(); + combined_chart.series_index = self.series.len(); mem::swap(&mut combined_chart.writer, &mut self.writer); combined_chart.write_chart_type(); mem::swap(&mut combined_chart.writer, &mut self.writer); } + let mut x_axis = self.x_axis.clone(); + let mut y_axis = self.y_axis.clone(); + // Reverse the X and Y axes for Bar charts. if self.chart_group_type == ChartType::Bar { - std::mem::swap(&mut self.x_axis, &mut self.y_axis); + std::mem::swap(&mut x_axis, &mut y_axis); } match self.chart_group_type { @@ -2874,28 +3027,59 @@ impl Chart { ChartType::Scatter => { // Write the c:valAx element. - self.write_cat_val_ax(); + self.write_cat_val_ax(&x_axis, &y_axis, self.axis_ids); // Write the c:valAx element. - self.write_val_ax(); + self.write_val_ax(&x_axis, &y_axis, self.axis_ids); } _ => { if self.x_axis.axis_type == ChartAxisType::Date { // Write the c:dateAx element. - self.write_date_ax(); + self.write_date_ax(&x_axis, &y_axis, self.axis_ids); } else { // Write the c:catAx element. - self.write_cat_ax(); + self.write_cat_ax(&x_axis, &y_axis, self.axis_ids); } // Write the c:valAx element. - self.write_val_ax(); + self.write_val_ax(&x_axis, &y_axis, self.axis_ids); } } - // Reset the X and Y axes for Bar charts. - if self.chart_group_type == ChartType::Bar { - std::mem::swap(&mut self.x_axis, &mut self.y_axis); + // TODO + self.check_for_secondary_axis(); + if self.has_secondary_axis { + let mut x_axis = self.x2_axis.clone(); + let mut y_axis = self.y2_axis.clone(); + + // Reverse the X and Y axes for Bar charts. + if self.chart_group_type == ChartType::Bar { + std::mem::swap(&mut x_axis, &mut y_axis); + } + + match self.chart_group_type { + ChartType::Pie | ChartType::Doughnut => {} + + ChartType::Scatter => { + // Write the c:valAx element. + self.write_cat_val_ax(&x_axis, &y_axis, self.axis2_ids); + + // Write the c:valAx element. + self.write_val_ax(&x_axis, &y_axis, self.axis2_ids); + } + _ => { + // Write the c:valAx element. + self.write_val_ax(&x_axis, &y_axis, self.axis2_ids); + + if self.x_axis.axis_type == ChartAxisType::Date { + // Write the c:dateAx element. + self.write_date_ax(&x_axis, &y_axis, self.axis2_ids); + } else { + // Write the c:catAx element. + self.write_cat_ax(&x_axis, &y_axis, self.axis2_ids); + } + } + } } // Write the c:dTable element. @@ -2913,37 +3097,52 @@ impl Chart { fn write_chart_type(&mut self) { match self.chart_type { ChartType::Area | ChartType::AreaStacked | ChartType::AreaPercentStacked => { - self.write_area_chart(); + self.write_area_chart(true); + self.write_area_chart(false); } ChartType::Bar | ChartType::BarStacked | ChartType::BarPercentStacked => { - self.write_bar_chart(); + self.write_bar_chart(true); + self.write_bar_chart(false); } ChartType::Column | ChartType::ColumnStacked | ChartType::ColumnPercentStacked => { - self.write_column_chart(); + self.write_column_chart(true); + self.write_column_chart(false); } - ChartType::Doughnut => self.write_doughnut_chart(), + ChartType::Doughnut => { + self.write_doughnut_chart(true); + self.write_doughnut_chart(false); + } ChartType::Line | ChartType::LineStacked | ChartType::LinePercentStacked => { - self.write_line_chart(); + self.write_line_chart(true); + self.write_line_chart(false); } - ChartType::Pie => self.write_pie_chart(), + ChartType::Pie => { + self.write_pie_chart(true); + self.write_pie_chart(false); + } ChartType::Radar | ChartType::RadarWithMarkers | ChartType::RadarFilled => { - self.write_radar_chart(); + self.write_radar_chart(true); + self.write_radar_chart(false); } ChartType::Scatter | ChartType::ScatterStraight | ChartType::ScatterStraightWithMarkers | ChartType::ScatterSmooth - | ChartType::ScatterSmoothWithMarkers => self.write_scatter_chart(), + | ChartType::ScatterSmoothWithMarkers => { + self.write_scatter_chart(true); + self.write_scatter_chart(false); + } ChartType::Stock => { - self.write_stock_chart(); + self.write_stock_chart(true); + self.write_stock_chart(false); } } } @@ -2982,9 +3181,22 @@ impl Chart { self.writer.xml_empty_tag("c:scatterStyle", &attributes); } + // Get the primary/secondary series for the chart. + fn get_series(&self, primary_axis: bool) -> Vec { + let mut series_copy = vec![]; + + for each_series in &self.series { + if each_series.y2_axis != primary_axis { + series_copy.push(each_series.clone()); + } + } + + series_copy + } + // Write the element. - fn write_series(&mut self) { - for (index, series) in self.series.clone().iter_mut().enumerate() { + fn write_series(&mut self, series: &Vec) { + for series in series { let max_points = series.value_range.number_of_points(); self.writer.xml_start_tag_only("c:ser"); @@ -3000,10 +3212,10 @@ impl Chart { } // Write the c:idx element. - self.write_idx(self.base_series_index + index); + self.write_idx(self.series_index); // Write the c:order element. - self.write_order(self.base_series_index + index); + self.write_order(self.series_index); self.write_series_title(&series.title); @@ -3077,22 +3289,24 @@ impl Chart { } } + self.series_index += 1; + self.writer.xml_end_tag("c:ser"); } } // Write the element for scatter charts. - fn write_scatter_series(&mut self) { - for (index, series) in self.series.clone().iter_mut().enumerate() { + fn write_scatter_series(&mut self, series: &mut Vec) { + for series in series { let max_points = series.value_range.number_of_points(); self.writer.xml_start_tag_only("c:ser"); // Write the c:idx element. - self.write_idx(index); + self.write_idx(self.series_index); // Write the c:order element. - self.write_order(index); + self.write_order(self.series_index); self.write_series_title(&series.title); @@ -3158,6 +3372,8 @@ impl Chart { } } + self.series_index += 1; + self.writer.xml_end_tag("c:ser"); } } @@ -3390,9 +3606,14 @@ impl Chart { } // Write both elements. - fn write_ax_ids(&mut self) { - self.write_ax_id(self.axis_ids.0); - self.write_ax_id(self.axis_ids.1); + fn write_ax_ids(&mut self, primary_axis: bool) { + if primary_axis { + self.write_ax_id(self.axis_ids.0); + self.write_ax_id(self.axis_ids.1); + } else { + self.write_ax_id(self.axis2_ids.0); + self.write_ax_id(self.axis2_ids.1); + } } // Write the element. @@ -3407,101 +3628,95 @@ impl Chart { // ----------------------------------------------------------------------- // Write the element. - fn write_cat_ax(&mut self) { + fn write_cat_ax(&mut self, x_axis: &ChartAxis, y_axis: &ChartAxis, axis_ids: (u32, u32)) { self.writer.xml_start_tag_only("c:catAx"); - self.write_ax_id(self.axis_ids.0); + self.write_ax_id(axis_ids.0); // Write the c:scaling element. - self.write_scaling(&self.x_axis.clone()); + self.write_scaling(x_axis); - if self.x_axis.is_hidden { + if x_axis.is_hidden { self.write_delete(); } // Write the c:axPos element. - self.write_ax_pos( - self.x_axis.axis_position, - self.y_axis.reverse, - self.y_axis.crossing, - ); + self.write_ax_pos(x_axis.axis_position, y_axis.reverse, y_axis.crossing); - self.write_major_gridlines(self.x_axis.clone()); - self.write_minor_gridlines(self.x_axis.clone()); + self.write_major_gridlines(x_axis); + self.write_minor_gridlines(x_axis); // Write the c:title element. - self.write_chart_title(&self.x_axis.title.clone()); + self.write_chart_title(&x_axis.title); // Write the c:numFmt element. - if !self.x_axis.num_format.is_empty() { - self.write_number_format( - &self.x_axis.num_format.clone(), - self.x_axis.num_format_linked_to_source, - ); + if !x_axis.num_format.is_empty() { + self.write_number_format(&x_axis.num_format, x_axis.num_format_linked_to_source); } else if self.category_has_num_format { self.write_number_format("General", true); } // Write the c:majorTickMark element. - if let Some(tick_type) = self.x_axis.major_tick_type { + if let Some(tick_type) = x_axis.major_tick_type { self.write_major_tick_mark(tick_type); } // Write the c:minorTickMark element. - if let Some(tick_type) = self.x_axis.minor_tick_type { + if let Some(tick_type) = x_axis.minor_tick_type { self.write_minor_tick_mark(tick_type); } // Write the c:tickLblPos element. - self.write_tick_label_position(self.x_axis.label_position); + self.write_tick_label_position(x_axis.label_position); - if self.x_axis.format.has_formatting() { + if x_axis.format.has_formatting() { // Write the c:spPr formatting element. - self.write_sp_pr(&self.x_axis.format.clone()); + self.write_sp_pr(&x_axis.format); } // Write the axis font elements. - if let Some(font) = &self.x_axis.font { - self.write_axis_font(&font.clone()); + if let Some(font) = &x_axis.font { + self.write_axis_font(font); } // Write the c:crossAx element. - self.write_cross_ax(self.axis_ids.1); + self.write_cross_ax(axis_ids.1); - // Write the c:crosses element. Note, the X crossing comes from the Y - // axis. - match self.y_axis.crossing { - ChartAxisCrossing::Automatic | ChartAxisCrossing::Min | ChartAxisCrossing::Max => { - self.write_crosses(&self.y_axis.crossing.to_string()); - } - ChartAxisCrossing::AxisValue(_) => { - self.write_crosses_at(&self.y_axis.crossing.to_string()); - } - ChartAxisCrossing::CategoryNumber(_) => { - // Ignore Category crossing on a Value axis. - self.write_crosses(&ChartAxisCrossing::Automatic.to_string()); + // Write the c:crosses element. Note, the X crossing comes from the Y axis. + if self.has_crosses || !x_axis.is_hidden { + match y_axis.crossing { + ChartAxisCrossing::Automatic | ChartAxisCrossing::Min | ChartAxisCrossing::Max => { + self.write_crosses(&y_axis.crossing.to_string()); + } + ChartAxisCrossing::AxisValue(_) => { + self.write_crosses_at(&y_axis.crossing.to_string()); + } + ChartAxisCrossing::CategoryNumber(_) => { + // Ignore Category crossing on a Value axis. + self.write_crosses(&ChartAxisCrossing::Automatic.to_string()); + } } } // Write the c:auto element. - if !self.x_axis.automatic { + if !x_axis.automatic { self.write_auto(); } // Write the c:lblAlgn element. - self.write_lbl_algn(&self.x_axis.label_alignment.to_string()); + self.write_lbl_algn(&x_axis.label_alignment.to_string()); // Write the c:lblOffset element. self.write_lbl_offset(); // Write the c:tickLblSkip element. - if self.x_axis.label_interval > 1 { - self.write_tick_lbl_skip(self.x_axis.label_interval); + if x_axis.label_interval > 1 { + self.write_tick_lbl_skip(x_axis.label_interval); } // Write the c:tickMarkSkip element. - if self.x_axis.tick_interval > 1 { - self.write_tick_mark_skip(self.x_axis.tick_interval); + if x_axis.tick_interval > 1 { + self.write_tick_mark_skip(x_axis.tick_interval); } self.writer.xml_end_tag("c:catAx"); @@ -3512,84 +3727,78 @@ impl Chart { // ----------------------------------------------------------------------- // Write the element. - fn write_date_ax(&mut self) { + fn write_date_ax(&mut self, x_axis: &ChartAxis, y_axis: &ChartAxis, axis_ids: (u32, u32)) { self.writer.xml_start_tag_only("c:dateAx"); - self.write_ax_id(self.axis_ids.0); + self.write_ax_id(axis_ids.0); // Write the c:scaling element. - self.write_scaling(&self.x_axis.clone()); + self.write_scaling(x_axis); - if self.x_axis.is_hidden { + if x_axis.is_hidden { self.write_delete(); } // Write the c:axPos element. - self.write_ax_pos( - self.x_axis.axis_position, - self.y_axis.reverse, - self.y_axis.crossing, - ); + self.write_ax_pos(x_axis.axis_position, y_axis.reverse, y_axis.crossing); - self.write_major_gridlines(self.x_axis.clone()); - self.write_minor_gridlines(self.x_axis.clone()); + self.write_major_gridlines(x_axis); + self.write_minor_gridlines(x_axis); // Write the c:title element. - self.write_chart_title(&self.x_axis.title.clone()); + self.write_chart_title(&x_axis.title); // Write the c:numFmt element. - if !self.x_axis.num_format.is_empty() { - self.write_number_format( - &self.x_axis.num_format.clone(), - self.x_axis.num_format_linked_to_source, - ); + if !x_axis.num_format.is_empty() { + self.write_number_format(&x_axis.num_format, x_axis.num_format_linked_to_source); } else if self.category_has_num_format { self.write_number_format("dd/mm/yyyy", true); } // Write the c:majorTickMark element. - if let Some(tick_type) = self.x_axis.major_tick_type { + if let Some(tick_type) = x_axis.major_tick_type { self.write_major_tick_mark(tick_type); } // Write the c:minorTickMark element. - if let Some(tick_type) = self.x_axis.minor_tick_type { + if let Some(tick_type) = x_axis.minor_tick_type { self.write_minor_tick_mark(tick_type); } // Write the c:tickLblPos element. - self.write_tick_label_position(self.x_axis.label_position); + self.write_tick_label_position(x_axis.label_position); - if self.x_axis.format.has_formatting() { + if x_axis.format.has_formatting() { // Write the c:spPr formatting element. - self.write_sp_pr(&self.x_axis.format.clone()); + self.write_sp_pr(&x_axis.format); } // Write the axis font elements. - if let Some(font) = &self.x_axis.font { + if let Some(font) = &x_axis.font { self.write_axis_font(&font.clone()); } // Write the c:crossAx element. - self.write_cross_ax(self.axis_ids.1); + self.write_cross_ax(axis_ids.1); - // Write the c:crosses element. Note, the X crossing comes from the Y - // axis. - match self.y_axis.crossing { - ChartAxisCrossing::Automatic | ChartAxisCrossing::Min | ChartAxisCrossing::Max => { - self.write_crosses(&self.y_axis.crossing.to_string()); - } - ChartAxisCrossing::AxisValue(_) => { - self.write_crosses_at(&self.y_axis.crossing.to_string()); - } - ChartAxisCrossing::CategoryNumber(_) => { - // Ignore Category crossing on a Value axis. - self.write_crosses(&ChartAxisCrossing::Automatic.to_string()); + // Write the c:crosses element. Note, the X crossing comes from the Y axis. + if self.has_crosses || !x_axis.is_hidden { + match y_axis.crossing { + ChartAxisCrossing::Automatic | ChartAxisCrossing::Min | ChartAxisCrossing::Max => { + self.write_crosses(&y_axis.crossing.to_string()); + } + ChartAxisCrossing::AxisValue(_) => { + self.write_crosses_at(&y_axis.crossing.to_string()); + } + ChartAxisCrossing::CategoryNumber(_) => { + // Ignore Category crossing on a Value axis. + self.write_crosses(&ChartAxisCrossing::Automatic.to_string()); + } } } // Write the c:auto element. - if self.x_axis.automatic { + if x_axis.automatic { self.write_auto(); } @@ -3597,32 +3806,32 @@ impl Chart { self.write_lbl_offset(); // Write the c:tickLblSkip element. - if self.x_axis.label_interval > 1 { - self.write_tick_lbl_skip(self.x_axis.label_interval); + if x_axis.label_interval > 1 { + self.write_tick_lbl_skip(x_axis.label_interval); } // Write the c:tickMarkSkip element. - if self.x_axis.tick_interval > 1 { - self.write_tick_mark_skip(self.x_axis.tick_interval); + if x_axis.tick_interval > 1 { + self.write_tick_mark_skip(x_axis.tick_interval); } // Write the c:majorUnit element. - if !self.x_axis.major_unit.is_empty() { - self.write_major_unit(self.x_axis.major_unit.clone()); + if !x_axis.major_unit.is_empty() { + self.write_major_unit(&x_axis.major_unit); } // Write the c:majorTimeUnit element. - if let Some(unit) = self.x_axis.major_unit_date_type { + if let Some(unit) = x_axis.major_unit_date_type { self.write_major_time_unit(unit); } // Write the c:minorUnit element. - if !self.x_axis.minor_unit.is_empty() { - self.write_minor_unit(self.x_axis.minor_unit.clone()); + if !x_axis.minor_unit.is_empty() { + self.write_minor_unit(&x_axis.minor_unit); } // Write the c:minorTimeUnit element. - if let Some(unit) = self.x_axis.minor_unit_date_type { + if let Some(unit) = x_axis.minor_unit_date_type { self.write_minor_time_unit(unit); } @@ -3634,97 +3843,87 @@ impl Chart { // ----------------------------------------------------------------------- // Write the element. - fn write_val_ax(&mut self) { + fn write_val_ax(&mut self, x_axis: &ChartAxis, y_axis: &ChartAxis, axis_ids: (u32, u32)) { self.writer.xml_start_tag_only("c:valAx"); - self.write_ax_id(self.axis_ids.1); + self.write_ax_id(axis_ids.1); // Write the c:scaling element. - self.write_scaling(&self.y_axis.clone()); + self.write_scaling(y_axis); - if self.y_axis.is_hidden { + if y_axis.is_hidden { self.write_delete(); } // Write the c:axPos element. - self.write_ax_pos( - self.y_axis.axis_position, - self.x_axis.reverse, - self.x_axis.crossing, - ); + self.write_ax_pos(y_axis.axis_position, x_axis.reverse, x_axis.crossing); // Write the Gridlines elements. - self.write_major_gridlines(self.y_axis.clone()); - self.write_minor_gridlines(self.y_axis.clone()); + self.write_major_gridlines(y_axis); + self.write_minor_gridlines(y_axis); // Write the c:title element. - self.write_chart_title(&self.y_axis.title.clone()); + self.write_chart_title(&y_axis.title); // Write the c:numFmt element. - if self.y_axis.num_format.is_empty() { + if y_axis.num_format.is_empty() { self.write_number_format(&self.default_num_format.clone(), true); } else { - self.write_number_format( - &self.y_axis.num_format.clone(), - self.y_axis.num_format_linked_to_source, - ); + self.write_number_format(&y_axis.num_format, y_axis.num_format_linked_to_source); } // Write the c:majorTickMark element. - if let Some(position) = self.y_axis.major_tick_type { + if let Some(position) = y_axis.major_tick_type { self.write_major_tick_mark(position); } // Write the c:minorTickMark element. - if let Some(position) = self.y_axis.minor_tick_type { + if let Some(position) = y_axis.minor_tick_type { self.write_minor_tick_mark(position); } // Write the c:tickLblPos element. - self.write_tick_label_position(self.y_axis.label_position); + self.write_tick_label_position(y_axis.label_position); - if self.y_axis.format.has_formatting() { + if y_axis.format.has_formatting() { // Write the c:spPr formatting element. - self.write_sp_pr(&self.y_axis.format.clone()); + self.write_sp_pr(&y_axis.format); } // Write the axis font elements. - if let Some(font) = &self.y_axis.font { - self.write_axis_font(&font.clone()); + if let Some(font) = &y_axis.font { + self.write_axis_font(font); } // Write the c:crossAx element. - self.write_cross_ax(self.axis_ids.0); + self.write_cross_ax(axis_ids.0); // Write the c:crosses element. Note, the Y crossing comes from the X // axis. - match self.x_axis.crossing { + match x_axis.crossing { ChartAxisCrossing::Automatic | ChartAxisCrossing::Min | ChartAxisCrossing::Max => { - self.write_crosses(&self.x_axis.crossing.to_string()); + self.write_crosses(&x_axis.crossing.to_string()); } ChartAxisCrossing::CategoryNumber(_) | ChartAxisCrossing::AxisValue(_) => { - self.write_crosses_at(&self.x_axis.crossing.to_string()); + self.write_crosses_at(&x_axis.crossing.to_string()); } } // Write the c:crossBetween element. - self.write_cross_between(self.x_axis.position_between_ticks); + self.write_cross_between(x_axis.position_between_ticks); // Write the c:majorUnit element. - if self.y_axis.axis_type != ChartAxisType::Category && !self.y_axis.major_unit.is_empty() { - self.write_major_unit(self.y_axis.major_unit.clone()); + if y_axis.axis_type != ChartAxisType::Category && !y_axis.major_unit.is_empty() { + self.write_major_unit(&y_axis.major_unit); } // Write the c:minorUnit element. - if self.y_axis.axis_type != ChartAxisType::Category && !self.y_axis.minor_unit.is_empty() { - self.write_minor_unit(self.y_axis.minor_unit.clone()); + if y_axis.axis_type != ChartAxisType::Category && !y_axis.minor_unit.is_empty() { + self.write_minor_unit(&y_axis.minor_unit); } // Write the c:dispUnits element. - if self.y_axis.display_units_type != ChartAxisDisplayUnitType::None { - self.write_disp_units( - self.y_axis.display_units_type, - self.y_axis.display_units_visible, - ); + if y_axis.display_units_type != ChartAxisDisplayUnitType::None { + self.write_disp_units(y_axis.display_units_type, y_axis.display_units_visible); } self.writer.xml_end_tag("c:valAx"); @@ -3735,98 +3934,88 @@ impl Chart { // ----------------------------------------------------------------------- // Write the category element for scatter charts. - fn write_cat_val_ax(&mut self) { + fn write_cat_val_ax(&mut self, x_axis: &ChartAxis, y_axis: &ChartAxis, axis_ids: (u32, u32)) { self.writer.xml_start_tag_only("c:valAx"); - self.write_ax_id(self.axis_ids.0); + self.write_ax_id(axis_ids.0); // Write the c:scaling element. - self.write_scaling(&self.x_axis.clone()); + self.write_scaling(x_axis); - if self.x_axis.is_hidden { + if x_axis.is_hidden { self.write_delete(); } // Write the c:axPos element. - self.write_ax_pos( - self.x_axis.axis_position, - self.y_axis.reverse, - self.y_axis.crossing, - ); + self.write_ax_pos(x_axis.axis_position, y_axis.reverse, y_axis.crossing); // Write the Gridlines elements. - self.write_major_gridlines(self.x_axis.clone()); - self.write_minor_gridlines(self.x_axis.clone()); + self.write_major_gridlines(x_axis); + self.write_minor_gridlines(x_axis); // Write the c:title element. - self.write_chart_title(&self.x_axis.title.clone()); + self.write_chart_title(&x_axis.title); // Write the c:numFmt element. - if self.x_axis.num_format.is_empty() { + if x_axis.num_format.is_empty() { self.write_number_format(&self.default_num_format.clone(), true); } else { - self.write_number_format( - &self.x_axis.num_format.clone(), - self.x_axis.num_format_linked_to_source, - ); + self.write_number_format(&x_axis.num_format, x_axis.num_format_linked_to_source); } // Write the c:majorTickMark element. - if let Some(position) = self.x_axis.major_tick_type { + if let Some(position) = x_axis.major_tick_type { self.write_major_tick_mark(position); } // Write the c:minorTickMark element. - if let Some(position) = self.x_axis.minor_tick_type { + if let Some(position) = x_axis.minor_tick_type { self.write_minor_tick_mark(position); } // Write the c:tickLblPos element. - self.write_tick_label_position(self.x_axis.label_position); + self.write_tick_label_position(x_axis.label_position); - if self.x_axis.format.has_formatting() { + if x_axis.format.has_formatting() { // Write the c:spPr formatting element. - self.write_sp_pr(&self.x_axis.format.clone()); + self.write_sp_pr(&x_axis.format); } // Write the axis font elements. - if let Some(font) = &self.x_axis.font { - self.write_axis_font(&font.clone()); + if let Some(font) = &x_axis.font { + self.write_axis_font(font); } // Write the c:crossAx element. - self.write_cross_ax(self.axis_ids.1); + self.write_cross_ax(axis_ids.1); // Write the c:crosses element. Note, the X crossing comes from the Y // axis. - match self.y_axis.crossing { + match y_axis.crossing { ChartAxisCrossing::Automatic | ChartAxisCrossing::Min | ChartAxisCrossing::Max => { - self.write_crosses(&self.y_axis.crossing.to_string()); + self.write_crosses(&y_axis.crossing.to_string()); } ChartAxisCrossing::CategoryNumber(_) | ChartAxisCrossing::AxisValue(_) => { - self.write_crosses_at(&self.y_axis.crossing.to_string()); + self.write_crosses_at(&y_axis.crossing.to_string()); } } // Write the c:crossBetween element. - self.write_cross_between(self.y_axis.position_between_ticks); + self.write_cross_between(y_axis.position_between_ticks); // Write the c:majorUnit element. - if self.x_axis.axis_type != ChartAxisType::Category && !self.x_axis.major_unit.is_empty() { - self.write_major_unit(self.x_axis.major_unit.clone()); + if x_axis.axis_type != ChartAxisType::Category && !x_axis.major_unit.is_empty() { + self.write_major_unit(&x_axis.major_unit); } // Write the c:minorUnit element. - if self.x_axis.axis_type != ChartAxisType::Category && !self.x_axis.minor_unit.is_empty() { - self.write_minor_unit(self.x_axis.minor_unit.clone()); + if x_axis.axis_type != ChartAxisType::Category && !x_axis.minor_unit.is_empty() { + self.write_minor_unit(&x_axis.minor_unit); } // Write the c:dispUnits element. - if self.x_axis.display_units_type != ChartAxisDisplayUnitType::None { - self.write_disp_units( - self.x_axis.display_units_type, - self.x_axis.display_units_visible, - ); + if x_axis.display_units_type != ChartAxisDisplayUnitType::None { + self.write_disp_units(x_axis.display_units_type, x_axis.display_units_visible); } self.writer.xml_end_tag("c:valAx"); @@ -3918,7 +4107,7 @@ impl Chart { } // Write the element. - fn write_major_gridlines(&mut self, axis: ChartAxis) { + fn write_major_gridlines(&mut self, axis: &ChartAxis) { if axis.major_gridlines { if let Some(line) = &axis.major_gridlines_line { self.writer.xml_start_tag_only("c:majorGridlines"); @@ -3936,7 +4125,7 @@ impl Chart { } // Write the element. - fn write_minor_gridlines(&mut self, axis: ChartAxis) { + fn write_minor_gridlines(&mut self, axis: &ChartAxis) { if axis.minor_gridlines { if let Some(line) = &axis.minor_gridlines_line { self.writer.xml_start_tag_only("c:minorGridlines"); @@ -4028,15 +4217,15 @@ impl Chart { } // Write the element. - fn write_major_unit(&mut self, value: String) { - let attributes = [("val", value)]; + fn write_major_unit(&mut self, value: &String) { + let attributes = [("val", value.to_string())]; self.writer.xml_empty_tag("c:majorUnit", &attributes); } // Write the element. - fn write_minor_unit(&mut self, value: String) { - let attributes = [("val", value)]; + fn write_minor_unit(&mut self, value: &String) { + let attributes = [("val", value.to_string())]; self.writer.xml_empty_tag("c:minorUnit", &attributes); } @@ -5912,6 +6101,7 @@ pub struct ChartSeries { pub(crate) y_error_bars: Option, pub(crate) delete_from_legend: bool, pub(crate) smooth: Option, + pub(crate) y2_axis: bool, } #[allow(clippy::new_without_default)] @@ -6027,6 +6217,7 @@ impl ChartSeries { y_error_bars: None, delete_from_legend: false, smooth: None, + y2_axis: false, } } @@ -6193,6 +6384,12 @@ impl ChartSeries { self } + /// TODO set y2 axis + pub fn set_y2_axis(&mut self, enable: bool) -> &mut ChartSeries { + self.y2_axis = enable; + self + } + /// Add a name for a chart series. /// /// Set the name for the series. The name is displayed in the formula bar. diff --git a/src/worksheet.rs b/src/worksheet.rs index 5192951d..fb3805ac 100644 --- a/src/worksheet.rs +++ b/src/worksheet.rs @@ -12239,7 +12239,7 @@ impl Worksheet { pub(crate) fn prepare_worksheet_charts(&mut self, mut chart_id: u32, drawing_id: u32) -> u32 { for chart in self.charts.values_mut() { chart.id = chart_id; - chart.add_axis_ids(); + chart.add_axis_ids(chart_id); chart_id += 1; } diff --git a/tests/input/bootstrap71.xlsx b/tests/input/bootstrap71.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f73e50618324edd8afb183538f725bf97fc10894 GIT binary patch literal 7030 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 X8p`(&D)?m>V*_juIy@!$_33{A^d;{l literal 0 HcmV?d00001 diff --git a/tests/input/chart_area04.xlsx b/tests/input/chart_area04.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..25222cfbb8ac8745bf7370fb328337769b850a2d GIT binary patch literal 9274 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 literal 0 HcmV?d00001 diff --git a/tests/input/chart_bar24.xlsx b/tests/input/chart_bar24.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..26e690afa509e1678349a7106f1c3bd505bb57cf GIT binary patch literal 9306 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 Xm1L3NEdEikV*$+JhA>V3q)QmO8-|jUGrHE^ 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 literal 0 HcmV?d00001 diff --git a/tests/input/chart_line02.xlsx b/tests/input/chart_line02.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..842fe30a22c0b43d4907cbe691a5cedc7b0aee20 GIT binary patch literal 9295 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 OaRBBBLl~y|arZx+dIUKD literal 0 HcmV?d00001 diff --git a/tests/input/chart_scatter07.xlsx b/tests/input/chart_scatter07.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..76b387f53bf2f0c50ab870664bd3e9a5f6b92e5e GIT binary patch literal 9400 zcmeHN^;?wdx}E{)8XA%A4naUbB&EASL>eT9?(Pt!LAq1AOBgz&OF+6rx*N{uT6?<| z%YFWUbN2Vc7uPq>ea-tm_tT$(H0%Q$06YK@0058!kYA7-SOEcm>jwY;Heep8EdsW( zH?p$VRdKO4veRL4wzMG2oB`6M17M)P|L^gCZU;W9^vkugftn}}Fd|FoZ2E!Z<5fX0 z;@>jdz00}7Rl94NcDMGXc`T_x;VL=tJZe=Oe3g|SKA(p7)_@fFv@2}b^&e_%#?%x= zc`z>=nZ)Vgs3EN|#cTt5jK{7Tdk+_Z9ECL+6}&I*+Mf0i68Z!jz21~w6Q-olX%BTH zpVEiBw&8SL9-T(yS}*T>J=RP61XSf~K`wj^LOa-RUWyz}cYBA@W-#5vXi+as)5NV6 zDv@$jtm>s?5yYim0OQjcsU(ZcbJ1g$^-=()nAf^;8&`Uaus#~s(mv0oArns<7A z29E*orO}R@mFay{q~<&2K^Y5;p7M??A6rti7iMB7Q!+7UQZGM&U^JC!BJvEJkySu^ zp;$mzz=bM5)vbu$%GB7Ii5_69nkat?jBsW;BWX=)hh`V=N+INgNri1gIw z<}GxcvtyF^oV!omm}Vy-FmIf>tJNJ<~D5HJvvA^7o`2asI|plU}+@zH@>ST+@gp@N;%lmQK%C(&cdK&*VfR z*V215b6%mC;3vFm96;Oo}f_i={PfJwI0uy(2Dkk}E8 z)`!F&`(~4jz}rh>39yvjj&PjwPGNMms(#E)*kc*Fa+VhT&vG0|>jx?DON5S#Lq~ZA zv*M)D@Y~vMgY>>@shyh^8idX0lHuW=Redry+(>^pskC&O8GKOUw#K~Uq11}wbSo+G zKGT~%C%5}KoZB|ZJEZ_Cydg4%Qi=s8Y>lI=!$(ovqrIi}iMcKbsA?9}r@I?U64Pe1 z)odQX&6tnR-fKnyw}J-ckQYGo?>DAqQ2IPpP!cv=h!#g!FBeIhAB*N9*z7^Z`~?^~ zp6tk*G)CB$nMb1fA82)|ioFpgfFHAnGj8uV&sgJz?&&|Ra%};_ECssjTd)8C!5_PB zXshRBW@Tc>`sWS%55r7WhWv0~&Co1Cad+5?ETOO(k=>^bfN&0Al%0~0ReGw&==DB2 z*N7})m?iRt==m(`;HB{}@vNkMUt$#Npxt0LW{sM$_F?VRRVA*;pkF~o<%_s$5nfN_ zl+=$wgkjCIWRx%~$hOk+t_%2@>!OTL>g;BvMMkW|I$$&m52zJ?xsa&8Uu)Y}H zEt)_?uxmrN!X^TXca=V#9~__1XU86T-BaJgs(wK#7T>>6^h!6<(e7K(p$a%`xK`>b zrD-oGVimP~MZ#RW&^|RiWvU}MmT-`kk@VgS#1r z#8*i5<;yzKE8PL&(>{5n>sk|hgJp2dwv4>?D!)AiFiIlF;^p51IK(zL{6;#r50o$apd> zZEWK>nK~3=XJu`2kPLX2^^TdaIx7OnUwA2n4hV8o*r!_9^5Pr6HA8JaG~N9YJ!3L0 z6d(^Dqbs9WODH_IPL|l2w|ZwW<&c$8$?^V!#WiNeXK!*EvcS|#c={={(y0Dpru+&8 z-L<~o^BK3akiQwaXvSZQ|KUaKOpT1}?fwinKOcUw`adoHmFPah2{6v^%Fhz3;vf$U zX7Tl1v^0soG0vTNo~QVg{%|oRPzjH$=OGwj%B+sFdjjtPtv#OjoXmDK{vfA* zpZr%0AF*7X$>BU4N2&5nGvPRai2nG=ZY1Tna%JEEE}zm?=9m2QUhm5=dO0-_{%I^x zj-c)9w4GT;Hv9c`u1M+UOfli-Jp+%i48$eX#Du~4F zL!5?krr@Zg$D^4QZ(w_aUOcdIhs}a<7^cxajYd1D81@qPR6?qPpkI0v{?X(_kSK+h z``y9{rNhaLUvP#91=X6(Y>`fhYjB4E=@^fxn^WM8^dpie zokw?RC)iG7NU`T;;!K#ym6UNkSoTmzJ zDaqX29%l#$iO7!5_6462G<8XR2d0Ji(8wm^!tYJQEo|nOAuhzHJ(^Y@zFt6(EoKr+Jh^>Q96iG1y}j~y$EsSfxbYn3=7jo#c8Xlyh?G&eC|ZxeWc>c4 z31bhp0m!3>PVTt!-D~8_dBT~O{W8RoX3Rn@xwD2H=a_@f@FARnFGK8 z_pHP=e7fL3{szu4r1ukr8e#h$ESG>`!9d*b$V6I0UytBrmUl0F^#=f8td|MpBA;Fo zjy6Rc!^x`5Ua>(UFi~7fQ@!rKUnn1czldH58hS^1Yk&Q1Bbak*7H=)qr(IIHD}F^yUTXkmg{Smr=Z8Cs zB=*y%R$6Q6p=uey>T8Bfwi-%`2CB+`%7PG0mW}5V#G4ivX8SVX(RB$9L*kheu&>h zEpfD{bi#MwVH|S}B5IJ3K>RG_Pl~mkaKM{NPZS$Tv>c3!=80WYCes6Xc`o)+o?+i! zkU+edJ+i`Lz!G?gm+T2E(#+s+MUo`WqD05boaKUT_eykk-;BFMQ7Mpkn1@OKd4w{e z;@i`D2ERQUZjocYb6_{)oJTxX_go&nFCVA1sMdqv)DK8I(sbi;H{qt&PMryg`CVM_ z^SCQDwP@PPno`@TbdVzJJ4jojpUHuMkB08N^Zdk|lf#tT*4A#}O9At09UP7P<_4EC zkMhl^tXSDY|HoVkb}y`?#%QI#Fm-tljBkAgMy>|sQu%jR)$!E{zOIBvDiO$JAoAz& zA!%GUqwtZDM8Kkc*)~0h8b3Pe{B11u*+Z`550FXNkO>mf*(HZ($;4>3kB$40K3{Mc zRFxnl$4+0y#?a;8@P2-ymCs>U^l(I`z^-nM!4D%|)erJDaX5hNn@($=EKjESq%{}h z?c3_P9r)z>PUj^G9V!uinu7OL^Zf#a4{43c^;uxa9Hrc|hg|YBid&0wdOrbcD|rff zNW_A%96fD#(Uw#!!_JNL##&i#=Tml>G6EL)1bSQs8X8AzARz1sE#sXQ>}OX+WR({e#*^>8N$Kzvv&7?VL^k=R;L@p@QouE%ZgS; zY?#$LHn-R^_v>;pjm`l~|Kc`}v1IU=ZILo(zaa{@OLG6~dE{jutDK5QYlEesk{2jS zIRssI9ID5jFjH__20K7fD_gi1yt=a__zbHG0Z|c`9>8$F0%8!eH8IKY5Uj!GKoLoh=yBR1n{FcB8l_>;1N zGe?X7(D&TLY@^mixvsRURO4Z~VOcr$g=E60y=E?i+s(ml8VvJnR7i+BR6aUr$vvZ!tFnU_(P{1Ge>j@A#tqlv2^*^-)Lxy8{nS~Z%w)_F-s z&-5&qr!}uV*1JXYe&~(3!k}PPjHiRaiXn%w(J9`%kBfB42 zXSOor%~v*1GyWll@Y#z-6g1276cIi0h#0||;UbIPR}fRf?1PHkDHB7!`8j3uW(|3H z4sfdR>hi(1P_l&#%WL}Dvq$tyTo9gZsf&%WyGxx=QNr**@0yq_*~9XNDlnd2&FNWB}tP|e3pm~b4DsorZrtlOgkE)sM^t|WxDY7tFbOF^kjBv`CQ zjo?CwyADZM4()=%Oe9byS)2(aw%^R`)8%-0b@BLitsUaG9L@b5tEM4G=pvu9`>5lo zZK7Z`(6%dpG=-T{qFl?V-1?B9@{Pluxdfx!I{D1T{O+CISykUeWHmA3%%xpjU60&C z7z=3&&W7Ctb>wGWTY@!9-at6TpL;u?v?wJj>#l^GKR2h1g$$BKx z2-b>LP4K+ojd``^Rf|z)ee;A#C*v%4?$ja0P-9HXQ8Y}wRIsbHAUYT>Z>uu*sjgO8 zFE*LZQUAbzW|1}RTgwxGy#|J~3xsvD4sxfF^=giOm`bq^#d9!vMnFbcZfj4m8LF>; zYO`UXgvKBGrU0r+oIkW_V5(;eO}&0zer6z5>T~k5Y#^_+syp&ZXJX4ynt)I|Axu*& zQ`5QAGVbV?v#8zgEbwRUJU2?YjQq(pu7-J?o~I49d{vj1Q^yS>5iC^N#|*(}q=HEe z%UnIeyg0ZyJi-i#$!$uPu_lOoLTAf+d%i?Px{ktdQkDBu)YQL7Pk;iYcuOxY>kv*w zg*cRu=ex16n#xL6vJ4(+z&5PZ;vF{E`Xu-}XJ1 zKS}iA=#oO`R+;y+$HSF?WeSTOb4tX(9fO((%P&WFhQO@Ic%4R$?$rmwKy^ zlChpPePZE>C{w+oKe z+zgQp;{KA1x-K&QLrvCaBt&^3C=t2*VU;{jAKO;$U39pJQ*?S!N5x-nh$beOCTu?? zjIx@YMPCBdQV8=OYhjX;jjD+T2j60MppYLV$~3Tj)!F{gNXv4UBDtf=pb%fBRv@Uu zR}W6#&9UfP7K%Gb>(5>BOsqDd5UT}6nmmQx<*dl)+&rIw#$cH zp(YxOO(P}t88vTgrbz}gzcyhAe7fJJf@7?nM0KXFKQ2E|H$;v)sd27ZSejg{b2$SN z>Tx`bqn=V8CftgBtc@pVqxK001_7L;_ar3=)9sZl?%WcaE009ufILw&Vz+AycTOEi z1mc|_KjRd$gd!as9 z2s#7$vkCB*qUl$;^fpEooXiFi+>5;=Wm;%-x56SP>thK`6v-(xvpZ`8sI|VRsjRyD z1RHILUlH_m+r(}D?ZfE#+Z{f~2L?H64>7m|Owy%vBqt27hxV?(+{v1>D%8BiWGi>A zhqKpP@%H2E(=~v1dJl{^=6SiPQce2tT=lqJ0RfEFT+P-zPr^+QU^5djPEznMZ)2xW z3V6gMKAY%&Ylx%9X>i{ZdWx){j*Wg)TTH|>G$yq?qXObkFu7)t2xUGywC8#*Nw71W z@YKfHA6*Io_Z7fISr4H&EiW7+_lFCe{7dG z$311ds8$Hc&B^=?}trjHP#}XdKh&Cx4;-EaKJ-F$Wh#Hj#vyeZy+t7F-=&L=5HU7}Jerass{@SVXtN8dLLTbj zdBQq^p~S<4 zW(=Ue5>LrO|lR%98}%89*N;d(2RF= z17dlAq1rbGelzO{WYMrpUnpKvYh_l&VOgT2K;Zg)VrkX~L3tNs24g%ZXCt2+K@uf> zhn4=6Lirh=B*B6DwlB|)TXG$$U`Yk`D;r%!73cTKJMEy8=UlvfxTrU#iH| z!d@j3r`Q*)@~iTh^ILq;;O?TNbD_@{XKNp>=`tEG%yYnq(`OmJSuZaZJ?JMx<%#_! z>qrAKK(kbS+$BJ6bi=WZZvjM%T@5VY2|Sv2*%sJb`_c+=Sb5FnQ)V4r=d=p+;?$1b zB<<*bdeFmmAji|_18%Sz$3399(H)Dbz84Jl^C} z=?VQGsfMS1y}3`804MbuU_Fi7qvQ4y+iJRtw|vj1Vw$dvZvH*I1;Q{x^V;8kJMk}V z{p!hMwc?do4BqbPqz`47$O`+)abz`p>8Q2)KA|E(c>AMk!x z^%vj()U81`>3)y(KEVAN_ZNUSv`B&i{Hl2Gi{3A&eu>KA{S^I2d37J*escE<;Q`_O z?f8}I-ADO*j_?Zu0DvWe7Vq~nhWp}w_w;`icPIIa_@91XK^hLq;vfA0G=M485O~Rd GJpB(g8bOu- literal 0 HcmV?d00001 diff --git a/tests/input/chart_stock02.xlsx b/tests/input/chart_stock02.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..a29fa0893c7431433f9d97a196fb7b321170e48a GIT binary patch literal 9723 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 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, [1, 2, 3, 4, 5])?; + worksheet.write_column(0, 1, [6, 8, 6, 4, 2])?; + + let mut chart = Chart::new(ChartType::Area); + // Test without chart ids + + chart.add_series().set_values(("Sheet1", 0, 0, 4, 0)); + + chart + .add_series() + .set_values(("Sheet1", 0, 1, 4, 1)) + .set_y2_axis(true); + + worksheet.insert_chart(8, 4, &chart)?; + + workbook.save(filename)?; + + Ok(()) +} + +#[test] +fn test_bootstrap71() { + let test_runner = common::TestRunner::new() + .set_name("bootstrap71") + .set_function(create_new_xlsx_file) + .ignore_elements("xl/workbook.xml", " 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, [1, 2, 3, 4, 5])?; + worksheet.write_column(0, 1, [6, 8, 6, 4, 2])?; + + let mut chart = Chart::new(ChartType::Area); + chart.set_axis_ids(63591168, 63592704); + chart.set_axis2_ids(74921856, 73764224); + + chart.add_series().set_values(("Sheet1", 0, 0, 4, 0)); + + chart + .add_series() + .set_values(("Sheet1", 0, 1, 4, 1)) + .set_y2_axis(true); + + worksheet.insert_chart(8, 4, &chart)?; + + workbook.save(filename)?; + + Ok(()) +} + +#[test] +fn test_chart_area04() { + let test_runner = common::TestRunner::new() + .set_name("chart_area04") + .set_function(create_new_xlsx_file) + .ignore_elements("xl/workbook.xml", " 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, [27, 33, 44, 12, 1])?; + worksheet.write_column(0, 1, [6, 8, 6, 4, 2])?; + + let mut chart = Chart::new(ChartType::Bar); + chart.set_axis_ids(63591168, 63592704); + chart.set_axis2_ids(65934464, 72628864); + + chart.add_series().set_values(("Sheet1", 0, 0, 4, 0)); + + chart + .add_series() + .set_values(("Sheet1", 0, 1, 4, 1)) + .set_y2_axis(true); + + worksheet.insert_chart(8, 4, &chart)?; + + workbook.save(filename)?; + + Ok(()) +} + +#[test] +fn test_chart_bar24() { + let test_runner = common::TestRunner::new() + .set_name("chart_bar24") + .ignore_elements("xl/workbook.xml", " 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, [1, 2, 3, 4, 5])?; + worksheet.write_column(0, 1, [6, 8, 6, 4, 2])?; + + let mut chart = Chart::new(ChartType::Column); + chart.set_axis_ids(63591936, 63593856); + chart.set_axis2_ids(63613568, 63612032); + + chart.add_series().set_values(("Sheet1", 0, 0, 4, 0)); + + chart + .add_series() + .set_values(("Sheet1", 0, 1, 4, 1)) + .set_y2_axis(true); + + worksheet.insert_chart(8, 4, &chart)?; + + workbook.save(filename)?; + + Ok(()) +} + +#[test] +fn test_chart_column04() { + let test_runner = common::TestRunner::new() + .set_name("chart_column04") + .set_function(create_new_xlsx_file) + .ignore_elements("xl/workbook.xml", " 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, [1, 2, 3, 4, 5])?; + worksheet.write_column(0, 1, [6, 8, 6, 4, 2])?; + + let mut chart = Chart::new(ChartType::Line); + chart.set_axis_ids(63593856, 63612032); + chart.set_axis2_ids(63615360, 63613568); + + chart.add_series().set_values(("Sheet1", 0, 0, 4, 0)); + + chart + .add_series() + .set_values(("Sheet1", 0, 1, 4, 1)) + .set_y2_axis(true); + + worksheet.insert_chart(8, 4, &chart)?; + + workbook.save(filename)?; + + Ok(()) +} + +#[test] +fn test_chart_line02() { + let test_runner = common::TestRunner::new() + .set_name("chart_line02") + .set_function(create_new_xlsx_file) + .ignore_elements("xl/workbook.xml", " 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, [27, 33, 44, 12, 1])?; + worksheet.write_column(0, 1, [6, 8, 6, 4, 2])?; + worksheet.write_column(0, 2, [20, 10, 30, 50, 40])?; + worksheet.write_column(0, 3, [0, 27, 23, 30, 40])?; + + let mut chart = Chart::new(ChartType::Scatter); + chart.set_axis_ids(63597952, 63616128); + chart.set_axis2_ids(63617664, 63619456); + + chart + .add_series() + .set_categories(("Sheet1", 0, 0, 4, 0)) + .set_values(("Sheet1", 0, 1, 4, 1)); + + chart + .add_series() + .set_categories(("Sheet1", 0, 2, 4, 2)) + .set_values(("Sheet1", 0, 3, 4, 3)) + .set_y2_axis(true); + + worksheet.insert_chart(8, 4, &chart)?; + + workbook.save(filename)?; + + Ok(()) +} + +#[test] +fn test_chart_scatter07() { + let test_runner = common::TestRunner::new() + .set_name("chart_scatter07") + .set_function(create_new_xlsx_file) + .ignore_elements("xl/workbook.xml", " Result<(), XlsxError> { + let mut workbook = Workbook::new(); + + let worksheet = workbook.add_worksheet(); + worksheet.set_column_width(0, 11)?; + worksheet.set_column_width(1, 11)?; + worksheet.set_column_width(2, 11)?; + worksheet.set_column_width(3, 11)?; + + worksheet.set_selection(3, 2, 3, 2)?; + + let date_format = Format::new().set_num_format_index(14); + + // Add some test data for the chart(s). + let dates = [ + ExcelDateTime::parse_from_str("2007-01-01")?, + ExcelDateTime::parse_from_str("2007-01-02")?, + ExcelDateTime::parse_from_str("2007-01-03")?, + ExcelDateTime::parse_from_str("2007-01-04")?, + ExcelDateTime::parse_from_str("2007-01-05")?, + ]; + let high = [27.2, 25.03, 19.05, 20.34, 18.5]; + let low = [23.49, 19.55, 15.12, 17.84, 16.34]; + let close = [25.45, 23.05, 17.32, 20.45, 17.34]; + + worksheet.write_column_with_format(0, 0, dates, &date_format)?; + worksheet.write_column(0, 1, high)?; + worksheet.write_column(0, 2, low)?; + worksheet.write_column(0, 3, close)?; + + let mut chart = Chart::new(ChartType::Stock); + chart.set_axis_ids(63596416, 63597952); + chart.set_axis2_ids(51206784, 51204480); + + chart + .add_series() + .set_categories(("Sheet1", 0, 0, 4, 0)) + .set_values(("Sheet1", 0, 1, 4, 1)) + .set_format(ChartLine::new().set_width(2.25).set_hidden(true)) + .set_marker(ChartMarker::new().set_none()); + + chart + .add_series() + .set_categories(("Sheet1", 0, 0, 4, 0)) + .set_values(("Sheet1", 0, 2, 4, 2)) + .set_format(ChartLine::new().set_width(2.25).set_hidden(true)) + .set_marker(ChartMarker::new().set_none()); + + chart + .add_series() + .set_categories(("Sheet1", 0, 0, 4, 0)) + .set_values(("Sheet1", 0, 3, 4, 3)) + .set_format(ChartLine::new().set_width(2.25).set_hidden(true)) + .set_marker( + ChartMarker::new() + .set_type(ChartMarkerType::ShortDash) + .set_size(3), + ) + .set_y2_axis(true); + + chart.set_high_low_lines(true); + + worksheet.insert_chart(8, 4, &chart)?; + + workbook.save(filename)?; + + Ok(()) +} + +#[test] +fn test_chart_stock02() { + let test_runner = common::TestRunner::new() + .set_name("chart_stock02") + .set_function(create_new_xlsx_file) + .ignore_elements("xl/workbook.xml", "