1#![allow(non_camel_case_types)]
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::fmt;
5use std::io::Write;
6
7use oxml_core::OxmlError;
8use oxml_core::raw_xml::{capture_element, capture_empty_element};
9use oxml_core::xml::{local_name, matches_local_name};
10use oxml_core::xml_text::{decode_plain, resolve_entity};
11use oxml_drawing::color::{
12 ColorChoice, ColorMap, ColorMapSlot, ResolvedColor, RgbColor, ThemeColorSlot,
13 apply_color_transforms, resolve_color,
14};
15use oxml_drawing::fill::Fill;
16use oxml_drawing::order::OrderedRawChildren;
17use oxml_drawing::shape_props::{CT_ShapeProperties, ShapePropertiesError};
18use oxml_drawing::text::{CT_TextBody, CT_TextCharacterProperties, TextError};
19use oxml_drawing::theme::CT_OfficeStyleSheet;
20use oxml_layout::{
21 Color, FillRule, FontManager, GlyphRun, GroupElement, Paint, Path, PathCommand, PathElement,
22 Point, PositionedElement, Rect, Stroke, Transform,
23};
24use oxml_sml::{Column, Workbook};
25use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
26use quick_xml::{Reader, Writer, XmlVersion};
27
28pub const C_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/chart";
30pub const C_PREFIX: &str = "c";
32pub const A_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
34pub const R_NS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
36const MAX_CATEGORY_ANNOTATIONS: usize = 16_384;
37const PLOT_MARGIN_LEFT: f64 = 36.0;
38const PLOT_MARGIN_RIGHT: f64 = 12.0;
39const PLOT_MARGIN_TOP: f64 = 12.0;
40const PLOT_MARGIN_BOTTOM: f64 = 28.0;
41
42#[derive(Debug)]
44pub enum ChartError {
45 Xml(OxmlError),
46 ShapeProperties(ShapePropertiesError),
47 Text(TextError),
48 UnexpectedElement(String),
49 MissingElement(String),
50 DuplicateElement(String),
51 InvalidAttribute {
52 element: String,
53 attribute: String,
54 value: String,
55 },
56 InvalidValue {
57 element: String,
58 value: String,
59 },
60}
61
62impl fmt::Display for ChartError {
63 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
64 match self {
65 Self::Xml(error) => error.fmt(formatter),
66 Self::ShapeProperties(error) => error.fmt(formatter),
67 Self::Text(error) => error.fmt(formatter),
68 Self::UnexpectedElement(element) => {
69 write!(formatter, "unexpected ChartML element: {element}")
70 }
71 Self::MissingElement(element) => {
72 write!(formatter, "ChartML requires {element}")
73 }
74 Self::DuplicateElement(element) => {
75 write!(formatter, "ChartML contains duplicate {element}")
76 }
77 Self::InvalidAttribute {
78 element,
79 attribute,
80 value,
81 } => write!(
82 formatter,
83 "ChartML {element} has invalid @{attribute}: {value}"
84 ),
85 Self::InvalidValue { element, value } => {
86 write!(formatter, "ChartML {element} has invalid value: {value}")
87 }
88 }
89 }
90}
91
92impl std::error::Error for ChartError {
93 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
94 match self {
95 Self::Xml(error) => Some(error),
96 Self::ShapeProperties(error) => Some(error),
97 Self::Text(error) => Some(error),
98 _ => None,
99 }
100 }
101}
102
103impl From<OxmlError> for ChartError {
104 fn from(error: OxmlError) -> Self {
105 Self::Xml(error)
106 }
107}
108
109impl From<ShapePropertiesError> for ChartError {
110 fn from(error: ShapePropertiesError) -> Self {
111 Self::ShapeProperties(error)
112 }
113}
114
115impl From<TextError> for ChartError {
116 fn from(error: TextError) -> Self {
117 Self::Text(error)
118 }
119}
120
121pub type Result<T> = std::result::Result<T, ChartError>;
122
123#[derive(Clone, Copy, Debug, Eq, PartialEq)]
125pub enum ChartKind {
126 Bar,
127 Line,
128 Pie,
129 Doughnut,
130 Area,
131 Scatter,
132 Radar,
133}
134
135#[derive(Clone, Debug, PartialEq)]
137pub struct ChartData {
138 pub categories: Vec<String>,
139 pub series: Vec<(String, Vec<f64>)>,
140 pub number_format: Option<String>,
141}
142
143pub fn authored_chart_parts(
145 kind: ChartKind,
146 data: &ChartData,
147 workbook_relationship_id: &str,
148) -> Result<(Vec<u8>, Vec<u8>)> {
149 validate_chart_data(kind, data)?;
150 let category_column = if kind == ChartKind::Scatter {
151 Column::Number {
152 header: "Category".to_owned(),
153 values: data
154 .categories
155 .iter()
156 .map(|value| value.parse::<f64>().expect("validated scatter category"))
157 .collect(),
158 number_format: None,
159 }
160 } else {
161 Column::Text {
162 header: "Category".to_owned(),
163 values: data.categories.clone(),
164 }
165 };
166 let mut columns = Vec::with_capacity(data.series.len() + 1);
167 columns.push(category_column);
168 columns.extend(data.series.iter().map(|(name, values)| Column::Number {
169 header: name.clone(),
170 values: values.clone(),
171 number_format: data.number_format.clone(),
172 }));
173 let workbook = Workbook::new("Sheet1", columns).map_err(invalid_authoring_value)?;
174 let category_formula = workbook
175 .formula_range(0)
176 .expect("validated categories produce a formula range");
177 let number_format = data.number_format.as_deref().unwrap_or("General");
178 let mut chart_series = Vec::with_capacity(data.series.len());
179 for (index, (name, values)) in data.series.iter().enumerate() {
180 let values_formula = workbook
181 .formula_range(index + 1)
182 .expect("validated series produces a formula range");
183 let values = NumericData::new(values_formula, number_format.to_owned(), values.clone())
184 .map_err(invalid_authoring_value)?;
185 let mut series = Series::new(index as u32, index as u32, values);
186 series.name = Some(
187 StringRef::new(
188 format!("Sheet1!${}$1", spreadsheet_column_name(index + 1)),
189 vec![name.clone()],
190 )
191 .map_err(invalid_authoring_value)?,
192 );
193 series.categories = Some(if kind == ChartKind::Scatter {
194 AxisData::Numeric(
195 NumericData::new(
196 category_formula.clone(),
197 "General".to_owned(),
198 data.categories
199 .iter()
200 .map(|value| value.parse::<f64>().expect("validated scatter category"))
201 .collect(),
202 )
203 .map_err(invalid_authoring_value)?,
204 )
205 } else {
206 AxisData::String(
207 StringRef::new(category_formula.clone(), data.categories.clone())
208 .map_err(invalid_authoring_value)?,
209 )
210 });
211 chart_series.push(series);
212 }
213
214 let category_axis_id = AxisId::new(48_650_112).map_err(invalid_authoring_value)?;
215 let value_axis_id = AxisId::new(48_672_768).map_err(invalid_authoring_value)?;
216 let axis_ids = [category_axis_id, value_axis_id];
217 let (plot, axes) = match kind {
218 ChartKind::Bar => (
219 Plot::bar(
220 BarDirection::Column,
221 BarGrouping::Clustered,
222 chart_series,
223 axis_ids,
224 ),
225 category_value_axes(category_axis_id, value_axis_id),
226 ),
227 ChartKind::Line => (
228 Plot::line(Grouping::Standard, chart_series, axis_ids),
229 category_value_axes(category_axis_id, value_axis_id),
230 ),
231 ChartKind::Pie => (Plot::pie(chart_series), Vec::new()),
232 ChartKind::Doughnut => (Plot::doughnut(chart_series), Vec::new()),
233 ChartKind::Area => (
234 Plot::area(Grouping::Standard, chart_series, axis_ids),
235 category_value_axes(category_axis_id, value_axis_id),
236 ),
237 ChartKind::Scatter => (
238 Plot::scatter(ScatterStyle::LineMarker, chart_series, axis_ids),
239 vec![
240 Axis::new(
241 AxisKind::Value,
242 category_axis_id,
243 AxisPosition::Bottom,
244 value_axis_id,
245 ),
246 Axis::new(
247 AxisKind::Value,
248 value_axis_id,
249 AxisPosition::Left,
250 category_axis_id,
251 ),
252 ],
253 ),
254 ChartKind::Radar => (
255 Plot::radar(RadarStyle::Standard, chart_series, axis_ids),
256 category_value_axes(category_axis_id, value_axis_id),
257 ),
258 };
259 let plot = plot.map_err(invalid_authoring_value)?;
260 let plot_area = CT_PlotArea::new(vec![plot], axes).map_err(invalid_authoring_value)?;
261 let chart_shell = format!(
262 r#"<c:chartSpace xmlns:c="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}"><c:chart><c:plotArea/></c:chart><c:externalData r:id="{workbook_relationship_id}"><c:autoUpdate val="0"/></c:externalData></c:chartSpace>"#,
263 );
264 let mut chart_space = CT_ChartSpace::from_xml(chart_shell.as_bytes())?;
265 chart_space.chart.auto_title_deleted = true;
266 chart_space.chart.plot_area = plot_area;
267 chart_space.chart.legend = (data.series.len() > 1).then(CT_Legend::default);
268 let chart_xml = chart_space.to_xml()?;
269 let workbook_bytes = workbook.to_xlsx_bytes().map_err(invalid_authoring_value)?;
270 Ok((chart_xml, workbook_bytes))
271}
272
273fn validate_chart_data(kind: ChartKind, data: &ChartData) -> Result<()> {
274 if data.categories.is_empty() {
275 return Err(invalid_authoring_value("at least one category is required"));
276 }
277 if data.series.is_empty() {
278 return Err(invalid_authoring_value("at least one series is required"));
279 }
280 if matches!(kind, ChartKind::Pie | ChartKind::Doughnut) && data.series.len() != 1 {
281 return Err(invalid_authoring_value(
282 "pie and doughnut charts require exactly one series",
283 ));
284 }
285 for (name, values) in &data.series {
286 if values.len() != data.categories.len() {
287 return Err(invalid_authoring_value(format!(
288 "series {name:?} has {} values for {} categories",
289 values.len(),
290 data.categories.len()
291 )));
292 }
293 if values.iter().any(|value| !value.is_finite()) {
294 return Err(invalid_authoring_value(format!(
295 "series {name:?} contains a nonfinite value"
296 )));
297 }
298 }
299 if kind == ChartKind::Scatter {
300 for category in &data.categories {
301 if category
302 .parse::<f64>()
303 .ok()
304 .is_none_or(|value| !value.is_finite())
305 {
306 return Err(invalid_authoring_value(format!(
307 "scatter category {category:?} is not a finite number"
308 )));
309 }
310 }
311 }
312 if let Some(format) = &data.number_format {
313 NumericData::new("Sheet1!$A$2".to_owned(), format.clone(), vec![0.0])?;
314 }
315 Ok(())
316}
317
318fn invalid_authoring_value(error: impl fmt::Display) -> ChartError {
319 ChartError::InvalidValue {
320 element: "chart authoring".to_owned(),
321 value: error.to_string(),
322 }
323}
324
325fn category_value_axes(category: AxisId, value: AxisId) -> Vec<Axis> {
326 vec![
327 Axis::new(AxisKind::Category, category, AxisPosition::Bottom, value),
328 Axis::new(AxisKind::Value, value, AxisPosition::Left, category),
329 ]
330}
331
332fn spreadsheet_column_name(mut index: usize) -> String {
333 let mut name = Vec::new();
334 index += 1;
335 while index != 0 {
336 let remainder = (index - 1) % 26;
337 name.push((b'A' + remainder as u8) as char);
338 index = (index - 1) / 26;
339 }
340 name.iter().rev().collect()
341}
342
343#[derive(Clone, Debug)]
345pub struct ChartGeometry {
346 pub plot_bounds: Rect,
347 pub elements: Vec<PositionedElement>,
348}
349
350#[derive(Clone, Debug)]
351struct AxisScale {
352 domain: Domain,
353 ticks: Vec<f64>,
354}
355
356#[derive(Clone, Copy, Debug)]
357struct AxisMapping {
358 domain: Domain,
359 orientation: Orientation,
360}
361
362#[derive(Clone, Copy, Debug, Default)]
363struct GeometryOptions {
364 category_orientation: Orientation,
365 value: Option<AxisMapping>,
366 scatter_x: Option<AxisMapping>,
367 scatter_y: Option<AxisMapping>,
368}
369
370#[derive(Default)]
371struct ChartAnnotations {
372 gridlines: Vec<PositionedElement>,
373 axis_lines: Vec<PositionedElement>,
374 ticks: Vec<PositionedElement>,
375 labels: Vec<PositionedElement>,
376}
377
378pub fn render_chart(
380 chart: &CT_Chart,
381 bounds: Rect,
382 theme: &CT_OfficeStyleSheet,
383 color_map: &ColorMap,
384 fonts: &mut FontManager,
385) -> Result<GroupElement> {
386 let plot_bounds = geometry_plot_bounds(bounds)?;
387 let plots = chart.plot_area.plots()?;
388 let plot = plots
389 .first()
390 .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
391 plot.validate()?;
392 let series_colours = resolve_series_colours(plot, theme, color_map)?;
393 let axes = chart.plot_area.axes()?;
394 let options = geometry_options(plot, &axes, chart.disp_blanks_as)?;
395 let plot_children = render_plot_geometry(
396 plot,
397 plot_bounds,
398 chart.disp_blanks_as,
399 options,
400 &series_colours,
401 )?;
402 if plot_children.is_empty() && !plot_can_render_without_marks(plot) {
403 return Err(ChartError::InvalidValue {
404 element: "c:plotArea".to_owned(),
405 value: "plot has no renderable cached data".to_owned(),
406 });
407 }
408
409 let mut chart_annotations =
410 render_axes_and_labels(plot, &axes, plot_bounds, chart.disp_blanks_as, fonts)?;
411 let mut annotations = Vec::new();
412 render_data_labels(
413 plot,
414 plot_bounds,
415 chart.disp_blanks_as,
416 options,
417 fonts,
418 &mut chart_annotations.labels,
419 )?;
420 render_legend(
421 chart,
422 plot,
423 plot_bounds,
424 fonts,
425 &series_colours,
426 &mut annotations,
427 &mut chart_annotations.labels,
428 )?;
429
430 validate_geometry_coordinates(&chart_annotations.gridlines)?;
431 validate_geometry_coordinates(&plot_children)?;
432 validate_geometry_coordinates(&chart_annotations.axis_lines)?;
433 validate_geometry_coordinates(&chart_annotations.ticks)?;
434 validate_geometry_coordinates(&annotations)?;
435 let mut children = chart_annotations.gridlines;
436 children.push(PositionedElement::Group(GroupElement {
437 transform: Transform::IDENTITY,
438 clip: Some(Path::rect(plot_bounds)),
439 opacity: 1.0,
440 effects: Vec::new(),
441 children: plot_children,
442 }));
443 children.extend(chart_annotations.axis_lines);
444 children.extend(chart_annotations.ticks);
445 children.extend(annotations);
446 children.extend(chart_annotations.labels);
447 Ok(GroupElement {
448 transform: Transform::IDENTITY,
449 clip: None,
450 opacity: 1.0,
451 effects: Vec::new(),
452 children,
453 })
454}
455
456pub fn render_chart_placeholder(bounds: Rect, fonts: &mut FontManager) -> Result<GroupElement> {
458 if !bounds.width.is_finite()
459 || !bounds.height.is_finite()
460 || bounds.width <= 0.0
461 || bounds.height <= 0.0
462 {
463 return Err(ChartError::InvalidValue {
464 element: "chart fallback bounds".to_owned(),
465 value: "width and height must be finite and positive".to_owned(),
466 });
467 }
468 let label = shape_label(
469 fonts,
470 "Unsupported chart",
471 Point {
472 x: 4.0,
473 y: (bounds.height / 2.0).max(9.0),
474 },
475 )?;
476 Ok(GroupElement {
477 transform: Transform::IDENTITY,
478 clip: Some(Path::rect(Rect {
479 x: 0.0,
480 y: 0.0,
481 width: bounds.width,
482 height: bounds.height,
483 })),
484 opacity: 1.0,
485 effects: Vec::new(),
486 children: vec![PositionedElement::Text(label)],
487 })
488}
489
490pub fn render_geometry(
492 chart: &CT_Chart,
493 bounds: Rect,
494 theme: &CT_OfficeStyleSheet,
495 color_map: &ColorMap,
496) -> Result<ChartGeometry> {
497 let plot_bounds = geometry_plot_bounds(bounds)?;
498 let plots = chart.plot_area.plots()?;
499 let plot = plots
500 .first()
501 .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
502 plot.validate()?;
503 let series_colours = resolve_series_colours(plot, theme, color_map)?;
504 let children = render_plot_geometry(
505 plot,
506 plot_bounds,
507 chart.disp_blanks_as,
508 geometry_options(plot, &[], chart.disp_blanks_as)?,
509 &series_colours,
510 )?;
511 if children.is_empty() && !plot_can_render_without_marks(plot) {
512 return Err(ChartError::InvalidValue {
513 element: "c:plotArea".to_owned(),
514 value: "plot has no renderable cached data".to_owned(),
515 });
516 }
517 validate_geometry_coordinates(&children)?;
518 Ok(ChartGeometry {
519 plot_bounds,
520 elements: vec![PositionedElement::Group(GroupElement {
521 transform: Transform::IDENTITY,
522 clip: None,
523 opacity: 1.0,
524 effects: Vec::new(),
525 children,
526 })],
527 })
528}
529
530fn plot_can_render_without_marks(plot: &Plot) -> bool {
531 matches!(
532 plot,
533 Plot::Radar { series, .. }
534 if series
535 .iter()
536 .any(|series| !series.values.values.is_empty())
537 )
538}
539
540fn geometry_plot_bounds(bounds: Rect) -> Result<Rect> {
541 if ![bounds.x, bounds.y, bounds.width, bounds.height]
542 .into_iter()
543 .all(f64::is_finite)
544 || bounds.width <= 0.0
545 || bounds.height <= 0.0
546 || !(bounds.x + bounds.width).is_finite()
547 || !(bounds.y + bounds.height).is_finite()
548 {
549 return Err(ChartError::InvalidValue {
550 element: "chart geometry bounds".to_owned(),
551 value: format!(
552 "x={}, y={}, width={}, height={}",
553 bounds.x, bounds.y, bounds.width, bounds.height
554 ),
555 });
556 }
557 let plot_bounds = Rect {
558 x: bounds.x + PLOT_MARGIN_LEFT,
559 y: bounds.y + PLOT_MARGIN_TOP,
560 width: bounds.width - PLOT_MARGIN_LEFT - PLOT_MARGIN_RIGHT,
561 height: bounds.height - PLOT_MARGIN_TOP - PLOT_MARGIN_BOTTOM,
562 };
563 if ![
564 plot_bounds.x,
565 plot_bounds.y,
566 plot_bounds.width,
567 plot_bounds.height,
568 plot_bounds.x + plot_bounds.width,
569 plot_bounds.y + plot_bounds.height,
570 ]
571 .into_iter()
572 .all(f64::is_finite)
573 || plot_bounds.width <= 0.0
574 || plot_bounds.height <= 0.0
575 {
576 return Err(ChartError::InvalidValue {
577 element: "chart geometry bounds".to_owned(),
578 value: "chart is too small for the fixed plot margins".to_owned(),
579 });
580 }
581 Ok(plot_bounds)
582}
583
584fn geometry_options(plot: &Plot, axes: &[Axis], blanks: DispBlanksAs) -> Result<GeometryOptions> {
585 let mut options = GeometryOptions::default();
586 if let Some(category_axis) = axes.iter().find(|axis| axis.kind != AxisKind::Value) {
587 options.category_orientation = category_axis.scaling.orientation;
588 }
589
590 if let Plot::Scatter { axis_ids, .. } = plot {
591 let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
592 if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[0]) {
593 options.scatter_x = Some(axis_mapping(axis, x_domain)?);
594 }
595 if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[1]) {
596 options.scatter_y = Some(axis_mapping(axis, y_domain)?);
597 }
598 } else if let Some(domain) = plot_value_domain(plot, blanks)? {
599 options.value = Some(
600 axes.iter()
601 .find(|axis| axis.kind == AxisKind::Value)
602 .map(|axis| axis_mapping(axis, domain))
603 .transpose()?
604 .unwrap_or(AxisMapping {
605 domain,
606 orientation: Orientation::MinMax,
607 }),
608 );
609 }
610 Ok(options)
611}
612
613fn axis_mapping(axis: &Axis, extent: Domain) -> Result<AxisMapping> {
614 if axis.scaling.log_base.is_some() {
615 return Err(ChartError::InvalidValue {
616 element: "c:scaling/c:logBase".to_owned(),
617 value: "native chart rendering currently supports linear axes".to_owned(),
618 });
619 }
620 let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
621 Ok(AxisMapping {
622 domain: scale.domain,
623 orientation: axis.scaling.orientation,
624 })
625}
626
627fn plot_value_domain(plot: &Plot, blanks: DispBlanksAs) -> Result<Option<Domain>> {
628 let domain = match plot {
629 Plot::Bar {
630 grouping, series, ..
631 } => {
632 let logical = logical_series_values(series)?;
633 let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
634 let layers =
635 stacked_series_bounds(&logical, stacked, *grouping == BarGrouping::PercentStacked)?;
636 domain_from_layers(&layers)?
637 }
638 Plot::Line {
639 grouping, series, ..
640 } => {
641 let logical = logical_series_values(series)?;
642 if *grouping == Grouping::Standard {
643 let values = if blanks == DispBlanksAs::Zero {
644 zero_filled_series(&logical, series_category_count(series)?)
645 .into_iter()
646 .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
647 .collect::<Vec<_>>()
648 } else {
649 logical
650 .into_iter()
651 .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
652 .collect::<Vec<_>>()
653 };
654 data_domain(&values, false)?
655 } else {
656 let layers =
657 stacked_series_bounds(&logical, true, *grouping == Grouping::PercentStacked)?;
658 domain_from_layers(&layers)?
659 }
660 }
661 Plot::Area {
662 grouping, series, ..
663 } => {
664 let logical = logical_series_values(series)?;
665 let layers = stacked_series_bounds(
666 &logical,
667 *grouping != Grouping::Standard,
668 *grouping == Grouping::PercentStacked,
669 )?;
670 domain_from_layers(&layers)?
671 }
672 Plot::Radar { series, .. } => {
673 let values = series
674 .iter()
675 .flat_map(|series| series.values.values.iter().copied())
676 .collect::<Vec<_>>();
677 data_domain(&values, true)?
678 }
679 Plot::Pie { .. } | Plot::Doughnut { .. } | Plot::Scatter { .. } => return Ok(None),
680 };
681 Ok(Some(domain))
682}
683
684fn scatter_domains(plot: &Plot, blanks: DispBlanksAs) -> Result<(Domain, Domain)> {
685 let Plot::Scatter { series, .. } = plot else {
686 return Err(ChartError::UnexpectedElement("non-scatter plot".to_owned()));
687 };
688 let mut x_values = Vec::new();
689 let mut y_values = Vec::new();
690 for series in series {
691 for (_, x, y) in scatter_render_points(series, blanks)? {
692 x_values.push(x);
693 y_values.push(y);
694 }
695 }
696 Ok((
697 data_domain(&x_values, false)?,
698 data_domain(&y_values, false)?,
699 ))
700}
701
702fn render_axes_and_labels(
703 plot: &Plot,
704 axes: &[Axis],
705 bounds: Rect,
706 blanks: DispBlanksAs,
707 fonts: &mut FontManager,
708) -> Result<ChartAnnotations> {
709 if matches!(plot, Plot::Radar { .. }) {
710 return render_radar_axes_and_labels(plot, axes, bounds, blanks, fonts);
711 }
712 let mut annotations = ChartAnnotations::default();
713 let series = plot_series(plot);
714 let category_count = if series.is_empty() {
715 1
716 } else {
717 series_category_count(series)?
718 };
719 for axis in axes {
720 if axis.deleted {
721 continue;
722 }
723 axis_line(&mut annotations.axis_lines, bounds, axis.position);
724 if axis.kind == AxisKind::Value {
725 let extent = axis_extent(plot, axis, blanks)?;
726 let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
727 for tick in &scale.ticks {
728 let coordinate = axis_tick_coordinate(
729 *tick,
730 scale.domain,
731 bounds,
732 axis.position,
733 axis.scaling.orientation,
734 )?;
735 if axis.major_gridlines.is_some() {
736 major_gridline(
737 &mut annotations.gridlines,
738 bounds,
739 axis.position,
740 coordinate,
741 );
742 }
743 axis_tick(
744 &mut annotations.ticks,
745 bounds,
746 axis.position,
747 coordinate,
748 axis.major_tick_mark,
749 );
750 if axis.tick_label_position != TickLabelPosition::None {
751 let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
752 let origin = tick_label_origin(
753 bounds,
754 axis.position,
755 axis.tick_label_position,
756 coordinate,
757 );
758 annotations
759 .labels
760 .push(PositionedElement::Text(shape_label_with_properties(
761 fonts,
762 &text,
763 origin,
764 axis.tx_pr.as_ref(),
765 )?));
766 }
767 }
768 } else {
769 let count = category_count;
770 if count > MAX_CATEGORY_ANNOTATIONS {
771 return Err(ChartError::InvalidValue {
772 element: "c:catAx annotations".to_owned(),
773 value: format!(
774 "logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"
775 ),
776 });
777 }
778 let category_labels: BTreeMap<_, _> =
779 if axis.tick_label_position != TickLabelPosition::None {
780 series
781 .first()
782 .map(|series| series_categories(series, axis.number_format.as_ref()))
783 .transpose()?
784 .unwrap_or_default()
785 .into_iter()
786 .collect()
787 } else {
788 BTreeMap::new()
789 };
790 for logical_index in 0..count {
791 let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
792 let fraction = (index as f64 + 0.5) / count as f64;
793 let coordinate =
794 if matches!(axis.position, AxisPosition::Bottom | AxisPosition::Top) {
795 bounds.x + fraction * bounds.width
796 } else {
797 bounds.y + fraction * bounds.height
798 };
799 if axis.major_gridlines.is_some() {
800 major_gridline(
801 &mut annotations.gridlines,
802 bounds,
803 axis.position,
804 coordinate,
805 );
806 }
807 axis_tick(
808 &mut annotations.ticks,
809 bounds,
810 axis.position,
811 coordinate,
812 axis.major_tick_mark,
813 );
814 if axis.tick_label_position != TickLabelPosition::None
815 && let Some(text) = category_labels.get(&logical_index)
816 {
817 let origin = tick_label_origin(
818 bounds,
819 axis.position,
820 axis.tick_label_position,
821 coordinate,
822 );
823 annotations
824 .labels
825 .push(PositionedElement::Text(shape_label_with_properties(
826 fonts,
827 text,
828 origin,
829 axis.tx_pr.as_ref(),
830 )?));
831 }
832 }
833 }
834 }
835 Ok(annotations)
836}
837
838fn render_radar_axes_and_labels(
839 plot: &Plot,
840 axes: &[Axis],
841 bounds: Rect,
842 blanks: DispBlanksAs,
843 fonts: &mut FontManager,
844) -> Result<ChartAnnotations> {
845 let mut annotations = ChartAnnotations::default();
846 let series = plot_series(plot);
847 let count = series_category_count(series)?;
848 if count > MAX_CATEGORY_ANNOTATIONS {
849 return Err(ChartError::InvalidValue {
850 element: "c:radarChart annotations".to_owned(),
851 value: format!("logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"),
852 });
853 }
854 let center = Point {
855 x: bounds.x + bounds.width / 2.0,
856 y: bounds.y + bounds.height / 2.0,
857 };
858 let radius = bounds.width.min(bounds.height) / 2.0;
859 for axis in axes {
860 if axis.deleted {
861 continue;
862 }
863 if axis.kind == AxisKind::Value {
864 let scale = nice_number_scale(
865 axis_extent(plot, axis, blanks)?,
866 axis.scaling.minimum,
867 axis.scaling.maximum,
868 6,
869 )?;
870 annotations.axis_lines.push(stroked_segment(
871 center,
872 radial_point(center, radius, 0, count),
873 Color::BLACK,
874 1.0,
875 ));
876 for tick in &scale.ticks {
877 let distance =
878 normalized_value_oriented(*tick, scale.domain, axis.scaling.orientation)?
879 * radius;
880 if axis.major_gridlines.is_some() && distance > 0.0 {
881 let points: Vec<_> = (0..count)
882 .map(|index| radial_point(center, distance, index, count))
883 .collect();
884 annotations
885 .gridlines
886 .push(PositionedElement::Path(PathElement {
887 path: polyline_path(&points, true),
888 fill: None,
889 stroke: Some(Stroke::new(
890 Paint::Solid(Color {
891 r: 0.82,
892 g: 0.82,
893 b: 0.82,
894 a: 1.0,
895 }),
896 0.5,
897 )),
898 }));
899 }
900 radar_value_tick(
901 &mut annotations.ticks,
902 center,
903 distance,
904 axis.major_tick_mark,
905 );
906 let origin = match axis.tick_label_position {
907 TickLabelPosition::High => Some(Point {
908 x: center.x + radius + 4.0,
909 y: center.y - distance + 3.0,
910 }),
911 TickLabelPosition::Low => Some(Point {
912 x: center.x - radius - 30.0,
913 y: center.y - distance + 3.0,
914 }),
915 TickLabelPosition::NextTo => Some(Point {
916 x: center.x + 4.0,
917 y: center.y - distance + 3.0,
918 }),
919 TickLabelPosition::None => None,
920 };
921 if let Some(origin) = origin {
922 let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
923 annotations
924 .labels
925 .push(PositionedElement::Text(shape_label_with_properties(
926 fonts,
927 &text,
928 origin,
929 axis.tx_pr.as_ref(),
930 )?));
931 }
932 }
933 } else {
934 let category_labels: BTreeMap<_, _> =
935 if axis.tick_label_position != TickLabelPosition::None {
936 series
937 .first()
938 .map(|series| series_categories(series, axis.number_format.as_ref()))
939 .transpose()?
940 .unwrap_or_default()
941 .into_iter()
942 .collect()
943 } else {
944 BTreeMap::new()
945 };
946 for logical_index in 0..count {
947 let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
948 let end = radial_point(center, radius, index, count);
949 annotations
950 .axis_lines
951 .push(stroked_segment(center, end, Color::BLACK, 1.0));
952 radar_category_tick(
953 &mut annotations.ticks,
954 center,
955 radius,
956 index,
957 count,
958 axis.major_tick_mark,
959 );
960 if let Some(text) = category_labels.get(&logical_index) {
961 let Some(origin) = radar_category_label_origin(
962 bounds,
963 center,
964 radius,
965 index,
966 count,
967 axis.tick_label_position,
968 ) else {
969 continue;
970 };
971 annotations
972 .labels
973 .push(PositionedElement::Text(shape_label_with_properties(
974 fonts,
975 text,
976 origin,
977 axis.tx_pr.as_ref(),
978 )?));
979 }
980 }
981 }
982 }
983 Ok(annotations)
984}
985
986fn radar_category_label_origin(
987 bounds: Rect,
988 center: Point,
989 radius: f64,
990 index: usize,
991 count: usize,
992 position: TickLabelPosition,
993) -> Option<Point> {
994 let label_radius = match position {
995 TickLabelPosition::High => radius + 18.0,
996 TickLabelPosition::Low => (radius - 10.0).max(0.0),
997 TickLabelPosition::NextTo => radius + 10.0,
998 TickLabelPosition::None => return None,
999 };
1000 let label = radial_point(center, label_radius, index, count);
1001 let mut origin = Point {
1002 x: label.x - 4.0,
1003 y: label.y + 3.0,
1004 };
1005 if position == TickLabelPosition::High {
1006 origin.x = origin.x.clamp(
1007 bounds.x - PLOT_MARGIN_LEFT,
1008 bounds.x + bounds.width + PLOT_MARGIN_RIGHT,
1009 );
1010 origin.y = origin.y.clamp(
1011 bounds.y - PLOT_MARGIN_TOP,
1012 bounds.y + bounds.height + PLOT_MARGIN_BOTTOM,
1013 );
1014 }
1015 Some(origin)
1016}
1017
1018fn radar_value_tick(
1019 elements: &mut Vec<PositionedElement>,
1020 center: Point,
1021 distance: f64,
1022 mark: TickMark,
1023) {
1024 let (left, right) = match mark {
1025 TickMark::None => return,
1026 TickMark::Inside => (4.0, 0.0),
1027 TickMark::Outside => (0.0, 4.0),
1028 TickMark::Cross => (2.0, 2.0),
1029 };
1030 let y = center.y - distance;
1031 elements.push(stroked_segment(
1032 Point {
1033 x: center.x - left,
1034 y,
1035 },
1036 Point {
1037 x: center.x + right,
1038 y,
1039 },
1040 Color::BLACK,
1041 1.0,
1042 ));
1043}
1044
1045fn radar_category_tick(
1046 elements: &mut Vec<PositionedElement>,
1047 center: Point,
1048 radius: f64,
1049 index: usize,
1050 count: usize,
1051 mark: TickMark,
1052) {
1053 let (inside, outside) = match mark {
1054 TickMark::None => return,
1055 TickMark::Inside => (4.0, 0.0),
1056 TickMark::Outside => (0.0, 4.0),
1057 TickMark::Cross => (2.0, 2.0),
1058 };
1059 elements.push(stroked_segment(
1060 radial_point(center, radius - inside, index, count),
1061 radial_point(center, radius + outside, index, count),
1062 Color::BLACK,
1063 1.0,
1064 ));
1065}
1066
1067fn axis_extent(plot: &Plot, axis: &Axis, blanks: DispBlanksAs) -> Result<Domain> {
1068 if let Plot::Scatter { axis_ids, .. } = plot {
1069 let (x, y) = scatter_domains(plot, blanks)?;
1070 return Ok(if axis.id == axis_ids[0] { x } else { y });
1071 }
1072 plot_value_domain(plot, blanks)?.ok_or_else(|| ChartError::InvalidValue {
1073 element: "chart axis".to_owned(),
1074 value: "axis-free plot supplied an axis".to_owned(),
1075 })
1076}
1077
1078fn axis_tick_coordinate(
1079 value: f64,
1080 domain: Domain,
1081 bounds: Rect,
1082 position: AxisPosition,
1083 orientation: Orientation,
1084) -> Result<f64> {
1085 if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
1086 map_x_oriented(value, domain, bounds, orientation)
1087 } else {
1088 map_y_oriented(value, domain, bounds, orientation)
1089 }
1090}
1091
1092fn axis_line(elements: &mut Vec<PositionedElement>, bounds: Rect, position: AxisPosition) {
1093 let (start, end) = match position {
1094 AxisPosition::Bottom => (
1095 Point {
1096 x: bounds.x,
1097 y: bounds.y + bounds.height,
1098 },
1099 Point {
1100 x: bounds.x + bounds.width,
1101 y: bounds.y + bounds.height,
1102 },
1103 ),
1104 AxisPosition::Top => (
1105 Point {
1106 x: bounds.x,
1107 y: bounds.y,
1108 },
1109 Point {
1110 x: bounds.x + bounds.width,
1111 y: bounds.y,
1112 },
1113 ),
1114 AxisPosition::Left => (
1115 Point {
1116 x: bounds.x,
1117 y: bounds.y,
1118 },
1119 Point {
1120 x: bounds.x,
1121 y: bounds.y + bounds.height,
1122 },
1123 ),
1124 AxisPosition::Right => (
1125 Point {
1126 x: bounds.x + bounds.width,
1127 y: bounds.y,
1128 },
1129 Point {
1130 x: bounds.x + bounds.width,
1131 y: bounds.y + bounds.height,
1132 },
1133 ),
1134 };
1135 elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
1136}
1137
1138fn major_gridline(
1139 elements: &mut Vec<PositionedElement>,
1140 bounds: Rect,
1141 position: AxisPosition,
1142 coordinate: f64,
1143) {
1144 let (start, end) = if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
1145 (
1146 Point {
1147 x: coordinate,
1148 y: bounds.y,
1149 },
1150 Point {
1151 x: coordinate,
1152 y: bounds.y + bounds.height,
1153 },
1154 )
1155 } else {
1156 (
1157 Point {
1158 x: bounds.x,
1159 y: coordinate,
1160 },
1161 Point {
1162 x: bounds.x + bounds.width,
1163 y: coordinate,
1164 },
1165 )
1166 };
1167 elements.push(stroked_segment(
1168 start,
1169 end,
1170 Color {
1171 r: 0.82,
1172 g: 0.82,
1173 b: 0.82,
1174 a: 1.0,
1175 },
1176 0.5,
1177 ));
1178}
1179
1180fn axis_tick(
1181 elements: &mut Vec<PositionedElement>,
1182 bounds: Rect,
1183 position: AxisPosition,
1184 coordinate: f64,
1185 mark: TickMark,
1186) {
1187 let (inside, outside) = match mark {
1188 TickMark::None => return,
1189 TickMark::Inside => (4.0, 0.0),
1190 TickMark::Outside => (0.0, 4.0),
1191 TickMark::Cross => (2.0, 2.0),
1192 };
1193 let (start, end) = match position {
1194 AxisPosition::Bottom => (
1195 Point {
1196 x: coordinate,
1197 y: bounds.y + bounds.height - inside,
1198 },
1199 Point {
1200 x: coordinate,
1201 y: bounds.y + bounds.height + outside,
1202 },
1203 ),
1204 AxisPosition::Top => (
1205 Point {
1206 x: coordinate,
1207 y: bounds.y + inside,
1208 },
1209 Point {
1210 x: coordinate,
1211 y: bounds.y - outside,
1212 },
1213 ),
1214 AxisPosition::Left => (
1215 Point {
1216 x: bounds.x + inside,
1217 y: coordinate,
1218 },
1219 Point {
1220 x: bounds.x - outside,
1221 y: coordinate,
1222 },
1223 ),
1224 AxisPosition::Right => (
1225 Point {
1226 x: bounds.x + bounds.width - inside,
1227 y: coordinate,
1228 },
1229 Point {
1230 x: bounds.x + bounds.width + outside,
1231 y: coordinate,
1232 },
1233 ),
1234 };
1235 elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
1236}
1237
1238fn tick_label_origin(
1239 bounds: Rect,
1240 axis_position: AxisPosition,
1241 label_position: TickLabelPosition,
1242 coordinate: f64,
1243) -> Point {
1244 let side = match label_position {
1245 TickLabelPosition::NextTo => axis_position,
1246 TickLabelPosition::High => {
1247 if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
1248 AxisPosition::Top
1249 } else {
1250 AxisPosition::Right
1251 }
1252 }
1253 TickLabelPosition::Low => {
1254 if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
1255 AxisPosition::Bottom
1256 } else {
1257 AxisPosition::Left
1258 }
1259 }
1260 TickLabelPosition::None => axis_position,
1261 };
1262 match side {
1263 AxisPosition::Bottom => Point {
1264 x: coordinate - 4.0,
1265 y: bounds.y + bounds.height + 12.0,
1266 },
1267 AxisPosition::Top => Point {
1268 x: coordinate - 4.0,
1269 y: bounds.y - 4.0,
1270 },
1271 AxisPosition::Left => Point {
1272 x: bounds.x - 30.0,
1273 y: coordinate + 3.0,
1274 },
1275 AxisPosition::Right => Point {
1276 x: bounds.x + bounds.width + 4.0,
1277 y: coordinate + 3.0,
1278 },
1279 }
1280}
1281
1282fn format_axis_value(format: Option<&NumberFormat>, value: f64) -> Result<String> {
1283 if let Some(format) = format {
1284 format.format_value(value)
1285 } else {
1286 NumberFormat::new("General".to_owned(), false)?.format_value(value)
1287 }
1288}
1289
1290fn shape_label(fonts: &mut FontManager, text: &str, origin: Point) -> Result<GlyphRun> {
1291 shape_label_with_properties(fonts, text, origin, None)
1292}
1293
1294fn shape_label_with_properties(
1295 fonts: &mut FontManager,
1296 text: &str,
1297 origin: Point,
1298 properties: Option<&CT_TextBody>,
1299) -> Result<GlyphRun> {
1300 const FAMILY: &str = "Carlito";
1301 const SIZE: f64 = 9.0;
1302 let properties = properties.and_then(chart_text_properties);
1303 let family = properties
1304 .and_then(|properties| properties.latin.as_ref())
1305 .map(|font| font.typeface.as_str())
1306 .unwrap_or(FAMILY);
1307 let size = properties
1308 .and_then(|properties| properties.font_size)
1309 .map(|size| f64::from(size) / 100.0)
1310 .unwrap_or(SIZE);
1311 let bold = properties
1312 .and_then(|properties| properties.bold)
1313 .unwrap_or(false);
1314 let italic = properties
1315 .and_then(|properties| properties.italic)
1316 .unwrap_or(false);
1317 let font_id = fonts
1318 .resolve_font_for_text(Some(family), bold, italic, text)
1319 .map_err(|error| ChartError::InvalidValue {
1320 element: "chart label font".to_owned(),
1321 value: error.to_string(),
1322 })?;
1323 let shaped =
1324 fonts
1325 .shape_text(font_id, text, size)
1326 .map_err(|error| ChartError::InvalidValue {
1327 element: "chart label shaping".to_owned(),
1328 value: error.to_string(),
1329 })?;
1330 Ok(GlyphRun {
1331 origin,
1332 font_id,
1333 font_size: size,
1334 glyph_ids: shaped.glyph_ids,
1335 advances: shaped.advances,
1336 text: text.to_owned(),
1337 source: None,
1338 color: Color::BLACK,
1339 bold,
1340 italic,
1341 field_kind: None,
1342 note: None,
1343 })
1344}
1345
1346fn chart_text_properties(body: &CT_TextBody) -> Option<&CT_TextCharacterProperties> {
1347 body.paragraphs()
1348 .first()
1349 .and_then(|paragraph| paragraph.properties.as_ref())
1350 .and_then(|properties| properties.default_run_properties.as_ref())
1351}
1352
1353fn stroked_segment(start: Point, end: Point, color: Color, width: f64) -> PositionedElement {
1354 PositionedElement::Path(PathElement {
1355 path: Path {
1356 commands: vec![PathCommand::MoveTo(start), PathCommand::LineTo(end)],
1357 fill_rule: FillRule::NonZero,
1358 },
1359 fill: None,
1360 stroke: Some(Stroke::new(Paint::Solid(color), width)),
1361 })
1362}
1363
1364fn plot_series(plot: &Plot) -> &[Series] {
1365 match plot {
1366 Plot::Bar { series, .. }
1367 | Plot::Line { series, .. }
1368 | Plot::Pie { series, .. }
1369 | Plot::Doughnut { series, .. }
1370 | Plot::Area { series, .. }
1371 | Plot::Scatter { series, .. }
1372 | Plot::Radar { series, .. } => series,
1373 }
1374}
1375
1376fn plot_data_labels(plot: &Plot) -> Option<&CT_DLbls> {
1377 match plot {
1378 Plot::Bar { data_labels, .. }
1379 | Plot::Line { data_labels, .. }
1380 | Plot::Pie { data_labels, .. }
1381 | Plot::Doughnut { data_labels, .. }
1382 | Plot::Area { data_labels, .. }
1383 | Plot::Scatter { data_labels, .. }
1384 | Plot::Radar { data_labels, .. } => data_labels.as_ref(),
1385 }
1386}
1387
1388fn series_categories(
1389 series: &Series,
1390 number_format: Option<&NumberFormat>,
1391) -> Result<Vec<(usize, String)>> {
1392 let Some(categories) = &series.categories else {
1393 return Ok(Vec::new());
1394 };
1395 match categories {
1396 AxisData::String(data) => {
1397 let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
1398 indexes
1399 .into_iter()
1400 .zip(&data.values)
1401 .map(|(index, value)| {
1402 usize::try_from(index)
1403 .map(|index| (index, value.clone()))
1404 .map_err(|_| ChartError::InvalidValue {
1405 element: "c:cat/c:pt/@idx".to_owned(),
1406 value: index.to_string(),
1407 })
1408 })
1409 .collect()
1410 }
1411 AxisData::Numeric(data) => {
1412 let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
1413 let cache_format;
1414 let format = if let Some(format) = number_format {
1415 format
1416 } else {
1417 cache_format = NumberFormat::new(data.format_code.clone(), false)?;
1418 &cache_format
1419 };
1420 indexes
1421 .into_iter()
1422 .zip(&data.values)
1423 .map(|(index, value)| {
1424 let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
1425 element: "c:cat/c:pt/@idx".to_owned(),
1426 value: index.to_string(),
1427 })?;
1428 let text = format.format_value(*value)?;
1429 Ok((index, text))
1430 })
1431 .collect()
1432 }
1433 }
1434}
1435
1436fn series_name(series: &Series, index: usize) -> String {
1437 series
1438 .name
1439 .as_ref()
1440 .and_then(|name| name.values.first())
1441 .cloned()
1442 .unwrap_or_else(|| format!("Series {}", index + 1))
1443}
1444
1445fn render_data_labels(
1446 plot: &Plot,
1447 bounds: Rect,
1448 blanks: DispBlanksAs,
1449 options: GeometryOptions,
1450 fonts: &mut FontManager,
1451 elements: &mut Vec<PositionedElement>,
1452) -> Result<()> {
1453 let plot_labels = plot_data_labels(plot);
1454 let anchors = plot_label_anchors(plot, bounds, blanks, options)?;
1455 for (series_index, series) in plot_series(plot).iter().enumerate() {
1456 let Some(labels) = series.data_labels.as_ref().or(plot_labels) else {
1457 continue;
1458 };
1459 let overrides = point_label_overrides(labels)?;
1460 let mut categories = None;
1461 let mut bubble_sizes = None;
1462 let (_, values) = logical_numeric_values(&series.values)?;
1463 let mut percentage_total = None;
1464 for (logical_index, value) in values {
1465 let point_override = overrides.get(&logical_index);
1466 if point_override.is_some_and(|point| point.deleted == Some(true)) {
1467 continue;
1468 }
1469 let effective_labels = point_override
1470 .map(|point| point.apply_to(labels))
1471 .unwrap_or_else(|| labels.clone());
1472 let labels = &effective_labels;
1473 let Some(anchor) = anchors.get(&(series_index, logical_index)).copied() else {
1474 continue;
1475 };
1476 let mut fields = Vec::new();
1477 if labels.show_series_name {
1478 fields.push(series_name(series, series_index));
1479 }
1480 if labels.show_category_name {
1481 if categories.is_none() {
1482 categories = Some(
1483 series_categories(series, None)?
1484 .into_iter()
1485 .collect::<BTreeMap<_, _>>(),
1486 );
1487 }
1488 if let Some(category) = categories
1489 .as_ref()
1490 .and_then(|categories| categories.get(&logical_index))
1491 {
1492 fields.push(category.clone());
1493 }
1494 }
1495 if labels.show_value {
1496 fields.push(format_axis_value(labels.number_format.as_ref(), value)?);
1497 }
1498 if labels.show_percent {
1499 let total = if let Some(total) = percentage_total {
1500 total
1501 } else {
1502 let total = series
1503 .values
1504 .values
1505 .iter()
1506 .copied()
1507 .filter(|value| *value > 0.0)
1508 .try_fold(0.0, |total, value| {
1509 checked_geometry_sum(total, value, "data-label percentage total")
1510 })?;
1511 percentage_total = Some(total);
1512 total
1513 };
1514 if total > 0.0 {
1515 let text = if let Some(format) = &labels.number_format {
1516 format.format_value(value / total)?
1517 } else {
1518 NumberFormat::new("0%".to_owned(), false)?.format_value(value / total)?
1519 };
1520 fields.push(text);
1521 }
1522 }
1523 if labels.show_bubble_size {
1524 if bubble_sizes.is_none() {
1525 bubble_sizes = Some(
1526 series
1527 .bubble_size
1528 .as_ref()
1529 .map(logical_numeric_values)
1530 .transpose()?
1531 .map(|(_, values)| values.into_iter().collect::<BTreeMap<_, _>>())
1532 .unwrap_or_default(),
1533 );
1534 }
1535 if let Some(value) = bubble_sizes
1536 .as_ref()
1537 .and_then(|sizes| sizes.get(&logical_index))
1538 {
1539 fields.push(format_axis_value(labels.number_format.as_ref(), *value)?);
1540 }
1541 }
1542 if fields.is_empty() {
1543 continue;
1544 }
1545 let separator = labels.separator.as_deref().unwrap_or(", ");
1546 let text = fields.join(separator);
1547 let origin = anchor.origin(labels.position);
1548 elements.push(PositionedElement::Text(shape_label(fonts, &text, origin)?));
1549 }
1550 }
1551 Ok(())
1552}
1553
1554#[derive(Clone, Debug, Default)]
1555struct PointLabelOverride {
1556 index: Option<usize>,
1557 deleted: Option<bool>,
1558 number_format: Option<NumberFormat>,
1559 position: Option<DataLabelPosition>,
1560 separator: Option<String>,
1561 show_legend_key: Option<bool>,
1562 show_value: Option<bool>,
1563 show_category_name: Option<bool>,
1564 show_series_name: Option<bool>,
1565 show_percent: Option<bool>,
1566 show_bubble_size: Option<bool>,
1567}
1568
1569impl PointLabelOverride {
1570 fn apply_to(&self, labels: &CT_DLbls) -> CT_DLbls {
1571 let mut effective = labels.clone();
1572 if let Some(value) = &self.number_format {
1573 effective.number_format = Some(value.clone());
1574 }
1575 if let Some(value) = self.position {
1576 effective.position = Some(value);
1577 }
1578 if let Some(value) = &self.separator {
1579 effective.separator = Some(value.clone());
1580 }
1581 if let Some(value) = self.show_legend_key {
1582 effective.show_legend_key = value;
1583 }
1584 if let Some(value) = self.show_value {
1585 effective.show_value = value;
1586 }
1587 if let Some(value) = self.show_category_name {
1588 effective.show_category_name = value;
1589 }
1590 if let Some(value) = self.show_series_name {
1591 effective.show_series_name = value;
1592 }
1593 if let Some(value) = self.show_percent {
1594 effective.show_percent = value;
1595 }
1596 if let Some(value) = self.show_bubble_size {
1597 effective.show_bubble_size = value;
1598 }
1599 effective
1600 }
1601}
1602
1603fn point_label_overrides(labels: &CT_DLbls) -> Result<BTreeMap<usize, PointLabelOverride>> {
1604 let mut namespaces = chart_namespace_defaults();
1605 for (name, namespace) in &labels.namespace_declarations {
1606 let prefix = if name == "xmlns" {
1607 Vec::new()
1608 } else if let Some(prefix) = name.strip_prefix("xmlns:") {
1609 prefix.as_bytes().to_vec()
1610 } else {
1611 continue;
1612 };
1613 upsert_namespace_binding(&mut namespaces, prefix, namespace.clone());
1614 }
1615 let mut overrides = BTreeMap::new();
1616 for raw in labels.raw_children.at(0) {
1617 if chart_root_local(raw, &namespaces)?.as_deref() != Some(b"dLbl") {
1618 continue;
1619 }
1620 let point = parse_point_label_override(raw, &namespaces)?;
1621 let index = point
1622 .index
1623 .ok_or_else(|| ChartError::MissingElement("c:dLbl/c:idx".to_owned()))?;
1624 if overrides.insert(index, point).is_some() {
1625 return Err(ChartError::DuplicateElement(format!(
1626 "c:dLbl[c:idx={index}]"
1627 )));
1628 }
1629 }
1630 Ok(overrides)
1631}
1632
1633fn parse_point_label_override(
1634 xml: &[u8],
1635 inherited: &NamespaceBindings,
1636) -> Result<PointLabelOverride> {
1637 let mut reader = Reader::from_reader(xml);
1638 let mut buffer = Vec::new();
1639 let mut namespaces = None;
1640 let mut state = PointLabelOverride::default();
1641 let mut order = 0;
1642 loop {
1643 match reader
1644 .read_event_into(&mut buffer)
1645 .map_err(OxmlError::from)?
1646 {
1647 Event::Start(element) if namespaces.is_none() => {
1648 if !matches_local_name(element.name().as_ref(), b"dLbl")
1649 || !element_is_in_namespace(&element, C_NS, inherited)?
1650 {
1651 return Err(ChartError::UnexpectedElement(element_name(&element)));
1652 }
1653 namespaces = Some(chart_bindings(inherited, &element)?);
1654 }
1655 Event::Start(element) => {
1656 let bindings = namespaces
1657 .as_ref()
1658 .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
1659 let name = chart_child_local(&element, bindings)?;
1660 let raw = capture_element(&mut reader, &element)?;
1661 parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
1662 }
1663 Event::Empty(element) => {
1664 let bindings = namespaces
1665 .as_ref()
1666 .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
1667 let name = chart_child_local(&element, bindings)?;
1668 let raw = capture_empty_element(&element)?;
1669 parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
1670 }
1671 Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbl") => {
1672 if state.index.is_none() {
1673 return Err(ChartError::MissingElement("c:dLbl/c:idx".to_owned()));
1674 }
1675 return Ok(state);
1676 }
1677 Event::Eof => return Err(missing_end("c:dLbl")),
1678 _ => {}
1679 }
1680 buffer.clear();
1681 }
1682}
1683
1684fn parse_point_label_child(
1685 state: &mut PointLabelOverride,
1686 name: Option<&[u8]>,
1687 raw: &[u8],
1688 namespaces: &NamespaceBindings,
1689 order: &mut usize,
1690) -> Result<()> {
1691 let Some(name) = name else {
1692 return Ok(());
1693 };
1694 let rank = match name {
1695 b"idx" => 0,
1696 b"delete" => 1,
1697 b"layout" => 2,
1698 b"tx" => 3,
1699 b"numFmt" => 4,
1700 b"spPr" => 5,
1701 b"txPr" => 6,
1702 b"dLblPos" => 7,
1703 b"showLegendKey" => 8,
1704 b"showVal" => 9,
1705 b"showCatName" => 10,
1706 b"showSerName" => 11,
1707 b"showPercent" => 12,
1708 b"showBubbleSize" => 13,
1709 b"separator" => 14,
1710 b"showLeaderLines" => 15,
1711 b"leaderLines" => 16,
1712 b"extLst" => 17,
1713 _ => return Ok(()),
1714 };
1715 if rank < *order {
1716 return Err(ChartError::InvalidValue {
1717 element: "c:dLbl".to_owned(),
1718 value: format!("c:{} is out of schema order", String::from_utf8_lossy(name)),
1719 });
1720 }
1721 *order = rank;
1722 match name {
1723 b"idx" => {
1724 let (index, _) = parse_u32_scalar(raw, "idx")?;
1725 let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
1726 element: "c:dLbl/c:idx".to_owned(),
1727 value: index.to_string(),
1728 })?;
1729 set_once(&mut state.index, index, "c:dLbl/c:idx")?;
1730 }
1731 b"delete" => set_once(
1732 &mut state.deleted,
1733 parse_bool_value(raw, "delete")?.0,
1734 "c:dLbl/c:delete",
1735 )?,
1736 b"numFmt" => set_once(
1737 &mut state.number_format,
1738 NumberFormat::from_xml(raw)?,
1739 "c:dLbl/c:numFmt",
1740 )?,
1741 b"dLblPos" => {
1742 let (value, _) = scalar_value(raw, "dLblPos")?;
1743 let value =
1744 value.ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
1745 let position = DataLabelPosition::parse(&value)
1746 .ok_or_else(|| invalid_attribute("dLblPos", "val", value))?;
1747 set_once(&mut state.position, position, "c:dLbl/c:dLblPos")?;
1748 }
1749 b"showLegendKey" => set_once(
1750 &mut state.show_legend_key,
1751 parse_bool_value(raw, "showLegendKey")?.0,
1752 "c:dLbl/c:showLegendKey",
1753 )?,
1754 b"showVal" => set_once(
1755 &mut state.show_value,
1756 parse_bool_value(raw, "showVal")?.0,
1757 "c:dLbl/c:showVal",
1758 )?,
1759 b"showCatName" => set_once(
1760 &mut state.show_category_name,
1761 parse_bool_value(raw, "showCatName")?.0,
1762 "c:dLbl/c:showCatName",
1763 )?,
1764 b"showSerName" => set_once(
1765 &mut state.show_series_name,
1766 parse_bool_value(raw, "showSerName")?.0,
1767 "c:dLbl/c:showSerName",
1768 )?,
1769 b"showPercent" => set_once(
1770 &mut state.show_percent,
1771 parse_bool_value(raw, "showPercent")?.0,
1772 "c:dLbl/c:showPercent",
1773 )?,
1774 b"showBubbleSize" => set_once(
1775 &mut state.show_bubble_size,
1776 parse_bool_value(raw, "showBubbleSize")?.0,
1777 "c:dLbl/c:showBubbleSize",
1778 )?,
1779 b"separator" => set_once(
1780 &mut state.separator,
1781 parse_text_element(raw, b"separator", namespaces)?.0,
1782 "c:dLbl/c:separator",
1783 )?,
1784 _ => {}
1785 }
1786 Ok(())
1787}
1788
1789#[derive(Clone, Copy, Debug)]
1790struct LabelAnchor {
1791 center: Point,
1792 base: Point,
1793 end: Point,
1794 direction: Point,
1795}
1796
1797impl LabelAnchor {
1798 const fn point(center: Point) -> Self {
1799 Self {
1800 center,
1801 base: center,
1802 end: center,
1803 direction: Point { x: 0.0, y: 0.0 },
1804 }
1805 }
1806
1807 const fn segment(center: Point, base: Point, end: Point) -> Self {
1808 Self {
1809 center,
1810 base,
1811 end,
1812 direction: Point {
1813 x: end.x - base.x,
1814 y: end.y - base.y,
1815 },
1816 }
1817 }
1818
1819 const fn directed_segment(center: Point, base: Point, end: Point, direction: Point) -> Self {
1820 Self {
1821 center,
1822 base,
1823 end,
1824 direction,
1825 }
1826 }
1827
1828 fn origin(self, position: Option<DataLabelPosition>) -> Point {
1829 let mut origin = self.center;
1830 match position.unwrap_or(DataLabelPosition::BestFit) {
1831 DataLabelPosition::Bottom => origin.y += 8.0,
1832 DataLabelPosition::Top => origin.y -= 5.0,
1833 DataLabelPosition::Left => origin.x -= 8.0,
1834 DataLabelPosition::Right => origin.x += 8.0,
1835 DataLabelPosition::InsideBase => {
1836 origin = self.along_segment(self.base, 5.0, true).unwrap_or(Point {
1837 x: origin.x,
1838 y: origin.y + 8.0,
1839 });
1840 }
1841 DataLabelPosition::InsideEnd => {
1842 origin = self.along_segment(self.end, -5.0, true).unwrap_or(origin);
1843 }
1844 DataLabelPosition::OutsideEnd => {
1845 origin = self.along_segment(self.end, 5.0, false).unwrap_or(Point {
1846 x: origin.x,
1847 y: origin.y - 5.0,
1848 });
1849 }
1850 DataLabelPosition::BestFit | DataLabelPosition::Center => {}
1851 }
1852 origin
1853 }
1854
1855 fn along_segment(self, origin: Point, distance: f64, clamp_inside: bool) -> Option<Point> {
1856 let dx = self.end.x - self.base.x;
1857 let dy = self.end.y - self.base.y;
1858 let length = dx.hypot(dy);
1859 let (dx, dy, direction_length) = if length.is_finite() && length > 0.0 {
1860 (dx, dy, length)
1861 } else {
1862 let direction_length = self.direction.x.hypot(self.direction.y);
1863 if !direction_length.is_finite() || direction_length == 0.0 {
1864 return None;
1865 }
1866 (self.direction.x, self.direction.y, direction_length)
1867 };
1868 let distance = if clamp_inside {
1869 distance.clamp(-length / 2.0, length / 2.0)
1870 } else {
1871 distance
1872 };
1873 Some(Point {
1874 x: origin.x + dx / direction_length * distance,
1875 y: origin.y + dy / direction_length * distance,
1876 })
1877 }
1878}
1879
1880fn plot_label_anchors(
1881 plot: &Plot,
1882 bounds: Rect,
1883 blanks: DispBlanksAs,
1884 options: GeometryOptions,
1885) -> Result<BTreeMap<(usize, usize), LabelAnchor>> {
1886 let mut anchors = BTreeMap::new();
1887 match plot {
1888 Plot::Bar {
1889 direction,
1890 grouping,
1891 gap_width,
1892 overlap,
1893 series,
1894 ..
1895 } => {
1896 for bar in bar_rectangles(
1897 *direction, *grouping, *gap_width, *overlap, series, bounds, options,
1898 )? {
1899 if !point_in_rect(bar.end, bounds) {
1900 continue;
1901 }
1902 let visible_base = Point {
1903 x: bar.base.x.clamp(bounds.x, bounds.x + bounds.width),
1904 y: bar.base.y.clamp(bounds.y, bounds.y + bounds.height),
1905 };
1906 let visible_end = Point {
1907 x: bar.end.x.clamp(bounds.x, bounds.x + bounds.width),
1908 y: bar.end.y.clamp(bounds.y, bounds.y + bounds.height),
1909 };
1910 anchors.insert(
1911 (bar.series_index, bar.logical_index),
1912 LabelAnchor::directed_segment(
1913 Point {
1914 x: (visible_base.x + visible_end.x) / 2.0,
1915 y: (visible_base.y + visible_end.y) / 2.0,
1916 },
1917 visible_base,
1918 visible_end,
1919 Point {
1920 x: bar.end.x - bar.base.x,
1921 y: bar.end.y - bar.base.y,
1922 },
1923 ),
1924 );
1925 }
1926 }
1927 Plot::Line {
1928 grouping, series, ..
1929 }
1930 | Plot::Area {
1931 grouping, series, ..
1932 } => {
1933 for (series_index, points) in
1934 category_label_points(*grouping, series, bounds, blanks, options)?
1935 .into_iter()
1936 .enumerate()
1937 {
1938 for (logical_index, point) in points {
1939 anchors.insert((series_index, logical_index), LabelAnchor::point(point));
1940 }
1941 }
1942 }
1943 Plot::Scatter { series, .. } => {
1944 let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
1945 let x_mapping = options.scatter_x.unwrap_or(AxisMapping {
1946 domain: x_domain,
1947 orientation: Orientation::MinMax,
1948 });
1949 let y_mapping = options.scatter_y.unwrap_or(AxisMapping {
1950 domain: y_domain,
1951 orientation: Orientation::MinMax,
1952 });
1953 for (series_index, series) in series.iter().enumerate() {
1954 let indexed = matched_scatter_points(series)?;
1955 let points = scatter_points(
1956 &indexed,
1957 x_mapping.domain,
1958 y_mapping.domain,
1959 bounds,
1960 x_mapping.orientation,
1961 y_mapping.orientation,
1962 )?;
1963 for ((logical_index, _, _), point) in indexed.into_iter().zip(points) {
1964 anchors.insert((series_index, logical_index), LabelAnchor::point(point));
1965 }
1966 }
1967 }
1968 Plot::Radar { series, .. } => {
1969 let center = Point {
1970 x: bounds.x + bounds.width / 2.0,
1971 y: bounds.y + bounds.height / 2.0,
1972 };
1973 let category_count = series_category_count(series)?;
1974 for (series_index, points) in radar_series_points(series, bounds, options)?
1975 .into_iter()
1976 .enumerate()
1977 {
1978 for (logical_index, point) in points {
1979 let spoke = radial_point(
1980 center,
1981 1.0,
1982 oriented_category_index(
1983 logical_index,
1984 category_count,
1985 options.category_orientation,
1986 ),
1987 category_count,
1988 );
1989 anchors.insert(
1990 (series_index, logical_index),
1991 LabelAnchor::directed_segment(
1992 point,
1993 center,
1994 point,
1995 Point {
1996 x: spoke.x - center.x,
1997 y: spoke.y - center.y,
1998 },
1999 ),
2000 );
2001 }
2002 }
2003 }
2004 Plot::Pie {
2005 first_slice_angle,
2006 series,
2007 ..
2008 } => {
2009 for slice in pie_slices(*first_slice_angle, None, series, bounds)? {
2010 anchors.insert(
2011 (slice.series_index, slice.logical_index),
2012 LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2013 );
2014 }
2015 }
2016 Plot::Doughnut {
2017 first_slice_angle,
2018 hole_size,
2019 series,
2020 ..
2021 } => {
2022 for slice in pie_slices(*first_slice_angle, Some(*hole_size), series, bounds)? {
2023 anchors.insert(
2024 (slice.series_index, slice.logical_index),
2025 LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2026 );
2027 }
2028 }
2029 }
2030 anchors.retain(|_, anchor| point_in_rect(anchor.end, bounds));
2031 Ok(anchors)
2032}
2033
2034fn category_label_points(
2035 grouping: Grouping,
2036 series: &[Series],
2037 bounds: Rect,
2038 blanks: DispBlanksAs,
2039 options: GeometryOptions,
2040) -> Result<Vec<Vec<(usize, Point)>>> {
2041 validate_series_values(series)?;
2042 let logical_series = logical_series_values(series)?;
2043 let category_count = series_category_count(series)?;
2044 let calculated_series = if blanks == DispBlanksAs::Zero {
2045 zero_filled_series(&logical_series, category_count)
2046 } else {
2047 logical_series.clone()
2048 };
2049 let layers = stacked_series_bounds(
2050 &calculated_series,
2051 grouping != Grouping::Standard,
2052 grouping == Grouping::PercentStacked,
2053 )?;
2054 let domain = options
2055 .value
2056 .map(|mapping| mapping.domain)
2057 .unwrap_or(domain_from_layers(&layers)?);
2058 let value_orientation = options
2059 .value
2060 .map(|mapping| mapping.orientation)
2061 .unwrap_or(Orientation::MinMax);
2062 let mut point_series = Vec::with_capacity(layers.len());
2063 for (series_index, layer) in layers.iter().enumerate() {
2064 let present: BTreeSet<_> = logical_series[series_index]
2065 .1
2066 .iter()
2067 .map(|(index, _)| *index)
2068 .collect();
2069 let indexed: Vec<_> = layer
2070 .iter()
2071 .filter(|(index, _, _)| present.contains(index))
2072 .map(|(index, _, upper)| (*index, *upper))
2073 .collect();
2074 let points = category_points(
2075 &indexed,
2076 category_count,
2077 domain,
2078 bounds,
2079 options.category_orientation,
2080 value_orientation,
2081 )?;
2082 point_series.push(
2083 indexed
2084 .into_iter()
2085 .map(|(index, _)| index)
2086 .zip(points)
2087 .collect(),
2088 );
2089 }
2090 Ok(point_series)
2091}
2092
2093fn point_in_rect(point: Point, bounds: Rect) -> bool {
2094 const EPSILON: f64 = 1.0e-9;
2095 point.x >= bounds.x - EPSILON
2096 && point.x <= bounds.x + bounds.width + EPSILON
2097 && point.y >= bounds.y - EPSILON
2098 && point.y <= bounds.y + bounds.height + EPSILON
2099}
2100
2101fn resolve_series_colours(
2102 plot: &Plot,
2103 theme: &CT_OfficeStyleSheet,
2104 color_map: &ColorMap,
2105) -> Result<Vec<Color>> {
2106 let lookup = theme_colour_lookup(theme);
2107 plot_series(plot)
2108 .iter()
2109 .enumerate()
2110 .map(|(index, series)| {
2111 if let Some(properties) = series.sp_pr.as_ref() {
2112 if let Some(fill) = properties.fill.as_ref() {
2113 return resolve_series_paint(fill, index, "fill", theme, color_map, &lookup);
2114 }
2115 if let Some(fill) = properties.line.as_ref().and_then(|line| line.fill.as_ref()) {
2116 return resolve_series_paint(fill, index, "line", theme, color_map, &lookup);
2117 }
2118 }
2119 resolve_series_accent(index, theme, color_map, &lookup)
2120 })
2121 .collect()
2122}
2123
2124fn resolve_series_paint(
2125 fill: &Fill,
2126 series_index: usize,
2127 source: &str,
2128 theme: &CT_OfficeStyleSheet,
2129 color_map: &ColorMap,
2130 lookup: &[(&str, RgbColor)],
2131) -> Result<Color> {
2132 match fill {
2133 Fill::NoFill(_) => Ok(Color {
2134 r: 0.0,
2135 g: 0.0,
2136 b: 0.0,
2137 a: 0.0,
2138 }),
2139 Fill::Solid(solid) => {
2140 let choice = solid
2141 .color
2142 .as_ref()
2143 .ok_or_else(|| ChartError::InvalidValue {
2144 element: format!("c:ser[{series_index}] direct {source} paint"),
2145 value: "solid fill has no colour choice".to_owned(),
2146 })?;
2147 resolve_direct_chart_colour(choice, theme, color_map, lookup, || {
2148 format!("c:ser[{series_index}] direct {source} paint")
2149 })
2150 }
2151 Fill::Gradient(_) => unsupported_series_paint(series_index, source, "gradient"),
2152 Fill::Pattern(_) => unsupported_series_paint(series_index, source, "pattern"),
2153 Fill::Blip(_) => unsupported_series_paint(series_index, source, "picture"),
2154 }
2155}
2156
2157fn resolve_direct_chart_colour(
2158 choice: &ColorChoice,
2159 theme: &CT_OfficeStyleSheet,
2160 color_map: &ColorMap,
2161 lookup: &[(&str, RgbColor)],
2162 element: impl FnOnce() -> String,
2163) -> Result<Color> {
2164 let ColorChoice::Scheme {
2165 value, transforms, ..
2166 } = choice
2167 else {
2168 return resolve_chart_colour(choice, color_map, lookup, element);
2169 };
2170 let theme_slot =
2171 mapped_theme_slot(value, color_map).ok_or_else(|| ChartError::InvalidValue {
2172 element: element(),
2173 value: format!("no concrete theme slot is available for scheme colour {value}"),
2174 })?;
2175 let theme_choice = theme.theme_elements.color_scheme.color(theme_slot);
2176 let base = concrete_theme_colour(theme_choice).ok_or_else(|| ChartError::InvalidValue {
2177 element: format!("a:clrScheme/a:{}", theme_slot.as_str()),
2178 value: "selected theme slot has no concrete sRGB or system fallback colour".to_owned(),
2179 })?;
2180 let mut combined = theme_choice.transforms().to_vec();
2181 combined.extend_from_slice(transforms);
2182 Ok(resolved_layout_colour(apply_color_transforms(
2183 base, &combined,
2184 )))
2185}
2186
2187fn mapped_theme_slot(value: &str, color_map: &ColorMap) -> Option<ThemeColorSlot> {
2188 let mapped = match value {
2189 "bg1" => color_map.theme_slot(ColorMapSlot::Background1),
2190 "tx1" => color_map.theme_slot(ColorMapSlot::Text1),
2191 "bg2" => color_map.theme_slot(ColorMapSlot::Background2),
2192 "tx2" => color_map.theme_slot(ColorMapSlot::Text2),
2193 "accent1" => color_map.theme_slot(ColorMapSlot::Accent1),
2194 "accent2" => color_map.theme_slot(ColorMapSlot::Accent2),
2195 "accent3" => color_map.theme_slot(ColorMapSlot::Accent3),
2196 "accent4" => color_map.theme_slot(ColorMapSlot::Accent4),
2197 "accent5" => color_map.theme_slot(ColorMapSlot::Accent5),
2198 "accent6" => color_map.theme_slot(ColorMapSlot::Accent6),
2199 "hlink" => color_map.theme_slot(ColorMapSlot::Hyperlink),
2200 "folHlink" => color_map.theme_slot(ColorMapSlot::FollowedHyperlink),
2201 "dk1" => ThemeColorSlot::Dark1,
2202 "lt1" => ThemeColorSlot::Light1,
2203 "dk2" => ThemeColorSlot::Dark2,
2204 "lt2" => ThemeColorSlot::Light2,
2205 _ => return None,
2206 };
2207 Some(mapped)
2208}
2209
2210fn unsupported_series_paint(series_index: usize, source: &str, kind: &str) -> Result<Color> {
2211 Err(ChartError::InvalidValue {
2212 element: format!("c:ser[{series_index}] direct {source} paint"),
2213 value: format!("unsupported {kind} paint"),
2214 })
2215}
2216
2217fn resolve_series_accent(
2218 series_index: usize,
2219 theme: &CT_OfficeStyleSheet,
2220 color_map: &ColorMap,
2221 lookup: &[(&str, RgbColor)],
2222) -> Result<Color> {
2223 const ACCENTS: [ColorMapSlot; 6] = [
2224 ColorMapSlot::Accent1,
2225 ColorMapSlot::Accent2,
2226 ColorMapSlot::Accent3,
2227 ColorMapSlot::Accent4,
2228 ColorMapSlot::Accent5,
2229 ColorMapSlot::Accent6,
2230 ];
2231 let semantic_slot = ACCENTS[series_index % ACCENTS.len()];
2232 let theme_slot = color_map.theme_slot(semantic_slot);
2233 let choice = theme.theme_elements.color_scheme.color(theme_slot);
2234 resolve_chart_colour(choice, color_map, lookup, || {
2235 format!("c:ser[{series_index}] theme accent")
2236 })
2237}
2238
2239fn resolve_chart_colour(
2240 choice: &ColorChoice,
2241 color_map: &ColorMap,
2242 lookup: &[(&str, RgbColor)],
2243 element: impl FnOnce() -> String,
2244) -> Result<Color> {
2245 resolve_color(choice, color_map, lookup)
2246 .map(resolved_layout_colour)
2247 .map_err(|error| ChartError::InvalidValue {
2248 element: element(),
2249 value: error.to_string(),
2250 })
2251}
2252
2253fn theme_colour_lookup(theme: &CT_OfficeStyleSheet) -> Vec<(&'static str, RgbColor)> {
2254 theme
2255 .theme_elements
2256 .color_scheme
2257 .iter()
2258 .filter_map(|(slot, choice)| {
2259 concrete_theme_colour(choice).map(|colour| (slot.as_str(), colour))
2260 })
2261 .collect()
2262}
2263
2264fn concrete_theme_colour(choice: &ColorChoice) -> Option<RgbColor> {
2265 match choice {
2266 ColorChoice::Srgb { value, .. } => Some(*value),
2267 ColorChoice::System { last_color, .. } => *last_color,
2268 ColorChoice::Scheme { .. } | ColorChoice::Preset { .. } => None,
2269 }
2270}
2271
2272fn resolved_layout_colour(colour: ResolvedColor) -> Color {
2273 Color {
2274 r: f64::from(colour.red) / 255.0,
2275 g: f64::from(colour.green) / 255.0,
2276 b: f64::from(colour.blue) / 255.0,
2277 a: f64::from(colour.alpha) / 255.0,
2278 }
2279}
2280
2281fn render_legend(
2282 chart: &CT_Chart,
2283 plot: &Plot,
2284 bounds: Rect,
2285 fonts: &mut FontManager,
2286 series_colours: &[Color],
2287 paths: &mut Vec<PositionedElement>,
2288 labels: &mut Vec<PositionedElement>,
2289) -> Result<()> {
2290 if chart.legend.is_none() {
2291 return Ok(());
2292 }
2293 let x = bounds.x + bounds.width - 58.0;
2294 for (index, (series, colour)) in plot_series(plot).iter().zip(series_colours).enumerate() {
2295 let y = bounds.y + 6.0 + index as f64 * 13.0;
2296 paths.push(filled_path(
2297 Path::rect(Rect {
2298 x,
2299 y,
2300 width: 7.0,
2301 height: 7.0,
2302 }),
2303 *colour,
2304 ));
2305 labels.push(PositionedElement::Text(shape_label(
2306 fonts,
2307 &series_name(series, index),
2308 Point {
2309 x: x + 10.0,
2310 y: y + 7.0,
2311 },
2312 )?));
2313 }
2314 Ok(())
2315}
2316
2317fn validate_geometry_coordinates(elements: &[PositionedElement]) -> Result<()> {
2318 fn finite_point(point: Point) -> bool {
2319 point.x.is_finite() && point.y.is_finite()
2320 }
2321
2322 for element in elements {
2323 match element {
2324 PositionedElement::Path(path) => {
2325 for command in &path.path.commands {
2326 let finite = match command {
2327 PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
2328 finite_point(*point)
2329 }
2330 PathCommand::CurveTo { c1, c2, to } => {
2331 finite_point(*c1) && finite_point(*c2) && finite_point(*to)
2332 }
2333 PathCommand::Close => true,
2334 };
2335 if !finite {
2336 return Err(ChartError::InvalidValue {
2337 element: "chart geometry coordinate".to_owned(),
2338 value: "generated path contains a nonfinite point".to_owned(),
2339 });
2340 }
2341 }
2342 }
2343 PositionedElement::Group(group) => validate_geometry_coordinates(&group.children)?,
2344 _ => {}
2345 }
2346 }
2347 Ok(())
2348}
2349
2350fn render_plot_geometry(
2351 plot: &Plot,
2352 bounds: Rect,
2353 blanks: DispBlanksAs,
2354 options: GeometryOptions,
2355 series_colours: &[Color],
2356) -> Result<Vec<PositionedElement>> {
2357 match plot {
2358 Plot::Bar {
2359 direction,
2360 grouping,
2361 gap_width,
2362 overlap,
2363 series,
2364 ..
2365 } => render_bar_geometry(
2366 *direction,
2367 *grouping,
2368 *gap_width,
2369 *overlap,
2370 series,
2371 bounds,
2372 options,
2373 series_colours,
2374 ),
2375 Plot::Line {
2376 grouping,
2377 marker,
2378 series,
2379 ..
2380 } => render_line_geometry(
2381 *grouping,
2382 *marker,
2383 series,
2384 bounds,
2385 blanks,
2386 options,
2387 series_colours,
2388 ),
2389 Plot::Pie {
2390 first_slice_angle,
2391 series,
2392 ..
2393 } => render_pie_geometry(*first_slice_angle, None, series, bounds, series_colours),
2394 Plot::Doughnut {
2395 first_slice_angle,
2396 hole_size,
2397 series,
2398 ..
2399 } => render_pie_geometry(
2400 *first_slice_angle,
2401 Some(*hole_size),
2402 series,
2403 bounds,
2404 series_colours,
2405 ),
2406 Plot::Area {
2407 grouping, series, ..
2408 } => render_area_geometry(*grouping, series, bounds, blanks, options, series_colours),
2409 Plot::Scatter { style, series, .. } => {
2410 render_scatter_geometry(*style, series, bounds, blanks, options, series_colours)
2411 }
2412 Plot::Radar { style, series, .. } => {
2413 render_radar_geometry(*style, series, bounds, options, series_colours)
2414 }
2415 }
2416}
2417
2418#[allow(clippy::too_many_arguments)]
2419fn render_bar_geometry(
2420 direction: BarDirection,
2421 grouping: BarGrouping,
2422 gap_width: u16,
2423 overlap: i8,
2424 series: &[Series],
2425 bounds: Rect,
2426 options: GeometryOptions,
2427 series_colours: &[Color],
2428) -> Result<Vec<PositionedElement>> {
2429 let bars = bar_rectangles(
2430 direction, grouping, gap_width, overlap, series, bounds, options,
2431 )?;
2432 bars.into_iter()
2433 .map(|bar| {
2434 Ok(filled_path(
2435 Path::rect(bar.rect),
2436 series_colour(series_colours, bar.series_index)?,
2437 ))
2438 })
2439 .collect()
2440}
2441
2442#[derive(Clone, Copy, Debug)]
2443struct BarRectangle {
2444 series_index: usize,
2445 logical_index: usize,
2446 rect: Rect,
2447 base: Point,
2448 end: Point,
2449}
2450
2451fn bar_rectangles(
2452 direction: BarDirection,
2453 grouping: BarGrouping,
2454 gap_width: u16,
2455 overlap: i8,
2456 series: &[Series],
2457 bounds: Rect,
2458 options: GeometryOptions,
2459) -> Result<Vec<BarRectangle>> {
2460 validate_series_values(series)?;
2461 let logical_series = logical_series_values(series)?;
2462 let category_count = series_category_count(series)?;
2463 let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
2464 let percent = grouping == BarGrouping::PercentStacked;
2465 let layers = stacked_series_bounds(&logical_series, stacked, percent)?;
2466 let domain = options
2467 .value
2468 .map(|mapping| mapping.domain)
2469 .unwrap_or(domain_from_layers(&layers)?);
2470 let value_orientation = options
2471 .value
2472 .map(|mapping| mapping.orientation)
2473 .unwrap_or(Orientation::MinMax);
2474 let category_extent = if direction == BarDirection::Column {
2475 bounds.width
2476 } else {
2477 bounds.height
2478 };
2479 let slot = category_extent / category_count as f64;
2480 let cluster_extent = slot * 100.0 / (100.0 + f64::from(gap_width));
2481 let bars_per_cluster = if stacked { 1 } else { series.len() };
2482 let overlap_fraction = if stacked {
2483 0.0
2484 } else {
2485 f64::from(overlap) / 100.0
2486 };
2487 let divisor =
2488 bars_per_cluster as f64 - (bars_per_cluster.saturating_sub(1)) as f64 * overlap_fraction;
2489 let bar_extent = cluster_extent / divisor;
2490 let advance = bar_extent * (1.0 - overlap_fraction);
2491 let mut bars = Vec::new();
2492 for (series_index, layer) in layers.iter().enumerate() {
2493 for &(logical_index, start, end) in layer {
2494 let category = oriented_category_index(
2495 logical_index,
2496 category_count,
2497 options.category_orientation,
2498 );
2499 let cluster_start = category as f64 / category_count as f64 * category_extent
2500 + (slot - cluster_extent) / 2.0;
2501 let category_start = if stacked {
2502 cluster_start
2503 } else {
2504 cluster_start + series_index as f64 * advance
2505 };
2506 let (rect, base, end) = if direction == BarDirection::Column {
2507 let y0 = map_y_oriented(start, domain, bounds, value_orientation)?;
2508 let y1 = map_y_oriented(end, domain, bounds, value_orientation)?;
2509 let center_x = bounds.x + category_start + bar_extent / 2.0;
2510 (
2511 Rect {
2512 x: bounds.x + category_start,
2513 y: y0.min(y1),
2514 width: bar_extent,
2515 height: (y1 - y0).abs(),
2516 },
2517 Point { x: center_x, y: y0 },
2518 Point { x: center_x, y: y1 },
2519 )
2520 } else {
2521 let x0 = map_x_oriented(start, domain, bounds, value_orientation)?;
2522 let x1 = map_x_oriented(end, domain, bounds, value_orientation)?;
2523 let center_y = bounds.y + category_start + bar_extent / 2.0;
2524 (
2525 Rect {
2526 x: x0.min(x1),
2527 y: bounds.y + category_start,
2528 width: (x1 - x0).abs(),
2529 height: bar_extent,
2530 },
2531 Point { x: x0, y: center_y },
2532 Point { x: x1, y: center_y },
2533 )
2534 };
2535 bars.push(BarRectangle {
2536 series_index,
2537 logical_index,
2538 rect,
2539 base,
2540 end,
2541 });
2542 }
2543 }
2544 Ok(bars)
2545}
2546
2547fn render_line_geometry(
2548 grouping: Grouping,
2549 marker: bool,
2550 series: &[Series],
2551 bounds: Rect,
2552 blanks: DispBlanksAs,
2553 options: GeometryOptions,
2554 series_colours: &[Color],
2555) -> Result<Vec<PositionedElement>> {
2556 validate_series_values(series)?;
2557 let logical_series = logical_series_values(series)?;
2558 let category_count = series_category_count(series)?;
2559 let calculated_series = if blanks == DispBlanksAs::Zero {
2560 zero_filled_series(&logical_series, category_count)
2561 } else {
2562 logical_series.clone()
2563 };
2564 let stacked = grouping != Grouping::Standard;
2565 let percent = grouping == Grouping::PercentStacked;
2566 let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
2567 let domain = options
2568 .value
2569 .map(|mapping| mapping.domain)
2570 .unwrap_or(domain_from_layers(&layers)?);
2571 let value_orientation = options
2572 .value
2573 .map(|mapping| mapping.orientation)
2574 .unwrap_or(Orientation::MinMax);
2575 let mut elements = Vec::new();
2576 for (series_index, layer) in layers.iter().enumerate() {
2577 let colour = series_colour(series_colours, series_index)?;
2578 let indexed: Vec<_> = layer
2579 .iter()
2580 .map(|(index, _, upper)| (*index, *upper))
2581 .collect();
2582 let indexes: Vec<_> = indexed.iter().map(|(index, _)| *index).collect();
2583 for (start, end) in contiguous_ranges(&indexes, blanks) {
2584 let points = category_points(
2585 &indexed[start..end],
2586 category_count,
2587 domain,
2588 bounds,
2589 options.category_orientation,
2590 value_orientation,
2591 )?;
2592 elements.push(stroked_path(polyline_path(&points, false), colour));
2593 }
2594 if marker {
2595 let present: BTreeSet<_> = logical_series[series_index]
2596 .1
2597 .iter()
2598 .map(|(index, _)| *index)
2599 .collect();
2600 let marker_values: Vec<_> = indexed
2601 .iter()
2602 .copied()
2603 .filter(|(index, _)| present.contains(index))
2604 .collect();
2605 let marker_points = category_points(
2606 &marker_values,
2607 category_count,
2608 domain,
2609 bounds,
2610 options.category_orientation,
2611 value_orientation,
2612 )?;
2613 push_markers(&mut elements, &marker_points, colour);
2614 }
2615 }
2616 Ok(elements)
2617}
2618
2619fn render_area_geometry(
2620 grouping: Grouping,
2621 series: &[Series],
2622 bounds: Rect,
2623 blanks: DispBlanksAs,
2624 options: GeometryOptions,
2625 series_colours: &[Color],
2626) -> Result<Vec<PositionedElement>> {
2627 validate_series_values(series)?;
2628 let logical_series = logical_series_values(series)?;
2629 let category_count = series_category_count(series)?;
2630 let calculated_series = if blanks == DispBlanksAs::Zero {
2631 zero_filled_series(&logical_series, category_count)
2632 } else {
2633 logical_series
2634 };
2635 let stacked = grouping != Grouping::Standard;
2636 let percent = grouping == Grouping::PercentStacked;
2637 let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
2638 let domain = options
2639 .value
2640 .map(|mapping| mapping.domain)
2641 .unwrap_or(domain_from_layers(&layers)?);
2642 let value_orientation = options
2643 .value
2644 .map(|mapping| mapping.orientation)
2645 .unwrap_or(Orientation::MinMax);
2646 let mut elements = Vec::new();
2647 for (series_index, layer) in layers.iter().enumerate() {
2648 let colour = series_colour(series_colours, series_index)?;
2649 let indexes: Vec<_> = layer.iter().map(|(index, _, _)| *index).collect();
2650 for (start, end) in contiguous_ranges(&indexes, blanks) {
2651 let segment = &layer[start..end];
2652 let top_values: Vec<_> = segment
2653 .iter()
2654 .map(|(index, _, upper)| (*index, *upper))
2655 .collect();
2656 let lower_values: Vec<_> = segment
2657 .iter()
2658 .map(|(index, lower, _)| (*index, *lower))
2659 .collect();
2660 let top = category_points(
2661 &top_values,
2662 category_count,
2663 domain,
2664 bounds,
2665 options.category_orientation,
2666 value_orientation,
2667 )?;
2668 let mut bottom = category_points(
2669 &lower_values,
2670 category_count,
2671 domain,
2672 bounds,
2673 options.category_orientation,
2674 value_orientation,
2675 )?;
2676 bottom.reverse();
2677 let mut commands = Vec::with_capacity(top.len() + bottom.len() + 2);
2678 commands.push(PathCommand::MoveTo(top[0]));
2679 commands.extend(top.iter().skip(1).copied().map(PathCommand::LineTo));
2680 commands.extend(bottom.into_iter().map(PathCommand::LineTo));
2681 commands.push(PathCommand::Close);
2682 elements.push(filled_and_stroked_path(
2683 Path {
2684 commands,
2685 fill_rule: FillRule::NonZero,
2686 },
2687 colour,
2688 ));
2689 }
2690 }
2691 Ok(elements)
2692}
2693
2694fn render_scatter_geometry(
2695 style: ScatterStyle,
2696 series: &[Series],
2697 bounds: Rect,
2698 blanks: DispBlanksAs,
2699 options: GeometryOptions,
2700 series_colours: &[Color],
2701) -> Result<Vec<PositionedElement>> {
2702 validate_series_values(series)?;
2703 let mut paired_series = Vec::with_capacity(series.len());
2704 let mut marker_series = Vec::with_capacity(series.len());
2705 let mut all_x = Vec::new();
2706 let mut all_y = Vec::new();
2707 for item in series {
2708 let markers = matched_scatter_points(item)?;
2709 let paired = scatter_render_points(item, blanks)?;
2710 all_x.extend(paired.iter().map(|(_, x, _)| *x));
2711 all_y.extend(paired.iter().map(|(_, _, y)| *y));
2712 paired_series.push(paired);
2713 marker_series.push(markers);
2714 }
2715 let x_domain = options
2716 .scatter_x
2717 .map(|mapping| mapping.domain)
2718 .unwrap_or(data_domain(&all_x, false)?);
2719 let y_domain = options
2720 .scatter_y
2721 .map(|mapping| mapping.domain)
2722 .unwrap_or(data_domain(&all_y, false)?);
2723 let x_orientation = options
2724 .scatter_x
2725 .map(|mapping| mapping.orientation)
2726 .unwrap_or(Orientation::MinMax);
2727 let y_orientation = options
2728 .scatter_y
2729 .map(|mapping| mapping.orientation)
2730 .unwrap_or(Orientation::MinMax);
2731 let draw_line = matches!(
2732 style,
2733 ScatterStyle::Line
2734 | ScatterStyle::LineMarker
2735 | ScatterStyle::Smooth
2736 | ScatterStyle::SmoothMarker
2737 );
2738 let draw_marker = matches!(
2739 style,
2740 ScatterStyle::Marker | ScatterStyle::LineMarker | ScatterStyle::SmoothMarker
2741 );
2742 let mut elements = Vec::new();
2743 for (series_index, paired) in paired_series.iter().enumerate() {
2744 let colour = series_colour(series_colours, series_index)?;
2745 if draw_line {
2746 let indexes: Vec<_> = paired.iter().map(|(index, _, _)| *index).collect();
2747 for (start, end) in contiguous_ranges(&indexes, blanks) {
2748 let points = scatter_points(
2749 &paired[start..end],
2750 x_domain,
2751 y_domain,
2752 bounds,
2753 x_orientation,
2754 y_orientation,
2755 )?;
2756 elements.push(stroked_path(polyline_path(&points, false), colour));
2757 }
2758 }
2759 if draw_marker {
2760 let marker_points = scatter_points(
2761 &marker_series[series_index],
2762 x_domain,
2763 y_domain,
2764 bounds,
2765 x_orientation,
2766 y_orientation,
2767 )?;
2768 push_markers(&mut elements, &marker_points, colour);
2769 }
2770 }
2771 Ok(elements)
2772}
2773
2774type ScatterPoint = (usize, f64, f64);
2775
2776struct ScatterPointMaps {
2777 x_count: usize,
2778 y_count: usize,
2779 x_by_index: BTreeMap<usize, f64>,
2780 y_by_index: BTreeMap<usize, f64>,
2781}
2782
2783fn scatter_point_maps(series: &Series) -> Result<ScatterPointMaps> {
2784 let Some(AxisData::Numeric(x_values)) = &series.categories else {
2785 return Err(ChartError::InvalidValue {
2786 element: "c:xVal".to_owned(),
2787 value: "scatter series requires numeric cached x values".to_owned(),
2788 });
2789 };
2790 validate_finite(&x_values.values, "c:xVal/c:numCache")?;
2791 let (x_count, x_points) = logical_numeric_values(x_values)?;
2792 let (y_count, y_points) = logical_numeric_values(&series.values)?;
2793 Ok(ScatterPointMaps {
2794 x_count,
2795 y_count,
2796 x_by_index: x_points.into_iter().collect(),
2797 y_by_index: y_points.into_iter().collect(),
2798 })
2799}
2800
2801fn matched_scatter_points(series: &Series) -> Result<Vec<ScatterPoint>> {
2802 let points = scatter_point_maps(series)?;
2803 Ok(points
2804 .y_by_index
2805 .iter()
2806 .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
2807 .collect())
2808}
2809
2810fn scatter_render_points(series: &Series, blanks: DispBlanksAs) -> Result<Vec<ScatterPoint>> {
2811 let points = scatter_point_maps(series)?;
2812 if blanks != DispBlanksAs::Zero {
2813 return Ok(points
2814 .y_by_index
2815 .iter()
2816 .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
2817 .collect());
2818 }
2819 let count = points.x_count.max(points.y_count);
2820 let present = points
2821 .x_by_index
2822 .keys()
2823 .chain(points.y_by_index.keys())
2824 .copied()
2825 .collect();
2826 Ok(zero_control_indexes(count, &present)
2827 .into_iter()
2828 .map(|index| {
2829 (
2830 index,
2831 points.x_by_index.get(&index).copied().unwrap_or(0.0),
2832 points.y_by_index.get(&index).copied().unwrap_or(0.0),
2833 )
2834 })
2835 .collect())
2836}
2837
2838fn scatter_points(
2839 values: &[(usize, f64, f64)],
2840 x_domain: Domain,
2841 y_domain: Domain,
2842 bounds: Rect,
2843 x_orientation: Orientation,
2844 y_orientation: Orientation,
2845) -> Result<Vec<Point>> {
2846 values
2847 .iter()
2848 .map(|(_, x, y)| {
2849 Ok(Point {
2850 x: map_x_oriented(*x, x_domain, bounds, x_orientation)?,
2851 y: map_y_oriented(*y, y_domain, bounds, y_orientation)?,
2852 })
2853 })
2854 .collect()
2855}
2856
2857fn render_radar_geometry(
2858 style: RadarStyle,
2859 series: &[Series],
2860 bounds: Rect,
2861 options: GeometryOptions,
2862 series_colours: &[Color],
2863) -> Result<Vec<PositionedElement>> {
2864 let point_series = radar_series_points(series, bounds, options)?;
2865 let mut elements = Vec::new();
2866 for (series_index, points) in point_series.iter().enumerate() {
2867 let colour = series_colour(series_colours, series_index)?;
2868 let plain_points: Vec<_> = points.iter().map(|(_, point)| *point).collect();
2869 if plain_points.is_empty() {
2870 continue;
2871 }
2872 let path = polyline_path(&plain_points, true);
2873 if style == RadarStyle::Filled {
2874 elements.push(filled_and_stroked_path(path, colour));
2875 } else {
2876 elements.push(stroked_path(path, colour));
2877 }
2878 if style == RadarStyle::Marker {
2879 push_markers(&mut elements, &plain_points, colour);
2880 }
2881 }
2882 Ok(elements)
2883}
2884
2885fn radar_series_points(
2886 series: &[Series],
2887 bounds: Rect,
2888 options: GeometryOptions,
2889) -> Result<Vec<Vec<(usize, Point)>>> {
2890 validate_series_values(series)?;
2891 let logical_series = logical_series_values(series)?;
2892 let category_count = series_category_count(series)?;
2893 let values = logical_series
2894 .iter()
2895 .flat_map(|(_, points)| points.iter().map(|(_, value)| *value))
2896 .collect::<Vec<_>>();
2897 let mapping = options.value.unwrap_or(AxisMapping {
2898 domain: data_domain(&values, true)?,
2899 orientation: Orientation::MinMax,
2900 });
2901 let center = Point {
2902 x: bounds.x + bounds.width / 2.0,
2903 y: bounds.y + bounds.height / 2.0,
2904 };
2905 let radius = bounds.width.min(bounds.height) / 2.0;
2906 let mut point_series = Vec::with_capacity(logical_series.len());
2907 for (_, logical) in &logical_series {
2908 let mut points = Vec::with_capacity(logical.len());
2909 for (category, value) in logical {
2910 if *value < mapping.domain.min || *value > mapping.domain.max {
2911 continue;
2912 }
2913 points.push((
2914 *category,
2915 radial_point(
2916 center,
2917 normalized_value_oriented(*value, mapping.domain, mapping.orientation)?
2918 * radius,
2919 oriented_category_index(
2920 *category,
2921 category_count,
2922 options.category_orientation,
2923 ),
2924 category_count,
2925 ),
2926 ));
2927 }
2928 point_series.push(points);
2929 }
2930 Ok(point_series)
2931}
2932
2933fn render_pie_geometry(
2934 first_slice_angle: u16,
2935 hole_size: Option<u8>,
2936 series: &[Series],
2937 bounds: Rect,
2938 series_colours: &[Color],
2939) -> Result<Vec<PositionedElement>> {
2940 pie_slices(first_slice_angle, hole_size, series, bounds)?
2941 .into_iter()
2942 .map(|slice| {
2943 Ok(filled_path(
2944 slice.path,
2945 series_colour(series_colours, slice.series_index)?,
2946 ))
2947 })
2948 .collect()
2949}
2950
2951struct PieSliceGeometry {
2952 series_index: usize,
2953 logical_index: usize,
2954 path: Path,
2955 anchor: Point,
2956 base: Point,
2957 end: Point,
2958}
2959
2960fn pie_slices(
2961 first_slice_angle: u16,
2962 hole_size: Option<u8>,
2963 series: &[Series],
2964 bounds: Rect,
2965) -> Result<Vec<PieSliceGeometry>> {
2966 validate_series_values(series)?;
2967 let center = Point {
2968 x: bounds.x + bounds.width / 2.0,
2969 y: bounds.y + bounds.height / 2.0,
2970 };
2971 let radius = bounds.width.min(bounds.height) / 2.0;
2972 let mut elements = Vec::new();
2973 for (series_index, item) in series.iter().enumerate() {
2974 let (_, logical) = logical_numeric_values(&item.values)?;
2975 let total = logical
2976 .iter()
2977 .map(|(_, value)| *value)
2978 .filter(|value| *value > 0.0)
2979 .try_fold(0.0, |total, value| {
2980 checked_geometry_sum(total, value, "pie value total")
2981 })?;
2982 if total <= 0.0 {
2983 continue;
2984 }
2985 let mut angle = f64::from(first_slice_angle).to_radians() - std::f64::consts::FRAC_PI_2;
2986 for (logical_index, value) in logical {
2987 if value <= 0.0 {
2988 continue;
2989 }
2990 let next = angle + value / total * std::f64::consts::TAU;
2991 let path = if let Some(hole_size) = hole_size {
2992 doughnut_wedge(
2993 center,
2994 radius,
2995 radius * f64::from(hole_size) / 100.0,
2996 angle,
2997 next,
2998 )
2999 } else {
3000 pie_wedge(center, radius, angle, next)
3001 };
3002 let inner_radius = hole_size
3003 .map(|hole| radius * f64::from(hole) / 100.0)
3004 .unwrap_or(0.0);
3005 let anchor_radius = hole_size
3006 .map(|_| (radius + inner_radius) / 2.0)
3007 .unwrap_or(radius * 2.0 / 3.0);
3008 let midpoint = (angle + next) / 2.0;
3009 elements.push(PieSliceGeometry {
3010 series_index,
3011 logical_index,
3012 path,
3013 anchor: circle_point(center, anchor_radius, midpoint),
3014 base: circle_point(center, inner_radius, midpoint),
3015 end: circle_point(center, radius, midpoint),
3016 });
3017 angle = next;
3018 }
3019 }
3020 Ok(elements)
3021}
3022
3023fn validate_series_values(series: &[Series]) -> Result<()> {
3024 if series.is_empty() {
3025 return Err(ChartError::MissingElement("c:ser".to_owned()));
3026 }
3027 for item in series {
3028 validate_finite(&item.values.values, "c:val/c:numCache")?;
3029 }
3030 Ok(())
3031}
3032
3033fn validate_finite(values: &[f64], element: &str) -> Result<()> {
3034 if let Some(value) = values.iter().find(|value| !value.is_finite()) {
3035 return Err(ChartError::InvalidValue {
3036 element: element.to_owned(),
3037 value: value.to_string(),
3038 });
3039 }
3040 Ok(())
3041}
3042
3043type IndexedValues = Vec<(usize, f64)>;
3044type LogicalSeries = (usize, IndexedValues);
3045type GeometryLayer = Vec<(usize, f64, f64)>;
3046
3047fn zero_control_indexes(count: usize, present: &BTreeSet<usize>) -> BTreeSet<usize> {
3048 let mut controls = present.clone();
3049 if count == 0 {
3050 return controls;
3051 }
3052 controls.insert(0);
3053 controls.insert(count - 1);
3054 for index in present {
3055 if *index > 0 {
3056 controls.insert(*index - 1);
3057 }
3058 if index + 1 < count {
3059 controls.insert(index + 1);
3060 }
3061 }
3062 controls
3063}
3064
3065fn zero_filled_series(series: &[LogicalSeries], count: usize) -> Vec<LogicalSeries> {
3066 let present = series
3067 .iter()
3068 .flat_map(|(_, points)| points.iter().map(|(index, _)| *index))
3069 .collect();
3070 let controls = zero_control_indexes(count, &present);
3071 series
3072 .iter()
3073 .map(|(declared, points)| {
3074 let values: BTreeMap<_, _> = points.iter().copied().collect();
3075 let filled = controls
3076 .iter()
3077 .map(|index| (*index, values.get(index).copied().unwrap_or(0.0)))
3078 .collect();
3079 (*declared, filled)
3080 })
3081 .collect()
3082}
3083
3084fn contiguous_ranges(indexes: &[usize], blanks: DispBlanksAs) -> Vec<(usize, usize)> {
3085 if indexes.is_empty() {
3086 return Vec::new();
3087 }
3088 if blanks != DispBlanksAs::Gap {
3089 return vec![(0, indexes.len())];
3090 }
3091 let mut ranges = Vec::new();
3092 let mut start = 0usize;
3093 for position in 1..indexes.len() {
3094 if indexes[position - 1].checked_add(1) != Some(indexes[position]) {
3095 ranges.push((start, position));
3096 start = position;
3097 }
3098 }
3099 ranges.push((start, indexes.len()));
3100 ranges
3101}
3102
3103fn logical_numeric_values(data: &NumericData) -> Result<LogicalSeries> {
3104 validate_finite(&data.values, "c:numCache")?;
3105 let (declared, indexes) = cache_layout(&data.markup, data.values.len())?;
3106 let declared = usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
3107 element: "c:ptCount".to_owned(),
3108 value: declared.to_string(),
3109 })?;
3110 let points = indexes
3111 .into_iter()
3112 .zip(data.values.iter().copied())
3113 .map(|(index, value)| {
3114 usize::try_from(index)
3115 .map(|index| (index, value))
3116 .map_err(|_| ChartError::InvalidValue {
3117 element: "c:pt/@idx".to_owned(),
3118 value: index.to_string(),
3119 })
3120 })
3121 .collect::<Result<Vec<_>>>()?;
3122 Ok((declared, points))
3123}
3124
3125fn logical_series_values(series: &[Series]) -> Result<Vec<LogicalSeries>> {
3126 series
3127 .iter()
3128 .map(|item| logical_numeric_values(&item.values))
3129 .collect()
3130}
3131
3132fn reference_logical_count(markup: &ReferenceMarkup, value_count: usize) -> Result<usize> {
3133 let (declared, _) = cache_layout(markup, value_count)?;
3134 usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
3135 element: "c:ptCount".to_owned(),
3136 value: declared.to_string(),
3137 })
3138}
3139
3140fn series_category_count(series: &[Series]) -> Result<usize> {
3141 let mut count = 0usize;
3142 for item in series {
3143 count = count.max(reference_logical_count(
3144 &item.values.markup,
3145 item.values.values.len(),
3146 )?);
3147 if let Some(categories) = &item.categories {
3148 let category_count = match categories {
3149 AxisData::String(data) => reference_logical_count(&data.markup, data.values.len())?,
3150 AxisData::Numeric(data) => {
3151 reference_logical_count(&data.markup, data.values.len())?
3152 }
3153 };
3154 count = count.max(category_count);
3155 }
3156 }
3157 if count == 0 {
3158 return Err(ChartError::InvalidValue {
3159 element: "c:ser".to_owned(),
3160 value: "cached series contain no points".to_owned(),
3161 });
3162 }
3163 Ok(count)
3164}
3165
3166fn stacked_series_bounds(
3167 series: &[LogicalSeries],
3168 stacked: bool,
3169 percent: bool,
3170) -> Result<Vec<GeometryLayer>> {
3171 let mut positive_totals = BTreeMap::<usize, f64>::new();
3172 let mut negative_totals = BTreeMap::<usize, f64>::new();
3173 if percent {
3174 for (_, points) in series {
3175 for &(index, value) in points {
3176 let totals = if value >= 0.0 {
3177 &mut positive_totals
3178 } else {
3179 &mut negative_totals
3180 };
3181 let total = totals.entry(index).or_default();
3182 *total = checked_geometry_sum(*total, value.abs(), "stacked percentage total")?;
3183 }
3184 }
3185 }
3186 let mut positive = BTreeMap::<usize, f64>::new();
3187 let mut negative = BTreeMap::<usize, f64>::new();
3188 let mut layers = Vec::with_capacity(series.len());
3189 for (_, points) in series {
3190 let mut layer = Vec::with_capacity(points.len());
3191 for &(index, original) in points {
3192 let total = if original >= 0.0 {
3193 positive_totals.get(&index).copied().unwrap_or(0.0)
3194 } else {
3195 negative_totals.get(&index).copied().unwrap_or(0.0)
3196 };
3197 let value = if percent && total != 0.0 {
3198 original / total
3199 } else {
3200 original
3201 };
3202 if stacked {
3203 let accumulators = if value >= 0.0 {
3204 &mut positive
3205 } else {
3206 &mut negative
3207 };
3208 let accumulator = accumulators.entry(index).or_default();
3209 let lower = *accumulator;
3210 *accumulator = checked_geometry_sum(*accumulator, value, "stacked value total")?;
3211 layer.push((index, lower, *accumulator));
3212 } else {
3213 layer.push((index, 0.0, value));
3214 }
3215 }
3216 layers.push(layer);
3217 }
3218 Ok(layers)
3219}
3220
3221fn checked_geometry_sum(current: f64, value: f64, context: &str) -> Result<f64> {
3222 let sum = current + value;
3223 if sum.is_finite() {
3224 Ok(sum)
3225 } else {
3226 Err(ChartError::InvalidValue {
3227 element: "chart geometry aggregate".to_owned(),
3228 value: format!("{context} is nonfinite"),
3229 })
3230 }
3231}
3232
3233#[derive(Clone, Copy, Debug, PartialEq)]
3234struct Domain {
3235 min: f64,
3236 max: f64,
3237}
3238
3239fn nice_number_scale(
3240 extent: Domain,
3241 pinned_minimum: Option<f64>,
3242 pinned_maximum: Option<f64>,
3243 target_tick_count: usize,
3244) -> Result<AxisScale> {
3245 if target_tick_count < 2
3246 || !extent.min.is_finite()
3247 || !extent.max.is_finite()
3248 || pinned_minimum.is_some_and(|value| !value.is_finite())
3249 || pinned_maximum.is_some_and(|value| !value.is_finite())
3250 {
3251 return Err(ChartError::InvalidValue {
3252 element: "chart axis scale".to_owned(),
3253 value: "scale bounds and target tick count must be finite and valid".to_owned(),
3254 });
3255 }
3256
3257 let mut data_min = extent.min.min(extent.max);
3258 let mut data_max = extent.min.max(extent.max);
3259 if data_min == data_max {
3260 let expanded = expand_constant_domain(data_min)?;
3261 data_min = expanded.min;
3262 data_max = expanded.max;
3263 }
3264 let requested_min = match (pinned_minimum, pinned_maximum) {
3265 (Some(minimum), _) => minimum,
3266 (None, Some(maximum)) if data_min >= maximum => {
3267 extend_bound(maximum, stable_positive_step(data_min, data_max, 3)?, false)?
3268 }
3269 (None, _) => data_min,
3270 };
3271 let requested_max = match (pinned_minimum, pinned_maximum) {
3272 (_, Some(maximum)) => maximum,
3273 (Some(minimum), None) if data_max <= minimum => {
3274 extend_bound(minimum, stable_positive_step(data_min, data_max, 3)?, true)?
3275 }
3276 (_, None) => data_max,
3277 };
3278 if requested_min >= requested_max {
3279 return Err(ChartError::InvalidValue {
3280 element: "c:scaling".to_owned(),
3281 value: format!("minimum {requested_min} is not less than maximum {requested_max}"),
3282 });
3283 }
3284
3285 let raw_step = stable_positive_step(requested_min, requested_max, target_tick_count)?;
3286 let step = nice_step(raw_step)?;
3287 let rounded_minimum = (requested_min / step).floor() * step;
3288 let rounded_maximum = (requested_max / step).ceil() * step;
3289 let lower_fallback = pinned_minimum.is_none() && !rounded_minimum.is_finite();
3290 let upper_fallback = pinned_maximum.is_none() && !rounded_maximum.is_finite();
3291 let minimum = pinned_minimum.unwrap_or(if rounded_minimum.is_finite() {
3292 rounded_minimum
3293 } else {
3294 requested_min
3295 });
3296 let maximum = pinned_maximum.unwrap_or(if rounded_maximum.is_finite() {
3297 rounded_maximum
3298 } else {
3299 requested_max
3300 });
3301 if !minimum.is_finite() || !maximum.is_finite() || minimum >= maximum {
3302 return Err(ChartError::InvalidValue {
3303 element: "chart axis scale".to_owned(),
3304 value: "nice-number bounds are nonfinite or empty".to_owned(),
3305 });
3306 }
3307
3308 let first = (minimum / step).ceil();
3309 let last = (maximum / step).floor();
3310 let tick_count = if last >= first {
3311 (last - first).min(1024.0) as usize + 1
3312 } else {
3313 0
3314 };
3315 let mut ticks = Vec::with_capacity(tick_count);
3316 for index in 0..tick_count {
3317 let tick = (first + index as f64) * step;
3318 if tick.is_finite() && tick >= minimum && tick <= maximum {
3319 ticks.push(clean_tick(tick, step));
3320 }
3321 }
3322 if lower_fallback && ticks.first().is_none_or(|tick| *tick > minimum) {
3323 ticks.insert(0, minimum);
3324 }
3325 if upper_fallback && ticks.last().is_none_or(|tick| *tick < maximum) {
3326 ticks.push(maximum);
3327 }
3328 if ticks.len() < 2 {
3329 ticks.extend([minimum, maximum]);
3330 ticks.sort_by(f64::total_cmp);
3331 }
3332 ticks.dedup_by(|left, right| *left == *right);
3333 if ticks.len() < 2 || !ticks.windows(2).all(|pair| pair[0] < pair[1]) {
3334 return Err(ChartError::InvalidValue {
3335 element: "chart axis scale".to_owned(),
3336 value: "scale cannot produce two increasing finite ticks".to_owned(),
3337 });
3338 }
3339 Ok(AxisScale {
3340 domain: Domain {
3341 min: minimum,
3342 max: maximum,
3343 },
3344 ticks,
3345 })
3346}
3347
3348const MIN_POSITIVE_SUBNORMAL: f64 = f64::from_bits(1);
3349
3350fn stable_positive_step(minimum: f64, maximum: f64, target_tick_count: usize) -> Result<f64> {
3351 let scale = minimum.abs().max(maximum.abs());
3352 let scale = if scale == 0.0 {
3353 MIN_POSITIVE_SUBNORMAL
3354 } else {
3355 scale
3356 };
3357 let normalized =
3358 (maximum / scale - minimum / scale) / target_tick_count.saturating_sub(1) as f64;
3359 let step = normalized * scale;
3360 if normalized.is_finite() && normalized > 0.0 && step.is_finite() {
3361 Ok(step.max(MIN_POSITIVE_SUBNORMAL))
3362 } else {
3363 Err(ChartError::InvalidValue {
3364 element: "chart axis scale".to_owned(),
3365 value: "scale step is nonfinite or empty".to_owned(),
3366 })
3367 }
3368}
3369
3370fn nice_step(raw_step: f64) -> Result<f64> {
3371 if !raw_step.is_finite() || raw_step <= 0.0 {
3372 return Err(ChartError::InvalidValue {
3373 element: "chart axis scale".to_owned(),
3374 value: format!("invalid raw tick step {raw_step}"),
3375 });
3376 }
3377 let magnitude = 10.0_f64
3378 .powf(raw_step.log10().floor())
3379 .max(MIN_POSITIVE_SUBNORMAL);
3380 let fraction = raw_step / magnitude;
3381 let multiplier = if fraction <= 1.0 {
3382 1.0
3383 } else if fraction <= 2.0 {
3384 2.0
3385 } else if fraction <= 5.0 {
3386 5.0
3387 } else {
3388 10.0
3389 };
3390 let step = (multiplier * magnitude).max(MIN_POSITIVE_SUBNORMAL);
3391 if step.is_finite() && step > 0.0 {
3392 Ok(step)
3393 } else {
3394 Err(ChartError::InvalidValue {
3395 element: "chart axis scale".to_owned(),
3396 value: "nice-number step is nonfinite".to_owned(),
3397 })
3398 }
3399}
3400
3401fn expand_constant_domain(value: f64) -> Result<Domain> {
3402 if value == 0.0 {
3403 return Ok(Domain {
3404 min: -1.0,
3405 max: 1.0,
3406 });
3407 }
3408 let padding = (value.abs() * 0.5).max(MIN_POSITIVE_SUBNORMAL);
3409 let minimum = value - padding;
3410 let maximum = value + padding;
3411 if minimum.is_finite() && maximum.is_finite() && minimum < maximum {
3412 return Ok(Domain {
3413 min: minimum,
3414 max: maximum,
3415 });
3416 }
3417 let domain = if value.is_sign_positive() {
3418 Domain {
3419 min: 0.0,
3420 max: value,
3421 }
3422 } else {
3423 Domain {
3424 min: value,
3425 max: 0.0,
3426 }
3427 };
3428 if domain.min < domain.max {
3429 Ok(domain)
3430 } else {
3431 Err(ChartError::InvalidValue {
3432 element: "chart axis scale".to_owned(),
3433 value: format!("constant value {value} has no finite renderable extent"),
3434 })
3435 }
3436}
3437
3438fn extend_bound(bound: f64, span: f64, positive: bool) -> Result<f64> {
3439 let candidate = if positive { bound + span } else { bound - span };
3440 let extended = if candidate.is_finite()
3441 && if positive {
3442 candidate > bound
3443 } else {
3444 candidate < bound
3445 } {
3446 candidate
3447 } else if positive {
3448 bound.next_up()
3449 } else {
3450 bound.next_down()
3451 };
3452 if extended.is_finite()
3453 && if positive {
3454 extended > bound
3455 } else {
3456 extended < bound
3457 }
3458 {
3459 Ok(extended)
3460 } else {
3461 Err(ChartError::InvalidValue {
3462 element: "c:scaling".to_owned(),
3463 value: format!("one-sided bound {bound} leaves no finite automatic extent"),
3464 })
3465 }
3466}
3467
3468fn clean_tick(value: f64, step: f64) -> f64 {
3469 if value.abs() <= step.abs() * 1.0e-12 {
3470 0.0
3471 } else {
3472 value
3473 }
3474}
3475
3476fn domain_from_layers(layers: &[GeometryLayer]) -> Result<Domain> {
3477 let values = layers
3478 .iter()
3479 .flatten()
3480 .flat_map(|(_, lower, upper)| [*lower, *upper]);
3481 let mut min = 0.0_f64;
3482 let mut max = 0.0_f64;
3483 for value in values {
3484 if !value.is_finite() {
3485 return Err(ChartError::InvalidValue {
3486 element: "chart geometry domain".to_owned(),
3487 value: "aggregate produced a nonfinite value".to_owned(),
3488 });
3489 }
3490 min = min.min(value);
3491 max = max.max(value);
3492 }
3493 if min == max {
3494 max = 1.0;
3495 }
3496 Ok(Domain { min, max })
3497}
3498
3499fn data_domain(values: &[f64], include_zero: bool) -> Result<Domain> {
3500 validate_finite(values, "chart geometry cache")?;
3501 let Some(first) = values.first().copied() else {
3502 return Err(ChartError::InvalidValue {
3503 element: "c:numCache".to_owned(),
3504 value: "cached series contain no points".to_owned(),
3505 });
3506 };
3507 let mut min = first;
3508 let mut max = first;
3509 for value in values.iter().copied() {
3510 min = min.min(value);
3511 max = max.max(value);
3512 }
3513 if include_zero {
3514 min = min.min(0.0);
3515 max = max.max(0.0);
3516 }
3517 if min == max {
3518 if !include_zero {
3519 return expand_constant_domain(min);
3520 }
3521 if min > 0.0 {
3522 min = 0.0;
3523 } else if max < 0.0 {
3524 max = 0.0;
3525 } else {
3526 max = 1.0;
3527 }
3528 }
3529 Ok(Domain { min, max })
3530}
3531
3532fn normalized_value(value: f64, domain: Domain) -> Result<f64> {
3533 let scale = value
3534 .abs()
3535 .max(domain.min.abs())
3536 .max(domain.max.abs())
3537 .max(1.0);
3538 let value = value / scale;
3539 let min = domain.min / scale;
3540 let max = domain.max / scale;
3541 let range = max - min;
3542 let normalized = (value - min) / range;
3543 if !range.is_finite() || range <= 0.0 || !normalized.is_finite() {
3544 return Err(ChartError::InvalidValue {
3545 element: "chart geometry domain".to_owned(),
3546 value: format!("cannot map finite domain {} to {}", domain.min, domain.max),
3547 });
3548 }
3549 Ok(normalized)
3550}
3551
3552fn normalized_value_oriented(value: f64, domain: Domain, orientation: Orientation) -> Result<f64> {
3553 let normalized = normalized_value(value, domain)?;
3554 Ok(if orientation == Orientation::MaxMin {
3555 1.0 - normalized
3556 } else {
3557 normalized
3558 })
3559}
3560
3561fn map_x_oriented(
3562 value: f64,
3563 domain: Domain,
3564 bounds: Rect,
3565 orientation: Orientation,
3566) -> Result<f64> {
3567 let coordinate =
3568 bounds.x + normalized_value_oriented(value, domain, orientation)? * bounds.width;
3569 if coordinate.is_finite() {
3570 Ok(coordinate)
3571 } else {
3572 Err(ChartError::InvalidValue {
3573 element: "chart geometry coordinate".to_owned(),
3574 value: "x coordinate is nonfinite".to_owned(),
3575 })
3576 }
3577}
3578
3579fn map_y_oriented(
3580 value: f64,
3581 domain: Domain,
3582 bounds: Rect,
3583 orientation: Orientation,
3584) -> Result<f64> {
3585 let coordinate = bounds.y + bounds.height
3586 - normalized_value_oriented(value, domain, orientation)? * bounds.height;
3587 if coordinate.is_finite() {
3588 Ok(coordinate)
3589 } else {
3590 Err(ChartError::InvalidValue {
3591 element: "chart geometry coordinate".to_owned(),
3592 value: "y coordinate is nonfinite".to_owned(),
3593 })
3594 }
3595}
3596
3597fn oriented_category_index(index: usize, count: usize, orientation: Orientation) -> usize {
3598 if orientation == Orientation::MaxMin && index < count {
3599 count - index - 1
3600 } else {
3601 index
3602 }
3603}
3604
3605fn category_points(
3606 values: &[(usize, f64)],
3607 count: usize,
3608 domain: Domain,
3609 bounds: Rect,
3610 category_orientation: Orientation,
3611 value_orientation: Orientation,
3612) -> Result<Vec<Point>> {
3613 values
3614 .iter()
3615 .map(|(index, value)| {
3616 let index = oriented_category_index(*index, count, category_orientation);
3617 let x = bounds.x + (index as f64 + 0.5) / count as f64 * bounds.width;
3618 let y = map_y_oriented(*value, domain, bounds, value_orientation)?;
3619 if !x.is_finite() {
3620 return Err(ChartError::InvalidValue {
3621 element: "chart geometry coordinate".to_owned(),
3622 value: "category x coordinate is nonfinite".to_owned(),
3623 });
3624 }
3625 Ok(Point { x, y })
3626 })
3627 .collect()
3628}
3629
3630fn polyline_path(points: &[Point], close: bool) -> Path {
3631 let mut commands = Vec::with_capacity(points.len() + usize::from(close));
3632 if let Some(first) = points.first() {
3633 commands.push(PathCommand::MoveTo(*first));
3634 commands.extend(points.iter().skip(1).copied().map(PathCommand::LineTo));
3635 if close {
3636 commands.push(PathCommand::Close);
3637 }
3638 }
3639 Path {
3640 commands,
3641 fill_rule: FillRule::NonZero,
3642 }
3643}
3644
3645fn pie_wedge(center: Point, radius: f64, start: f64, end: f64) -> Path {
3646 let first = circle_point(center, radius, start);
3647 let mut commands = vec![PathCommand::MoveTo(center), PathCommand::LineTo(first)];
3648 append_arc(&mut commands, center, radius, start, end);
3649 commands.push(PathCommand::Close);
3650 Path {
3651 commands,
3652 fill_rule: FillRule::NonZero,
3653 }
3654}
3655
3656fn doughnut_wedge(
3657 center: Point,
3658 outer_radius: f64,
3659 inner_radius: f64,
3660 start: f64,
3661 end: f64,
3662) -> Path {
3663 let outer_start = circle_point(center, outer_radius, start);
3664 let inner_end = circle_point(center, inner_radius, end);
3665 let mut commands = vec![PathCommand::MoveTo(outer_start)];
3666 append_arc(&mut commands, center, outer_radius, start, end);
3667 commands.push(PathCommand::LineTo(inner_end));
3668 append_arc(&mut commands, center, inner_radius, end, start);
3669 commands.push(PathCommand::Close);
3670 Path {
3671 commands,
3672 fill_rule: FillRule::NonZero,
3673 }
3674}
3675
3676fn append_arc(commands: &mut Vec<PathCommand>, center: Point, radius: f64, start: f64, end: f64) {
3677 let segments = ((end - start).abs() / std::f64::consts::FRAC_PI_2)
3678 .ceil()
3679 .max(1.0) as usize;
3680 let sweep = (end - start) / segments as f64;
3681 for segment in 0..segments {
3682 let angle = start + segment as f64 * sweep;
3683 let next = angle + sweep;
3684 let factor = 4.0 / 3.0 * (sweep / 4.0).tan();
3685 let from = circle_point(center, radius, angle);
3686 let to = circle_point(center, radius, next);
3687 commands.push(PathCommand::CurveTo {
3688 c1: Point {
3689 x: from.x - factor * radius * angle.sin(),
3690 y: from.y + factor * radius * angle.cos(),
3691 },
3692 c2: Point {
3693 x: to.x + factor * radius * next.sin(),
3694 y: to.y - factor * radius * next.cos(),
3695 },
3696 to,
3697 });
3698 }
3699}
3700
3701fn circle_point(center: Point, radius: f64, angle: f64) -> Point {
3702 Point {
3703 x: center.x + radius * angle.cos(),
3704 y: center.y + radius * angle.sin(),
3705 }
3706}
3707
3708fn radial_point(center: Point, radius: f64, index: usize, count: usize) -> Point {
3709 circle_point(
3710 center,
3711 radius,
3712 index as f64 * std::f64::consts::TAU / count as f64 - std::f64::consts::FRAC_PI_2,
3713 )
3714}
3715
3716fn marker_path(center: Point) -> Path {
3717 const RADIUS: f64 = 2.5;
3718 const KAPPA: f64 = 0.552_284_749_830_793_6;
3719 let control = RADIUS * KAPPA;
3720 Path {
3721 commands: vec![
3722 PathCommand::MoveTo(Point {
3723 x: center.x + RADIUS,
3724 y: center.y,
3725 }),
3726 PathCommand::CurveTo {
3727 c1: Point {
3728 x: center.x + RADIUS,
3729 y: center.y + control,
3730 },
3731 c2: Point {
3732 x: center.x + control,
3733 y: center.y + RADIUS,
3734 },
3735 to: Point {
3736 x: center.x,
3737 y: center.y + RADIUS,
3738 },
3739 },
3740 PathCommand::CurveTo {
3741 c1: Point {
3742 x: center.x - control,
3743 y: center.y + RADIUS,
3744 },
3745 c2: Point {
3746 x: center.x - RADIUS,
3747 y: center.y + control,
3748 },
3749 to: Point {
3750 x: center.x - RADIUS,
3751 y: center.y,
3752 },
3753 },
3754 PathCommand::CurveTo {
3755 c1: Point {
3756 x: center.x - RADIUS,
3757 y: center.y - control,
3758 },
3759 c2: Point {
3760 x: center.x - control,
3761 y: center.y - RADIUS,
3762 },
3763 to: Point {
3764 x: center.x,
3765 y: center.y - RADIUS,
3766 },
3767 },
3768 PathCommand::CurveTo {
3769 c1: Point {
3770 x: center.x + control,
3771 y: center.y - RADIUS,
3772 },
3773 c2: Point {
3774 x: center.x + RADIUS,
3775 y: center.y - control,
3776 },
3777 to: Point {
3778 x: center.x + RADIUS,
3779 y: center.y,
3780 },
3781 },
3782 PathCommand::Close,
3783 ],
3784 fill_rule: FillRule::NonZero,
3785 }
3786}
3787
3788fn push_markers(elements: &mut Vec<PositionedElement>, points: &[Point], colour: Color) {
3789 for point in points {
3790 elements.push(filled_path(marker_path(*point), colour));
3791 }
3792}
3793
3794fn series_colour(series_colours: &[Color], index: usize) -> Result<Color> {
3795 series_colours
3796 .get(index)
3797 .copied()
3798 .ok_or_else(|| ChartError::InvalidValue {
3799 element: "chart series palette".to_owned(),
3800 value: format!("missing resolved colour for series {index}"),
3801 })
3802}
3803
3804fn filled_path(path: Path, color: Color) -> PositionedElement {
3805 PositionedElement::Path(PathElement {
3806 path,
3807 fill: Some(Paint::Solid(color)),
3808 stroke: None,
3809 })
3810}
3811
3812fn stroked_path(path: Path, color: Color) -> PositionedElement {
3813 PositionedElement::Path(PathElement {
3814 path,
3815 fill: None,
3816 stroke: Some(Stroke::new(Paint::Solid(color), 1.5)),
3817 })
3818}
3819
3820fn filled_and_stroked_path(path: Path, color: Color) -> PositionedElement {
3821 PositionedElement::Path(PathElement {
3822 path,
3823 fill: Some(Paint::Solid(Color {
3824 a: color.a * 0.55,
3825 ..color
3826 })),
3827 stroke: Some(Stroke::new(Paint::Solid(color), 1.0)),
3828 })
3829}
3830
3831type NamespaceBindings = Vec<(Vec<u8>, String)>;
3832type XmlAttributes = Vec<(String, String)>;
3833type RootAttributes = (XmlAttributes, XmlAttributes);
3834
3835#[derive(Clone, Debug, Default, Eq, PartialEq)]
3836struct ScalarMarkup {
3837 raw_attributes: XmlAttributes,
3838 raw_content: Vec<Vec<u8>>,
3839}
3840
3841#[derive(Clone, Debug, Default, Eq, PartialEq)]
3842struct TextMarkup {
3843 raw_attributes: XmlAttributes,
3844 raw_children: OrderedRawChildren,
3845}
3846
3847#[derive(Clone, Debug, Default, Eq, PartialEq)]
3848struct PointMarkup {
3849 raw_attributes: XmlAttributes,
3850 raw_children: OrderedRawChildren,
3851 value: TextMarkup,
3852}
3853
3854#[derive(Clone, Debug, Default, Eq, PartialEq)]
3855struct ReferenceMarkup {
3856 raw_attributes: XmlAttributes,
3857 raw_children: OrderedRawChildren,
3858 formula: TextMarkup,
3859 cache_attributes: XmlAttributes,
3860 cache_children: OrderedRawChildren,
3861 format_code: Option<TextMarkup>,
3862 point_count: ScalarMarkup,
3863 declared_point_count: Option<u32>,
3864 point_indexes: Vec<u32>,
3865 points: Vec<PointMarkup>,
3866}
3867
3868#[derive(Clone, Debug, Default, Eq, PartialEq)]
3869struct WrapperMarkup {
3870 raw_attributes: XmlAttributes,
3871 raw_children: OrderedRawChildren,
3872}
3873
3874#[derive(Clone, Debug, PartialEq)]
3876pub struct StringRef {
3877 pub formula: String,
3878 pub values: Vec<String>,
3879 markup: ReferenceMarkup,
3880}
3881
3882impl StringRef {
3883 pub fn new(formula: String, values: Vec<String>) -> Result<Self> {
3884 let reference = Self {
3885 formula,
3886 values,
3887 markup: ReferenceMarkup::default(),
3888 };
3889 reference.validate()?;
3890 Ok(reference)
3891 }
3892
3893 pub fn from_xml(xml: &[u8]) -> Result<Self> {
3894 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
3895 }
3896
3897 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
3898 let parsed = parse_reference(xml, b"strRef", b"strCache", false, inherited)?;
3899 let values = parsed.values;
3900 let reference = Self {
3901 formula: parsed.formula,
3902 values,
3903 markup: parsed.markup,
3904 };
3905 reference.validate()?;
3906 Ok(reference)
3907 }
3908
3909 pub fn to_xml(&self) -> Result<Vec<u8>> {
3910 self.validate()?;
3911 let mut writer = Writer::new(Vec::new());
3912 self.write_xml(&mut writer, true)?;
3913 Ok(writer.into_inner())
3914 }
3915
3916 fn validate(&self) -> Result<()> {
3917 validate_formula(&self.formula, "c:strRef/c:f")?;
3918 for value in &self.values {
3919 validate_xml_text(value, "c:strCache/c:pt/c:v")?;
3920 }
3921 validate_point_count(self.values.len(), "c:strCache")
3922 }
3923
3924 fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
3925 self.validate()?;
3926 let mut start = BytesStart::new("c:strRef");
3927 if standalone {
3928 start.push_attribute(("xmlns:c", C_NS));
3929 }
3930 push_attributes(&mut start, &self.markup.raw_attributes);
3931 writer
3932 .write_event(Event::Start(start))
3933 .map_err(OxmlError::from)?;
3934 emit_raw(writer, self.markup.raw_children.at(0))?;
3935 write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
3936 emit_raw(writer, self.markup.raw_children.at(1))?;
3937 write_string_cache(writer, &self.values, &self.markup)?;
3938 emit_raw(writer, self.markup.raw_children.at(2))?;
3939 writer
3940 .write_event(Event::End(BytesEnd::new("c:strRef")))
3941 .map_err(OxmlError::from)?;
3942 Ok(())
3943 }
3944}
3945
3946#[derive(Clone, Debug, PartialEq)]
3948pub struct NumericData {
3949 pub formula: String,
3950 pub format_code: String,
3951 pub values: Vec<f64>,
3952 markup: ReferenceMarkup,
3953}
3954
3955impl NumericData {
3956 pub fn new(formula: String, format_code: String, values: Vec<f64>) -> Result<Self> {
3957 validate_formula(&formula, "c:numRef/c:f")?;
3958 validate_format_code(&format_code)?;
3959 validate_numeric_values(&values)?;
3960 validate_point_count(values.len(), "c:numCache")?;
3961 Ok(Self {
3962 formula,
3963 format_code,
3964 values,
3965 markup: ReferenceMarkup::default(),
3966 })
3967 }
3968
3969 pub fn from_xml(xml: &[u8]) -> Result<Self> {
3970 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
3971 }
3972
3973 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
3974 let parsed = parse_reference(xml, b"numRef", b"numCache", true, inherited)?;
3975 let format_code = parsed
3976 .format_code
3977 .ok_or_else(|| ChartError::MissingElement("c:formatCode".to_owned()))?;
3978 let mut values = Vec::with_capacity(parsed.values.len());
3979 for value in parsed.values {
3980 let number = value.parse::<f64>().map_err(|_| ChartError::InvalidValue {
3981 element: "c:v".to_owned(),
3982 value: value.clone(),
3983 })?;
3984 if !number.is_finite() {
3985 return Err(ChartError::InvalidValue {
3986 element: "c:v".to_owned(),
3987 value,
3988 });
3989 }
3990 values.push(number);
3991 }
3992 let data = Self {
3993 formula: parsed.formula,
3994 format_code,
3995 values,
3996 markup: parsed.markup,
3997 };
3998 data.validate()?;
3999 Ok(data)
4000 }
4001
4002 pub fn to_xml(&self) -> Result<Vec<u8>> {
4003 self.validate()?;
4004 let mut writer = Writer::new(Vec::new());
4005 self.write_xml(&mut writer, true)?;
4006 Ok(writer.into_inner())
4007 }
4008
4009 fn validate(&self) -> Result<()> {
4010 validate_formula(&self.formula, "c:numRef/c:f")?;
4011 validate_format_code(&self.format_code)?;
4012 validate_numeric_values(&self.values)?;
4013 validate_point_count(self.values.len(), "c:numCache")
4014 }
4015
4016 fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
4017 self.validate()?;
4018 let mut start = BytesStart::new("c:numRef");
4019 if standalone {
4020 start.push_attribute(("xmlns:c", C_NS));
4021 }
4022 push_attributes(&mut start, &self.markup.raw_attributes);
4023 writer
4024 .write_event(Event::Start(start))
4025 .map_err(OxmlError::from)?;
4026 emit_raw(writer, self.markup.raw_children.at(0))?;
4027 write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
4028 emit_raw(writer, self.markup.raw_children.at(1))?;
4029 write_numeric_cache(writer, self, &self.markup)?;
4030 emit_raw(writer, self.markup.raw_children.at(2))?;
4031 writer
4032 .write_event(Event::End(BytesEnd::new("c:numRef")))
4033 .map_err(OxmlError::from)?;
4034 Ok(())
4035 }
4036}
4037
4038#[derive(Clone, Debug, PartialEq)]
4039pub enum AxisData {
4040 String(StringRef),
4041 Numeric(NumericData),
4042}
4043
4044#[derive(Clone, Debug, PartialEq)]
4046pub struct Series {
4047 pub index: u32,
4048 pub order: u32,
4049 pub name: Option<StringRef>,
4050 pub categories: Option<AxisData>,
4051 pub values: NumericData,
4052 pub bubble_size: Option<NumericData>,
4053 pub sp_pr: Option<CT_ShapeProperties>,
4054 pub data_labels: Option<CT_DLbls>,
4055 index_markup: ScalarMarkup,
4056 order_markup: ScalarMarkup,
4057 name_markup: Option<WrapperMarkup>,
4058 categories_markup: Option<WrapperMarkup>,
4059 values_markup: WrapperMarkup,
4060 bubble_size_markup: Option<WrapperMarkup>,
4061 opaque_name: bool,
4062 opaque_categories: bool,
4063 opaque_bubble_size: bool,
4064 uses_scatter_wrappers: bool,
4065 parsed_from_xml: bool,
4066 raw_attributes: XmlAttributes,
4067 namespace_declarations: XmlAttributes,
4068 raw_children: OrderedRawChildren,
4069}
4070
4071impl Series {
4072 pub fn new(index: u32, order: u32, values: NumericData) -> Self {
4073 Self {
4074 index,
4075 order,
4076 name: None,
4077 categories: None,
4078 values,
4079 bubble_size: None,
4080 sp_pr: None,
4081 data_labels: None,
4082 index_markup: ScalarMarkup::default(),
4083 order_markup: ScalarMarkup::default(),
4084 name_markup: None,
4085 categories_markup: None,
4086 values_markup: WrapperMarkup::default(),
4087 bubble_size_markup: None,
4088 opaque_name: false,
4089 opaque_categories: false,
4090 opaque_bubble_size: false,
4091 uses_scatter_wrappers: false,
4092 parsed_from_xml: false,
4093 raw_attributes: Vec::new(),
4094 namespace_declarations: Vec::new(),
4095 raw_children: OrderedRawChildren::default(),
4096 }
4097 }
4098
4099 pub fn from_xml(xml: &[u8]) -> Result<Self> {
4100 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
4101 }
4102
4103 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
4104 let mut reader = Reader::from_reader(xml);
4105 let mut buffer = Vec::new();
4106 loop {
4107 match reader
4108 .read_event_into(&mut buffer)
4109 .map_err(OxmlError::from)?
4110 {
4111 Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4112 chart_root_prefix(&element)?;
4113 if !element_is_in_namespace(&element, C_NS, inherited)? {
4114 return Err(ChartError::UnexpectedElement(element_name(&element)));
4115 }
4116 return Self::from_element(&mut reader, &element, inherited);
4117 }
4118 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4119 return Err(ChartError::MissingElement("c:idx".to_owned()));
4120 }
4121 Event::Start(element) | Event::Empty(element) => {
4122 return Err(ChartError::UnexpectedElement(element_name(&element)));
4123 }
4124 Event::Eof => return Err(ChartError::MissingElement("c:ser".to_owned())),
4125 _ => {}
4126 }
4127 buffer.clear();
4128 }
4129 }
4130
4131 fn from_element(
4132 reader: &mut Reader<&[u8]>,
4133 start: &BytesStart<'_>,
4134 inherited: &NamespaceBindings,
4135 ) -> Result<Self> {
4136 reject_conflicting_prefix(start, b"a", A_NS)?;
4137 let namespaces = chart_bindings(inherited, start)?;
4138 require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
4139 require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
4140 require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
4141 let (raw_attributes, _) =
4142 capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
4143 let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
4144 let mut state = SeriesParseState::default();
4145 let mut buffer = Vec::new();
4146 loop {
4147 match reader
4148 .read_event_into(&mut buffer)
4149 .map_err(OxmlError::from)?
4150 {
4151 Event::Start(element) => {
4152 let name = chart_child_local(&element, &namespaces)?;
4153 let raw = capture_element(reader, &element)?;
4154 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
4155 }
4156 Event::Empty(element) => {
4157 let name = chart_child_local(&element, &namespaces)?;
4158 let raw = capture_empty_element(&element)?;
4159 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
4160 }
4161 event @ (Event::Text(_)
4162 | Event::CData(_)
4163 | Event::Comment(_)
4164 | Event::PI(_)
4165 | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
4166 Event::End(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4167 break;
4168 }
4169 Event::Eof => return Err(missing_end("c:ser")),
4170 _ => {}
4171 }
4172 buffer.clear();
4173 }
4174 state.finish(raw_attributes, namespace_declarations)
4175 }
4176
4177 pub fn to_xml(&self) -> Result<Vec<u8>> {
4178 self.to_xml_for_plot(self.uses_scatter_wrappers)
4179 }
4180
4181 fn to_xml_for_plot(&self, scatter: bool) -> Result<Vec<u8>> {
4182 if self.opaque_name && self.name.is_some() {
4183 return Err(ChartError::DuplicateElement("c:tx".to_owned()));
4184 }
4185 if self.opaque_categories && self.categories.is_some() {
4186 return Err(ChartError::DuplicateElement("c:cat".to_owned()));
4187 }
4188 if self.opaque_bubble_size && self.bubble_size.is_some() {
4189 return Err(ChartError::DuplicateElement("c:bubbleSize".to_owned()));
4190 }
4191 self.values.validate()?;
4192 if let Some(name) = &self.name {
4193 name.validate()?;
4194 }
4195 if let Some(categories) = &self.categories {
4196 if scatter && !matches!(categories, AxisData::Numeric(_)) {
4197 return Err(ChartError::InvalidValue {
4198 element: "c:xVal".to_owned(),
4199 value: "scatter x values must use a numeric cache".to_owned(),
4200 });
4201 }
4202 match categories {
4203 AxisData::String(reference) => reference.validate()?,
4204 AxisData::Numeric(reference) => reference.validate()?,
4205 }
4206 }
4207 if scatter && self.categories.is_none() {
4208 return Err(ChartError::MissingElement("c:xVal".to_owned()));
4209 }
4210 if let Some(size) = &self.bubble_size {
4211 size.validate()?;
4212 }
4213 if let Some(labels) = &self.data_labels {
4214 labels.validate()?;
4215 }
4216 let mut writer = Writer::new(Vec::new());
4217 let mut start = BytesStart::new("c:ser");
4218 start.push_attribute(("xmlns:c", C_NS));
4219 start.push_attribute(("xmlns:a", A_NS));
4220 start.push_attribute(("xmlns:r", R_NS));
4221 push_attributes(&mut start, &self.namespace_declarations);
4222 push_attributes(&mut start, &self.raw_attributes);
4223 writer
4224 .write_event(Event::Start(start))
4225 .map_err(OxmlError::from)?;
4226 emit_raw(&mut writer, self.raw_children.at(0))?;
4227 write_scalar(
4228 &mut writer,
4229 "c:idx",
4230 &self.index.to_string(),
4231 Some(&self.index_markup),
4232 )?;
4233 emit_raw(&mut writer, self.raw_children.at(1))?;
4234 write_scalar(
4235 &mut writer,
4236 "c:order",
4237 &self.order.to_string(),
4238 Some(&self.order_markup),
4239 )?;
4240 emit_raw(&mut writer, self.raw_children.at(2))?;
4241 if let Some(name) = &self.name {
4242 write_wrapper_start(
4243 &mut writer,
4244 "c:tx",
4245 self.name_markup
4246 .as_ref()
4247 .unwrap_or(&WrapperMarkup::default()),
4248 )?;
4249 name.write_xml(&mut writer, false)?;
4250 write_wrapper_end(
4251 &mut writer,
4252 "c:tx",
4253 self.name_markup
4254 .as_ref()
4255 .unwrap_or(&WrapperMarkup::default()),
4256 )?;
4257 }
4258 emit_raw(&mut writer, self.raw_children.at(3))?;
4259 if let Some(properties) = &self.sp_pr {
4260 properties.write_xml_as(&mut writer, "c:spPr")?;
4261 }
4262 emit_raw(&mut writer, self.raw_children.at(4))?;
4263 if let Some(labels) = &self.data_labels {
4264 labels.write_xml(&mut writer, false)?;
4265 }
4266 emit_raw(&mut writer, self.raw_children.at(5))?;
4267 if let Some(categories) = &self.categories {
4268 let default_markup = WrapperMarkup::default();
4269 let markup = self.categories_markup.as_ref().unwrap_or(&default_markup);
4270 write_wrapper_start(
4271 &mut writer,
4272 if scatter { "c:xVal" } else { "c:cat" },
4273 markup,
4274 )?;
4275 match categories {
4276 AxisData::String(reference) => reference.write_xml(&mut writer, false)?,
4277 AxisData::Numeric(reference) => reference.write_xml(&mut writer, false)?,
4278 }
4279 write_wrapper_end(
4280 &mut writer,
4281 if scatter { "c:xVal" } else { "c:cat" },
4282 markup,
4283 )?;
4284 }
4285 emit_raw(&mut writer, self.raw_children.at(6))?;
4286 write_wrapper_start(
4287 &mut writer,
4288 if scatter { "c:yVal" } else { "c:val" },
4289 &self.values_markup,
4290 )?;
4291 self.values.write_xml(&mut writer, false)?;
4292 write_wrapper_end(
4293 &mut writer,
4294 if scatter { "c:yVal" } else { "c:val" },
4295 &self.values_markup,
4296 )?;
4297 emit_raw(&mut writer, self.raw_children.at(7))?;
4298 if let Some(size) = &self.bubble_size {
4299 let default_markup = WrapperMarkup::default();
4300 let markup = self.bubble_size_markup.as_ref().unwrap_or(&default_markup);
4301 write_wrapper_start(&mut writer, "c:bubbleSize", markup)?;
4302 size.write_xml(&mut writer, false)?;
4303 write_wrapper_end(&mut writer, "c:bubbleSize", markup)?;
4304 }
4305 emit_raw(&mut writer, self.raw_children.at(8))?;
4306 writer
4307 .write_event(Event::End(BytesEnd::new("c:ser")))
4308 .map_err(OxmlError::from)?;
4309 Ok(writer.into_inner())
4310 }
4311
4312 pub fn raw_children(&self) -> &OrderedRawChildren {
4313 &self.raw_children
4314 }
4315}
4316
4317#[derive(Default)]
4318struct SeriesParseState {
4319 index: Option<(u32, ScalarMarkup)>,
4320 order: Option<(u32, ScalarMarkup)>,
4321 name: Option<(StringRef, WrapperMarkup)>,
4322 categories: Option<(AxisData, WrapperMarkup)>,
4323 values: Option<(NumericData, WrapperMarkup)>,
4324 bubble_size: Option<(NumericData, WrapperMarkup)>,
4325 data_labels: Option<CT_DLbls>,
4326 name_seen: bool,
4327 categories_seen: bool,
4328 bubble_size_seen: bool,
4329 opaque_name: bool,
4330 opaque_categories: bool,
4331 opaque_bubble_size: bool,
4332 saw_standard_wrapper: bool,
4333 saw_scatter_wrapper: bool,
4334 sp_pr: Option<CT_ShapeProperties>,
4335 raw_children: OrderedRawChildren,
4336 boundary: usize,
4337}
4338
4339impl SeriesParseState {
4340 fn capture_event(&mut self, raw: Vec<u8>) {
4341 self.raw_children.push(self.boundary, raw);
4342 }
4343
4344 fn parse_child(
4345 &mut self,
4346 name: &[u8],
4347 raw: Vec<u8>,
4348 namespaces: &NamespaceBindings,
4349 ) -> Result<()> {
4350 match name {
4351 b"idx" => {
4352 set_once(&mut self.index, parse_u32_scalar(&raw, "idx")?, "c:idx")?;
4353 self.boundary = self.boundary.max(1);
4354 }
4355 b"order" => {
4356 set_once(&mut self.order, parse_u32_scalar(&raw, "order")?, "c:order")?;
4357 self.boundary = self.boundary.max(2);
4358 }
4359 b"tx" => {
4360 mark_once(&mut self.name_seen, "c:tx")?;
4361 let parsed = parse_wrapper(&raw, b"tx", &[b"strRef"], namespaces)?;
4362 if let Some((_, reference)) = parsed.choice {
4363 self.name = Some((
4364 StringRef::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4365 parsed.markup,
4366 ));
4367 } else {
4368 self.opaque_name = true;
4369 self.raw_children.push(2, raw);
4370 }
4371 self.boundary = self.boundary.max(3);
4372 }
4373 b"spPr" => {
4374 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
4375 let properties = CT_ShapeProperties::from_xml(&raw)?;
4376 let mut writer = Writer::new(Vec::new());
4377 properties.write_xml_as(&mut writer, "c:spPr")?;
4378 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
4379 set_once(&mut self.sp_pr, properties, "c:spPr")?;
4380 self.boundary = self.boundary.max(4);
4381 }
4382 b"dLbls" => {
4383 set_once(
4384 &mut self.data_labels,
4385 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
4386 "c:dLbls",
4387 )?;
4388 self.boundary = self.boundary.max(5);
4389 }
4390 b"cat" => {
4391 self.saw_standard_wrapper = true;
4392 mark_once(&mut self.categories_seen, "c:cat")?;
4393 let parsed = parse_wrapper(&raw, b"cat", &[b"strRef", b"numRef"], namespaces)?;
4394 if let Some((choice, reference)) = parsed.choice {
4395 let categories = if choice == b"strRef" {
4396 AxisData::String(StringRef::from_xml_with_namespaces(
4397 &reference,
4398 &parsed.namespaces,
4399 )?)
4400 } else {
4401 AxisData::Numeric(NumericData::from_xml_with_namespaces(
4402 &reference,
4403 &parsed.namespaces,
4404 )?)
4405 };
4406 self.categories = Some((categories, parsed.markup));
4407 } else {
4408 self.opaque_categories = true;
4409 self.raw_children.push(5, raw);
4410 }
4411 self.boundary = self.boundary.max(6);
4412 }
4413 b"xVal" => {
4414 self.saw_scatter_wrapper = true;
4415 mark_once(&mut self.categories_seen, "c:xVal")?;
4416 let parsed = parse_wrapper(&raw, b"xVal", &[b"numRef"], namespaces)?;
4417 let (_, reference) = parsed
4418 .choice
4419 .ok_or_else(|| ChartError::MissingElement("c:xVal/c:numRef".to_owned()))?;
4420 self.categories = Some((
4421 AxisData::Numeric(NumericData::from_xml_with_namespaces(
4422 &reference,
4423 &parsed.namespaces,
4424 )?),
4425 parsed.markup,
4426 ));
4427 self.boundary = self.boundary.max(6);
4428 }
4429 b"val" | b"yVal" => {
4430 if name == b"yVal" {
4431 self.saw_scatter_wrapper = true;
4432 } else {
4433 self.saw_standard_wrapper = true;
4434 }
4435 let wrapper = if name == b"yVal" {
4436 b"yVal".as_slice()
4437 } else {
4438 b"val".as_slice()
4439 };
4440 let parsed = parse_wrapper(&raw, wrapper, &[b"numRef"], namespaces)?;
4441 let (_, reference) = parsed.choice.ok_or_else(|| {
4442 ChartError::MissingElement(format!(
4443 "c:{}/c:numRef",
4444 String::from_utf8_lossy(wrapper)
4445 ))
4446 })?;
4447 set_once(
4448 &mut self.values,
4449 (
4450 NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4451 parsed.markup,
4452 ),
4453 "c:val or c:yVal",
4454 )?;
4455 self.boundary = self.boundary.max(7);
4456 }
4457 b"bubbleSize" => {
4458 mark_once(&mut self.bubble_size_seen, "c:bubbleSize")?;
4459 let parsed = parse_wrapper(&raw, b"bubbleSize", &[b"numRef"], namespaces)?;
4460 if let Some((_, reference)) = parsed.choice {
4461 self.bubble_size = Some((
4462 NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4463 parsed.markup,
4464 ));
4465 } else {
4466 self.opaque_bubble_size = true;
4467 self.raw_children.push(7, raw);
4468 }
4469 self.boundary = self.boundary.max(8);
4470 }
4471 _ => {
4472 let boundary = series_raw_boundary(name, self.boundary);
4473 self.raw_children.push(boundary, raw);
4474 self.boundary = self.boundary.max(boundary);
4475 }
4476 }
4477 Ok(())
4478 }
4479
4480 fn finish(
4481 self,
4482 raw_attributes: XmlAttributes,
4483 namespace_declarations: XmlAttributes,
4484 ) -> Result<Series> {
4485 if self.saw_standard_wrapper && self.saw_scatter_wrapper {
4486 return Err(ChartError::InvalidValue {
4487 element: "c:ser".to_owned(),
4488 value: "category/value and x/y wrappers cannot be combined".to_owned(),
4489 });
4490 }
4491 let (index, index_markup) = self
4492 .index
4493 .ok_or_else(|| ChartError::MissingElement("c:idx".to_owned()))?;
4494 let (order, order_markup) = self
4495 .order
4496 .ok_or_else(|| ChartError::MissingElement("c:order".to_owned()))?;
4497 let (values, values_markup) = self
4498 .values
4499 .ok_or_else(|| ChartError::MissingElement("c:val".to_owned()))?;
4500 let (name, name_markup) = self
4501 .name
4502 .map(|(value, markup)| (Some(value), Some(markup)))
4503 .unwrap_or((None, None));
4504 let (categories, categories_markup) = self
4505 .categories
4506 .map(|(value, markup)| (Some(value), Some(markup)))
4507 .unwrap_or((None, None));
4508 let (bubble_size, bubble_size_markup) = self
4509 .bubble_size
4510 .map(|(value, markup)| (Some(value), Some(markup)))
4511 .unwrap_or((None, None));
4512 Ok(Series {
4513 index,
4514 order,
4515 name,
4516 categories,
4517 values,
4518 bubble_size,
4519 sp_pr: self.sp_pr,
4520 data_labels: self.data_labels,
4521 index_markup,
4522 order_markup,
4523 name_markup,
4524 categories_markup,
4525 values_markup,
4526 bubble_size_markup,
4527 opaque_name: self.opaque_name,
4528 opaque_categories: self.opaque_categories,
4529 opaque_bubble_size: self.opaque_bubble_size,
4530 uses_scatter_wrappers: self.saw_scatter_wrapper,
4531 parsed_from_xml: true,
4532 raw_attributes,
4533 namespace_declarations,
4534 raw_children: raw_children_in_schema_order(&self.raw_children, 8),
4535 })
4536 }
4537}
4538
4539fn series_raw_boundary(name: &[u8], current: usize) -> usize {
4540 match name {
4541 b"marker" | b"invertIfNegative" | b"pictureOptions" | b"explosion" | b"dPt" => 4,
4542 b"trendline" | b"errBars" => 5,
4543 b"shape" | b"smooth" => 7,
4544 b"extLst" => 8,
4545 _ => current,
4546 }
4547}
4548
4549struct ParsedReference {
4550 formula: String,
4551 format_code: Option<String>,
4552 values: Vec<String>,
4553 markup: ReferenceMarkup,
4554}
4555
4556fn parse_reference(
4557 xml: &[u8],
4558 reference_local: &[u8],
4559 cache_local: &[u8],
4560 numeric: bool,
4561 inherited: &NamespaceBindings,
4562) -> Result<ParsedReference> {
4563 let mut reader = Reader::from_reader(xml);
4564 let mut buffer = Vec::new();
4565 loop {
4566 match reader
4567 .read_event_into(&mut buffer)
4568 .map_err(OxmlError::from)?
4569 {
4570 Event::Start(element)
4571 if matches_local_name(element.name().as_ref(), reference_local) =>
4572 {
4573 let namespaces = typed_rewrite_bindings(&element, inherited)?;
4574 let raw_attributes = capture_fixed_attributes(&element, &["xmlns:c"])?;
4575 let mut formula: Option<(String, TextMarkup)> = None;
4576 let mut cache: Option<(Option<String>, Vec<String>, CacheMarkup)> = None;
4577 let mut raw_children = OrderedRawChildren::default();
4578 let mut boundary = 0usize;
4579 let mut inner = Vec::new();
4580 loop {
4581 match reader
4582 .read_event_into(&mut inner)
4583 .map_err(OxmlError::from)?
4584 {
4585 Event::Start(child) => {
4586 let name = chart_child_local(&child, &namespaces)?;
4587 let raw = capture_element(&mut reader, &child)?;
4588 match name.as_deref().unwrap_or_default() {
4589 b"f" => {
4590 set_once(
4591 &mut formula,
4592 parse_text_element(&raw, b"f", &namespaces)?,
4593 "c:f",
4594 )?;
4595 boundary = boundary.max(1);
4596 }
4597 name if name == cache_local => {
4598 set_once(
4599 &mut cache,
4600 parse_cache(&raw, cache_local, numeric, &namespaces)?,
4601 &format!("c:{}", String::from_utf8_lossy(cache_local)),
4602 )?;
4603 boundary = boundary.max(2);
4604 }
4605 _ => raw_children.push(boundary, raw),
4606 }
4607 }
4608 Event::Empty(child) => {
4609 let name = chart_child_local(&child, &namespaces)?;
4610 let raw = capture_empty_element(&child)?;
4611 match name.as_deref().unwrap_or_default() {
4612 b"f" => {
4613 set_once(
4614 &mut formula,
4615 parse_text_element(&raw, b"f", &namespaces)?,
4616 "c:f",
4617 )?;
4618 boundary = boundary.max(1);
4619 }
4620 name if name == cache_local => {
4621 set_once(
4622 &mut cache,
4623 parse_cache(&raw, cache_local, numeric, &namespaces)?,
4624 &format!("c:{}", String::from_utf8_lossy(cache_local)),
4625 )?;
4626 boundary = boundary.max(2);
4627 }
4628 _ => raw_children.push(boundary, raw),
4629 }
4630 }
4631 event @ (Event::Text(_)
4632 | Event::CData(_)
4633 | Event::Comment(_)
4634 | Event::PI(_)
4635 | Event::GeneralRef(_)) => {
4636 raw_children.push(boundary, capture_event(event)?);
4637 }
4638 Event::End(end)
4639 if matches_local_name(end.name().as_ref(), reference_local) =>
4640 {
4641 let (formula, formula_markup) = formula
4642 .ok_or_else(|| ChartError::MissingElement("c:f".to_owned()))?;
4643 validate_formula(&formula, "c:f")?;
4644 let (format_code, values, cache_markup) = cache.ok_or_else(|| {
4645 ChartError::MissingElement(format!(
4646 "c:{}",
4647 String::from_utf8_lossy(cache_local)
4648 ))
4649 })?;
4650 let markup = ReferenceMarkup {
4651 raw_attributes,
4652 raw_children: raw_children_in_schema_order(&raw_children, 2),
4653 formula: formula_markup,
4654 cache_attributes: cache_markup.raw_attributes,
4655 cache_children: cache_markup.raw_children,
4656 format_code: cache_markup.format_code,
4657 point_count: cache_markup.point_count,
4658 declared_point_count: cache_markup.declared_point_count,
4659 point_indexes: cache_markup.point_indexes,
4660 points: cache_markup.points,
4661 };
4662 return Ok(ParsedReference {
4663 formula,
4664 format_code,
4665 values,
4666 markup,
4667 });
4668 }
4669 Event::Eof => {
4670 return Err(missing_end(&format!(
4671 "c:{}",
4672 String::from_utf8_lossy(reference_local)
4673 )));
4674 }
4675 _ => {}
4676 }
4677 inner.clear();
4678 }
4679 }
4680 Event::Empty(element)
4681 if matches_local_name(element.name().as_ref(), reference_local) =>
4682 {
4683 return Err(ChartError::MissingElement("c:f".to_owned()));
4684 }
4685 Event::Start(element) | Event::Empty(element) => {
4686 return Err(ChartError::UnexpectedElement(element_name(&element)));
4687 }
4688 Event::Eof => {
4689 return Err(ChartError::MissingElement(format!(
4690 "c:{}",
4691 String::from_utf8_lossy(reference_local)
4692 )));
4693 }
4694 _ => {}
4695 }
4696 buffer.clear();
4697 }
4698}
4699
4700struct CacheMarkup {
4701 raw_attributes: XmlAttributes,
4702 raw_children: OrderedRawChildren,
4703 format_code: Option<TextMarkup>,
4704 point_count: ScalarMarkup,
4705 declared_point_count: Option<u32>,
4706 point_indexes: Vec<u32>,
4707 points: Vec<PointMarkup>,
4708}
4709
4710fn parse_cache(
4711 xml: &[u8],
4712 cache_local: &[u8],
4713 numeric: bool,
4714 inherited: &NamespaceBindings,
4715) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
4716 let mut reader = Reader::from_reader(xml);
4717 let mut buffer = Vec::new();
4718 loop {
4719 match reader
4720 .read_event_into(&mut buffer)
4721 .map_err(OxmlError::from)?
4722 {
4723 Event::Start(element) if matches_local_name(element.name().as_ref(), cache_local) => {
4724 let namespaces = typed_rewrite_bindings(&element, inherited)?;
4725 let raw_attributes = capture_attributes(&element)?;
4726 let mut state = CacheParseState::new(numeric, namespaces);
4727 let mut inner = Vec::new();
4728 loop {
4729 match reader
4730 .read_event_into(&mut inner)
4731 .map_err(OxmlError::from)?
4732 {
4733 Event::Start(child) => {
4734 let name = chart_child_local(&child, &state.namespaces)?;
4735 let raw = capture_element(&mut reader, &child)?;
4736 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
4737 }
4738 Event::Empty(child) => {
4739 let name = chart_child_local(&child, &state.namespaces)?;
4740 let raw = capture_empty_element(&child)?;
4741 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
4742 }
4743 event @ (Event::Text(_)
4744 | Event::CData(_)
4745 | Event::Comment(_)
4746 | Event::PI(_)
4747 | Event::GeneralRef(_)) => {
4748 state.capture_event(capture_event(event)?);
4749 }
4750 Event::End(end) if matches_local_name(end.name().as_ref(), cache_local) => {
4751 return state.finish(raw_attributes);
4752 }
4753 Event::Eof => {
4754 return Err(missing_end(&format!(
4755 "c:{}",
4756 String::from_utf8_lossy(cache_local)
4757 )));
4758 }
4759 _ => {}
4760 }
4761 inner.clear();
4762 }
4763 }
4764 Event::Empty(element) if matches_local_name(element.name().as_ref(), cache_local) => {
4765 return Err(ChartError::MissingElement("c:ptCount".to_owned()));
4766 }
4767 Event::Start(element) | Event::Empty(element) => {
4768 return Err(ChartError::UnexpectedElement(element_name(&element)));
4769 }
4770 Event::Eof => {
4771 return Err(ChartError::MissingElement(format!(
4772 "c:{}",
4773 String::from_utf8_lossy(cache_local)
4774 )));
4775 }
4776 _ => {}
4777 }
4778 buffer.clear();
4779 }
4780}
4781
4782struct CacheParseState {
4783 numeric: bool,
4784 namespaces: NamespaceBindings,
4785 point_base: usize,
4786 boundary: usize,
4787 format_code: Option<(String, TextMarkup)>,
4788 point_count: Option<(u32, ScalarMarkup)>,
4789 points: Vec<(u32, String, PointMarkup)>,
4790 raw_children: OrderedRawChildren,
4791}
4792
4793impl CacheParseState {
4794 fn new(numeric: bool, namespaces: NamespaceBindings) -> Self {
4795 Self {
4796 numeric,
4797 namespaces,
4798 point_base: usize::from(numeric) + 1,
4799 boundary: 0,
4800 format_code: None,
4801 point_count: None,
4802 points: Vec::new(),
4803 raw_children: OrderedRawChildren::default(),
4804 }
4805 }
4806
4807 fn capture_event(&mut self, raw: Vec<u8>) {
4808 self.raw_children.push(self.boundary, raw);
4809 }
4810
4811 fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
4812 match name {
4813 b"formatCode" if self.numeric => {
4814 set_once(
4815 &mut self.format_code,
4816 parse_text_element(&raw, b"formatCode", &self.namespaces)?,
4817 "c:formatCode",
4818 )?;
4819 self.boundary = self.boundary.max(1);
4820 }
4821 b"ptCount" => {
4822 set_once(
4823 &mut self.point_count,
4824 parse_u32_scalar(&raw, "ptCount")?,
4825 "c:ptCount",
4826 )?;
4827 self.boundary = self.boundary.max(self.point_base);
4828 }
4829 b"pt" => {
4830 self.points.push(parse_point(&raw, &self.namespaces)?);
4831 self.boundary = self.boundary.max(self.point_base + self.points.len());
4832 }
4833 _ => self.raw_children.push(self.boundary, raw),
4834 }
4835 Ok(())
4836 }
4837
4838 fn finish(
4839 self,
4840 raw_attributes: XmlAttributes,
4841 ) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
4842 let (declared, point_count_markup) = self
4843 .point_count
4844 .ok_or_else(|| ChartError::MissingElement("c:ptCount".to_owned()))?;
4845 let actual = u32::try_from(self.points.len()).map_err(|_| ChartError::InvalidValue {
4846 element: "c:ptCount".to_owned(),
4847 value: self.points.len().to_string(),
4848 })?;
4849 if declared < actual {
4850 return Err(invalid_attribute("ptCount", "val", declared.to_string()));
4851 }
4852 let mut values = Vec::with_capacity(self.points.len());
4853 let mut point_indexes = Vec::with_capacity(self.points.len());
4854 let mut point_markup = Vec::with_capacity(self.points.len());
4855 let mut previous = None;
4856 for (index, value, markup) in self.points {
4857 if index >= declared || previous.is_some_and(|last| index <= last) {
4858 return Err(invalid_attribute("pt", "idx", index.to_string()));
4859 }
4860 previous = Some(index);
4861 values.push(value);
4862 point_indexes.push(index);
4863 point_markup.push(markup);
4864 }
4865 let (format_code, format_markup) = self
4866 .format_code
4867 .map(|(value, markup)| (Some(value), Some(markup)))
4868 .unwrap_or((None, None));
4869 if self.numeric && format_code.is_none() {
4870 return Err(ChartError::MissingElement("c:formatCode".to_owned()));
4871 }
4872 Ok((
4873 format_code,
4874 values,
4875 CacheMarkup {
4876 raw_attributes,
4877 raw_children: raw_children_in_schema_order(
4878 &self.raw_children,
4879 self.point_base + point_markup.len(),
4880 ),
4881 format_code: format_markup,
4882 point_count: point_count_markup,
4883 declared_point_count: Some(declared),
4884 point_indexes,
4885 points: point_markup,
4886 },
4887 ))
4888 }
4889}
4890
4891fn parse_point(xml: &[u8], inherited: &NamespaceBindings) -> Result<(u32, String, PointMarkup)> {
4892 let mut reader = Reader::from_reader(xml);
4893 let mut buffer = Vec::new();
4894 loop {
4895 match reader
4896 .read_event_into(&mut buffer)
4897 .map_err(OxmlError::from)?
4898 {
4899 Event::Start(element) if matches_local_name(element.name().as_ref(), b"pt") => {
4900 let namespaces = typed_rewrite_bindings(&element, inherited)?;
4901 let index = required_u32_attribute(&element, "pt", b"idx")?;
4902 let raw_attributes = capture_attributes_except(&element, b"idx")?;
4903 let mut value: Option<(String, TextMarkup)> = None;
4904 let mut raw_children = OrderedRawChildren::default();
4905 let mut boundary = 0usize;
4906 let mut inner = Vec::new();
4907 loop {
4908 match reader
4909 .read_event_into(&mut inner)
4910 .map_err(OxmlError::from)?
4911 {
4912 Event::Start(child) => {
4913 let name = chart_child_local(&child, &namespaces)?;
4914 let raw = capture_element(&mut reader, &child)?;
4915 if name.as_deref() == Some(b"v") {
4916 set_once(
4917 &mut value,
4918 parse_text_element(&raw, b"v", &namespaces)?,
4919 "c:v",
4920 )?;
4921 boundary = 1;
4922 } else {
4923 raw_children.push(boundary, raw);
4924 }
4925 }
4926 Event::Empty(child) => {
4927 let name = chart_child_local(&child, &namespaces)?;
4928 let raw = capture_empty_element(&child)?;
4929 if name.as_deref() == Some(b"v") {
4930 set_once(
4931 &mut value,
4932 parse_text_element(&raw, b"v", &namespaces)?,
4933 "c:v",
4934 )?;
4935 boundary = 1;
4936 } else {
4937 raw_children.push(boundary, raw);
4938 }
4939 }
4940 event @ (Event::Text(_)
4941 | Event::CData(_)
4942 | Event::Comment(_)
4943 | Event::PI(_)
4944 | Event::GeneralRef(_)) => {
4945 raw_children.push(boundary, capture_event(event)?);
4946 }
4947 Event::End(end) if matches_local_name(end.name().as_ref(), b"pt") => {
4948 let (value, value_markup) = value
4949 .ok_or_else(|| ChartError::MissingElement("c:v".to_owned()))?;
4950 return Ok((
4951 index,
4952 value,
4953 PointMarkup {
4954 raw_attributes,
4955 raw_children: raw_children_in_schema_order(&raw_children, 1),
4956 value: value_markup,
4957 },
4958 ));
4959 }
4960 Event::Eof => return Err(missing_end("c:pt")),
4961 _ => {}
4962 }
4963 inner.clear();
4964 }
4965 }
4966 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pt") => {
4967 return Err(ChartError::MissingElement("c:v".to_owned()));
4968 }
4969 Event::Start(element) | Event::Empty(element) => {
4970 return Err(ChartError::UnexpectedElement(element_name(&element)));
4971 }
4972 Event::Eof => return Err(ChartError::MissingElement("c:pt".to_owned())),
4973 _ => {}
4974 }
4975 buffer.clear();
4976 }
4977}
4978
4979fn parse_text_element(
4980 xml: &[u8],
4981 local: &[u8],
4982 inherited: &NamespaceBindings,
4983) -> Result<(String, TextMarkup)> {
4984 let mut reader = Reader::from_reader(xml);
4985 let mut buffer = Vec::new();
4986 loop {
4987 match reader
4988 .read_event_into(&mut buffer)
4989 .map_err(OxmlError::from)?
4990 {
4991 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
4992 typed_rewrite_bindings(&element, inherited)?;
4993 let raw_attributes = capture_attributes(&element)?;
4994 let mut value = String::new();
4995 let mut raw_children = OrderedRawChildren::default();
4996 let mut seen_text = false;
4997 let mut inner = Vec::new();
4998 loop {
4999 match reader
5000 .read_event_into(&mut inner)
5001 .map_err(OxmlError::from)?
5002 {
5003 Event::Text(text) => {
5004 value.push_str(&decode_plain(&text));
5005 seen_text = true;
5006 }
5007 Event::CData(text) => {
5008 let decoded =
5009 text.decode().map_err(|error| ChartError::InvalidValue {
5010 element: format!("c:{}", String::from_utf8_lossy(local)),
5011 value: error.to_string(),
5012 })?;
5013 value.push_str(&decoded);
5014 seen_text = true;
5015 }
5016 Event::GeneralRef(reference) => {
5017 value.push_str(&resolve_entity(&reference));
5018 seen_text = true;
5019 }
5020 event @ (Event::Comment(_) | Event::PI(_)) => {
5021 raw_children.push(usize::from(seen_text), capture_event(event)?);
5022 }
5023 Event::Start(child) | Event::Empty(child) => {
5024 return Err(ChartError::UnexpectedElement(element_name(&child)));
5025 }
5026 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
5027 return Ok((
5028 value,
5029 TextMarkup {
5030 raw_attributes,
5031 raw_children: raw_children_in_schema_order(&raw_children, 1),
5032 },
5033 ));
5034 }
5035 Event::Eof => {
5036 return Err(missing_end(&format!(
5037 "c:{}",
5038 String::from_utf8_lossy(local)
5039 )));
5040 }
5041 _ => {}
5042 }
5043 inner.clear();
5044 }
5045 }
5046 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
5047 typed_rewrite_bindings(&element, inherited)?;
5048 return Ok((
5049 String::new(),
5050 TextMarkup {
5051 raw_attributes: capture_attributes(&element)?,
5052 raw_children: OrderedRawChildren::default(),
5053 },
5054 ));
5055 }
5056 Event::Start(element) | Event::Empty(element) => {
5057 return Err(ChartError::UnexpectedElement(element_name(&element)));
5058 }
5059 Event::Eof => {
5060 return Err(ChartError::MissingElement(format!(
5061 "c:{}",
5062 String::from_utf8_lossy(local)
5063 )));
5064 }
5065 _ => {}
5066 }
5067 buffer.clear();
5068 }
5069}
5070
5071struct ParsedWrapper {
5072 choice: Option<(Vec<u8>, Vec<u8>)>,
5073 markup: WrapperMarkup,
5074 namespaces: NamespaceBindings,
5075}
5076
5077fn parse_wrapper(
5078 xml: &[u8],
5079 local: &[u8],
5080 choices: &[&[u8]],
5081 inherited: &NamespaceBindings,
5082) -> Result<ParsedWrapper> {
5083 let mut reader = Reader::from_reader(xml);
5084 let mut buffer = Vec::new();
5085 loop {
5086 match reader
5087 .read_event_into(&mut buffer)
5088 .map_err(OxmlError::from)?
5089 {
5090 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
5091 let namespaces = typed_rewrite_bindings(&element, inherited)?;
5092 let raw_attributes = capture_attributes(&element)?;
5093 let mut choice = None;
5094 let mut raw_children = OrderedRawChildren::default();
5095 let mut boundary = 0usize;
5096 let mut inner = Vec::new();
5097 loop {
5098 match reader
5099 .read_event_into(&mut inner)
5100 .map_err(OxmlError::from)?
5101 {
5102 Event::Start(child) => {
5103 let name = chart_child_local(&child, &namespaces)?;
5104 let raw = capture_element(&mut reader, &child)?;
5105 if let Some(name) =
5106 name.filter(|name| choices.contains(&name.as_slice()))
5107 {
5108 if choice.is_some() {
5109 return Err(ChartError::DuplicateElement(format!(
5110 "c:{} reference",
5111 String::from_utf8_lossy(local)
5112 )));
5113 }
5114 choice = Some((name, raw));
5115 boundary = 1;
5116 } else {
5117 raw_children.push(boundary, raw);
5118 }
5119 }
5120 Event::Empty(child) => {
5121 let name = chart_child_local(&child, &namespaces)?;
5122 let raw = capture_empty_element(&child)?;
5123 if let Some(name) =
5124 name.filter(|name| choices.contains(&name.as_slice()))
5125 {
5126 if choice.is_some() {
5127 return Err(ChartError::DuplicateElement(format!(
5128 "c:{} reference",
5129 String::from_utf8_lossy(local)
5130 )));
5131 }
5132 choice = Some((name, raw));
5133 boundary = 1;
5134 } else {
5135 raw_children.push(boundary, raw);
5136 }
5137 }
5138 event @ (Event::Text(_)
5139 | Event::CData(_)
5140 | Event::Comment(_)
5141 | Event::PI(_)
5142 | Event::GeneralRef(_)) => {
5143 raw_children.push(boundary, capture_event(event)?);
5144 }
5145 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
5146 return Ok(ParsedWrapper {
5147 choice,
5148 markup: WrapperMarkup {
5149 raw_attributes,
5150 raw_children: raw_children_in_schema_order(&raw_children, 1),
5151 },
5152 namespaces,
5153 });
5154 }
5155 Event::Eof => {
5156 return Err(missing_end(&format!(
5157 "c:{}",
5158 String::from_utf8_lossy(local)
5159 )));
5160 }
5161 _ => {}
5162 }
5163 inner.clear();
5164 }
5165 }
5166 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
5167 let namespaces = chart_bindings(inherited, &element)?;
5168 return Ok(ParsedWrapper {
5169 choice: None,
5170 markup: WrapperMarkup {
5171 raw_attributes: capture_attributes(&element)?,
5172 raw_children: OrderedRawChildren::default(),
5173 },
5174 namespaces,
5175 });
5176 }
5177 Event::Start(element) | Event::Empty(element) => {
5178 return Err(ChartError::UnexpectedElement(element_name(&element)));
5179 }
5180 Event::Eof => {
5181 return Err(ChartError::MissingElement(format!(
5182 "c:{}",
5183 String::from_utf8_lossy(local)
5184 )));
5185 }
5186 _ => {}
5187 }
5188 buffer.clear();
5189 }
5190}
5191
5192fn write_string_cache(
5193 writer: &mut Writer<Vec<u8>>,
5194 values: &[String],
5195 markup: &ReferenceMarkup,
5196) -> Result<()> {
5197 let (declared_count, indexes) = cache_layout(markup, values.len())?;
5198 let mut start = BytesStart::new("c:strCache");
5199 push_attributes(&mut start, &markup.cache_attributes);
5200 writer
5201 .write_event(Event::Start(start))
5202 .map_err(OxmlError::from)?;
5203 emit_raw(writer, markup.cache_children.at(0))?;
5204 write_scalar(
5205 writer,
5206 "c:ptCount",
5207 &declared_count.to_string(),
5208 Some(&markup.point_count),
5209 )?;
5210 emit_cache_point_raw(writer, markup, 1, values.len(), 0)?;
5211 for (position, value) in values.iter().enumerate() {
5212 let default_markup = PointMarkup::default();
5213 write_point(
5214 writer,
5215 indexes[position],
5216 value,
5217 markup.points.get(position).unwrap_or(&default_markup),
5218 )?;
5219 emit_cache_point_raw(writer, markup, 1, values.len(), position + 1)?;
5220 }
5221 writer
5222 .write_event(Event::End(BytesEnd::new("c:strCache")))
5223 .map_err(OxmlError::from)?;
5224 Ok(())
5225}
5226
5227fn write_numeric_cache(
5228 writer: &mut Writer<Vec<u8>>,
5229 data: &NumericData,
5230 markup: &ReferenceMarkup,
5231) -> Result<()> {
5232 let (declared_count, indexes) = cache_layout(markup, data.values.len())?;
5233 let mut start = BytesStart::new("c:numCache");
5234 push_attributes(&mut start, &markup.cache_attributes);
5235 writer
5236 .write_event(Event::Start(start))
5237 .map_err(OxmlError::from)?;
5238 emit_raw(writer, markup.cache_children.at(0))?;
5239 let default_format_markup = TextMarkup::default();
5240 write_text(
5241 writer,
5242 "c:formatCode",
5243 &data.format_code,
5244 markup
5245 .format_code
5246 .as_ref()
5247 .unwrap_or(&default_format_markup),
5248 )?;
5249 emit_raw(writer, markup.cache_children.at(1))?;
5250 write_scalar(
5251 writer,
5252 "c:ptCount",
5253 &declared_count.to_string(),
5254 Some(&markup.point_count),
5255 )?;
5256 emit_cache_point_raw(writer, markup, 2, data.values.len(), 0)?;
5257 for (position, value) in data.values.iter().enumerate() {
5258 let default_markup = PointMarkup::default();
5259 write_point(
5260 writer,
5261 indexes[position],
5262 &value.to_string(),
5263 markup.points.get(position).unwrap_or(&default_markup),
5264 )?;
5265 emit_cache_point_raw(writer, markup, 2, data.values.len(), position + 1)?;
5266 }
5267 writer
5268 .write_event(Event::End(BytesEnd::new("c:numCache")))
5269 .map_err(OxmlError::from)?;
5270 Ok(())
5271}
5272
5273fn emit_cache_point_raw(
5274 writer: &mut Writer<Vec<u8>>,
5275 markup: &ReferenceMarkup,
5276 point_base: usize,
5277 current_count: usize,
5278 completed_points: usize,
5279) -> Result<()> {
5280 let original_count = markup.points.len();
5281 if current_count == 0 {
5282 for boundary in point_base..=point_base + original_count {
5283 emit_raw(writer, markup.cache_children.at(boundary))?;
5284 }
5285 } else if completed_points == 0 {
5286 if original_count > 0 {
5287 emit_raw(writer, markup.cache_children.at(point_base))?;
5288 }
5289 } else if completed_points < current_count {
5290 if completed_points < original_count {
5291 emit_raw(
5292 writer,
5293 markup.cache_children.at(point_base + completed_points),
5294 )?;
5295 }
5296 } else {
5297 let first_tail = if original_count == 0 {
5298 point_base
5299 } else {
5300 point_base + current_count.min(original_count)
5301 };
5302 for boundary in first_tail..=point_base + original_count {
5303 emit_raw(writer, markup.cache_children.at(boundary))?;
5304 }
5305 }
5306 Ok(())
5307}
5308
5309fn write_point(
5310 writer: &mut Writer<Vec<u8>>,
5311 index: u32,
5312 value: &str,
5313 markup: &PointMarkup,
5314) -> Result<()> {
5315 let index = index.to_string();
5316 let mut start = BytesStart::new("c:pt");
5317 start.push_attribute(("idx", index.as_str()));
5318 push_attributes(&mut start, &markup.raw_attributes);
5319 writer
5320 .write_event(Event::Start(start))
5321 .map_err(OxmlError::from)?;
5322 emit_raw(writer, markup.raw_children.at(0))?;
5323 write_text(writer, "c:v", value, &markup.value)?;
5324 emit_raw(writer, markup.raw_children.at(1))?;
5325 writer
5326 .write_event(Event::End(BytesEnd::new("c:pt")))
5327 .map_err(OxmlError::from)?;
5328 Ok(())
5329}
5330
5331fn cache_layout(markup: &ReferenceMarkup, value_count: usize) -> Result<(u32, Vec<u32>)> {
5332 if markup.point_indexes.len() == value_count
5333 && let Some(declared) = markup.declared_point_count
5334 {
5335 return Ok((declared, markup.point_indexes.clone()));
5336 }
5337 let declared = u32::try_from(value_count).map_err(|_| ChartError::InvalidValue {
5338 element: "c:ptCount".to_owned(),
5339 value: value_count.to_string(),
5340 })?;
5341 Ok((declared, (0..declared).collect()))
5342}
5343
5344fn write_text(
5345 writer: &mut Writer<Vec<u8>>,
5346 tag: &str,
5347 value: &str,
5348 markup: &TextMarkup,
5349) -> Result<()> {
5350 let mut start = BytesStart::new(tag);
5351 push_attributes(&mut start, &markup.raw_attributes);
5352 writer
5353 .write_event(Event::Start(start))
5354 .map_err(OxmlError::from)?;
5355 emit_raw(writer, markup.raw_children.at(0))?;
5356 writer
5357 .write_event(Event::Text(BytesText::new(value)))
5358 .map_err(OxmlError::from)?;
5359 emit_raw(writer, markup.raw_children.at(1))?;
5360 writer
5361 .write_event(Event::End(BytesEnd::new(tag)))
5362 .map_err(OxmlError::from)?;
5363 Ok(())
5364}
5365
5366fn write_wrapper_start(
5367 writer: &mut Writer<Vec<u8>>,
5368 tag: &str,
5369 markup: &WrapperMarkup,
5370) -> Result<()> {
5371 let mut start = BytesStart::new(tag);
5372 push_attributes(&mut start, &markup.raw_attributes);
5373 writer
5374 .write_event(Event::Start(start))
5375 .map_err(OxmlError::from)?;
5376 emit_raw(writer, markup.raw_children.at(0))
5377}
5378
5379fn write_wrapper_end(
5380 writer: &mut Writer<Vec<u8>>,
5381 tag: &str,
5382 markup: &WrapperMarkup,
5383) -> Result<()> {
5384 emit_raw(writer, markup.raw_children.at(1))?;
5385 writer
5386 .write_event(Event::End(BytesEnd::new(tag)))
5387 .map_err(OxmlError::from)?;
5388 Ok(())
5389}
5390
5391fn validate_formula(formula: &str, element: &str) -> Result<()> {
5392 if formula.trim().is_empty() {
5393 return Err(ChartError::InvalidValue {
5394 element: element.to_owned(),
5395 value: formula.to_owned(),
5396 });
5397 }
5398 validate_xml_text(formula, element)
5399}
5400
5401fn validate_format_code(format_code: &str) -> Result<()> {
5402 if format_code.is_empty() {
5403 return Err(ChartError::InvalidValue {
5404 element: "c:formatCode".to_owned(),
5405 value: format_code.to_owned(),
5406 });
5407 }
5408 validate_xml_text(format_code, "c:formatCode")
5409}
5410
5411fn validate_xml_text(value: &str, element: &str) -> Result<()> {
5412 if value.chars().all(|character| {
5413 matches!(
5414 character,
5415 '\u{9}' | '\u{A}' | '\u{D}' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}'
5416 )
5417 }) {
5418 return Ok(());
5419 }
5420 Err(ChartError::InvalidValue {
5421 element: element.to_owned(),
5422 value: value.to_owned(),
5423 })
5424}
5425
5426fn validate_numeric_values(values: &[f64]) -> Result<()> {
5427 if let Some(value) = values.iter().find(|value| !value.is_finite()) {
5428 return Err(ChartError::InvalidValue {
5429 element: "c:v".to_owned(),
5430 value: value.to_string(),
5431 });
5432 }
5433 Ok(())
5434}
5435
5436fn validate_point_count(count: usize, element: &str) -> Result<()> {
5437 u32::try_from(count)
5438 .map(|_| ())
5439 .map_err(|_| ChartError::InvalidValue {
5440 element: element.to_owned(),
5441 value: count.to_string(),
5442 })
5443}
5444
5445fn parse_u32_scalar(xml: &[u8], local: &str) -> Result<(u32, ScalarMarkup)> {
5446 let (value, markup) = scalar_value(xml, local)?;
5447 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
5448 let parsed = value
5449 .parse::<u32>()
5450 .map_err(|_| invalid_attribute(local, "val", value))?;
5451 Ok((parsed, markup))
5452}
5453
5454fn required_u32_attribute(element: &BytesStart<'_>, local: &str, attribute: &[u8]) -> Result<u32> {
5455 let value = attribute_value(element, attribute)?.ok_or_else(|| {
5456 invalid_attribute(
5457 local,
5458 &String::from_utf8_lossy(attribute),
5459 "<missing>".to_owned(),
5460 )
5461 })?;
5462 value
5463 .parse::<u32>()
5464 .map_err(|_| invalid_attribute(local, &String::from_utf8_lossy(attribute), value))
5465}
5466
5467fn chart_root_prefix(element: &BytesStart<'_>) -> Result<Vec<u8>> {
5468 let name = element.name();
5469 let qualified = name.as_ref();
5470 let prefix = qualified
5471 .iter()
5472 .position(|byte| *byte == b':')
5473 .map(|position| qualified[..position].to_vec())
5474 .unwrap_or_default();
5475 let declaration = if prefix.is_empty() {
5476 b"xmlns".to_vec()
5477 } else {
5478 let mut declaration = b"xmlns:".to_vec();
5479 declaration.extend_from_slice(&prefix);
5480 declaration
5481 };
5482 if let Some(namespace) = attribute_value(element, &declaration)?
5483 && namespace != C_NS
5484 {
5485 return Err(invalid_attribute(
5486 &element_name(element),
5487 "namespace",
5488 namespace,
5489 ));
5490 }
5491 reject_conflicting_prefix(element, b"c", C_NS)?;
5492 Ok(prefix)
5493}
5494
5495fn typed_rewrite_bindings(
5496 element: &BytesStart<'_>,
5497 inherited: &NamespaceBindings,
5498) -> Result<NamespaceBindings> {
5499 chart_root_prefix(element)?;
5500 if !element_is_in_namespace(element, C_NS, inherited)? {
5501 return Err(ChartError::UnexpectedElement(element_name(element)));
5502 }
5503 let bindings = chart_bindings(inherited, element)?;
5504 require_fixed_namespace(&bindings, b"c", C_NS, element)?;
5505 Ok(bindings)
5506}
5507
5508fn chart_namespace_defaults() -> NamespaceBindings {
5509 vec![
5510 (b"c".to_vec(), C_NS.to_owned()),
5511 (b"a".to_vec(), A_NS.to_owned()),
5512 (b"r".to_vec(), R_NS.to_owned()),
5513 ]
5514}
5515
5516fn chart_bindings(
5517 inherited: &NamespaceBindings,
5518 element: &BytesStart<'_>,
5519) -> Result<NamespaceBindings> {
5520 let mut bindings = chart_namespace_defaults();
5521 for (prefix, namespace) in inherited
5522 .iter()
5523 .cloned()
5524 .chain(namespace_bindings(element)?)
5525 {
5526 upsert_namespace_binding(&mut bindings, prefix, namespace);
5527 }
5528 Ok(bindings)
5529}
5530
5531fn root_chart_bindings(
5532 xml: &[u8],
5533 local: &[u8],
5534 inherited: &NamespaceBindings,
5535) -> Result<NamespaceBindings> {
5536 let mut reader = Reader::from_reader(xml);
5537 let mut buffer = Vec::new();
5538 loop {
5539 match reader
5540 .read_event_into(&mut buffer)
5541 .map_err(OxmlError::from)?
5542 {
5543 Event::Start(element) | Event::Empty(element)
5544 if matches_local_name(element.name().as_ref(), local) =>
5545 {
5546 if !element_is_in_namespace(&element, C_NS, inherited)? {
5547 return Err(ChartError::UnexpectedElement(element_name(&element)));
5548 }
5549 return chart_bindings(inherited, &element);
5550 }
5551 Event::Start(element) | Event::Empty(element) => {
5552 return Err(ChartError::UnexpectedElement(element_name(&element)));
5553 }
5554 Event::Eof => {
5555 return Err(ChartError::MissingElement(format!(
5556 "c:{}",
5557 String::from_utf8_lossy(local)
5558 )));
5559 }
5560 _ => {}
5561 }
5562 buffer.clear();
5563 }
5564}
5565
5566fn chart_child_local(
5567 element: &BytesStart<'_>,
5568 inherited: &NamespaceBindings,
5569) -> Result<Option<Vec<u8>>> {
5570 Ok(element_is_in_namespace(element, C_NS, inherited)?
5571 .then(|| local_name(element.name().as_ref()).to_vec()))
5572}
5573
5574fn capture_fixed_root_attributes(
5575 start: &BytesStart<'_>,
5576 fixed: &[&str],
5577) -> Result<(XmlAttributes, XmlAttributes)> {
5578 let mut attributes = Vec::new();
5579 let mut namespaces = Vec::new();
5580 for attribute in start.attributes() {
5581 let attribute = attribute.map_err(OxmlError::from)?;
5582 let name = std::str::from_utf8(attribute.key.as_ref())
5583 .map_err(OxmlError::from)?
5584 .to_owned();
5585 let value = attribute
5586 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
5587 .map_err(OxmlError::from)?
5588 .into_owned();
5589 if fixed.contains(&name.as_str()) {
5590 continue;
5591 }
5592 if name == "xmlns" || name.starts_with("xmlns:") {
5593 namespaces.push((name, value));
5594 } else {
5595 attributes.push((name, value));
5596 }
5597 }
5598 Ok((attributes, namespaces))
5599}
5600
5601fn require_fixed_namespace(
5602 bindings: &NamespaceBindings,
5603 prefix: &[u8],
5604 expected: &str,
5605 element: &BytesStart<'_>,
5606) -> Result<()> {
5607 let actual = bindings
5608 .iter()
5609 .find(|(candidate, _)| candidate.as_slice() == prefix)
5610 .map(|(_, namespace)| namespace.as_str());
5611 if actual == Some(expected) {
5612 return Ok(());
5613 }
5614 Err(invalid_attribute(
5615 &element_name(element),
5616 &format!("xmlns:{}", String::from_utf8_lossy(prefix)),
5617 actual.unwrap_or("<missing>").to_owned(),
5618 ))
5619}
5620
5621fn standalone_namespace_declarations(bindings: &NamespaceBindings) -> Result<XmlAttributes> {
5622 bindings
5623 .iter()
5624 .filter(|(prefix, _)| prefix != b"c" && prefix != b"a" && prefix != b"r")
5625 .map(|(prefix, namespace)| {
5626 let prefix = std::str::from_utf8(prefix).map_err(OxmlError::from)?;
5627 let name = if prefix.is_empty() {
5628 "xmlns".to_owned()
5629 } else {
5630 format!("xmlns:{prefix}")
5631 };
5632 Ok((name, namespace.clone()))
5633 })
5634 .collect()
5635}
5636
5637fn capture_fixed_attributes(start: &BytesStart<'_>, fixed: &[&str]) -> Result<XmlAttributes> {
5638 let mut attributes = Vec::new();
5639 for attribute in start.attributes() {
5640 let attribute = attribute.map_err(OxmlError::from)?;
5641 let name = std::str::from_utf8(attribute.key.as_ref())
5642 .map_err(OxmlError::from)?
5643 .to_owned();
5644 if fixed.contains(&name.as_str()) {
5645 continue;
5646 }
5647 let value = attribute
5648 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
5649 .map_err(OxmlError::from)?
5650 .into_owned();
5651 attributes.push((name, value));
5652 }
5653 Ok(attributes)
5654}
5655
5656#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
5661pub struct AxisId(i64);
5662
5663impl AxisId {
5664 pub fn new(value: i64) -> Result<Self> {
5665 if (i64::from(i32::MIN)..=i64::from(u32::MAX)).contains(&value) {
5666 Ok(Self(value))
5667 } else {
5668 Err(ChartError::InvalidValue {
5669 element: "c:axId".to_owned(),
5670 value: value.to_string(),
5671 })
5672 }
5673 }
5674
5675 pub const fn value(self) -> i64 {
5676 self.0
5677 }
5678}
5679
5680#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5682pub enum BarDirection {
5683 Bar,
5684 Column,
5685}
5686
5687impl BarDirection {
5688 fn parse(value: &str) -> Option<Self> {
5689 match value {
5690 "bar" => Some(Self::Bar),
5691 "col" => Some(Self::Column),
5692 _ => None,
5693 }
5694 }
5695
5696 const fn as_str(self) -> &'static str {
5697 match self {
5698 Self::Bar => "bar",
5699 Self::Column => "col",
5700 }
5701 }
5702}
5703
5704#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5706pub enum BarGrouping {
5707 Clustered,
5708 PercentStacked,
5709 Stacked,
5710 Standard,
5711}
5712
5713impl BarGrouping {
5714 fn parse(value: &str) -> Option<Self> {
5715 match value {
5716 "clustered" => Some(Self::Clustered),
5717 "percentStacked" => Some(Self::PercentStacked),
5718 "stacked" => Some(Self::Stacked),
5719 "standard" => Some(Self::Standard),
5720 _ => None,
5721 }
5722 }
5723
5724 const fn as_str(self) -> &'static str {
5725 match self {
5726 Self::Clustered => "clustered",
5727 Self::PercentStacked => "percentStacked",
5728 Self::Stacked => "stacked",
5729 Self::Standard => "standard",
5730 }
5731 }
5732}
5733
5734#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5736pub enum Grouping {
5737 PercentStacked,
5738 Stacked,
5739 Standard,
5740}
5741
5742impl Grouping {
5743 fn parse(value: &str) -> Option<Self> {
5744 match value {
5745 "percentStacked" => Some(Self::PercentStacked),
5746 "stacked" => Some(Self::Stacked),
5747 "standard" => Some(Self::Standard),
5748 _ => None,
5749 }
5750 }
5751
5752 const fn as_str(self) -> &'static str {
5753 match self {
5754 Self::PercentStacked => "percentStacked",
5755 Self::Stacked => "stacked",
5756 Self::Standard => "standard",
5757 }
5758 }
5759}
5760
5761#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5763pub enum ScatterStyle {
5764 Line,
5765 LineMarker,
5766 Marker,
5767 None,
5768 Smooth,
5769 SmoothMarker,
5770}
5771
5772impl ScatterStyle {
5773 fn parse(value: &str) -> Option<Self> {
5774 match value {
5775 "line" => Some(Self::Line),
5776 "lineMarker" => Some(Self::LineMarker),
5777 "marker" => Some(Self::Marker),
5778 "none" => Some(Self::None),
5779 "smooth" => Some(Self::Smooth),
5780 "smoothMarker" => Some(Self::SmoothMarker),
5781 _ => None,
5782 }
5783 }
5784
5785 const fn as_str(self) -> &'static str {
5786 match self {
5787 Self::Line => "line",
5788 Self::LineMarker => "lineMarker",
5789 Self::Marker => "marker",
5790 Self::None => "none",
5791 Self::Smooth => "smooth",
5792 Self::SmoothMarker => "smoothMarker",
5793 }
5794 }
5795}
5796
5797#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5799pub enum RadarStyle {
5800 Filled,
5801 Marker,
5802 Standard,
5803}
5804
5805impl RadarStyle {
5806 fn parse(value: &str) -> Option<Self> {
5807 match value {
5808 "filled" => Some(Self::Filled),
5809 "marker" => Some(Self::Marker),
5810 "standard" => Some(Self::Standard),
5811 _ => None,
5812 }
5813 }
5814
5815 const fn as_str(self) -> &'static str {
5816 match self {
5817 Self::Filled => "filled",
5818 Self::Marker => "marker",
5819 Self::Standard => "standard",
5820 }
5821 }
5822}
5823
5824#[derive(Clone, Debug, PartialEq)]
5826pub enum Plot {
5827 Bar {
5828 direction: BarDirection,
5829 grouping: BarGrouping,
5830 gap_width: u16,
5831 overlap: i8,
5832 series: Vec<Series>,
5833 data_labels: Option<CT_DLbls>,
5834 axis_ids: [AxisId; 2],
5835 },
5836 Line {
5837 grouping: Grouping,
5838 marker: bool,
5839 smooth: bool,
5840 series: Vec<Series>,
5841 data_labels: Option<CT_DLbls>,
5842 axis_ids: [AxisId; 2],
5843 },
5844 Pie {
5845 first_slice_angle: u16,
5846 series: Vec<Series>,
5847 data_labels: Option<CT_DLbls>,
5848 },
5849 Doughnut {
5850 first_slice_angle: u16,
5851 hole_size: u8,
5852 series: Vec<Series>,
5853 data_labels: Option<CT_DLbls>,
5854 },
5855 Area {
5856 grouping: Grouping,
5857 series: Vec<Series>,
5858 data_labels: Option<CT_DLbls>,
5859 axis_ids: [AxisId; 2],
5860 },
5861 Scatter {
5862 style: ScatterStyle,
5863 series: Vec<Series>,
5864 data_labels: Option<CT_DLbls>,
5865 axis_ids: [AxisId; 2],
5866 },
5867 Radar {
5868 style: RadarStyle,
5869 series: Vec<Series>,
5870 data_labels: Option<CT_DLbls>,
5871 axis_ids: [AxisId; 2],
5872 },
5873}
5874
5875impl Plot {
5876 pub fn bar(
5877 direction: BarDirection,
5878 grouping: BarGrouping,
5879 series: Vec<Series>,
5880 axis_ids: [AxisId; 2],
5881 ) -> Result<Self> {
5882 let plot = Self::Bar {
5883 direction,
5884 grouping,
5885 gap_width: 150,
5886 overlap: 0,
5887 series,
5888 data_labels: None,
5889 axis_ids,
5890 };
5891 plot.validate()?;
5892 Ok(plot)
5893 }
5894
5895 pub fn line(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
5896 let plot = Self::Line {
5897 grouping,
5898 marker: false,
5899 smooth: false,
5900 series,
5901 data_labels: None,
5902 axis_ids,
5903 };
5904 plot.validate()?;
5905 Ok(plot)
5906 }
5907
5908 pub fn pie(series: Vec<Series>) -> Result<Self> {
5909 let plot = Self::Pie {
5910 first_slice_angle: 0,
5911 series,
5912 data_labels: None,
5913 };
5914 plot.validate()?;
5915 Ok(plot)
5916 }
5917
5918 pub fn doughnut(series: Vec<Series>) -> Result<Self> {
5919 let plot = Self::Doughnut {
5920 first_slice_angle: 0,
5921 hole_size: 50,
5922 series,
5923 data_labels: None,
5924 };
5925 plot.validate()?;
5926 Ok(plot)
5927 }
5928
5929 pub fn area(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
5930 let plot = Self::Area {
5931 grouping,
5932 series,
5933 data_labels: None,
5934 axis_ids,
5935 };
5936 plot.validate()?;
5937 Ok(plot)
5938 }
5939
5940 pub fn scatter(
5941 style: ScatterStyle,
5942 series: Vec<Series>,
5943 axis_ids: [AxisId; 2],
5944 ) -> Result<Self> {
5945 let plot = Self::Scatter {
5946 style,
5947 series,
5948 data_labels: None,
5949 axis_ids,
5950 };
5951 plot.validate()?;
5952 Ok(plot)
5953 }
5954
5955 pub fn radar(style: RadarStyle, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
5956 let plot = Self::Radar {
5957 style,
5958 series,
5959 data_labels: None,
5960 axis_ids,
5961 };
5962 plot.validate()?;
5963 Ok(plot)
5964 }
5965
5966 fn axis_ids(&self) -> Option<[AxisId; 2]> {
5967 match self {
5968 Self::Bar { axis_ids, .. }
5969 | Self::Line { axis_ids, .. }
5970 | Self::Area { axis_ids, .. }
5971 | Self::Scatter { axis_ids, .. }
5972 | Self::Radar { axis_ids, .. } => Some(*axis_ids),
5973 Self::Pie { .. } | Self::Doughnut { .. } => None,
5974 }
5975 }
5976
5977 fn validate(&self) -> Result<()> {
5978 let (series, labels, axis_ids) = match self {
5979 Self::Bar {
5980 gap_width,
5981 overlap,
5982 series,
5983 data_labels,
5984 axis_ids,
5985 ..
5986 } => {
5987 if *gap_width > 500 {
5988 return Err(ChartError::InvalidValue {
5989 element: "c:gapWidth".to_owned(),
5990 value: gap_width.to_string(),
5991 });
5992 }
5993 if !(-100..=100).contains(overlap) {
5994 return Err(ChartError::InvalidValue {
5995 element: "c:overlap".to_owned(),
5996 value: overlap.to_string(),
5997 });
5998 }
5999 (series, data_labels, Some(axis_ids))
6000 }
6001 Self::Line {
6002 series,
6003 data_labels,
6004 axis_ids,
6005 ..
6006 } => (series, data_labels, Some(axis_ids)),
6007 Self::Pie {
6008 first_slice_angle,
6009 series,
6010 data_labels,
6011 } => {
6012 if *first_slice_angle > 360 {
6013 return Err(ChartError::InvalidValue {
6014 element: "c:firstSliceAng".to_owned(),
6015 value: first_slice_angle.to_string(),
6016 });
6017 }
6018 (series, data_labels, None)
6019 }
6020 Self::Doughnut {
6021 first_slice_angle,
6022 hole_size,
6023 series,
6024 data_labels,
6025 } => {
6026 if *first_slice_angle > 360 {
6027 return Err(ChartError::InvalidValue {
6028 element: "c:firstSliceAng".to_owned(),
6029 value: first_slice_angle.to_string(),
6030 });
6031 }
6032 if !(10..=90).contains(hole_size) {
6033 return Err(ChartError::InvalidValue {
6034 element: "c:holeSize".to_owned(),
6035 value: hole_size.to_string(),
6036 });
6037 }
6038 (series, data_labels, None)
6039 }
6040 Self::Area {
6041 series,
6042 data_labels,
6043 axis_ids,
6044 ..
6045 }
6046 | Self::Radar {
6047 series,
6048 data_labels,
6049 axis_ids,
6050 ..
6051 } => (series, data_labels, Some(axis_ids)),
6052 Self::Scatter {
6053 series,
6054 data_labels,
6055 axis_ids,
6056 ..
6057 } => {
6058 for item in series {
6059 if !matches!(item.categories, Some(AxisData::Numeric(_))) {
6060 return Err(ChartError::InvalidValue {
6061 element: "c:xVal".to_owned(),
6062 value: "scatter series requires numeric x values".to_owned(),
6063 });
6064 }
6065 }
6066 (series, data_labels, Some(axis_ids))
6067 }
6068 };
6069 if series.is_empty() {
6070 return Err(ChartError::MissingElement("c:ser".to_owned()));
6071 }
6072 let scatter_plot = matches!(self, Self::Scatter { .. });
6073 for item in series {
6074 if item.bubble_size.is_some() || item.opaque_bubble_size {
6075 return Err(ChartError::InvalidValue {
6076 element: "c:bubbleSize".to_owned(),
6077 value: "bubble size is only valid in an opaque bubble plot".to_owned(),
6078 });
6079 }
6080 if item.uses_scatter_wrappers && !scatter_plot {
6081 return Err(ChartError::InvalidValue {
6082 element: "c:ser".to_owned(),
6083 value: "x/y wrappers are only valid in scatter plots".to_owned(),
6084 });
6085 }
6086 if scatter_plot && item.parsed_from_xml && !item.uses_scatter_wrappers {
6087 return Err(ChartError::InvalidValue {
6088 element: "c:ser".to_owned(),
6089 value: "scatter plots require xVal/yVal wrappers".to_owned(),
6090 });
6091 }
6092 }
6093 if let Some(axis_ids) = axis_ids {
6094 if axis_ids[0] == axis_ids[1] {
6095 return Err(ChartError::DuplicateElement(format!(
6096 "c:axId {}",
6097 axis_ids[0].value()
6098 )));
6099 }
6100 for id in axis_ids {
6101 AxisId::new(id.value())?;
6102 }
6103 }
6104 if let Some(labels) = labels {
6105 labels.validate()?;
6106 }
6107 for item in series {
6108 item.values.validate()?;
6109 }
6110 Ok(())
6111 }
6112}
6113
6114#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
6116pub enum AxisKind {
6117 Category,
6118 Value,
6119 Date,
6120 Series,
6121}
6122
6123impl AxisKind {
6124 fn parse(local: &[u8]) -> Option<Self> {
6125 match local {
6126 b"catAx" => Some(Self::Category),
6127 b"valAx" => Some(Self::Value),
6128 b"dateAx" => Some(Self::Date),
6129 b"serAx" => Some(Self::Series),
6130 _ => None,
6131 }
6132 }
6133
6134 const fn local(self) -> &'static str {
6135 match self {
6136 Self::Category => "catAx",
6137 Self::Value => "valAx",
6138 Self::Date => "dateAx",
6139 Self::Series => "serAx",
6140 }
6141 }
6142}
6143
6144#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6146pub enum Orientation {
6147 #[default]
6148 MinMax,
6149 MaxMin,
6150}
6151
6152impl Orientation {
6153 fn parse(value: &str) -> Option<Self> {
6154 match value {
6155 "minMax" => Some(Self::MinMax),
6156 "maxMin" => Some(Self::MaxMin),
6157 _ => None,
6158 }
6159 }
6160
6161 const fn as_str(self) -> &'static str {
6162 match self {
6163 Self::MinMax => "minMax",
6164 Self::MaxMin => "maxMin",
6165 }
6166 }
6167}
6168
6169#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6171pub enum AxisPosition {
6172 Bottom,
6173 Left,
6174 Right,
6175 Top,
6176}
6177
6178impl AxisPosition {
6179 fn parse(value: &str) -> Option<Self> {
6180 match value {
6181 "b" => Some(Self::Bottom),
6182 "l" => Some(Self::Left),
6183 "r" => Some(Self::Right),
6184 "t" => Some(Self::Top),
6185 _ => None,
6186 }
6187 }
6188
6189 const fn as_str(self) -> &'static str {
6190 match self {
6191 Self::Bottom => "b",
6192 Self::Left => "l",
6193 Self::Right => "r",
6194 Self::Top => "t",
6195 }
6196 }
6197}
6198
6199#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6201pub enum TickMark {
6202 Cross,
6203 Inside,
6204 #[default]
6205 None,
6206 Outside,
6207}
6208
6209impl TickMark {
6210 fn parse(value: &str) -> Option<Self> {
6211 match value {
6212 "cross" => Some(Self::Cross),
6213 "in" => Some(Self::Inside),
6214 "none" => Some(Self::None),
6215 "out" => Some(Self::Outside),
6216 _ => None,
6217 }
6218 }
6219
6220 const fn as_str(self) -> &'static str {
6221 match self {
6222 Self::Cross => "cross",
6223 Self::Inside => "in",
6224 Self::None => "none",
6225 Self::Outside => "out",
6226 }
6227 }
6228}
6229
6230#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6232pub enum TickLabelPosition {
6233 High,
6234 Low,
6235 #[default]
6236 NextTo,
6237 None,
6238}
6239
6240impl TickLabelPosition {
6241 fn parse(value: &str) -> Option<Self> {
6242 match value {
6243 "high" => Some(Self::High),
6244 "low" => Some(Self::Low),
6245 "nextTo" => Some(Self::NextTo),
6246 "none" => Some(Self::None),
6247 _ => None,
6248 }
6249 }
6250
6251 const fn as_str(self) -> &'static str {
6252 match self {
6253 Self::High => "high",
6254 Self::Low => "low",
6255 Self::NextTo => "nextTo",
6256 Self::None => "none",
6257 }
6258 }
6259}
6260
6261#[derive(Clone, Debug)]
6263pub struct Scaling {
6264 pub log_base: Option<f64>,
6265 pub orientation: Orientation,
6266 pub maximum: Option<f64>,
6267 pub minimum: Option<f64>,
6268 log_base_markup: Option<ScalarMarkup>,
6269 orientation_markup: Option<ScalarMarkup>,
6270 maximum_markup: Option<ScalarMarkup>,
6271 minimum_markup: Option<ScalarMarkup>,
6272 raw_attributes: XmlAttributes,
6273 raw_children: OrderedRawChildren,
6274}
6275
6276impl PartialEq for Scaling {
6277 fn eq(&self, other: &Self) -> bool {
6278 self.log_base == other.log_base
6279 && self.orientation == other.orientation
6280 && self.maximum == other.maximum
6281 && self.minimum == other.minimum
6282 && optional_scalar_markup_eq(&self.log_base_markup, &other.log_base_markup)
6283 && optional_scalar_markup_eq(&self.orientation_markup, &other.orientation_markup)
6284 && optional_scalar_markup_eq(&self.maximum_markup, &other.maximum_markup)
6285 && optional_scalar_markup_eq(&self.minimum_markup, &other.minimum_markup)
6286 && self.raw_attributes == other.raw_attributes
6287 && self.raw_children == other.raw_children
6288 }
6289}
6290
6291impl Default for Scaling {
6292 fn default() -> Self {
6293 Self {
6294 log_base: None,
6295 orientation: Orientation::MinMax,
6296 maximum: None,
6297 minimum: None,
6298 log_base_markup: None,
6299 orientation_markup: None,
6300 maximum_markup: None,
6301 minimum_markup: None,
6302 raw_attributes: Vec::new(),
6303 raw_children: OrderedRawChildren::default(),
6304 }
6305 }
6306}
6307
6308#[derive(Clone, Debug, Default, PartialEq)]
6310pub struct ChartLines {
6311 pub sp_pr: Option<CT_ShapeProperties>,
6312 raw_attributes: XmlAttributes,
6313 raw_children: OrderedRawChildren,
6314}
6315
6316#[derive(Clone, Debug, Eq, PartialEq)]
6318pub struct NumberFormat {
6319 pub format_code: String,
6320 pub source_linked: bool,
6321 raw_attributes: XmlAttributes,
6322 raw_content: Vec<Vec<u8>>,
6323}
6324
6325#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6327pub enum DataLabelPosition {
6328 BestFit,
6329 Bottom,
6330 Center,
6331 InsideBase,
6332 InsideEnd,
6333 Left,
6334 OutsideEnd,
6335 Right,
6336 Top,
6337}
6338
6339impl DataLabelPosition {
6340 fn parse(value: &str) -> Option<Self> {
6341 match value {
6342 "bestFit" => Some(Self::BestFit),
6343 "b" => Some(Self::Bottom),
6344 "ctr" => Some(Self::Center),
6345 "inBase" => Some(Self::InsideBase),
6346 "inEnd" => Some(Self::InsideEnd),
6347 "l" => Some(Self::Left),
6348 "outEnd" => Some(Self::OutsideEnd),
6349 "r" => Some(Self::Right),
6350 "t" => Some(Self::Top),
6351 _ => None,
6352 }
6353 }
6354
6355 const fn as_str(self) -> &'static str {
6356 match self {
6357 Self::BestFit => "bestFit",
6358 Self::Bottom => "b",
6359 Self::Center => "ctr",
6360 Self::InsideBase => "inBase",
6361 Self::InsideEnd => "inEnd",
6362 Self::Left => "l",
6363 Self::OutsideEnd => "outEnd",
6364 Self::Right => "r",
6365 Self::Top => "t",
6366 }
6367 }
6368}
6369
6370#[derive(Clone, Debug, Default, PartialEq)]
6372pub struct CT_DLbls {
6373 pub number_format: Option<NumberFormat>,
6374 pub position: Option<DataLabelPosition>,
6375 pub separator: Option<String>,
6376 pub show_legend_key: bool,
6377 pub show_value: bool,
6378 pub show_category_name: bool,
6379 pub show_series_name: bool,
6380 pub show_percent: bool,
6381 pub show_bubble_size: bool,
6382 position_markup: Option<ScalarMarkup>,
6383 show_legend_key_markup: Option<ScalarMarkup>,
6384 show_value_markup: Option<ScalarMarkup>,
6385 show_category_name_markup: Option<ScalarMarkup>,
6386 show_series_name_markup: Option<ScalarMarkup>,
6387 show_percent_markup: Option<ScalarMarkup>,
6388 show_bubble_size_markup: Option<ScalarMarkup>,
6389 separator_markup: Option<TextMarkup>,
6390 raw_attributes: XmlAttributes,
6391 namespace_declarations: XmlAttributes,
6392 raw_children: OrderedRawChildren,
6393}
6394
6395#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6397pub enum DispBlanksAs {
6398 #[default]
6399 Gap,
6400 Zero,
6401 Span,
6402}
6403
6404impl DispBlanksAs {
6405 fn parse(value: &str) -> Option<Self> {
6406 match value {
6407 "gap" => Some(Self::Gap),
6408 "zero" => Some(Self::Zero),
6409 "span" => Some(Self::Span),
6410 _ => None,
6411 }
6412 }
6413
6414 const fn as_str(self) -> &'static str {
6415 match self {
6416 Self::Gap => "gap",
6417 Self::Zero => "zero",
6418 Self::Span => "span",
6419 }
6420 }
6421}
6422
6423#[derive(Clone, Debug, Default, Eq, PartialEq)]
6425pub struct CT_Title {
6426 raw_attributes: Vec<(String, String)>,
6427 raw_children: OrderedRawChildren,
6428}
6429
6430#[derive(Clone, Debug, Default, PartialEq)]
6432struct PlotMarkup {
6433 raw_attributes: XmlAttributes,
6434 raw_children: OrderedRawChildren,
6435 direction: Option<ScalarMarkup>,
6436 grouping: Option<ScalarMarkup>,
6437 gap_width: Option<ScalarMarkup>,
6438 overlap: Option<ScalarMarkup>,
6439 marker: Option<ScalarMarkup>,
6440 smooth: Option<ScalarMarkup>,
6441 first_slice_angle: Option<ScalarMarkup>,
6442 hole_size: Option<ScalarMarkup>,
6443 style: Option<ScalarMarkup>,
6444 axis_ids: Vec<AxisIdMarkup>,
6445 original_series_keys: Vec<(u32, u32)>,
6446 original_axis_ids: Vec<AxisId>,
6447 parsed_bar: Option<bool>,
6448 parsed_remaining: Option<&'static str>,
6449}
6450
6451#[derive(Clone, Debug, PartialEq)]
6453pub struct CT_PlotArea {
6454 raw_attributes: Vec<(String, String)>,
6455 raw_children: OrderedRawChildren,
6456 namespace_bindings: NamespaceBindings,
6457 plots: Option<Vec<Plot>>,
6458 plot_markup: Vec<PlotMarkup>,
6459 axes: Vec<Axis>,
6460}
6461
6462#[derive(Clone, Debug, Default, Eq, PartialEq)]
6464pub struct CT_Legend {
6465 raw_attributes: Vec<(String, String)>,
6466 raw_children: OrderedRawChildren,
6467}
6468
6469#[derive(Clone, Debug, PartialEq)]
6470struct AxisIdMarkup {
6471 scalar: ScalarMarkup,
6472 parsed: AxisId,
6473 lexical: String,
6474}
6475
6476#[derive(Clone, Debug)]
6478pub struct Axis {
6479 pub kind: AxisKind,
6480 pub id: AxisId,
6481 pub scaling: Scaling,
6482 pub deleted: bool,
6483 pub position: AxisPosition,
6484 pub major_gridlines: Option<ChartLines>,
6485 pub minor_gridlines: Option<ChartLines>,
6486 pub title: Option<CT_Title>,
6487 pub number_format: Option<NumberFormat>,
6488 pub major_tick_mark: TickMark,
6489 pub minor_tick_mark: TickMark,
6490 pub tick_label_position: TickLabelPosition,
6491 pub sp_pr: Option<CT_ShapeProperties>,
6492 pub tx_pr: Option<CT_TextBody>,
6493 pub cross_axis: AxisId,
6494 parsed_kind: Option<AxisKind>,
6495 id_markup: AxisIdMarkup,
6496 delete_markup: Option<ScalarMarkup>,
6497 position_markup: ScalarMarkup,
6498 major_tick_mark_markup: Option<ScalarMarkup>,
6499 minor_tick_mark_markup: Option<ScalarMarkup>,
6500 tick_label_position_markup: Option<ScalarMarkup>,
6501 cross_axis_markup: AxisIdMarkup,
6502 raw_attributes: XmlAttributes,
6503 namespace_declarations: XmlAttributes,
6504 raw_children: OrderedRawChildren,
6505}
6506
6507impl PartialEq for Axis {
6508 fn eq(&self, other: &Self) -> bool {
6509 self.kind == other.kind
6510 && self.id == other.id
6511 && axis_id_markup_eq(&self.id_markup, &other.id_markup)
6512 && self.scaling == other.scaling
6513 && self.deleted == other.deleted
6514 && optional_scalar_markup_eq(&self.delete_markup, &other.delete_markup)
6515 && self.position == other.position
6516 && self.position_markup == other.position_markup
6517 && self.major_gridlines == other.major_gridlines
6518 && self.minor_gridlines == other.minor_gridlines
6519 && self.title == other.title
6520 && self.number_format == other.number_format
6521 && self.major_tick_mark == other.major_tick_mark
6522 && optional_scalar_markup_eq(
6523 &self.major_tick_mark_markup,
6524 &other.major_tick_mark_markup,
6525 )
6526 && self.minor_tick_mark == other.minor_tick_mark
6527 && optional_scalar_markup_eq(
6528 &self.minor_tick_mark_markup,
6529 &other.minor_tick_mark_markup,
6530 )
6531 && self.tick_label_position == other.tick_label_position
6532 && optional_scalar_markup_eq(
6533 &self.tick_label_position_markup,
6534 &other.tick_label_position_markup,
6535 )
6536 && self.sp_pr == other.sp_pr
6537 && self.tx_pr == other.tx_pr
6538 && self.cross_axis == other.cross_axis
6539 && axis_id_markup_eq(&self.cross_axis_markup, &other.cross_axis_markup)
6540 && self.raw_attributes == other.raw_attributes
6541 && self.namespace_declarations == other.namespace_declarations
6542 && self.raw_children == other.raw_children
6543 }
6544}
6545
6546impl Axis {
6547 pub fn new(kind: AxisKind, id: AxisId, position: AxisPosition, cross_axis: AxisId) -> Self {
6548 Self {
6549 kind,
6550 id,
6551 scaling: Scaling::default(),
6552 deleted: false,
6553 position,
6554 major_gridlines: None,
6555 minor_gridlines: None,
6556 title: None,
6557 number_format: None,
6558 major_tick_mark: TickMark::None,
6559 minor_tick_mark: TickMark::None,
6560 tick_label_position: TickLabelPosition::NextTo,
6561 sp_pr: None,
6562 tx_pr: None,
6563 cross_axis,
6564 parsed_kind: None,
6565 id_markup: default_axis_id_markup(id),
6566 delete_markup: None,
6567 position_markup: ScalarMarkup::default(),
6568 major_tick_mark_markup: None,
6569 minor_tick_mark_markup: None,
6570 tick_label_position_markup: None,
6571 cross_axis_markup: default_axis_id_markup(cross_axis),
6572 raw_attributes: Vec::new(),
6573 namespace_declarations: Vec::new(),
6574 raw_children: OrderedRawChildren::default(),
6575 }
6576 }
6577
6578 pub fn from_xml(xml: &[u8]) -> Result<Self> {
6579 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
6580 }
6581
6582 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
6583 let mut reader = Reader::from_reader(xml);
6584 let mut buffer = Vec::new();
6585 loop {
6586 match reader
6587 .read_event_into(&mut buffer)
6588 .map_err(OxmlError::from)?
6589 {
6590 Event::Start(element) => {
6591 let name = element.name();
6592 let local = local_name(name.as_ref());
6593 let Some(kind) = AxisKind::parse(local) else {
6594 return Err(ChartError::UnexpectedElement(element_name(&element)));
6595 };
6596 chart_root_prefix(&element)?;
6597 if !element_is_in_namespace(&element, C_NS, inherited)? {
6598 return Err(ChartError::UnexpectedElement(element_name(&element)));
6599 }
6600 return Self::from_element(&mut reader, &element, inherited, kind);
6601 }
6602 Event::Empty(element) => {
6603 let name = element.name();
6604 let local = local_name(name.as_ref());
6605 if AxisKind::parse(local).is_some() {
6606 return Err(ChartError::MissingElement("c:axId".to_owned()));
6607 }
6608 return Err(ChartError::UnexpectedElement(element_name(&element)));
6609 }
6610 Event::Eof => return Err(ChartError::MissingElement("ChartML axis".to_owned())),
6611 _ => {}
6612 }
6613 buffer.clear();
6614 }
6615 }
6616
6617 fn from_element(
6618 reader: &mut Reader<&[u8]>,
6619 start: &BytesStart<'_>,
6620 inherited: &NamespaceBindings,
6621 kind: AxisKind,
6622 ) -> Result<Self> {
6623 reject_conflicting_prefix(start, b"a", A_NS)?;
6624 reject_conflicting_prefix(start, b"r", R_NS)?;
6625 let namespaces = chart_bindings(inherited, start)?;
6626 require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
6627 require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
6628 require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
6629 let (raw_attributes, _) =
6630 capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
6631 let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
6632 let mut state = AxisParseState::default();
6633 let mut buffer = Vec::new();
6634 loop {
6635 match reader
6636 .read_event_into(&mut buffer)
6637 .map_err(OxmlError::from)?
6638 {
6639 Event::Start(element) => {
6640 let name = chart_child_local(&element, &namespaces)?;
6641 let raw = capture_element(reader, &element)?;
6642 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
6643 }
6644 Event::Empty(element) => {
6645 let name = chart_child_local(&element, &namespaces)?;
6646 let raw = capture_empty_element(&element)?;
6647 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
6648 }
6649 event @ (Event::Text(_)
6650 | Event::CData(_)
6651 | Event::Comment(_)
6652 | Event::PI(_)
6653 | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
6654 Event::End(element)
6655 if matches_local_name(element.name().as_ref(), kind.local().as_bytes()) =>
6656 {
6657 break;
6658 }
6659 Event::Eof => return Err(missing_end(&format!("c:{}", kind.local()))),
6660 _ => {}
6661 }
6662 buffer.clear();
6663 }
6664 state.finish(kind, raw_attributes, namespace_declarations)
6665 }
6666
6667 pub fn to_xml(&self) -> Result<Vec<u8>> {
6668 self.validate()?;
6669 let mut writer = Writer::new(Vec::new());
6670 let root = format!("c:{}", self.kind.local());
6671 let mut start = BytesStart::new(&root);
6672 start.push_attribute(("xmlns:c", C_NS));
6673 start.push_attribute(("xmlns:a", A_NS));
6674 start.push_attribute(("xmlns:r", R_NS));
6675 push_attributes(&mut start, &self.namespace_declarations);
6676 push_attributes(&mut start, &self.raw_attributes);
6677 writer
6678 .write_event(Event::Start(start))
6679 .map_err(OxmlError::from)?;
6680 emit_raw(&mut writer, self.raw_children.at(0))?;
6681 write_axis_id(&mut writer, "c:axId", self.id, &self.id_markup)?;
6682 emit_raw(&mut writer, self.raw_children.at(1))?;
6683 self.scaling.write_xml(&mut writer)?;
6684 emit_raw(&mut writer, self.raw_children.at(2))?;
6685 if self.deleted || self.delete_markup.is_some() {
6686 write_scalar(
6687 &mut writer,
6688 "c:delete",
6689 bool_lexical(self.deleted),
6690 self.delete_markup.as_ref(),
6691 )?;
6692 }
6693 emit_raw(&mut writer, self.raw_children.at(3))?;
6694 write_scalar(
6695 &mut writer,
6696 "c:axPos",
6697 self.position.as_str(),
6698 Some(&self.position_markup),
6699 )?;
6700 emit_raw(&mut writer, self.raw_children.at(4))?;
6701 if let Some(lines) = &self.major_gridlines {
6702 lines.write_xml(&mut writer, "c:majorGridlines")?;
6703 }
6704 emit_raw(&mut writer, self.raw_children.at(5))?;
6705 if let Some(lines) = &self.minor_gridlines {
6706 lines.write_xml(&mut writer, "c:minorGridlines")?;
6707 }
6708 emit_raw(&mut writer, self.raw_children.at(6))?;
6709 if let Some(title) = &self.title {
6710 title.write_xml(&mut writer)?;
6711 }
6712 emit_raw(&mut writer, self.raw_children.at(7))?;
6713 if let Some(number_format) = &self.number_format {
6714 number_format.write_xml(&mut writer)?;
6715 }
6716 emit_raw(&mut writer, self.raw_children.at(8))?;
6717 if self.major_tick_mark != TickMark::None || self.major_tick_mark_markup.is_some() {
6718 write_scalar(
6719 &mut writer,
6720 "c:majorTickMark",
6721 self.major_tick_mark.as_str(),
6722 self.major_tick_mark_markup.as_ref(),
6723 )?;
6724 }
6725 emit_raw(&mut writer, self.raw_children.at(9))?;
6726 if self.minor_tick_mark != TickMark::None || self.minor_tick_mark_markup.is_some() {
6727 write_scalar(
6728 &mut writer,
6729 "c:minorTickMark",
6730 self.minor_tick_mark.as_str(),
6731 self.minor_tick_mark_markup.as_ref(),
6732 )?;
6733 }
6734 emit_raw(&mut writer, self.raw_children.at(10))?;
6735 if self.tick_label_position != TickLabelPosition::NextTo
6736 || self.tick_label_position_markup.is_some()
6737 {
6738 write_scalar(
6739 &mut writer,
6740 "c:tickLblPos",
6741 self.tick_label_position.as_str(),
6742 self.tick_label_position_markup.as_ref(),
6743 )?;
6744 }
6745 emit_raw(&mut writer, self.raw_children.at(11))?;
6746 if let Some(properties) = &self.sp_pr {
6747 properties.write_xml_as(&mut writer, "c:spPr")?;
6748 }
6749 emit_raw(&mut writer, self.raw_children.at(12))?;
6750 if let Some(text) = &self.tx_pr {
6751 text.write_xml_as(&mut writer, "c:txPr")?;
6752 }
6753 emit_raw(&mut writer, self.raw_children.at(13))?;
6754 write_axis_id(
6755 &mut writer,
6756 "c:crossAx",
6757 self.cross_axis,
6758 &self.cross_axis_markup,
6759 )?;
6760 emit_raw(&mut writer, self.raw_children.at(14))?;
6761 writer
6762 .write_event(Event::End(BytesEnd::new(&root)))
6763 .map_err(OxmlError::from)?;
6764 Ok(writer.into_inner())
6765 }
6766
6767 pub fn raw_children(&self) -> &OrderedRawChildren {
6768 &self.raw_children
6769 }
6770
6771 fn validate(&self) -> Result<()> {
6772 if let Some(parsed_kind) = self.parsed_kind
6773 && self.kind != parsed_kind
6774 {
6775 return Err(ChartError::InvalidValue {
6776 element: "ChartML axis root".to_owned(),
6777 value: format!(
6778 "parsed c:{} cannot be relabelled c:{}",
6779 parsed_kind.local(),
6780 self.kind.local()
6781 ),
6782 });
6783 }
6784 AxisId::new(self.id.value())?;
6785 AxisId::new(self.cross_axis.value())?;
6786 self.scaling.validate()?;
6787 if let Some(number_format) = &self.number_format {
6788 number_format.validate()?;
6789 }
6790 Ok(())
6791 }
6792}
6793
6794#[derive(Default)]
6795struct AxisParseState {
6796 id: Option<(AxisId, AxisIdMarkup)>,
6797 scaling: Option<Scaling>,
6798 deleted: Option<(bool, ScalarMarkup)>,
6799 position: Option<(AxisPosition, ScalarMarkup)>,
6800 major_gridlines: Option<ChartLines>,
6801 minor_gridlines: Option<ChartLines>,
6802 title: Option<CT_Title>,
6803 number_format: Option<NumberFormat>,
6804 major_tick_mark: Option<(TickMark, ScalarMarkup)>,
6805 minor_tick_mark: Option<(TickMark, ScalarMarkup)>,
6806 tick_label_position: Option<(TickLabelPosition, ScalarMarkup)>,
6807 sp_pr: Option<CT_ShapeProperties>,
6808 tx_pr: Option<CT_TextBody>,
6809 cross_axis: Option<(AxisId, AxisIdMarkup)>,
6810 raw_children: OrderedRawChildren,
6811 boundary: usize,
6812}
6813
6814impl AxisParseState {
6815 fn capture_event(&mut self, raw: Vec<u8>) {
6816 self.raw_children.push(self.boundary, raw);
6817 }
6818
6819 fn parse_child(
6820 &mut self,
6821 name: &[u8],
6822 raw: Vec<u8>,
6823 namespaces: &NamespaceBindings,
6824 ) -> Result<()> {
6825 match name {
6826 b"axId" => {
6827 set_once(&mut self.id, parse_axis_id_scalar(&raw, "axId")?, "c:axId")?;
6828 self.boundary = self.boundary.max(1);
6829 }
6830 b"scaling" => {
6831 set_once(
6832 &mut self.scaling,
6833 Scaling::from_xml(&raw, namespaces)?,
6834 "c:scaling",
6835 )?;
6836 self.boundary = self.boundary.max(2);
6837 }
6838 b"delete" => {
6839 set_once(
6840 &mut self.deleted,
6841 parse_bool_value(&raw, "delete")?,
6842 "c:delete",
6843 )?;
6844 self.boundary = self.boundary.max(3);
6845 }
6846 b"axPos" => {
6847 set_once(&mut self.position, parse_axis_position(&raw)?, "c:axPos")?;
6848 self.boundary = self.boundary.max(4);
6849 }
6850 b"majorGridlines" => {
6851 set_once(
6852 &mut self.major_gridlines,
6853 ChartLines::from_xml(&raw, b"majorGridlines", namespaces)?,
6854 "c:majorGridlines",
6855 )?;
6856 self.boundary = self.boundary.max(5);
6857 }
6858 b"minorGridlines" => {
6859 set_once(
6860 &mut self.minor_gridlines,
6861 ChartLines::from_xml(&raw, b"minorGridlines", namespaces)?,
6862 "c:minorGridlines",
6863 )?;
6864 self.boundary = self.boundary.max(6);
6865 }
6866 b"title" => {
6867 set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
6868 self.boundary = self.boundary.max(7);
6869 }
6870 b"numFmt" => {
6871 set_once(
6872 &mut self.number_format,
6873 NumberFormat::from_xml(&raw)?,
6874 "c:numFmt",
6875 )?;
6876 self.boundary = self.boundary.max(8);
6877 }
6878 b"majorTickMark" => {
6879 set_once(
6880 &mut self.major_tick_mark,
6881 parse_tick_mark(&raw, "majorTickMark")?,
6882 "c:majorTickMark",
6883 )?;
6884 self.boundary = self.boundary.max(9);
6885 }
6886 b"minorTickMark" => {
6887 set_once(
6888 &mut self.minor_tick_mark,
6889 parse_tick_mark(&raw, "minorTickMark")?,
6890 "c:minorTickMark",
6891 )?;
6892 self.boundary = self.boundary.max(10);
6893 }
6894 b"tickLblPos" => {
6895 set_once(
6896 &mut self.tick_label_position,
6897 parse_tick_label_position(&raw)?,
6898 "c:tickLblPos",
6899 )?;
6900 self.boundary = self.boundary.max(11);
6901 }
6902 b"spPr" => {
6903 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
6904 let properties = CT_ShapeProperties::from_xml(&raw)?;
6905 let mut writer = Writer::new(Vec::new());
6906 properties.write_xml_as(&mut writer, "c:spPr")?;
6907 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
6908 set_once(&mut self.sp_pr, properties, "c:spPr")?;
6909 self.boundary = self.boundary.max(12);
6910 }
6911 b"txPr" => {
6912 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
6913 let text = CT_TextBody::from_xml_as(&raw, b"txPr")?;
6914 let mut writer = Writer::new(Vec::new());
6915 text.write_xml_as(&mut writer, "c:txPr")?;
6916 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
6917 set_once(&mut self.tx_pr, text, "c:txPr")?;
6918 self.boundary = self.boundary.max(13);
6919 }
6920 b"crossAx" => {
6921 set_once(
6922 &mut self.cross_axis,
6923 parse_axis_id_scalar(&raw, "crossAx")?,
6924 "c:crossAx",
6925 )?;
6926 self.boundary = self.boundary.max(14);
6927 }
6928 _ => self.raw_children.push(self.boundary, raw),
6929 }
6930 Ok(())
6931 }
6932
6933 fn finish(
6934 self,
6935 kind: AxisKind,
6936 raw_attributes: XmlAttributes,
6937 namespace_declarations: XmlAttributes,
6938 ) -> Result<Axis> {
6939 let (id, id_markup) = self
6940 .id
6941 .ok_or_else(|| ChartError::MissingElement("c:axId".to_owned()))?;
6942 let scaling = self
6943 .scaling
6944 .ok_or_else(|| ChartError::MissingElement("c:scaling".to_owned()))?;
6945 let (position, position_markup) = self
6946 .position
6947 .ok_or_else(|| ChartError::MissingElement("c:axPos".to_owned()))?;
6948 let (cross_axis, cross_axis_markup) = self
6949 .cross_axis
6950 .ok_or_else(|| ChartError::MissingElement("c:crossAx".to_owned()))?;
6951 let (deleted, delete_markup) = self
6952 .deleted
6953 .map(|(value, markup)| (value, Some(markup)))
6954 .unwrap_or((false, None));
6955 let (major_tick_mark, major_tick_mark_markup) = self
6956 .major_tick_mark
6957 .map(|(value, markup)| (value, Some(markup)))
6958 .unwrap_or((TickMark::None, None));
6959 let (minor_tick_mark, minor_tick_mark_markup) = self
6960 .minor_tick_mark
6961 .map(|(value, markup)| (value, Some(markup)))
6962 .unwrap_or((TickMark::None, None));
6963 let (tick_label_position, tick_label_position_markup) = self
6964 .tick_label_position
6965 .map(|(value, markup)| (value, Some(markup)))
6966 .unwrap_or((TickLabelPosition::NextTo, None));
6967 let axis = Axis {
6968 kind,
6969 id,
6970 scaling,
6971 deleted,
6972 position,
6973 major_gridlines: self.major_gridlines,
6974 minor_gridlines: self.minor_gridlines,
6975 title: self.title,
6976 number_format: self.number_format,
6977 major_tick_mark,
6978 minor_tick_mark,
6979 tick_label_position,
6980 sp_pr: self.sp_pr,
6981 tx_pr: self.tx_pr,
6982 cross_axis,
6983 parsed_kind: Some(kind),
6984 id_markup,
6985 delete_markup,
6986 position_markup,
6987 major_tick_mark_markup,
6988 minor_tick_mark_markup,
6989 tick_label_position_markup,
6990 cross_axis_markup,
6991 raw_attributes,
6992 namespace_declarations,
6993 raw_children: raw_children_in_schema_order(&self.raw_children, 14),
6994 };
6995 axis.validate()?;
6996 Ok(axis)
6997 }
6998}
6999
7000impl Scaling {
7001 fn from_xml(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7002 let mut reader = Reader::from_reader(xml);
7003 let mut buffer = Vec::new();
7004 loop {
7005 match reader
7006 .read_event_into(&mut buffer)
7007 .map_err(OxmlError::from)?
7008 {
7009 Event::Start(element)
7010 if matches_local_name(element.name().as_ref(), b"scaling") =>
7011 {
7012 let namespaces = typed_rewrite_bindings(&element, inherited)?;
7013 let raw_attributes = capture_attributes(&element)?;
7014 let mut log_base = None;
7015 let mut orientation = None;
7016 let mut maximum = None;
7017 let mut minimum = None;
7018 let mut raw_children = OrderedRawChildren::default();
7019 let mut boundary = 0usize;
7020 let mut inner = Vec::new();
7021 loop {
7022 match reader
7023 .read_event_into(&mut inner)
7024 .map_err(OxmlError::from)?
7025 {
7026 Event::Start(child) => {
7027 let name = chart_child_local(&child, &namespaces)?;
7028 let raw = capture_element(&mut reader, &child)?;
7029 parse_scaling_child(
7030 name.as_deref().unwrap_or_default(),
7031 raw,
7032 &mut log_base,
7033 &mut orientation,
7034 &mut maximum,
7035 &mut minimum,
7036 &mut raw_children,
7037 &mut boundary,
7038 )?;
7039 }
7040 Event::Empty(child) => {
7041 let name = chart_child_local(&child, &namespaces)?;
7042 let raw = capture_empty_element(&child)?;
7043 parse_scaling_child(
7044 name.as_deref().unwrap_or_default(),
7045 raw,
7046 &mut log_base,
7047 &mut orientation,
7048 &mut maximum,
7049 &mut minimum,
7050 &mut raw_children,
7051 &mut boundary,
7052 )?;
7053 }
7054 event @ (Event::Text(_)
7055 | Event::CData(_)
7056 | Event::Comment(_)
7057 | Event::PI(_)
7058 | Event::GeneralRef(_)) => {
7059 raw_children.push(boundary, capture_event(event)?);
7060 }
7061 Event::End(end)
7062 if matches_local_name(end.name().as_ref(), b"scaling") =>
7063 {
7064 let (log_base, log_base_markup) = optional_markup(log_base);
7065 let (orientation, orientation_markup) = orientation
7066 .map(|(value, markup)| (value, Some(markup)))
7067 .unwrap_or((Orientation::MinMax, None));
7068 let (maximum, maximum_markup) = optional_markup(maximum);
7069 let (minimum, minimum_markup) = optional_markup(minimum);
7070 let scaling = Self {
7071 log_base,
7072 orientation,
7073 maximum,
7074 minimum,
7075 log_base_markup,
7076 orientation_markup,
7077 maximum_markup,
7078 minimum_markup,
7079 raw_attributes,
7080 raw_children: raw_children_in_schema_order(&raw_children, 4),
7081 };
7082 scaling.validate()?;
7083 return Ok(scaling);
7084 }
7085 Event::Eof => return Err(missing_end("c:scaling")),
7086 _ => {}
7087 }
7088 inner.clear();
7089 }
7090 }
7091 Event::Empty(element)
7092 if matches_local_name(element.name().as_ref(), b"scaling") =>
7093 {
7094 typed_rewrite_bindings(&element, inherited)?;
7095 return Ok(Self {
7096 raw_attributes: capture_attributes(&element)?,
7097 ..Self::default()
7098 });
7099 }
7100 Event::Start(element) | Event::Empty(element) => {
7101 return Err(ChartError::UnexpectedElement(element_name(&element)));
7102 }
7103 Event::Eof => return Err(ChartError::MissingElement("c:scaling".to_owned())),
7104 _ => {}
7105 }
7106 buffer.clear();
7107 }
7108 }
7109
7110 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7111 self.validate()?;
7112 let mut start = BytesStart::new("c:scaling");
7113 push_attributes(&mut start, &self.raw_attributes);
7114 writer
7115 .write_event(Event::Start(start))
7116 .map_err(OxmlError::from)?;
7117 emit_raw(writer, self.raw_children.at(0))?;
7118 if let Some(value) = self.log_base {
7119 write_scalar(
7120 writer,
7121 "c:logBase",
7122 &value.to_string(),
7123 self.log_base_markup.as_ref(),
7124 )?;
7125 }
7126 emit_raw(writer, self.raw_children.at(1))?;
7127 if self.orientation != Orientation::MinMax || self.orientation_markup.is_some() {
7128 write_scalar(
7129 writer,
7130 "c:orientation",
7131 self.orientation.as_str(),
7132 self.orientation_markup.as_ref(),
7133 )?;
7134 }
7135 emit_raw(writer, self.raw_children.at(2))?;
7136 if let Some(value) = self.maximum {
7137 write_scalar(
7138 writer,
7139 "c:max",
7140 &value.to_string(),
7141 self.maximum_markup.as_ref(),
7142 )?;
7143 }
7144 emit_raw(writer, self.raw_children.at(3))?;
7145 if let Some(value) = self.minimum {
7146 write_scalar(
7147 writer,
7148 "c:min",
7149 &value.to_string(),
7150 self.minimum_markup.as_ref(),
7151 )?;
7152 }
7153 emit_raw(writer, self.raw_children.at(4))?;
7154 writer
7155 .write_event(Event::End(BytesEnd::new("c:scaling")))
7156 .map_err(OxmlError::from)?;
7157 Ok(())
7158 }
7159
7160 fn validate(&self) -> Result<()> {
7161 if let Some(value) = self.log_base
7162 && (!value.is_finite() || !(2.0..=1000.0).contains(&value))
7163 {
7164 return Err(ChartError::InvalidValue {
7165 element: "c:logBase".to_owned(),
7166 value: value.to_string(),
7167 });
7168 }
7169 for (element, value) in [("c:max", self.maximum), ("c:min", self.minimum)] {
7170 if let Some(value) = value
7171 && !value.is_finite()
7172 {
7173 return Err(ChartError::InvalidValue {
7174 element: element.to_owned(),
7175 value: value.to_string(),
7176 });
7177 }
7178 }
7179 if let (Some(minimum), Some(maximum)) = (self.minimum, self.maximum)
7180 && minimum > maximum
7181 {
7182 return Err(ChartError::InvalidValue {
7183 element: "c:scaling".to_owned(),
7184 value: format!("minimum {minimum} exceeds maximum {maximum}"),
7185 });
7186 }
7187 Ok(())
7188 }
7189}
7190
7191#[allow(clippy::too_many_arguments)]
7192fn parse_scaling_child(
7193 name: &[u8],
7194 raw: Vec<u8>,
7195 log_base: &mut Option<(f64, ScalarMarkup)>,
7196 orientation: &mut Option<(Orientation, ScalarMarkup)>,
7197 maximum: &mut Option<(f64, ScalarMarkup)>,
7198 minimum: &mut Option<(f64, ScalarMarkup)>,
7199 raw_children: &mut OrderedRawChildren,
7200 boundary: &mut usize,
7201) -> Result<()> {
7202 match name {
7203 b"logBase" => {
7204 set_once(log_base, parse_f64_scalar(&raw, "logBase")?, "c:logBase")?;
7205 *boundary = (*boundary).max(1);
7206 }
7207 b"orientation" => {
7208 let (value, markup) = scalar_value(&raw, "orientation")?;
7209 let value = value
7210 .ok_or_else(|| invalid_attribute("orientation", "val", "<missing>".to_owned()))?;
7211 let parsed = Orientation::parse(&value)
7212 .ok_or_else(|| invalid_attribute("orientation", "val", value))?;
7213 set_once(orientation, (parsed, markup), "c:orientation")?;
7214 *boundary = (*boundary).max(2);
7215 }
7216 b"max" => {
7217 set_once(maximum, parse_f64_scalar(&raw, "max")?, "c:max")?;
7218 *boundary = (*boundary).max(3);
7219 }
7220 b"min" => {
7221 set_once(minimum, parse_f64_scalar(&raw, "min")?, "c:min")?;
7222 *boundary = (*boundary).max(4);
7223 }
7224 _ => raw_children.push(*boundary, raw),
7225 }
7226 Ok(())
7227}
7228
7229impl ChartLines {
7230 fn from_xml(xml: &[u8], local: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7231 let mut reader = Reader::from_reader(xml);
7232 let mut buffer = Vec::new();
7233 loop {
7234 match reader
7235 .read_event_into(&mut buffer)
7236 .map_err(OxmlError::from)?
7237 {
7238 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
7239 let namespaces = typed_rewrite_bindings(&element, inherited)?;
7240 let raw_attributes = capture_attributes(&element)?;
7241 let mut sp_pr = None;
7242 let mut raw_children = OrderedRawChildren::default();
7243 let mut boundary = 0usize;
7244 let mut inner = Vec::new();
7245 loop {
7246 match reader
7247 .read_event_into(&mut inner)
7248 .map_err(OxmlError::from)?
7249 {
7250 Event::Start(child) => {
7251 let name = chart_child_local(&child, &namespaces)?;
7252 let raw = capture_element(&mut reader, &child)?;
7253 if name.as_deref() == Some(b"spPr") {
7254 parse_chart_lines_shape(&raw, &namespaces, &mut sp_pr)?;
7255 boundary = 1;
7256 } else {
7257 raw_children.push(boundary, raw);
7258 }
7259 }
7260 Event::Empty(child) => {
7261 let name = chart_child_local(&child, &namespaces)?;
7262 let raw = capture_empty_element(&child)?;
7263 if name.as_deref() == Some(b"spPr") {
7264 parse_chart_lines_shape(&raw, &namespaces, &mut sp_pr)?;
7265 boundary = 1;
7266 } else {
7267 raw_children.push(boundary, raw);
7268 }
7269 }
7270 event @ (Event::Text(_)
7271 | Event::CData(_)
7272 | Event::Comment(_)
7273 | Event::PI(_)
7274 | Event::GeneralRef(_)) => {
7275 raw_children.push(boundary, capture_event(event)?);
7276 }
7277 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
7278 return Ok(Self {
7279 sp_pr,
7280 raw_attributes,
7281 raw_children: raw_children_in_schema_order(&raw_children, 1),
7282 });
7283 }
7284 Event::Eof => {
7285 return Err(missing_end(&format!(
7286 "c:{}",
7287 String::from_utf8_lossy(local)
7288 )));
7289 }
7290 _ => {}
7291 }
7292 inner.clear();
7293 }
7294 }
7295 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
7296 typed_rewrite_bindings(&element, inherited)?;
7297 return Ok(Self {
7298 sp_pr: None,
7299 raw_attributes: capture_attributes(&element)?,
7300 raw_children: OrderedRawChildren::default(),
7301 });
7302 }
7303 Event::Start(element) | Event::Empty(element) => {
7304 return Err(ChartError::UnexpectedElement(element_name(&element)));
7305 }
7306 Event::Eof => {
7307 return Err(ChartError::MissingElement(format!(
7308 "c:{}",
7309 String::from_utf8_lossy(local)
7310 )));
7311 }
7312 _ => {}
7313 }
7314 buffer.clear();
7315 }
7316 }
7317
7318 fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
7319 let mut start = BytesStart::new(tag);
7320 push_attributes(&mut start, &self.raw_attributes);
7321 if self.sp_pr.is_none() && self.raw_children.is_empty() {
7322 writer
7323 .write_event(Event::Empty(start))
7324 .map_err(OxmlError::from)?;
7325 return Ok(());
7326 }
7327 writer
7328 .write_event(Event::Start(start))
7329 .map_err(OxmlError::from)?;
7330 emit_raw(writer, self.raw_children.at(0))?;
7331 if let Some(properties) = &self.sp_pr {
7332 properties.write_xml_as(writer, "c:spPr")?;
7333 }
7334 emit_raw(writer, self.raw_children.at(1))?;
7335 writer
7336 .write_event(Event::End(BytesEnd::new(tag)))
7337 .map_err(OxmlError::from)?;
7338 Ok(())
7339 }
7340}
7341
7342fn parse_chart_lines_shape(
7343 raw: &[u8],
7344 namespaces: &NamespaceBindings,
7345 slot: &mut Option<CT_ShapeProperties>,
7346) -> Result<()> {
7347 reject_conflicting_prefix_in_xml(raw, b"a", A_NS)?;
7348 let properties = CT_ShapeProperties::from_xml(raw)?;
7349 let mut writer = Writer::new(Vec::new());
7350 properties.write_xml_as(&mut writer, "c:spPr")?;
7351 reject_rewritten_foreign_elements(raw, &writer.into_inner(), namespaces, b"spPr")?;
7352 set_once(slot, properties, "c:spPr")
7353}
7354
7355impl NumberFormat {
7356 pub fn new(format_code: String, source_linked: bool) -> Result<Self> {
7357 let number_format = Self {
7358 format_code,
7359 source_linked,
7360 raw_attributes: Vec::new(),
7361 raw_content: Vec::new(),
7362 };
7363 number_format.validate()?;
7364 Ok(number_format)
7365 }
7366
7367 fn from_xml(xml: &[u8]) -> Result<Self> {
7368 let mut reader = Reader::from_reader(xml);
7369 let mut buffer = Vec::new();
7370 loop {
7371 match reader
7372 .read_event_into(&mut buffer)
7373 .map_err(OxmlError::from)?
7374 {
7375 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
7376 return Self::from_element(&element, Vec::new());
7377 }
7378 Event::Start(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
7379 let mut raw_content = Vec::new();
7380 let mut inner = Vec::new();
7381 loop {
7382 match reader
7383 .read_event_into(&mut inner)
7384 .map_err(OxmlError::from)?
7385 {
7386 Event::End(end)
7387 if matches_local_name(end.name().as_ref(), b"numFmt") =>
7388 {
7389 return Self::from_element(&element, raw_content);
7390 }
7391 event @ (Event::Text(_)
7392 | Event::CData(_)
7393 | Event::Comment(_)
7394 | Event::PI(_)
7395 | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
7396 Event::Eof => return Err(missing_end("c:numFmt")),
7397 _ => return Err(ChartError::UnexpectedElement("c:numFmt".to_owned())),
7398 }
7399 inner.clear();
7400 }
7401 }
7402 Event::Start(element) | Event::Empty(element) => {
7403 return Err(ChartError::UnexpectedElement(element_name(&element)));
7404 }
7405 Event::Eof => return Err(ChartError::MissingElement("c:numFmt".to_owned())),
7406 _ => {}
7407 }
7408 buffer.clear();
7409 }
7410 }
7411
7412 fn from_element(element: &BytesStart<'_>, raw_content: Vec<Vec<u8>>) -> Result<Self> {
7413 reject_conflicting_prefix(element, b"c", C_NS)?;
7414 let format_code = attribute_value(element, b"formatCode")?
7415 .ok_or_else(|| invalid_attribute("numFmt", "formatCode", "<missing>".to_owned()))?;
7416 let source_linked = attribute_value(element, b"sourceLinked")?
7417 .ok_or_else(|| invalid_attribute("numFmt", "sourceLinked", "<missing>".to_owned()))?;
7418 let source_linked = parse_bool_lexical("numFmt", "sourceLinked", &source_linked)?;
7419 let number_format = Self {
7420 format_code,
7421 source_linked,
7422 raw_attributes: capture_attributes_excluding(
7423 element,
7424 &[b"formatCode", b"sourceLinked"],
7425 )?,
7426 raw_content,
7427 };
7428 number_format.validate()?;
7429 Ok(number_format)
7430 }
7431
7432 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7433 self.validate()?;
7434 let mut start = BytesStart::new("c:numFmt");
7435 start.push_attribute(("formatCode", self.format_code.as_str()));
7436 start.push_attribute(("sourceLinked", bool_lexical(self.source_linked)));
7437 push_attributes(&mut start, &self.raw_attributes);
7438 if self.raw_content.is_empty() {
7439 writer
7440 .write_event(Event::Empty(start))
7441 .map_err(OxmlError::from)?;
7442 return Ok(());
7443 }
7444 writer
7445 .write_event(Event::Start(start))
7446 .map_err(OxmlError::from)?;
7447 for raw in &self.raw_content {
7448 writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
7449 }
7450 writer
7451 .write_event(Event::End(BytesEnd::new("c:numFmt")))
7452 .map_err(OxmlError::from)?;
7453 Ok(())
7454 }
7455
7456 pub fn format_value(&self, value: f64) -> Result<String> {
7458 if !value.is_finite() {
7459 return Err(ChartError::InvalidValue {
7460 element: "c:numFmt value".to_owned(),
7461 value: value.to_string(),
7462 });
7463 }
7464 if self.format_code == "General" {
7465 return Ok(value.to_string());
7466 }
7467
7468 let (numeric_code, percentage) = self
7469 .format_code
7470 .strip_suffix('%')
7471 .map(|code| (code, true))
7472 .unwrap_or((&self.format_code, false));
7473 let precision = if numeric_code == "0" {
7474 Some(0)
7475 } else if let Some(decimals) = numeric_code.strip_prefix("0.")
7476 && !decimals.is_empty()
7477 && decimals.bytes().all(|byte| byte == b'0')
7478 {
7479 Some(decimals.len())
7480 } else {
7481 None
7482 };
7483 let Some(precision) = precision else {
7484 return Err(ChartError::InvalidValue {
7485 element: "c:numFmt/@formatCode projection".to_owned(),
7486 value: self.format_code.clone(),
7487 });
7488 };
7489 let projected = if percentage { value * 100.0 } else { value };
7490 if !projected.is_finite() {
7491 return Err(ChartError::InvalidValue {
7492 element: "c:numFmt value".to_owned(),
7493 value: projected.to_string(),
7494 });
7495 }
7496 let mut formatted = format!("{projected:.precision$}");
7497 if percentage {
7498 formatted.push('%');
7499 }
7500 Ok(formatted)
7501 }
7502
7503 fn validate(&self) -> Result<()> {
7504 if self.format_code.is_empty() {
7505 return Err(ChartError::InvalidValue {
7506 element: "c:numFmt/@formatCode".to_owned(),
7507 value: self.format_code.clone(),
7508 });
7509 }
7510 validate_xml_text(&self.format_code, "c:numFmt/@formatCode")
7511 }
7512}
7513
7514impl CT_DLbls {
7515 pub fn from_xml(xml: &[u8]) -> Result<Self> {
7516 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
7517 }
7518
7519 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7520 let mut reader = Reader::from_reader(xml);
7521 let mut buffer = Vec::new();
7522 loop {
7523 match reader
7524 .read_event_into(&mut buffer)
7525 .map_err(OxmlError::from)?
7526 {
7527 Event::Start(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7528 chart_root_prefix(&element)?;
7529 if !element_is_in_namespace(&element, C_NS, inherited)? {
7530 return Err(ChartError::UnexpectedElement(element_name(&element)));
7531 }
7532 return Self::from_element(&mut reader, &element, inherited);
7533 }
7534 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7535 chart_root_prefix(&element)?;
7536 let namespaces = chart_bindings(inherited, &element)?;
7537 if !element_is_in_namespace(&element, C_NS, inherited)? {
7538 return Err(ChartError::UnexpectedElement(element_name(&element)));
7539 }
7540 let (raw_attributes, _) = capture_fixed_root_attributes(
7541 &element,
7542 &["xmlns:c", "xmlns:a", "xmlns:r"],
7543 )?;
7544 return Ok(Self {
7545 raw_attributes,
7546 namespace_declarations: standalone_namespace_declarations(&namespaces)?,
7547 ..Self::default()
7548 });
7549 }
7550 Event::Start(element) | Event::Empty(element) => {
7551 return Err(ChartError::UnexpectedElement(element_name(&element)));
7552 }
7553 Event::Eof => return Err(ChartError::MissingElement("c:dLbls".to_owned())),
7554 _ => {}
7555 }
7556 buffer.clear();
7557 }
7558 }
7559
7560 fn from_element(
7561 reader: &mut Reader<&[u8]>,
7562 start: &BytesStart<'_>,
7563 inherited: &NamespaceBindings,
7564 ) -> Result<Self> {
7565 reject_conflicting_prefix(start, b"a", A_NS)?;
7566 let namespaces = chart_bindings(inherited, start)?;
7567 require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
7568 let (raw_attributes, _) =
7569 capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
7570 let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
7571 let mut state = DataLabelsParseState::new(namespaces);
7572 let mut buffer = Vec::new();
7573 loop {
7574 match reader
7575 .read_event_into(&mut buffer)
7576 .map_err(OxmlError::from)?
7577 {
7578 Event::Start(element) => {
7579 let name = chart_child_local(&element, &state.namespaces)?;
7580 let raw = capture_element(reader, &element)?;
7581 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
7582 }
7583 Event::Empty(element) => {
7584 let name = chart_child_local(&element, &state.namespaces)?;
7585 let raw = capture_empty_element(&element)?;
7586 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
7587 }
7588 event @ (Event::Text(_)
7589 | Event::CData(_)
7590 | Event::Comment(_)
7591 | Event::PI(_)
7592 | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
7593 Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7594 return state.finish(raw_attributes, namespace_declarations);
7595 }
7596 Event::Eof => return Err(missing_end("c:dLbls")),
7597 _ => {}
7598 }
7599 buffer.clear();
7600 }
7601 }
7602
7603 pub fn to_xml(&self) -> Result<Vec<u8>> {
7604 let mut writer = Writer::new(Vec::new());
7605 self.write_xml(&mut writer, true)?;
7606 Ok(writer.into_inner())
7607 }
7608
7609 fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
7610 self.validate()?;
7611 let mut start = BytesStart::new("c:dLbls");
7612 if standalone {
7613 start.push_attribute(("xmlns:c", C_NS));
7614 start.push_attribute(("xmlns:a", A_NS));
7615 start.push_attribute(("xmlns:r", R_NS));
7616 }
7617 push_attributes(&mut start, &self.namespace_declarations);
7618 push_attributes(&mut start, &self.raw_attributes);
7619 writer
7620 .write_event(Event::Start(start))
7621 .map_err(OxmlError::from)?;
7622 emit_raw(writer, self.raw_children.at(0))?;
7623 if let Some(number_format) = &self.number_format {
7624 number_format.write_xml(writer)?;
7625 }
7626 emit_raw(writer, self.raw_children.at(1))?;
7627 if let Some(position) = self.position {
7628 write_scalar(
7629 writer,
7630 "c:dLblPos",
7631 position.as_str(),
7632 self.position_markup.as_ref(),
7633 )?;
7634 }
7635 emit_raw(writer, self.raw_children.at(2))?;
7636 write_optional_bool(
7637 writer,
7638 "c:showLegendKey",
7639 self.show_legend_key,
7640 self.show_legend_key_markup.as_ref(),
7641 )?;
7642 emit_raw(writer, self.raw_children.at(3))?;
7643 write_optional_bool(
7644 writer,
7645 "c:showVal",
7646 self.show_value,
7647 self.show_value_markup.as_ref(),
7648 )?;
7649 emit_raw(writer, self.raw_children.at(4))?;
7650 write_optional_bool(
7651 writer,
7652 "c:showCatName",
7653 self.show_category_name,
7654 self.show_category_name_markup.as_ref(),
7655 )?;
7656 emit_raw(writer, self.raw_children.at(5))?;
7657 write_optional_bool(
7658 writer,
7659 "c:showSerName",
7660 self.show_series_name,
7661 self.show_series_name_markup.as_ref(),
7662 )?;
7663 emit_raw(writer, self.raw_children.at(6))?;
7664 write_optional_bool(
7665 writer,
7666 "c:showPercent",
7667 self.show_percent,
7668 self.show_percent_markup.as_ref(),
7669 )?;
7670 emit_raw(writer, self.raw_children.at(7))?;
7671 write_optional_bool(
7672 writer,
7673 "c:showBubbleSize",
7674 self.show_bubble_size,
7675 self.show_bubble_size_markup.as_ref(),
7676 )?;
7677 emit_raw(writer, self.raw_children.at(8))?;
7678 if let Some(separator) = &self.separator {
7679 write_text(
7680 writer,
7681 "c:separator",
7682 separator,
7683 self.separator_markup
7684 .as_ref()
7685 .unwrap_or(&TextMarkup::default()),
7686 )?;
7687 }
7688 emit_raw(writer, self.raw_children.at(9))?;
7689 writer
7690 .write_event(Event::End(BytesEnd::new("c:dLbls")))
7691 .map_err(OxmlError::from)?;
7692 Ok(())
7693 }
7694
7695 fn validate(&self) -> Result<()> {
7696 if let Some(number_format) = &self.number_format {
7697 number_format.validate()?;
7698 }
7699 if let Some(separator) = &self.separator {
7700 validate_xml_text(separator, "c:separator")?;
7701 }
7702 Ok(())
7703 }
7704
7705 pub fn raw_children(&self) -> &OrderedRawChildren {
7706 &self.raw_children
7707 }
7708}
7709
7710struct DataLabelsParseState {
7711 namespaces: NamespaceBindings,
7712 number_format: Option<NumberFormat>,
7713 position: Option<(DataLabelPosition, ScalarMarkup)>,
7714 show_legend_key: Option<(bool, ScalarMarkup)>,
7715 show_value: Option<(bool, ScalarMarkup)>,
7716 show_category_name: Option<(bool, ScalarMarkup)>,
7717 show_series_name: Option<(bool, ScalarMarkup)>,
7718 show_percent: Option<(bool, ScalarMarkup)>,
7719 show_bubble_size: Option<(bool, ScalarMarkup)>,
7720 separator: Option<(String, TextMarkup)>,
7721 raw_children: OrderedRawChildren,
7722 boundary: usize,
7723}
7724
7725impl DataLabelsParseState {
7726 fn new(namespaces: NamespaceBindings) -> Self {
7727 Self {
7728 namespaces,
7729 number_format: None,
7730 position: None,
7731 show_legend_key: None,
7732 show_value: None,
7733 show_category_name: None,
7734 show_series_name: None,
7735 show_percent: None,
7736 show_bubble_size: None,
7737 separator: None,
7738 raw_children: OrderedRawChildren::default(),
7739 boundary: 0,
7740 }
7741 }
7742
7743 fn capture_event(&mut self, raw: Vec<u8>) {
7744 self.raw_children.push(self.boundary, raw);
7745 }
7746
7747 fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
7748 match name {
7749 b"numFmt" => {
7750 set_once(
7751 &mut self.number_format,
7752 NumberFormat::from_xml(&raw)?,
7753 "c:numFmt",
7754 )?;
7755 self.boundary = self.boundary.max(1);
7756 }
7757 b"dLblPos" => {
7758 let (value, markup) = scalar_value(&raw, "dLblPos")?;
7759 let lexical = value
7760 .ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
7761 let position = DataLabelPosition::parse(&lexical)
7762 .ok_or_else(|| invalid_attribute("dLblPos", "val", lexical))?;
7763 set_once(&mut self.position, (position, markup), "c:dLblPos")?;
7764 self.boundary = self.boundary.max(2);
7765 }
7766 b"showLegendKey" => {
7767 set_once(
7768 &mut self.show_legend_key,
7769 parse_bool_value(&raw, "showLegendKey")?,
7770 "c:showLegendKey",
7771 )?;
7772 self.boundary = self.boundary.max(3);
7773 }
7774 b"showVal" => {
7775 set_once(
7776 &mut self.show_value,
7777 parse_bool_value(&raw, "showVal")?,
7778 "c:showVal",
7779 )?;
7780 self.boundary = self.boundary.max(4);
7781 }
7782 b"showCatName" => {
7783 set_once(
7784 &mut self.show_category_name,
7785 parse_bool_value(&raw, "showCatName")?,
7786 "c:showCatName",
7787 )?;
7788 self.boundary = self.boundary.max(5);
7789 }
7790 b"showSerName" => {
7791 set_once(
7792 &mut self.show_series_name,
7793 parse_bool_value(&raw, "showSerName")?,
7794 "c:showSerName",
7795 )?;
7796 self.boundary = self.boundary.max(6);
7797 }
7798 b"showPercent" => {
7799 set_once(
7800 &mut self.show_percent,
7801 parse_bool_value(&raw, "showPercent")?,
7802 "c:showPercent",
7803 )?;
7804 self.boundary = self.boundary.max(7);
7805 }
7806 b"showBubbleSize" => {
7807 set_once(
7808 &mut self.show_bubble_size,
7809 parse_bool_value(&raw, "showBubbleSize")?,
7810 "c:showBubbleSize",
7811 )?;
7812 self.boundary = self.boundary.max(8);
7813 }
7814 b"separator" => {
7815 set_once(
7816 &mut self.separator,
7817 parse_text_element(&raw, b"separator", &self.namespaces)?,
7818 "c:separator",
7819 )?;
7820 self.boundary = self.boundary.max(9);
7821 }
7822 _ => {
7823 let boundary = data_labels_raw_boundary(name, self.boundary);
7824 self.raw_children.push(boundary, raw);
7825 self.boundary = self.boundary.max(boundary);
7826 }
7827 }
7828 Ok(())
7829 }
7830
7831 fn finish(
7832 self,
7833 raw_attributes: XmlAttributes,
7834 namespace_declarations: XmlAttributes,
7835 ) -> Result<CT_DLbls> {
7836 let (position, position_markup) = optional_markup(self.position);
7837 let (show_legend_key, show_legend_key_markup) = optional_bool_markup(self.show_legend_key);
7838 let (show_value, show_value_markup) = optional_bool_markup(self.show_value);
7839 let (show_category_name, show_category_name_markup) =
7840 optional_bool_markup(self.show_category_name);
7841 let (show_series_name, show_series_name_markup) =
7842 optional_bool_markup(self.show_series_name);
7843 let (show_percent, show_percent_markup) = optional_bool_markup(self.show_percent);
7844 let (show_bubble_size, show_bubble_size_markup) =
7845 optional_bool_markup(self.show_bubble_size);
7846 let (separator, separator_markup) = self
7847 .separator
7848 .map(|(value, markup)| (Some(value), Some(markup)))
7849 .unwrap_or((None, None));
7850 let labels = CT_DLbls {
7851 number_format: self.number_format,
7852 position,
7853 separator,
7854 show_legend_key,
7855 show_value,
7856 show_category_name,
7857 show_series_name,
7858 show_percent,
7859 show_bubble_size,
7860 position_markup,
7861 show_legend_key_markup,
7862 show_value_markup,
7863 show_category_name_markup,
7864 show_series_name_markup,
7865 show_percent_markup,
7866 show_bubble_size_markup,
7867 separator_markup,
7868 raw_attributes,
7869 namespace_declarations,
7870 raw_children: raw_children_in_schema_order(&self.raw_children, 9),
7871 };
7872 labels.validate()?;
7873 Ok(labels)
7874 }
7875}
7876
7877fn data_labels_raw_boundary(name: &[u8], current: usize) -> usize {
7878 match name {
7879 b"dLbl" | b"delete" => 0,
7880 b"spPr" | b"txPr" => 1,
7881 b"showLeaderLines" | b"leaderLines" | b"extLst" => 9,
7882 _ => current,
7883 }
7884}
7885
7886fn optional_bool_markup(value: Option<(bool, ScalarMarkup)>) -> (bool, Option<ScalarMarkup>) {
7887 value
7888 .map(|(value, markup)| (value, Some(markup)))
7889 .unwrap_or((false, None))
7890}
7891
7892fn write_optional_bool(
7893 writer: &mut Writer<Vec<u8>>,
7894 tag: &str,
7895 value: bool,
7896 markup: Option<&ScalarMarkup>,
7897) -> Result<()> {
7898 if value || markup.is_some() {
7899 write_scalar(writer, tag, bool_lexical(value), markup)?;
7900 }
7901 Ok(())
7902}
7903
7904fn default_axis_id_markup(id: AxisId) -> AxisIdMarkup {
7905 AxisIdMarkup {
7906 scalar: ScalarMarkup::default(),
7907 parsed: id,
7908 lexical: id.value().to_string(),
7909 }
7910}
7911
7912fn parse_axis_id_scalar(xml: &[u8], local: &str) -> Result<(AxisId, AxisIdMarkup)> {
7913 let (value, scalar) = scalar_value(xml, local)?;
7914 let lexical = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
7915 let parsed = lexical
7916 .parse::<i64>()
7917 .map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
7918 let id = AxisId::new(parsed).map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
7919 Ok((
7920 id,
7921 AxisIdMarkup {
7922 scalar,
7923 parsed: id,
7924 lexical,
7925 },
7926 ))
7927}
7928
7929fn write_axis_id<W: Write>(
7930 writer: &mut Writer<W>,
7931 tag: &str,
7932 id: AxisId,
7933 markup: &AxisIdMarkup,
7934) -> Result<()> {
7935 AxisId::new(id.value())?;
7936 let normalized;
7937 let value = if id == markup.parsed {
7938 markup.lexical.as_str()
7939 } else {
7940 normalized = id.value().to_string();
7941 normalized.as_str()
7942 };
7943 write_scalar(writer, tag, value, Some(&markup.scalar))
7944}
7945
7946fn parse_f64_scalar(xml: &[u8], local: &str) -> Result<(f64, ScalarMarkup)> {
7947 let (value, markup) = scalar_value(xml, local)?;
7948 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
7949 let parsed = value
7950 .parse::<f64>()
7951 .map_err(|_| invalid_attribute(local, "val", value.clone()))?;
7952 if !parsed.is_finite() {
7953 return Err(invalid_attribute(local, "val", value));
7954 }
7955 Ok((parsed, markup))
7956}
7957
7958fn parse_axis_position(xml: &[u8]) -> Result<(AxisPosition, ScalarMarkup)> {
7959 let (value, markup) = scalar_value(xml, "axPos")?;
7960 let value = value.ok_or_else(|| invalid_attribute("axPos", "val", "<missing>".to_owned()))?;
7961 let parsed =
7962 AxisPosition::parse(&value).ok_or_else(|| invalid_attribute("axPos", "val", value))?;
7963 Ok((parsed, markup))
7964}
7965
7966fn parse_tick_mark(xml: &[u8], local: &str) -> Result<(TickMark, ScalarMarkup)> {
7967 let (value, markup) = scalar_value(xml, local)?;
7968 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
7969 let parsed = TickMark::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
7970 Ok((parsed, markup))
7971}
7972
7973fn parse_tick_label_position(xml: &[u8]) -> Result<(TickLabelPosition, ScalarMarkup)> {
7974 let (value, markup) = scalar_value(xml, "tickLblPos")?;
7975 let value =
7976 value.ok_or_else(|| invalid_attribute("tickLblPos", "val", "<missing>".to_owned()))?;
7977 let parsed = TickLabelPosition::parse(&value)
7978 .ok_or_else(|| invalid_attribute("tickLblPos", "val", value))?;
7979 Ok((parsed, markup))
7980}
7981
7982fn optional_markup<T>(value: Option<(T, ScalarMarkup)>) -> (Option<T>, Option<ScalarMarkup>) {
7983 value
7984 .map(|(value, markup)| (Some(value), Some(markup)))
7985 .unwrap_or((None, None))
7986}
7987
7988fn optional_scalar_markup_eq(left: &Option<ScalarMarkup>, right: &Option<ScalarMarkup>) -> bool {
7989 let empty = ScalarMarkup::default();
7990 left.as_ref().unwrap_or(&empty) == right.as_ref().unwrap_or(&empty)
7991}
7992
7993fn axis_id_markup_eq(left: &AxisIdMarkup, right: &AxisIdMarkup) -> bool {
7994 left.scalar == right.scalar
7995}
7996
7997const fn bool_lexical(value: bool) -> &'static str {
7998 if value { "1" } else { "0" }
7999}
8000
8001fn parse_bool_lexical(element: &str, attribute: &str, value: &str) -> Result<bool> {
8002 match value {
8003 "1" | "true" => Ok(true),
8004 "0" | "false" => Ok(false),
8005 _ => Err(invalid_attribute(element, attribute, value.to_owned())),
8006 }
8007}
8008
8009impl CT_Title {
8010 fn from_xml(xml: &[u8]) -> Result<Self> {
8011 let (raw_attributes, raw_children) = parse_raw_shell(xml, b"title", "c:title")?;
8012 Ok(Self {
8013 raw_attributes,
8014 raw_children,
8015 })
8016 }
8017
8018 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8019 write_raw_shell(writer, "c:title", &self.raw_attributes, &self.raw_children)
8020 }
8021
8022 pub fn raw_children(&self) -> &OrderedRawChildren {
8023 &self.raw_children
8024 }
8025}
8026
8027impl CT_PlotArea {
8028 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
8029 let (raw_attributes, captured_children) = parse_raw_shell(xml, b"plotArea", "c:plotArea")?;
8030 let namespace_bindings = root_chart_bindings(xml, b"plotArea", inherited)?;
8031 let direct_roots: Vec<_> = captured_children
8032 .at(0)
8033 .filter_map(|raw| chart_root_local(raw, &namespace_bindings).transpose())
8034 .collect::<Result<_>>()?;
8035 let plot_roots: Vec<_> = direct_roots
8036 .iter()
8037 .filter(|local| is_plot_container(local))
8038 .cloned()
8039 .collect();
8040 let typed_local = (plot_roots.len() == 1
8041 && matches!(
8042 plot_roots[0].as_slice(),
8043 b"barChart"
8044 | b"lineChart"
8045 | b"pieChart"
8046 | b"doughnutChart"
8047 | b"areaChart"
8048 | b"scatterChart"
8049 | b"radarChart"
8050 ))
8051 .then(|| plot_roots[0].clone());
8052
8053 let Some(typed_local) = typed_local else {
8054 return Ok(Self {
8055 raw_attributes,
8056 raw_children: captured_children,
8057 namespace_bindings,
8058 plots: None,
8059 plot_markup: Vec::new(),
8060 axes: Vec::new(),
8061 });
8062 };
8063
8064 let mut raw_children = OrderedRawChildren::default();
8065 let mut plots = Vec::new();
8066 let mut plot_markup = Vec::new();
8067 let mut axes = Vec::new();
8068 let mut boundary = 0usize;
8069 for raw in captured_children.at(0) {
8070 let local = chart_root_local(raw, &namespace_bindings)?;
8071 if local.as_deref() == Some(typed_local.as_slice()) {
8072 let (plot, markup) = parse_plot(raw, &namespace_bindings)?;
8073 plots.push(plot);
8074 plot_markup.push(markup);
8075 boundary = 1;
8076 } else if local.as_deref().and_then(AxisKind::parse).is_some() {
8077 axes.push(Axis::from_xml_with_namespaces(raw, &namespace_bindings)?);
8078 boundary = axes.len() + 1;
8079 } else {
8080 raw_children.push(boundary, raw.to_vec());
8081 }
8082 }
8083 let plot_area = Self {
8084 raw_attributes,
8085 raw_children,
8086 namespace_bindings,
8087 plots: Some(plots),
8088 plot_markup,
8089 axes,
8090 };
8091 plot_area.validate_typed()?;
8092 Ok(plot_area)
8093 }
8094
8095 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8096 let Some(plots) = &self.plots else {
8097 return write_raw_shell(
8098 writer,
8099 "c:plotArea",
8100 &self.raw_attributes,
8101 &self.raw_children,
8102 );
8103 };
8104 self.validate_typed()?;
8105 let mut start = BytesStart::new("c:plotArea");
8106 push_attributes(&mut start, &self.raw_attributes);
8107 writer
8108 .write_event(Event::Start(start))
8109 .map_err(OxmlError::from)?;
8110 emit_raw(writer, self.raw_children.at(0))?;
8111 for (index, plot) in plots.iter().enumerate() {
8112 write_plot(
8113 writer,
8114 plot,
8115 self.plot_markup
8116 .get(index)
8117 .unwrap_or(&PlotMarkup::default()),
8118 )?;
8119 }
8120 emit_raw(writer, self.raw_children.at(1))?;
8121 for (index, axis) in self.axes.iter().enumerate() {
8122 writer
8123 .get_mut()
8124 .write_all(&axis.to_xml()?)
8125 .map_err(OxmlError::from)?;
8126 emit_raw(writer, self.raw_children.at(index + 2))?;
8127 }
8128 writer
8129 .write_event(Event::End(BytesEnd::new("c:plotArea")))
8130 .map_err(OxmlError::from)?;
8131 Ok(())
8132 }
8133
8134 pub fn raw_children(&self) -> &OrderedRawChildren {
8135 &self.raw_children
8136 }
8137
8138 pub fn new(plots: Vec<Plot>, axes: Vec<Axis>) -> Result<Self> {
8140 let plot_area = Self {
8141 raw_attributes: Vec::new(),
8142 raw_children: OrderedRawChildren::default(),
8143 namespace_bindings: chart_namespace_defaults(),
8144 plot_markup: vec![PlotMarkup::default(); plots.len()],
8145 plots: Some(plots),
8146 axes,
8147 };
8148 plot_area.validate_typed()?;
8149 Ok(plot_area)
8150 }
8151
8152 pub fn plots(&self) -> Result<&[Plot]> {
8154 self.plots
8155 .as_deref()
8156 .ok_or_else(|| ChartError::InvalidValue {
8157 element: "c:plotArea".to_owned(),
8158 value: "unsupported or combination plot area is opaque".to_owned(),
8159 })
8160 }
8161
8162 pub fn plots_mut(&mut self) -> Result<&mut [Plot]> {
8164 self.plots
8165 .as_deref_mut()
8166 .ok_or_else(|| ChartError::InvalidValue {
8167 element: "c:plotArea".to_owned(),
8168 value: "cannot combine typed plots with an opaque plot choice".to_owned(),
8169 })
8170 }
8171
8172 pub fn series(&self) -> Result<Vec<Series>> {
8174 if let Some(plots) = &self.plots {
8175 return Ok(plots
8176 .iter()
8177 .flat_map(|plot| match plot {
8178 Plot::Bar { series, .. }
8179 | Plot::Line { series, .. }
8180 | Plot::Pie { series, .. }
8181 | Plot::Doughnut { series, .. }
8182 | Plot::Area { series, .. }
8183 | Plot::Scatter { series, .. }
8184 | Plot::Radar { series, .. } => series.clone(),
8185 })
8186 .collect());
8187 }
8188 let mut series = Vec::new();
8189 for raw in self.raw_children.at(0) {
8190 parse_plot_series(raw, &self.namespace_bindings, &mut series)?;
8191 }
8192 Ok(series)
8193 }
8194
8195 pub fn axes(&self) -> Result<Vec<Axis>> {
8197 if self.plots.is_some() {
8198 self.validate_typed()?;
8199 return Ok(self.axes.clone());
8200 }
8201 let mut axes = Vec::new();
8202 for raw in self.raw_children.at(0) {
8203 if let Some(axis) = parse_plot_axis(raw, &self.namespace_bindings)? {
8204 axes.push(axis);
8205 }
8206 }
8207 validate_axis_pairs(&axes)?;
8208 Ok(axes)
8209 }
8210
8211 fn validate_typed(&self) -> Result<()> {
8212 let plots = self
8213 .plots
8214 .as_ref()
8215 .ok_or_else(|| ChartError::InvalidValue {
8216 element: "c:plotArea".to_owned(),
8217 value: "opaque plot area has no typed validation view".to_owned(),
8218 })?;
8219 if plots.len() != 1 {
8220 return Err(ChartError::InvalidValue {
8221 element: "c:plotArea".to_owned(),
8222 value: "typed plot area requires exactly one plot family".to_owned(),
8223 });
8224 }
8225 validate_axis_pairs(&self.axes)?;
8226 for plot in plots {
8227 plot.validate()?;
8228 if let Some(axis_ids) = plot.axis_ids() {
8229 for id in axis_ids {
8230 if !self.axes.iter().any(|axis| axis.id == id) {
8231 return Err(ChartError::InvalidValue {
8232 element: "c:axId".to_owned(),
8233 value: format!("plot references missing axis {}", id.value()),
8234 });
8235 }
8236 }
8237 } else if !self.axes.is_empty() {
8238 return Err(ChartError::InvalidValue {
8239 element: "c:plotArea".to_owned(),
8240 value: "pie and doughnut plot areas must not own axes".to_owned(),
8241 });
8242 }
8243 if plot.axis_ids().is_some() && self.axes.len() != 2 {
8244 return Err(ChartError::InvalidValue {
8245 element: "c:plotArea".to_owned(),
8246 value: format!(
8247 "axis-owned plot requires exactly 2 axes, found {}",
8248 self.axes.len()
8249 ),
8250 });
8251 }
8252 }
8253 Ok(())
8254 }
8255}
8256
8257impl Default for CT_PlotArea {
8258 fn default() -> Self {
8259 Self {
8260 raw_attributes: Vec::new(),
8261 raw_children: OrderedRawChildren::default(),
8262 namespace_bindings: chart_namespace_defaults(),
8263 plots: Some(Vec::new()),
8264 plot_markup: Vec::new(),
8265 axes: Vec::new(),
8266 }
8267 }
8268}
8269
8270fn chart_root_local(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Vec<u8>>> {
8271 let mut reader = Reader::from_reader(xml);
8272 let mut buffer = Vec::new();
8273 loop {
8274 match reader
8275 .read_event_into(&mut buffer)
8276 .map_err(OxmlError::from)?
8277 {
8278 Event::Start(element) | Event::Empty(element) => {
8279 return Ok(element_is_in_namespace(&element, C_NS, inherited)?
8280 .then(|| local_name(element.name().as_ref()).to_vec()));
8281 }
8282 Event::Eof => return Ok(None),
8283 _ => {}
8284 }
8285 buffer.clear();
8286 }
8287}
8288
8289fn is_plot_container(local: &[u8]) -> bool {
8290 matches!(
8291 local,
8292 b"areaChart"
8293 | b"area3DChart"
8294 | b"barChart"
8295 | b"bar3DChart"
8296 | b"bubbleChart"
8297 | b"doughnutChart"
8298 | b"lineChart"
8299 | b"line3DChart"
8300 | b"ofPieChart"
8301 | b"pieChart"
8302 | b"pie3DChart"
8303 | b"radarChart"
8304 | b"scatterChart"
8305 | b"stockChart"
8306 | b"surfaceChart"
8307 | b"surface3DChart"
8308 )
8309}
8310
8311fn parse_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8312 let local = chart_root_local(xml, inherited)?
8313 .ok_or_else(|| ChartError::MissingElement("c:barChart or c:lineChart".to_owned()))?;
8314 match local.as_slice() {
8315 b"barChart" => parse_bar_plot(xml, inherited),
8316 b"lineChart" => parse_line_plot(xml, inherited),
8317 b"pieChart" | b"doughnutChart" | b"areaChart" | b"scatterChart" | b"radarChart" => {
8318 parse_remaining_plot(xml, inherited, &local)
8319 }
8320 _ => Err(ChartError::UnexpectedElement(format!(
8321 "c:{}",
8322 String::from_utf8_lossy(&local)
8323 ))),
8324 }
8325}
8326
8327fn parse_remaining_plot(
8328 xml: &[u8],
8329 inherited: &NamespaceBindings,
8330 local: &[u8],
8331) -> Result<(Plot, PlotMarkup)> {
8332 let root = String::from_utf8_lossy(local).into_owned();
8333 let mut reader = Reader::from_reader(xml);
8334 let mut buffer = Vec::new();
8335 loop {
8336 match reader
8337 .read_event_into(&mut buffer)
8338 .map_err(OxmlError::from)?
8339 {
8340 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
8341 let namespaces = typed_rewrite_bindings(&element, inherited)?;
8342 let (mut raw_attributes, namespace_declarations) =
8343 capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8344 raw_attributes.splice(0..0, namespace_declarations);
8345 let mut grouping = None;
8346 let mut style = None;
8347 let mut first_slice_angle = None;
8348 let mut hole_size = None;
8349 let mut series = Vec::new();
8350 let mut data_labels = None;
8351 let mut axis_ids = Vec::new();
8352 let mut raw_children = OrderedRawChildren::default();
8353 let mut boundary = 0usize;
8354 let mut inner = Vec::new();
8355 loop {
8356 match reader
8357 .read_event_into(&mut inner)
8358 .map_err(OxmlError::from)?
8359 {
8360 Event::Start(child) => {
8361 let name = chart_child_local(&child, &namespaces)?;
8362 let raw = capture_element(&mut reader, &child)?;
8363 parse_remaining_plot_child(
8364 local,
8365 name.as_deref().unwrap_or_default(),
8366 raw,
8367 &namespaces,
8368 &mut grouping,
8369 &mut style,
8370 &mut first_slice_angle,
8371 &mut hole_size,
8372 &mut series,
8373 &mut data_labels,
8374 &mut axis_ids,
8375 &mut raw_children,
8376 &mut boundary,
8377 )?;
8378 }
8379 Event::Empty(child) => {
8380 let name = chart_child_local(&child, &namespaces)?;
8381 let raw = capture_empty_element(&child)?;
8382 parse_remaining_plot_child(
8383 local,
8384 name.as_deref().unwrap_or_default(),
8385 raw,
8386 &namespaces,
8387 &mut grouping,
8388 &mut style,
8389 &mut first_slice_angle,
8390 &mut hole_size,
8391 &mut series,
8392 &mut data_labels,
8393 &mut axis_ids,
8394 &mut raw_children,
8395 &mut boundary,
8396 )?;
8397 }
8398 event @ (Event::Text(_)
8399 | Event::CData(_)
8400 | Event::Comment(_)
8401 | Event::PI(_)
8402 | Event::GeneralRef(_)) => {
8403 raw_children.push(boundary, capture_event(event)?);
8404 }
8405 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
8406 let needs_axes =
8407 matches!(local, b"areaChart" | b"scatterChart" | b"radarChart");
8408 if needs_axes && axis_ids.len() != 2 {
8409 return Err(ChartError::InvalidValue {
8410 element: format!("c:{root}/c:axId"),
8411 value: format!("expected 2, found {}", axis_ids.len()),
8412 });
8413 }
8414 if !needs_axes && !axis_ids.is_empty() {
8415 return Err(ChartError::InvalidValue {
8416 element: format!("c:{root}/c:axId"),
8417 value: "axis-free plot contains axis references".to_owned(),
8418 });
8419 }
8420 let parsed_ids = needs_axes.then(|| [axis_ids[0].0, axis_ids[1].0]);
8421 let original_series_keys = series
8422 .iter()
8423 .map(|item| (item.index, item.order))
8424 .collect::<Vec<_>>();
8425 let plot = match local {
8426 b"pieChart" => Plot::Pie {
8427 first_slice_angle: first_slice_angle
8428 .as_ref()
8429 .map_or(0, |item| item.0),
8430 series,
8431 data_labels,
8432 },
8433 b"doughnutChart" => Plot::Doughnut {
8434 first_slice_angle: first_slice_angle
8435 .as_ref()
8436 .map_or(0, |item| item.0),
8437 hole_size: hole_size.as_ref().map_or(50, |item| item.0),
8438 series,
8439 data_labels,
8440 },
8441 b"areaChart" => Plot::Area {
8442 grouping: grouping
8443 .as_ref()
8444 .ok_or_else(|| {
8445 ChartError::MissingElement("c:grouping".to_owned())
8446 })?
8447 .0,
8448 series,
8449 data_labels,
8450 axis_ids: parsed_ids.ok_or_else(|| {
8451 ChartError::MissingElement("c:areaChart/c:axId".to_owned())
8452 })?,
8453 },
8454 b"scatterChart" => {
8455 let lexical = style
8456 .as_ref()
8457 .ok_or_else(|| {
8458 ChartError::MissingElement("c:scatterStyle".to_owned())
8459 })?
8460 .0
8461 .as_str();
8462 Plot::Scatter {
8463 style: ScatterStyle::parse(lexical).ok_or_else(|| {
8464 invalid_attribute(
8465 "scatterStyle",
8466 "val",
8467 lexical.to_owned(),
8468 )
8469 })?,
8470 series,
8471 data_labels,
8472 axis_ids: parsed_ids.ok_or_else(|| {
8473 ChartError::MissingElement(
8474 "c:scatterChart/c:axId".to_owned(),
8475 )
8476 })?,
8477 }
8478 }
8479 b"radarChart" => {
8480 let lexical = style
8481 .as_ref()
8482 .ok_or_else(|| {
8483 ChartError::MissingElement("c:radarStyle".to_owned())
8484 })?
8485 .0
8486 .as_str();
8487 Plot::Radar {
8488 style: RadarStyle::parse(lexical).ok_or_else(|| {
8489 invalid_attribute(
8490 "radarStyle",
8491 "val",
8492 lexical.to_owned(),
8493 )
8494 })?,
8495 series,
8496 data_labels,
8497 axis_ids: parsed_ids.ok_or_else(|| {
8498 ChartError::MissingElement(
8499 "c:radarChart/c:axId".to_owned(),
8500 )
8501 })?,
8502 }
8503 }
8504 _ => {
8505 return Err(ChartError::UnexpectedElement(format!(
8506 "c:{}",
8507 String::from_utf8_lossy(local)
8508 )));
8509 }
8510 };
8511 plot.validate()?;
8512 let final_boundary = boundary.max(original_series_keys.len() + 6);
8513 return Ok((
8514 plot,
8515 PlotMarkup {
8516 raw_attributes,
8517 raw_children: raw_children_in_schema_order(
8518 &raw_children,
8519 final_boundary,
8520 ),
8521 grouping: grouping.map(|item| item.1),
8522 first_slice_angle: first_slice_angle.map(|item| item.1),
8523 hole_size: hole_size.map(|item| item.1),
8524 style: style.map(|item| item.1),
8525 axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8526 original_series_keys,
8527 original_axis_ids: parsed_ids
8528 .map_or_else(Vec::new, |ids| ids.to_vec()),
8529 parsed_remaining: match local {
8530 b"pieChart" => Some("pieChart"),
8531 b"doughnutChart" => Some("doughnutChart"),
8532 b"areaChart" => Some("areaChart"),
8533 b"scatterChart" => Some("scatterChart"),
8534 b"radarChart" => Some("radarChart"),
8535 _ => None,
8536 },
8537 ..PlotMarkup::default()
8538 },
8539 ));
8540 }
8541 Event::Eof => return Err(missing_end(&format!("c:{root}"))),
8542 _ => {}
8543 }
8544 inner.clear();
8545 }
8546 }
8547 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
8548 return Err(ChartError::MissingElement("c:ser".to_owned()));
8549 }
8550 Event::Start(element) | Event::Empty(element) => {
8551 return Err(ChartError::UnexpectedElement(element_name(&element)));
8552 }
8553 Event::Eof => return Err(ChartError::MissingElement(format!("c:{root}"))),
8554 _ => {}
8555 }
8556 buffer.clear();
8557 }
8558}
8559
8560#[allow(clippy::too_many_arguments)]
8561fn parse_remaining_plot_child(
8562 root: &[u8],
8563 name: &[u8],
8564 raw: Vec<u8>,
8565 namespaces: &NamespaceBindings,
8566 grouping: &mut Option<(Grouping, ScalarMarkup)>,
8567 style: &mut Option<(String, ScalarMarkup)>,
8568 first_slice_angle: &mut Option<(u16, ScalarMarkup)>,
8569 hole_size: &mut Option<(u8, ScalarMarkup)>,
8570 series: &mut Vec<Series>,
8571 data_labels: &mut Option<CT_DLbls>,
8572 axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
8573 raw_children: &mut OrderedRawChildren,
8574 boundary: &mut usize,
8575) -> Result<()> {
8576 let property_first = matches!(root, b"areaChart" | b"scatterChart" | b"radarChart");
8577 let base = usize::from(property_first);
8578 match name {
8579 b"grouping" if root == b"areaChart" => {
8580 let (value, markup) = required_scalar(&raw, "grouping")?;
8581 let parsed = Grouping::parse(&value)
8582 .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
8583 set_once(grouping, (parsed, markup), "c:grouping")?;
8584 *boundary = (*boundary).max(1);
8585 }
8586 b"scatterStyle" if root == b"scatterChart" => {
8587 let (value, markup) = required_scalar(&raw, "scatterStyle")?;
8588 if ScatterStyle::parse(&value).is_none() {
8589 return Err(invalid_attribute("scatterStyle", "val", value));
8590 }
8591 set_once(style, (value, markup), "c:scatterStyle")?;
8592 *boundary = (*boundary).max(1);
8593 }
8594 b"radarStyle" if root == b"radarChart" => {
8595 let (value, markup) = required_scalar(&raw, "radarStyle")?;
8596 if RadarStyle::parse(&value).is_none() {
8597 return Err(invalid_attribute("radarStyle", "val", value));
8598 }
8599 set_once(style, (value, markup), "c:radarStyle")?;
8600 *boundary = (*boundary).max(1);
8601 }
8602 b"ser" => {
8603 series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
8604 *boundary = (*boundary).max(base + series.len());
8605 }
8606 b"dLbls" => {
8607 set_once(
8608 data_labels,
8609 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
8610 "c:dLbls",
8611 )?;
8612 *boundary = (*boundary).max(base + series.len() + 1);
8613 }
8614 b"firstSliceAng" if matches!(root, b"pieChart" | b"doughnutChart") => {
8615 let (value, markup) = required_scalar(&raw, "firstSliceAng")?;
8616 let parsed = value
8617 .parse::<u16>()
8618 .map_err(|_| invalid_attribute("firstSliceAng", "val", value.clone()))?;
8619 if parsed > 360 {
8620 return Err(invalid_attribute("firstSliceAng", "val", value));
8621 }
8622 set_once(first_slice_angle, (parsed, markup), "c:firstSliceAng")?;
8623 *boundary = (*boundary).max(series.len() + 2);
8624 }
8625 b"holeSize" if root == b"doughnutChart" => {
8626 let (value, markup) = required_scalar(&raw, "holeSize")?;
8627 let parsed = value
8628 .parse::<u8>()
8629 .map_err(|_| invalid_attribute("holeSize", "val", value.clone()))?;
8630 if !(10..=90).contains(&parsed) {
8631 return Err(invalid_attribute("holeSize", "val", value));
8632 }
8633 set_once(hole_size, (parsed, markup), "c:holeSize")?;
8634 *boundary = (*boundary).max(series.len() + 3);
8635 }
8636 b"axId" if property_first => {
8637 if axis_ids.len() == 2 {
8638 return Err(ChartError::DuplicateElement("c:axId".to_owned()));
8639 }
8640 axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
8641 *boundary = (*boundary).max(base + series.len() + 1 + axis_ids.len());
8642 }
8643 b"axId" => {
8644 return Err(ChartError::InvalidValue {
8645 element: "c:axId".to_owned(),
8646 value: "axis-free plot contains axis reference".to_owned(),
8647 });
8648 }
8649 _ => {
8650 let raw_boundary = match (root, name) {
8651 (_, b"varyColors") => base,
8652 (b"pieChart", b"extLst") => series.len() + 2,
8653 (b"doughnutChart", b"extLst") => series.len() + 3,
8654 (b"areaChart" | b"scatterChart" | b"radarChart", b"dropLines") => {
8655 base + series.len() + 1
8656 }
8657 (b"areaChart" | b"scatterChart" | b"radarChart", b"extLst") => {
8658 base + series.len() + 3
8659 }
8660 _ => *boundary,
8661 };
8662 raw_children.push(raw_boundary, raw);
8663 *boundary = (*boundary).max(raw_boundary);
8664 }
8665 }
8666 Ok(())
8667}
8668
8669fn parse_bar_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8670 let mut reader = Reader::from_reader(xml);
8671 let mut buffer = Vec::new();
8672 loop {
8673 match reader
8674 .read_event_into(&mut buffer)
8675 .map_err(OxmlError::from)?
8676 {
8677 Event::Start(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
8678 let namespaces = typed_rewrite_bindings(&element, inherited)?;
8679 let (mut raw_attributes, namespace_declarations) =
8680 capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8681 raw_attributes.splice(0..0, namespace_declarations);
8682 let mut direction = None;
8683 let mut grouping = None;
8684 let mut gap_width = None;
8685 let mut overlap = None;
8686 let mut series = Vec::new();
8687 let mut data_labels = None;
8688 let mut axis_ids = Vec::new();
8689 let mut raw_children = OrderedRawChildren::default();
8690 let mut boundary = 0usize;
8691 let mut inner = Vec::new();
8692 loop {
8693 match reader
8694 .read_event_into(&mut inner)
8695 .map_err(OxmlError::from)?
8696 {
8697 Event::Start(child) => {
8698 let name = chart_child_local(&child, &namespaces)?;
8699 let raw = capture_element(&mut reader, &child)?;
8700 parse_bar_child(
8701 name.as_deref().unwrap_or_default(),
8702 raw,
8703 &namespaces,
8704 &mut direction,
8705 &mut grouping,
8706 &mut gap_width,
8707 &mut overlap,
8708 &mut series,
8709 &mut data_labels,
8710 &mut axis_ids,
8711 &mut raw_children,
8712 &mut boundary,
8713 )?;
8714 }
8715 Event::Empty(child) => {
8716 let name = chart_child_local(&child, &namespaces)?;
8717 let raw = capture_empty_element(&child)?;
8718 parse_bar_child(
8719 name.as_deref().unwrap_or_default(),
8720 raw,
8721 &namespaces,
8722 &mut direction,
8723 &mut grouping,
8724 &mut gap_width,
8725 &mut overlap,
8726 &mut series,
8727 &mut data_labels,
8728 &mut axis_ids,
8729 &mut raw_children,
8730 &mut boundary,
8731 )?;
8732 }
8733 event @ (Event::Text(_)
8734 | Event::CData(_)
8735 | Event::Comment(_)
8736 | Event::PI(_)
8737 | Event::GeneralRef(_)) => {
8738 raw_children.push(boundary, capture_event(event)?);
8739 }
8740 Event::End(end) if matches_local_name(end.name().as_ref(), b"barChart") => {
8741 let (direction, direction_markup) = direction
8742 .ok_or_else(|| ChartError::MissingElement("c:barDir".to_owned()))?;
8743 let (grouping, grouping_markup) = grouping.ok_or_else(|| {
8744 ChartError::MissingElement("c:grouping".to_owned())
8745 })?;
8746 if axis_ids.len() != 2 {
8747 return Err(ChartError::InvalidValue {
8748 element: "c:barChart/c:axId".to_owned(),
8749 value: format!("expected 2, found {}", axis_ids.len()),
8750 });
8751 }
8752 let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
8753 let original_series_keys: Vec<(u32, u32)> =
8754 series.iter().map(|item| (item.index, item.order)).collect();
8755 let plot = Plot::Bar {
8756 direction,
8757 grouping,
8758 gap_width: gap_width.as_ref().map_or(150, |item| item.0),
8759 overlap: overlap.as_ref().map_or(0, |item| item.0),
8760 series,
8761 data_labels,
8762 axis_ids: parsed_ids,
8763 };
8764 plot.validate()?;
8765 return Ok((
8766 plot,
8767 PlotMarkup {
8768 raw_attributes,
8769 raw_children: raw_children_in_schema_order(
8770 &raw_children,
8771 original_series_keys.len() + 7,
8772 ),
8773 direction: Some(direction_markup),
8774 grouping: Some(grouping_markup),
8775 gap_width: gap_width.map(|item| item.1),
8776 overlap: overlap.map(|item| item.1),
8777 marker: None,
8778 smooth: None,
8779 first_slice_angle: None,
8780 hole_size: None,
8781 style: None,
8782 axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8783 original_series_keys,
8784 original_axis_ids: parsed_ids.to_vec(),
8785 parsed_bar: Some(true),
8786 parsed_remaining: None,
8787 },
8788 ));
8789 }
8790 Event::Eof => return Err(missing_end("c:barChart")),
8791 _ => {}
8792 }
8793 inner.clear();
8794 }
8795 }
8796 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
8797 return Err(ChartError::MissingElement("c:barDir".to_owned()));
8798 }
8799 Event::Start(element) | Event::Empty(element) => {
8800 return Err(ChartError::UnexpectedElement(element_name(&element)));
8801 }
8802 Event::Eof => return Err(ChartError::MissingElement("c:barChart".to_owned())),
8803 _ => {}
8804 }
8805 buffer.clear();
8806 }
8807}
8808
8809#[allow(clippy::too_many_arguments)]
8810fn parse_bar_child(
8811 name: &[u8],
8812 raw: Vec<u8>,
8813 namespaces: &NamespaceBindings,
8814 direction: &mut Option<(BarDirection, ScalarMarkup)>,
8815 grouping: &mut Option<(BarGrouping, ScalarMarkup)>,
8816 gap_width: &mut Option<(u16, ScalarMarkup)>,
8817 overlap: &mut Option<(i8, ScalarMarkup)>,
8818 series: &mut Vec<Series>,
8819 data_labels: &mut Option<CT_DLbls>,
8820 axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
8821 raw_children: &mut OrderedRawChildren,
8822 boundary: &mut usize,
8823) -> Result<()> {
8824 match name {
8825 b"barDir" => {
8826 let (value, markup) = required_scalar(&raw, "barDir")?;
8827 let value = BarDirection::parse(&value)
8828 .ok_or_else(|| invalid_attribute("barDir", "val", value))?;
8829 set_once(direction, (value, markup), "c:barDir")?;
8830 *boundary = (*boundary).max(1);
8831 }
8832 b"grouping" => {
8833 let (value, markup) = required_scalar(&raw, "grouping")?;
8834 let value = BarGrouping::parse(&value)
8835 .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
8836 set_once(grouping, (value, markup), "c:grouping")?;
8837 *boundary = (*boundary).max(2);
8838 }
8839 b"ser" => {
8840 series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
8841 *boundary = (*boundary).max(series.len() + 2);
8842 }
8843 b"dLbls" => {
8844 set_once(
8845 data_labels,
8846 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
8847 "c:dLbls",
8848 )?;
8849 *boundary = (*boundary).max(series.len() + 3);
8850 }
8851 b"gapWidth" => {
8852 let (value, markup) = required_scalar(&raw, "gapWidth")?;
8853 let parsed = value
8854 .parse::<u16>()
8855 .map_err(|_| invalid_attribute("gapWidth", "val", value.clone()))?;
8856 if parsed > 500 {
8857 return Err(invalid_attribute("gapWidth", "val", value));
8858 }
8859 set_once(gap_width, (parsed, markup), "c:gapWidth")?;
8860 *boundary = (*boundary).max(series.len() + 4);
8861 }
8862 b"overlap" => {
8863 let (value, markup) = required_scalar(&raw, "overlap")?;
8864 let parsed = value
8865 .parse::<i8>()
8866 .map_err(|_| invalid_attribute("overlap", "val", value.clone()))?;
8867 if !(-100..=100).contains(&parsed) {
8868 return Err(invalid_attribute("overlap", "val", value));
8869 }
8870 set_once(overlap, (parsed, markup), "c:overlap")?;
8871 *boundary = (*boundary).max(series.len() + 5);
8872 }
8873 b"axId" => {
8874 if axis_ids.len() == 2 {
8875 return Err(ChartError::DuplicateElement("c:axId".to_owned()));
8876 }
8877 axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
8878 *boundary = (*boundary).max(series.len() + 5 + axis_ids.len());
8879 }
8880 _ => raw_children.push(*boundary, raw),
8881 }
8882 Ok(())
8883}
8884
8885fn parse_line_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8886 let mut reader = Reader::from_reader(xml);
8887 let mut buffer = Vec::new();
8888 loop {
8889 match reader
8890 .read_event_into(&mut buffer)
8891 .map_err(OxmlError::from)?
8892 {
8893 Event::Start(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
8894 let namespaces = typed_rewrite_bindings(&element, inherited)?;
8895 let (mut raw_attributes, namespace_declarations) =
8896 capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8897 raw_attributes.splice(0..0, namespace_declarations);
8898 let mut grouping = None;
8899 let mut marker = None;
8900 let mut smooth = None;
8901 let mut series = Vec::new();
8902 let mut data_labels = None;
8903 let mut axis_ids = Vec::new();
8904 let mut raw_children = OrderedRawChildren::default();
8905 let mut boundary = 0usize;
8906 let mut inner = Vec::new();
8907 loop {
8908 match reader
8909 .read_event_into(&mut inner)
8910 .map_err(OxmlError::from)?
8911 {
8912 Event::Start(child) => {
8913 let name = chart_child_local(&child, &namespaces)?;
8914 let raw = capture_element(&mut reader, &child)?;
8915 parse_line_child(
8916 name.as_deref().unwrap_or_default(),
8917 raw,
8918 &namespaces,
8919 &mut grouping,
8920 &mut marker,
8921 &mut smooth,
8922 &mut series,
8923 &mut data_labels,
8924 &mut axis_ids,
8925 &mut raw_children,
8926 &mut boundary,
8927 )?;
8928 }
8929 Event::Empty(child) => {
8930 let name = chart_child_local(&child, &namespaces)?;
8931 let raw = capture_empty_element(&child)?;
8932 parse_line_child(
8933 name.as_deref().unwrap_or_default(),
8934 raw,
8935 &namespaces,
8936 &mut grouping,
8937 &mut marker,
8938 &mut smooth,
8939 &mut series,
8940 &mut data_labels,
8941 &mut axis_ids,
8942 &mut raw_children,
8943 &mut boundary,
8944 )?;
8945 }
8946 event @ (Event::Text(_)
8947 | Event::CData(_)
8948 | Event::Comment(_)
8949 | Event::PI(_)
8950 | Event::GeneralRef(_)) => {
8951 raw_children.push(boundary, capture_event(event)?);
8952 }
8953 Event::End(end)
8954 if matches_local_name(end.name().as_ref(), b"lineChart") =>
8955 {
8956 let (grouping, grouping_markup) = grouping.ok_or_else(|| {
8957 ChartError::MissingElement("c:grouping".to_owned())
8958 })?;
8959 if axis_ids.len() != 2 {
8960 return Err(ChartError::InvalidValue {
8961 element: "c:lineChart/c:axId".to_owned(),
8962 value: format!("expected 2, found {}", axis_ids.len()),
8963 });
8964 }
8965 let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
8966 let original_series_keys: Vec<(u32, u32)> =
8967 series.iter().map(|item| (item.index, item.order)).collect();
8968 let plot = Plot::Line {
8969 grouping,
8970 marker: marker.as_ref().is_some_and(|item| item.0),
8971 smooth: smooth.as_ref().is_some_and(|item| item.0),
8972 series,
8973 data_labels,
8974 axis_ids: parsed_ids,
8975 };
8976 plot.validate()?;
8977 return Ok((
8978 plot,
8979 PlotMarkup {
8980 raw_attributes,
8981 raw_children: raw_children_in_schema_order(
8982 &raw_children,
8983 original_series_keys.len() + 6,
8984 ),
8985 direction: None,
8986 grouping: Some(grouping_markup),
8987 gap_width: None,
8988 overlap: None,
8989 marker: marker.map(|item| item.1),
8990 smooth: smooth.map(|item| item.1),
8991 first_slice_angle: None,
8992 hole_size: None,
8993 style: None,
8994 axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8995 original_series_keys,
8996 original_axis_ids: parsed_ids.to_vec(),
8997 parsed_bar: Some(false),
8998 parsed_remaining: None,
8999 },
9000 ));
9001 }
9002 Event::Eof => return Err(missing_end("c:lineChart")),
9003 _ => {}
9004 }
9005 inner.clear();
9006 }
9007 }
9008 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
9009 return Err(ChartError::MissingElement("c:grouping".to_owned()));
9010 }
9011 Event::Start(element) | Event::Empty(element) => {
9012 return Err(ChartError::UnexpectedElement(element_name(&element)));
9013 }
9014 Event::Eof => return Err(ChartError::MissingElement("c:lineChart".to_owned())),
9015 _ => {}
9016 }
9017 buffer.clear();
9018 }
9019}
9020
9021#[allow(clippy::too_many_arguments)]
9022fn parse_line_child(
9023 name: &[u8],
9024 raw: Vec<u8>,
9025 namespaces: &NamespaceBindings,
9026 grouping: &mut Option<(Grouping, ScalarMarkup)>,
9027 marker: &mut Option<(bool, ScalarMarkup)>,
9028 smooth: &mut Option<(bool, ScalarMarkup)>,
9029 series: &mut Vec<Series>,
9030 data_labels: &mut Option<CT_DLbls>,
9031 axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
9032 raw_children: &mut OrderedRawChildren,
9033 boundary: &mut usize,
9034) -> Result<()> {
9035 match name {
9036 b"grouping" => {
9037 let (value, markup) = required_scalar(&raw, "grouping")?;
9038 let value = Grouping::parse(&value)
9039 .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
9040 set_once(grouping, (value, markup), "c:grouping")?;
9041 *boundary = (*boundary).max(1);
9042 }
9043 b"ser" => {
9044 series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
9045 *boundary = (*boundary).max(series.len() + 1);
9046 }
9047 b"dLbls" => {
9048 set_once(
9049 data_labels,
9050 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
9051 "c:dLbls",
9052 )?;
9053 *boundary = (*boundary).max(series.len() + 2);
9054 }
9055 b"marker" => {
9056 set_once(marker, parse_bool_value(&raw, "marker")?, "c:marker")?;
9057 *boundary = (*boundary).max(series.len() + 3);
9058 }
9059 b"smooth" => {
9060 set_once(smooth, parse_bool_value(&raw, "smooth")?, "c:smooth")?;
9061 *boundary = (*boundary).max(series.len() + 4);
9062 }
9063 b"axId" => {
9064 if axis_ids.len() == 2 {
9065 return Err(ChartError::DuplicateElement("c:axId".to_owned()));
9066 }
9067 axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
9068 *boundary = (*boundary).max(series.len() + 4 + axis_ids.len());
9069 }
9070 _ => raw_children.push(*boundary, raw),
9071 }
9072 Ok(())
9073}
9074
9075fn required_scalar(xml: &[u8], local: &str) -> Result<(String, ScalarMarkup)> {
9076 let (value, markup) = scalar_value(xml, local)?;
9077 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
9078 Ok((value, markup))
9079}
9080
9081fn write_plot<W: Write>(writer: &mut Writer<W>, plot: &Plot, markup: &PlotMarkup) -> Result<()> {
9082 plot.validate()?;
9083 if let Some(parsed_bar) = markup.parsed_bar
9084 && parsed_bar != matches!(plot, Plot::Bar { .. })
9085 {
9086 return Err(ChartError::InvalidValue {
9087 element: "c:plotArea".to_owned(),
9088 value: "a parsed plot family cannot be replaced while preserved payload remains"
9089 .to_owned(),
9090 });
9091 }
9092 let current_remaining = match plot {
9093 Plot::Pie { .. } => Some("pieChart"),
9094 Plot::Doughnut { .. } => Some("doughnutChart"),
9095 Plot::Area { .. } => Some("areaChart"),
9096 Plot::Scatter { .. } => Some("scatterChart"),
9097 Plot::Radar { .. } => Some("radarChart"),
9098 Plot::Bar { .. } | Plot::Line { .. } => None,
9099 };
9100 if let Some(parsed) = markup.parsed_remaining
9101 && current_remaining != Some(parsed)
9102 {
9103 return Err(ChartError::InvalidValue {
9104 element: "c:plotArea".to_owned(),
9105 value: "a parsed plot family cannot be replaced while preserved payload remains"
9106 .to_owned(),
9107 });
9108 }
9109 match plot {
9110 Plot::Bar {
9111 direction,
9112 grouping,
9113 gap_width,
9114 overlap,
9115 series,
9116 data_labels,
9117 axis_ids,
9118 } => {
9119 let mut start = BytesStart::new("c:barChart");
9120 push_attributes(&mut start, &markup.raw_attributes);
9121 writer
9122 .write_event(Event::Start(start))
9123 .map_err(OxmlError::from)?;
9124 emit_raw(writer, markup.raw_children.at(0))?;
9125 write_scalar(
9126 writer,
9127 "c:barDir",
9128 direction.as_str(),
9129 markup.direction.as_ref(),
9130 )?;
9131 emit_raw(writer, markup.raw_children.at(1))?;
9132 write_scalar(
9133 writer,
9134 "c:grouping",
9135 grouping.as_str(),
9136 markup.grouping.as_ref(),
9137 )?;
9138 let original_to_current =
9139 series_original_to_current(&markup.original_series_keys, series);
9140 emit_repeated_raw(
9141 writer,
9142 &markup.raw_children,
9143 2,
9144 &original_to_current,
9145 series.len(),
9146 0,
9147 )?;
9148 for (index, item) in series.iter().enumerate() {
9149 writer
9150 .get_mut()
9151 .write_all(&item.to_xml()?)
9152 .map_err(OxmlError::from)?;
9153 emit_repeated_raw(
9154 writer,
9155 &markup.raw_children,
9156 2,
9157 &original_to_current,
9158 series.len(),
9159 index + 1,
9160 )?;
9161 }
9162 if let Some(labels) = data_labels {
9163 writer
9164 .get_mut()
9165 .write_all(&labels.to_xml()?)
9166 .map_err(OxmlError::from)?;
9167 }
9168 let trailing = markup.original_series_keys.len();
9169 emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9170 if *gap_width != 150 || markup.gap_width.is_some() {
9171 write_scalar(
9172 writer,
9173 "c:gapWidth",
9174 &gap_width.to_string(),
9175 markup.gap_width.as_ref(),
9176 )?;
9177 }
9178 emit_raw(writer, markup.raw_children.at(trailing + 4))?;
9179 if *overlap != 0 || markup.overlap.is_some() {
9180 write_scalar(
9181 writer,
9182 "c:overlap",
9183 &overlap.to_string(),
9184 markup.overlap.as_ref(),
9185 )?;
9186 }
9187 let axis_original_to_current =
9188 axis_original_to_current(&markup.original_axis_ids, axis_ids);
9189 let axis_current_to_original =
9190 invert_original_to_current(&axis_original_to_current, axis_ids.len());
9191 emit_repeated_raw(
9192 writer,
9193 &markup.raw_children,
9194 trailing + 5,
9195 &axis_original_to_current,
9196 axis_ids.len(),
9197 0,
9198 )?;
9199 for (index, id) in axis_ids.iter().enumerate() {
9200 let default_markup = default_axis_id_markup(*id);
9201 let original_markup = axis_current_to_original[index]
9202 .and_then(|original| markup.axis_ids.get(original));
9203 write_axis_id(
9204 writer,
9205 "c:axId",
9206 *id,
9207 original_markup.unwrap_or(&default_markup),
9208 )?;
9209 emit_repeated_raw(
9210 writer,
9211 &markup.raw_children,
9212 trailing + 5,
9213 &axis_original_to_current,
9214 axis_ids.len(),
9215 index + 1,
9216 )?;
9217 }
9218 writer
9219 .write_event(Event::End(BytesEnd::new("c:barChart")))
9220 .map_err(OxmlError::from)?;
9221 }
9222 Plot::Line {
9223 grouping,
9224 marker,
9225 smooth,
9226 series,
9227 data_labels,
9228 axis_ids,
9229 } => {
9230 let mut start = BytesStart::new("c:lineChart");
9231 push_attributes(&mut start, &markup.raw_attributes);
9232 writer
9233 .write_event(Event::Start(start))
9234 .map_err(OxmlError::from)?;
9235 emit_raw(writer, markup.raw_children.at(0))?;
9236 write_scalar(
9237 writer,
9238 "c:grouping",
9239 grouping.as_str(),
9240 markup.grouping.as_ref(),
9241 )?;
9242 let original_to_current =
9243 series_original_to_current(&markup.original_series_keys, series);
9244 emit_repeated_raw(
9245 writer,
9246 &markup.raw_children,
9247 1,
9248 &original_to_current,
9249 series.len(),
9250 0,
9251 )?;
9252 for (index, item) in series.iter().enumerate() {
9253 writer
9254 .get_mut()
9255 .write_all(&item.to_xml()?)
9256 .map_err(OxmlError::from)?;
9257 emit_repeated_raw(
9258 writer,
9259 &markup.raw_children,
9260 1,
9261 &original_to_current,
9262 series.len(),
9263 index + 1,
9264 )?;
9265 }
9266 if let Some(labels) = data_labels {
9267 writer
9268 .get_mut()
9269 .write_all(&labels.to_xml()?)
9270 .map_err(OxmlError::from)?;
9271 }
9272 let trailing = markup.original_series_keys.len();
9273 emit_raw(writer, markup.raw_children.at(trailing + 2))?;
9274 if *marker || markup.marker.is_some() {
9275 write_scalar(
9276 writer,
9277 "c:marker",
9278 bool_lexical(*marker),
9279 markup.marker.as_ref(),
9280 )?;
9281 }
9282 emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9283 if *smooth || markup.smooth.is_some() {
9284 write_scalar(
9285 writer,
9286 "c:smooth",
9287 bool_lexical(*smooth),
9288 markup.smooth.as_ref(),
9289 )?;
9290 }
9291 let axis_original_to_current =
9292 axis_original_to_current(&markup.original_axis_ids, axis_ids);
9293 let axis_current_to_original =
9294 invert_original_to_current(&axis_original_to_current, axis_ids.len());
9295 emit_repeated_raw(
9296 writer,
9297 &markup.raw_children,
9298 trailing + 4,
9299 &axis_original_to_current,
9300 axis_ids.len(),
9301 0,
9302 )?;
9303 for (index, id) in axis_ids.iter().enumerate() {
9304 let default_markup = default_axis_id_markup(*id);
9305 let original_markup = axis_current_to_original[index]
9306 .and_then(|original| markup.axis_ids.get(original));
9307 write_axis_id(
9308 writer,
9309 "c:axId",
9310 *id,
9311 original_markup.unwrap_or(&default_markup),
9312 )?;
9313 emit_repeated_raw(
9314 writer,
9315 &markup.raw_children,
9316 trailing + 4,
9317 &axis_original_to_current,
9318 axis_ids.len(),
9319 index + 1,
9320 )?;
9321 }
9322 writer
9323 .write_event(Event::End(BytesEnd::new("c:lineChart")))
9324 .map_err(OxmlError::from)?;
9325 }
9326 remaining => {
9327 let (
9328 root,
9329 property,
9330 series,
9331 data_labels,
9332 axis_ids,
9333 first_slice_angle,
9334 hole_size,
9335 scatter,
9336 ) = match remaining {
9337 Plot::Pie {
9338 first_slice_angle,
9339 series,
9340 data_labels,
9341 } => (
9342 "pieChart",
9343 None,
9344 series,
9345 data_labels,
9346 None,
9347 Some(*first_slice_angle),
9348 None,
9349 false,
9350 ),
9351 Plot::Doughnut {
9352 first_slice_angle,
9353 hole_size,
9354 series,
9355 data_labels,
9356 } => (
9357 "doughnutChart",
9358 None,
9359 series,
9360 data_labels,
9361 None,
9362 Some(*first_slice_angle),
9363 Some(*hole_size),
9364 false,
9365 ),
9366 Plot::Area {
9367 grouping,
9368 series,
9369 data_labels,
9370 axis_ids,
9371 } => (
9372 "areaChart",
9373 Some(("c:grouping", grouping.as_str(), markup.grouping.as_ref())),
9374 series,
9375 data_labels,
9376 Some(axis_ids),
9377 None,
9378 None,
9379 false,
9380 ),
9381 Plot::Scatter {
9382 style,
9383 series,
9384 data_labels,
9385 axis_ids,
9386 } => (
9387 "scatterChart",
9388 Some(("c:scatterStyle", style.as_str(), markup.style.as_ref())),
9389 series,
9390 data_labels,
9391 Some(axis_ids),
9392 None,
9393 None,
9394 true,
9395 ),
9396 Plot::Radar {
9397 style,
9398 series,
9399 data_labels,
9400 axis_ids,
9401 } => (
9402 "radarChart",
9403 Some(("c:radarStyle", style.as_str(), markup.style.as_ref())),
9404 series,
9405 data_labels,
9406 Some(axis_ids),
9407 None,
9408 None,
9409 false,
9410 ),
9411 Plot::Bar { .. } | Plot::Line { .. } => unreachable!("handled above"),
9412 };
9413 let tag = format!("c:{root}");
9414 let mut start = BytesStart::new(&tag);
9415 push_attributes(&mut start, &markup.raw_attributes);
9416 writer
9417 .write_event(Event::Start(start))
9418 .map_err(OxmlError::from)?;
9419 let base = usize::from(property.is_some());
9420 if let Some((name, value, scalar_markup)) = property {
9421 emit_raw(writer, markup.raw_children.at(0))?;
9422 write_scalar(writer, name, value, scalar_markup)?;
9423 }
9424 let original_to_current =
9425 series_original_to_current(&markup.original_series_keys, series);
9426 emit_repeated_raw(
9427 writer,
9428 &markup.raw_children,
9429 base,
9430 &original_to_current,
9431 series.len(),
9432 0,
9433 )?;
9434 for (index, item) in series.iter().enumerate() {
9435 writer
9436 .get_mut()
9437 .write_all(&item.to_xml_for_plot(scatter)?)
9438 .map_err(OxmlError::from)?;
9439 emit_repeated_raw(
9440 writer,
9441 &markup.raw_children,
9442 base,
9443 &original_to_current,
9444 series.len(),
9445 index + 1,
9446 )?;
9447 }
9448 if let Some(labels) = data_labels {
9449 writer
9450 .get_mut()
9451 .write_all(&labels.to_xml()?)
9452 .map_err(OxmlError::from)?;
9453 }
9454 let trailing = base + markup.original_series_keys.len();
9455 if let Some(axis_ids) = axis_ids {
9456 let axis_original_to_current =
9457 axis_original_to_current(&markup.original_axis_ids, axis_ids);
9458 let axis_current_to_original =
9459 invert_original_to_current(&axis_original_to_current, axis_ids.len());
9460 emit_repeated_raw(
9461 writer,
9462 &markup.raw_children,
9463 trailing + 1,
9464 &axis_original_to_current,
9465 axis_ids.len(),
9466 0,
9467 )?;
9468 for (index, id) in axis_ids.iter().enumerate() {
9469 let default_markup = default_axis_id_markup(*id);
9470 let original_markup = axis_current_to_original[index]
9471 .and_then(|original| markup.axis_ids.get(original));
9472 write_axis_id(
9473 writer,
9474 "c:axId",
9475 *id,
9476 original_markup.unwrap_or(&default_markup),
9477 )?;
9478 emit_repeated_raw(
9479 writer,
9480 &markup.raw_children,
9481 trailing + 1,
9482 &axis_original_to_current,
9483 axis_ids.len(),
9484 index + 1,
9485 )?;
9486 }
9487 } else {
9488 emit_raw(writer, markup.raw_children.at(trailing + 1))?;
9489 if let Some(angle) = first_slice_angle {
9490 if angle != 0 || markup.first_slice_angle.is_some() {
9491 write_scalar(
9492 writer,
9493 "c:firstSliceAng",
9494 &angle.to_string(),
9495 markup.first_slice_angle.as_ref(),
9496 )?;
9497 }
9498 emit_raw(writer, markup.raw_children.at(trailing + 2))?;
9499 }
9500 if let Some(size) = hole_size {
9501 if size != 50 || markup.hole_size.is_some() {
9502 write_scalar(
9503 writer,
9504 "c:holeSize",
9505 &size.to_string(),
9506 markup.hole_size.as_ref(),
9507 )?;
9508 }
9509 emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9510 }
9511 }
9512 writer
9513 .write_event(Event::End(BytesEnd::new(&tag)))
9514 .map_err(OxmlError::from)?;
9515 }
9516 }
9517 Ok(())
9518}
9519
9520fn series_original_to_current(original: &[(u32, u32)], current: &[Series]) -> Vec<Option<usize>> {
9521 let mut used = vec![false; current.len()];
9522 let mut matches = vec![None; original.len()];
9523 for original_index in 0..original.len().min(current.len()) {
9524 if original[original_index]
9525 == (current[original_index].index, current[original_index].order)
9526 {
9527 matches[original_index] = Some(original_index);
9528 used[original_index] = true;
9529 }
9530 }
9531 for (original_index, key) in original.iter().enumerate() {
9532 if matches[original_index].is_some() {
9533 continue;
9534 }
9535 if let Some(current_index) = current
9536 .iter()
9537 .enumerate()
9538 .position(|(index, item)| !used[index] && (item.index, item.order) == *key)
9539 {
9540 matches[original_index] = Some(current_index);
9541 used[current_index] = true;
9542 }
9543 }
9544 match_unidentified_by_position(&mut matches, &mut used);
9545 matches
9546}
9547
9548fn axis_original_to_current(original: &[AxisId], current: &[AxisId]) -> Vec<Option<usize>> {
9549 let mut used = vec![false; current.len()];
9550 let mut matches = vec![None; original.len()];
9551 for (original_index, id) in original.iter().enumerate() {
9552 if let Some(current_index) = current
9553 .iter()
9554 .enumerate()
9555 .position(|(index, current_id)| !used[index] && current_id == id)
9556 {
9557 matches[original_index] = Some(current_index);
9558 used[current_index] = true;
9559 }
9560 }
9561 match_unidentified_by_position(&mut matches, &mut used);
9562 matches
9563}
9564
9565fn match_unidentified_by_position(matches: &mut [Option<usize>], used: &mut [bool]) {
9566 for original_index in 0..matches.len() {
9567 if matches[original_index].is_none() && original_index < used.len() && !used[original_index]
9568 {
9569 matches[original_index] = Some(original_index);
9570 used[original_index] = true;
9571 }
9572 }
9573 for matched in matches.iter_mut().filter(|matched| matched.is_none()) {
9574 if let Some(current_index) = used.iter().position(|used| !used) {
9575 *matched = Some(current_index);
9576 used[current_index] = true;
9577 }
9578 }
9579}
9580
9581fn invert_original_to_current(
9582 original_to_current: &[Option<usize>],
9583 current_len: usize,
9584) -> Vec<Option<usize>> {
9585 let mut current_to_original = vec![None; current_len];
9586 for (original, current) in original_to_current.iter().enumerate() {
9587 if let Some(current) = current {
9588 current_to_original[*current] = Some(original);
9589 }
9590 }
9591 current_to_original
9592}
9593
9594fn emit_repeated_raw<W: Write>(
9595 writer: &mut Writer<W>,
9596 raw_children: &OrderedRawChildren,
9597 offset: usize,
9598 original_to_current: &[Option<usize>],
9599 current_len: usize,
9600 current_boundary: usize,
9601) -> Result<()> {
9602 if current_boundary == 0 {
9603 emit_raw(writer, raw_children.at(offset))?;
9604 }
9605 for original_boundary in 1..=original_to_current.len() {
9606 let effective = original_to_current
9607 .iter()
9608 .skip(original_boundary)
9609 .flatten()
9610 .copied()
9611 .next()
9612 .unwrap_or(current_len);
9613 if effective == current_boundary {
9614 emit_raw(writer, raw_children.at(offset + original_boundary))?;
9615 }
9616 }
9617 Ok(())
9618}
9619
9620fn parse_plot_axis(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Axis>> {
9621 let mut reader = Reader::from_reader(xml);
9622 let mut buffer = Vec::new();
9623 loop {
9624 match reader
9625 .read_event_into(&mut buffer)
9626 .map_err(OxmlError::from)?
9627 {
9628 Event::Start(element) | Event::Empty(element) => {
9629 if !element_is_in_namespace(&element, C_NS, inherited)? {
9630 return Ok(None);
9631 }
9632 let name = element.name();
9633 if AxisKind::parse(local_name(name.as_ref())).is_none() {
9634 return Ok(None);
9635 }
9636 return Axis::from_xml_with_namespaces(xml, inherited).map(Some);
9637 }
9638 Event::Eof => return Ok(None),
9639 _ => {}
9640 }
9641 buffer.clear();
9642 }
9643}
9644
9645fn validate_axis_pairs(axes: &[Axis]) -> Result<()> {
9646 for (index, axis) in axes.iter().enumerate() {
9647 if axes[..index].iter().any(|other| other.id == axis.id) {
9648 return Err(ChartError::DuplicateElement(format!(
9649 "c:axId {}",
9650 axis.id.value()
9651 )));
9652 }
9653 }
9654 for axis in axes {
9655 if axis.id == axis.cross_axis {
9656 return Err(ChartError::InvalidValue {
9657 element: "c:crossAx".to_owned(),
9658 value: format!("axis {} crosses itself", axis.id.value()),
9659 });
9660 }
9661 let crossed = axes
9662 .iter()
9663 .find(|candidate| candidate.id == axis.cross_axis)
9664 .ok_or_else(|| ChartError::InvalidValue {
9665 element: "c:crossAx".to_owned(),
9666 value: format!(
9667 "axis {} references missing axis {}",
9668 axis.id.value(),
9669 axis.cross_axis.value()
9670 ),
9671 })?;
9672 if crossed.cross_axis != axis.id {
9673 return Err(ChartError::InvalidValue {
9674 element: "c:crossAx".to_owned(),
9675 value: format!(
9676 "axis {} references {}, which references {}",
9677 axis.id.value(),
9678 crossed.id.value(),
9679 crossed.cross_axis.value()
9680 ),
9681 });
9682 }
9683 }
9684 Ok(())
9685}
9686
9687fn parse_plot_series(
9688 xml: &[u8],
9689 inherited: &NamespaceBindings,
9690 series: &mut Vec<Series>,
9691) -> Result<()> {
9692 let mut reader = Reader::from_reader(xml);
9693 let mut buffer = Vec::new();
9694 loop {
9695 match reader
9696 .read_event_into(&mut buffer)
9697 .map_err(OxmlError::from)?
9698 {
9699 Event::Start(element) => {
9700 if !element_is_in_namespace(&element, C_NS, inherited)? {
9701 return Ok(());
9702 }
9703 let local = local_name(element.name().as_ref()).to_vec();
9704 if !is_supported_series_plot(&local) {
9705 return Ok(());
9706 }
9707 let namespaces = chart_bindings(inherited, &element)?;
9708 let mut inner = Vec::new();
9709 loop {
9710 match reader
9711 .read_event_into(&mut inner)
9712 .map_err(OxmlError::from)?
9713 {
9714 Event::Start(child) => {
9715 let name = chart_child_local(&child, &namespaces)?;
9716 let raw = capture_element(&mut reader, &child)?;
9717 if name.as_deref() == Some(b"ser") {
9718 series.push(Series::from_xml_with_namespaces(&raw, &namespaces)?);
9719 }
9720 }
9721 Event::Empty(child) => {
9722 let name = chart_child_local(&child, &namespaces)?;
9723 if name.as_deref() == Some(b"ser") {
9724 return Err(ChartError::MissingElement("c:ser/c:idx".to_owned()));
9725 }
9726 }
9727 Event::End(end) if matches_local_name(end.name().as_ref(), &local) => {
9728 return Ok(());
9729 }
9730 Event::Eof => {
9731 return Err(missing_end(&format!(
9732 "c:{}",
9733 String::from_utf8_lossy(&local)
9734 )));
9735 }
9736 _ => {}
9737 }
9738 inner.clear();
9739 }
9740 }
9741 Event::Empty(_) | Event::Eof => return Ok(()),
9742 _ => {}
9743 }
9744 buffer.clear();
9745 }
9746}
9747
9748fn is_supported_series_plot(local: &[u8]) -> bool {
9749 matches!(
9750 local,
9751 b"areaChart"
9752 | b"area3DChart"
9753 | b"barChart"
9754 | b"bar3DChart"
9755 | b"doughnutChart"
9756 | b"lineChart"
9757 | b"line3DChart"
9758 | b"ofPieChart"
9759 | b"pieChart"
9760 | b"pie3DChart"
9761 | b"radarChart"
9762 | b"stockChart"
9763 | b"surfaceChart"
9764 | b"surface3DChart"
9765 )
9766}
9767
9768impl CT_Legend {
9769 fn from_xml(xml: &[u8]) -> Result<Self> {
9770 let (raw_attributes, raw_children) = parse_raw_shell(xml, b"legend", "c:legend")?;
9771 Ok(Self {
9772 raw_attributes,
9773 raw_children,
9774 })
9775 }
9776
9777 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
9778 write_raw_shell(writer, "c:legend", &self.raw_attributes, &self.raw_children)
9779 }
9780
9781 pub fn raw_children(&self) -> &OrderedRawChildren {
9782 &self.raw_children
9783 }
9784}
9785
9786fn parse_raw_shell(
9787 xml: &[u8],
9788 local: &[u8],
9789 tag: &str,
9790) -> Result<(XmlAttributes, OrderedRawChildren)> {
9791 let mut reader = Reader::from_reader(xml);
9792 let mut buffer = Vec::new();
9793 loop {
9794 match reader
9795 .read_event_into(&mut buffer)
9796 .map_err(OxmlError::from)?
9797 {
9798 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
9799 reject_conflicting_prefix(&element, b"c", C_NS)?;
9800 let raw_attributes = capture_attributes(&element)?;
9801 let mut raw_children = OrderedRawChildren::default();
9802 let mut child_buffer = Vec::new();
9803 loop {
9804 match reader
9805 .read_event_into(&mut child_buffer)
9806 .map_err(OxmlError::from)?
9807 {
9808 Event::Start(child) => {
9809 raw_children.push(0, capture_element(&mut reader, &child)?)
9810 }
9811 Event::Empty(child) => raw_children.push(0, capture_empty_element(&child)?),
9812 event @ (Event::Text(_)
9813 | Event::CData(_)
9814 | Event::Comment(_)
9815 | Event::PI(_)
9816 | Event::GeneralRef(_)) => {
9817 raw_children.push(0, capture_event(event)?);
9818 }
9819 Event::End(end) if matches_local_name(end.name().as_ref(), local) => break,
9820 Event::Eof => return Err(missing_end(tag)),
9821 _ => {}
9822 }
9823 child_buffer.clear();
9824 }
9825 return Ok((raw_attributes, raw_children));
9826 }
9827 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
9828 reject_conflicting_prefix(&element, b"c", C_NS)?;
9829 return Ok((capture_attributes(&element)?, OrderedRawChildren::default()));
9830 }
9831 Event::Start(element) | Event::Empty(element) => {
9832 return Err(ChartError::UnexpectedElement(element_name(&element)));
9833 }
9834 Event::Eof => return Err(ChartError::MissingElement(tag.to_owned())),
9835 _ => {}
9836 }
9837 buffer.clear();
9838 }
9839}
9840
9841fn write_raw_shell<W: Write>(
9842 writer: &mut Writer<W>,
9843 tag: &str,
9844 raw_attributes: &XmlAttributes,
9845 raw_children: &OrderedRawChildren,
9846) -> Result<()> {
9847 let mut start = BytesStart::new(tag);
9848 push_attributes(&mut start, raw_attributes);
9849 if raw_children.is_empty() {
9850 writer
9851 .write_event(Event::Empty(start))
9852 .map_err(OxmlError::from)?;
9853 return Ok(());
9854 }
9855 writer
9856 .write_event(Event::Start(start))
9857 .map_err(OxmlError::from)?;
9858 emit_raw(writer, raw_children.at(0))?;
9859 writer
9860 .write_event(Event::End(BytesEnd::new(tag)))
9861 .map_err(OxmlError::from)?;
9862 Ok(())
9863}
9864
9865#[derive(Clone, Debug, PartialEq)]
9867pub struct CT_Chart {
9868 pub title: Option<CT_Title>,
9869 pub auto_title_deleted: bool,
9870 pub plot_area: CT_PlotArea,
9871 pub legend: Option<CT_Legend>,
9872 pub plot_vis_only: bool,
9873 pub disp_blanks_as: DispBlanksAs,
9874 auto_title_deleted_markup: Option<ScalarMarkup>,
9875 plot_vis_only_markup: Option<ScalarMarkup>,
9876 disp_blanks_as_markup: Option<ScalarMarkup>,
9877 raw_attributes: Vec<(String, String)>,
9878 raw_children: OrderedRawChildren,
9879}
9880
9881impl CT_Chart {
9882 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
9883 let mut reader = Reader::from_reader(xml);
9884 let mut buffer = Vec::new();
9885 loop {
9886 match reader
9887 .read_event_into(&mut buffer)
9888 .map_err(OxmlError::from)?
9889 {
9890 Event::Start(element) if matches_local_name(element.name().as_ref(), b"chart") => {
9891 reject_conflicting_prefix(&element, b"c", C_NS)?;
9892 return Self::from_element(&mut reader, &element, inherited);
9893 }
9894 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"chart") => {
9895 return Err(ChartError::MissingElement("c:plotArea".to_owned()));
9896 }
9897 Event::Start(element) | Event::Empty(element) => {
9898 return Err(ChartError::UnexpectedElement(element_name(&element)));
9899 }
9900 Event::Eof => return Err(ChartError::MissingElement("c:chart".to_owned())),
9901 _ => {}
9902 }
9903 buffer.clear();
9904 }
9905 }
9906
9907 fn from_element(
9908 reader: &mut Reader<&[u8]>,
9909 start: &BytesStart<'_>,
9910 inherited: &NamespaceBindings,
9911 ) -> Result<Self> {
9912 let namespaces = bindings_with_local(inherited, start)?;
9913 let raw_attributes = capture_attributes(start)?;
9914 let mut state = ChartParseState::default();
9915 let mut buffer = Vec::new();
9916
9917 loop {
9918 match reader
9919 .read_event_into(&mut buffer)
9920 .map_err(OxmlError::from)?
9921 {
9922 Event::Start(element) => {
9923 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
9924 local_name(element.name().as_ref()).to_vec()
9925 } else {
9926 Vec::new()
9927 };
9928 let raw = capture_element(reader, &element)?;
9929 state.parse_child(&name, raw, &namespaces)?;
9930 }
9931 Event::Empty(element) => {
9932 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
9933 local_name(element.name().as_ref()).to_vec()
9934 } else {
9935 Vec::new()
9936 };
9937 let raw = capture_empty_element(&element)?;
9938 state.parse_child(&name, raw, &namespaces)?;
9939 }
9940 event @ (Event::Text(_)
9941 | Event::CData(_)
9942 | Event::Comment(_)
9943 | Event::PI(_)
9944 | Event::GeneralRef(_)) => {
9945 state.capture_event(capture_event(event)?);
9946 }
9947 Event::End(element) if matches_local_name(element.name().as_ref(), b"chart") => {
9948 break;
9949 }
9950 Event::Eof => return Err(missing_end("c:chart")),
9951 _ => {}
9952 }
9953 buffer.clear();
9954 }
9955
9956 state.finish(raw_attributes)
9957 }
9958
9959 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
9960 let mut start = BytesStart::new("c:chart");
9961 push_attributes(&mut start, &self.raw_attributes);
9962 writer
9963 .write_event(Event::Start(start))
9964 .map_err(OxmlError::from)?;
9965 emit_raw(writer, self.raw_children.at(0))?;
9966 if let Some(title) = &self.title {
9967 title.write_xml(writer)?;
9968 }
9969 emit_raw(writer, self.raw_children.at(1))?;
9970 if self.auto_title_deleted_markup.is_some() || self.auto_title_deleted {
9971 write_scalar(
9972 writer,
9973 "c:autoTitleDeleted",
9974 if self.auto_title_deleted { "1" } else { "0" },
9975 self.auto_title_deleted_markup.as_ref(),
9976 )?;
9977 }
9978 for raw_boundary in 2..8 {
9979 emit_raw(writer, self.raw_children.at(raw_boundary))?;
9980 }
9981 self.plot_area.write_xml(writer)?;
9982 emit_raw(writer, self.raw_children.at(8))?;
9983 if let Some(legend) = &self.legend {
9984 legend.write_xml(writer)?;
9985 }
9986 emit_raw(writer, self.raw_children.at(9))?;
9987 if self.plot_vis_only_markup.is_some() || !self.plot_vis_only {
9988 write_scalar(
9989 writer,
9990 "c:plotVisOnly",
9991 if self.plot_vis_only { "1" } else { "0" },
9992 self.plot_vis_only_markup.as_ref(),
9993 )?;
9994 }
9995 emit_raw(writer, self.raw_children.at(10))?;
9996 if self.disp_blanks_as_markup.is_some() || self.disp_blanks_as != DispBlanksAs::Gap {
9997 write_scalar(
9998 writer,
9999 "c:dispBlanksAs",
10000 self.disp_blanks_as.as_str(),
10001 self.disp_blanks_as_markup.as_ref(),
10002 )?;
10003 }
10004 for raw_boundary in 11..=13 {
10005 emit_raw(writer, self.raw_children.at(raw_boundary))?;
10006 }
10007 writer
10008 .write_event(Event::End(BytesEnd::new("c:chart")))
10009 .map_err(OxmlError::from)?;
10010 Ok(())
10011 }
10012
10013 pub fn raw_children(&self) -> &OrderedRawChildren {
10014 &self.raw_children
10015 }
10016}
10017
10018#[derive(Default)]
10019struct ChartParseState {
10020 title: Option<CT_Title>,
10021 auto_title_deleted: Option<(bool, ScalarMarkup)>,
10022 plot_area: Option<CT_PlotArea>,
10023 legend: Option<CT_Legend>,
10024 plot_vis_only: Option<(bool, ScalarMarkup)>,
10025 disp_blanks_as: Option<(DispBlanksAs, ScalarMarkup)>,
10026 raw_children: OrderedRawChildren,
10027 boundary: usize,
10028}
10029
10030impl ChartParseState {
10031 fn capture_event(&mut self, raw: Vec<u8>) {
10032 self.raw_children.push(self.boundary, raw);
10033 }
10034
10035 fn parse_child(
10036 &mut self,
10037 name: &[u8],
10038 raw: Vec<u8>,
10039 namespaces: &NamespaceBindings,
10040 ) -> Result<()> {
10041 match name {
10042 b"title" => {
10043 set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
10044 self.boundary = self.boundary.max(1);
10045 }
10046 b"autoTitleDeleted" => {
10047 set_once(
10048 &mut self.auto_title_deleted,
10049 parse_bool_value(&raw, "autoTitleDeleted")?,
10050 "c:autoTitleDeleted",
10051 )?;
10052 self.boundary = self.boundary.max(2);
10053 }
10054 b"plotArea" => {
10055 set_once(
10056 &mut self.plot_area,
10057 CT_PlotArea::from_xml_with_namespaces(&raw, namespaces)?,
10058 "c:plotArea",
10059 )?;
10060 self.boundary = self.boundary.max(8);
10061 }
10062 b"legend" => {
10063 set_once(&mut self.legend, CT_Legend::from_xml(&raw)?, "c:legend")?;
10064 self.boundary = self.boundary.max(9);
10065 }
10066 b"plotVisOnly" => {
10067 set_once(
10068 &mut self.plot_vis_only,
10069 parse_bool_value(&raw, "plotVisOnly")?,
10070 "c:plotVisOnly",
10071 )?;
10072 self.boundary = self.boundary.max(10);
10073 }
10074 b"dispBlanksAs" => {
10075 set_once(
10076 &mut self.disp_blanks_as,
10077 parse_enum_value(&raw, "dispBlanksAs")?,
10078 "c:dispBlanksAs",
10079 )?;
10080 self.boundary = self.boundary.max(11);
10081 }
10082 _ => {
10083 self.raw_children
10084 .push(chart_raw_slot(name).unwrap_or(self.boundary), raw);
10085 self.boundary = self.boundary.max(chart_raw_boundary_after(name));
10086 }
10087 }
10088 Ok(())
10089 }
10090
10091 fn finish(self, raw_attributes: XmlAttributes) -> Result<CT_Chart> {
10092 let (auto_title_deleted, auto_title_deleted_markup) = self
10093 .auto_title_deleted
10094 .map(|(value, markup)| (value, Some(markup)))
10095 .unwrap_or((false, None));
10096 let (plot_vis_only, plot_vis_only_markup) = self
10097 .plot_vis_only
10098 .map(|(value, markup)| (value, Some(markup)))
10099 .unwrap_or((true, None));
10100 let (disp_blanks_as, disp_blanks_as_markup) = self
10101 .disp_blanks_as
10102 .map(|(value, markup)| (value, Some(markup)))
10103 .unwrap_or((DispBlanksAs::Gap, None));
10104 Ok(CT_Chart {
10105 title: self.title,
10106 auto_title_deleted,
10107 plot_area: self
10108 .plot_area
10109 .ok_or_else(|| ChartError::MissingElement("c:plotArea".to_owned()))?,
10110 legend: self.legend,
10111 plot_vis_only,
10112 disp_blanks_as,
10113 auto_title_deleted_markup,
10114 plot_vis_only_markup,
10115 disp_blanks_as_markup,
10116 raw_attributes,
10117 raw_children: raw_children_in_schema_order(&self.raw_children, 13),
10118 })
10119 }
10120}
10121
10122fn chart_raw_slot(name: &[u8]) -> Option<usize> {
10123 match name {
10124 b"pivotFmts" => Some(2),
10125 b"view3D" => Some(3),
10126 b"floor" => Some(4),
10127 b"sideWall" => Some(5),
10128 b"backWall" => Some(6),
10129 b"showDLblsOverMax" | b"extLst" => Some(11),
10132 _ => None,
10133 }
10134}
10135
10136fn chart_raw_boundary_after(name: &[u8]) -> usize {
10137 match name {
10138 b"title" => 1,
10139 b"autoTitleDeleted" => 2,
10140 b"pivotFmts" => 3,
10141 b"view3D" => 4,
10142 b"floor" => 5,
10143 b"sideWall" => 6,
10144 b"backWall" => 7,
10145 b"plotArea" => 8,
10146 b"legend" => 9,
10147 b"plotVisOnly" => 10,
10148 b"dispBlanksAs" => 11,
10149 b"showDLblsOverMax" | b"extLst" => 11,
10150 _ => 0,
10151 }
10152}
10153
10154#[derive(Clone, Debug, PartialEq)]
10156pub struct CT_ChartSpace {
10157 pub chart: CT_Chart,
10158 pub sp_pr: Option<CT_ShapeProperties>,
10159 pub tx_pr: Option<CT_TextBody>,
10160 raw_attributes: Vec<(String, String)>,
10161 namespace_declarations: Vec<(String, String)>,
10162 raw_children: OrderedRawChildren,
10163}
10164
10165impl CT_ChartSpace {
10166 pub fn from_xml(xml: &[u8]) -> Result<Self> {
10168 let mut reader = Reader::from_reader(xml);
10169 let mut buffer = Vec::new();
10170 loop {
10171 match reader
10172 .read_event_into(&mut buffer)
10173 .map_err(OxmlError::from)?
10174 {
10175 Event::Start(element)
10176 if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10177 {
10178 validate_chart_space_namespace(&element)?;
10179 return Self::from_element(&mut reader, &element);
10180 }
10181 Event::Empty(element)
10182 if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10183 {
10184 validate_chart_space_namespace(&element)?;
10185 return Err(ChartError::MissingElement("c:chart".to_owned()));
10186 }
10187 Event::Start(element) | Event::Empty(element) => {
10188 return Err(ChartError::UnexpectedElement(element_name(&element)));
10189 }
10190 Event::Eof => return Err(ChartError::MissingElement("c:chartSpace".to_owned())),
10191 _ => {}
10192 }
10193 buffer.clear();
10194 }
10195 }
10196
10197 fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
10198 let namespaces = namespace_bindings(start)?;
10199 let (raw_attributes, namespace_declarations) = capture_root_attributes(start)?;
10200 let mut state = ChartSpaceParseState::default();
10201 let mut buffer = Vec::new();
10202
10203 loop {
10204 match reader
10205 .read_event_into(&mut buffer)
10206 .map_err(OxmlError::from)?
10207 {
10208 Event::Start(element) => {
10209 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10210 local_name(element.name().as_ref()).to_vec()
10211 } else {
10212 Vec::new()
10213 };
10214 let raw = capture_element(reader, &element)?;
10215 state.parse_child(&name, raw, &namespaces)?;
10216 }
10217 Event::Empty(element) => {
10218 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10219 local_name(element.name().as_ref()).to_vec()
10220 } else {
10221 Vec::new()
10222 };
10223 let raw = capture_empty_element(&element)?;
10224 state.parse_child(&name, raw, &namespaces)?;
10225 }
10226 event @ (Event::Text(_)
10227 | Event::CData(_)
10228 | Event::Comment(_)
10229 | Event::PI(_)
10230 | Event::GeneralRef(_)) => {
10231 state.capture_event(capture_event(event)?);
10232 }
10233 Event::End(element)
10234 if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10235 {
10236 break;
10237 }
10238 Event::Eof => return Err(missing_end("c:chartSpace")),
10239 _ => {}
10240 }
10241 buffer.clear();
10242 }
10243
10244 state.finish(raw_attributes, namespace_declarations)
10245 }
10246
10247 pub fn to_xml(&self) -> Result<Vec<u8>> {
10249 let mut writer = Writer::new(Vec::new());
10250 let mut start = BytesStart::new("c:chartSpace");
10251 start.push_attribute(("xmlns:c", C_NS));
10252 start.push_attribute(("xmlns:a", A_NS));
10253 start.push_attribute(("xmlns:r", R_NS));
10254 push_attributes(&mut start, &self.namespace_declarations);
10255 push_attributes(&mut start, &self.raw_attributes);
10256 writer
10257 .write_event(Event::Start(start))
10258 .map_err(OxmlError::from)?;
10259 for raw_boundary in 0..8 {
10260 emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
10261 }
10262 self.chart.write_xml(&mut writer)?;
10263 emit_raw(&mut writer, self.raw_children.at(8))?;
10264 if let Some(properties) = &self.sp_pr {
10265 properties.write_xml_as(&mut writer, "c:spPr")?;
10266 }
10267 emit_raw(&mut writer, self.raw_children.at(9))?;
10268 if let Some(text) = &self.tx_pr {
10269 text.write_xml_as(&mut writer, "c:txPr")?;
10270 }
10271 for raw_boundary in 10..=14 {
10272 emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
10273 }
10274 writer
10275 .write_event(Event::End(BytesEnd::new("c:chartSpace")))
10276 .map_err(OxmlError::from)?;
10277 Ok(writer.into_inner())
10278 }
10279
10280 pub fn raw_children(&self) -> &OrderedRawChildren {
10281 &self.raw_children
10282 }
10283}
10284
10285#[derive(Default)]
10286struct ChartSpaceParseState {
10287 chart: Option<CT_Chart>,
10288 sp_pr: Option<CT_ShapeProperties>,
10289 tx_pr: Option<CT_TextBody>,
10290 raw_children: OrderedRawChildren,
10291 boundary: usize,
10292}
10293
10294impl ChartSpaceParseState {
10295 fn capture_event(&mut self, raw: Vec<u8>) {
10296 self.raw_children.push(self.boundary, raw);
10297 }
10298
10299 fn parse_child(
10300 &mut self,
10301 name: &[u8],
10302 raw: Vec<u8>,
10303 namespaces: &NamespaceBindings,
10304 ) -> Result<()> {
10305 match name {
10306 b"chart" => {
10307 set_once(
10308 &mut self.chart,
10309 CT_Chart::from_xml_with_namespaces(&raw, namespaces)?,
10310 "c:chart",
10311 )?;
10312 self.boundary = self.boundary.max(8);
10313 }
10314 b"spPr" => {
10315 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
10316 let properties = CT_ShapeProperties::from_xml(&raw)?;
10317 let mut writer = Writer::new(Vec::new());
10318 properties.write_xml_as(&mut writer, "c:spPr")?;
10319 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
10320 set_once(&mut self.sp_pr, properties, "c:spPr")?;
10321 self.boundary = self.boundary.max(9);
10322 }
10323 b"txPr" => {
10324 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
10325 let text = CT_TextBody::from_xml_as(&raw, b"txPr")?;
10326 let mut writer = Writer::new(Vec::new());
10327 text.write_xml_as(&mut writer, "c:txPr")?;
10328 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
10329 set_once(&mut self.tx_pr, text, "c:txPr")?;
10330 self.boundary = self.boundary.max(10);
10331 }
10332 _ => {
10333 self.raw_children
10334 .push(chart_space_raw_slot(name).unwrap_or(self.boundary), raw);
10335 self.boundary = self.boundary.max(chart_space_raw_boundary_after(name));
10336 }
10337 }
10338 Ok(())
10339 }
10340
10341 fn finish(
10342 self,
10343 raw_attributes: XmlAttributes,
10344 namespace_declarations: XmlAttributes,
10345 ) -> Result<CT_ChartSpace> {
10346 Ok(CT_ChartSpace {
10347 chart: self
10348 .chart
10349 .ok_or_else(|| ChartError::MissingElement("c:chart".to_owned()))?,
10350 sp_pr: self.sp_pr,
10351 tx_pr: self.tx_pr,
10352 raw_attributes,
10353 namespace_declarations,
10354 raw_children: raw_children_in_schema_order(&self.raw_children, 14),
10355 })
10356 }
10357}
10358
10359fn raw_children_in_schema_order(
10360 raw_children: &OrderedRawChildren,
10361 final_boundary: usize,
10362) -> OrderedRawChildren {
10363 let mut ordered = OrderedRawChildren::default();
10364 for boundary in 0..=final_boundary {
10365 for raw in raw_children.at(boundary) {
10366 ordered.push(boundary, raw.to_vec());
10367 }
10368 }
10369 ordered
10370}
10371
10372fn chart_space_raw_slot(name: &[u8]) -> Option<usize> {
10373 match name {
10374 b"date1904" => Some(0),
10375 b"lang" => Some(1),
10376 b"roundedCorners" => Some(2),
10377 b"style" => Some(3),
10378 b"clrMapOvr" => Some(4),
10379 b"pivotSource" => Some(5),
10380 b"protection" => Some(6),
10381 b"externalData" => Some(10),
10382 b"printSettings" => Some(11),
10383 b"userShapes" => Some(12),
10384 b"extLst" => Some(13),
10385 _ => None,
10386 }
10387}
10388
10389fn chart_space_raw_boundary_after(name: &[u8]) -> usize {
10390 match name {
10391 b"date1904" => 1,
10392 b"lang" => 2,
10393 b"roundedCorners" => 3,
10394 b"style" => 4,
10395 b"clrMapOvr" => 5,
10396 b"pivotSource" => 6,
10397 b"protection" => 7,
10398 b"chart" => 8,
10399 b"spPr" => 9,
10400 b"txPr" => 10,
10401 b"externalData" => 11,
10402 b"printSettings" => 12,
10403 b"userShapes" => 13,
10404 b"extLst" => 14,
10405 _ => 0,
10406 }
10407}
10408
10409fn parse_bool_value(xml: &[u8], local: &str) -> Result<(bool, ScalarMarkup)> {
10410 let (value, markup) = scalar_value(xml, local)?;
10411 let value = match value.as_deref().unwrap_or("1") {
10412 "1" | "true" => true,
10413 "0" | "false" => false,
10414 invalid => return Err(invalid_attribute(local, "val", invalid.to_owned())),
10415 };
10416 Ok((value, markup))
10417}
10418
10419fn parse_enum_value(xml: &[u8], local: &str) -> Result<(DispBlanksAs, ScalarMarkup)> {
10420 let (value, markup) = scalar_value(xml, local)?;
10421 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
10422 let parsed =
10423 DispBlanksAs::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
10424 Ok((parsed, markup))
10425}
10426
10427fn scalar_value(xml: &[u8], expected: &str) -> Result<(Option<String>, ScalarMarkup)> {
10428 let mut reader = Reader::from_reader(xml);
10429 let mut buffer = Vec::new();
10430 loop {
10431 match reader
10432 .read_event_into(&mut buffer)
10433 .map_err(OxmlError::from)?
10434 {
10435 Event::Empty(element)
10436 if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
10437 {
10438 reject_conflicting_prefix(&element, b"c", C_NS)?;
10439 return Ok((
10440 attribute_value(&element, b"val")?,
10441 ScalarMarkup {
10442 raw_attributes: capture_attributes_except(&element, b"val")?,
10443 raw_content: Vec::new(),
10444 },
10445 ));
10446 }
10447 Event::Start(element)
10448 if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
10449 {
10450 reject_conflicting_prefix(&element, b"c", C_NS)?;
10451 let value = attribute_value(&element, b"val")?;
10452 let raw_attributes = capture_attributes_except(&element, b"val")?;
10453 let mut raw_content = Vec::new();
10454 let mut inner = Vec::new();
10455 loop {
10456 match reader
10457 .read_event_into(&mut inner)
10458 .map_err(OxmlError::from)?
10459 {
10460 Event::End(end)
10461 if matches_local_name(end.name().as_ref(), expected.as_bytes()) =>
10462 {
10463 return Ok((
10464 value,
10465 ScalarMarkup {
10466 raw_attributes,
10467 raw_content,
10468 },
10469 ));
10470 }
10471 event @ (Event::Text(_)
10472 | Event::CData(_)
10473 | Event::Comment(_)
10474 | Event::PI(_)
10475 | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
10476 Event::Eof => return Err(missing_end(expected)),
10477 _ => return Err(ChartError::UnexpectedElement(expected.to_owned())),
10478 }
10479 inner.clear();
10480 }
10481 }
10482 Event::Start(element) | Event::Empty(element) => {
10483 return Err(ChartError::UnexpectedElement(element_name(&element)));
10484 }
10485 Event::Eof => return Err(ChartError::MissingElement(expected.to_owned())),
10486 _ => {}
10487 }
10488 buffer.clear();
10489 }
10490}
10491
10492fn set_once<T>(slot: &mut Option<T>, value: T, name: &str) -> Result<()> {
10493 if slot.is_some() {
10494 return Err(ChartError::DuplicateElement(name.to_owned()));
10495 }
10496 *slot = Some(value);
10497 Ok(())
10498}
10499
10500fn mark_once(seen: &mut bool, name: &str) -> Result<()> {
10501 if *seen {
10502 return Err(ChartError::DuplicateElement(name.to_owned()));
10503 }
10504 *seen = true;
10505 Ok(())
10506}
10507
10508fn validate_chart_space_namespace(element: &BytesStart<'_>) -> Result<()> {
10509 let name = element.name();
10510 let qualified = name.as_ref();
10511 let declaration = match qualified.iter().position(|byte| *byte == b':') {
10512 Some(position) => {
10513 let mut declaration = b"xmlns:".to_vec();
10514 declaration.extend_from_slice(&qualified[..position]);
10515 declaration
10516 }
10517 None => b"xmlns".to_vec(),
10518 };
10519 let namespace = attribute_value(element, &declaration)?
10520 .ok_or_else(|| invalid_attribute("chartSpace", "namespace", "<missing>".to_owned()))?;
10521 if namespace != C_NS {
10522 return Err(invalid_attribute("chartSpace", "namespace", namespace));
10523 }
10524 reject_conflicting_prefix(element, b"c", C_NS)?;
10525 reject_conflicting_prefix(element, b"a", A_NS)?;
10526 reject_conflicting_prefix(element, b"r", R_NS)
10527}
10528
10529fn reject_conflicting_prefix_in_xml(xml: &[u8], prefix: &[u8], namespace: &str) -> Result<()> {
10530 let mut reader = Reader::from_reader(xml);
10531 let mut buffer = Vec::new();
10532 loop {
10533 match reader
10534 .read_event_into(&mut buffer)
10535 .map_err(OxmlError::from)?
10536 {
10537 Event::Start(element) | Event::Empty(element) => {
10538 return reject_conflicting_prefix(&element, prefix, namespace);
10539 }
10540 Event::Eof => return Err(ChartError::MissingElement("XML root".to_owned())),
10541 _ => {}
10542 }
10543 buffer.clear();
10544 }
10545}
10546
10547fn reject_rewritten_foreign_elements(
10548 original: &[u8],
10549 written: &[u8],
10550 inherited: &NamespaceBindings,
10551 root_local: &[u8],
10552) -> Result<()> {
10553 let original = foreign_element_regions(original, inherited, root_local)?;
10554 let mut written_namespaces = inherited.clone();
10555 written_namespaces.retain(|(prefix, _)| prefix != b"a");
10556 written_namespaces.push((b"a".to_vec(), A_NS.to_owned()));
10557 let written = foreign_element_regions(written, &written_namespaces, root_local)?;
10558 if original != written {
10559 return Err(invalid_attribute(
10560 &String::from_utf8_lossy(root_local),
10561 "namespace",
10562 "foreign DrawingML content would be converted to typed content".to_owned(),
10563 ));
10564 }
10565 Ok(())
10566}
10567
10568fn foreign_element_regions(
10569 xml: &[u8],
10570 inherited: &NamespaceBindings,
10571 root_local: &[u8],
10572) -> Result<Vec<Vec<u8>>> {
10573 let mut reader = Reader::from_reader(xml);
10574 let mut buffer = Vec::new();
10575 let root_namespaces = loop {
10576 match reader
10577 .read_event_into(&mut buffer)
10578 .map_err(OxmlError::from)?
10579 {
10580 Event::Start(element) if matches_local_name(element.name().as_ref(), root_local) => {
10581 break bindings_with_local(inherited, &element)?;
10582 }
10583 Event::Empty(element) if matches_local_name(element.name().as_ref(), root_local) => {
10584 return Ok(Vec::new());
10585 }
10586 Event::Start(element) | Event::Empty(element) => {
10587 return Err(ChartError::UnexpectedElement(element_name(&element)));
10588 }
10589 Event::Eof => {
10590 return Err(ChartError::MissingElement(
10591 String::from_utf8_lossy(root_local).into_owned(),
10592 ));
10593 }
10594 _ => {}
10595 }
10596 buffer.clear();
10597 };
10598
10599 let mut namespace_stack = vec![root_namespaces];
10600 let mut foreign = Vec::new();
10601 let mut child_buffer = Vec::new();
10602 loop {
10603 match reader
10604 .read_event_into(&mut child_buffer)
10605 .map_err(OxmlError::from)?
10606 {
10607 Event::Start(element) => {
10608 let namespaces = namespace_stack.last().expect("root namespace binding");
10609 if element_is_in_namespace(&element, A_NS, namespaces)? {
10610 namespace_stack.push(bindings_with_local(namespaces, &element)?);
10611 } else {
10612 foreign.push(capture_element(&mut reader, &element)?);
10613 }
10614 }
10615 Event::Empty(element) => {
10616 let namespaces = namespace_stack.last().expect("root namespace binding");
10617 if !element_is_in_namespace(&element, A_NS, namespaces)? {
10618 foreign.push(capture_empty_element(&element)?);
10619 }
10620 }
10621 Event::End(element) if matches_local_name(element.name().as_ref(), root_local) => {
10622 return Ok(foreign);
10623 }
10624 Event::End(_) => {
10625 namespace_stack.pop();
10626 }
10627 Event::Eof => {
10628 return Err(missing_end(&String::from_utf8_lossy(root_local)));
10629 }
10630 _ => {}
10631 }
10632 child_buffer.clear();
10633 }
10634}
10635
10636fn reject_conflicting_prefix(
10637 element: &BytesStart<'_>,
10638 prefix: &[u8],
10639 namespace: &str,
10640) -> Result<()> {
10641 let mut key = b"xmlns:".to_vec();
10642 key.extend_from_slice(prefix);
10643 if let Some(value) = attribute_value(element, &key)?
10644 && value != namespace
10645 {
10646 return Err(invalid_attribute(
10647 &element_name(element),
10648 &String::from_utf8_lossy(&key),
10649 value,
10650 ));
10651 }
10652 Ok(())
10653}
10654
10655fn namespace_bindings(element: &BytesStart<'_>) -> Result<NamespaceBindings> {
10656 let mut bindings = Vec::new();
10657 for attribute in element.attributes() {
10658 let attribute = attribute.map_err(OxmlError::from)?;
10659 let key = attribute.key.as_ref();
10660 let prefix = if key == b"xmlns" {
10661 Vec::new()
10662 } else if let Some(prefix) = key.strip_prefix(b"xmlns:") {
10663 prefix.to_vec()
10664 } else {
10665 continue;
10666 };
10667 let namespace = attribute
10668 .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
10669 .map_err(OxmlError::from)?
10670 .into_owned();
10671 bindings.push((prefix, namespace));
10672 }
10673 Ok(bindings)
10674}
10675
10676fn bindings_with_local(
10677 inherited: &NamespaceBindings,
10678 element: &BytesStart<'_>,
10679) -> Result<NamespaceBindings> {
10680 let mut bindings = inherited.clone();
10681 for (prefix, namespace) in namespace_bindings(element)? {
10682 upsert_namespace_binding(&mut bindings, prefix, namespace);
10683 }
10684 Ok(bindings)
10685}
10686
10687fn upsert_namespace_binding(bindings: &mut NamespaceBindings, prefix: Vec<u8>, namespace: String) {
10688 if let Some((_, current)) = bindings.iter_mut().find(|(current, _)| current == &prefix) {
10689 *current = namespace;
10690 } else {
10691 bindings.push((prefix, namespace));
10692 }
10693}
10694
10695fn element_is_in_namespace(
10696 element: &BytesStart<'_>,
10697 expected: &str,
10698 inherited: &NamespaceBindings,
10699) -> Result<bool> {
10700 let name = element.name();
10701 let qualified = name.as_ref();
10702 let prefix = qualified
10703 .iter()
10704 .position(|byte| *byte == b':')
10705 .map(|position| &qualified[..position])
10706 .unwrap_or_default();
10707 let local = namespace_bindings(element)?;
10708 let namespace = local
10709 .iter()
10710 .rev()
10711 .chain(inherited.iter().rev())
10712 .find(|(candidate, _)| candidate.as_slice() == prefix)
10713 .map(|(_, namespace)| namespace.as_str());
10714 Ok(namespace == Some(expected))
10715}
10716
10717fn attribute_value(element: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
10718 for attribute in element.attributes() {
10719 let attribute = attribute.map_err(OxmlError::from)?;
10720 if attribute.key.as_ref() == name {
10721 return Ok(Some(
10722 attribute
10723 .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
10724 .map_err(OxmlError::from)?
10725 .into_owned(),
10726 ));
10727 }
10728 }
10729 Ok(None)
10730}
10731
10732fn capture_root_attributes(start: &BytesStart<'_>) -> Result<RootAttributes> {
10733 let mut attributes = Vec::new();
10734 let mut namespaces = Vec::new();
10735 for attribute in start.attributes() {
10736 let attribute = attribute.map_err(OxmlError::from)?;
10737 let name = std::str::from_utf8(attribute.key.as_ref())
10738 .map_err(OxmlError::from)?
10739 .to_owned();
10740 let value = attribute
10741 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10742 .map_err(OxmlError::from)?
10743 .into_owned();
10744 if name == "xmlns:c" || name == "xmlns:a" || name == "xmlns:r" {
10745 continue;
10746 }
10747 if name == "xmlns" || name.starts_with("xmlns:") {
10748 namespaces.push((name, value));
10749 } else {
10750 attributes.push((name, value));
10751 }
10752 }
10753 Ok((attributes, namespaces))
10754}
10755
10756fn capture_attributes(start: &BytesStart<'_>) -> Result<XmlAttributes> {
10757 let mut attributes = Vec::new();
10758 for attribute in start.attributes() {
10759 let attribute = attribute.map_err(OxmlError::from)?;
10760 attributes.push((
10761 std::str::from_utf8(attribute.key.as_ref())
10762 .map_err(OxmlError::from)?
10763 .to_owned(),
10764 attribute
10765 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10766 .map_err(OxmlError::from)?
10767 .into_owned(),
10768 ));
10769 }
10770 Ok(attributes)
10771}
10772
10773fn capture_attributes_except(start: &BytesStart<'_>, excluded: &[u8]) -> Result<XmlAttributes> {
10774 let mut attributes = Vec::new();
10775 for attribute in start.attributes() {
10776 let attribute = attribute.map_err(OxmlError::from)?;
10777 if attribute.key.as_ref() == excluded {
10778 continue;
10779 }
10780 attributes.push((
10781 std::str::from_utf8(attribute.key.as_ref())
10782 .map_err(OxmlError::from)?
10783 .to_owned(),
10784 attribute
10785 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10786 .map_err(OxmlError::from)?
10787 .into_owned(),
10788 ));
10789 }
10790 Ok(attributes)
10791}
10792
10793fn capture_attributes_excluding(
10794 start: &BytesStart<'_>,
10795 excluded: &[&[u8]],
10796) -> Result<XmlAttributes> {
10797 let mut attributes = Vec::new();
10798 for attribute in start.attributes() {
10799 let attribute = attribute.map_err(OxmlError::from)?;
10800 if excluded.contains(&attribute.key.as_ref()) {
10801 continue;
10802 }
10803 attributes.push((
10804 std::str::from_utf8(attribute.key.as_ref())
10805 .map_err(OxmlError::from)?
10806 .to_owned(),
10807 attribute
10808 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10809 .map_err(OxmlError::from)?
10810 .into_owned(),
10811 ));
10812 }
10813 Ok(attributes)
10814}
10815
10816fn push_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
10817 for (name, value) in attributes {
10818 start.push_attribute((name.as_str(), value.as_str()));
10819 }
10820}
10821
10822fn write_scalar<W: Write>(
10823 writer: &mut Writer<W>,
10824 name: &str,
10825 value: &str,
10826 markup: Option<&ScalarMarkup>,
10827) -> Result<()> {
10828 let mut start = BytesStart::new(name);
10829 start.push_attribute(("val", value));
10830 if let Some(markup) = markup {
10831 push_attributes(&mut start, &markup.raw_attributes);
10832 }
10833 let has_content = markup.is_some_and(|markup| !markup.raw_content.is_empty());
10834 if !has_content {
10835 writer
10836 .write_event(Event::Empty(start))
10837 .map_err(OxmlError::from)?;
10838 return Ok(());
10839 }
10840 writer
10841 .write_event(Event::Start(start))
10842 .map_err(OxmlError::from)?;
10843 if let Some(markup) = markup {
10844 for raw in &markup.raw_content {
10845 writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
10846 }
10847 }
10848 writer
10849 .write_event(Event::End(BytesEnd::new(name)))
10850 .map_err(OxmlError::from)?;
10851 Ok(())
10852}
10853
10854fn capture_event(event: Event<'_>) -> Result<Vec<u8>> {
10855 let mut writer = Writer::new(Vec::new());
10856 writer
10857 .write_event(event.into_owned())
10858 .map_err(OxmlError::from)?;
10859 Ok(writer.into_inner())
10860}
10861
10862fn emit_raw<'a, W: Write>(
10863 writer: &mut Writer<W>,
10864 children: impl Iterator<Item = &'a [u8]>,
10865) -> Result<()> {
10866 for child in children {
10867 writer.get_mut().write_all(child).map_err(OxmlError::from)?;
10868 }
10869 Ok(())
10870}
10871
10872fn invalid_attribute(element: &str, attribute: &str, value: String) -> ChartError {
10873 ChartError::InvalidAttribute {
10874 element: element.to_owned(),
10875 attribute: attribute.to_owned(),
10876 value,
10877 }
10878}
10879
10880fn element_name(element: &BytesStart<'_>) -> String {
10881 String::from_utf8_lossy(element.name().as_ref()).into_owned()
10882}
10883
10884fn missing_end(element: &str) -> ChartError {
10885 ChartError::Xml(OxmlError::MissingElement(format!("closing {element}")))
10886}
10887
10888#[cfg(test)]
10889mod tests {
10890 use std::collections::HashSet;
10891 use std::fs;
10892 use std::path::{Path, PathBuf};
10893 use std::process::Command;
10894
10895 use oxml_core::raw_xml::{capture_element, capture_empty_element};
10896 use oxml_drawing::color::{ColorMap, ColorMapSlot, RgbColor, ThemeColorSlot};
10897 use oxml_drawing::theme::CT_OfficeStyleSheet;
10898 use oxml_layout::{
10899 Color, FontManager, LayoutResult, PageFrame, Paint, PathCommand, Point, PositionedElement,
10900 Rect,
10901 };
10902 use oxml_opc::OpcPackage;
10903 use quick_xml::Reader;
10904 use quick_xml::events::Event;
10905
10906 use super::{
10907 A_NS, Axis, AxisData, AxisId, AxisKind, AxisPosition, BarDirection, BarGrouping, C_NS,
10908 CT_ChartSpace, CT_DLbls, CT_ShapeProperties, CT_TextBody, ChartGeometry, DataLabelPosition,
10909 DispBlanksAs, Domain, Grouping, NumberFormat, NumericData, Orientation, Plot, R_NS,
10910 ScatterStyle, Series, StringRef, TickLabelPosition, TickMark, capture_event, local_name,
10911 matches_local_name, nice_number_scale, render_chart as render_chart_with_theme,
10912 render_geometry as render_geometry_with_theme,
10913 };
10914
10915 const MANIFEST: &str = include_str!("../../../scripts/pptx-corpus-manifest.tsv");
10916 const EXPECTED_DECKS: usize = 50;
10917 const LIBREOFFICE_VERSION: &str =
10918 "LibreOffice 26.2.5.2 cd7284b4cbbfeb507e630c1aac019f4157393acb";
10919 const PDFTOTEXT_VERSION: &str = "pdftotext version 26.01.0";
10920 const PDFTOPPM_VERSION: &str = "pdftoppm version 26.01.0";
10921 const PLOT_RENDER_NORMALIZED_MAE_THRESHOLD: f64 = 0.0;
10922
10923 fn render_geometry(chart: &super::CT_Chart, bounds: Rect) -> super::Result<ChartGeometry> {
10924 render_geometry_with_theme(
10925 chart,
10926 bounds,
10927 &CT_OfficeStyleSheet::office_default(),
10928 &ColorMap::default(),
10929 )
10930 }
10931
10932 fn render_chart(
10933 chart: &super::CT_Chart,
10934 bounds: Rect,
10935 fonts: &mut FontManager,
10936 ) -> super::Result<oxml_layout::GroupElement> {
10937 render_chart_with_theme(
10938 chart,
10939 bounds,
10940 &CT_OfficeStyleSheet::office_default(),
10941 &ColorMap::default(),
10942 fonts,
10943 )
10944 }
10945
10946 #[test]
10947 fn bar_chart_rasterises_at_computed_positions() {
10948 let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
10949 let geometry = render_geometry(
10950 &chart.chart,
10951 Rect {
10952 x: 0.0,
10953 y: 0.0,
10954 width: 200.0,
10955 height: 140.0,
10956 },
10957 )
10958 .unwrap();
10959 assert_eq!(
10960 geometry.plot_bounds,
10961 Rect {
10962 x: 36.0,
10963 y: 12.0,
10964 width: 152.0,
10965 height: 100.0,
10966 }
10967 );
10968 let bounds = path_bounds(&geometry);
10969 assert_eq!(bounds.len(), 2);
10970 assert_rect_near(
10971 bounds[0],
10972 Rect {
10973 x: 58.8,
10974 y: 62.0,
10975 width: 30.4,
10976 height: 50.0,
10977 },
10978 );
10979 assert_rect_near(
10980 bounds[1],
10981 Rect {
10982 x: 134.8,
10983 y: 12.0,
10984 width: 30.4,
10985 height: 100.0,
10986 },
10987 );
10988
10989 let layout = LayoutResult::new(
10990 vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
10991 Vec::new(),
10992 None,
10993 Vec::new(),
10994 );
10995 let png = oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap();
10996 let pixmap = tiny_skia::Pixmap::decode_png(&png).unwrap();
10997 for (x, y) in [(74, 87), (150, 37)] {
10998 let pixel = pixmap.pixel(x, y).unwrap();
10999 assert_ne!((pixel.red(), pixel.green(), pixel.blue()), (255, 255, 255));
11000 }
11001 }
11002
11003 #[test]
11004 fn unstyled_four_series_use_accent_one_through_four() {
11005 let chart = four_series_bar_chart();
11006 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11007 let colours = path_colours(&geometry);
11008
11009 assert_eq!(
11010 &colours[..4],
11011 &[
11012 Color::from_hex("156082"),
11013 Color::from_hex("E97132"),
11014 Color::from_hex("196B24"),
11015 Color::from_hex("0F9ED5"),
11016 ]
11017 );
11018 }
11019
11020 #[test]
11021 fn direct_series_solid_colour_overrides_theme_accent() {
11022 for (paint, expected) in [
11023 (
11024 "<a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill>",
11025 Color::from_hex("123456"),
11026 ),
11027 (
11028 "<a:ln><a:solidFill><a:srgbClr val=\"654321\"/></a:solidFill></a:ln>",
11029 Color::from_hex("654321"),
11030 ),
11031 (
11032 "<a:noFill/>",
11033 Color {
11034 r: 0.0,
11035 g: 0.0,
11036 b: 0.0,
11037 a: 0.0,
11038 },
11039 ),
11040 ] {
11041 let styled =
11042 plot_series(0).replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
11043 let plot = bar_plot("").replace(&plot_series(0), &styled);
11044 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11045 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11046
11047 assert_eq!(path_colours(&geometry)[0], expected);
11048 }
11049
11050 let styled = plot_series(0).replace(
11051 "<q:cat>",
11052 "<q:spPr><a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill></q:spPr><q:cat>",
11053 );
11054 let plot = bar_plot("").replace(&plot_series(0), &styled);
11055 let xml = chart_with_plot(&plot).replace(
11056 "</q:plotArea></q:chart>",
11057 "</q:plotArea><q:legend/></q:chart>",
11058 );
11059 let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
11060 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
11061 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
11062 assert!(group.children.iter().any(|element| matches!(
11063 element,
11064 PositionedElement::Path(path)
11065 if path.fill == Some(Paint::Solid(Color::from_hex("123456")))
11066 )));
11067
11068 let mut theme = CT_OfficeStyleSheet::office_default();
11069 let preset = oxml_drawing::fill::Fill::from_xml(
11070 br#"<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:prstClr val="red"/></a:solidFill>"#,
11071 )
11072 .unwrap();
11073 let oxml_drawing::fill::Fill::Solid(preset) = preset else {
11074 panic!("expected solid fill");
11075 };
11076 theme.theme_elements.color_scheme.hyperlink = preset.color.unwrap();
11077 let geometry =
11078 render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &ColorMap::default())
11079 .unwrap();
11080 assert_eq!(path_colours(&geometry)[0], Color::from_hex("123456"));
11081
11082 for (paint, expected_alpha) in [
11083 ("<a:noFill/>", 0.0),
11084 (
11085 "<a:solidFill><a:srgbClr val=\"123456\"><a:alpha val=\"50000\"/></a:srgbClr></a:solidFill>",
11086 (128.0 / 255.0) * 0.55,
11087 ),
11088 ] {
11089 let (_, area, _) = remaining_plot_fixtures()
11090 .into_iter()
11091 .find(|(kind, _, _)| *kind == "areaChart")
11092 .unwrap();
11093 let styled_area = area.replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
11094 let chart =
11095 CT_ChartSpace::from_xml(chart_with_optional_axes(&styled_area, true).as_bytes())
11096 .unwrap();
11097 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11098 let actual_alpha = path_colours(&geometry)[0].a;
11099
11100 assert!((actual_alpha - expected_alpha).abs() < 1.0e-12);
11101 }
11102 }
11103
11104 #[test]
11105 fn series_accent_cycle_repeats_after_six() {
11106 let chart = seven_series_bar_chart();
11107 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11108 let colours = path_colours(&geometry);
11109
11110 assert_eq!(colours[0], Color::from_hex("156082"));
11111 assert_eq!(colours[6], colours[0]);
11112 }
11113
11114 #[test]
11115 fn series_colours_honor_colour_map_and_transform_order() {
11116 let styled = plot_series(0).replace(
11117 "<q:cat>",
11118 "<q:spPr><a:solidFill><a:schemeClr val=\"accent1\"><a:shade val=\"80000\"/><a:lumOff val=\"5000\"/><a:alpha val=\"50000\"/></a:schemeClr></a:solidFill></q:spPr><q:cat>",
11119 );
11120 let plot = bar_plot("").replace(&plot_series(0), &styled);
11121 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11122 let color_map =
11123 ColorMap::default().with_overrides(&[(ColorMapSlot::Accent1, ThemeColorSlot::Accent4)]);
11124 let mut theme = CT_OfficeStyleSheet::office_default();
11125 let theme_fill = oxml_drawing::fill::Fill::from_xml(
11126 br#"<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:srgbClr val="336699"><a:tint val="20000"/><a:lumMod val="90000"/></a:srgbClr></a:solidFill>"#,
11127 )
11128 .unwrap();
11129 let oxml_drawing::fill::Fill::Solid(theme_fill) = theme_fill else {
11130 panic!("expected solid fill");
11131 };
11132 let theme_colour = theme_fill.color.unwrap();
11133 theme.theme_elements.color_scheme.accent4 = theme_colour.clone();
11134 let geometry =
11135 render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &color_map).unwrap();
11136 let series_fill = oxml_drawing::fill::Fill::from_xml(
11137 br#"<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:schemeClr val="accent1"><a:shade val="80000"/><a:lumOff val="5000"/><a:alpha val="50000"/></a:schemeClr></a:solidFill>"#,
11138 )
11139 .unwrap();
11140 let oxml_drawing::fill::Fill::Solid(series_fill) = series_fill else {
11141 panic!("expected solid fill");
11142 };
11143 let mut transforms = theme_colour.transforms().to_vec();
11144 transforms.extend_from_slice(series_fill.color.as_ref().unwrap().transforms());
11145 let expected = oxml_drawing::color::apply_color_transforms(
11146 RgbColor::parse("336699").unwrap(),
11147 &transforms,
11148 );
11149
11150 assert_eq!(path_colours(&geometry)[0], resolved_layout_colour(expected));
11151 }
11152
11153 #[test]
11154 fn chart_colours_do_not_use_word_tint_shade() {
11155 let styled = plot_series(0).replace(
11156 "<q:cat>",
11157 "<q:spPr><a:solidFill><a:srgbClr val=\"808080\"><a:tint val=\"50000\"/></a:srgbClr></a:solidFill></q:spPr><q:cat>",
11158 );
11159 let plot = bar_plot("").replace(&plot_series(0), &styled);
11160 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11161 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11162
11163 assert_eq!(
11164 path_colours(&geometry)[0],
11165 Color {
11166 r: 205.0 / 255.0,
11167 g: 205.0 / 255.0,
11168 b: 205.0 / 255.0,
11169 a: 1.0,
11170 }
11171 );
11172 }
11173
11174 #[test]
11175 fn unsupported_direct_series_paint_is_contextual() {
11176 let styled = plot_series(0).replace(
11177 "<q:cat>",
11178 "<q:spPr><a:gradFill><a:gsLst><a:gs pos=\"0\"><a:srgbClr val=\"FF0000\"/></a:gs><a:gs pos=\"100000\"><a:srgbClr val=\"0000FF\"/></a:gs></a:gsLst></a:gradFill></q:spPr><q:cat>",
11179 );
11180 let plot = bar_plot("").replace(&plot_series(0), &styled);
11181 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11182 let error = render_geometry(&chart.chart, chart_bounds()).unwrap_err();
11183
11184 assert!(error.to_string().contains("c:ser[0] direct fill paint"));
11185 assert!(error.to_string().contains("gradient"));
11186 }
11187
11188 #[test]
11189 fn resolved_chart_palette_raster_is_deterministic() {
11190 let raster = || {
11191 let chart = four_series_bar_chart();
11192 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
11193 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
11194 let layout = LayoutResult::new(
11195 vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
11196 fonts.all_font_data(),
11197 None,
11198 Vec::new(),
11199 );
11200 oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
11201 };
11202 let chart = four_series_bar_chart();
11203 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11204
11205 assert_eq!(path_colours(&geometry)[0], Color::from_hex("156082"));
11206 assert_eq!(raster(), raster());
11207 }
11208
11209 #[test]
11210 fn bar_geometry_handles_direction_grouping_gap_and_overlap() {
11211 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11212 let Plot::Bar {
11213 overlap, series, ..
11214 } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11215 else {
11216 panic!("expected bar plot");
11217 };
11218 let mut second = series[0].clone();
11219 second.index = 1;
11220 second.order = 1;
11221 second.values.values = vec![2.0, 1.0];
11222 series.push(second);
11223
11224 *overlap = 25;
11225 let clustered = render_geometry(&chart.chart, chart_bounds()).unwrap();
11226 let clustered_bounds = path_bounds(&clustered);
11227 let clustered_width = 30.4 / 1.75;
11228 let clustered_advance = clustered_width * 0.75;
11229 assert_rects_near(
11230 &clustered_bounds,
11231 &[
11232 Rect {
11233 x: 58.8,
11234 y: 62.0,
11235 width: clustered_width,
11236 height: 50.0,
11237 },
11238 Rect {
11239 x: 134.8,
11240 y: 12.0,
11241 width: clustered_width,
11242 height: 100.0,
11243 },
11244 Rect {
11245 x: 58.8 + clustered_advance,
11246 y: 12.0,
11247 width: clustered_width,
11248 height: 100.0,
11249 },
11250 Rect {
11251 x: 134.8 + clustered_advance,
11252 y: 62.0,
11253 width: clustered_width,
11254 height: 50.0,
11255 },
11256 ],
11257 );
11258
11259 let Plot::Bar { direction, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
11260 unreachable!()
11261 };
11262 *direction = BarDirection::Bar;
11263 let horizontal = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11264 let horizontal_width = 20.0 / 1.75;
11265 let horizontal_advance = horizontal_width * 0.75;
11266 assert_rects_near(
11267 &horizontal,
11268 &[
11269 Rect {
11270 x: 36.0,
11271 y: 27.0,
11272 width: 76.0,
11273 height: horizontal_width,
11274 },
11275 Rect {
11276 x: 36.0,
11277 y: 77.0,
11278 width: 152.0,
11279 height: horizontal_width,
11280 },
11281 Rect {
11282 x: 36.0,
11283 y: 27.0 + horizontal_advance,
11284 width: 152.0,
11285 height: horizontal_width,
11286 },
11287 Rect {
11288 x: 36.0,
11289 y: 77.0 + horizontal_advance,
11290 width: 76.0,
11291 height: horizontal_width,
11292 },
11293 ],
11294 );
11295
11296 let Plot::Bar {
11297 direction,
11298 grouping,
11299 ..
11300 } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11301 else {
11302 unreachable!()
11303 };
11304 *direction = BarDirection::Column;
11305 *grouping = BarGrouping::Stacked;
11306 let stacked = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11307 assert_rects_near(
11308 &stacked,
11309 &[
11310 Rect {
11311 x: 58.8,
11312 y: 112.0 - 100.0 / 3.0,
11313 width: 30.4,
11314 height: 100.0 / 3.0,
11315 },
11316 Rect {
11317 x: 134.8,
11318 y: 112.0 - 200.0 / 3.0,
11319 width: 30.4,
11320 height: 200.0 / 3.0,
11321 },
11322 Rect {
11323 x: 58.8,
11324 y: 12.0,
11325 width: 30.4,
11326 height: 200.0 / 3.0,
11327 },
11328 Rect {
11329 x: 134.8,
11330 y: 12.0,
11331 width: 30.4,
11332 height: 100.0 / 3.0,
11333 },
11334 ],
11335 );
11336
11337 let Plot::Bar {
11338 grouping, series, ..
11339 } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11340 else {
11341 unreachable!()
11342 };
11343 *grouping = BarGrouping::PercentStacked;
11344 series[1].values.values = vec![2.0, 2.0];
11345 let percent = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11346 assert_rects_near(
11347 &percent,
11348 &[
11349 Rect {
11350 x: 58.8,
11351 y: 112.0 - 100.0 / 3.0,
11352 width: 30.4,
11353 height: 100.0 / 3.0,
11354 },
11355 Rect {
11356 x: 134.8,
11357 y: 62.0,
11358 width: 30.4,
11359 height: 50.0,
11360 },
11361 Rect {
11362 x: 58.8,
11363 y: 12.0,
11364 width: 30.4,
11365 height: 200.0 / 3.0,
11366 },
11367 Rect {
11368 x: 134.8,
11369 y: 12.0,
11370 width: 30.4,
11371 height: 50.0,
11372 },
11373 ],
11374 );
11375 }
11376
11377 #[test]
11378 fn line_scatter_and_radar_emit_paths_and_markers() {
11379 let line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
11380 let line_geometry = render_geometry(&line.chart, chart_bounds()).unwrap();
11381 assert_path_points(
11382 path_commands(&line_geometry)[0],
11383 &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
11384 false,
11385 );
11386 assert_rects_near(
11387 &path_bounds(&line_geometry)[1..],
11388 &[
11389 Rect {
11390 x: 71.5,
11391 y: 109.5,
11392 width: 5.0,
11393 height: 5.0,
11394 },
11395 Rect {
11396 x: 147.5,
11397 y: 9.5,
11398 width: 5.0,
11399 height: 5.0,
11400 },
11401 ],
11402 );
11403
11404 let (scatter_plot, _) = remaining_plot_fixtures()
11405 .into_iter()
11406 .find(|(kind, _, _)| *kind == "scatterChart")
11407 .map(|(_, plot, axes)| (plot, axes))
11408 .unwrap();
11409 let mut scatter =
11410 CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
11411 .unwrap();
11412 let Plot::Scatter { style, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
11413 else {
11414 unreachable!()
11415 };
11416 *style = ScatterStyle::LineMarker;
11417 let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
11418 assert_path_points(
11419 path_commands(&scatter_geometry)[0],
11420 &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
11421 false,
11422 );
11423 assert_rects_near(
11424 &path_bounds(&scatter_geometry)[1..],
11425 &[
11426 Rect {
11427 x: 33.5,
11428 y: 109.5,
11429 width: 5.0,
11430 height: 5.0,
11431 },
11432 Rect {
11433 x: 185.5,
11434 y: 9.5,
11435 width: 5.0,
11436 height: 5.0,
11437 },
11438 ],
11439 );
11440
11441 let (radar_plot, _) = remaining_plot_fixtures()
11442 .into_iter()
11443 .find(|(kind, _, _)| *kind == "radarChart")
11444 .map(|(_, plot, axes)| (plot, axes))
11445 .unwrap();
11446 let radar = CT_ChartSpace::from_xml(chart_with_optional_axes(&radar_plot, true).as_bytes())
11447 .unwrap();
11448 let radar_geometry = render_geometry(&radar.chart, chart_bounds()).unwrap();
11449 assert_path_points(
11450 path_commands(&radar_geometry)[0],
11451 &[Point { x: 112.0, y: 37.0 }, Point { x: 112.0, y: 112.0 }],
11452 true,
11453 );
11454 assert_rects_near(
11455 &path_bounds(&radar_geometry)[1..],
11456 &[
11457 Rect {
11458 x: 109.5,
11459 y: 34.5,
11460 width: 5.0,
11461 height: 5.0,
11462 },
11463 Rect {
11464 x: 109.5,
11465 y: 109.5,
11466 width: 5.0,
11467 height: 5.0,
11468 },
11469 ],
11470 );
11471 }
11472
11473 #[test]
11474 fn pie_doughnut_and_area_emit_closed_paths() {
11475 let fixtures = remaining_plot_fixtures();
11476 let pie_plot = fixtures
11477 .iter()
11478 .find(|(kind, _, _)| *kind == "pieChart")
11479 .unwrap();
11480 let pie =
11481 CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot.1, pie_plot.2).as_bytes())
11482 .unwrap();
11483 let pie_geometry = render_geometry(&pie.chart, chart_bounds()).unwrap();
11484 let pie_commands = path_commands(&pie_geometry);
11485 assert_eq!(pie_commands.len(), 2);
11486 let [
11487 PathCommand::MoveTo(center),
11488 PathCommand::LineTo(start),
11489 ..,
11490 PathCommand::CurveTo { to: end, .. },
11491 PathCommand::Close,
11492 ] = pie_commands[0]
11493 else {
11494 panic!("expected closed cubic pie wedge");
11495 };
11496 assert_point_near(*center, Point { x: 112.0, y: 62.0 });
11497 assert_point_near(
11498 *start,
11499 Point {
11500 x: 137.0,
11501 y: 62.0 - 25.0 * 3.0_f64.sqrt(),
11502 },
11503 );
11504 assert_point_near(
11505 *end,
11506 Point {
11507 x: 137.0,
11508 y: 62.0 + 25.0 * 3.0_f64.sqrt(),
11509 },
11510 );
11511
11512 let doughnut_plot = fixtures
11513 .iter()
11514 .find(|(kind, _, _)| *kind == "doughnutChart")
11515 .unwrap();
11516 let doughnut = CT_ChartSpace::from_xml(
11517 chart_with_optional_axes(&doughnut_plot.1, doughnut_plot.2).as_bytes(),
11518 )
11519 .unwrap();
11520 let doughnut_geometry = render_geometry(&doughnut.chart, chart_bounds()).unwrap();
11521 let doughnut_commands = path_commands(&doughnut_geometry);
11522 assert_eq!(doughnut_commands.len(), 2);
11523 let commands = doughnut_commands[0];
11524 assert_eq!(commands.len(), 7);
11525 let PathCommand::MoveTo(outer_start) = commands[0] else {
11526 panic!("expected doughnut outer start");
11527 };
11528 let PathCommand::LineTo(inner_end) = commands[3] else {
11529 panic!("expected doughnut inner-radius join");
11530 };
11531 let PathCommand::CurveTo {
11532 to: inner_start, ..
11533 } = commands[5]
11534 else {
11535 panic!("expected reverse inner arc");
11536 };
11537 let diagonal = 25.0 * 2.0_f64.sqrt();
11538 assert_point_near(
11539 outer_start,
11540 Point {
11541 x: 112.0 + diagonal,
11542 y: 62.0 - diagonal,
11543 },
11544 );
11545 assert_point_near(
11546 inner_end,
11547 Point {
11548 x: 112.0 + 30.0 * 75.0_f64.to_radians().cos(),
11549 y: 62.0 + 30.0 * 75.0_f64.to_radians().sin(),
11550 },
11551 );
11552 let inner_diagonal = 15.0 * 2.0_f64.sqrt();
11553 assert_point_near(
11554 inner_start,
11555 Point {
11556 x: 112.0 + inner_diagonal,
11557 y: 62.0 - inner_diagonal,
11558 },
11559 );
11560 assert_eq!(commands.last(), Some(&PathCommand::Close));
11561
11562 let area_plot = fixtures
11563 .iter()
11564 .find(|(kind, _, _)| *kind == "areaChart")
11565 .unwrap();
11566 let mut area =
11567 CT_ChartSpace::from_xml(chart_with_optional_axes(&area_plot.1, area_plot.2).as_bytes())
11568 .unwrap();
11569 let area_geometry = render_geometry(&area.chart, chart_bounds()).unwrap();
11570 assert_path_points(
11571 path_commands(&area_geometry)[0],
11572 &[
11573 Point { x: 74.0, y: 62.0 },
11574 Point { x: 150.0, y: 12.0 },
11575 Point { x: 150.0, y: 112.0 },
11576 Point { x: 74.0, y: 112.0 },
11577 ],
11578 true,
11579 );
11580
11581 let Plot::Area {
11582 grouping, series, ..
11583 } = &mut area.chart.plot_area.plots_mut().unwrap()[0]
11584 else {
11585 unreachable!()
11586 };
11587 *grouping = Grouping::Stacked;
11588 let mut second = series[0].clone();
11589 second.index = 1;
11590 second.order = 1;
11591 second.values.values = vec![2.0, 1.0];
11592 series.push(second);
11593 let stacked_area = render_geometry(&area.chart, chart_bounds()).unwrap();
11594 assert_path_points(
11595 path_commands(&stacked_area)[1],
11596 &[
11597 Point { x: 74.0, y: 12.0 },
11598 Point { x: 150.0, y: 12.0 },
11599 Point {
11600 x: 150.0,
11601 y: 112.0 - 200.0 / 3.0,
11602 },
11603 Point {
11604 x: 74.0,
11605 y: 112.0 - 100.0 / 3.0,
11606 },
11607 ],
11608 true,
11609 );
11610 }
11611
11612 #[test]
11613 fn sparse_cache_indexes_preserve_slots_and_scatter_pairing() {
11614 let series = sparse_category_series();
11615 let bar = format!(
11616 r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{series}<q:gapWidth val="150"/><q:overlap val="0"/><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:barChart>"#
11617 );
11618 let bar = CT_ChartSpace::from_xml(chart_with_plot(&bar).as_bytes()).unwrap();
11619 assert_rects_near(
11620 &path_bounds(&render_geometry(&bar.chart, chart_bounds()).unwrap()),
11621 &[
11622 Rect {
11623 x: 51.2,
11624 y: 62.0,
11625 width: 304.0 / 15.0,
11626 height: 50.0,
11627 },
11628 Rect {
11629 x: 36.0 + 304.0 / 3.0 + 15.2,
11630 y: 12.0,
11631 width: 304.0 / 15.0,
11632 height: 100.0,
11633 },
11634 ],
11635 );
11636
11637 let line = format!(
11638 r#"<q:lineChart><q:grouping val="standard"/>{series}<q:marker val="0"/><q:smooth val="0"/><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:lineChart>"#
11639 );
11640 let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line).as_bytes()).unwrap();
11641 let first = Point {
11642 x: 36.0 + 76.0 / 3.0,
11643 y: 62.0,
11644 };
11645 let missing = Point { x: 112.0, y: 112.0 };
11646 let last = Point {
11647 x: 36.0 + 380.0 / 3.0,
11648 y: 12.0,
11649 };
11650 let gap = render_geometry(&line.chart, chart_bounds()).unwrap();
11651 let gap_commands = path_commands(&gap);
11652 assert_eq!(gap_commands.len(), 2);
11653 assert_path_points(
11654 gap_commands[0],
11655 &[Point {
11656 x: first.x,
11657 y: 112.0,
11658 }],
11659 false,
11660 );
11661 assert_path_points(gap_commands[1], &[last], false);
11662 line.chart.disp_blanks_as = DispBlanksAs::Zero;
11663 assert_path_points(
11664 path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
11665 &[first, missing, last],
11666 false,
11667 );
11668 line.chart.disp_blanks_as = DispBlanksAs::Span;
11669 assert_path_points(
11670 path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
11671 &[
11672 Point {
11673 x: first.x,
11674 y: 112.0,
11675 },
11676 last,
11677 ],
11678 false,
11679 );
11680
11681 let area = format!(
11682 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:areaChart>"#
11683 );
11684 let mut area = CT_ChartSpace::from_xml(chart_with_plot(&area).as_bytes()).unwrap();
11685 let area_gap = render_geometry(&area.chart, chart_bounds()).unwrap();
11686 let area_gap_commands = path_commands(&area_gap);
11687 assert_eq!(area_gap_commands.len(), 2);
11688 assert_path_points(
11689 area_gap_commands[0],
11690 &[
11691 first,
11692 Point {
11693 x: first.x,
11694 y: 112.0,
11695 },
11696 ],
11697 true,
11698 );
11699 assert_path_points(
11700 area_gap_commands[1],
11701 &[
11702 last,
11703 Point {
11704 x: last.x,
11705 y: 112.0,
11706 },
11707 ],
11708 true,
11709 );
11710 area.chart.disp_blanks_as = DispBlanksAs::Zero;
11711 assert_path_points(
11712 path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
11713 &[
11714 first,
11715 missing,
11716 last,
11717 Point {
11718 x: last.x,
11719 y: 112.0,
11720 },
11721 missing,
11722 Point {
11723 x: first.x,
11724 y: 112.0,
11725 },
11726 ],
11727 true,
11728 );
11729 area.chart.disp_blanks_as = DispBlanksAs::Span;
11730 assert_path_points(
11731 path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
11732 &[
11733 first,
11734 last,
11735 Point {
11736 x: last.x,
11737 y: 112.0,
11738 },
11739 Point {
11740 x: first.x,
11741 y: 112.0,
11742 },
11743 ],
11744 true,
11745 );
11746
11747 let radar = format!(
11748 r#"<q:radarChart><q:radarStyle val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#
11749 );
11750 let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
11751 assert_path_points(
11752 path_commands(&render_geometry(&radar.chart, chart_bounds()).unwrap())[0],
11753 &[
11754 Point { x: 112.0, y: 37.0 },
11755 Point {
11756 x: 112.0 - 25.0 * 3.0_f64.sqrt(),
11757 y: 87.0,
11758 },
11759 ],
11760 true,
11761 );
11762
11763 let scatter = format!(
11764 r#"<q:scatterChart><q:scatterStyle val="lineMarker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:scatterChart>"#,
11765 sparse_scatter_series()
11766 );
11767 let mut scatter = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
11768 let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
11769 assert_eq!(geometry_children(&scatter_geometry).len(), 4);
11770 let gap_commands = path_commands(&scatter_geometry);
11771 assert_path_points(gap_commands[0], &[Point { x: 36.0, y: 112.0 }], false);
11772 assert_path_points(gap_commands[1], &[Point { x: 188.0, y: 12.0 }], false);
11773
11774 scatter.chart.disp_blanks_as = DispBlanksAs::Zero;
11775 let zero = render_geometry(&scatter.chart, chart_bounds()).unwrap();
11776 assert_path_points(
11777 path_commands(&zero)[0],
11778 &[
11779 Point { x: 74.0, y: 87.0 },
11780 Point { x: 36.0, y: 62.0 },
11781 Point { x: 150.0, y: 112.0 },
11782 Point { x: 188.0, y: 12.0 },
11783 ],
11784 false,
11785 );
11786
11787 scatter.chart.disp_blanks_as = DispBlanksAs::Span;
11788 assert_path_points(
11789 path_commands(&render_geometry(&scatter.chart, chart_bounds()).unwrap())[0],
11790 &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
11791 false,
11792 );
11793 }
11794
11795 #[test]
11796 fn sparse_scatter_domains_and_labels_use_only_matched_points() {
11797 let scatter = format!(
11798 r#"<q:scatterChart><q:scatterStyle val="lineMarker"/>{}<q:dLbls><q:showVal val="1"/></q:dLbls><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:scatterChart>"#,
11799 sparse_scatter_series()
11800 );
11801 let chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
11802 let plot = &chart.chart.plot_area.plots().unwrap()[0];
11803 assert_eq!(
11804 super::scatter_domains(plot, DispBlanksAs::Gap).unwrap(),
11805 (
11806 Domain {
11807 min: 10.0,
11808 max: 40.0,
11809 },
11810 Domain {
11811 min: 100.0,
11812 max: 400.0,
11813 },
11814 )
11815 );
11816 let axes = chart.chart.plot_area.axes().unwrap();
11817 let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
11818 let anchors =
11819 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
11820 assert_eq!(
11821 anchors.keys().copied().collect::<Vec<_>>(),
11822 vec![(0, 0), (0, 3)]
11823 );
11824 assert_eq!(anchors[&(0, 0)].center, Point { x: 0.0, y: 140.0 });
11825 assert_eq!(anchors[&(0, 3)].center, Point { x: 200.0, y: 0.0 });
11826 }
11827
11828 #[test]
11829 fn sparse_bubble_size_labels_resolve_logical_indexes() {
11830 let scatter = format!(
11831 r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:dLbls><q:showBubbleSize val="1"/></q:dLbls><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:scatterChart>"#,
11832 sparse_scatter_series()
11833 );
11834 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
11835 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
11836 let Plot::Scatter { series, .. } = plot else {
11837 panic!("expected scatter plot");
11838 };
11839 series[0].bubble_size = Some(
11840 NumericData::from_xml(
11841 format!(
11842 r#"<q:numRef xmlns:q="{C_NS}"><q:f>Sheet1!$C$2:$C$5</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="4"/><q:pt idx="0"><q:v>5</q:v></q:pt><q:pt idx="3"><q:v>9</q:v></q:pt></q:numCache></q:numRef>"#
11843 )
11844 .as_bytes(),
11845 )
11846 .unwrap(),
11847 );
11848 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
11849 let mut rendered = Vec::new();
11850 super::render_data_labels(
11851 plot,
11852 chart_bounds(),
11853 DispBlanksAs::Gap,
11854 super::GeometryOptions::default(),
11855 &mut fonts,
11856 &mut rendered,
11857 )
11858 .unwrap();
11859 let text: Vec<_> = rendered
11860 .iter()
11861 .filter_map(|element| match element {
11862 PositionedElement::Text(run) => Some(run.text.as_str()),
11863 _ => None,
11864 })
11865 .collect();
11866 assert_eq!(text, vec!["5", "9"]);
11867 }
11868
11869 #[test]
11870 fn geometry_rejects_invalid_bounds_and_opaque_plots() {
11871 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11872 for bounds in [
11873 Rect {
11874 x: 0.0,
11875 y: 0.0,
11876 width: f64::NAN,
11877 height: 100.0,
11878 },
11879 Rect {
11880 x: 0.0,
11881 y: 0.0,
11882 width: 0.0,
11883 height: 100.0,
11884 },
11885 Rect {
11886 x: 0.0,
11887 y: 0.0,
11888 width: 40.0,
11889 height: 30.0,
11890 },
11891 ] {
11892 assert!(render_geometry(&chart.chart, bounds).is_err());
11893 }
11894
11895 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
11896 unreachable!()
11897 };
11898 series[0].values.values.clear();
11899 assert!(render_geometry(&chart.chart, chart_bounds()).is_err());
11900
11901 let opaque = CT_ChartSpace::from_xml(
11902 chart_with_plot(r#"<q:bar3DChart><q:barDir val="col"/></q:bar3DChart>"#).as_bytes(),
11903 )
11904 .unwrap();
11905 let error = render_geometry(&opaque.chart, chart_bounds()).unwrap_err();
11906 assert!(error.to_string().contains("unsupported or combination"));
11907
11908 let (scatter_plot, _) = remaining_plot_fixtures()
11909 .into_iter()
11910 .find(|(kind, _, _)| *kind == "scatterChart")
11911 .map(|(_, plot, axes)| (plot, axes))
11912 .unwrap();
11913 let mut scatter =
11914 CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
11915 .unwrap();
11916 for invalid in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
11917 let Plot::Scatter { series, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
11918 else {
11919 unreachable!()
11920 };
11921 let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
11922 unreachable!()
11923 };
11924 x_values.values[0] = invalid;
11925 let error = render_geometry(&scatter.chart, chart_bounds()).unwrap_err();
11926 assert!(error.to_string().contains("xVal"));
11927 }
11928 }
11929
11930 #[test]
11931 fn finite_extremes_never_produce_nonfinite_geometry() {
11932 let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
11933 let Plot::Line { series, .. } = &mut line.chart.plot_area.plots_mut().unwrap()[0] else {
11934 unreachable!()
11935 };
11936 series[0].values.values = vec![-f64::MAX, f64::MAX];
11937 let extreme_line = render_geometry(&line.chart, chart_bounds()).unwrap();
11938 assert_path_points(
11939 path_commands(&extreme_line)[0],
11940 &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
11941 false,
11942 );
11943
11944 let mut stacked =
11945 CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11946 let Plot::Bar {
11947 grouping, series, ..
11948 } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
11949 else {
11950 unreachable!()
11951 };
11952 *grouping = BarGrouping::Stacked;
11953 series[0].values.values = vec![f64::MAX, 1.0];
11954 let mut second = series[0].clone();
11955 second.index = 1;
11956 second.order = 1;
11957 series.push(second);
11958 let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
11959 assert!(error.to_string().contains("stacked value total"));
11960
11961 let Plot::Bar { grouping, .. } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
11962 else {
11963 unreachable!()
11964 };
11965 *grouping = BarGrouping::PercentStacked;
11966 let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
11967 assert!(error.to_string().contains("stacked percentage total"));
11968
11969 let (pie_plot, _) = remaining_plot_fixtures()
11970 .into_iter()
11971 .find(|(kind, _, _)| *kind == "pieChart")
11972 .map(|(_, plot, axes)| (plot, axes))
11973 .unwrap();
11974 let mut pie =
11975 CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot, false).as_bytes()).unwrap();
11976 let Plot::Pie { series, .. } = &mut pie.chart.plot_area.plots_mut().unwrap()[0] else {
11977 unreachable!()
11978 };
11979 series[0].values.values = vec![f64::MAX, f64::MAX];
11980 let error = render_geometry(&pie.chart, chart_bounds()).unwrap_err();
11981 assert!(error.to_string().contains("pie value total"));
11982
11983 let (scatter_plot, _) = remaining_plot_fixtures()
11984 .into_iter()
11985 .find(|(kind, _, _)| *kind == "scatterChart")
11986 .map(|(_, plot, axes)| (plot, axes))
11987 .unwrap();
11988 let mut scatter =
11989 CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
11990 .unwrap();
11991 let Plot::Scatter { style, series, .. } =
11992 &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
11993 else {
11994 unreachable!()
11995 };
11996 *style = ScatterStyle::Line;
11997 let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
11998 unreachable!()
11999 };
12000 x_values.values = vec![-f64::MAX, f64::MAX];
12001 series[0].values.values = vec![-f64::MAX, f64::MAX];
12002 let extreme_scatter = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12003 assert_path_points(
12004 path_commands(&extreme_scatter)[0],
12005 &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
12006 false,
12007 );
12008 }
12009
12010 #[test]
12011 fn geometry_is_backend_neutral_and_deterministic() {
12012 let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12013 let first = render_geometry(&chart.chart, chart_bounds()).unwrap();
12014 let second = render_geometry(&chart.chart, chart_bounds()).unwrap();
12015 assert_eq!(
12016 format!("{:?}", first.elements),
12017 format!("{:?}", second.elements)
12018 );
12019
12020 let raster = |geometry: ChartGeometry| {
12021 let layout = LayoutResult::new(
12022 vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
12023 Vec::new(),
12024 None,
12025 Vec::new(),
12026 );
12027 oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
12028 };
12029 assert_eq!(raster(first), raster(second));
12030 }
12031
12032 #[test]
12033 fn zero_to_one_hundred_axis_uses_expected_ticks() {
12034 let scale = nice_number_scale(
12035 Domain {
12036 min: 0.0,
12037 max: 100.0,
12038 },
12039 None,
12040 None,
12041 6,
12042 )
12043 .unwrap();
12044 assert_eq!(scale.ticks, vec![0.0, 20.0, 40.0, 60.0, 80.0, 100.0]);
12045 }
12046
12047 #[test]
12048 fn nice_number_ticks_cover_unpinned_extents() {
12049 for domain in [
12050 Domain {
12051 min: 3.0,
12052 max: 97.0,
12053 },
12054 Domain {
12055 min: -97.0,
12056 max: -3.0,
12057 },
12058 Domain {
12059 min: -3.5,
12060 max: 8.25,
12061 },
12062 Domain {
12063 min: 0.125,
12064 max: 0.875,
12065 },
12066 Domain {
12067 min: 1.0e12,
12068 max: 7.0e12,
12069 },
12070 Domain {
12071 min: -f64::MAX,
12072 max: f64::MAX,
12073 },
12074 Domain {
12075 min: f64::from_bits(1),
12076 max: f64::from_bits(5),
12077 },
12078 ] {
12079 let scale = nice_number_scale(domain, None, None, 6).unwrap();
12080 assert!(scale.domain.min <= domain.min);
12081 assert!(scale.domain.max >= domain.max);
12082 assert!(scale.ticks.iter().all(|tick| tick.is_finite()));
12083 assert!(scale.ticks.windows(2).all(|pair| pair[0] < pair[1]));
12084 }
12085
12086 let constant = nice_number_scale(Domain { min: 4.0, max: 4.0 }, None, None, 6).unwrap();
12087 assert!(constant.domain.min < 4.0 && constant.domain.max > 4.0);
12088 }
12089
12090 #[test]
12091 fn one_sided_explicit_scale_beyond_data_derives_a_valid_opposite_bound() {
12092 let minimum = nice_number_scale(
12093 Domain {
12094 min: 0.0,
12095 max: 10.0,
12096 },
12097 Some(20.0),
12098 None,
12099 6,
12100 )
12101 .unwrap();
12102 assert_eq!(minimum.domain.min, 20.0);
12103 assert!(minimum.domain.max > 20.0);
12104 assert!(minimum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
12105
12106 let maximum = nice_number_scale(
12107 Domain {
12108 min: 0.0,
12109 max: 10.0,
12110 },
12111 None,
12112 Some(-20.0),
12113 6,
12114 )
12115 .unwrap();
12116 assert_eq!(maximum.domain.max, -20.0);
12117 assert!(maximum.domain.min < -20.0);
12118 assert!(maximum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
12119 }
12120
12121 #[test]
12122 fn standard_line_domain_does_not_force_zero() {
12123 let mut chart =
12124 CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
12125 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12126 let Plot::Line { series, .. } = plot else {
12127 panic!("expected line plot");
12128 };
12129 series[0].values.values = vec![1_000.0, 1_010.0];
12130 assert_eq!(
12131 super::plot_value_domain(plot, DispBlanksAs::Gap).unwrap(),
12132 Some(Domain {
12133 min: 1_000.0,
12134 max: 1_010.0,
12135 })
12136 );
12137 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12138 assert_path_points(
12139 path_commands(&geometry)[0],
12140 &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
12141 false,
12142 );
12143 }
12144
12145 #[test]
12146 fn explicit_scaling_bounds_and_orientation_are_honored() {
12147 let scale = nice_number_scale(
12148 Domain {
12149 min: 25.0,
12150 max: 75.0,
12151 },
12152 Some(0.0),
12153 Some(100.0),
12154 6,
12155 )
12156 .unwrap();
12157 assert_eq!(
12158 scale.domain,
12159 Domain {
12160 min: 0.0,
12161 max: 100.0
12162 }
12163 );
12164 assert_eq!(
12165 super::map_y_oriented(0.0, scale.domain, chart_bounds(), Orientation::MaxMin).unwrap(),
12166 0.0
12167 );
12168 assert_eq!(
12169 super::map_y_oriented(100.0, scale.domain, chart_bounds(), Orientation::MaxMin)
12170 .unwrap(),
12171 140.0
12172 );
12173
12174 let mut chart = labelled_bar_chart();
12175 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12176 panic!("expected bar plot");
12177 };
12178 series[0].values.values = vec![25.0, 75.0];
12179 let value_axis = chart
12180 .chart
12181 .plot_area
12182 .axes
12183 .iter_mut()
12184 .find(|axis| axis.kind == AxisKind::Value)
12185 .unwrap();
12186 value_axis.scaling.orientation = Orientation::MaxMin;
12187 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12188 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12189 let plot = group
12190 .children
12191 .iter()
12192 .find_map(|element| match element {
12193 PositionedElement::Group(group) => Some(group),
12194 _ => None,
12195 })
12196 .unwrap();
12197 let bars: Vec<_> = plot
12198 .children
12199 .iter()
12200 .filter_map(|element| match element {
12201 PositionedElement::Path(path) => path.path.bounds(),
12202 _ => None,
12203 })
12204 .collect();
12205 assert_eq!(bars.len(), 2);
12206 assert!((bars[0].y - 12.0).abs() < 1.0e-9);
12207 assert!((bars[0].height - 25.0).abs() < 1.0e-9);
12208 assert!((bars[1].height - 75.0).abs() < 1.0e-9);
12209 }
12210
12211 #[test]
12212 fn family_label_anchors_share_rendered_geometry() {
12213 let mut bar = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12214 let bar_plot = &mut bar.chart.plot_area.plots_mut().unwrap()[0];
12215 let Plot::Bar { series, .. } = bar_plot else {
12216 panic!("expected bar plot");
12217 };
12218 let mut second = series[0].clone();
12219 second.index = 1;
12220 second.order = 1;
12221 series.push(second);
12222 let anchors = super::plot_label_anchors(
12223 bar_plot,
12224 chart_bounds(),
12225 DispBlanksAs::Gap,
12226 super::GeometryOptions::default(),
12227 )
12228 .unwrap();
12229 assert_point_near(anchors[&(0, 0)].center, Point { x: 40.0, y: 105.0 });
12230 assert_point_near(anchors[&(1, 0)].center, Point { x: 60.0, y: 105.0 });
12231
12232 let pie = CT_ChartSpace::from_xml(
12233 chart_with_optional_axes(
12234 &remaining_plot_fixtures()
12235 .into_iter()
12236 .find(|(kind, _, _)| *kind == "pieChart")
12237 .unwrap()
12238 .1,
12239 false,
12240 )
12241 .as_bytes(),
12242 )
12243 .unwrap();
12244 let Plot::Pie {
12245 first_slice_angle,
12246 series,
12247 ..
12248 } = &pie.chart.plot_area.plots().unwrap()[0]
12249 else {
12250 panic!("expected pie plot");
12251 };
12252 let slices = super::pie_slices(*first_slice_angle, None, series, chart_bounds()).unwrap();
12253 assert_eq!(slices.len(), 2);
12254 let first_span = std::f64::consts::TAU / 3.0;
12255 let first_midpoint = f64::from(*first_slice_angle).to_radians()
12256 - std::f64::consts::FRAC_PI_2
12257 + first_span / 2.0;
12258 assert_point_near(
12259 slices[0].anchor,
12260 super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, first_midpoint),
12261 );
12262 let second_midpoint = first_midpoint + std::f64::consts::PI;
12263 assert_point_near(
12264 slices[1].anchor,
12265 super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, second_midpoint),
12266 );
12267
12268 let radar = format!(
12269 r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12270 plot_series(0)
12271 );
12272 let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
12273 let Plot::Radar { series, .. } = &radar.chart.plot_area.plots().unwrap()[0] else {
12274 panic!("expected radar plot");
12275 };
12276 let radar_points =
12277 super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
12278 .unwrap();
12279 assert_eq!(radar_points[0][0], (0, Point { x: 100.0, y: 35.0 }));
12280 assert_eq!(radar_points[0][1], (1, Point { x: 100.0, y: 140.0 }));
12281 }
12282
12283 #[test]
12284 fn radar_annotations_use_spokes_perimeter_labels_and_radial_gridlines() {
12285 let radar_plot = format!(
12286 r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12287 sparse_category_series()
12288 );
12289 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12290 for axis in &mut chart.chart.plot_area.axes {
12291 axis.major_tick_mark = TickMark::Cross;
12292 if axis.kind == AxisKind::Value {
12293 axis.major_gridlines = Some(super::ChartLines::default());
12294 }
12295 }
12296 let plot = &chart.chart.plot_area.plots().unwrap()[0];
12297 let axes = chart.chart.plot_area.axes().unwrap();
12298 let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
12299 let center = Point { x: 112.0, y: 62.0 };
12300 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12301 let annotations =
12302 super::render_axes_and_labels(plot, &axes, bounds, DispBlanksAs::Gap, &mut fonts)
12303 .unwrap();
12304 assert_eq!(annotations.axis_lines.len(), 4);
12305 for spoke in annotations.axis_lines.iter().take(3) {
12306 let PositionedElement::Path(spoke) = spoke else {
12307 panic!("radar spoke must be a path");
12308 };
12309 assert_eq!(
12310 spoke.path.commands.first(),
12311 Some(&PathCommand::MoveTo(center))
12312 );
12313 }
12314 assert!(!annotations.gridlines.is_empty());
12315 for gridline in &annotations.gridlines {
12316 let PositionedElement::Path(gridline) = gridline else {
12317 panic!("radar gridline must be a path");
12318 };
12319 assert_eq!(gridline.path.commands.last(), Some(&PathCommand::Close));
12320 let radii: Vec<_> = gridline
12321 .path
12322 .commands
12323 .iter()
12324 .filter_map(|command| match command {
12325 PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
12326 Some((point.x - center.x).hypot(point.y - center.y))
12327 }
12328 _ => None,
12329 })
12330 .collect();
12331 assert_eq!(radii.len(), 3);
12332 assert!(
12333 radii
12334 .windows(2)
12335 .all(|pair| (pair[0] - pair[1]).abs() < 1.0e-9)
12336 );
12337 }
12338 let text: Vec<_> = annotations
12339 .labels
12340 .iter()
12341 .filter_map(|element| match element {
12342 PositionedElement::Text(run) => Some(run.text.as_str()),
12343 _ => None,
12344 })
12345 .collect();
12346 assert!(text.contains(&"North"));
12347 assert!(text.contains(&"West"));
12348 }
12349
12350 #[test]
12351 fn radar_tick_label_positions_are_distinct() {
12352 let radar_plot = format!(
12353 r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12354 plot_series(0)
12355 );
12356 let chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12357 let plot = &chart.chart.plot_area.plots().unwrap()[0];
12358 let axes = chart.chart.plot_area.axes().unwrap();
12359 let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
12360 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12361
12362 let category_axis = axes
12363 .iter()
12364 .find(|axis| axis.kind == AxisKind::Category)
12365 .unwrap();
12366 for (position, expected) in [
12367 (TickLabelPosition::High, Point { x: 108.0, y: 0.0 }),
12368 (TickLabelPosition::Low, Point { x: 108.0, y: 25.0 }),
12369 (TickLabelPosition::NextTo, Point { x: 108.0, y: 5.0 }),
12370 ] {
12371 let mut axis = category_axis.clone();
12372 axis.tick_label_position = position;
12373 let annotations =
12374 super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
12375 .unwrap();
12376 let north = annotations
12377 .labels
12378 .iter()
12379 .find_map(|element| match element {
12380 PositionedElement::Text(run) if run.text == "North" => Some(run),
12381 _ => None,
12382 })
12383 .unwrap();
12384 assert_point_near(north.origin, expected);
12385 }
12386 let mut hidden_category = category_axis.clone();
12387 hidden_category.tick_label_position = TickLabelPosition::None;
12388 let annotations = super::render_axes_and_labels(
12389 plot,
12390 &[hidden_category],
12391 bounds,
12392 DispBlanksAs::Gap,
12393 &mut fonts,
12394 )
12395 .unwrap();
12396 assert!(annotations.labels.is_empty());
12397
12398 let narrow_bounds = Rect {
12399 x: 36.0,
12400 y: 12.0,
12401 width: 24.0,
12402 height: 100.0,
12403 };
12404 let narrow_center = Point { x: 48.0, y: 62.0 };
12405 assert_eq!(
12406 super::radar_category_label_origin(
12407 narrow_bounds,
12408 narrow_center,
12409 12.0,
12410 1,
12411 4,
12412 TickLabelPosition::High,
12413 ),
12414 Some(Point { x: 72.0, y: 65.0 })
12415 );
12416 assert_eq!(
12417 super::radar_category_label_origin(
12418 narrow_bounds,
12419 narrow_center,
12420 12.0,
12421 1,
12422 4,
12423 TickLabelPosition::NextTo,
12424 ),
12425 Some(Point { x: 66.0, y: 65.0 })
12426 );
12427
12428 let value_axis = axes
12429 .iter()
12430 .find(|axis| axis.kind == AxisKind::Value)
12431 .unwrap();
12432 for (position, expected) in [
12433 (TickLabelPosition::High, Point { x: 166.0, y: 65.0 }),
12434 (TickLabelPosition::Low, Point { x: 32.0, y: 65.0 }),
12435 (TickLabelPosition::NextTo, Point { x: 116.0, y: 65.0 }),
12436 ] {
12437 let mut axis = value_axis.clone();
12438 axis.tick_label_position = position;
12439 let annotations =
12440 super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
12441 .unwrap();
12442 let zero = annotations
12443 .labels
12444 .iter()
12445 .find_map(|element| match element {
12446 PositionedElement::Text(run) if run.text == "0" => Some(run),
12447 _ => None,
12448 })
12449 .unwrap();
12450 assert_point_near(zero.origin, expected);
12451 }
12452 let mut hidden_value = value_axis.clone();
12453 hidden_value.tick_label_position = TickLabelPosition::None;
12454 let annotations = super::render_axes_and_labels(
12455 plot,
12456 &[hidden_value],
12457 bounds,
12458 DispBlanksAs::Gap,
12459 &mut fonts,
12460 )
12461 .unwrap();
12462 assert!(annotations.labels.is_empty());
12463 }
12464
12465 #[test]
12466 fn radar_negative_and_mixed_sign_domains_preserve_values() {
12467 let radar_plot = format!(
12468 r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12469 plot_series(0)
12470 );
12471 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12472 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12473 let Plot::Radar { series, .. } = plot else {
12474 panic!("expected radar plot");
12475 };
12476 series[0].values.values = vec![-10.0, -5.0];
12477 let points =
12478 super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
12479 .unwrap();
12480 assert_eq!(points[0][0], (0, Point { x: 100.0, y: 70.0 }));
12481 assert_eq!(points[0][1], (1, Point { x: 100.0, y: 105.0 }));
12482
12483 series[0].values.values = vec![-5.0, 10.0];
12484 let options = super::GeometryOptions {
12485 value: Some(super::AxisMapping {
12486 domain: Domain {
12487 min: -10.0,
12488 max: 10.0,
12489 },
12490 orientation: Orientation::MinMax,
12491 }),
12492 ..super::GeometryOptions::default()
12493 };
12494 let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
12495 assert_eq!(points[0][0], (0, Point { x: 100.0, y: 52.5 }));
12496 assert_eq!(points[0][1], (1, Point { x: 100.0, y: 140.0 }));
12497 }
12498
12499 #[test]
12500 fn radar_explicit_bounds_suppress_out_of_range_points_and_labels() {
12501 let radar_plot = format!(
12502 r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:dLbls><q:showVal val="1"/></q:dLbls><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12503 plot_series(0)
12504 );
12505 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12506 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12507 let Plot::Radar { series, .. } = plot else {
12508 panic!("expected radar plot");
12509 };
12510 series[0].values.values = vec![25.0, 75.0];
12511 let options = super::GeometryOptions {
12512 value: Some(super::AxisMapping {
12513 domain: Domain {
12514 min: 50.0,
12515 max: 100.0,
12516 },
12517 orientation: Orientation::MinMax,
12518 }),
12519 ..super::GeometryOptions::default()
12520 };
12521 let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
12522 assert_eq!(points[0], vec![(1, Point { x: 100.0, y: 105.0 })]);
12523 let anchors =
12524 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12525 assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 1)]);
12526
12527 let Plot::Radar { series, .. } = plot else {
12528 panic!("expected radar plot");
12529 };
12530 series[0].values.values = vec![75.0, 125.0];
12531 let reversed = super::GeometryOptions {
12532 value: Some(super::AxisMapping {
12533 domain: Domain {
12534 min: 50.0,
12535 max: 100.0,
12536 },
12537 orientation: Orientation::MaxMin,
12538 }),
12539 ..super::GeometryOptions::default()
12540 };
12541 let points = super::radar_series_points(series, chart_bounds(), reversed).unwrap();
12542 assert_eq!(points[0], vec![(0, Point { x: 100.0, y: 35.0 })]);
12543 let anchors =
12544 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
12545 assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
12546
12547 let value_axis = chart
12548 .chart
12549 .plot_area
12550 .axes
12551 .iter_mut()
12552 .find(|axis| axis.kind == AxisKind::Value)
12553 .unwrap();
12554 value_axis.scaling.minimum = Some(50.0);
12555 value_axis.scaling.maximum = Some(100.0);
12556 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12557 let Plot::Radar { series, .. } = plot else {
12558 panic!("expected radar plot");
12559 };
12560 series[0].values.values = vec![25.0, 25.0];
12561 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12562 let rendered = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12563 assert!(rendered.children.iter().any(|element| matches!(
12564 element,
12565 PositionedElement::Group(group) if group.clip.is_some() && group.children.is_empty()
12566 )));
12567 assert!(
12568 rendered
12569 .children
12570 .iter()
12571 .any(|element| matches!(element, PositionedElement::Path(_)))
12572 );
12573 assert!(!rendered.children.iter().any(|element| matches!(
12574 element,
12575 PositionedElement::Text(run) if run.text == "25"
12576 )));
12577 }
12578
12579 #[test]
12580 fn inside_and_outside_label_positions_follow_family_geometry() {
12581 let mut horizontal =
12582 CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12583 let plot = &mut horizontal.chart.plot_area.plots_mut().unwrap()[0];
12584 let Plot::Bar { direction, .. } = plot else {
12585 panic!("expected bar plot");
12586 };
12587 *direction = BarDirection::Bar;
12588 let anchors = super::plot_label_anchors(
12589 plot,
12590 chart_bounds(),
12591 DispBlanksAs::Gap,
12592 super::GeometryOptions::default(),
12593 )
12594 .unwrap();
12595 let anchor = anchors[&(0, 0)];
12596 assert_point_near(
12597 anchor.origin(Some(DataLabelPosition::InsideBase)),
12598 Point { x: 5.0, y: 35.0 },
12599 );
12600 assert_point_near(
12601 anchor.origin(Some(DataLabelPosition::InsideEnd)),
12602 Point { x: 95.0, y: 35.0 },
12603 );
12604 assert_point_near(
12605 anchor.origin(Some(DataLabelPosition::OutsideEnd)),
12606 Point { x: 105.0, y: 35.0 },
12607 );
12608
12609 let mut negative =
12610 CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12611 let plot = &mut negative.chart.plot_area.plots_mut().unwrap()[0];
12612 let Plot::Bar { series, .. } = plot else {
12613 panic!("expected bar plot");
12614 };
12615 series[0].values.values = vec![-1.0, -2.0];
12616 let anchors = super::plot_label_anchors(
12617 plot,
12618 chart_bounds(),
12619 DispBlanksAs::Gap,
12620 super::GeometryOptions::default(),
12621 )
12622 .unwrap();
12623 assert_point_near(
12624 anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12625 Point { x: 50.0, y: 75.0 },
12626 );
12627 let reversed = super::GeometryOptions {
12628 value: Some(super::AxisMapping {
12629 domain: Domain {
12630 min: -2.0,
12631 max: 0.0,
12632 },
12633 orientation: Orientation::MaxMin,
12634 }),
12635 ..super::GeometryOptions::default()
12636 };
12637 let anchors =
12638 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
12639 assert_point_near(
12640 anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12641 Point { x: 50.0, y: 65.0 },
12642 );
12643
12644 let pie = CT_ChartSpace::from_xml(
12645 chart_with_optional_axes(
12646 &remaining_plot_fixtures()
12647 .into_iter()
12648 .find(|(kind, _, _)| *kind == "pieChart")
12649 .unwrap()
12650 .1,
12651 false,
12652 )
12653 .as_bytes(),
12654 )
12655 .unwrap();
12656 let Plot::Pie { series, .. } = &pie.chart.plot_area.plots().unwrap()[0] else {
12657 panic!("expected pie plot");
12658 };
12659 let pie_slice = &super::pie_slices(30, None, series, chart_bounds()).unwrap()[0];
12660 assert_point_near(
12661 super::LabelAnchor::segment(pie_slice.anchor, pie_slice.base, pie_slice.end)
12662 .origin(Some(DataLabelPosition::OutsideEnd)),
12663 Point { x: 175.0, y: 70.0 },
12664 );
12665 let doughnut_slice = &super::pie_slices(30, Some(60), series, chart_bounds()).unwrap()[0];
12666 assert_point_near(
12667 super::LabelAnchor::segment(
12668 doughnut_slice.anchor,
12669 doughnut_slice.base,
12670 doughnut_slice.end,
12671 )
12672 .origin(Some(DataLabelPosition::InsideBase)),
12673 Point { x: 147.0, y: 70.0 },
12674 );
12675
12676 let short = super::LabelAnchor::segment(
12677 Point { x: 2.0, y: 0.0 },
12678 Point { x: 0.0, y: 0.0 },
12679 Point { x: 4.0, y: 0.0 },
12680 );
12681 assert_eq!(
12682 short.origin(Some(DataLabelPosition::InsideBase)),
12683 Point { x: 2.0, y: 0.0 }
12684 );
12685 assert_eq!(
12686 short.origin(Some(DataLabelPosition::InsideEnd)),
12687 Point { x: 2.0, y: 0.0 }
12688 );
12689 let thin_bounds = Rect {
12690 x: 0.0,
12691 y: 0.0,
12692 width: 80.0,
12693 height: 80.0,
12694 };
12695 let thin = &super::pie_slices(30, Some(90), series, thin_bounds).unwrap()[0];
12696 let thin = super::LabelAnchor::segment(thin.anchor, thin.base, thin.end);
12697 assert_point_near(
12698 thin.origin(Some(DataLabelPosition::InsideBase)),
12699 Point { x: 78.0, y: 40.0 },
12700 );
12701 assert_point_near(
12702 thin.origin(Some(DataLabelPosition::InsideEnd)),
12703 Point { x: 78.0, y: 40.0 },
12704 );
12705 }
12706
12707 #[test]
12708 fn degenerate_bar_and_radar_anchors_preserve_family_direction() {
12709 let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12710 let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
12711 let Plot::Bar { series, .. } = plot else {
12712 panic!("expected bar plot");
12713 };
12714 series[0].values.values = vec![80.0, 125.0];
12715 for (direction, orientation, boundary, outside) in [
12716 (
12717 BarDirection::Column,
12718 Orientation::MinMax,
12719 Point { x: 50.0, y: 140.0 },
12720 Point { x: 50.0, y: 135.0 },
12721 ),
12722 (
12723 BarDirection::Column,
12724 Orientation::MaxMin,
12725 Point { x: 50.0, y: 0.0 },
12726 Point { x: 50.0, y: 5.0 },
12727 ),
12728 (
12729 BarDirection::Bar,
12730 Orientation::MinMax,
12731 Point { x: 0.0, y: 35.0 },
12732 Point { x: 5.0, y: 35.0 },
12733 ),
12734 (
12735 BarDirection::Bar,
12736 Orientation::MaxMin,
12737 Point { x: 200.0, y: 35.0 },
12738 Point { x: 195.0, y: 35.0 },
12739 ),
12740 ] {
12741 let Plot::Bar {
12742 direction: plot_direction,
12743 ..
12744 } = plot
12745 else {
12746 panic!("expected bar plot");
12747 };
12748 *plot_direction = direction;
12749 let options = super::GeometryOptions {
12750 value: Some(super::AxisMapping {
12751 domain: Domain {
12752 min: 80.0,
12753 max: 100.0,
12754 },
12755 orientation,
12756 }),
12757 ..super::GeometryOptions::default()
12758 };
12759 let anchors =
12760 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options)
12761 .unwrap();
12762 let anchor = anchors[&(0, 0)];
12763 assert_point_near(anchor.center, boundary);
12764 assert_point_near(anchor.base, boundary);
12765 assert_point_near(anchor.end, boundary);
12766 assert_point_near(anchor.origin(Some(DataLabelPosition::InsideBase)), boundary);
12767 assert_point_near(anchor.origin(Some(DataLabelPosition::InsideEnd)), boundary);
12768 assert_point_near(anchor.origin(Some(DataLabelPosition::OutsideEnd)), outside);
12769 }
12770
12771 let radar_plot = format!(
12772 r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12773 plot_series(0)
12774 );
12775 let mut radar = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12776 let plot = &mut radar.chart.plot_area.plots_mut().unwrap()[0];
12777 let Plot::Radar { series, .. } = plot else {
12778 panic!("expected radar plot");
12779 };
12780 series[0].values.values = vec![0.0, 0.0, 0.0, 0.0];
12781 let anchors = super::plot_label_anchors(
12782 plot,
12783 chart_bounds(),
12784 DispBlanksAs::Gap,
12785 super::GeometryOptions::default(),
12786 )
12787 .unwrap();
12788 let center = Point { x: 100.0, y: 70.0 };
12789 for logical_index in 0..4 {
12790 assert_point_near(
12791 anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideBase)),
12792 center,
12793 );
12794 assert_point_near(
12795 anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideEnd)),
12796 center,
12797 );
12798 }
12799 assert_point_near(
12800 anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12801 Point { x: 100.0, y: 65.0 },
12802 );
12803 assert_point_near(
12804 anchors[&(0, 1)].origin(Some(DataLabelPosition::OutsideEnd)),
12805 Point { x: 105.0, y: 70.0 },
12806 );
12807 assert_point_near(
12808 anchors[&(0, 2)].origin(Some(DataLabelPosition::OutsideEnd)),
12809 Point { x: 100.0, y: 75.0 },
12810 );
12811 assert_point_near(
12812 anchors[&(0, 3)].origin(Some(DataLabelPosition::OutsideEnd)),
12813 Point { x: 95.0, y: 70.0 },
12814 );
12815 }
12816
12817 #[test]
12818 fn labels_outside_explicit_axis_bounds_are_suppressed() {
12819 let mut chart =
12820 CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
12821 {
12822 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12823 let Plot::Line { series, .. } = plot else {
12824 panic!("expected line plot");
12825 };
12826 series[0].values.values = vec![25.0, 75.0];
12827 }
12828 let value_axis = chart
12829 .chart
12830 .plot_area
12831 .axes
12832 .iter_mut()
12833 .find(|axis| axis.kind == AxisKind::Value)
12834 .unwrap();
12835 value_axis.scaling.minimum = Some(50.0);
12836 value_axis.scaling.maximum = Some(100.0);
12837 value_axis.scaling.orientation = Orientation::MaxMin;
12838 let plot = &chart.chart.plot_area.plots().unwrap()[0];
12839 let axes = chart.chart.plot_area.axes().unwrap();
12840 let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
12841 let anchors =
12842 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12843 assert!(!anchors.contains_key(&(0, 0)));
12844 assert_point_near(anchors[&(0, 1)].center, Point { x: 150.0, y: 70.0 });
12845
12846 let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12847 let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
12848 let Plot::Bar { series, .. } = plot else {
12849 panic!("expected bar plot");
12850 };
12851 series[0].values.values = vec![90.0, 125.0];
12852 let options = super::GeometryOptions {
12853 value: Some(super::AxisMapping {
12854 domain: Domain {
12855 min: 80.0,
12856 max: 100.0,
12857 },
12858 orientation: Orientation::MinMax,
12859 }),
12860 ..super::GeometryOptions::default()
12861 };
12862 let anchors =
12863 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12864 assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
12865 assert_point_near(anchors[&(0, 0)].base, Point { x: 50.0, y: 140.0 });
12866 assert_point_near(anchors[&(0, 0)].end, Point { x: 50.0, y: 70.0 });
12867 assert_point_near(anchors[&(0, 0)].center, Point { x: 50.0, y: 105.0 });
12868 assert_point_near(
12869 anchors[&(0, 0)].origin(Some(DataLabelPosition::InsideBase)),
12870 Point { x: 50.0, y: 135.0 },
12871 );
12872 }
12873
12874 #[test]
12875 fn zero_bars_render_and_emit_requested_value_labels() {
12876 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12877 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12878 let Plot::Bar { series, .. } = plot else {
12879 panic!("expected bar plot");
12880 };
12881 series[0].values.values = vec![0.0, 0.0];
12882 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12883 assert_eq!(path_commands(&geometry).len(), 2);
12884 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12885 render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12886 let plot = &chart.chart.plot_area.plots().unwrap()[0];
12887 let mut labels = Vec::new();
12888 super::render_data_labels(
12889 plot,
12890 chart_bounds(),
12891 DispBlanksAs::Gap,
12892 super::GeometryOptions::default(),
12893 &mut fonts,
12894 &mut labels,
12895 )
12896 .unwrap();
12897 let text: Vec<_> = labels
12898 .iter()
12899 .filter_map(|element| match element {
12900 PositionedElement::Text(run) => Some(run.text.as_str()),
12901 _ => None,
12902 })
12903 .collect();
12904 assert_eq!(text, vec!["0", "0"]);
12905 }
12906
12907 #[test]
12908 fn axes_gridlines_and_tick_marks_follow_model_state() {
12909 let chart = labelled_bar_chart();
12910 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12911 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12912 let ranks: Vec<_> = group
12913 .children
12914 .iter()
12915 .map(|element| match element {
12916 PositionedElement::Group(group) => {
12917 assert!(group.clip.is_some());
12918 1
12919 }
12920 PositionedElement::Path(path)
12921 if path
12922 .stroke
12923 .as_ref()
12924 .is_some_and(|stroke| stroke.width == 0.5) =>
12925 {
12926 0
12927 }
12928 PositionedElement::Path(path) if path.fill.is_some() => 4,
12929 PositionedElement::Path(path) => {
12930 let bounds = path.path.bounds().expect("axis or tick bounds");
12931 assert_eq!(path.stroke.as_ref().map(|stroke| stroke.width), Some(1.0));
12932 if bounds.width > 4.0 || bounds.height > 4.0 {
12933 2
12934 } else {
12935 3
12936 }
12937 }
12938 PositionedElement::Text(_) => 5,
12939 _ => panic!("unexpected labelled chart element"),
12940 })
12941 .collect();
12942 assert!(ranks.windows(2).all(|pair| pair[0] <= pair[1]), "{ranks:?}");
12943 for required in 0..=5 {
12944 assert!(
12945 ranks.contains(&required),
12946 "missing z-order rank {required}: {ranks:?}"
12947 );
12948 }
12949 assert_eq!(ranks.iter().filter(|rank| **rank == 1).count(), 1);
12950 assert_eq!(ranks.iter().filter(|rank| **rank == 2).count(), 2);
12951
12952 let mut deleted = chart.clone();
12953 for axis in &mut deleted.chart.plot_area.axes {
12954 axis.deleted = true;
12955 }
12956 let deleted_group = render_chart(&deleted.chart, chart_bounds(), &mut fonts).unwrap();
12957 assert_eq!(
12958 deleted_group
12959 .children
12960 .iter()
12961 .filter(|element| {
12962 matches!(
12963 element,
12964 PositionedElement::Path(path) if path.stroke.is_some()
12965 )
12966 })
12967 .count(),
12968 0
12969 );
12970 }
12971
12972 #[test]
12973 fn category_ticks_cover_unlabelled_and_sparse_logical_slots() {
12974 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12975 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12976 panic!("expected bar plot");
12977 };
12978 series[0].categories = None;
12979 let plot = &chart.chart.plot_area.plots().unwrap()[0];
12980 let mut category_axis = chart
12981 .chart
12982 .plot_area
12983 .axes()
12984 .unwrap()
12985 .into_iter()
12986 .find(|axis| axis.kind == AxisKind::Category)
12987 .unwrap();
12988 category_axis.major_gridlines = Some(super::ChartLines::default());
12989 category_axis.major_tick_mark = TickMark::Cross;
12990 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12991 let annotations = super::render_axes_and_labels(
12992 plot,
12993 &[category_axis],
12994 chart_bounds(),
12995 DispBlanksAs::Gap,
12996 &mut fonts,
12997 )
12998 .unwrap();
12999 assert_eq!(annotations.gridlines.len(), 2);
13000 assert_eq!(annotations.ticks.len(), 2);
13001 assert!(annotations.labels.is_empty());
13002
13003 let sparse_plot = format!(
13004 r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{}<q:gapWidth val="150"/><q:overlap val="0"/><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:barChart>"#,
13005 sparse_category_series()
13006 );
13007 let sparse = CT_ChartSpace::from_xml(chart_with_plot(&sparse_plot).as_bytes()).unwrap();
13008 let plot = &sparse.chart.plot_area.plots().unwrap()[0];
13009 let mut category_axis = sparse
13010 .chart
13011 .plot_area
13012 .axes()
13013 .unwrap()
13014 .into_iter()
13015 .find(|axis| axis.kind == AxisKind::Category)
13016 .unwrap();
13017 category_axis.major_gridlines = Some(super::ChartLines::default());
13018 category_axis.major_tick_mark = TickMark::Cross;
13019 let annotations = super::render_axes_and_labels(
13020 plot,
13021 &[category_axis],
13022 chart_bounds(),
13023 DispBlanksAs::Gap,
13024 &mut fonts,
13025 )
13026 .unwrap();
13027 assert_eq!(annotations.gridlines.len(), 3);
13028 assert_eq!(annotations.ticks.len(), 3);
13029 assert_eq!(annotations.labels.len(), 2);
13030
13031 let oversized_plot = sparse_plot.replacen(
13032 r#"<q:ptCount val="3"/>"#,
13033 r#"<q:ptCount val="4294967295"/>"#,
13034 1,
13035 );
13036 let oversized =
13037 CT_ChartSpace::from_xml(chart_with_plot(&oversized_plot).as_bytes()).unwrap();
13038 let plot = &oversized.chart.plot_area.plots().unwrap()[0];
13039 let category_axis = oversized
13040 .chart
13041 .plot_area
13042 .axes()
13043 .unwrap()
13044 .into_iter()
13045 .find(|axis| axis.kind == AxisKind::Category)
13046 .unwrap();
13047 let result = super::render_axes_and_labels(
13048 plot,
13049 &[category_axis],
13050 chart_bounds(),
13051 DispBlanksAs::Gap,
13052 &mut fonts,
13053 );
13054 let error = match result {
13055 Err(error) => error,
13056 Ok(_) => panic!("oversized category annotations rendered"),
13057 };
13058 assert!(
13059 error
13060 .to_string()
13061 .contains("logical category count 4294967295")
13062 );
13063 assert!(error.to_string().contains("exceeds 16384"));
13064 }
13065
13066 #[test]
13067 fn unsupported_numeric_category_formats_return_projection_errors() {
13068 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13069 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
13070 panic!("expected bar plot");
13071 };
13072 series[0].categories = Some(AxisData::Numeric(
13073 NumericData::new(
13074 "Sheet1!$A$2:$A$3".to_owned(),
13075 "General".to_owned(),
13076 vec![1.0, 2.0],
13077 )
13078 .unwrap(),
13079 ));
13080 let plot = &chart.chart.plot_area.plots().unwrap()[0];
13081 let mut category_axis = chart
13082 .chart
13083 .plot_area
13084 .axes()
13085 .unwrap()
13086 .into_iter()
13087 .find(|axis| axis.kind == AxisKind::Category)
13088 .unwrap();
13089 category_axis.number_format = Some(NumberFormat::new("0.00".to_owned(), false).unwrap());
13090 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13091 let annotations = super::render_axes_and_labels(
13092 plot,
13093 &[category_axis.clone()],
13094 chart_bounds(),
13095 DispBlanksAs::Gap,
13096 &mut fonts,
13097 )
13098 .unwrap();
13099 let text: Vec<_> = annotations
13100 .labels
13101 .iter()
13102 .filter_map(|element| match element {
13103 PositionedElement::Text(run) => Some(run.text.as_str()),
13104 _ => None,
13105 })
13106 .collect();
13107 assert_eq!(text, vec!["1.00", "2.00"]);
13108
13109 category_axis.number_format = Some(NumberFormat::new("#,##0".to_owned(), false).unwrap());
13110 let result = super::render_axes_and_labels(
13111 plot,
13112 &[category_axis],
13113 chart_bounds(),
13114 DispBlanksAs::Gap,
13115 &mut fonts,
13116 );
13117 let error = match result {
13118 Err(error) => error,
13119 Ok(_) => panic!("unsupported category-axis format rendered"),
13120 };
13121 assert!(
13122 error
13123 .to_string()
13124 .contains("c:numFmt/@formatCode projection")
13125 );
13126 assert!(error.to_string().contains("#,##0"));
13127 }
13128
13129 #[test]
13130 fn labels_and_legend_shape_with_deterministic_fonts() {
13131 let chart = labelled_bar_chart();
13132 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13133 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13134 let labels: Vec<_> = group
13135 .children
13136 .iter()
13137 .filter_map(|element| match element {
13138 PositionedElement::Text(run) => Some(run),
13139 _ => None,
13140 })
13141 .collect();
13142 for text in ["North", "South", "20", "Sales", "100"] {
13143 assert!(labels.iter().any(|run| run.text == text), "missing {text}");
13144 }
13145 let styled_tick = labels.iter().find(|run| run.text == "20").unwrap();
13146 assert_eq!(styled_tick.font_size, 12.0);
13147 assert!(styled_tick.bold);
13148 assert!(
13149 labels
13150 .iter()
13151 .all(|run| !run.glyph_ids.is_empty() && run.glyph_ids.len() == run.advances.len())
13152 );
13153 }
13154
13155 #[test]
13156 fn labelled_chart_raster_is_deterministic() {
13157 let raster = || {
13158 let chart = labelled_bar_chart();
13159 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13160 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13161 let layout = LayoutResult::new(
13162 vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
13163 fonts.all_font_data(),
13164 None,
13165 Vec::new(),
13166 );
13167 oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
13168 };
13169 assert_eq!(raster(), raster());
13170 }
13171
13172 fn labelled_bar_chart() -> CT_ChartSpace {
13173 let series = plot_series(0)
13174 .replace(
13175 "<q:cat>",
13176 "<q:tx><q:strRef><q:f>Sheet1!$B$1</q:f><q:strCache><q:ptCount val=\"1\"/><q:pt idx=\"0\"><q:v>Sales</q:v></q:pt></q:strCache></q:strRef></q:tx><q:cat>",
13177 )
13178 .replace("<q:v>1</q:v>", "<q:v>0</q:v>")
13179 .replace("<q:v>2</q:v>", "<q:v>100</q:v>");
13180 let plot = format!(
13181 r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{series}<q:dLbls><q:numFmt formatCode="0" sourceLinked="0"/><q:showVal val="1"/></q:dLbls><q:gapWidth val="150"/><q:overlap val="0"/><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:barChart>"#
13182 );
13183 let xml = chart_with_plot(&plot)
13184 .replacen(
13185 "<q:scaling/>",
13186 "<q:scaling><q:orientation val=\"minMax\"/></q:scaling>",
13187 1,
13188 )
13189 .replacen(
13190 "<q:scaling/>",
13191 "<q:scaling><q:max val=\"100\"/><q:min val=\"0\"/></q:scaling>",
13192 1,
13193 )
13194 .replace(
13195 "<q:axPos val=\"b\"/>",
13196 "<q:axPos val=\"b\"/><q:majorTickMark val=\"cross\"/>",
13197 )
13198 .replace(
13199 "<q:axPos val=\"l\"/>",
13200 "<q:axPos val=\"l\"/><q:majorGridlines/><q:numFmt formatCode=\"0\" sourceLinked=\"0\"/><q:majorTickMark val=\"out\"/><q:txPr><a:bodyPr/><a:p><a:pPr><a:defRPr sz=\"1200\" b=\"1\"><a:latin typeface=\"Carlito\"/></a:defRPr></a:pPr></a:p></q:txPr>",
13201 )
13202 .replace("</q:plotArea></q:chart>", "</q:plotArea><q:legend/></q:chart>");
13203 CT_ChartSpace::from_xml(xml.as_bytes()).unwrap()
13204 }
13205
13206 fn chart_bounds() -> Rect {
13207 Rect {
13208 x: 0.0,
13209 y: 0.0,
13210 width: 200.0,
13211 height: 140.0,
13212 }
13213 }
13214
13215 fn four_series_bar_chart() -> CT_ChartSpace {
13216 let series = (0..4).map(plot_series).collect::<String>();
13217 let plot = bar_plot("").replace(&plot_series(0), &series);
13218 CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
13219 }
13220
13221 fn seven_series_bar_chart() -> CT_ChartSpace {
13222 let series = (0..7).map(plot_series).collect::<String>();
13223 let plot = bar_plot("").replace(&plot_series(0), &series);
13224 CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
13225 }
13226
13227 fn path_colours(geometry: &ChartGeometry) -> Vec<Color> {
13228 let mut colours = geometry_children(geometry)
13229 .iter()
13230 .filter_map(|element| match element {
13231 PositionedElement::Path(path) => match path
13232 .fill
13233 .as_ref()
13234 .or_else(|| path.stroke.as_ref().map(|stroke| &stroke.paint))
13235 {
13236 Some(Paint::Solid(colour)) => Some(*colour),
13237 _ => None,
13238 },
13239 _ => None,
13240 })
13241 .collect::<Vec<_>>();
13242 colours.dedup_by(|left, right| left == right);
13243 colours
13244 }
13245
13246 fn resolved_layout_colour(colour: oxml_drawing::color::ResolvedColor) -> Color {
13247 Color {
13248 r: f64::from(colour.red) / 255.0,
13249 g: f64::from(colour.green) / 255.0,
13250 b: f64::from(colour.blue) / 255.0,
13251 a: f64::from(colour.alpha) / 255.0,
13252 }
13253 }
13254
13255 fn geometry_children(geometry: &ChartGeometry) -> &[PositionedElement] {
13256 let [PositionedElement::Group(group)] = geometry.elements.as_slice() else {
13257 panic!("chart geometry must contain one group");
13258 };
13259 &group.children
13260 }
13261
13262 fn path_commands(geometry: &ChartGeometry) -> Vec<&[PathCommand]> {
13263 geometry_children(geometry)
13264 .iter()
13265 .map(|element| {
13266 let PositionedElement::Path(path) = element else {
13267 panic!("chart geometry child must be a path");
13268 };
13269 path.path.commands.as_slice()
13270 })
13271 .collect()
13272 }
13273
13274 fn path_bounds(geometry: &ChartGeometry) -> Vec<Rect> {
13275 geometry_children(geometry)
13276 .iter()
13277 .map(|element| {
13278 let PositionedElement::Path(path) = element else {
13279 panic!("chart geometry child must be a path");
13280 };
13281 path.path.bounds().expect("chart path has bounds")
13282 })
13283 .collect()
13284 }
13285
13286 fn assert_rect_near(actual: Rect, expected: Rect) {
13287 assert_near(actual.x, expected.x);
13288 assert_near(actual.y, expected.y);
13289 assert_near(actual.width, expected.width);
13290 assert_near(actual.height, expected.height);
13291 }
13292
13293 fn assert_rects_near(actual: &[Rect], expected: &[Rect]) {
13294 assert_eq!(actual.len(), expected.len());
13295 for (actual, expected) in actual.iter().copied().zip(expected.iter().copied()) {
13296 assert_rect_near(actual, expected);
13297 }
13298 }
13299
13300 fn assert_path_points(commands: &[PathCommand], expected: &[Point], closed: bool) {
13301 let points: Vec<_> = commands
13302 .iter()
13303 .filter_map(|command| match command {
13304 PathCommand::MoveTo(point) | PathCommand::LineTo(point) => Some(*point),
13305 PathCommand::Close => None,
13306 PathCommand::CurveTo { .. } => panic!("expected polygonal path"),
13307 })
13308 .collect();
13309 assert_eq!(points.len(), expected.len());
13310 for (actual, expected) in points.into_iter().zip(expected.iter().copied()) {
13311 assert_point_near(actual, expected);
13312 }
13313 assert_eq!(commands.last() == Some(&PathCommand::Close), closed);
13314 }
13315
13316 fn assert_point_near(actual: Point, expected: Point) {
13317 assert_near(actual.x, expected.x);
13318 assert_near(actual.y, expected.y);
13319 }
13320
13321 fn assert_near(actual: f64, expected: f64) {
13322 assert!(
13323 (actual - expected).abs() < 1e-9,
13324 "expected {expected}, got {actual}"
13325 );
13326 }
13327
13328 #[test]
13329 fn remaining_v1_plots_round_trip_and_render() {
13330 for (kind, plot, axes) in remaining_plot_fixtures() {
13331 let xml = chart_with_optional_axes(&plot, axes);
13332 let parsed = CT_ChartSpace::from_xml(xml.as_bytes())
13333 .unwrap_or_else(|error| panic!("{kind}: parse failed: {error}"));
13334 assert_eq!(parsed.chart.plot_area.plots().unwrap().len(), 1);
13335 assert_eq!(
13336 parsed.chart.plot_area.axes().unwrap().len(),
13337 usize::from(axes) * 2
13338 );
13339 let written = parsed
13340 .to_xml()
13341 .unwrap_or_else(|error| panic!("{kind}: write failed: {error}"));
13342 assert_eq!(
13343 parsed,
13344 CT_ChartSpace::from_xml(&written)
13345 .unwrap_or_else(|error| panic!("{kind}: reparse failed: {error}"))
13346 );
13347 }
13348 if let Some(corpus) = require_or_skip_corpus() {
13349 verify_remaining_plot_viewer_gate(&corpus);
13350 }
13351 }
13352
13353 #[test]
13354 fn remaining_plot_families_write_fixed_prefixes_in_schema_order() {
13355 for (kind, plot, axes) in remaining_plot_fixtures() {
13356 let parsed =
13357 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
13358 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13359 assert!(written.contains(&format!("<c:{kind}")));
13360 assert!(!written.contains(&format!("<q:{kind}")));
13361 let series = written.find("<c:ser").unwrap();
13362 let labels = written.find("<c:dLbls").unwrap();
13363 assert!(series < labels, "{kind}: series must precede labels");
13364 if matches!(kind, "areaChart" | "scatterChart" | "radarChart") {
13365 let property = match kind {
13366 "areaChart" => "<c:grouping",
13367 "scatterChart" => "<c:scatterStyle",
13368 "radarChart" => "<c:radarStyle",
13369 _ => unreachable!(),
13370 };
13371 assert!(written.find(property).unwrap() < series);
13372 assert!(labels < written.find("<c:axId").unwrap());
13373 } else {
13374 assert!(labels < written.find("<c:firstSliceAng").unwrap());
13375 }
13376 if kind == "doughnutChart" {
13377 assert!(
13378 written.find("<c:firstSliceAng").unwrap()
13379 < written.find("<c:holeSize").unwrap()
13380 );
13381 }
13382 }
13383 }
13384
13385 #[test]
13386 fn scatter_series_map_numeric_categories_and_values_to_x_and_y() {
13387 let plot = remaining_plot_fixtures()
13388 .into_iter()
13389 .find(|(kind, _, _)| *kind == "scatterChart")
13390 .unwrap()
13391 .1;
13392 let mut parsed =
13393 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, true).as_bytes()).unwrap();
13394 let standalone_series = {
13395 let Plot::Scatter { series, .. } = &mut parsed.chart.plot_area.plots_mut().unwrap()[0]
13396 else {
13397 panic!("expected scatter plot");
13398 };
13399 let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
13400 panic!("expected numeric x values");
13401 };
13402 x_values.values[0] = 3.5;
13403 series[0].values.values[0] = 7.5;
13404 series[0].clone()
13405 };
13406 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13407 assert!(written.contains("<c:xVal>"));
13408 assert!(written.contains("<c:yVal>"));
13409 assert!(!written.contains("<c:cat>"));
13410 assert!(!written.contains("<c:val>"));
13411 assert!(written.contains("<c:v>3.5</c:v>"));
13412 assert!(written.contains("<c:v>7.5</c:v>"));
13413 let standalone = String::from_utf8(standalone_series.to_xml().unwrap()).unwrap();
13414 assert!(standalone.contains("<c:xVal>"));
13415 assert!(standalone.contains("<c:yVal>"));
13416 assert!(!standalone.contains("<c:cat>"));
13417 assert!(!standalone.contains("<c:val>"));
13418 }
13419
13420 #[test]
13421 fn malformed_remaining_plots_return_errors_without_panicking() {
13422 let series = plot_series(0);
13423 let scatter = numeric_plot_series(0);
13424 let opaque_bubble = series.replace(
13425 "</q:val></q:ser>",
13426 "</q:val><q:bubbleSize><x:opaque/></q:bubbleSize></q:ser>",
13427 );
13428 let cases = [
13429 r#"<q:pieChart/>"#.to_owned(),
13430 format!(r#"<q:pieChart>{series}<q:firstSliceAng val="361"/></q:pieChart>"#),
13431 format!(
13432 r#"<q:pieChart>{series}<q:firstSliceAng val="1"/><q:firstSliceAng val="2"/></q:pieChart>"#
13433 ),
13434 format!(r#"<q:doughnutChart>{series}<q:holeSize val="9"/></q:doughnutChart>"#),
13435 format!(
13436 r#"<q:doughnutChart>{series}<q:holeSize val="50"/><q:holeSize val="60"/></q:doughnutChart>"#
13437 ),
13438 format!(
13439 r#"<q:areaChart><q:grouping val="clustered"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
13440 ),
13441 format!(
13442 r#"<q:areaChart><q:grouping val="standard"/><q:grouping val="stacked"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
13443 ),
13444 format!(
13445 r#"<q:scatterChart><q:scatterStyle val="dots"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13446 ),
13447 format!(
13448 r#"<q:scatterChart><q:scatterStyle val="marker"/><q:scatterStyle val="line"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13449 ),
13450 format!(
13451 r#"<q:radarChart><q:radarStyle val="smooth"/>{series}<q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
13452 ),
13453 format!(
13454 r#"<q:radarChart><q:radarStyle val="marker"/>{series}<q:dLbls/><q:dLbls/><q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
13455 ),
13456 format!(r#"<q:pieChart>{series}<q:axId val="1"/></q:pieChart>"#),
13457 format!(
13458 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/></q:areaChart>"#
13459 ),
13460 r#"<q:areaChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:areaChart>"#.to_owned(),
13461 format!(
13462 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/><q:axId val="2"/><q:axId val="3"/></q:areaChart>"#
13463 ),
13464 format!(
13465 r#"<q:scatterChart><q:scatterStyle val="marker"/>{series}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13466 ),
13467 format!(
13468 r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13469 scatter
13470 .replace("<q:yVal>", "<q:val>")
13471 .replace("</q:yVal>", "</q:val>")
13472 ),
13473 format!(
13474 r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13475 scatter_without_x(0)
13476 ),
13477 format!(
13478 r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13479 scatter_without_y(0)
13480 ),
13481 format!(r#"<q:pieChart>{opaque_bubble}</q:pieChart>"#),
13482 ];
13483 for plot in cases {
13484 let xml = chart_with_optional_axes(&plot, false);
13485 let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
13486 assert!(result.is_ok(), "parser panicked for {plot}");
13487 assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
13488 }
13489
13490 let missing_axis = format!(
13491 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="7"/></q:areaChart>"#
13492 );
13493 let missing_axis =
13494 CT_ChartSpace::from_xml(chart_with_optional_axes(&missing_axis, true).as_bytes())
13495 .unwrap_err();
13496 assert!(missing_axis.to_string().contains("missing axis 7"));
13497
13498 let values =
13499 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
13500 let mut bubble_series = Series::new(0, 0, values.clone());
13501 bubble_series.bubble_size = Some(values);
13502 assert!(Plot::pie(vec![bubble_series]).is_err());
13503 }
13504
13505 #[test]
13506 fn unsupported_plot_families_and_children_remain_byte_preserved() {
13507 for raw in [
13508 r#"<q:pie3DChart x:keep="3d"><q:ser><x:opaque/></q:ser></q:pie3DChart>"#,
13509 r#"<q:ofPieChart x:keep="of"><x:opaque/></q:ofPieChart>"#,
13510 r#"<q:stockChart x:keep="stock"><x:opaque/></q:stockChart>"#,
13511 r#"<q:surfaceChart x:keep="surface"><x:opaque/></q:surfaceChart>"#,
13512 r#"<q:bubbleChart x:keep="bubble"><x:opaque/></q:bubbleChart>"#,
13513 ] {
13514 let parsed =
13515 CT_ChartSpace::from_xml(chart_with_optional_axes(raw, false).as_bytes()).unwrap();
13516 assert!(parsed.chart.plot_area.plots().is_err());
13517 let written = parsed.to_xml().unwrap();
13518 assert!(
13519 written
13520 .windows(raw.len())
13521 .any(|window| window == raw.as_bytes())
13522 );
13523 }
13524
13525 let pie = remaining_plot_fixtures()[0].1.clone();
13526 let preserved = r#"<!--point--><q:dPt x:id="1"><q:explosion val="7"/><x:tail/></q:dPt>"#;
13527 let plot = pie.replace("<q:dLbls>", &format!("{preserved}<q:dLbls>"));
13528 let parsed =
13529 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, false).as_bytes()).unwrap();
13530 let written = parsed.to_xml().unwrap();
13531 assert!(
13532 written
13533 .windows(preserved.len())
13534 .any(|window| window == preserved.as_bytes())
13535 );
13536
13537 for (kind, mut plot, axes) in [
13538 ("pieChart", remaining_plot_fixtures()[0].1.clone(), false),
13539 ("areaChart", remaining_plot_fixtures()[2].1.clone(), true),
13540 ] {
13541 plot = plot.replace(r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#, "");
13542 if kind == "pieChart" {
13543 plot = plot.replace(
13544 "</q:extLst></q:pieChart>",
13545 "</q:extLst><!--after-ext--></q:pieChart>",
13546 );
13547 } else {
13548 plot = plot.replace(
13549 "</q:dropLines><q:axId",
13550 "</q:dropLines><!--after-drop-lines--><q:axId",
13551 );
13552 }
13553 let mut parsed =
13554 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
13555 let labels =
13556 CT_DLbls::from_xml(format!(r#"<c:dLbls xmlns:c="{C_NS}"/>"#).as_bytes()).unwrap();
13557 match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
13558 Plot::Pie { data_labels, .. } | Plot::Area { data_labels, .. } => {
13559 *data_labels = Some(labels)
13560 }
13561 _ => panic!("expected pie or area"),
13562 }
13563 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13564 assert!(
13565 written.find("<c:dLbls").unwrap() < written.find("<q:extLst").unwrap(),
13566 "{kind}: inserted labels must precede the preserved extension"
13567 );
13568 if kind == "areaChart" {
13569 assert!(written.find("<c:dLbls").unwrap() < written.find("<q:dropLines").unwrap());
13570 assert!(
13571 written.find("<q:dropLines").unwrap()
13572 < written.find("<!--after-drop-lines-->").unwrap()
13573 );
13574 } else {
13575 assert!(
13576 written.find("<q:extLst").unwrap() < written.find("<!--after-ext-->").unwrap()
13577 );
13578 }
13579 }
13580 }
13581
13582 #[test]
13583 fn every_supported_corpus_plot_round_trips_structurally() {
13584 let Some(corpus) = require_or_skip_corpus() else {
13585 return;
13586 };
13587 verify_fetched_corpus(&corpus);
13588 let mut pie_count = 0usize;
13589 let mut bar_count = 0usize;
13590 let mut line_count = 0usize;
13591 for path in manifest_paths() {
13592 let package = OpcPackage::open(corpus.join(path)).unwrap();
13593 for (part, xml) in &package.parts {
13594 if !is_chart_part(part) {
13595 continue;
13596 }
13597 let parsed = CT_ChartSpace::from_xml(xml).unwrap();
13598 if let Ok(plots) = parsed.chart.plot_area.plots() {
13599 for plot in plots {
13600 match plot {
13601 Plot::Pie { .. } => pie_count += 1,
13602 Plot::Bar { .. } => bar_count += 1,
13603 Plot::Line { .. } => line_count += 1,
13604 _ => {}
13605 }
13606 }
13607 }
13608 let written = parsed.to_xml().unwrap();
13609 assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
13610 }
13611 }
13612 assert_eq!(pie_count, 1, "typed corpus pie coverage changed");
13613 assert_eq!(bar_count, 11, "typed corpus bar coverage changed");
13614 assert_eq!(line_count, 2, "typed corpus line coverage changed");
13615 }
13616
13617 fn standalone_axis(local: &str, children: &str) -> String {
13618 format!(
13619 r#"<c:{local} xmlns:c="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}">{children}</c:{local}>"#
13620 )
13621 }
13622
13623 fn minimal_axis(local: &str, id: &str, position: &str, cross_axis: &str) -> String {
13624 format!(
13625 r#"<c:{local}><c:axId val="{id}"/><c:scaling/><c:axPos val="{position}"/><c:crossAx val="{cross_axis}"/></c:{local}>"#
13626 )
13627 }
13628
13629 fn chart_with_axes(axes: &[String]) -> String {
13630 format!(
13631 r#"<c:chartSpace xmlns:c="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}"><c:chart><c:plotArea>{}</c:plotArea></c:chart></c:chartSpace>"#,
13632 axes.join("")
13633 )
13634 }
13635
13636 #[test]
13637 fn data_labels_write_fixed_prefixes_in_schema_order() {
13638 let xml = format!(
13639 r#"<q:dLbls xmlns:q="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}" xmlns:x="urn:producer"><q:dLbl><q:idx val="0"/><x:point/></q:dLbl><q:numFmt formatCode="0.0%" sourceLinked="0"/><q:spPr><x:shape/></q:spPr><q:txPr><x:text/></q:txPr><q:dLblPos val="outEnd"/><q:showLegendKey val="1"/><q:showVal val="true"/><q:showCatName val="0"/><q:showSerName val="1"/><q:showPercent val="0"/><q:showBubbleSize val="1"/><q:separator> / </q:separator><q:leaderLines><x:line/></q:leaderLines><q:extLst><q:ext uri="labels"><x:tail/></q:ext></q:extLst></q:dLbls>"#
13640 );
13641 let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
13642 assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0%");
13643 assert_eq!(parsed.position, Some(DataLabelPosition::OutsideEnd));
13644 assert_eq!(parsed.separator.as_deref(), Some(" / "));
13645 assert!(parsed.show_legend_key);
13646 assert!(parsed.show_value);
13647 assert!(!parsed.show_category_name);
13648 assert!(parsed.show_series_name);
13649 assert!(!parsed.show_percent);
13650 assert!(parsed.show_bubble_size);
13651
13652 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13653 assert!(written.starts_with(&format!(
13654 "<c:dLbls xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
13655 )));
13656 let mut cursor = 0usize;
13657 for tag in [
13658 "<q:dLbl",
13659 "<c:numFmt",
13660 "<q:spPr",
13661 "<q:txPr",
13662 "<c:dLblPos",
13663 "<c:showLegendKey",
13664 "<c:showVal",
13665 "<c:showCatName",
13666 "<c:showSerName",
13667 "<c:showPercent",
13668 "<c:showBubbleSize",
13669 "<c:separator",
13670 "<q:leaderLines",
13671 "<q:extLst",
13672 ] {
13673 let position = written[cursor..]
13674 .find(tag)
13675 .unwrap_or_else(|| panic!("missing ordered data-label tag {tag}"));
13676 cursor += position + tag.len();
13677 }
13678 assert_eq!(parsed, CT_DLbls::from_xml(written.as_bytes()).unwrap());
13679 }
13680
13681 #[test]
13682 fn percentage_formatted_label_renders_with_correct_text() {
13683 let Some(corpus) = require_or_skip_corpus() else {
13684 return;
13685 };
13686 verify_fetched_corpus(&corpus);
13687 assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
13688 assert_command_version("pdftotext", &["-v"], PDFTOTEXT_VERSION);
13689
13690 let source = corpus.join("bar-chart.pptx");
13691 let mut package = OpcPackage::open(&source).expect("open percentage-label source deck");
13692 let chart_part = "/ppt/charts/chart1.xml";
13693 let original_parts = package.parts.clone();
13694 let chart_xml = package
13695 .get_part(chart_part)
13696 .expect("bar chart part")
13697 .to_vec();
13698 let original_series = first_series_xml(&chart_xml).expect("bar chart series");
13699 let mut series = Series::from_xml(&original_series).expect("parse bar chart series");
13700 series.values.values.fill(0.25);
13701 let labels = CT_DLbls {
13702 number_format: Some(NumberFormat::new("0%".to_owned(), false).unwrap()),
13703 position: Some(DataLabelPosition::OutsideEnd),
13704 show_value: true,
13705 ..CT_DLbls::default()
13706 };
13707 series.data_labels = Some(labels);
13708 let replacement = series.to_xml().expect("serialize typed percentage labels");
13709 let candidate_chart = replace_once(&chart_xml, &original_series, &replacement);
13710 package.set_part(chart_part, candidate_chart);
13711 for (part, bytes) in &original_parts {
13712 if part == chart_part {
13713 assert_ne!(package.parts[part], *bytes);
13714 } else {
13715 assert_eq!(
13716 package.parts[part], *bytes,
13717 "unexpected changed part {part}"
13718 );
13719 }
13720 }
13721
13722 let temp_root =
13723 std::env::temp_dir().join(format!("oxml-chart-f123-label-gate-{}", std::process::id()));
13724 let profile = std::env::temp_dir().join(format!(
13725 "oxml-chart-f123-label-profile-{}",
13726 std::process::id()
13727 ));
13728 if temp_root.exists() {
13729 fs::remove_dir_all(&temp_root).expect("remove stale F-123 evidence");
13730 }
13731 if profile.exists() {
13732 fs::remove_dir_all(&profile).expect("remove stale F-123 profile");
13733 }
13734 fs::create_dir_all(&temp_root).expect("create F-123 evidence root");
13735 let unbound_candidate = temp_root.join("f123-percentage-label.pptx");
13736 package
13737 .save(&unbound_candidate)
13738 .expect("save percentage-label candidate");
13739 let candidate_sha = sha256(&unbound_candidate);
13740 let evidence_dir = temp_root.join(&candidate_sha);
13741 fs::create_dir(&evidence_dir).expect("create SHA-bound evidence directory");
13742 let candidate = evidence_dir.join("f123-percentage-label.pptx");
13743 fs::rename(&unbound_candidate, &candidate).expect("bind candidate to evidence SHA");
13744 assert_eq!(sha256(&candidate), candidate_sha);
13745
13746 let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
13747 let conversion = Command::new("soffice")
13748 .args([
13749 "--headless",
13750 &profile_argument,
13751 "--convert-to",
13752 "pdf:impress_pdf_Export",
13753 "--outdir",
13754 ])
13755 .arg(&evidence_dir)
13756 .arg(&candidate)
13757 .output()
13758 .expect("run pinned LibreOffice percentage-label gate");
13759 assert!(
13760 conversion.status.success(),
13761 "LibreOffice conversion failed: {}",
13762 String::from_utf8_lossy(&conversion.stderr)
13763 );
13764 let pdf = evidence_dir.join("f123-percentage-label.pdf");
13765 assert!(
13766 pdf.is_file(),
13767 "LibreOffice did not create {}",
13768 pdf.display()
13769 );
13770 let text_path = evidence_dir.join("f123-percentage-label.txt");
13771 let extraction = Command::new("pdftotext")
13772 .arg(&pdf)
13773 .arg(&text_path)
13774 .output()
13775 .expect("run pinned Poppler percentage-label extraction");
13776 assert!(
13777 extraction.status.success(),
13778 "pdftotext failed: {}",
13779 String::from_utf8_lossy(&extraction.stderr)
13780 );
13781 let extracted = fs::read_to_string(&text_path).expect("read percentage-label text");
13782 assert!(
13783 extracted.contains("25%"),
13784 "SHA-bound viewer text lacks 25%: {extracted:?}"
13785 );
13786 for artifact in [&candidate, &pdf, &text_path] {
13787 assert_eq!(
13788 artifact.parent(),
13789 Some(evidence_dir.as_path()),
13790 "evidence escaped candidate SHA"
13791 );
13792 }
13793 eprintln!("F-123 candidate/render/text evidence SHA-256 {candidate_sha}, extracted 25%");
13794 fs::remove_dir_all(&temp_root).expect("remove F-123 evidence");
13795 if profile.exists() {
13796 fs::remove_dir_all(&profile).expect("remove F-123 profile");
13797 }
13798 }
13799
13800 #[test]
13801 fn common_number_formats_project_cached_values_deterministically() {
13802 for (code, value, expected) in [
13803 ("General", 0.25, "0.25"),
13804 ("0", 12.6, "13"),
13805 ("0.0", 12.64, "12.6"),
13806 ("0.00", -12.645, "-12.64"),
13807 ("0%", 0.25, "25%"),
13808 ("0.0%", 0.256, "25.6%"),
13809 ("0.00%", 0.256, "25.60%"),
13810 ] {
13811 let format = NumberFormat::new(code.to_owned(), false).unwrap();
13812 assert_eq!(format.format_value(value).unwrap(), expected, "{code}");
13813 }
13814 }
13815
13816 #[test]
13817 fn malformed_data_labels_and_number_formats_return_errors_without_panicking() {
13818 let cases = [
13819 format!(
13820 r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="1"/><c:showVal val="0"/></c:dLbls>"#
13821 ),
13822 format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="yes"/></c:dLbls>"#),
13823 format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:dLblPos val="middle"/></c:dLbls>"#),
13824 format!(
13825 r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="" sourceLinked="1"/></c:dLbls>"#
13826 ),
13827 format!(
13828 r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="0" sourceLinked="maybe"/></c:dLbls>"#
13829 ),
13830 ];
13831 for xml in cases {
13832 let result = std::panic::catch_unwind(|| CT_DLbls::from_xml(xml.as_bytes()));
13833 assert!(result.is_ok(), "data-label parser panicked for {xml}");
13834 assert!(result.unwrap().is_err(), "malformed labels parsed: {xml}");
13835 }
13836
13837 let invalid_code = NumberFormat::new("bad\u{0}code".to_owned(), false);
13838 assert!(invalid_code.is_err());
13839 let unsupported = NumberFormat::new("#,##0".to_owned(), false).unwrap();
13840 assert!(unsupported.format_value(12.0).is_err());
13841 let general = NumberFormat::new("General".to_owned(), false).unwrap();
13842 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
13843 assert!(general.format_value(value).is_err());
13844 }
13845
13846 let invalid_separator = CT_DLbls {
13847 separator: Some("bad\u{0}separator".to_owned()),
13848 ..CT_DLbls::default()
13849 };
13850 assert!(invalid_separator.to_xml().is_err());
13851 }
13852
13853 #[test]
13854 fn data_labels_preserve_point_overrides_and_extensions_byte_for_byte() {
13855 let xml = format!(
13856 r#"<q:dLbls xmlns:q="{C_NS}" xmlns:x="urn:producer" x:keep="labels"><!--before--><q:dLbl x:keep="point"><q:idx val="0"/><q:spPr><x:shape/></q:spPr><q:txPr><x:text/></q:txPr><q:extLst><q:ext uri="point"><x:point/></q:ext></q:extLst></q:dLbl><?after-point?><q:numFmt formatCode="0%" sourceLinked="0"/><q:spPr><x:collection-shape/></q:spPr><q:txPr><x:collection-text/></q:txPr><q:showVal val="1"/><q:separator><![CDATA[ | ]]></q:separator><q:showLeaderLines val="1"/><q:leaderLines><q:spPr><x:leader-shape/></q:spPr></q:leaderLines><q:extLst><q:ext uri="collection"><x:tail/></q:ext></q:extLst><!--after--></q:dLbls>"#
13857 );
13858 let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
13859 let written = parsed.to_xml().unwrap();
13860 for raw in [
13861 br#"<!--before-->"#.as_slice(),
13862 br#"<q:dLbl x:keep="point"><q:idx val="0"/><q:spPr><x:shape/></q:spPr><q:txPr><x:text/></q:txPr><q:extLst><q:ext uri="point"><x:point/></q:ext></q:extLst></q:dLbl>"#.as_slice(),
13863 br#"<?after-point?>"#.as_slice(),
13864 br#"<q:spPr><x:collection-shape/></q:spPr>"#.as_slice(),
13865 br#"<q:txPr><x:collection-text/></q:txPr>"#.as_slice(),
13866 br#"<q:showLeaderLines val="1"/>"#.as_slice(),
13867 br#"<q:leaderLines><q:spPr><x:leader-shape/></q:spPr></q:leaderLines>"#.as_slice(),
13868 br#"<q:extLst><q:ext uri="collection"><x:tail/></q:ext></q:extLst>"#.as_slice(),
13869 br#"<!--after-->"#.as_slice(),
13870 ] {
13871 assert!(
13872 written.windows(raw.len()).any(|window| window == raw),
13873 "preserved data-label bytes changed: {}",
13874 String::from_utf8_lossy(raw)
13875 );
13876 }
13877 assert_eq!(parsed, CT_DLbls::from_xml(&written).unwrap());
13878 }
13879
13880 #[test]
13881 fn every_corpus_data_label_collection_round_trips_structurally() {
13882 let Some(corpus) = require_or_skip_corpus() else {
13883 return;
13884 };
13885 verify_fetched_corpus(&corpus);
13886 let mut label_collections = 0usize;
13887 let mut axis_number_formats = 0usize;
13888 for path in manifest_paths() {
13889 let package = OpcPackage::open(corpus.join(path))
13890 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
13891 for (part, xml) in &package.parts {
13892 if !is_chart_part(part) {
13893 continue;
13894 }
13895 let chart = CT_ChartSpace::from_xml(xml)
13896 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
13897 for series in chart.chart.plot_area.series().unwrap_or_else(|error| {
13898 panic!("{path} {part}: series projection failed: {error}")
13899 }) {
13900 if let Some(labels) = series.data_labels {
13901 let written = labels.to_xml().unwrap_or_else(|error| {
13902 panic!("{path} {part}: label write failed: {error}")
13903 });
13904 let reparsed = CT_DLbls::from_xml(&written).unwrap_or_else(|error| {
13905 panic!("{path} {part}: written labels parse failed: {error}")
13906 });
13907 assert_eq!(labels, reparsed, "{path} {part}: labels changed");
13908 label_collections += 1;
13909 }
13910 }
13911 for axis in chart.chart.plot_area.axes().unwrap_or_else(|error| {
13912 panic!("{path} {part}: axis projection failed: {error}")
13913 }) {
13914 if axis.number_format.is_some() {
13915 let written = axis.to_xml().unwrap_or_else(|error| {
13916 panic!("{path} {part}: axis write failed: {error}")
13917 });
13918 let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
13919 panic!("{path} {part}: written axis parse failed: {error}")
13920 });
13921 assert_eq!(axis, reparsed, "{path} {part}: axis changed");
13922 axis_number_formats += 1;
13923 }
13924 }
13925 }
13926 }
13927 assert!(label_collections > 0, "the pinned corpus has no c:dLbls");
13928 assert!(
13929 axis_number_formats > 0,
13930 "the pinned corpus has no axis c:numFmt"
13931 );
13932 eprintln!(
13933 "ChartML data-label corpus gate checked {label_collections} collections and {axis_number_formats} axis number formats"
13934 );
13935 }
13936
13937 fn full_axis(local: &str, position: &str) -> String {
13938 format!(
13939 r#"<q:{local} xmlns:q="{C_NS}" xmlns:d="{A_NS}" xmlns:r="{R_NS}" xmlns:x="urn:producer"><q:axId val="-1125255920"/><q:scaling><q:logBase val="10"/><q:orientation val="maxMin"/><q:max val="100"/><q:min val="-10"/></q:scaling><q:delete val="1"/><q:axPos val="{position}"/><q:majorGridlines><q:spPr><d:ln/></q:spPr></q:majorGridlines><q:minorGridlines/><q:title><x:title/></q:title><q:numFmt formatCode="0.0" sourceLinked="0"/><q:majorTickMark val="cross"/><q:minorTickMark val="in"/><q:tickLblPos val="high"/><q:spPr><d:solidFill><d:srgbClr val="112233"/></d:solidFill></q:spPr><q:txPr><d:bodyPr/><d:p/></q:txPr><q:crossAx val="-1004303696"/></q:{local}>"#
13940 )
13941 }
13942
13943 #[test]
13944 fn axis_id_pairs_are_reciprocal() {
13945 assert_eq!(
13946 AxisId::new(i64::from(i32::MIN)).unwrap().value(),
13947 i64::from(i32::MIN)
13948 );
13949 assert_eq!(
13950 AxisId::new(i64::from(u32::MAX)).unwrap().value(),
13951 i64::from(u32::MAX)
13952 );
13953 assert!(AxisId::new(i64::from(i32::MIN) - 1).is_err());
13954 assert!(AxisId::new(i64::from(u32::MAX) + 1).is_err());
13955
13956 let valid = chart_with_axes(&[
13957 minimal_axis("catAx", "-1884094432", "b", "-1884097184"),
13958 minimal_axis("valAx", "-1884097184", "l", "-1884094432"),
13959 ]);
13960 let axes = CT_ChartSpace::from_xml(valid.as_bytes())
13961 .unwrap()
13962 .chart
13963 .plot_area
13964 .axes()
13965 .unwrap();
13966 assert_eq!(axes.len(), 2);
13967 assert_eq!(axes[0].id.value(), -1_884_094_432);
13968 assert_eq!(axes[0].cross_axis, axes[1].id);
13969 assert_eq!(axes[1].cross_axis, axes[0].id);
13970
13971 let invalid_sets = [
13972 vec![
13973 minimal_axis("catAx", "1", "b", "2"),
13974 minimal_axis("valAx", "1", "l", "2"),
13975 ],
13976 vec![minimal_axis("catAx", "1", "b", "1")],
13977 vec![minimal_axis("catAx", "1", "b", "2")],
13978 vec![
13979 minimal_axis("catAx", "1", "b", "2"),
13980 minimal_axis("valAx", "2", "l", "3"),
13981 minimal_axis("serAx", "3", "r", "2"),
13982 ],
13983 ];
13984 for axes in invalid_sets {
13985 let chart = CT_ChartSpace::from_xml(chart_with_axes(&axes).as_bytes()).unwrap();
13986 assert!(chart.chart.plot_area.axes().is_err());
13987 }
13988
13989 let empty = CT_ChartSpace::from_xml(chart_with_axes(&[]).as_bytes()).unwrap();
13990 assert!(empty.chart.plot_area.axes().unwrap().is_empty());
13991 }
13992
13993 #[test]
13994 fn all_axis_forms_write_fixed_prefixes_in_schema_order() {
13995 for (local, kind, position) in [
13996 ("catAx", AxisKind::Category, AxisPosition::Bottom),
13997 ("valAx", AxisKind::Value, AxisPosition::Left),
13998 ("dateAx", AxisKind::Date, AxisPosition::Top),
13999 ("serAx", AxisKind::Series, AxisPosition::Right),
14000 ] {
14001 let xml = full_axis(local, position.as_str());
14002 let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
14003 assert_eq!(parsed.kind, kind);
14004 assert_eq!(parsed.id.value(), -1_125_255_920);
14005 assert_eq!(parsed.scaling.log_base, Some(10.0));
14006 assert_eq!(parsed.scaling.orientation, Orientation::MaxMin);
14007 assert_eq!(parsed.scaling.maximum, Some(100.0));
14008 assert_eq!(parsed.scaling.minimum, Some(-10.0));
14009 assert!(parsed.deleted);
14010 assert_eq!(parsed.position, position);
14011 assert!(parsed.major_gridlines.is_some());
14012 assert!(parsed.minor_gridlines.is_some());
14013 assert!(parsed.title.is_some());
14014 assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0");
14015 assert_eq!(parsed.major_tick_mark, TickMark::Cross);
14016 assert_eq!(parsed.minor_tick_mark, TickMark::Inside);
14017 assert_eq!(parsed.tick_label_position, TickLabelPosition::High);
14018 assert!(parsed.sp_pr.is_some());
14019 assert!(parsed.tx_pr.is_some());
14020 assert_eq!(parsed.cross_axis.value(), -1_004_303_696);
14021
14022 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14023 assert!(written.starts_with(&format!(
14024 "<c:{local} xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
14025 )));
14026 assert!(written.contains("<c:axId val=\"-1125255920\""));
14027 let mut cursor = 0usize;
14028 for tag in [
14029 "<c:axId",
14030 "<c:scaling",
14031 "<c:delete",
14032 "<c:axPos",
14033 "<c:majorGridlines",
14034 "<c:minorGridlines",
14035 "<c:title",
14036 "<c:numFmt",
14037 "<c:majorTickMark",
14038 "<c:minorTickMark",
14039 "<c:tickLblPos",
14040 "<c:spPr",
14041 "<c:txPr",
14042 "<c:crossAx",
14043 ] {
14044 let position = written[cursor..]
14045 .find(tag)
14046 .unwrap_or_else(|| panic!("missing ordered axis tag {tag}"));
14047 cursor += position + tag.len();
14048 }
14049 assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
14050 }
14051
14052 let mut constructed = Axis::new(
14053 AxisKind::Value,
14054 AxisId::new(1).unwrap(),
14055 AxisPosition::Left,
14056 AxisId::new(2).unwrap(),
14057 );
14058 assert_eq!(
14059 constructed,
14060 Axis::from_xml(&constructed.to_xml().unwrap()).unwrap()
14061 );
14062 constructed.kind = AxisKind::Category;
14063 constructed.id = AxisId::new(3).unwrap();
14064 constructed.cross_axis = AxisId::new(4).unwrap();
14065 constructed.scaling.minimum = Some(0.0);
14066 constructed.scaling.maximum = Some(10.0);
14067 let written = constructed.to_xml().unwrap();
14068 assert!(written.starts_with(b"<c:catAx"));
14069 assert_eq!(constructed, Axis::from_xml(&written).unwrap());
14070 }
14071
14072 #[test]
14073 fn axis_equality_normalizes_ids_without_losing_lexical_preservation() {
14074 let padded = Axis::from_xml(
14075 standalone_axis(
14076 "catAx",
14077 "<c:axId val=\"01\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"002\"/>",
14078 )
14079 .as_bytes(),
14080 )
14081 .unwrap();
14082 let normalized = Axis::from_xml(
14083 standalone_axis(
14084 "catAx",
14085 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14086 )
14087 .as_bytes(),
14088 )
14089 .unwrap();
14090 assert_eq!(padded, normalized);
14091
14092 let preserved = String::from_utf8(padded.to_xml().unwrap()).unwrap();
14093 assert!(preserved.contains("<c:axId val=\"01\""));
14094 assert!(preserved.contains("<c:crossAx val=\"002\""));
14095
14096 let mut mutated = padded;
14097 mutated.id = AxisId::new(3).unwrap();
14098 mutated.cross_axis = AxisId::new(4).unwrap();
14099 let rewritten = String::from_utf8(mutated.to_xml().unwrap()).unwrap();
14100 assert!(rewritten.contains("<c:axId val=\"3\""));
14101 assert!(rewritten.contains("<c:crossAx val=\"4\""));
14102 assert!(!rewritten.contains("val=\"01\""));
14103 assert!(!rewritten.contains("val=\"002\""));
14104 }
14105
14106 #[test]
14107 fn malformed_axis_values_return_errors_without_panicking() {
14108 let cases = [
14109 standalone_axis(
14110 "catAx",
14111 "<c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14112 ),
14113 standalone_axis(
14114 "catAx",
14115 "<c:axId val=\"1\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14116 ),
14117 standalone_axis(
14118 "catAx",
14119 "<c:axId val=\"1\"/><c:scaling/><c:crossAx val=\"2\"/>",
14120 ),
14121 standalone_axis(
14122 "catAx",
14123 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/>",
14124 ),
14125 standalone_axis(
14126 "catAx",
14127 "<c:axId val=\"-2147483649\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14128 ),
14129 standalone_axis(
14130 "catAx",
14131 "<c:axId val=\"4294967296\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14132 ),
14133 standalone_axis(
14134 "catAx",
14135 "<c:axId val=\"1\"/><c:scaling><c:logBase val=\"1\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14136 ),
14137 standalone_axis(
14138 "catAx",
14139 "<c:axId val=\"1\"/><c:scaling><c:max val=\"NaN\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14140 ),
14141 standalone_axis(
14142 "catAx",
14143 "<c:axId val=\"1\"/><c:scaling><c:max val=\"1\"/><c:min val=\"2\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14144 ),
14145 standalone_axis(
14146 "catAx",
14147 "<c:axId val=\"1\"/><c:scaling><c:orientation val=\"sideways\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14148 ),
14149 standalone_axis(
14150 "catAx",
14151 "<c:axId val=\"1\"/><c:scaling/><c:delete val=\"yes\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14152 ),
14153 standalone_axis(
14154 "catAx",
14155 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"middle\"/><c:crossAx val=\"2\"/>",
14156 ),
14157 standalone_axis(
14158 "catAx",
14159 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:majorTickMark val=\"near\"/><c:crossAx val=\"2\"/>",
14160 ),
14161 standalone_axis(
14162 "catAx",
14163 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:tickLblPos val=\"middle\"/><c:crossAx val=\"2\"/>",
14164 ),
14165 standalone_axis(
14166 "catAx",
14167 "<c:axId val=\"1\"/><c:axId val=\"2\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14168 ),
14169 standalone_axis(
14170 "catAx",
14171 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt sourceLinked=\"1\"/><c:crossAx val=\"2\"/>",
14172 ),
14173 standalone_axis(
14174 "catAx",
14175 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt formatCode=\"0\" sourceLinked=\"maybe\"/><c:crossAx val=\"2\"/>",
14176 ),
14177 ];
14178 for xml in cases {
14179 let result = std::panic::catch_unwind(|| Axis::from_xml(xml.as_bytes()));
14180 assert!(result.is_ok(), "axis parser panicked for {xml}");
14181 assert!(result.unwrap().is_err(), "malformed axis parsed: {xml}");
14182 }
14183
14184 let mut axis = Axis::new(
14185 AxisKind::Value,
14186 AxisId::new(1).unwrap(),
14187 AxisPosition::Left,
14188 AxisId::new(2).unwrap(),
14189 );
14190 axis.scaling.maximum = Some(f64::INFINITY);
14191 assert!(axis.to_xml().is_err());
14192 }
14193
14194 #[test]
14195 fn axes_preserve_unmodelled_children_byte_for_byte() {
14196 let xml = format!(
14197 r#"<q:catAx xmlns:q="{C_NS}" xmlns:d="{A_NS}" xmlns:x="urn:producer" x:keep="axis"><!--before--><q:axId val="-1884094432" x:keep="id"/><?after-id?><q:scaling x:keep="scaling"><!--scale--><q:orientation val="minMax"/><q:extLst><q:ext uri="scale"><x:data/></q:ext></q:extLst></q:scaling><q:axPos val="b"/><q:majorGridlines x:keep="grid"><x:grid/></q:majorGridlines><q:title><x:title/></q:title><q:crossAx val="-1884097184"/><q:crosses val="autoZero"/><q:auto val="1"><x:auto/></q:auto><q:extLst><q:ext uri="axis"><x:tail/></q:ext></q:extLst><!--after--></q:catAx>"#
14198 );
14199 let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
14200 let mut relabelled = parsed.clone();
14201 relabelled.kind = AxisKind::Value;
14202 assert!(
14203 relabelled.to_xml().is_err(),
14204 "a parsed category axis must not retain its tail under a value-axis root"
14205 );
14206 let written = parsed.to_xml().unwrap();
14207 for raw in [
14208 br#"<!--before-->"#.as_slice(),
14209 br#"<?after-id?>"#.as_slice(),
14210 br#"<!--scale-->"#.as_slice(),
14211 br#"<q:extLst><q:ext uri="scale"><x:data/></q:ext></q:extLst>"#.as_slice(),
14212 br#"<x:grid/>"#.as_slice(),
14213 br#"<x:title/>"#.as_slice(),
14214 br#"<q:crosses val="autoZero"/>"#.as_slice(),
14215 br#"<q:auto val="1"><x:auto/></q:auto>"#.as_slice(),
14216 br#"<q:extLst><q:ext uri="axis"><x:tail/></q:ext></q:extLst>"#.as_slice(),
14217 br#"<!--after-->"#.as_slice(),
14218 ] {
14219 assert!(
14220 written.windows(raw.len()).any(|window| window == raw),
14221 "preserved axis bytes changed: {}",
14222 String::from_utf8_lossy(raw)
14223 );
14224 }
14225 let written = String::from_utf8(written).unwrap();
14226 assert!(written.starts_with("<c:catAx"));
14227 assert!(written.contains("x:keep=\"axis\""));
14228 assert!(written.contains("x:keep=\"id\""));
14229 assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
14230 }
14231
14232 #[test]
14233 fn every_corpus_axis_round_trips_structurally() {
14234 let Some(corpus) = require_or_skip_corpus() else {
14235 return;
14236 };
14237 verify_fetched_corpus(&corpus);
14238 let mut axis_count = 0usize;
14239 let mut chart_parts = 0usize;
14240 let mut kinds = HashSet::new();
14241 for path in manifest_paths() {
14242 let package = OpcPackage::open(corpus.join(path))
14243 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14244 for (part, xml) in &package.parts {
14245 if !is_chart_part(part) {
14246 continue;
14247 }
14248 let chart = CT_ChartSpace::from_xml(xml)
14249 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14250 let axes = chart.chart.plot_area.axes().unwrap_or_else(|error| {
14251 panic!("{path} {part}: axis projection failed: {error}")
14252 });
14253 for axis in axes {
14254 kinds.insert(axis.kind);
14255 let written = axis.to_xml().unwrap_or_else(|error| {
14256 panic!("{path} {part}: axis write failed: {error}")
14257 });
14258 let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
14259 panic!("{path} {part}: written axis parse failed: {error}")
14260 });
14261 assert_eq!(axis, reparsed, "{path} {part}: axis model changed");
14262 axis_count += 1;
14263 }
14264 chart_parts += 1;
14265 }
14266 }
14267 assert!(
14268 axis_count > 0,
14269 "the pinned corpus contained no ChartML axes"
14270 );
14271 assert!(kinds.contains(&AxisKind::Category));
14272 assert!(kinds.contains(&AxisKind::Value));
14273
14274 for kind in ["dateAx", "serAx"] {
14275 let parsed = Axis::from_xml(
14276 standalone_axis(
14277 kind,
14278 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14279 )
14280 .as_bytes(),
14281 )
14282 .unwrap();
14283 assert_eq!(parsed, Axis::from_xml(&parsed.to_xml().unwrap()).unwrap());
14284 }
14285 eprintln!(
14286 "ChartML axis corpus gate checked {axis_count} axes across {chart_parts} chart parts"
14287 );
14288 }
14289
14290 #[test]
14291 fn series_formula_and_cache_are_consistent_with_one_source() {
14292 let values = NumericData::new(
14293 "'Sales 24'!$B$2:$B$4".to_owned(),
14294 "0.0".to_owned(),
14295 vec![4.25, 8.5, 17.0],
14296 )
14297 .unwrap();
14298 let mut series = Series::new(3, 1, values);
14299 series.categories = Some(AxisData::String(
14300 StringRef::new(
14301 "'Sales 24'!$A$2:$A$4".to_owned(),
14302 vec!["North".to_owned(), "South".to_owned(), "West".to_owned()],
14303 )
14304 .unwrap(),
14305 ));
14306
14307 let written = String::from_utf8(series.to_xml().unwrap()).unwrap();
14308 assert!(written.contains("<c:f>'Sales 24'!$B$2:$B$4</c:f>"));
14309 assert!(written.contains("<c:formatCode>0.0</c:formatCode>"));
14310 assert_eq!(written.matches("<c:ptCount val=\"3\"").count(), 2);
14311 for (index, value) in ["4.25", "8.5", "17"].iter().enumerate() {
14312 assert!(written.contains(&format!("<c:pt idx=\"{index}\"><c:v>{value}</c:v></c:pt>")));
14313 }
14314 let reparsed = Series::from_xml(written.as_bytes()).unwrap();
14315 assert_eq!(reparsed.index, series.index);
14316 assert_eq!(reparsed.order, series.order);
14317 match (&reparsed.categories, &series.categories) {
14318 (Some(AxisData::String(left)), Some(AxisData::String(right))) => {
14319 assert_eq!(left.formula, right.formula);
14320 assert_eq!(left.values, right.values);
14321 }
14322 _ => panic!("expected string categories"),
14323 }
14324 assert_eq!(reparsed.values.formula, series.values.formula);
14325 assert_eq!(reparsed.values.format_code, series.values.format_code);
14326 assert_eq!(reparsed.values.values, series.values.values);
14327 }
14328
14329 #[test]
14330 fn string_and_numeric_references_write_fixed_prefixes_in_schema_order() {
14331 let string_xml = br#"<q:strRef xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:strCache><q:pt idx="0"><q:v>West</q:v></q:pt><q:ptCount val="1"/></q:strCache><q:f>Sheet1!$A$2</q:f></q:strRef>"#;
14332 let string_ref = StringRef::from_xml(string_xml).unwrap();
14333 let written = String::from_utf8(string_ref.to_xml().unwrap()).unwrap();
14334 assert!(written.starts_with("<c:strRef xmlns:c="));
14335 assert!(written.find("<c:f>").unwrap() < written.find("<c:strCache>").unwrap());
14336 assert!(written.find("<c:ptCount").unwrap() < written.find("<c:pt idx=").unwrap());
14337
14338 let numeric_xml = br#"<q:numRef xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:numCache><q:ptCount val="2"/><q:pt idx="0"><q:v>1.5</q:v></q:pt><q:pt idx="1"><q:v>2.5</q:v></q:pt><q:formatCode>0.00</q:formatCode></q:numCache><q:f>Sheet1!$B$2:$B$3</q:f></q:numRef>"#;
14339 let numeric = NumericData::from_xml(numeric_xml).unwrap();
14340 let written = String::from_utf8(numeric.to_xml().unwrap()).unwrap();
14341 let positions: Vec<_> = [
14342 "<c:f>",
14343 "<c:numCache>",
14344 "<c:formatCode>",
14345 "<c:ptCount",
14346 "<c:pt idx=",
14347 ]
14348 .iter()
14349 .map(|tag| written.find(tag).unwrap())
14350 .collect();
14351 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14352 assert_eq!(numeric, NumericData::from_xml(written.as_bytes()).unwrap());
14353 }
14354
14355 #[test]
14356 fn mixed_chartml_aliases_resolve_by_namespace_uri() {
14357 let xml = br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="4"/><q:order val="2"/><c:cat><q:strRef><c:f>Sheet1!$A$2:$A$3</c:f><q:strCache><c:ptCount val="2"/><q:pt idx="0"><c:v>North</c:v></q:pt><c:pt idx="1"><q:v>West</q:v></c:pt></q:strCache></q:strRef></c:cat><q:val><c:numRef><q:f>Sheet1!$B$2:$B$3</q:f><c:numCache><q:formatCode>0.0</q:formatCode><c:ptCount val="2"/><q:pt idx="0"><c:v>1.5</c:v></q:pt><c:pt idx="1"><q:v>2.5</q:v></c:pt></c:numCache></c:numRef></q:val></q:ser>"#;
14358 let parsed = Series::from_xml(xml).unwrap();
14359 assert_eq!(parsed.index, 4);
14360 assert_eq!(parsed.order, 2);
14361 assert_eq!(parsed.values.values, vec![1.5, 2.5]);
14362 assert!(matches!(parsed.categories, Some(AxisData::String(_))));
14363 assert_eq!(parsed, Series::from_xml(&parsed.to_xml().unwrap()).unwrap());
14364 }
14365
14366 #[test]
14367 fn plot_area_series_ignores_inherited_foreign_plot_aliases() {
14368 let xml = br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:producer"><c:chart><c:plotArea><x:barChart><x:ser><x:idx val="9"/><x:order val="9"/><x:val><x:numRef><x:f>foreign</x:f><x:numCache><x:formatCode>General</x:formatCode><x:ptCount val="0"/></x:numCache></x:numRef></x:val></x:ser></x:barChart><c:barChart><q:ser><q:idx val="1"/><q:order val="0"/><q:marker><x:data/></q:marker><q:val><q:numRef><q:f>Sheet1!$B$2</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="1"/><q:pt idx="0"><q:v>3</q:v></q:pt></q:numCache></q:numRef></q:val></q:ser></c:barChart><c:pieChart/></c:plotArea></c:chart></c:chartSpace>"#;
14369 let chart = CT_ChartSpace::from_xml(xml).unwrap();
14370 let series = chart.chart.plot_area.series().unwrap();
14371 assert_eq!(series.len(), 1);
14372 assert_eq!(series[0].index, 1);
14373 let written = String::from_utf8(series[0].to_xml().unwrap()).unwrap();
14374 assert!(
14375 written.contains(r#"xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart""#)
14376 );
14377 assert!(written.contains(r#"xmlns:x="urn:producer""#));
14378 assert!(written.contains("<q:marker><x:data/></q:marker>"));
14379 assert_eq!(series[0], Series::from_xml(written.as_bytes()).unwrap());
14380
14381 let conflicting = br#"<q:chartSpace xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:chart><q:plotArea><q:barChart xmlns:c="urn:foreign"><q:ser><q:idx val="0"/><q:order val="0"/><q:val><q:numRef><q:f>Sheet1!$A$1</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="0"/></q:numCache></q:numRef></q:val></q:ser></q:barChart><q:pieChart/></q:plotArea></q:chart></q:chartSpace>"#;
14382 let chart = CT_ChartSpace::from_xml(conflicting).unwrap();
14383 assert!(chart.chart.plot_area.series().is_err());
14384 }
14385
14386 #[test]
14387 fn malformed_series_and_cache_values_return_errors_without_panicking() {
14388 let cases: &[&[u8]] = &[
14389 br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:order val="0"/><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#,
14390 br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/></c:ser>"#,
14391 br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:idx val="1"/><c:order val="0"/><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#,
14392 br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:val><c:numRef><c:f></c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#,
14393 br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/><c:pt idx="0"><c:v>1</c:v></c:pt></c:numCache></c:numRef></c:val></c:ser>"#,
14394 br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:val><c:numRef><c:f>S!$A$1:$A$2</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="2"/><c:pt idx="0"><c:v>1</c:v></c:pt><c:pt idx="2"><c:v>2</c:v></c:pt></c:numCache></c:numRef></c:val></c:ser>"#,
14395 br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="1"/><c:pt idx="0"><c:v>NaN</c:v></c:pt></c:numCache></c:numRef></c:val></c:ser>"#,
14396 br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="urn:producer"><c:idx val="0"/><c:order val="0"/><a:marker/><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#,
14397 br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:producer"><c:idx val="0"/><c:order val="0"/><c:spPr><x:solidFill><x:srgbClr val="112233"/></x:solidFill></c:spPr><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#,
14398 br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:tx><c:v>opaque</c:v></c:tx><c:tx><c:strRef><c:f>S!$A$1</c:f><c:strCache><c:ptCount val="0"/></c:strCache></c:strRef></c:tx><c:val><c:numRef><c:f>S!$B$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#,
14399 br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:cat><c:multiLvlStrRef/></c:cat><c:cat><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:cat><c:val><c:numRef><c:f>S!$B$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#,
14400 br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:val><c:numRef><c:f>S!$B$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val><c:bubbleSize><c:numLit/></c:bubbleSize><c:bubbleSize><c:numRef><c:f>S!$C$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:bubbleSize></c:ser>"#,
14401 br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:idx val="0"/><q:order val="0"/><q:val xmlns:c="urn:producer"><q:numRef><q:f>S!$A$1</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="0"/></q:numCache></q:numRef></q:val></q:ser>"#,
14402 br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:idx val="0"/><q:order val="0"/><q:val><q:numRef><q:f>S!$A$1</q:f><q:numCache xmlns:c="urn:producer"><q:formatCode>General</q:formatCode><q:ptCount val="0"/></q:numCache></q:numRef></q:val></q:ser>"#,
14403 br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:idx val="0"/><q:order val="0"/><q:val><q:numRef><q:f>S!$A$1</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="1"/><q:pt xmlns:c="urn:producer" idx="0"><q:v>1</q:v></q:pt></q:numCache></q:numRef></q:val></q:ser>"#,
14404 br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:idx val="0"/><q:order val="0"/><q:val><q:numRef><q:f>S!$A$1</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount xmlns:c="urn:producer" val="0"/></q:numCache></q:numRef></q:val></q:ser>"#,
14405 br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:idx val="0"/><q:order val="0"/><q:val><q:numRef><q:f xmlns:c="urn:producer">S!$A$1</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="0"/></q:numCache></q:numRef></q:val></q:ser>"#,
14406 ];
14407 for xml in cases {
14408 let result = std::panic::catch_unwind(|| Series::from_xml(xml));
14409 assert!(result.is_ok(), "series parser panicked");
14410 assert!(
14411 result.unwrap().is_err(),
14412 "malformed series parsed: {}",
14413 String::from_utf8_lossy(xml)
14414 );
14415 }
14416 assert!(
14417 NumericData::new(
14418 "S!$A$1".to_owned(),
14419 "General".to_owned(),
14420 vec![f64::INFINITY]
14421 )
14422 .is_err()
14423 );
14424 }
14425
14426 #[test]
14427 fn chart_text_rejects_xml_forbidden_characters_and_escapes_metacharacters() {
14428 assert!(StringRef::new("S!$A$1\u{1}".to_owned(), Vec::new()).is_err());
14429 assert!(StringRef::new("S!$A$1".to_owned(), vec!["invalid\u{1}".to_owned()]).is_err());
14430 assert!(
14431 NumericData::new("S!$A$1\u{1}".to_owned(), "General".to_owned(), Vec::new()).is_err()
14432 );
14433 assert!(
14434 NumericData::new("S!$A$1".to_owned(), "General\u{1}".to_owned(), Vec::new()).is_err()
14435 );
14436
14437 let mut strings = StringRef::new("S!$A$1".to_owned(), vec!["valid".to_owned()]).unwrap();
14438 strings.formula = "invalid\u{1}".to_owned();
14439 assert!(strings.to_xml().is_err());
14440 strings.formula = "S!$A$1".to_owned();
14441 strings.values[0] = "invalid\u{1}".to_owned();
14442 assert!(strings.to_xml().is_err());
14443
14444 let mut numbers =
14445 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap();
14446 numbers.formula = "invalid\u{1}".to_owned();
14447 assert!(numbers.to_xml().is_err());
14448 numbers.formula = "S!$A$1".to_owned();
14449 numbers.format_code = "invalid\u{1}".to_owned();
14450 assert!(numbers.to_xml().is_err());
14451
14452 let strings =
14453 StringRef::new("S!$A$1&".to_owned(), vec!["North < West & East".to_owned()]).unwrap();
14454 let written = String::from_utf8(strings.to_xml().unwrap()).unwrap();
14455 assert!(written.contains("S!$A$1&"));
14456 assert!(written.contains("North < West & East"));
14457 }
14458
14459 #[test]
14460 fn series_preserves_unmodelled_children_byte_for_byte() {
14461 let xml = br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:d="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer" x:keep="series"><!--before--><q:idx val="0" x:keep="idx"/><x:between/><q:order val="0"/><q:tx x:keep="tx"><q:strRef x:keep="ref"><q:f x:keep="formula">S!$B$1</q:f><q:strCache x:keep="cache"><q:ptCount val="1" x:keep="count"/><!--point--><q:pt idx="0" x:keep="point"><q:v x:keep="value">Revenue</q:v></q:pt><x:cacheExt/></q:strCache></q:strRef></q:tx><q:spPr><d:noFill/><x:shapeExt/></q:spPr><q:dPt x:id="one"><q:idx val="0"/></q:dPt><q:dLbls x:id="labels"/><q:cat><q:strRef><q:f>S!$A$2:$A$3</q:f><q:strCache><q:ptCount val="2"/><q:pt idx="0"><q:v>North</q:v></q:pt><q:pt idx="1"><q:v>West</q:v></q:pt></q:strCache></q:strRef></q:cat><q:trendline x:id="trend"/><q:val><q:numRef><q:f>S!$B$2:$B$3</q:f><q:numCache><q:formatCode>0.0</q:formatCode><q:ptCount val="2"/><q:pt idx="0"><q:v>1.5</q:v></q:pt><q:pt idx="1"><q:v>2.5</q:v></q:pt></q:numCache></q:numRef></q:val><q:extLst><q:ext uri="keep"><x:data/></q:ext></q:extLst></q:ser>"#;
14462 let parsed = Series::from_xml(xml).unwrap();
14463 let written = parsed.to_xml().unwrap();
14464 for raw in [
14465 br#"<!--before-->"#.as_slice(),
14466 br#"<x:between/>"#.as_slice(),
14467 br#"<q:dPt x:id="one"><q:idx val="0"/></q:dPt>"#.as_slice(),
14468 br#"<q:trendline x:id="trend"/>"#.as_slice(),
14469 br#"<q:extLst><q:ext uri="keep"><x:data/></q:ext></q:extLst>"#.as_slice(),
14470 br#"<!--point-->"#.as_slice(),
14471 br#"<x:cacheExt/>"#.as_slice(),
14472 ] {
14473 assert!(
14474 written.windows(raw.len()).any(|window| window == raw),
14475 "preserved series bytes changed: {}",
14476 String::from_utf8_lossy(raw)
14477 );
14478 }
14479 let written = String::from_utf8(written.clone()).unwrap();
14480 assert!(written.contains(r#"x:keep="series""#));
14481 assert!(written.contains(r#"x:keep="formula""#));
14482 assert!(written.contains("<c:dLbls"));
14483 assert!(written.contains(r#"x:id="labels""#));
14484 assert!(parsed.data_labels.is_some());
14485 assert_eq!(parsed, Series::from_xml(written.as_bytes()).unwrap());
14486 }
14487
14488 #[test]
14489 fn public_series_edits_do_not_duplicate_or_drop_preserved_payloads() {
14490 let opaque_xml = br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:tx><c:v>opaque</c:v></c:tx><c:cat><c:multiLvlStrRef/></c:cat><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val><c:bubbleSize><c:numLit/></c:bubbleSize></c:ser>"#;
14491 let parsed = Series::from_xml(opaque_xml).unwrap();
14492
14493 let mut edited = parsed.clone();
14494 edited.name = Some(StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap());
14495 assert!(edited.to_xml().is_err());
14496
14497 let mut edited = parsed.clone();
14498 edited.categories = Some(AxisData::Numeric(
14499 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap(),
14500 ));
14501 assert!(edited.to_xml().is_err());
14502
14503 let mut edited = parsed;
14504 edited.bubble_size =
14505 Some(NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
14506 assert!(edited.to_xml().is_err());
14507
14508 let cache_xml = br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:producer"><c:idx val="0"/><c:order val="0"/><c:val><c:numRef><c:f>S!$A$1:$A$2</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="2"/><c:pt idx="0"><c:v>1</c:v></c:pt><c:pt idx="1"><c:v>2</c:v></c:pt><c:extLst><c:ext uri="keep"><x:data/></c:ext></c:extLst></c:numCache></c:numRef></c:val></c:ser>"#;
14509 let parsed = Series::from_xml(cache_xml).unwrap();
14510 let tail = "<c:extLst><c:ext uri=\"keep\"><x:data/></c:ext></c:extLst>";
14511
14512 let mut shortened = parsed.clone();
14513 shortened.values.values.truncate(1);
14514 let written = String::from_utf8(shortened.to_xml().unwrap()).unwrap();
14515 assert!(!written.contains("<c:pt idx=\"1\""));
14516 assert!(written.find("<c:pt idx=\"0\"").unwrap() < written.find(tail).unwrap());
14517 assert!(Series::from_xml(written.as_bytes()).is_ok());
14518
14519 let mut grown = parsed;
14520 grown.values.values.push(3.0);
14521 let written = String::from_utf8(grown.to_xml().unwrap()).unwrap();
14522 assert!(written.find("<c:pt idx=\"2\"").unwrap() < written.find(tail).unwrap());
14523 assert!(Series::from_xml(written.as_bytes()).is_ok());
14524
14525 let ordered_xml = br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><c:idx val="0"/><c:order val="0"/><c:marker><c:symbol val="circle"/></c:marker><c:val><c:numRef><c:f>S!$B$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val><c:extLst><c:ext uri="keep"/></c:extLst></c:ser>"#;
14526 let mut edited = Series::from_xml(ordered_xml).unwrap();
14527 edited.name = Some(StringRef::new("S!$B$1".to_owned(), Vec::new()).unwrap());
14528 edited.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
14529 edited.categories = Some(AxisData::String(
14530 StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap(),
14531 ));
14532 edited.bubble_size =
14533 Some(NumericData::new("S!$C$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
14534 let written = String::from_utf8(edited.to_xml().unwrap()).unwrap();
14535 let positions: Vec<_> = [
14536 "<c:tx>",
14537 "<c:spPr",
14538 "<c:marker>",
14539 "<c:cat>",
14540 "<c:val>",
14541 "<c:bubbleSize>",
14542 "<c:extLst>",
14543 ]
14544 .iter()
14545 .map(|tag| written.find(tag).unwrap())
14546 .collect();
14547 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14548 assert!(Series::from_xml(written.as_bytes()).is_ok());
14549 }
14550
14551 #[test]
14552 fn every_corpus_series_round_trips_structurally() {
14553 let Some(corpus) = require_or_skip_corpus() else {
14554 return;
14555 };
14556 verify_fetched_corpus(&corpus);
14557 let mut series_count = 0usize;
14558 let mut chart_parts = 0usize;
14559 for path in manifest_paths() {
14560 let package = OpcPackage::open(corpus.join(path))
14561 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14562 for (part, xml) in &package.parts {
14563 if !is_chart_part(part) {
14564 continue;
14565 }
14566 let chart = CT_ChartSpace::from_xml(xml)
14567 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14568 let parsed_series =
14569 chart.chart.plot_area.series().unwrap_or_else(|error| {
14570 panic!("{path} {part}: series parse failed: {error}")
14571 });
14572 for series in parsed_series {
14573 let written = series.to_xml().unwrap_or_else(|error| {
14574 panic!("{path} {part}: series write failed: {error}")
14575 });
14576 let reparsed = Series::from_xml(&written).unwrap_or_else(|error| {
14577 panic!("{path} {part}: written series parse failed: {error}")
14578 });
14579 assert_eq!(series, reparsed, "{path} {part}: series model changed");
14580 series_count += 1;
14581 }
14582 chart_parts += 1;
14583 }
14584 }
14585 assert!(
14586 series_count > 0,
14587 "the pinned corpus contained no supported series"
14588 );
14589 eprintln!(
14590 "ChartML series corpus gate checked {series_count} series across {chart_parts} chart parts"
14591 );
14592 }
14593
14594 #[test]
14595 fn chart_space_reads_aliases_and_writes_fixed_prefixes_in_schema_order() {
14596 let xml = br#"<ch:chartSpace xmlns:ch="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:d="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:rel="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:x="urn:producer"><x:before/><ch:chart><ch:plotVisOnly val="0"/><ch:legend><x:legend/></ch:legend><ch:title><x:title/></ch:title><ch:plotArea><x:plot/></ch:plotArea><ch:dispBlanksAs val="span"/></ch:chart><ch:txPr><d:bodyPr/><d:p/></ch:txPr><ch:spPr><d:solidFill><d:srgbClr val="112233"/></d:solidFill></ch:spPr><x:after/></ch:chartSpace>"#;
14597 let parsed = CT_ChartSpace::from_xml(xml).unwrap();
14598 assert!(!parsed.chart.auto_title_deleted);
14599 assert!(!parsed.chart.plot_vis_only);
14600 assert_eq!(parsed.chart.disp_blanks_as, DispBlanksAs::Span);
14601 assert!(parsed.chart.title.is_some());
14602 assert!(parsed.chart.legend.is_some());
14603 assert!(parsed.sp_pr.is_some());
14604 assert!(parsed.tx_pr.is_some());
14605
14606 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14607 assert!(written.starts_with(
14608 r#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships""#
14609 ));
14610 for tag in [
14611 "<c:chart>",
14612 "<c:title>",
14613 "<c:plotArea>",
14614 "<c:legend>",
14615 "<c:plotVisOnly",
14616 "<c:dispBlanksAs",
14617 "<c:spPr>",
14618 "<c:txPr>",
14619 ] {
14620 assert!(written.contains(tag), "missing fixed-prefix tag {tag}");
14621 }
14622 let positions: Vec<_> = [
14623 "<c:title>",
14624 "<c:plotArea>",
14625 "<c:legend>",
14626 "<c:plotVisOnly",
14627 "<c:dispBlanksAs",
14628 "<c:spPr>",
14629 "<c:txPr>",
14630 ]
14631 .iter()
14632 .map(|tag| written.find(tag).unwrap())
14633 .collect();
14634 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14635 assert!(written.contains("<a:solidFill><a:srgbClr"));
14636 assert!(written.contains("<a:bodyPr/><a:p/>"));
14637 assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
14638 }
14639
14640 #[test]
14641 fn public_core_edits_keep_preserved_children_in_their_schema_slots() {
14642 let xml = br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><c:chart><c:pivotFmts><c:pivotFmt/></c:pivotFmts><c:plotArea/></c:chart><c:externalData r:id="rId1"/></c:chartSpace>"#;
14643 let mut parsed = CT_ChartSpace::from_xml(xml).unwrap();
14644 parsed.chart.auto_title_deleted = true;
14645 parsed.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
14646 parsed.tx_pr =
14647 Some(CT_TextBody::from_xml(br#"<a:txBody><a:bodyPr/><a:p/></a:txBody>"#).unwrap());
14648 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14649 let positions: Vec<_> = [
14650 "<c:autoTitleDeleted",
14651 "<c:pivotFmts>",
14652 "<c:plotArea",
14653 "<c:spPr",
14654 "<c:txPr",
14655 "<c:externalData",
14656 ]
14657 .iter()
14658 .map(|tag| written.find(tag).unwrap())
14659 .collect();
14660 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14661 assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
14662 }
14663
14664 #[test]
14665 fn malformed_core_chart_values_return_errors_without_panicking() {
14666 let cases: &[&[u8]] = &[
14667 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"/>"#,
14668 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart/></c:chartSpace>"#,
14669 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:plotArea/></c:chart></c:chartSpace>"#,
14670 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:autoTitleDeleted val="yes"/><c:plotArea/></c:chart></c:chartSpace>"#,
14671 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:dispBlanksAs val="empty"/></c:chart></c:chartSpace>"#,
14672 br#"<c:chartSpace xmlns:c="urn:wrong"><c:chart><c:plotArea/></c:chart></c:chartSpace>"#,
14673 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:foreign"><x:chart><x:plotArea/></x:chart></c:chartSpace>"#,
14674 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/></c:chart><c:chart><c:plotArea/></c:chart></c:chartSpace>"#,
14675 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:title/><c:title/><c:plotArea/></c:chart></c:chartSpace>"#,
14676 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:plotVisOnly><c:child/></c:plotVisOnly></c:chart></c:chartSpace>"#,
14677 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:foreign"><c:chart><c:plotArea/></c:chart><c:txPr><a:bodyPr/><a:p><x:r><x:t>foreign</x:t></x:r></a:p></c:txPr></c:chartSpace>"#,
14678 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:foreign"><c:chart><c:plotArea/></c:chart><c:spPr><a:solidFill><x:srgbClr val="112233"/></a:solidFill></c:spPr></c:chartSpace>"#,
14679 ];
14680 for xml in cases {
14681 let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml));
14682 assert!(result.is_ok(), "ChartML parser panicked");
14683 assert!(
14684 result.unwrap().is_err(),
14685 "malformed ChartML parsed: {}",
14686 String::from_utf8_lossy(xml)
14687 );
14688 }
14689 }
14690
14691 #[test]
14692 fn point_label_overrides_render_without_changing_preserved_xml() {
14693 let raw_overrides = r#"<q:dLbl><q:idx val="1"/><q:numFmt formatCode="0.0" sourceLinked="0"/><q:dLblPos val="r"/><q:showVal val="1"/><q:showCatName val="1"/><q:separator> / </q:separator><q:extLst><x:point/></q:extLst></q:dLbl>"#;
14694 let plot = bar_plot("").replace(
14695 r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
14696 &format!(
14697 r#"<q:dLbls>{raw_overrides}<q:numFmt formatCode="0" sourceLinked="0"/><q:showVal val="1"/><q:showCatName val="1"/><q:separator>, </q:separator></q:dLbls>"#
14698 ),
14699 );
14700 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
14701 let plot = &chart.chart.plot_area.plots().unwrap()[0];
14702 let Plot::Bar { data_labels, .. } = plot else {
14703 panic!("expected bar plot");
14704 };
14705 let labels = data_labels.as_ref().unwrap();
14706 let written = labels.to_xml().unwrap();
14707 assert!(
14708 written
14709 .windows(raw_overrides.len())
14710 .any(|window| window == raw_overrides.as_bytes())
14711 );
14712 let overrides = super::point_label_overrides(labels).unwrap();
14713 assert_eq!(overrides.keys().copied().collect::<Vec<_>>(), vec![1]);
14714
14715 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14716 let mut rendered = Vec::new();
14717 super::render_data_labels(
14718 plot,
14719 chart_bounds(),
14720 DispBlanksAs::Gap,
14721 super::GeometryOptions::default(),
14722 &mut fonts,
14723 &mut rendered,
14724 )
14725 .unwrap();
14726 let labels: Vec<_> = rendered
14727 .iter()
14728 .filter_map(|element| match element {
14729 PositionedElement::Text(run) => Some(run),
14730 _ => None,
14731 })
14732 .collect();
14733 assert_eq!(labels.len(), 2);
14734 assert_eq!(labels[0].text, "North, 1");
14735 assert_eq!(labels[1].text, "South / 2.0");
14736 assert_point_near(labels[0].origin, Point { x: 50.0, y: 105.0 });
14737 assert_point_near(labels[1].origin, Point { x: 158.0, y: 70.0 });
14738 assert_eq!(
14739 CT_DLbls::from_xml(&written).unwrap().to_xml().unwrap(),
14740 written
14741 );
14742 }
14743
14744 #[test]
14745 fn malformed_point_label_overrides_return_contextual_errors() {
14746 let cases = [
14747 r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:showVal val="1"/></q:dLbl></q:dLbls>"#,
14748 r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="0"/><q:idx val="1"/></q:dLbl></q:dLbls>"#,
14749 r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="point"/></q:dLbl></q:dLbls>"#,
14750 r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="0"/><q:showVal val="maybe"/></q:dLbl></q:dLbls>"#,
14751 r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="0"/><q:dLblPos val="middle"/></q:dLbl></q:dLbls>"#,
14752 r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="0"/><q:showVal val="1"/><q:dLblPos val="r"/></q:dLbl></q:dLbls>"#,
14753 r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="0"/></q:dLbl><q:dLbl><q:idx val="0"/></q:dLbl></q:dLbls>"#,
14754 ];
14755 for xml in cases {
14756 let labels = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
14757 let result = std::panic::catch_unwind(|| super::point_label_overrides(&labels));
14758 assert!(result.is_ok(), "point-label parser panicked for {xml}");
14759 assert!(
14760 result.unwrap().is_err(),
14761 "malformed point labels parsed: {xml}"
14762 );
14763 }
14764
14765 let foreign = format!(
14766 r#"<q:dLbls xmlns:q="{C_NS}" xmlns:x="urn:foreign"><x:dLbl><x:idx val="0"/></x:dLbl></q:dLbls>"#
14767 );
14768 let labels = CT_DLbls::from_xml(foreign.as_bytes()).unwrap();
14769 assert!(super::point_label_overrides(&labels).unwrap().is_empty());
14770 }
14771
14772 #[test]
14773 fn percentage_label_aggregation_rejects_nonfinite_totals() {
14774 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14775 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14776 let Plot::Bar { series, .. } = plot else {
14777 panic!("expected bar plot");
14778 };
14779 series[0].values.values = vec![f64::MAX, f64::MAX];
14780 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14781 super::render_data_labels(
14782 plot,
14783 chart_bounds(),
14784 DispBlanksAs::Gap,
14785 super::GeometryOptions::default(),
14786 &mut fonts,
14787 &mut Vec::new(),
14788 )
14789 .expect("value-only labels do not need a percentage total");
14790 let Plot::Bar { data_labels, .. } = plot else {
14791 panic!("expected bar plot");
14792 };
14793 data_labels.as_mut().unwrap().show_percent = true;
14794 let error = super::render_data_labels(
14795 plot,
14796 chart_bounds(),
14797 DispBlanksAs::Gap,
14798 super::GeometryOptions::default(),
14799 &mut fonts,
14800 &mut Vec::new(),
14801 )
14802 .unwrap_err();
14803 assert!(error.to_string().contains("data-label percentage total"));
14804
14805 let overrides = r#"<q:dLbl><q:idx val="0"/><q:showPercent val="0"/></q:dLbl><q:dLbl><q:idx val="1"/><q:showPercent val="0"/></q:dLbl>"#;
14806 let plot_xml = bar_plot("").replace(
14807 r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
14808 &format!(
14809 r#"<q:dLbls>{overrides}<q:showVal val="1"/><q:showPercent val="1"/></q:dLbls>"#
14810 ),
14811 );
14812 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
14813 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14814 let Plot::Bar { series, .. } = plot else {
14815 panic!("expected bar plot");
14816 };
14817 series[0].values.values = vec![f64::MAX, f64::MAX];
14818 super::render_data_labels(
14819 plot,
14820 chart_bounds(),
14821 DispBlanksAs::Gap,
14822 super::GeometryOptions::default(),
14823 &mut fonts,
14824 &mut Vec::new(),
14825 )
14826 .expect("effective point overrides disable percentage aggregation");
14827 }
14828
14829 #[test]
14830 fn percentage_labels_use_effective_number_format_precision() {
14831 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14832 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14833 let Plot::Bar {
14834 series,
14835 data_labels,
14836 ..
14837 } = plot
14838 else {
14839 panic!("expected bar plot");
14840 };
14841 series[0].values.values = vec![1.0, 2.0];
14842 let labels = data_labels.as_mut().unwrap();
14843 labels.show_value = false;
14844 labels.show_percent = true;
14845 labels.number_format = Some(NumberFormat::new("0.00%".to_owned(), false).unwrap());
14846 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14847 let mut rendered = Vec::new();
14848 super::render_data_labels(
14849 plot,
14850 chart_bounds(),
14851 DispBlanksAs::Gap,
14852 super::GeometryOptions::default(),
14853 &mut fonts,
14854 &mut rendered,
14855 )
14856 .unwrap();
14857 let text: Vec<_> = rendered
14858 .iter()
14859 .filter_map(|element| match element {
14860 PositionedElement::Text(run) => Some(run.text.as_str()),
14861 _ => None,
14862 })
14863 .collect();
14864 assert_eq!(text, vec!["33.33%", "66.67%"]);
14865
14866 let point_override = r#"<q:dLbl><q:idx val="0"/><q:numFmt formatCode="0.00%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbl>"#;
14867 let plot_xml = bar_plot("").replace(
14868 r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
14869 &format!(
14870 r#"<q:dLbls>{point_override}<q:numFmt formatCode="0%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbls>"#
14871 ),
14872 );
14873 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
14874 let plot = &chart.chart.plot_area.plots().unwrap()[0];
14875 let mut rendered = Vec::new();
14876 super::render_data_labels(
14877 plot,
14878 chart_bounds(),
14879 DispBlanksAs::Gap,
14880 super::GeometryOptions::default(),
14881 &mut fonts,
14882 &mut rendered,
14883 )
14884 .unwrap();
14885 let text: Vec<_> = rendered
14886 .iter()
14887 .filter_map(|element| match element {
14888 PositionedElement::Text(run) => Some(run.text.as_str()),
14889 _ => None,
14890 })
14891 .collect();
14892 assert_eq!(text, vec!["33.33%", "67%"]);
14893 }
14894
14895 #[test]
14896 fn core_chart_shells_preserve_unmodelled_children_byte_for_byte() {
14897 let preserved = [
14898 br#"<x:root x:id="1"><x:nested>one & two</x:nested><!--root--></x:root>"#
14899 .as_slice(),
14900 br#"<x:title x:id="2"><x:nested><![CDATA[title]]></x:nested></x:title>"#.as_slice(),
14901 br#"<x:plot x:id="3"><x:nested>plot</x:nested></x:plot>"#.as_slice(),
14902 br#"<x:legend x:id="4"><x:nested>legend</x:nested></x:legend>"#.as_slice(),
14903 br#"<x:extension x:id="5"><x:nested>extension</x:nested></x:extension>"#.as_slice(),
14904 br#"<!--space-comment-->"#.as_slice(),
14905 br#"<?space processing?>"#.as_slice(),
14906 br#"<!--chart-comment-->"#.as_slice(),
14907 br#"<?title processing?>"#.as_slice(),
14908 br#"<!--flag-comment-->"#.as_slice(),
14909 br#"<?flag processing?>"#.as_slice(),
14910 br#"<x:producerShape/>"#.as_slice(),
14911 br#"<x:producerText/>"#.as_slice(),
14912 ];
14913 let xml = br#"<q:chartSpace xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer"><!--space-comment--><?space processing?><x:root x:id="1"><x:nested>one & two</x:nested><!--root--></x:root><q:chart><!--chart-comment--><q:autoTitleDeleted val="0" x:keep="one & two"><!--flag-comment--><?flag processing?></q:autoTitleDeleted><q:title data="keep"><?title processing?><x:title x:id="2"><x:nested><![CDATA[title]]></x:nested></x:title></q:title><q:plotArea data="keep"><x:plot x:id="3"><x:nested>plot</x:nested></x:plot></q:plotArea><q:legend data="keep"><x:legend x:id="4"><x:nested>legend</x:nested></x:legend></q:legend><x:extension x:id="5"><x:nested>extension</x:nested></x:extension></q:chart><q:spPr><x:producerShape/></q:spPr><q:txPr><a:bodyPr/><a:p><x:producerText/></a:p></q:txPr></q:chartSpace>"#;
14914 let parsed = CT_ChartSpace::from_xml(xml).unwrap();
14915 let written = parsed.to_xml().unwrap();
14916 for raw in preserved {
14917 assert!(
14918 written.windows(raw.len()).any(|window| window == raw),
14919 "preserved subtree changed: {}",
14920 String::from_utf8_lossy(raw)
14921 );
14922 }
14923 let written_text = String::from_utf8(written.clone()).unwrap();
14924 assert!(written_text.contains(r#"x:keep="one & two""#));
14925 assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
14926 }
14927
14928 #[test]
14929 fn every_corpus_chart_part_round_trips_structurally() {
14930 let Some(corpus) = require_or_skip_corpus() else {
14931 return;
14932 };
14933 verify_fetched_corpus(&corpus);
14934 let mut chart_parts = 0usize;
14935 let mut decks_with_charts = HashSet::new();
14936 for path in manifest_paths() {
14937 let package = OpcPackage::open(corpus.join(path))
14938 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14939 for (part, xml) in &package.parts {
14940 if !is_chart_part(part) {
14941 continue;
14942 }
14943 let parsed = CT_ChartSpace::from_xml(xml)
14944 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14945 let written = parsed
14946 .to_xml()
14947 .unwrap_or_else(|error| panic!("{path} {part}: write failed: {error}"));
14948 let original_preserved = preserved_chartml_regions(xml)
14949 .unwrap_or_else(|error| panic!("{path} {part}: original scan failed: {error}"));
14950 let written_preserved = preserved_chartml_regions(&written)
14951 .unwrap_or_else(|error| panic!("{path} {part}: written scan failed: {error}"));
14952 assert_eq!(
14953 original_preserved.len(),
14954 written_preserved.len(),
14955 "{path} {part}: preserved ChartML region count changed"
14956 );
14957 for (index, (original, written)) in original_preserved
14958 .iter()
14959 .zip(&written_preserved)
14960 .enumerate()
14961 {
14962 assert_eq!(
14963 original.parent_path, written.parent_path,
14964 "{path} {part}: preserved region {index} parent changed"
14965 );
14966 assert_eq!(
14967 original.boundary, written.boundary,
14968 "{path} {part}: preserved region {index} boundary changed"
14969 );
14970 assert_eq!(
14971 original.sibling_order, written.sibling_order,
14972 "{path} {part}: preserved region {index} order changed"
14973 );
14974 assert_eq!(
14975 original.xml, written.xml,
14976 "{path} {part}: preserved region {index} bytes changed"
14977 );
14978 }
14979 let reparsed = CT_ChartSpace::from_xml(&written)
14980 .unwrap_or_else(|error| panic!("{path} {part}: written parse failed: {error}"));
14981 assert_eq!(parsed, reparsed, "{path} {part}: core model changed");
14982 chart_parts += 1;
14983 decks_with_charts.insert(path);
14984 }
14985 }
14986 assert!(
14987 chart_parts > 0,
14988 "the pinned corpus contained no chart parts"
14989 );
14990 eprintln!(
14991 "ChartML corpus gate checked {chart_parts} chart parts across {} decks",
14992 decks_with_charts.len()
14993 );
14994 }
14995
14996 #[derive(Debug, Eq, PartialEq)]
14997 struct PreservedChartRegion {
14998 parent_path: Vec<Vec<u8>>,
14999 boundary: usize,
15000 sibling_order: usize,
15001 xml: Vec<u8>,
15002 }
15003
15004 struct PreservationFrame {
15005 local_name: Vec<u8>,
15006 boundary: usize,
15007 next_sibling: usize,
15008 }
15009
15010 fn preserved_chartml_regions(xml: &[u8]) -> Result<Vec<PreservedChartRegion>, String> {
15011 let mut reader = Reader::from_reader(xml);
15012 let mut buffer = Vec::new();
15013 let mut parents: Vec<PreservationFrame> = Vec::new();
15014 let mut preserved = Vec::new();
15015 loop {
15016 match reader.read_event_into(&mut buffer) {
15017 Ok(Event::Start(element)) => {
15018 let child = local_name(element.name().as_ref()).to_vec();
15019 if parents.is_empty() && child == b"chartSpace" {
15020 parents.push(PreservationFrame {
15021 local_name: child,
15022 boundary: 0,
15023 next_sibling: 0,
15024 });
15025 } else if let Some(parent) = parents.last()
15026 && is_opaque_chartml_child(&parent.local_name, &child)
15027 {
15028 let boundary =
15029 preserved_child_boundary(&parent.local_name, &child, parent.boundary);
15030 let xml = capture_element(&mut reader, &element)
15031 .map_err(|error| error.to_string())?;
15032 record_preserved_region(&mut parents, &mut preserved, boundary, xml);
15033 advance_preservation_boundary(&mut parents, &child);
15034 } else {
15035 advance_preservation_boundary(&mut parents, &child);
15036 if is_chartml_preservation_parent(&child) {
15037 parents.push(PreservationFrame {
15038 local_name: child,
15039 boundary: 0,
15040 next_sibling: 0,
15041 });
15042 } else {
15043 capture_element(&mut reader, &element)
15044 .map_err(|error| error.to_string())?;
15045 }
15046 }
15047 }
15048 Ok(Event::Empty(element)) => {
15049 let name = element.name();
15050 let child = local_name(name.as_ref()).to_vec();
15051 if let Some(parent) = parents.last()
15052 && is_opaque_chartml_child(&parent.local_name, &child)
15053 {
15054 let boundary =
15055 preserved_child_boundary(&parent.local_name, &child, parent.boundary);
15056 let xml =
15057 capture_empty_element(&element).map_err(|error| error.to_string())?;
15058 record_preserved_region(&mut parents, &mut preserved, boundary, xml);
15059 }
15060 advance_preservation_boundary(&mut parents, &child);
15061 }
15062 Ok(
15063 event @ (Event::Comment(_)
15064 | Event::PI(_)
15065 | Event::CData(_)
15066 | Event::GeneralRef(_)),
15067 ) if !parents.is_empty() => {
15068 let boundary = parents.last().expect("preservation parent").boundary;
15069 let xml = capture_event(event).map_err(|error| error.to_string())?;
15070 record_preserved_region(&mut parents, &mut preserved, boundary, xml);
15071 }
15072 Ok(Event::End(_)) => {
15073 parents.pop();
15074 }
15075 Ok(Event::Eof) => break,
15076 Ok(_) => {}
15077 Err(error) => return Err(error.to_string()),
15078 }
15079 buffer.clear();
15080 }
15081 Ok(preserved)
15082 }
15083
15084 fn record_preserved_region(
15085 parents: &mut [PreservationFrame],
15086 preserved: &mut Vec<PreservedChartRegion>,
15087 boundary: usize,
15088 xml: Vec<u8>,
15089 ) {
15090 let sibling_order = parents.last().expect("preservation parent").next_sibling;
15091 parents
15092 .last_mut()
15093 .expect("preservation parent")
15094 .next_sibling += 1;
15095 preserved.push(PreservedChartRegion {
15096 parent_path: parents
15097 .iter()
15098 .map(|parent| parent.local_name.clone())
15099 .collect(),
15100 boundary,
15101 sibling_order,
15102 xml,
15103 });
15104 }
15105
15106 fn advance_preservation_boundary(parents: &mut [PreservationFrame], child: &[u8]) {
15107 let Some(parent) = parents.last_mut() else {
15108 return;
15109 };
15110 parent.boundary = parent
15111 .boundary
15112 .max(preserved_boundary_after(&parent.local_name, child));
15113 }
15114
15115 fn preserved_child_boundary(parent: &[u8], child: &[u8], current: usize) -> usize {
15116 match parent {
15117 b"chartSpace" => match child {
15118 b"date1904" => 0,
15119 b"lang" => 1,
15120 b"roundedCorners" => 2,
15121 b"style" => 3,
15122 b"clrMapOvr" => 4,
15123 b"pivotSource" => 5,
15124 b"protection" => 6,
15125 b"externalData" => 10,
15126 b"printSettings" => 11,
15127 b"userShapes" => 12,
15128 b"extLst" => 13,
15129 _ => current,
15130 },
15131 b"chart" => match child {
15132 b"pivotFmts" => 2,
15133 b"view3D" => 3,
15134 b"floor" => 4,
15135 b"sideWall" => 5,
15136 b"backWall" => 6,
15137 b"showDLblsOverMax" | b"extLst" => 11,
15138 _ => current,
15139 },
15140 _ => current,
15141 }
15142 }
15143
15144 fn preserved_boundary_after(parent: &[u8], child: &[u8]) -> usize {
15145 match parent {
15146 b"chartSpace" => match child {
15147 b"date1904" => 1,
15148 b"lang" => 2,
15149 b"roundedCorners" => 3,
15150 b"style" => 4,
15151 b"clrMapOvr" => 5,
15152 b"pivotSource" => 6,
15153 b"protection" => 7,
15154 b"chart" => 8,
15155 b"spPr" => 9,
15156 b"txPr" => 10,
15157 b"externalData" => 11,
15158 b"printSettings" => 12,
15159 b"userShapes" => 13,
15160 b"extLst" => 14,
15161 _ => 0,
15162 },
15163 b"chart" => match child {
15164 b"title" => 1,
15165 b"autoTitleDeleted" => 2,
15166 b"pivotFmts" => 3,
15167 b"view3D" => 4,
15168 b"floor" => 5,
15169 b"sideWall" => 6,
15170 b"backWall" => 7,
15171 b"plotArea" => 8,
15172 b"legend" => 9,
15173 b"plotVisOnly" => 10,
15174 b"dispBlanksAs" => 11,
15175 b"showDLblsOverMax" | b"extLst" => 11,
15176 _ => 0,
15177 },
15178 b"pieChart" => match child {
15179 b"ser" => 1,
15180 b"dLbls" => 2,
15181 b"firstSliceAng" => 3,
15182 _ => 0,
15183 },
15184 b"doughnutChart" => match child {
15185 b"ser" => 1,
15186 b"dLbls" => 2,
15187 b"firstSliceAng" => 3,
15188 b"holeSize" => 4,
15189 _ => 0,
15190 },
15191 b"areaChart" => match child {
15192 b"grouping" => 1,
15193 b"ser" => 2,
15194 b"dLbls" => 3,
15195 b"axId" => 4,
15196 _ => 0,
15197 },
15198 b"scatterChart" | b"radarChart" => match child {
15199 b"scatterStyle" | b"radarStyle" => 1,
15200 b"ser" => 2,
15201 b"dLbls" => 3,
15202 b"axId" => 4,
15203 _ => 0,
15204 },
15205 _ => 0,
15206 }
15207 }
15208
15209 fn is_opaque_chartml_child(parent: &[u8], child: &[u8]) -> bool {
15210 match parent {
15211 b"chartSpace" => !matches!(child, b"chart" | b"spPr" | b"txPr"),
15212 b"chart" => !matches!(
15213 child,
15214 b"title"
15215 | b"autoTitleDeleted"
15216 | b"plotArea"
15217 | b"legend"
15218 | b"plotVisOnly"
15219 | b"dispBlanksAs"
15220 ),
15221 b"plotArea" => !matches!(
15222 child,
15223 b"barChart"
15224 | b"lineChart"
15225 | b"pieChart"
15226 | b"doughnutChart"
15227 | b"areaChart"
15228 | b"scatterChart"
15229 | b"radarChart"
15230 | b"catAx"
15231 | b"valAx"
15232 | b"dateAx"
15233 | b"serAx"
15234 ),
15235 b"pieChart" => !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng"),
15236 b"doughnutChart" => {
15237 !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng" | b"holeSize")
15238 }
15239 b"areaChart" => !matches!(child, b"grouping" | b"ser" | b"dLbls" | b"axId"),
15240 b"scatterChart" => !matches!(child, b"scatterStyle" | b"ser" | b"dLbls" | b"axId"),
15241 b"radarChart" => !matches!(child, b"radarStyle" | b"ser" | b"dLbls" | b"axId"),
15242 b"title" | b"legend" => true,
15243 _ => false,
15244 }
15245 }
15246
15247 fn is_chartml_preservation_parent(parent: &[u8]) -> bool {
15248 matches!(
15249 parent,
15250 b"chartSpace"
15251 | b"chart"
15252 | b"title"
15253 | b"plotArea"
15254 | b"pieChart"
15255 | b"doughnutChart"
15256 | b"areaChart"
15257 | b"scatterChart"
15258 | b"radarChart"
15259 | b"legend"
15260 | b"autoTitleDeleted"
15261 | b"plotVisOnly"
15262 | b"dispBlanksAs"
15263 )
15264 }
15265
15266 #[test]
15267 fn oxml_chart_is_an_explicit_publication_candidate() {
15268 let manifest = include_str!("../Cargo.toml");
15269 assert!(manifest.contains("name = \"oxml-chart\""));
15270 assert!(manifest.contains("version = \"0.7.0\""));
15271 assert!(manifest.contains("publish = true"));
15272 for dependency in [
15273 "oxml-core.workspace",
15274 "oxml-drawing.workspace",
15275 "quick-xml.workspace",
15276 ] {
15277 assert!(manifest.contains(dependency));
15278 }
15279 assert!(!manifest.contains("rpptx.workspace"));
15280 assert!(!manifest.contains("rdocx.workspace"));
15281 }
15282
15283 fn first_series_xml(xml: &[u8]) -> Option<Vec<u8>> {
15284 let mut reader = Reader::from_reader(xml);
15285 let mut buffer = Vec::new();
15286 loop {
15287 match reader.read_event_into(&mut buffer).ok()? {
15288 Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
15289 return capture_element(&mut reader, &element).ok();
15290 }
15291 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
15292 return capture_empty_element(&element).ok();
15293 }
15294 Event::Eof => return None,
15295 _ => {}
15296 }
15297 buffer.clear();
15298 }
15299 }
15300
15301 fn replace_once(haystack: &[u8], needle: &[u8], replacement: &[u8]) -> Vec<u8> {
15302 let offset = haystack
15303 .windows(needle.len())
15304 .position(|window| window == needle)
15305 .expect("serialized source series occurs in chart part");
15306 assert!(
15307 !haystack[offset + needle.len()..]
15308 .windows(needle.len())
15309 .any(|window| window == needle),
15310 "source series unexpectedly occurs more than once"
15311 );
15312 let mut result = Vec::with_capacity(haystack.len() - needle.len() + replacement.len());
15313 result.extend_from_slice(&haystack[..offset]);
15314 result.extend_from_slice(replacement);
15315 result.extend_from_slice(&haystack[offset + needle.len()..]);
15316 result
15317 }
15318
15319 fn assert_command_version(program: &str, arguments: &[&str], expected: &str) {
15320 let output = Command::new(program)
15321 .args(arguments)
15322 .output()
15323 .unwrap_or_else(|error| panic!("run {program} version check: {error}"));
15324 assert!(
15325 output.status.success(),
15326 "{program} version check failed: {}",
15327 String::from_utf8_lossy(&output.stderr)
15328 );
15329 let stdout = String::from_utf8(output.stdout).expect("version stdout is UTF-8");
15330 let stderr = String::from_utf8(output.stderr).expect("version stderr is UTF-8");
15331 let actual = if stdout.trim().is_empty() {
15332 stderr.trim()
15333 } else {
15334 stdout.trim()
15335 };
15336 assert_eq!(
15337 actual.lines().next(),
15338 Some(expected),
15339 "{program} version drift"
15340 );
15341 }
15342
15343 fn sha256(path: &Path) -> String {
15344 let output = Command::new("shasum")
15345 .args(["-a", "256"])
15346 .arg(path)
15347 .output()
15348 .unwrap_or_else(|error| panic!("{}: run shasum: {error}", path.display()));
15349 assert!(
15350 output.status.success(),
15351 "{}: shasum failed: {}",
15352 path.display(),
15353 String::from_utf8_lossy(&output.stderr)
15354 );
15355 String::from_utf8(output.stdout)
15356 .expect("shasum output is UTF-8")
15357 .split_whitespace()
15358 .next()
15359 .expect("shasum digest")
15360 .to_owned()
15361 }
15362
15363 fn workspace_root() -> PathBuf {
15364 Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
15365 }
15366
15367 fn corpus_dir() -> PathBuf {
15368 std::env::var_os("RDOCX_PPTX_CORPUS_DIR")
15369 .map(PathBuf::from)
15370 .unwrap_or_else(|| workspace_root().join("corpus/pptx"))
15371 }
15372
15373 fn require_or_skip_corpus() -> Option<PathBuf> {
15374 let corpus = corpus_dir();
15375 if corpus.is_dir() {
15376 return Some(corpus);
15377 }
15378 assert_ne!(
15379 std::env::var_os("RDOCX_PPTX_CORPUS_REQUIRED").as_deref(),
15380 Some(std::ffi::OsStr::new("1")),
15381 "the pinned corpus is required but {} does not exist",
15382 corpus.display()
15383 );
15384 eprintln!(
15385 "ChartML corpus gate skipped because {} is absent",
15386 corpus.display()
15387 );
15388 None
15389 }
15390
15391 fn verify_fetched_corpus(corpus: &Path) {
15392 let status = Command::new("python3")
15393 .arg(workspace_root().join("scripts/fetch_pptx_corpus.py"))
15394 .arg("--check")
15395 .env("RDOCX_PPTX_CORPUS_DIR", corpus)
15396 .status()
15397 .expect("run corpus verifier");
15398 assert!(status.success(), "pinned corpus verification failed");
15399 }
15400
15401 fn manifest_paths() -> Vec<&'static str> {
15402 let mut lines = MANIFEST.lines();
15403 assert_eq!(lines.next(), Some("path\tproducer\tsha256\turl"));
15404 let paths: Vec<_> = lines
15405 .map(|line| line.split('\t').next().expect("manifest path"))
15406 .collect();
15407 assert_eq!(paths.len(), EXPECTED_DECKS);
15408 paths
15409 }
15410
15411 fn is_chart_part(part: &str) -> bool {
15412 let Some(stem) = part
15413 .strip_prefix("/ppt/charts/chart")
15414 .and_then(|name| name.strip_suffix(".xml"))
15415 else {
15416 return false;
15417 };
15418 !stem.is_empty() && stem.bytes().all(|byte| byte.is_ascii_digit())
15419 }
15420
15421 fn plot_series(index: u32) -> String {
15422 format!(
15423 r#"<q:ser><q:idx val="{index}"/><q:order val="{index}"/><q:cat><q:strRef><q:f>Sheet1!$A$2:$A$3</q:f><q:strCache><q:ptCount val="2"/><q:pt idx="0"><q:v>North</q:v></q:pt><q:pt idx="1"><q:v>South</q:v></q:pt></q:strCache></q:strRef></q:cat><q:val><q:numRef><q:f>Sheet1!$B$2:$B$3</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="2"/><q:pt idx="0"><q:v>1</q:v></q:pt><q:pt idx="1"><q:v>2</q:v></q:pt></q:numCache></q:numRef></q:val></q:ser>"#
15424 )
15425 }
15426
15427 fn sparse_category_series() -> &'static str {
15428 r#"<q:ser><q:idx val="0"/><q:order val="0"/><q:cat><q:strRef><q:f>Sheet1!$A$2:$A$4</q:f><q:strCache><q:ptCount val="3"/><q:pt idx="0"><q:v>North</q:v></q:pt><q:pt idx="2"><q:v>West</q:v></q:pt></q:strCache></q:strRef></q:cat><q:val><q:numRef><q:f>Sheet1!$B$2:$B$4</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="3"/><q:pt idx="0"><q:v>1</q:v></q:pt><q:pt idx="2"><q:v>2</q:v></q:pt></q:numCache></q:numRef></q:val></q:ser>"#
15429 }
15430
15431 fn sparse_scatter_series() -> &'static str {
15432 r#"<q:ser><q:idx val="0"/><q:order val="0"/><q:xVal><q:numRef><q:f>Sheet1!$A$2:$A$5</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="4"/><q:pt idx="0"><q:v>10</q:v></q:pt><q:pt idx="2"><q:v>30</q:v></q:pt><q:pt idx="3"><q:v>40</q:v></q:pt></q:numCache></q:numRef></q:xVal><q:yVal><q:numRef><q:f>Sheet1!$B$2:$B$5</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="4"/><q:pt idx="0"><q:v>100</q:v></q:pt><q:pt idx="1"><q:v>200</q:v></q:pt><q:pt idx="3"><q:v>400</q:v></q:pt></q:numCache></q:numRef></q:yVal></q:ser>"#
15433 }
15434
15435 fn chart_with_plot(plot: &str) -> String {
15436 format!(
15437 r#"<q:chartSpace xmlns:q="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}" xmlns:x="urn:producer"><q:chart><q:plotArea>{plot}<q:catAx><q:axId val="-1884094432"/><q:scaling/><q:axPos val="b"/><q:crossAx val="-1884097184"/></q:catAx><q:valAx><q:axId val="-1884097184"/><q:scaling/><q:axPos val="l"/><q:crossAx val="-1884094432"/></q:valAx></q:plotArea></q:chart></q:chartSpace>"#
15438 )
15439 }
15440
15441 fn chart_with_optional_axes(plot: &str, axes: bool) -> String {
15442 let axis_xml = if axes {
15443 r#"<q:catAx><q:axId val="-1884094432"/><q:scaling/><q:axPos val="b"/><q:crossAx val="-1884097184"/></q:catAx><q:valAx><q:axId val="-1884097184"/><q:scaling/><q:axPos val="l"/><q:crossAx val="-1884094432"/></q:valAx>"#
15444 } else {
15445 ""
15446 };
15447 format!(
15448 r#"<q:chartSpace xmlns:q="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}" xmlns:x="urn:producer"><q:chart><q:plotArea>{plot}{axis_xml}</q:plotArea></q:chart></q:chartSpace>"#
15449 )
15450 }
15451
15452 fn numeric_plot_series(index: u32) -> String {
15453 format!(
15454 r#"<q:ser><q:idx val="{index}"/><q:order val="{index}"/><q:xVal><q:numRef><q:f>Sheet1!$A$2:$A$3</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="2"/><q:pt idx="0"><q:v>1</q:v></q:pt><q:pt idx="1"><q:v>2</q:v></q:pt></q:numCache></q:numRef></q:xVal><q:yVal><q:numRef><q:f>Sheet1!$B$2:$B$3</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="2"/><q:pt idx="0"><q:v>4</q:v></q:pt><q:pt idx="1"><q:v>8</q:v></q:pt></q:numCache></q:numRef></q:yVal></q:ser>"#
15455 )
15456 }
15457
15458 fn scatter_without_x(index: u32) -> String {
15459 format!(
15460 r#"<q:ser><q:idx val="{index}"/><q:order val="{index}"/><q:yVal><q:numRef><q:f>Sheet1!$B$2</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="1"/><q:pt idx="0"><q:v>4</q:v></q:pt></q:numCache></q:numRef></q:yVal></q:ser>"#
15461 )
15462 }
15463
15464 fn scatter_without_y(index: u32) -> String {
15465 format!(
15466 r#"<q:ser><q:idx val="{index}"/><q:order val="{index}"/><q:xVal><q:numRef><q:f>Sheet1!$A$2</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="1"/><q:pt idx="0"><q:v>1</q:v></q:pt></q:numCache></q:numRef></q:xVal></q:ser>"#
15467 )
15468 }
15469
15470 fn remaining_plot_fixtures() -> Vec<(&'static str, String, bool)> {
15471 let series = plot_series(0);
15472 let scatter = numeric_plot_series(0);
15473 vec![
15474 (
15475 "pieChart",
15476 format!(
15477 r#"<q:pieChart x:keep="pie"><q:varyColors val="1"/>{series}<q:dLbls><q:showVal val="1"/></q:dLbls><q:firstSliceAng val="30"/><q:extLst><x:pie/></q:extLst></q:pieChart>"#
15478 ),
15479 false,
15480 ),
15481 (
15482 "doughnutChart",
15483 format!(
15484 r#"<q:doughnutChart x:keep="doughnut"><q:varyColors val="1"/>{series}<q:dLbls><q:showPercent val="1"/></q:dLbls><q:firstSliceAng val="45"/><q:holeSize val="60"/><q:extLst><x:doughnut/></q:extLst></q:doughnutChart>"#
15485 ),
15486 false,
15487 ),
15488 (
15489 "areaChart",
15490 format!(
15491 r#"<q:areaChart x:keep="area"><q:grouping val="standard"/><q:varyColors val="0"/>{series}<q:dLbls><q:showVal val="1"/></q:dLbls><q:dropLines><x:line/></q:dropLines><q:axId val="-1884094432"/><q:axId val="-1884097184"/><q:extLst><x:area/></q:extLst></q:areaChart>"#
15492 ),
15493 true,
15494 ),
15495 (
15496 "scatterChart",
15497 format!(
15498 r#"<q:scatterChart x:keep="scatter"><q:scatterStyle val="marker"/><q:varyColors val="0"/>{scatter}<q:dLbls><q:showVal val="1"/></q:dLbls><q:axId val="-1884094432"/><q:axId val="-1884097184"/><q:extLst><x:scatter/></q:extLst></q:scatterChart>"#
15499 ),
15500 true,
15501 ),
15502 (
15503 "radarChart",
15504 format!(
15505 r#"<q:radarChart x:keep="radar"><q:radarStyle val="marker"/><q:varyColors val="0"/>{series}<q:dLbls><q:showVal val="1"/></q:dLbls><q:axId val="-1884094432"/><q:axId val="-1884097184"/><q:extLst><x:radar/></q:extLst></q:radarChart>"#
15506 ),
15507 true,
15508 ),
15509 ]
15510 }
15511
15512 fn bar_plot(extra: &str) -> String {
15513 format!(
15514 r#"<q:barChart x:keep="bar"><q:barDir val="col"/><q:grouping val="clustered"/><q:varyColors val="1"/>{}<q:dLbls><q:showVal val="1"/></q:dLbls><q:gapWidth val="150"/><q:overlap val="0"/><q:serLines><x:line/></q:serLines><q:axId val="-1884094432"/><q:axId val="-1884097184"/>{extra}</q:barChart>"#,
15515 plot_series(0)
15516 )
15517 }
15518
15519 fn line_plot(extra: &str) -> String {
15520 format!(
15521 r#"<q:lineChart x:keep="line"><q:grouping val="standard"/><q:varyColors val="0"/>{}<q:dLbls><q:showVal val="1"/></q:dLbls><q:dropLines><x:line/></q:dropLines><q:marker val="1"/><q:smooth val="0"/><q:axId val="-1884094432"/><q:axId val="-1884097184"/>{extra}</q:lineChart>"#,
15522 plot_series(0)
15523 )
15524 }
15525
15526 #[test]
15527 fn bar_and_line_plots_round_trip_and_render() {
15528 for (xml, is_bar) in [
15529 (chart_with_plot(&bar_plot("")), true),
15530 (chart_with_plot(&line_plot("")), false),
15531 ] {
15532 let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15533 let plots = parsed.chart.plot_area.plots().unwrap();
15534 assert_eq!(plots.len(), 1);
15535 assert_eq!(matches!(plots[0], Plot::Bar { .. }), is_bar);
15536 assert_eq!(matches!(plots[0], Plot::Line { .. }), !is_bar);
15537 assert_eq!(parsed.chart.plot_area.axes().unwrap().len(), 2);
15538 let written = parsed.to_xml().unwrap();
15539 let reparsed = CT_ChartSpace::from_xml(&written).unwrap();
15540 assert_eq!(parsed, reparsed);
15541 }
15542 if let Some(corpus) = require_or_skip_corpus() {
15543 verify_bar_and_line_viewer_gate(&corpus);
15544 }
15545 }
15546
15547 #[test]
15548 fn ppm_parser_preserves_whitespace_valued_first_pixels() {
15549 for first_pixel in *b" \n\r\t" {
15550 let mut ppm = b"P6\n1 1\n255\n".to_vec();
15551 ppm.extend_from_slice(&[first_pixel, 0x7f, 0xff]);
15552 let (width, height, pixels) = ppm_pixels(&ppm);
15553 assert_eq!((width, height), (1, 1));
15554 assert_eq!(pixels, [first_pixel, 0x7f, 0xff]);
15555 }
15556
15557 let ppm = b"P6\r\n1 1\r\n255\r\n\x7f\xff";
15558 assert_eq!(ppm_pixels(ppm).2, [b'\n', 0x7f, 0xff]);
15559 }
15560
15561 #[test]
15562 fn bar_and_line_plots_write_fixed_prefixes_in_schema_order() {
15563 for xml in [
15564 chart_with_plot(&bar_plot(r#"<q:extLst><x:bar-tail/></q:extLst>"#)),
15565 chart_with_plot(&line_plot(r#"<q:extLst><x:line-tail/></q:extLst>"#)),
15566 ] {
15567 let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15568 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15569 assert!(written.contains("<c:barChart") || written.contains("<c:lineChart"));
15570 assert!(!written.contains("<q:barChart"));
15571 assert!(!written.contains("<q:lineChart"));
15572 if written.contains("<c:barChart") {
15573 let positions: Vec<_> = [
15574 "<c:barDir",
15575 "<c:grouping",
15576 "<q:varyColors",
15577 "<c:ser",
15578 "<c:dLbls",
15579 "<c:gapWidth",
15580 "<c:overlap",
15581 "<q:serLines",
15582 "<c:axId",
15583 "<q:extLst",
15584 ]
15585 .iter()
15586 .map(|tag| written.find(tag).unwrap())
15587 .collect();
15588 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15589 } else {
15590 let positions: Vec<_> = [
15591 "<c:grouping",
15592 "<q:varyColors",
15593 "<c:ser",
15594 "<c:dLbls",
15595 "<q:dropLines",
15596 "<c:marker",
15597 "<c:smooth",
15598 "<c:axId",
15599 "<q:extLst",
15600 ]
15601 .iter()
15602 .map(|tag| written.find(tag).unwrap())
15603 .collect();
15604 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15605 }
15606 assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
15607 }
15608
15609 for plot in [bar_plot(""), line_plot("")] {
15610 let first = plot_series(0);
15611 let with_series_boundary = plot.replacen(
15612 &first,
15613 &format!(
15614 "{first}<!--between-series--><?series-boundary?>\n {}",
15615 plot_series(1)
15616 ),
15617 1,
15618 );
15619 let with_axis_boundary = with_series_boundary.replacen(
15620 r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
15621 r#"<q:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
15622 <q:axId val="-1884097184"/>"#,
15623 1,
15624 );
15625 let parsed =
15626 CT_ChartSpace::from_xml(chart_with_plot(&with_axis_boundary).as_bytes()).unwrap();
15627 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15628 assert!(written.contains("</c:ser><!--between-series--><?series-boundary?>\n <c:ser"));
15629 assert!(written.contains(
15630 r#"<c:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
15631 <c:axId val="-1884097184"/>"#
15632 ));
15633 assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
15634 }
15635 }
15636
15637 #[test]
15638 fn malformed_bar_and_line_plots_return_errors_without_panicking() {
15639 let valid_series = plot_series(0);
15640 let cases = [
15641 format!(r#"<q:barChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15642 format!(r#"<q:barChart><q:barDir val="diagonal"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15643 format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{valid_series}<q:gapWidth val="501"/><q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15644 format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{valid_series}<q:overlap val="-101"/><q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15645 format!(r#"<q:lineChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
15646 format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:marker val="maybe"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
15647 r#"<q:lineChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#.to_owned(),
15648 format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:axId val="1"/><q:axId val="1"/></q:lineChart>"#),
15649 format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/><q:grouping val="stacked"/>{valid_series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:barChart>"#),
15650 format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15651 ];
15652 for plot in cases {
15653 let xml = chart_with_plot(&plot);
15654 let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
15655 assert!(result.is_ok(), "plot parser panicked for {plot}");
15656 assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
15657 }
15658
15659 let missing_axis = format!(
15660 r#"<q:chartSpace xmlns:q="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}"><q:chart><q:plotArea>{}<q:catAx><q:axId val="-1884094432"/><q:scaling/><q:axPos val="b"/><q:crossAx val="-1884097184"/></q:catAx></q:plotArea></q:chart></q:chartSpace>"#,
15661 bar_plot("")
15662 );
15663 assert!(CT_ChartSpace::from_xml(missing_axis.as_bytes()).is_err());
15664
15665 let values =
15666 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
15667 let series = vec![Series::new(0, 0, values)];
15668 let ids = [AxisId::new(1).unwrap(), AxisId::new(2).unwrap()];
15669 let mut invalid =
15670 Plot::bar(BarDirection::Column, BarGrouping::Clustered, series, ids).unwrap();
15671 if let Plot::Bar { gap_width, .. } = &mut invalid {
15672 *gap_width = 501;
15673 }
15674 assert!(invalid.validate().is_err());
15675 }
15676
15677 #[test]
15678 fn unsupported_and_combo_plots_remain_byte_preserved() {
15679 let bar = bar_plot("");
15680 let line = line_plot("");
15681 let combo = chart_with_plot(&format!("<!--before-->{bar}<?between?>{line}<!--after-->"));
15682 let parsed = CT_ChartSpace::from_xml(combo.as_bytes()).unwrap();
15683 assert!(parsed.chart.plot_area.plots().is_err());
15684 let written = parsed.to_xml().unwrap();
15685 for raw in [
15686 bar.as_bytes(),
15687 line.as_bytes(),
15688 br#"<!--before-->"#.as_slice(),
15689 br#"<?between?>"#.as_slice(),
15690 br#"<!--after-->"#.as_slice(),
15691 ] {
15692 assert!(written.windows(raw.len()).any(|window| window == raw));
15693 }
15694
15695 let unsupported = chart_with_plot(
15696 r#"<q:bar3DChart x:keep="three"><q:barDir val="col"/><q:ser><x:opaque/></q:ser></q:bar3DChart>"#,
15697 );
15698 let parsed = CT_ChartSpace::from_xml(unsupported.as_bytes()).unwrap();
15699 assert!(parsed.chart.plot_area.plots().is_err());
15700 assert!(
15701 String::from_utf8(parsed.to_xml().unwrap())
15702 .unwrap()
15703 .contains(r#"<q:bar3DChart x:keep="three">"#)
15704 );
15705 }
15706
15707 #[test]
15708 fn public_plot_edits_preserve_axes_and_unselected_payloads() {
15709 let xml = chart_with_plot(&bar_plot(
15710 r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#,
15711 ));
15712 let mut parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15713 let axes_before = parsed.chart.plot_area.axes().unwrap();
15714 match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
15715 Plot::Bar {
15716 gap_width,
15717 overlap,
15718 series,
15719 ..
15720 } => {
15721 *gap_width = 225;
15722 *overlap = -25;
15723 series[0].index = 7;
15724 series[0].values.values[0] = 9.0;
15725 }
15726 _ => panic!("expected bar plot"),
15727 }
15728 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15729 assert!(written.contains(r#"<c:gapWidth val="225"/>"#));
15730 assert!(written.contains(r#"<c:overlap val="-25"/>"#));
15731 assert!(written.contains("<c:v>9</c:v>"));
15732 assert!(written.contains("<q:varyColors val=\"1\"/>"));
15733 assert!(written.find("<q:varyColors").unwrap() < written.find("<c:ser").unwrap());
15734 assert!(written.contains("<q:serLines><x:line/></q:serLines>"));
15735 assert!(written.contains(r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#));
15736 let reparsed = CT_ChartSpace::from_xml(written.as_bytes()).unwrap();
15737 assert_eq!(axes_before, reparsed.chart.plot_area.axes().unwrap());
15738
15739 let first = plot_series(0);
15740 let colliding_series =
15741 bar_plot("").replacen(&first, &format!("{first}{}", plot_series(1)), 1);
15742 let mut collision =
15743 CT_ChartSpace::from_xml(chart_with_plot(&colliding_series).as_bytes()).unwrap();
15744 if let Plot::Bar { series, .. } = &mut collision.chart.plot_area.plots_mut().unwrap()[0] {
15745 series[0].index = series[1].index;
15746 series[0].order = series[1].order;
15747 }
15748 let collision = String::from_utf8(collision.to_xml().unwrap()).unwrap();
15749 assert!(collision.find("<q:varyColors").unwrap() < collision.find("<c:ser").unwrap());
15750
15751 let mut inserted = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15752 if let Plot::Bar { series, .. } = &mut inserted.chart.plot_area.plots_mut().unwrap()[0] {
15753 let mut new_series = series[0].clone();
15754 new_series.index = 99;
15755 new_series.order = 99;
15756 series.insert(0, new_series);
15757 }
15758 let inserted = String::from_utf8(inserted.to_xml().unwrap()).unwrap();
15759 assert!(inserted.find("<q:varyColors").unwrap() < inserted.find("<c:ser").unwrap());
15760
15761 let axis_payload = bar_plot("").replacen(
15762 r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
15763 r#"<q:axId val="-1884094432" x:slot="category"/><!--axis-anchor--><q:axId val="-1884097184" x:slot="value"/>"#,
15764 1,
15765 );
15766 let mut swapped =
15767 CT_ChartSpace::from_xml(chart_with_plot(&axis_payload).as_bytes()).unwrap();
15768 if let Plot::Bar { axis_ids, .. } = &mut swapped.chart.plot_area.plots_mut().unwrap()[0] {
15769 axis_ids.swap(0, 1);
15770 }
15771 let swapped = String::from_utf8(swapped.to_xml().unwrap()).unwrap();
15772 let anchor = swapped.find("<!--axis-anchor-->").unwrap();
15773 let value_axis = swapped
15774 .find(r#"<c:axId val="-1884097184" x:slot="value"/>"#)
15775 .unwrap();
15776 let category_axis = swapped
15777 .find(r#"<c:axId val="-1884094432" x:slot="category"/>"#)
15778 .unwrap();
15779 let series_lines = swapped.find("<q:serLines").unwrap();
15780 assert!(series_lines < value_axis && series_lines < category_axis);
15781 assert!(anchor < value_axis && value_axis < category_axis);
15782
15783 let mut replaced = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15784 let Plot::Bar {
15785 series, axis_ids, ..
15786 } = &replaced.chart.plot_area.plots().unwrap()[0]
15787 else {
15788 panic!("expected bar plot");
15789 };
15790 let line = Plot::line(Grouping::Standard, series.clone(), *axis_ids).unwrap();
15791 replaced.chart.plot_area.plots_mut().unwrap()[0] = line;
15792 assert!(replaced.to_xml().is_err());
15793 }
15794
15795 #[test]
15796 fn every_corpus_bar_and_line_plot_round_trips_structurally() {
15797 let Some(corpus) = require_or_skip_corpus() else {
15798 return;
15799 };
15800 verify_fetched_corpus(&corpus);
15801 let mut typed_bar_count = 0usize;
15802 let mut typed_line_count = 0usize;
15803 let mut opaque_bar_count = 0usize;
15804 let mut opaque_line_count = 0usize;
15805 let mut opaque_combo_count = 0usize;
15806 for path in manifest_paths() {
15807 let package = OpcPackage::open(corpus.join(path))
15808 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
15809 for (part, xml) in &package.parts {
15810 if !is_chart_part(part) {
15811 continue;
15812 }
15813 let chart = CT_ChartSpace::from_xml(xml)
15814 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
15815 match chart.chart.plot_area.plots() {
15816 Ok(plots) => {
15817 for plot in plots {
15818 match plot {
15819 Plot::Bar { .. } => typed_bar_count += 1,
15820 Plot::Line { .. } => typed_line_count += 1,
15821 _ => {}
15822 }
15823 }
15824 }
15825 Err(_) => {
15826 let mut preserved_plot_count = 0usize;
15827 for raw in chart.chart.plot_area.raw_children.at(0) {
15828 if let Ok(Some(local)) = super::chart_root_local(
15829 raw,
15830 &chart.chart.plot_area.namespace_bindings,
15831 ) {
15832 match local.as_slice() {
15833 b"barChart" => {
15834 opaque_bar_count += 1;
15835 preserved_plot_count += 1;
15836 }
15837 b"lineChart" => {
15838 opaque_line_count += 1;
15839 preserved_plot_count += 1;
15840 }
15841 _ => {}
15842 }
15843 }
15844 }
15845 if preserved_plot_count > 1 {
15846 opaque_combo_count += 1;
15847 }
15848 }
15849 }
15850 let written = chart
15851 .to_xml()
15852 .unwrap_or_else(|error| panic!("{path} {part}: chart write failed: {error}"));
15853 let reparsed = CT_ChartSpace::from_xml(&written).unwrap_or_else(|error| {
15854 panic!("{path} {part}: written chart parse failed: {error}")
15855 });
15856 assert_eq!(chart, reparsed, "{path} {part}: chart model changed");
15857 }
15858 }
15859 assert_eq!(typed_bar_count, 11, "typed corpus bar coverage changed");
15860 assert_eq!(typed_line_count, 2, "typed corpus line coverage changed");
15861 assert_eq!(opaque_bar_count, 1, "preserved combo bar coverage changed");
15862 assert_eq!(
15863 opaque_line_count, 1,
15864 "preserved combo line coverage changed"
15865 );
15866 assert_eq!(opaque_combo_count, 1, "preserved combo coverage changed");
15867 assert_eq!(typed_bar_count + opaque_bar_count, 12);
15868 assert_eq!(typed_line_count + opaque_line_count, 3);
15869 eprintln!(
15870 "ChartML plot corpus gate checked {typed_bar_count} typed bar, {typed_line_count} typed line, and one preserved bar-line combination"
15871 );
15872 }
15873
15874 fn verify_remaining_plot_viewer_gate(corpus: &Path) {
15875 verify_fetched_corpus(corpus);
15876 assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
15877 assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
15878 let source = corpus.join("bar-chart.pptx");
15879 let source_sha = sha256(&source);
15880 let temp_root = std::env::temp_dir().join(format!(
15881 "oxml-chart-f122-viewer-gate-{}",
15882 std::process::id()
15883 ));
15884 if temp_root.exists() {
15885 fs::remove_dir_all(&temp_root).expect("remove stale F-122 evidence");
15886 }
15887 fs::create_dir_all(&temp_root).expect("create F-122 evidence root");
15888
15889 for (kind, plot, axes) in remaining_plot_fixtures() {
15890 let mut package = OpcPackage::open(&source).expect("open F-122 viewer source");
15891 package.set_part(
15892 "/ppt/charts/chart1.xml",
15893 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes())
15894 .expect("parse F-122 viewer chart")
15895 .to_xml()
15896 .expect("serialize F-122 viewer chart"),
15897 );
15898 let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
15899 package.save(&unbound).expect("save F-122 viewer candidate");
15900 let candidate_sha = sha256(&unbound);
15901 let evidence = temp_root.join(format!("{source_sha}-{candidate_sha}"));
15902 fs::create_dir(&evidence).expect("create SHA-bound F-122 evidence directory");
15903 let candidate = evidence.join(format!("{kind}-candidate.pptx"));
15904 fs::rename(&unbound, &candidate).expect("bind F-122 candidate to SHA");
15905 assert_eq!(sha256(&candidate), candidate_sha);
15906 let render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
15907 let bytes = fs::read(&render).expect("read F-122 viewer PPM");
15908 let (width, height, pixels) = ppm_pixels(&bytes);
15909 let left = width / 5;
15910 let right = width * 4 / 5;
15911 let top = height * 3 / 20;
15912 let bottom = height * 17 / 20;
15913 let mut nonblank = 0usize;
15914 for y in top..bottom {
15915 for x in left..right {
15916 let offset = (y * width + x) * 3;
15917 if pixels[offset..offset + 3]
15918 .iter()
15919 .any(|channel| *channel < 245)
15920 {
15921 nonblank += 1;
15922 }
15923 }
15924 }
15925 const CHART_RECTANGLE_NONBLANK_THRESHOLD: usize = 1_000;
15926 assert!(
15927 nonblank >= CHART_RECTANGLE_NONBLANK_THRESHOLD,
15928 "{kind}: chart rectangle [{left},{top}) to [{right},{bottom}) contains {nonblank} nonblank pixels, below {CHART_RECTANGLE_NONBLANK_THRESHOLD}"
15929 );
15930 eprintln!(
15931 "F-122 {kind} viewer gate source deck {source_sha}, candidate deck {candidate_sha}, render {}, chart rectangle [{left},{top}) to [{right},{bottom}), RGB<245 nonblank pixels {nonblank} >= {CHART_RECTANGLE_NONBLANK_THRESHOLD}",
15932 sha256(&render)
15933 );
15934 }
15935 fs::remove_dir_all(&temp_root).expect("remove F-122 viewer evidence");
15936 }
15937
15938 fn verify_bar_and_line_viewer_gate(corpus: &Path) {
15939 verify_fetched_corpus(corpus);
15940 assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
15941 assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
15942 let temp_root = std::env::temp_dir().join(format!(
15943 "oxml-chart-f121-viewer-gate-{}",
15944 std::process::id()
15945 ));
15946 if temp_root.exists() {
15947 fs::remove_dir_all(&temp_root).expect("remove stale F-121 evidence");
15948 }
15949 fs::create_dir_all(&temp_root).expect("create F-121 evidence root");
15950
15951 for (kind, deck) in [("bar", "bar-chart.pptx"), ("line", "line-chart.pptx")] {
15952 let source = corpus.join(deck);
15953 let original_sha = sha256(&source);
15954 let mut package = OpcPackage::open(&source)
15955 .unwrap_or_else(|error| panic!("{deck}: open viewer source: {error}"));
15956 let chart_part = "/ppt/charts/chart1.xml";
15957 let original_parts = package.parts.clone();
15958 let parsed = CT_ChartSpace::from_xml(
15959 package
15960 .get_part(chart_part)
15961 .unwrap_or_else(|| panic!("{deck}: missing {chart_part}")),
15962 )
15963 .unwrap_or_else(|error| panic!("{deck}: parse viewer chart: {error}"));
15964 assert!(
15965 parsed.chart.plot_area.plots().is_ok(),
15966 "{deck}: plot is opaque"
15967 );
15968 package.set_part(
15969 chart_part,
15970 parsed
15971 .to_xml()
15972 .unwrap_or_else(|error| panic!("{deck}: serialize viewer chart: {error}")),
15973 );
15974 for (part, bytes) in &original_parts {
15975 if part == chart_part {
15976 assert_ne!(package.parts[part], *bytes, "{deck}: chart did not rewrite");
15977 } else {
15978 assert_eq!(package.parts[part], *bytes, "{deck}: changed part {part}");
15979 }
15980 }
15981
15982 let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
15983 package
15984 .save(&unbound)
15985 .unwrap_or_else(|error| panic!("{deck}: save viewer candidate: {error}"));
15986 let candidate_sha = sha256(&unbound);
15987 let evidence = temp_root.join(format!("{original_sha}-{candidate_sha}"));
15988 fs::create_dir(&evidence).expect("create SHA-bound F-121 evidence directory");
15989 let original = evidence.join(format!("{kind}-original.pptx"));
15990 let candidate = evidence.join(format!("{kind}-candidate.pptx"));
15991 fs::copy(&source, &original).expect("copy SHA-bound viewer original");
15992 fs::rename(&unbound, &candidate).expect("bind viewer candidate to SHA");
15993 assert_eq!(sha256(&original), original_sha);
15994 assert_eq!(sha256(&candidate), candidate_sha);
15995
15996 let original_render = render_deck_to_ppm(&original, &evidence, kind, "original");
15997 let candidate_render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
15998 let original_bytes = fs::read(&original_render).expect("read original PPM");
15999 let candidate_bytes = fs::read(&candidate_render).expect("read candidate PPM");
16000 let normalized_mae = normalized_ppm_mae(&original_bytes, &candidate_bytes);
16001 assert!(
16002 normalized_mae <= PLOT_RENDER_NORMALIZED_MAE_THRESHOLD,
16003 "{deck}: normalized RGB MAE {normalized_mae:.8} exceeds {:.8}",
16004 PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
16005 );
16006 eprintln!(
16007 "F-121 {kind} viewer gate original deck {original_sha}, candidate deck {candidate_sha}, original render {}, candidate render {}, normalized RGB MAE {normalized_mae:.8} <= {:.8}",
16008 sha256(&original_render),
16009 sha256(&candidate_render),
16010 PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
16011 );
16012 }
16013 fs::remove_dir_all(&temp_root).expect("remove F-121 viewer evidence");
16014 }
16015
16016 fn render_deck_to_ppm(deck: &Path, evidence: &Path, kind: &str, side: &str) -> PathBuf {
16017 let profile = std::env::temp_dir().join(format!(
16018 "oxml-chart-f121-{kind}-{side}-profile-{}",
16019 std::process::id()
16020 ));
16021 if profile.exists() {
16022 fs::remove_dir_all(&profile).expect("remove stale F-121 viewer profile");
16023 }
16024 let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
16025 let conversion = Command::new("soffice")
16026 .args([
16027 "--headless",
16028 &profile_argument,
16029 "--convert-to",
16030 "pdf:impress_pdf_Export",
16031 "--outdir",
16032 ])
16033 .arg(evidence)
16034 .arg(deck)
16035 .output()
16036 .expect("run pinned LibreOffice F-121 viewer gate");
16037 assert!(
16038 conversion.status.success(),
16039 "{}: LibreOffice conversion failed: {}",
16040 deck.display(),
16041 String::from_utf8_lossy(&conversion.stderr)
16042 );
16043 let pdf = deck.with_extension("pdf");
16044 assert!(
16045 pdf.is_file(),
16046 "LibreOffice did not create {}",
16047 pdf.display()
16048 );
16049 let prefix = evidence.join(format!("{kind}-{side}"));
16050 let raster = Command::new("pdftoppm")
16051 .args(["-f", "1", "-singlefile", "-r", "150"])
16052 .arg(&pdf)
16053 .arg(&prefix)
16054 .output()
16055 .expect("run pinned Poppler F-121 raster gate");
16056 assert!(
16057 raster.status.success(),
16058 "{}: pdftoppm failed: {}",
16059 pdf.display(),
16060 String::from_utf8_lossy(&raster.stderr)
16061 );
16062 if profile.exists() {
16063 fs::remove_dir_all(&profile).expect("remove F-121 viewer profile");
16064 }
16065 prefix.with_extension("ppm")
16066 }
16067
16068 fn normalized_ppm_mae(left: &[u8], right: &[u8]) -> f64 {
16069 let (left_width, left_height, left_pixels) = ppm_pixels(left);
16070 let (right_width, right_height, right_pixels) = ppm_pixels(right);
16071 assert_eq!((left_width, left_height), (right_width, right_height));
16072 assert_eq!(left_pixels.len(), right_pixels.len());
16073 let difference: u64 = left_pixels
16074 .iter()
16075 .zip(right_pixels)
16076 .map(|(left, right)| u64::from(left.abs_diff(*right)))
16077 .sum();
16078 difference as f64 / (left_pixels.len() as f64 * 255.0)
16079 }
16080
16081 fn ppm_pixels(bytes: &[u8]) -> (usize, usize, &[u8]) {
16082 let mut cursor = 0usize;
16083 let mut tokens = Vec::new();
16084 while tokens.len() < 4 {
16085 while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
16086 cursor += 1;
16087 }
16088 if cursor < bytes.len() && bytes[cursor] == b'#' {
16089 while cursor < bytes.len() && bytes[cursor] != b'\n' {
16090 cursor += 1;
16091 }
16092 continue;
16093 }
16094 let start = cursor;
16095 while cursor < bytes.len() && !bytes[cursor].is_ascii_whitespace() {
16096 cursor += 1;
16097 }
16098 tokens.push(std::str::from_utf8(&bytes[start..cursor]).expect("PPM header UTF-8"));
16099 }
16100 assert_eq!(tokens[0], "P6");
16101 let width = tokens[1].parse::<usize>().expect("PPM width");
16102 let height = tokens[2].parse::<usize>().expect("PPM height");
16103 assert_eq!(tokens[3], "255");
16104 assert!(
16105 cursor < bytes.len() && bytes[cursor].is_ascii_whitespace(),
16106 "PPM header must end with whitespace"
16107 );
16108 cursor += 1;
16109 let pixels = &bytes[cursor..];
16110 assert_eq!(pixels.len(), width * height * 3, "PPM pixel length");
16111 (width, height, pixels)
16112 }
16113}