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 quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
25use quick_xml::{Reader, Writer, XmlVersion};
26
27pub const C_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/chart";
29pub const C_PREFIX: &str = "c";
31pub const A_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
33pub const R_NS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
35const MAX_CATEGORY_ANNOTATIONS: usize = 16_384;
36const PLOT_MARGIN_LEFT: f64 = 36.0;
37const PLOT_MARGIN_RIGHT: f64 = 12.0;
38const PLOT_MARGIN_TOP: f64 = 12.0;
39const PLOT_MARGIN_BOTTOM: f64 = 28.0;
40
41#[derive(Debug)]
43pub enum ChartError {
44 Xml(OxmlError),
45 ShapeProperties(ShapePropertiesError),
46 Text(TextError),
47 UnexpectedElement(String),
48 MissingElement(String),
49 DuplicateElement(String),
50 InvalidAttribute {
51 element: String,
52 attribute: String,
53 value: String,
54 },
55 InvalidValue {
56 element: String,
57 value: String,
58 },
59}
60
61impl fmt::Display for ChartError {
62 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 Self::Xml(error) => error.fmt(formatter),
65 Self::ShapeProperties(error) => error.fmt(formatter),
66 Self::Text(error) => error.fmt(formatter),
67 Self::UnexpectedElement(element) => {
68 write!(formatter, "unexpected ChartML element: {element}")
69 }
70 Self::MissingElement(element) => {
71 write!(formatter, "ChartML requires {element}")
72 }
73 Self::DuplicateElement(element) => {
74 write!(formatter, "ChartML contains duplicate {element}")
75 }
76 Self::InvalidAttribute {
77 element,
78 attribute,
79 value,
80 } => write!(
81 formatter,
82 "ChartML {element} has invalid @{attribute}: {value}"
83 ),
84 Self::InvalidValue { element, value } => {
85 write!(formatter, "ChartML {element} has invalid value: {value}")
86 }
87 }
88 }
89}
90
91impl std::error::Error for ChartError {
92 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
93 match self {
94 Self::Xml(error) => Some(error),
95 Self::ShapeProperties(error) => Some(error),
96 Self::Text(error) => Some(error),
97 _ => None,
98 }
99 }
100}
101
102impl From<OxmlError> for ChartError {
103 fn from(error: OxmlError) -> Self {
104 Self::Xml(error)
105 }
106}
107
108impl From<ShapePropertiesError> for ChartError {
109 fn from(error: ShapePropertiesError) -> Self {
110 Self::ShapeProperties(error)
111 }
112}
113
114impl From<TextError> for ChartError {
115 fn from(error: TextError) -> Self {
116 Self::Text(error)
117 }
118}
119
120pub type Result<T> = std::result::Result<T, ChartError>;
121
122#[derive(Clone, Debug)]
124pub struct ChartGeometry {
125 pub plot_bounds: Rect,
126 pub elements: Vec<PositionedElement>,
127}
128
129#[derive(Clone, Debug)]
130struct AxisScale {
131 domain: Domain,
132 ticks: Vec<f64>,
133}
134
135#[derive(Clone, Copy, Debug)]
136struct AxisMapping {
137 domain: Domain,
138 orientation: Orientation,
139}
140
141#[derive(Clone, Copy, Debug, Default)]
142struct GeometryOptions {
143 category_orientation: Orientation,
144 value: Option<AxisMapping>,
145 scatter_x: Option<AxisMapping>,
146 scatter_y: Option<AxisMapping>,
147}
148
149#[derive(Default)]
150struct ChartAnnotations {
151 gridlines: Vec<PositionedElement>,
152 axis_lines: Vec<PositionedElement>,
153 ticks: Vec<PositionedElement>,
154 labels: Vec<PositionedElement>,
155}
156
157pub fn render_chart(
159 chart: &CT_Chart,
160 bounds: Rect,
161 theme: &CT_OfficeStyleSheet,
162 color_map: &ColorMap,
163 fonts: &mut FontManager,
164) -> Result<GroupElement> {
165 let plot_bounds = geometry_plot_bounds(bounds)?;
166 let plots = chart.plot_area.plots()?;
167 let plot = plots
168 .first()
169 .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
170 plot.validate()?;
171 let series_colours = resolve_series_colours(plot, theme, color_map)?;
172 let axes = chart.plot_area.axes()?;
173 let options = geometry_options(plot, &axes, chart.disp_blanks_as)?;
174 let plot_children = render_plot_geometry(
175 plot,
176 plot_bounds,
177 chart.disp_blanks_as,
178 options,
179 &series_colours,
180 )?;
181 if plot_children.is_empty() && !plot_can_render_without_marks(plot) {
182 return Err(ChartError::InvalidValue {
183 element: "c:plotArea".to_owned(),
184 value: "plot has no renderable cached data".to_owned(),
185 });
186 }
187
188 let mut chart_annotations =
189 render_axes_and_labels(plot, &axes, plot_bounds, chart.disp_blanks_as, fonts)?;
190 let mut annotations = Vec::new();
191 render_data_labels(
192 plot,
193 plot_bounds,
194 chart.disp_blanks_as,
195 options,
196 fonts,
197 &mut chart_annotations.labels,
198 )?;
199 render_legend(
200 chart,
201 plot,
202 plot_bounds,
203 fonts,
204 &series_colours,
205 &mut annotations,
206 &mut chart_annotations.labels,
207 )?;
208
209 validate_geometry_coordinates(&chart_annotations.gridlines)?;
210 validate_geometry_coordinates(&plot_children)?;
211 validate_geometry_coordinates(&chart_annotations.axis_lines)?;
212 validate_geometry_coordinates(&chart_annotations.ticks)?;
213 validate_geometry_coordinates(&annotations)?;
214 let mut children = chart_annotations.gridlines;
215 children.push(PositionedElement::Group(GroupElement {
216 transform: Transform::IDENTITY,
217 clip: Some(Path::rect(plot_bounds)),
218 opacity: 1.0,
219 effects: Vec::new(),
220 children: plot_children,
221 }));
222 children.extend(chart_annotations.axis_lines);
223 children.extend(chart_annotations.ticks);
224 children.extend(annotations);
225 children.extend(chart_annotations.labels);
226 Ok(GroupElement {
227 transform: Transform::IDENTITY,
228 clip: None,
229 opacity: 1.0,
230 effects: Vec::new(),
231 children,
232 })
233}
234
235pub fn render_chart_placeholder(bounds: Rect, fonts: &mut FontManager) -> Result<GroupElement> {
237 if !bounds.width.is_finite()
238 || !bounds.height.is_finite()
239 || bounds.width <= 0.0
240 || bounds.height <= 0.0
241 {
242 return Err(ChartError::InvalidValue {
243 element: "chart fallback bounds".to_owned(),
244 value: "width and height must be finite and positive".to_owned(),
245 });
246 }
247 let label = shape_label(
248 fonts,
249 "Unsupported chart",
250 Point {
251 x: 4.0,
252 y: (bounds.height / 2.0).max(9.0),
253 },
254 )?;
255 Ok(GroupElement {
256 transform: Transform::IDENTITY,
257 clip: Some(Path::rect(Rect {
258 x: 0.0,
259 y: 0.0,
260 width: bounds.width,
261 height: bounds.height,
262 })),
263 opacity: 1.0,
264 effects: Vec::new(),
265 children: vec![PositionedElement::Text(label)],
266 })
267}
268
269pub fn render_geometry(
271 chart: &CT_Chart,
272 bounds: Rect,
273 theme: &CT_OfficeStyleSheet,
274 color_map: &ColorMap,
275) -> Result<ChartGeometry> {
276 let plot_bounds = geometry_plot_bounds(bounds)?;
277 let plots = chart.plot_area.plots()?;
278 let plot = plots
279 .first()
280 .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
281 plot.validate()?;
282 let series_colours = resolve_series_colours(plot, theme, color_map)?;
283 let children = render_plot_geometry(
284 plot,
285 plot_bounds,
286 chart.disp_blanks_as,
287 geometry_options(plot, &[], chart.disp_blanks_as)?,
288 &series_colours,
289 )?;
290 if children.is_empty() && !plot_can_render_without_marks(plot) {
291 return Err(ChartError::InvalidValue {
292 element: "c:plotArea".to_owned(),
293 value: "plot has no renderable cached data".to_owned(),
294 });
295 }
296 validate_geometry_coordinates(&children)?;
297 Ok(ChartGeometry {
298 plot_bounds,
299 elements: vec![PositionedElement::Group(GroupElement {
300 transform: Transform::IDENTITY,
301 clip: None,
302 opacity: 1.0,
303 effects: Vec::new(),
304 children,
305 })],
306 })
307}
308
309fn plot_can_render_without_marks(plot: &Plot) -> bool {
310 matches!(
311 plot,
312 Plot::Radar { series, .. }
313 if series
314 .iter()
315 .any(|series| !series.values.values.is_empty())
316 )
317}
318
319fn geometry_plot_bounds(bounds: Rect) -> Result<Rect> {
320 if ![bounds.x, bounds.y, bounds.width, bounds.height]
321 .into_iter()
322 .all(f64::is_finite)
323 || bounds.width <= 0.0
324 || bounds.height <= 0.0
325 || !(bounds.x + bounds.width).is_finite()
326 || !(bounds.y + bounds.height).is_finite()
327 {
328 return Err(ChartError::InvalidValue {
329 element: "chart geometry bounds".to_owned(),
330 value: format!(
331 "x={}, y={}, width={}, height={}",
332 bounds.x, bounds.y, bounds.width, bounds.height
333 ),
334 });
335 }
336 let plot_bounds = Rect {
337 x: bounds.x + PLOT_MARGIN_LEFT,
338 y: bounds.y + PLOT_MARGIN_TOP,
339 width: bounds.width - PLOT_MARGIN_LEFT - PLOT_MARGIN_RIGHT,
340 height: bounds.height - PLOT_MARGIN_TOP - PLOT_MARGIN_BOTTOM,
341 };
342 if ![
343 plot_bounds.x,
344 plot_bounds.y,
345 plot_bounds.width,
346 plot_bounds.height,
347 plot_bounds.x + plot_bounds.width,
348 plot_bounds.y + plot_bounds.height,
349 ]
350 .into_iter()
351 .all(f64::is_finite)
352 || plot_bounds.width <= 0.0
353 || plot_bounds.height <= 0.0
354 {
355 return Err(ChartError::InvalidValue {
356 element: "chart geometry bounds".to_owned(),
357 value: "chart is too small for the fixed plot margins".to_owned(),
358 });
359 }
360 Ok(plot_bounds)
361}
362
363fn geometry_options(plot: &Plot, axes: &[Axis], blanks: DispBlanksAs) -> Result<GeometryOptions> {
364 let mut options = GeometryOptions::default();
365 if let Some(category_axis) = axes.iter().find(|axis| axis.kind != AxisKind::Value) {
366 options.category_orientation = category_axis.scaling.orientation;
367 }
368
369 if let Plot::Scatter { axis_ids, .. } = plot {
370 let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
371 if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[0]) {
372 options.scatter_x = Some(axis_mapping(axis, x_domain)?);
373 }
374 if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[1]) {
375 options.scatter_y = Some(axis_mapping(axis, y_domain)?);
376 }
377 } else if let Some(domain) = plot_value_domain(plot, blanks)? {
378 options.value = Some(
379 axes.iter()
380 .find(|axis| axis.kind == AxisKind::Value)
381 .map(|axis| axis_mapping(axis, domain))
382 .transpose()?
383 .unwrap_or(AxisMapping {
384 domain,
385 orientation: Orientation::MinMax,
386 }),
387 );
388 }
389 Ok(options)
390}
391
392fn axis_mapping(axis: &Axis, extent: Domain) -> Result<AxisMapping> {
393 if axis.scaling.log_base.is_some() {
394 return Err(ChartError::InvalidValue {
395 element: "c:scaling/c:logBase".to_owned(),
396 value: "native chart rendering currently supports linear axes".to_owned(),
397 });
398 }
399 let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
400 Ok(AxisMapping {
401 domain: scale.domain,
402 orientation: axis.scaling.orientation,
403 })
404}
405
406fn plot_value_domain(plot: &Plot, blanks: DispBlanksAs) -> Result<Option<Domain>> {
407 let domain = match plot {
408 Plot::Bar {
409 grouping, series, ..
410 } => {
411 let logical = logical_series_values(series)?;
412 let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
413 let layers =
414 stacked_series_bounds(&logical, stacked, *grouping == BarGrouping::PercentStacked)?;
415 domain_from_layers(&layers)?
416 }
417 Plot::Line {
418 grouping, series, ..
419 } => {
420 let logical = logical_series_values(series)?;
421 if *grouping == Grouping::Standard {
422 let values = if blanks == DispBlanksAs::Zero {
423 zero_filled_series(&logical, series_category_count(series)?)
424 .into_iter()
425 .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
426 .collect::<Vec<_>>()
427 } else {
428 logical
429 .into_iter()
430 .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
431 .collect::<Vec<_>>()
432 };
433 data_domain(&values, false)?
434 } else {
435 let layers =
436 stacked_series_bounds(&logical, true, *grouping == Grouping::PercentStacked)?;
437 domain_from_layers(&layers)?
438 }
439 }
440 Plot::Area {
441 grouping, series, ..
442 } => {
443 let logical = logical_series_values(series)?;
444 let layers = stacked_series_bounds(
445 &logical,
446 *grouping != Grouping::Standard,
447 *grouping == Grouping::PercentStacked,
448 )?;
449 domain_from_layers(&layers)?
450 }
451 Plot::Radar { series, .. } => {
452 let values = series
453 .iter()
454 .flat_map(|series| series.values.values.iter().copied())
455 .collect::<Vec<_>>();
456 data_domain(&values, true)?
457 }
458 Plot::Pie { .. } | Plot::Doughnut { .. } | Plot::Scatter { .. } => return Ok(None),
459 };
460 Ok(Some(domain))
461}
462
463fn scatter_domains(plot: &Plot, blanks: DispBlanksAs) -> Result<(Domain, Domain)> {
464 let Plot::Scatter { series, .. } = plot else {
465 return Err(ChartError::UnexpectedElement("non-scatter plot".to_owned()));
466 };
467 let mut x_values = Vec::new();
468 let mut y_values = Vec::new();
469 for series in series {
470 for (_, x, y) in scatter_render_points(series, blanks)? {
471 x_values.push(x);
472 y_values.push(y);
473 }
474 }
475 Ok((
476 data_domain(&x_values, false)?,
477 data_domain(&y_values, false)?,
478 ))
479}
480
481fn render_axes_and_labels(
482 plot: &Plot,
483 axes: &[Axis],
484 bounds: Rect,
485 blanks: DispBlanksAs,
486 fonts: &mut FontManager,
487) -> Result<ChartAnnotations> {
488 if matches!(plot, Plot::Radar { .. }) {
489 return render_radar_axes_and_labels(plot, axes, bounds, blanks, fonts);
490 }
491 let mut annotations = ChartAnnotations::default();
492 let series = plot_series(plot);
493 let category_count = if series.is_empty() {
494 1
495 } else {
496 series_category_count(series)?
497 };
498 for axis in axes {
499 if axis.deleted {
500 continue;
501 }
502 axis_line(&mut annotations.axis_lines, bounds, axis.position);
503 if axis.kind == AxisKind::Value {
504 let extent = axis_extent(plot, axis, blanks)?;
505 let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
506 for tick in &scale.ticks {
507 let coordinate = axis_tick_coordinate(
508 *tick,
509 scale.domain,
510 bounds,
511 axis.position,
512 axis.scaling.orientation,
513 )?;
514 if axis.major_gridlines.is_some() {
515 major_gridline(
516 &mut annotations.gridlines,
517 bounds,
518 axis.position,
519 coordinate,
520 );
521 }
522 axis_tick(
523 &mut annotations.ticks,
524 bounds,
525 axis.position,
526 coordinate,
527 axis.major_tick_mark,
528 );
529 if axis.tick_label_position != TickLabelPosition::None {
530 let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
531 let origin = tick_label_origin(
532 bounds,
533 axis.position,
534 axis.tick_label_position,
535 coordinate,
536 );
537 annotations
538 .labels
539 .push(PositionedElement::Text(shape_label_with_properties(
540 fonts,
541 &text,
542 origin,
543 axis.tx_pr.as_ref(),
544 )?));
545 }
546 }
547 } else {
548 let count = category_count;
549 if count > MAX_CATEGORY_ANNOTATIONS {
550 return Err(ChartError::InvalidValue {
551 element: "c:catAx annotations".to_owned(),
552 value: format!(
553 "logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"
554 ),
555 });
556 }
557 let category_labels: BTreeMap<_, _> =
558 if axis.tick_label_position != TickLabelPosition::None {
559 series
560 .first()
561 .map(|series| series_categories(series, axis.number_format.as_ref()))
562 .transpose()?
563 .unwrap_or_default()
564 .into_iter()
565 .collect()
566 } else {
567 BTreeMap::new()
568 };
569 for logical_index in 0..count {
570 let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
571 let fraction = (index as f64 + 0.5) / count as f64;
572 let coordinate =
573 if matches!(axis.position, AxisPosition::Bottom | AxisPosition::Top) {
574 bounds.x + fraction * bounds.width
575 } else {
576 bounds.y + fraction * bounds.height
577 };
578 if axis.major_gridlines.is_some() {
579 major_gridline(
580 &mut annotations.gridlines,
581 bounds,
582 axis.position,
583 coordinate,
584 );
585 }
586 axis_tick(
587 &mut annotations.ticks,
588 bounds,
589 axis.position,
590 coordinate,
591 axis.major_tick_mark,
592 );
593 if axis.tick_label_position != TickLabelPosition::None
594 && let Some(text) = category_labels.get(&logical_index)
595 {
596 let origin = tick_label_origin(
597 bounds,
598 axis.position,
599 axis.tick_label_position,
600 coordinate,
601 );
602 annotations
603 .labels
604 .push(PositionedElement::Text(shape_label_with_properties(
605 fonts,
606 text,
607 origin,
608 axis.tx_pr.as_ref(),
609 )?));
610 }
611 }
612 }
613 }
614 Ok(annotations)
615}
616
617fn render_radar_axes_and_labels(
618 plot: &Plot,
619 axes: &[Axis],
620 bounds: Rect,
621 blanks: DispBlanksAs,
622 fonts: &mut FontManager,
623) -> Result<ChartAnnotations> {
624 let mut annotations = ChartAnnotations::default();
625 let series = plot_series(plot);
626 let count = series_category_count(series)?;
627 if count > MAX_CATEGORY_ANNOTATIONS {
628 return Err(ChartError::InvalidValue {
629 element: "c:radarChart annotations".to_owned(),
630 value: format!("logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"),
631 });
632 }
633 let center = Point {
634 x: bounds.x + bounds.width / 2.0,
635 y: bounds.y + bounds.height / 2.0,
636 };
637 let radius = bounds.width.min(bounds.height) / 2.0;
638 for axis in axes {
639 if axis.deleted {
640 continue;
641 }
642 if axis.kind == AxisKind::Value {
643 let scale = nice_number_scale(
644 axis_extent(plot, axis, blanks)?,
645 axis.scaling.minimum,
646 axis.scaling.maximum,
647 6,
648 )?;
649 annotations.axis_lines.push(stroked_segment(
650 center,
651 radial_point(center, radius, 0, count),
652 Color::BLACK,
653 1.0,
654 ));
655 for tick in &scale.ticks {
656 let distance =
657 normalized_value_oriented(*tick, scale.domain, axis.scaling.orientation)?
658 * radius;
659 if axis.major_gridlines.is_some() && distance > 0.0 {
660 let points: Vec<_> = (0..count)
661 .map(|index| radial_point(center, distance, index, count))
662 .collect();
663 annotations
664 .gridlines
665 .push(PositionedElement::Path(PathElement {
666 path: polyline_path(&points, true),
667 fill: None,
668 stroke: Some(Stroke::new(
669 Paint::Solid(Color {
670 r: 0.82,
671 g: 0.82,
672 b: 0.82,
673 a: 1.0,
674 }),
675 0.5,
676 )),
677 }));
678 }
679 radar_value_tick(
680 &mut annotations.ticks,
681 center,
682 distance,
683 axis.major_tick_mark,
684 );
685 let origin = match axis.tick_label_position {
686 TickLabelPosition::High => Some(Point {
687 x: center.x + radius + 4.0,
688 y: center.y - distance + 3.0,
689 }),
690 TickLabelPosition::Low => Some(Point {
691 x: center.x - radius - 30.0,
692 y: center.y - distance + 3.0,
693 }),
694 TickLabelPosition::NextTo => Some(Point {
695 x: center.x + 4.0,
696 y: center.y - distance + 3.0,
697 }),
698 TickLabelPosition::None => None,
699 };
700 if let Some(origin) = origin {
701 let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
702 annotations
703 .labels
704 .push(PositionedElement::Text(shape_label_with_properties(
705 fonts,
706 &text,
707 origin,
708 axis.tx_pr.as_ref(),
709 )?));
710 }
711 }
712 } else {
713 let category_labels: BTreeMap<_, _> =
714 if axis.tick_label_position != TickLabelPosition::None {
715 series
716 .first()
717 .map(|series| series_categories(series, axis.number_format.as_ref()))
718 .transpose()?
719 .unwrap_or_default()
720 .into_iter()
721 .collect()
722 } else {
723 BTreeMap::new()
724 };
725 for logical_index in 0..count {
726 let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
727 let end = radial_point(center, radius, index, count);
728 annotations
729 .axis_lines
730 .push(stroked_segment(center, end, Color::BLACK, 1.0));
731 radar_category_tick(
732 &mut annotations.ticks,
733 center,
734 radius,
735 index,
736 count,
737 axis.major_tick_mark,
738 );
739 if let Some(text) = category_labels.get(&logical_index) {
740 let Some(origin) = radar_category_label_origin(
741 bounds,
742 center,
743 radius,
744 index,
745 count,
746 axis.tick_label_position,
747 ) else {
748 continue;
749 };
750 annotations
751 .labels
752 .push(PositionedElement::Text(shape_label_with_properties(
753 fonts,
754 text,
755 origin,
756 axis.tx_pr.as_ref(),
757 )?));
758 }
759 }
760 }
761 }
762 Ok(annotations)
763}
764
765fn radar_category_label_origin(
766 bounds: Rect,
767 center: Point,
768 radius: f64,
769 index: usize,
770 count: usize,
771 position: TickLabelPosition,
772) -> Option<Point> {
773 let label_radius = match position {
774 TickLabelPosition::High => radius + 18.0,
775 TickLabelPosition::Low => (radius - 10.0).max(0.0),
776 TickLabelPosition::NextTo => radius + 10.0,
777 TickLabelPosition::None => return None,
778 };
779 let label = radial_point(center, label_radius, index, count);
780 let mut origin = Point {
781 x: label.x - 4.0,
782 y: label.y + 3.0,
783 };
784 if position == TickLabelPosition::High {
785 origin.x = origin.x.clamp(
786 bounds.x - PLOT_MARGIN_LEFT,
787 bounds.x + bounds.width + PLOT_MARGIN_RIGHT,
788 );
789 origin.y = origin.y.clamp(
790 bounds.y - PLOT_MARGIN_TOP,
791 bounds.y + bounds.height + PLOT_MARGIN_BOTTOM,
792 );
793 }
794 Some(origin)
795}
796
797fn radar_value_tick(
798 elements: &mut Vec<PositionedElement>,
799 center: Point,
800 distance: f64,
801 mark: TickMark,
802) {
803 let (left, right) = match mark {
804 TickMark::None => return,
805 TickMark::Inside => (4.0, 0.0),
806 TickMark::Outside => (0.0, 4.0),
807 TickMark::Cross => (2.0, 2.0),
808 };
809 let y = center.y - distance;
810 elements.push(stroked_segment(
811 Point {
812 x: center.x - left,
813 y,
814 },
815 Point {
816 x: center.x + right,
817 y,
818 },
819 Color::BLACK,
820 1.0,
821 ));
822}
823
824fn radar_category_tick(
825 elements: &mut Vec<PositionedElement>,
826 center: Point,
827 radius: f64,
828 index: usize,
829 count: usize,
830 mark: TickMark,
831) {
832 let (inside, outside) = match mark {
833 TickMark::None => return,
834 TickMark::Inside => (4.0, 0.0),
835 TickMark::Outside => (0.0, 4.0),
836 TickMark::Cross => (2.0, 2.0),
837 };
838 elements.push(stroked_segment(
839 radial_point(center, radius - inside, index, count),
840 radial_point(center, radius + outside, index, count),
841 Color::BLACK,
842 1.0,
843 ));
844}
845
846fn axis_extent(plot: &Plot, axis: &Axis, blanks: DispBlanksAs) -> Result<Domain> {
847 if let Plot::Scatter { axis_ids, .. } = plot {
848 let (x, y) = scatter_domains(plot, blanks)?;
849 return Ok(if axis.id == axis_ids[0] { x } else { y });
850 }
851 plot_value_domain(plot, blanks)?.ok_or_else(|| ChartError::InvalidValue {
852 element: "chart axis".to_owned(),
853 value: "axis-free plot supplied an axis".to_owned(),
854 })
855}
856
857fn axis_tick_coordinate(
858 value: f64,
859 domain: Domain,
860 bounds: Rect,
861 position: AxisPosition,
862 orientation: Orientation,
863) -> Result<f64> {
864 if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
865 map_x_oriented(value, domain, bounds, orientation)
866 } else {
867 map_y_oriented(value, domain, bounds, orientation)
868 }
869}
870
871fn axis_line(elements: &mut Vec<PositionedElement>, bounds: Rect, position: AxisPosition) {
872 let (start, end) = match position {
873 AxisPosition::Bottom => (
874 Point {
875 x: bounds.x,
876 y: bounds.y + bounds.height,
877 },
878 Point {
879 x: bounds.x + bounds.width,
880 y: bounds.y + bounds.height,
881 },
882 ),
883 AxisPosition::Top => (
884 Point {
885 x: bounds.x,
886 y: bounds.y,
887 },
888 Point {
889 x: bounds.x + bounds.width,
890 y: bounds.y,
891 },
892 ),
893 AxisPosition::Left => (
894 Point {
895 x: bounds.x,
896 y: bounds.y,
897 },
898 Point {
899 x: bounds.x,
900 y: bounds.y + bounds.height,
901 },
902 ),
903 AxisPosition::Right => (
904 Point {
905 x: bounds.x + bounds.width,
906 y: bounds.y,
907 },
908 Point {
909 x: bounds.x + bounds.width,
910 y: bounds.y + bounds.height,
911 },
912 ),
913 };
914 elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
915}
916
917fn major_gridline(
918 elements: &mut Vec<PositionedElement>,
919 bounds: Rect,
920 position: AxisPosition,
921 coordinate: f64,
922) {
923 let (start, end) = if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
924 (
925 Point {
926 x: coordinate,
927 y: bounds.y,
928 },
929 Point {
930 x: coordinate,
931 y: bounds.y + bounds.height,
932 },
933 )
934 } else {
935 (
936 Point {
937 x: bounds.x,
938 y: coordinate,
939 },
940 Point {
941 x: bounds.x + bounds.width,
942 y: coordinate,
943 },
944 )
945 };
946 elements.push(stroked_segment(
947 start,
948 end,
949 Color {
950 r: 0.82,
951 g: 0.82,
952 b: 0.82,
953 a: 1.0,
954 },
955 0.5,
956 ));
957}
958
959fn axis_tick(
960 elements: &mut Vec<PositionedElement>,
961 bounds: Rect,
962 position: AxisPosition,
963 coordinate: f64,
964 mark: TickMark,
965) {
966 let (inside, outside) = match mark {
967 TickMark::None => return,
968 TickMark::Inside => (4.0, 0.0),
969 TickMark::Outside => (0.0, 4.0),
970 TickMark::Cross => (2.0, 2.0),
971 };
972 let (start, end) = match position {
973 AxisPosition::Bottom => (
974 Point {
975 x: coordinate,
976 y: bounds.y + bounds.height - inside,
977 },
978 Point {
979 x: coordinate,
980 y: bounds.y + bounds.height + outside,
981 },
982 ),
983 AxisPosition::Top => (
984 Point {
985 x: coordinate,
986 y: bounds.y + inside,
987 },
988 Point {
989 x: coordinate,
990 y: bounds.y - outside,
991 },
992 ),
993 AxisPosition::Left => (
994 Point {
995 x: bounds.x + inside,
996 y: coordinate,
997 },
998 Point {
999 x: bounds.x - outside,
1000 y: coordinate,
1001 },
1002 ),
1003 AxisPosition::Right => (
1004 Point {
1005 x: bounds.x + bounds.width - inside,
1006 y: coordinate,
1007 },
1008 Point {
1009 x: bounds.x + bounds.width + outside,
1010 y: coordinate,
1011 },
1012 ),
1013 };
1014 elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
1015}
1016
1017fn tick_label_origin(
1018 bounds: Rect,
1019 axis_position: AxisPosition,
1020 label_position: TickLabelPosition,
1021 coordinate: f64,
1022) -> Point {
1023 let side = match label_position {
1024 TickLabelPosition::NextTo => axis_position,
1025 TickLabelPosition::High => {
1026 if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
1027 AxisPosition::Top
1028 } else {
1029 AxisPosition::Right
1030 }
1031 }
1032 TickLabelPosition::Low => {
1033 if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
1034 AxisPosition::Bottom
1035 } else {
1036 AxisPosition::Left
1037 }
1038 }
1039 TickLabelPosition::None => axis_position,
1040 };
1041 match side {
1042 AxisPosition::Bottom => Point {
1043 x: coordinate - 4.0,
1044 y: bounds.y + bounds.height + 12.0,
1045 },
1046 AxisPosition::Top => Point {
1047 x: coordinate - 4.0,
1048 y: bounds.y - 4.0,
1049 },
1050 AxisPosition::Left => Point {
1051 x: bounds.x - 30.0,
1052 y: coordinate + 3.0,
1053 },
1054 AxisPosition::Right => Point {
1055 x: bounds.x + bounds.width + 4.0,
1056 y: coordinate + 3.0,
1057 },
1058 }
1059}
1060
1061fn format_axis_value(format: Option<&NumberFormat>, value: f64) -> Result<String> {
1062 if let Some(format) = format {
1063 format.format_value(value)
1064 } else {
1065 NumberFormat::new("General".to_owned(), false)?.format_value(value)
1066 }
1067}
1068
1069fn shape_label(fonts: &mut FontManager, text: &str, origin: Point) -> Result<GlyphRun> {
1070 shape_label_with_properties(fonts, text, origin, None)
1071}
1072
1073fn shape_label_with_properties(
1074 fonts: &mut FontManager,
1075 text: &str,
1076 origin: Point,
1077 properties: Option<&CT_TextBody>,
1078) -> Result<GlyphRun> {
1079 const FAMILY: &str = "Carlito";
1080 const SIZE: f64 = 9.0;
1081 let properties = properties.and_then(chart_text_properties);
1082 let family = properties
1083 .and_then(|properties| properties.latin.as_ref())
1084 .map(|font| font.typeface.as_str())
1085 .unwrap_or(FAMILY);
1086 let size = properties
1087 .and_then(|properties| properties.font_size)
1088 .map(|size| f64::from(size) / 100.0)
1089 .unwrap_or(SIZE);
1090 let bold = properties
1091 .and_then(|properties| properties.bold)
1092 .unwrap_or(false);
1093 let italic = properties
1094 .and_then(|properties| properties.italic)
1095 .unwrap_or(false);
1096 let font_id = fonts
1097 .resolve_font_for_text(Some(family), bold, italic, text)
1098 .map_err(|error| ChartError::InvalidValue {
1099 element: "chart label font".to_owned(),
1100 value: error.to_string(),
1101 })?;
1102 let shaped =
1103 fonts
1104 .shape_text(font_id, text, size)
1105 .map_err(|error| ChartError::InvalidValue {
1106 element: "chart label shaping".to_owned(),
1107 value: error.to_string(),
1108 })?;
1109 Ok(GlyphRun {
1110 origin,
1111 font_id,
1112 font_size: size,
1113 glyph_ids: shaped.glyph_ids,
1114 advances: shaped.advances,
1115 text: text.to_owned(),
1116 color: Color::BLACK,
1117 bold,
1118 italic,
1119 field_kind: None,
1120 footnote_id: None,
1121 })
1122}
1123
1124fn chart_text_properties(body: &CT_TextBody) -> Option<&CT_TextCharacterProperties> {
1125 body.paragraphs()
1126 .first()
1127 .and_then(|paragraph| paragraph.properties.as_ref())
1128 .and_then(|properties| properties.default_run_properties.as_ref())
1129}
1130
1131fn stroked_segment(start: Point, end: Point, color: Color, width: f64) -> PositionedElement {
1132 PositionedElement::Path(PathElement {
1133 path: Path {
1134 commands: vec![PathCommand::MoveTo(start), PathCommand::LineTo(end)],
1135 fill_rule: FillRule::NonZero,
1136 },
1137 fill: None,
1138 stroke: Some(Stroke::new(Paint::Solid(color), width)),
1139 })
1140}
1141
1142fn plot_series(plot: &Plot) -> &[Series] {
1143 match plot {
1144 Plot::Bar { series, .. }
1145 | Plot::Line { series, .. }
1146 | Plot::Pie { series, .. }
1147 | Plot::Doughnut { series, .. }
1148 | Plot::Area { series, .. }
1149 | Plot::Scatter { series, .. }
1150 | Plot::Radar { series, .. } => series,
1151 }
1152}
1153
1154fn plot_data_labels(plot: &Plot) -> Option<&CT_DLbls> {
1155 match plot {
1156 Plot::Bar { data_labels, .. }
1157 | Plot::Line { data_labels, .. }
1158 | Plot::Pie { data_labels, .. }
1159 | Plot::Doughnut { data_labels, .. }
1160 | Plot::Area { data_labels, .. }
1161 | Plot::Scatter { data_labels, .. }
1162 | Plot::Radar { data_labels, .. } => data_labels.as_ref(),
1163 }
1164}
1165
1166fn series_categories(
1167 series: &Series,
1168 number_format: Option<&NumberFormat>,
1169) -> Result<Vec<(usize, String)>> {
1170 let Some(categories) = &series.categories else {
1171 return Ok(Vec::new());
1172 };
1173 match categories {
1174 AxisData::String(data) => {
1175 let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
1176 indexes
1177 .into_iter()
1178 .zip(&data.values)
1179 .map(|(index, value)| {
1180 usize::try_from(index)
1181 .map(|index| (index, value.clone()))
1182 .map_err(|_| ChartError::InvalidValue {
1183 element: "c:cat/c:pt/@idx".to_owned(),
1184 value: index.to_string(),
1185 })
1186 })
1187 .collect()
1188 }
1189 AxisData::Numeric(data) => {
1190 let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
1191 let cache_format;
1192 let format = if let Some(format) = number_format {
1193 format
1194 } else {
1195 cache_format = NumberFormat::new(data.format_code.clone(), false)?;
1196 &cache_format
1197 };
1198 indexes
1199 .into_iter()
1200 .zip(&data.values)
1201 .map(|(index, value)| {
1202 let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
1203 element: "c:cat/c:pt/@idx".to_owned(),
1204 value: index.to_string(),
1205 })?;
1206 let text = format.format_value(*value)?;
1207 Ok((index, text))
1208 })
1209 .collect()
1210 }
1211 }
1212}
1213
1214fn series_name(series: &Series, index: usize) -> String {
1215 series
1216 .name
1217 .as_ref()
1218 .and_then(|name| name.values.first())
1219 .cloned()
1220 .unwrap_or_else(|| format!("Series {}", index + 1))
1221}
1222
1223fn render_data_labels(
1224 plot: &Plot,
1225 bounds: Rect,
1226 blanks: DispBlanksAs,
1227 options: GeometryOptions,
1228 fonts: &mut FontManager,
1229 elements: &mut Vec<PositionedElement>,
1230) -> Result<()> {
1231 let plot_labels = plot_data_labels(plot);
1232 let anchors = plot_label_anchors(plot, bounds, blanks, options)?;
1233 for (series_index, series) in plot_series(plot).iter().enumerate() {
1234 let Some(labels) = series.data_labels.as_ref().or(plot_labels) else {
1235 continue;
1236 };
1237 let overrides = point_label_overrides(labels)?;
1238 let mut categories = None;
1239 let mut bubble_sizes = None;
1240 let (_, values) = logical_numeric_values(&series.values)?;
1241 let mut percentage_total = None;
1242 for (logical_index, value) in values {
1243 let point_override = overrides.get(&logical_index);
1244 if point_override.is_some_and(|point| point.deleted == Some(true)) {
1245 continue;
1246 }
1247 let effective_labels = point_override
1248 .map(|point| point.apply_to(labels))
1249 .unwrap_or_else(|| labels.clone());
1250 let labels = &effective_labels;
1251 let Some(anchor) = anchors.get(&(series_index, logical_index)).copied() else {
1252 continue;
1253 };
1254 let mut fields = Vec::new();
1255 if labels.show_series_name {
1256 fields.push(series_name(series, series_index));
1257 }
1258 if labels.show_category_name {
1259 if categories.is_none() {
1260 categories = Some(
1261 series_categories(series, None)?
1262 .into_iter()
1263 .collect::<BTreeMap<_, _>>(),
1264 );
1265 }
1266 if let Some(category) = categories
1267 .as_ref()
1268 .and_then(|categories| categories.get(&logical_index))
1269 {
1270 fields.push(category.clone());
1271 }
1272 }
1273 if labels.show_value {
1274 fields.push(format_axis_value(labels.number_format.as_ref(), value)?);
1275 }
1276 if labels.show_percent {
1277 let total = if let Some(total) = percentage_total {
1278 total
1279 } else {
1280 let total = series
1281 .values
1282 .values
1283 .iter()
1284 .copied()
1285 .filter(|value| *value > 0.0)
1286 .try_fold(0.0, |total, value| {
1287 checked_geometry_sum(total, value, "data-label percentage total")
1288 })?;
1289 percentage_total = Some(total);
1290 total
1291 };
1292 if total > 0.0 {
1293 let text = if let Some(format) = &labels.number_format {
1294 format.format_value(value / total)?
1295 } else {
1296 NumberFormat::new("0%".to_owned(), false)?.format_value(value / total)?
1297 };
1298 fields.push(text);
1299 }
1300 }
1301 if labels.show_bubble_size {
1302 if bubble_sizes.is_none() {
1303 bubble_sizes = Some(
1304 series
1305 .bubble_size
1306 .as_ref()
1307 .map(logical_numeric_values)
1308 .transpose()?
1309 .map(|(_, values)| values.into_iter().collect::<BTreeMap<_, _>>())
1310 .unwrap_or_default(),
1311 );
1312 }
1313 if let Some(value) = bubble_sizes
1314 .as_ref()
1315 .and_then(|sizes| sizes.get(&logical_index))
1316 {
1317 fields.push(format_axis_value(labels.number_format.as_ref(), *value)?);
1318 }
1319 }
1320 if fields.is_empty() {
1321 continue;
1322 }
1323 let separator = labels.separator.as_deref().unwrap_or(", ");
1324 let text = fields.join(separator);
1325 let origin = anchor.origin(labels.position);
1326 elements.push(PositionedElement::Text(shape_label(fonts, &text, origin)?));
1327 }
1328 }
1329 Ok(())
1330}
1331
1332#[derive(Clone, Debug, Default)]
1333struct PointLabelOverride {
1334 index: Option<usize>,
1335 deleted: Option<bool>,
1336 number_format: Option<NumberFormat>,
1337 position: Option<DataLabelPosition>,
1338 separator: Option<String>,
1339 show_legend_key: Option<bool>,
1340 show_value: Option<bool>,
1341 show_category_name: Option<bool>,
1342 show_series_name: Option<bool>,
1343 show_percent: Option<bool>,
1344 show_bubble_size: Option<bool>,
1345}
1346
1347impl PointLabelOverride {
1348 fn apply_to(&self, labels: &CT_DLbls) -> CT_DLbls {
1349 let mut effective = labels.clone();
1350 if let Some(value) = &self.number_format {
1351 effective.number_format = Some(value.clone());
1352 }
1353 if let Some(value) = self.position {
1354 effective.position = Some(value);
1355 }
1356 if let Some(value) = &self.separator {
1357 effective.separator = Some(value.clone());
1358 }
1359 if let Some(value) = self.show_legend_key {
1360 effective.show_legend_key = value;
1361 }
1362 if let Some(value) = self.show_value {
1363 effective.show_value = value;
1364 }
1365 if let Some(value) = self.show_category_name {
1366 effective.show_category_name = value;
1367 }
1368 if let Some(value) = self.show_series_name {
1369 effective.show_series_name = value;
1370 }
1371 if let Some(value) = self.show_percent {
1372 effective.show_percent = value;
1373 }
1374 if let Some(value) = self.show_bubble_size {
1375 effective.show_bubble_size = value;
1376 }
1377 effective
1378 }
1379}
1380
1381fn point_label_overrides(labels: &CT_DLbls) -> Result<BTreeMap<usize, PointLabelOverride>> {
1382 let mut namespaces = chart_namespace_defaults();
1383 for (name, namespace) in &labels.namespace_declarations {
1384 let prefix = if name == "xmlns" {
1385 Vec::new()
1386 } else if let Some(prefix) = name.strip_prefix("xmlns:") {
1387 prefix.as_bytes().to_vec()
1388 } else {
1389 continue;
1390 };
1391 upsert_namespace_binding(&mut namespaces, prefix, namespace.clone());
1392 }
1393 let mut overrides = BTreeMap::new();
1394 for raw in labels.raw_children.at(0) {
1395 if chart_root_local(raw, &namespaces)?.as_deref() != Some(b"dLbl") {
1396 continue;
1397 }
1398 let point = parse_point_label_override(raw, &namespaces)?;
1399 let index = point
1400 .index
1401 .ok_or_else(|| ChartError::MissingElement("c:dLbl/c:idx".to_owned()))?;
1402 if overrides.insert(index, point).is_some() {
1403 return Err(ChartError::DuplicateElement(format!(
1404 "c:dLbl[c:idx={index}]"
1405 )));
1406 }
1407 }
1408 Ok(overrides)
1409}
1410
1411fn parse_point_label_override(
1412 xml: &[u8],
1413 inherited: &NamespaceBindings,
1414) -> Result<PointLabelOverride> {
1415 let mut reader = Reader::from_reader(xml);
1416 let mut buffer = Vec::new();
1417 let mut namespaces = None;
1418 let mut state = PointLabelOverride::default();
1419 let mut order = 0;
1420 loop {
1421 match reader
1422 .read_event_into(&mut buffer)
1423 .map_err(OxmlError::from)?
1424 {
1425 Event::Start(element) if namespaces.is_none() => {
1426 if !matches_local_name(element.name().as_ref(), b"dLbl")
1427 || !element_is_in_namespace(&element, C_NS, inherited)?
1428 {
1429 return Err(ChartError::UnexpectedElement(element_name(&element)));
1430 }
1431 namespaces = Some(chart_bindings(inherited, &element)?);
1432 }
1433 Event::Start(element) => {
1434 let bindings = namespaces
1435 .as_ref()
1436 .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
1437 let name = chart_child_local(&element, bindings)?;
1438 let raw = capture_element(&mut reader, &element)?;
1439 parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
1440 }
1441 Event::Empty(element) => {
1442 let bindings = namespaces
1443 .as_ref()
1444 .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
1445 let name = chart_child_local(&element, bindings)?;
1446 let raw = capture_empty_element(&element)?;
1447 parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
1448 }
1449 Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbl") => {
1450 if state.index.is_none() {
1451 return Err(ChartError::MissingElement("c:dLbl/c:idx".to_owned()));
1452 }
1453 return Ok(state);
1454 }
1455 Event::Eof => return Err(missing_end("c:dLbl")),
1456 _ => {}
1457 }
1458 buffer.clear();
1459 }
1460}
1461
1462fn parse_point_label_child(
1463 state: &mut PointLabelOverride,
1464 name: Option<&[u8]>,
1465 raw: &[u8],
1466 namespaces: &NamespaceBindings,
1467 order: &mut usize,
1468) -> Result<()> {
1469 let Some(name) = name else {
1470 return Ok(());
1471 };
1472 let rank = match name {
1473 b"idx" => 0,
1474 b"delete" => 1,
1475 b"layout" => 2,
1476 b"tx" => 3,
1477 b"numFmt" => 4,
1478 b"spPr" => 5,
1479 b"txPr" => 6,
1480 b"dLblPos" => 7,
1481 b"showLegendKey" => 8,
1482 b"showVal" => 9,
1483 b"showCatName" => 10,
1484 b"showSerName" => 11,
1485 b"showPercent" => 12,
1486 b"showBubbleSize" => 13,
1487 b"separator" => 14,
1488 b"showLeaderLines" => 15,
1489 b"leaderLines" => 16,
1490 b"extLst" => 17,
1491 _ => return Ok(()),
1492 };
1493 if rank < *order {
1494 return Err(ChartError::InvalidValue {
1495 element: "c:dLbl".to_owned(),
1496 value: format!("c:{} is out of schema order", String::from_utf8_lossy(name)),
1497 });
1498 }
1499 *order = rank;
1500 match name {
1501 b"idx" => {
1502 let (index, _) = parse_u32_scalar(raw, "idx")?;
1503 let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
1504 element: "c:dLbl/c:idx".to_owned(),
1505 value: index.to_string(),
1506 })?;
1507 set_once(&mut state.index, index, "c:dLbl/c:idx")?;
1508 }
1509 b"delete" => set_once(
1510 &mut state.deleted,
1511 parse_bool_value(raw, "delete")?.0,
1512 "c:dLbl/c:delete",
1513 )?,
1514 b"numFmt" => set_once(
1515 &mut state.number_format,
1516 NumberFormat::from_xml(raw)?,
1517 "c:dLbl/c:numFmt",
1518 )?,
1519 b"dLblPos" => {
1520 let (value, _) = scalar_value(raw, "dLblPos")?;
1521 let value =
1522 value.ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
1523 let position = DataLabelPosition::parse(&value)
1524 .ok_or_else(|| invalid_attribute("dLblPos", "val", value))?;
1525 set_once(&mut state.position, position, "c:dLbl/c:dLblPos")?;
1526 }
1527 b"showLegendKey" => set_once(
1528 &mut state.show_legend_key,
1529 parse_bool_value(raw, "showLegendKey")?.0,
1530 "c:dLbl/c:showLegendKey",
1531 )?,
1532 b"showVal" => set_once(
1533 &mut state.show_value,
1534 parse_bool_value(raw, "showVal")?.0,
1535 "c:dLbl/c:showVal",
1536 )?,
1537 b"showCatName" => set_once(
1538 &mut state.show_category_name,
1539 parse_bool_value(raw, "showCatName")?.0,
1540 "c:dLbl/c:showCatName",
1541 )?,
1542 b"showSerName" => set_once(
1543 &mut state.show_series_name,
1544 parse_bool_value(raw, "showSerName")?.0,
1545 "c:dLbl/c:showSerName",
1546 )?,
1547 b"showPercent" => set_once(
1548 &mut state.show_percent,
1549 parse_bool_value(raw, "showPercent")?.0,
1550 "c:dLbl/c:showPercent",
1551 )?,
1552 b"showBubbleSize" => set_once(
1553 &mut state.show_bubble_size,
1554 parse_bool_value(raw, "showBubbleSize")?.0,
1555 "c:dLbl/c:showBubbleSize",
1556 )?,
1557 b"separator" => set_once(
1558 &mut state.separator,
1559 parse_text_element(raw, b"separator", namespaces)?.0,
1560 "c:dLbl/c:separator",
1561 )?,
1562 _ => {}
1563 }
1564 Ok(())
1565}
1566
1567#[derive(Clone, Copy, Debug)]
1568struct LabelAnchor {
1569 center: Point,
1570 base: Point,
1571 end: Point,
1572 direction: Point,
1573}
1574
1575impl LabelAnchor {
1576 const fn point(center: Point) -> Self {
1577 Self {
1578 center,
1579 base: center,
1580 end: center,
1581 direction: Point { x: 0.0, y: 0.0 },
1582 }
1583 }
1584
1585 const fn segment(center: Point, base: Point, end: Point) -> Self {
1586 Self {
1587 center,
1588 base,
1589 end,
1590 direction: Point {
1591 x: end.x - base.x,
1592 y: end.y - base.y,
1593 },
1594 }
1595 }
1596
1597 const fn directed_segment(center: Point, base: Point, end: Point, direction: Point) -> Self {
1598 Self {
1599 center,
1600 base,
1601 end,
1602 direction,
1603 }
1604 }
1605
1606 fn origin(self, position: Option<DataLabelPosition>) -> Point {
1607 let mut origin = self.center;
1608 match position.unwrap_or(DataLabelPosition::BestFit) {
1609 DataLabelPosition::Bottom => origin.y += 8.0,
1610 DataLabelPosition::Top => origin.y -= 5.0,
1611 DataLabelPosition::Left => origin.x -= 8.0,
1612 DataLabelPosition::Right => origin.x += 8.0,
1613 DataLabelPosition::InsideBase => {
1614 origin = self.along_segment(self.base, 5.0, true).unwrap_or(Point {
1615 x: origin.x,
1616 y: origin.y + 8.0,
1617 });
1618 }
1619 DataLabelPosition::InsideEnd => {
1620 origin = self.along_segment(self.end, -5.0, true).unwrap_or(origin);
1621 }
1622 DataLabelPosition::OutsideEnd => {
1623 origin = self.along_segment(self.end, 5.0, false).unwrap_or(Point {
1624 x: origin.x,
1625 y: origin.y - 5.0,
1626 });
1627 }
1628 DataLabelPosition::BestFit | DataLabelPosition::Center => {}
1629 }
1630 origin
1631 }
1632
1633 fn along_segment(self, origin: Point, distance: f64, clamp_inside: bool) -> Option<Point> {
1634 let dx = self.end.x - self.base.x;
1635 let dy = self.end.y - self.base.y;
1636 let length = dx.hypot(dy);
1637 let (dx, dy, direction_length) = if length.is_finite() && length > 0.0 {
1638 (dx, dy, length)
1639 } else {
1640 let direction_length = self.direction.x.hypot(self.direction.y);
1641 if !direction_length.is_finite() || direction_length == 0.0 {
1642 return None;
1643 }
1644 (self.direction.x, self.direction.y, direction_length)
1645 };
1646 let distance = if clamp_inside {
1647 distance.clamp(-length / 2.0, length / 2.0)
1648 } else {
1649 distance
1650 };
1651 Some(Point {
1652 x: origin.x + dx / direction_length * distance,
1653 y: origin.y + dy / direction_length * distance,
1654 })
1655 }
1656}
1657
1658fn plot_label_anchors(
1659 plot: &Plot,
1660 bounds: Rect,
1661 blanks: DispBlanksAs,
1662 options: GeometryOptions,
1663) -> Result<BTreeMap<(usize, usize), LabelAnchor>> {
1664 let mut anchors = BTreeMap::new();
1665 match plot {
1666 Plot::Bar {
1667 direction,
1668 grouping,
1669 gap_width,
1670 overlap,
1671 series,
1672 ..
1673 } => {
1674 for bar in bar_rectangles(
1675 *direction, *grouping, *gap_width, *overlap, series, bounds, options,
1676 )? {
1677 if !point_in_rect(bar.end, bounds) {
1678 continue;
1679 }
1680 let visible_base = Point {
1681 x: bar.base.x.clamp(bounds.x, bounds.x + bounds.width),
1682 y: bar.base.y.clamp(bounds.y, bounds.y + bounds.height),
1683 };
1684 let visible_end = Point {
1685 x: bar.end.x.clamp(bounds.x, bounds.x + bounds.width),
1686 y: bar.end.y.clamp(bounds.y, bounds.y + bounds.height),
1687 };
1688 anchors.insert(
1689 (bar.series_index, bar.logical_index),
1690 LabelAnchor::directed_segment(
1691 Point {
1692 x: (visible_base.x + visible_end.x) / 2.0,
1693 y: (visible_base.y + visible_end.y) / 2.0,
1694 },
1695 visible_base,
1696 visible_end,
1697 Point {
1698 x: bar.end.x - bar.base.x,
1699 y: bar.end.y - bar.base.y,
1700 },
1701 ),
1702 );
1703 }
1704 }
1705 Plot::Line {
1706 grouping, series, ..
1707 }
1708 | Plot::Area {
1709 grouping, series, ..
1710 } => {
1711 for (series_index, points) in
1712 category_label_points(*grouping, series, bounds, blanks, options)?
1713 .into_iter()
1714 .enumerate()
1715 {
1716 for (logical_index, point) in points {
1717 anchors.insert((series_index, logical_index), LabelAnchor::point(point));
1718 }
1719 }
1720 }
1721 Plot::Scatter { series, .. } => {
1722 let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
1723 let x_mapping = options.scatter_x.unwrap_or(AxisMapping {
1724 domain: x_domain,
1725 orientation: Orientation::MinMax,
1726 });
1727 let y_mapping = options.scatter_y.unwrap_or(AxisMapping {
1728 domain: y_domain,
1729 orientation: Orientation::MinMax,
1730 });
1731 for (series_index, series) in series.iter().enumerate() {
1732 let indexed = matched_scatter_points(series)?;
1733 let points = scatter_points(
1734 &indexed,
1735 x_mapping.domain,
1736 y_mapping.domain,
1737 bounds,
1738 x_mapping.orientation,
1739 y_mapping.orientation,
1740 )?;
1741 for ((logical_index, _, _), point) in indexed.into_iter().zip(points) {
1742 anchors.insert((series_index, logical_index), LabelAnchor::point(point));
1743 }
1744 }
1745 }
1746 Plot::Radar { series, .. } => {
1747 let center = Point {
1748 x: bounds.x + bounds.width / 2.0,
1749 y: bounds.y + bounds.height / 2.0,
1750 };
1751 let category_count = series_category_count(series)?;
1752 for (series_index, points) in radar_series_points(series, bounds, options)?
1753 .into_iter()
1754 .enumerate()
1755 {
1756 for (logical_index, point) in points {
1757 let spoke = radial_point(
1758 center,
1759 1.0,
1760 oriented_category_index(
1761 logical_index,
1762 category_count,
1763 options.category_orientation,
1764 ),
1765 category_count,
1766 );
1767 anchors.insert(
1768 (series_index, logical_index),
1769 LabelAnchor::directed_segment(
1770 point,
1771 center,
1772 point,
1773 Point {
1774 x: spoke.x - center.x,
1775 y: spoke.y - center.y,
1776 },
1777 ),
1778 );
1779 }
1780 }
1781 }
1782 Plot::Pie {
1783 first_slice_angle,
1784 series,
1785 ..
1786 } => {
1787 for slice in pie_slices(*first_slice_angle, None, series, bounds)? {
1788 anchors.insert(
1789 (slice.series_index, slice.logical_index),
1790 LabelAnchor::segment(slice.anchor, slice.base, slice.end),
1791 );
1792 }
1793 }
1794 Plot::Doughnut {
1795 first_slice_angle,
1796 hole_size,
1797 series,
1798 ..
1799 } => {
1800 for slice in pie_slices(*first_slice_angle, Some(*hole_size), series, bounds)? {
1801 anchors.insert(
1802 (slice.series_index, slice.logical_index),
1803 LabelAnchor::segment(slice.anchor, slice.base, slice.end),
1804 );
1805 }
1806 }
1807 }
1808 anchors.retain(|_, anchor| point_in_rect(anchor.end, bounds));
1809 Ok(anchors)
1810}
1811
1812fn category_label_points(
1813 grouping: Grouping,
1814 series: &[Series],
1815 bounds: Rect,
1816 blanks: DispBlanksAs,
1817 options: GeometryOptions,
1818) -> Result<Vec<Vec<(usize, Point)>>> {
1819 validate_series_values(series)?;
1820 let logical_series = logical_series_values(series)?;
1821 let category_count = series_category_count(series)?;
1822 let calculated_series = if blanks == DispBlanksAs::Zero {
1823 zero_filled_series(&logical_series, category_count)
1824 } else {
1825 logical_series.clone()
1826 };
1827 let layers = stacked_series_bounds(
1828 &calculated_series,
1829 grouping != Grouping::Standard,
1830 grouping == Grouping::PercentStacked,
1831 )?;
1832 let domain = options
1833 .value
1834 .map(|mapping| mapping.domain)
1835 .unwrap_or(domain_from_layers(&layers)?);
1836 let value_orientation = options
1837 .value
1838 .map(|mapping| mapping.orientation)
1839 .unwrap_or(Orientation::MinMax);
1840 let mut point_series = Vec::with_capacity(layers.len());
1841 for (series_index, layer) in layers.iter().enumerate() {
1842 let present: BTreeSet<_> = logical_series[series_index]
1843 .1
1844 .iter()
1845 .map(|(index, _)| *index)
1846 .collect();
1847 let indexed: Vec<_> = layer
1848 .iter()
1849 .filter(|(index, _, _)| present.contains(index))
1850 .map(|(index, _, upper)| (*index, *upper))
1851 .collect();
1852 let points = category_points(
1853 &indexed,
1854 category_count,
1855 domain,
1856 bounds,
1857 options.category_orientation,
1858 value_orientation,
1859 )?;
1860 point_series.push(
1861 indexed
1862 .into_iter()
1863 .map(|(index, _)| index)
1864 .zip(points)
1865 .collect(),
1866 );
1867 }
1868 Ok(point_series)
1869}
1870
1871fn point_in_rect(point: Point, bounds: Rect) -> bool {
1872 const EPSILON: f64 = 1.0e-9;
1873 point.x >= bounds.x - EPSILON
1874 && point.x <= bounds.x + bounds.width + EPSILON
1875 && point.y >= bounds.y - EPSILON
1876 && point.y <= bounds.y + bounds.height + EPSILON
1877}
1878
1879fn resolve_series_colours(
1880 plot: &Plot,
1881 theme: &CT_OfficeStyleSheet,
1882 color_map: &ColorMap,
1883) -> Result<Vec<Color>> {
1884 let lookup = theme_colour_lookup(theme);
1885 plot_series(plot)
1886 .iter()
1887 .enumerate()
1888 .map(|(index, series)| {
1889 if let Some(properties) = series.sp_pr.as_ref() {
1890 if let Some(fill) = properties.fill.as_ref() {
1891 return resolve_series_paint(fill, index, "fill", theme, color_map, &lookup);
1892 }
1893 if let Some(fill) = properties.line.as_ref().and_then(|line| line.fill.as_ref()) {
1894 return resolve_series_paint(fill, index, "line", theme, color_map, &lookup);
1895 }
1896 }
1897 resolve_series_accent(index, theme, color_map, &lookup)
1898 })
1899 .collect()
1900}
1901
1902fn resolve_series_paint(
1903 fill: &Fill,
1904 series_index: usize,
1905 source: &str,
1906 theme: &CT_OfficeStyleSheet,
1907 color_map: &ColorMap,
1908 lookup: &[(&str, RgbColor)],
1909) -> Result<Color> {
1910 match fill {
1911 Fill::NoFill(_) => Ok(Color {
1912 r: 0.0,
1913 g: 0.0,
1914 b: 0.0,
1915 a: 0.0,
1916 }),
1917 Fill::Solid(solid) => {
1918 let choice = solid
1919 .color
1920 .as_ref()
1921 .ok_or_else(|| ChartError::InvalidValue {
1922 element: format!("c:ser[{series_index}] direct {source} paint"),
1923 value: "solid fill has no colour choice".to_owned(),
1924 })?;
1925 resolve_direct_chart_colour(choice, theme, color_map, lookup, || {
1926 format!("c:ser[{series_index}] direct {source} paint")
1927 })
1928 }
1929 Fill::Gradient(_) => unsupported_series_paint(series_index, source, "gradient"),
1930 Fill::Pattern(_) => unsupported_series_paint(series_index, source, "pattern"),
1931 Fill::Blip(_) => unsupported_series_paint(series_index, source, "picture"),
1932 }
1933}
1934
1935fn resolve_direct_chart_colour(
1936 choice: &ColorChoice,
1937 theme: &CT_OfficeStyleSheet,
1938 color_map: &ColorMap,
1939 lookup: &[(&str, RgbColor)],
1940 element: impl FnOnce() -> String,
1941) -> Result<Color> {
1942 let ColorChoice::Scheme {
1943 value, transforms, ..
1944 } = choice
1945 else {
1946 return resolve_chart_colour(choice, color_map, lookup, element);
1947 };
1948 let theme_slot =
1949 mapped_theme_slot(value, color_map).ok_or_else(|| ChartError::InvalidValue {
1950 element: element(),
1951 value: format!("no concrete theme slot is available for scheme colour {value}"),
1952 })?;
1953 let theme_choice = theme.theme_elements.color_scheme.color(theme_slot);
1954 let base = concrete_theme_colour(theme_choice).ok_or_else(|| ChartError::InvalidValue {
1955 element: format!("a:clrScheme/a:{}", theme_slot.as_str()),
1956 value: "selected theme slot has no concrete sRGB or system fallback colour".to_owned(),
1957 })?;
1958 let mut combined = theme_choice.transforms().to_vec();
1959 combined.extend_from_slice(transforms);
1960 Ok(resolved_layout_colour(apply_color_transforms(
1961 base, &combined,
1962 )))
1963}
1964
1965fn mapped_theme_slot(value: &str, color_map: &ColorMap) -> Option<ThemeColorSlot> {
1966 let mapped = match value {
1967 "bg1" => color_map.theme_slot(ColorMapSlot::Background1),
1968 "tx1" => color_map.theme_slot(ColorMapSlot::Text1),
1969 "bg2" => color_map.theme_slot(ColorMapSlot::Background2),
1970 "tx2" => color_map.theme_slot(ColorMapSlot::Text2),
1971 "accent1" => color_map.theme_slot(ColorMapSlot::Accent1),
1972 "accent2" => color_map.theme_slot(ColorMapSlot::Accent2),
1973 "accent3" => color_map.theme_slot(ColorMapSlot::Accent3),
1974 "accent4" => color_map.theme_slot(ColorMapSlot::Accent4),
1975 "accent5" => color_map.theme_slot(ColorMapSlot::Accent5),
1976 "accent6" => color_map.theme_slot(ColorMapSlot::Accent6),
1977 "hlink" => color_map.theme_slot(ColorMapSlot::Hyperlink),
1978 "folHlink" => color_map.theme_slot(ColorMapSlot::FollowedHyperlink),
1979 "dk1" => ThemeColorSlot::Dark1,
1980 "lt1" => ThemeColorSlot::Light1,
1981 "dk2" => ThemeColorSlot::Dark2,
1982 "lt2" => ThemeColorSlot::Light2,
1983 _ => return None,
1984 };
1985 Some(mapped)
1986}
1987
1988fn unsupported_series_paint(series_index: usize, source: &str, kind: &str) -> Result<Color> {
1989 Err(ChartError::InvalidValue {
1990 element: format!("c:ser[{series_index}] direct {source} paint"),
1991 value: format!("unsupported {kind} paint"),
1992 })
1993}
1994
1995fn resolve_series_accent(
1996 series_index: usize,
1997 theme: &CT_OfficeStyleSheet,
1998 color_map: &ColorMap,
1999 lookup: &[(&str, RgbColor)],
2000) -> Result<Color> {
2001 const ACCENTS: [ColorMapSlot; 6] = [
2002 ColorMapSlot::Accent1,
2003 ColorMapSlot::Accent2,
2004 ColorMapSlot::Accent3,
2005 ColorMapSlot::Accent4,
2006 ColorMapSlot::Accent5,
2007 ColorMapSlot::Accent6,
2008 ];
2009 let semantic_slot = ACCENTS[series_index % ACCENTS.len()];
2010 let theme_slot = color_map.theme_slot(semantic_slot);
2011 let choice = theme.theme_elements.color_scheme.color(theme_slot);
2012 resolve_chart_colour(choice, color_map, lookup, || {
2013 format!("c:ser[{series_index}] theme accent")
2014 })
2015}
2016
2017fn resolve_chart_colour(
2018 choice: &ColorChoice,
2019 color_map: &ColorMap,
2020 lookup: &[(&str, RgbColor)],
2021 element: impl FnOnce() -> String,
2022) -> Result<Color> {
2023 resolve_color(choice, color_map, lookup)
2024 .map(resolved_layout_colour)
2025 .map_err(|error| ChartError::InvalidValue {
2026 element: element(),
2027 value: error.to_string(),
2028 })
2029}
2030
2031fn theme_colour_lookup(theme: &CT_OfficeStyleSheet) -> Vec<(&'static str, RgbColor)> {
2032 theme
2033 .theme_elements
2034 .color_scheme
2035 .iter()
2036 .filter_map(|(slot, choice)| {
2037 concrete_theme_colour(choice).map(|colour| (slot.as_str(), colour))
2038 })
2039 .collect()
2040}
2041
2042fn concrete_theme_colour(choice: &ColorChoice) -> Option<RgbColor> {
2043 match choice {
2044 ColorChoice::Srgb { value, .. } => Some(*value),
2045 ColorChoice::System { last_color, .. } => *last_color,
2046 ColorChoice::Scheme { .. } | ColorChoice::Preset { .. } => None,
2047 }
2048}
2049
2050fn resolved_layout_colour(colour: ResolvedColor) -> Color {
2051 Color {
2052 r: f64::from(colour.red) / 255.0,
2053 g: f64::from(colour.green) / 255.0,
2054 b: f64::from(colour.blue) / 255.0,
2055 a: f64::from(colour.alpha) / 255.0,
2056 }
2057}
2058
2059fn render_legend(
2060 chart: &CT_Chart,
2061 plot: &Plot,
2062 bounds: Rect,
2063 fonts: &mut FontManager,
2064 series_colours: &[Color],
2065 paths: &mut Vec<PositionedElement>,
2066 labels: &mut Vec<PositionedElement>,
2067) -> Result<()> {
2068 if chart.legend.is_none() {
2069 return Ok(());
2070 }
2071 let x = bounds.x + bounds.width - 58.0;
2072 for (index, (series, colour)) in plot_series(plot).iter().zip(series_colours).enumerate() {
2073 let y = bounds.y + 6.0 + index as f64 * 13.0;
2074 paths.push(filled_path(
2075 Path::rect(Rect {
2076 x,
2077 y,
2078 width: 7.0,
2079 height: 7.0,
2080 }),
2081 *colour,
2082 ));
2083 labels.push(PositionedElement::Text(shape_label(
2084 fonts,
2085 &series_name(series, index),
2086 Point {
2087 x: x + 10.0,
2088 y: y + 7.0,
2089 },
2090 )?));
2091 }
2092 Ok(())
2093}
2094
2095fn validate_geometry_coordinates(elements: &[PositionedElement]) -> Result<()> {
2096 fn finite_point(point: Point) -> bool {
2097 point.x.is_finite() && point.y.is_finite()
2098 }
2099
2100 for element in elements {
2101 match element {
2102 PositionedElement::Path(path) => {
2103 for command in &path.path.commands {
2104 let finite = match command {
2105 PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
2106 finite_point(*point)
2107 }
2108 PathCommand::CurveTo { c1, c2, to } => {
2109 finite_point(*c1) && finite_point(*c2) && finite_point(*to)
2110 }
2111 PathCommand::Close => true,
2112 };
2113 if !finite {
2114 return Err(ChartError::InvalidValue {
2115 element: "chart geometry coordinate".to_owned(),
2116 value: "generated path contains a nonfinite point".to_owned(),
2117 });
2118 }
2119 }
2120 }
2121 PositionedElement::Group(group) => validate_geometry_coordinates(&group.children)?,
2122 _ => {}
2123 }
2124 }
2125 Ok(())
2126}
2127
2128fn render_plot_geometry(
2129 plot: &Plot,
2130 bounds: Rect,
2131 blanks: DispBlanksAs,
2132 options: GeometryOptions,
2133 series_colours: &[Color],
2134) -> Result<Vec<PositionedElement>> {
2135 match plot {
2136 Plot::Bar {
2137 direction,
2138 grouping,
2139 gap_width,
2140 overlap,
2141 series,
2142 ..
2143 } => render_bar_geometry(
2144 *direction,
2145 *grouping,
2146 *gap_width,
2147 *overlap,
2148 series,
2149 bounds,
2150 options,
2151 series_colours,
2152 ),
2153 Plot::Line {
2154 grouping,
2155 marker,
2156 series,
2157 ..
2158 } => render_line_geometry(
2159 *grouping,
2160 *marker,
2161 series,
2162 bounds,
2163 blanks,
2164 options,
2165 series_colours,
2166 ),
2167 Plot::Pie {
2168 first_slice_angle,
2169 series,
2170 ..
2171 } => render_pie_geometry(*first_slice_angle, None, series, bounds, series_colours),
2172 Plot::Doughnut {
2173 first_slice_angle,
2174 hole_size,
2175 series,
2176 ..
2177 } => render_pie_geometry(
2178 *first_slice_angle,
2179 Some(*hole_size),
2180 series,
2181 bounds,
2182 series_colours,
2183 ),
2184 Plot::Area {
2185 grouping, series, ..
2186 } => render_area_geometry(*grouping, series, bounds, blanks, options, series_colours),
2187 Plot::Scatter { style, series, .. } => {
2188 render_scatter_geometry(*style, series, bounds, blanks, options, series_colours)
2189 }
2190 Plot::Radar { style, series, .. } => {
2191 render_radar_geometry(*style, series, bounds, options, series_colours)
2192 }
2193 }
2194}
2195
2196#[allow(clippy::too_many_arguments)]
2197fn render_bar_geometry(
2198 direction: BarDirection,
2199 grouping: BarGrouping,
2200 gap_width: u16,
2201 overlap: i8,
2202 series: &[Series],
2203 bounds: Rect,
2204 options: GeometryOptions,
2205 series_colours: &[Color],
2206) -> Result<Vec<PositionedElement>> {
2207 let bars = bar_rectangles(
2208 direction, grouping, gap_width, overlap, series, bounds, options,
2209 )?;
2210 bars.into_iter()
2211 .map(|bar| {
2212 Ok(filled_path(
2213 Path::rect(bar.rect),
2214 series_colour(series_colours, bar.series_index)?,
2215 ))
2216 })
2217 .collect()
2218}
2219
2220#[derive(Clone, Copy, Debug)]
2221struct BarRectangle {
2222 series_index: usize,
2223 logical_index: usize,
2224 rect: Rect,
2225 base: Point,
2226 end: Point,
2227}
2228
2229fn bar_rectangles(
2230 direction: BarDirection,
2231 grouping: BarGrouping,
2232 gap_width: u16,
2233 overlap: i8,
2234 series: &[Series],
2235 bounds: Rect,
2236 options: GeometryOptions,
2237) -> Result<Vec<BarRectangle>> {
2238 validate_series_values(series)?;
2239 let logical_series = logical_series_values(series)?;
2240 let category_count = series_category_count(series)?;
2241 let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
2242 let percent = grouping == BarGrouping::PercentStacked;
2243 let layers = stacked_series_bounds(&logical_series, stacked, percent)?;
2244 let domain = options
2245 .value
2246 .map(|mapping| mapping.domain)
2247 .unwrap_or(domain_from_layers(&layers)?);
2248 let value_orientation = options
2249 .value
2250 .map(|mapping| mapping.orientation)
2251 .unwrap_or(Orientation::MinMax);
2252 let category_extent = if direction == BarDirection::Column {
2253 bounds.width
2254 } else {
2255 bounds.height
2256 };
2257 let slot = category_extent / category_count as f64;
2258 let cluster_extent = slot * 100.0 / (100.0 + f64::from(gap_width));
2259 let bars_per_cluster = if stacked { 1 } else { series.len() };
2260 let overlap_fraction = if stacked {
2261 0.0
2262 } else {
2263 f64::from(overlap) / 100.0
2264 };
2265 let divisor =
2266 bars_per_cluster as f64 - (bars_per_cluster.saturating_sub(1)) as f64 * overlap_fraction;
2267 let bar_extent = cluster_extent / divisor;
2268 let advance = bar_extent * (1.0 - overlap_fraction);
2269 let mut bars = Vec::new();
2270 for (series_index, layer) in layers.iter().enumerate() {
2271 for &(logical_index, start, end) in layer {
2272 let category = oriented_category_index(
2273 logical_index,
2274 category_count,
2275 options.category_orientation,
2276 );
2277 let cluster_start = category as f64 / category_count as f64 * category_extent
2278 + (slot - cluster_extent) / 2.0;
2279 let category_start = if stacked {
2280 cluster_start
2281 } else {
2282 cluster_start + series_index as f64 * advance
2283 };
2284 let (rect, base, end) = if direction == BarDirection::Column {
2285 let y0 = map_y_oriented(start, domain, bounds, value_orientation)?;
2286 let y1 = map_y_oriented(end, domain, bounds, value_orientation)?;
2287 let center_x = bounds.x + category_start + bar_extent / 2.0;
2288 (
2289 Rect {
2290 x: bounds.x + category_start,
2291 y: y0.min(y1),
2292 width: bar_extent,
2293 height: (y1 - y0).abs(),
2294 },
2295 Point { x: center_x, y: y0 },
2296 Point { x: center_x, y: y1 },
2297 )
2298 } else {
2299 let x0 = map_x_oriented(start, domain, bounds, value_orientation)?;
2300 let x1 = map_x_oriented(end, domain, bounds, value_orientation)?;
2301 let center_y = bounds.y + category_start + bar_extent / 2.0;
2302 (
2303 Rect {
2304 x: x0.min(x1),
2305 y: bounds.y + category_start,
2306 width: (x1 - x0).abs(),
2307 height: bar_extent,
2308 },
2309 Point { x: x0, y: center_y },
2310 Point { x: x1, y: center_y },
2311 )
2312 };
2313 bars.push(BarRectangle {
2314 series_index,
2315 logical_index,
2316 rect,
2317 base,
2318 end,
2319 });
2320 }
2321 }
2322 Ok(bars)
2323}
2324
2325fn render_line_geometry(
2326 grouping: Grouping,
2327 marker: bool,
2328 series: &[Series],
2329 bounds: Rect,
2330 blanks: DispBlanksAs,
2331 options: GeometryOptions,
2332 series_colours: &[Color],
2333) -> Result<Vec<PositionedElement>> {
2334 validate_series_values(series)?;
2335 let logical_series = logical_series_values(series)?;
2336 let category_count = series_category_count(series)?;
2337 let calculated_series = if blanks == DispBlanksAs::Zero {
2338 zero_filled_series(&logical_series, category_count)
2339 } else {
2340 logical_series.clone()
2341 };
2342 let stacked = grouping != Grouping::Standard;
2343 let percent = grouping == Grouping::PercentStacked;
2344 let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
2345 let domain = options
2346 .value
2347 .map(|mapping| mapping.domain)
2348 .unwrap_or(domain_from_layers(&layers)?);
2349 let value_orientation = options
2350 .value
2351 .map(|mapping| mapping.orientation)
2352 .unwrap_or(Orientation::MinMax);
2353 let mut elements = Vec::new();
2354 for (series_index, layer) in layers.iter().enumerate() {
2355 let colour = series_colour(series_colours, series_index)?;
2356 let indexed: Vec<_> = layer
2357 .iter()
2358 .map(|(index, _, upper)| (*index, *upper))
2359 .collect();
2360 let indexes: Vec<_> = indexed.iter().map(|(index, _)| *index).collect();
2361 for (start, end) in contiguous_ranges(&indexes, blanks) {
2362 let points = category_points(
2363 &indexed[start..end],
2364 category_count,
2365 domain,
2366 bounds,
2367 options.category_orientation,
2368 value_orientation,
2369 )?;
2370 elements.push(stroked_path(polyline_path(&points, false), colour));
2371 }
2372 if marker {
2373 let present: BTreeSet<_> = logical_series[series_index]
2374 .1
2375 .iter()
2376 .map(|(index, _)| *index)
2377 .collect();
2378 let marker_values: Vec<_> = indexed
2379 .iter()
2380 .copied()
2381 .filter(|(index, _)| present.contains(index))
2382 .collect();
2383 let marker_points = category_points(
2384 &marker_values,
2385 category_count,
2386 domain,
2387 bounds,
2388 options.category_orientation,
2389 value_orientation,
2390 )?;
2391 push_markers(&mut elements, &marker_points, colour);
2392 }
2393 }
2394 Ok(elements)
2395}
2396
2397fn render_area_geometry(
2398 grouping: Grouping,
2399 series: &[Series],
2400 bounds: Rect,
2401 blanks: DispBlanksAs,
2402 options: GeometryOptions,
2403 series_colours: &[Color],
2404) -> Result<Vec<PositionedElement>> {
2405 validate_series_values(series)?;
2406 let logical_series = logical_series_values(series)?;
2407 let category_count = series_category_count(series)?;
2408 let calculated_series = if blanks == DispBlanksAs::Zero {
2409 zero_filled_series(&logical_series, category_count)
2410 } else {
2411 logical_series
2412 };
2413 let stacked = grouping != Grouping::Standard;
2414 let percent = grouping == Grouping::PercentStacked;
2415 let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
2416 let domain = options
2417 .value
2418 .map(|mapping| mapping.domain)
2419 .unwrap_or(domain_from_layers(&layers)?);
2420 let value_orientation = options
2421 .value
2422 .map(|mapping| mapping.orientation)
2423 .unwrap_or(Orientation::MinMax);
2424 let mut elements = Vec::new();
2425 for (series_index, layer) in layers.iter().enumerate() {
2426 let colour = series_colour(series_colours, series_index)?;
2427 let indexes: Vec<_> = layer.iter().map(|(index, _, _)| *index).collect();
2428 for (start, end) in contiguous_ranges(&indexes, blanks) {
2429 let segment = &layer[start..end];
2430 let top_values: Vec<_> = segment
2431 .iter()
2432 .map(|(index, _, upper)| (*index, *upper))
2433 .collect();
2434 let lower_values: Vec<_> = segment
2435 .iter()
2436 .map(|(index, lower, _)| (*index, *lower))
2437 .collect();
2438 let top = category_points(
2439 &top_values,
2440 category_count,
2441 domain,
2442 bounds,
2443 options.category_orientation,
2444 value_orientation,
2445 )?;
2446 let mut bottom = category_points(
2447 &lower_values,
2448 category_count,
2449 domain,
2450 bounds,
2451 options.category_orientation,
2452 value_orientation,
2453 )?;
2454 bottom.reverse();
2455 let mut commands = Vec::with_capacity(top.len() + bottom.len() + 2);
2456 commands.push(PathCommand::MoveTo(top[0]));
2457 commands.extend(top.iter().skip(1).copied().map(PathCommand::LineTo));
2458 commands.extend(bottom.into_iter().map(PathCommand::LineTo));
2459 commands.push(PathCommand::Close);
2460 elements.push(filled_and_stroked_path(
2461 Path {
2462 commands,
2463 fill_rule: FillRule::NonZero,
2464 },
2465 colour,
2466 ));
2467 }
2468 }
2469 Ok(elements)
2470}
2471
2472fn render_scatter_geometry(
2473 style: ScatterStyle,
2474 series: &[Series],
2475 bounds: Rect,
2476 blanks: DispBlanksAs,
2477 options: GeometryOptions,
2478 series_colours: &[Color],
2479) -> Result<Vec<PositionedElement>> {
2480 validate_series_values(series)?;
2481 let mut paired_series = Vec::with_capacity(series.len());
2482 let mut marker_series = Vec::with_capacity(series.len());
2483 let mut all_x = Vec::new();
2484 let mut all_y = Vec::new();
2485 for item in series {
2486 let markers = matched_scatter_points(item)?;
2487 let paired = scatter_render_points(item, blanks)?;
2488 all_x.extend(paired.iter().map(|(_, x, _)| *x));
2489 all_y.extend(paired.iter().map(|(_, _, y)| *y));
2490 paired_series.push(paired);
2491 marker_series.push(markers);
2492 }
2493 let x_domain = options
2494 .scatter_x
2495 .map(|mapping| mapping.domain)
2496 .unwrap_or(data_domain(&all_x, false)?);
2497 let y_domain = options
2498 .scatter_y
2499 .map(|mapping| mapping.domain)
2500 .unwrap_or(data_domain(&all_y, false)?);
2501 let x_orientation = options
2502 .scatter_x
2503 .map(|mapping| mapping.orientation)
2504 .unwrap_or(Orientation::MinMax);
2505 let y_orientation = options
2506 .scatter_y
2507 .map(|mapping| mapping.orientation)
2508 .unwrap_or(Orientation::MinMax);
2509 let draw_line = matches!(
2510 style,
2511 ScatterStyle::Line
2512 | ScatterStyle::LineMarker
2513 | ScatterStyle::Smooth
2514 | ScatterStyle::SmoothMarker
2515 );
2516 let draw_marker = matches!(
2517 style,
2518 ScatterStyle::Marker | ScatterStyle::LineMarker | ScatterStyle::SmoothMarker
2519 );
2520 let mut elements = Vec::new();
2521 for (series_index, paired) in paired_series.iter().enumerate() {
2522 let colour = series_colour(series_colours, series_index)?;
2523 if draw_line {
2524 let indexes: Vec<_> = paired.iter().map(|(index, _, _)| *index).collect();
2525 for (start, end) in contiguous_ranges(&indexes, blanks) {
2526 let points = scatter_points(
2527 &paired[start..end],
2528 x_domain,
2529 y_domain,
2530 bounds,
2531 x_orientation,
2532 y_orientation,
2533 )?;
2534 elements.push(stroked_path(polyline_path(&points, false), colour));
2535 }
2536 }
2537 if draw_marker {
2538 let marker_points = scatter_points(
2539 &marker_series[series_index],
2540 x_domain,
2541 y_domain,
2542 bounds,
2543 x_orientation,
2544 y_orientation,
2545 )?;
2546 push_markers(&mut elements, &marker_points, colour);
2547 }
2548 }
2549 Ok(elements)
2550}
2551
2552type ScatterPoint = (usize, f64, f64);
2553
2554struct ScatterPointMaps {
2555 x_count: usize,
2556 y_count: usize,
2557 x_by_index: BTreeMap<usize, f64>,
2558 y_by_index: BTreeMap<usize, f64>,
2559}
2560
2561fn scatter_point_maps(series: &Series) -> Result<ScatterPointMaps> {
2562 let Some(AxisData::Numeric(x_values)) = &series.categories else {
2563 return Err(ChartError::InvalidValue {
2564 element: "c:xVal".to_owned(),
2565 value: "scatter series requires numeric cached x values".to_owned(),
2566 });
2567 };
2568 validate_finite(&x_values.values, "c:xVal/c:numCache")?;
2569 let (x_count, x_points) = logical_numeric_values(x_values)?;
2570 let (y_count, y_points) = logical_numeric_values(&series.values)?;
2571 Ok(ScatterPointMaps {
2572 x_count,
2573 y_count,
2574 x_by_index: x_points.into_iter().collect(),
2575 y_by_index: y_points.into_iter().collect(),
2576 })
2577}
2578
2579fn matched_scatter_points(series: &Series) -> Result<Vec<ScatterPoint>> {
2580 let points = scatter_point_maps(series)?;
2581 Ok(points
2582 .y_by_index
2583 .iter()
2584 .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
2585 .collect())
2586}
2587
2588fn scatter_render_points(series: &Series, blanks: DispBlanksAs) -> Result<Vec<ScatterPoint>> {
2589 let points = scatter_point_maps(series)?;
2590 if blanks != DispBlanksAs::Zero {
2591 return Ok(points
2592 .y_by_index
2593 .iter()
2594 .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
2595 .collect());
2596 }
2597 let count = points.x_count.max(points.y_count);
2598 let present = points
2599 .x_by_index
2600 .keys()
2601 .chain(points.y_by_index.keys())
2602 .copied()
2603 .collect();
2604 Ok(zero_control_indexes(count, &present)
2605 .into_iter()
2606 .map(|index| {
2607 (
2608 index,
2609 points.x_by_index.get(&index).copied().unwrap_or(0.0),
2610 points.y_by_index.get(&index).copied().unwrap_or(0.0),
2611 )
2612 })
2613 .collect())
2614}
2615
2616fn scatter_points(
2617 values: &[(usize, f64, f64)],
2618 x_domain: Domain,
2619 y_domain: Domain,
2620 bounds: Rect,
2621 x_orientation: Orientation,
2622 y_orientation: Orientation,
2623) -> Result<Vec<Point>> {
2624 values
2625 .iter()
2626 .map(|(_, x, y)| {
2627 Ok(Point {
2628 x: map_x_oriented(*x, x_domain, bounds, x_orientation)?,
2629 y: map_y_oriented(*y, y_domain, bounds, y_orientation)?,
2630 })
2631 })
2632 .collect()
2633}
2634
2635fn render_radar_geometry(
2636 style: RadarStyle,
2637 series: &[Series],
2638 bounds: Rect,
2639 options: GeometryOptions,
2640 series_colours: &[Color],
2641) -> Result<Vec<PositionedElement>> {
2642 let point_series = radar_series_points(series, bounds, options)?;
2643 let mut elements = Vec::new();
2644 for (series_index, points) in point_series.iter().enumerate() {
2645 let colour = series_colour(series_colours, series_index)?;
2646 let plain_points: Vec<_> = points.iter().map(|(_, point)| *point).collect();
2647 if plain_points.is_empty() {
2648 continue;
2649 }
2650 let path = polyline_path(&plain_points, true);
2651 if style == RadarStyle::Filled {
2652 elements.push(filled_and_stroked_path(path, colour));
2653 } else {
2654 elements.push(stroked_path(path, colour));
2655 }
2656 if style == RadarStyle::Marker {
2657 push_markers(&mut elements, &plain_points, colour);
2658 }
2659 }
2660 Ok(elements)
2661}
2662
2663fn radar_series_points(
2664 series: &[Series],
2665 bounds: Rect,
2666 options: GeometryOptions,
2667) -> Result<Vec<Vec<(usize, Point)>>> {
2668 validate_series_values(series)?;
2669 let logical_series = logical_series_values(series)?;
2670 let category_count = series_category_count(series)?;
2671 let values = logical_series
2672 .iter()
2673 .flat_map(|(_, points)| points.iter().map(|(_, value)| *value))
2674 .collect::<Vec<_>>();
2675 let mapping = options.value.unwrap_or(AxisMapping {
2676 domain: data_domain(&values, true)?,
2677 orientation: Orientation::MinMax,
2678 });
2679 let center = Point {
2680 x: bounds.x + bounds.width / 2.0,
2681 y: bounds.y + bounds.height / 2.0,
2682 };
2683 let radius = bounds.width.min(bounds.height) / 2.0;
2684 let mut point_series = Vec::with_capacity(logical_series.len());
2685 for (_, logical) in &logical_series {
2686 let mut points = Vec::with_capacity(logical.len());
2687 for (category, value) in logical {
2688 if *value < mapping.domain.min || *value > mapping.domain.max {
2689 continue;
2690 }
2691 points.push((
2692 *category,
2693 radial_point(
2694 center,
2695 normalized_value_oriented(*value, mapping.domain, mapping.orientation)?
2696 * radius,
2697 oriented_category_index(
2698 *category,
2699 category_count,
2700 options.category_orientation,
2701 ),
2702 category_count,
2703 ),
2704 ));
2705 }
2706 point_series.push(points);
2707 }
2708 Ok(point_series)
2709}
2710
2711fn render_pie_geometry(
2712 first_slice_angle: u16,
2713 hole_size: Option<u8>,
2714 series: &[Series],
2715 bounds: Rect,
2716 series_colours: &[Color],
2717) -> Result<Vec<PositionedElement>> {
2718 pie_slices(first_slice_angle, hole_size, series, bounds)?
2719 .into_iter()
2720 .map(|slice| {
2721 Ok(filled_path(
2722 slice.path,
2723 series_colour(series_colours, slice.series_index)?,
2724 ))
2725 })
2726 .collect()
2727}
2728
2729struct PieSliceGeometry {
2730 series_index: usize,
2731 logical_index: usize,
2732 path: Path,
2733 anchor: Point,
2734 base: Point,
2735 end: Point,
2736}
2737
2738fn pie_slices(
2739 first_slice_angle: u16,
2740 hole_size: Option<u8>,
2741 series: &[Series],
2742 bounds: Rect,
2743) -> Result<Vec<PieSliceGeometry>> {
2744 validate_series_values(series)?;
2745 let center = Point {
2746 x: bounds.x + bounds.width / 2.0,
2747 y: bounds.y + bounds.height / 2.0,
2748 };
2749 let radius = bounds.width.min(bounds.height) / 2.0;
2750 let mut elements = Vec::new();
2751 for (series_index, item) in series.iter().enumerate() {
2752 let (_, logical) = logical_numeric_values(&item.values)?;
2753 let total = logical
2754 .iter()
2755 .map(|(_, value)| *value)
2756 .filter(|value| *value > 0.0)
2757 .try_fold(0.0, |total, value| {
2758 checked_geometry_sum(total, value, "pie value total")
2759 })?;
2760 if total <= 0.0 {
2761 continue;
2762 }
2763 let mut angle = f64::from(first_slice_angle).to_radians() - std::f64::consts::FRAC_PI_2;
2764 for (logical_index, value) in logical {
2765 if value <= 0.0 {
2766 continue;
2767 }
2768 let next = angle + value / total * std::f64::consts::TAU;
2769 let path = if let Some(hole_size) = hole_size {
2770 doughnut_wedge(
2771 center,
2772 radius,
2773 radius * f64::from(hole_size) / 100.0,
2774 angle,
2775 next,
2776 )
2777 } else {
2778 pie_wedge(center, radius, angle, next)
2779 };
2780 let inner_radius = hole_size
2781 .map(|hole| radius * f64::from(hole) / 100.0)
2782 .unwrap_or(0.0);
2783 let anchor_radius = hole_size
2784 .map(|_| (radius + inner_radius) / 2.0)
2785 .unwrap_or(radius * 2.0 / 3.0);
2786 let midpoint = (angle + next) / 2.0;
2787 elements.push(PieSliceGeometry {
2788 series_index,
2789 logical_index,
2790 path,
2791 anchor: circle_point(center, anchor_radius, midpoint),
2792 base: circle_point(center, inner_radius, midpoint),
2793 end: circle_point(center, radius, midpoint),
2794 });
2795 angle = next;
2796 }
2797 }
2798 Ok(elements)
2799}
2800
2801fn validate_series_values(series: &[Series]) -> Result<()> {
2802 if series.is_empty() {
2803 return Err(ChartError::MissingElement("c:ser".to_owned()));
2804 }
2805 for item in series {
2806 validate_finite(&item.values.values, "c:val/c:numCache")?;
2807 }
2808 Ok(())
2809}
2810
2811fn validate_finite(values: &[f64], element: &str) -> Result<()> {
2812 if let Some(value) = values.iter().find(|value| !value.is_finite()) {
2813 return Err(ChartError::InvalidValue {
2814 element: element.to_owned(),
2815 value: value.to_string(),
2816 });
2817 }
2818 Ok(())
2819}
2820
2821type IndexedValues = Vec<(usize, f64)>;
2822type LogicalSeries = (usize, IndexedValues);
2823type GeometryLayer = Vec<(usize, f64, f64)>;
2824
2825fn zero_control_indexes(count: usize, present: &BTreeSet<usize>) -> BTreeSet<usize> {
2826 let mut controls = present.clone();
2827 if count == 0 {
2828 return controls;
2829 }
2830 controls.insert(0);
2831 controls.insert(count - 1);
2832 for index in present {
2833 if *index > 0 {
2834 controls.insert(*index - 1);
2835 }
2836 if index + 1 < count {
2837 controls.insert(index + 1);
2838 }
2839 }
2840 controls
2841}
2842
2843fn zero_filled_series(series: &[LogicalSeries], count: usize) -> Vec<LogicalSeries> {
2844 let present = series
2845 .iter()
2846 .flat_map(|(_, points)| points.iter().map(|(index, _)| *index))
2847 .collect();
2848 let controls = zero_control_indexes(count, &present);
2849 series
2850 .iter()
2851 .map(|(declared, points)| {
2852 let values: BTreeMap<_, _> = points.iter().copied().collect();
2853 let filled = controls
2854 .iter()
2855 .map(|index| (*index, values.get(index).copied().unwrap_or(0.0)))
2856 .collect();
2857 (*declared, filled)
2858 })
2859 .collect()
2860}
2861
2862fn contiguous_ranges(indexes: &[usize], blanks: DispBlanksAs) -> Vec<(usize, usize)> {
2863 if indexes.is_empty() {
2864 return Vec::new();
2865 }
2866 if blanks != DispBlanksAs::Gap {
2867 return vec![(0, indexes.len())];
2868 }
2869 let mut ranges = Vec::new();
2870 let mut start = 0usize;
2871 for position in 1..indexes.len() {
2872 if indexes[position - 1].checked_add(1) != Some(indexes[position]) {
2873 ranges.push((start, position));
2874 start = position;
2875 }
2876 }
2877 ranges.push((start, indexes.len()));
2878 ranges
2879}
2880
2881fn logical_numeric_values(data: &NumericData) -> Result<LogicalSeries> {
2882 validate_finite(&data.values, "c:numCache")?;
2883 let (declared, indexes) = cache_layout(&data.markup, data.values.len())?;
2884 let declared = usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
2885 element: "c:ptCount".to_owned(),
2886 value: declared.to_string(),
2887 })?;
2888 let points = indexes
2889 .into_iter()
2890 .zip(data.values.iter().copied())
2891 .map(|(index, value)| {
2892 usize::try_from(index)
2893 .map(|index| (index, value))
2894 .map_err(|_| ChartError::InvalidValue {
2895 element: "c:pt/@idx".to_owned(),
2896 value: index.to_string(),
2897 })
2898 })
2899 .collect::<Result<Vec<_>>>()?;
2900 Ok((declared, points))
2901}
2902
2903fn logical_series_values(series: &[Series]) -> Result<Vec<LogicalSeries>> {
2904 series
2905 .iter()
2906 .map(|item| logical_numeric_values(&item.values))
2907 .collect()
2908}
2909
2910fn reference_logical_count(markup: &ReferenceMarkup, value_count: usize) -> Result<usize> {
2911 let (declared, _) = cache_layout(markup, value_count)?;
2912 usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
2913 element: "c:ptCount".to_owned(),
2914 value: declared.to_string(),
2915 })
2916}
2917
2918fn series_category_count(series: &[Series]) -> Result<usize> {
2919 let mut count = 0usize;
2920 for item in series {
2921 count = count.max(reference_logical_count(
2922 &item.values.markup,
2923 item.values.values.len(),
2924 )?);
2925 if let Some(categories) = &item.categories {
2926 let category_count = match categories {
2927 AxisData::String(data) => reference_logical_count(&data.markup, data.values.len())?,
2928 AxisData::Numeric(data) => {
2929 reference_logical_count(&data.markup, data.values.len())?
2930 }
2931 };
2932 count = count.max(category_count);
2933 }
2934 }
2935 if count == 0 {
2936 return Err(ChartError::InvalidValue {
2937 element: "c:ser".to_owned(),
2938 value: "cached series contain no points".to_owned(),
2939 });
2940 }
2941 Ok(count)
2942}
2943
2944fn stacked_series_bounds(
2945 series: &[LogicalSeries],
2946 stacked: bool,
2947 percent: bool,
2948) -> Result<Vec<GeometryLayer>> {
2949 let mut positive_totals = BTreeMap::<usize, f64>::new();
2950 let mut negative_totals = BTreeMap::<usize, f64>::new();
2951 if percent {
2952 for (_, points) in series {
2953 for &(index, value) in points {
2954 let totals = if value >= 0.0 {
2955 &mut positive_totals
2956 } else {
2957 &mut negative_totals
2958 };
2959 let total = totals.entry(index).or_default();
2960 *total = checked_geometry_sum(*total, value.abs(), "stacked percentage total")?;
2961 }
2962 }
2963 }
2964 let mut positive = BTreeMap::<usize, f64>::new();
2965 let mut negative = BTreeMap::<usize, f64>::new();
2966 let mut layers = Vec::with_capacity(series.len());
2967 for (_, points) in series {
2968 let mut layer = Vec::with_capacity(points.len());
2969 for &(index, original) in points {
2970 let total = if original >= 0.0 {
2971 positive_totals.get(&index).copied().unwrap_or(0.0)
2972 } else {
2973 negative_totals.get(&index).copied().unwrap_or(0.0)
2974 };
2975 let value = if percent && total != 0.0 {
2976 original / total
2977 } else {
2978 original
2979 };
2980 if stacked {
2981 let accumulators = if value >= 0.0 {
2982 &mut positive
2983 } else {
2984 &mut negative
2985 };
2986 let accumulator = accumulators.entry(index).or_default();
2987 let lower = *accumulator;
2988 *accumulator = checked_geometry_sum(*accumulator, value, "stacked value total")?;
2989 layer.push((index, lower, *accumulator));
2990 } else {
2991 layer.push((index, 0.0, value));
2992 }
2993 }
2994 layers.push(layer);
2995 }
2996 Ok(layers)
2997}
2998
2999fn checked_geometry_sum(current: f64, value: f64, context: &str) -> Result<f64> {
3000 let sum = current + value;
3001 if sum.is_finite() {
3002 Ok(sum)
3003 } else {
3004 Err(ChartError::InvalidValue {
3005 element: "chart geometry aggregate".to_owned(),
3006 value: format!("{context} is nonfinite"),
3007 })
3008 }
3009}
3010
3011#[derive(Clone, Copy, Debug, PartialEq)]
3012struct Domain {
3013 min: f64,
3014 max: f64,
3015}
3016
3017fn nice_number_scale(
3018 extent: Domain,
3019 pinned_minimum: Option<f64>,
3020 pinned_maximum: Option<f64>,
3021 target_tick_count: usize,
3022) -> Result<AxisScale> {
3023 if target_tick_count < 2
3024 || !extent.min.is_finite()
3025 || !extent.max.is_finite()
3026 || pinned_minimum.is_some_and(|value| !value.is_finite())
3027 || pinned_maximum.is_some_and(|value| !value.is_finite())
3028 {
3029 return Err(ChartError::InvalidValue {
3030 element: "chart axis scale".to_owned(),
3031 value: "scale bounds and target tick count must be finite and valid".to_owned(),
3032 });
3033 }
3034
3035 let mut data_min = extent.min.min(extent.max);
3036 let mut data_max = extent.min.max(extent.max);
3037 if data_min == data_max {
3038 let expanded = expand_constant_domain(data_min)?;
3039 data_min = expanded.min;
3040 data_max = expanded.max;
3041 }
3042 let requested_min = match (pinned_minimum, pinned_maximum) {
3043 (Some(minimum), _) => minimum,
3044 (None, Some(maximum)) if data_min >= maximum => {
3045 extend_bound(maximum, stable_positive_step(data_min, data_max, 3)?, false)?
3046 }
3047 (None, _) => data_min,
3048 };
3049 let requested_max = match (pinned_minimum, pinned_maximum) {
3050 (_, Some(maximum)) => maximum,
3051 (Some(minimum), None) if data_max <= minimum => {
3052 extend_bound(minimum, stable_positive_step(data_min, data_max, 3)?, true)?
3053 }
3054 (_, None) => data_max,
3055 };
3056 if requested_min >= requested_max {
3057 return Err(ChartError::InvalidValue {
3058 element: "c:scaling".to_owned(),
3059 value: format!("minimum {requested_min} is not less than maximum {requested_max}"),
3060 });
3061 }
3062
3063 let raw_step = stable_positive_step(requested_min, requested_max, target_tick_count)?;
3064 let step = nice_step(raw_step)?;
3065 let rounded_minimum = (requested_min / step).floor() * step;
3066 let rounded_maximum = (requested_max / step).ceil() * step;
3067 let lower_fallback = pinned_minimum.is_none() && !rounded_minimum.is_finite();
3068 let upper_fallback = pinned_maximum.is_none() && !rounded_maximum.is_finite();
3069 let minimum = pinned_minimum.unwrap_or(if rounded_minimum.is_finite() {
3070 rounded_minimum
3071 } else {
3072 requested_min
3073 });
3074 let maximum = pinned_maximum.unwrap_or(if rounded_maximum.is_finite() {
3075 rounded_maximum
3076 } else {
3077 requested_max
3078 });
3079 if !minimum.is_finite() || !maximum.is_finite() || minimum >= maximum {
3080 return Err(ChartError::InvalidValue {
3081 element: "chart axis scale".to_owned(),
3082 value: "nice-number bounds are nonfinite or empty".to_owned(),
3083 });
3084 }
3085
3086 let first = (minimum / step).ceil();
3087 let last = (maximum / step).floor();
3088 let tick_count = if last >= first {
3089 (last - first).min(1024.0) as usize + 1
3090 } else {
3091 0
3092 };
3093 let mut ticks = Vec::with_capacity(tick_count);
3094 for index in 0..tick_count {
3095 let tick = (first + index as f64) * step;
3096 if tick.is_finite() && tick >= minimum && tick <= maximum {
3097 ticks.push(clean_tick(tick, step));
3098 }
3099 }
3100 if lower_fallback && ticks.first().is_none_or(|tick| *tick > minimum) {
3101 ticks.insert(0, minimum);
3102 }
3103 if upper_fallback && ticks.last().is_none_or(|tick| *tick < maximum) {
3104 ticks.push(maximum);
3105 }
3106 if ticks.len() < 2 {
3107 ticks.extend([minimum, maximum]);
3108 ticks.sort_by(f64::total_cmp);
3109 }
3110 ticks.dedup_by(|left, right| *left == *right);
3111 if ticks.len() < 2 || !ticks.windows(2).all(|pair| pair[0] < pair[1]) {
3112 return Err(ChartError::InvalidValue {
3113 element: "chart axis scale".to_owned(),
3114 value: "scale cannot produce two increasing finite ticks".to_owned(),
3115 });
3116 }
3117 Ok(AxisScale {
3118 domain: Domain {
3119 min: minimum,
3120 max: maximum,
3121 },
3122 ticks,
3123 })
3124}
3125
3126const MIN_POSITIVE_SUBNORMAL: f64 = f64::from_bits(1);
3127
3128fn stable_positive_step(minimum: f64, maximum: f64, target_tick_count: usize) -> Result<f64> {
3129 let scale = minimum.abs().max(maximum.abs());
3130 let scale = if scale == 0.0 {
3131 MIN_POSITIVE_SUBNORMAL
3132 } else {
3133 scale
3134 };
3135 let normalized =
3136 (maximum / scale - minimum / scale) / target_tick_count.saturating_sub(1) as f64;
3137 let step = normalized * scale;
3138 if normalized.is_finite() && normalized > 0.0 && step.is_finite() {
3139 Ok(step.max(MIN_POSITIVE_SUBNORMAL))
3140 } else {
3141 Err(ChartError::InvalidValue {
3142 element: "chart axis scale".to_owned(),
3143 value: "scale step is nonfinite or empty".to_owned(),
3144 })
3145 }
3146}
3147
3148fn nice_step(raw_step: f64) -> Result<f64> {
3149 if !raw_step.is_finite() || raw_step <= 0.0 {
3150 return Err(ChartError::InvalidValue {
3151 element: "chart axis scale".to_owned(),
3152 value: format!("invalid raw tick step {raw_step}"),
3153 });
3154 }
3155 let magnitude = 10.0_f64
3156 .powf(raw_step.log10().floor())
3157 .max(MIN_POSITIVE_SUBNORMAL);
3158 let fraction = raw_step / magnitude;
3159 let multiplier = if fraction <= 1.0 {
3160 1.0
3161 } else if fraction <= 2.0 {
3162 2.0
3163 } else if fraction <= 5.0 {
3164 5.0
3165 } else {
3166 10.0
3167 };
3168 let step = (multiplier * magnitude).max(MIN_POSITIVE_SUBNORMAL);
3169 if step.is_finite() && step > 0.0 {
3170 Ok(step)
3171 } else {
3172 Err(ChartError::InvalidValue {
3173 element: "chart axis scale".to_owned(),
3174 value: "nice-number step is nonfinite".to_owned(),
3175 })
3176 }
3177}
3178
3179fn expand_constant_domain(value: f64) -> Result<Domain> {
3180 if value == 0.0 {
3181 return Ok(Domain {
3182 min: -1.0,
3183 max: 1.0,
3184 });
3185 }
3186 let padding = (value.abs() * 0.5).max(MIN_POSITIVE_SUBNORMAL);
3187 let minimum = value - padding;
3188 let maximum = value + padding;
3189 if minimum.is_finite() && maximum.is_finite() && minimum < maximum {
3190 return Ok(Domain {
3191 min: minimum,
3192 max: maximum,
3193 });
3194 }
3195 let domain = if value.is_sign_positive() {
3196 Domain {
3197 min: 0.0,
3198 max: value,
3199 }
3200 } else {
3201 Domain {
3202 min: value,
3203 max: 0.0,
3204 }
3205 };
3206 if domain.min < domain.max {
3207 Ok(domain)
3208 } else {
3209 Err(ChartError::InvalidValue {
3210 element: "chart axis scale".to_owned(),
3211 value: format!("constant value {value} has no finite renderable extent"),
3212 })
3213 }
3214}
3215
3216fn extend_bound(bound: f64, span: f64, positive: bool) -> Result<f64> {
3217 let candidate = if positive { bound + span } else { bound - span };
3218 let extended = if candidate.is_finite()
3219 && if positive {
3220 candidate > bound
3221 } else {
3222 candidate < bound
3223 } {
3224 candidate
3225 } else if positive {
3226 bound.next_up()
3227 } else {
3228 bound.next_down()
3229 };
3230 if extended.is_finite()
3231 && if positive {
3232 extended > bound
3233 } else {
3234 extended < bound
3235 }
3236 {
3237 Ok(extended)
3238 } else {
3239 Err(ChartError::InvalidValue {
3240 element: "c:scaling".to_owned(),
3241 value: format!("one-sided bound {bound} leaves no finite automatic extent"),
3242 })
3243 }
3244}
3245
3246fn clean_tick(value: f64, step: f64) -> f64 {
3247 if value.abs() <= step.abs() * 1.0e-12 {
3248 0.0
3249 } else {
3250 value
3251 }
3252}
3253
3254fn domain_from_layers(layers: &[GeometryLayer]) -> Result<Domain> {
3255 let values = layers
3256 .iter()
3257 .flatten()
3258 .flat_map(|(_, lower, upper)| [*lower, *upper]);
3259 let mut min = 0.0_f64;
3260 let mut max = 0.0_f64;
3261 for value in values {
3262 if !value.is_finite() {
3263 return Err(ChartError::InvalidValue {
3264 element: "chart geometry domain".to_owned(),
3265 value: "aggregate produced a nonfinite value".to_owned(),
3266 });
3267 }
3268 min = min.min(value);
3269 max = max.max(value);
3270 }
3271 if min == max {
3272 max = 1.0;
3273 }
3274 Ok(Domain { min, max })
3275}
3276
3277fn data_domain(values: &[f64], include_zero: bool) -> Result<Domain> {
3278 validate_finite(values, "chart geometry cache")?;
3279 let Some(first) = values.first().copied() else {
3280 return Err(ChartError::InvalidValue {
3281 element: "c:numCache".to_owned(),
3282 value: "cached series contain no points".to_owned(),
3283 });
3284 };
3285 let mut min = first;
3286 let mut max = first;
3287 for value in values.iter().copied() {
3288 min = min.min(value);
3289 max = max.max(value);
3290 }
3291 if include_zero {
3292 min = min.min(0.0);
3293 max = max.max(0.0);
3294 }
3295 if min == max {
3296 if !include_zero {
3297 return expand_constant_domain(min);
3298 }
3299 if min > 0.0 {
3300 min = 0.0;
3301 } else if max < 0.0 {
3302 max = 0.0;
3303 } else {
3304 max = 1.0;
3305 }
3306 }
3307 Ok(Domain { min, max })
3308}
3309
3310fn normalized_value(value: f64, domain: Domain) -> Result<f64> {
3311 let scale = value
3312 .abs()
3313 .max(domain.min.abs())
3314 .max(domain.max.abs())
3315 .max(1.0);
3316 let value = value / scale;
3317 let min = domain.min / scale;
3318 let max = domain.max / scale;
3319 let range = max - min;
3320 let normalized = (value - min) / range;
3321 if !range.is_finite() || range <= 0.0 || !normalized.is_finite() {
3322 return Err(ChartError::InvalidValue {
3323 element: "chart geometry domain".to_owned(),
3324 value: format!("cannot map finite domain {} to {}", domain.min, domain.max),
3325 });
3326 }
3327 Ok(normalized)
3328}
3329
3330fn normalized_value_oriented(value: f64, domain: Domain, orientation: Orientation) -> Result<f64> {
3331 let normalized = normalized_value(value, domain)?;
3332 Ok(if orientation == Orientation::MaxMin {
3333 1.0 - normalized
3334 } else {
3335 normalized
3336 })
3337}
3338
3339fn map_x_oriented(
3340 value: f64,
3341 domain: Domain,
3342 bounds: Rect,
3343 orientation: Orientation,
3344) -> Result<f64> {
3345 let coordinate =
3346 bounds.x + normalized_value_oriented(value, domain, orientation)? * bounds.width;
3347 if coordinate.is_finite() {
3348 Ok(coordinate)
3349 } else {
3350 Err(ChartError::InvalidValue {
3351 element: "chart geometry coordinate".to_owned(),
3352 value: "x coordinate is nonfinite".to_owned(),
3353 })
3354 }
3355}
3356
3357fn map_y_oriented(
3358 value: f64,
3359 domain: Domain,
3360 bounds: Rect,
3361 orientation: Orientation,
3362) -> Result<f64> {
3363 let coordinate = bounds.y + bounds.height
3364 - normalized_value_oriented(value, domain, orientation)? * bounds.height;
3365 if coordinate.is_finite() {
3366 Ok(coordinate)
3367 } else {
3368 Err(ChartError::InvalidValue {
3369 element: "chart geometry coordinate".to_owned(),
3370 value: "y coordinate is nonfinite".to_owned(),
3371 })
3372 }
3373}
3374
3375fn oriented_category_index(index: usize, count: usize, orientation: Orientation) -> usize {
3376 if orientation == Orientation::MaxMin && index < count {
3377 count - index - 1
3378 } else {
3379 index
3380 }
3381}
3382
3383fn category_points(
3384 values: &[(usize, f64)],
3385 count: usize,
3386 domain: Domain,
3387 bounds: Rect,
3388 category_orientation: Orientation,
3389 value_orientation: Orientation,
3390) -> Result<Vec<Point>> {
3391 values
3392 .iter()
3393 .map(|(index, value)| {
3394 let index = oriented_category_index(*index, count, category_orientation);
3395 let x = bounds.x + (index as f64 + 0.5) / count as f64 * bounds.width;
3396 let y = map_y_oriented(*value, domain, bounds, value_orientation)?;
3397 if !x.is_finite() {
3398 return Err(ChartError::InvalidValue {
3399 element: "chart geometry coordinate".to_owned(),
3400 value: "category x coordinate is nonfinite".to_owned(),
3401 });
3402 }
3403 Ok(Point { x, y })
3404 })
3405 .collect()
3406}
3407
3408fn polyline_path(points: &[Point], close: bool) -> Path {
3409 let mut commands = Vec::with_capacity(points.len() + usize::from(close));
3410 if let Some(first) = points.first() {
3411 commands.push(PathCommand::MoveTo(*first));
3412 commands.extend(points.iter().skip(1).copied().map(PathCommand::LineTo));
3413 if close {
3414 commands.push(PathCommand::Close);
3415 }
3416 }
3417 Path {
3418 commands,
3419 fill_rule: FillRule::NonZero,
3420 }
3421}
3422
3423fn pie_wedge(center: Point, radius: f64, start: f64, end: f64) -> Path {
3424 let first = circle_point(center, radius, start);
3425 let mut commands = vec![PathCommand::MoveTo(center), PathCommand::LineTo(first)];
3426 append_arc(&mut commands, center, radius, start, end);
3427 commands.push(PathCommand::Close);
3428 Path {
3429 commands,
3430 fill_rule: FillRule::NonZero,
3431 }
3432}
3433
3434fn doughnut_wedge(
3435 center: Point,
3436 outer_radius: f64,
3437 inner_radius: f64,
3438 start: f64,
3439 end: f64,
3440) -> Path {
3441 let outer_start = circle_point(center, outer_radius, start);
3442 let inner_end = circle_point(center, inner_radius, end);
3443 let mut commands = vec![PathCommand::MoveTo(outer_start)];
3444 append_arc(&mut commands, center, outer_radius, start, end);
3445 commands.push(PathCommand::LineTo(inner_end));
3446 append_arc(&mut commands, center, inner_radius, end, start);
3447 commands.push(PathCommand::Close);
3448 Path {
3449 commands,
3450 fill_rule: FillRule::NonZero,
3451 }
3452}
3453
3454fn append_arc(commands: &mut Vec<PathCommand>, center: Point, radius: f64, start: f64, end: f64) {
3455 let segments = ((end - start).abs() / std::f64::consts::FRAC_PI_2)
3456 .ceil()
3457 .max(1.0) as usize;
3458 let sweep = (end - start) / segments as f64;
3459 for segment in 0..segments {
3460 let angle = start + segment as f64 * sweep;
3461 let next = angle + sweep;
3462 let factor = 4.0 / 3.0 * (sweep / 4.0).tan();
3463 let from = circle_point(center, radius, angle);
3464 let to = circle_point(center, radius, next);
3465 commands.push(PathCommand::CurveTo {
3466 c1: Point {
3467 x: from.x - factor * radius * angle.sin(),
3468 y: from.y + factor * radius * angle.cos(),
3469 },
3470 c2: Point {
3471 x: to.x + factor * radius * next.sin(),
3472 y: to.y - factor * radius * next.cos(),
3473 },
3474 to,
3475 });
3476 }
3477}
3478
3479fn circle_point(center: Point, radius: f64, angle: f64) -> Point {
3480 Point {
3481 x: center.x + radius * angle.cos(),
3482 y: center.y + radius * angle.sin(),
3483 }
3484}
3485
3486fn radial_point(center: Point, radius: f64, index: usize, count: usize) -> Point {
3487 circle_point(
3488 center,
3489 radius,
3490 index as f64 * std::f64::consts::TAU / count as f64 - std::f64::consts::FRAC_PI_2,
3491 )
3492}
3493
3494fn marker_path(center: Point) -> Path {
3495 const RADIUS: f64 = 2.5;
3496 const KAPPA: f64 = 0.552_284_749_830_793_6;
3497 let control = RADIUS * KAPPA;
3498 Path {
3499 commands: vec![
3500 PathCommand::MoveTo(Point {
3501 x: center.x + RADIUS,
3502 y: center.y,
3503 }),
3504 PathCommand::CurveTo {
3505 c1: Point {
3506 x: center.x + RADIUS,
3507 y: center.y + control,
3508 },
3509 c2: Point {
3510 x: center.x + control,
3511 y: center.y + RADIUS,
3512 },
3513 to: Point {
3514 x: center.x,
3515 y: center.y + RADIUS,
3516 },
3517 },
3518 PathCommand::CurveTo {
3519 c1: Point {
3520 x: center.x - control,
3521 y: center.y + RADIUS,
3522 },
3523 c2: Point {
3524 x: center.x - RADIUS,
3525 y: center.y + control,
3526 },
3527 to: Point {
3528 x: center.x - RADIUS,
3529 y: center.y,
3530 },
3531 },
3532 PathCommand::CurveTo {
3533 c1: Point {
3534 x: center.x - RADIUS,
3535 y: center.y - control,
3536 },
3537 c2: Point {
3538 x: center.x - control,
3539 y: center.y - RADIUS,
3540 },
3541 to: Point {
3542 x: center.x,
3543 y: center.y - RADIUS,
3544 },
3545 },
3546 PathCommand::CurveTo {
3547 c1: Point {
3548 x: center.x + control,
3549 y: center.y - RADIUS,
3550 },
3551 c2: Point {
3552 x: center.x + RADIUS,
3553 y: center.y - control,
3554 },
3555 to: Point {
3556 x: center.x + RADIUS,
3557 y: center.y,
3558 },
3559 },
3560 PathCommand::Close,
3561 ],
3562 fill_rule: FillRule::NonZero,
3563 }
3564}
3565
3566fn push_markers(elements: &mut Vec<PositionedElement>, points: &[Point], colour: Color) {
3567 for point in points {
3568 elements.push(filled_path(marker_path(*point), colour));
3569 }
3570}
3571
3572fn series_colour(series_colours: &[Color], index: usize) -> Result<Color> {
3573 series_colours
3574 .get(index)
3575 .copied()
3576 .ok_or_else(|| ChartError::InvalidValue {
3577 element: "chart series palette".to_owned(),
3578 value: format!("missing resolved colour for series {index}"),
3579 })
3580}
3581
3582fn filled_path(path: Path, color: Color) -> PositionedElement {
3583 PositionedElement::Path(PathElement {
3584 path,
3585 fill: Some(Paint::Solid(color)),
3586 stroke: None,
3587 })
3588}
3589
3590fn stroked_path(path: Path, color: Color) -> PositionedElement {
3591 PositionedElement::Path(PathElement {
3592 path,
3593 fill: None,
3594 stroke: Some(Stroke::new(Paint::Solid(color), 1.5)),
3595 })
3596}
3597
3598fn filled_and_stroked_path(path: Path, color: Color) -> PositionedElement {
3599 PositionedElement::Path(PathElement {
3600 path,
3601 fill: Some(Paint::Solid(Color {
3602 a: color.a * 0.55,
3603 ..color
3604 })),
3605 stroke: Some(Stroke::new(Paint::Solid(color), 1.0)),
3606 })
3607}
3608
3609type NamespaceBindings = Vec<(Vec<u8>, String)>;
3610type XmlAttributes = Vec<(String, String)>;
3611type RootAttributes = (XmlAttributes, XmlAttributes);
3612
3613#[derive(Clone, Debug, Default, Eq, PartialEq)]
3614struct ScalarMarkup {
3615 raw_attributes: XmlAttributes,
3616 raw_content: Vec<Vec<u8>>,
3617}
3618
3619#[derive(Clone, Debug, Default, Eq, PartialEq)]
3620struct TextMarkup {
3621 raw_attributes: XmlAttributes,
3622 raw_children: OrderedRawChildren,
3623}
3624
3625#[derive(Clone, Debug, Default, Eq, PartialEq)]
3626struct PointMarkup {
3627 raw_attributes: XmlAttributes,
3628 raw_children: OrderedRawChildren,
3629 value: TextMarkup,
3630}
3631
3632#[derive(Clone, Debug, Default, Eq, PartialEq)]
3633struct ReferenceMarkup {
3634 raw_attributes: XmlAttributes,
3635 raw_children: OrderedRawChildren,
3636 formula: TextMarkup,
3637 cache_attributes: XmlAttributes,
3638 cache_children: OrderedRawChildren,
3639 format_code: Option<TextMarkup>,
3640 point_count: ScalarMarkup,
3641 declared_point_count: Option<u32>,
3642 point_indexes: Vec<u32>,
3643 points: Vec<PointMarkup>,
3644}
3645
3646#[derive(Clone, Debug, Default, Eq, PartialEq)]
3647struct WrapperMarkup {
3648 raw_attributes: XmlAttributes,
3649 raw_children: OrderedRawChildren,
3650}
3651
3652#[derive(Clone, Debug, PartialEq)]
3654pub struct StringRef {
3655 pub formula: String,
3656 pub values: Vec<String>,
3657 markup: ReferenceMarkup,
3658}
3659
3660impl StringRef {
3661 pub fn new(formula: String, values: Vec<String>) -> Result<Self> {
3662 let reference = Self {
3663 formula,
3664 values,
3665 markup: ReferenceMarkup::default(),
3666 };
3667 reference.validate()?;
3668 Ok(reference)
3669 }
3670
3671 pub fn from_xml(xml: &[u8]) -> Result<Self> {
3672 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
3673 }
3674
3675 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
3676 let parsed = parse_reference(xml, b"strRef", b"strCache", false, inherited)?;
3677 let values = parsed.values;
3678 let reference = Self {
3679 formula: parsed.formula,
3680 values,
3681 markup: parsed.markup,
3682 };
3683 reference.validate()?;
3684 Ok(reference)
3685 }
3686
3687 pub fn to_xml(&self) -> Result<Vec<u8>> {
3688 self.validate()?;
3689 let mut writer = Writer::new(Vec::new());
3690 self.write_xml(&mut writer, true)?;
3691 Ok(writer.into_inner())
3692 }
3693
3694 fn validate(&self) -> Result<()> {
3695 validate_formula(&self.formula, "c:strRef/c:f")?;
3696 for value in &self.values {
3697 validate_xml_text(value, "c:strCache/c:pt/c:v")?;
3698 }
3699 validate_point_count(self.values.len(), "c:strCache")
3700 }
3701
3702 fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
3703 self.validate()?;
3704 let mut start = BytesStart::new("c:strRef");
3705 if standalone {
3706 start.push_attribute(("xmlns:c", C_NS));
3707 }
3708 push_attributes(&mut start, &self.markup.raw_attributes);
3709 writer
3710 .write_event(Event::Start(start))
3711 .map_err(OxmlError::from)?;
3712 emit_raw(writer, self.markup.raw_children.at(0))?;
3713 write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
3714 emit_raw(writer, self.markup.raw_children.at(1))?;
3715 write_string_cache(writer, &self.values, &self.markup)?;
3716 emit_raw(writer, self.markup.raw_children.at(2))?;
3717 writer
3718 .write_event(Event::End(BytesEnd::new("c:strRef")))
3719 .map_err(OxmlError::from)?;
3720 Ok(())
3721 }
3722}
3723
3724#[derive(Clone, Debug, PartialEq)]
3726pub struct NumericData {
3727 pub formula: String,
3728 pub format_code: String,
3729 pub values: Vec<f64>,
3730 markup: ReferenceMarkup,
3731}
3732
3733impl NumericData {
3734 pub fn new(formula: String, format_code: String, values: Vec<f64>) -> Result<Self> {
3735 validate_formula(&formula, "c:numRef/c:f")?;
3736 validate_format_code(&format_code)?;
3737 validate_numeric_values(&values)?;
3738 validate_point_count(values.len(), "c:numCache")?;
3739 Ok(Self {
3740 formula,
3741 format_code,
3742 values,
3743 markup: ReferenceMarkup::default(),
3744 })
3745 }
3746
3747 pub fn from_xml(xml: &[u8]) -> Result<Self> {
3748 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
3749 }
3750
3751 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
3752 let parsed = parse_reference(xml, b"numRef", b"numCache", true, inherited)?;
3753 let format_code = parsed
3754 .format_code
3755 .ok_or_else(|| ChartError::MissingElement("c:formatCode".to_owned()))?;
3756 let mut values = Vec::with_capacity(parsed.values.len());
3757 for value in parsed.values {
3758 let number = value.parse::<f64>().map_err(|_| ChartError::InvalidValue {
3759 element: "c:v".to_owned(),
3760 value: value.clone(),
3761 })?;
3762 if !number.is_finite() {
3763 return Err(ChartError::InvalidValue {
3764 element: "c:v".to_owned(),
3765 value,
3766 });
3767 }
3768 values.push(number);
3769 }
3770 let data = Self {
3771 formula: parsed.formula,
3772 format_code,
3773 values,
3774 markup: parsed.markup,
3775 };
3776 data.validate()?;
3777 Ok(data)
3778 }
3779
3780 pub fn to_xml(&self) -> Result<Vec<u8>> {
3781 self.validate()?;
3782 let mut writer = Writer::new(Vec::new());
3783 self.write_xml(&mut writer, true)?;
3784 Ok(writer.into_inner())
3785 }
3786
3787 fn validate(&self) -> Result<()> {
3788 validate_formula(&self.formula, "c:numRef/c:f")?;
3789 validate_format_code(&self.format_code)?;
3790 validate_numeric_values(&self.values)?;
3791 validate_point_count(self.values.len(), "c:numCache")
3792 }
3793
3794 fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
3795 self.validate()?;
3796 let mut start = BytesStart::new("c:numRef");
3797 if standalone {
3798 start.push_attribute(("xmlns:c", C_NS));
3799 }
3800 push_attributes(&mut start, &self.markup.raw_attributes);
3801 writer
3802 .write_event(Event::Start(start))
3803 .map_err(OxmlError::from)?;
3804 emit_raw(writer, self.markup.raw_children.at(0))?;
3805 write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
3806 emit_raw(writer, self.markup.raw_children.at(1))?;
3807 write_numeric_cache(writer, self, &self.markup)?;
3808 emit_raw(writer, self.markup.raw_children.at(2))?;
3809 writer
3810 .write_event(Event::End(BytesEnd::new("c:numRef")))
3811 .map_err(OxmlError::from)?;
3812 Ok(())
3813 }
3814}
3815
3816#[derive(Clone, Debug, PartialEq)]
3817pub enum AxisData {
3818 String(StringRef),
3819 Numeric(NumericData),
3820}
3821
3822#[derive(Clone, Debug, PartialEq)]
3824pub struct Series {
3825 pub index: u32,
3826 pub order: u32,
3827 pub name: Option<StringRef>,
3828 pub categories: Option<AxisData>,
3829 pub values: NumericData,
3830 pub bubble_size: Option<NumericData>,
3831 pub sp_pr: Option<CT_ShapeProperties>,
3832 pub data_labels: Option<CT_DLbls>,
3833 index_markup: ScalarMarkup,
3834 order_markup: ScalarMarkup,
3835 name_markup: Option<WrapperMarkup>,
3836 categories_markup: Option<WrapperMarkup>,
3837 values_markup: WrapperMarkup,
3838 bubble_size_markup: Option<WrapperMarkup>,
3839 opaque_name: bool,
3840 opaque_categories: bool,
3841 opaque_bubble_size: bool,
3842 uses_scatter_wrappers: bool,
3843 parsed_from_xml: bool,
3844 raw_attributes: XmlAttributes,
3845 namespace_declarations: XmlAttributes,
3846 raw_children: OrderedRawChildren,
3847}
3848
3849impl Series {
3850 pub fn new(index: u32, order: u32, values: NumericData) -> Self {
3851 Self {
3852 index,
3853 order,
3854 name: None,
3855 categories: None,
3856 values,
3857 bubble_size: None,
3858 sp_pr: None,
3859 data_labels: None,
3860 index_markup: ScalarMarkup::default(),
3861 order_markup: ScalarMarkup::default(),
3862 name_markup: None,
3863 categories_markup: None,
3864 values_markup: WrapperMarkup::default(),
3865 bubble_size_markup: None,
3866 opaque_name: false,
3867 opaque_categories: false,
3868 opaque_bubble_size: false,
3869 uses_scatter_wrappers: false,
3870 parsed_from_xml: false,
3871 raw_attributes: Vec::new(),
3872 namespace_declarations: Vec::new(),
3873 raw_children: OrderedRawChildren::default(),
3874 }
3875 }
3876
3877 pub fn from_xml(xml: &[u8]) -> Result<Self> {
3878 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
3879 }
3880
3881 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
3882 let mut reader = Reader::from_reader(xml);
3883 let mut buffer = Vec::new();
3884 loop {
3885 match reader
3886 .read_event_into(&mut buffer)
3887 .map_err(OxmlError::from)?
3888 {
3889 Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
3890 chart_root_prefix(&element)?;
3891 if !element_is_in_namespace(&element, C_NS, inherited)? {
3892 return Err(ChartError::UnexpectedElement(element_name(&element)));
3893 }
3894 return Self::from_element(&mut reader, &element, inherited);
3895 }
3896 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
3897 return Err(ChartError::MissingElement("c:idx".to_owned()));
3898 }
3899 Event::Start(element) | Event::Empty(element) => {
3900 return Err(ChartError::UnexpectedElement(element_name(&element)));
3901 }
3902 Event::Eof => return Err(ChartError::MissingElement("c:ser".to_owned())),
3903 _ => {}
3904 }
3905 buffer.clear();
3906 }
3907 }
3908
3909 fn from_element(
3910 reader: &mut Reader<&[u8]>,
3911 start: &BytesStart<'_>,
3912 inherited: &NamespaceBindings,
3913 ) -> Result<Self> {
3914 reject_conflicting_prefix(start, b"a", A_NS)?;
3915 let namespaces = chart_bindings(inherited, start)?;
3916 require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
3917 require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
3918 require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
3919 let (raw_attributes, _) =
3920 capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
3921 let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
3922 let mut state = SeriesParseState::default();
3923 let mut buffer = Vec::new();
3924 loop {
3925 match reader
3926 .read_event_into(&mut buffer)
3927 .map_err(OxmlError::from)?
3928 {
3929 Event::Start(element) => {
3930 let name = chart_child_local(&element, &namespaces)?;
3931 let raw = capture_element(reader, &element)?;
3932 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
3933 }
3934 Event::Empty(element) => {
3935 let name = chart_child_local(&element, &namespaces)?;
3936 let raw = capture_empty_element(&element)?;
3937 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
3938 }
3939 event @ (Event::Text(_)
3940 | Event::CData(_)
3941 | Event::Comment(_)
3942 | Event::PI(_)
3943 | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
3944 Event::End(element) if matches_local_name(element.name().as_ref(), b"ser") => {
3945 break;
3946 }
3947 Event::Eof => return Err(missing_end("c:ser")),
3948 _ => {}
3949 }
3950 buffer.clear();
3951 }
3952 state.finish(raw_attributes, namespace_declarations)
3953 }
3954
3955 pub fn to_xml(&self) -> Result<Vec<u8>> {
3956 self.to_xml_for_plot(self.uses_scatter_wrappers)
3957 }
3958
3959 fn to_xml_for_plot(&self, scatter: bool) -> Result<Vec<u8>> {
3960 if self.opaque_name && self.name.is_some() {
3961 return Err(ChartError::DuplicateElement("c:tx".to_owned()));
3962 }
3963 if self.opaque_categories && self.categories.is_some() {
3964 return Err(ChartError::DuplicateElement("c:cat".to_owned()));
3965 }
3966 if self.opaque_bubble_size && self.bubble_size.is_some() {
3967 return Err(ChartError::DuplicateElement("c:bubbleSize".to_owned()));
3968 }
3969 self.values.validate()?;
3970 if let Some(name) = &self.name {
3971 name.validate()?;
3972 }
3973 if let Some(categories) = &self.categories {
3974 if scatter && !matches!(categories, AxisData::Numeric(_)) {
3975 return Err(ChartError::InvalidValue {
3976 element: "c:xVal".to_owned(),
3977 value: "scatter x values must use a numeric cache".to_owned(),
3978 });
3979 }
3980 match categories {
3981 AxisData::String(reference) => reference.validate()?,
3982 AxisData::Numeric(reference) => reference.validate()?,
3983 }
3984 }
3985 if scatter && self.categories.is_none() {
3986 return Err(ChartError::MissingElement("c:xVal".to_owned()));
3987 }
3988 if let Some(size) = &self.bubble_size {
3989 size.validate()?;
3990 }
3991 if let Some(labels) = &self.data_labels {
3992 labels.validate()?;
3993 }
3994 let mut writer = Writer::new(Vec::new());
3995 let mut start = BytesStart::new("c:ser");
3996 start.push_attribute(("xmlns:c", C_NS));
3997 start.push_attribute(("xmlns:a", A_NS));
3998 start.push_attribute(("xmlns:r", R_NS));
3999 push_attributes(&mut start, &self.namespace_declarations);
4000 push_attributes(&mut start, &self.raw_attributes);
4001 writer
4002 .write_event(Event::Start(start))
4003 .map_err(OxmlError::from)?;
4004 emit_raw(&mut writer, self.raw_children.at(0))?;
4005 write_scalar(
4006 &mut writer,
4007 "c:idx",
4008 &self.index.to_string(),
4009 Some(&self.index_markup),
4010 )?;
4011 emit_raw(&mut writer, self.raw_children.at(1))?;
4012 write_scalar(
4013 &mut writer,
4014 "c:order",
4015 &self.order.to_string(),
4016 Some(&self.order_markup),
4017 )?;
4018 emit_raw(&mut writer, self.raw_children.at(2))?;
4019 if let Some(name) = &self.name {
4020 write_wrapper_start(
4021 &mut writer,
4022 "c:tx",
4023 self.name_markup
4024 .as_ref()
4025 .unwrap_or(&WrapperMarkup::default()),
4026 )?;
4027 name.write_xml(&mut writer, false)?;
4028 write_wrapper_end(
4029 &mut writer,
4030 "c:tx",
4031 self.name_markup
4032 .as_ref()
4033 .unwrap_or(&WrapperMarkup::default()),
4034 )?;
4035 }
4036 emit_raw(&mut writer, self.raw_children.at(3))?;
4037 if let Some(properties) = &self.sp_pr {
4038 properties.write_xml_as(&mut writer, "c:spPr")?;
4039 }
4040 emit_raw(&mut writer, self.raw_children.at(4))?;
4041 if let Some(labels) = &self.data_labels {
4042 labels.write_xml(&mut writer, false)?;
4043 }
4044 emit_raw(&mut writer, self.raw_children.at(5))?;
4045 if let Some(categories) = &self.categories {
4046 let default_markup = WrapperMarkup::default();
4047 let markup = self.categories_markup.as_ref().unwrap_or(&default_markup);
4048 write_wrapper_start(
4049 &mut writer,
4050 if scatter { "c:xVal" } else { "c:cat" },
4051 markup,
4052 )?;
4053 match categories {
4054 AxisData::String(reference) => reference.write_xml(&mut writer, false)?,
4055 AxisData::Numeric(reference) => reference.write_xml(&mut writer, false)?,
4056 }
4057 write_wrapper_end(
4058 &mut writer,
4059 if scatter { "c:xVal" } else { "c:cat" },
4060 markup,
4061 )?;
4062 }
4063 emit_raw(&mut writer, self.raw_children.at(6))?;
4064 write_wrapper_start(
4065 &mut writer,
4066 if scatter { "c:yVal" } else { "c:val" },
4067 &self.values_markup,
4068 )?;
4069 self.values.write_xml(&mut writer, false)?;
4070 write_wrapper_end(
4071 &mut writer,
4072 if scatter { "c:yVal" } else { "c:val" },
4073 &self.values_markup,
4074 )?;
4075 emit_raw(&mut writer, self.raw_children.at(7))?;
4076 if let Some(size) = &self.bubble_size {
4077 let default_markup = WrapperMarkup::default();
4078 let markup = self.bubble_size_markup.as_ref().unwrap_or(&default_markup);
4079 write_wrapper_start(&mut writer, "c:bubbleSize", markup)?;
4080 size.write_xml(&mut writer, false)?;
4081 write_wrapper_end(&mut writer, "c:bubbleSize", markup)?;
4082 }
4083 emit_raw(&mut writer, self.raw_children.at(8))?;
4084 writer
4085 .write_event(Event::End(BytesEnd::new("c:ser")))
4086 .map_err(OxmlError::from)?;
4087 Ok(writer.into_inner())
4088 }
4089
4090 pub fn raw_children(&self) -> &OrderedRawChildren {
4091 &self.raw_children
4092 }
4093}
4094
4095#[derive(Default)]
4096struct SeriesParseState {
4097 index: Option<(u32, ScalarMarkup)>,
4098 order: Option<(u32, ScalarMarkup)>,
4099 name: Option<(StringRef, WrapperMarkup)>,
4100 categories: Option<(AxisData, WrapperMarkup)>,
4101 values: Option<(NumericData, WrapperMarkup)>,
4102 bubble_size: Option<(NumericData, WrapperMarkup)>,
4103 data_labels: Option<CT_DLbls>,
4104 name_seen: bool,
4105 categories_seen: bool,
4106 bubble_size_seen: bool,
4107 opaque_name: bool,
4108 opaque_categories: bool,
4109 opaque_bubble_size: bool,
4110 saw_standard_wrapper: bool,
4111 saw_scatter_wrapper: bool,
4112 sp_pr: Option<CT_ShapeProperties>,
4113 raw_children: OrderedRawChildren,
4114 boundary: usize,
4115}
4116
4117impl SeriesParseState {
4118 fn capture_event(&mut self, raw: Vec<u8>) {
4119 self.raw_children.push(self.boundary, raw);
4120 }
4121
4122 fn parse_child(
4123 &mut self,
4124 name: &[u8],
4125 raw: Vec<u8>,
4126 namespaces: &NamespaceBindings,
4127 ) -> Result<()> {
4128 match name {
4129 b"idx" => {
4130 set_once(&mut self.index, parse_u32_scalar(&raw, "idx")?, "c:idx")?;
4131 self.boundary = self.boundary.max(1);
4132 }
4133 b"order" => {
4134 set_once(&mut self.order, parse_u32_scalar(&raw, "order")?, "c:order")?;
4135 self.boundary = self.boundary.max(2);
4136 }
4137 b"tx" => {
4138 mark_once(&mut self.name_seen, "c:tx")?;
4139 let parsed = parse_wrapper(&raw, b"tx", &[b"strRef"], namespaces)?;
4140 if let Some((_, reference)) = parsed.choice {
4141 self.name = Some((
4142 StringRef::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4143 parsed.markup,
4144 ));
4145 } else {
4146 self.opaque_name = true;
4147 self.raw_children.push(2, raw);
4148 }
4149 self.boundary = self.boundary.max(3);
4150 }
4151 b"spPr" => {
4152 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
4153 let properties = CT_ShapeProperties::from_xml(&raw)?;
4154 let mut writer = Writer::new(Vec::new());
4155 properties.write_xml_as(&mut writer, "c:spPr")?;
4156 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
4157 set_once(&mut self.sp_pr, properties, "c:spPr")?;
4158 self.boundary = self.boundary.max(4);
4159 }
4160 b"dLbls" => {
4161 set_once(
4162 &mut self.data_labels,
4163 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
4164 "c:dLbls",
4165 )?;
4166 self.boundary = self.boundary.max(5);
4167 }
4168 b"cat" => {
4169 self.saw_standard_wrapper = true;
4170 mark_once(&mut self.categories_seen, "c:cat")?;
4171 let parsed = parse_wrapper(&raw, b"cat", &[b"strRef", b"numRef"], namespaces)?;
4172 if let Some((choice, reference)) = parsed.choice {
4173 let categories = if choice == b"strRef" {
4174 AxisData::String(StringRef::from_xml_with_namespaces(
4175 &reference,
4176 &parsed.namespaces,
4177 )?)
4178 } else {
4179 AxisData::Numeric(NumericData::from_xml_with_namespaces(
4180 &reference,
4181 &parsed.namespaces,
4182 )?)
4183 };
4184 self.categories = Some((categories, parsed.markup));
4185 } else {
4186 self.opaque_categories = true;
4187 self.raw_children.push(5, raw);
4188 }
4189 self.boundary = self.boundary.max(6);
4190 }
4191 b"xVal" => {
4192 self.saw_scatter_wrapper = true;
4193 mark_once(&mut self.categories_seen, "c:xVal")?;
4194 let parsed = parse_wrapper(&raw, b"xVal", &[b"numRef"], namespaces)?;
4195 let (_, reference) = parsed
4196 .choice
4197 .ok_or_else(|| ChartError::MissingElement("c:xVal/c:numRef".to_owned()))?;
4198 self.categories = Some((
4199 AxisData::Numeric(NumericData::from_xml_with_namespaces(
4200 &reference,
4201 &parsed.namespaces,
4202 )?),
4203 parsed.markup,
4204 ));
4205 self.boundary = self.boundary.max(6);
4206 }
4207 b"val" | b"yVal" => {
4208 if name == b"yVal" {
4209 self.saw_scatter_wrapper = true;
4210 } else {
4211 self.saw_standard_wrapper = true;
4212 }
4213 let wrapper = if name == b"yVal" {
4214 b"yVal".as_slice()
4215 } else {
4216 b"val".as_slice()
4217 };
4218 let parsed = parse_wrapper(&raw, wrapper, &[b"numRef"], namespaces)?;
4219 let (_, reference) = parsed.choice.ok_or_else(|| {
4220 ChartError::MissingElement(format!(
4221 "c:{}/c:numRef",
4222 String::from_utf8_lossy(wrapper)
4223 ))
4224 })?;
4225 set_once(
4226 &mut self.values,
4227 (
4228 NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4229 parsed.markup,
4230 ),
4231 "c:val or c:yVal",
4232 )?;
4233 self.boundary = self.boundary.max(7);
4234 }
4235 b"bubbleSize" => {
4236 mark_once(&mut self.bubble_size_seen, "c:bubbleSize")?;
4237 let parsed = parse_wrapper(&raw, b"bubbleSize", &[b"numRef"], namespaces)?;
4238 if let Some((_, reference)) = parsed.choice {
4239 self.bubble_size = Some((
4240 NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4241 parsed.markup,
4242 ));
4243 } else {
4244 self.opaque_bubble_size = true;
4245 self.raw_children.push(7, raw);
4246 }
4247 self.boundary = self.boundary.max(8);
4248 }
4249 _ => {
4250 let boundary = series_raw_boundary(name, self.boundary);
4251 self.raw_children.push(boundary, raw);
4252 self.boundary = self.boundary.max(boundary);
4253 }
4254 }
4255 Ok(())
4256 }
4257
4258 fn finish(
4259 self,
4260 raw_attributes: XmlAttributes,
4261 namespace_declarations: XmlAttributes,
4262 ) -> Result<Series> {
4263 if self.saw_standard_wrapper && self.saw_scatter_wrapper {
4264 return Err(ChartError::InvalidValue {
4265 element: "c:ser".to_owned(),
4266 value: "category/value and x/y wrappers cannot be combined".to_owned(),
4267 });
4268 }
4269 let (index, index_markup) = self
4270 .index
4271 .ok_or_else(|| ChartError::MissingElement("c:idx".to_owned()))?;
4272 let (order, order_markup) = self
4273 .order
4274 .ok_or_else(|| ChartError::MissingElement("c:order".to_owned()))?;
4275 let (values, values_markup) = self
4276 .values
4277 .ok_or_else(|| ChartError::MissingElement("c:val".to_owned()))?;
4278 let (name, name_markup) = self
4279 .name
4280 .map(|(value, markup)| (Some(value), Some(markup)))
4281 .unwrap_or((None, None));
4282 let (categories, categories_markup) = self
4283 .categories
4284 .map(|(value, markup)| (Some(value), Some(markup)))
4285 .unwrap_or((None, None));
4286 let (bubble_size, bubble_size_markup) = self
4287 .bubble_size
4288 .map(|(value, markup)| (Some(value), Some(markup)))
4289 .unwrap_or((None, None));
4290 Ok(Series {
4291 index,
4292 order,
4293 name,
4294 categories,
4295 values,
4296 bubble_size,
4297 sp_pr: self.sp_pr,
4298 data_labels: self.data_labels,
4299 index_markup,
4300 order_markup,
4301 name_markup,
4302 categories_markup,
4303 values_markup,
4304 bubble_size_markup,
4305 opaque_name: self.opaque_name,
4306 opaque_categories: self.opaque_categories,
4307 opaque_bubble_size: self.opaque_bubble_size,
4308 uses_scatter_wrappers: self.saw_scatter_wrapper,
4309 parsed_from_xml: true,
4310 raw_attributes,
4311 namespace_declarations,
4312 raw_children: raw_children_in_schema_order(&self.raw_children, 8),
4313 })
4314 }
4315}
4316
4317fn series_raw_boundary(name: &[u8], current: usize) -> usize {
4318 match name {
4319 b"marker" | b"invertIfNegative" | b"pictureOptions" | b"explosion" | b"dPt" => 4,
4320 b"trendline" | b"errBars" => 5,
4321 b"shape" | b"smooth" => 7,
4322 b"extLst" => 8,
4323 _ => current,
4324 }
4325}
4326
4327struct ParsedReference {
4328 formula: String,
4329 format_code: Option<String>,
4330 values: Vec<String>,
4331 markup: ReferenceMarkup,
4332}
4333
4334fn parse_reference(
4335 xml: &[u8],
4336 reference_local: &[u8],
4337 cache_local: &[u8],
4338 numeric: bool,
4339 inherited: &NamespaceBindings,
4340) -> Result<ParsedReference> {
4341 let mut reader = Reader::from_reader(xml);
4342 let mut buffer = Vec::new();
4343 loop {
4344 match reader
4345 .read_event_into(&mut buffer)
4346 .map_err(OxmlError::from)?
4347 {
4348 Event::Start(element)
4349 if matches_local_name(element.name().as_ref(), reference_local) =>
4350 {
4351 let namespaces = typed_rewrite_bindings(&element, inherited)?;
4352 let raw_attributes = capture_fixed_attributes(&element, &["xmlns:c"])?;
4353 let mut formula: Option<(String, TextMarkup)> = None;
4354 let mut cache: Option<(Option<String>, Vec<String>, CacheMarkup)> = None;
4355 let mut raw_children = OrderedRawChildren::default();
4356 let mut boundary = 0usize;
4357 let mut inner = Vec::new();
4358 loop {
4359 match reader
4360 .read_event_into(&mut inner)
4361 .map_err(OxmlError::from)?
4362 {
4363 Event::Start(child) => {
4364 let name = chart_child_local(&child, &namespaces)?;
4365 let raw = capture_element(&mut reader, &child)?;
4366 match name.as_deref().unwrap_or_default() {
4367 b"f" => {
4368 set_once(
4369 &mut formula,
4370 parse_text_element(&raw, b"f", &namespaces)?,
4371 "c:f",
4372 )?;
4373 boundary = boundary.max(1);
4374 }
4375 name if name == cache_local => {
4376 set_once(
4377 &mut cache,
4378 parse_cache(&raw, cache_local, numeric, &namespaces)?,
4379 &format!("c:{}", String::from_utf8_lossy(cache_local)),
4380 )?;
4381 boundary = boundary.max(2);
4382 }
4383 _ => raw_children.push(boundary, raw),
4384 }
4385 }
4386 Event::Empty(child) => {
4387 let name = chart_child_local(&child, &namespaces)?;
4388 let raw = capture_empty_element(&child)?;
4389 match name.as_deref().unwrap_or_default() {
4390 b"f" => {
4391 set_once(
4392 &mut formula,
4393 parse_text_element(&raw, b"f", &namespaces)?,
4394 "c:f",
4395 )?;
4396 boundary = boundary.max(1);
4397 }
4398 name if name == cache_local => {
4399 set_once(
4400 &mut cache,
4401 parse_cache(&raw, cache_local, numeric, &namespaces)?,
4402 &format!("c:{}", String::from_utf8_lossy(cache_local)),
4403 )?;
4404 boundary = boundary.max(2);
4405 }
4406 _ => raw_children.push(boundary, raw),
4407 }
4408 }
4409 event @ (Event::Text(_)
4410 | Event::CData(_)
4411 | Event::Comment(_)
4412 | Event::PI(_)
4413 | Event::GeneralRef(_)) => {
4414 raw_children.push(boundary, capture_event(event)?);
4415 }
4416 Event::End(end)
4417 if matches_local_name(end.name().as_ref(), reference_local) =>
4418 {
4419 let (formula, formula_markup) = formula
4420 .ok_or_else(|| ChartError::MissingElement("c:f".to_owned()))?;
4421 validate_formula(&formula, "c:f")?;
4422 let (format_code, values, cache_markup) = cache.ok_or_else(|| {
4423 ChartError::MissingElement(format!(
4424 "c:{}",
4425 String::from_utf8_lossy(cache_local)
4426 ))
4427 })?;
4428 let markup = ReferenceMarkup {
4429 raw_attributes,
4430 raw_children: raw_children_in_schema_order(&raw_children, 2),
4431 formula: formula_markup,
4432 cache_attributes: cache_markup.raw_attributes,
4433 cache_children: cache_markup.raw_children,
4434 format_code: cache_markup.format_code,
4435 point_count: cache_markup.point_count,
4436 declared_point_count: cache_markup.declared_point_count,
4437 point_indexes: cache_markup.point_indexes,
4438 points: cache_markup.points,
4439 };
4440 return Ok(ParsedReference {
4441 formula,
4442 format_code,
4443 values,
4444 markup,
4445 });
4446 }
4447 Event::Eof => {
4448 return Err(missing_end(&format!(
4449 "c:{}",
4450 String::from_utf8_lossy(reference_local)
4451 )));
4452 }
4453 _ => {}
4454 }
4455 inner.clear();
4456 }
4457 }
4458 Event::Empty(element)
4459 if matches_local_name(element.name().as_ref(), reference_local) =>
4460 {
4461 return Err(ChartError::MissingElement("c:f".to_owned()));
4462 }
4463 Event::Start(element) | Event::Empty(element) => {
4464 return Err(ChartError::UnexpectedElement(element_name(&element)));
4465 }
4466 Event::Eof => {
4467 return Err(ChartError::MissingElement(format!(
4468 "c:{}",
4469 String::from_utf8_lossy(reference_local)
4470 )));
4471 }
4472 _ => {}
4473 }
4474 buffer.clear();
4475 }
4476}
4477
4478struct CacheMarkup {
4479 raw_attributes: XmlAttributes,
4480 raw_children: OrderedRawChildren,
4481 format_code: Option<TextMarkup>,
4482 point_count: ScalarMarkup,
4483 declared_point_count: Option<u32>,
4484 point_indexes: Vec<u32>,
4485 points: Vec<PointMarkup>,
4486}
4487
4488fn parse_cache(
4489 xml: &[u8],
4490 cache_local: &[u8],
4491 numeric: bool,
4492 inherited: &NamespaceBindings,
4493) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
4494 let mut reader = Reader::from_reader(xml);
4495 let mut buffer = Vec::new();
4496 loop {
4497 match reader
4498 .read_event_into(&mut buffer)
4499 .map_err(OxmlError::from)?
4500 {
4501 Event::Start(element) if matches_local_name(element.name().as_ref(), cache_local) => {
4502 let namespaces = typed_rewrite_bindings(&element, inherited)?;
4503 let raw_attributes = capture_attributes(&element)?;
4504 let mut state = CacheParseState::new(numeric, namespaces);
4505 let mut inner = Vec::new();
4506 loop {
4507 match reader
4508 .read_event_into(&mut inner)
4509 .map_err(OxmlError::from)?
4510 {
4511 Event::Start(child) => {
4512 let name = chart_child_local(&child, &state.namespaces)?;
4513 let raw = capture_element(&mut reader, &child)?;
4514 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
4515 }
4516 Event::Empty(child) => {
4517 let name = chart_child_local(&child, &state.namespaces)?;
4518 let raw = capture_empty_element(&child)?;
4519 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
4520 }
4521 event @ (Event::Text(_)
4522 | Event::CData(_)
4523 | Event::Comment(_)
4524 | Event::PI(_)
4525 | Event::GeneralRef(_)) => {
4526 state.capture_event(capture_event(event)?);
4527 }
4528 Event::End(end) if matches_local_name(end.name().as_ref(), cache_local) => {
4529 return state.finish(raw_attributes);
4530 }
4531 Event::Eof => {
4532 return Err(missing_end(&format!(
4533 "c:{}",
4534 String::from_utf8_lossy(cache_local)
4535 )));
4536 }
4537 _ => {}
4538 }
4539 inner.clear();
4540 }
4541 }
4542 Event::Empty(element) if matches_local_name(element.name().as_ref(), cache_local) => {
4543 return Err(ChartError::MissingElement("c:ptCount".to_owned()));
4544 }
4545 Event::Start(element) | Event::Empty(element) => {
4546 return Err(ChartError::UnexpectedElement(element_name(&element)));
4547 }
4548 Event::Eof => {
4549 return Err(ChartError::MissingElement(format!(
4550 "c:{}",
4551 String::from_utf8_lossy(cache_local)
4552 )));
4553 }
4554 _ => {}
4555 }
4556 buffer.clear();
4557 }
4558}
4559
4560struct CacheParseState {
4561 numeric: bool,
4562 namespaces: NamespaceBindings,
4563 point_base: usize,
4564 boundary: usize,
4565 format_code: Option<(String, TextMarkup)>,
4566 point_count: Option<(u32, ScalarMarkup)>,
4567 points: Vec<(u32, String, PointMarkup)>,
4568 raw_children: OrderedRawChildren,
4569}
4570
4571impl CacheParseState {
4572 fn new(numeric: bool, namespaces: NamespaceBindings) -> Self {
4573 Self {
4574 numeric,
4575 namespaces,
4576 point_base: usize::from(numeric) + 1,
4577 boundary: 0,
4578 format_code: None,
4579 point_count: None,
4580 points: Vec::new(),
4581 raw_children: OrderedRawChildren::default(),
4582 }
4583 }
4584
4585 fn capture_event(&mut self, raw: Vec<u8>) {
4586 self.raw_children.push(self.boundary, raw);
4587 }
4588
4589 fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
4590 match name {
4591 b"formatCode" if self.numeric => {
4592 set_once(
4593 &mut self.format_code,
4594 parse_text_element(&raw, b"formatCode", &self.namespaces)?,
4595 "c:formatCode",
4596 )?;
4597 self.boundary = self.boundary.max(1);
4598 }
4599 b"ptCount" => {
4600 set_once(
4601 &mut self.point_count,
4602 parse_u32_scalar(&raw, "ptCount")?,
4603 "c:ptCount",
4604 )?;
4605 self.boundary = self.boundary.max(self.point_base);
4606 }
4607 b"pt" => {
4608 self.points.push(parse_point(&raw, &self.namespaces)?);
4609 self.boundary = self.boundary.max(self.point_base + self.points.len());
4610 }
4611 _ => self.raw_children.push(self.boundary, raw),
4612 }
4613 Ok(())
4614 }
4615
4616 fn finish(
4617 self,
4618 raw_attributes: XmlAttributes,
4619 ) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
4620 let (declared, point_count_markup) = self
4621 .point_count
4622 .ok_or_else(|| ChartError::MissingElement("c:ptCount".to_owned()))?;
4623 let actual = u32::try_from(self.points.len()).map_err(|_| ChartError::InvalidValue {
4624 element: "c:ptCount".to_owned(),
4625 value: self.points.len().to_string(),
4626 })?;
4627 if declared < actual {
4628 return Err(invalid_attribute("ptCount", "val", declared.to_string()));
4629 }
4630 let mut values = Vec::with_capacity(self.points.len());
4631 let mut point_indexes = Vec::with_capacity(self.points.len());
4632 let mut point_markup = Vec::with_capacity(self.points.len());
4633 let mut previous = None;
4634 for (index, value, markup) in self.points {
4635 if index >= declared || previous.is_some_and(|last| index <= last) {
4636 return Err(invalid_attribute("pt", "idx", index.to_string()));
4637 }
4638 previous = Some(index);
4639 values.push(value);
4640 point_indexes.push(index);
4641 point_markup.push(markup);
4642 }
4643 let (format_code, format_markup) = self
4644 .format_code
4645 .map(|(value, markup)| (Some(value), Some(markup)))
4646 .unwrap_or((None, None));
4647 if self.numeric && format_code.is_none() {
4648 return Err(ChartError::MissingElement("c:formatCode".to_owned()));
4649 }
4650 Ok((
4651 format_code,
4652 values,
4653 CacheMarkup {
4654 raw_attributes,
4655 raw_children: raw_children_in_schema_order(
4656 &self.raw_children,
4657 self.point_base + point_markup.len(),
4658 ),
4659 format_code: format_markup,
4660 point_count: point_count_markup,
4661 declared_point_count: Some(declared),
4662 point_indexes,
4663 points: point_markup,
4664 },
4665 ))
4666 }
4667}
4668
4669fn parse_point(xml: &[u8], inherited: &NamespaceBindings) -> Result<(u32, String, PointMarkup)> {
4670 let mut reader = Reader::from_reader(xml);
4671 let mut buffer = Vec::new();
4672 loop {
4673 match reader
4674 .read_event_into(&mut buffer)
4675 .map_err(OxmlError::from)?
4676 {
4677 Event::Start(element) if matches_local_name(element.name().as_ref(), b"pt") => {
4678 let namespaces = typed_rewrite_bindings(&element, inherited)?;
4679 let index = required_u32_attribute(&element, "pt", b"idx")?;
4680 let raw_attributes = capture_attributes_except(&element, b"idx")?;
4681 let mut value: Option<(String, TextMarkup)> = None;
4682 let mut raw_children = OrderedRawChildren::default();
4683 let mut boundary = 0usize;
4684 let mut inner = Vec::new();
4685 loop {
4686 match reader
4687 .read_event_into(&mut inner)
4688 .map_err(OxmlError::from)?
4689 {
4690 Event::Start(child) => {
4691 let name = chart_child_local(&child, &namespaces)?;
4692 let raw = capture_element(&mut reader, &child)?;
4693 if name.as_deref() == Some(b"v") {
4694 set_once(
4695 &mut value,
4696 parse_text_element(&raw, b"v", &namespaces)?,
4697 "c:v",
4698 )?;
4699 boundary = 1;
4700 } else {
4701 raw_children.push(boundary, raw);
4702 }
4703 }
4704 Event::Empty(child) => {
4705 let name = chart_child_local(&child, &namespaces)?;
4706 let raw = capture_empty_element(&child)?;
4707 if name.as_deref() == Some(b"v") {
4708 set_once(
4709 &mut value,
4710 parse_text_element(&raw, b"v", &namespaces)?,
4711 "c:v",
4712 )?;
4713 boundary = 1;
4714 } else {
4715 raw_children.push(boundary, raw);
4716 }
4717 }
4718 event @ (Event::Text(_)
4719 | Event::CData(_)
4720 | Event::Comment(_)
4721 | Event::PI(_)
4722 | Event::GeneralRef(_)) => {
4723 raw_children.push(boundary, capture_event(event)?);
4724 }
4725 Event::End(end) if matches_local_name(end.name().as_ref(), b"pt") => {
4726 let (value, value_markup) = value
4727 .ok_or_else(|| ChartError::MissingElement("c:v".to_owned()))?;
4728 return Ok((
4729 index,
4730 value,
4731 PointMarkup {
4732 raw_attributes,
4733 raw_children: raw_children_in_schema_order(&raw_children, 1),
4734 value: value_markup,
4735 },
4736 ));
4737 }
4738 Event::Eof => return Err(missing_end("c:pt")),
4739 _ => {}
4740 }
4741 inner.clear();
4742 }
4743 }
4744 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pt") => {
4745 return Err(ChartError::MissingElement("c:v".to_owned()));
4746 }
4747 Event::Start(element) | Event::Empty(element) => {
4748 return Err(ChartError::UnexpectedElement(element_name(&element)));
4749 }
4750 Event::Eof => return Err(ChartError::MissingElement("c:pt".to_owned())),
4751 _ => {}
4752 }
4753 buffer.clear();
4754 }
4755}
4756
4757fn parse_text_element(
4758 xml: &[u8],
4759 local: &[u8],
4760 inherited: &NamespaceBindings,
4761) -> Result<(String, TextMarkup)> {
4762 let mut reader = Reader::from_reader(xml);
4763 let mut buffer = Vec::new();
4764 loop {
4765 match reader
4766 .read_event_into(&mut buffer)
4767 .map_err(OxmlError::from)?
4768 {
4769 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
4770 typed_rewrite_bindings(&element, inherited)?;
4771 let raw_attributes = capture_attributes(&element)?;
4772 let mut value = String::new();
4773 let mut raw_children = OrderedRawChildren::default();
4774 let mut seen_text = false;
4775 let mut inner = Vec::new();
4776 loop {
4777 match reader
4778 .read_event_into(&mut inner)
4779 .map_err(OxmlError::from)?
4780 {
4781 Event::Text(text) => {
4782 value.push_str(&decode_plain(&text));
4783 seen_text = true;
4784 }
4785 Event::CData(text) => {
4786 let decoded =
4787 text.decode().map_err(|error| ChartError::InvalidValue {
4788 element: format!("c:{}", String::from_utf8_lossy(local)),
4789 value: error.to_string(),
4790 })?;
4791 value.push_str(&decoded);
4792 seen_text = true;
4793 }
4794 Event::GeneralRef(reference) => {
4795 value.push_str(&resolve_entity(&reference));
4796 seen_text = true;
4797 }
4798 event @ (Event::Comment(_) | Event::PI(_)) => {
4799 raw_children.push(usize::from(seen_text), capture_event(event)?);
4800 }
4801 Event::Start(child) | Event::Empty(child) => {
4802 return Err(ChartError::UnexpectedElement(element_name(&child)));
4803 }
4804 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
4805 return Ok((
4806 value,
4807 TextMarkup {
4808 raw_attributes,
4809 raw_children: raw_children_in_schema_order(&raw_children, 1),
4810 },
4811 ));
4812 }
4813 Event::Eof => {
4814 return Err(missing_end(&format!(
4815 "c:{}",
4816 String::from_utf8_lossy(local)
4817 )));
4818 }
4819 _ => {}
4820 }
4821 inner.clear();
4822 }
4823 }
4824 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
4825 typed_rewrite_bindings(&element, inherited)?;
4826 return Ok((
4827 String::new(),
4828 TextMarkup {
4829 raw_attributes: capture_attributes(&element)?,
4830 raw_children: OrderedRawChildren::default(),
4831 },
4832 ));
4833 }
4834 Event::Start(element) | Event::Empty(element) => {
4835 return Err(ChartError::UnexpectedElement(element_name(&element)));
4836 }
4837 Event::Eof => {
4838 return Err(ChartError::MissingElement(format!(
4839 "c:{}",
4840 String::from_utf8_lossy(local)
4841 )));
4842 }
4843 _ => {}
4844 }
4845 buffer.clear();
4846 }
4847}
4848
4849struct ParsedWrapper {
4850 choice: Option<(Vec<u8>, Vec<u8>)>,
4851 markup: WrapperMarkup,
4852 namespaces: NamespaceBindings,
4853}
4854
4855fn parse_wrapper(
4856 xml: &[u8],
4857 local: &[u8],
4858 choices: &[&[u8]],
4859 inherited: &NamespaceBindings,
4860) -> Result<ParsedWrapper> {
4861 let mut reader = Reader::from_reader(xml);
4862 let mut buffer = Vec::new();
4863 loop {
4864 match reader
4865 .read_event_into(&mut buffer)
4866 .map_err(OxmlError::from)?
4867 {
4868 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
4869 let namespaces = typed_rewrite_bindings(&element, inherited)?;
4870 let raw_attributes = capture_attributes(&element)?;
4871 let mut choice = None;
4872 let mut raw_children = OrderedRawChildren::default();
4873 let mut boundary = 0usize;
4874 let mut inner = Vec::new();
4875 loop {
4876 match reader
4877 .read_event_into(&mut inner)
4878 .map_err(OxmlError::from)?
4879 {
4880 Event::Start(child) => {
4881 let name = chart_child_local(&child, &namespaces)?;
4882 let raw = capture_element(&mut reader, &child)?;
4883 if let Some(name) =
4884 name.filter(|name| choices.contains(&name.as_slice()))
4885 {
4886 if choice.is_some() {
4887 return Err(ChartError::DuplicateElement(format!(
4888 "c:{} reference",
4889 String::from_utf8_lossy(local)
4890 )));
4891 }
4892 choice = Some((name, raw));
4893 boundary = 1;
4894 } else {
4895 raw_children.push(boundary, raw);
4896 }
4897 }
4898 Event::Empty(child) => {
4899 let name = chart_child_local(&child, &namespaces)?;
4900 let raw = capture_empty_element(&child)?;
4901 if let Some(name) =
4902 name.filter(|name| choices.contains(&name.as_slice()))
4903 {
4904 if choice.is_some() {
4905 return Err(ChartError::DuplicateElement(format!(
4906 "c:{} reference",
4907 String::from_utf8_lossy(local)
4908 )));
4909 }
4910 choice = Some((name, raw));
4911 boundary = 1;
4912 } else {
4913 raw_children.push(boundary, raw);
4914 }
4915 }
4916 event @ (Event::Text(_)
4917 | Event::CData(_)
4918 | Event::Comment(_)
4919 | Event::PI(_)
4920 | Event::GeneralRef(_)) => {
4921 raw_children.push(boundary, capture_event(event)?);
4922 }
4923 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
4924 return Ok(ParsedWrapper {
4925 choice,
4926 markup: WrapperMarkup {
4927 raw_attributes,
4928 raw_children: raw_children_in_schema_order(&raw_children, 1),
4929 },
4930 namespaces,
4931 });
4932 }
4933 Event::Eof => {
4934 return Err(missing_end(&format!(
4935 "c:{}",
4936 String::from_utf8_lossy(local)
4937 )));
4938 }
4939 _ => {}
4940 }
4941 inner.clear();
4942 }
4943 }
4944 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
4945 let namespaces = chart_bindings(inherited, &element)?;
4946 return Ok(ParsedWrapper {
4947 choice: None,
4948 markup: WrapperMarkup {
4949 raw_attributes: capture_attributes(&element)?,
4950 raw_children: OrderedRawChildren::default(),
4951 },
4952 namespaces,
4953 });
4954 }
4955 Event::Start(element) | Event::Empty(element) => {
4956 return Err(ChartError::UnexpectedElement(element_name(&element)));
4957 }
4958 Event::Eof => {
4959 return Err(ChartError::MissingElement(format!(
4960 "c:{}",
4961 String::from_utf8_lossy(local)
4962 )));
4963 }
4964 _ => {}
4965 }
4966 buffer.clear();
4967 }
4968}
4969
4970fn write_string_cache(
4971 writer: &mut Writer<Vec<u8>>,
4972 values: &[String],
4973 markup: &ReferenceMarkup,
4974) -> Result<()> {
4975 let (declared_count, indexes) = cache_layout(markup, values.len())?;
4976 let mut start = BytesStart::new("c:strCache");
4977 push_attributes(&mut start, &markup.cache_attributes);
4978 writer
4979 .write_event(Event::Start(start))
4980 .map_err(OxmlError::from)?;
4981 emit_raw(writer, markup.cache_children.at(0))?;
4982 write_scalar(
4983 writer,
4984 "c:ptCount",
4985 &declared_count.to_string(),
4986 Some(&markup.point_count),
4987 )?;
4988 emit_cache_point_raw(writer, markup, 1, values.len(), 0)?;
4989 for (position, value) in values.iter().enumerate() {
4990 let default_markup = PointMarkup::default();
4991 write_point(
4992 writer,
4993 indexes[position],
4994 value,
4995 markup.points.get(position).unwrap_or(&default_markup),
4996 )?;
4997 emit_cache_point_raw(writer, markup, 1, values.len(), position + 1)?;
4998 }
4999 writer
5000 .write_event(Event::End(BytesEnd::new("c:strCache")))
5001 .map_err(OxmlError::from)?;
5002 Ok(())
5003}
5004
5005fn write_numeric_cache(
5006 writer: &mut Writer<Vec<u8>>,
5007 data: &NumericData,
5008 markup: &ReferenceMarkup,
5009) -> Result<()> {
5010 let (declared_count, indexes) = cache_layout(markup, data.values.len())?;
5011 let mut start = BytesStart::new("c:numCache");
5012 push_attributes(&mut start, &markup.cache_attributes);
5013 writer
5014 .write_event(Event::Start(start))
5015 .map_err(OxmlError::from)?;
5016 emit_raw(writer, markup.cache_children.at(0))?;
5017 let default_format_markup = TextMarkup::default();
5018 write_text(
5019 writer,
5020 "c:formatCode",
5021 &data.format_code,
5022 markup
5023 .format_code
5024 .as_ref()
5025 .unwrap_or(&default_format_markup),
5026 )?;
5027 emit_raw(writer, markup.cache_children.at(1))?;
5028 write_scalar(
5029 writer,
5030 "c:ptCount",
5031 &declared_count.to_string(),
5032 Some(&markup.point_count),
5033 )?;
5034 emit_cache_point_raw(writer, markup, 2, data.values.len(), 0)?;
5035 for (position, value) in data.values.iter().enumerate() {
5036 let default_markup = PointMarkup::default();
5037 write_point(
5038 writer,
5039 indexes[position],
5040 &value.to_string(),
5041 markup.points.get(position).unwrap_or(&default_markup),
5042 )?;
5043 emit_cache_point_raw(writer, markup, 2, data.values.len(), position + 1)?;
5044 }
5045 writer
5046 .write_event(Event::End(BytesEnd::new("c:numCache")))
5047 .map_err(OxmlError::from)?;
5048 Ok(())
5049}
5050
5051fn emit_cache_point_raw(
5052 writer: &mut Writer<Vec<u8>>,
5053 markup: &ReferenceMarkup,
5054 point_base: usize,
5055 current_count: usize,
5056 completed_points: usize,
5057) -> Result<()> {
5058 let original_count = markup.points.len();
5059 if current_count == 0 {
5060 for boundary in point_base..=point_base + original_count {
5061 emit_raw(writer, markup.cache_children.at(boundary))?;
5062 }
5063 } else if completed_points == 0 {
5064 if original_count > 0 {
5065 emit_raw(writer, markup.cache_children.at(point_base))?;
5066 }
5067 } else if completed_points < current_count {
5068 if completed_points < original_count {
5069 emit_raw(
5070 writer,
5071 markup.cache_children.at(point_base + completed_points),
5072 )?;
5073 }
5074 } else {
5075 let first_tail = if original_count == 0 {
5076 point_base
5077 } else {
5078 point_base + current_count.min(original_count)
5079 };
5080 for boundary in first_tail..=point_base + original_count {
5081 emit_raw(writer, markup.cache_children.at(boundary))?;
5082 }
5083 }
5084 Ok(())
5085}
5086
5087fn write_point(
5088 writer: &mut Writer<Vec<u8>>,
5089 index: u32,
5090 value: &str,
5091 markup: &PointMarkup,
5092) -> Result<()> {
5093 let index = index.to_string();
5094 let mut start = BytesStart::new("c:pt");
5095 start.push_attribute(("idx", index.as_str()));
5096 push_attributes(&mut start, &markup.raw_attributes);
5097 writer
5098 .write_event(Event::Start(start))
5099 .map_err(OxmlError::from)?;
5100 emit_raw(writer, markup.raw_children.at(0))?;
5101 write_text(writer, "c:v", value, &markup.value)?;
5102 emit_raw(writer, markup.raw_children.at(1))?;
5103 writer
5104 .write_event(Event::End(BytesEnd::new("c:pt")))
5105 .map_err(OxmlError::from)?;
5106 Ok(())
5107}
5108
5109fn cache_layout(markup: &ReferenceMarkup, value_count: usize) -> Result<(u32, Vec<u32>)> {
5110 if markup.point_indexes.len() == value_count
5111 && let Some(declared) = markup.declared_point_count
5112 {
5113 return Ok((declared, markup.point_indexes.clone()));
5114 }
5115 let declared = u32::try_from(value_count).map_err(|_| ChartError::InvalidValue {
5116 element: "c:ptCount".to_owned(),
5117 value: value_count.to_string(),
5118 })?;
5119 Ok((declared, (0..declared).collect()))
5120}
5121
5122fn write_text(
5123 writer: &mut Writer<Vec<u8>>,
5124 tag: &str,
5125 value: &str,
5126 markup: &TextMarkup,
5127) -> Result<()> {
5128 let mut start = BytesStart::new(tag);
5129 push_attributes(&mut start, &markup.raw_attributes);
5130 writer
5131 .write_event(Event::Start(start))
5132 .map_err(OxmlError::from)?;
5133 emit_raw(writer, markup.raw_children.at(0))?;
5134 writer
5135 .write_event(Event::Text(BytesText::new(value)))
5136 .map_err(OxmlError::from)?;
5137 emit_raw(writer, markup.raw_children.at(1))?;
5138 writer
5139 .write_event(Event::End(BytesEnd::new(tag)))
5140 .map_err(OxmlError::from)?;
5141 Ok(())
5142}
5143
5144fn write_wrapper_start(
5145 writer: &mut Writer<Vec<u8>>,
5146 tag: &str,
5147 markup: &WrapperMarkup,
5148) -> Result<()> {
5149 let mut start = BytesStart::new(tag);
5150 push_attributes(&mut start, &markup.raw_attributes);
5151 writer
5152 .write_event(Event::Start(start))
5153 .map_err(OxmlError::from)?;
5154 emit_raw(writer, markup.raw_children.at(0))
5155}
5156
5157fn write_wrapper_end(
5158 writer: &mut Writer<Vec<u8>>,
5159 tag: &str,
5160 markup: &WrapperMarkup,
5161) -> Result<()> {
5162 emit_raw(writer, markup.raw_children.at(1))?;
5163 writer
5164 .write_event(Event::End(BytesEnd::new(tag)))
5165 .map_err(OxmlError::from)?;
5166 Ok(())
5167}
5168
5169fn validate_formula(formula: &str, element: &str) -> Result<()> {
5170 if formula.trim().is_empty() {
5171 return Err(ChartError::InvalidValue {
5172 element: element.to_owned(),
5173 value: formula.to_owned(),
5174 });
5175 }
5176 validate_xml_text(formula, element)
5177}
5178
5179fn validate_format_code(format_code: &str) -> Result<()> {
5180 if format_code.is_empty() {
5181 return Err(ChartError::InvalidValue {
5182 element: "c:formatCode".to_owned(),
5183 value: format_code.to_owned(),
5184 });
5185 }
5186 validate_xml_text(format_code, "c:formatCode")
5187}
5188
5189fn validate_xml_text(value: &str, element: &str) -> Result<()> {
5190 if value.chars().all(|character| {
5191 matches!(
5192 character,
5193 '\u{9}' | '\u{A}' | '\u{D}' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}'
5194 )
5195 }) {
5196 return Ok(());
5197 }
5198 Err(ChartError::InvalidValue {
5199 element: element.to_owned(),
5200 value: value.to_owned(),
5201 })
5202}
5203
5204fn validate_numeric_values(values: &[f64]) -> Result<()> {
5205 if let Some(value) = values.iter().find(|value| !value.is_finite()) {
5206 return Err(ChartError::InvalidValue {
5207 element: "c:v".to_owned(),
5208 value: value.to_string(),
5209 });
5210 }
5211 Ok(())
5212}
5213
5214fn validate_point_count(count: usize, element: &str) -> Result<()> {
5215 u32::try_from(count)
5216 .map(|_| ())
5217 .map_err(|_| ChartError::InvalidValue {
5218 element: element.to_owned(),
5219 value: count.to_string(),
5220 })
5221}
5222
5223fn parse_u32_scalar(xml: &[u8], local: &str) -> Result<(u32, ScalarMarkup)> {
5224 let (value, markup) = scalar_value(xml, local)?;
5225 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
5226 let parsed = value
5227 .parse::<u32>()
5228 .map_err(|_| invalid_attribute(local, "val", value))?;
5229 Ok((parsed, markup))
5230}
5231
5232fn required_u32_attribute(element: &BytesStart<'_>, local: &str, attribute: &[u8]) -> Result<u32> {
5233 let value = attribute_value(element, attribute)?.ok_or_else(|| {
5234 invalid_attribute(
5235 local,
5236 &String::from_utf8_lossy(attribute),
5237 "<missing>".to_owned(),
5238 )
5239 })?;
5240 value
5241 .parse::<u32>()
5242 .map_err(|_| invalid_attribute(local, &String::from_utf8_lossy(attribute), value))
5243}
5244
5245fn chart_root_prefix(element: &BytesStart<'_>) -> Result<Vec<u8>> {
5246 let name = element.name();
5247 let qualified = name.as_ref();
5248 let prefix = qualified
5249 .iter()
5250 .position(|byte| *byte == b':')
5251 .map(|position| qualified[..position].to_vec())
5252 .unwrap_or_default();
5253 let declaration = if prefix.is_empty() {
5254 b"xmlns".to_vec()
5255 } else {
5256 let mut declaration = b"xmlns:".to_vec();
5257 declaration.extend_from_slice(&prefix);
5258 declaration
5259 };
5260 if let Some(namespace) = attribute_value(element, &declaration)?
5261 && namespace != C_NS
5262 {
5263 return Err(invalid_attribute(
5264 &element_name(element),
5265 "namespace",
5266 namespace,
5267 ));
5268 }
5269 reject_conflicting_prefix(element, b"c", C_NS)?;
5270 Ok(prefix)
5271}
5272
5273fn typed_rewrite_bindings(
5274 element: &BytesStart<'_>,
5275 inherited: &NamespaceBindings,
5276) -> Result<NamespaceBindings> {
5277 chart_root_prefix(element)?;
5278 if !element_is_in_namespace(element, C_NS, inherited)? {
5279 return Err(ChartError::UnexpectedElement(element_name(element)));
5280 }
5281 let bindings = chart_bindings(inherited, element)?;
5282 require_fixed_namespace(&bindings, b"c", C_NS, element)?;
5283 Ok(bindings)
5284}
5285
5286fn chart_namespace_defaults() -> NamespaceBindings {
5287 vec![
5288 (b"c".to_vec(), C_NS.to_owned()),
5289 (b"a".to_vec(), A_NS.to_owned()),
5290 (b"r".to_vec(), R_NS.to_owned()),
5291 ]
5292}
5293
5294fn chart_bindings(
5295 inherited: &NamespaceBindings,
5296 element: &BytesStart<'_>,
5297) -> Result<NamespaceBindings> {
5298 let mut bindings = chart_namespace_defaults();
5299 for (prefix, namespace) in inherited
5300 .iter()
5301 .cloned()
5302 .chain(namespace_bindings(element)?)
5303 {
5304 upsert_namespace_binding(&mut bindings, prefix, namespace);
5305 }
5306 Ok(bindings)
5307}
5308
5309fn root_chart_bindings(
5310 xml: &[u8],
5311 local: &[u8],
5312 inherited: &NamespaceBindings,
5313) -> Result<NamespaceBindings> {
5314 let mut reader = Reader::from_reader(xml);
5315 let mut buffer = Vec::new();
5316 loop {
5317 match reader
5318 .read_event_into(&mut buffer)
5319 .map_err(OxmlError::from)?
5320 {
5321 Event::Start(element) | Event::Empty(element)
5322 if matches_local_name(element.name().as_ref(), local) =>
5323 {
5324 if !element_is_in_namespace(&element, C_NS, inherited)? {
5325 return Err(ChartError::UnexpectedElement(element_name(&element)));
5326 }
5327 return chart_bindings(inherited, &element);
5328 }
5329 Event::Start(element) | Event::Empty(element) => {
5330 return Err(ChartError::UnexpectedElement(element_name(&element)));
5331 }
5332 Event::Eof => {
5333 return Err(ChartError::MissingElement(format!(
5334 "c:{}",
5335 String::from_utf8_lossy(local)
5336 )));
5337 }
5338 _ => {}
5339 }
5340 buffer.clear();
5341 }
5342}
5343
5344fn chart_child_local(
5345 element: &BytesStart<'_>,
5346 inherited: &NamespaceBindings,
5347) -> Result<Option<Vec<u8>>> {
5348 Ok(element_is_in_namespace(element, C_NS, inherited)?
5349 .then(|| local_name(element.name().as_ref()).to_vec()))
5350}
5351
5352fn capture_fixed_root_attributes(
5353 start: &BytesStart<'_>,
5354 fixed: &[&str],
5355) -> Result<(XmlAttributes, XmlAttributes)> {
5356 let mut attributes = Vec::new();
5357 let mut namespaces = Vec::new();
5358 for attribute in start.attributes() {
5359 let attribute = attribute.map_err(OxmlError::from)?;
5360 let name = std::str::from_utf8(attribute.key.as_ref())
5361 .map_err(OxmlError::from)?
5362 .to_owned();
5363 let value = attribute
5364 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
5365 .map_err(OxmlError::from)?
5366 .into_owned();
5367 if fixed.contains(&name.as_str()) {
5368 continue;
5369 }
5370 if name == "xmlns" || name.starts_with("xmlns:") {
5371 namespaces.push((name, value));
5372 } else {
5373 attributes.push((name, value));
5374 }
5375 }
5376 Ok((attributes, namespaces))
5377}
5378
5379fn require_fixed_namespace(
5380 bindings: &NamespaceBindings,
5381 prefix: &[u8],
5382 expected: &str,
5383 element: &BytesStart<'_>,
5384) -> Result<()> {
5385 let actual = bindings
5386 .iter()
5387 .find(|(candidate, _)| candidate.as_slice() == prefix)
5388 .map(|(_, namespace)| namespace.as_str());
5389 if actual == Some(expected) {
5390 return Ok(());
5391 }
5392 Err(invalid_attribute(
5393 &element_name(element),
5394 &format!("xmlns:{}", String::from_utf8_lossy(prefix)),
5395 actual.unwrap_or("<missing>").to_owned(),
5396 ))
5397}
5398
5399fn standalone_namespace_declarations(bindings: &NamespaceBindings) -> Result<XmlAttributes> {
5400 bindings
5401 .iter()
5402 .filter(|(prefix, _)| prefix != b"c" && prefix != b"a" && prefix != b"r")
5403 .map(|(prefix, namespace)| {
5404 let prefix = std::str::from_utf8(prefix).map_err(OxmlError::from)?;
5405 let name = if prefix.is_empty() {
5406 "xmlns".to_owned()
5407 } else {
5408 format!("xmlns:{prefix}")
5409 };
5410 Ok((name, namespace.clone()))
5411 })
5412 .collect()
5413}
5414
5415fn capture_fixed_attributes(start: &BytesStart<'_>, fixed: &[&str]) -> Result<XmlAttributes> {
5416 let mut attributes = Vec::new();
5417 for attribute in start.attributes() {
5418 let attribute = attribute.map_err(OxmlError::from)?;
5419 let name = std::str::from_utf8(attribute.key.as_ref())
5420 .map_err(OxmlError::from)?
5421 .to_owned();
5422 if fixed.contains(&name.as_str()) {
5423 continue;
5424 }
5425 let value = attribute
5426 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
5427 .map_err(OxmlError::from)?
5428 .into_owned();
5429 attributes.push((name, value));
5430 }
5431 Ok(attributes)
5432}
5433
5434#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
5439pub struct AxisId(i64);
5440
5441impl AxisId {
5442 pub fn new(value: i64) -> Result<Self> {
5443 if (i64::from(i32::MIN)..=i64::from(u32::MAX)).contains(&value) {
5444 Ok(Self(value))
5445 } else {
5446 Err(ChartError::InvalidValue {
5447 element: "c:axId".to_owned(),
5448 value: value.to_string(),
5449 })
5450 }
5451 }
5452
5453 pub const fn value(self) -> i64 {
5454 self.0
5455 }
5456}
5457
5458#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5460pub enum BarDirection {
5461 Bar,
5462 Column,
5463}
5464
5465impl BarDirection {
5466 fn parse(value: &str) -> Option<Self> {
5467 match value {
5468 "bar" => Some(Self::Bar),
5469 "col" => Some(Self::Column),
5470 _ => None,
5471 }
5472 }
5473
5474 const fn as_str(self) -> &'static str {
5475 match self {
5476 Self::Bar => "bar",
5477 Self::Column => "col",
5478 }
5479 }
5480}
5481
5482#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5484pub enum BarGrouping {
5485 Clustered,
5486 PercentStacked,
5487 Stacked,
5488 Standard,
5489}
5490
5491impl BarGrouping {
5492 fn parse(value: &str) -> Option<Self> {
5493 match value {
5494 "clustered" => Some(Self::Clustered),
5495 "percentStacked" => Some(Self::PercentStacked),
5496 "stacked" => Some(Self::Stacked),
5497 "standard" => Some(Self::Standard),
5498 _ => None,
5499 }
5500 }
5501
5502 const fn as_str(self) -> &'static str {
5503 match self {
5504 Self::Clustered => "clustered",
5505 Self::PercentStacked => "percentStacked",
5506 Self::Stacked => "stacked",
5507 Self::Standard => "standard",
5508 }
5509 }
5510}
5511
5512#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5514pub enum Grouping {
5515 PercentStacked,
5516 Stacked,
5517 Standard,
5518}
5519
5520impl Grouping {
5521 fn parse(value: &str) -> Option<Self> {
5522 match value {
5523 "percentStacked" => Some(Self::PercentStacked),
5524 "stacked" => Some(Self::Stacked),
5525 "standard" => Some(Self::Standard),
5526 _ => None,
5527 }
5528 }
5529
5530 const fn as_str(self) -> &'static str {
5531 match self {
5532 Self::PercentStacked => "percentStacked",
5533 Self::Stacked => "stacked",
5534 Self::Standard => "standard",
5535 }
5536 }
5537}
5538
5539#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5541pub enum ScatterStyle {
5542 Line,
5543 LineMarker,
5544 Marker,
5545 None,
5546 Smooth,
5547 SmoothMarker,
5548}
5549
5550impl ScatterStyle {
5551 fn parse(value: &str) -> Option<Self> {
5552 match value {
5553 "line" => Some(Self::Line),
5554 "lineMarker" => Some(Self::LineMarker),
5555 "marker" => Some(Self::Marker),
5556 "none" => Some(Self::None),
5557 "smooth" => Some(Self::Smooth),
5558 "smoothMarker" => Some(Self::SmoothMarker),
5559 _ => None,
5560 }
5561 }
5562
5563 const fn as_str(self) -> &'static str {
5564 match self {
5565 Self::Line => "line",
5566 Self::LineMarker => "lineMarker",
5567 Self::Marker => "marker",
5568 Self::None => "none",
5569 Self::Smooth => "smooth",
5570 Self::SmoothMarker => "smoothMarker",
5571 }
5572 }
5573}
5574
5575#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5577pub enum RadarStyle {
5578 Filled,
5579 Marker,
5580 Standard,
5581}
5582
5583impl RadarStyle {
5584 fn parse(value: &str) -> Option<Self> {
5585 match value {
5586 "filled" => Some(Self::Filled),
5587 "marker" => Some(Self::Marker),
5588 "standard" => Some(Self::Standard),
5589 _ => None,
5590 }
5591 }
5592
5593 const fn as_str(self) -> &'static str {
5594 match self {
5595 Self::Filled => "filled",
5596 Self::Marker => "marker",
5597 Self::Standard => "standard",
5598 }
5599 }
5600}
5601
5602#[derive(Clone, Debug, PartialEq)]
5604pub enum Plot {
5605 Bar {
5606 direction: BarDirection,
5607 grouping: BarGrouping,
5608 gap_width: u16,
5609 overlap: i8,
5610 series: Vec<Series>,
5611 data_labels: Option<CT_DLbls>,
5612 axis_ids: [AxisId; 2],
5613 },
5614 Line {
5615 grouping: Grouping,
5616 marker: bool,
5617 smooth: bool,
5618 series: Vec<Series>,
5619 data_labels: Option<CT_DLbls>,
5620 axis_ids: [AxisId; 2],
5621 },
5622 Pie {
5623 first_slice_angle: u16,
5624 series: Vec<Series>,
5625 data_labels: Option<CT_DLbls>,
5626 },
5627 Doughnut {
5628 first_slice_angle: u16,
5629 hole_size: u8,
5630 series: Vec<Series>,
5631 data_labels: Option<CT_DLbls>,
5632 },
5633 Area {
5634 grouping: Grouping,
5635 series: Vec<Series>,
5636 data_labels: Option<CT_DLbls>,
5637 axis_ids: [AxisId; 2],
5638 },
5639 Scatter {
5640 style: ScatterStyle,
5641 series: Vec<Series>,
5642 data_labels: Option<CT_DLbls>,
5643 axis_ids: [AxisId; 2],
5644 },
5645 Radar {
5646 style: RadarStyle,
5647 series: Vec<Series>,
5648 data_labels: Option<CT_DLbls>,
5649 axis_ids: [AxisId; 2],
5650 },
5651}
5652
5653impl Plot {
5654 pub fn bar(
5655 direction: BarDirection,
5656 grouping: BarGrouping,
5657 series: Vec<Series>,
5658 axis_ids: [AxisId; 2],
5659 ) -> Result<Self> {
5660 let plot = Self::Bar {
5661 direction,
5662 grouping,
5663 gap_width: 150,
5664 overlap: 0,
5665 series,
5666 data_labels: None,
5667 axis_ids,
5668 };
5669 plot.validate()?;
5670 Ok(plot)
5671 }
5672
5673 pub fn line(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
5674 let plot = Self::Line {
5675 grouping,
5676 marker: false,
5677 smooth: false,
5678 series,
5679 data_labels: None,
5680 axis_ids,
5681 };
5682 plot.validate()?;
5683 Ok(plot)
5684 }
5685
5686 pub fn pie(series: Vec<Series>) -> Result<Self> {
5687 let plot = Self::Pie {
5688 first_slice_angle: 0,
5689 series,
5690 data_labels: None,
5691 };
5692 plot.validate()?;
5693 Ok(plot)
5694 }
5695
5696 pub fn doughnut(series: Vec<Series>) -> Result<Self> {
5697 let plot = Self::Doughnut {
5698 first_slice_angle: 0,
5699 hole_size: 50,
5700 series,
5701 data_labels: None,
5702 };
5703 plot.validate()?;
5704 Ok(plot)
5705 }
5706
5707 pub fn area(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
5708 let plot = Self::Area {
5709 grouping,
5710 series,
5711 data_labels: None,
5712 axis_ids,
5713 };
5714 plot.validate()?;
5715 Ok(plot)
5716 }
5717
5718 pub fn scatter(
5719 style: ScatterStyle,
5720 series: Vec<Series>,
5721 axis_ids: [AxisId; 2],
5722 ) -> Result<Self> {
5723 let plot = Self::Scatter {
5724 style,
5725 series,
5726 data_labels: None,
5727 axis_ids,
5728 };
5729 plot.validate()?;
5730 Ok(plot)
5731 }
5732
5733 pub fn radar(style: RadarStyle, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
5734 let plot = Self::Radar {
5735 style,
5736 series,
5737 data_labels: None,
5738 axis_ids,
5739 };
5740 plot.validate()?;
5741 Ok(plot)
5742 }
5743
5744 fn axis_ids(&self) -> Option<[AxisId; 2]> {
5745 match self {
5746 Self::Bar { axis_ids, .. }
5747 | Self::Line { axis_ids, .. }
5748 | Self::Area { axis_ids, .. }
5749 | Self::Scatter { axis_ids, .. }
5750 | Self::Radar { axis_ids, .. } => Some(*axis_ids),
5751 Self::Pie { .. } | Self::Doughnut { .. } => None,
5752 }
5753 }
5754
5755 fn validate(&self) -> Result<()> {
5756 let (series, labels, axis_ids) = match self {
5757 Self::Bar {
5758 gap_width,
5759 overlap,
5760 series,
5761 data_labels,
5762 axis_ids,
5763 ..
5764 } => {
5765 if *gap_width > 500 {
5766 return Err(ChartError::InvalidValue {
5767 element: "c:gapWidth".to_owned(),
5768 value: gap_width.to_string(),
5769 });
5770 }
5771 if !(-100..=100).contains(overlap) {
5772 return Err(ChartError::InvalidValue {
5773 element: "c:overlap".to_owned(),
5774 value: overlap.to_string(),
5775 });
5776 }
5777 (series, data_labels, Some(axis_ids))
5778 }
5779 Self::Line {
5780 series,
5781 data_labels,
5782 axis_ids,
5783 ..
5784 } => (series, data_labels, Some(axis_ids)),
5785 Self::Pie {
5786 first_slice_angle,
5787 series,
5788 data_labels,
5789 } => {
5790 if *first_slice_angle > 360 {
5791 return Err(ChartError::InvalidValue {
5792 element: "c:firstSliceAng".to_owned(),
5793 value: first_slice_angle.to_string(),
5794 });
5795 }
5796 (series, data_labels, None)
5797 }
5798 Self::Doughnut {
5799 first_slice_angle,
5800 hole_size,
5801 series,
5802 data_labels,
5803 } => {
5804 if *first_slice_angle > 360 {
5805 return Err(ChartError::InvalidValue {
5806 element: "c:firstSliceAng".to_owned(),
5807 value: first_slice_angle.to_string(),
5808 });
5809 }
5810 if !(10..=90).contains(hole_size) {
5811 return Err(ChartError::InvalidValue {
5812 element: "c:holeSize".to_owned(),
5813 value: hole_size.to_string(),
5814 });
5815 }
5816 (series, data_labels, None)
5817 }
5818 Self::Area {
5819 series,
5820 data_labels,
5821 axis_ids,
5822 ..
5823 }
5824 | Self::Radar {
5825 series,
5826 data_labels,
5827 axis_ids,
5828 ..
5829 } => (series, data_labels, Some(axis_ids)),
5830 Self::Scatter {
5831 series,
5832 data_labels,
5833 axis_ids,
5834 ..
5835 } => {
5836 for item in series {
5837 if !matches!(item.categories, Some(AxisData::Numeric(_))) {
5838 return Err(ChartError::InvalidValue {
5839 element: "c:xVal".to_owned(),
5840 value: "scatter series requires numeric x values".to_owned(),
5841 });
5842 }
5843 }
5844 (series, data_labels, Some(axis_ids))
5845 }
5846 };
5847 if series.is_empty() {
5848 return Err(ChartError::MissingElement("c:ser".to_owned()));
5849 }
5850 let scatter_plot = matches!(self, Self::Scatter { .. });
5851 for item in series {
5852 if item.bubble_size.is_some() || item.opaque_bubble_size {
5853 return Err(ChartError::InvalidValue {
5854 element: "c:bubbleSize".to_owned(),
5855 value: "bubble size is only valid in an opaque bubble plot".to_owned(),
5856 });
5857 }
5858 if item.uses_scatter_wrappers && !scatter_plot {
5859 return Err(ChartError::InvalidValue {
5860 element: "c:ser".to_owned(),
5861 value: "x/y wrappers are only valid in scatter plots".to_owned(),
5862 });
5863 }
5864 if scatter_plot && item.parsed_from_xml && !item.uses_scatter_wrappers {
5865 return Err(ChartError::InvalidValue {
5866 element: "c:ser".to_owned(),
5867 value: "scatter plots require xVal/yVal wrappers".to_owned(),
5868 });
5869 }
5870 }
5871 if let Some(axis_ids) = axis_ids {
5872 if axis_ids[0] == axis_ids[1] {
5873 return Err(ChartError::DuplicateElement(format!(
5874 "c:axId {}",
5875 axis_ids[0].value()
5876 )));
5877 }
5878 for id in axis_ids {
5879 AxisId::new(id.value())?;
5880 }
5881 }
5882 if let Some(labels) = labels {
5883 labels.validate()?;
5884 }
5885 for item in series {
5886 item.values.validate()?;
5887 }
5888 Ok(())
5889 }
5890}
5891
5892#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
5894pub enum AxisKind {
5895 Category,
5896 Value,
5897 Date,
5898 Series,
5899}
5900
5901impl AxisKind {
5902 fn parse(local: &[u8]) -> Option<Self> {
5903 match local {
5904 b"catAx" => Some(Self::Category),
5905 b"valAx" => Some(Self::Value),
5906 b"dateAx" => Some(Self::Date),
5907 b"serAx" => Some(Self::Series),
5908 _ => None,
5909 }
5910 }
5911
5912 const fn local(self) -> &'static str {
5913 match self {
5914 Self::Category => "catAx",
5915 Self::Value => "valAx",
5916 Self::Date => "dateAx",
5917 Self::Series => "serAx",
5918 }
5919 }
5920}
5921
5922#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
5924pub enum Orientation {
5925 #[default]
5926 MinMax,
5927 MaxMin,
5928}
5929
5930impl Orientation {
5931 fn parse(value: &str) -> Option<Self> {
5932 match value {
5933 "minMax" => Some(Self::MinMax),
5934 "maxMin" => Some(Self::MaxMin),
5935 _ => None,
5936 }
5937 }
5938
5939 const fn as_str(self) -> &'static str {
5940 match self {
5941 Self::MinMax => "minMax",
5942 Self::MaxMin => "maxMin",
5943 }
5944 }
5945}
5946
5947#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5949pub enum AxisPosition {
5950 Bottom,
5951 Left,
5952 Right,
5953 Top,
5954}
5955
5956impl AxisPosition {
5957 fn parse(value: &str) -> Option<Self> {
5958 match value {
5959 "b" => Some(Self::Bottom),
5960 "l" => Some(Self::Left),
5961 "r" => Some(Self::Right),
5962 "t" => Some(Self::Top),
5963 _ => None,
5964 }
5965 }
5966
5967 const fn as_str(self) -> &'static str {
5968 match self {
5969 Self::Bottom => "b",
5970 Self::Left => "l",
5971 Self::Right => "r",
5972 Self::Top => "t",
5973 }
5974 }
5975}
5976
5977#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
5979pub enum TickMark {
5980 Cross,
5981 Inside,
5982 #[default]
5983 None,
5984 Outside,
5985}
5986
5987impl TickMark {
5988 fn parse(value: &str) -> Option<Self> {
5989 match value {
5990 "cross" => Some(Self::Cross),
5991 "in" => Some(Self::Inside),
5992 "none" => Some(Self::None),
5993 "out" => Some(Self::Outside),
5994 _ => None,
5995 }
5996 }
5997
5998 const fn as_str(self) -> &'static str {
5999 match self {
6000 Self::Cross => "cross",
6001 Self::Inside => "in",
6002 Self::None => "none",
6003 Self::Outside => "out",
6004 }
6005 }
6006}
6007
6008#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6010pub enum TickLabelPosition {
6011 High,
6012 Low,
6013 #[default]
6014 NextTo,
6015 None,
6016}
6017
6018impl TickLabelPosition {
6019 fn parse(value: &str) -> Option<Self> {
6020 match value {
6021 "high" => Some(Self::High),
6022 "low" => Some(Self::Low),
6023 "nextTo" => Some(Self::NextTo),
6024 "none" => Some(Self::None),
6025 _ => None,
6026 }
6027 }
6028
6029 const fn as_str(self) -> &'static str {
6030 match self {
6031 Self::High => "high",
6032 Self::Low => "low",
6033 Self::NextTo => "nextTo",
6034 Self::None => "none",
6035 }
6036 }
6037}
6038
6039#[derive(Clone, Debug)]
6041pub struct Scaling {
6042 pub log_base: Option<f64>,
6043 pub orientation: Orientation,
6044 pub maximum: Option<f64>,
6045 pub minimum: Option<f64>,
6046 log_base_markup: Option<ScalarMarkup>,
6047 orientation_markup: Option<ScalarMarkup>,
6048 maximum_markup: Option<ScalarMarkup>,
6049 minimum_markup: Option<ScalarMarkup>,
6050 raw_attributes: XmlAttributes,
6051 raw_children: OrderedRawChildren,
6052}
6053
6054impl PartialEq for Scaling {
6055 fn eq(&self, other: &Self) -> bool {
6056 self.log_base == other.log_base
6057 && self.orientation == other.orientation
6058 && self.maximum == other.maximum
6059 && self.minimum == other.minimum
6060 && optional_scalar_markup_eq(&self.log_base_markup, &other.log_base_markup)
6061 && optional_scalar_markup_eq(&self.orientation_markup, &other.orientation_markup)
6062 && optional_scalar_markup_eq(&self.maximum_markup, &other.maximum_markup)
6063 && optional_scalar_markup_eq(&self.minimum_markup, &other.minimum_markup)
6064 && self.raw_attributes == other.raw_attributes
6065 && self.raw_children == other.raw_children
6066 }
6067}
6068
6069impl Default for Scaling {
6070 fn default() -> Self {
6071 Self {
6072 log_base: None,
6073 orientation: Orientation::MinMax,
6074 maximum: None,
6075 minimum: None,
6076 log_base_markup: None,
6077 orientation_markup: None,
6078 maximum_markup: None,
6079 minimum_markup: None,
6080 raw_attributes: Vec::new(),
6081 raw_children: OrderedRawChildren::default(),
6082 }
6083 }
6084}
6085
6086#[derive(Clone, Debug, Default, PartialEq)]
6088pub struct ChartLines {
6089 pub sp_pr: Option<CT_ShapeProperties>,
6090 raw_attributes: XmlAttributes,
6091 raw_children: OrderedRawChildren,
6092}
6093
6094#[derive(Clone, Debug, Eq, PartialEq)]
6096pub struct NumberFormat {
6097 pub format_code: String,
6098 pub source_linked: bool,
6099 raw_attributes: XmlAttributes,
6100 raw_content: Vec<Vec<u8>>,
6101}
6102
6103#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6105pub enum DataLabelPosition {
6106 BestFit,
6107 Bottom,
6108 Center,
6109 InsideBase,
6110 InsideEnd,
6111 Left,
6112 OutsideEnd,
6113 Right,
6114 Top,
6115}
6116
6117impl DataLabelPosition {
6118 fn parse(value: &str) -> Option<Self> {
6119 match value {
6120 "bestFit" => Some(Self::BestFit),
6121 "b" => Some(Self::Bottom),
6122 "ctr" => Some(Self::Center),
6123 "inBase" => Some(Self::InsideBase),
6124 "inEnd" => Some(Self::InsideEnd),
6125 "l" => Some(Self::Left),
6126 "outEnd" => Some(Self::OutsideEnd),
6127 "r" => Some(Self::Right),
6128 "t" => Some(Self::Top),
6129 _ => None,
6130 }
6131 }
6132
6133 const fn as_str(self) -> &'static str {
6134 match self {
6135 Self::BestFit => "bestFit",
6136 Self::Bottom => "b",
6137 Self::Center => "ctr",
6138 Self::InsideBase => "inBase",
6139 Self::InsideEnd => "inEnd",
6140 Self::Left => "l",
6141 Self::OutsideEnd => "outEnd",
6142 Self::Right => "r",
6143 Self::Top => "t",
6144 }
6145 }
6146}
6147
6148#[derive(Clone, Debug, Default, PartialEq)]
6150pub struct CT_DLbls {
6151 pub number_format: Option<NumberFormat>,
6152 pub position: Option<DataLabelPosition>,
6153 pub separator: Option<String>,
6154 pub show_legend_key: bool,
6155 pub show_value: bool,
6156 pub show_category_name: bool,
6157 pub show_series_name: bool,
6158 pub show_percent: bool,
6159 pub show_bubble_size: bool,
6160 position_markup: Option<ScalarMarkup>,
6161 show_legend_key_markup: Option<ScalarMarkup>,
6162 show_value_markup: Option<ScalarMarkup>,
6163 show_category_name_markup: Option<ScalarMarkup>,
6164 show_series_name_markup: Option<ScalarMarkup>,
6165 show_percent_markup: Option<ScalarMarkup>,
6166 show_bubble_size_markup: Option<ScalarMarkup>,
6167 separator_markup: Option<TextMarkup>,
6168 raw_attributes: XmlAttributes,
6169 namespace_declarations: XmlAttributes,
6170 raw_children: OrderedRawChildren,
6171}
6172
6173#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6175pub enum DispBlanksAs {
6176 #[default]
6177 Gap,
6178 Zero,
6179 Span,
6180}
6181
6182impl DispBlanksAs {
6183 fn parse(value: &str) -> Option<Self> {
6184 match value {
6185 "gap" => Some(Self::Gap),
6186 "zero" => Some(Self::Zero),
6187 "span" => Some(Self::Span),
6188 _ => None,
6189 }
6190 }
6191
6192 const fn as_str(self) -> &'static str {
6193 match self {
6194 Self::Gap => "gap",
6195 Self::Zero => "zero",
6196 Self::Span => "span",
6197 }
6198 }
6199}
6200
6201#[derive(Clone, Debug, Default, Eq, PartialEq)]
6203pub struct CT_Title {
6204 raw_attributes: Vec<(String, String)>,
6205 raw_children: OrderedRawChildren,
6206}
6207
6208#[derive(Clone, Debug, Default, PartialEq)]
6210struct PlotMarkup {
6211 raw_attributes: XmlAttributes,
6212 raw_children: OrderedRawChildren,
6213 direction: Option<ScalarMarkup>,
6214 grouping: Option<ScalarMarkup>,
6215 gap_width: Option<ScalarMarkup>,
6216 overlap: Option<ScalarMarkup>,
6217 marker: Option<ScalarMarkup>,
6218 smooth: Option<ScalarMarkup>,
6219 first_slice_angle: Option<ScalarMarkup>,
6220 hole_size: Option<ScalarMarkup>,
6221 style: Option<ScalarMarkup>,
6222 axis_ids: Vec<AxisIdMarkup>,
6223 original_series_keys: Vec<(u32, u32)>,
6224 original_axis_ids: Vec<AxisId>,
6225 parsed_bar: Option<bool>,
6226 parsed_remaining: Option<&'static str>,
6227}
6228
6229#[derive(Clone, Debug, PartialEq)]
6231pub struct CT_PlotArea {
6232 raw_attributes: Vec<(String, String)>,
6233 raw_children: OrderedRawChildren,
6234 namespace_bindings: NamespaceBindings,
6235 plots: Option<Vec<Plot>>,
6236 plot_markup: Vec<PlotMarkup>,
6237 axes: Vec<Axis>,
6238}
6239
6240#[derive(Clone, Debug, Default, Eq, PartialEq)]
6242pub struct CT_Legend {
6243 raw_attributes: Vec<(String, String)>,
6244 raw_children: OrderedRawChildren,
6245}
6246
6247#[derive(Clone, Debug, PartialEq)]
6248struct AxisIdMarkup {
6249 scalar: ScalarMarkup,
6250 parsed: AxisId,
6251 lexical: String,
6252}
6253
6254#[derive(Clone, Debug)]
6256pub struct Axis {
6257 pub kind: AxisKind,
6258 pub id: AxisId,
6259 pub scaling: Scaling,
6260 pub deleted: bool,
6261 pub position: AxisPosition,
6262 pub major_gridlines: Option<ChartLines>,
6263 pub minor_gridlines: Option<ChartLines>,
6264 pub title: Option<CT_Title>,
6265 pub number_format: Option<NumberFormat>,
6266 pub major_tick_mark: TickMark,
6267 pub minor_tick_mark: TickMark,
6268 pub tick_label_position: TickLabelPosition,
6269 pub sp_pr: Option<CT_ShapeProperties>,
6270 pub tx_pr: Option<CT_TextBody>,
6271 pub cross_axis: AxisId,
6272 parsed_kind: Option<AxisKind>,
6273 id_markup: AxisIdMarkup,
6274 delete_markup: Option<ScalarMarkup>,
6275 position_markup: ScalarMarkup,
6276 major_tick_mark_markup: Option<ScalarMarkup>,
6277 minor_tick_mark_markup: Option<ScalarMarkup>,
6278 tick_label_position_markup: Option<ScalarMarkup>,
6279 cross_axis_markup: AxisIdMarkup,
6280 raw_attributes: XmlAttributes,
6281 namespace_declarations: XmlAttributes,
6282 raw_children: OrderedRawChildren,
6283}
6284
6285impl PartialEq for Axis {
6286 fn eq(&self, other: &Self) -> bool {
6287 self.kind == other.kind
6288 && self.id == other.id
6289 && axis_id_markup_eq(&self.id_markup, &other.id_markup)
6290 && self.scaling == other.scaling
6291 && self.deleted == other.deleted
6292 && optional_scalar_markup_eq(&self.delete_markup, &other.delete_markup)
6293 && self.position == other.position
6294 && self.position_markup == other.position_markup
6295 && self.major_gridlines == other.major_gridlines
6296 && self.minor_gridlines == other.minor_gridlines
6297 && self.title == other.title
6298 && self.number_format == other.number_format
6299 && self.major_tick_mark == other.major_tick_mark
6300 && optional_scalar_markup_eq(
6301 &self.major_tick_mark_markup,
6302 &other.major_tick_mark_markup,
6303 )
6304 && self.minor_tick_mark == other.minor_tick_mark
6305 && optional_scalar_markup_eq(
6306 &self.minor_tick_mark_markup,
6307 &other.minor_tick_mark_markup,
6308 )
6309 && self.tick_label_position == other.tick_label_position
6310 && optional_scalar_markup_eq(
6311 &self.tick_label_position_markup,
6312 &other.tick_label_position_markup,
6313 )
6314 && self.sp_pr == other.sp_pr
6315 && self.tx_pr == other.tx_pr
6316 && self.cross_axis == other.cross_axis
6317 && axis_id_markup_eq(&self.cross_axis_markup, &other.cross_axis_markup)
6318 && self.raw_attributes == other.raw_attributes
6319 && self.namespace_declarations == other.namespace_declarations
6320 && self.raw_children == other.raw_children
6321 }
6322}
6323
6324impl Axis {
6325 pub fn new(kind: AxisKind, id: AxisId, position: AxisPosition, cross_axis: AxisId) -> Self {
6326 Self {
6327 kind,
6328 id,
6329 scaling: Scaling::default(),
6330 deleted: false,
6331 position,
6332 major_gridlines: None,
6333 minor_gridlines: None,
6334 title: None,
6335 number_format: None,
6336 major_tick_mark: TickMark::None,
6337 minor_tick_mark: TickMark::None,
6338 tick_label_position: TickLabelPosition::NextTo,
6339 sp_pr: None,
6340 tx_pr: None,
6341 cross_axis,
6342 parsed_kind: None,
6343 id_markup: default_axis_id_markup(id),
6344 delete_markup: None,
6345 position_markup: ScalarMarkup::default(),
6346 major_tick_mark_markup: None,
6347 minor_tick_mark_markup: None,
6348 tick_label_position_markup: None,
6349 cross_axis_markup: default_axis_id_markup(cross_axis),
6350 raw_attributes: Vec::new(),
6351 namespace_declarations: Vec::new(),
6352 raw_children: OrderedRawChildren::default(),
6353 }
6354 }
6355
6356 pub fn from_xml(xml: &[u8]) -> Result<Self> {
6357 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
6358 }
6359
6360 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
6361 let mut reader = Reader::from_reader(xml);
6362 let mut buffer = Vec::new();
6363 loop {
6364 match reader
6365 .read_event_into(&mut buffer)
6366 .map_err(OxmlError::from)?
6367 {
6368 Event::Start(element) => {
6369 let name = element.name();
6370 let local = local_name(name.as_ref());
6371 let Some(kind) = AxisKind::parse(local) else {
6372 return Err(ChartError::UnexpectedElement(element_name(&element)));
6373 };
6374 chart_root_prefix(&element)?;
6375 if !element_is_in_namespace(&element, C_NS, inherited)? {
6376 return Err(ChartError::UnexpectedElement(element_name(&element)));
6377 }
6378 return Self::from_element(&mut reader, &element, inherited, kind);
6379 }
6380 Event::Empty(element) => {
6381 let name = element.name();
6382 let local = local_name(name.as_ref());
6383 if AxisKind::parse(local).is_some() {
6384 return Err(ChartError::MissingElement("c:axId".to_owned()));
6385 }
6386 return Err(ChartError::UnexpectedElement(element_name(&element)));
6387 }
6388 Event::Eof => return Err(ChartError::MissingElement("ChartML axis".to_owned())),
6389 _ => {}
6390 }
6391 buffer.clear();
6392 }
6393 }
6394
6395 fn from_element(
6396 reader: &mut Reader<&[u8]>,
6397 start: &BytesStart<'_>,
6398 inherited: &NamespaceBindings,
6399 kind: AxisKind,
6400 ) -> Result<Self> {
6401 reject_conflicting_prefix(start, b"a", A_NS)?;
6402 reject_conflicting_prefix(start, b"r", R_NS)?;
6403 let namespaces = chart_bindings(inherited, start)?;
6404 require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
6405 require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
6406 require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
6407 let (raw_attributes, _) =
6408 capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
6409 let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
6410 let mut state = AxisParseState::default();
6411 let mut buffer = Vec::new();
6412 loop {
6413 match reader
6414 .read_event_into(&mut buffer)
6415 .map_err(OxmlError::from)?
6416 {
6417 Event::Start(element) => {
6418 let name = chart_child_local(&element, &namespaces)?;
6419 let raw = capture_element(reader, &element)?;
6420 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
6421 }
6422 Event::Empty(element) => {
6423 let name = chart_child_local(&element, &namespaces)?;
6424 let raw = capture_empty_element(&element)?;
6425 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
6426 }
6427 event @ (Event::Text(_)
6428 | Event::CData(_)
6429 | Event::Comment(_)
6430 | Event::PI(_)
6431 | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
6432 Event::End(element)
6433 if matches_local_name(element.name().as_ref(), kind.local().as_bytes()) =>
6434 {
6435 break;
6436 }
6437 Event::Eof => return Err(missing_end(&format!("c:{}", kind.local()))),
6438 _ => {}
6439 }
6440 buffer.clear();
6441 }
6442 state.finish(kind, raw_attributes, namespace_declarations)
6443 }
6444
6445 pub fn to_xml(&self) -> Result<Vec<u8>> {
6446 self.validate()?;
6447 let mut writer = Writer::new(Vec::new());
6448 let root = format!("c:{}", self.kind.local());
6449 let mut start = BytesStart::new(&root);
6450 start.push_attribute(("xmlns:c", C_NS));
6451 start.push_attribute(("xmlns:a", A_NS));
6452 start.push_attribute(("xmlns:r", R_NS));
6453 push_attributes(&mut start, &self.namespace_declarations);
6454 push_attributes(&mut start, &self.raw_attributes);
6455 writer
6456 .write_event(Event::Start(start))
6457 .map_err(OxmlError::from)?;
6458 emit_raw(&mut writer, self.raw_children.at(0))?;
6459 write_axis_id(&mut writer, "c:axId", self.id, &self.id_markup)?;
6460 emit_raw(&mut writer, self.raw_children.at(1))?;
6461 self.scaling.write_xml(&mut writer)?;
6462 emit_raw(&mut writer, self.raw_children.at(2))?;
6463 if self.deleted || self.delete_markup.is_some() {
6464 write_scalar(
6465 &mut writer,
6466 "c:delete",
6467 bool_lexical(self.deleted),
6468 self.delete_markup.as_ref(),
6469 )?;
6470 }
6471 emit_raw(&mut writer, self.raw_children.at(3))?;
6472 write_scalar(
6473 &mut writer,
6474 "c:axPos",
6475 self.position.as_str(),
6476 Some(&self.position_markup),
6477 )?;
6478 emit_raw(&mut writer, self.raw_children.at(4))?;
6479 if let Some(lines) = &self.major_gridlines {
6480 lines.write_xml(&mut writer, "c:majorGridlines")?;
6481 }
6482 emit_raw(&mut writer, self.raw_children.at(5))?;
6483 if let Some(lines) = &self.minor_gridlines {
6484 lines.write_xml(&mut writer, "c:minorGridlines")?;
6485 }
6486 emit_raw(&mut writer, self.raw_children.at(6))?;
6487 if let Some(title) = &self.title {
6488 title.write_xml(&mut writer)?;
6489 }
6490 emit_raw(&mut writer, self.raw_children.at(7))?;
6491 if let Some(number_format) = &self.number_format {
6492 number_format.write_xml(&mut writer)?;
6493 }
6494 emit_raw(&mut writer, self.raw_children.at(8))?;
6495 if self.major_tick_mark != TickMark::None || self.major_tick_mark_markup.is_some() {
6496 write_scalar(
6497 &mut writer,
6498 "c:majorTickMark",
6499 self.major_tick_mark.as_str(),
6500 self.major_tick_mark_markup.as_ref(),
6501 )?;
6502 }
6503 emit_raw(&mut writer, self.raw_children.at(9))?;
6504 if self.minor_tick_mark != TickMark::None || self.minor_tick_mark_markup.is_some() {
6505 write_scalar(
6506 &mut writer,
6507 "c:minorTickMark",
6508 self.minor_tick_mark.as_str(),
6509 self.minor_tick_mark_markup.as_ref(),
6510 )?;
6511 }
6512 emit_raw(&mut writer, self.raw_children.at(10))?;
6513 if self.tick_label_position != TickLabelPosition::NextTo
6514 || self.tick_label_position_markup.is_some()
6515 {
6516 write_scalar(
6517 &mut writer,
6518 "c:tickLblPos",
6519 self.tick_label_position.as_str(),
6520 self.tick_label_position_markup.as_ref(),
6521 )?;
6522 }
6523 emit_raw(&mut writer, self.raw_children.at(11))?;
6524 if let Some(properties) = &self.sp_pr {
6525 properties.write_xml_as(&mut writer, "c:spPr")?;
6526 }
6527 emit_raw(&mut writer, self.raw_children.at(12))?;
6528 if let Some(text) = &self.tx_pr {
6529 text.write_xml_as(&mut writer, "c:txPr")?;
6530 }
6531 emit_raw(&mut writer, self.raw_children.at(13))?;
6532 write_axis_id(
6533 &mut writer,
6534 "c:crossAx",
6535 self.cross_axis,
6536 &self.cross_axis_markup,
6537 )?;
6538 emit_raw(&mut writer, self.raw_children.at(14))?;
6539 writer
6540 .write_event(Event::End(BytesEnd::new(&root)))
6541 .map_err(OxmlError::from)?;
6542 Ok(writer.into_inner())
6543 }
6544
6545 pub fn raw_children(&self) -> &OrderedRawChildren {
6546 &self.raw_children
6547 }
6548
6549 fn validate(&self) -> Result<()> {
6550 if let Some(parsed_kind) = self.parsed_kind
6551 && self.kind != parsed_kind
6552 {
6553 return Err(ChartError::InvalidValue {
6554 element: "ChartML axis root".to_owned(),
6555 value: format!(
6556 "parsed c:{} cannot be relabelled c:{}",
6557 parsed_kind.local(),
6558 self.kind.local()
6559 ),
6560 });
6561 }
6562 AxisId::new(self.id.value())?;
6563 AxisId::new(self.cross_axis.value())?;
6564 self.scaling.validate()?;
6565 if let Some(number_format) = &self.number_format {
6566 number_format.validate()?;
6567 }
6568 Ok(())
6569 }
6570}
6571
6572#[derive(Default)]
6573struct AxisParseState {
6574 id: Option<(AxisId, AxisIdMarkup)>,
6575 scaling: Option<Scaling>,
6576 deleted: Option<(bool, ScalarMarkup)>,
6577 position: Option<(AxisPosition, ScalarMarkup)>,
6578 major_gridlines: Option<ChartLines>,
6579 minor_gridlines: Option<ChartLines>,
6580 title: Option<CT_Title>,
6581 number_format: Option<NumberFormat>,
6582 major_tick_mark: Option<(TickMark, ScalarMarkup)>,
6583 minor_tick_mark: Option<(TickMark, ScalarMarkup)>,
6584 tick_label_position: Option<(TickLabelPosition, ScalarMarkup)>,
6585 sp_pr: Option<CT_ShapeProperties>,
6586 tx_pr: Option<CT_TextBody>,
6587 cross_axis: Option<(AxisId, AxisIdMarkup)>,
6588 raw_children: OrderedRawChildren,
6589 boundary: usize,
6590}
6591
6592impl AxisParseState {
6593 fn capture_event(&mut self, raw: Vec<u8>) {
6594 self.raw_children.push(self.boundary, raw);
6595 }
6596
6597 fn parse_child(
6598 &mut self,
6599 name: &[u8],
6600 raw: Vec<u8>,
6601 namespaces: &NamespaceBindings,
6602 ) -> Result<()> {
6603 match name {
6604 b"axId" => {
6605 set_once(&mut self.id, parse_axis_id_scalar(&raw, "axId")?, "c:axId")?;
6606 self.boundary = self.boundary.max(1);
6607 }
6608 b"scaling" => {
6609 set_once(
6610 &mut self.scaling,
6611 Scaling::from_xml(&raw, namespaces)?,
6612 "c:scaling",
6613 )?;
6614 self.boundary = self.boundary.max(2);
6615 }
6616 b"delete" => {
6617 set_once(
6618 &mut self.deleted,
6619 parse_bool_value(&raw, "delete")?,
6620 "c:delete",
6621 )?;
6622 self.boundary = self.boundary.max(3);
6623 }
6624 b"axPos" => {
6625 set_once(&mut self.position, parse_axis_position(&raw)?, "c:axPos")?;
6626 self.boundary = self.boundary.max(4);
6627 }
6628 b"majorGridlines" => {
6629 set_once(
6630 &mut self.major_gridlines,
6631 ChartLines::from_xml(&raw, b"majorGridlines", namespaces)?,
6632 "c:majorGridlines",
6633 )?;
6634 self.boundary = self.boundary.max(5);
6635 }
6636 b"minorGridlines" => {
6637 set_once(
6638 &mut self.minor_gridlines,
6639 ChartLines::from_xml(&raw, b"minorGridlines", namespaces)?,
6640 "c:minorGridlines",
6641 )?;
6642 self.boundary = self.boundary.max(6);
6643 }
6644 b"title" => {
6645 set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
6646 self.boundary = self.boundary.max(7);
6647 }
6648 b"numFmt" => {
6649 set_once(
6650 &mut self.number_format,
6651 NumberFormat::from_xml(&raw)?,
6652 "c:numFmt",
6653 )?;
6654 self.boundary = self.boundary.max(8);
6655 }
6656 b"majorTickMark" => {
6657 set_once(
6658 &mut self.major_tick_mark,
6659 parse_tick_mark(&raw, "majorTickMark")?,
6660 "c:majorTickMark",
6661 )?;
6662 self.boundary = self.boundary.max(9);
6663 }
6664 b"minorTickMark" => {
6665 set_once(
6666 &mut self.minor_tick_mark,
6667 parse_tick_mark(&raw, "minorTickMark")?,
6668 "c:minorTickMark",
6669 )?;
6670 self.boundary = self.boundary.max(10);
6671 }
6672 b"tickLblPos" => {
6673 set_once(
6674 &mut self.tick_label_position,
6675 parse_tick_label_position(&raw)?,
6676 "c:tickLblPos",
6677 )?;
6678 self.boundary = self.boundary.max(11);
6679 }
6680 b"spPr" => {
6681 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
6682 let properties = CT_ShapeProperties::from_xml(&raw)?;
6683 let mut writer = Writer::new(Vec::new());
6684 properties.write_xml_as(&mut writer, "c:spPr")?;
6685 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
6686 set_once(&mut self.sp_pr, properties, "c:spPr")?;
6687 self.boundary = self.boundary.max(12);
6688 }
6689 b"txPr" => {
6690 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
6691 let text = CT_TextBody::from_xml_as(&raw, b"txPr")?;
6692 let mut writer = Writer::new(Vec::new());
6693 text.write_xml_as(&mut writer, "c:txPr")?;
6694 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
6695 set_once(&mut self.tx_pr, text, "c:txPr")?;
6696 self.boundary = self.boundary.max(13);
6697 }
6698 b"crossAx" => {
6699 set_once(
6700 &mut self.cross_axis,
6701 parse_axis_id_scalar(&raw, "crossAx")?,
6702 "c:crossAx",
6703 )?;
6704 self.boundary = self.boundary.max(14);
6705 }
6706 _ => self.raw_children.push(self.boundary, raw),
6707 }
6708 Ok(())
6709 }
6710
6711 fn finish(
6712 self,
6713 kind: AxisKind,
6714 raw_attributes: XmlAttributes,
6715 namespace_declarations: XmlAttributes,
6716 ) -> Result<Axis> {
6717 let (id, id_markup) = self
6718 .id
6719 .ok_or_else(|| ChartError::MissingElement("c:axId".to_owned()))?;
6720 let scaling = self
6721 .scaling
6722 .ok_or_else(|| ChartError::MissingElement("c:scaling".to_owned()))?;
6723 let (position, position_markup) = self
6724 .position
6725 .ok_or_else(|| ChartError::MissingElement("c:axPos".to_owned()))?;
6726 let (cross_axis, cross_axis_markup) = self
6727 .cross_axis
6728 .ok_or_else(|| ChartError::MissingElement("c:crossAx".to_owned()))?;
6729 let (deleted, delete_markup) = self
6730 .deleted
6731 .map(|(value, markup)| (value, Some(markup)))
6732 .unwrap_or((false, None));
6733 let (major_tick_mark, major_tick_mark_markup) = self
6734 .major_tick_mark
6735 .map(|(value, markup)| (value, Some(markup)))
6736 .unwrap_or((TickMark::None, None));
6737 let (minor_tick_mark, minor_tick_mark_markup) = self
6738 .minor_tick_mark
6739 .map(|(value, markup)| (value, Some(markup)))
6740 .unwrap_or((TickMark::None, None));
6741 let (tick_label_position, tick_label_position_markup) = self
6742 .tick_label_position
6743 .map(|(value, markup)| (value, Some(markup)))
6744 .unwrap_or((TickLabelPosition::NextTo, None));
6745 let axis = Axis {
6746 kind,
6747 id,
6748 scaling,
6749 deleted,
6750 position,
6751 major_gridlines: self.major_gridlines,
6752 minor_gridlines: self.minor_gridlines,
6753 title: self.title,
6754 number_format: self.number_format,
6755 major_tick_mark,
6756 minor_tick_mark,
6757 tick_label_position,
6758 sp_pr: self.sp_pr,
6759 tx_pr: self.tx_pr,
6760 cross_axis,
6761 parsed_kind: Some(kind),
6762 id_markup,
6763 delete_markup,
6764 position_markup,
6765 major_tick_mark_markup,
6766 minor_tick_mark_markup,
6767 tick_label_position_markup,
6768 cross_axis_markup,
6769 raw_attributes,
6770 namespace_declarations,
6771 raw_children: raw_children_in_schema_order(&self.raw_children, 14),
6772 };
6773 axis.validate()?;
6774 Ok(axis)
6775 }
6776}
6777
6778impl Scaling {
6779 fn from_xml(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
6780 let mut reader = Reader::from_reader(xml);
6781 let mut buffer = Vec::new();
6782 loop {
6783 match reader
6784 .read_event_into(&mut buffer)
6785 .map_err(OxmlError::from)?
6786 {
6787 Event::Start(element)
6788 if matches_local_name(element.name().as_ref(), b"scaling") =>
6789 {
6790 let namespaces = typed_rewrite_bindings(&element, inherited)?;
6791 let raw_attributes = capture_attributes(&element)?;
6792 let mut log_base = None;
6793 let mut orientation = None;
6794 let mut maximum = None;
6795 let mut minimum = None;
6796 let mut raw_children = OrderedRawChildren::default();
6797 let mut boundary = 0usize;
6798 let mut inner = Vec::new();
6799 loop {
6800 match reader
6801 .read_event_into(&mut inner)
6802 .map_err(OxmlError::from)?
6803 {
6804 Event::Start(child) => {
6805 let name = chart_child_local(&child, &namespaces)?;
6806 let raw = capture_element(&mut reader, &child)?;
6807 parse_scaling_child(
6808 name.as_deref().unwrap_or_default(),
6809 raw,
6810 &mut log_base,
6811 &mut orientation,
6812 &mut maximum,
6813 &mut minimum,
6814 &mut raw_children,
6815 &mut boundary,
6816 )?;
6817 }
6818 Event::Empty(child) => {
6819 let name = chart_child_local(&child, &namespaces)?;
6820 let raw = capture_empty_element(&child)?;
6821 parse_scaling_child(
6822 name.as_deref().unwrap_or_default(),
6823 raw,
6824 &mut log_base,
6825 &mut orientation,
6826 &mut maximum,
6827 &mut minimum,
6828 &mut raw_children,
6829 &mut boundary,
6830 )?;
6831 }
6832 event @ (Event::Text(_)
6833 | Event::CData(_)
6834 | Event::Comment(_)
6835 | Event::PI(_)
6836 | Event::GeneralRef(_)) => {
6837 raw_children.push(boundary, capture_event(event)?);
6838 }
6839 Event::End(end)
6840 if matches_local_name(end.name().as_ref(), b"scaling") =>
6841 {
6842 let (log_base, log_base_markup) = optional_markup(log_base);
6843 let (orientation, orientation_markup) = orientation
6844 .map(|(value, markup)| (value, Some(markup)))
6845 .unwrap_or((Orientation::MinMax, None));
6846 let (maximum, maximum_markup) = optional_markup(maximum);
6847 let (minimum, minimum_markup) = optional_markup(minimum);
6848 let scaling = Self {
6849 log_base,
6850 orientation,
6851 maximum,
6852 minimum,
6853 log_base_markup,
6854 orientation_markup,
6855 maximum_markup,
6856 minimum_markup,
6857 raw_attributes,
6858 raw_children: raw_children_in_schema_order(&raw_children, 4),
6859 };
6860 scaling.validate()?;
6861 return Ok(scaling);
6862 }
6863 Event::Eof => return Err(missing_end("c:scaling")),
6864 _ => {}
6865 }
6866 inner.clear();
6867 }
6868 }
6869 Event::Empty(element)
6870 if matches_local_name(element.name().as_ref(), b"scaling") =>
6871 {
6872 typed_rewrite_bindings(&element, inherited)?;
6873 return Ok(Self {
6874 raw_attributes: capture_attributes(&element)?,
6875 ..Self::default()
6876 });
6877 }
6878 Event::Start(element) | Event::Empty(element) => {
6879 return Err(ChartError::UnexpectedElement(element_name(&element)));
6880 }
6881 Event::Eof => return Err(ChartError::MissingElement("c:scaling".to_owned())),
6882 _ => {}
6883 }
6884 buffer.clear();
6885 }
6886 }
6887
6888 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
6889 self.validate()?;
6890 let mut start = BytesStart::new("c:scaling");
6891 push_attributes(&mut start, &self.raw_attributes);
6892 writer
6893 .write_event(Event::Start(start))
6894 .map_err(OxmlError::from)?;
6895 emit_raw(writer, self.raw_children.at(0))?;
6896 if let Some(value) = self.log_base {
6897 write_scalar(
6898 writer,
6899 "c:logBase",
6900 &value.to_string(),
6901 self.log_base_markup.as_ref(),
6902 )?;
6903 }
6904 emit_raw(writer, self.raw_children.at(1))?;
6905 if self.orientation != Orientation::MinMax || self.orientation_markup.is_some() {
6906 write_scalar(
6907 writer,
6908 "c:orientation",
6909 self.orientation.as_str(),
6910 self.orientation_markup.as_ref(),
6911 )?;
6912 }
6913 emit_raw(writer, self.raw_children.at(2))?;
6914 if let Some(value) = self.maximum {
6915 write_scalar(
6916 writer,
6917 "c:max",
6918 &value.to_string(),
6919 self.maximum_markup.as_ref(),
6920 )?;
6921 }
6922 emit_raw(writer, self.raw_children.at(3))?;
6923 if let Some(value) = self.minimum {
6924 write_scalar(
6925 writer,
6926 "c:min",
6927 &value.to_string(),
6928 self.minimum_markup.as_ref(),
6929 )?;
6930 }
6931 emit_raw(writer, self.raw_children.at(4))?;
6932 writer
6933 .write_event(Event::End(BytesEnd::new("c:scaling")))
6934 .map_err(OxmlError::from)?;
6935 Ok(())
6936 }
6937
6938 fn validate(&self) -> Result<()> {
6939 if let Some(value) = self.log_base
6940 && (!value.is_finite() || !(2.0..=1000.0).contains(&value))
6941 {
6942 return Err(ChartError::InvalidValue {
6943 element: "c:logBase".to_owned(),
6944 value: value.to_string(),
6945 });
6946 }
6947 for (element, value) in [("c:max", self.maximum), ("c:min", self.minimum)] {
6948 if let Some(value) = value
6949 && !value.is_finite()
6950 {
6951 return Err(ChartError::InvalidValue {
6952 element: element.to_owned(),
6953 value: value.to_string(),
6954 });
6955 }
6956 }
6957 if let (Some(minimum), Some(maximum)) = (self.minimum, self.maximum)
6958 && minimum > maximum
6959 {
6960 return Err(ChartError::InvalidValue {
6961 element: "c:scaling".to_owned(),
6962 value: format!("minimum {minimum} exceeds maximum {maximum}"),
6963 });
6964 }
6965 Ok(())
6966 }
6967}
6968
6969#[allow(clippy::too_many_arguments)]
6970fn parse_scaling_child(
6971 name: &[u8],
6972 raw: Vec<u8>,
6973 log_base: &mut Option<(f64, ScalarMarkup)>,
6974 orientation: &mut Option<(Orientation, ScalarMarkup)>,
6975 maximum: &mut Option<(f64, ScalarMarkup)>,
6976 minimum: &mut Option<(f64, ScalarMarkup)>,
6977 raw_children: &mut OrderedRawChildren,
6978 boundary: &mut usize,
6979) -> Result<()> {
6980 match name {
6981 b"logBase" => {
6982 set_once(log_base, parse_f64_scalar(&raw, "logBase")?, "c:logBase")?;
6983 *boundary = (*boundary).max(1);
6984 }
6985 b"orientation" => {
6986 let (value, markup) = scalar_value(&raw, "orientation")?;
6987 let value = value
6988 .ok_or_else(|| invalid_attribute("orientation", "val", "<missing>".to_owned()))?;
6989 let parsed = Orientation::parse(&value)
6990 .ok_or_else(|| invalid_attribute("orientation", "val", value))?;
6991 set_once(orientation, (parsed, markup), "c:orientation")?;
6992 *boundary = (*boundary).max(2);
6993 }
6994 b"max" => {
6995 set_once(maximum, parse_f64_scalar(&raw, "max")?, "c:max")?;
6996 *boundary = (*boundary).max(3);
6997 }
6998 b"min" => {
6999 set_once(minimum, parse_f64_scalar(&raw, "min")?, "c:min")?;
7000 *boundary = (*boundary).max(4);
7001 }
7002 _ => raw_children.push(*boundary, raw),
7003 }
7004 Ok(())
7005}
7006
7007impl ChartLines {
7008 fn from_xml(xml: &[u8], local: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7009 let mut reader = Reader::from_reader(xml);
7010 let mut buffer = Vec::new();
7011 loop {
7012 match reader
7013 .read_event_into(&mut buffer)
7014 .map_err(OxmlError::from)?
7015 {
7016 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
7017 let namespaces = typed_rewrite_bindings(&element, inherited)?;
7018 let raw_attributes = capture_attributes(&element)?;
7019 let mut sp_pr = None;
7020 let mut raw_children = OrderedRawChildren::default();
7021 let mut boundary = 0usize;
7022 let mut inner = Vec::new();
7023 loop {
7024 match reader
7025 .read_event_into(&mut inner)
7026 .map_err(OxmlError::from)?
7027 {
7028 Event::Start(child) => {
7029 let name = chart_child_local(&child, &namespaces)?;
7030 let raw = capture_element(&mut reader, &child)?;
7031 if name.as_deref() == Some(b"spPr") {
7032 parse_chart_lines_shape(&raw, &namespaces, &mut sp_pr)?;
7033 boundary = 1;
7034 } else {
7035 raw_children.push(boundary, raw);
7036 }
7037 }
7038 Event::Empty(child) => {
7039 let name = chart_child_local(&child, &namespaces)?;
7040 let raw = capture_empty_element(&child)?;
7041 if name.as_deref() == Some(b"spPr") {
7042 parse_chart_lines_shape(&raw, &namespaces, &mut sp_pr)?;
7043 boundary = 1;
7044 } else {
7045 raw_children.push(boundary, raw);
7046 }
7047 }
7048 event @ (Event::Text(_)
7049 | Event::CData(_)
7050 | Event::Comment(_)
7051 | Event::PI(_)
7052 | Event::GeneralRef(_)) => {
7053 raw_children.push(boundary, capture_event(event)?);
7054 }
7055 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
7056 return Ok(Self {
7057 sp_pr,
7058 raw_attributes,
7059 raw_children: raw_children_in_schema_order(&raw_children, 1),
7060 });
7061 }
7062 Event::Eof => {
7063 return Err(missing_end(&format!(
7064 "c:{}",
7065 String::from_utf8_lossy(local)
7066 )));
7067 }
7068 _ => {}
7069 }
7070 inner.clear();
7071 }
7072 }
7073 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
7074 typed_rewrite_bindings(&element, inherited)?;
7075 return Ok(Self {
7076 sp_pr: None,
7077 raw_attributes: capture_attributes(&element)?,
7078 raw_children: OrderedRawChildren::default(),
7079 });
7080 }
7081 Event::Start(element) | Event::Empty(element) => {
7082 return Err(ChartError::UnexpectedElement(element_name(&element)));
7083 }
7084 Event::Eof => {
7085 return Err(ChartError::MissingElement(format!(
7086 "c:{}",
7087 String::from_utf8_lossy(local)
7088 )));
7089 }
7090 _ => {}
7091 }
7092 buffer.clear();
7093 }
7094 }
7095
7096 fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
7097 let mut start = BytesStart::new(tag);
7098 push_attributes(&mut start, &self.raw_attributes);
7099 if self.sp_pr.is_none() && self.raw_children.is_empty() {
7100 writer
7101 .write_event(Event::Empty(start))
7102 .map_err(OxmlError::from)?;
7103 return Ok(());
7104 }
7105 writer
7106 .write_event(Event::Start(start))
7107 .map_err(OxmlError::from)?;
7108 emit_raw(writer, self.raw_children.at(0))?;
7109 if let Some(properties) = &self.sp_pr {
7110 properties.write_xml_as(writer, "c:spPr")?;
7111 }
7112 emit_raw(writer, self.raw_children.at(1))?;
7113 writer
7114 .write_event(Event::End(BytesEnd::new(tag)))
7115 .map_err(OxmlError::from)?;
7116 Ok(())
7117 }
7118}
7119
7120fn parse_chart_lines_shape(
7121 raw: &[u8],
7122 namespaces: &NamespaceBindings,
7123 slot: &mut Option<CT_ShapeProperties>,
7124) -> Result<()> {
7125 reject_conflicting_prefix_in_xml(raw, b"a", A_NS)?;
7126 let properties = CT_ShapeProperties::from_xml(raw)?;
7127 let mut writer = Writer::new(Vec::new());
7128 properties.write_xml_as(&mut writer, "c:spPr")?;
7129 reject_rewritten_foreign_elements(raw, &writer.into_inner(), namespaces, b"spPr")?;
7130 set_once(slot, properties, "c:spPr")
7131}
7132
7133impl NumberFormat {
7134 pub fn new(format_code: String, source_linked: bool) -> Result<Self> {
7135 let number_format = Self {
7136 format_code,
7137 source_linked,
7138 raw_attributes: Vec::new(),
7139 raw_content: Vec::new(),
7140 };
7141 number_format.validate()?;
7142 Ok(number_format)
7143 }
7144
7145 fn from_xml(xml: &[u8]) -> Result<Self> {
7146 let mut reader = Reader::from_reader(xml);
7147 let mut buffer = Vec::new();
7148 loop {
7149 match reader
7150 .read_event_into(&mut buffer)
7151 .map_err(OxmlError::from)?
7152 {
7153 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
7154 return Self::from_element(&element, Vec::new());
7155 }
7156 Event::Start(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
7157 let mut raw_content = Vec::new();
7158 let mut inner = Vec::new();
7159 loop {
7160 match reader
7161 .read_event_into(&mut inner)
7162 .map_err(OxmlError::from)?
7163 {
7164 Event::End(end)
7165 if matches_local_name(end.name().as_ref(), b"numFmt") =>
7166 {
7167 return Self::from_element(&element, raw_content);
7168 }
7169 event @ (Event::Text(_)
7170 | Event::CData(_)
7171 | Event::Comment(_)
7172 | Event::PI(_)
7173 | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
7174 Event::Eof => return Err(missing_end("c:numFmt")),
7175 _ => return Err(ChartError::UnexpectedElement("c:numFmt".to_owned())),
7176 }
7177 inner.clear();
7178 }
7179 }
7180 Event::Start(element) | Event::Empty(element) => {
7181 return Err(ChartError::UnexpectedElement(element_name(&element)));
7182 }
7183 Event::Eof => return Err(ChartError::MissingElement("c:numFmt".to_owned())),
7184 _ => {}
7185 }
7186 buffer.clear();
7187 }
7188 }
7189
7190 fn from_element(element: &BytesStart<'_>, raw_content: Vec<Vec<u8>>) -> Result<Self> {
7191 reject_conflicting_prefix(element, b"c", C_NS)?;
7192 let format_code = attribute_value(element, b"formatCode")?
7193 .ok_or_else(|| invalid_attribute("numFmt", "formatCode", "<missing>".to_owned()))?;
7194 let source_linked = attribute_value(element, b"sourceLinked")?
7195 .ok_or_else(|| invalid_attribute("numFmt", "sourceLinked", "<missing>".to_owned()))?;
7196 let source_linked = parse_bool_lexical("numFmt", "sourceLinked", &source_linked)?;
7197 let number_format = Self {
7198 format_code,
7199 source_linked,
7200 raw_attributes: capture_attributes_excluding(
7201 element,
7202 &[b"formatCode", b"sourceLinked"],
7203 )?,
7204 raw_content,
7205 };
7206 number_format.validate()?;
7207 Ok(number_format)
7208 }
7209
7210 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7211 self.validate()?;
7212 let mut start = BytesStart::new("c:numFmt");
7213 start.push_attribute(("formatCode", self.format_code.as_str()));
7214 start.push_attribute(("sourceLinked", bool_lexical(self.source_linked)));
7215 push_attributes(&mut start, &self.raw_attributes);
7216 if self.raw_content.is_empty() {
7217 writer
7218 .write_event(Event::Empty(start))
7219 .map_err(OxmlError::from)?;
7220 return Ok(());
7221 }
7222 writer
7223 .write_event(Event::Start(start))
7224 .map_err(OxmlError::from)?;
7225 for raw in &self.raw_content {
7226 writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
7227 }
7228 writer
7229 .write_event(Event::End(BytesEnd::new("c:numFmt")))
7230 .map_err(OxmlError::from)?;
7231 Ok(())
7232 }
7233
7234 pub fn format_value(&self, value: f64) -> Result<String> {
7236 if !value.is_finite() {
7237 return Err(ChartError::InvalidValue {
7238 element: "c:numFmt value".to_owned(),
7239 value: value.to_string(),
7240 });
7241 }
7242 if self.format_code == "General" {
7243 return Ok(value.to_string());
7244 }
7245
7246 let (numeric_code, percentage) = self
7247 .format_code
7248 .strip_suffix('%')
7249 .map(|code| (code, true))
7250 .unwrap_or((&self.format_code, false));
7251 let precision = if numeric_code == "0" {
7252 Some(0)
7253 } else if let Some(decimals) = numeric_code.strip_prefix("0.")
7254 && !decimals.is_empty()
7255 && decimals.bytes().all(|byte| byte == b'0')
7256 {
7257 Some(decimals.len())
7258 } else {
7259 None
7260 };
7261 let Some(precision) = precision else {
7262 return Err(ChartError::InvalidValue {
7263 element: "c:numFmt/@formatCode projection".to_owned(),
7264 value: self.format_code.clone(),
7265 });
7266 };
7267 let projected = if percentage { value * 100.0 } else { value };
7268 if !projected.is_finite() {
7269 return Err(ChartError::InvalidValue {
7270 element: "c:numFmt value".to_owned(),
7271 value: projected.to_string(),
7272 });
7273 }
7274 let mut formatted = format!("{projected:.precision$}");
7275 if percentage {
7276 formatted.push('%');
7277 }
7278 Ok(formatted)
7279 }
7280
7281 fn validate(&self) -> Result<()> {
7282 if self.format_code.is_empty() {
7283 return Err(ChartError::InvalidValue {
7284 element: "c:numFmt/@formatCode".to_owned(),
7285 value: self.format_code.clone(),
7286 });
7287 }
7288 validate_xml_text(&self.format_code, "c:numFmt/@formatCode")
7289 }
7290}
7291
7292impl CT_DLbls {
7293 pub fn from_xml(xml: &[u8]) -> Result<Self> {
7294 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
7295 }
7296
7297 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7298 let mut reader = Reader::from_reader(xml);
7299 let mut buffer = Vec::new();
7300 loop {
7301 match reader
7302 .read_event_into(&mut buffer)
7303 .map_err(OxmlError::from)?
7304 {
7305 Event::Start(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7306 chart_root_prefix(&element)?;
7307 if !element_is_in_namespace(&element, C_NS, inherited)? {
7308 return Err(ChartError::UnexpectedElement(element_name(&element)));
7309 }
7310 return Self::from_element(&mut reader, &element, inherited);
7311 }
7312 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7313 chart_root_prefix(&element)?;
7314 let namespaces = chart_bindings(inherited, &element)?;
7315 if !element_is_in_namespace(&element, C_NS, inherited)? {
7316 return Err(ChartError::UnexpectedElement(element_name(&element)));
7317 }
7318 let (raw_attributes, _) = capture_fixed_root_attributes(
7319 &element,
7320 &["xmlns:c", "xmlns:a", "xmlns:r"],
7321 )?;
7322 return Ok(Self {
7323 raw_attributes,
7324 namespace_declarations: standalone_namespace_declarations(&namespaces)?,
7325 ..Self::default()
7326 });
7327 }
7328 Event::Start(element) | Event::Empty(element) => {
7329 return Err(ChartError::UnexpectedElement(element_name(&element)));
7330 }
7331 Event::Eof => return Err(ChartError::MissingElement("c:dLbls".to_owned())),
7332 _ => {}
7333 }
7334 buffer.clear();
7335 }
7336 }
7337
7338 fn from_element(
7339 reader: &mut Reader<&[u8]>,
7340 start: &BytesStart<'_>,
7341 inherited: &NamespaceBindings,
7342 ) -> Result<Self> {
7343 reject_conflicting_prefix(start, b"a", A_NS)?;
7344 let namespaces = chart_bindings(inherited, start)?;
7345 require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
7346 let (raw_attributes, _) =
7347 capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
7348 let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
7349 let mut state = DataLabelsParseState::new(namespaces);
7350 let mut buffer = Vec::new();
7351 loop {
7352 match reader
7353 .read_event_into(&mut buffer)
7354 .map_err(OxmlError::from)?
7355 {
7356 Event::Start(element) => {
7357 let name = chart_child_local(&element, &state.namespaces)?;
7358 let raw = capture_element(reader, &element)?;
7359 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
7360 }
7361 Event::Empty(element) => {
7362 let name = chart_child_local(&element, &state.namespaces)?;
7363 let raw = capture_empty_element(&element)?;
7364 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
7365 }
7366 event @ (Event::Text(_)
7367 | Event::CData(_)
7368 | Event::Comment(_)
7369 | Event::PI(_)
7370 | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
7371 Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7372 return state.finish(raw_attributes, namespace_declarations);
7373 }
7374 Event::Eof => return Err(missing_end("c:dLbls")),
7375 _ => {}
7376 }
7377 buffer.clear();
7378 }
7379 }
7380
7381 pub fn to_xml(&self) -> Result<Vec<u8>> {
7382 let mut writer = Writer::new(Vec::new());
7383 self.write_xml(&mut writer, true)?;
7384 Ok(writer.into_inner())
7385 }
7386
7387 fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
7388 self.validate()?;
7389 let mut start = BytesStart::new("c:dLbls");
7390 if standalone {
7391 start.push_attribute(("xmlns:c", C_NS));
7392 start.push_attribute(("xmlns:a", A_NS));
7393 start.push_attribute(("xmlns:r", R_NS));
7394 }
7395 push_attributes(&mut start, &self.namespace_declarations);
7396 push_attributes(&mut start, &self.raw_attributes);
7397 writer
7398 .write_event(Event::Start(start))
7399 .map_err(OxmlError::from)?;
7400 emit_raw(writer, self.raw_children.at(0))?;
7401 if let Some(number_format) = &self.number_format {
7402 number_format.write_xml(writer)?;
7403 }
7404 emit_raw(writer, self.raw_children.at(1))?;
7405 if let Some(position) = self.position {
7406 write_scalar(
7407 writer,
7408 "c:dLblPos",
7409 position.as_str(),
7410 self.position_markup.as_ref(),
7411 )?;
7412 }
7413 emit_raw(writer, self.raw_children.at(2))?;
7414 write_optional_bool(
7415 writer,
7416 "c:showLegendKey",
7417 self.show_legend_key,
7418 self.show_legend_key_markup.as_ref(),
7419 )?;
7420 emit_raw(writer, self.raw_children.at(3))?;
7421 write_optional_bool(
7422 writer,
7423 "c:showVal",
7424 self.show_value,
7425 self.show_value_markup.as_ref(),
7426 )?;
7427 emit_raw(writer, self.raw_children.at(4))?;
7428 write_optional_bool(
7429 writer,
7430 "c:showCatName",
7431 self.show_category_name,
7432 self.show_category_name_markup.as_ref(),
7433 )?;
7434 emit_raw(writer, self.raw_children.at(5))?;
7435 write_optional_bool(
7436 writer,
7437 "c:showSerName",
7438 self.show_series_name,
7439 self.show_series_name_markup.as_ref(),
7440 )?;
7441 emit_raw(writer, self.raw_children.at(6))?;
7442 write_optional_bool(
7443 writer,
7444 "c:showPercent",
7445 self.show_percent,
7446 self.show_percent_markup.as_ref(),
7447 )?;
7448 emit_raw(writer, self.raw_children.at(7))?;
7449 write_optional_bool(
7450 writer,
7451 "c:showBubbleSize",
7452 self.show_bubble_size,
7453 self.show_bubble_size_markup.as_ref(),
7454 )?;
7455 emit_raw(writer, self.raw_children.at(8))?;
7456 if let Some(separator) = &self.separator {
7457 write_text(
7458 writer,
7459 "c:separator",
7460 separator,
7461 self.separator_markup
7462 .as_ref()
7463 .unwrap_or(&TextMarkup::default()),
7464 )?;
7465 }
7466 emit_raw(writer, self.raw_children.at(9))?;
7467 writer
7468 .write_event(Event::End(BytesEnd::new("c:dLbls")))
7469 .map_err(OxmlError::from)?;
7470 Ok(())
7471 }
7472
7473 fn validate(&self) -> Result<()> {
7474 if let Some(number_format) = &self.number_format {
7475 number_format.validate()?;
7476 }
7477 if let Some(separator) = &self.separator {
7478 validate_xml_text(separator, "c:separator")?;
7479 }
7480 Ok(())
7481 }
7482
7483 pub fn raw_children(&self) -> &OrderedRawChildren {
7484 &self.raw_children
7485 }
7486}
7487
7488struct DataLabelsParseState {
7489 namespaces: NamespaceBindings,
7490 number_format: Option<NumberFormat>,
7491 position: Option<(DataLabelPosition, ScalarMarkup)>,
7492 show_legend_key: Option<(bool, ScalarMarkup)>,
7493 show_value: Option<(bool, ScalarMarkup)>,
7494 show_category_name: Option<(bool, ScalarMarkup)>,
7495 show_series_name: Option<(bool, ScalarMarkup)>,
7496 show_percent: Option<(bool, ScalarMarkup)>,
7497 show_bubble_size: Option<(bool, ScalarMarkup)>,
7498 separator: Option<(String, TextMarkup)>,
7499 raw_children: OrderedRawChildren,
7500 boundary: usize,
7501}
7502
7503impl DataLabelsParseState {
7504 fn new(namespaces: NamespaceBindings) -> Self {
7505 Self {
7506 namespaces,
7507 number_format: None,
7508 position: None,
7509 show_legend_key: None,
7510 show_value: None,
7511 show_category_name: None,
7512 show_series_name: None,
7513 show_percent: None,
7514 show_bubble_size: None,
7515 separator: None,
7516 raw_children: OrderedRawChildren::default(),
7517 boundary: 0,
7518 }
7519 }
7520
7521 fn capture_event(&mut self, raw: Vec<u8>) {
7522 self.raw_children.push(self.boundary, raw);
7523 }
7524
7525 fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
7526 match name {
7527 b"numFmt" => {
7528 set_once(
7529 &mut self.number_format,
7530 NumberFormat::from_xml(&raw)?,
7531 "c:numFmt",
7532 )?;
7533 self.boundary = self.boundary.max(1);
7534 }
7535 b"dLblPos" => {
7536 let (value, markup) = scalar_value(&raw, "dLblPos")?;
7537 let lexical = value
7538 .ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
7539 let position = DataLabelPosition::parse(&lexical)
7540 .ok_or_else(|| invalid_attribute("dLblPos", "val", lexical))?;
7541 set_once(&mut self.position, (position, markup), "c:dLblPos")?;
7542 self.boundary = self.boundary.max(2);
7543 }
7544 b"showLegendKey" => {
7545 set_once(
7546 &mut self.show_legend_key,
7547 parse_bool_value(&raw, "showLegendKey")?,
7548 "c:showLegendKey",
7549 )?;
7550 self.boundary = self.boundary.max(3);
7551 }
7552 b"showVal" => {
7553 set_once(
7554 &mut self.show_value,
7555 parse_bool_value(&raw, "showVal")?,
7556 "c:showVal",
7557 )?;
7558 self.boundary = self.boundary.max(4);
7559 }
7560 b"showCatName" => {
7561 set_once(
7562 &mut self.show_category_name,
7563 parse_bool_value(&raw, "showCatName")?,
7564 "c:showCatName",
7565 )?;
7566 self.boundary = self.boundary.max(5);
7567 }
7568 b"showSerName" => {
7569 set_once(
7570 &mut self.show_series_name,
7571 parse_bool_value(&raw, "showSerName")?,
7572 "c:showSerName",
7573 )?;
7574 self.boundary = self.boundary.max(6);
7575 }
7576 b"showPercent" => {
7577 set_once(
7578 &mut self.show_percent,
7579 parse_bool_value(&raw, "showPercent")?,
7580 "c:showPercent",
7581 )?;
7582 self.boundary = self.boundary.max(7);
7583 }
7584 b"showBubbleSize" => {
7585 set_once(
7586 &mut self.show_bubble_size,
7587 parse_bool_value(&raw, "showBubbleSize")?,
7588 "c:showBubbleSize",
7589 )?;
7590 self.boundary = self.boundary.max(8);
7591 }
7592 b"separator" => {
7593 set_once(
7594 &mut self.separator,
7595 parse_text_element(&raw, b"separator", &self.namespaces)?,
7596 "c:separator",
7597 )?;
7598 self.boundary = self.boundary.max(9);
7599 }
7600 _ => {
7601 let boundary = data_labels_raw_boundary(name, self.boundary);
7602 self.raw_children.push(boundary, raw);
7603 self.boundary = self.boundary.max(boundary);
7604 }
7605 }
7606 Ok(())
7607 }
7608
7609 fn finish(
7610 self,
7611 raw_attributes: XmlAttributes,
7612 namespace_declarations: XmlAttributes,
7613 ) -> Result<CT_DLbls> {
7614 let (position, position_markup) = optional_markup(self.position);
7615 let (show_legend_key, show_legend_key_markup) = optional_bool_markup(self.show_legend_key);
7616 let (show_value, show_value_markup) = optional_bool_markup(self.show_value);
7617 let (show_category_name, show_category_name_markup) =
7618 optional_bool_markup(self.show_category_name);
7619 let (show_series_name, show_series_name_markup) =
7620 optional_bool_markup(self.show_series_name);
7621 let (show_percent, show_percent_markup) = optional_bool_markup(self.show_percent);
7622 let (show_bubble_size, show_bubble_size_markup) =
7623 optional_bool_markup(self.show_bubble_size);
7624 let (separator, separator_markup) = self
7625 .separator
7626 .map(|(value, markup)| (Some(value), Some(markup)))
7627 .unwrap_or((None, None));
7628 let labels = CT_DLbls {
7629 number_format: self.number_format,
7630 position,
7631 separator,
7632 show_legend_key,
7633 show_value,
7634 show_category_name,
7635 show_series_name,
7636 show_percent,
7637 show_bubble_size,
7638 position_markup,
7639 show_legend_key_markup,
7640 show_value_markup,
7641 show_category_name_markup,
7642 show_series_name_markup,
7643 show_percent_markup,
7644 show_bubble_size_markup,
7645 separator_markup,
7646 raw_attributes,
7647 namespace_declarations,
7648 raw_children: raw_children_in_schema_order(&self.raw_children, 9),
7649 };
7650 labels.validate()?;
7651 Ok(labels)
7652 }
7653}
7654
7655fn data_labels_raw_boundary(name: &[u8], current: usize) -> usize {
7656 match name {
7657 b"dLbl" | b"delete" => 0,
7658 b"spPr" | b"txPr" => 1,
7659 b"showLeaderLines" | b"leaderLines" | b"extLst" => 9,
7660 _ => current,
7661 }
7662}
7663
7664fn optional_bool_markup(value: Option<(bool, ScalarMarkup)>) -> (bool, Option<ScalarMarkup>) {
7665 value
7666 .map(|(value, markup)| (value, Some(markup)))
7667 .unwrap_or((false, None))
7668}
7669
7670fn write_optional_bool(
7671 writer: &mut Writer<Vec<u8>>,
7672 tag: &str,
7673 value: bool,
7674 markup: Option<&ScalarMarkup>,
7675) -> Result<()> {
7676 if value || markup.is_some() {
7677 write_scalar(writer, tag, bool_lexical(value), markup)?;
7678 }
7679 Ok(())
7680}
7681
7682fn default_axis_id_markup(id: AxisId) -> AxisIdMarkup {
7683 AxisIdMarkup {
7684 scalar: ScalarMarkup::default(),
7685 parsed: id,
7686 lexical: id.value().to_string(),
7687 }
7688}
7689
7690fn parse_axis_id_scalar(xml: &[u8], local: &str) -> Result<(AxisId, AxisIdMarkup)> {
7691 let (value, scalar) = scalar_value(xml, local)?;
7692 let lexical = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
7693 let parsed = lexical
7694 .parse::<i64>()
7695 .map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
7696 let id = AxisId::new(parsed).map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
7697 Ok((
7698 id,
7699 AxisIdMarkup {
7700 scalar,
7701 parsed: id,
7702 lexical,
7703 },
7704 ))
7705}
7706
7707fn write_axis_id<W: Write>(
7708 writer: &mut Writer<W>,
7709 tag: &str,
7710 id: AxisId,
7711 markup: &AxisIdMarkup,
7712) -> Result<()> {
7713 AxisId::new(id.value())?;
7714 let normalized;
7715 let value = if id == markup.parsed {
7716 markup.lexical.as_str()
7717 } else {
7718 normalized = id.value().to_string();
7719 normalized.as_str()
7720 };
7721 write_scalar(writer, tag, value, Some(&markup.scalar))
7722}
7723
7724fn parse_f64_scalar(xml: &[u8], local: &str) -> Result<(f64, ScalarMarkup)> {
7725 let (value, markup) = scalar_value(xml, local)?;
7726 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
7727 let parsed = value
7728 .parse::<f64>()
7729 .map_err(|_| invalid_attribute(local, "val", value.clone()))?;
7730 if !parsed.is_finite() {
7731 return Err(invalid_attribute(local, "val", value));
7732 }
7733 Ok((parsed, markup))
7734}
7735
7736fn parse_axis_position(xml: &[u8]) -> Result<(AxisPosition, ScalarMarkup)> {
7737 let (value, markup) = scalar_value(xml, "axPos")?;
7738 let value = value.ok_or_else(|| invalid_attribute("axPos", "val", "<missing>".to_owned()))?;
7739 let parsed =
7740 AxisPosition::parse(&value).ok_or_else(|| invalid_attribute("axPos", "val", value))?;
7741 Ok((parsed, markup))
7742}
7743
7744fn parse_tick_mark(xml: &[u8], local: &str) -> Result<(TickMark, ScalarMarkup)> {
7745 let (value, markup) = scalar_value(xml, local)?;
7746 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
7747 let parsed = TickMark::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
7748 Ok((parsed, markup))
7749}
7750
7751fn parse_tick_label_position(xml: &[u8]) -> Result<(TickLabelPosition, ScalarMarkup)> {
7752 let (value, markup) = scalar_value(xml, "tickLblPos")?;
7753 let value =
7754 value.ok_or_else(|| invalid_attribute("tickLblPos", "val", "<missing>".to_owned()))?;
7755 let parsed = TickLabelPosition::parse(&value)
7756 .ok_or_else(|| invalid_attribute("tickLblPos", "val", value))?;
7757 Ok((parsed, markup))
7758}
7759
7760fn optional_markup<T>(value: Option<(T, ScalarMarkup)>) -> (Option<T>, Option<ScalarMarkup>) {
7761 value
7762 .map(|(value, markup)| (Some(value), Some(markup)))
7763 .unwrap_or((None, None))
7764}
7765
7766fn optional_scalar_markup_eq(left: &Option<ScalarMarkup>, right: &Option<ScalarMarkup>) -> bool {
7767 let empty = ScalarMarkup::default();
7768 left.as_ref().unwrap_or(&empty) == right.as_ref().unwrap_or(&empty)
7769}
7770
7771fn axis_id_markup_eq(left: &AxisIdMarkup, right: &AxisIdMarkup) -> bool {
7772 left.scalar == right.scalar
7773}
7774
7775const fn bool_lexical(value: bool) -> &'static str {
7776 if value { "1" } else { "0" }
7777}
7778
7779fn parse_bool_lexical(element: &str, attribute: &str, value: &str) -> Result<bool> {
7780 match value {
7781 "1" | "true" => Ok(true),
7782 "0" | "false" => Ok(false),
7783 _ => Err(invalid_attribute(element, attribute, value.to_owned())),
7784 }
7785}
7786
7787impl CT_Title {
7788 fn from_xml(xml: &[u8]) -> Result<Self> {
7789 let (raw_attributes, raw_children) = parse_raw_shell(xml, b"title", "c:title")?;
7790 Ok(Self {
7791 raw_attributes,
7792 raw_children,
7793 })
7794 }
7795
7796 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7797 write_raw_shell(writer, "c:title", &self.raw_attributes, &self.raw_children)
7798 }
7799
7800 pub fn raw_children(&self) -> &OrderedRawChildren {
7801 &self.raw_children
7802 }
7803}
7804
7805impl CT_PlotArea {
7806 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7807 let (raw_attributes, captured_children) = parse_raw_shell(xml, b"plotArea", "c:plotArea")?;
7808 let namespace_bindings = root_chart_bindings(xml, b"plotArea", inherited)?;
7809 let direct_roots: Vec<_> = captured_children
7810 .at(0)
7811 .filter_map(|raw| chart_root_local(raw, &namespace_bindings).transpose())
7812 .collect::<Result<_>>()?;
7813 let plot_roots: Vec<_> = direct_roots
7814 .iter()
7815 .filter(|local| is_plot_container(local))
7816 .cloned()
7817 .collect();
7818 let typed_local = (plot_roots.len() == 1
7819 && matches!(
7820 plot_roots[0].as_slice(),
7821 b"barChart"
7822 | b"lineChart"
7823 | b"pieChart"
7824 | b"doughnutChart"
7825 | b"areaChart"
7826 | b"scatterChart"
7827 | b"radarChart"
7828 ))
7829 .then(|| plot_roots[0].clone());
7830
7831 let Some(typed_local) = typed_local else {
7832 return Ok(Self {
7833 raw_attributes,
7834 raw_children: captured_children,
7835 namespace_bindings,
7836 plots: None,
7837 plot_markup: Vec::new(),
7838 axes: Vec::new(),
7839 });
7840 };
7841
7842 let mut raw_children = OrderedRawChildren::default();
7843 let mut plots = Vec::new();
7844 let mut plot_markup = Vec::new();
7845 let mut axes = Vec::new();
7846 let mut boundary = 0usize;
7847 for raw in captured_children.at(0) {
7848 let local = chart_root_local(raw, &namespace_bindings)?;
7849 if local.as_deref() == Some(typed_local.as_slice()) {
7850 let (plot, markup) = parse_plot(raw, &namespace_bindings)?;
7851 plots.push(plot);
7852 plot_markup.push(markup);
7853 boundary = 1;
7854 } else if local.as_deref().and_then(AxisKind::parse).is_some() {
7855 axes.push(Axis::from_xml_with_namespaces(raw, &namespace_bindings)?);
7856 boundary = axes.len() + 1;
7857 } else {
7858 raw_children.push(boundary, raw.to_vec());
7859 }
7860 }
7861 let plot_area = Self {
7862 raw_attributes,
7863 raw_children,
7864 namespace_bindings,
7865 plots: Some(plots),
7866 plot_markup,
7867 axes,
7868 };
7869 plot_area.validate_typed()?;
7870 Ok(plot_area)
7871 }
7872
7873 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7874 let Some(plots) = &self.plots else {
7875 return write_raw_shell(
7876 writer,
7877 "c:plotArea",
7878 &self.raw_attributes,
7879 &self.raw_children,
7880 );
7881 };
7882 self.validate_typed()?;
7883 let mut start = BytesStart::new("c:plotArea");
7884 push_attributes(&mut start, &self.raw_attributes);
7885 writer
7886 .write_event(Event::Start(start))
7887 .map_err(OxmlError::from)?;
7888 emit_raw(writer, self.raw_children.at(0))?;
7889 for (index, plot) in plots.iter().enumerate() {
7890 write_plot(
7891 writer,
7892 plot,
7893 self.plot_markup
7894 .get(index)
7895 .unwrap_or(&PlotMarkup::default()),
7896 )?;
7897 }
7898 emit_raw(writer, self.raw_children.at(1))?;
7899 for (index, axis) in self.axes.iter().enumerate() {
7900 writer
7901 .get_mut()
7902 .write_all(&axis.to_xml()?)
7903 .map_err(OxmlError::from)?;
7904 emit_raw(writer, self.raw_children.at(index + 2))?;
7905 }
7906 writer
7907 .write_event(Event::End(BytesEnd::new("c:plotArea")))
7908 .map_err(OxmlError::from)?;
7909 Ok(())
7910 }
7911
7912 pub fn raw_children(&self) -> &OrderedRawChildren {
7913 &self.raw_children
7914 }
7915
7916 pub fn new(plots: Vec<Plot>, axes: Vec<Axis>) -> Result<Self> {
7918 let plot_area = Self {
7919 raw_attributes: Vec::new(),
7920 raw_children: OrderedRawChildren::default(),
7921 namespace_bindings: chart_namespace_defaults(),
7922 plot_markup: vec![PlotMarkup::default(); plots.len()],
7923 plots: Some(plots),
7924 axes,
7925 };
7926 plot_area.validate_typed()?;
7927 Ok(plot_area)
7928 }
7929
7930 pub fn plots(&self) -> Result<&[Plot]> {
7932 self.plots
7933 .as_deref()
7934 .ok_or_else(|| ChartError::InvalidValue {
7935 element: "c:plotArea".to_owned(),
7936 value: "unsupported or combination plot area is opaque".to_owned(),
7937 })
7938 }
7939
7940 pub fn plots_mut(&mut self) -> Result<&mut [Plot]> {
7942 self.plots
7943 .as_deref_mut()
7944 .ok_or_else(|| ChartError::InvalidValue {
7945 element: "c:plotArea".to_owned(),
7946 value: "cannot combine typed plots with an opaque plot choice".to_owned(),
7947 })
7948 }
7949
7950 pub fn series(&self) -> Result<Vec<Series>> {
7952 if let Some(plots) = &self.plots {
7953 return Ok(plots
7954 .iter()
7955 .flat_map(|plot| match plot {
7956 Plot::Bar { series, .. }
7957 | Plot::Line { series, .. }
7958 | Plot::Pie { series, .. }
7959 | Plot::Doughnut { series, .. }
7960 | Plot::Area { series, .. }
7961 | Plot::Scatter { series, .. }
7962 | Plot::Radar { series, .. } => series.clone(),
7963 })
7964 .collect());
7965 }
7966 let mut series = Vec::new();
7967 for raw in self.raw_children.at(0) {
7968 parse_plot_series(raw, &self.namespace_bindings, &mut series)?;
7969 }
7970 Ok(series)
7971 }
7972
7973 pub fn axes(&self) -> Result<Vec<Axis>> {
7975 if self.plots.is_some() {
7976 self.validate_typed()?;
7977 return Ok(self.axes.clone());
7978 }
7979 let mut axes = Vec::new();
7980 for raw in self.raw_children.at(0) {
7981 if let Some(axis) = parse_plot_axis(raw, &self.namespace_bindings)? {
7982 axes.push(axis);
7983 }
7984 }
7985 validate_axis_pairs(&axes)?;
7986 Ok(axes)
7987 }
7988
7989 fn validate_typed(&self) -> Result<()> {
7990 let plots = self
7991 .plots
7992 .as_ref()
7993 .ok_or_else(|| ChartError::InvalidValue {
7994 element: "c:plotArea".to_owned(),
7995 value: "opaque plot area has no typed validation view".to_owned(),
7996 })?;
7997 if plots.len() != 1 {
7998 return Err(ChartError::InvalidValue {
7999 element: "c:plotArea".to_owned(),
8000 value: "typed plot area requires exactly one plot family".to_owned(),
8001 });
8002 }
8003 validate_axis_pairs(&self.axes)?;
8004 for plot in plots {
8005 plot.validate()?;
8006 if let Some(axis_ids) = plot.axis_ids() {
8007 for id in axis_ids {
8008 if !self.axes.iter().any(|axis| axis.id == id) {
8009 return Err(ChartError::InvalidValue {
8010 element: "c:axId".to_owned(),
8011 value: format!("plot references missing axis {}", id.value()),
8012 });
8013 }
8014 }
8015 } else if !self.axes.is_empty() {
8016 return Err(ChartError::InvalidValue {
8017 element: "c:plotArea".to_owned(),
8018 value: "pie and doughnut plot areas must not own axes".to_owned(),
8019 });
8020 }
8021 if plot.axis_ids().is_some() && self.axes.len() != 2 {
8022 return Err(ChartError::InvalidValue {
8023 element: "c:plotArea".to_owned(),
8024 value: format!(
8025 "axis-owned plot requires exactly 2 axes, found {}",
8026 self.axes.len()
8027 ),
8028 });
8029 }
8030 }
8031 Ok(())
8032 }
8033}
8034
8035impl Default for CT_PlotArea {
8036 fn default() -> Self {
8037 Self {
8038 raw_attributes: Vec::new(),
8039 raw_children: OrderedRawChildren::default(),
8040 namespace_bindings: chart_namespace_defaults(),
8041 plots: Some(Vec::new()),
8042 plot_markup: Vec::new(),
8043 axes: Vec::new(),
8044 }
8045 }
8046}
8047
8048fn chart_root_local(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Vec<u8>>> {
8049 let mut reader = Reader::from_reader(xml);
8050 let mut buffer = Vec::new();
8051 loop {
8052 match reader
8053 .read_event_into(&mut buffer)
8054 .map_err(OxmlError::from)?
8055 {
8056 Event::Start(element) | Event::Empty(element) => {
8057 return Ok(element_is_in_namespace(&element, C_NS, inherited)?
8058 .then(|| local_name(element.name().as_ref()).to_vec()));
8059 }
8060 Event::Eof => return Ok(None),
8061 _ => {}
8062 }
8063 buffer.clear();
8064 }
8065}
8066
8067fn is_plot_container(local: &[u8]) -> bool {
8068 matches!(
8069 local,
8070 b"areaChart"
8071 | b"area3DChart"
8072 | b"barChart"
8073 | b"bar3DChart"
8074 | b"bubbleChart"
8075 | b"doughnutChart"
8076 | b"lineChart"
8077 | b"line3DChart"
8078 | b"ofPieChart"
8079 | b"pieChart"
8080 | b"pie3DChart"
8081 | b"radarChart"
8082 | b"scatterChart"
8083 | b"stockChart"
8084 | b"surfaceChart"
8085 | b"surface3DChart"
8086 )
8087}
8088
8089fn parse_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8090 let local = chart_root_local(xml, inherited)?
8091 .ok_or_else(|| ChartError::MissingElement("c:barChart or c:lineChart".to_owned()))?;
8092 match local.as_slice() {
8093 b"barChart" => parse_bar_plot(xml, inherited),
8094 b"lineChart" => parse_line_plot(xml, inherited),
8095 b"pieChart" | b"doughnutChart" | b"areaChart" | b"scatterChart" | b"radarChart" => {
8096 parse_remaining_plot(xml, inherited, &local)
8097 }
8098 _ => Err(ChartError::UnexpectedElement(format!(
8099 "c:{}",
8100 String::from_utf8_lossy(&local)
8101 ))),
8102 }
8103}
8104
8105fn parse_remaining_plot(
8106 xml: &[u8],
8107 inherited: &NamespaceBindings,
8108 local: &[u8],
8109) -> Result<(Plot, PlotMarkup)> {
8110 let root = String::from_utf8_lossy(local).into_owned();
8111 let mut reader = Reader::from_reader(xml);
8112 let mut buffer = Vec::new();
8113 loop {
8114 match reader
8115 .read_event_into(&mut buffer)
8116 .map_err(OxmlError::from)?
8117 {
8118 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
8119 let namespaces = typed_rewrite_bindings(&element, inherited)?;
8120 let (mut raw_attributes, namespace_declarations) =
8121 capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8122 raw_attributes.splice(0..0, namespace_declarations);
8123 let mut grouping = None;
8124 let mut style = None;
8125 let mut first_slice_angle = None;
8126 let mut hole_size = None;
8127 let mut series = Vec::new();
8128 let mut data_labels = None;
8129 let mut axis_ids = Vec::new();
8130 let mut raw_children = OrderedRawChildren::default();
8131 let mut boundary = 0usize;
8132 let mut inner = Vec::new();
8133 loop {
8134 match reader
8135 .read_event_into(&mut inner)
8136 .map_err(OxmlError::from)?
8137 {
8138 Event::Start(child) => {
8139 let name = chart_child_local(&child, &namespaces)?;
8140 let raw = capture_element(&mut reader, &child)?;
8141 parse_remaining_plot_child(
8142 local,
8143 name.as_deref().unwrap_or_default(),
8144 raw,
8145 &namespaces,
8146 &mut grouping,
8147 &mut style,
8148 &mut first_slice_angle,
8149 &mut hole_size,
8150 &mut series,
8151 &mut data_labels,
8152 &mut axis_ids,
8153 &mut raw_children,
8154 &mut boundary,
8155 )?;
8156 }
8157 Event::Empty(child) => {
8158 let name = chart_child_local(&child, &namespaces)?;
8159 let raw = capture_empty_element(&child)?;
8160 parse_remaining_plot_child(
8161 local,
8162 name.as_deref().unwrap_or_default(),
8163 raw,
8164 &namespaces,
8165 &mut grouping,
8166 &mut style,
8167 &mut first_slice_angle,
8168 &mut hole_size,
8169 &mut series,
8170 &mut data_labels,
8171 &mut axis_ids,
8172 &mut raw_children,
8173 &mut boundary,
8174 )?;
8175 }
8176 event @ (Event::Text(_)
8177 | Event::CData(_)
8178 | Event::Comment(_)
8179 | Event::PI(_)
8180 | Event::GeneralRef(_)) => {
8181 raw_children.push(boundary, capture_event(event)?);
8182 }
8183 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
8184 let needs_axes =
8185 matches!(local, b"areaChart" | b"scatterChart" | b"radarChart");
8186 if needs_axes && axis_ids.len() != 2 {
8187 return Err(ChartError::InvalidValue {
8188 element: format!("c:{root}/c:axId"),
8189 value: format!("expected 2, found {}", axis_ids.len()),
8190 });
8191 }
8192 if !needs_axes && !axis_ids.is_empty() {
8193 return Err(ChartError::InvalidValue {
8194 element: format!("c:{root}/c:axId"),
8195 value: "axis-free plot contains axis references".to_owned(),
8196 });
8197 }
8198 let parsed_ids = needs_axes.then(|| [axis_ids[0].0, axis_ids[1].0]);
8199 let original_series_keys = series
8200 .iter()
8201 .map(|item| (item.index, item.order))
8202 .collect::<Vec<_>>();
8203 let plot = match local {
8204 b"pieChart" => Plot::Pie {
8205 first_slice_angle: first_slice_angle
8206 .as_ref()
8207 .map_or(0, |item| item.0),
8208 series,
8209 data_labels,
8210 },
8211 b"doughnutChart" => Plot::Doughnut {
8212 first_slice_angle: first_slice_angle
8213 .as_ref()
8214 .map_or(0, |item| item.0),
8215 hole_size: hole_size.as_ref().map_or(50, |item| item.0),
8216 series,
8217 data_labels,
8218 },
8219 b"areaChart" => Plot::Area {
8220 grouping: grouping
8221 .as_ref()
8222 .ok_or_else(|| {
8223 ChartError::MissingElement("c:grouping".to_owned())
8224 })?
8225 .0,
8226 series,
8227 data_labels,
8228 axis_ids: parsed_ids.ok_or_else(|| {
8229 ChartError::MissingElement("c:areaChart/c:axId".to_owned())
8230 })?,
8231 },
8232 b"scatterChart" => {
8233 let lexical = style
8234 .as_ref()
8235 .ok_or_else(|| {
8236 ChartError::MissingElement("c:scatterStyle".to_owned())
8237 })?
8238 .0
8239 .as_str();
8240 Plot::Scatter {
8241 style: ScatterStyle::parse(lexical).ok_or_else(|| {
8242 invalid_attribute(
8243 "scatterStyle",
8244 "val",
8245 lexical.to_owned(),
8246 )
8247 })?,
8248 series,
8249 data_labels,
8250 axis_ids: parsed_ids.ok_or_else(|| {
8251 ChartError::MissingElement(
8252 "c:scatterChart/c:axId".to_owned(),
8253 )
8254 })?,
8255 }
8256 }
8257 b"radarChart" => {
8258 let lexical = style
8259 .as_ref()
8260 .ok_or_else(|| {
8261 ChartError::MissingElement("c:radarStyle".to_owned())
8262 })?
8263 .0
8264 .as_str();
8265 Plot::Radar {
8266 style: RadarStyle::parse(lexical).ok_or_else(|| {
8267 invalid_attribute(
8268 "radarStyle",
8269 "val",
8270 lexical.to_owned(),
8271 )
8272 })?,
8273 series,
8274 data_labels,
8275 axis_ids: parsed_ids.ok_or_else(|| {
8276 ChartError::MissingElement(
8277 "c:radarChart/c:axId".to_owned(),
8278 )
8279 })?,
8280 }
8281 }
8282 _ => {
8283 return Err(ChartError::UnexpectedElement(format!(
8284 "c:{}",
8285 String::from_utf8_lossy(local)
8286 )));
8287 }
8288 };
8289 plot.validate()?;
8290 let final_boundary = boundary.max(original_series_keys.len() + 6);
8291 return Ok((
8292 plot,
8293 PlotMarkup {
8294 raw_attributes,
8295 raw_children: raw_children_in_schema_order(
8296 &raw_children,
8297 final_boundary,
8298 ),
8299 grouping: grouping.map(|item| item.1),
8300 first_slice_angle: first_slice_angle.map(|item| item.1),
8301 hole_size: hole_size.map(|item| item.1),
8302 style: style.map(|item| item.1),
8303 axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8304 original_series_keys,
8305 original_axis_ids: parsed_ids
8306 .map_or_else(Vec::new, |ids| ids.to_vec()),
8307 parsed_remaining: match local {
8308 b"pieChart" => Some("pieChart"),
8309 b"doughnutChart" => Some("doughnutChart"),
8310 b"areaChart" => Some("areaChart"),
8311 b"scatterChart" => Some("scatterChart"),
8312 b"radarChart" => Some("radarChart"),
8313 _ => None,
8314 },
8315 ..PlotMarkup::default()
8316 },
8317 ));
8318 }
8319 Event::Eof => return Err(missing_end(&format!("c:{root}"))),
8320 _ => {}
8321 }
8322 inner.clear();
8323 }
8324 }
8325 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
8326 return Err(ChartError::MissingElement("c:ser".to_owned()));
8327 }
8328 Event::Start(element) | Event::Empty(element) => {
8329 return Err(ChartError::UnexpectedElement(element_name(&element)));
8330 }
8331 Event::Eof => return Err(ChartError::MissingElement(format!("c:{root}"))),
8332 _ => {}
8333 }
8334 buffer.clear();
8335 }
8336}
8337
8338#[allow(clippy::too_many_arguments)]
8339fn parse_remaining_plot_child(
8340 root: &[u8],
8341 name: &[u8],
8342 raw: Vec<u8>,
8343 namespaces: &NamespaceBindings,
8344 grouping: &mut Option<(Grouping, ScalarMarkup)>,
8345 style: &mut Option<(String, ScalarMarkup)>,
8346 first_slice_angle: &mut Option<(u16, ScalarMarkup)>,
8347 hole_size: &mut Option<(u8, ScalarMarkup)>,
8348 series: &mut Vec<Series>,
8349 data_labels: &mut Option<CT_DLbls>,
8350 axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
8351 raw_children: &mut OrderedRawChildren,
8352 boundary: &mut usize,
8353) -> Result<()> {
8354 let property_first = matches!(root, b"areaChart" | b"scatterChart" | b"radarChart");
8355 let base = usize::from(property_first);
8356 match name {
8357 b"grouping" if root == b"areaChart" => {
8358 let (value, markup) = required_scalar(&raw, "grouping")?;
8359 let parsed = Grouping::parse(&value)
8360 .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
8361 set_once(grouping, (parsed, markup), "c:grouping")?;
8362 *boundary = (*boundary).max(1);
8363 }
8364 b"scatterStyle" if root == b"scatterChart" => {
8365 let (value, markup) = required_scalar(&raw, "scatterStyle")?;
8366 if ScatterStyle::parse(&value).is_none() {
8367 return Err(invalid_attribute("scatterStyle", "val", value));
8368 }
8369 set_once(style, (value, markup), "c:scatterStyle")?;
8370 *boundary = (*boundary).max(1);
8371 }
8372 b"radarStyle" if root == b"radarChart" => {
8373 let (value, markup) = required_scalar(&raw, "radarStyle")?;
8374 if RadarStyle::parse(&value).is_none() {
8375 return Err(invalid_attribute("radarStyle", "val", value));
8376 }
8377 set_once(style, (value, markup), "c:radarStyle")?;
8378 *boundary = (*boundary).max(1);
8379 }
8380 b"ser" => {
8381 series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
8382 *boundary = (*boundary).max(base + series.len());
8383 }
8384 b"dLbls" => {
8385 set_once(
8386 data_labels,
8387 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
8388 "c:dLbls",
8389 )?;
8390 *boundary = (*boundary).max(base + series.len() + 1);
8391 }
8392 b"firstSliceAng" if matches!(root, b"pieChart" | b"doughnutChart") => {
8393 let (value, markup) = required_scalar(&raw, "firstSliceAng")?;
8394 let parsed = value
8395 .parse::<u16>()
8396 .map_err(|_| invalid_attribute("firstSliceAng", "val", value.clone()))?;
8397 if parsed > 360 {
8398 return Err(invalid_attribute("firstSliceAng", "val", value));
8399 }
8400 set_once(first_slice_angle, (parsed, markup), "c:firstSliceAng")?;
8401 *boundary = (*boundary).max(series.len() + 2);
8402 }
8403 b"holeSize" if root == b"doughnutChart" => {
8404 let (value, markup) = required_scalar(&raw, "holeSize")?;
8405 let parsed = value
8406 .parse::<u8>()
8407 .map_err(|_| invalid_attribute("holeSize", "val", value.clone()))?;
8408 if !(10..=90).contains(&parsed) {
8409 return Err(invalid_attribute("holeSize", "val", value));
8410 }
8411 set_once(hole_size, (parsed, markup), "c:holeSize")?;
8412 *boundary = (*boundary).max(series.len() + 3);
8413 }
8414 b"axId" if property_first => {
8415 if axis_ids.len() == 2 {
8416 return Err(ChartError::DuplicateElement("c:axId".to_owned()));
8417 }
8418 axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
8419 *boundary = (*boundary).max(base + series.len() + 1 + axis_ids.len());
8420 }
8421 b"axId" => {
8422 return Err(ChartError::InvalidValue {
8423 element: "c:axId".to_owned(),
8424 value: "axis-free plot contains axis reference".to_owned(),
8425 });
8426 }
8427 _ => {
8428 let raw_boundary = match (root, name) {
8429 (_, b"varyColors") => base,
8430 (b"pieChart", b"extLst") => series.len() + 2,
8431 (b"doughnutChart", b"extLst") => series.len() + 3,
8432 (b"areaChart" | b"scatterChart" | b"radarChart", b"dropLines") => {
8433 base + series.len() + 1
8434 }
8435 (b"areaChart" | b"scatterChart" | b"radarChart", b"extLst") => {
8436 base + series.len() + 3
8437 }
8438 _ => *boundary,
8439 };
8440 raw_children.push(raw_boundary, raw);
8441 *boundary = (*boundary).max(raw_boundary);
8442 }
8443 }
8444 Ok(())
8445}
8446
8447fn parse_bar_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8448 let mut reader = Reader::from_reader(xml);
8449 let mut buffer = Vec::new();
8450 loop {
8451 match reader
8452 .read_event_into(&mut buffer)
8453 .map_err(OxmlError::from)?
8454 {
8455 Event::Start(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
8456 let namespaces = typed_rewrite_bindings(&element, inherited)?;
8457 let (mut raw_attributes, namespace_declarations) =
8458 capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8459 raw_attributes.splice(0..0, namespace_declarations);
8460 let mut direction = None;
8461 let mut grouping = None;
8462 let mut gap_width = None;
8463 let mut overlap = None;
8464 let mut series = Vec::new();
8465 let mut data_labels = None;
8466 let mut axis_ids = Vec::new();
8467 let mut raw_children = OrderedRawChildren::default();
8468 let mut boundary = 0usize;
8469 let mut inner = Vec::new();
8470 loop {
8471 match reader
8472 .read_event_into(&mut inner)
8473 .map_err(OxmlError::from)?
8474 {
8475 Event::Start(child) => {
8476 let name = chart_child_local(&child, &namespaces)?;
8477 let raw = capture_element(&mut reader, &child)?;
8478 parse_bar_child(
8479 name.as_deref().unwrap_or_default(),
8480 raw,
8481 &namespaces,
8482 &mut direction,
8483 &mut grouping,
8484 &mut gap_width,
8485 &mut overlap,
8486 &mut series,
8487 &mut data_labels,
8488 &mut axis_ids,
8489 &mut raw_children,
8490 &mut boundary,
8491 )?;
8492 }
8493 Event::Empty(child) => {
8494 let name = chart_child_local(&child, &namespaces)?;
8495 let raw = capture_empty_element(&child)?;
8496 parse_bar_child(
8497 name.as_deref().unwrap_or_default(),
8498 raw,
8499 &namespaces,
8500 &mut direction,
8501 &mut grouping,
8502 &mut gap_width,
8503 &mut overlap,
8504 &mut series,
8505 &mut data_labels,
8506 &mut axis_ids,
8507 &mut raw_children,
8508 &mut boundary,
8509 )?;
8510 }
8511 event @ (Event::Text(_)
8512 | Event::CData(_)
8513 | Event::Comment(_)
8514 | Event::PI(_)
8515 | Event::GeneralRef(_)) => {
8516 raw_children.push(boundary, capture_event(event)?);
8517 }
8518 Event::End(end) if matches_local_name(end.name().as_ref(), b"barChart") => {
8519 let (direction, direction_markup) = direction
8520 .ok_or_else(|| ChartError::MissingElement("c:barDir".to_owned()))?;
8521 let (grouping, grouping_markup) = grouping.ok_or_else(|| {
8522 ChartError::MissingElement("c:grouping".to_owned())
8523 })?;
8524 if axis_ids.len() != 2 {
8525 return Err(ChartError::InvalidValue {
8526 element: "c:barChart/c:axId".to_owned(),
8527 value: format!("expected 2, found {}", axis_ids.len()),
8528 });
8529 }
8530 let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
8531 let original_series_keys: Vec<(u32, u32)> =
8532 series.iter().map(|item| (item.index, item.order)).collect();
8533 let plot = Plot::Bar {
8534 direction,
8535 grouping,
8536 gap_width: gap_width.as_ref().map_or(150, |item| item.0),
8537 overlap: overlap.as_ref().map_or(0, |item| item.0),
8538 series,
8539 data_labels,
8540 axis_ids: parsed_ids,
8541 };
8542 plot.validate()?;
8543 return Ok((
8544 plot,
8545 PlotMarkup {
8546 raw_attributes,
8547 raw_children: raw_children_in_schema_order(
8548 &raw_children,
8549 original_series_keys.len() + 7,
8550 ),
8551 direction: Some(direction_markup),
8552 grouping: Some(grouping_markup),
8553 gap_width: gap_width.map(|item| item.1),
8554 overlap: overlap.map(|item| item.1),
8555 marker: None,
8556 smooth: None,
8557 first_slice_angle: None,
8558 hole_size: None,
8559 style: None,
8560 axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8561 original_series_keys,
8562 original_axis_ids: parsed_ids.to_vec(),
8563 parsed_bar: Some(true),
8564 parsed_remaining: None,
8565 },
8566 ));
8567 }
8568 Event::Eof => return Err(missing_end("c:barChart")),
8569 _ => {}
8570 }
8571 inner.clear();
8572 }
8573 }
8574 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
8575 return Err(ChartError::MissingElement("c:barDir".to_owned()));
8576 }
8577 Event::Start(element) | Event::Empty(element) => {
8578 return Err(ChartError::UnexpectedElement(element_name(&element)));
8579 }
8580 Event::Eof => return Err(ChartError::MissingElement("c:barChart".to_owned())),
8581 _ => {}
8582 }
8583 buffer.clear();
8584 }
8585}
8586
8587#[allow(clippy::too_many_arguments)]
8588fn parse_bar_child(
8589 name: &[u8],
8590 raw: Vec<u8>,
8591 namespaces: &NamespaceBindings,
8592 direction: &mut Option<(BarDirection, ScalarMarkup)>,
8593 grouping: &mut Option<(BarGrouping, ScalarMarkup)>,
8594 gap_width: &mut Option<(u16, ScalarMarkup)>,
8595 overlap: &mut Option<(i8, ScalarMarkup)>,
8596 series: &mut Vec<Series>,
8597 data_labels: &mut Option<CT_DLbls>,
8598 axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
8599 raw_children: &mut OrderedRawChildren,
8600 boundary: &mut usize,
8601) -> Result<()> {
8602 match name {
8603 b"barDir" => {
8604 let (value, markup) = required_scalar(&raw, "barDir")?;
8605 let value = BarDirection::parse(&value)
8606 .ok_or_else(|| invalid_attribute("barDir", "val", value))?;
8607 set_once(direction, (value, markup), "c:barDir")?;
8608 *boundary = (*boundary).max(1);
8609 }
8610 b"grouping" => {
8611 let (value, markup) = required_scalar(&raw, "grouping")?;
8612 let value = BarGrouping::parse(&value)
8613 .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
8614 set_once(grouping, (value, markup), "c:grouping")?;
8615 *boundary = (*boundary).max(2);
8616 }
8617 b"ser" => {
8618 series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
8619 *boundary = (*boundary).max(series.len() + 2);
8620 }
8621 b"dLbls" => {
8622 set_once(
8623 data_labels,
8624 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
8625 "c:dLbls",
8626 )?;
8627 *boundary = (*boundary).max(series.len() + 3);
8628 }
8629 b"gapWidth" => {
8630 let (value, markup) = required_scalar(&raw, "gapWidth")?;
8631 let parsed = value
8632 .parse::<u16>()
8633 .map_err(|_| invalid_attribute("gapWidth", "val", value.clone()))?;
8634 if parsed > 500 {
8635 return Err(invalid_attribute("gapWidth", "val", value));
8636 }
8637 set_once(gap_width, (parsed, markup), "c:gapWidth")?;
8638 *boundary = (*boundary).max(series.len() + 4);
8639 }
8640 b"overlap" => {
8641 let (value, markup) = required_scalar(&raw, "overlap")?;
8642 let parsed = value
8643 .parse::<i8>()
8644 .map_err(|_| invalid_attribute("overlap", "val", value.clone()))?;
8645 if !(-100..=100).contains(&parsed) {
8646 return Err(invalid_attribute("overlap", "val", value));
8647 }
8648 set_once(overlap, (parsed, markup), "c:overlap")?;
8649 *boundary = (*boundary).max(series.len() + 5);
8650 }
8651 b"axId" => {
8652 if axis_ids.len() == 2 {
8653 return Err(ChartError::DuplicateElement("c:axId".to_owned()));
8654 }
8655 axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
8656 *boundary = (*boundary).max(series.len() + 5 + axis_ids.len());
8657 }
8658 _ => raw_children.push(*boundary, raw),
8659 }
8660 Ok(())
8661}
8662
8663fn parse_line_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8664 let mut reader = Reader::from_reader(xml);
8665 let mut buffer = Vec::new();
8666 loop {
8667 match reader
8668 .read_event_into(&mut buffer)
8669 .map_err(OxmlError::from)?
8670 {
8671 Event::Start(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
8672 let namespaces = typed_rewrite_bindings(&element, inherited)?;
8673 let (mut raw_attributes, namespace_declarations) =
8674 capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8675 raw_attributes.splice(0..0, namespace_declarations);
8676 let mut grouping = None;
8677 let mut marker = None;
8678 let mut smooth = None;
8679 let mut series = Vec::new();
8680 let mut data_labels = None;
8681 let mut axis_ids = Vec::new();
8682 let mut raw_children = OrderedRawChildren::default();
8683 let mut boundary = 0usize;
8684 let mut inner = Vec::new();
8685 loop {
8686 match reader
8687 .read_event_into(&mut inner)
8688 .map_err(OxmlError::from)?
8689 {
8690 Event::Start(child) => {
8691 let name = chart_child_local(&child, &namespaces)?;
8692 let raw = capture_element(&mut reader, &child)?;
8693 parse_line_child(
8694 name.as_deref().unwrap_or_default(),
8695 raw,
8696 &namespaces,
8697 &mut grouping,
8698 &mut marker,
8699 &mut smooth,
8700 &mut series,
8701 &mut data_labels,
8702 &mut axis_ids,
8703 &mut raw_children,
8704 &mut boundary,
8705 )?;
8706 }
8707 Event::Empty(child) => {
8708 let name = chart_child_local(&child, &namespaces)?;
8709 let raw = capture_empty_element(&child)?;
8710 parse_line_child(
8711 name.as_deref().unwrap_or_default(),
8712 raw,
8713 &namespaces,
8714 &mut grouping,
8715 &mut marker,
8716 &mut smooth,
8717 &mut series,
8718 &mut data_labels,
8719 &mut axis_ids,
8720 &mut raw_children,
8721 &mut boundary,
8722 )?;
8723 }
8724 event @ (Event::Text(_)
8725 | Event::CData(_)
8726 | Event::Comment(_)
8727 | Event::PI(_)
8728 | Event::GeneralRef(_)) => {
8729 raw_children.push(boundary, capture_event(event)?);
8730 }
8731 Event::End(end)
8732 if matches_local_name(end.name().as_ref(), b"lineChart") =>
8733 {
8734 let (grouping, grouping_markup) = grouping.ok_or_else(|| {
8735 ChartError::MissingElement("c:grouping".to_owned())
8736 })?;
8737 if axis_ids.len() != 2 {
8738 return Err(ChartError::InvalidValue {
8739 element: "c:lineChart/c:axId".to_owned(),
8740 value: format!("expected 2, found {}", axis_ids.len()),
8741 });
8742 }
8743 let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
8744 let original_series_keys: Vec<(u32, u32)> =
8745 series.iter().map(|item| (item.index, item.order)).collect();
8746 let plot = Plot::Line {
8747 grouping,
8748 marker: marker.as_ref().is_some_and(|item| item.0),
8749 smooth: smooth.as_ref().is_some_and(|item| item.0),
8750 series,
8751 data_labels,
8752 axis_ids: parsed_ids,
8753 };
8754 plot.validate()?;
8755 return Ok((
8756 plot,
8757 PlotMarkup {
8758 raw_attributes,
8759 raw_children: raw_children_in_schema_order(
8760 &raw_children,
8761 original_series_keys.len() + 6,
8762 ),
8763 direction: None,
8764 grouping: Some(grouping_markup),
8765 gap_width: None,
8766 overlap: None,
8767 marker: marker.map(|item| item.1),
8768 smooth: smooth.map(|item| item.1),
8769 first_slice_angle: None,
8770 hole_size: None,
8771 style: None,
8772 axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8773 original_series_keys,
8774 original_axis_ids: parsed_ids.to_vec(),
8775 parsed_bar: Some(false),
8776 parsed_remaining: None,
8777 },
8778 ));
8779 }
8780 Event::Eof => return Err(missing_end("c:lineChart")),
8781 _ => {}
8782 }
8783 inner.clear();
8784 }
8785 }
8786 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
8787 return Err(ChartError::MissingElement("c:grouping".to_owned()));
8788 }
8789 Event::Start(element) | Event::Empty(element) => {
8790 return Err(ChartError::UnexpectedElement(element_name(&element)));
8791 }
8792 Event::Eof => return Err(ChartError::MissingElement("c:lineChart".to_owned())),
8793 _ => {}
8794 }
8795 buffer.clear();
8796 }
8797}
8798
8799#[allow(clippy::too_many_arguments)]
8800fn parse_line_child(
8801 name: &[u8],
8802 raw: Vec<u8>,
8803 namespaces: &NamespaceBindings,
8804 grouping: &mut Option<(Grouping, ScalarMarkup)>,
8805 marker: &mut Option<(bool, ScalarMarkup)>,
8806 smooth: &mut Option<(bool, ScalarMarkup)>,
8807 series: &mut Vec<Series>,
8808 data_labels: &mut Option<CT_DLbls>,
8809 axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
8810 raw_children: &mut OrderedRawChildren,
8811 boundary: &mut usize,
8812) -> Result<()> {
8813 match name {
8814 b"grouping" => {
8815 let (value, markup) = required_scalar(&raw, "grouping")?;
8816 let value = Grouping::parse(&value)
8817 .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
8818 set_once(grouping, (value, markup), "c:grouping")?;
8819 *boundary = (*boundary).max(1);
8820 }
8821 b"ser" => {
8822 series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
8823 *boundary = (*boundary).max(series.len() + 1);
8824 }
8825 b"dLbls" => {
8826 set_once(
8827 data_labels,
8828 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
8829 "c:dLbls",
8830 )?;
8831 *boundary = (*boundary).max(series.len() + 2);
8832 }
8833 b"marker" => {
8834 set_once(marker, parse_bool_value(&raw, "marker")?, "c:marker")?;
8835 *boundary = (*boundary).max(series.len() + 3);
8836 }
8837 b"smooth" => {
8838 set_once(smooth, parse_bool_value(&raw, "smooth")?, "c:smooth")?;
8839 *boundary = (*boundary).max(series.len() + 4);
8840 }
8841 b"axId" => {
8842 if axis_ids.len() == 2 {
8843 return Err(ChartError::DuplicateElement("c:axId".to_owned()));
8844 }
8845 axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
8846 *boundary = (*boundary).max(series.len() + 4 + axis_ids.len());
8847 }
8848 _ => raw_children.push(*boundary, raw),
8849 }
8850 Ok(())
8851}
8852
8853fn required_scalar(xml: &[u8], local: &str) -> Result<(String, ScalarMarkup)> {
8854 let (value, markup) = scalar_value(xml, local)?;
8855 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8856 Ok((value, markup))
8857}
8858
8859fn write_plot<W: Write>(writer: &mut Writer<W>, plot: &Plot, markup: &PlotMarkup) -> Result<()> {
8860 plot.validate()?;
8861 if let Some(parsed_bar) = markup.parsed_bar
8862 && parsed_bar != matches!(plot, Plot::Bar { .. })
8863 {
8864 return Err(ChartError::InvalidValue {
8865 element: "c:plotArea".to_owned(),
8866 value: "a parsed plot family cannot be replaced while preserved payload remains"
8867 .to_owned(),
8868 });
8869 }
8870 let current_remaining = match plot {
8871 Plot::Pie { .. } => Some("pieChart"),
8872 Plot::Doughnut { .. } => Some("doughnutChart"),
8873 Plot::Area { .. } => Some("areaChart"),
8874 Plot::Scatter { .. } => Some("scatterChart"),
8875 Plot::Radar { .. } => Some("radarChart"),
8876 Plot::Bar { .. } | Plot::Line { .. } => None,
8877 };
8878 if let Some(parsed) = markup.parsed_remaining
8879 && current_remaining != Some(parsed)
8880 {
8881 return Err(ChartError::InvalidValue {
8882 element: "c:plotArea".to_owned(),
8883 value: "a parsed plot family cannot be replaced while preserved payload remains"
8884 .to_owned(),
8885 });
8886 }
8887 match plot {
8888 Plot::Bar {
8889 direction,
8890 grouping,
8891 gap_width,
8892 overlap,
8893 series,
8894 data_labels,
8895 axis_ids,
8896 } => {
8897 let mut start = BytesStart::new("c:barChart");
8898 push_attributes(&mut start, &markup.raw_attributes);
8899 writer
8900 .write_event(Event::Start(start))
8901 .map_err(OxmlError::from)?;
8902 emit_raw(writer, markup.raw_children.at(0))?;
8903 write_scalar(
8904 writer,
8905 "c:barDir",
8906 direction.as_str(),
8907 markup.direction.as_ref(),
8908 )?;
8909 emit_raw(writer, markup.raw_children.at(1))?;
8910 write_scalar(
8911 writer,
8912 "c:grouping",
8913 grouping.as_str(),
8914 markup.grouping.as_ref(),
8915 )?;
8916 let original_to_current =
8917 series_original_to_current(&markup.original_series_keys, series);
8918 emit_repeated_raw(
8919 writer,
8920 &markup.raw_children,
8921 2,
8922 &original_to_current,
8923 series.len(),
8924 0,
8925 )?;
8926 for (index, item) in series.iter().enumerate() {
8927 writer
8928 .get_mut()
8929 .write_all(&item.to_xml()?)
8930 .map_err(OxmlError::from)?;
8931 emit_repeated_raw(
8932 writer,
8933 &markup.raw_children,
8934 2,
8935 &original_to_current,
8936 series.len(),
8937 index + 1,
8938 )?;
8939 }
8940 if let Some(labels) = data_labels {
8941 writer
8942 .get_mut()
8943 .write_all(&labels.to_xml()?)
8944 .map_err(OxmlError::from)?;
8945 }
8946 let trailing = markup.original_series_keys.len();
8947 emit_raw(writer, markup.raw_children.at(trailing + 3))?;
8948 if *gap_width != 150 || markup.gap_width.is_some() {
8949 write_scalar(
8950 writer,
8951 "c:gapWidth",
8952 &gap_width.to_string(),
8953 markup.gap_width.as_ref(),
8954 )?;
8955 }
8956 emit_raw(writer, markup.raw_children.at(trailing + 4))?;
8957 if *overlap != 0 || markup.overlap.is_some() {
8958 write_scalar(
8959 writer,
8960 "c:overlap",
8961 &overlap.to_string(),
8962 markup.overlap.as_ref(),
8963 )?;
8964 }
8965 let axis_original_to_current =
8966 axis_original_to_current(&markup.original_axis_ids, axis_ids);
8967 let axis_current_to_original =
8968 invert_original_to_current(&axis_original_to_current, axis_ids.len());
8969 emit_repeated_raw(
8970 writer,
8971 &markup.raw_children,
8972 trailing + 5,
8973 &axis_original_to_current,
8974 axis_ids.len(),
8975 0,
8976 )?;
8977 for (index, id) in axis_ids.iter().enumerate() {
8978 let default_markup = default_axis_id_markup(*id);
8979 let original_markup = axis_current_to_original[index]
8980 .and_then(|original| markup.axis_ids.get(original));
8981 write_axis_id(
8982 writer,
8983 "c:axId",
8984 *id,
8985 original_markup.unwrap_or(&default_markup),
8986 )?;
8987 emit_repeated_raw(
8988 writer,
8989 &markup.raw_children,
8990 trailing + 5,
8991 &axis_original_to_current,
8992 axis_ids.len(),
8993 index + 1,
8994 )?;
8995 }
8996 writer
8997 .write_event(Event::End(BytesEnd::new("c:barChart")))
8998 .map_err(OxmlError::from)?;
8999 }
9000 Plot::Line {
9001 grouping,
9002 marker,
9003 smooth,
9004 series,
9005 data_labels,
9006 axis_ids,
9007 } => {
9008 let mut start = BytesStart::new("c:lineChart");
9009 push_attributes(&mut start, &markup.raw_attributes);
9010 writer
9011 .write_event(Event::Start(start))
9012 .map_err(OxmlError::from)?;
9013 emit_raw(writer, markup.raw_children.at(0))?;
9014 write_scalar(
9015 writer,
9016 "c:grouping",
9017 grouping.as_str(),
9018 markup.grouping.as_ref(),
9019 )?;
9020 let original_to_current =
9021 series_original_to_current(&markup.original_series_keys, series);
9022 emit_repeated_raw(
9023 writer,
9024 &markup.raw_children,
9025 1,
9026 &original_to_current,
9027 series.len(),
9028 0,
9029 )?;
9030 for (index, item) in series.iter().enumerate() {
9031 writer
9032 .get_mut()
9033 .write_all(&item.to_xml()?)
9034 .map_err(OxmlError::from)?;
9035 emit_repeated_raw(
9036 writer,
9037 &markup.raw_children,
9038 1,
9039 &original_to_current,
9040 series.len(),
9041 index + 1,
9042 )?;
9043 }
9044 if let Some(labels) = data_labels {
9045 writer
9046 .get_mut()
9047 .write_all(&labels.to_xml()?)
9048 .map_err(OxmlError::from)?;
9049 }
9050 let trailing = markup.original_series_keys.len();
9051 emit_raw(writer, markup.raw_children.at(trailing + 2))?;
9052 if *marker || markup.marker.is_some() {
9053 write_scalar(
9054 writer,
9055 "c:marker",
9056 bool_lexical(*marker),
9057 markup.marker.as_ref(),
9058 )?;
9059 }
9060 emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9061 if *smooth || markup.smooth.is_some() {
9062 write_scalar(
9063 writer,
9064 "c:smooth",
9065 bool_lexical(*smooth),
9066 markup.smooth.as_ref(),
9067 )?;
9068 }
9069 let axis_original_to_current =
9070 axis_original_to_current(&markup.original_axis_ids, axis_ids);
9071 let axis_current_to_original =
9072 invert_original_to_current(&axis_original_to_current, axis_ids.len());
9073 emit_repeated_raw(
9074 writer,
9075 &markup.raw_children,
9076 trailing + 4,
9077 &axis_original_to_current,
9078 axis_ids.len(),
9079 0,
9080 )?;
9081 for (index, id) in axis_ids.iter().enumerate() {
9082 let default_markup = default_axis_id_markup(*id);
9083 let original_markup = axis_current_to_original[index]
9084 .and_then(|original| markup.axis_ids.get(original));
9085 write_axis_id(
9086 writer,
9087 "c:axId",
9088 *id,
9089 original_markup.unwrap_or(&default_markup),
9090 )?;
9091 emit_repeated_raw(
9092 writer,
9093 &markup.raw_children,
9094 trailing + 4,
9095 &axis_original_to_current,
9096 axis_ids.len(),
9097 index + 1,
9098 )?;
9099 }
9100 writer
9101 .write_event(Event::End(BytesEnd::new("c:lineChart")))
9102 .map_err(OxmlError::from)?;
9103 }
9104 remaining => {
9105 let (
9106 root,
9107 property,
9108 series,
9109 data_labels,
9110 axis_ids,
9111 first_slice_angle,
9112 hole_size,
9113 scatter,
9114 ) = match remaining {
9115 Plot::Pie {
9116 first_slice_angle,
9117 series,
9118 data_labels,
9119 } => (
9120 "pieChart",
9121 None,
9122 series,
9123 data_labels,
9124 None,
9125 Some(*first_slice_angle),
9126 None,
9127 false,
9128 ),
9129 Plot::Doughnut {
9130 first_slice_angle,
9131 hole_size,
9132 series,
9133 data_labels,
9134 } => (
9135 "doughnutChart",
9136 None,
9137 series,
9138 data_labels,
9139 None,
9140 Some(*first_slice_angle),
9141 Some(*hole_size),
9142 false,
9143 ),
9144 Plot::Area {
9145 grouping,
9146 series,
9147 data_labels,
9148 axis_ids,
9149 } => (
9150 "areaChart",
9151 Some(("c:grouping", grouping.as_str(), markup.grouping.as_ref())),
9152 series,
9153 data_labels,
9154 Some(axis_ids),
9155 None,
9156 None,
9157 false,
9158 ),
9159 Plot::Scatter {
9160 style,
9161 series,
9162 data_labels,
9163 axis_ids,
9164 } => (
9165 "scatterChart",
9166 Some(("c:scatterStyle", style.as_str(), markup.style.as_ref())),
9167 series,
9168 data_labels,
9169 Some(axis_ids),
9170 None,
9171 None,
9172 true,
9173 ),
9174 Plot::Radar {
9175 style,
9176 series,
9177 data_labels,
9178 axis_ids,
9179 } => (
9180 "radarChart",
9181 Some(("c:radarStyle", style.as_str(), markup.style.as_ref())),
9182 series,
9183 data_labels,
9184 Some(axis_ids),
9185 None,
9186 None,
9187 false,
9188 ),
9189 Plot::Bar { .. } | Plot::Line { .. } => unreachable!("handled above"),
9190 };
9191 let tag = format!("c:{root}");
9192 let mut start = BytesStart::new(&tag);
9193 push_attributes(&mut start, &markup.raw_attributes);
9194 writer
9195 .write_event(Event::Start(start))
9196 .map_err(OxmlError::from)?;
9197 let base = usize::from(property.is_some());
9198 if let Some((name, value, scalar_markup)) = property {
9199 emit_raw(writer, markup.raw_children.at(0))?;
9200 write_scalar(writer, name, value, scalar_markup)?;
9201 }
9202 let original_to_current =
9203 series_original_to_current(&markup.original_series_keys, series);
9204 emit_repeated_raw(
9205 writer,
9206 &markup.raw_children,
9207 base,
9208 &original_to_current,
9209 series.len(),
9210 0,
9211 )?;
9212 for (index, item) in series.iter().enumerate() {
9213 writer
9214 .get_mut()
9215 .write_all(&item.to_xml_for_plot(scatter)?)
9216 .map_err(OxmlError::from)?;
9217 emit_repeated_raw(
9218 writer,
9219 &markup.raw_children,
9220 base,
9221 &original_to_current,
9222 series.len(),
9223 index + 1,
9224 )?;
9225 }
9226 if let Some(labels) = data_labels {
9227 writer
9228 .get_mut()
9229 .write_all(&labels.to_xml()?)
9230 .map_err(OxmlError::from)?;
9231 }
9232 let trailing = base + markup.original_series_keys.len();
9233 if let Some(axis_ids) = axis_ids {
9234 let axis_original_to_current =
9235 axis_original_to_current(&markup.original_axis_ids, axis_ids);
9236 let axis_current_to_original =
9237 invert_original_to_current(&axis_original_to_current, axis_ids.len());
9238 emit_repeated_raw(
9239 writer,
9240 &markup.raw_children,
9241 trailing + 1,
9242 &axis_original_to_current,
9243 axis_ids.len(),
9244 0,
9245 )?;
9246 for (index, id) in axis_ids.iter().enumerate() {
9247 let default_markup = default_axis_id_markup(*id);
9248 let original_markup = axis_current_to_original[index]
9249 .and_then(|original| markup.axis_ids.get(original));
9250 write_axis_id(
9251 writer,
9252 "c:axId",
9253 *id,
9254 original_markup.unwrap_or(&default_markup),
9255 )?;
9256 emit_repeated_raw(
9257 writer,
9258 &markup.raw_children,
9259 trailing + 1,
9260 &axis_original_to_current,
9261 axis_ids.len(),
9262 index + 1,
9263 )?;
9264 }
9265 } else {
9266 emit_raw(writer, markup.raw_children.at(trailing + 1))?;
9267 if let Some(angle) = first_slice_angle {
9268 if angle != 0 || markup.first_slice_angle.is_some() {
9269 write_scalar(
9270 writer,
9271 "c:firstSliceAng",
9272 &angle.to_string(),
9273 markup.first_slice_angle.as_ref(),
9274 )?;
9275 }
9276 emit_raw(writer, markup.raw_children.at(trailing + 2))?;
9277 }
9278 if let Some(size) = hole_size {
9279 if size != 50 || markup.hole_size.is_some() {
9280 write_scalar(
9281 writer,
9282 "c:holeSize",
9283 &size.to_string(),
9284 markup.hole_size.as_ref(),
9285 )?;
9286 }
9287 emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9288 }
9289 }
9290 writer
9291 .write_event(Event::End(BytesEnd::new(&tag)))
9292 .map_err(OxmlError::from)?;
9293 }
9294 }
9295 Ok(())
9296}
9297
9298fn series_original_to_current(original: &[(u32, u32)], current: &[Series]) -> Vec<Option<usize>> {
9299 let mut used = vec![false; current.len()];
9300 let mut matches = vec![None; original.len()];
9301 for original_index in 0..original.len().min(current.len()) {
9302 if original[original_index]
9303 == (current[original_index].index, current[original_index].order)
9304 {
9305 matches[original_index] = Some(original_index);
9306 used[original_index] = true;
9307 }
9308 }
9309 for (original_index, key) in original.iter().enumerate() {
9310 if matches[original_index].is_some() {
9311 continue;
9312 }
9313 if let Some(current_index) = current
9314 .iter()
9315 .enumerate()
9316 .position(|(index, item)| !used[index] && (item.index, item.order) == *key)
9317 {
9318 matches[original_index] = Some(current_index);
9319 used[current_index] = true;
9320 }
9321 }
9322 match_unidentified_by_position(&mut matches, &mut used);
9323 matches
9324}
9325
9326fn axis_original_to_current(original: &[AxisId], current: &[AxisId]) -> Vec<Option<usize>> {
9327 let mut used = vec![false; current.len()];
9328 let mut matches = vec![None; original.len()];
9329 for (original_index, id) in original.iter().enumerate() {
9330 if let Some(current_index) = current
9331 .iter()
9332 .enumerate()
9333 .position(|(index, current_id)| !used[index] && current_id == id)
9334 {
9335 matches[original_index] = Some(current_index);
9336 used[current_index] = true;
9337 }
9338 }
9339 match_unidentified_by_position(&mut matches, &mut used);
9340 matches
9341}
9342
9343fn match_unidentified_by_position(matches: &mut [Option<usize>], used: &mut [bool]) {
9344 for original_index in 0..matches.len() {
9345 if matches[original_index].is_none() && original_index < used.len() && !used[original_index]
9346 {
9347 matches[original_index] = Some(original_index);
9348 used[original_index] = true;
9349 }
9350 }
9351 for matched in matches.iter_mut().filter(|matched| matched.is_none()) {
9352 if let Some(current_index) = used.iter().position(|used| !used) {
9353 *matched = Some(current_index);
9354 used[current_index] = true;
9355 }
9356 }
9357}
9358
9359fn invert_original_to_current(
9360 original_to_current: &[Option<usize>],
9361 current_len: usize,
9362) -> Vec<Option<usize>> {
9363 let mut current_to_original = vec![None; current_len];
9364 for (original, current) in original_to_current.iter().enumerate() {
9365 if let Some(current) = current {
9366 current_to_original[*current] = Some(original);
9367 }
9368 }
9369 current_to_original
9370}
9371
9372fn emit_repeated_raw<W: Write>(
9373 writer: &mut Writer<W>,
9374 raw_children: &OrderedRawChildren,
9375 offset: usize,
9376 original_to_current: &[Option<usize>],
9377 current_len: usize,
9378 current_boundary: usize,
9379) -> Result<()> {
9380 if current_boundary == 0 {
9381 emit_raw(writer, raw_children.at(offset))?;
9382 }
9383 for original_boundary in 1..=original_to_current.len() {
9384 let effective = original_to_current
9385 .iter()
9386 .skip(original_boundary)
9387 .flatten()
9388 .copied()
9389 .next()
9390 .unwrap_or(current_len);
9391 if effective == current_boundary {
9392 emit_raw(writer, raw_children.at(offset + original_boundary))?;
9393 }
9394 }
9395 Ok(())
9396}
9397
9398fn parse_plot_axis(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Axis>> {
9399 let mut reader = Reader::from_reader(xml);
9400 let mut buffer = Vec::new();
9401 loop {
9402 match reader
9403 .read_event_into(&mut buffer)
9404 .map_err(OxmlError::from)?
9405 {
9406 Event::Start(element) | Event::Empty(element) => {
9407 if !element_is_in_namespace(&element, C_NS, inherited)? {
9408 return Ok(None);
9409 }
9410 let name = element.name();
9411 if AxisKind::parse(local_name(name.as_ref())).is_none() {
9412 return Ok(None);
9413 }
9414 return Axis::from_xml_with_namespaces(xml, inherited).map(Some);
9415 }
9416 Event::Eof => return Ok(None),
9417 _ => {}
9418 }
9419 buffer.clear();
9420 }
9421}
9422
9423fn validate_axis_pairs(axes: &[Axis]) -> Result<()> {
9424 for (index, axis) in axes.iter().enumerate() {
9425 if axes[..index].iter().any(|other| other.id == axis.id) {
9426 return Err(ChartError::DuplicateElement(format!(
9427 "c:axId {}",
9428 axis.id.value()
9429 )));
9430 }
9431 }
9432 for axis in axes {
9433 if axis.id == axis.cross_axis {
9434 return Err(ChartError::InvalidValue {
9435 element: "c:crossAx".to_owned(),
9436 value: format!("axis {} crosses itself", axis.id.value()),
9437 });
9438 }
9439 let crossed = axes
9440 .iter()
9441 .find(|candidate| candidate.id == axis.cross_axis)
9442 .ok_or_else(|| ChartError::InvalidValue {
9443 element: "c:crossAx".to_owned(),
9444 value: format!(
9445 "axis {} references missing axis {}",
9446 axis.id.value(),
9447 axis.cross_axis.value()
9448 ),
9449 })?;
9450 if crossed.cross_axis != axis.id {
9451 return Err(ChartError::InvalidValue {
9452 element: "c:crossAx".to_owned(),
9453 value: format!(
9454 "axis {} references {}, which references {}",
9455 axis.id.value(),
9456 crossed.id.value(),
9457 crossed.cross_axis.value()
9458 ),
9459 });
9460 }
9461 }
9462 Ok(())
9463}
9464
9465fn parse_plot_series(
9466 xml: &[u8],
9467 inherited: &NamespaceBindings,
9468 series: &mut Vec<Series>,
9469) -> Result<()> {
9470 let mut reader = Reader::from_reader(xml);
9471 let mut buffer = Vec::new();
9472 loop {
9473 match reader
9474 .read_event_into(&mut buffer)
9475 .map_err(OxmlError::from)?
9476 {
9477 Event::Start(element) => {
9478 if !element_is_in_namespace(&element, C_NS, inherited)? {
9479 return Ok(());
9480 }
9481 let local = local_name(element.name().as_ref()).to_vec();
9482 if !is_supported_series_plot(&local) {
9483 return Ok(());
9484 }
9485 let namespaces = chart_bindings(inherited, &element)?;
9486 let mut inner = Vec::new();
9487 loop {
9488 match reader
9489 .read_event_into(&mut inner)
9490 .map_err(OxmlError::from)?
9491 {
9492 Event::Start(child) => {
9493 let name = chart_child_local(&child, &namespaces)?;
9494 let raw = capture_element(&mut reader, &child)?;
9495 if name.as_deref() == Some(b"ser") {
9496 series.push(Series::from_xml_with_namespaces(&raw, &namespaces)?);
9497 }
9498 }
9499 Event::Empty(child) => {
9500 let name = chart_child_local(&child, &namespaces)?;
9501 if name.as_deref() == Some(b"ser") {
9502 return Err(ChartError::MissingElement("c:ser/c:idx".to_owned()));
9503 }
9504 }
9505 Event::End(end) if matches_local_name(end.name().as_ref(), &local) => {
9506 return Ok(());
9507 }
9508 Event::Eof => {
9509 return Err(missing_end(&format!(
9510 "c:{}",
9511 String::from_utf8_lossy(&local)
9512 )));
9513 }
9514 _ => {}
9515 }
9516 inner.clear();
9517 }
9518 }
9519 Event::Empty(_) | Event::Eof => return Ok(()),
9520 _ => {}
9521 }
9522 buffer.clear();
9523 }
9524}
9525
9526fn is_supported_series_plot(local: &[u8]) -> bool {
9527 matches!(
9528 local,
9529 b"areaChart"
9530 | b"area3DChart"
9531 | b"barChart"
9532 | b"bar3DChart"
9533 | b"doughnutChart"
9534 | b"lineChart"
9535 | b"line3DChart"
9536 | b"ofPieChart"
9537 | b"pieChart"
9538 | b"pie3DChart"
9539 | b"radarChart"
9540 | b"stockChart"
9541 | b"surfaceChart"
9542 | b"surface3DChart"
9543 )
9544}
9545
9546impl CT_Legend {
9547 fn from_xml(xml: &[u8]) -> Result<Self> {
9548 let (raw_attributes, raw_children) = parse_raw_shell(xml, b"legend", "c:legend")?;
9549 Ok(Self {
9550 raw_attributes,
9551 raw_children,
9552 })
9553 }
9554
9555 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
9556 write_raw_shell(writer, "c:legend", &self.raw_attributes, &self.raw_children)
9557 }
9558
9559 pub fn raw_children(&self) -> &OrderedRawChildren {
9560 &self.raw_children
9561 }
9562}
9563
9564fn parse_raw_shell(
9565 xml: &[u8],
9566 local: &[u8],
9567 tag: &str,
9568) -> Result<(XmlAttributes, OrderedRawChildren)> {
9569 let mut reader = Reader::from_reader(xml);
9570 let mut buffer = Vec::new();
9571 loop {
9572 match reader
9573 .read_event_into(&mut buffer)
9574 .map_err(OxmlError::from)?
9575 {
9576 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
9577 reject_conflicting_prefix(&element, b"c", C_NS)?;
9578 let raw_attributes = capture_attributes(&element)?;
9579 let mut raw_children = OrderedRawChildren::default();
9580 let mut child_buffer = Vec::new();
9581 loop {
9582 match reader
9583 .read_event_into(&mut child_buffer)
9584 .map_err(OxmlError::from)?
9585 {
9586 Event::Start(child) => {
9587 raw_children.push(0, capture_element(&mut reader, &child)?)
9588 }
9589 Event::Empty(child) => raw_children.push(0, capture_empty_element(&child)?),
9590 event @ (Event::Text(_)
9591 | Event::CData(_)
9592 | Event::Comment(_)
9593 | Event::PI(_)
9594 | Event::GeneralRef(_)) => {
9595 raw_children.push(0, capture_event(event)?);
9596 }
9597 Event::End(end) if matches_local_name(end.name().as_ref(), local) => break,
9598 Event::Eof => return Err(missing_end(tag)),
9599 _ => {}
9600 }
9601 child_buffer.clear();
9602 }
9603 return Ok((raw_attributes, raw_children));
9604 }
9605 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
9606 reject_conflicting_prefix(&element, b"c", C_NS)?;
9607 return Ok((capture_attributes(&element)?, OrderedRawChildren::default()));
9608 }
9609 Event::Start(element) | Event::Empty(element) => {
9610 return Err(ChartError::UnexpectedElement(element_name(&element)));
9611 }
9612 Event::Eof => return Err(ChartError::MissingElement(tag.to_owned())),
9613 _ => {}
9614 }
9615 buffer.clear();
9616 }
9617}
9618
9619fn write_raw_shell<W: Write>(
9620 writer: &mut Writer<W>,
9621 tag: &str,
9622 raw_attributes: &XmlAttributes,
9623 raw_children: &OrderedRawChildren,
9624) -> Result<()> {
9625 let mut start = BytesStart::new(tag);
9626 push_attributes(&mut start, raw_attributes);
9627 if raw_children.is_empty() {
9628 writer
9629 .write_event(Event::Empty(start))
9630 .map_err(OxmlError::from)?;
9631 return Ok(());
9632 }
9633 writer
9634 .write_event(Event::Start(start))
9635 .map_err(OxmlError::from)?;
9636 emit_raw(writer, raw_children.at(0))?;
9637 writer
9638 .write_event(Event::End(BytesEnd::new(tag)))
9639 .map_err(OxmlError::from)?;
9640 Ok(())
9641}
9642
9643#[derive(Clone, Debug, PartialEq)]
9645pub struct CT_Chart {
9646 pub title: Option<CT_Title>,
9647 pub auto_title_deleted: bool,
9648 pub plot_area: CT_PlotArea,
9649 pub legend: Option<CT_Legend>,
9650 pub plot_vis_only: bool,
9651 pub disp_blanks_as: DispBlanksAs,
9652 auto_title_deleted_markup: Option<ScalarMarkup>,
9653 plot_vis_only_markup: Option<ScalarMarkup>,
9654 disp_blanks_as_markup: Option<ScalarMarkup>,
9655 raw_attributes: Vec<(String, String)>,
9656 raw_children: OrderedRawChildren,
9657}
9658
9659impl CT_Chart {
9660 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
9661 let mut reader = Reader::from_reader(xml);
9662 let mut buffer = Vec::new();
9663 loop {
9664 match reader
9665 .read_event_into(&mut buffer)
9666 .map_err(OxmlError::from)?
9667 {
9668 Event::Start(element) if matches_local_name(element.name().as_ref(), b"chart") => {
9669 reject_conflicting_prefix(&element, b"c", C_NS)?;
9670 return Self::from_element(&mut reader, &element, inherited);
9671 }
9672 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"chart") => {
9673 return Err(ChartError::MissingElement("c:plotArea".to_owned()));
9674 }
9675 Event::Start(element) | Event::Empty(element) => {
9676 return Err(ChartError::UnexpectedElement(element_name(&element)));
9677 }
9678 Event::Eof => return Err(ChartError::MissingElement("c:chart".to_owned())),
9679 _ => {}
9680 }
9681 buffer.clear();
9682 }
9683 }
9684
9685 fn from_element(
9686 reader: &mut Reader<&[u8]>,
9687 start: &BytesStart<'_>,
9688 inherited: &NamespaceBindings,
9689 ) -> Result<Self> {
9690 let namespaces = bindings_with_local(inherited, start)?;
9691 let raw_attributes = capture_attributes(start)?;
9692 let mut state = ChartParseState::default();
9693 let mut buffer = Vec::new();
9694
9695 loop {
9696 match reader
9697 .read_event_into(&mut buffer)
9698 .map_err(OxmlError::from)?
9699 {
9700 Event::Start(element) => {
9701 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
9702 local_name(element.name().as_ref()).to_vec()
9703 } else {
9704 Vec::new()
9705 };
9706 let raw = capture_element(reader, &element)?;
9707 state.parse_child(&name, raw, &namespaces)?;
9708 }
9709 Event::Empty(element) => {
9710 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
9711 local_name(element.name().as_ref()).to_vec()
9712 } else {
9713 Vec::new()
9714 };
9715 let raw = capture_empty_element(&element)?;
9716 state.parse_child(&name, raw, &namespaces)?;
9717 }
9718 event @ (Event::Text(_)
9719 | Event::CData(_)
9720 | Event::Comment(_)
9721 | Event::PI(_)
9722 | Event::GeneralRef(_)) => {
9723 state.capture_event(capture_event(event)?);
9724 }
9725 Event::End(element) if matches_local_name(element.name().as_ref(), b"chart") => {
9726 break;
9727 }
9728 Event::Eof => return Err(missing_end("c:chart")),
9729 _ => {}
9730 }
9731 buffer.clear();
9732 }
9733
9734 state.finish(raw_attributes)
9735 }
9736
9737 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
9738 let mut start = BytesStart::new("c:chart");
9739 push_attributes(&mut start, &self.raw_attributes);
9740 writer
9741 .write_event(Event::Start(start))
9742 .map_err(OxmlError::from)?;
9743 emit_raw(writer, self.raw_children.at(0))?;
9744 if let Some(title) = &self.title {
9745 title.write_xml(writer)?;
9746 }
9747 emit_raw(writer, self.raw_children.at(1))?;
9748 if self.auto_title_deleted_markup.is_some() || self.auto_title_deleted {
9749 write_scalar(
9750 writer,
9751 "c:autoTitleDeleted",
9752 if self.auto_title_deleted { "1" } else { "0" },
9753 self.auto_title_deleted_markup.as_ref(),
9754 )?;
9755 }
9756 for raw_boundary in 2..8 {
9757 emit_raw(writer, self.raw_children.at(raw_boundary))?;
9758 }
9759 self.plot_area.write_xml(writer)?;
9760 emit_raw(writer, self.raw_children.at(8))?;
9761 if let Some(legend) = &self.legend {
9762 legend.write_xml(writer)?;
9763 }
9764 emit_raw(writer, self.raw_children.at(9))?;
9765 if self.plot_vis_only_markup.is_some() || !self.plot_vis_only {
9766 write_scalar(
9767 writer,
9768 "c:plotVisOnly",
9769 if self.plot_vis_only { "1" } else { "0" },
9770 self.plot_vis_only_markup.as_ref(),
9771 )?;
9772 }
9773 emit_raw(writer, self.raw_children.at(10))?;
9774 if self.disp_blanks_as_markup.is_some() || self.disp_blanks_as != DispBlanksAs::Gap {
9775 write_scalar(
9776 writer,
9777 "c:dispBlanksAs",
9778 self.disp_blanks_as.as_str(),
9779 self.disp_blanks_as_markup.as_ref(),
9780 )?;
9781 }
9782 for raw_boundary in 11..=13 {
9783 emit_raw(writer, self.raw_children.at(raw_boundary))?;
9784 }
9785 writer
9786 .write_event(Event::End(BytesEnd::new("c:chart")))
9787 .map_err(OxmlError::from)?;
9788 Ok(())
9789 }
9790
9791 pub fn raw_children(&self) -> &OrderedRawChildren {
9792 &self.raw_children
9793 }
9794}
9795
9796#[derive(Default)]
9797struct ChartParseState {
9798 title: Option<CT_Title>,
9799 auto_title_deleted: Option<(bool, ScalarMarkup)>,
9800 plot_area: Option<CT_PlotArea>,
9801 legend: Option<CT_Legend>,
9802 plot_vis_only: Option<(bool, ScalarMarkup)>,
9803 disp_blanks_as: Option<(DispBlanksAs, ScalarMarkup)>,
9804 raw_children: OrderedRawChildren,
9805 boundary: usize,
9806}
9807
9808impl ChartParseState {
9809 fn capture_event(&mut self, raw: Vec<u8>) {
9810 self.raw_children.push(self.boundary, raw);
9811 }
9812
9813 fn parse_child(
9814 &mut self,
9815 name: &[u8],
9816 raw: Vec<u8>,
9817 namespaces: &NamespaceBindings,
9818 ) -> Result<()> {
9819 match name {
9820 b"title" => {
9821 set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
9822 self.boundary = self.boundary.max(1);
9823 }
9824 b"autoTitleDeleted" => {
9825 set_once(
9826 &mut self.auto_title_deleted,
9827 parse_bool_value(&raw, "autoTitleDeleted")?,
9828 "c:autoTitleDeleted",
9829 )?;
9830 self.boundary = self.boundary.max(2);
9831 }
9832 b"plotArea" => {
9833 set_once(
9834 &mut self.plot_area,
9835 CT_PlotArea::from_xml_with_namespaces(&raw, namespaces)?,
9836 "c:plotArea",
9837 )?;
9838 self.boundary = self.boundary.max(8);
9839 }
9840 b"legend" => {
9841 set_once(&mut self.legend, CT_Legend::from_xml(&raw)?, "c:legend")?;
9842 self.boundary = self.boundary.max(9);
9843 }
9844 b"plotVisOnly" => {
9845 set_once(
9846 &mut self.plot_vis_only,
9847 parse_bool_value(&raw, "plotVisOnly")?,
9848 "c:plotVisOnly",
9849 )?;
9850 self.boundary = self.boundary.max(10);
9851 }
9852 b"dispBlanksAs" => {
9853 set_once(
9854 &mut self.disp_blanks_as,
9855 parse_enum_value(&raw, "dispBlanksAs")?,
9856 "c:dispBlanksAs",
9857 )?;
9858 self.boundary = self.boundary.max(11);
9859 }
9860 _ => {
9861 self.raw_children
9862 .push(chart_raw_slot(name).unwrap_or(self.boundary), raw);
9863 self.boundary = self.boundary.max(chart_raw_boundary_after(name));
9864 }
9865 }
9866 Ok(())
9867 }
9868
9869 fn finish(self, raw_attributes: XmlAttributes) -> Result<CT_Chart> {
9870 let (auto_title_deleted, auto_title_deleted_markup) = self
9871 .auto_title_deleted
9872 .map(|(value, markup)| (value, Some(markup)))
9873 .unwrap_or((false, None));
9874 let (plot_vis_only, plot_vis_only_markup) = self
9875 .plot_vis_only
9876 .map(|(value, markup)| (value, Some(markup)))
9877 .unwrap_or((true, None));
9878 let (disp_blanks_as, disp_blanks_as_markup) = self
9879 .disp_blanks_as
9880 .map(|(value, markup)| (value, Some(markup)))
9881 .unwrap_or((DispBlanksAs::Gap, None));
9882 Ok(CT_Chart {
9883 title: self.title,
9884 auto_title_deleted,
9885 plot_area: self
9886 .plot_area
9887 .ok_or_else(|| ChartError::MissingElement("c:plotArea".to_owned()))?,
9888 legend: self.legend,
9889 plot_vis_only,
9890 disp_blanks_as,
9891 auto_title_deleted_markup,
9892 plot_vis_only_markup,
9893 disp_blanks_as_markup,
9894 raw_attributes,
9895 raw_children: raw_children_in_schema_order(&self.raw_children, 13),
9896 })
9897 }
9898}
9899
9900fn chart_raw_slot(name: &[u8]) -> Option<usize> {
9901 match name {
9902 b"pivotFmts" => Some(2),
9903 b"view3D" => Some(3),
9904 b"floor" => Some(4),
9905 b"sideWall" => Some(5),
9906 b"backWall" => Some(6),
9907 b"showDLblsOverMax" | b"extLst" => Some(11),
9910 _ => None,
9911 }
9912}
9913
9914fn chart_raw_boundary_after(name: &[u8]) -> usize {
9915 match name {
9916 b"title" => 1,
9917 b"autoTitleDeleted" => 2,
9918 b"pivotFmts" => 3,
9919 b"view3D" => 4,
9920 b"floor" => 5,
9921 b"sideWall" => 6,
9922 b"backWall" => 7,
9923 b"plotArea" => 8,
9924 b"legend" => 9,
9925 b"plotVisOnly" => 10,
9926 b"dispBlanksAs" => 11,
9927 b"showDLblsOverMax" | b"extLst" => 11,
9928 _ => 0,
9929 }
9930}
9931
9932#[derive(Clone, Debug, PartialEq)]
9934pub struct CT_ChartSpace {
9935 pub chart: CT_Chart,
9936 pub sp_pr: Option<CT_ShapeProperties>,
9937 pub tx_pr: Option<CT_TextBody>,
9938 raw_attributes: Vec<(String, String)>,
9939 namespace_declarations: Vec<(String, String)>,
9940 raw_children: OrderedRawChildren,
9941}
9942
9943impl CT_ChartSpace {
9944 pub fn from_xml(xml: &[u8]) -> Result<Self> {
9946 let mut reader = Reader::from_reader(xml);
9947 let mut buffer = Vec::new();
9948 loop {
9949 match reader
9950 .read_event_into(&mut buffer)
9951 .map_err(OxmlError::from)?
9952 {
9953 Event::Start(element)
9954 if matches_local_name(element.name().as_ref(), b"chartSpace") =>
9955 {
9956 validate_chart_space_namespace(&element)?;
9957 return Self::from_element(&mut reader, &element);
9958 }
9959 Event::Empty(element)
9960 if matches_local_name(element.name().as_ref(), b"chartSpace") =>
9961 {
9962 validate_chart_space_namespace(&element)?;
9963 return Err(ChartError::MissingElement("c:chart".to_owned()));
9964 }
9965 Event::Start(element) | Event::Empty(element) => {
9966 return Err(ChartError::UnexpectedElement(element_name(&element)));
9967 }
9968 Event::Eof => return Err(ChartError::MissingElement("c:chartSpace".to_owned())),
9969 _ => {}
9970 }
9971 buffer.clear();
9972 }
9973 }
9974
9975 fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
9976 let namespaces = namespace_bindings(start)?;
9977 let (raw_attributes, namespace_declarations) = capture_root_attributes(start)?;
9978 let mut state = ChartSpaceParseState::default();
9979 let mut buffer = Vec::new();
9980
9981 loop {
9982 match reader
9983 .read_event_into(&mut buffer)
9984 .map_err(OxmlError::from)?
9985 {
9986 Event::Start(element) => {
9987 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
9988 local_name(element.name().as_ref()).to_vec()
9989 } else {
9990 Vec::new()
9991 };
9992 let raw = capture_element(reader, &element)?;
9993 state.parse_child(&name, raw, &namespaces)?;
9994 }
9995 Event::Empty(element) => {
9996 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
9997 local_name(element.name().as_ref()).to_vec()
9998 } else {
9999 Vec::new()
10000 };
10001 let raw = capture_empty_element(&element)?;
10002 state.parse_child(&name, raw, &namespaces)?;
10003 }
10004 event @ (Event::Text(_)
10005 | Event::CData(_)
10006 | Event::Comment(_)
10007 | Event::PI(_)
10008 | Event::GeneralRef(_)) => {
10009 state.capture_event(capture_event(event)?);
10010 }
10011 Event::End(element)
10012 if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10013 {
10014 break;
10015 }
10016 Event::Eof => return Err(missing_end("c:chartSpace")),
10017 _ => {}
10018 }
10019 buffer.clear();
10020 }
10021
10022 state.finish(raw_attributes, namespace_declarations)
10023 }
10024
10025 pub fn to_xml(&self) -> Result<Vec<u8>> {
10027 let mut writer = Writer::new(Vec::new());
10028 let mut start = BytesStart::new("c:chartSpace");
10029 start.push_attribute(("xmlns:c", C_NS));
10030 start.push_attribute(("xmlns:a", A_NS));
10031 start.push_attribute(("xmlns:r", R_NS));
10032 push_attributes(&mut start, &self.namespace_declarations);
10033 push_attributes(&mut start, &self.raw_attributes);
10034 writer
10035 .write_event(Event::Start(start))
10036 .map_err(OxmlError::from)?;
10037 for raw_boundary in 0..8 {
10038 emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
10039 }
10040 self.chart.write_xml(&mut writer)?;
10041 emit_raw(&mut writer, self.raw_children.at(8))?;
10042 if let Some(properties) = &self.sp_pr {
10043 properties.write_xml_as(&mut writer, "c:spPr")?;
10044 }
10045 emit_raw(&mut writer, self.raw_children.at(9))?;
10046 if let Some(text) = &self.tx_pr {
10047 text.write_xml_as(&mut writer, "c:txPr")?;
10048 }
10049 for raw_boundary in 10..=14 {
10050 emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
10051 }
10052 writer
10053 .write_event(Event::End(BytesEnd::new("c:chartSpace")))
10054 .map_err(OxmlError::from)?;
10055 Ok(writer.into_inner())
10056 }
10057
10058 pub fn raw_children(&self) -> &OrderedRawChildren {
10059 &self.raw_children
10060 }
10061}
10062
10063#[derive(Default)]
10064struct ChartSpaceParseState {
10065 chart: Option<CT_Chart>,
10066 sp_pr: Option<CT_ShapeProperties>,
10067 tx_pr: Option<CT_TextBody>,
10068 raw_children: OrderedRawChildren,
10069 boundary: usize,
10070}
10071
10072impl ChartSpaceParseState {
10073 fn capture_event(&mut self, raw: Vec<u8>) {
10074 self.raw_children.push(self.boundary, raw);
10075 }
10076
10077 fn parse_child(
10078 &mut self,
10079 name: &[u8],
10080 raw: Vec<u8>,
10081 namespaces: &NamespaceBindings,
10082 ) -> Result<()> {
10083 match name {
10084 b"chart" => {
10085 set_once(
10086 &mut self.chart,
10087 CT_Chart::from_xml_with_namespaces(&raw, namespaces)?,
10088 "c:chart",
10089 )?;
10090 self.boundary = self.boundary.max(8);
10091 }
10092 b"spPr" => {
10093 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
10094 let properties = CT_ShapeProperties::from_xml(&raw)?;
10095 let mut writer = Writer::new(Vec::new());
10096 properties.write_xml_as(&mut writer, "c:spPr")?;
10097 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
10098 set_once(&mut self.sp_pr, properties, "c:spPr")?;
10099 self.boundary = self.boundary.max(9);
10100 }
10101 b"txPr" => {
10102 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
10103 let text = CT_TextBody::from_xml_as(&raw, b"txPr")?;
10104 let mut writer = Writer::new(Vec::new());
10105 text.write_xml_as(&mut writer, "c:txPr")?;
10106 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
10107 set_once(&mut self.tx_pr, text, "c:txPr")?;
10108 self.boundary = self.boundary.max(10);
10109 }
10110 _ => {
10111 self.raw_children
10112 .push(chart_space_raw_slot(name).unwrap_or(self.boundary), raw);
10113 self.boundary = self.boundary.max(chart_space_raw_boundary_after(name));
10114 }
10115 }
10116 Ok(())
10117 }
10118
10119 fn finish(
10120 self,
10121 raw_attributes: XmlAttributes,
10122 namespace_declarations: XmlAttributes,
10123 ) -> Result<CT_ChartSpace> {
10124 Ok(CT_ChartSpace {
10125 chart: self
10126 .chart
10127 .ok_or_else(|| ChartError::MissingElement("c:chart".to_owned()))?,
10128 sp_pr: self.sp_pr,
10129 tx_pr: self.tx_pr,
10130 raw_attributes,
10131 namespace_declarations,
10132 raw_children: raw_children_in_schema_order(&self.raw_children, 14),
10133 })
10134 }
10135}
10136
10137fn raw_children_in_schema_order(
10138 raw_children: &OrderedRawChildren,
10139 final_boundary: usize,
10140) -> OrderedRawChildren {
10141 let mut ordered = OrderedRawChildren::default();
10142 for boundary in 0..=final_boundary {
10143 for raw in raw_children.at(boundary) {
10144 ordered.push(boundary, raw.to_vec());
10145 }
10146 }
10147 ordered
10148}
10149
10150fn chart_space_raw_slot(name: &[u8]) -> Option<usize> {
10151 match name {
10152 b"date1904" => Some(0),
10153 b"lang" => Some(1),
10154 b"roundedCorners" => Some(2),
10155 b"style" => Some(3),
10156 b"clrMapOvr" => Some(4),
10157 b"pivotSource" => Some(5),
10158 b"protection" => Some(6),
10159 b"externalData" => Some(10),
10160 b"printSettings" => Some(11),
10161 b"userShapes" => Some(12),
10162 b"extLst" => Some(13),
10163 _ => None,
10164 }
10165}
10166
10167fn chart_space_raw_boundary_after(name: &[u8]) -> usize {
10168 match name {
10169 b"date1904" => 1,
10170 b"lang" => 2,
10171 b"roundedCorners" => 3,
10172 b"style" => 4,
10173 b"clrMapOvr" => 5,
10174 b"pivotSource" => 6,
10175 b"protection" => 7,
10176 b"chart" => 8,
10177 b"spPr" => 9,
10178 b"txPr" => 10,
10179 b"externalData" => 11,
10180 b"printSettings" => 12,
10181 b"userShapes" => 13,
10182 b"extLst" => 14,
10183 _ => 0,
10184 }
10185}
10186
10187fn parse_bool_value(xml: &[u8], local: &str) -> Result<(bool, ScalarMarkup)> {
10188 let (value, markup) = scalar_value(xml, local)?;
10189 let value = match value.as_deref().unwrap_or("1") {
10190 "1" | "true" => true,
10191 "0" | "false" => false,
10192 invalid => return Err(invalid_attribute(local, "val", invalid.to_owned())),
10193 };
10194 Ok((value, markup))
10195}
10196
10197fn parse_enum_value(xml: &[u8], local: &str) -> Result<(DispBlanksAs, ScalarMarkup)> {
10198 let (value, markup) = scalar_value(xml, local)?;
10199 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
10200 let parsed =
10201 DispBlanksAs::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
10202 Ok((parsed, markup))
10203}
10204
10205fn scalar_value(xml: &[u8], expected: &str) -> Result<(Option<String>, ScalarMarkup)> {
10206 let mut reader = Reader::from_reader(xml);
10207 let mut buffer = Vec::new();
10208 loop {
10209 match reader
10210 .read_event_into(&mut buffer)
10211 .map_err(OxmlError::from)?
10212 {
10213 Event::Empty(element)
10214 if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
10215 {
10216 reject_conflicting_prefix(&element, b"c", C_NS)?;
10217 return Ok((
10218 attribute_value(&element, b"val")?,
10219 ScalarMarkup {
10220 raw_attributes: capture_attributes_except(&element, b"val")?,
10221 raw_content: Vec::new(),
10222 },
10223 ));
10224 }
10225 Event::Start(element)
10226 if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
10227 {
10228 reject_conflicting_prefix(&element, b"c", C_NS)?;
10229 let value = attribute_value(&element, b"val")?;
10230 let raw_attributes = capture_attributes_except(&element, b"val")?;
10231 let mut raw_content = Vec::new();
10232 let mut inner = Vec::new();
10233 loop {
10234 match reader
10235 .read_event_into(&mut inner)
10236 .map_err(OxmlError::from)?
10237 {
10238 Event::End(end)
10239 if matches_local_name(end.name().as_ref(), expected.as_bytes()) =>
10240 {
10241 return Ok((
10242 value,
10243 ScalarMarkup {
10244 raw_attributes,
10245 raw_content,
10246 },
10247 ));
10248 }
10249 event @ (Event::Text(_)
10250 | Event::CData(_)
10251 | Event::Comment(_)
10252 | Event::PI(_)
10253 | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
10254 Event::Eof => return Err(missing_end(expected)),
10255 _ => return Err(ChartError::UnexpectedElement(expected.to_owned())),
10256 }
10257 inner.clear();
10258 }
10259 }
10260 Event::Start(element) | Event::Empty(element) => {
10261 return Err(ChartError::UnexpectedElement(element_name(&element)));
10262 }
10263 Event::Eof => return Err(ChartError::MissingElement(expected.to_owned())),
10264 _ => {}
10265 }
10266 buffer.clear();
10267 }
10268}
10269
10270fn set_once<T>(slot: &mut Option<T>, value: T, name: &str) -> Result<()> {
10271 if slot.is_some() {
10272 return Err(ChartError::DuplicateElement(name.to_owned()));
10273 }
10274 *slot = Some(value);
10275 Ok(())
10276}
10277
10278fn mark_once(seen: &mut bool, name: &str) -> Result<()> {
10279 if *seen {
10280 return Err(ChartError::DuplicateElement(name.to_owned()));
10281 }
10282 *seen = true;
10283 Ok(())
10284}
10285
10286fn validate_chart_space_namespace(element: &BytesStart<'_>) -> Result<()> {
10287 let name = element.name();
10288 let qualified = name.as_ref();
10289 let declaration = match qualified.iter().position(|byte| *byte == b':') {
10290 Some(position) => {
10291 let mut declaration = b"xmlns:".to_vec();
10292 declaration.extend_from_slice(&qualified[..position]);
10293 declaration
10294 }
10295 None => b"xmlns".to_vec(),
10296 };
10297 let namespace = attribute_value(element, &declaration)?
10298 .ok_or_else(|| invalid_attribute("chartSpace", "namespace", "<missing>".to_owned()))?;
10299 if namespace != C_NS {
10300 return Err(invalid_attribute("chartSpace", "namespace", namespace));
10301 }
10302 reject_conflicting_prefix(element, b"c", C_NS)?;
10303 reject_conflicting_prefix(element, b"a", A_NS)?;
10304 reject_conflicting_prefix(element, b"r", R_NS)
10305}
10306
10307fn reject_conflicting_prefix_in_xml(xml: &[u8], prefix: &[u8], namespace: &str) -> Result<()> {
10308 let mut reader = Reader::from_reader(xml);
10309 let mut buffer = Vec::new();
10310 loop {
10311 match reader
10312 .read_event_into(&mut buffer)
10313 .map_err(OxmlError::from)?
10314 {
10315 Event::Start(element) | Event::Empty(element) => {
10316 return reject_conflicting_prefix(&element, prefix, namespace);
10317 }
10318 Event::Eof => return Err(ChartError::MissingElement("XML root".to_owned())),
10319 _ => {}
10320 }
10321 buffer.clear();
10322 }
10323}
10324
10325fn reject_rewritten_foreign_elements(
10326 original: &[u8],
10327 written: &[u8],
10328 inherited: &NamespaceBindings,
10329 root_local: &[u8],
10330) -> Result<()> {
10331 let original = foreign_element_regions(original, inherited, root_local)?;
10332 let mut written_namespaces = inherited.clone();
10333 written_namespaces.retain(|(prefix, _)| prefix != b"a");
10334 written_namespaces.push((b"a".to_vec(), A_NS.to_owned()));
10335 let written = foreign_element_regions(written, &written_namespaces, root_local)?;
10336 if original != written {
10337 return Err(invalid_attribute(
10338 &String::from_utf8_lossy(root_local),
10339 "namespace",
10340 "foreign DrawingML content would be converted to typed content".to_owned(),
10341 ));
10342 }
10343 Ok(())
10344}
10345
10346fn foreign_element_regions(
10347 xml: &[u8],
10348 inherited: &NamespaceBindings,
10349 root_local: &[u8],
10350) -> Result<Vec<Vec<u8>>> {
10351 let mut reader = Reader::from_reader(xml);
10352 let mut buffer = Vec::new();
10353 let root_namespaces = loop {
10354 match reader
10355 .read_event_into(&mut buffer)
10356 .map_err(OxmlError::from)?
10357 {
10358 Event::Start(element) if matches_local_name(element.name().as_ref(), root_local) => {
10359 break bindings_with_local(inherited, &element)?;
10360 }
10361 Event::Empty(element) if matches_local_name(element.name().as_ref(), root_local) => {
10362 return Ok(Vec::new());
10363 }
10364 Event::Start(element) | Event::Empty(element) => {
10365 return Err(ChartError::UnexpectedElement(element_name(&element)));
10366 }
10367 Event::Eof => {
10368 return Err(ChartError::MissingElement(
10369 String::from_utf8_lossy(root_local).into_owned(),
10370 ));
10371 }
10372 _ => {}
10373 }
10374 buffer.clear();
10375 };
10376
10377 let mut namespace_stack = vec![root_namespaces];
10378 let mut foreign = Vec::new();
10379 let mut child_buffer = Vec::new();
10380 loop {
10381 match reader
10382 .read_event_into(&mut child_buffer)
10383 .map_err(OxmlError::from)?
10384 {
10385 Event::Start(element) => {
10386 let namespaces = namespace_stack.last().expect("root namespace binding");
10387 if element_is_in_namespace(&element, A_NS, namespaces)? {
10388 namespace_stack.push(bindings_with_local(namespaces, &element)?);
10389 } else {
10390 foreign.push(capture_element(&mut reader, &element)?);
10391 }
10392 }
10393 Event::Empty(element) => {
10394 let namespaces = namespace_stack.last().expect("root namespace binding");
10395 if !element_is_in_namespace(&element, A_NS, namespaces)? {
10396 foreign.push(capture_empty_element(&element)?);
10397 }
10398 }
10399 Event::End(element) if matches_local_name(element.name().as_ref(), root_local) => {
10400 return Ok(foreign);
10401 }
10402 Event::End(_) => {
10403 namespace_stack.pop();
10404 }
10405 Event::Eof => {
10406 return Err(missing_end(&String::from_utf8_lossy(root_local)));
10407 }
10408 _ => {}
10409 }
10410 child_buffer.clear();
10411 }
10412}
10413
10414fn reject_conflicting_prefix(
10415 element: &BytesStart<'_>,
10416 prefix: &[u8],
10417 namespace: &str,
10418) -> Result<()> {
10419 let mut key = b"xmlns:".to_vec();
10420 key.extend_from_slice(prefix);
10421 if let Some(value) = attribute_value(element, &key)?
10422 && value != namespace
10423 {
10424 return Err(invalid_attribute(
10425 &element_name(element),
10426 &String::from_utf8_lossy(&key),
10427 value,
10428 ));
10429 }
10430 Ok(())
10431}
10432
10433fn namespace_bindings(element: &BytesStart<'_>) -> Result<NamespaceBindings> {
10434 let mut bindings = Vec::new();
10435 for attribute in element.attributes() {
10436 let attribute = attribute.map_err(OxmlError::from)?;
10437 let key = attribute.key.as_ref();
10438 let prefix = if key == b"xmlns" {
10439 Vec::new()
10440 } else if let Some(prefix) = key.strip_prefix(b"xmlns:") {
10441 prefix.to_vec()
10442 } else {
10443 continue;
10444 };
10445 let namespace = attribute
10446 .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
10447 .map_err(OxmlError::from)?
10448 .into_owned();
10449 bindings.push((prefix, namespace));
10450 }
10451 Ok(bindings)
10452}
10453
10454fn bindings_with_local(
10455 inherited: &NamespaceBindings,
10456 element: &BytesStart<'_>,
10457) -> Result<NamespaceBindings> {
10458 let mut bindings = inherited.clone();
10459 for (prefix, namespace) in namespace_bindings(element)? {
10460 upsert_namespace_binding(&mut bindings, prefix, namespace);
10461 }
10462 Ok(bindings)
10463}
10464
10465fn upsert_namespace_binding(bindings: &mut NamespaceBindings, prefix: Vec<u8>, namespace: String) {
10466 if let Some((_, current)) = bindings.iter_mut().find(|(current, _)| current == &prefix) {
10467 *current = namespace;
10468 } else {
10469 bindings.push((prefix, namespace));
10470 }
10471}
10472
10473fn element_is_in_namespace(
10474 element: &BytesStart<'_>,
10475 expected: &str,
10476 inherited: &NamespaceBindings,
10477) -> Result<bool> {
10478 let name = element.name();
10479 let qualified = name.as_ref();
10480 let prefix = qualified
10481 .iter()
10482 .position(|byte| *byte == b':')
10483 .map(|position| &qualified[..position])
10484 .unwrap_or_default();
10485 let local = namespace_bindings(element)?;
10486 let namespace = local
10487 .iter()
10488 .rev()
10489 .chain(inherited.iter().rev())
10490 .find(|(candidate, _)| candidate.as_slice() == prefix)
10491 .map(|(_, namespace)| namespace.as_str());
10492 Ok(namespace == Some(expected))
10493}
10494
10495fn attribute_value(element: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
10496 for attribute in element.attributes() {
10497 let attribute = attribute.map_err(OxmlError::from)?;
10498 if attribute.key.as_ref() == name {
10499 return Ok(Some(
10500 attribute
10501 .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
10502 .map_err(OxmlError::from)?
10503 .into_owned(),
10504 ));
10505 }
10506 }
10507 Ok(None)
10508}
10509
10510fn capture_root_attributes(start: &BytesStart<'_>) -> Result<RootAttributes> {
10511 let mut attributes = Vec::new();
10512 let mut namespaces = Vec::new();
10513 for attribute in start.attributes() {
10514 let attribute = attribute.map_err(OxmlError::from)?;
10515 let name = std::str::from_utf8(attribute.key.as_ref())
10516 .map_err(OxmlError::from)?
10517 .to_owned();
10518 let value = attribute
10519 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10520 .map_err(OxmlError::from)?
10521 .into_owned();
10522 if name == "xmlns:c" || name == "xmlns:a" || name == "xmlns:r" {
10523 continue;
10524 }
10525 if name == "xmlns" || name.starts_with("xmlns:") {
10526 namespaces.push((name, value));
10527 } else {
10528 attributes.push((name, value));
10529 }
10530 }
10531 Ok((attributes, namespaces))
10532}
10533
10534fn capture_attributes(start: &BytesStart<'_>) -> Result<XmlAttributes> {
10535 let mut attributes = Vec::new();
10536 for attribute in start.attributes() {
10537 let attribute = attribute.map_err(OxmlError::from)?;
10538 attributes.push((
10539 std::str::from_utf8(attribute.key.as_ref())
10540 .map_err(OxmlError::from)?
10541 .to_owned(),
10542 attribute
10543 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10544 .map_err(OxmlError::from)?
10545 .into_owned(),
10546 ));
10547 }
10548 Ok(attributes)
10549}
10550
10551fn capture_attributes_except(start: &BytesStart<'_>, excluded: &[u8]) -> Result<XmlAttributes> {
10552 let mut attributes = Vec::new();
10553 for attribute in start.attributes() {
10554 let attribute = attribute.map_err(OxmlError::from)?;
10555 if attribute.key.as_ref() == excluded {
10556 continue;
10557 }
10558 attributes.push((
10559 std::str::from_utf8(attribute.key.as_ref())
10560 .map_err(OxmlError::from)?
10561 .to_owned(),
10562 attribute
10563 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10564 .map_err(OxmlError::from)?
10565 .into_owned(),
10566 ));
10567 }
10568 Ok(attributes)
10569}
10570
10571fn capture_attributes_excluding(
10572 start: &BytesStart<'_>,
10573 excluded: &[&[u8]],
10574) -> Result<XmlAttributes> {
10575 let mut attributes = Vec::new();
10576 for attribute in start.attributes() {
10577 let attribute = attribute.map_err(OxmlError::from)?;
10578 if excluded.contains(&attribute.key.as_ref()) {
10579 continue;
10580 }
10581 attributes.push((
10582 std::str::from_utf8(attribute.key.as_ref())
10583 .map_err(OxmlError::from)?
10584 .to_owned(),
10585 attribute
10586 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10587 .map_err(OxmlError::from)?
10588 .into_owned(),
10589 ));
10590 }
10591 Ok(attributes)
10592}
10593
10594fn push_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
10595 for (name, value) in attributes {
10596 start.push_attribute((name.as_str(), value.as_str()));
10597 }
10598}
10599
10600fn write_scalar<W: Write>(
10601 writer: &mut Writer<W>,
10602 name: &str,
10603 value: &str,
10604 markup: Option<&ScalarMarkup>,
10605) -> Result<()> {
10606 let mut start = BytesStart::new(name);
10607 start.push_attribute(("val", value));
10608 if let Some(markup) = markup {
10609 push_attributes(&mut start, &markup.raw_attributes);
10610 }
10611 let has_content = markup.is_some_and(|markup| !markup.raw_content.is_empty());
10612 if !has_content {
10613 writer
10614 .write_event(Event::Empty(start))
10615 .map_err(OxmlError::from)?;
10616 return Ok(());
10617 }
10618 writer
10619 .write_event(Event::Start(start))
10620 .map_err(OxmlError::from)?;
10621 if let Some(markup) = markup {
10622 for raw in &markup.raw_content {
10623 writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
10624 }
10625 }
10626 writer
10627 .write_event(Event::End(BytesEnd::new(name)))
10628 .map_err(OxmlError::from)?;
10629 Ok(())
10630}
10631
10632fn capture_event(event: Event<'_>) -> Result<Vec<u8>> {
10633 let mut writer = Writer::new(Vec::new());
10634 writer
10635 .write_event(event.into_owned())
10636 .map_err(OxmlError::from)?;
10637 Ok(writer.into_inner())
10638}
10639
10640fn emit_raw<'a, W: Write>(
10641 writer: &mut Writer<W>,
10642 children: impl Iterator<Item = &'a [u8]>,
10643) -> Result<()> {
10644 for child in children {
10645 writer.get_mut().write_all(child).map_err(OxmlError::from)?;
10646 }
10647 Ok(())
10648}
10649
10650fn invalid_attribute(element: &str, attribute: &str, value: String) -> ChartError {
10651 ChartError::InvalidAttribute {
10652 element: element.to_owned(),
10653 attribute: attribute.to_owned(),
10654 value,
10655 }
10656}
10657
10658fn element_name(element: &BytesStart<'_>) -> String {
10659 String::from_utf8_lossy(element.name().as_ref()).into_owned()
10660}
10661
10662fn missing_end(element: &str) -> ChartError {
10663 ChartError::Xml(OxmlError::MissingElement(format!("closing {element}")))
10664}
10665
10666#[cfg(test)]
10667mod tests {
10668 use std::collections::HashSet;
10669 use std::fs;
10670 use std::path::{Path, PathBuf};
10671 use std::process::Command;
10672
10673 use oxml_core::raw_xml::{capture_element, capture_empty_element};
10674 use oxml_drawing::color::{ColorMap, ColorMapSlot, RgbColor, ThemeColorSlot};
10675 use oxml_drawing::theme::CT_OfficeStyleSheet;
10676 use oxml_layout::{
10677 Color, FontManager, LayoutResult, PageFrame, Paint, PathCommand, Point, PositionedElement,
10678 Rect,
10679 };
10680 use oxml_opc::OpcPackage;
10681 use quick_xml::Reader;
10682 use quick_xml::events::Event;
10683
10684 use super::{
10685 A_NS, Axis, AxisData, AxisId, AxisKind, AxisPosition, BarDirection, BarGrouping, C_NS,
10686 CT_ChartSpace, CT_DLbls, CT_ShapeProperties, CT_TextBody, ChartGeometry, DataLabelPosition,
10687 DispBlanksAs, Domain, Grouping, NumberFormat, NumericData, Orientation, Plot, R_NS,
10688 ScatterStyle, Series, StringRef, TickLabelPosition, TickMark, capture_event, local_name,
10689 matches_local_name, nice_number_scale, render_chart as render_chart_with_theme,
10690 render_geometry as render_geometry_with_theme,
10691 };
10692
10693 const MANIFEST: &str = include_str!("../../../scripts/pptx-corpus-manifest.tsv");
10694 const EXPECTED_DECKS: usize = 50;
10695 const LIBREOFFICE_VERSION: &str =
10696 "LibreOffice 26.2.5.2 cd7284b4cbbfeb507e630c1aac019f4157393acb";
10697 const PDFTOTEXT_VERSION: &str = "pdftotext version 26.01.0";
10698 const PDFTOPPM_VERSION: &str = "pdftoppm version 26.01.0";
10699 const PLOT_RENDER_NORMALIZED_MAE_THRESHOLD: f64 = 0.0;
10700
10701 fn render_geometry(chart: &super::CT_Chart, bounds: Rect) -> super::Result<ChartGeometry> {
10702 render_geometry_with_theme(
10703 chart,
10704 bounds,
10705 &CT_OfficeStyleSheet::office_default(),
10706 &ColorMap::default(),
10707 )
10708 }
10709
10710 fn render_chart(
10711 chart: &super::CT_Chart,
10712 bounds: Rect,
10713 fonts: &mut FontManager,
10714 ) -> super::Result<oxml_layout::GroupElement> {
10715 render_chart_with_theme(
10716 chart,
10717 bounds,
10718 &CT_OfficeStyleSheet::office_default(),
10719 &ColorMap::default(),
10720 fonts,
10721 )
10722 }
10723
10724 #[test]
10725 fn bar_chart_rasterises_at_computed_positions() {
10726 let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
10727 let geometry = render_geometry(
10728 &chart.chart,
10729 Rect {
10730 x: 0.0,
10731 y: 0.0,
10732 width: 200.0,
10733 height: 140.0,
10734 },
10735 )
10736 .unwrap();
10737 assert_eq!(
10738 geometry.plot_bounds,
10739 Rect {
10740 x: 36.0,
10741 y: 12.0,
10742 width: 152.0,
10743 height: 100.0,
10744 }
10745 );
10746 let bounds = path_bounds(&geometry);
10747 assert_eq!(bounds.len(), 2);
10748 assert_rect_near(
10749 bounds[0],
10750 Rect {
10751 x: 58.8,
10752 y: 62.0,
10753 width: 30.4,
10754 height: 50.0,
10755 },
10756 );
10757 assert_rect_near(
10758 bounds[1],
10759 Rect {
10760 x: 134.8,
10761 y: 12.0,
10762 width: 30.4,
10763 height: 100.0,
10764 },
10765 );
10766
10767 let layout = LayoutResult::new(
10768 vec![PageFrame::new(1, 200.0, 140.0, geometry.elements)],
10769 Vec::new(),
10770 None,
10771 Vec::new(),
10772 );
10773 let png = oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap();
10774 let pixmap = tiny_skia::Pixmap::decode_png(&png).unwrap();
10775 for (x, y) in [(74, 87), (150, 37)] {
10776 let pixel = pixmap.pixel(x, y).unwrap();
10777 assert_ne!((pixel.red(), pixel.green(), pixel.blue()), (255, 255, 255));
10778 }
10779 }
10780
10781 #[test]
10782 fn unstyled_four_series_use_accent_one_through_four() {
10783 let chart = four_series_bar_chart();
10784 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
10785 let colours = path_colours(&geometry);
10786
10787 assert_eq!(
10788 &colours[..4],
10789 &[
10790 Color::from_hex("156082"),
10791 Color::from_hex("E97132"),
10792 Color::from_hex("196B24"),
10793 Color::from_hex("0F9ED5"),
10794 ]
10795 );
10796 }
10797
10798 #[test]
10799 fn direct_series_solid_colour_overrides_theme_accent() {
10800 for (paint, expected) in [
10801 (
10802 "<a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill>",
10803 Color::from_hex("123456"),
10804 ),
10805 (
10806 "<a:ln><a:solidFill><a:srgbClr val=\"654321\"/></a:solidFill></a:ln>",
10807 Color::from_hex("654321"),
10808 ),
10809 (
10810 "<a:noFill/>",
10811 Color {
10812 r: 0.0,
10813 g: 0.0,
10814 b: 0.0,
10815 a: 0.0,
10816 },
10817 ),
10818 ] {
10819 let styled =
10820 plot_series(0).replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
10821 let plot = bar_plot("").replace(&plot_series(0), &styled);
10822 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
10823 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
10824
10825 assert_eq!(path_colours(&geometry)[0], expected);
10826 }
10827
10828 let styled = plot_series(0).replace(
10829 "<q:cat>",
10830 "<q:spPr><a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill></q:spPr><q:cat>",
10831 );
10832 let plot = bar_plot("").replace(&plot_series(0), &styled);
10833 let xml = chart_with_plot(&plot).replace(
10834 "</q:plotArea></q:chart>",
10835 "</q:plotArea><q:legend/></q:chart>",
10836 );
10837 let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
10838 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
10839 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
10840 assert!(group.children.iter().any(|element| matches!(
10841 element,
10842 PositionedElement::Path(path)
10843 if path.fill == Some(Paint::Solid(Color::from_hex("123456")))
10844 )));
10845
10846 let mut theme = CT_OfficeStyleSheet::office_default();
10847 let preset = oxml_drawing::fill::Fill::from_xml(
10848 br#"<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:prstClr val="red"/></a:solidFill>"#,
10849 )
10850 .unwrap();
10851 let oxml_drawing::fill::Fill::Solid(preset) = preset else {
10852 panic!("expected solid fill");
10853 };
10854 theme.theme_elements.color_scheme.hyperlink = preset.color.unwrap();
10855 let geometry =
10856 render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &ColorMap::default())
10857 .unwrap();
10858 assert_eq!(path_colours(&geometry)[0], Color::from_hex("123456"));
10859
10860 for (paint, expected_alpha) in [
10861 ("<a:noFill/>", 0.0),
10862 (
10863 "<a:solidFill><a:srgbClr val=\"123456\"><a:alpha val=\"50000\"/></a:srgbClr></a:solidFill>",
10864 (128.0 / 255.0) * 0.55,
10865 ),
10866 ] {
10867 let (_, area, _) = remaining_plot_fixtures()
10868 .into_iter()
10869 .find(|(kind, _, _)| *kind == "areaChart")
10870 .unwrap();
10871 let styled_area = area.replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
10872 let chart =
10873 CT_ChartSpace::from_xml(chart_with_optional_axes(&styled_area, true).as_bytes())
10874 .unwrap();
10875 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
10876 let actual_alpha = path_colours(&geometry)[0].a;
10877
10878 assert!((actual_alpha - expected_alpha).abs() < 1.0e-12);
10879 }
10880 }
10881
10882 #[test]
10883 fn series_accent_cycle_repeats_after_six() {
10884 let chart = seven_series_bar_chart();
10885 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
10886 let colours = path_colours(&geometry);
10887
10888 assert_eq!(colours[0], Color::from_hex("156082"));
10889 assert_eq!(colours[6], colours[0]);
10890 }
10891
10892 #[test]
10893 fn series_colours_honor_colour_map_and_transform_order() {
10894 let styled = plot_series(0).replace(
10895 "<q:cat>",
10896 "<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>",
10897 );
10898 let plot = bar_plot("").replace(&plot_series(0), &styled);
10899 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
10900 let color_map =
10901 ColorMap::default().with_overrides(&[(ColorMapSlot::Accent1, ThemeColorSlot::Accent4)]);
10902 let mut theme = CT_OfficeStyleSheet::office_default();
10903 let theme_fill = oxml_drawing::fill::Fill::from_xml(
10904 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>"#,
10905 )
10906 .unwrap();
10907 let oxml_drawing::fill::Fill::Solid(theme_fill) = theme_fill else {
10908 panic!("expected solid fill");
10909 };
10910 let theme_colour = theme_fill.color.unwrap();
10911 theme.theme_elements.color_scheme.accent4 = theme_colour.clone();
10912 let geometry =
10913 render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &color_map).unwrap();
10914 let series_fill = oxml_drawing::fill::Fill::from_xml(
10915 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>"#,
10916 )
10917 .unwrap();
10918 let oxml_drawing::fill::Fill::Solid(series_fill) = series_fill else {
10919 panic!("expected solid fill");
10920 };
10921 let mut transforms = theme_colour.transforms().to_vec();
10922 transforms.extend_from_slice(series_fill.color.as_ref().unwrap().transforms());
10923 let expected = oxml_drawing::color::apply_color_transforms(
10924 RgbColor::parse("336699").unwrap(),
10925 &transforms,
10926 );
10927
10928 assert_eq!(path_colours(&geometry)[0], resolved_layout_colour(expected));
10929 }
10930
10931 #[test]
10932 fn chart_colours_do_not_use_word_tint_shade() {
10933 let styled = plot_series(0).replace(
10934 "<q:cat>",
10935 "<q:spPr><a:solidFill><a:srgbClr val=\"808080\"><a:tint val=\"50000\"/></a:srgbClr></a:solidFill></q:spPr><q:cat>",
10936 );
10937 let plot = bar_plot("").replace(&plot_series(0), &styled);
10938 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
10939 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
10940
10941 assert_eq!(
10942 path_colours(&geometry)[0],
10943 Color {
10944 r: 205.0 / 255.0,
10945 g: 205.0 / 255.0,
10946 b: 205.0 / 255.0,
10947 a: 1.0,
10948 }
10949 );
10950 }
10951
10952 #[test]
10953 fn unsupported_direct_series_paint_is_contextual() {
10954 let styled = plot_series(0).replace(
10955 "<q:cat>",
10956 "<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>",
10957 );
10958 let plot = bar_plot("").replace(&plot_series(0), &styled);
10959 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
10960 let error = render_geometry(&chart.chart, chart_bounds()).unwrap_err();
10961
10962 assert!(error.to_string().contains("c:ser[0] direct fill paint"));
10963 assert!(error.to_string().contains("gradient"));
10964 }
10965
10966 #[test]
10967 fn resolved_chart_palette_raster_is_deterministic() {
10968 let raster = || {
10969 let chart = four_series_bar_chart();
10970 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
10971 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
10972 let layout = LayoutResult::new(
10973 vec![PageFrame::new(
10974 1,
10975 200.0,
10976 140.0,
10977 vec![PositionedElement::Group(group)],
10978 )],
10979 fonts.all_font_data(),
10980 None,
10981 Vec::new(),
10982 );
10983 oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
10984 };
10985 let chart = four_series_bar_chart();
10986 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
10987
10988 assert_eq!(path_colours(&geometry)[0], Color::from_hex("156082"));
10989 assert_eq!(raster(), raster());
10990 }
10991
10992 #[test]
10993 fn bar_geometry_handles_direction_grouping_gap_and_overlap() {
10994 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
10995 let Plot::Bar {
10996 overlap, series, ..
10997 } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
10998 else {
10999 panic!("expected bar plot");
11000 };
11001 let mut second = series[0].clone();
11002 second.index = 1;
11003 second.order = 1;
11004 second.values.values = vec![2.0, 1.0];
11005 series.push(second);
11006
11007 *overlap = 25;
11008 let clustered = render_geometry(&chart.chart, chart_bounds()).unwrap();
11009 let clustered_bounds = path_bounds(&clustered);
11010 let clustered_width = 30.4 / 1.75;
11011 let clustered_advance = clustered_width * 0.75;
11012 assert_rects_near(
11013 &clustered_bounds,
11014 &[
11015 Rect {
11016 x: 58.8,
11017 y: 62.0,
11018 width: clustered_width,
11019 height: 50.0,
11020 },
11021 Rect {
11022 x: 134.8,
11023 y: 12.0,
11024 width: clustered_width,
11025 height: 100.0,
11026 },
11027 Rect {
11028 x: 58.8 + clustered_advance,
11029 y: 12.0,
11030 width: clustered_width,
11031 height: 100.0,
11032 },
11033 Rect {
11034 x: 134.8 + clustered_advance,
11035 y: 62.0,
11036 width: clustered_width,
11037 height: 50.0,
11038 },
11039 ],
11040 );
11041
11042 let Plot::Bar { direction, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
11043 unreachable!()
11044 };
11045 *direction = BarDirection::Bar;
11046 let horizontal = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11047 let horizontal_width = 20.0 / 1.75;
11048 let horizontal_advance = horizontal_width * 0.75;
11049 assert_rects_near(
11050 &horizontal,
11051 &[
11052 Rect {
11053 x: 36.0,
11054 y: 27.0,
11055 width: 76.0,
11056 height: horizontal_width,
11057 },
11058 Rect {
11059 x: 36.0,
11060 y: 77.0,
11061 width: 152.0,
11062 height: horizontal_width,
11063 },
11064 Rect {
11065 x: 36.0,
11066 y: 27.0 + horizontal_advance,
11067 width: 152.0,
11068 height: horizontal_width,
11069 },
11070 Rect {
11071 x: 36.0,
11072 y: 77.0 + horizontal_advance,
11073 width: 76.0,
11074 height: horizontal_width,
11075 },
11076 ],
11077 );
11078
11079 let Plot::Bar {
11080 direction,
11081 grouping,
11082 ..
11083 } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11084 else {
11085 unreachable!()
11086 };
11087 *direction = BarDirection::Column;
11088 *grouping = BarGrouping::Stacked;
11089 let stacked = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11090 assert_rects_near(
11091 &stacked,
11092 &[
11093 Rect {
11094 x: 58.8,
11095 y: 112.0 - 100.0 / 3.0,
11096 width: 30.4,
11097 height: 100.0 / 3.0,
11098 },
11099 Rect {
11100 x: 134.8,
11101 y: 112.0 - 200.0 / 3.0,
11102 width: 30.4,
11103 height: 200.0 / 3.0,
11104 },
11105 Rect {
11106 x: 58.8,
11107 y: 12.0,
11108 width: 30.4,
11109 height: 200.0 / 3.0,
11110 },
11111 Rect {
11112 x: 134.8,
11113 y: 12.0,
11114 width: 30.4,
11115 height: 100.0 / 3.0,
11116 },
11117 ],
11118 );
11119
11120 let Plot::Bar {
11121 grouping, series, ..
11122 } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11123 else {
11124 unreachable!()
11125 };
11126 *grouping = BarGrouping::PercentStacked;
11127 series[1].values.values = vec![2.0, 2.0];
11128 let percent = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11129 assert_rects_near(
11130 &percent,
11131 &[
11132 Rect {
11133 x: 58.8,
11134 y: 112.0 - 100.0 / 3.0,
11135 width: 30.4,
11136 height: 100.0 / 3.0,
11137 },
11138 Rect {
11139 x: 134.8,
11140 y: 62.0,
11141 width: 30.4,
11142 height: 50.0,
11143 },
11144 Rect {
11145 x: 58.8,
11146 y: 12.0,
11147 width: 30.4,
11148 height: 200.0 / 3.0,
11149 },
11150 Rect {
11151 x: 134.8,
11152 y: 12.0,
11153 width: 30.4,
11154 height: 50.0,
11155 },
11156 ],
11157 );
11158 }
11159
11160 #[test]
11161 fn line_scatter_and_radar_emit_paths_and_markers() {
11162 let line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
11163 let line_geometry = render_geometry(&line.chart, chart_bounds()).unwrap();
11164 assert_path_points(
11165 path_commands(&line_geometry)[0],
11166 &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
11167 false,
11168 );
11169 assert_rects_near(
11170 &path_bounds(&line_geometry)[1..],
11171 &[
11172 Rect {
11173 x: 71.5,
11174 y: 109.5,
11175 width: 5.0,
11176 height: 5.0,
11177 },
11178 Rect {
11179 x: 147.5,
11180 y: 9.5,
11181 width: 5.0,
11182 height: 5.0,
11183 },
11184 ],
11185 );
11186
11187 let (scatter_plot, _) = remaining_plot_fixtures()
11188 .into_iter()
11189 .find(|(kind, _, _)| *kind == "scatterChart")
11190 .map(|(_, plot, axes)| (plot, axes))
11191 .unwrap();
11192 let mut scatter =
11193 CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
11194 .unwrap();
11195 let Plot::Scatter { style, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
11196 else {
11197 unreachable!()
11198 };
11199 *style = ScatterStyle::LineMarker;
11200 let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
11201 assert_path_points(
11202 path_commands(&scatter_geometry)[0],
11203 &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
11204 false,
11205 );
11206 assert_rects_near(
11207 &path_bounds(&scatter_geometry)[1..],
11208 &[
11209 Rect {
11210 x: 33.5,
11211 y: 109.5,
11212 width: 5.0,
11213 height: 5.0,
11214 },
11215 Rect {
11216 x: 185.5,
11217 y: 9.5,
11218 width: 5.0,
11219 height: 5.0,
11220 },
11221 ],
11222 );
11223
11224 let (radar_plot, _) = remaining_plot_fixtures()
11225 .into_iter()
11226 .find(|(kind, _, _)| *kind == "radarChart")
11227 .map(|(_, plot, axes)| (plot, axes))
11228 .unwrap();
11229 let radar = CT_ChartSpace::from_xml(chart_with_optional_axes(&radar_plot, true).as_bytes())
11230 .unwrap();
11231 let radar_geometry = render_geometry(&radar.chart, chart_bounds()).unwrap();
11232 assert_path_points(
11233 path_commands(&radar_geometry)[0],
11234 &[Point { x: 112.0, y: 37.0 }, Point { x: 112.0, y: 112.0 }],
11235 true,
11236 );
11237 assert_rects_near(
11238 &path_bounds(&radar_geometry)[1..],
11239 &[
11240 Rect {
11241 x: 109.5,
11242 y: 34.5,
11243 width: 5.0,
11244 height: 5.0,
11245 },
11246 Rect {
11247 x: 109.5,
11248 y: 109.5,
11249 width: 5.0,
11250 height: 5.0,
11251 },
11252 ],
11253 );
11254 }
11255
11256 #[test]
11257 fn pie_doughnut_and_area_emit_closed_paths() {
11258 let fixtures = remaining_plot_fixtures();
11259 let pie_plot = fixtures
11260 .iter()
11261 .find(|(kind, _, _)| *kind == "pieChart")
11262 .unwrap();
11263 let pie =
11264 CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot.1, pie_plot.2).as_bytes())
11265 .unwrap();
11266 let pie_geometry = render_geometry(&pie.chart, chart_bounds()).unwrap();
11267 let pie_commands = path_commands(&pie_geometry);
11268 assert_eq!(pie_commands.len(), 2);
11269 let [
11270 PathCommand::MoveTo(center),
11271 PathCommand::LineTo(start),
11272 ..,
11273 PathCommand::CurveTo { to: end, .. },
11274 PathCommand::Close,
11275 ] = pie_commands[0]
11276 else {
11277 panic!("expected closed cubic pie wedge");
11278 };
11279 assert_point_near(*center, Point { x: 112.0, y: 62.0 });
11280 assert_point_near(
11281 *start,
11282 Point {
11283 x: 137.0,
11284 y: 62.0 - 25.0 * 3.0_f64.sqrt(),
11285 },
11286 );
11287 assert_point_near(
11288 *end,
11289 Point {
11290 x: 137.0,
11291 y: 62.0 + 25.0 * 3.0_f64.sqrt(),
11292 },
11293 );
11294
11295 let doughnut_plot = fixtures
11296 .iter()
11297 .find(|(kind, _, _)| *kind == "doughnutChart")
11298 .unwrap();
11299 let doughnut = CT_ChartSpace::from_xml(
11300 chart_with_optional_axes(&doughnut_plot.1, doughnut_plot.2).as_bytes(),
11301 )
11302 .unwrap();
11303 let doughnut_geometry = render_geometry(&doughnut.chart, chart_bounds()).unwrap();
11304 let doughnut_commands = path_commands(&doughnut_geometry);
11305 assert_eq!(doughnut_commands.len(), 2);
11306 let commands = doughnut_commands[0];
11307 assert_eq!(commands.len(), 7);
11308 let PathCommand::MoveTo(outer_start) = commands[0] else {
11309 panic!("expected doughnut outer start");
11310 };
11311 let PathCommand::LineTo(inner_end) = commands[3] else {
11312 panic!("expected doughnut inner-radius join");
11313 };
11314 let PathCommand::CurveTo {
11315 to: inner_start, ..
11316 } = commands[5]
11317 else {
11318 panic!("expected reverse inner arc");
11319 };
11320 let diagonal = 25.0 * 2.0_f64.sqrt();
11321 assert_point_near(
11322 outer_start,
11323 Point {
11324 x: 112.0 + diagonal,
11325 y: 62.0 - diagonal,
11326 },
11327 );
11328 assert_point_near(
11329 inner_end,
11330 Point {
11331 x: 112.0 + 30.0 * 75.0_f64.to_radians().cos(),
11332 y: 62.0 + 30.0 * 75.0_f64.to_radians().sin(),
11333 },
11334 );
11335 let inner_diagonal = 15.0 * 2.0_f64.sqrt();
11336 assert_point_near(
11337 inner_start,
11338 Point {
11339 x: 112.0 + inner_diagonal,
11340 y: 62.0 - inner_diagonal,
11341 },
11342 );
11343 assert_eq!(commands.last(), Some(&PathCommand::Close));
11344
11345 let area_plot = fixtures
11346 .iter()
11347 .find(|(kind, _, _)| *kind == "areaChart")
11348 .unwrap();
11349 let mut area =
11350 CT_ChartSpace::from_xml(chart_with_optional_axes(&area_plot.1, area_plot.2).as_bytes())
11351 .unwrap();
11352 let area_geometry = render_geometry(&area.chart, chart_bounds()).unwrap();
11353 assert_path_points(
11354 path_commands(&area_geometry)[0],
11355 &[
11356 Point { x: 74.0, y: 62.0 },
11357 Point { x: 150.0, y: 12.0 },
11358 Point { x: 150.0, y: 112.0 },
11359 Point { x: 74.0, y: 112.0 },
11360 ],
11361 true,
11362 );
11363
11364 let Plot::Area {
11365 grouping, series, ..
11366 } = &mut area.chart.plot_area.plots_mut().unwrap()[0]
11367 else {
11368 unreachable!()
11369 };
11370 *grouping = Grouping::Stacked;
11371 let mut second = series[0].clone();
11372 second.index = 1;
11373 second.order = 1;
11374 second.values.values = vec![2.0, 1.0];
11375 series.push(second);
11376 let stacked_area = render_geometry(&area.chart, chart_bounds()).unwrap();
11377 assert_path_points(
11378 path_commands(&stacked_area)[1],
11379 &[
11380 Point { x: 74.0, y: 12.0 },
11381 Point { x: 150.0, y: 12.0 },
11382 Point {
11383 x: 150.0,
11384 y: 112.0 - 200.0 / 3.0,
11385 },
11386 Point {
11387 x: 74.0,
11388 y: 112.0 - 100.0 / 3.0,
11389 },
11390 ],
11391 true,
11392 );
11393 }
11394
11395 #[test]
11396 fn sparse_cache_indexes_preserve_slots_and_scatter_pairing() {
11397 let series = sparse_category_series();
11398 let bar = format!(
11399 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>"#
11400 );
11401 let bar = CT_ChartSpace::from_xml(chart_with_plot(&bar).as_bytes()).unwrap();
11402 assert_rects_near(
11403 &path_bounds(&render_geometry(&bar.chart, chart_bounds()).unwrap()),
11404 &[
11405 Rect {
11406 x: 51.2,
11407 y: 62.0,
11408 width: 304.0 / 15.0,
11409 height: 50.0,
11410 },
11411 Rect {
11412 x: 36.0 + 304.0 / 3.0 + 15.2,
11413 y: 12.0,
11414 width: 304.0 / 15.0,
11415 height: 100.0,
11416 },
11417 ],
11418 );
11419
11420 let line = format!(
11421 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>"#
11422 );
11423 let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line).as_bytes()).unwrap();
11424 let first = Point {
11425 x: 36.0 + 76.0 / 3.0,
11426 y: 62.0,
11427 };
11428 let missing = Point { x: 112.0, y: 112.0 };
11429 let last = Point {
11430 x: 36.0 + 380.0 / 3.0,
11431 y: 12.0,
11432 };
11433 let gap = render_geometry(&line.chart, chart_bounds()).unwrap();
11434 let gap_commands = path_commands(&gap);
11435 assert_eq!(gap_commands.len(), 2);
11436 assert_path_points(
11437 gap_commands[0],
11438 &[Point {
11439 x: first.x,
11440 y: 112.0,
11441 }],
11442 false,
11443 );
11444 assert_path_points(gap_commands[1], &[last], false);
11445 line.chart.disp_blanks_as = DispBlanksAs::Zero;
11446 assert_path_points(
11447 path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
11448 &[first, missing, last],
11449 false,
11450 );
11451 line.chart.disp_blanks_as = DispBlanksAs::Span;
11452 assert_path_points(
11453 path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
11454 &[
11455 Point {
11456 x: first.x,
11457 y: 112.0,
11458 },
11459 last,
11460 ],
11461 false,
11462 );
11463
11464 let area = format!(
11465 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:areaChart>"#
11466 );
11467 let mut area = CT_ChartSpace::from_xml(chart_with_plot(&area).as_bytes()).unwrap();
11468 let area_gap = render_geometry(&area.chart, chart_bounds()).unwrap();
11469 let area_gap_commands = path_commands(&area_gap);
11470 assert_eq!(area_gap_commands.len(), 2);
11471 assert_path_points(
11472 area_gap_commands[0],
11473 &[
11474 first,
11475 Point {
11476 x: first.x,
11477 y: 112.0,
11478 },
11479 ],
11480 true,
11481 );
11482 assert_path_points(
11483 area_gap_commands[1],
11484 &[
11485 last,
11486 Point {
11487 x: last.x,
11488 y: 112.0,
11489 },
11490 ],
11491 true,
11492 );
11493 area.chart.disp_blanks_as = DispBlanksAs::Zero;
11494 assert_path_points(
11495 path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
11496 &[
11497 first,
11498 missing,
11499 last,
11500 Point {
11501 x: last.x,
11502 y: 112.0,
11503 },
11504 missing,
11505 Point {
11506 x: first.x,
11507 y: 112.0,
11508 },
11509 ],
11510 true,
11511 );
11512 area.chart.disp_blanks_as = DispBlanksAs::Span;
11513 assert_path_points(
11514 path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
11515 &[
11516 first,
11517 last,
11518 Point {
11519 x: last.x,
11520 y: 112.0,
11521 },
11522 Point {
11523 x: first.x,
11524 y: 112.0,
11525 },
11526 ],
11527 true,
11528 );
11529
11530 let radar = format!(
11531 r#"<q:radarChart><q:radarStyle val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#
11532 );
11533 let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
11534 assert_path_points(
11535 path_commands(&render_geometry(&radar.chart, chart_bounds()).unwrap())[0],
11536 &[
11537 Point { x: 112.0, y: 37.0 },
11538 Point {
11539 x: 112.0 - 25.0 * 3.0_f64.sqrt(),
11540 y: 87.0,
11541 },
11542 ],
11543 true,
11544 );
11545
11546 let scatter = format!(
11547 r#"<q:scatterChart><q:scatterStyle val="lineMarker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:scatterChart>"#,
11548 sparse_scatter_series()
11549 );
11550 let mut scatter = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
11551 let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
11552 assert_eq!(geometry_children(&scatter_geometry).len(), 4);
11553 let gap_commands = path_commands(&scatter_geometry);
11554 assert_path_points(gap_commands[0], &[Point { x: 36.0, y: 112.0 }], false);
11555 assert_path_points(gap_commands[1], &[Point { x: 188.0, y: 12.0 }], false);
11556
11557 scatter.chart.disp_blanks_as = DispBlanksAs::Zero;
11558 let zero = render_geometry(&scatter.chart, chart_bounds()).unwrap();
11559 assert_path_points(
11560 path_commands(&zero)[0],
11561 &[
11562 Point { x: 74.0, y: 87.0 },
11563 Point { x: 36.0, y: 62.0 },
11564 Point { x: 150.0, y: 112.0 },
11565 Point { x: 188.0, y: 12.0 },
11566 ],
11567 false,
11568 );
11569
11570 scatter.chart.disp_blanks_as = DispBlanksAs::Span;
11571 assert_path_points(
11572 path_commands(&render_geometry(&scatter.chart, chart_bounds()).unwrap())[0],
11573 &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
11574 false,
11575 );
11576 }
11577
11578 #[test]
11579 fn sparse_scatter_domains_and_labels_use_only_matched_points() {
11580 let scatter = format!(
11581 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>"#,
11582 sparse_scatter_series()
11583 );
11584 let chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
11585 let plot = &chart.chart.plot_area.plots().unwrap()[0];
11586 assert_eq!(
11587 super::scatter_domains(plot, DispBlanksAs::Gap).unwrap(),
11588 (
11589 Domain {
11590 min: 10.0,
11591 max: 40.0,
11592 },
11593 Domain {
11594 min: 100.0,
11595 max: 400.0,
11596 },
11597 )
11598 );
11599 let axes = chart.chart.plot_area.axes().unwrap();
11600 let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
11601 let anchors =
11602 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
11603 assert_eq!(
11604 anchors.keys().copied().collect::<Vec<_>>(),
11605 vec![(0, 0), (0, 3)]
11606 );
11607 assert_eq!(anchors[&(0, 0)].center, Point { x: 0.0, y: 140.0 });
11608 assert_eq!(anchors[&(0, 3)].center, Point { x: 200.0, y: 0.0 });
11609 }
11610
11611 #[test]
11612 fn sparse_bubble_size_labels_resolve_logical_indexes() {
11613 let scatter = format!(
11614 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>"#,
11615 sparse_scatter_series()
11616 );
11617 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
11618 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
11619 let Plot::Scatter { series, .. } = plot else {
11620 panic!("expected scatter plot");
11621 };
11622 series[0].bubble_size = Some(
11623 NumericData::from_xml(
11624 format!(
11625 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>"#
11626 )
11627 .as_bytes(),
11628 )
11629 .unwrap(),
11630 );
11631 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
11632 let mut rendered = Vec::new();
11633 super::render_data_labels(
11634 plot,
11635 chart_bounds(),
11636 DispBlanksAs::Gap,
11637 super::GeometryOptions::default(),
11638 &mut fonts,
11639 &mut rendered,
11640 )
11641 .unwrap();
11642 let text: Vec<_> = rendered
11643 .iter()
11644 .filter_map(|element| match element {
11645 PositionedElement::Text(run) => Some(run.text.as_str()),
11646 _ => None,
11647 })
11648 .collect();
11649 assert_eq!(text, vec!["5", "9"]);
11650 }
11651
11652 #[test]
11653 fn geometry_rejects_invalid_bounds_and_opaque_plots() {
11654 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11655 for bounds in [
11656 Rect {
11657 x: 0.0,
11658 y: 0.0,
11659 width: f64::NAN,
11660 height: 100.0,
11661 },
11662 Rect {
11663 x: 0.0,
11664 y: 0.0,
11665 width: 0.0,
11666 height: 100.0,
11667 },
11668 Rect {
11669 x: 0.0,
11670 y: 0.0,
11671 width: 40.0,
11672 height: 30.0,
11673 },
11674 ] {
11675 assert!(render_geometry(&chart.chart, bounds).is_err());
11676 }
11677
11678 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
11679 unreachable!()
11680 };
11681 series[0].values.values.clear();
11682 assert!(render_geometry(&chart.chart, chart_bounds()).is_err());
11683
11684 let opaque = CT_ChartSpace::from_xml(
11685 chart_with_plot(r#"<q:bar3DChart><q:barDir val="col"/></q:bar3DChart>"#).as_bytes(),
11686 )
11687 .unwrap();
11688 let error = render_geometry(&opaque.chart, chart_bounds()).unwrap_err();
11689 assert!(error.to_string().contains("unsupported or combination"));
11690
11691 let (scatter_plot, _) = remaining_plot_fixtures()
11692 .into_iter()
11693 .find(|(kind, _, _)| *kind == "scatterChart")
11694 .map(|(_, plot, axes)| (plot, axes))
11695 .unwrap();
11696 let mut scatter =
11697 CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
11698 .unwrap();
11699 for invalid in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
11700 let Plot::Scatter { series, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
11701 else {
11702 unreachable!()
11703 };
11704 let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
11705 unreachable!()
11706 };
11707 x_values.values[0] = invalid;
11708 let error = render_geometry(&scatter.chart, chart_bounds()).unwrap_err();
11709 assert!(error.to_string().contains("xVal"));
11710 }
11711 }
11712
11713 #[test]
11714 fn finite_extremes_never_produce_nonfinite_geometry() {
11715 let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
11716 let Plot::Line { series, .. } = &mut line.chart.plot_area.plots_mut().unwrap()[0] else {
11717 unreachable!()
11718 };
11719 series[0].values.values = vec![-f64::MAX, f64::MAX];
11720 let extreme_line = render_geometry(&line.chart, chart_bounds()).unwrap();
11721 assert_path_points(
11722 path_commands(&extreme_line)[0],
11723 &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
11724 false,
11725 );
11726
11727 let mut stacked =
11728 CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11729 let Plot::Bar {
11730 grouping, series, ..
11731 } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
11732 else {
11733 unreachable!()
11734 };
11735 *grouping = BarGrouping::Stacked;
11736 series[0].values.values = vec![f64::MAX, 1.0];
11737 let mut second = series[0].clone();
11738 second.index = 1;
11739 second.order = 1;
11740 series.push(second);
11741 let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
11742 assert!(error.to_string().contains("stacked value total"));
11743
11744 let Plot::Bar { grouping, .. } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
11745 else {
11746 unreachable!()
11747 };
11748 *grouping = BarGrouping::PercentStacked;
11749 let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
11750 assert!(error.to_string().contains("stacked percentage total"));
11751
11752 let (pie_plot, _) = remaining_plot_fixtures()
11753 .into_iter()
11754 .find(|(kind, _, _)| *kind == "pieChart")
11755 .map(|(_, plot, axes)| (plot, axes))
11756 .unwrap();
11757 let mut pie =
11758 CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot, false).as_bytes()).unwrap();
11759 let Plot::Pie { series, .. } = &mut pie.chart.plot_area.plots_mut().unwrap()[0] else {
11760 unreachable!()
11761 };
11762 series[0].values.values = vec![f64::MAX, f64::MAX];
11763 let error = render_geometry(&pie.chart, chart_bounds()).unwrap_err();
11764 assert!(error.to_string().contains("pie value total"));
11765
11766 let (scatter_plot, _) = remaining_plot_fixtures()
11767 .into_iter()
11768 .find(|(kind, _, _)| *kind == "scatterChart")
11769 .map(|(_, plot, axes)| (plot, axes))
11770 .unwrap();
11771 let mut scatter =
11772 CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
11773 .unwrap();
11774 let Plot::Scatter { style, series, .. } =
11775 &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
11776 else {
11777 unreachable!()
11778 };
11779 *style = ScatterStyle::Line;
11780 let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
11781 unreachable!()
11782 };
11783 x_values.values = vec![-f64::MAX, f64::MAX];
11784 series[0].values.values = vec![-f64::MAX, f64::MAX];
11785 let extreme_scatter = render_geometry(&scatter.chart, chart_bounds()).unwrap();
11786 assert_path_points(
11787 path_commands(&extreme_scatter)[0],
11788 &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
11789 false,
11790 );
11791 }
11792
11793 #[test]
11794 fn geometry_is_backend_neutral_and_deterministic() {
11795 let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11796 let first = render_geometry(&chart.chart, chart_bounds()).unwrap();
11797 let second = render_geometry(&chart.chart, chart_bounds()).unwrap();
11798 assert_eq!(
11799 format!("{:?}", first.elements),
11800 format!("{:?}", second.elements)
11801 );
11802
11803 let raster = |geometry: ChartGeometry| {
11804 let layout = LayoutResult::new(
11805 vec![PageFrame::new(1, 200.0, 140.0, geometry.elements)],
11806 Vec::new(),
11807 None,
11808 Vec::new(),
11809 );
11810 oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
11811 };
11812 assert_eq!(raster(first), raster(second));
11813 }
11814
11815 #[test]
11816 fn zero_to_one_hundred_axis_uses_expected_ticks() {
11817 let scale = nice_number_scale(
11818 Domain {
11819 min: 0.0,
11820 max: 100.0,
11821 },
11822 None,
11823 None,
11824 6,
11825 )
11826 .unwrap();
11827 assert_eq!(scale.ticks, vec![0.0, 20.0, 40.0, 60.0, 80.0, 100.0]);
11828 }
11829
11830 #[test]
11831 fn nice_number_ticks_cover_unpinned_extents() {
11832 for domain in [
11833 Domain {
11834 min: 3.0,
11835 max: 97.0,
11836 },
11837 Domain {
11838 min: -97.0,
11839 max: -3.0,
11840 },
11841 Domain {
11842 min: -3.5,
11843 max: 8.25,
11844 },
11845 Domain {
11846 min: 0.125,
11847 max: 0.875,
11848 },
11849 Domain {
11850 min: 1.0e12,
11851 max: 7.0e12,
11852 },
11853 Domain {
11854 min: -f64::MAX,
11855 max: f64::MAX,
11856 },
11857 Domain {
11858 min: f64::from_bits(1),
11859 max: f64::from_bits(5),
11860 },
11861 ] {
11862 let scale = nice_number_scale(domain, None, None, 6).unwrap();
11863 assert!(scale.domain.min <= domain.min);
11864 assert!(scale.domain.max >= domain.max);
11865 assert!(scale.ticks.iter().all(|tick| tick.is_finite()));
11866 assert!(scale.ticks.windows(2).all(|pair| pair[0] < pair[1]));
11867 }
11868
11869 let constant = nice_number_scale(Domain { min: 4.0, max: 4.0 }, None, None, 6).unwrap();
11870 assert!(constant.domain.min < 4.0 && constant.domain.max > 4.0);
11871 }
11872
11873 #[test]
11874 fn one_sided_explicit_scale_beyond_data_derives_a_valid_opposite_bound() {
11875 let minimum = nice_number_scale(
11876 Domain {
11877 min: 0.0,
11878 max: 10.0,
11879 },
11880 Some(20.0),
11881 None,
11882 6,
11883 )
11884 .unwrap();
11885 assert_eq!(minimum.domain.min, 20.0);
11886 assert!(minimum.domain.max > 20.0);
11887 assert!(minimum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
11888
11889 let maximum = nice_number_scale(
11890 Domain {
11891 min: 0.0,
11892 max: 10.0,
11893 },
11894 None,
11895 Some(-20.0),
11896 6,
11897 )
11898 .unwrap();
11899 assert_eq!(maximum.domain.max, -20.0);
11900 assert!(maximum.domain.min < -20.0);
11901 assert!(maximum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
11902 }
11903
11904 #[test]
11905 fn standard_line_domain_does_not_force_zero() {
11906 let mut chart =
11907 CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
11908 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
11909 let Plot::Line { series, .. } = plot else {
11910 panic!("expected line plot");
11911 };
11912 series[0].values.values = vec![1_000.0, 1_010.0];
11913 assert_eq!(
11914 super::plot_value_domain(plot, DispBlanksAs::Gap).unwrap(),
11915 Some(Domain {
11916 min: 1_000.0,
11917 max: 1_010.0,
11918 })
11919 );
11920 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11921 assert_path_points(
11922 path_commands(&geometry)[0],
11923 &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
11924 false,
11925 );
11926 }
11927
11928 #[test]
11929 fn explicit_scaling_bounds_and_orientation_are_honored() {
11930 let scale = nice_number_scale(
11931 Domain {
11932 min: 25.0,
11933 max: 75.0,
11934 },
11935 Some(0.0),
11936 Some(100.0),
11937 6,
11938 )
11939 .unwrap();
11940 assert_eq!(
11941 scale.domain,
11942 Domain {
11943 min: 0.0,
11944 max: 100.0
11945 }
11946 );
11947 assert_eq!(
11948 super::map_y_oriented(0.0, scale.domain, chart_bounds(), Orientation::MaxMin).unwrap(),
11949 0.0
11950 );
11951 assert_eq!(
11952 super::map_y_oriented(100.0, scale.domain, chart_bounds(), Orientation::MaxMin)
11953 .unwrap(),
11954 140.0
11955 );
11956
11957 let mut chart = labelled_bar_chart();
11958 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
11959 panic!("expected bar plot");
11960 };
11961 series[0].values.values = vec![25.0, 75.0];
11962 let value_axis = chart
11963 .chart
11964 .plot_area
11965 .axes
11966 .iter_mut()
11967 .find(|axis| axis.kind == AxisKind::Value)
11968 .unwrap();
11969 value_axis.scaling.orientation = Orientation::MaxMin;
11970 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
11971 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
11972 let plot = group
11973 .children
11974 .iter()
11975 .find_map(|element| match element {
11976 PositionedElement::Group(group) => Some(group),
11977 _ => None,
11978 })
11979 .unwrap();
11980 let bars: Vec<_> = plot
11981 .children
11982 .iter()
11983 .filter_map(|element| match element {
11984 PositionedElement::Path(path) => path.path.bounds(),
11985 _ => None,
11986 })
11987 .collect();
11988 assert_eq!(bars.len(), 2);
11989 assert!((bars[0].y - 12.0).abs() < 1.0e-9);
11990 assert!((bars[0].height - 25.0).abs() < 1.0e-9);
11991 assert!((bars[1].height - 75.0).abs() < 1.0e-9);
11992 }
11993
11994 #[test]
11995 fn family_label_anchors_share_rendered_geometry() {
11996 let mut bar = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11997 let bar_plot = &mut bar.chart.plot_area.plots_mut().unwrap()[0];
11998 let Plot::Bar { series, .. } = bar_plot else {
11999 panic!("expected bar plot");
12000 };
12001 let mut second = series[0].clone();
12002 second.index = 1;
12003 second.order = 1;
12004 series.push(second);
12005 let anchors = super::plot_label_anchors(
12006 bar_plot,
12007 chart_bounds(),
12008 DispBlanksAs::Gap,
12009 super::GeometryOptions::default(),
12010 )
12011 .unwrap();
12012 assert_point_near(anchors[&(0, 0)].center, Point { x: 40.0, y: 105.0 });
12013 assert_point_near(anchors[&(1, 0)].center, Point { x: 60.0, y: 105.0 });
12014
12015 let pie = CT_ChartSpace::from_xml(
12016 chart_with_optional_axes(
12017 &remaining_plot_fixtures()
12018 .into_iter()
12019 .find(|(kind, _, _)| *kind == "pieChart")
12020 .unwrap()
12021 .1,
12022 false,
12023 )
12024 .as_bytes(),
12025 )
12026 .unwrap();
12027 let Plot::Pie {
12028 first_slice_angle,
12029 series,
12030 ..
12031 } = &pie.chart.plot_area.plots().unwrap()[0]
12032 else {
12033 panic!("expected pie plot");
12034 };
12035 let slices = super::pie_slices(*first_slice_angle, None, series, chart_bounds()).unwrap();
12036 assert_eq!(slices.len(), 2);
12037 let first_span = std::f64::consts::TAU / 3.0;
12038 let first_midpoint = f64::from(*first_slice_angle).to_radians()
12039 - std::f64::consts::FRAC_PI_2
12040 + first_span / 2.0;
12041 assert_point_near(
12042 slices[0].anchor,
12043 super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, first_midpoint),
12044 );
12045 let second_midpoint = first_midpoint + std::f64::consts::PI;
12046 assert_point_near(
12047 slices[1].anchor,
12048 super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, second_midpoint),
12049 );
12050
12051 let radar = format!(
12052 r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12053 plot_series(0)
12054 );
12055 let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
12056 let Plot::Radar { series, .. } = &radar.chart.plot_area.plots().unwrap()[0] else {
12057 panic!("expected radar plot");
12058 };
12059 let radar_points =
12060 super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
12061 .unwrap();
12062 assert_eq!(radar_points[0][0], (0, Point { x: 100.0, y: 35.0 }));
12063 assert_eq!(radar_points[0][1], (1, Point { x: 100.0, y: 140.0 }));
12064 }
12065
12066 #[test]
12067 fn radar_annotations_use_spokes_perimeter_labels_and_radial_gridlines() {
12068 let radar_plot = format!(
12069 r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12070 sparse_category_series()
12071 );
12072 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12073 for axis in &mut chart.chart.plot_area.axes {
12074 axis.major_tick_mark = TickMark::Cross;
12075 if axis.kind == AxisKind::Value {
12076 axis.major_gridlines = Some(super::ChartLines::default());
12077 }
12078 }
12079 let plot = &chart.chart.plot_area.plots().unwrap()[0];
12080 let axes = chart.chart.plot_area.axes().unwrap();
12081 let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
12082 let center = Point { x: 112.0, y: 62.0 };
12083 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12084 let annotations =
12085 super::render_axes_and_labels(plot, &axes, bounds, DispBlanksAs::Gap, &mut fonts)
12086 .unwrap();
12087 assert_eq!(annotations.axis_lines.len(), 4);
12088 for spoke in annotations.axis_lines.iter().take(3) {
12089 let PositionedElement::Path(spoke) = spoke else {
12090 panic!("radar spoke must be a path");
12091 };
12092 assert_eq!(
12093 spoke.path.commands.first(),
12094 Some(&PathCommand::MoveTo(center))
12095 );
12096 }
12097 assert!(!annotations.gridlines.is_empty());
12098 for gridline in &annotations.gridlines {
12099 let PositionedElement::Path(gridline) = gridline else {
12100 panic!("radar gridline must be a path");
12101 };
12102 assert_eq!(gridline.path.commands.last(), Some(&PathCommand::Close));
12103 let radii: Vec<_> = gridline
12104 .path
12105 .commands
12106 .iter()
12107 .filter_map(|command| match command {
12108 PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
12109 Some((point.x - center.x).hypot(point.y - center.y))
12110 }
12111 _ => None,
12112 })
12113 .collect();
12114 assert_eq!(radii.len(), 3);
12115 assert!(
12116 radii
12117 .windows(2)
12118 .all(|pair| (pair[0] - pair[1]).abs() < 1.0e-9)
12119 );
12120 }
12121 let text: Vec<_> = annotations
12122 .labels
12123 .iter()
12124 .filter_map(|element| match element {
12125 PositionedElement::Text(run) => Some(run.text.as_str()),
12126 _ => None,
12127 })
12128 .collect();
12129 assert!(text.contains(&"North"));
12130 assert!(text.contains(&"West"));
12131 }
12132
12133 #[test]
12134 fn radar_tick_label_positions_are_distinct() {
12135 let radar_plot = format!(
12136 r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12137 plot_series(0)
12138 );
12139 let chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12140 let plot = &chart.chart.plot_area.plots().unwrap()[0];
12141 let axes = chart.chart.plot_area.axes().unwrap();
12142 let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
12143 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12144
12145 let category_axis = axes
12146 .iter()
12147 .find(|axis| axis.kind == AxisKind::Category)
12148 .unwrap();
12149 for (position, expected) in [
12150 (TickLabelPosition::High, Point { x: 108.0, y: 0.0 }),
12151 (TickLabelPosition::Low, Point { x: 108.0, y: 25.0 }),
12152 (TickLabelPosition::NextTo, Point { x: 108.0, y: 5.0 }),
12153 ] {
12154 let mut axis = category_axis.clone();
12155 axis.tick_label_position = position;
12156 let annotations =
12157 super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
12158 .unwrap();
12159 let north = annotations
12160 .labels
12161 .iter()
12162 .find_map(|element| match element {
12163 PositionedElement::Text(run) if run.text == "North" => Some(run),
12164 _ => None,
12165 })
12166 .unwrap();
12167 assert_point_near(north.origin, expected);
12168 }
12169 let mut hidden_category = category_axis.clone();
12170 hidden_category.tick_label_position = TickLabelPosition::None;
12171 let annotations = super::render_axes_and_labels(
12172 plot,
12173 &[hidden_category],
12174 bounds,
12175 DispBlanksAs::Gap,
12176 &mut fonts,
12177 )
12178 .unwrap();
12179 assert!(annotations.labels.is_empty());
12180
12181 let narrow_bounds = Rect {
12182 x: 36.0,
12183 y: 12.0,
12184 width: 24.0,
12185 height: 100.0,
12186 };
12187 let narrow_center = Point { x: 48.0, y: 62.0 };
12188 assert_eq!(
12189 super::radar_category_label_origin(
12190 narrow_bounds,
12191 narrow_center,
12192 12.0,
12193 1,
12194 4,
12195 TickLabelPosition::High,
12196 ),
12197 Some(Point { x: 72.0, y: 65.0 })
12198 );
12199 assert_eq!(
12200 super::radar_category_label_origin(
12201 narrow_bounds,
12202 narrow_center,
12203 12.0,
12204 1,
12205 4,
12206 TickLabelPosition::NextTo,
12207 ),
12208 Some(Point { x: 66.0, y: 65.0 })
12209 );
12210
12211 let value_axis = axes
12212 .iter()
12213 .find(|axis| axis.kind == AxisKind::Value)
12214 .unwrap();
12215 for (position, expected) in [
12216 (TickLabelPosition::High, Point { x: 166.0, y: 65.0 }),
12217 (TickLabelPosition::Low, Point { x: 32.0, y: 65.0 }),
12218 (TickLabelPosition::NextTo, Point { x: 116.0, y: 65.0 }),
12219 ] {
12220 let mut axis = value_axis.clone();
12221 axis.tick_label_position = position;
12222 let annotations =
12223 super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
12224 .unwrap();
12225 let zero = annotations
12226 .labels
12227 .iter()
12228 .find_map(|element| match element {
12229 PositionedElement::Text(run) if run.text == "0" => Some(run),
12230 _ => None,
12231 })
12232 .unwrap();
12233 assert_point_near(zero.origin, expected);
12234 }
12235 let mut hidden_value = value_axis.clone();
12236 hidden_value.tick_label_position = TickLabelPosition::None;
12237 let annotations = super::render_axes_and_labels(
12238 plot,
12239 &[hidden_value],
12240 bounds,
12241 DispBlanksAs::Gap,
12242 &mut fonts,
12243 )
12244 .unwrap();
12245 assert!(annotations.labels.is_empty());
12246 }
12247
12248 #[test]
12249 fn radar_negative_and_mixed_sign_domains_preserve_values() {
12250 let radar_plot = format!(
12251 r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12252 plot_series(0)
12253 );
12254 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12255 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12256 let Plot::Radar { series, .. } = plot else {
12257 panic!("expected radar plot");
12258 };
12259 series[0].values.values = vec![-10.0, -5.0];
12260 let points =
12261 super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
12262 .unwrap();
12263 assert_eq!(points[0][0], (0, Point { x: 100.0, y: 70.0 }));
12264 assert_eq!(points[0][1], (1, Point { x: 100.0, y: 105.0 }));
12265
12266 series[0].values.values = vec![-5.0, 10.0];
12267 let options = super::GeometryOptions {
12268 value: Some(super::AxisMapping {
12269 domain: Domain {
12270 min: -10.0,
12271 max: 10.0,
12272 },
12273 orientation: Orientation::MinMax,
12274 }),
12275 ..super::GeometryOptions::default()
12276 };
12277 let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
12278 assert_eq!(points[0][0], (0, Point { x: 100.0, y: 52.5 }));
12279 assert_eq!(points[0][1], (1, Point { x: 100.0, y: 140.0 }));
12280 }
12281
12282 #[test]
12283 fn radar_explicit_bounds_suppress_out_of_range_points_and_labels() {
12284 let radar_plot = format!(
12285 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>"#,
12286 plot_series(0)
12287 );
12288 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12289 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12290 let Plot::Radar { series, .. } = plot else {
12291 panic!("expected radar plot");
12292 };
12293 series[0].values.values = vec![25.0, 75.0];
12294 let options = super::GeometryOptions {
12295 value: Some(super::AxisMapping {
12296 domain: Domain {
12297 min: 50.0,
12298 max: 100.0,
12299 },
12300 orientation: Orientation::MinMax,
12301 }),
12302 ..super::GeometryOptions::default()
12303 };
12304 let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
12305 assert_eq!(points[0], vec![(1, Point { x: 100.0, y: 105.0 })]);
12306 let anchors =
12307 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12308 assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 1)]);
12309
12310 let Plot::Radar { series, .. } = plot else {
12311 panic!("expected radar plot");
12312 };
12313 series[0].values.values = vec![75.0, 125.0];
12314 let reversed = super::GeometryOptions {
12315 value: Some(super::AxisMapping {
12316 domain: Domain {
12317 min: 50.0,
12318 max: 100.0,
12319 },
12320 orientation: Orientation::MaxMin,
12321 }),
12322 ..super::GeometryOptions::default()
12323 };
12324 let points = super::radar_series_points(series, chart_bounds(), reversed).unwrap();
12325 assert_eq!(points[0], vec![(0, Point { x: 100.0, y: 35.0 })]);
12326 let anchors =
12327 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
12328 assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
12329
12330 let value_axis = chart
12331 .chart
12332 .plot_area
12333 .axes
12334 .iter_mut()
12335 .find(|axis| axis.kind == AxisKind::Value)
12336 .unwrap();
12337 value_axis.scaling.minimum = Some(50.0);
12338 value_axis.scaling.maximum = Some(100.0);
12339 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12340 let Plot::Radar { series, .. } = plot else {
12341 panic!("expected radar plot");
12342 };
12343 series[0].values.values = vec![25.0, 25.0];
12344 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12345 let rendered = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12346 assert!(rendered.children.iter().any(|element| matches!(
12347 element,
12348 PositionedElement::Group(group) if group.clip.is_some() && group.children.is_empty()
12349 )));
12350 assert!(
12351 rendered
12352 .children
12353 .iter()
12354 .any(|element| matches!(element, PositionedElement::Path(_)))
12355 );
12356 assert!(!rendered.children.iter().any(|element| matches!(
12357 element,
12358 PositionedElement::Text(run) if run.text == "25"
12359 )));
12360 }
12361
12362 #[test]
12363 fn inside_and_outside_label_positions_follow_family_geometry() {
12364 let mut horizontal =
12365 CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12366 let plot = &mut horizontal.chart.plot_area.plots_mut().unwrap()[0];
12367 let Plot::Bar { direction, .. } = plot else {
12368 panic!("expected bar plot");
12369 };
12370 *direction = BarDirection::Bar;
12371 let anchors = super::plot_label_anchors(
12372 plot,
12373 chart_bounds(),
12374 DispBlanksAs::Gap,
12375 super::GeometryOptions::default(),
12376 )
12377 .unwrap();
12378 let anchor = anchors[&(0, 0)];
12379 assert_point_near(
12380 anchor.origin(Some(DataLabelPosition::InsideBase)),
12381 Point { x: 5.0, y: 35.0 },
12382 );
12383 assert_point_near(
12384 anchor.origin(Some(DataLabelPosition::InsideEnd)),
12385 Point { x: 95.0, y: 35.0 },
12386 );
12387 assert_point_near(
12388 anchor.origin(Some(DataLabelPosition::OutsideEnd)),
12389 Point { x: 105.0, y: 35.0 },
12390 );
12391
12392 let mut negative =
12393 CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12394 let plot = &mut negative.chart.plot_area.plots_mut().unwrap()[0];
12395 let Plot::Bar { series, .. } = plot else {
12396 panic!("expected bar plot");
12397 };
12398 series[0].values.values = vec![-1.0, -2.0];
12399 let anchors = super::plot_label_anchors(
12400 plot,
12401 chart_bounds(),
12402 DispBlanksAs::Gap,
12403 super::GeometryOptions::default(),
12404 )
12405 .unwrap();
12406 assert_point_near(
12407 anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12408 Point { x: 50.0, y: 75.0 },
12409 );
12410 let reversed = super::GeometryOptions {
12411 value: Some(super::AxisMapping {
12412 domain: Domain {
12413 min: -2.0,
12414 max: 0.0,
12415 },
12416 orientation: Orientation::MaxMin,
12417 }),
12418 ..super::GeometryOptions::default()
12419 };
12420 let anchors =
12421 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
12422 assert_point_near(
12423 anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12424 Point { x: 50.0, y: 65.0 },
12425 );
12426
12427 let pie = CT_ChartSpace::from_xml(
12428 chart_with_optional_axes(
12429 &remaining_plot_fixtures()
12430 .into_iter()
12431 .find(|(kind, _, _)| *kind == "pieChart")
12432 .unwrap()
12433 .1,
12434 false,
12435 )
12436 .as_bytes(),
12437 )
12438 .unwrap();
12439 let Plot::Pie { series, .. } = &pie.chart.plot_area.plots().unwrap()[0] else {
12440 panic!("expected pie plot");
12441 };
12442 let pie_slice = &super::pie_slices(30, None, series, chart_bounds()).unwrap()[0];
12443 assert_point_near(
12444 super::LabelAnchor::segment(pie_slice.anchor, pie_slice.base, pie_slice.end)
12445 .origin(Some(DataLabelPosition::OutsideEnd)),
12446 Point { x: 175.0, y: 70.0 },
12447 );
12448 let doughnut_slice = &super::pie_slices(30, Some(60), series, chart_bounds()).unwrap()[0];
12449 assert_point_near(
12450 super::LabelAnchor::segment(
12451 doughnut_slice.anchor,
12452 doughnut_slice.base,
12453 doughnut_slice.end,
12454 )
12455 .origin(Some(DataLabelPosition::InsideBase)),
12456 Point { x: 147.0, y: 70.0 },
12457 );
12458
12459 let short = super::LabelAnchor::segment(
12460 Point { x: 2.0, y: 0.0 },
12461 Point { x: 0.0, y: 0.0 },
12462 Point { x: 4.0, y: 0.0 },
12463 );
12464 assert_eq!(
12465 short.origin(Some(DataLabelPosition::InsideBase)),
12466 Point { x: 2.0, y: 0.0 }
12467 );
12468 assert_eq!(
12469 short.origin(Some(DataLabelPosition::InsideEnd)),
12470 Point { x: 2.0, y: 0.0 }
12471 );
12472 let thin_bounds = Rect {
12473 x: 0.0,
12474 y: 0.0,
12475 width: 80.0,
12476 height: 80.0,
12477 };
12478 let thin = &super::pie_slices(30, Some(90), series, thin_bounds).unwrap()[0];
12479 let thin = super::LabelAnchor::segment(thin.anchor, thin.base, thin.end);
12480 assert_point_near(
12481 thin.origin(Some(DataLabelPosition::InsideBase)),
12482 Point { x: 78.0, y: 40.0 },
12483 );
12484 assert_point_near(
12485 thin.origin(Some(DataLabelPosition::InsideEnd)),
12486 Point { x: 78.0, y: 40.0 },
12487 );
12488 }
12489
12490 #[test]
12491 fn degenerate_bar_and_radar_anchors_preserve_family_direction() {
12492 let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12493 let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
12494 let Plot::Bar { series, .. } = plot else {
12495 panic!("expected bar plot");
12496 };
12497 series[0].values.values = vec![80.0, 125.0];
12498 for (direction, orientation, boundary, outside) in [
12499 (
12500 BarDirection::Column,
12501 Orientation::MinMax,
12502 Point { x: 50.0, y: 140.0 },
12503 Point { x: 50.0, y: 135.0 },
12504 ),
12505 (
12506 BarDirection::Column,
12507 Orientation::MaxMin,
12508 Point { x: 50.0, y: 0.0 },
12509 Point { x: 50.0, y: 5.0 },
12510 ),
12511 (
12512 BarDirection::Bar,
12513 Orientation::MinMax,
12514 Point { x: 0.0, y: 35.0 },
12515 Point { x: 5.0, y: 35.0 },
12516 ),
12517 (
12518 BarDirection::Bar,
12519 Orientation::MaxMin,
12520 Point { x: 200.0, y: 35.0 },
12521 Point { x: 195.0, y: 35.0 },
12522 ),
12523 ] {
12524 let Plot::Bar {
12525 direction: plot_direction,
12526 ..
12527 } = plot
12528 else {
12529 panic!("expected bar plot");
12530 };
12531 *plot_direction = direction;
12532 let options = super::GeometryOptions {
12533 value: Some(super::AxisMapping {
12534 domain: Domain {
12535 min: 80.0,
12536 max: 100.0,
12537 },
12538 orientation,
12539 }),
12540 ..super::GeometryOptions::default()
12541 };
12542 let anchors =
12543 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options)
12544 .unwrap();
12545 let anchor = anchors[&(0, 0)];
12546 assert_point_near(anchor.center, boundary);
12547 assert_point_near(anchor.base, boundary);
12548 assert_point_near(anchor.end, boundary);
12549 assert_point_near(anchor.origin(Some(DataLabelPosition::InsideBase)), boundary);
12550 assert_point_near(anchor.origin(Some(DataLabelPosition::InsideEnd)), boundary);
12551 assert_point_near(anchor.origin(Some(DataLabelPosition::OutsideEnd)), outside);
12552 }
12553
12554 let radar_plot = format!(
12555 r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12556 plot_series(0)
12557 );
12558 let mut radar = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12559 let plot = &mut radar.chart.plot_area.plots_mut().unwrap()[0];
12560 let Plot::Radar { series, .. } = plot else {
12561 panic!("expected radar plot");
12562 };
12563 series[0].values.values = vec![0.0, 0.0, 0.0, 0.0];
12564 let anchors = super::plot_label_anchors(
12565 plot,
12566 chart_bounds(),
12567 DispBlanksAs::Gap,
12568 super::GeometryOptions::default(),
12569 )
12570 .unwrap();
12571 let center = Point { x: 100.0, y: 70.0 };
12572 for logical_index in 0..4 {
12573 assert_point_near(
12574 anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideBase)),
12575 center,
12576 );
12577 assert_point_near(
12578 anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideEnd)),
12579 center,
12580 );
12581 }
12582 assert_point_near(
12583 anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12584 Point { x: 100.0, y: 65.0 },
12585 );
12586 assert_point_near(
12587 anchors[&(0, 1)].origin(Some(DataLabelPosition::OutsideEnd)),
12588 Point { x: 105.0, y: 70.0 },
12589 );
12590 assert_point_near(
12591 anchors[&(0, 2)].origin(Some(DataLabelPosition::OutsideEnd)),
12592 Point { x: 100.0, y: 75.0 },
12593 );
12594 assert_point_near(
12595 anchors[&(0, 3)].origin(Some(DataLabelPosition::OutsideEnd)),
12596 Point { x: 95.0, y: 70.0 },
12597 );
12598 }
12599
12600 #[test]
12601 fn labels_outside_explicit_axis_bounds_are_suppressed() {
12602 let mut chart =
12603 CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
12604 {
12605 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12606 let Plot::Line { series, .. } = plot else {
12607 panic!("expected line plot");
12608 };
12609 series[0].values.values = vec![25.0, 75.0];
12610 }
12611 let value_axis = chart
12612 .chart
12613 .plot_area
12614 .axes
12615 .iter_mut()
12616 .find(|axis| axis.kind == AxisKind::Value)
12617 .unwrap();
12618 value_axis.scaling.minimum = Some(50.0);
12619 value_axis.scaling.maximum = Some(100.0);
12620 value_axis.scaling.orientation = Orientation::MaxMin;
12621 let plot = &chart.chart.plot_area.plots().unwrap()[0];
12622 let axes = chart.chart.plot_area.axes().unwrap();
12623 let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
12624 let anchors =
12625 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12626 assert!(!anchors.contains_key(&(0, 0)));
12627 assert_point_near(anchors[&(0, 1)].center, Point { x: 150.0, y: 70.0 });
12628
12629 let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12630 let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
12631 let Plot::Bar { series, .. } = plot else {
12632 panic!("expected bar plot");
12633 };
12634 series[0].values.values = vec![90.0, 125.0];
12635 let options = super::GeometryOptions {
12636 value: Some(super::AxisMapping {
12637 domain: Domain {
12638 min: 80.0,
12639 max: 100.0,
12640 },
12641 orientation: Orientation::MinMax,
12642 }),
12643 ..super::GeometryOptions::default()
12644 };
12645 let anchors =
12646 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12647 assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
12648 assert_point_near(anchors[&(0, 0)].base, Point { x: 50.0, y: 140.0 });
12649 assert_point_near(anchors[&(0, 0)].end, Point { x: 50.0, y: 70.0 });
12650 assert_point_near(anchors[&(0, 0)].center, Point { x: 50.0, y: 105.0 });
12651 assert_point_near(
12652 anchors[&(0, 0)].origin(Some(DataLabelPosition::InsideBase)),
12653 Point { x: 50.0, y: 135.0 },
12654 );
12655 }
12656
12657 #[test]
12658 fn zero_bars_render_and_emit_requested_value_labels() {
12659 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12660 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12661 let Plot::Bar { series, .. } = plot else {
12662 panic!("expected bar plot");
12663 };
12664 series[0].values.values = vec![0.0, 0.0];
12665 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12666 assert_eq!(path_commands(&geometry).len(), 2);
12667 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12668 render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12669 let plot = &chart.chart.plot_area.plots().unwrap()[0];
12670 let mut labels = Vec::new();
12671 super::render_data_labels(
12672 plot,
12673 chart_bounds(),
12674 DispBlanksAs::Gap,
12675 super::GeometryOptions::default(),
12676 &mut fonts,
12677 &mut labels,
12678 )
12679 .unwrap();
12680 let text: Vec<_> = labels
12681 .iter()
12682 .filter_map(|element| match element {
12683 PositionedElement::Text(run) => Some(run.text.as_str()),
12684 _ => None,
12685 })
12686 .collect();
12687 assert_eq!(text, vec!["0", "0"]);
12688 }
12689
12690 #[test]
12691 fn axes_gridlines_and_tick_marks_follow_model_state() {
12692 let chart = labelled_bar_chart();
12693 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12694 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12695 let ranks: Vec<_> = group
12696 .children
12697 .iter()
12698 .map(|element| match element {
12699 PositionedElement::Group(group) => {
12700 assert!(group.clip.is_some());
12701 1
12702 }
12703 PositionedElement::Path(path)
12704 if path
12705 .stroke
12706 .as_ref()
12707 .is_some_and(|stroke| stroke.width == 0.5) =>
12708 {
12709 0
12710 }
12711 PositionedElement::Path(path) if path.fill.is_some() => 4,
12712 PositionedElement::Path(path) => {
12713 let bounds = path.path.bounds().expect("axis or tick bounds");
12714 assert_eq!(path.stroke.as_ref().map(|stroke| stroke.width), Some(1.0));
12715 if bounds.width > 4.0 || bounds.height > 4.0 {
12716 2
12717 } else {
12718 3
12719 }
12720 }
12721 PositionedElement::Text(_) => 5,
12722 _ => panic!("unexpected labelled chart element"),
12723 })
12724 .collect();
12725 assert!(ranks.windows(2).all(|pair| pair[0] <= pair[1]), "{ranks:?}");
12726 for required in 0..=5 {
12727 assert!(
12728 ranks.contains(&required),
12729 "missing z-order rank {required}: {ranks:?}"
12730 );
12731 }
12732 assert_eq!(ranks.iter().filter(|rank| **rank == 1).count(), 1);
12733 assert_eq!(ranks.iter().filter(|rank| **rank == 2).count(), 2);
12734
12735 let mut deleted = chart.clone();
12736 for axis in &mut deleted.chart.plot_area.axes {
12737 axis.deleted = true;
12738 }
12739 let deleted_group = render_chart(&deleted.chart, chart_bounds(), &mut fonts).unwrap();
12740 assert_eq!(
12741 deleted_group
12742 .children
12743 .iter()
12744 .filter(|element| {
12745 matches!(
12746 element,
12747 PositionedElement::Path(path) if path.stroke.is_some()
12748 )
12749 })
12750 .count(),
12751 0
12752 );
12753 }
12754
12755 #[test]
12756 fn category_ticks_cover_unlabelled_and_sparse_logical_slots() {
12757 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12758 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12759 panic!("expected bar plot");
12760 };
12761 series[0].categories = None;
12762 let plot = &chart.chart.plot_area.plots().unwrap()[0];
12763 let mut category_axis = chart
12764 .chart
12765 .plot_area
12766 .axes()
12767 .unwrap()
12768 .into_iter()
12769 .find(|axis| axis.kind == AxisKind::Category)
12770 .unwrap();
12771 category_axis.major_gridlines = Some(super::ChartLines::default());
12772 category_axis.major_tick_mark = TickMark::Cross;
12773 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12774 let annotations = super::render_axes_and_labels(
12775 plot,
12776 &[category_axis],
12777 chart_bounds(),
12778 DispBlanksAs::Gap,
12779 &mut fonts,
12780 )
12781 .unwrap();
12782 assert_eq!(annotations.gridlines.len(), 2);
12783 assert_eq!(annotations.ticks.len(), 2);
12784 assert!(annotations.labels.is_empty());
12785
12786 let sparse_plot = format!(
12787 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>"#,
12788 sparse_category_series()
12789 );
12790 let sparse = CT_ChartSpace::from_xml(chart_with_plot(&sparse_plot).as_bytes()).unwrap();
12791 let plot = &sparse.chart.plot_area.plots().unwrap()[0];
12792 let mut category_axis = sparse
12793 .chart
12794 .plot_area
12795 .axes()
12796 .unwrap()
12797 .into_iter()
12798 .find(|axis| axis.kind == AxisKind::Category)
12799 .unwrap();
12800 category_axis.major_gridlines = Some(super::ChartLines::default());
12801 category_axis.major_tick_mark = TickMark::Cross;
12802 let annotations = super::render_axes_and_labels(
12803 plot,
12804 &[category_axis],
12805 chart_bounds(),
12806 DispBlanksAs::Gap,
12807 &mut fonts,
12808 )
12809 .unwrap();
12810 assert_eq!(annotations.gridlines.len(), 3);
12811 assert_eq!(annotations.ticks.len(), 3);
12812 assert_eq!(annotations.labels.len(), 2);
12813
12814 let oversized_plot = sparse_plot.replacen(
12815 r#"<q:ptCount val="3"/>"#,
12816 r#"<q:ptCount val="4294967295"/>"#,
12817 1,
12818 );
12819 let oversized =
12820 CT_ChartSpace::from_xml(chart_with_plot(&oversized_plot).as_bytes()).unwrap();
12821 let plot = &oversized.chart.plot_area.plots().unwrap()[0];
12822 let category_axis = oversized
12823 .chart
12824 .plot_area
12825 .axes()
12826 .unwrap()
12827 .into_iter()
12828 .find(|axis| axis.kind == AxisKind::Category)
12829 .unwrap();
12830 let result = super::render_axes_and_labels(
12831 plot,
12832 &[category_axis],
12833 chart_bounds(),
12834 DispBlanksAs::Gap,
12835 &mut fonts,
12836 );
12837 let error = match result {
12838 Err(error) => error,
12839 Ok(_) => panic!("oversized category annotations rendered"),
12840 };
12841 assert!(
12842 error
12843 .to_string()
12844 .contains("logical category count 4294967295")
12845 );
12846 assert!(error.to_string().contains("exceeds 16384"));
12847 }
12848
12849 #[test]
12850 fn unsupported_numeric_category_formats_return_projection_errors() {
12851 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12852 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12853 panic!("expected bar plot");
12854 };
12855 series[0].categories = Some(AxisData::Numeric(
12856 NumericData::new(
12857 "Sheet1!$A$2:$A$3".to_owned(),
12858 "General".to_owned(),
12859 vec![1.0, 2.0],
12860 )
12861 .unwrap(),
12862 ));
12863 let plot = &chart.chart.plot_area.plots().unwrap()[0];
12864 let mut category_axis = chart
12865 .chart
12866 .plot_area
12867 .axes()
12868 .unwrap()
12869 .into_iter()
12870 .find(|axis| axis.kind == AxisKind::Category)
12871 .unwrap();
12872 category_axis.number_format = Some(NumberFormat::new("0.00".to_owned(), false).unwrap());
12873 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12874 let annotations = super::render_axes_and_labels(
12875 plot,
12876 &[category_axis.clone()],
12877 chart_bounds(),
12878 DispBlanksAs::Gap,
12879 &mut fonts,
12880 )
12881 .unwrap();
12882 let text: Vec<_> = annotations
12883 .labels
12884 .iter()
12885 .filter_map(|element| match element {
12886 PositionedElement::Text(run) => Some(run.text.as_str()),
12887 _ => None,
12888 })
12889 .collect();
12890 assert_eq!(text, vec!["1.00", "2.00"]);
12891
12892 category_axis.number_format = Some(NumberFormat::new("#,##0".to_owned(), false).unwrap());
12893 let result = super::render_axes_and_labels(
12894 plot,
12895 &[category_axis],
12896 chart_bounds(),
12897 DispBlanksAs::Gap,
12898 &mut fonts,
12899 );
12900 let error = match result {
12901 Err(error) => error,
12902 Ok(_) => panic!("unsupported category-axis format rendered"),
12903 };
12904 assert!(
12905 error
12906 .to_string()
12907 .contains("c:numFmt/@formatCode projection")
12908 );
12909 assert!(error.to_string().contains("#,##0"));
12910 }
12911
12912 #[test]
12913 fn labels_and_legend_shape_with_deterministic_fonts() {
12914 let chart = labelled_bar_chart();
12915 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12916 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12917 let labels: Vec<_> = group
12918 .children
12919 .iter()
12920 .filter_map(|element| match element {
12921 PositionedElement::Text(run) => Some(run),
12922 _ => None,
12923 })
12924 .collect();
12925 for text in ["North", "South", "20", "Sales", "100"] {
12926 assert!(labels.iter().any(|run| run.text == text), "missing {text}");
12927 }
12928 let styled_tick = labels.iter().find(|run| run.text == "20").unwrap();
12929 assert_eq!(styled_tick.font_size, 12.0);
12930 assert!(styled_tick.bold);
12931 assert!(
12932 labels
12933 .iter()
12934 .all(|run| !run.glyph_ids.is_empty() && run.glyph_ids.len() == run.advances.len())
12935 );
12936 }
12937
12938 #[test]
12939 fn labelled_chart_raster_is_deterministic() {
12940 let raster = || {
12941 let chart = labelled_bar_chart();
12942 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12943 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12944 let layout = LayoutResult::new(
12945 vec![PageFrame::new(
12946 1,
12947 200.0,
12948 140.0,
12949 vec![PositionedElement::Group(group)],
12950 )],
12951 fonts.all_font_data(),
12952 None,
12953 Vec::new(),
12954 );
12955 oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
12956 };
12957 assert_eq!(raster(), raster());
12958 }
12959
12960 fn labelled_bar_chart() -> CT_ChartSpace {
12961 let series = plot_series(0)
12962 .replace(
12963 "<q:cat>",
12964 "<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>",
12965 )
12966 .replace("<q:v>1</q:v>", "<q:v>0</q:v>")
12967 .replace("<q:v>2</q:v>", "<q:v>100</q:v>");
12968 let plot = format!(
12969 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>"#
12970 );
12971 let xml = chart_with_plot(&plot)
12972 .replacen(
12973 "<q:scaling/>",
12974 "<q:scaling><q:orientation val=\"minMax\"/></q:scaling>",
12975 1,
12976 )
12977 .replacen(
12978 "<q:scaling/>",
12979 "<q:scaling><q:max val=\"100\"/><q:min val=\"0\"/></q:scaling>",
12980 1,
12981 )
12982 .replace(
12983 "<q:axPos val=\"b\"/>",
12984 "<q:axPos val=\"b\"/><q:majorTickMark val=\"cross\"/>",
12985 )
12986 .replace(
12987 "<q:axPos val=\"l\"/>",
12988 "<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>",
12989 )
12990 .replace("</q:plotArea></q:chart>", "</q:plotArea><q:legend/></q:chart>");
12991 CT_ChartSpace::from_xml(xml.as_bytes()).unwrap()
12992 }
12993
12994 fn chart_bounds() -> Rect {
12995 Rect {
12996 x: 0.0,
12997 y: 0.0,
12998 width: 200.0,
12999 height: 140.0,
13000 }
13001 }
13002
13003 fn four_series_bar_chart() -> CT_ChartSpace {
13004 let series = (0..4).map(plot_series).collect::<String>();
13005 let plot = bar_plot("").replace(&plot_series(0), &series);
13006 CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
13007 }
13008
13009 fn seven_series_bar_chart() -> CT_ChartSpace {
13010 let series = (0..7).map(plot_series).collect::<String>();
13011 let plot = bar_plot("").replace(&plot_series(0), &series);
13012 CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
13013 }
13014
13015 fn path_colours(geometry: &ChartGeometry) -> Vec<Color> {
13016 let mut colours = geometry_children(geometry)
13017 .iter()
13018 .filter_map(|element| match element {
13019 PositionedElement::Path(path) => match path
13020 .fill
13021 .as_ref()
13022 .or_else(|| path.stroke.as_ref().map(|stroke| &stroke.paint))
13023 {
13024 Some(Paint::Solid(colour)) => Some(*colour),
13025 _ => None,
13026 },
13027 _ => None,
13028 })
13029 .collect::<Vec<_>>();
13030 colours.dedup_by(|left, right| left == right);
13031 colours
13032 }
13033
13034 fn resolved_layout_colour(colour: oxml_drawing::color::ResolvedColor) -> Color {
13035 Color {
13036 r: f64::from(colour.red) / 255.0,
13037 g: f64::from(colour.green) / 255.0,
13038 b: f64::from(colour.blue) / 255.0,
13039 a: f64::from(colour.alpha) / 255.0,
13040 }
13041 }
13042
13043 fn geometry_children(geometry: &ChartGeometry) -> &[PositionedElement] {
13044 let [PositionedElement::Group(group)] = geometry.elements.as_slice() else {
13045 panic!("chart geometry must contain one group");
13046 };
13047 &group.children
13048 }
13049
13050 fn path_commands(geometry: &ChartGeometry) -> Vec<&[PathCommand]> {
13051 geometry_children(geometry)
13052 .iter()
13053 .map(|element| {
13054 let PositionedElement::Path(path) = element else {
13055 panic!("chart geometry child must be a path");
13056 };
13057 path.path.commands.as_slice()
13058 })
13059 .collect()
13060 }
13061
13062 fn path_bounds(geometry: &ChartGeometry) -> Vec<Rect> {
13063 geometry_children(geometry)
13064 .iter()
13065 .map(|element| {
13066 let PositionedElement::Path(path) = element else {
13067 panic!("chart geometry child must be a path");
13068 };
13069 path.path.bounds().expect("chart path has bounds")
13070 })
13071 .collect()
13072 }
13073
13074 fn assert_rect_near(actual: Rect, expected: Rect) {
13075 assert_near(actual.x, expected.x);
13076 assert_near(actual.y, expected.y);
13077 assert_near(actual.width, expected.width);
13078 assert_near(actual.height, expected.height);
13079 }
13080
13081 fn assert_rects_near(actual: &[Rect], expected: &[Rect]) {
13082 assert_eq!(actual.len(), expected.len());
13083 for (actual, expected) in actual.iter().copied().zip(expected.iter().copied()) {
13084 assert_rect_near(actual, expected);
13085 }
13086 }
13087
13088 fn assert_path_points(commands: &[PathCommand], expected: &[Point], closed: bool) {
13089 let points: Vec<_> = commands
13090 .iter()
13091 .filter_map(|command| match command {
13092 PathCommand::MoveTo(point) | PathCommand::LineTo(point) => Some(*point),
13093 PathCommand::Close => None,
13094 PathCommand::CurveTo { .. } => panic!("expected polygonal path"),
13095 })
13096 .collect();
13097 assert_eq!(points.len(), expected.len());
13098 for (actual, expected) in points.into_iter().zip(expected.iter().copied()) {
13099 assert_point_near(actual, expected);
13100 }
13101 assert_eq!(commands.last() == Some(&PathCommand::Close), closed);
13102 }
13103
13104 fn assert_point_near(actual: Point, expected: Point) {
13105 assert_near(actual.x, expected.x);
13106 assert_near(actual.y, expected.y);
13107 }
13108
13109 fn assert_near(actual: f64, expected: f64) {
13110 assert!(
13111 (actual - expected).abs() < 1e-9,
13112 "expected {expected}, got {actual}"
13113 );
13114 }
13115
13116 #[test]
13117 fn remaining_v1_plots_round_trip_and_render() {
13118 for (kind, plot, axes) in remaining_plot_fixtures() {
13119 let xml = chart_with_optional_axes(&plot, axes);
13120 let parsed = CT_ChartSpace::from_xml(xml.as_bytes())
13121 .unwrap_or_else(|error| panic!("{kind}: parse failed: {error}"));
13122 assert_eq!(parsed.chart.plot_area.plots().unwrap().len(), 1);
13123 assert_eq!(
13124 parsed.chart.plot_area.axes().unwrap().len(),
13125 usize::from(axes) * 2
13126 );
13127 let written = parsed
13128 .to_xml()
13129 .unwrap_or_else(|error| panic!("{kind}: write failed: {error}"));
13130 assert_eq!(
13131 parsed,
13132 CT_ChartSpace::from_xml(&written)
13133 .unwrap_or_else(|error| panic!("{kind}: reparse failed: {error}"))
13134 );
13135 }
13136 if let Some(corpus) = require_or_skip_corpus() {
13137 verify_remaining_plot_viewer_gate(&corpus);
13138 }
13139 }
13140
13141 #[test]
13142 fn remaining_plot_families_write_fixed_prefixes_in_schema_order() {
13143 for (kind, plot, axes) in remaining_plot_fixtures() {
13144 let parsed =
13145 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
13146 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13147 assert!(written.contains(&format!("<c:{kind}")));
13148 assert!(!written.contains(&format!("<q:{kind}")));
13149 let series = written.find("<c:ser").unwrap();
13150 let labels = written.find("<c:dLbls").unwrap();
13151 assert!(series < labels, "{kind}: series must precede labels");
13152 if matches!(kind, "areaChart" | "scatterChart" | "radarChart") {
13153 let property = match kind {
13154 "areaChart" => "<c:grouping",
13155 "scatterChart" => "<c:scatterStyle",
13156 "radarChart" => "<c:radarStyle",
13157 _ => unreachable!(),
13158 };
13159 assert!(written.find(property).unwrap() < series);
13160 assert!(labels < written.find("<c:axId").unwrap());
13161 } else {
13162 assert!(labels < written.find("<c:firstSliceAng").unwrap());
13163 }
13164 if kind == "doughnutChart" {
13165 assert!(
13166 written.find("<c:firstSliceAng").unwrap()
13167 < written.find("<c:holeSize").unwrap()
13168 );
13169 }
13170 }
13171 }
13172
13173 #[test]
13174 fn scatter_series_map_numeric_categories_and_values_to_x_and_y() {
13175 let plot = remaining_plot_fixtures()
13176 .into_iter()
13177 .find(|(kind, _, _)| *kind == "scatterChart")
13178 .unwrap()
13179 .1;
13180 let mut parsed =
13181 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, true).as_bytes()).unwrap();
13182 let standalone_series = {
13183 let Plot::Scatter { series, .. } = &mut parsed.chart.plot_area.plots_mut().unwrap()[0]
13184 else {
13185 panic!("expected scatter plot");
13186 };
13187 let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
13188 panic!("expected numeric x values");
13189 };
13190 x_values.values[0] = 3.5;
13191 series[0].values.values[0] = 7.5;
13192 series[0].clone()
13193 };
13194 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13195 assert!(written.contains("<c:xVal>"));
13196 assert!(written.contains("<c:yVal>"));
13197 assert!(!written.contains("<c:cat>"));
13198 assert!(!written.contains("<c:val>"));
13199 assert!(written.contains("<c:v>3.5</c:v>"));
13200 assert!(written.contains("<c:v>7.5</c:v>"));
13201 let standalone = String::from_utf8(standalone_series.to_xml().unwrap()).unwrap();
13202 assert!(standalone.contains("<c:xVal>"));
13203 assert!(standalone.contains("<c:yVal>"));
13204 assert!(!standalone.contains("<c:cat>"));
13205 assert!(!standalone.contains("<c:val>"));
13206 }
13207
13208 #[test]
13209 fn malformed_remaining_plots_return_errors_without_panicking() {
13210 let series = plot_series(0);
13211 let scatter = numeric_plot_series(0);
13212 let opaque_bubble = series.replace(
13213 "</q:val></q:ser>",
13214 "</q:val><q:bubbleSize><x:opaque/></q:bubbleSize></q:ser>",
13215 );
13216 let cases = [
13217 r#"<q:pieChart/>"#.to_owned(),
13218 format!(r#"<q:pieChart>{series}<q:firstSliceAng val="361"/></q:pieChart>"#),
13219 format!(
13220 r#"<q:pieChart>{series}<q:firstSliceAng val="1"/><q:firstSliceAng val="2"/></q:pieChart>"#
13221 ),
13222 format!(r#"<q:doughnutChart>{series}<q:holeSize val="9"/></q:doughnutChart>"#),
13223 format!(
13224 r#"<q:doughnutChart>{series}<q:holeSize val="50"/><q:holeSize val="60"/></q:doughnutChart>"#
13225 ),
13226 format!(
13227 r#"<q:areaChart><q:grouping val="clustered"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
13228 ),
13229 format!(
13230 r#"<q:areaChart><q:grouping val="standard"/><q:grouping val="stacked"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
13231 ),
13232 format!(
13233 r#"<q:scatterChart><q:scatterStyle val="dots"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13234 ),
13235 format!(
13236 r#"<q:scatterChart><q:scatterStyle val="marker"/><q:scatterStyle val="line"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13237 ),
13238 format!(
13239 r#"<q:radarChart><q:radarStyle val="smooth"/>{series}<q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
13240 ),
13241 format!(
13242 r#"<q:radarChart><q:radarStyle val="marker"/>{series}<q:dLbls/><q:dLbls/><q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
13243 ),
13244 format!(r#"<q:pieChart>{series}<q:axId val="1"/></q:pieChart>"#),
13245 format!(
13246 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/></q:areaChart>"#
13247 ),
13248 r#"<q:areaChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:areaChart>"#.to_owned(),
13249 format!(
13250 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/><q:axId val="2"/><q:axId val="3"/></q:areaChart>"#
13251 ),
13252 format!(
13253 r#"<q:scatterChart><q:scatterStyle val="marker"/>{series}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13254 ),
13255 format!(
13256 r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13257 scatter
13258 .replace("<q:yVal>", "<q:val>")
13259 .replace("</q:yVal>", "</q:val>")
13260 ),
13261 format!(
13262 r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13263 scatter_without_x(0)
13264 ),
13265 format!(
13266 r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13267 scatter_without_y(0)
13268 ),
13269 format!(r#"<q:pieChart>{opaque_bubble}</q:pieChart>"#),
13270 ];
13271 for plot in cases {
13272 let xml = chart_with_optional_axes(&plot, false);
13273 let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
13274 assert!(result.is_ok(), "parser panicked for {plot}");
13275 assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
13276 }
13277
13278 let missing_axis = format!(
13279 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="7"/></q:areaChart>"#
13280 );
13281 let missing_axis =
13282 CT_ChartSpace::from_xml(chart_with_optional_axes(&missing_axis, true).as_bytes())
13283 .unwrap_err();
13284 assert!(missing_axis.to_string().contains("missing axis 7"));
13285
13286 let values =
13287 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
13288 let mut bubble_series = Series::new(0, 0, values.clone());
13289 bubble_series.bubble_size = Some(values);
13290 assert!(Plot::pie(vec![bubble_series]).is_err());
13291 }
13292
13293 #[test]
13294 fn unsupported_plot_families_and_children_remain_byte_preserved() {
13295 for raw in [
13296 r#"<q:pie3DChart x:keep="3d"><q:ser><x:opaque/></q:ser></q:pie3DChart>"#,
13297 r#"<q:ofPieChart x:keep="of"><x:opaque/></q:ofPieChart>"#,
13298 r#"<q:stockChart x:keep="stock"><x:opaque/></q:stockChart>"#,
13299 r#"<q:surfaceChart x:keep="surface"><x:opaque/></q:surfaceChart>"#,
13300 r#"<q:bubbleChart x:keep="bubble"><x:opaque/></q:bubbleChart>"#,
13301 ] {
13302 let parsed =
13303 CT_ChartSpace::from_xml(chart_with_optional_axes(raw, false).as_bytes()).unwrap();
13304 assert!(parsed.chart.plot_area.plots().is_err());
13305 let written = parsed.to_xml().unwrap();
13306 assert!(
13307 written
13308 .windows(raw.len())
13309 .any(|window| window == raw.as_bytes())
13310 );
13311 }
13312
13313 let pie = remaining_plot_fixtures()[0].1.clone();
13314 let preserved = r#"<!--point--><q:dPt x:id="1"><q:explosion val="7"/><x:tail/></q:dPt>"#;
13315 let plot = pie.replace("<q:dLbls>", &format!("{preserved}<q:dLbls>"));
13316 let parsed =
13317 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, false).as_bytes()).unwrap();
13318 let written = parsed.to_xml().unwrap();
13319 assert!(
13320 written
13321 .windows(preserved.len())
13322 .any(|window| window == preserved.as_bytes())
13323 );
13324
13325 for (kind, mut plot, axes) in [
13326 ("pieChart", remaining_plot_fixtures()[0].1.clone(), false),
13327 ("areaChart", remaining_plot_fixtures()[2].1.clone(), true),
13328 ] {
13329 plot = plot.replace(r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#, "");
13330 if kind == "pieChart" {
13331 plot = plot.replace(
13332 "</q:extLst></q:pieChart>",
13333 "</q:extLst><!--after-ext--></q:pieChart>",
13334 );
13335 } else {
13336 plot = plot.replace(
13337 "</q:dropLines><q:axId",
13338 "</q:dropLines><!--after-drop-lines--><q:axId",
13339 );
13340 }
13341 let mut parsed =
13342 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
13343 let labels =
13344 CT_DLbls::from_xml(format!(r#"<c:dLbls xmlns:c="{C_NS}"/>"#).as_bytes()).unwrap();
13345 match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
13346 Plot::Pie { data_labels, .. } | Plot::Area { data_labels, .. } => {
13347 *data_labels = Some(labels)
13348 }
13349 _ => panic!("expected pie or area"),
13350 }
13351 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13352 assert!(
13353 written.find("<c:dLbls").unwrap() < written.find("<q:extLst").unwrap(),
13354 "{kind}: inserted labels must precede the preserved extension"
13355 );
13356 if kind == "areaChart" {
13357 assert!(written.find("<c:dLbls").unwrap() < written.find("<q:dropLines").unwrap());
13358 assert!(
13359 written.find("<q:dropLines").unwrap()
13360 < written.find("<!--after-drop-lines-->").unwrap()
13361 );
13362 } else {
13363 assert!(
13364 written.find("<q:extLst").unwrap() < written.find("<!--after-ext-->").unwrap()
13365 );
13366 }
13367 }
13368 }
13369
13370 #[test]
13371 fn every_supported_corpus_plot_round_trips_structurally() {
13372 let Some(corpus) = require_or_skip_corpus() else {
13373 return;
13374 };
13375 verify_fetched_corpus(&corpus);
13376 let mut pie_count = 0usize;
13377 let mut bar_count = 0usize;
13378 let mut line_count = 0usize;
13379 for path in manifest_paths() {
13380 let package = OpcPackage::open(corpus.join(path)).unwrap();
13381 for (part, xml) in &package.parts {
13382 if !is_chart_part(part) {
13383 continue;
13384 }
13385 let parsed = CT_ChartSpace::from_xml(xml).unwrap();
13386 if let Ok(plots) = parsed.chart.plot_area.plots() {
13387 for plot in plots {
13388 match plot {
13389 Plot::Pie { .. } => pie_count += 1,
13390 Plot::Bar { .. } => bar_count += 1,
13391 Plot::Line { .. } => line_count += 1,
13392 _ => {}
13393 }
13394 }
13395 }
13396 let written = parsed.to_xml().unwrap();
13397 assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
13398 }
13399 }
13400 assert_eq!(pie_count, 1, "typed corpus pie coverage changed");
13401 assert_eq!(bar_count, 11, "typed corpus bar coverage changed");
13402 assert_eq!(line_count, 2, "typed corpus line coverage changed");
13403 }
13404
13405 fn standalone_axis(local: &str, children: &str) -> String {
13406 format!(
13407 r#"<c:{local} xmlns:c="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}">{children}</c:{local}>"#
13408 )
13409 }
13410
13411 fn minimal_axis(local: &str, id: &str, position: &str, cross_axis: &str) -> String {
13412 format!(
13413 r#"<c:{local}><c:axId val="{id}"/><c:scaling/><c:axPos val="{position}"/><c:crossAx val="{cross_axis}"/></c:{local}>"#
13414 )
13415 }
13416
13417 fn chart_with_axes(axes: &[String]) -> String {
13418 format!(
13419 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>"#,
13420 axes.join("")
13421 )
13422 }
13423
13424 #[test]
13425 fn data_labels_write_fixed_prefixes_in_schema_order() {
13426 let xml = format!(
13427 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>"#
13428 );
13429 let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
13430 assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0%");
13431 assert_eq!(parsed.position, Some(DataLabelPosition::OutsideEnd));
13432 assert_eq!(parsed.separator.as_deref(), Some(" / "));
13433 assert!(parsed.show_legend_key);
13434 assert!(parsed.show_value);
13435 assert!(!parsed.show_category_name);
13436 assert!(parsed.show_series_name);
13437 assert!(!parsed.show_percent);
13438 assert!(parsed.show_bubble_size);
13439
13440 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13441 assert!(written.starts_with(&format!(
13442 "<c:dLbls xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
13443 )));
13444 let mut cursor = 0usize;
13445 for tag in [
13446 "<q:dLbl",
13447 "<c:numFmt",
13448 "<q:spPr",
13449 "<q:txPr",
13450 "<c:dLblPos",
13451 "<c:showLegendKey",
13452 "<c:showVal",
13453 "<c:showCatName",
13454 "<c:showSerName",
13455 "<c:showPercent",
13456 "<c:showBubbleSize",
13457 "<c:separator",
13458 "<q:leaderLines",
13459 "<q:extLst",
13460 ] {
13461 let position = written[cursor..]
13462 .find(tag)
13463 .unwrap_or_else(|| panic!("missing ordered data-label tag {tag}"));
13464 cursor += position + tag.len();
13465 }
13466 assert_eq!(parsed, CT_DLbls::from_xml(written.as_bytes()).unwrap());
13467 }
13468
13469 #[test]
13470 fn percentage_formatted_label_renders_with_correct_text() {
13471 let Some(corpus) = require_or_skip_corpus() else {
13472 return;
13473 };
13474 verify_fetched_corpus(&corpus);
13475 assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
13476 assert_command_version("pdftotext", &["-v"], PDFTOTEXT_VERSION);
13477
13478 let source = corpus.join("bar-chart.pptx");
13479 let mut package = OpcPackage::open(&source).expect("open percentage-label source deck");
13480 let chart_part = "/ppt/charts/chart1.xml";
13481 let original_parts = package.parts.clone();
13482 let chart_xml = package
13483 .get_part(chart_part)
13484 .expect("bar chart part")
13485 .to_vec();
13486 let original_series = first_series_xml(&chart_xml).expect("bar chart series");
13487 let mut series = Series::from_xml(&original_series).expect("parse bar chart series");
13488 series.values.values.fill(0.25);
13489 let labels = CT_DLbls {
13490 number_format: Some(NumberFormat::new("0%".to_owned(), false).unwrap()),
13491 position: Some(DataLabelPosition::OutsideEnd),
13492 show_value: true,
13493 ..CT_DLbls::default()
13494 };
13495 series.data_labels = Some(labels);
13496 let replacement = series.to_xml().expect("serialize typed percentage labels");
13497 let candidate_chart = replace_once(&chart_xml, &original_series, &replacement);
13498 package.set_part(chart_part, candidate_chart);
13499 for (part, bytes) in &original_parts {
13500 if part == chart_part {
13501 assert_ne!(package.parts[part], *bytes);
13502 } else {
13503 assert_eq!(
13504 package.parts[part], *bytes,
13505 "unexpected changed part {part}"
13506 );
13507 }
13508 }
13509
13510 let temp_root = std::env::temp_dir().join(format!(
13511 "rpptx-chart-f123-label-gate-{}",
13512 std::process::id()
13513 ));
13514 let profile = std::env::temp_dir().join(format!(
13515 "rpptx-chart-f123-label-profile-{}",
13516 std::process::id()
13517 ));
13518 if temp_root.exists() {
13519 fs::remove_dir_all(&temp_root).expect("remove stale F-123 evidence");
13520 }
13521 if profile.exists() {
13522 fs::remove_dir_all(&profile).expect("remove stale F-123 profile");
13523 }
13524 fs::create_dir_all(&temp_root).expect("create F-123 evidence root");
13525 let unbound_candidate = temp_root.join("f123-percentage-label.pptx");
13526 package
13527 .save(&unbound_candidate)
13528 .expect("save percentage-label candidate");
13529 let candidate_sha = sha256(&unbound_candidate);
13530 let evidence_dir = temp_root.join(&candidate_sha);
13531 fs::create_dir(&evidence_dir).expect("create SHA-bound evidence directory");
13532 let candidate = evidence_dir.join("f123-percentage-label.pptx");
13533 fs::rename(&unbound_candidate, &candidate).expect("bind candidate to evidence SHA");
13534 assert_eq!(sha256(&candidate), candidate_sha);
13535
13536 let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
13537 let conversion = Command::new("soffice")
13538 .args([
13539 "--headless",
13540 &profile_argument,
13541 "--convert-to",
13542 "pdf:impress_pdf_Export",
13543 "--outdir",
13544 ])
13545 .arg(&evidence_dir)
13546 .arg(&candidate)
13547 .output()
13548 .expect("run pinned LibreOffice percentage-label gate");
13549 assert!(
13550 conversion.status.success(),
13551 "LibreOffice conversion failed: {}",
13552 String::from_utf8_lossy(&conversion.stderr)
13553 );
13554 let pdf = evidence_dir.join("f123-percentage-label.pdf");
13555 assert!(
13556 pdf.is_file(),
13557 "LibreOffice did not create {}",
13558 pdf.display()
13559 );
13560 let text_path = evidence_dir.join("f123-percentage-label.txt");
13561 let extraction = Command::new("pdftotext")
13562 .arg(&pdf)
13563 .arg(&text_path)
13564 .output()
13565 .expect("run pinned Poppler percentage-label extraction");
13566 assert!(
13567 extraction.status.success(),
13568 "pdftotext failed: {}",
13569 String::from_utf8_lossy(&extraction.stderr)
13570 );
13571 let extracted = fs::read_to_string(&text_path).expect("read percentage-label text");
13572 assert!(
13573 extracted.contains("25%"),
13574 "SHA-bound viewer text lacks 25%: {extracted:?}"
13575 );
13576 for artifact in [&candidate, &pdf, &text_path] {
13577 assert_eq!(
13578 artifact.parent(),
13579 Some(evidence_dir.as_path()),
13580 "evidence escaped candidate SHA"
13581 );
13582 }
13583 eprintln!("F-123 candidate/render/text evidence SHA-256 {candidate_sha}, extracted 25%");
13584 fs::remove_dir_all(&temp_root).expect("remove F-123 evidence");
13585 if profile.exists() {
13586 fs::remove_dir_all(&profile).expect("remove F-123 profile");
13587 }
13588 }
13589
13590 #[test]
13591 fn common_number_formats_project_cached_values_deterministically() {
13592 for (code, value, expected) in [
13593 ("General", 0.25, "0.25"),
13594 ("0", 12.6, "13"),
13595 ("0.0", 12.64, "12.6"),
13596 ("0.00", -12.645, "-12.64"),
13597 ("0%", 0.25, "25%"),
13598 ("0.0%", 0.256, "25.6%"),
13599 ("0.00%", 0.256, "25.60%"),
13600 ] {
13601 let format = NumberFormat::new(code.to_owned(), false).unwrap();
13602 assert_eq!(format.format_value(value).unwrap(), expected, "{code}");
13603 }
13604 }
13605
13606 #[test]
13607 fn malformed_data_labels_and_number_formats_return_errors_without_panicking() {
13608 let cases = [
13609 format!(
13610 r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="1"/><c:showVal val="0"/></c:dLbls>"#
13611 ),
13612 format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="yes"/></c:dLbls>"#),
13613 format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:dLblPos val="middle"/></c:dLbls>"#),
13614 format!(
13615 r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="" sourceLinked="1"/></c:dLbls>"#
13616 ),
13617 format!(
13618 r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="0" sourceLinked="maybe"/></c:dLbls>"#
13619 ),
13620 ];
13621 for xml in cases {
13622 let result = std::panic::catch_unwind(|| CT_DLbls::from_xml(xml.as_bytes()));
13623 assert!(result.is_ok(), "data-label parser panicked for {xml}");
13624 assert!(result.unwrap().is_err(), "malformed labels parsed: {xml}");
13625 }
13626
13627 let invalid_code = NumberFormat::new("bad\u{0}code".to_owned(), false);
13628 assert!(invalid_code.is_err());
13629 let unsupported = NumberFormat::new("#,##0".to_owned(), false).unwrap();
13630 assert!(unsupported.format_value(12.0).is_err());
13631 let general = NumberFormat::new("General".to_owned(), false).unwrap();
13632 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
13633 assert!(general.format_value(value).is_err());
13634 }
13635
13636 let invalid_separator = CT_DLbls {
13637 separator: Some("bad\u{0}separator".to_owned()),
13638 ..CT_DLbls::default()
13639 };
13640 assert!(invalid_separator.to_xml().is_err());
13641 }
13642
13643 #[test]
13644 fn data_labels_preserve_point_overrides_and_extensions_byte_for_byte() {
13645 let xml = format!(
13646 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>"#
13647 );
13648 let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
13649 let written = parsed.to_xml().unwrap();
13650 for raw in [
13651 br#"<!--before-->"#.as_slice(),
13652 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(),
13653 br#"<?after-point?>"#.as_slice(),
13654 br#"<q:spPr><x:collection-shape/></q:spPr>"#.as_slice(),
13655 br#"<q:txPr><x:collection-text/></q:txPr>"#.as_slice(),
13656 br#"<q:showLeaderLines val="1"/>"#.as_slice(),
13657 br#"<q:leaderLines><q:spPr><x:leader-shape/></q:spPr></q:leaderLines>"#.as_slice(),
13658 br#"<q:extLst><q:ext uri="collection"><x:tail/></q:ext></q:extLst>"#.as_slice(),
13659 br#"<!--after-->"#.as_slice(),
13660 ] {
13661 assert!(
13662 written.windows(raw.len()).any(|window| window == raw),
13663 "preserved data-label bytes changed: {}",
13664 String::from_utf8_lossy(raw)
13665 );
13666 }
13667 assert_eq!(parsed, CT_DLbls::from_xml(&written).unwrap());
13668 }
13669
13670 #[test]
13671 fn every_corpus_data_label_collection_round_trips_structurally() {
13672 let Some(corpus) = require_or_skip_corpus() else {
13673 return;
13674 };
13675 verify_fetched_corpus(&corpus);
13676 let mut label_collections = 0usize;
13677 let mut axis_number_formats = 0usize;
13678 for path in manifest_paths() {
13679 let package = OpcPackage::open(corpus.join(path))
13680 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
13681 for (part, xml) in &package.parts {
13682 if !is_chart_part(part) {
13683 continue;
13684 }
13685 let chart = CT_ChartSpace::from_xml(xml)
13686 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
13687 for series in chart.chart.plot_area.series().unwrap_or_else(|error| {
13688 panic!("{path} {part}: series projection failed: {error}")
13689 }) {
13690 if let Some(labels) = series.data_labels {
13691 let written = labels.to_xml().unwrap_or_else(|error| {
13692 panic!("{path} {part}: label write failed: {error}")
13693 });
13694 let reparsed = CT_DLbls::from_xml(&written).unwrap_or_else(|error| {
13695 panic!("{path} {part}: written labels parse failed: {error}")
13696 });
13697 assert_eq!(labels, reparsed, "{path} {part}: labels changed");
13698 label_collections += 1;
13699 }
13700 }
13701 for axis in chart.chart.plot_area.axes().unwrap_or_else(|error| {
13702 panic!("{path} {part}: axis projection failed: {error}")
13703 }) {
13704 if axis.number_format.is_some() {
13705 let written = axis.to_xml().unwrap_or_else(|error| {
13706 panic!("{path} {part}: axis write failed: {error}")
13707 });
13708 let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
13709 panic!("{path} {part}: written axis parse failed: {error}")
13710 });
13711 assert_eq!(axis, reparsed, "{path} {part}: axis changed");
13712 axis_number_formats += 1;
13713 }
13714 }
13715 }
13716 }
13717 assert!(label_collections > 0, "the pinned corpus has no c:dLbls");
13718 assert!(
13719 axis_number_formats > 0,
13720 "the pinned corpus has no axis c:numFmt"
13721 );
13722 eprintln!(
13723 "ChartML data-label corpus gate checked {label_collections} collections and {axis_number_formats} axis number formats"
13724 );
13725 }
13726
13727 fn full_axis(local: &str, position: &str) -> String {
13728 format!(
13729 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}>"#
13730 )
13731 }
13732
13733 #[test]
13734 fn axis_id_pairs_are_reciprocal() {
13735 assert_eq!(
13736 AxisId::new(i64::from(i32::MIN)).unwrap().value(),
13737 i64::from(i32::MIN)
13738 );
13739 assert_eq!(
13740 AxisId::new(i64::from(u32::MAX)).unwrap().value(),
13741 i64::from(u32::MAX)
13742 );
13743 assert!(AxisId::new(i64::from(i32::MIN) - 1).is_err());
13744 assert!(AxisId::new(i64::from(u32::MAX) + 1).is_err());
13745
13746 let valid = chart_with_axes(&[
13747 minimal_axis("catAx", "-1884094432", "b", "-1884097184"),
13748 minimal_axis("valAx", "-1884097184", "l", "-1884094432"),
13749 ]);
13750 let axes = CT_ChartSpace::from_xml(valid.as_bytes())
13751 .unwrap()
13752 .chart
13753 .plot_area
13754 .axes()
13755 .unwrap();
13756 assert_eq!(axes.len(), 2);
13757 assert_eq!(axes[0].id.value(), -1_884_094_432);
13758 assert_eq!(axes[0].cross_axis, axes[1].id);
13759 assert_eq!(axes[1].cross_axis, axes[0].id);
13760
13761 let invalid_sets = [
13762 vec![
13763 minimal_axis("catAx", "1", "b", "2"),
13764 minimal_axis("valAx", "1", "l", "2"),
13765 ],
13766 vec![minimal_axis("catAx", "1", "b", "1")],
13767 vec![minimal_axis("catAx", "1", "b", "2")],
13768 vec![
13769 minimal_axis("catAx", "1", "b", "2"),
13770 minimal_axis("valAx", "2", "l", "3"),
13771 minimal_axis("serAx", "3", "r", "2"),
13772 ],
13773 ];
13774 for axes in invalid_sets {
13775 let chart = CT_ChartSpace::from_xml(chart_with_axes(&axes).as_bytes()).unwrap();
13776 assert!(chart.chart.plot_area.axes().is_err());
13777 }
13778
13779 let empty = CT_ChartSpace::from_xml(chart_with_axes(&[]).as_bytes()).unwrap();
13780 assert!(empty.chart.plot_area.axes().unwrap().is_empty());
13781 }
13782
13783 #[test]
13784 fn all_axis_forms_write_fixed_prefixes_in_schema_order() {
13785 for (local, kind, position) in [
13786 ("catAx", AxisKind::Category, AxisPosition::Bottom),
13787 ("valAx", AxisKind::Value, AxisPosition::Left),
13788 ("dateAx", AxisKind::Date, AxisPosition::Top),
13789 ("serAx", AxisKind::Series, AxisPosition::Right),
13790 ] {
13791 let xml = full_axis(local, position.as_str());
13792 let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
13793 assert_eq!(parsed.kind, kind);
13794 assert_eq!(parsed.id.value(), -1_125_255_920);
13795 assert_eq!(parsed.scaling.log_base, Some(10.0));
13796 assert_eq!(parsed.scaling.orientation, Orientation::MaxMin);
13797 assert_eq!(parsed.scaling.maximum, Some(100.0));
13798 assert_eq!(parsed.scaling.minimum, Some(-10.0));
13799 assert!(parsed.deleted);
13800 assert_eq!(parsed.position, position);
13801 assert!(parsed.major_gridlines.is_some());
13802 assert!(parsed.minor_gridlines.is_some());
13803 assert!(parsed.title.is_some());
13804 assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0");
13805 assert_eq!(parsed.major_tick_mark, TickMark::Cross);
13806 assert_eq!(parsed.minor_tick_mark, TickMark::Inside);
13807 assert_eq!(parsed.tick_label_position, TickLabelPosition::High);
13808 assert!(parsed.sp_pr.is_some());
13809 assert!(parsed.tx_pr.is_some());
13810 assert_eq!(parsed.cross_axis.value(), -1_004_303_696);
13811
13812 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13813 assert!(written.starts_with(&format!(
13814 "<c:{local} xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
13815 )));
13816 assert!(written.contains("<c:axId val=\"-1125255920\""));
13817 let mut cursor = 0usize;
13818 for tag in [
13819 "<c:axId",
13820 "<c:scaling",
13821 "<c:delete",
13822 "<c:axPos",
13823 "<c:majorGridlines",
13824 "<c:minorGridlines",
13825 "<c:title",
13826 "<c:numFmt",
13827 "<c:majorTickMark",
13828 "<c:minorTickMark",
13829 "<c:tickLblPos",
13830 "<c:spPr",
13831 "<c:txPr",
13832 "<c:crossAx",
13833 ] {
13834 let position = written[cursor..]
13835 .find(tag)
13836 .unwrap_or_else(|| panic!("missing ordered axis tag {tag}"));
13837 cursor += position + tag.len();
13838 }
13839 assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
13840 }
13841
13842 let mut constructed = Axis::new(
13843 AxisKind::Value,
13844 AxisId::new(1).unwrap(),
13845 AxisPosition::Left,
13846 AxisId::new(2).unwrap(),
13847 );
13848 assert_eq!(
13849 constructed,
13850 Axis::from_xml(&constructed.to_xml().unwrap()).unwrap()
13851 );
13852 constructed.kind = AxisKind::Category;
13853 constructed.id = AxisId::new(3).unwrap();
13854 constructed.cross_axis = AxisId::new(4).unwrap();
13855 constructed.scaling.minimum = Some(0.0);
13856 constructed.scaling.maximum = Some(10.0);
13857 let written = constructed.to_xml().unwrap();
13858 assert!(written.starts_with(b"<c:catAx"));
13859 assert_eq!(constructed, Axis::from_xml(&written).unwrap());
13860 }
13861
13862 #[test]
13863 fn axis_equality_normalizes_ids_without_losing_lexical_preservation() {
13864 let padded = Axis::from_xml(
13865 standalone_axis(
13866 "catAx",
13867 "<c:axId val=\"01\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"002\"/>",
13868 )
13869 .as_bytes(),
13870 )
13871 .unwrap();
13872 let normalized = Axis::from_xml(
13873 standalone_axis(
13874 "catAx",
13875 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13876 )
13877 .as_bytes(),
13878 )
13879 .unwrap();
13880 assert_eq!(padded, normalized);
13881
13882 let preserved = String::from_utf8(padded.to_xml().unwrap()).unwrap();
13883 assert!(preserved.contains("<c:axId val=\"01\""));
13884 assert!(preserved.contains("<c:crossAx val=\"002\""));
13885
13886 let mut mutated = padded;
13887 mutated.id = AxisId::new(3).unwrap();
13888 mutated.cross_axis = AxisId::new(4).unwrap();
13889 let rewritten = String::from_utf8(mutated.to_xml().unwrap()).unwrap();
13890 assert!(rewritten.contains("<c:axId val=\"3\""));
13891 assert!(rewritten.contains("<c:crossAx val=\"4\""));
13892 assert!(!rewritten.contains("val=\"01\""));
13893 assert!(!rewritten.contains("val=\"002\""));
13894 }
13895
13896 #[test]
13897 fn malformed_axis_values_return_errors_without_panicking() {
13898 let cases = [
13899 standalone_axis(
13900 "catAx",
13901 "<c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13902 ),
13903 standalone_axis(
13904 "catAx",
13905 "<c:axId val=\"1\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13906 ),
13907 standalone_axis(
13908 "catAx",
13909 "<c:axId val=\"1\"/><c:scaling/><c:crossAx val=\"2\"/>",
13910 ),
13911 standalone_axis(
13912 "catAx",
13913 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/>",
13914 ),
13915 standalone_axis(
13916 "catAx",
13917 "<c:axId val=\"-2147483649\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13918 ),
13919 standalone_axis(
13920 "catAx",
13921 "<c:axId val=\"4294967296\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13922 ),
13923 standalone_axis(
13924 "catAx",
13925 "<c:axId val=\"1\"/><c:scaling><c:logBase val=\"1\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13926 ),
13927 standalone_axis(
13928 "catAx",
13929 "<c:axId val=\"1\"/><c:scaling><c:max val=\"NaN\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13930 ),
13931 standalone_axis(
13932 "catAx",
13933 "<c:axId val=\"1\"/><c:scaling><c:max val=\"1\"/><c:min val=\"2\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13934 ),
13935 standalone_axis(
13936 "catAx",
13937 "<c:axId val=\"1\"/><c:scaling><c:orientation val=\"sideways\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13938 ),
13939 standalone_axis(
13940 "catAx",
13941 "<c:axId val=\"1\"/><c:scaling/><c:delete val=\"yes\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13942 ),
13943 standalone_axis(
13944 "catAx",
13945 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"middle\"/><c:crossAx val=\"2\"/>",
13946 ),
13947 standalone_axis(
13948 "catAx",
13949 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:majorTickMark val=\"near\"/><c:crossAx val=\"2\"/>",
13950 ),
13951 standalone_axis(
13952 "catAx",
13953 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:tickLblPos val=\"middle\"/><c:crossAx val=\"2\"/>",
13954 ),
13955 standalone_axis(
13956 "catAx",
13957 "<c:axId val=\"1\"/><c:axId val=\"2\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13958 ),
13959 standalone_axis(
13960 "catAx",
13961 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt sourceLinked=\"1\"/><c:crossAx val=\"2\"/>",
13962 ),
13963 standalone_axis(
13964 "catAx",
13965 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt formatCode=\"0\" sourceLinked=\"maybe\"/><c:crossAx val=\"2\"/>",
13966 ),
13967 ];
13968 for xml in cases {
13969 let result = std::panic::catch_unwind(|| Axis::from_xml(xml.as_bytes()));
13970 assert!(result.is_ok(), "axis parser panicked for {xml}");
13971 assert!(result.unwrap().is_err(), "malformed axis parsed: {xml}");
13972 }
13973
13974 let mut axis = Axis::new(
13975 AxisKind::Value,
13976 AxisId::new(1).unwrap(),
13977 AxisPosition::Left,
13978 AxisId::new(2).unwrap(),
13979 );
13980 axis.scaling.maximum = Some(f64::INFINITY);
13981 assert!(axis.to_xml().is_err());
13982 }
13983
13984 #[test]
13985 fn axes_preserve_unmodelled_children_byte_for_byte() {
13986 let xml = format!(
13987 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>"#
13988 );
13989 let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
13990 let mut relabelled = parsed.clone();
13991 relabelled.kind = AxisKind::Value;
13992 assert!(
13993 relabelled.to_xml().is_err(),
13994 "a parsed category axis must not retain its tail under a value-axis root"
13995 );
13996 let written = parsed.to_xml().unwrap();
13997 for raw in [
13998 br#"<!--before-->"#.as_slice(),
13999 br#"<?after-id?>"#.as_slice(),
14000 br#"<!--scale-->"#.as_slice(),
14001 br#"<q:extLst><q:ext uri="scale"><x:data/></q:ext></q:extLst>"#.as_slice(),
14002 br#"<x:grid/>"#.as_slice(),
14003 br#"<x:title/>"#.as_slice(),
14004 br#"<q:crosses val="autoZero"/>"#.as_slice(),
14005 br#"<q:auto val="1"><x:auto/></q:auto>"#.as_slice(),
14006 br#"<q:extLst><q:ext uri="axis"><x:tail/></q:ext></q:extLst>"#.as_slice(),
14007 br#"<!--after-->"#.as_slice(),
14008 ] {
14009 assert!(
14010 written.windows(raw.len()).any(|window| window == raw),
14011 "preserved axis bytes changed: {}",
14012 String::from_utf8_lossy(raw)
14013 );
14014 }
14015 let written = String::from_utf8(written).unwrap();
14016 assert!(written.starts_with("<c:catAx"));
14017 assert!(written.contains("x:keep=\"axis\""));
14018 assert!(written.contains("x:keep=\"id\""));
14019 assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
14020 }
14021
14022 #[test]
14023 fn every_corpus_axis_round_trips_structurally() {
14024 let Some(corpus) = require_or_skip_corpus() else {
14025 return;
14026 };
14027 verify_fetched_corpus(&corpus);
14028 let mut axis_count = 0usize;
14029 let mut chart_parts = 0usize;
14030 let mut kinds = HashSet::new();
14031 for path in manifest_paths() {
14032 let package = OpcPackage::open(corpus.join(path))
14033 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14034 for (part, xml) in &package.parts {
14035 if !is_chart_part(part) {
14036 continue;
14037 }
14038 let chart = CT_ChartSpace::from_xml(xml)
14039 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14040 let axes = chart.chart.plot_area.axes().unwrap_or_else(|error| {
14041 panic!("{path} {part}: axis projection failed: {error}")
14042 });
14043 for axis in axes {
14044 kinds.insert(axis.kind);
14045 let written = axis.to_xml().unwrap_or_else(|error| {
14046 panic!("{path} {part}: axis write failed: {error}")
14047 });
14048 let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
14049 panic!("{path} {part}: written axis parse failed: {error}")
14050 });
14051 assert_eq!(axis, reparsed, "{path} {part}: axis model changed");
14052 axis_count += 1;
14053 }
14054 chart_parts += 1;
14055 }
14056 }
14057 assert!(
14058 axis_count > 0,
14059 "the pinned corpus contained no ChartML axes"
14060 );
14061 assert!(kinds.contains(&AxisKind::Category));
14062 assert!(kinds.contains(&AxisKind::Value));
14063
14064 for kind in ["dateAx", "serAx"] {
14065 let parsed = Axis::from_xml(
14066 standalone_axis(
14067 kind,
14068 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14069 )
14070 .as_bytes(),
14071 )
14072 .unwrap();
14073 assert_eq!(parsed, Axis::from_xml(&parsed.to_xml().unwrap()).unwrap());
14074 }
14075 eprintln!(
14076 "ChartML axis corpus gate checked {axis_count} axes across {chart_parts} chart parts"
14077 );
14078 }
14079
14080 #[test]
14081 fn series_formula_and_cache_are_consistent_with_one_source() {
14082 let values = NumericData::new(
14083 "'Sales 24'!$B$2:$B$4".to_owned(),
14084 "0.0".to_owned(),
14085 vec![4.25, 8.5, 17.0],
14086 )
14087 .unwrap();
14088 let mut series = Series::new(3, 1, values);
14089 series.categories = Some(AxisData::String(
14090 StringRef::new(
14091 "'Sales 24'!$A$2:$A$4".to_owned(),
14092 vec!["North".to_owned(), "South".to_owned(), "West".to_owned()],
14093 )
14094 .unwrap(),
14095 ));
14096
14097 let written = String::from_utf8(series.to_xml().unwrap()).unwrap();
14098 assert!(written.contains("<c:f>'Sales 24'!$B$2:$B$4</c:f>"));
14099 assert!(written.contains("<c:formatCode>0.0</c:formatCode>"));
14100 assert_eq!(written.matches("<c:ptCount val=\"3\"").count(), 2);
14101 for (index, value) in ["4.25", "8.5", "17"].iter().enumerate() {
14102 assert!(written.contains(&format!("<c:pt idx=\"{index}\"><c:v>{value}</c:v></c:pt>")));
14103 }
14104 let reparsed = Series::from_xml(written.as_bytes()).unwrap();
14105 assert_eq!(reparsed.index, series.index);
14106 assert_eq!(reparsed.order, series.order);
14107 match (&reparsed.categories, &series.categories) {
14108 (Some(AxisData::String(left)), Some(AxisData::String(right))) => {
14109 assert_eq!(left.formula, right.formula);
14110 assert_eq!(left.values, right.values);
14111 }
14112 _ => panic!("expected string categories"),
14113 }
14114 assert_eq!(reparsed.values.formula, series.values.formula);
14115 assert_eq!(reparsed.values.format_code, series.values.format_code);
14116 assert_eq!(reparsed.values.values, series.values.values);
14117 }
14118
14119 #[test]
14120 fn string_and_numeric_references_write_fixed_prefixes_in_schema_order() {
14121 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>"#;
14122 let string_ref = StringRef::from_xml(string_xml).unwrap();
14123 let written = String::from_utf8(string_ref.to_xml().unwrap()).unwrap();
14124 assert!(written.starts_with("<c:strRef xmlns:c="));
14125 assert!(written.find("<c:f>").unwrap() < written.find("<c:strCache>").unwrap());
14126 assert!(written.find("<c:ptCount").unwrap() < written.find("<c:pt idx=").unwrap());
14127
14128 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>"#;
14129 let numeric = NumericData::from_xml(numeric_xml).unwrap();
14130 let written = String::from_utf8(numeric.to_xml().unwrap()).unwrap();
14131 let positions: Vec<_> = [
14132 "<c:f>",
14133 "<c:numCache>",
14134 "<c:formatCode>",
14135 "<c:ptCount",
14136 "<c:pt idx=",
14137 ]
14138 .iter()
14139 .map(|tag| written.find(tag).unwrap())
14140 .collect();
14141 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14142 assert_eq!(numeric, NumericData::from_xml(written.as_bytes()).unwrap());
14143 }
14144
14145 #[test]
14146 fn mixed_chartml_aliases_resolve_by_namespace_uri() {
14147 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>"#;
14148 let parsed = Series::from_xml(xml).unwrap();
14149 assert_eq!(parsed.index, 4);
14150 assert_eq!(parsed.order, 2);
14151 assert_eq!(parsed.values.values, vec![1.5, 2.5]);
14152 assert!(matches!(parsed.categories, Some(AxisData::String(_))));
14153 assert_eq!(parsed, Series::from_xml(&parsed.to_xml().unwrap()).unwrap());
14154 }
14155
14156 #[test]
14157 fn plot_area_series_ignores_inherited_foreign_plot_aliases() {
14158 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>"#;
14159 let chart = CT_ChartSpace::from_xml(xml).unwrap();
14160 let series = chart.chart.plot_area.series().unwrap();
14161 assert_eq!(series.len(), 1);
14162 assert_eq!(series[0].index, 1);
14163 let written = String::from_utf8(series[0].to_xml().unwrap()).unwrap();
14164 assert!(
14165 written.contains(r#"xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart""#)
14166 );
14167 assert!(written.contains(r#"xmlns:x="urn:producer""#));
14168 assert!(written.contains("<q:marker><x:data/></q:marker>"));
14169 assert_eq!(series[0], Series::from_xml(written.as_bytes()).unwrap());
14170
14171 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>"#;
14172 let chart = CT_ChartSpace::from_xml(conflicting).unwrap();
14173 assert!(chart.chart.plot_area.series().is_err());
14174 }
14175
14176 #[test]
14177 fn malformed_series_and_cache_values_return_errors_without_panicking() {
14178 let cases: &[&[u8]] = &[
14179 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>"#,
14180 br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/></c:ser>"#,
14181 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>"#,
14182 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>"#,
14183 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>"#,
14184 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>"#,
14185 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>"#,
14186 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>"#,
14187 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>"#,
14188 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>"#,
14189 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>"#,
14190 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>"#,
14191 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>"#,
14192 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>"#,
14193 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>"#,
14194 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>"#,
14195 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>"#,
14196 ];
14197 for xml in cases {
14198 let result = std::panic::catch_unwind(|| Series::from_xml(xml));
14199 assert!(result.is_ok(), "series parser panicked");
14200 assert!(
14201 result.unwrap().is_err(),
14202 "malformed series parsed: {}",
14203 String::from_utf8_lossy(xml)
14204 );
14205 }
14206 assert!(
14207 NumericData::new(
14208 "S!$A$1".to_owned(),
14209 "General".to_owned(),
14210 vec![f64::INFINITY]
14211 )
14212 .is_err()
14213 );
14214 }
14215
14216 #[test]
14217 fn chart_text_rejects_xml_forbidden_characters_and_escapes_metacharacters() {
14218 assert!(StringRef::new("S!$A$1\u{1}".to_owned(), Vec::new()).is_err());
14219 assert!(StringRef::new("S!$A$1".to_owned(), vec!["invalid\u{1}".to_owned()]).is_err());
14220 assert!(
14221 NumericData::new("S!$A$1\u{1}".to_owned(), "General".to_owned(), Vec::new()).is_err()
14222 );
14223 assert!(
14224 NumericData::new("S!$A$1".to_owned(), "General\u{1}".to_owned(), Vec::new()).is_err()
14225 );
14226
14227 let mut strings = StringRef::new("S!$A$1".to_owned(), vec!["valid".to_owned()]).unwrap();
14228 strings.formula = "invalid\u{1}".to_owned();
14229 assert!(strings.to_xml().is_err());
14230 strings.formula = "S!$A$1".to_owned();
14231 strings.values[0] = "invalid\u{1}".to_owned();
14232 assert!(strings.to_xml().is_err());
14233
14234 let mut numbers =
14235 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap();
14236 numbers.formula = "invalid\u{1}".to_owned();
14237 assert!(numbers.to_xml().is_err());
14238 numbers.formula = "S!$A$1".to_owned();
14239 numbers.format_code = "invalid\u{1}".to_owned();
14240 assert!(numbers.to_xml().is_err());
14241
14242 let strings =
14243 StringRef::new("S!$A$1&".to_owned(), vec!["North < West & East".to_owned()]).unwrap();
14244 let written = String::from_utf8(strings.to_xml().unwrap()).unwrap();
14245 assert!(written.contains("S!$A$1&"));
14246 assert!(written.contains("North < West & East"));
14247 }
14248
14249 #[test]
14250 fn series_preserves_unmodelled_children_byte_for_byte() {
14251 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>"#;
14252 let parsed = Series::from_xml(xml).unwrap();
14253 let written = parsed.to_xml().unwrap();
14254 for raw in [
14255 br#"<!--before-->"#.as_slice(),
14256 br#"<x:between/>"#.as_slice(),
14257 br#"<q:dPt x:id="one"><q:idx val="0"/></q:dPt>"#.as_slice(),
14258 br#"<q:trendline x:id="trend"/>"#.as_slice(),
14259 br#"<q:extLst><q:ext uri="keep"><x:data/></q:ext></q:extLst>"#.as_slice(),
14260 br#"<!--point-->"#.as_slice(),
14261 br#"<x:cacheExt/>"#.as_slice(),
14262 ] {
14263 assert!(
14264 written.windows(raw.len()).any(|window| window == raw),
14265 "preserved series bytes changed: {}",
14266 String::from_utf8_lossy(raw)
14267 );
14268 }
14269 let written = String::from_utf8(written.clone()).unwrap();
14270 assert!(written.contains(r#"x:keep="series""#));
14271 assert!(written.contains(r#"x:keep="formula""#));
14272 assert!(written.contains("<c:dLbls"));
14273 assert!(written.contains(r#"x:id="labels""#));
14274 assert!(parsed.data_labels.is_some());
14275 assert_eq!(parsed, Series::from_xml(written.as_bytes()).unwrap());
14276 }
14277
14278 #[test]
14279 fn public_series_edits_do_not_duplicate_or_drop_preserved_payloads() {
14280 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>"#;
14281 let parsed = Series::from_xml(opaque_xml).unwrap();
14282
14283 let mut edited = parsed.clone();
14284 edited.name = Some(StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap());
14285 assert!(edited.to_xml().is_err());
14286
14287 let mut edited = parsed.clone();
14288 edited.categories = Some(AxisData::Numeric(
14289 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap(),
14290 ));
14291 assert!(edited.to_xml().is_err());
14292
14293 let mut edited = parsed;
14294 edited.bubble_size =
14295 Some(NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
14296 assert!(edited.to_xml().is_err());
14297
14298 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>"#;
14299 let parsed = Series::from_xml(cache_xml).unwrap();
14300 let tail = "<c:extLst><c:ext uri=\"keep\"><x:data/></c:ext></c:extLst>";
14301
14302 let mut shortened = parsed.clone();
14303 shortened.values.values.truncate(1);
14304 let written = String::from_utf8(shortened.to_xml().unwrap()).unwrap();
14305 assert!(!written.contains("<c:pt idx=\"1\""));
14306 assert!(written.find("<c:pt idx=\"0\"").unwrap() < written.find(tail).unwrap());
14307 assert!(Series::from_xml(written.as_bytes()).is_ok());
14308
14309 let mut grown = parsed;
14310 grown.values.values.push(3.0);
14311 let written = String::from_utf8(grown.to_xml().unwrap()).unwrap();
14312 assert!(written.find("<c:pt idx=\"2\"").unwrap() < written.find(tail).unwrap());
14313 assert!(Series::from_xml(written.as_bytes()).is_ok());
14314
14315 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>"#;
14316 let mut edited = Series::from_xml(ordered_xml).unwrap();
14317 edited.name = Some(StringRef::new("S!$B$1".to_owned(), Vec::new()).unwrap());
14318 edited.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
14319 edited.categories = Some(AxisData::String(
14320 StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap(),
14321 ));
14322 edited.bubble_size =
14323 Some(NumericData::new("S!$C$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
14324 let written = String::from_utf8(edited.to_xml().unwrap()).unwrap();
14325 let positions: Vec<_> = [
14326 "<c:tx>",
14327 "<c:spPr",
14328 "<c:marker>",
14329 "<c:cat>",
14330 "<c:val>",
14331 "<c:bubbleSize>",
14332 "<c:extLst>",
14333 ]
14334 .iter()
14335 .map(|tag| written.find(tag).unwrap())
14336 .collect();
14337 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14338 assert!(Series::from_xml(written.as_bytes()).is_ok());
14339 }
14340
14341 #[test]
14342 fn every_corpus_series_round_trips_structurally() {
14343 let Some(corpus) = require_or_skip_corpus() else {
14344 return;
14345 };
14346 verify_fetched_corpus(&corpus);
14347 let mut series_count = 0usize;
14348 let mut chart_parts = 0usize;
14349 for path in manifest_paths() {
14350 let package = OpcPackage::open(corpus.join(path))
14351 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14352 for (part, xml) in &package.parts {
14353 if !is_chart_part(part) {
14354 continue;
14355 }
14356 let chart = CT_ChartSpace::from_xml(xml)
14357 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14358 let parsed_series =
14359 chart.chart.plot_area.series().unwrap_or_else(|error| {
14360 panic!("{path} {part}: series parse failed: {error}")
14361 });
14362 for series in parsed_series {
14363 let written = series.to_xml().unwrap_or_else(|error| {
14364 panic!("{path} {part}: series write failed: {error}")
14365 });
14366 let reparsed = Series::from_xml(&written).unwrap_or_else(|error| {
14367 panic!("{path} {part}: written series parse failed: {error}")
14368 });
14369 assert_eq!(series, reparsed, "{path} {part}: series model changed");
14370 series_count += 1;
14371 }
14372 chart_parts += 1;
14373 }
14374 }
14375 assert!(
14376 series_count > 0,
14377 "the pinned corpus contained no supported series"
14378 );
14379 eprintln!(
14380 "ChartML series corpus gate checked {series_count} series across {chart_parts} chart parts"
14381 );
14382 }
14383
14384 #[test]
14385 fn chart_space_reads_aliases_and_writes_fixed_prefixes_in_schema_order() {
14386 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>"#;
14387 let parsed = CT_ChartSpace::from_xml(xml).unwrap();
14388 assert!(!parsed.chart.auto_title_deleted);
14389 assert!(!parsed.chart.plot_vis_only);
14390 assert_eq!(parsed.chart.disp_blanks_as, DispBlanksAs::Span);
14391 assert!(parsed.chart.title.is_some());
14392 assert!(parsed.chart.legend.is_some());
14393 assert!(parsed.sp_pr.is_some());
14394 assert!(parsed.tx_pr.is_some());
14395
14396 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14397 assert!(written.starts_with(
14398 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""#
14399 ));
14400 for tag in [
14401 "<c:chart>",
14402 "<c:title>",
14403 "<c:plotArea>",
14404 "<c:legend>",
14405 "<c:plotVisOnly",
14406 "<c:dispBlanksAs",
14407 "<c:spPr>",
14408 "<c:txPr>",
14409 ] {
14410 assert!(written.contains(tag), "missing fixed-prefix tag {tag}");
14411 }
14412 let positions: Vec<_> = [
14413 "<c:title>",
14414 "<c:plotArea>",
14415 "<c:legend>",
14416 "<c:plotVisOnly",
14417 "<c:dispBlanksAs",
14418 "<c:spPr>",
14419 "<c:txPr>",
14420 ]
14421 .iter()
14422 .map(|tag| written.find(tag).unwrap())
14423 .collect();
14424 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14425 assert!(written.contains("<a:solidFill><a:srgbClr"));
14426 assert!(written.contains("<a:bodyPr/><a:p/>"));
14427 assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
14428 }
14429
14430 #[test]
14431 fn public_core_edits_keep_preserved_children_in_their_schema_slots() {
14432 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>"#;
14433 let mut parsed = CT_ChartSpace::from_xml(xml).unwrap();
14434 parsed.chart.auto_title_deleted = true;
14435 parsed.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
14436 parsed.tx_pr =
14437 Some(CT_TextBody::from_xml(br#"<a:txBody><a:bodyPr/><a:p/></a:txBody>"#).unwrap());
14438 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14439 let positions: Vec<_> = [
14440 "<c:autoTitleDeleted",
14441 "<c:pivotFmts>",
14442 "<c:plotArea",
14443 "<c:spPr",
14444 "<c:txPr",
14445 "<c:externalData",
14446 ]
14447 .iter()
14448 .map(|tag| written.find(tag).unwrap())
14449 .collect();
14450 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14451 assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
14452 }
14453
14454 #[test]
14455 fn malformed_core_chart_values_return_errors_without_panicking() {
14456 let cases: &[&[u8]] = &[
14457 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"/>"#,
14458 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart/></c:chartSpace>"#,
14459 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:plotArea/></c:chart></c:chartSpace>"#,
14460 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:autoTitleDeleted val="yes"/><c:plotArea/></c:chart></c:chartSpace>"#,
14461 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:dispBlanksAs val="empty"/></c:chart></c:chartSpace>"#,
14462 br#"<c:chartSpace xmlns:c="urn:wrong"><c:chart><c:plotArea/></c:chart></c:chartSpace>"#,
14463 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:foreign"><x:chart><x:plotArea/></x:chart></c:chartSpace>"#,
14464 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>"#,
14465 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:title/><c:title/><c:plotArea/></c:chart></c:chartSpace>"#,
14466 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>"#,
14467 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>"#,
14468 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>"#,
14469 ];
14470 for xml in cases {
14471 let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml));
14472 assert!(result.is_ok(), "ChartML parser panicked");
14473 assert!(
14474 result.unwrap().is_err(),
14475 "malformed ChartML parsed: {}",
14476 String::from_utf8_lossy(xml)
14477 );
14478 }
14479 }
14480
14481 #[test]
14482 fn point_label_overrides_render_without_changing_preserved_xml() {
14483 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>"#;
14484 let plot = bar_plot("").replace(
14485 r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
14486 &format!(
14487 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>"#
14488 ),
14489 );
14490 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
14491 let plot = &chart.chart.plot_area.plots().unwrap()[0];
14492 let Plot::Bar { data_labels, .. } = plot else {
14493 panic!("expected bar plot");
14494 };
14495 let labels = data_labels.as_ref().unwrap();
14496 let written = labels.to_xml().unwrap();
14497 assert!(
14498 written
14499 .windows(raw_overrides.len())
14500 .any(|window| window == raw_overrides.as_bytes())
14501 );
14502 let overrides = super::point_label_overrides(labels).unwrap();
14503 assert_eq!(overrides.keys().copied().collect::<Vec<_>>(), vec![1]);
14504
14505 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14506 let mut rendered = Vec::new();
14507 super::render_data_labels(
14508 plot,
14509 chart_bounds(),
14510 DispBlanksAs::Gap,
14511 super::GeometryOptions::default(),
14512 &mut fonts,
14513 &mut rendered,
14514 )
14515 .unwrap();
14516 let labels: Vec<_> = rendered
14517 .iter()
14518 .filter_map(|element| match element {
14519 PositionedElement::Text(run) => Some(run),
14520 _ => None,
14521 })
14522 .collect();
14523 assert_eq!(labels.len(), 2);
14524 assert_eq!(labels[0].text, "North, 1");
14525 assert_eq!(labels[1].text, "South / 2.0");
14526 assert_point_near(labels[0].origin, Point { x: 50.0, y: 105.0 });
14527 assert_point_near(labels[1].origin, Point { x: 158.0, y: 70.0 });
14528 assert_eq!(
14529 CT_DLbls::from_xml(&written).unwrap().to_xml().unwrap(),
14530 written
14531 );
14532 }
14533
14534 #[test]
14535 fn malformed_point_label_overrides_return_contextual_errors() {
14536 let cases = [
14537 r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:showVal val="1"/></q:dLbl></q:dLbls>"#,
14538 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>"#,
14539 r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="point"/></q:dLbl></q:dLbls>"#,
14540 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>"#,
14541 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>"#,
14542 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>"#,
14543 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>"#,
14544 ];
14545 for xml in cases {
14546 let labels = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
14547 let result = std::panic::catch_unwind(|| super::point_label_overrides(&labels));
14548 assert!(result.is_ok(), "point-label parser panicked for {xml}");
14549 assert!(
14550 result.unwrap().is_err(),
14551 "malformed point labels parsed: {xml}"
14552 );
14553 }
14554
14555 let foreign = format!(
14556 r#"<q:dLbls xmlns:q="{C_NS}" xmlns:x="urn:foreign"><x:dLbl><x:idx val="0"/></x:dLbl></q:dLbls>"#
14557 );
14558 let labels = CT_DLbls::from_xml(foreign.as_bytes()).unwrap();
14559 assert!(super::point_label_overrides(&labels).unwrap().is_empty());
14560 }
14561
14562 #[test]
14563 fn percentage_label_aggregation_rejects_nonfinite_totals() {
14564 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14565 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14566 let Plot::Bar { series, .. } = plot else {
14567 panic!("expected bar plot");
14568 };
14569 series[0].values.values = vec![f64::MAX, f64::MAX];
14570 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14571 super::render_data_labels(
14572 plot,
14573 chart_bounds(),
14574 DispBlanksAs::Gap,
14575 super::GeometryOptions::default(),
14576 &mut fonts,
14577 &mut Vec::new(),
14578 )
14579 .expect("value-only labels do not need a percentage total");
14580 let Plot::Bar { data_labels, .. } = plot else {
14581 panic!("expected bar plot");
14582 };
14583 data_labels.as_mut().unwrap().show_percent = true;
14584 let error = super::render_data_labels(
14585 plot,
14586 chart_bounds(),
14587 DispBlanksAs::Gap,
14588 super::GeometryOptions::default(),
14589 &mut fonts,
14590 &mut Vec::new(),
14591 )
14592 .unwrap_err();
14593 assert!(error.to_string().contains("data-label percentage total"));
14594
14595 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>"#;
14596 let plot_xml = bar_plot("").replace(
14597 r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
14598 &format!(
14599 r#"<q:dLbls>{overrides}<q:showVal val="1"/><q:showPercent val="1"/></q:dLbls>"#
14600 ),
14601 );
14602 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
14603 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14604 let Plot::Bar { series, .. } = plot else {
14605 panic!("expected bar plot");
14606 };
14607 series[0].values.values = vec![f64::MAX, f64::MAX];
14608 super::render_data_labels(
14609 plot,
14610 chart_bounds(),
14611 DispBlanksAs::Gap,
14612 super::GeometryOptions::default(),
14613 &mut fonts,
14614 &mut Vec::new(),
14615 )
14616 .expect("effective point overrides disable percentage aggregation");
14617 }
14618
14619 #[test]
14620 fn percentage_labels_use_effective_number_format_precision() {
14621 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14622 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14623 let Plot::Bar {
14624 series,
14625 data_labels,
14626 ..
14627 } = plot
14628 else {
14629 panic!("expected bar plot");
14630 };
14631 series[0].values.values = vec![1.0, 2.0];
14632 let labels = data_labels.as_mut().unwrap();
14633 labels.show_value = false;
14634 labels.show_percent = true;
14635 labels.number_format = Some(NumberFormat::new("0.00%".to_owned(), false).unwrap());
14636 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14637 let mut rendered = Vec::new();
14638 super::render_data_labels(
14639 plot,
14640 chart_bounds(),
14641 DispBlanksAs::Gap,
14642 super::GeometryOptions::default(),
14643 &mut fonts,
14644 &mut rendered,
14645 )
14646 .unwrap();
14647 let text: Vec<_> = rendered
14648 .iter()
14649 .filter_map(|element| match element {
14650 PositionedElement::Text(run) => Some(run.text.as_str()),
14651 _ => None,
14652 })
14653 .collect();
14654 assert_eq!(text, vec!["33.33%", "66.67%"]);
14655
14656 let point_override = r#"<q:dLbl><q:idx val="0"/><q:numFmt formatCode="0.00%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbl>"#;
14657 let plot_xml = bar_plot("").replace(
14658 r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
14659 &format!(
14660 r#"<q:dLbls>{point_override}<q:numFmt formatCode="0%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbls>"#
14661 ),
14662 );
14663 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
14664 let plot = &chart.chart.plot_area.plots().unwrap()[0];
14665 let mut rendered = Vec::new();
14666 super::render_data_labels(
14667 plot,
14668 chart_bounds(),
14669 DispBlanksAs::Gap,
14670 super::GeometryOptions::default(),
14671 &mut fonts,
14672 &mut rendered,
14673 )
14674 .unwrap();
14675 let text: Vec<_> = rendered
14676 .iter()
14677 .filter_map(|element| match element {
14678 PositionedElement::Text(run) => Some(run.text.as_str()),
14679 _ => None,
14680 })
14681 .collect();
14682 assert_eq!(text, vec!["33.33%", "67%"]);
14683 }
14684
14685 #[test]
14686 fn core_chart_shells_preserve_unmodelled_children_byte_for_byte() {
14687 let preserved = [
14688 br#"<x:root x:id="1"><x:nested>one & two</x:nested><!--root--></x:root>"#
14689 .as_slice(),
14690 br#"<x:title x:id="2"><x:nested><![CDATA[title]]></x:nested></x:title>"#.as_slice(),
14691 br#"<x:plot x:id="3"><x:nested>plot</x:nested></x:plot>"#.as_slice(),
14692 br#"<x:legend x:id="4"><x:nested>legend</x:nested></x:legend>"#.as_slice(),
14693 br#"<x:extension x:id="5"><x:nested>extension</x:nested></x:extension>"#.as_slice(),
14694 br#"<!--space-comment-->"#.as_slice(),
14695 br#"<?space processing?>"#.as_slice(),
14696 br#"<!--chart-comment-->"#.as_slice(),
14697 br#"<?title processing?>"#.as_slice(),
14698 br#"<!--flag-comment-->"#.as_slice(),
14699 br#"<?flag processing?>"#.as_slice(),
14700 br#"<x:producerShape/>"#.as_slice(),
14701 br#"<x:producerText/>"#.as_slice(),
14702 ];
14703 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>"#;
14704 let parsed = CT_ChartSpace::from_xml(xml).unwrap();
14705 let written = parsed.to_xml().unwrap();
14706 for raw in preserved {
14707 assert!(
14708 written.windows(raw.len()).any(|window| window == raw),
14709 "preserved subtree changed: {}",
14710 String::from_utf8_lossy(raw)
14711 );
14712 }
14713 let written_text = String::from_utf8(written.clone()).unwrap();
14714 assert!(written_text.contains(r#"x:keep="one & two""#));
14715 assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
14716 }
14717
14718 #[test]
14719 fn every_corpus_chart_part_round_trips_structurally() {
14720 let Some(corpus) = require_or_skip_corpus() else {
14721 return;
14722 };
14723 verify_fetched_corpus(&corpus);
14724 let mut chart_parts = 0usize;
14725 let mut decks_with_charts = HashSet::new();
14726 for path in manifest_paths() {
14727 let package = OpcPackage::open(corpus.join(path))
14728 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14729 for (part, xml) in &package.parts {
14730 if !is_chart_part(part) {
14731 continue;
14732 }
14733 let parsed = CT_ChartSpace::from_xml(xml)
14734 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14735 let written = parsed
14736 .to_xml()
14737 .unwrap_or_else(|error| panic!("{path} {part}: write failed: {error}"));
14738 let original_preserved = preserved_chartml_regions(xml)
14739 .unwrap_or_else(|error| panic!("{path} {part}: original scan failed: {error}"));
14740 let written_preserved = preserved_chartml_regions(&written)
14741 .unwrap_or_else(|error| panic!("{path} {part}: written scan failed: {error}"));
14742 assert_eq!(
14743 original_preserved.len(),
14744 written_preserved.len(),
14745 "{path} {part}: preserved ChartML region count changed"
14746 );
14747 for (index, (original, written)) in original_preserved
14748 .iter()
14749 .zip(&written_preserved)
14750 .enumerate()
14751 {
14752 assert_eq!(
14753 original.parent_path, written.parent_path,
14754 "{path} {part}: preserved region {index} parent changed"
14755 );
14756 assert_eq!(
14757 original.boundary, written.boundary,
14758 "{path} {part}: preserved region {index} boundary changed"
14759 );
14760 assert_eq!(
14761 original.sibling_order, written.sibling_order,
14762 "{path} {part}: preserved region {index} order changed"
14763 );
14764 assert_eq!(
14765 original.xml, written.xml,
14766 "{path} {part}: preserved region {index} bytes changed"
14767 );
14768 }
14769 let reparsed = CT_ChartSpace::from_xml(&written)
14770 .unwrap_or_else(|error| panic!("{path} {part}: written parse failed: {error}"));
14771 assert_eq!(parsed, reparsed, "{path} {part}: core model changed");
14772 chart_parts += 1;
14773 decks_with_charts.insert(path);
14774 }
14775 }
14776 assert!(
14777 chart_parts > 0,
14778 "the pinned corpus contained no chart parts"
14779 );
14780 eprintln!(
14781 "ChartML corpus gate checked {chart_parts} chart parts across {} decks",
14782 decks_with_charts.len()
14783 );
14784 }
14785
14786 #[derive(Debug, Eq, PartialEq)]
14787 struct PreservedChartRegion {
14788 parent_path: Vec<Vec<u8>>,
14789 boundary: usize,
14790 sibling_order: usize,
14791 xml: Vec<u8>,
14792 }
14793
14794 struct PreservationFrame {
14795 local_name: Vec<u8>,
14796 boundary: usize,
14797 next_sibling: usize,
14798 }
14799
14800 fn preserved_chartml_regions(xml: &[u8]) -> Result<Vec<PreservedChartRegion>, String> {
14801 let mut reader = Reader::from_reader(xml);
14802 let mut buffer = Vec::new();
14803 let mut parents: Vec<PreservationFrame> = Vec::new();
14804 let mut preserved = Vec::new();
14805 loop {
14806 match reader.read_event_into(&mut buffer) {
14807 Ok(Event::Start(element)) => {
14808 let child = local_name(element.name().as_ref()).to_vec();
14809 if parents.is_empty() && child == b"chartSpace" {
14810 parents.push(PreservationFrame {
14811 local_name: child,
14812 boundary: 0,
14813 next_sibling: 0,
14814 });
14815 } else if let Some(parent) = parents.last()
14816 && is_opaque_chartml_child(&parent.local_name, &child)
14817 {
14818 let boundary =
14819 preserved_child_boundary(&parent.local_name, &child, parent.boundary);
14820 let xml = capture_element(&mut reader, &element)
14821 .map_err(|error| error.to_string())?;
14822 record_preserved_region(&mut parents, &mut preserved, boundary, xml);
14823 advance_preservation_boundary(&mut parents, &child);
14824 } else {
14825 advance_preservation_boundary(&mut parents, &child);
14826 if is_chartml_preservation_parent(&child) {
14827 parents.push(PreservationFrame {
14828 local_name: child,
14829 boundary: 0,
14830 next_sibling: 0,
14831 });
14832 } else {
14833 capture_element(&mut reader, &element)
14834 .map_err(|error| error.to_string())?;
14835 }
14836 }
14837 }
14838 Ok(Event::Empty(element)) => {
14839 let name = element.name();
14840 let child = local_name(name.as_ref()).to_vec();
14841 if let Some(parent) = parents.last()
14842 && is_opaque_chartml_child(&parent.local_name, &child)
14843 {
14844 let boundary =
14845 preserved_child_boundary(&parent.local_name, &child, parent.boundary);
14846 let xml =
14847 capture_empty_element(&element).map_err(|error| error.to_string())?;
14848 record_preserved_region(&mut parents, &mut preserved, boundary, xml);
14849 }
14850 advance_preservation_boundary(&mut parents, &child);
14851 }
14852 Ok(
14853 event @ (Event::Comment(_)
14854 | Event::PI(_)
14855 | Event::CData(_)
14856 | Event::GeneralRef(_)),
14857 ) if !parents.is_empty() => {
14858 let boundary = parents.last().expect("preservation parent").boundary;
14859 let xml = capture_event(event).map_err(|error| error.to_string())?;
14860 record_preserved_region(&mut parents, &mut preserved, boundary, xml);
14861 }
14862 Ok(Event::End(_)) => {
14863 parents.pop();
14864 }
14865 Ok(Event::Eof) => break,
14866 Ok(_) => {}
14867 Err(error) => return Err(error.to_string()),
14868 }
14869 buffer.clear();
14870 }
14871 Ok(preserved)
14872 }
14873
14874 fn record_preserved_region(
14875 parents: &mut [PreservationFrame],
14876 preserved: &mut Vec<PreservedChartRegion>,
14877 boundary: usize,
14878 xml: Vec<u8>,
14879 ) {
14880 let sibling_order = parents.last().expect("preservation parent").next_sibling;
14881 parents
14882 .last_mut()
14883 .expect("preservation parent")
14884 .next_sibling += 1;
14885 preserved.push(PreservedChartRegion {
14886 parent_path: parents
14887 .iter()
14888 .map(|parent| parent.local_name.clone())
14889 .collect(),
14890 boundary,
14891 sibling_order,
14892 xml,
14893 });
14894 }
14895
14896 fn advance_preservation_boundary(parents: &mut [PreservationFrame], child: &[u8]) {
14897 let Some(parent) = parents.last_mut() else {
14898 return;
14899 };
14900 parent.boundary = parent
14901 .boundary
14902 .max(preserved_boundary_after(&parent.local_name, child));
14903 }
14904
14905 fn preserved_child_boundary(parent: &[u8], child: &[u8], current: usize) -> usize {
14906 match parent {
14907 b"chartSpace" => match child {
14908 b"date1904" => 0,
14909 b"lang" => 1,
14910 b"roundedCorners" => 2,
14911 b"style" => 3,
14912 b"clrMapOvr" => 4,
14913 b"pivotSource" => 5,
14914 b"protection" => 6,
14915 b"externalData" => 10,
14916 b"printSettings" => 11,
14917 b"userShapes" => 12,
14918 b"extLst" => 13,
14919 _ => current,
14920 },
14921 b"chart" => match child {
14922 b"pivotFmts" => 2,
14923 b"view3D" => 3,
14924 b"floor" => 4,
14925 b"sideWall" => 5,
14926 b"backWall" => 6,
14927 b"showDLblsOverMax" | b"extLst" => 11,
14928 _ => current,
14929 },
14930 _ => current,
14931 }
14932 }
14933
14934 fn preserved_boundary_after(parent: &[u8], child: &[u8]) -> usize {
14935 match parent {
14936 b"chartSpace" => match child {
14937 b"date1904" => 1,
14938 b"lang" => 2,
14939 b"roundedCorners" => 3,
14940 b"style" => 4,
14941 b"clrMapOvr" => 5,
14942 b"pivotSource" => 6,
14943 b"protection" => 7,
14944 b"chart" => 8,
14945 b"spPr" => 9,
14946 b"txPr" => 10,
14947 b"externalData" => 11,
14948 b"printSettings" => 12,
14949 b"userShapes" => 13,
14950 b"extLst" => 14,
14951 _ => 0,
14952 },
14953 b"chart" => match child {
14954 b"title" => 1,
14955 b"autoTitleDeleted" => 2,
14956 b"pivotFmts" => 3,
14957 b"view3D" => 4,
14958 b"floor" => 5,
14959 b"sideWall" => 6,
14960 b"backWall" => 7,
14961 b"plotArea" => 8,
14962 b"legend" => 9,
14963 b"plotVisOnly" => 10,
14964 b"dispBlanksAs" => 11,
14965 b"showDLblsOverMax" | b"extLst" => 11,
14966 _ => 0,
14967 },
14968 b"pieChart" => match child {
14969 b"ser" => 1,
14970 b"dLbls" => 2,
14971 b"firstSliceAng" => 3,
14972 _ => 0,
14973 },
14974 b"doughnutChart" => match child {
14975 b"ser" => 1,
14976 b"dLbls" => 2,
14977 b"firstSliceAng" => 3,
14978 b"holeSize" => 4,
14979 _ => 0,
14980 },
14981 b"areaChart" => match child {
14982 b"grouping" => 1,
14983 b"ser" => 2,
14984 b"dLbls" => 3,
14985 b"axId" => 4,
14986 _ => 0,
14987 },
14988 b"scatterChart" | b"radarChart" => match child {
14989 b"scatterStyle" | b"radarStyle" => 1,
14990 b"ser" => 2,
14991 b"dLbls" => 3,
14992 b"axId" => 4,
14993 _ => 0,
14994 },
14995 _ => 0,
14996 }
14997 }
14998
14999 fn is_opaque_chartml_child(parent: &[u8], child: &[u8]) -> bool {
15000 match parent {
15001 b"chartSpace" => !matches!(child, b"chart" | b"spPr" | b"txPr"),
15002 b"chart" => !matches!(
15003 child,
15004 b"title"
15005 | b"autoTitleDeleted"
15006 | b"plotArea"
15007 | b"legend"
15008 | b"plotVisOnly"
15009 | b"dispBlanksAs"
15010 ),
15011 b"plotArea" => !matches!(
15012 child,
15013 b"barChart"
15014 | b"lineChart"
15015 | b"pieChart"
15016 | b"doughnutChart"
15017 | b"areaChart"
15018 | b"scatterChart"
15019 | b"radarChart"
15020 | b"catAx"
15021 | b"valAx"
15022 | b"dateAx"
15023 | b"serAx"
15024 ),
15025 b"pieChart" => !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng"),
15026 b"doughnutChart" => {
15027 !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng" | b"holeSize")
15028 }
15029 b"areaChart" => !matches!(child, b"grouping" | b"ser" | b"dLbls" | b"axId"),
15030 b"scatterChart" => !matches!(child, b"scatterStyle" | b"ser" | b"dLbls" | b"axId"),
15031 b"radarChart" => !matches!(child, b"radarStyle" | b"ser" | b"dLbls" | b"axId"),
15032 b"title" | b"legend" => true,
15033 _ => false,
15034 }
15035 }
15036
15037 fn is_chartml_preservation_parent(parent: &[u8]) -> bool {
15038 matches!(
15039 parent,
15040 b"chartSpace"
15041 | b"chart"
15042 | b"title"
15043 | b"plotArea"
15044 | b"pieChart"
15045 | b"doughnutChart"
15046 | b"areaChart"
15047 | b"scatterChart"
15048 | b"radarChart"
15049 | b"legend"
15050 | b"autoTitleDeleted"
15051 | b"plotVisOnly"
15052 | b"dispBlanksAs"
15053 )
15054 }
15055
15056 #[test]
15057 fn rpptx_chart_is_an_explicit_publication_candidate() {
15058 let manifest = include_str!("../Cargo.toml");
15059 let workspace = include_str!("../../../Cargo.toml");
15060 assert!(manifest.contains("name = \"rpptx-chart\""));
15061 assert!(manifest.contains("version = \"0.1.3\""));
15062 assert!(manifest.contains("publish = true"));
15063 for dependency in [
15064 "oxml-core.workspace",
15065 "oxml-drawing.workspace",
15066 "quick-xml.workspace",
15067 ] {
15068 assert!(manifest.contains(dependency));
15069 }
15070 assert!(!manifest.contains("rpptx.workspace"));
15071 assert!(!manifest.contains("rdocx.workspace"));
15072 assert!(workspace.contains("\"crates/rpptx-chart\""));
15073 assert!(
15074 workspace
15075 .contains("rpptx-chart = { path = \"crates/rpptx-chart\", version = \"0.1.3\" }")
15076 );
15077 }
15078
15079 fn first_series_xml(xml: &[u8]) -> Option<Vec<u8>> {
15080 let mut reader = Reader::from_reader(xml);
15081 let mut buffer = Vec::new();
15082 loop {
15083 match reader.read_event_into(&mut buffer).ok()? {
15084 Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
15085 return capture_element(&mut reader, &element).ok();
15086 }
15087 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
15088 return capture_empty_element(&element).ok();
15089 }
15090 Event::Eof => return None,
15091 _ => {}
15092 }
15093 buffer.clear();
15094 }
15095 }
15096
15097 fn replace_once(haystack: &[u8], needle: &[u8], replacement: &[u8]) -> Vec<u8> {
15098 let offset = haystack
15099 .windows(needle.len())
15100 .position(|window| window == needle)
15101 .expect("serialized source series occurs in chart part");
15102 assert!(
15103 !haystack[offset + needle.len()..]
15104 .windows(needle.len())
15105 .any(|window| window == needle),
15106 "source series unexpectedly occurs more than once"
15107 );
15108 let mut result = Vec::with_capacity(haystack.len() - needle.len() + replacement.len());
15109 result.extend_from_slice(&haystack[..offset]);
15110 result.extend_from_slice(replacement);
15111 result.extend_from_slice(&haystack[offset + needle.len()..]);
15112 result
15113 }
15114
15115 fn assert_command_version(program: &str, arguments: &[&str], expected: &str) {
15116 let output = Command::new(program)
15117 .args(arguments)
15118 .output()
15119 .unwrap_or_else(|error| panic!("run {program} version check: {error}"));
15120 assert!(
15121 output.status.success(),
15122 "{program} version check failed: {}",
15123 String::from_utf8_lossy(&output.stderr)
15124 );
15125 let stdout = String::from_utf8(output.stdout).expect("version stdout is UTF-8");
15126 let stderr = String::from_utf8(output.stderr).expect("version stderr is UTF-8");
15127 let actual = if stdout.trim().is_empty() {
15128 stderr.trim()
15129 } else {
15130 stdout.trim()
15131 };
15132 assert_eq!(
15133 actual.lines().next(),
15134 Some(expected),
15135 "{program} version drift"
15136 );
15137 }
15138
15139 fn sha256(path: &Path) -> String {
15140 let output = Command::new("shasum")
15141 .args(["-a", "256"])
15142 .arg(path)
15143 .output()
15144 .unwrap_or_else(|error| panic!("{}: run shasum: {error}", path.display()));
15145 assert!(
15146 output.status.success(),
15147 "{}: shasum failed: {}",
15148 path.display(),
15149 String::from_utf8_lossy(&output.stderr)
15150 );
15151 String::from_utf8(output.stdout)
15152 .expect("shasum output is UTF-8")
15153 .split_whitespace()
15154 .next()
15155 .expect("shasum digest")
15156 .to_owned()
15157 }
15158
15159 fn workspace_root() -> PathBuf {
15160 Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
15161 }
15162
15163 fn corpus_dir() -> PathBuf {
15164 std::env::var_os("RDOCX_PPTX_CORPUS_DIR")
15165 .map(PathBuf::from)
15166 .unwrap_or_else(|| workspace_root().join("corpus/pptx"))
15167 }
15168
15169 fn require_or_skip_corpus() -> Option<PathBuf> {
15170 let corpus = corpus_dir();
15171 if corpus.is_dir() {
15172 return Some(corpus);
15173 }
15174 assert_ne!(
15175 std::env::var_os("RDOCX_PPTX_CORPUS_REQUIRED").as_deref(),
15176 Some(std::ffi::OsStr::new("1")),
15177 "the pinned corpus is required but {} does not exist",
15178 corpus.display()
15179 );
15180 eprintln!(
15181 "ChartML corpus gate skipped because {} is absent",
15182 corpus.display()
15183 );
15184 None
15185 }
15186
15187 fn verify_fetched_corpus(corpus: &Path) {
15188 let status = Command::new("python3")
15189 .arg(workspace_root().join("scripts/fetch_pptx_corpus.py"))
15190 .arg("--check")
15191 .env("RDOCX_PPTX_CORPUS_DIR", corpus)
15192 .status()
15193 .expect("run corpus verifier");
15194 assert!(status.success(), "pinned corpus verification failed");
15195 }
15196
15197 fn manifest_paths() -> Vec<&'static str> {
15198 let mut lines = MANIFEST.lines();
15199 assert_eq!(lines.next(), Some("path\tproducer\tsha256\turl"));
15200 let paths: Vec<_> = lines
15201 .map(|line| line.split('\t').next().expect("manifest path"))
15202 .collect();
15203 assert_eq!(paths.len(), EXPECTED_DECKS);
15204 paths
15205 }
15206
15207 fn is_chart_part(part: &str) -> bool {
15208 let Some(stem) = part
15209 .strip_prefix("/ppt/charts/chart")
15210 .and_then(|name| name.strip_suffix(".xml"))
15211 else {
15212 return false;
15213 };
15214 !stem.is_empty() && stem.bytes().all(|byte| byte.is_ascii_digit())
15215 }
15216
15217 fn plot_series(index: u32) -> String {
15218 format!(
15219 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>"#
15220 )
15221 }
15222
15223 fn sparse_category_series() -> &'static str {
15224 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>"#
15225 }
15226
15227 fn sparse_scatter_series() -> &'static str {
15228 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>"#
15229 }
15230
15231 fn chart_with_plot(plot: &str) -> String {
15232 format!(
15233 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>"#
15234 )
15235 }
15236
15237 fn chart_with_optional_axes(plot: &str, axes: bool) -> String {
15238 let axis_xml = if axes {
15239 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>"#
15240 } else {
15241 ""
15242 };
15243 format!(
15244 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>"#
15245 )
15246 }
15247
15248 fn numeric_plot_series(index: u32) -> String {
15249 format!(
15250 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>"#
15251 )
15252 }
15253
15254 fn scatter_without_x(index: u32) -> String {
15255 format!(
15256 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>"#
15257 )
15258 }
15259
15260 fn scatter_without_y(index: u32) -> String {
15261 format!(
15262 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>"#
15263 )
15264 }
15265
15266 fn remaining_plot_fixtures() -> Vec<(&'static str, String, bool)> {
15267 let series = plot_series(0);
15268 let scatter = numeric_plot_series(0);
15269 vec![
15270 (
15271 "pieChart",
15272 format!(
15273 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>"#
15274 ),
15275 false,
15276 ),
15277 (
15278 "doughnutChart",
15279 format!(
15280 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>"#
15281 ),
15282 false,
15283 ),
15284 (
15285 "areaChart",
15286 format!(
15287 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>"#
15288 ),
15289 true,
15290 ),
15291 (
15292 "scatterChart",
15293 format!(
15294 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>"#
15295 ),
15296 true,
15297 ),
15298 (
15299 "radarChart",
15300 format!(
15301 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>"#
15302 ),
15303 true,
15304 ),
15305 ]
15306 }
15307
15308 fn bar_plot(extra: &str) -> String {
15309 format!(
15310 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>"#,
15311 plot_series(0)
15312 )
15313 }
15314
15315 fn line_plot(extra: &str) -> String {
15316 format!(
15317 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>"#,
15318 plot_series(0)
15319 )
15320 }
15321
15322 #[test]
15323 fn bar_and_line_plots_round_trip_and_render() {
15324 for (xml, is_bar) in [
15325 (chart_with_plot(&bar_plot("")), true),
15326 (chart_with_plot(&line_plot("")), false),
15327 ] {
15328 let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15329 let plots = parsed.chart.plot_area.plots().unwrap();
15330 assert_eq!(plots.len(), 1);
15331 assert_eq!(matches!(plots[0], Plot::Bar { .. }), is_bar);
15332 assert_eq!(matches!(plots[0], Plot::Line { .. }), !is_bar);
15333 assert_eq!(parsed.chart.plot_area.axes().unwrap().len(), 2);
15334 let written = parsed.to_xml().unwrap();
15335 let reparsed = CT_ChartSpace::from_xml(&written).unwrap();
15336 assert_eq!(parsed, reparsed);
15337 }
15338 if let Some(corpus) = require_or_skip_corpus() {
15339 verify_bar_and_line_viewer_gate(&corpus);
15340 }
15341 }
15342
15343 #[test]
15344 fn ppm_parser_preserves_whitespace_valued_first_pixels() {
15345 for first_pixel in *b" \n\r\t" {
15346 let mut ppm = b"P6\n1 1\n255\n".to_vec();
15347 ppm.extend_from_slice(&[first_pixel, 0x7f, 0xff]);
15348 let (width, height, pixels) = ppm_pixels(&ppm);
15349 assert_eq!((width, height), (1, 1));
15350 assert_eq!(pixels, [first_pixel, 0x7f, 0xff]);
15351 }
15352
15353 let ppm = b"P6\r\n1 1\r\n255\r\n\x7f\xff";
15354 assert_eq!(ppm_pixels(ppm).2, [b'\n', 0x7f, 0xff]);
15355 }
15356
15357 #[test]
15358 fn bar_and_line_plots_write_fixed_prefixes_in_schema_order() {
15359 for xml in [
15360 chart_with_plot(&bar_plot(r#"<q:extLst><x:bar-tail/></q:extLst>"#)),
15361 chart_with_plot(&line_plot(r#"<q:extLst><x:line-tail/></q:extLst>"#)),
15362 ] {
15363 let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15364 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15365 assert!(written.contains("<c:barChart") || written.contains("<c:lineChart"));
15366 assert!(!written.contains("<q:barChart"));
15367 assert!(!written.contains("<q:lineChart"));
15368 if written.contains("<c:barChart") {
15369 let positions: Vec<_> = [
15370 "<c:barDir",
15371 "<c:grouping",
15372 "<q:varyColors",
15373 "<c:ser",
15374 "<c:dLbls",
15375 "<c:gapWidth",
15376 "<c:overlap",
15377 "<q:serLines",
15378 "<c:axId",
15379 "<q:extLst",
15380 ]
15381 .iter()
15382 .map(|tag| written.find(tag).unwrap())
15383 .collect();
15384 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15385 } else {
15386 let positions: Vec<_> = [
15387 "<c:grouping",
15388 "<q:varyColors",
15389 "<c:ser",
15390 "<c:dLbls",
15391 "<q:dropLines",
15392 "<c:marker",
15393 "<c:smooth",
15394 "<c:axId",
15395 "<q:extLst",
15396 ]
15397 .iter()
15398 .map(|tag| written.find(tag).unwrap())
15399 .collect();
15400 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15401 }
15402 assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
15403 }
15404
15405 for plot in [bar_plot(""), line_plot("")] {
15406 let first = plot_series(0);
15407 let with_series_boundary = plot.replacen(
15408 &first,
15409 &format!(
15410 "{first}<!--between-series--><?series-boundary?>\n {}",
15411 plot_series(1)
15412 ),
15413 1,
15414 );
15415 let with_axis_boundary = with_series_boundary.replacen(
15416 r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
15417 r#"<q:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
15418 <q:axId val="-1884097184"/>"#,
15419 1,
15420 );
15421 let parsed =
15422 CT_ChartSpace::from_xml(chart_with_plot(&with_axis_boundary).as_bytes()).unwrap();
15423 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15424 assert!(written.contains("</c:ser><!--between-series--><?series-boundary?>\n <c:ser"));
15425 assert!(written.contains(
15426 r#"<c:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
15427 <c:axId val="-1884097184"/>"#
15428 ));
15429 assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
15430 }
15431 }
15432
15433 #[test]
15434 fn malformed_bar_and_line_plots_return_errors_without_panicking() {
15435 let valid_series = plot_series(0);
15436 let cases = [
15437 format!(r#"<q:barChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15438 format!(r#"<q:barChart><q:barDir val="diagonal"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15439 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>"#),
15440 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>"#),
15441 format!(r#"<q:lineChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
15442 format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:marker val="maybe"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
15443 r#"<q:lineChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#.to_owned(),
15444 format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:axId val="1"/><q:axId val="1"/></q:lineChart>"#),
15445 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>"#),
15446 format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15447 ];
15448 for plot in cases {
15449 let xml = chart_with_plot(&plot);
15450 let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
15451 assert!(result.is_ok(), "plot parser panicked for {plot}");
15452 assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
15453 }
15454
15455 let missing_axis = format!(
15456 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>"#,
15457 bar_plot("")
15458 );
15459 assert!(CT_ChartSpace::from_xml(missing_axis.as_bytes()).is_err());
15460
15461 let values =
15462 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
15463 let series = vec![Series::new(0, 0, values)];
15464 let ids = [AxisId::new(1).unwrap(), AxisId::new(2).unwrap()];
15465 let mut invalid =
15466 Plot::bar(BarDirection::Column, BarGrouping::Clustered, series, ids).unwrap();
15467 if let Plot::Bar { gap_width, .. } = &mut invalid {
15468 *gap_width = 501;
15469 }
15470 assert!(invalid.validate().is_err());
15471 }
15472
15473 #[test]
15474 fn unsupported_and_combo_plots_remain_byte_preserved() {
15475 let bar = bar_plot("");
15476 let line = line_plot("");
15477 let combo = chart_with_plot(&format!("<!--before-->{bar}<?between?>{line}<!--after-->"));
15478 let parsed = CT_ChartSpace::from_xml(combo.as_bytes()).unwrap();
15479 assert!(parsed.chart.plot_area.plots().is_err());
15480 let written = parsed.to_xml().unwrap();
15481 for raw in [
15482 bar.as_bytes(),
15483 line.as_bytes(),
15484 br#"<!--before-->"#.as_slice(),
15485 br#"<?between?>"#.as_slice(),
15486 br#"<!--after-->"#.as_slice(),
15487 ] {
15488 assert!(written.windows(raw.len()).any(|window| window == raw));
15489 }
15490
15491 let unsupported = chart_with_plot(
15492 r#"<q:bar3DChart x:keep="three"><q:barDir val="col"/><q:ser><x:opaque/></q:ser></q:bar3DChart>"#,
15493 );
15494 let parsed = CT_ChartSpace::from_xml(unsupported.as_bytes()).unwrap();
15495 assert!(parsed.chart.plot_area.plots().is_err());
15496 assert!(
15497 String::from_utf8(parsed.to_xml().unwrap())
15498 .unwrap()
15499 .contains(r#"<q:bar3DChart x:keep="three">"#)
15500 );
15501 }
15502
15503 #[test]
15504 fn public_plot_edits_preserve_axes_and_unselected_payloads() {
15505 let xml = chart_with_plot(&bar_plot(
15506 r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#,
15507 ));
15508 let mut parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15509 let axes_before = parsed.chart.plot_area.axes().unwrap();
15510 match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
15511 Plot::Bar {
15512 gap_width,
15513 overlap,
15514 series,
15515 ..
15516 } => {
15517 *gap_width = 225;
15518 *overlap = -25;
15519 series[0].index = 7;
15520 series[0].values.values[0] = 9.0;
15521 }
15522 _ => panic!("expected bar plot"),
15523 }
15524 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15525 assert!(written.contains(r#"<c:gapWidth val="225"/>"#));
15526 assert!(written.contains(r#"<c:overlap val="-25"/>"#));
15527 assert!(written.contains("<c:v>9</c:v>"));
15528 assert!(written.contains("<q:varyColors val=\"1\"/>"));
15529 assert!(written.find("<q:varyColors").unwrap() < written.find("<c:ser").unwrap());
15530 assert!(written.contains("<q:serLines><x:line/></q:serLines>"));
15531 assert!(written.contains(r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#));
15532 let reparsed = CT_ChartSpace::from_xml(written.as_bytes()).unwrap();
15533 assert_eq!(axes_before, reparsed.chart.plot_area.axes().unwrap());
15534
15535 let first = plot_series(0);
15536 let colliding_series =
15537 bar_plot("").replacen(&first, &format!("{first}{}", plot_series(1)), 1);
15538 let mut collision =
15539 CT_ChartSpace::from_xml(chart_with_plot(&colliding_series).as_bytes()).unwrap();
15540 if let Plot::Bar { series, .. } = &mut collision.chart.plot_area.plots_mut().unwrap()[0] {
15541 series[0].index = series[1].index;
15542 series[0].order = series[1].order;
15543 }
15544 let collision = String::from_utf8(collision.to_xml().unwrap()).unwrap();
15545 assert!(collision.find("<q:varyColors").unwrap() < collision.find("<c:ser").unwrap());
15546
15547 let mut inserted = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15548 if let Plot::Bar { series, .. } = &mut inserted.chart.plot_area.plots_mut().unwrap()[0] {
15549 let mut new_series = series[0].clone();
15550 new_series.index = 99;
15551 new_series.order = 99;
15552 series.insert(0, new_series);
15553 }
15554 let inserted = String::from_utf8(inserted.to_xml().unwrap()).unwrap();
15555 assert!(inserted.find("<q:varyColors").unwrap() < inserted.find("<c:ser").unwrap());
15556
15557 let axis_payload = bar_plot("").replacen(
15558 r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
15559 r#"<q:axId val="-1884094432" x:slot="category"/><!--axis-anchor--><q:axId val="-1884097184" x:slot="value"/>"#,
15560 1,
15561 );
15562 let mut swapped =
15563 CT_ChartSpace::from_xml(chart_with_plot(&axis_payload).as_bytes()).unwrap();
15564 if let Plot::Bar { axis_ids, .. } = &mut swapped.chart.plot_area.plots_mut().unwrap()[0] {
15565 axis_ids.swap(0, 1);
15566 }
15567 let swapped = String::from_utf8(swapped.to_xml().unwrap()).unwrap();
15568 let anchor = swapped.find("<!--axis-anchor-->").unwrap();
15569 let value_axis = swapped
15570 .find(r#"<c:axId val="-1884097184" x:slot="value"/>"#)
15571 .unwrap();
15572 let category_axis = swapped
15573 .find(r#"<c:axId val="-1884094432" x:slot="category"/>"#)
15574 .unwrap();
15575 let series_lines = swapped.find("<q:serLines").unwrap();
15576 assert!(series_lines < value_axis && series_lines < category_axis);
15577 assert!(anchor < value_axis && value_axis < category_axis);
15578
15579 let mut replaced = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15580 let Plot::Bar {
15581 series, axis_ids, ..
15582 } = &replaced.chart.plot_area.plots().unwrap()[0]
15583 else {
15584 panic!("expected bar plot");
15585 };
15586 let line = Plot::line(Grouping::Standard, series.clone(), *axis_ids).unwrap();
15587 replaced.chart.plot_area.plots_mut().unwrap()[0] = line;
15588 assert!(replaced.to_xml().is_err());
15589 }
15590
15591 #[test]
15592 fn every_corpus_bar_and_line_plot_round_trips_structurally() {
15593 let Some(corpus) = require_or_skip_corpus() else {
15594 return;
15595 };
15596 verify_fetched_corpus(&corpus);
15597 let mut typed_bar_count = 0usize;
15598 let mut typed_line_count = 0usize;
15599 let mut opaque_bar_count = 0usize;
15600 let mut opaque_line_count = 0usize;
15601 let mut opaque_combo_count = 0usize;
15602 for path in manifest_paths() {
15603 let package = OpcPackage::open(corpus.join(path))
15604 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
15605 for (part, xml) in &package.parts {
15606 if !is_chart_part(part) {
15607 continue;
15608 }
15609 let chart = CT_ChartSpace::from_xml(xml)
15610 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
15611 match chart.chart.plot_area.plots() {
15612 Ok(plots) => {
15613 for plot in plots {
15614 match plot {
15615 Plot::Bar { .. } => typed_bar_count += 1,
15616 Plot::Line { .. } => typed_line_count += 1,
15617 _ => {}
15618 }
15619 }
15620 }
15621 Err(_) => {
15622 let mut preserved_plot_count = 0usize;
15623 for raw in chart.chart.plot_area.raw_children.at(0) {
15624 if let Ok(Some(local)) = super::chart_root_local(
15625 raw,
15626 &chart.chart.plot_area.namespace_bindings,
15627 ) {
15628 match local.as_slice() {
15629 b"barChart" => {
15630 opaque_bar_count += 1;
15631 preserved_plot_count += 1;
15632 }
15633 b"lineChart" => {
15634 opaque_line_count += 1;
15635 preserved_plot_count += 1;
15636 }
15637 _ => {}
15638 }
15639 }
15640 }
15641 if preserved_plot_count > 1 {
15642 opaque_combo_count += 1;
15643 }
15644 }
15645 }
15646 let written = chart
15647 .to_xml()
15648 .unwrap_or_else(|error| panic!("{path} {part}: chart write failed: {error}"));
15649 let reparsed = CT_ChartSpace::from_xml(&written).unwrap_or_else(|error| {
15650 panic!("{path} {part}: written chart parse failed: {error}")
15651 });
15652 assert_eq!(chart, reparsed, "{path} {part}: chart model changed");
15653 }
15654 }
15655 assert_eq!(typed_bar_count, 11, "typed corpus bar coverage changed");
15656 assert_eq!(typed_line_count, 2, "typed corpus line coverage changed");
15657 assert_eq!(opaque_bar_count, 1, "preserved combo bar coverage changed");
15658 assert_eq!(
15659 opaque_line_count, 1,
15660 "preserved combo line coverage changed"
15661 );
15662 assert_eq!(opaque_combo_count, 1, "preserved combo coverage changed");
15663 assert_eq!(typed_bar_count + opaque_bar_count, 12);
15664 assert_eq!(typed_line_count + opaque_line_count, 3);
15665 eprintln!(
15666 "ChartML plot corpus gate checked {typed_bar_count} typed bar, {typed_line_count} typed line, and one preserved bar-line combination"
15667 );
15668 }
15669
15670 fn verify_remaining_plot_viewer_gate(corpus: &Path) {
15671 verify_fetched_corpus(corpus);
15672 assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
15673 assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
15674 let source = corpus.join("bar-chart.pptx");
15675 let source_sha = sha256(&source);
15676 let temp_root = std::env::temp_dir().join(format!(
15677 "rpptx-chart-f122-viewer-gate-{}",
15678 std::process::id()
15679 ));
15680 if temp_root.exists() {
15681 fs::remove_dir_all(&temp_root).expect("remove stale F-122 evidence");
15682 }
15683 fs::create_dir_all(&temp_root).expect("create F-122 evidence root");
15684
15685 for (kind, plot, axes) in remaining_plot_fixtures() {
15686 let mut package = OpcPackage::open(&source).expect("open F-122 viewer source");
15687 package.set_part(
15688 "/ppt/charts/chart1.xml",
15689 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes())
15690 .expect("parse F-122 viewer chart")
15691 .to_xml()
15692 .expect("serialize F-122 viewer chart"),
15693 );
15694 let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
15695 package.save(&unbound).expect("save F-122 viewer candidate");
15696 let candidate_sha = sha256(&unbound);
15697 let evidence = temp_root.join(format!("{source_sha}-{candidate_sha}"));
15698 fs::create_dir(&evidence).expect("create SHA-bound F-122 evidence directory");
15699 let candidate = evidence.join(format!("{kind}-candidate.pptx"));
15700 fs::rename(&unbound, &candidate).expect("bind F-122 candidate to SHA");
15701 assert_eq!(sha256(&candidate), candidate_sha);
15702 let render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
15703 let bytes = fs::read(&render).expect("read F-122 viewer PPM");
15704 let (width, height, pixels) = ppm_pixels(&bytes);
15705 let left = width / 5;
15706 let right = width * 4 / 5;
15707 let top = height * 3 / 20;
15708 let bottom = height * 17 / 20;
15709 let mut nonblank = 0usize;
15710 for y in top..bottom {
15711 for x in left..right {
15712 let offset = (y * width + x) * 3;
15713 if pixels[offset..offset + 3]
15714 .iter()
15715 .any(|channel| *channel < 245)
15716 {
15717 nonblank += 1;
15718 }
15719 }
15720 }
15721 const CHART_RECTANGLE_NONBLANK_THRESHOLD: usize = 1_000;
15722 assert!(
15723 nonblank >= CHART_RECTANGLE_NONBLANK_THRESHOLD,
15724 "{kind}: chart rectangle [{left},{top}) to [{right},{bottom}) contains {nonblank} nonblank pixels, below {CHART_RECTANGLE_NONBLANK_THRESHOLD}"
15725 );
15726 eprintln!(
15727 "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}",
15728 sha256(&render)
15729 );
15730 }
15731 fs::remove_dir_all(&temp_root).expect("remove F-122 viewer evidence");
15732 }
15733
15734 fn verify_bar_and_line_viewer_gate(corpus: &Path) {
15735 verify_fetched_corpus(corpus);
15736 assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
15737 assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
15738 let temp_root = std::env::temp_dir().join(format!(
15739 "rpptx-chart-f121-viewer-gate-{}",
15740 std::process::id()
15741 ));
15742 if temp_root.exists() {
15743 fs::remove_dir_all(&temp_root).expect("remove stale F-121 evidence");
15744 }
15745 fs::create_dir_all(&temp_root).expect("create F-121 evidence root");
15746
15747 for (kind, deck) in [("bar", "bar-chart.pptx"), ("line", "line-chart.pptx")] {
15748 let source = corpus.join(deck);
15749 let original_sha = sha256(&source);
15750 let mut package = OpcPackage::open(&source)
15751 .unwrap_or_else(|error| panic!("{deck}: open viewer source: {error}"));
15752 let chart_part = "/ppt/charts/chart1.xml";
15753 let original_parts = package.parts.clone();
15754 let parsed = CT_ChartSpace::from_xml(
15755 package
15756 .get_part(chart_part)
15757 .unwrap_or_else(|| panic!("{deck}: missing {chart_part}")),
15758 )
15759 .unwrap_or_else(|error| panic!("{deck}: parse viewer chart: {error}"));
15760 assert!(
15761 parsed.chart.plot_area.plots().is_ok(),
15762 "{deck}: plot is opaque"
15763 );
15764 package.set_part(
15765 chart_part,
15766 parsed
15767 .to_xml()
15768 .unwrap_or_else(|error| panic!("{deck}: serialize viewer chart: {error}")),
15769 );
15770 for (part, bytes) in &original_parts {
15771 if part == chart_part {
15772 assert_ne!(package.parts[part], *bytes, "{deck}: chart did not rewrite");
15773 } else {
15774 assert_eq!(package.parts[part], *bytes, "{deck}: changed part {part}");
15775 }
15776 }
15777
15778 let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
15779 package
15780 .save(&unbound)
15781 .unwrap_or_else(|error| panic!("{deck}: save viewer candidate: {error}"));
15782 let candidate_sha = sha256(&unbound);
15783 let evidence = temp_root.join(format!("{original_sha}-{candidate_sha}"));
15784 fs::create_dir(&evidence).expect("create SHA-bound F-121 evidence directory");
15785 let original = evidence.join(format!("{kind}-original.pptx"));
15786 let candidate = evidence.join(format!("{kind}-candidate.pptx"));
15787 fs::copy(&source, &original).expect("copy SHA-bound viewer original");
15788 fs::rename(&unbound, &candidate).expect("bind viewer candidate to SHA");
15789 assert_eq!(sha256(&original), original_sha);
15790 assert_eq!(sha256(&candidate), candidate_sha);
15791
15792 let original_render = render_deck_to_ppm(&original, &evidence, kind, "original");
15793 let candidate_render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
15794 let original_bytes = fs::read(&original_render).expect("read original PPM");
15795 let candidate_bytes = fs::read(&candidate_render).expect("read candidate PPM");
15796 let normalized_mae = normalized_ppm_mae(&original_bytes, &candidate_bytes);
15797 assert!(
15798 normalized_mae <= PLOT_RENDER_NORMALIZED_MAE_THRESHOLD,
15799 "{deck}: normalized RGB MAE {normalized_mae:.8} exceeds {:.8}",
15800 PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
15801 );
15802 eprintln!(
15803 "F-121 {kind} viewer gate original deck {original_sha}, candidate deck {candidate_sha}, original render {}, candidate render {}, normalized RGB MAE {normalized_mae:.8} <= {:.8}",
15804 sha256(&original_render),
15805 sha256(&candidate_render),
15806 PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
15807 );
15808 }
15809 fs::remove_dir_all(&temp_root).expect("remove F-121 viewer evidence");
15810 }
15811
15812 fn render_deck_to_ppm(deck: &Path, evidence: &Path, kind: &str, side: &str) -> PathBuf {
15813 let profile = std::env::temp_dir().join(format!(
15814 "rpptx-chart-f121-{kind}-{side}-profile-{}",
15815 std::process::id()
15816 ));
15817 if profile.exists() {
15818 fs::remove_dir_all(&profile).expect("remove stale F-121 viewer profile");
15819 }
15820 let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
15821 let conversion = Command::new("soffice")
15822 .args([
15823 "--headless",
15824 &profile_argument,
15825 "--convert-to",
15826 "pdf:impress_pdf_Export",
15827 "--outdir",
15828 ])
15829 .arg(evidence)
15830 .arg(deck)
15831 .output()
15832 .expect("run pinned LibreOffice F-121 viewer gate");
15833 assert!(
15834 conversion.status.success(),
15835 "{}: LibreOffice conversion failed: {}",
15836 deck.display(),
15837 String::from_utf8_lossy(&conversion.stderr)
15838 );
15839 let pdf = deck.with_extension("pdf");
15840 assert!(
15841 pdf.is_file(),
15842 "LibreOffice did not create {}",
15843 pdf.display()
15844 );
15845 let prefix = evidence.join(format!("{kind}-{side}"));
15846 let raster = Command::new("pdftoppm")
15847 .args(["-f", "1", "-singlefile", "-r", "150"])
15848 .arg(&pdf)
15849 .arg(&prefix)
15850 .output()
15851 .expect("run pinned Poppler F-121 raster gate");
15852 assert!(
15853 raster.status.success(),
15854 "{}: pdftoppm failed: {}",
15855 pdf.display(),
15856 String::from_utf8_lossy(&raster.stderr)
15857 );
15858 if profile.exists() {
15859 fs::remove_dir_all(&profile).expect("remove F-121 viewer profile");
15860 }
15861 prefix.with_extension("ppm")
15862 }
15863
15864 fn normalized_ppm_mae(left: &[u8], right: &[u8]) -> f64 {
15865 let (left_width, left_height, left_pixels) = ppm_pixels(left);
15866 let (right_width, right_height, right_pixels) = ppm_pixels(right);
15867 assert_eq!((left_width, left_height), (right_width, right_height));
15868 assert_eq!(left_pixels.len(), right_pixels.len());
15869 let difference: u64 = left_pixels
15870 .iter()
15871 .zip(right_pixels)
15872 .map(|(left, right)| u64::from(left.abs_diff(*right)))
15873 .sum();
15874 difference as f64 / (left_pixels.len() as f64 * 255.0)
15875 }
15876
15877 fn ppm_pixels(bytes: &[u8]) -> (usize, usize, &[u8]) {
15878 let mut cursor = 0usize;
15879 let mut tokens = Vec::new();
15880 while tokens.len() < 4 {
15881 while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
15882 cursor += 1;
15883 }
15884 if cursor < bytes.len() && bytes[cursor] == b'#' {
15885 while cursor < bytes.len() && bytes[cursor] != b'\n' {
15886 cursor += 1;
15887 }
15888 continue;
15889 }
15890 let start = cursor;
15891 while cursor < bytes.len() && !bytes[cursor].is_ascii_whitespace() {
15892 cursor += 1;
15893 }
15894 tokens.push(std::str::from_utf8(&bytes[start..cursor]).expect("PPM header UTF-8"));
15895 }
15896 assert_eq!(tokens[0], "P6");
15897 let width = tokens[1].parse::<usize>().expect("PPM width");
15898 let height = tokens[2].parse::<usize>().expect("PPM height");
15899 assert_eq!(tokens[3], "255");
15900 assert!(
15901 cursor < bytes.len() && bytes[cursor].is_ascii_whitespace(),
15902 "PPM header must end with whitespace"
15903 );
15904 cursor += 1;
15905 let pixels = &bytes[cursor..];
15906 assert_eq!(pixels.len(), width * height * 3, "PPM pixel length");
15907 (width, height, pixels)
15908 }
15909}