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, ThemeColorSlot, apply_color_transforms,
13 resolve_color,
14};
15use oxml_drawing::fill::{Fill, SolidFill};
16use oxml_drawing::line::CT_LineProperties;
17use oxml_drawing::order::OrderedRawChildren;
18use oxml_drawing::shape_props::{CT_ShapeProperties, ShapePropertiesError};
19use oxml_drawing::text::{CT_TextBody, CT_TextCharacterProperties, TextError};
20use oxml_drawing::theme::CT_OfficeStyleSheet;
21use oxml_layout::{
22 Color, FillRule, FontManager, GlyphRun, GroupElement, Paint, Path, PathCommand, PathElement,
23 Point, PositionedElement, Rect, Stroke, Transform,
24};
25use oxml_sml::{Column, Workbook};
26use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
27use quick_xml::{Reader, Writer, XmlVersion};
28
29pub use oxml_drawing::color::RgbColor;
30
31pub const C_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/chart";
33pub const C_PREFIX: &str = "c";
35pub const A_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
37pub const R_NS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
39const MAX_CATEGORY_ANNOTATIONS: usize = 16_384;
40const PLOT_MARGIN_LEFT: f64 = 36.0;
41const PLOT_MARGIN_RIGHT: f64 = 12.0;
42const PLOT_MARGIN_TOP: f64 = 12.0;
43const PLOT_MARGIN_BOTTOM: f64 = 28.0;
44
45#[derive(Debug)]
47pub enum ChartError {
48 Xml(OxmlError),
49 ShapeProperties(ShapePropertiesError),
50 Text(TextError),
51 UnexpectedElement(String),
52 MissingElement(String),
53 DuplicateElement(String),
54 InvalidAttribute {
55 element: String,
56 attribute: String,
57 value: String,
58 },
59 InvalidValue {
60 element: String,
61 value: String,
62 },
63}
64
65impl fmt::Display for ChartError {
66 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67 match self {
68 Self::Xml(error) => error.fmt(formatter),
69 Self::ShapeProperties(error) => error.fmt(formatter),
70 Self::Text(error) => error.fmt(formatter),
71 Self::UnexpectedElement(element) => {
72 write!(formatter, "unexpected ChartML element: {element}")
73 }
74 Self::MissingElement(element) => {
75 write!(formatter, "ChartML requires {element}")
76 }
77 Self::DuplicateElement(element) => {
78 write!(formatter, "ChartML contains duplicate {element}")
79 }
80 Self::InvalidAttribute {
81 element,
82 attribute,
83 value,
84 } => write!(
85 formatter,
86 "ChartML {element} has invalid @{attribute}: {value}"
87 ),
88 Self::InvalidValue { element, value } => {
89 write!(formatter, "ChartML {element} has invalid value: {value}")
90 }
91 }
92 }
93}
94
95impl std::error::Error for ChartError {
96 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
97 match self {
98 Self::Xml(error) => Some(error),
99 Self::ShapeProperties(error) => Some(error),
100 Self::Text(error) => Some(error),
101 _ => None,
102 }
103 }
104}
105
106impl From<OxmlError> for ChartError {
107 fn from(error: OxmlError) -> Self {
108 Self::Xml(error)
109 }
110}
111
112impl From<ShapePropertiesError> for ChartError {
113 fn from(error: ShapePropertiesError) -> Self {
114 Self::ShapeProperties(error)
115 }
116}
117
118impl From<TextError> for ChartError {
119 fn from(error: TextError) -> Self {
120 Self::Text(error)
121 }
122}
123
124pub type Result<T> = std::result::Result<T, ChartError>;
125
126#[derive(Clone, Copy, Debug, Eq, PartialEq)]
128pub enum ChartKind {
129 Bar,
130 Line,
131 Pie,
132 Doughnut,
133 Area,
134 Scatter,
135 Radar,
136}
137
138#[derive(Clone, Debug, Default, PartialEq)]
140pub struct ChartData {
141 pub categories: Vec<String>,
142 pub series: Vec<(String, Vec<f64>)>,
143 pub number_format: Option<String>,
144 pub category_axis_title: Option<String>,
145 pub value_axis_title: Option<String>,
146 pub palette: Vec<RgbColor>,
147}
148
149pub fn authored_chart_parts(
151 kind: ChartKind,
152 data: &ChartData,
153 workbook_relationship_id: &str,
154) -> Result<(Vec<u8>, Vec<u8>)> {
155 validate_chart_data(kind, data)?;
156 let category_column = if kind == ChartKind::Scatter {
157 Column::Number {
158 header: "Category".to_owned(),
159 values: data
160 .categories
161 .iter()
162 .map(|value| value.parse::<f64>().expect("validated scatter category"))
163 .collect(),
164 number_format: None,
165 }
166 } else {
167 Column::Text {
168 header: "Category".to_owned(),
169 values: data.categories.clone(),
170 }
171 };
172 let mut columns = Vec::with_capacity(data.series.len() + 1);
173 columns.push(category_column);
174 columns.extend(data.series.iter().map(|(name, values)| Column::Number {
175 header: name.clone(),
176 values: values.clone(),
177 number_format: data.number_format.clone(),
178 }));
179 let workbook = Workbook::new("Sheet1", columns).map_err(invalid_authoring_value)?;
180 let category_formula = workbook
181 .formula_range(0)
182 .expect("validated categories produce a formula range");
183 let number_format = data.number_format.as_deref().unwrap_or("General");
184 let mut chart_series = Vec::with_capacity(data.series.len());
185 for (index, (name, values)) in data.series.iter().enumerate() {
186 let values_formula = workbook
187 .formula_range(index + 1)
188 .expect("validated series produces a formula range");
189 let values = NumericData::new(values_formula, number_format.to_owned(), values.clone())
190 .map_err(invalid_authoring_value)?;
191 let mut series = Series::new(index as u32, index as u32, values);
192 series.name = Some(
193 StringRef::new(
194 format!("Sheet1!${}$1", spreadsheet_column_name(index + 1)),
195 vec![name.clone()],
196 )
197 .map_err(invalid_authoring_value)?,
198 );
199 series.categories = Some(if kind == ChartKind::Scatter {
200 AxisData::Numeric(
201 NumericData::new(
202 category_formula.clone(),
203 "General".to_owned(),
204 data.categories
205 .iter()
206 .map(|value| value.parse::<f64>().expect("validated scatter category"))
207 .collect(),
208 )
209 .map_err(invalid_authoring_value)?,
210 )
211 } else {
212 AxisData::String(
213 StringRef::new(category_formula.clone(), data.categories.clone())
214 .map_err(invalid_authoring_value)?,
215 )
216 });
217 chart_series.push(series);
218 }
219 apply_palette(kind, data, &mut chart_series);
220
221 let category_axis_id = AxisId::new(48_650_112).map_err(invalid_authoring_value)?;
222 let value_axis_id = AxisId::new(48_672_768).map_err(invalid_authoring_value)?;
223 let axis_ids = [category_axis_id, value_axis_id];
224 let (plot, axes) = match kind {
225 ChartKind::Bar => (
226 Plot::bar(
227 BarDirection::Column,
228 BarGrouping::Clustered,
229 chart_series,
230 axis_ids,
231 ),
232 category_value_axes(category_axis_id, value_axis_id, data)?,
233 ),
234 ChartKind::Line => (
235 Plot::line(Grouping::Standard, chart_series, axis_ids),
236 category_value_axes(category_axis_id, value_axis_id, data)?,
237 ),
238 ChartKind::Pie => (Plot::pie(chart_series), Vec::new()),
239 ChartKind::Doughnut => (Plot::doughnut(chart_series), Vec::new()),
240 ChartKind::Area => (
241 Plot::area(Grouping::Standard, chart_series, axis_ids),
242 category_value_axes(category_axis_id, value_axis_id, data)?,
243 ),
244 ChartKind::Scatter => (
245 Plot::scatter(ScatterStyle::LineMarker, chart_series, axis_ids),
246 authored_axes(
247 [
248 Axis::new(
249 AxisKind::Value,
250 category_axis_id,
251 AxisPosition::Bottom,
252 value_axis_id,
253 ),
254 Axis::new(
255 AxisKind::Value,
256 value_axis_id,
257 AxisPosition::Left,
258 category_axis_id,
259 ),
260 ],
261 data,
262 )?,
263 ),
264 ChartKind::Radar => (
265 Plot::radar(RadarStyle::Standard, chart_series, axis_ids),
266 category_value_axes(category_axis_id, value_axis_id, data)?,
267 ),
268 };
269 let mut plot = plot.map_err(invalid_authoring_value)?;
270 if let Plot::Pie { data_labels, .. } | Plot::Doughnut { data_labels, .. } = &mut plot {
271 *data_labels = Some(CT_DLbls {
272 show_percent: true,
273 ..CT_DLbls::default()
274 });
275 }
276 let plot_area = CT_PlotArea::new(vec![plot], axes).map_err(invalid_authoring_value)?;
277 let chart_shell = format!(
278 r#"<c:chartSpace xmlns:c="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}"><c:chart><c:plotArea/></c:chart><c:externalData r:id="{workbook_relationship_id}"><c:autoUpdate val="0"/></c:externalData></c:chartSpace>"#,
279 );
280 let mut chart_space = CT_ChartSpace::from_xml(chart_shell.as_bytes())?;
281 chart_space.chart.auto_title_deleted = true;
282 chart_space.chart.plot_area = plot_area;
283 chart_space.chart.legend = (data.series.len() > 1
284 || matches!(kind, ChartKind::Pie | ChartKind::Doughnut))
285 .then(CT_Legend::default);
286 let chart_xml = chart_space.to_xml()?;
287 let workbook_bytes = workbook.to_xlsx_bytes().map_err(invalid_authoring_value)?;
288 Ok((chart_xml, workbook_bytes))
289}
290
291fn validate_chart_data(kind: ChartKind, data: &ChartData) -> Result<()> {
292 if data.categories.is_empty() {
293 return Err(invalid_authoring_value("at least one category is required"));
294 }
295 if data.series.is_empty() {
296 return Err(invalid_authoring_value("at least one series is required"));
297 }
298 if matches!(kind, ChartKind::Pie | ChartKind::Doughnut) && data.series.len() != 1 {
299 return Err(invalid_authoring_value(
300 "pie and doughnut charts require exactly one series",
301 ));
302 }
303 for (name, values) in &data.series {
304 if values.len() != data.categories.len() {
305 return Err(invalid_authoring_value(format!(
306 "series {name:?} has {} values for {} categories",
307 values.len(),
308 data.categories.len()
309 )));
310 }
311 if values.iter().any(|value| !value.is_finite()) {
312 return Err(invalid_authoring_value(format!(
313 "series {name:?} contains a nonfinite value"
314 )));
315 }
316 }
317 if kind == ChartKind::Scatter {
318 for category in &data.categories {
319 if category
320 .parse::<f64>()
321 .ok()
322 .is_none_or(|value| !value.is_finite())
323 {
324 return Err(invalid_authoring_value(format!(
325 "scatter category {category:?} is not a finite number"
326 )));
327 }
328 }
329 }
330 if let Some(format) = &data.number_format {
331 NumericData::new("Sheet1!$A$2".to_owned(), format.clone(), vec![0.0])?;
332 }
333 Ok(())
334}
335
336fn invalid_authoring_value(error: impl fmt::Display) -> ChartError {
337 ChartError::InvalidValue {
338 element: "chart authoring".to_owned(),
339 value: error.to_string(),
340 }
341}
342
343fn category_value_axes(category: AxisId, value: AxisId, data: &ChartData) -> Result<Vec<Axis>> {
344 authored_axes(
345 [
346 Axis::new(AxisKind::Category, category, AxisPosition::Bottom, value),
347 Axis::new(AxisKind::Value, value, AxisPosition::Left, category),
348 ],
349 data,
350 )
351}
352
353fn authored_axes(mut axes: [Axis; 2], data: &ChartData) -> Result<Vec<Axis>> {
354 for axis in &mut axes {
355 axis.set_deleted(false);
356 }
357 axes[0].title = data
358 .category_axis_title
359 .as_deref()
360 .map(CT_Title::plain_text);
361 axes[1].title = data.value_axis_title.as_deref().map(CT_Title::plain_text);
362 axes[1].number_format = data
363 .number_format
364 .as_ref()
365 .map(|format| NumberFormat::new(format.clone(), false))
366 .transpose()?;
367 Ok(axes.into())
368}
369
370fn apply_palette(kind: ChartKind, data: &ChartData, series: &mut [Series]) {
371 if data.palette.is_empty() {
372 return;
373 }
374 for (index, item) in series.iter_mut().enumerate() {
375 item.sp_pr = Some(series_shape(kind, data.palette[index % data.palette.len()]));
376 }
377 if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) && series.len() == 1 {
378 series[0].authored_data_points = data
379 .categories
380 .iter()
381 .enumerate()
382 .map(|(index, _)| DataPoint {
383 index: index as u32,
384 sp_pr: series_shape(kind, data.palette[index % data.palette.len()]),
385 })
386 .collect();
387 }
388}
389
390fn series_shape(kind: ChartKind, color: RgbColor) -> CT_ShapeProperties {
391 let mut solid = SolidFill::default();
392 solid.color = Some(ColorChoice::srgb(color));
393 let fill = Fill::Solid(solid);
394 let mut properties = CT_ShapeProperties::default();
395 if matches!(
396 kind,
397 ChartKind::Line | ChartKind::Scatter | ChartKind::Radar
398 ) {
399 let mut line = CT_LineProperties::default();
400 line.fill = Some(fill);
401 properties.line = Some(line);
402 } else {
403 properties.fill = Some(fill);
404 }
405 properties
406}
407
408fn spreadsheet_column_name(mut index: usize) -> String {
409 let mut name = Vec::new();
410 index += 1;
411 while index != 0 {
412 let remainder = (index - 1) % 26;
413 name.push((b'A' + remainder as u8) as char);
414 index = (index - 1) / 26;
415 }
416 name.iter().rev().collect()
417}
418
419#[derive(Clone, Debug)]
421pub struct ChartGeometry {
422 pub plot_bounds: Rect,
423 pub elements: Vec<PositionedElement>,
424}
425
426#[derive(Clone, Debug)]
427struct AxisScale {
428 domain: Domain,
429 ticks: Vec<f64>,
430}
431
432#[derive(Clone, Copy, Debug)]
433struct AxisMapping {
434 domain: Domain,
435 orientation: Orientation,
436}
437
438#[derive(Clone, Copy, Debug, Default)]
439struct GeometryOptions {
440 category_orientation: Orientation,
441 value: Option<AxisMapping>,
442 scatter_x: Option<AxisMapping>,
443 scatter_y: Option<AxisMapping>,
444}
445
446#[derive(Default)]
447struct ChartAnnotations {
448 gridlines: Vec<PositionedElement>,
449 axis_lines: Vec<PositionedElement>,
450 ticks: Vec<PositionedElement>,
451 labels: Vec<PositionedElement>,
452}
453
454pub fn render_chart(
456 chart: &CT_Chart,
457 bounds: Rect,
458 theme: &CT_OfficeStyleSheet,
459 color_map: &ColorMap,
460 fonts: &mut FontManager,
461) -> Result<GroupElement> {
462 let plot_bounds = geometry_plot_bounds(bounds)?;
463 let plots = chart.plot_area.plots()?;
464 let plot = plots
465 .first()
466 .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
467 plot.validate()?;
468 let series_colours = resolve_series_colours(plot, theme, color_map)?;
469 let axes = chart.plot_area.axes()?;
470 let options = geometry_options(plot, &axes, chart.disp_blanks_as)?;
471 let plot_children = render_plot_geometry(
472 plot,
473 plot_bounds,
474 chart.disp_blanks_as,
475 options,
476 &series_colours,
477 )?;
478 if plot_children.is_empty() && !plot_can_render_without_marks(plot) {
479 return Err(ChartError::InvalidValue {
480 element: "c:plotArea".to_owned(),
481 value: "plot has no renderable cached data".to_owned(),
482 });
483 }
484
485 let mut chart_annotations =
486 render_axes_and_labels(plot, &axes, plot_bounds, chart.disp_blanks_as, fonts)?;
487 let mut annotations = Vec::new();
488 render_data_labels(
489 plot,
490 plot_bounds,
491 chart.disp_blanks_as,
492 options,
493 fonts,
494 &mut chart_annotations.labels,
495 )?;
496 render_legend(
497 chart,
498 plot,
499 plot_bounds,
500 fonts,
501 &series_colours,
502 &mut annotations,
503 &mut chart_annotations.labels,
504 )?;
505
506 validate_geometry_coordinates(&chart_annotations.gridlines)?;
507 validate_geometry_coordinates(&plot_children)?;
508 validate_geometry_coordinates(&chart_annotations.axis_lines)?;
509 validate_geometry_coordinates(&chart_annotations.ticks)?;
510 validate_geometry_coordinates(&annotations)?;
511 let mut children = chart_annotations.gridlines;
512 children.push(PositionedElement::Group(GroupElement {
513 transform: Transform::IDENTITY,
514 clip: Some(Path::rect(plot_bounds)),
515 opacity: 1.0,
516 effects: Vec::new(),
517 children: plot_children,
518 }));
519 children.extend(chart_annotations.axis_lines);
520 children.extend(chart_annotations.ticks);
521 children.extend(annotations);
522 children.extend(chart_annotations.labels);
523 Ok(GroupElement {
524 transform: Transform::IDENTITY,
525 clip: None,
526 opacity: 1.0,
527 effects: Vec::new(),
528 children,
529 })
530}
531
532pub fn render_chart_placeholder(bounds: Rect, fonts: &mut FontManager) -> Result<GroupElement> {
534 if !bounds.width.is_finite()
535 || !bounds.height.is_finite()
536 || bounds.width <= 0.0
537 || bounds.height <= 0.0
538 {
539 return Err(ChartError::InvalidValue {
540 element: "chart fallback bounds".to_owned(),
541 value: "width and height must be finite and positive".to_owned(),
542 });
543 }
544 let label = shape_label(
545 fonts,
546 "Unsupported chart",
547 Point {
548 x: 4.0,
549 y: (bounds.height / 2.0).max(9.0),
550 },
551 )?;
552 Ok(GroupElement {
553 transform: Transform::IDENTITY,
554 clip: Some(Path::rect(Rect {
555 x: 0.0,
556 y: 0.0,
557 width: bounds.width,
558 height: bounds.height,
559 })),
560 opacity: 1.0,
561 effects: Vec::new(),
562 children: vec![PositionedElement::Text(label)],
563 })
564}
565
566pub fn render_geometry(
568 chart: &CT_Chart,
569 bounds: Rect,
570 theme: &CT_OfficeStyleSheet,
571 color_map: &ColorMap,
572) -> Result<ChartGeometry> {
573 let plot_bounds = geometry_plot_bounds(bounds)?;
574 let plots = chart.plot_area.plots()?;
575 let plot = plots
576 .first()
577 .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
578 plot.validate()?;
579 let series_colours = resolve_series_colours(plot, theme, color_map)?;
580 let children = render_plot_geometry(
581 plot,
582 plot_bounds,
583 chart.disp_blanks_as,
584 geometry_options(plot, &[], chart.disp_blanks_as)?,
585 &series_colours,
586 )?;
587 if children.is_empty() && !plot_can_render_without_marks(plot) {
588 return Err(ChartError::InvalidValue {
589 element: "c:plotArea".to_owned(),
590 value: "plot has no renderable cached data".to_owned(),
591 });
592 }
593 validate_geometry_coordinates(&children)?;
594 Ok(ChartGeometry {
595 plot_bounds,
596 elements: vec![PositionedElement::Group(GroupElement {
597 transform: Transform::IDENTITY,
598 clip: None,
599 opacity: 1.0,
600 effects: Vec::new(),
601 children,
602 })],
603 })
604}
605
606fn plot_can_render_without_marks(plot: &Plot) -> bool {
607 matches!(
608 plot,
609 Plot::Radar { series, .. }
610 if series
611 .iter()
612 .any(|series| !series.values.values.is_empty())
613 )
614}
615
616fn geometry_plot_bounds(bounds: Rect) -> Result<Rect> {
617 if ![bounds.x, bounds.y, bounds.width, bounds.height]
618 .into_iter()
619 .all(f64::is_finite)
620 || bounds.width <= 0.0
621 || bounds.height <= 0.0
622 || !(bounds.x + bounds.width).is_finite()
623 || !(bounds.y + bounds.height).is_finite()
624 {
625 return Err(ChartError::InvalidValue {
626 element: "chart geometry bounds".to_owned(),
627 value: format!(
628 "x={}, y={}, width={}, height={}",
629 bounds.x, bounds.y, bounds.width, bounds.height
630 ),
631 });
632 }
633 let plot_bounds = Rect {
634 x: bounds.x + PLOT_MARGIN_LEFT,
635 y: bounds.y + PLOT_MARGIN_TOP,
636 width: bounds.width - PLOT_MARGIN_LEFT - PLOT_MARGIN_RIGHT,
637 height: bounds.height - PLOT_MARGIN_TOP - PLOT_MARGIN_BOTTOM,
638 };
639 if ![
640 plot_bounds.x,
641 plot_bounds.y,
642 plot_bounds.width,
643 plot_bounds.height,
644 plot_bounds.x + plot_bounds.width,
645 plot_bounds.y + plot_bounds.height,
646 ]
647 .into_iter()
648 .all(f64::is_finite)
649 || plot_bounds.width <= 0.0
650 || plot_bounds.height <= 0.0
651 {
652 return Err(ChartError::InvalidValue {
653 element: "chart geometry bounds".to_owned(),
654 value: "chart is too small for the fixed plot margins".to_owned(),
655 });
656 }
657 Ok(plot_bounds)
658}
659
660fn geometry_options(plot: &Plot, axes: &[Axis], blanks: DispBlanksAs) -> Result<GeometryOptions> {
661 let mut options = GeometryOptions::default();
662 if let Some(category_axis) = axes.iter().find(|axis| axis.kind != AxisKind::Value) {
663 options.category_orientation = category_axis.scaling.orientation;
664 }
665
666 if let Plot::Scatter { axis_ids, .. } = plot {
667 let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
668 if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[0]) {
669 options.scatter_x = Some(axis_mapping(axis, x_domain)?);
670 }
671 if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[1]) {
672 options.scatter_y = Some(axis_mapping(axis, y_domain)?);
673 }
674 } else if let Some(domain) = plot_value_domain(plot, blanks)? {
675 options.value = Some(
676 axes.iter()
677 .find(|axis| axis.kind == AxisKind::Value)
678 .map(|axis| axis_mapping(axis, domain))
679 .transpose()?
680 .unwrap_or(AxisMapping {
681 domain,
682 orientation: Orientation::MinMax,
683 }),
684 );
685 }
686 Ok(options)
687}
688
689fn axis_mapping(axis: &Axis, extent: Domain) -> Result<AxisMapping> {
690 if axis.scaling.log_base.is_some() {
691 return Err(ChartError::InvalidValue {
692 element: "c:scaling/c:logBase".to_owned(),
693 value: "native chart rendering currently supports linear axes".to_owned(),
694 });
695 }
696 let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
697 Ok(AxisMapping {
698 domain: scale.domain,
699 orientation: axis.scaling.orientation,
700 })
701}
702
703fn plot_value_domain(plot: &Plot, blanks: DispBlanksAs) -> Result<Option<Domain>> {
704 let domain = match plot {
705 Plot::Bar {
706 grouping, series, ..
707 } => {
708 let logical = logical_series_values(series)?;
709 let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
710 let layers =
711 stacked_series_bounds(&logical, stacked, *grouping == BarGrouping::PercentStacked)?;
712 domain_from_layers(&layers)?
713 }
714 Plot::Line {
715 grouping, series, ..
716 } => {
717 let logical = logical_series_values(series)?;
718 if *grouping == Grouping::Standard {
719 let values = if blanks == DispBlanksAs::Zero {
720 zero_filled_series(&logical, series_category_count(series)?)
721 .into_iter()
722 .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
723 .collect::<Vec<_>>()
724 } else {
725 logical
726 .into_iter()
727 .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
728 .collect::<Vec<_>>()
729 };
730 data_domain(&values, false)?
731 } else {
732 let layers =
733 stacked_series_bounds(&logical, true, *grouping == Grouping::PercentStacked)?;
734 domain_from_layers(&layers)?
735 }
736 }
737 Plot::Area {
738 grouping, series, ..
739 } => {
740 let logical = logical_series_values(series)?;
741 let layers = stacked_series_bounds(
742 &logical,
743 *grouping != Grouping::Standard,
744 *grouping == Grouping::PercentStacked,
745 )?;
746 domain_from_layers(&layers)?
747 }
748 Plot::Radar { series, .. } => {
749 let values = series
750 .iter()
751 .flat_map(|series| series.values.values.iter().copied())
752 .collect::<Vec<_>>();
753 data_domain(&values, true)?
754 }
755 Plot::Pie { .. } | Plot::Doughnut { .. } | Plot::Scatter { .. } => return Ok(None),
756 };
757 Ok(Some(domain))
758}
759
760fn scatter_domains(plot: &Plot, blanks: DispBlanksAs) -> Result<(Domain, Domain)> {
761 let Plot::Scatter { series, .. } = plot else {
762 return Err(ChartError::UnexpectedElement("non-scatter plot".to_owned()));
763 };
764 let mut x_values = Vec::new();
765 let mut y_values = Vec::new();
766 for series in series {
767 for (_, x, y) in scatter_render_points(series, blanks)? {
768 x_values.push(x);
769 y_values.push(y);
770 }
771 }
772 Ok((
773 data_domain(&x_values, false)?,
774 data_domain(&y_values, false)?,
775 ))
776}
777
778fn render_axes_and_labels(
779 plot: &Plot,
780 axes: &[Axis],
781 bounds: Rect,
782 blanks: DispBlanksAs,
783 fonts: &mut FontManager,
784) -> Result<ChartAnnotations> {
785 if matches!(plot, Plot::Radar { .. }) {
786 return render_radar_axes_and_labels(plot, axes, bounds, blanks, fonts);
787 }
788 let mut annotations = ChartAnnotations::default();
789 let series = plot_series(plot);
790 let category_count = if series.is_empty() {
791 1
792 } else {
793 series_category_count(series)?
794 };
795 for axis in axes {
796 if axis.deleted {
797 continue;
798 }
799 axis_line(&mut annotations.axis_lines, bounds, axis.position);
800 if axis.kind == AxisKind::Value {
801 let extent = axis_extent(plot, axis, blanks)?;
802 let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
803 for tick in &scale.ticks {
804 let coordinate = axis_tick_coordinate(
805 *tick,
806 scale.domain,
807 bounds,
808 axis.position,
809 axis.scaling.orientation,
810 )?;
811 if axis.major_gridlines.is_some() {
812 major_gridline(
813 &mut annotations.gridlines,
814 bounds,
815 axis.position,
816 coordinate,
817 );
818 }
819 axis_tick(
820 &mut annotations.ticks,
821 bounds,
822 axis.position,
823 coordinate,
824 axis.major_tick_mark,
825 );
826 if axis.tick_label_position != TickLabelPosition::None {
827 let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
828 let origin = tick_label_origin(
829 bounds,
830 axis.position,
831 axis.tick_label_position,
832 coordinate,
833 );
834 annotations
835 .labels
836 .push(PositionedElement::Text(shape_label_with_properties(
837 fonts,
838 &text,
839 origin,
840 axis.tx_pr.as_ref(),
841 )?));
842 }
843 }
844 } else {
845 let count = category_count;
846 if count > MAX_CATEGORY_ANNOTATIONS {
847 return Err(ChartError::InvalidValue {
848 element: "c:catAx annotations".to_owned(),
849 value: format!(
850 "logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"
851 ),
852 });
853 }
854 let category_labels: BTreeMap<_, _> =
855 if axis.tick_label_position != TickLabelPosition::None {
856 series
857 .first()
858 .map(|series| series_categories(series, axis.number_format.as_ref()))
859 .transpose()?
860 .unwrap_or_default()
861 .into_iter()
862 .collect()
863 } else {
864 BTreeMap::new()
865 };
866 for logical_index in 0..count {
867 let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
868 let fraction = (index as f64 + 0.5) / count as f64;
869 let coordinate =
870 if matches!(axis.position, AxisPosition::Bottom | AxisPosition::Top) {
871 bounds.x + fraction * bounds.width
872 } else {
873 bounds.y + fraction * bounds.height
874 };
875 if axis.major_gridlines.is_some() {
876 major_gridline(
877 &mut annotations.gridlines,
878 bounds,
879 axis.position,
880 coordinate,
881 );
882 }
883 axis_tick(
884 &mut annotations.ticks,
885 bounds,
886 axis.position,
887 coordinate,
888 axis.major_tick_mark,
889 );
890 if axis.tick_label_position != TickLabelPosition::None
891 && let Some(text) = category_labels.get(&logical_index)
892 {
893 let origin = tick_label_origin(
894 bounds,
895 axis.position,
896 axis.tick_label_position,
897 coordinate,
898 );
899 annotations
900 .labels
901 .push(PositionedElement::Text(shape_label_with_properties(
902 fonts,
903 text,
904 origin,
905 axis.tx_pr.as_ref(),
906 )?));
907 }
908 }
909 }
910 }
911 Ok(annotations)
912}
913
914fn render_radar_axes_and_labels(
915 plot: &Plot,
916 axes: &[Axis],
917 bounds: Rect,
918 blanks: DispBlanksAs,
919 fonts: &mut FontManager,
920) -> Result<ChartAnnotations> {
921 let mut annotations = ChartAnnotations::default();
922 let series = plot_series(plot);
923 let count = series_category_count(series)?;
924 if count > MAX_CATEGORY_ANNOTATIONS {
925 return Err(ChartError::InvalidValue {
926 element: "c:radarChart annotations".to_owned(),
927 value: format!("logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"),
928 });
929 }
930 let center = Point {
931 x: bounds.x + bounds.width / 2.0,
932 y: bounds.y + bounds.height / 2.0,
933 };
934 let radius = bounds.width.min(bounds.height) / 2.0;
935 for axis in axes {
936 if axis.deleted {
937 continue;
938 }
939 if axis.kind == AxisKind::Value {
940 let scale = nice_number_scale(
941 axis_extent(plot, axis, blanks)?,
942 axis.scaling.minimum,
943 axis.scaling.maximum,
944 6,
945 )?;
946 annotations.axis_lines.push(stroked_segment(
947 center,
948 radial_point(center, radius, 0, count),
949 Color::BLACK,
950 1.0,
951 ));
952 for tick in &scale.ticks {
953 let distance =
954 normalized_value_oriented(*tick, scale.domain, axis.scaling.orientation)?
955 * radius;
956 if axis.major_gridlines.is_some() && distance > 0.0 {
957 let points: Vec<_> = (0..count)
958 .map(|index| radial_point(center, distance, index, count))
959 .collect();
960 annotations
961 .gridlines
962 .push(PositionedElement::Path(PathElement {
963 path: polyline_path(&points, true),
964 fill: None,
965 stroke: Some(Stroke::new(
966 Paint::Solid(Color {
967 r: 0.82,
968 g: 0.82,
969 b: 0.82,
970 a: 1.0,
971 }),
972 0.5,
973 )),
974 }));
975 }
976 radar_value_tick(
977 &mut annotations.ticks,
978 center,
979 distance,
980 axis.major_tick_mark,
981 );
982 let origin = match axis.tick_label_position {
983 TickLabelPosition::High => Some(Point {
984 x: center.x + radius + 4.0,
985 y: center.y - distance + 3.0,
986 }),
987 TickLabelPosition::Low => Some(Point {
988 x: center.x - radius - 30.0,
989 y: center.y - distance + 3.0,
990 }),
991 TickLabelPosition::NextTo => Some(Point {
992 x: center.x + 4.0,
993 y: center.y - distance + 3.0,
994 }),
995 TickLabelPosition::None => None,
996 };
997 if let Some(origin) = origin {
998 let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
999 annotations
1000 .labels
1001 .push(PositionedElement::Text(shape_label_with_properties(
1002 fonts,
1003 &text,
1004 origin,
1005 axis.tx_pr.as_ref(),
1006 )?));
1007 }
1008 }
1009 } else {
1010 let category_labels: BTreeMap<_, _> =
1011 if axis.tick_label_position != TickLabelPosition::None {
1012 series
1013 .first()
1014 .map(|series| series_categories(series, axis.number_format.as_ref()))
1015 .transpose()?
1016 .unwrap_or_default()
1017 .into_iter()
1018 .collect()
1019 } else {
1020 BTreeMap::new()
1021 };
1022 for logical_index in 0..count {
1023 let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
1024 let end = radial_point(center, radius, index, count);
1025 annotations
1026 .axis_lines
1027 .push(stroked_segment(center, end, Color::BLACK, 1.0));
1028 radar_category_tick(
1029 &mut annotations.ticks,
1030 center,
1031 radius,
1032 index,
1033 count,
1034 axis.major_tick_mark,
1035 );
1036 if let Some(text) = category_labels.get(&logical_index) {
1037 let Some(origin) = radar_category_label_origin(
1038 bounds,
1039 center,
1040 radius,
1041 index,
1042 count,
1043 axis.tick_label_position,
1044 ) else {
1045 continue;
1046 };
1047 annotations
1048 .labels
1049 .push(PositionedElement::Text(shape_label_with_properties(
1050 fonts,
1051 text,
1052 origin,
1053 axis.tx_pr.as_ref(),
1054 )?));
1055 }
1056 }
1057 }
1058 }
1059 Ok(annotations)
1060}
1061
1062fn radar_category_label_origin(
1063 bounds: Rect,
1064 center: Point,
1065 radius: f64,
1066 index: usize,
1067 count: usize,
1068 position: TickLabelPosition,
1069) -> Option<Point> {
1070 let label_radius = match position {
1071 TickLabelPosition::High => radius + 18.0,
1072 TickLabelPosition::Low => (radius - 10.0).max(0.0),
1073 TickLabelPosition::NextTo => radius + 10.0,
1074 TickLabelPosition::None => return None,
1075 };
1076 let label = radial_point(center, label_radius, index, count);
1077 let mut origin = Point {
1078 x: label.x - 4.0,
1079 y: label.y + 3.0,
1080 };
1081 if position == TickLabelPosition::High {
1082 origin.x = origin.x.clamp(
1083 bounds.x - PLOT_MARGIN_LEFT,
1084 bounds.x + bounds.width + PLOT_MARGIN_RIGHT,
1085 );
1086 origin.y = origin.y.clamp(
1087 bounds.y - PLOT_MARGIN_TOP,
1088 bounds.y + bounds.height + PLOT_MARGIN_BOTTOM,
1089 );
1090 }
1091 Some(origin)
1092}
1093
1094fn radar_value_tick(
1095 elements: &mut Vec<PositionedElement>,
1096 center: Point,
1097 distance: f64,
1098 mark: TickMark,
1099) {
1100 let (left, right) = match mark {
1101 TickMark::None => return,
1102 TickMark::Inside => (4.0, 0.0),
1103 TickMark::Outside => (0.0, 4.0),
1104 TickMark::Cross => (2.0, 2.0),
1105 };
1106 let y = center.y - distance;
1107 elements.push(stroked_segment(
1108 Point {
1109 x: center.x - left,
1110 y,
1111 },
1112 Point {
1113 x: center.x + right,
1114 y,
1115 },
1116 Color::BLACK,
1117 1.0,
1118 ));
1119}
1120
1121fn radar_category_tick(
1122 elements: &mut Vec<PositionedElement>,
1123 center: Point,
1124 radius: f64,
1125 index: usize,
1126 count: usize,
1127 mark: TickMark,
1128) {
1129 let (inside, outside) = match mark {
1130 TickMark::None => return,
1131 TickMark::Inside => (4.0, 0.0),
1132 TickMark::Outside => (0.0, 4.0),
1133 TickMark::Cross => (2.0, 2.0),
1134 };
1135 elements.push(stroked_segment(
1136 radial_point(center, radius - inside, index, count),
1137 radial_point(center, radius + outside, index, count),
1138 Color::BLACK,
1139 1.0,
1140 ));
1141}
1142
1143fn axis_extent(plot: &Plot, axis: &Axis, blanks: DispBlanksAs) -> Result<Domain> {
1144 if let Plot::Scatter { axis_ids, .. } = plot {
1145 let (x, y) = scatter_domains(plot, blanks)?;
1146 return Ok(if axis.id == axis_ids[0] { x } else { y });
1147 }
1148 plot_value_domain(plot, blanks)?.ok_or_else(|| ChartError::InvalidValue {
1149 element: "chart axis".to_owned(),
1150 value: "axis-free plot supplied an axis".to_owned(),
1151 })
1152}
1153
1154fn axis_tick_coordinate(
1155 value: f64,
1156 domain: Domain,
1157 bounds: Rect,
1158 position: AxisPosition,
1159 orientation: Orientation,
1160) -> Result<f64> {
1161 if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
1162 map_x_oriented(value, domain, bounds, orientation)
1163 } else {
1164 map_y_oriented(value, domain, bounds, orientation)
1165 }
1166}
1167
1168fn axis_line(elements: &mut Vec<PositionedElement>, bounds: Rect, position: AxisPosition) {
1169 let (start, end) = match position {
1170 AxisPosition::Bottom => (
1171 Point {
1172 x: bounds.x,
1173 y: bounds.y + bounds.height,
1174 },
1175 Point {
1176 x: bounds.x + bounds.width,
1177 y: bounds.y + bounds.height,
1178 },
1179 ),
1180 AxisPosition::Top => (
1181 Point {
1182 x: bounds.x,
1183 y: bounds.y,
1184 },
1185 Point {
1186 x: bounds.x + bounds.width,
1187 y: bounds.y,
1188 },
1189 ),
1190 AxisPosition::Left => (
1191 Point {
1192 x: bounds.x,
1193 y: bounds.y,
1194 },
1195 Point {
1196 x: bounds.x,
1197 y: bounds.y + bounds.height,
1198 },
1199 ),
1200 AxisPosition::Right => (
1201 Point {
1202 x: bounds.x + bounds.width,
1203 y: bounds.y,
1204 },
1205 Point {
1206 x: bounds.x + bounds.width,
1207 y: bounds.y + bounds.height,
1208 },
1209 ),
1210 };
1211 elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
1212}
1213
1214fn major_gridline(
1215 elements: &mut Vec<PositionedElement>,
1216 bounds: Rect,
1217 position: AxisPosition,
1218 coordinate: f64,
1219) {
1220 let (start, end) = if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
1221 (
1222 Point {
1223 x: coordinate,
1224 y: bounds.y,
1225 },
1226 Point {
1227 x: coordinate,
1228 y: bounds.y + bounds.height,
1229 },
1230 )
1231 } else {
1232 (
1233 Point {
1234 x: bounds.x,
1235 y: coordinate,
1236 },
1237 Point {
1238 x: bounds.x + bounds.width,
1239 y: coordinate,
1240 },
1241 )
1242 };
1243 elements.push(stroked_segment(
1244 start,
1245 end,
1246 Color {
1247 r: 0.82,
1248 g: 0.82,
1249 b: 0.82,
1250 a: 1.0,
1251 },
1252 0.5,
1253 ));
1254}
1255
1256fn axis_tick(
1257 elements: &mut Vec<PositionedElement>,
1258 bounds: Rect,
1259 position: AxisPosition,
1260 coordinate: f64,
1261 mark: TickMark,
1262) {
1263 let (inside, outside) = match mark {
1264 TickMark::None => return,
1265 TickMark::Inside => (4.0, 0.0),
1266 TickMark::Outside => (0.0, 4.0),
1267 TickMark::Cross => (2.0, 2.0),
1268 };
1269 let (start, end) = match position {
1270 AxisPosition::Bottom => (
1271 Point {
1272 x: coordinate,
1273 y: bounds.y + bounds.height - inside,
1274 },
1275 Point {
1276 x: coordinate,
1277 y: bounds.y + bounds.height + outside,
1278 },
1279 ),
1280 AxisPosition::Top => (
1281 Point {
1282 x: coordinate,
1283 y: bounds.y + inside,
1284 },
1285 Point {
1286 x: coordinate,
1287 y: bounds.y - outside,
1288 },
1289 ),
1290 AxisPosition::Left => (
1291 Point {
1292 x: bounds.x + inside,
1293 y: coordinate,
1294 },
1295 Point {
1296 x: bounds.x - outside,
1297 y: coordinate,
1298 },
1299 ),
1300 AxisPosition::Right => (
1301 Point {
1302 x: bounds.x + bounds.width - inside,
1303 y: coordinate,
1304 },
1305 Point {
1306 x: bounds.x + bounds.width + outside,
1307 y: coordinate,
1308 },
1309 ),
1310 };
1311 elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
1312}
1313
1314fn tick_label_origin(
1315 bounds: Rect,
1316 axis_position: AxisPosition,
1317 label_position: TickLabelPosition,
1318 coordinate: f64,
1319) -> Point {
1320 let side = match label_position {
1321 TickLabelPosition::NextTo => axis_position,
1322 TickLabelPosition::High => {
1323 if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
1324 AxisPosition::Top
1325 } else {
1326 AxisPosition::Right
1327 }
1328 }
1329 TickLabelPosition::Low => {
1330 if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
1331 AxisPosition::Bottom
1332 } else {
1333 AxisPosition::Left
1334 }
1335 }
1336 TickLabelPosition::None => axis_position,
1337 };
1338 match side {
1339 AxisPosition::Bottom => Point {
1340 x: coordinate - 4.0,
1341 y: bounds.y + bounds.height + 12.0,
1342 },
1343 AxisPosition::Top => Point {
1344 x: coordinate - 4.0,
1345 y: bounds.y - 4.0,
1346 },
1347 AxisPosition::Left => Point {
1348 x: bounds.x - 30.0,
1349 y: coordinate + 3.0,
1350 },
1351 AxisPosition::Right => Point {
1352 x: bounds.x + bounds.width + 4.0,
1353 y: coordinate + 3.0,
1354 },
1355 }
1356}
1357
1358fn format_axis_value(format: Option<&NumberFormat>, value: f64) -> Result<String> {
1359 if let Some(format) = format {
1360 format.format_value(value)
1361 } else {
1362 NumberFormat::new("General".to_owned(), false)?.format_value(value)
1363 }
1364}
1365
1366fn shape_label(fonts: &mut FontManager, text: &str, origin: Point) -> Result<GlyphRun> {
1367 shape_label_with_properties(fonts, text, origin, None)
1368}
1369
1370fn shape_label_with_properties(
1371 fonts: &mut FontManager,
1372 text: &str,
1373 origin: Point,
1374 properties: Option<&CT_TextBody>,
1375) -> Result<GlyphRun> {
1376 const FAMILY: &str = "Carlito";
1377 const SIZE: f64 = 9.0;
1378 let properties = properties.and_then(chart_text_properties);
1379 let family = properties
1380 .and_then(|properties| properties.latin.as_ref())
1381 .map(|font| font.typeface.as_str())
1382 .unwrap_or(FAMILY);
1383 let size = properties
1384 .and_then(|properties| properties.font_size)
1385 .map(|size| f64::from(size) / 100.0)
1386 .unwrap_or(SIZE);
1387 let bold = properties
1388 .and_then(|properties| properties.bold)
1389 .unwrap_or(false);
1390 let italic = properties
1391 .and_then(|properties| properties.italic)
1392 .unwrap_or(false);
1393 let font_id = fonts
1394 .resolve_font_for_text(Some(family), bold, italic, text)
1395 .map_err(|error| ChartError::InvalidValue {
1396 element: "chart label font".to_owned(),
1397 value: error.to_string(),
1398 })?;
1399 let shaped =
1400 fonts
1401 .shape_text(font_id, text, size)
1402 .map_err(|error| ChartError::InvalidValue {
1403 element: "chart label shaping".to_owned(),
1404 value: error.to_string(),
1405 })?;
1406 Ok(GlyphRun {
1407 origin,
1408 font_id,
1409 font_size: size,
1410 glyph_ids: shaped.glyph_ids,
1411 advances: shaped.advances,
1412 text: text.to_owned(),
1413 source: None,
1414 color: Color::BLACK,
1415 bold,
1416 italic,
1417 field_kind: None,
1418 field_source: None,
1419 note: None,
1420 note_reference_source: None,
1421 tab_aligned: None,
1422 })
1423}
1424
1425fn chart_text_properties(body: &CT_TextBody) -> Option<&CT_TextCharacterProperties> {
1426 body.paragraphs()
1427 .first()
1428 .and_then(|paragraph| paragraph.properties.as_ref())
1429 .and_then(|properties| properties.default_run_properties.as_ref())
1430}
1431
1432fn stroked_segment(start: Point, end: Point, color: Color, width: f64) -> PositionedElement {
1433 PositionedElement::Path(PathElement {
1434 path: Path {
1435 commands: vec![PathCommand::MoveTo(start), PathCommand::LineTo(end)],
1436 fill_rule: FillRule::NonZero,
1437 },
1438 fill: None,
1439 stroke: Some(Stroke::new(Paint::Solid(color), width)),
1440 })
1441}
1442
1443fn plot_series(plot: &Plot) -> &[Series] {
1444 match plot {
1445 Plot::Bar { series, .. }
1446 | Plot::Line { series, .. }
1447 | Plot::Pie { series, .. }
1448 | Plot::Doughnut { series, .. }
1449 | Plot::Area { series, .. }
1450 | Plot::Scatter { series, .. }
1451 | Plot::Radar { series, .. } => series,
1452 }
1453}
1454
1455fn plot_data_labels(plot: &Plot) -> Option<&CT_DLbls> {
1456 match plot {
1457 Plot::Bar { data_labels, .. }
1458 | Plot::Line { data_labels, .. }
1459 | Plot::Pie { data_labels, .. }
1460 | Plot::Doughnut { data_labels, .. }
1461 | Plot::Area { data_labels, .. }
1462 | Plot::Scatter { data_labels, .. }
1463 | Plot::Radar { data_labels, .. } => data_labels.as_ref(),
1464 }
1465}
1466
1467fn series_categories(
1468 series: &Series,
1469 number_format: Option<&NumberFormat>,
1470) -> Result<Vec<(usize, String)>> {
1471 let Some(categories) = &series.categories else {
1472 return Ok(Vec::new());
1473 };
1474 match categories {
1475 AxisData::String(data) => {
1476 let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
1477 indexes
1478 .into_iter()
1479 .zip(&data.values)
1480 .map(|(index, value)| {
1481 usize::try_from(index)
1482 .map(|index| (index, value.clone()))
1483 .map_err(|_| ChartError::InvalidValue {
1484 element: "c:cat/c:pt/@idx".to_owned(),
1485 value: index.to_string(),
1486 })
1487 })
1488 .collect()
1489 }
1490 AxisData::Numeric(data) => {
1491 let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
1492 let cache_format;
1493 let format = if let Some(format) = number_format {
1494 format
1495 } else {
1496 cache_format = NumberFormat::new(data.format_code.clone(), false)?;
1497 &cache_format
1498 };
1499 indexes
1500 .into_iter()
1501 .zip(&data.values)
1502 .map(|(index, value)| {
1503 let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
1504 element: "c:cat/c:pt/@idx".to_owned(),
1505 value: index.to_string(),
1506 })?;
1507 let text = format.format_value(*value)?;
1508 Ok((index, text))
1509 })
1510 .collect()
1511 }
1512 }
1513}
1514
1515fn series_name(series: &Series, index: usize) -> String {
1516 series
1517 .name
1518 .as_ref()
1519 .and_then(|name| name.values.first())
1520 .cloned()
1521 .unwrap_or_else(|| format!("Series {}", index + 1))
1522}
1523
1524fn render_data_labels(
1525 plot: &Plot,
1526 bounds: Rect,
1527 blanks: DispBlanksAs,
1528 options: GeometryOptions,
1529 fonts: &mut FontManager,
1530 elements: &mut Vec<PositionedElement>,
1531) -> Result<()> {
1532 let plot_labels = plot_data_labels(plot);
1533 let anchors = plot_label_anchors(plot, bounds, blanks, options)?;
1534 for (series_index, series) in plot_series(plot).iter().enumerate() {
1535 let Some(labels) = series.data_labels.as_ref().or(plot_labels) else {
1536 continue;
1537 };
1538 let overrides = point_label_overrides(labels)?;
1539 let mut categories = None;
1540 let mut bubble_sizes = None;
1541 let (_, values) = logical_numeric_values(&series.values)?;
1542 let mut percentage_total = None;
1543 for (logical_index, value) in values {
1544 let point_override = overrides.get(&logical_index);
1545 if point_override.is_some_and(|point| point.deleted == Some(true)) {
1546 continue;
1547 }
1548 let effective_labels = point_override
1549 .map(|point| point.apply_to(labels))
1550 .unwrap_or_else(|| labels.clone());
1551 let labels = &effective_labels;
1552 let Some(anchor) = anchors.get(&(series_index, logical_index)).copied() else {
1553 continue;
1554 };
1555 let mut fields = Vec::new();
1556 if labels.show_series_name {
1557 fields.push(series_name(series, series_index));
1558 }
1559 if labels.show_category_name {
1560 if categories.is_none() {
1561 categories = Some(
1562 series_categories(series, None)?
1563 .into_iter()
1564 .collect::<BTreeMap<_, _>>(),
1565 );
1566 }
1567 if let Some(category) = categories
1568 .as_ref()
1569 .and_then(|categories| categories.get(&logical_index))
1570 {
1571 fields.push(category.clone());
1572 }
1573 }
1574 if labels.show_value {
1575 fields.push(format_axis_value(labels.number_format.as_ref(), value)?);
1576 }
1577 if labels.show_percent {
1578 let total = if let Some(total) = percentage_total {
1579 total
1580 } else {
1581 let total = series
1582 .values
1583 .values
1584 .iter()
1585 .copied()
1586 .filter(|value| *value > 0.0)
1587 .try_fold(0.0, |total, value| {
1588 checked_geometry_sum(total, value, "data-label percentage total")
1589 })?;
1590 percentage_total = Some(total);
1591 total
1592 };
1593 if total > 0.0 {
1594 let text = if let Some(format) = &labels.number_format {
1595 format.format_value(value / total)?
1596 } else {
1597 NumberFormat::new("0%".to_owned(), false)?.format_value(value / total)?
1598 };
1599 fields.push(text);
1600 }
1601 }
1602 if labels.show_bubble_size {
1603 if bubble_sizes.is_none() {
1604 bubble_sizes = Some(
1605 series
1606 .bubble_size
1607 .as_ref()
1608 .map(logical_numeric_values)
1609 .transpose()?
1610 .map(|(_, values)| values.into_iter().collect::<BTreeMap<_, _>>())
1611 .unwrap_or_default(),
1612 );
1613 }
1614 if let Some(value) = bubble_sizes
1615 .as_ref()
1616 .and_then(|sizes| sizes.get(&logical_index))
1617 {
1618 fields.push(format_axis_value(labels.number_format.as_ref(), *value)?);
1619 }
1620 }
1621 if fields.is_empty() {
1622 continue;
1623 }
1624 let separator = labels.separator.as_deref().unwrap_or(", ");
1625 let text = fields.join(separator);
1626 let origin = anchor.origin(labels.position);
1627 elements.push(PositionedElement::Text(shape_label(fonts, &text, origin)?));
1628 }
1629 }
1630 Ok(())
1631}
1632
1633#[derive(Clone, Debug, Default)]
1634struct PointLabelOverride {
1635 index: Option<usize>,
1636 deleted: Option<bool>,
1637 number_format: Option<NumberFormat>,
1638 position: Option<DataLabelPosition>,
1639 separator: Option<String>,
1640 show_legend_key: Option<bool>,
1641 show_value: Option<bool>,
1642 show_category_name: Option<bool>,
1643 show_series_name: Option<bool>,
1644 show_percent: Option<bool>,
1645 show_bubble_size: Option<bool>,
1646}
1647
1648impl PointLabelOverride {
1649 fn apply_to(&self, labels: &CT_DLbls) -> CT_DLbls {
1650 let mut effective = labels.clone();
1651 if let Some(value) = &self.number_format {
1652 effective.number_format = Some(value.clone());
1653 }
1654 if let Some(value) = self.position {
1655 effective.position = Some(value);
1656 }
1657 if let Some(value) = &self.separator {
1658 effective.separator = Some(value.clone());
1659 }
1660 if let Some(value) = self.show_legend_key {
1661 effective.show_legend_key = value;
1662 }
1663 if let Some(value) = self.show_value {
1664 effective.show_value = value;
1665 }
1666 if let Some(value) = self.show_category_name {
1667 effective.show_category_name = value;
1668 }
1669 if let Some(value) = self.show_series_name {
1670 effective.show_series_name = value;
1671 }
1672 if let Some(value) = self.show_percent {
1673 effective.show_percent = value;
1674 }
1675 if let Some(value) = self.show_bubble_size {
1676 effective.show_bubble_size = value;
1677 }
1678 effective
1679 }
1680}
1681
1682fn point_label_overrides(labels: &CT_DLbls) -> Result<BTreeMap<usize, PointLabelOverride>> {
1683 let mut namespaces = chart_namespace_defaults();
1684 for (name, namespace) in &labels.namespace_declarations {
1685 let prefix = if name == "xmlns" {
1686 Vec::new()
1687 } else if let Some(prefix) = name.strip_prefix("xmlns:") {
1688 prefix.as_bytes().to_vec()
1689 } else {
1690 continue;
1691 };
1692 upsert_namespace_binding(&mut namespaces, prefix, namespace.clone());
1693 }
1694 let mut overrides = BTreeMap::new();
1695 for raw in labels.raw_children.at(0) {
1696 if chart_root_local(raw, &namespaces)?.as_deref() != Some(b"dLbl") {
1697 continue;
1698 }
1699 let point = parse_point_label_override(raw, &namespaces)?;
1700 let index = point
1701 .index
1702 .ok_or_else(|| ChartError::MissingElement("c:dLbl/c:idx".to_owned()))?;
1703 if overrides.insert(index, point).is_some() {
1704 return Err(ChartError::DuplicateElement(format!(
1705 "c:dLbl[c:idx={index}]"
1706 )));
1707 }
1708 }
1709 Ok(overrides)
1710}
1711
1712fn parse_point_label_override(
1713 xml: &[u8],
1714 inherited: &NamespaceBindings,
1715) -> Result<PointLabelOverride> {
1716 let mut reader = Reader::from_reader(xml);
1717 let mut buffer = Vec::new();
1718 let mut namespaces = None;
1719 let mut state = PointLabelOverride::default();
1720 let mut order = 0;
1721 loop {
1722 match reader
1723 .read_event_into(&mut buffer)
1724 .map_err(OxmlError::from)?
1725 {
1726 Event::Start(element) if namespaces.is_none() => {
1727 if !matches_local_name(element.name().as_ref(), b"dLbl")
1728 || !element_is_in_namespace(&element, C_NS, inherited)?
1729 {
1730 return Err(ChartError::UnexpectedElement(element_name(&element)));
1731 }
1732 namespaces = Some(chart_bindings(inherited, &element)?);
1733 }
1734 Event::Start(element) => {
1735 let bindings = namespaces
1736 .as_ref()
1737 .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
1738 let name = chart_child_local(&element, bindings)?;
1739 let raw = capture_element(&mut reader, &element)?;
1740 parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
1741 }
1742 Event::Empty(element) => {
1743 let bindings = namespaces
1744 .as_ref()
1745 .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
1746 let name = chart_child_local(&element, bindings)?;
1747 let raw = capture_empty_element(&element)?;
1748 parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
1749 }
1750 Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbl") => {
1751 if state.index.is_none() {
1752 return Err(ChartError::MissingElement("c:dLbl/c:idx".to_owned()));
1753 }
1754 return Ok(state);
1755 }
1756 Event::Eof => return Err(missing_end("c:dLbl")),
1757 _ => {}
1758 }
1759 buffer.clear();
1760 }
1761}
1762
1763fn parse_point_label_child(
1764 state: &mut PointLabelOverride,
1765 name: Option<&[u8]>,
1766 raw: &[u8],
1767 namespaces: &NamespaceBindings,
1768 order: &mut usize,
1769) -> Result<()> {
1770 let Some(name) = name else {
1771 return Ok(());
1772 };
1773 let rank = match name {
1774 b"idx" => 0,
1775 b"delete" => 1,
1776 b"layout" => 2,
1777 b"tx" => 3,
1778 b"numFmt" => 4,
1779 b"spPr" => 5,
1780 b"txPr" => 6,
1781 b"dLblPos" => 7,
1782 b"showLegendKey" => 8,
1783 b"showVal" => 9,
1784 b"showCatName" => 10,
1785 b"showSerName" => 11,
1786 b"showPercent" => 12,
1787 b"showBubbleSize" => 13,
1788 b"separator" => 14,
1789 b"showLeaderLines" => 15,
1790 b"leaderLines" => 16,
1791 b"extLst" => 17,
1792 _ => return Ok(()),
1793 };
1794 if rank < *order {
1795 return Err(ChartError::InvalidValue {
1796 element: "c:dLbl".to_owned(),
1797 value: format!("c:{} is out of schema order", String::from_utf8_lossy(name)),
1798 });
1799 }
1800 *order = rank;
1801 match name {
1802 b"idx" => {
1803 let (index, _) = parse_u32_scalar(raw, "idx")?;
1804 let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
1805 element: "c:dLbl/c:idx".to_owned(),
1806 value: index.to_string(),
1807 })?;
1808 set_once(&mut state.index, index, "c:dLbl/c:idx")?;
1809 }
1810 b"delete" => set_once(
1811 &mut state.deleted,
1812 parse_bool_value(raw, "delete")?.0,
1813 "c:dLbl/c:delete",
1814 )?,
1815 b"numFmt" => set_once(
1816 &mut state.number_format,
1817 NumberFormat::from_xml(raw)?,
1818 "c:dLbl/c:numFmt",
1819 )?,
1820 b"dLblPos" => {
1821 let (value, _) = scalar_value(raw, "dLblPos")?;
1822 let value =
1823 value.ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
1824 let position = DataLabelPosition::parse(&value)
1825 .ok_or_else(|| invalid_attribute("dLblPos", "val", value))?;
1826 set_once(&mut state.position, position, "c:dLbl/c:dLblPos")?;
1827 }
1828 b"showLegendKey" => set_once(
1829 &mut state.show_legend_key,
1830 parse_bool_value(raw, "showLegendKey")?.0,
1831 "c:dLbl/c:showLegendKey",
1832 )?,
1833 b"showVal" => set_once(
1834 &mut state.show_value,
1835 parse_bool_value(raw, "showVal")?.0,
1836 "c:dLbl/c:showVal",
1837 )?,
1838 b"showCatName" => set_once(
1839 &mut state.show_category_name,
1840 parse_bool_value(raw, "showCatName")?.0,
1841 "c:dLbl/c:showCatName",
1842 )?,
1843 b"showSerName" => set_once(
1844 &mut state.show_series_name,
1845 parse_bool_value(raw, "showSerName")?.0,
1846 "c:dLbl/c:showSerName",
1847 )?,
1848 b"showPercent" => set_once(
1849 &mut state.show_percent,
1850 parse_bool_value(raw, "showPercent")?.0,
1851 "c:dLbl/c:showPercent",
1852 )?,
1853 b"showBubbleSize" => set_once(
1854 &mut state.show_bubble_size,
1855 parse_bool_value(raw, "showBubbleSize")?.0,
1856 "c:dLbl/c:showBubbleSize",
1857 )?,
1858 b"separator" => set_once(
1859 &mut state.separator,
1860 parse_text_element(raw, b"separator", namespaces)?.0,
1861 "c:dLbl/c:separator",
1862 )?,
1863 _ => {}
1864 }
1865 Ok(())
1866}
1867
1868#[derive(Clone, Copy, Debug)]
1869struct LabelAnchor {
1870 center: Point,
1871 base: Point,
1872 end: Point,
1873 direction: Point,
1874}
1875
1876impl LabelAnchor {
1877 const fn point(center: Point) -> Self {
1878 Self {
1879 center,
1880 base: center,
1881 end: center,
1882 direction: Point { x: 0.0, y: 0.0 },
1883 }
1884 }
1885
1886 const fn segment(center: Point, base: Point, end: Point) -> Self {
1887 Self {
1888 center,
1889 base,
1890 end,
1891 direction: Point {
1892 x: end.x - base.x,
1893 y: end.y - base.y,
1894 },
1895 }
1896 }
1897
1898 const fn directed_segment(center: Point, base: Point, end: Point, direction: Point) -> Self {
1899 Self {
1900 center,
1901 base,
1902 end,
1903 direction,
1904 }
1905 }
1906
1907 fn origin(self, position: Option<DataLabelPosition>) -> Point {
1908 let mut origin = self.center;
1909 match position.unwrap_or(DataLabelPosition::BestFit) {
1910 DataLabelPosition::Bottom => origin.y += 8.0,
1911 DataLabelPosition::Top => origin.y -= 5.0,
1912 DataLabelPosition::Left => origin.x -= 8.0,
1913 DataLabelPosition::Right => origin.x += 8.0,
1914 DataLabelPosition::InsideBase => {
1915 origin = self.along_segment(self.base, 5.0, true).unwrap_or(Point {
1916 x: origin.x,
1917 y: origin.y + 8.0,
1918 });
1919 }
1920 DataLabelPosition::InsideEnd => {
1921 origin = self.along_segment(self.end, -5.0, true).unwrap_or(origin);
1922 }
1923 DataLabelPosition::OutsideEnd => {
1924 origin = self.along_segment(self.end, 5.0, false).unwrap_or(Point {
1925 x: origin.x,
1926 y: origin.y - 5.0,
1927 });
1928 }
1929 DataLabelPosition::BestFit | DataLabelPosition::Center => {}
1930 }
1931 origin
1932 }
1933
1934 fn along_segment(self, origin: Point, distance: f64, clamp_inside: bool) -> Option<Point> {
1935 let dx = self.end.x - self.base.x;
1936 let dy = self.end.y - self.base.y;
1937 let length = dx.hypot(dy);
1938 let (dx, dy, direction_length) = if length.is_finite() && length > 0.0 {
1939 (dx, dy, length)
1940 } else {
1941 let direction_length = self.direction.x.hypot(self.direction.y);
1942 if !direction_length.is_finite() || direction_length == 0.0 {
1943 return None;
1944 }
1945 (self.direction.x, self.direction.y, direction_length)
1946 };
1947 let distance = if clamp_inside {
1948 distance.clamp(-length / 2.0, length / 2.0)
1949 } else {
1950 distance
1951 };
1952 Some(Point {
1953 x: origin.x + dx / direction_length * distance,
1954 y: origin.y + dy / direction_length * distance,
1955 })
1956 }
1957}
1958
1959fn plot_label_anchors(
1960 plot: &Plot,
1961 bounds: Rect,
1962 blanks: DispBlanksAs,
1963 options: GeometryOptions,
1964) -> Result<BTreeMap<(usize, usize), LabelAnchor>> {
1965 let mut anchors = BTreeMap::new();
1966 match plot {
1967 Plot::Bar {
1968 direction,
1969 grouping,
1970 gap_width,
1971 overlap,
1972 series,
1973 ..
1974 } => {
1975 for bar in bar_rectangles(
1976 *direction, *grouping, *gap_width, *overlap, series, bounds, options,
1977 )? {
1978 if !point_in_rect(bar.end, bounds) {
1979 continue;
1980 }
1981 let visible_base = Point {
1982 x: bar.base.x.clamp(bounds.x, bounds.x + bounds.width),
1983 y: bar.base.y.clamp(bounds.y, bounds.y + bounds.height),
1984 };
1985 let visible_end = Point {
1986 x: bar.end.x.clamp(bounds.x, bounds.x + bounds.width),
1987 y: bar.end.y.clamp(bounds.y, bounds.y + bounds.height),
1988 };
1989 anchors.insert(
1990 (bar.series_index, bar.logical_index),
1991 LabelAnchor::directed_segment(
1992 Point {
1993 x: (visible_base.x + visible_end.x) / 2.0,
1994 y: (visible_base.y + visible_end.y) / 2.0,
1995 },
1996 visible_base,
1997 visible_end,
1998 Point {
1999 x: bar.end.x - bar.base.x,
2000 y: bar.end.y - bar.base.y,
2001 },
2002 ),
2003 );
2004 }
2005 }
2006 Plot::Line {
2007 grouping, series, ..
2008 }
2009 | Plot::Area {
2010 grouping, series, ..
2011 } => {
2012 for (series_index, points) in
2013 category_label_points(*grouping, series, bounds, blanks, options)?
2014 .into_iter()
2015 .enumerate()
2016 {
2017 for (logical_index, point) in points {
2018 anchors.insert((series_index, logical_index), LabelAnchor::point(point));
2019 }
2020 }
2021 }
2022 Plot::Scatter { series, .. } => {
2023 let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
2024 let x_mapping = options.scatter_x.unwrap_or(AxisMapping {
2025 domain: x_domain,
2026 orientation: Orientation::MinMax,
2027 });
2028 let y_mapping = options.scatter_y.unwrap_or(AxisMapping {
2029 domain: y_domain,
2030 orientation: Orientation::MinMax,
2031 });
2032 for (series_index, series) in series.iter().enumerate() {
2033 let indexed = matched_scatter_points(series)?;
2034 let points = scatter_points(
2035 &indexed,
2036 x_mapping.domain,
2037 y_mapping.domain,
2038 bounds,
2039 x_mapping.orientation,
2040 y_mapping.orientation,
2041 )?;
2042 for ((logical_index, _, _), point) in indexed.into_iter().zip(points) {
2043 anchors.insert((series_index, logical_index), LabelAnchor::point(point));
2044 }
2045 }
2046 }
2047 Plot::Radar { series, .. } => {
2048 let center = Point {
2049 x: bounds.x + bounds.width / 2.0,
2050 y: bounds.y + bounds.height / 2.0,
2051 };
2052 let category_count = series_category_count(series)?;
2053 for (series_index, points) in radar_series_points(series, bounds, options)?
2054 .into_iter()
2055 .enumerate()
2056 {
2057 for (logical_index, point) in points {
2058 let spoke = radial_point(
2059 center,
2060 1.0,
2061 oriented_category_index(
2062 logical_index,
2063 category_count,
2064 options.category_orientation,
2065 ),
2066 category_count,
2067 );
2068 anchors.insert(
2069 (series_index, logical_index),
2070 LabelAnchor::directed_segment(
2071 point,
2072 center,
2073 point,
2074 Point {
2075 x: spoke.x - center.x,
2076 y: spoke.y - center.y,
2077 },
2078 ),
2079 );
2080 }
2081 }
2082 }
2083 Plot::Pie {
2084 first_slice_angle,
2085 series,
2086 ..
2087 } => {
2088 for slice in pie_slices(*first_slice_angle, None, series, bounds)? {
2089 anchors.insert(
2090 (slice.series_index, slice.logical_index),
2091 LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2092 );
2093 }
2094 }
2095 Plot::Doughnut {
2096 first_slice_angle,
2097 hole_size,
2098 series,
2099 ..
2100 } => {
2101 for slice in pie_slices(*first_slice_angle, Some(*hole_size), series, bounds)? {
2102 anchors.insert(
2103 (slice.series_index, slice.logical_index),
2104 LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2105 );
2106 }
2107 }
2108 }
2109 anchors.retain(|_, anchor| point_in_rect(anchor.end, bounds));
2110 Ok(anchors)
2111}
2112
2113fn category_label_points(
2114 grouping: Grouping,
2115 series: &[Series],
2116 bounds: Rect,
2117 blanks: DispBlanksAs,
2118 options: GeometryOptions,
2119) -> Result<Vec<Vec<(usize, Point)>>> {
2120 validate_series_values(series)?;
2121 let logical_series = logical_series_values(series)?;
2122 let category_count = series_category_count(series)?;
2123 let calculated_series = if blanks == DispBlanksAs::Zero {
2124 zero_filled_series(&logical_series, category_count)
2125 } else {
2126 logical_series.clone()
2127 };
2128 let layers = stacked_series_bounds(
2129 &calculated_series,
2130 grouping != Grouping::Standard,
2131 grouping == Grouping::PercentStacked,
2132 )?;
2133 let domain = options
2134 .value
2135 .map(|mapping| mapping.domain)
2136 .unwrap_or(domain_from_layers(&layers)?);
2137 let value_orientation = options
2138 .value
2139 .map(|mapping| mapping.orientation)
2140 .unwrap_or(Orientation::MinMax);
2141 let mut point_series = Vec::with_capacity(layers.len());
2142 for (series_index, layer) in layers.iter().enumerate() {
2143 let present: BTreeSet<_> = logical_series[series_index]
2144 .1
2145 .iter()
2146 .map(|(index, _)| *index)
2147 .collect();
2148 let indexed: Vec<_> = layer
2149 .iter()
2150 .filter(|(index, _, _)| present.contains(index))
2151 .map(|(index, _, upper)| (*index, *upper))
2152 .collect();
2153 let points = category_points(
2154 &indexed,
2155 category_count,
2156 domain,
2157 bounds,
2158 options.category_orientation,
2159 value_orientation,
2160 )?;
2161 point_series.push(
2162 indexed
2163 .into_iter()
2164 .map(|(index, _)| index)
2165 .zip(points)
2166 .collect(),
2167 );
2168 }
2169 Ok(point_series)
2170}
2171
2172fn point_in_rect(point: Point, bounds: Rect) -> bool {
2173 const EPSILON: f64 = 1.0e-9;
2174 point.x >= bounds.x - EPSILON
2175 && point.x <= bounds.x + bounds.width + EPSILON
2176 && point.y >= bounds.y - EPSILON
2177 && point.y <= bounds.y + bounds.height + EPSILON
2178}
2179
2180fn resolve_series_colours(
2181 plot: &Plot,
2182 theme: &CT_OfficeStyleSheet,
2183 color_map: &ColorMap,
2184) -> Result<Vec<Color>> {
2185 let lookup = theme_colour_lookup(theme);
2186 plot_series(plot)
2187 .iter()
2188 .enumerate()
2189 .map(|(index, series)| {
2190 if let Some(properties) = series.sp_pr.as_ref() {
2191 if let Some(fill) = properties.fill.as_ref() {
2192 return resolve_series_paint(fill, index, "fill", theme, color_map, &lookup);
2193 }
2194 if let Some(fill) = properties.line.as_ref().and_then(|line| line.fill.as_ref()) {
2195 return resolve_series_paint(fill, index, "line", theme, color_map, &lookup);
2196 }
2197 }
2198 resolve_series_accent(index, theme, color_map, &lookup)
2199 })
2200 .collect()
2201}
2202
2203fn resolve_series_paint(
2204 fill: &Fill,
2205 series_index: usize,
2206 source: &str,
2207 theme: &CT_OfficeStyleSheet,
2208 color_map: &ColorMap,
2209 lookup: &[(&str, RgbColor)],
2210) -> Result<Color> {
2211 match fill {
2212 Fill::NoFill(_) => Ok(Color {
2213 r: 0.0,
2214 g: 0.0,
2215 b: 0.0,
2216 a: 0.0,
2217 }),
2218 Fill::Solid(solid) => {
2219 let choice = solid
2220 .color
2221 .as_ref()
2222 .ok_or_else(|| ChartError::InvalidValue {
2223 element: format!("c:ser[{series_index}] direct {source} paint"),
2224 value: "solid fill has no colour choice".to_owned(),
2225 })?;
2226 resolve_direct_chart_colour(choice, theme, color_map, lookup, || {
2227 format!("c:ser[{series_index}] direct {source} paint")
2228 })
2229 }
2230 Fill::Gradient(_) => unsupported_series_paint(series_index, source, "gradient"),
2231 Fill::Pattern(_) => unsupported_series_paint(series_index, source, "pattern"),
2232 Fill::Blip(_) => unsupported_series_paint(series_index, source, "picture"),
2233 }
2234}
2235
2236fn resolve_direct_chart_colour(
2237 choice: &ColorChoice,
2238 theme: &CT_OfficeStyleSheet,
2239 color_map: &ColorMap,
2240 lookup: &[(&str, RgbColor)],
2241 element: impl FnOnce() -> String,
2242) -> Result<Color> {
2243 let ColorChoice::Scheme {
2244 value, transforms, ..
2245 } = choice
2246 else {
2247 return resolve_chart_colour(choice, color_map, lookup, element);
2248 };
2249 let theme_slot =
2250 mapped_theme_slot(value, color_map).ok_or_else(|| ChartError::InvalidValue {
2251 element: element(),
2252 value: format!("no concrete theme slot is available for scheme colour {value}"),
2253 })?;
2254 let theme_choice = theme.theme_elements.color_scheme.color(theme_slot);
2255 let base = concrete_theme_colour(theme_choice).ok_or_else(|| ChartError::InvalidValue {
2256 element: format!("a:clrScheme/a:{}", theme_slot.as_str()),
2257 value: "selected theme slot has no concrete sRGB or system fallback colour".to_owned(),
2258 })?;
2259 let mut combined = theme_choice.transforms().to_vec();
2260 combined.extend_from_slice(transforms);
2261 Ok(resolved_layout_colour(apply_color_transforms(
2262 base, &combined,
2263 )))
2264}
2265
2266fn mapped_theme_slot(value: &str, color_map: &ColorMap) -> Option<ThemeColorSlot> {
2267 let mapped = match value {
2268 "bg1" => color_map.theme_slot(ColorMapSlot::Background1),
2269 "tx1" => color_map.theme_slot(ColorMapSlot::Text1),
2270 "bg2" => color_map.theme_slot(ColorMapSlot::Background2),
2271 "tx2" => color_map.theme_slot(ColorMapSlot::Text2),
2272 "accent1" => color_map.theme_slot(ColorMapSlot::Accent1),
2273 "accent2" => color_map.theme_slot(ColorMapSlot::Accent2),
2274 "accent3" => color_map.theme_slot(ColorMapSlot::Accent3),
2275 "accent4" => color_map.theme_slot(ColorMapSlot::Accent4),
2276 "accent5" => color_map.theme_slot(ColorMapSlot::Accent5),
2277 "accent6" => color_map.theme_slot(ColorMapSlot::Accent6),
2278 "hlink" => color_map.theme_slot(ColorMapSlot::Hyperlink),
2279 "folHlink" => color_map.theme_slot(ColorMapSlot::FollowedHyperlink),
2280 "dk1" => ThemeColorSlot::Dark1,
2281 "lt1" => ThemeColorSlot::Light1,
2282 "dk2" => ThemeColorSlot::Dark2,
2283 "lt2" => ThemeColorSlot::Light2,
2284 _ => return None,
2285 };
2286 Some(mapped)
2287}
2288
2289fn unsupported_series_paint(series_index: usize, source: &str, kind: &str) -> Result<Color> {
2290 Err(ChartError::InvalidValue {
2291 element: format!("c:ser[{series_index}] direct {source} paint"),
2292 value: format!("unsupported {kind} paint"),
2293 })
2294}
2295
2296fn resolve_series_accent(
2297 series_index: usize,
2298 theme: &CT_OfficeStyleSheet,
2299 color_map: &ColorMap,
2300 lookup: &[(&str, RgbColor)],
2301) -> Result<Color> {
2302 const ACCENTS: [ColorMapSlot; 6] = [
2303 ColorMapSlot::Accent1,
2304 ColorMapSlot::Accent2,
2305 ColorMapSlot::Accent3,
2306 ColorMapSlot::Accent4,
2307 ColorMapSlot::Accent5,
2308 ColorMapSlot::Accent6,
2309 ];
2310 let semantic_slot = ACCENTS[series_index % ACCENTS.len()];
2311 let theme_slot = color_map.theme_slot(semantic_slot);
2312 let choice = theme.theme_elements.color_scheme.color(theme_slot);
2313 resolve_chart_colour(choice, color_map, lookup, || {
2314 format!("c:ser[{series_index}] theme accent")
2315 })
2316}
2317
2318fn resolve_chart_colour(
2319 choice: &ColorChoice,
2320 color_map: &ColorMap,
2321 lookup: &[(&str, RgbColor)],
2322 element: impl FnOnce() -> String,
2323) -> Result<Color> {
2324 resolve_color(choice, color_map, lookup)
2325 .map(resolved_layout_colour)
2326 .map_err(|error| ChartError::InvalidValue {
2327 element: element(),
2328 value: error.to_string(),
2329 })
2330}
2331
2332fn theme_colour_lookup(theme: &CT_OfficeStyleSheet) -> Vec<(&'static str, RgbColor)> {
2333 theme
2334 .theme_elements
2335 .color_scheme
2336 .iter()
2337 .filter_map(|(slot, choice)| {
2338 concrete_theme_colour(choice).map(|colour| (slot.as_str(), colour))
2339 })
2340 .collect()
2341}
2342
2343fn concrete_theme_colour(choice: &ColorChoice) -> Option<RgbColor> {
2344 match choice {
2345 ColorChoice::Srgb { value, .. } => Some(*value),
2346 ColorChoice::System { last_color, .. } => *last_color,
2347 ColorChoice::Scheme { .. } | ColorChoice::Preset { .. } => None,
2348 }
2349}
2350
2351fn resolved_layout_colour(colour: ResolvedColor) -> Color {
2352 Color {
2353 r: f64::from(colour.red) / 255.0,
2354 g: f64::from(colour.green) / 255.0,
2355 b: f64::from(colour.blue) / 255.0,
2356 a: f64::from(colour.alpha) / 255.0,
2357 }
2358}
2359
2360fn render_legend(
2361 chart: &CT_Chart,
2362 plot: &Plot,
2363 bounds: Rect,
2364 fonts: &mut FontManager,
2365 series_colours: &[Color],
2366 paths: &mut Vec<PositionedElement>,
2367 labels: &mut Vec<PositionedElement>,
2368) -> Result<()> {
2369 if chart.legend.is_none() {
2370 return Ok(());
2371 }
2372 let x = bounds.x + bounds.width - 58.0;
2373 for (index, (series, colour)) in plot_series(plot).iter().zip(series_colours).enumerate() {
2374 let y = bounds.y + 6.0 + index as f64 * 13.0;
2375 paths.push(filled_path(
2376 Path::rect(Rect {
2377 x,
2378 y,
2379 width: 7.0,
2380 height: 7.0,
2381 }),
2382 *colour,
2383 ));
2384 labels.push(PositionedElement::Text(shape_label(
2385 fonts,
2386 &series_name(series, index),
2387 Point {
2388 x: x + 10.0,
2389 y: y + 7.0,
2390 },
2391 )?));
2392 }
2393 Ok(())
2394}
2395
2396fn validate_geometry_coordinates(elements: &[PositionedElement]) -> Result<()> {
2397 fn finite_point(point: Point) -> bool {
2398 point.x.is_finite() && point.y.is_finite()
2399 }
2400
2401 for element in elements {
2402 match element {
2403 PositionedElement::Path(path) => {
2404 for command in &path.path.commands {
2405 let finite = match command {
2406 PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
2407 finite_point(*point)
2408 }
2409 PathCommand::CurveTo { c1, c2, to } => {
2410 finite_point(*c1) && finite_point(*c2) && finite_point(*to)
2411 }
2412 PathCommand::Close => true,
2413 };
2414 if !finite {
2415 return Err(ChartError::InvalidValue {
2416 element: "chart geometry coordinate".to_owned(),
2417 value: "generated path contains a nonfinite point".to_owned(),
2418 });
2419 }
2420 }
2421 }
2422 PositionedElement::Group(group) => validate_geometry_coordinates(&group.children)?,
2423 _ => {}
2424 }
2425 }
2426 Ok(())
2427}
2428
2429fn render_plot_geometry(
2430 plot: &Plot,
2431 bounds: Rect,
2432 blanks: DispBlanksAs,
2433 options: GeometryOptions,
2434 series_colours: &[Color],
2435) -> Result<Vec<PositionedElement>> {
2436 match plot {
2437 Plot::Bar {
2438 direction,
2439 grouping,
2440 gap_width,
2441 overlap,
2442 series,
2443 ..
2444 } => render_bar_geometry(
2445 *direction,
2446 *grouping,
2447 *gap_width,
2448 *overlap,
2449 series,
2450 bounds,
2451 options,
2452 series_colours,
2453 ),
2454 Plot::Line {
2455 grouping,
2456 marker,
2457 series,
2458 ..
2459 } => render_line_geometry(
2460 *grouping,
2461 *marker,
2462 series,
2463 bounds,
2464 blanks,
2465 options,
2466 series_colours,
2467 ),
2468 Plot::Pie {
2469 first_slice_angle,
2470 series,
2471 ..
2472 } => render_pie_geometry(*first_slice_angle, None, series, bounds, series_colours),
2473 Plot::Doughnut {
2474 first_slice_angle,
2475 hole_size,
2476 series,
2477 ..
2478 } => render_pie_geometry(
2479 *first_slice_angle,
2480 Some(*hole_size),
2481 series,
2482 bounds,
2483 series_colours,
2484 ),
2485 Plot::Area {
2486 grouping, series, ..
2487 } => render_area_geometry(*grouping, series, bounds, blanks, options, series_colours),
2488 Plot::Scatter { style, series, .. } => {
2489 render_scatter_geometry(*style, series, bounds, blanks, options, series_colours)
2490 }
2491 Plot::Radar { style, series, .. } => {
2492 render_radar_geometry(*style, series, bounds, options, series_colours)
2493 }
2494 }
2495}
2496
2497#[allow(clippy::too_many_arguments)]
2498fn render_bar_geometry(
2499 direction: BarDirection,
2500 grouping: BarGrouping,
2501 gap_width: u16,
2502 overlap: i8,
2503 series: &[Series],
2504 bounds: Rect,
2505 options: GeometryOptions,
2506 series_colours: &[Color],
2507) -> Result<Vec<PositionedElement>> {
2508 let bars = bar_rectangles(
2509 direction, grouping, gap_width, overlap, series, bounds, options,
2510 )?;
2511 bars.into_iter()
2512 .map(|bar| {
2513 Ok(filled_path(
2514 Path::rect(bar.rect),
2515 series_colour(series_colours, bar.series_index)?,
2516 ))
2517 })
2518 .collect()
2519}
2520
2521#[derive(Clone, Copy, Debug)]
2522struct BarRectangle {
2523 series_index: usize,
2524 logical_index: usize,
2525 rect: Rect,
2526 base: Point,
2527 end: Point,
2528}
2529
2530fn bar_rectangles(
2531 direction: BarDirection,
2532 grouping: BarGrouping,
2533 gap_width: u16,
2534 overlap: i8,
2535 series: &[Series],
2536 bounds: Rect,
2537 options: GeometryOptions,
2538) -> Result<Vec<BarRectangle>> {
2539 validate_series_values(series)?;
2540 let logical_series = logical_series_values(series)?;
2541 let category_count = series_category_count(series)?;
2542 let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
2543 let percent = grouping == BarGrouping::PercentStacked;
2544 let layers = stacked_series_bounds(&logical_series, stacked, percent)?;
2545 let domain = options
2546 .value
2547 .map(|mapping| mapping.domain)
2548 .unwrap_or(domain_from_layers(&layers)?);
2549 let value_orientation = options
2550 .value
2551 .map(|mapping| mapping.orientation)
2552 .unwrap_or(Orientation::MinMax);
2553 let category_extent = if direction == BarDirection::Column {
2554 bounds.width
2555 } else {
2556 bounds.height
2557 };
2558 let slot = category_extent / category_count as f64;
2559 let cluster_extent = slot * 100.0 / (100.0 + f64::from(gap_width));
2560 let bars_per_cluster = if stacked { 1 } else { series.len() };
2561 let overlap_fraction = if stacked {
2562 0.0
2563 } else {
2564 f64::from(overlap) / 100.0
2565 };
2566 let divisor =
2567 bars_per_cluster as f64 - (bars_per_cluster.saturating_sub(1)) as f64 * overlap_fraction;
2568 let bar_extent = cluster_extent / divisor;
2569 let advance = bar_extent * (1.0 - overlap_fraction);
2570 let mut bars = Vec::new();
2571 for (series_index, layer) in layers.iter().enumerate() {
2572 for &(logical_index, start, end) in layer {
2573 let category = oriented_category_index(
2574 logical_index,
2575 category_count,
2576 options.category_orientation,
2577 );
2578 let cluster_start = category as f64 / category_count as f64 * category_extent
2579 + (slot - cluster_extent) / 2.0;
2580 let category_start = if stacked {
2581 cluster_start
2582 } else {
2583 cluster_start + series_index as f64 * advance
2584 };
2585 let (rect, base, end) = if direction == BarDirection::Column {
2586 let y0 = map_y_oriented(start, domain, bounds, value_orientation)?;
2587 let y1 = map_y_oriented(end, domain, bounds, value_orientation)?;
2588 let center_x = bounds.x + category_start + bar_extent / 2.0;
2589 (
2590 Rect {
2591 x: bounds.x + category_start,
2592 y: y0.min(y1),
2593 width: bar_extent,
2594 height: (y1 - y0).abs(),
2595 },
2596 Point { x: center_x, y: y0 },
2597 Point { x: center_x, y: y1 },
2598 )
2599 } else {
2600 let x0 = map_x_oriented(start, domain, bounds, value_orientation)?;
2601 let x1 = map_x_oriented(end, domain, bounds, value_orientation)?;
2602 let center_y = bounds.y + category_start + bar_extent / 2.0;
2603 (
2604 Rect {
2605 x: x0.min(x1),
2606 y: bounds.y + category_start,
2607 width: (x1 - x0).abs(),
2608 height: bar_extent,
2609 },
2610 Point { x: x0, y: center_y },
2611 Point { x: x1, y: center_y },
2612 )
2613 };
2614 bars.push(BarRectangle {
2615 series_index,
2616 logical_index,
2617 rect,
2618 base,
2619 end,
2620 });
2621 }
2622 }
2623 Ok(bars)
2624}
2625
2626fn render_line_geometry(
2627 grouping: Grouping,
2628 marker: bool,
2629 series: &[Series],
2630 bounds: Rect,
2631 blanks: DispBlanksAs,
2632 options: GeometryOptions,
2633 series_colours: &[Color],
2634) -> Result<Vec<PositionedElement>> {
2635 validate_series_values(series)?;
2636 let logical_series = logical_series_values(series)?;
2637 let category_count = series_category_count(series)?;
2638 let calculated_series = if blanks == DispBlanksAs::Zero {
2639 zero_filled_series(&logical_series, category_count)
2640 } else {
2641 logical_series.clone()
2642 };
2643 let stacked = grouping != Grouping::Standard;
2644 let percent = grouping == Grouping::PercentStacked;
2645 let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
2646 let domain = options
2647 .value
2648 .map(|mapping| mapping.domain)
2649 .unwrap_or(domain_from_layers(&layers)?);
2650 let value_orientation = options
2651 .value
2652 .map(|mapping| mapping.orientation)
2653 .unwrap_or(Orientation::MinMax);
2654 let mut elements = Vec::new();
2655 for (series_index, layer) in layers.iter().enumerate() {
2656 let colour = series_colour(series_colours, series_index)?;
2657 let indexed: Vec<_> = layer
2658 .iter()
2659 .map(|(index, _, upper)| (*index, *upper))
2660 .collect();
2661 let indexes: Vec<_> = indexed.iter().map(|(index, _)| *index).collect();
2662 for (start, end) in contiguous_ranges(&indexes, blanks) {
2663 let points = category_points(
2664 &indexed[start..end],
2665 category_count,
2666 domain,
2667 bounds,
2668 options.category_orientation,
2669 value_orientation,
2670 )?;
2671 elements.push(stroked_path(polyline_path(&points, false), colour));
2672 }
2673 if marker {
2674 let present: BTreeSet<_> = logical_series[series_index]
2675 .1
2676 .iter()
2677 .map(|(index, _)| *index)
2678 .collect();
2679 let marker_values: Vec<_> = indexed
2680 .iter()
2681 .copied()
2682 .filter(|(index, _)| present.contains(index))
2683 .collect();
2684 let marker_points = category_points(
2685 &marker_values,
2686 category_count,
2687 domain,
2688 bounds,
2689 options.category_orientation,
2690 value_orientation,
2691 )?;
2692 push_markers(&mut elements, &marker_points, colour);
2693 }
2694 }
2695 Ok(elements)
2696}
2697
2698fn render_area_geometry(
2699 grouping: Grouping,
2700 series: &[Series],
2701 bounds: Rect,
2702 blanks: DispBlanksAs,
2703 options: GeometryOptions,
2704 series_colours: &[Color],
2705) -> Result<Vec<PositionedElement>> {
2706 validate_series_values(series)?;
2707 let logical_series = logical_series_values(series)?;
2708 let category_count = series_category_count(series)?;
2709 let calculated_series = if blanks == DispBlanksAs::Zero {
2710 zero_filled_series(&logical_series, category_count)
2711 } else {
2712 logical_series
2713 };
2714 let stacked = grouping != Grouping::Standard;
2715 let percent = grouping == Grouping::PercentStacked;
2716 let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
2717 let domain = options
2718 .value
2719 .map(|mapping| mapping.domain)
2720 .unwrap_or(domain_from_layers(&layers)?);
2721 let value_orientation = options
2722 .value
2723 .map(|mapping| mapping.orientation)
2724 .unwrap_or(Orientation::MinMax);
2725 let mut elements = Vec::new();
2726 for (series_index, layer) in layers.iter().enumerate() {
2727 let colour = series_colour(series_colours, series_index)?;
2728 let indexes: Vec<_> = layer.iter().map(|(index, _, _)| *index).collect();
2729 for (start, end) in contiguous_ranges(&indexes, blanks) {
2730 let segment = &layer[start..end];
2731 let top_values: Vec<_> = segment
2732 .iter()
2733 .map(|(index, _, upper)| (*index, *upper))
2734 .collect();
2735 let lower_values: Vec<_> = segment
2736 .iter()
2737 .map(|(index, lower, _)| (*index, *lower))
2738 .collect();
2739 let top = category_points(
2740 &top_values,
2741 category_count,
2742 domain,
2743 bounds,
2744 options.category_orientation,
2745 value_orientation,
2746 )?;
2747 let mut bottom = category_points(
2748 &lower_values,
2749 category_count,
2750 domain,
2751 bounds,
2752 options.category_orientation,
2753 value_orientation,
2754 )?;
2755 bottom.reverse();
2756 let mut commands = Vec::with_capacity(top.len() + bottom.len() + 2);
2757 commands.push(PathCommand::MoveTo(top[0]));
2758 commands.extend(top.iter().skip(1).copied().map(PathCommand::LineTo));
2759 commands.extend(bottom.into_iter().map(PathCommand::LineTo));
2760 commands.push(PathCommand::Close);
2761 elements.push(filled_and_stroked_path(
2762 Path {
2763 commands,
2764 fill_rule: FillRule::NonZero,
2765 },
2766 colour,
2767 ));
2768 }
2769 }
2770 Ok(elements)
2771}
2772
2773fn render_scatter_geometry(
2774 style: ScatterStyle,
2775 series: &[Series],
2776 bounds: Rect,
2777 blanks: DispBlanksAs,
2778 options: GeometryOptions,
2779 series_colours: &[Color],
2780) -> Result<Vec<PositionedElement>> {
2781 validate_series_values(series)?;
2782 let mut paired_series = Vec::with_capacity(series.len());
2783 let mut marker_series = Vec::with_capacity(series.len());
2784 let mut all_x = Vec::new();
2785 let mut all_y = Vec::new();
2786 for item in series {
2787 let markers = matched_scatter_points(item)?;
2788 let paired = scatter_render_points(item, blanks)?;
2789 all_x.extend(paired.iter().map(|(_, x, _)| *x));
2790 all_y.extend(paired.iter().map(|(_, _, y)| *y));
2791 paired_series.push(paired);
2792 marker_series.push(markers);
2793 }
2794 let x_domain = options
2795 .scatter_x
2796 .map(|mapping| mapping.domain)
2797 .unwrap_or(data_domain(&all_x, false)?);
2798 let y_domain = options
2799 .scatter_y
2800 .map(|mapping| mapping.domain)
2801 .unwrap_or(data_domain(&all_y, false)?);
2802 let x_orientation = options
2803 .scatter_x
2804 .map(|mapping| mapping.orientation)
2805 .unwrap_or(Orientation::MinMax);
2806 let y_orientation = options
2807 .scatter_y
2808 .map(|mapping| mapping.orientation)
2809 .unwrap_or(Orientation::MinMax);
2810 let draw_line = matches!(
2811 style,
2812 ScatterStyle::Line
2813 | ScatterStyle::LineMarker
2814 | ScatterStyle::Smooth
2815 | ScatterStyle::SmoothMarker
2816 );
2817 let draw_marker = matches!(
2818 style,
2819 ScatterStyle::Marker | ScatterStyle::LineMarker | ScatterStyle::SmoothMarker
2820 );
2821 let mut elements = Vec::new();
2822 for (series_index, paired) in paired_series.iter().enumerate() {
2823 let colour = series_colour(series_colours, series_index)?;
2824 if draw_line {
2825 let indexes: Vec<_> = paired.iter().map(|(index, _, _)| *index).collect();
2826 for (start, end) in contiguous_ranges(&indexes, blanks) {
2827 let points = scatter_points(
2828 &paired[start..end],
2829 x_domain,
2830 y_domain,
2831 bounds,
2832 x_orientation,
2833 y_orientation,
2834 )?;
2835 elements.push(stroked_path(polyline_path(&points, false), colour));
2836 }
2837 }
2838 if draw_marker {
2839 let marker_points = scatter_points(
2840 &marker_series[series_index],
2841 x_domain,
2842 y_domain,
2843 bounds,
2844 x_orientation,
2845 y_orientation,
2846 )?;
2847 push_markers(&mut elements, &marker_points, colour);
2848 }
2849 }
2850 Ok(elements)
2851}
2852
2853type ScatterPoint = (usize, f64, f64);
2854
2855struct ScatterPointMaps {
2856 x_count: usize,
2857 y_count: usize,
2858 x_by_index: BTreeMap<usize, f64>,
2859 y_by_index: BTreeMap<usize, f64>,
2860}
2861
2862fn scatter_point_maps(series: &Series) -> Result<ScatterPointMaps> {
2863 let Some(AxisData::Numeric(x_values)) = &series.categories else {
2864 return Err(ChartError::InvalidValue {
2865 element: "c:xVal".to_owned(),
2866 value: "scatter series requires numeric cached x values".to_owned(),
2867 });
2868 };
2869 validate_finite(&x_values.values, "c:xVal/c:numCache")?;
2870 let (x_count, x_points) = logical_numeric_values(x_values)?;
2871 let (y_count, y_points) = logical_numeric_values(&series.values)?;
2872 Ok(ScatterPointMaps {
2873 x_count,
2874 y_count,
2875 x_by_index: x_points.into_iter().collect(),
2876 y_by_index: y_points.into_iter().collect(),
2877 })
2878}
2879
2880fn matched_scatter_points(series: &Series) -> Result<Vec<ScatterPoint>> {
2881 let points = scatter_point_maps(series)?;
2882 Ok(points
2883 .y_by_index
2884 .iter()
2885 .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
2886 .collect())
2887}
2888
2889fn scatter_render_points(series: &Series, blanks: DispBlanksAs) -> Result<Vec<ScatterPoint>> {
2890 let points = scatter_point_maps(series)?;
2891 if blanks != DispBlanksAs::Zero {
2892 return Ok(points
2893 .y_by_index
2894 .iter()
2895 .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
2896 .collect());
2897 }
2898 let count = points.x_count.max(points.y_count);
2899 let present = points
2900 .x_by_index
2901 .keys()
2902 .chain(points.y_by_index.keys())
2903 .copied()
2904 .collect();
2905 Ok(zero_control_indexes(count, &present)
2906 .into_iter()
2907 .map(|index| {
2908 (
2909 index,
2910 points.x_by_index.get(&index).copied().unwrap_or(0.0),
2911 points.y_by_index.get(&index).copied().unwrap_or(0.0),
2912 )
2913 })
2914 .collect())
2915}
2916
2917fn scatter_points(
2918 values: &[(usize, f64, f64)],
2919 x_domain: Domain,
2920 y_domain: Domain,
2921 bounds: Rect,
2922 x_orientation: Orientation,
2923 y_orientation: Orientation,
2924) -> Result<Vec<Point>> {
2925 values
2926 .iter()
2927 .map(|(_, x, y)| {
2928 Ok(Point {
2929 x: map_x_oriented(*x, x_domain, bounds, x_orientation)?,
2930 y: map_y_oriented(*y, y_domain, bounds, y_orientation)?,
2931 })
2932 })
2933 .collect()
2934}
2935
2936fn render_radar_geometry(
2937 style: RadarStyle,
2938 series: &[Series],
2939 bounds: Rect,
2940 options: GeometryOptions,
2941 series_colours: &[Color],
2942) -> Result<Vec<PositionedElement>> {
2943 let point_series = radar_series_points(series, bounds, options)?;
2944 let mut elements = Vec::new();
2945 for (series_index, points) in point_series.iter().enumerate() {
2946 let colour = series_colour(series_colours, series_index)?;
2947 let plain_points: Vec<_> = points.iter().map(|(_, point)| *point).collect();
2948 if plain_points.is_empty() {
2949 continue;
2950 }
2951 let path = polyline_path(&plain_points, true);
2952 if style == RadarStyle::Filled {
2953 elements.push(filled_and_stroked_path(path, colour));
2954 } else {
2955 elements.push(stroked_path(path, colour));
2956 }
2957 if style == RadarStyle::Marker {
2958 push_markers(&mut elements, &plain_points, colour);
2959 }
2960 }
2961 Ok(elements)
2962}
2963
2964fn radar_series_points(
2965 series: &[Series],
2966 bounds: Rect,
2967 options: GeometryOptions,
2968) -> Result<Vec<Vec<(usize, Point)>>> {
2969 validate_series_values(series)?;
2970 let logical_series = logical_series_values(series)?;
2971 let category_count = series_category_count(series)?;
2972 let values = logical_series
2973 .iter()
2974 .flat_map(|(_, points)| points.iter().map(|(_, value)| *value))
2975 .collect::<Vec<_>>();
2976 let mapping = options.value.unwrap_or(AxisMapping {
2977 domain: data_domain(&values, true)?,
2978 orientation: Orientation::MinMax,
2979 });
2980 let center = Point {
2981 x: bounds.x + bounds.width / 2.0,
2982 y: bounds.y + bounds.height / 2.0,
2983 };
2984 let radius = bounds.width.min(bounds.height) / 2.0;
2985 let mut point_series = Vec::with_capacity(logical_series.len());
2986 for (_, logical) in &logical_series {
2987 let mut points = Vec::with_capacity(logical.len());
2988 for (category, value) in logical {
2989 if *value < mapping.domain.min || *value > mapping.domain.max {
2990 continue;
2991 }
2992 points.push((
2993 *category,
2994 radial_point(
2995 center,
2996 normalized_value_oriented(*value, mapping.domain, mapping.orientation)?
2997 * radius,
2998 oriented_category_index(
2999 *category,
3000 category_count,
3001 options.category_orientation,
3002 ),
3003 category_count,
3004 ),
3005 ));
3006 }
3007 point_series.push(points);
3008 }
3009 Ok(point_series)
3010}
3011
3012fn render_pie_geometry(
3013 first_slice_angle: u16,
3014 hole_size: Option<u8>,
3015 series: &[Series],
3016 bounds: Rect,
3017 series_colours: &[Color],
3018) -> Result<Vec<PositionedElement>> {
3019 pie_slices(first_slice_angle, hole_size, series, bounds)?
3020 .into_iter()
3021 .map(|slice| {
3022 Ok(filled_path(
3023 slice.path,
3024 series_colour(series_colours, slice.series_index)?,
3025 ))
3026 })
3027 .collect()
3028}
3029
3030struct PieSliceGeometry {
3031 series_index: usize,
3032 logical_index: usize,
3033 path: Path,
3034 anchor: Point,
3035 base: Point,
3036 end: Point,
3037}
3038
3039fn pie_slices(
3040 first_slice_angle: u16,
3041 hole_size: Option<u8>,
3042 series: &[Series],
3043 bounds: Rect,
3044) -> Result<Vec<PieSliceGeometry>> {
3045 validate_series_values(series)?;
3046 let center = Point {
3047 x: bounds.x + bounds.width / 2.0,
3048 y: bounds.y + bounds.height / 2.0,
3049 };
3050 let radius = bounds.width.min(bounds.height) / 2.0;
3051 let mut elements = Vec::new();
3052 for (series_index, item) in series.iter().enumerate() {
3053 let (_, logical) = logical_numeric_values(&item.values)?;
3054 let total = logical
3055 .iter()
3056 .map(|(_, value)| *value)
3057 .filter(|value| *value > 0.0)
3058 .try_fold(0.0, |total, value| {
3059 checked_geometry_sum(total, value, "pie value total")
3060 })?;
3061 if total <= 0.0 {
3062 continue;
3063 }
3064 let mut angle = f64::from(first_slice_angle).to_radians() - std::f64::consts::FRAC_PI_2;
3065 for (logical_index, value) in logical {
3066 if value <= 0.0 {
3067 continue;
3068 }
3069 let next = angle + value / total * std::f64::consts::TAU;
3070 let path = if let Some(hole_size) = hole_size {
3071 doughnut_wedge(
3072 center,
3073 radius,
3074 radius * f64::from(hole_size) / 100.0,
3075 angle,
3076 next,
3077 )
3078 } else {
3079 pie_wedge(center, radius, angle, next)
3080 };
3081 let inner_radius = hole_size
3082 .map(|hole| radius * f64::from(hole) / 100.0)
3083 .unwrap_or(0.0);
3084 let anchor_radius = hole_size
3085 .map(|_| (radius + inner_radius) / 2.0)
3086 .unwrap_or(radius * 2.0 / 3.0);
3087 let midpoint = (angle + next) / 2.0;
3088 elements.push(PieSliceGeometry {
3089 series_index,
3090 logical_index,
3091 path,
3092 anchor: circle_point(center, anchor_radius, midpoint),
3093 base: circle_point(center, inner_radius, midpoint),
3094 end: circle_point(center, radius, midpoint),
3095 });
3096 angle = next;
3097 }
3098 }
3099 Ok(elements)
3100}
3101
3102fn validate_series_values(series: &[Series]) -> Result<()> {
3103 if series.is_empty() {
3104 return Err(ChartError::MissingElement("c:ser".to_owned()));
3105 }
3106 for item in series {
3107 validate_finite(&item.values.values, "c:val/c:numCache")?;
3108 }
3109 Ok(())
3110}
3111
3112fn validate_finite(values: &[f64], element: &str) -> Result<()> {
3113 if let Some(value) = values.iter().find(|value| !value.is_finite()) {
3114 return Err(ChartError::InvalidValue {
3115 element: element.to_owned(),
3116 value: value.to_string(),
3117 });
3118 }
3119 Ok(())
3120}
3121
3122type IndexedValues = Vec<(usize, f64)>;
3123type LogicalSeries = (usize, IndexedValues);
3124type GeometryLayer = Vec<(usize, f64, f64)>;
3125
3126fn zero_control_indexes(count: usize, present: &BTreeSet<usize>) -> BTreeSet<usize> {
3127 let mut controls = present.clone();
3128 if count == 0 {
3129 return controls;
3130 }
3131 controls.insert(0);
3132 controls.insert(count - 1);
3133 for index in present {
3134 if *index > 0 {
3135 controls.insert(*index - 1);
3136 }
3137 if index + 1 < count {
3138 controls.insert(index + 1);
3139 }
3140 }
3141 controls
3142}
3143
3144fn zero_filled_series(series: &[LogicalSeries], count: usize) -> Vec<LogicalSeries> {
3145 let present = series
3146 .iter()
3147 .flat_map(|(_, points)| points.iter().map(|(index, _)| *index))
3148 .collect();
3149 let controls = zero_control_indexes(count, &present);
3150 series
3151 .iter()
3152 .map(|(declared, points)| {
3153 let values: BTreeMap<_, _> = points.iter().copied().collect();
3154 let filled = controls
3155 .iter()
3156 .map(|index| (*index, values.get(index).copied().unwrap_or(0.0)))
3157 .collect();
3158 (*declared, filled)
3159 })
3160 .collect()
3161}
3162
3163fn contiguous_ranges(indexes: &[usize], blanks: DispBlanksAs) -> Vec<(usize, usize)> {
3164 if indexes.is_empty() {
3165 return Vec::new();
3166 }
3167 if blanks != DispBlanksAs::Gap {
3168 return vec![(0, indexes.len())];
3169 }
3170 let mut ranges = Vec::new();
3171 let mut start = 0usize;
3172 for position in 1..indexes.len() {
3173 if indexes[position - 1].checked_add(1) != Some(indexes[position]) {
3174 ranges.push((start, position));
3175 start = position;
3176 }
3177 }
3178 ranges.push((start, indexes.len()));
3179 ranges
3180}
3181
3182fn logical_numeric_values(data: &NumericData) -> Result<LogicalSeries> {
3183 validate_finite(&data.values, "c:numCache")?;
3184 let (declared, indexes) = cache_layout(&data.markup, data.values.len())?;
3185 let declared = usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
3186 element: "c:ptCount".to_owned(),
3187 value: declared.to_string(),
3188 })?;
3189 let points = indexes
3190 .into_iter()
3191 .zip(data.values.iter().copied())
3192 .map(|(index, value)| {
3193 usize::try_from(index)
3194 .map(|index| (index, value))
3195 .map_err(|_| ChartError::InvalidValue {
3196 element: "c:pt/@idx".to_owned(),
3197 value: index.to_string(),
3198 })
3199 })
3200 .collect::<Result<Vec<_>>>()?;
3201 Ok((declared, points))
3202}
3203
3204fn logical_series_values(series: &[Series]) -> Result<Vec<LogicalSeries>> {
3205 series
3206 .iter()
3207 .map(|item| logical_numeric_values(&item.values))
3208 .collect()
3209}
3210
3211fn reference_logical_count(markup: &ReferenceMarkup, value_count: usize) -> Result<usize> {
3212 let (declared, _) = cache_layout(markup, value_count)?;
3213 usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
3214 element: "c:ptCount".to_owned(),
3215 value: declared.to_string(),
3216 })
3217}
3218
3219fn series_category_count(series: &[Series]) -> Result<usize> {
3220 let mut count = 0usize;
3221 for item in series {
3222 count = count.max(reference_logical_count(
3223 &item.values.markup,
3224 item.values.values.len(),
3225 )?);
3226 if let Some(categories) = &item.categories {
3227 let category_count = match categories {
3228 AxisData::String(data) => reference_logical_count(&data.markup, data.values.len())?,
3229 AxisData::Numeric(data) => {
3230 reference_logical_count(&data.markup, data.values.len())?
3231 }
3232 };
3233 count = count.max(category_count);
3234 }
3235 }
3236 if count == 0 {
3237 return Err(ChartError::InvalidValue {
3238 element: "c:ser".to_owned(),
3239 value: "cached series contain no points".to_owned(),
3240 });
3241 }
3242 Ok(count)
3243}
3244
3245fn stacked_series_bounds(
3246 series: &[LogicalSeries],
3247 stacked: bool,
3248 percent: bool,
3249) -> Result<Vec<GeometryLayer>> {
3250 let mut positive_totals = BTreeMap::<usize, f64>::new();
3251 let mut negative_totals = BTreeMap::<usize, f64>::new();
3252 if percent {
3253 for (_, points) in series {
3254 for &(index, value) in points {
3255 let totals = if value >= 0.0 {
3256 &mut positive_totals
3257 } else {
3258 &mut negative_totals
3259 };
3260 let total = totals.entry(index).or_default();
3261 *total = checked_geometry_sum(*total, value.abs(), "stacked percentage total")?;
3262 }
3263 }
3264 }
3265 let mut positive = BTreeMap::<usize, f64>::new();
3266 let mut negative = BTreeMap::<usize, f64>::new();
3267 let mut layers = Vec::with_capacity(series.len());
3268 for (_, points) in series {
3269 let mut layer = Vec::with_capacity(points.len());
3270 for &(index, original) in points {
3271 let total = if original >= 0.0 {
3272 positive_totals.get(&index).copied().unwrap_or(0.0)
3273 } else {
3274 negative_totals.get(&index).copied().unwrap_or(0.0)
3275 };
3276 let value = if percent && total != 0.0 {
3277 original / total
3278 } else {
3279 original
3280 };
3281 if stacked {
3282 let accumulators = if value >= 0.0 {
3283 &mut positive
3284 } else {
3285 &mut negative
3286 };
3287 let accumulator = accumulators.entry(index).or_default();
3288 let lower = *accumulator;
3289 *accumulator = checked_geometry_sum(*accumulator, value, "stacked value total")?;
3290 layer.push((index, lower, *accumulator));
3291 } else {
3292 layer.push((index, 0.0, value));
3293 }
3294 }
3295 layers.push(layer);
3296 }
3297 Ok(layers)
3298}
3299
3300fn checked_geometry_sum(current: f64, value: f64, context: &str) -> Result<f64> {
3301 let sum = current + value;
3302 if sum.is_finite() {
3303 Ok(sum)
3304 } else {
3305 Err(ChartError::InvalidValue {
3306 element: "chart geometry aggregate".to_owned(),
3307 value: format!("{context} is nonfinite"),
3308 })
3309 }
3310}
3311
3312#[derive(Clone, Copy, Debug, PartialEq)]
3313struct Domain {
3314 min: f64,
3315 max: f64,
3316}
3317
3318fn nice_number_scale(
3319 extent: Domain,
3320 pinned_minimum: Option<f64>,
3321 pinned_maximum: Option<f64>,
3322 target_tick_count: usize,
3323) -> Result<AxisScale> {
3324 if target_tick_count < 2
3325 || !extent.min.is_finite()
3326 || !extent.max.is_finite()
3327 || pinned_minimum.is_some_and(|value| !value.is_finite())
3328 || pinned_maximum.is_some_and(|value| !value.is_finite())
3329 {
3330 return Err(ChartError::InvalidValue {
3331 element: "chart axis scale".to_owned(),
3332 value: "scale bounds and target tick count must be finite and valid".to_owned(),
3333 });
3334 }
3335
3336 let mut data_min = extent.min.min(extent.max);
3337 let mut data_max = extent.min.max(extent.max);
3338 if data_min == data_max {
3339 let expanded = expand_constant_domain(data_min)?;
3340 data_min = expanded.min;
3341 data_max = expanded.max;
3342 }
3343 let requested_min = match (pinned_minimum, pinned_maximum) {
3344 (Some(minimum), _) => minimum,
3345 (None, Some(maximum)) if data_min >= maximum => {
3346 extend_bound(maximum, stable_positive_step(data_min, data_max, 3)?, false)?
3347 }
3348 (None, _) => data_min,
3349 };
3350 let requested_max = match (pinned_minimum, pinned_maximum) {
3351 (_, Some(maximum)) => maximum,
3352 (Some(minimum), None) if data_max <= minimum => {
3353 extend_bound(minimum, stable_positive_step(data_min, data_max, 3)?, true)?
3354 }
3355 (_, None) => data_max,
3356 };
3357 if requested_min >= requested_max {
3358 return Err(ChartError::InvalidValue {
3359 element: "c:scaling".to_owned(),
3360 value: format!("minimum {requested_min} is not less than maximum {requested_max}"),
3361 });
3362 }
3363
3364 let raw_step = stable_positive_step(requested_min, requested_max, target_tick_count)?;
3365 let step = nice_step(raw_step)?;
3366 let rounded_minimum = (requested_min / step).floor() * step;
3367 let rounded_maximum = (requested_max / step).ceil() * step;
3368 let lower_fallback = pinned_minimum.is_none() && !rounded_minimum.is_finite();
3369 let upper_fallback = pinned_maximum.is_none() && !rounded_maximum.is_finite();
3370 let minimum = pinned_minimum.unwrap_or(if rounded_minimum.is_finite() {
3371 rounded_minimum
3372 } else {
3373 requested_min
3374 });
3375 let maximum = pinned_maximum.unwrap_or(if rounded_maximum.is_finite() {
3376 rounded_maximum
3377 } else {
3378 requested_max
3379 });
3380 if !minimum.is_finite() || !maximum.is_finite() || minimum >= maximum {
3381 return Err(ChartError::InvalidValue {
3382 element: "chart axis scale".to_owned(),
3383 value: "nice-number bounds are nonfinite or empty".to_owned(),
3384 });
3385 }
3386
3387 let first = (minimum / step).ceil();
3388 let last = (maximum / step).floor();
3389 let tick_count = if last >= first {
3390 (last - first).min(1024.0) as usize + 1
3391 } else {
3392 0
3393 };
3394 let mut ticks = Vec::with_capacity(tick_count);
3395 for index in 0..tick_count {
3396 let tick = (first + index as f64) * step;
3397 if tick.is_finite() && tick >= minimum && tick <= maximum {
3398 ticks.push(clean_tick(tick, step));
3399 }
3400 }
3401 if lower_fallback && ticks.first().is_none_or(|tick| *tick > minimum) {
3402 ticks.insert(0, minimum);
3403 }
3404 if upper_fallback && ticks.last().is_none_or(|tick| *tick < maximum) {
3405 ticks.push(maximum);
3406 }
3407 if ticks.len() < 2 {
3408 ticks.extend([minimum, maximum]);
3409 ticks.sort_by(f64::total_cmp);
3410 }
3411 ticks.dedup_by(|left, right| *left == *right);
3412 if ticks.len() < 2 || !ticks.windows(2).all(|pair| pair[0] < pair[1]) {
3413 return Err(ChartError::InvalidValue {
3414 element: "chart axis scale".to_owned(),
3415 value: "scale cannot produce two increasing finite ticks".to_owned(),
3416 });
3417 }
3418 Ok(AxisScale {
3419 domain: Domain {
3420 min: minimum,
3421 max: maximum,
3422 },
3423 ticks,
3424 })
3425}
3426
3427const MIN_POSITIVE_SUBNORMAL: f64 = f64::from_bits(1);
3428
3429fn stable_positive_step(minimum: f64, maximum: f64, target_tick_count: usize) -> Result<f64> {
3430 let scale = minimum.abs().max(maximum.abs());
3431 let scale = if scale == 0.0 {
3432 MIN_POSITIVE_SUBNORMAL
3433 } else {
3434 scale
3435 };
3436 let normalized =
3437 (maximum / scale - minimum / scale) / target_tick_count.saturating_sub(1) as f64;
3438 let step = normalized * scale;
3439 if normalized.is_finite() && normalized > 0.0 && step.is_finite() {
3440 Ok(step.max(MIN_POSITIVE_SUBNORMAL))
3441 } else {
3442 Err(ChartError::InvalidValue {
3443 element: "chart axis scale".to_owned(),
3444 value: "scale step is nonfinite or empty".to_owned(),
3445 })
3446 }
3447}
3448
3449fn nice_step(raw_step: f64) -> Result<f64> {
3450 if !raw_step.is_finite() || raw_step <= 0.0 {
3451 return Err(ChartError::InvalidValue {
3452 element: "chart axis scale".to_owned(),
3453 value: format!("invalid raw tick step {raw_step}"),
3454 });
3455 }
3456 let magnitude = 10.0_f64
3457 .powf(raw_step.log10().floor())
3458 .max(MIN_POSITIVE_SUBNORMAL);
3459 let fraction = raw_step / magnitude;
3460 let multiplier = if fraction <= 1.0 {
3461 1.0
3462 } else if fraction <= 2.0 {
3463 2.0
3464 } else if fraction <= 5.0 {
3465 5.0
3466 } else {
3467 10.0
3468 };
3469 let step = (multiplier * magnitude).max(MIN_POSITIVE_SUBNORMAL);
3470 if step.is_finite() && step > 0.0 {
3471 Ok(step)
3472 } else {
3473 Err(ChartError::InvalidValue {
3474 element: "chart axis scale".to_owned(),
3475 value: "nice-number step is nonfinite".to_owned(),
3476 })
3477 }
3478}
3479
3480fn expand_constant_domain(value: f64) -> Result<Domain> {
3481 if value == 0.0 {
3482 return Ok(Domain {
3483 min: -1.0,
3484 max: 1.0,
3485 });
3486 }
3487 let padding = (value.abs() * 0.5).max(MIN_POSITIVE_SUBNORMAL);
3488 let minimum = value - padding;
3489 let maximum = value + padding;
3490 if minimum.is_finite() && maximum.is_finite() && minimum < maximum {
3491 return Ok(Domain {
3492 min: minimum,
3493 max: maximum,
3494 });
3495 }
3496 let domain = if value.is_sign_positive() {
3497 Domain {
3498 min: 0.0,
3499 max: value,
3500 }
3501 } else {
3502 Domain {
3503 min: value,
3504 max: 0.0,
3505 }
3506 };
3507 if domain.min < domain.max {
3508 Ok(domain)
3509 } else {
3510 Err(ChartError::InvalidValue {
3511 element: "chart axis scale".to_owned(),
3512 value: format!("constant value {value} has no finite renderable extent"),
3513 })
3514 }
3515}
3516
3517fn extend_bound(bound: f64, span: f64, positive: bool) -> Result<f64> {
3518 let candidate = if positive { bound + span } else { bound - span };
3519 let extended = if candidate.is_finite()
3520 && if positive {
3521 candidate > bound
3522 } else {
3523 candidate < bound
3524 } {
3525 candidate
3526 } else if positive {
3527 bound.next_up()
3528 } else {
3529 bound.next_down()
3530 };
3531 if extended.is_finite()
3532 && if positive {
3533 extended > bound
3534 } else {
3535 extended < bound
3536 }
3537 {
3538 Ok(extended)
3539 } else {
3540 Err(ChartError::InvalidValue {
3541 element: "c:scaling".to_owned(),
3542 value: format!("one-sided bound {bound} leaves no finite automatic extent"),
3543 })
3544 }
3545}
3546
3547fn clean_tick(value: f64, step: f64) -> f64 {
3548 if value.abs() <= step.abs() * 1.0e-12 {
3549 0.0
3550 } else {
3551 value
3552 }
3553}
3554
3555fn domain_from_layers(layers: &[GeometryLayer]) -> Result<Domain> {
3556 let values = layers
3557 .iter()
3558 .flatten()
3559 .flat_map(|(_, lower, upper)| [*lower, *upper]);
3560 let mut min = 0.0_f64;
3561 let mut max = 0.0_f64;
3562 for value in values {
3563 if !value.is_finite() {
3564 return Err(ChartError::InvalidValue {
3565 element: "chart geometry domain".to_owned(),
3566 value: "aggregate produced a nonfinite value".to_owned(),
3567 });
3568 }
3569 min = min.min(value);
3570 max = max.max(value);
3571 }
3572 if min == max {
3573 max = 1.0;
3574 }
3575 Ok(Domain { min, max })
3576}
3577
3578fn data_domain(values: &[f64], include_zero: bool) -> Result<Domain> {
3579 validate_finite(values, "chart geometry cache")?;
3580 let Some(first) = values.first().copied() else {
3581 return Err(ChartError::InvalidValue {
3582 element: "c:numCache".to_owned(),
3583 value: "cached series contain no points".to_owned(),
3584 });
3585 };
3586 let mut min = first;
3587 let mut max = first;
3588 for value in values.iter().copied() {
3589 min = min.min(value);
3590 max = max.max(value);
3591 }
3592 if include_zero {
3593 min = min.min(0.0);
3594 max = max.max(0.0);
3595 }
3596 if min == max {
3597 if !include_zero {
3598 return expand_constant_domain(min);
3599 }
3600 if min > 0.0 {
3601 min = 0.0;
3602 } else if max < 0.0 {
3603 max = 0.0;
3604 } else {
3605 max = 1.0;
3606 }
3607 }
3608 Ok(Domain { min, max })
3609}
3610
3611fn normalized_value(value: f64, domain: Domain) -> Result<f64> {
3612 let scale = value
3613 .abs()
3614 .max(domain.min.abs())
3615 .max(domain.max.abs())
3616 .max(1.0);
3617 let value = value / scale;
3618 let min = domain.min / scale;
3619 let max = domain.max / scale;
3620 let range = max - min;
3621 let normalized = (value - min) / range;
3622 if !range.is_finite() || range <= 0.0 || !normalized.is_finite() {
3623 return Err(ChartError::InvalidValue {
3624 element: "chart geometry domain".to_owned(),
3625 value: format!("cannot map finite domain {} to {}", domain.min, domain.max),
3626 });
3627 }
3628 Ok(normalized)
3629}
3630
3631fn normalized_value_oriented(value: f64, domain: Domain, orientation: Orientation) -> Result<f64> {
3632 let normalized = normalized_value(value, domain)?;
3633 Ok(if orientation == Orientation::MaxMin {
3634 1.0 - normalized
3635 } else {
3636 normalized
3637 })
3638}
3639
3640fn map_x_oriented(
3641 value: f64,
3642 domain: Domain,
3643 bounds: Rect,
3644 orientation: Orientation,
3645) -> Result<f64> {
3646 let coordinate =
3647 bounds.x + normalized_value_oriented(value, domain, orientation)? * bounds.width;
3648 if coordinate.is_finite() {
3649 Ok(coordinate)
3650 } else {
3651 Err(ChartError::InvalidValue {
3652 element: "chart geometry coordinate".to_owned(),
3653 value: "x coordinate is nonfinite".to_owned(),
3654 })
3655 }
3656}
3657
3658fn map_y_oriented(
3659 value: f64,
3660 domain: Domain,
3661 bounds: Rect,
3662 orientation: Orientation,
3663) -> Result<f64> {
3664 let coordinate = bounds.y + bounds.height
3665 - normalized_value_oriented(value, domain, orientation)? * bounds.height;
3666 if coordinate.is_finite() {
3667 Ok(coordinate)
3668 } else {
3669 Err(ChartError::InvalidValue {
3670 element: "chart geometry coordinate".to_owned(),
3671 value: "y coordinate is nonfinite".to_owned(),
3672 })
3673 }
3674}
3675
3676fn oriented_category_index(index: usize, count: usize, orientation: Orientation) -> usize {
3677 if orientation == Orientation::MaxMin && index < count {
3678 count - index - 1
3679 } else {
3680 index
3681 }
3682}
3683
3684fn category_points(
3685 values: &[(usize, f64)],
3686 count: usize,
3687 domain: Domain,
3688 bounds: Rect,
3689 category_orientation: Orientation,
3690 value_orientation: Orientation,
3691) -> Result<Vec<Point>> {
3692 values
3693 .iter()
3694 .map(|(index, value)| {
3695 let index = oriented_category_index(*index, count, category_orientation);
3696 let x = bounds.x + (index as f64 + 0.5) / count as f64 * bounds.width;
3697 let y = map_y_oriented(*value, domain, bounds, value_orientation)?;
3698 if !x.is_finite() {
3699 return Err(ChartError::InvalidValue {
3700 element: "chart geometry coordinate".to_owned(),
3701 value: "category x coordinate is nonfinite".to_owned(),
3702 });
3703 }
3704 Ok(Point { x, y })
3705 })
3706 .collect()
3707}
3708
3709fn polyline_path(points: &[Point], close: bool) -> Path {
3710 let mut commands = Vec::with_capacity(points.len() + usize::from(close));
3711 if let Some(first) = points.first() {
3712 commands.push(PathCommand::MoveTo(*first));
3713 commands.extend(points.iter().skip(1).copied().map(PathCommand::LineTo));
3714 if close {
3715 commands.push(PathCommand::Close);
3716 }
3717 }
3718 Path {
3719 commands,
3720 fill_rule: FillRule::NonZero,
3721 }
3722}
3723
3724fn pie_wedge(center: Point, radius: f64, start: f64, end: f64) -> Path {
3725 let first = circle_point(center, radius, start);
3726 let mut commands = vec![PathCommand::MoveTo(center), PathCommand::LineTo(first)];
3727 append_arc(&mut commands, center, radius, start, end);
3728 commands.push(PathCommand::Close);
3729 Path {
3730 commands,
3731 fill_rule: FillRule::NonZero,
3732 }
3733}
3734
3735fn doughnut_wedge(
3736 center: Point,
3737 outer_radius: f64,
3738 inner_radius: f64,
3739 start: f64,
3740 end: f64,
3741) -> Path {
3742 let outer_start = circle_point(center, outer_radius, start);
3743 let inner_end = circle_point(center, inner_radius, end);
3744 let mut commands = vec![PathCommand::MoveTo(outer_start)];
3745 append_arc(&mut commands, center, outer_radius, start, end);
3746 commands.push(PathCommand::LineTo(inner_end));
3747 append_arc(&mut commands, center, inner_radius, end, start);
3748 commands.push(PathCommand::Close);
3749 Path {
3750 commands,
3751 fill_rule: FillRule::NonZero,
3752 }
3753}
3754
3755fn append_arc(commands: &mut Vec<PathCommand>, center: Point, radius: f64, start: f64, end: f64) {
3756 let segments = ((end - start).abs() / std::f64::consts::FRAC_PI_2)
3757 .ceil()
3758 .max(1.0) as usize;
3759 let sweep = (end - start) / segments as f64;
3760 for segment in 0..segments {
3761 let angle = start + segment as f64 * sweep;
3762 let next = angle + sweep;
3763 let factor = 4.0 / 3.0 * (sweep / 4.0).tan();
3764 let from = circle_point(center, radius, angle);
3765 let to = circle_point(center, radius, next);
3766 commands.push(PathCommand::CurveTo {
3767 c1: Point {
3768 x: from.x - factor * radius * angle.sin(),
3769 y: from.y + factor * radius * angle.cos(),
3770 },
3771 c2: Point {
3772 x: to.x + factor * radius * next.sin(),
3773 y: to.y - factor * radius * next.cos(),
3774 },
3775 to,
3776 });
3777 }
3778}
3779
3780fn circle_point(center: Point, radius: f64, angle: f64) -> Point {
3781 Point {
3782 x: center.x + radius * angle.cos(),
3783 y: center.y + radius * angle.sin(),
3784 }
3785}
3786
3787fn radial_point(center: Point, radius: f64, index: usize, count: usize) -> Point {
3788 circle_point(
3789 center,
3790 radius,
3791 index as f64 * std::f64::consts::TAU / count as f64 - std::f64::consts::FRAC_PI_2,
3792 )
3793}
3794
3795fn marker_path(center: Point) -> Path {
3796 const RADIUS: f64 = 2.5;
3797 const KAPPA: f64 = 0.552_284_749_830_793_6;
3798 let control = RADIUS * KAPPA;
3799 Path {
3800 commands: vec![
3801 PathCommand::MoveTo(Point {
3802 x: center.x + RADIUS,
3803 y: center.y,
3804 }),
3805 PathCommand::CurveTo {
3806 c1: Point {
3807 x: center.x + RADIUS,
3808 y: center.y + control,
3809 },
3810 c2: Point {
3811 x: center.x + control,
3812 y: center.y + RADIUS,
3813 },
3814 to: Point {
3815 x: center.x,
3816 y: center.y + RADIUS,
3817 },
3818 },
3819 PathCommand::CurveTo {
3820 c1: Point {
3821 x: center.x - control,
3822 y: center.y + RADIUS,
3823 },
3824 c2: Point {
3825 x: center.x - RADIUS,
3826 y: center.y + control,
3827 },
3828 to: Point {
3829 x: center.x - RADIUS,
3830 y: center.y,
3831 },
3832 },
3833 PathCommand::CurveTo {
3834 c1: Point {
3835 x: center.x - RADIUS,
3836 y: center.y - control,
3837 },
3838 c2: Point {
3839 x: center.x - control,
3840 y: center.y - RADIUS,
3841 },
3842 to: Point {
3843 x: center.x,
3844 y: center.y - RADIUS,
3845 },
3846 },
3847 PathCommand::CurveTo {
3848 c1: Point {
3849 x: center.x + control,
3850 y: center.y - RADIUS,
3851 },
3852 c2: Point {
3853 x: center.x + RADIUS,
3854 y: center.y - control,
3855 },
3856 to: Point {
3857 x: center.x + RADIUS,
3858 y: center.y,
3859 },
3860 },
3861 PathCommand::Close,
3862 ],
3863 fill_rule: FillRule::NonZero,
3864 }
3865}
3866
3867fn push_markers(elements: &mut Vec<PositionedElement>, points: &[Point], colour: Color) {
3868 for point in points {
3869 elements.push(filled_path(marker_path(*point), colour));
3870 }
3871}
3872
3873fn series_colour(series_colours: &[Color], index: usize) -> Result<Color> {
3874 series_colours
3875 .get(index)
3876 .copied()
3877 .ok_or_else(|| ChartError::InvalidValue {
3878 element: "chart series palette".to_owned(),
3879 value: format!("missing resolved colour for series {index}"),
3880 })
3881}
3882
3883fn filled_path(path: Path, color: Color) -> PositionedElement {
3884 PositionedElement::Path(PathElement {
3885 path,
3886 fill: Some(Paint::Solid(color)),
3887 stroke: None,
3888 })
3889}
3890
3891fn stroked_path(path: Path, color: Color) -> PositionedElement {
3892 PositionedElement::Path(PathElement {
3893 path,
3894 fill: None,
3895 stroke: Some(Stroke::new(Paint::Solid(color), 1.5)),
3896 })
3897}
3898
3899fn filled_and_stroked_path(path: Path, color: Color) -> PositionedElement {
3900 PositionedElement::Path(PathElement {
3901 path,
3902 fill: Some(Paint::Solid(Color {
3903 a: color.a * 0.55,
3904 ..color
3905 })),
3906 stroke: Some(Stroke::new(Paint::Solid(color), 1.0)),
3907 })
3908}
3909
3910type NamespaceBindings = Vec<(Vec<u8>, String)>;
3911type XmlAttributes = Vec<(String, String)>;
3912type RootAttributes = (XmlAttributes, XmlAttributes);
3913
3914#[derive(Clone, Debug, Default, Eq, PartialEq)]
3915struct ScalarMarkup {
3916 raw_attributes: XmlAttributes,
3917 raw_content: Vec<Vec<u8>>,
3918}
3919
3920#[derive(Clone, Debug, Default, Eq, PartialEq)]
3921struct TextMarkup {
3922 raw_attributes: XmlAttributes,
3923 raw_children: OrderedRawChildren,
3924}
3925
3926#[derive(Clone, Debug, Default, Eq, PartialEq)]
3927struct PointMarkup {
3928 raw_attributes: XmlAttributes,
3929 raw_children: OrderedRawChildren,
3930 value: TextMarkup,
3931}
3932
3933#[derive(Clone, Debug, Default, Eq, PartialEq)]
3934struct ReferenceMarkup {
3935 raw_attributes: XmlAttributes,
3936 raw_children: OrderedRawChildren,
3937 formula: TextMarkup,
3938 cache_attributes: XmlAttributes,
3939 cache_children: OrderedRawChildren,
3940 format_code: Option<TextMarkup>,
3941 format_code_omitted: bool,
3942 point_count: ScalarMarkup,
3943 declared_point_count: Option<u32>,
3944 point_indexes: Vec<u32>,
3945 points: Vec<PointMarkup>,
3946}
3947
3948#[derive(Clone, Debug, Default, Eq, PartialEq)]
3949struct WrapperMarkup {
3950 raw_attributes: XmlAttributes,
3951 raw_children: OrderedRawChildren,
3952}
3953
3954#[derive(Clone, Debug, PartialEq)]
3956pub struct StringRef {
3957 pub formula: String,
3958 pub values: Vec<String>,
3959 markup: ReferenceMarkup,
3960}
3961
3962impl StringRef {
3963 pub fn new(formula: String, values: Vec<String>) -> Result<Self> {
3964 let reference = Self {
3965 formula,
3966 values,
3967 markup: ReferenceMarkup::default(),
3968 };
3969 reference.validate()?;
3970 Ok(reference)
3971 }
3972
3973 pub fn from_xml(xml: &[u8]) -> Result<Self> {
3974 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
3975 }
3976
3977 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
3978 let parsed = parse_reference(xml, b"strRef", b"strCache", false, inherited)?;
3979 let values = parsed.values;
3980 let reference = Self {
3981 formula: parsed.formula,
3982 values,
3983 markup: parsed.markup,
3984 };
3985 reference.validate()?;
3986 Ok(reference)
3987 }
3988
3989 pub fn to_xml(&self) -> Result<Vec<u8>> {
3990 self.validate()?;
3991 let mut writer = Writer::new(Vec::new());
3992 self.write_xml(&mut writer, true)?;
3993 Ok(writer.into_inner())
3994 }
3995
3996 fn validate(&self) -> Result<()> {
3997 validate_formula(&self.formula, "c:strRef/c:f")?;
3998 for value in &self.values {
3999 validate_xml_text(value, "c:strCache/c:pt/c:v")?;
4000 }
4001 validate_point_count(self.values.len(), "c:strCache")
4002 }
4003
4004 fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
4005 self.validate()?;
4006 let mut start = BytesStart::new("c:strRef");
4007 if standalone {
4008 start.push_attribute(("xmlns:c", C_NS));
4009 }
4010 push_attributes(&mut start, &self.markup.raw_attributes);
4011 writer
4012 .write_event(Event::Start(start))
4013 .map_err(OxmlError::from)?;
4014 emit_raw(writer, self.markup.raw_children.at(0))?;
4015 write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
4016 emit_raw(writer, self.markup.raw_children.at(1))?;
4017 write_string_cache(writer, &self.values, &self.markup)?;
4018 emit_raw(writer, self.markup.raw_children.at(2))?;
4019 writer
4020 .write_event(Event::End(BytesEnd::new("c:strRef")))
4021 .map_err(OxmlError::from)?;
4022 Ok(())
4023 }
4024}
4025
4026#[derive(Clone, Debug, PartialEq)]
4028pub struct NumericData {
4029 pub formula: String,
4030 pub format_code: String,
4031 pub values: Vec<f64>,
4032 markup: ReferenceMarkup,
4033}
4034
4035impl NumericData {
4036 pub fn new(formula: String, format_code: String, values: Vec<f64>) -> Result<Self> {
4037 validate_formula(&formula, "c:numRef/c:f")?;
4038 validate_format_code(&format_code)?;
4039 validate_numeric_values(&values)?;
4040 validate_point_count(values.len(), "c:numCache")?;
4041 Ok(Self {
4042 formula,
4043 format_code,
4044 values,
4045 markup: ReferenceMarkup::default(),
4046 })
4047 }
4048
4049 pub fn from_xml(xml: &[u8]) -> Result<Self> {
4050 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
4051 }
4052
4053 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
4054 let parsed = parse_reference(xml, b"numRef", b"numCache", true, inherited)?;
4055 let format_code = parsed.format_code.unwrap_or_else(|| "General".to_owned());
4056 let mut values = Vec::with_capacity(parsed.values.len());
4057 for value in parsed.values {
4058 let number = value.parse::<f64>().map_err(|_| ChartError::InvalidValue {
4059 element: "c:v".to_owned(),
4060 value: value.clone(),
4061 })?;
4062 if !number.is_finite() {
4063 return Err(ChartError::InvalidValue {
4064 element: "c:v".to_owned(),
4065 value,
4066 });
4067 }
4068 values.push(number);
4069 }
4070 let data = Self {
4071 formula: parsed.formula,
4072 format_code,
4073 values,
4074 markup: parsed.markup,
4075 };
4076 data.validate()?;
4077 Ok(data)
4078 }
4079
4080 pub fn to_xml(&self) -> Result<Vec<u8>> {
4081 self.validate()?;
4082 let mut writer = Writer::new(Vec::new());
4083 self.write_xml(&mut writer, true)?;
4084 Ok(writer.into_inner())
4085 }
4086
4087 fn validate(&self) -> Result<()> {
4088 validate_formula(&self.formula, "c:numRef/c:f")?;
4089 validate_format_code(&self.format_code)?;
4090 validate_numeric_values(&self.values)?;
4091 validate_point_count(self.values.len(), "c:numCache")
4092 }
4093
4094 fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
4095 self.validate()?;
4096 let mut start = BytesStart::new("c:numRef");
4097 if standalone {
4098 start.push_attribute(("xmlns:c", C_NS));
4099 }
4100 push_attributes(&mut start, &self.markup.raw_attributes);
4101 writer
4102 .write_event(Event::Start(start))
4103 .map_err(OxmlError::from)?;
4104 emit_raw(writer, self.markup.raw_children.at(0))?;
4105 write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
4106 emit_raw(writer, self.markup.raw_children.at(1))?;
4107 write_numeric_cache(writer, self, &self.markup)?;
4108 emit_raw(writer, self.markup.raw_children.at(2))?;
4109 writer
4110 .write_event(Event::End(BytesEnd::new("c:numRef")))
4111 .map_err(OxmlError::from)?;
4112 Ok(())
4113 }
4114}
4115
4116#[derive(Clone, Debug, PartialEq)]
4117pub enum AxisData {
4118 String(StringRef),
4119 Numeric(NumericData),
4120}
4121
4122#[derive(Clone, Debug, PartialEq)]
4123struct DataPoint {
4124 index: u32,
4125 sp_pr: CT_ShapeProperties,
4126}
4127
4128impl DataPoint {
4129 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
4130 writer
4131 .write_event(Event::Start(BytesStart::new("c:dPt")))
4132 .map_err(OxmlError::from)?;
4133 write_scalar(writer, "c:idx", &self.index.to_string(), None)?;
4134 self.sp_pr.write_xml_as(writer, "c:spPr")?;
4135 writer
4136 .write_event(Event::End(BytesEnd::new("c:dPt")))
4137 .map_err(OxmlError::from)?;
4138 Ok(())
4139 }
4140}
4141
4142#[derive(Clone, Debug, PartialEq)]
4144pub struct Series {
4145 pub index: u32,
4146 pub order: u32,
4147 pub name: Option<StringRef>,
4148 pub categories: Option<AxisData>,
4149 pub values: NumericData,
4150 pub bubble_size: Option<NumericData>,
4151 pub sp_pr: Option<CT_ShapeProperties>,
4152 pub data_labels: Option<CT_DLbls>,
4153 authored_data_points: Vec<DataPoint>,
4154 index_markup: ScalarMarkup,
4155 order_markup: ScalarMarkup,
4156 name_markup: Option<WrapperMarkup>,
4157 categories_markup: Option<WrapperMarkup>,
4158 values_markup: WrapperMarkup,
4159 bubble_size_markup: Option<WrapperMarkup>,
4160 opaque_name: bool,
4161 opaque_categories: bool,
4162 opaque_bubble_size: bool,
4163 uses_scatter_wrappers: bool,
4164 parsed_from_xml: bool,
4165 raw_attributes: XmlAttributes,
4166 namespace_declarations: XmlAttributes,
4167 raw_children: OrderedRawChildren,
4168}
4169
4170impl Series {
4171 pub fn new(index: u32, order: u32, values: NumericData) -> Self {
4172 Self {
4173 index,
4174 order,
4175 name: None,
4176 categories: None,
4177 values,
4178 bubble_size: None,
4179 sp_pr: None,
4180 data_labels: None,
4181 authored_data_points: Vec::new(),
4182 index_markup: ScalarMarkup::default(),
4183 order_markup: ScalarMarkup::default(),
4184 name_markup: None,
4185 categories_markup: None,
4186 values_markup: WrapperMarkup::default(),
4187 bubble_size_markup: None,
4188 opaque_name: false,
4189 opaque_categories: false,
4190 opaque_bubble_size: false,
4191 uses_scatter_wrappers: false,
4192 parsed_from_xml: false,
4193 raw_attributes: Vec::new(),
4194 namespace_declarations: Vec::new(),
4195 raw_children: OrderedRawChildren::default(),
4196 }
4197 }
4198
4199 pub fn from_xml(xml: &[u8]) -> Result<Self> {
4200 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
4201 }
4202
4203 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
4204 let mut reader = Reader::from_reader(xml);
4205 let mut buffer = Vec::new();
4206 loop {
4207 match reader
4208 .read_event_into(&mut buffer)
4209 .map_err(OxmlError::from)?
4210 {
4211 Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4212 chart_root_prefix(&element)?;
4213 if !element_is_in_namespace(&element, C_NS, inherited)? {
4214 return Err(ChartError::UnexpectedElement(element_name(&element)));
4215 }
4216 return Self::from_element(&mut reader, &element, inherited);
4217 }
4218 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4219 return Err(ChartError::MissingElement("c:idx".to_owned()));
4220 }
4221 Event::Start(element) | Event::Empty(element) => {
4222 return Err(ChartError::UnexpectedElement(element_name(&element)));
4223 }
4224 Event::Eof => return Err(ChartError::MissingElement("c:ser".to_owned())),
4225 _ => {}
4226 }
4227 buffer.clear();
4228 }
4229 }
4230
4231 fn from_element(
4232 reader: &mut Reader<&[u8]>,
4233 start: &BytesStart<'_>,
4234 inherited: &NamespaceBindings,
4235 ) -> Result<Self> {
4236 reject_conflicting_prefix(start, b"a", A_NS)?;
4237 let namespaces = chart_bindings(inherited, start)?;
4238 require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
4239 require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
4240 require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
4241 let (raw_attributes, _) =
4242 capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
4243 let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
4244 let mut state = SeriesParseState::default();
4245 let mut buffer = Vec::new();
4246 loop {
4247 match reader
4248 .read_event_into(&mut buffer)
4249 .map_err(OxmlError::from)?
4250 {
4251 Event::Start(element) => {
4252 let name = chart_child_local(&element, &namespaces)?;
4253 let raw = capture_element(reader, &element)?;
4254 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
4255 }
4256 Event::Empty(element) => {
4257 let name = chart_child_local(&element, &namespaces)?;
4258 let raw = capture_empty_element(&element)?;
4259 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
4260 }
4261 event @ (Event::Text(_)
4262 | Event::CData(_)
4263 | Event::Comment(_)
4264 | Event::PI(_)
4265 | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
4266 Event::End(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4267 break;
4268 }
4269 Event::Eof => return Err(missing_end("c:ser")),
4270 _ => {}
4271 }
4272 buffer.clear();
4273 }
4274 state.finish(raw_attributes, namespace_declarations)
4275 }
4276
4277 pub fn to_xml(&self) -> Result<Vec<u8>> {
4278 self.to_xml_for_plot(self.uses_scatter_wrappers)
4279 }
4280
4281 fn to_xml_for_plot(&self, scatter: bool) -> Result<Vec<u8>> {
4282 if self.opaque_name && self.name.is_some() {
4283 return Err(ChartError::DuplicateElement("c:tx".to_owned()));
4284 }
4285 if self.opaque_categories && self.categories.is_some() {
4286 return Err(ChartError::DuplicateElement("c:cat".to_owned()));
4287 }
4288 if self.opaque_bubble_size && self.bubble_size.is_some() {
4289 return Err(ChartError::DuplicateElement("c:bubbleSize".to_owned()));
4290 }
4291 self.values.validate()?;
4292 if let Some(name) = &self.name {
4293 name.validate()?;
4294 }
4295 if let Some(categories) = &self.categories {
4296 if scatter && !matches!(categories, AxisData::Numeric(_)) {
4297 return Err(ChartError::InvalidValue {
4298 element: "c:xVal".to_owned(),
4299 value: "scatter x values must use a numeric cache".to_owned(),
4300 });
4301 }
4302 match categories {
4303 AxisData::String(reference) => reference.validate()?,
4304 AxisData::Numeric(reference) => reference.validate()?,
4305 }
4306 }
4307 if scatter && self.categories.is_none() {
4308 return Err(ChartError::MissingElement("c:xVal".to_owned()));
4309 }
4310 if let Some(size) = &self.bubble_size {
4311 size.validate()?;
4312 }
4313 if let Some(labels) = &self.data_labels {
4314 labels.validate()?;
4315 }
4316 let mut writer = Writer::new(Vec::new());
4317 let mut start = BytesStart::new("c:ser");
4318 start.push_attribute(("xmlns:c", C_NS));
4319 start.push_attribute(("xmlns:a", A_NS));
4320 start.push_attribute(("xmlns:r", R_NS));
4321 push_attributes(&mut start, &self.namespace_declarations);
4322 push_attributes(&mut start, &self.raw_attributes);
4323 writer
4324 .write_event(Event::Start(start))
4325 .map_err(OxmlError::from)?;
4326 emit_raw(&mut writer, self.raw_children.at(0))?;
4327 write_scalar(
4328 &mut writer,
4329 "c:idx",
4330 &self.index.to_string(),
4331 Some(&self.index_markup),
4332 )?;
4333 emit_raw(&mut writer, self.raw_children.at(1))?;
4334 write_scalar(
4335 &mut writer,
4336 "c:order",
4337 &self.order.to_string(),
4338 Some(&self.order_markup),
4339 )?;
4340 emit_raw(&mut writer, self.raw_children.at(2))?;
4341 if let Some(name) = &self.name {
4342 write_wrapper_start(
4343 &mut writer,
4344 "c:tx",
4345 self.name_markup
4346 .as_ref()
4347 .unwrap_or(&WrapperMarkup::default()),
4348 )?;
4349 name.write_xml(&mut writer, false)?;
4350 write_wrapper_end(
4351 &mut writer,
4352 "c:tx",
4353 self.name_markup
4354 .as_ref()
4355 .unwrap_or(&WrapperMarkup::default()),
4356 )?;
4357 }
4358 emit_raw(&mut writer, self.raw_children.at(3))?;
4359 if let Some(properties) = &self.sp_pr {
4360 properties.write_xml_as(&mut writer, "c:spPr")?;
4361 }
4362 emit_raw(&mut writer, self.raw_children.at(4))?;
4363 for point in &self.authored_data_points {
4364 point.write_xml(&mut writer)?;
4365 }
4366 if let Some(labels) = &self.data_labels {
4367 labels.write_xml(&mut writer, false)?;
4368 }
4369 emit_raw(&mut writer, self.raw_children.at(5))?;
4370 if let Some(categories) = &self.categories {
4371 let default_markup = WrapperMarkup::default();
4372 let markup = self.categories_markup.as_ref().unwrap_or(&default_markup);
4373 write_wrapper_start(
4374 &mut writer,
4375 if scatter { "c:xVal" } else { "c:cat" },
4376 markup,
4377 )?;
4378 match categories {
4379 AxisData::String(reference) => reference.write_xml(&mut writer, false)?,
4380 AxisData::Numeric(reference) => reference.write_xml(&mut writer, false)?,
4381 }
4382 write_wrapper_end(
4383 &mut writer,
4384 if scatter { "c:xVal" } else { "c:cat" },
4385 markup,
4386 )?;
4387 }
4388 emit_raw(&mut writer, self.raw_children.at(6))?;
4389 write_wrapper_start(
4390 &mut writer,
4391 if scatter { "c:yVal" } else { "c:val" },
4392 &self.values_markup,
4393 )?;
4394 self.values.write_xml(&mut writer, false)?;
4395 write_wrapper_end(
4396 &mut writer,
4397 if scatter { "c:yVal" } else { "c:val" },
4398 &self.values_markup,
4399 )?;
4400 emit_raw(&mut writer, self.raw_children.at(7))?;
4401 if let Some(size) = &self.bubble_size {
4402 let default_markup = WrapperMarkup::default();
4403 let markup = self.bubble_size_markup.as_ref().unwrap_or(&default_markup);
4404 write_wrapper_start(&mut writer, "c:bubbleSize", markup)?;
4405 size.write_xml(&mut writer, false)?;
4406 write_wrapper_end(&mut writer, "c:bubbleSize", markup)?;
4407 }
4408 emit_raw(&mut writer, self.raw_children.at(8))?;
4409 writer
4410 .write_event(Event::End(BytesEnd::new("c:ser")))
4411 .map_err(OxmlError::from)?;
4412 Ok(writer.into_inner())
4413 }
4414
4415 pub fn raw_children(&self) -> &OrderedRawChildren {
4416 &self.raw_children
4417 }
4418}
4419
4420#[derive(Default)]
4421struct SeriesParseState {
4422 index: Option<(u32, ScalarMarkup)>,
4423 order: Option<(u32, ScalarMarkup)>,
4424 name: Option<(StringRef, WrapperMarkup)>,
4425 categories: Option<(AxisData, WrapperMarkup)>,
4426 values: Option<(NumericData, WrapperMarkup)>,
4427 bubble_size: Option<(NumericData, WrapperMarkup)>,
4428 data_labels: Option<CT_DLbls>,
4429 name_seen: bool,
4430 categories_seen: bool,
4431 bubble_size_seen: bool,
4432 opaque_name: bool,
4433 opaque_categories: bool,
4434 opaque_bubble_size: bool,
4435 saw_standard_wrapper: bool,
4436 saw_scatter_wrapper: bool,
4437 sp_pr: Option<CT_ShapeProperties>,
4438 raw_children: OrderedRawChildren,
4439 boundary: usize,
4440}
4441
4442impl SeriesParseState {
4443 fn capture_event(&mut self, raw: Vec<u8>) {
4444 self.raw_children.push(self.boundary, raw);
4445 }
4446
4447 fn parse_child(
4448 &mut self,
4449 name: &[u8],
4450 raw: Vec<u8>,
4451 namespaces: &NamespaceBindings,
4452 ) -> Result<()> {
4453 match name {
4454 b"idx" => {
4455 set_once(&mut self.index, parse_u32_scalar(&raw, "idx")?, "c:idx")?;
4456 self.boundary = self.boundary.max(1);
4457 }
4458 b"order" => {
4459 set_once(&mut self.order, parse_u32_scalar(&raw, "order")?, "c:order")?;
4460 self.boundary = self.boundary.max(2);
4461 }
4462 b"tx" => {
4463 mark_once(&mut self.name_seen, "c:tx")?;
4464 let parsed = parse_wrapper(&raw, b"tx", &[b"strRef"], namespaces)?;
4465 if let Some((_, reference)) = parsed.choice {
4466 self.name = Some((
4467 StringRef::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4468 parsed.markup,
4469 ));
4470 } else {
4471 self.opaque_name = true;
4472 self.raw_children.push(2, raw);
4473 }
4474 self.boundary = self.boundary.max(3);
4475 }
4476 b"spPr" => {
4477 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
4478 let properties = CT_ShapeProperties::from_xml(&raw)?;
4479 let mut writer = Writer::new(Vec::new());
4480 properties.write_xml_as(&mut writer, "c:spPr")?;
4481 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
4482 set_once(&mut self.sp_pr, properties, "c:spPr")?;
4483 self.boundary = self.boundary.max(4);
4484 }
4485 b"dLbls" => {
4486 set_once(
4487 &mut self.data_labels,
4488 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
4489 "c:dLbls",
4490 )?;
4491 self.boundary = self.boundary.max(5);
4492 }
4493 b"cat" => {
4494 self.saw_standard_wrapper = true;
4495 mark_once(&mut self.categories_seen, "c:cat")?;
4496 let parsed = parse_wrapper(&raw, b"cat", &[b"strRef", b"numRef"], namespaces)?;
4497 if let Some((choice, reference)) = parsed.choice {
4498 let categories = if choice == b"strRef" {
4499 AxisData::String(StringRef::from_xml_with_namespaces(
4500 &reference,
4501 &parsed.namespaces,
4502 )?)
4503 } else {
4504 AxisData::Numeric(NumericData::from_xml_with_namespaces(
4505 &reference,
4506 &parsed.namespaces,
4507 )?)
4508 };
4509 self.categories = Some((categories, parsed.markup));
4510 } else {
4511 self.opaque_categories = true;
4512 self.raw_children.push(5, raw);
4513 }
4514 self.boundary = self.boundary.max(6);
4515 }
4516 b"xVal" => {
4517 self.saw_scatter_wrapper = true;
4518 mark_once(&mut self.categories_seen, "c:xVal")?;
4519 let parsed = parse_wrapper(&raw, b"xVal", &[b"numRef"], namespaces)?;
4520 let (_, reference) = parsed
4521 .choice
4522 .ok_or_else(|| ChartError::MissingElement("c:xVal/c:numRef".to_owned()))?;
4523 self.categories = Some((
4524 AxisData::Numeric(NumericData::from_xml_with_namespaces(
4525 &reference,
4526 &parsed.namespaces,
4527 )?),
4528 parsed.markup,
4529 ));
4530 self.boundary = self.boundary.max(6);
4531 }
4532 b"val" | b"yVal" => {
4533 if name == b"yVal" {
4534 self.saw_scatter_wrapper = true;
4535 } else {
4536 self.saw_standard_wrapper = true;
4537 }
4538 let wrapper = if name == b"yVal" {
4539 b"yVal".as_slice()
4540 } else {
4541 b"val".as_slice()
4542 };
4543 let parsed = parse_wrapper(&raw, wrapper, &[b"numRef"], namespaces)?;
4544 let (_, reference) = parsed.choice.ok_or_else(|| {
4545 ChartError::MissingElement(format!(
4546 "c:{}/c:numRef",
4547 String::from_utf8_lossy(wrapper)
4548 ))
4549 })?;
4550 set_once(
4551 &mut self.values,
4552 (
4553 NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4554 parsed.markup,
4555 ),
4556 "c:val or c:yVal",
4557 )?;
4558 self.boundary = self.boundary.max(7);
4559 }
4560 b"bubbleSize" => {
4561 mark_once(&mut self.bubble_size_seen, "c:bubbleSize")?;
4562 let parsed = parse_wrapper(&raw, b"bubbleSize", &[b"numRef"], namespaces)?;
4563 if let Some((_, reference)) = parsed.choice {
4564 self.bubble_size = Some((
4565 NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4566 parsed.markup,
4567 ));
4568 } else {
4569 self.opaque_bubble_size = true;
4570 self.raw_children.push(7, raw);
4571 }
4572 self.boundary = self.boundary.max(8);
4573 }
4574 _ => {
4575 let boundary = series_raw_boundary(name, self.boundary);
4576 self.raw_children.push(boundary, raw);
4577 self.boundary = self.boundary.max(boundary);
4578 }
4579 }
4580 Ok(())
4581 }
4582
4583 fn finish(
4584 self,
4585 raw_attributes: XmlAttributes,
4586 namespace_declarations: XmlAttributes,
4587 ) -> Result<Series> {
4588 if self.saw_standard_wrapper && self.saw_scatter_wrapper {
4589 return Err(ChartError::InvalidValue {
4590 element: "c:ser".to_owned(),
4591 value: "category/value and x/y wrappers cannot be combined".to_owned(),
4592 });
4593 }
4594 let (index, index_markup) = self
4595 .index
4596 .ok_or_else(|| ChartError::MissingElement("c:idx".to_owned()))?;
4597 let (order, order_markup) = self
4598 .order
4599 .ok_or_else(|| ChartError::MissingElement("c:order".to_owned()))?;
4600 let (values, values_markup) = self
4601 .values
4602 .ok_or_else(|| ChartError::MissingElement("c:val".to_owned()))?;
4603 let (name, name_markup) = self
4604 .name
4605 .map(|(value, markup)| (Some(value), Some(markup)))
4606 .unwrap_or((None, None));
4607 let (categories, categories_markup) = self
4608 .categories
4609 .map(|(value, markup)| (Some(value), Some(markup)))
4610 .unwrap_or((None, None));
4611 let (bubble_size, bubble_size_markup) = self
4612 .bubble_size
4613 .map(|(value, markup)| (Some(value), Some(markup)))
4614 .unwrap_or((None, None));
4615 Ok(Series {
4616 index,
4617 order,
4618 name,
4619 categories,
4620 values,
4621 bubble_size,
4622 sp_pr: self.sp_pr,
4623 data_labels: self.data_labels,
4624 authored_data_points: Vec::new(),
4625 index_markup,
4626 order_markup,
4627 name_markup,
4628 categories_markup,
4629 values_markup,
4630 bubble_size_markup,
4631 opaque_name: self.opaque_name,
4632 opaque_categories: self.opaque_categories,
4633 opaque_bubble_size: self.opaque_bubble_size,
4634 uses_scatter_wrappers: self.saw_scatter_wrapper,
4635 parsed_from_xml: true,
4636 raw_attributes,
4637 namespace_declarations,
4638 raw_children: raw_children_in_schema_order(&self.raw_children, 8),
4639 })
4640 }
4641}
4642
4643fn series_raw_boundary(name: &[u8], current: usize) -> usize {
4644 match name {
4645 b"marker" | b"invertIfNegative" | b"pictureOptions" | b"explosion" | b"dPt" => 4,
4646 b"trendline" | b"errBars" => 5,
4647 b"shape" | b"smooth" => 7,
4648 b"extLst" => 8,
4649 _ => current,
4650 }
4651}
4652
4653struct ParsedReference {
4654 formula: String,
4655 format_code: Option<String>,
4656 values: Vec<String>,
4657 markup: ReferenceMarkup,
4658}
4659
4660fn parse_reference(
4661 xml: &[u8],
4662 reference_local: &[u8],
4663 cache_local: &[u8],
4664 numeric: bool,
4665 inherited: &NamespaceBindings,
4666) -> Result<ParsedReference> {
4667 let mut reader = Reader::from_reader(xml);
4668 let mut buffer = Vec::new();
4669 loop {
4670 match reader
4671 .read_event_into(&mut buffer)
4672 .map_err(OxmlError::from)?
4673 {
4674 Event::Start(element)
4675 if matches_local_name(element.name().as_ref(), reference_local) =>
4676 {
4677 let namespaces = typed_rewrite_bindings(&element, inherited)?;
4678 let raw_attributes = capture_fixed_attributes(&element, &["xmlns:c"])?;
4679 let mut formula: Option<(String, TextMarkup)> = None;
4680 let mut cache: Option<(Option<String>, Vec<String>, CacheMarkup)> = None;
4681 let mut raw_children = OrderedRawChildren::default();
4682 let mut boundary = 0usize;
4683 let mut inner = Vec::new();
4684 loop {
4685 match reader
4686 .read_event_into(&mut inner)
4687 .map_err(OxmlError::from)?
4688 {
4689 Event::Start(child) => {
4690 let name = chart_child_local(&child, &namespaces)?;
4691 let raw = capture_element(&mut reader, &child)?;
4692 match name.as_deref().unwrap_or_default() {
4693 b"f" => {
4694 set_once(
4695 &mut formula,
4696 parse_text_element(&raw, b"f", &namespaces)?,
4697 "c:f",
4698 )?;
4699 boundary = boundary.max(1);
4700 }
4701 name if name == cache_local => {
4702 set_once(
4703 &mut cache,
4704 parse_cache(&raw, cache_local, numeric, &namespaces)?,
4705 &format!("c:{}", String::from_utf8_lossy(cache_local)),
4706 )?;
4707 boundary = boundary.max(2);
4708 }
4709 _ => raw_children.push(boundary, raw),
4710 }
4711 }
4712 Event::Empty(child) => {
4713 let name = chart_child_local(&child, &namespaces)?;
4714 let raw = capture_empty_element(&child)?;
4715 match name.as_deref().unwrap_or_default() {
4716 b"f" => {
4717 set_once(
4718 &mut formula,
4719 parse_text_element(&raw, b"f", &namespaces)?,
4720 "c:f",
4721 )?;
4722 boundary = boundary.max(1);
4723 }
4724 name if name == cache_local => {
4725 set_once(
4726 &mut cache,
4727 parse_cache(&raw, cache_local, numeric, &namespaces)?,
4728 &format!("c:{}", String::from_utf8_lossy(cache_local)),
4729 )?;
4730 boundary = boundary.max(2);
4731 }
4732 _ => raw_children.push(boundary, raw),
4733 }
4734 }
4735 event @ (Event::Text(_)
4736 | Event::CData(_)
4737 | Event::Comment(_)
4738 | Event::PI(_)
4739 | Event::GeneralRef(_)) => {
4740 raw_children.push(boundary, capture_event(event)?);
4741 }
4742 Event::End(end)
4743 if matches_local_name(end.name().as_ref(), reference_local) =>
4744 {
4745 let (formula, formula_markup) = formula
4746 .ok_or_else(|| ChartError::MissingElement("c:f".to_owned()))?;
4747 validate_formula(&formula, "c:f")?;
4748 let (format_code, values, cache_markup) = cache.ok_or_else(|| {
4749 ChartError::MissingElement(format!(
4750 "c:{}",
4751 String::from_utf8_lossy(cache_local)
4752 ))
4753 })?;
4754 let markup = ReferenceMarkup {
4755 raw_attributes,
4756 raw_children: raw_children_in_schema_order(&raw_children, 2),
4757 formula: formula_markup,
4758 cache_attributes: cache_markup.raw_attributes,
4759 cache_children: cache_markup.raw_children,
4760 format_code: cache_markup.format_code,
4761 format_code_omitted: cache_markup.format_code_omitted,
4762 point_count: cache_markup.point_count,
4763 declared_point_count: cache_markup.declared_point_count,
4764 point_indexes: cache_markup.point_indexes,
4765 points: cache_markup.points,
4766 };
4767 return Ok(ParsedReference {
4768 formula,
4769 format_code,
4770 values,
4771 markup,
4772 });
4773 }
4774 Event::Eof => {
4775 return Err(missing_end(&format!(
4776 "c:{}",
4777 String::from_utf8_lossy(reference_local)
4778 )));
4779 }
4780 _ => {}
4781 }
4782 inner.clear();
4783 }
4784 }
4785 Event::Empty(element)
4786 if matches_local_name(element.name().as_ref(), reference_local) =>
4787 {
4788 return Err(ChartError::MissingElement("c:f".to_owned()));
4789 }
4790 Event::Start(element) | Event::Empty(element) => {
4791 return Err(ChartError::UnexpectedElement(element_name(&element)));
4792 }
4793 Event::Eof => {
4794 return Err(ChartError::MissingElement(format!(
4795 "c:{}",
4796 String::from_utf8_lossy(reference_local)
4797 )));
4798 }
4799 _ => {}
4800 }
4801 buffer.clear();
4802 }
4803}
4804
4805struct CacheMarkup {
4806 raw_attributes: XmlAttributes,
4807 raw_children: OrderedRawChildren,
4808 format_code: Option<TextMarkup>,
4809 format_code_omitted: bool,
4810 point_count: ScalarMarkup,
4811 declared_point_count: Option<u32>,
4812 point_indexes: Vec<u32>,
4813 points: Vec<PointMarkup>,
4814}
4815
4816fn parse_cache(
4817 xml: &[u8],
4818 cache_local: &[u8],
4819 numeric: bool,
4820 inherited: &NamespaceBindings,
4821) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
4822 let mut reader = Reader::from_reader(xml);
4823 let mut buffer = Vec::new();
4824 loop {
4825 match reader
4826 .read_event_into(&mut buffer)
4827 .map_err(OxmlError::from)?
4828 {
4829 Event::Start(element) if matches_local_name(element.name().as_ref(), cache_local) => {
4830 let namespaces = typed_rewrite_bindings(&element, inherited)?;
4831 let raw_attributes = capture_attributes(&element)?;
4832 let mut state = CacheParseState::new(numeric, namespaces);
4833 let mut inner = Vec::new();
4834 loop {
4835 match reader
4836 .read_event_into(&mut inner)
4837 .map_err(OxmlError::from)?
4838 {
4839 Event::Start(child) => {
4840 let name = chart_child_local(&child, &state.namespaces)?;
4841 let raw = capture_element(&mut reader, &child)?;
4842 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
4843 }
4844 Event::Empty(child) => {
4845 let name = chart_child_local(&child, &state.namespaces)?;
4846 let raw = capture_empty_element(&child)?;
4847 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
4848 }
4849 event @ (Event::Text(_)
4850 | Event::CData(_)
4851 | Event::Comment(_)
4852 | Event::PI(_)
4853 | Event::GeneralRef(_)) => {
4854 state.capture_event(capture_event(event)?);
4855 }
4856 Event::End(end) if matches_local_name(end.name().as_ref(), cache_local) => {
4857 return state.finish(raw_attributes);
4858 }
4859 Event::Eof => {
4860 return Err(missing_end(&format!(
4861 "c:{}",
4862 String::from_utf8_lossy(cache_local)
4863 )));
4864 }
4865 _ => {}
4866 }
4867 inner.clear();
4868 }
4869 }
4870 Event::Empty(element) if matches_local_name(element.name().as_ref(), cache_local) => {
4871 return Err(ChartError::MissingElement("c:ptCount".to_owned()));
4872 }
4873 Event::Start(element) | Event::Empty(element) => {
4874 return Err(ChartError::UnexpectedElement(element_name(&element)));
4875 }
4876 Event::Eof => {
4877 return Err(ChartError::MissingElement(format!(
4878 "c:{}",
4879 String::from_utf8_lossy(cache_local)
4880 )));
4881 }
4882 _ => {}
4883 }
4884 buffer.clear();
4885 }
4886}
4887
4888struct CacheParseState {
4889 numeric: bool,
4890 namespaces: NamespaceBindings,
4891 point_base: usize,
4892 boundary: usize,
4893 format_code: Option<(String, TextMarkup)>,
4894 point_count: Option<(u32, ScalarMarkup)>,
4895 points: Vec<(u32, String, PointMarkup)>,
4896 raw_children: OrderedRawChildren,
4897}
4898
4899impl CacheParseState {
4900 fn new(numeric: bool, namespaces: NamespaceBindings) -> Self {
4901 Self {
4902 numeric,
4903 namespaces,
4904 point_base: usize::from(numeric) + 1,
4905 boundary: 0,
4906 format_code: None,
4907 point_count: None,
4908 points: Vec::new(),
4909 raw_children: OrderedRawChildren::default(),
4910 }
4911 }
4912
4913 fn capture_event(&mut self, raw: Vec<u8>) {
4914 self.raw_children.push(self.boundary, raw);
4915 }
4916
4917 fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
4918 match name {
4919 b"formatCode" if self.numeric => {
4920 set_once(
4921 &mut self.format_code,
4922 parse_text_element(&raw, b"formatCode", &self.namespaces)?,
4923 "c:formatCode",
4924 )?;
4925 self.boundary = self.boundary.max(1);
4926 }
4927 b"ptCount" => {
4928 set_once(
4929 &mut self.point_count,
4930 parse_u32_scalar(&raw, "ptCount")?,
4931 "c:ptCount",
4932 )?;
4933 self.boundary = self.boundary.max(self.point_base);
4934 }
4935 b"pt" => {
4936 self.points.push(parse_point(&raw, &self.namespaces)?);
4937 self.boundary = self.boundary.max(self.point_base + self.points.len());
4938 }
4939 _ => self.raw_children.push(self.boundary, raw),
4940 }
4941 Ok(())
4942 }
4943
4944 fn finish(
4945 self,
4946 raw_attributes: XmlAttributes,
4947 ) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
4948 let (declared, point_count_markup) = self
4949 .point_count
4950 .ok_or_else(|| ChartError::MissingElement("c:ptCount".to_owned()))?;
4951 let actual = u32::try_from(self.points.len()).map_err(|_| ChartError::InvalidValue {
4952 element: "c:ptCount".to_owned(),
4953 value: self.points.len().to_string(),
4954 })?;
4955 if declared < actual {
4956 return Err(invalid_attribute("ptCount", "val", declared.to_string()));
4957 }
4958 let mut values = Vec::with_capacity(self.points.len());
4959 let mut point_indexes = Vec::with_capacity(self.points.len());
4960 let mut point_markup = Vec::with_capacity(self.points.len());
4961 let mut previous = None;
4962 for (index, value, markup) in self.points {
4963 if index >= declared || previous.is_some_and(|last| index <= last) {
4964 return Err(invalid_attribute("pt", "idx", index.to_string()));
4965 }
4966 previous = Some(index);
4967 values.push(value);
4968 point_indexes.push(index);
4969 point_markup.push(markup);
4970 }
4971 let (format_code, format_markup) = self
4972 .format_code
4973 .map(|(value, markup)| (Some(value), Some(markup)))
4974 .unwrap_or((None, None));
4975 let format_code_omitted = self.numeric && format_code.is_none();
4976 Ok((
4977 format_code,
4978 values,
4979 CacheMarkup {
4980 raw_attributes,
4981 raw_children: raw_children_in_schema_order(
4982 &self.raw_children,
4983 self.point_base + point_markup.len(),
4984 ),
4985 format_code: format_markup,
4986 format_code_omitted,
4987 point_count: point_count_markup,
4988 declared_point_count: Some(declared),
4989 point_indexes,
4990 points: point_markup,
4991 },
4992 ))
4993 }
4994}
4995
4996fn parse_point(xml: &[u8], inherited: &NamespaceBindings) -> Result<(u32, String, PointMarkup)> {
4997 let mut reader = Reader::from_reader(xml);
4998 let mut buffer = Vec::new();
4999 loop {
5000 match reader
5001 .read_event_into(&mut buffer)
5002 .map_err(OxmlError::from)?
5003 {
5004 Event::Start(element) if matches_local_name(element.name().as_ref(), b"pt") => {
5005 let namespaces = typed_rewrite_bindings(&element, inherited)?;
5006 let index = required_u32_attribute(&element, "pt", b"idx")?;
5007 let raw_attributes = capture_attributes_except(&element, b"idx")?;
5008 let mut value: Option<(String, TextMarkup)> = None;
5009 let mut raw_children = OrderedRawChildren::default();
5010 let mut boundary = 0usize;
5011 let mut inner = Vec::new();
5012 loop {
5013 match reader
5014 .read_event_into(&mut inner)
5015 .map_err(OxmlError::from)?
5016 {
5017 Event::Start(child) => {
5018 let name = chart_child_local(&child, &namespaces)?;
5019 let raw = capture_element(&mut reader, &child)?;
5020 if name.as_deref() == Some(b"v") {
5021 set_once(
5022 &mut value,
5023 parse_text_element(&raw, b"v", &namespaces)?,
5024 "c:v",
5025 )?;
5026 boundary = 1;
5027 } else {
5028 raw_children.push(boundary, raw);
5029 }
5030 }
5031 Event::Empty(child) => {
5032 let name = chart_child_local(&child, &namespaces)?;
5033 let raw = capture_empty_element(&child)?;
5034 if name.as_deref() == Some(b"v") {
5035 set_once(
5036 &mut value,
5037 parse_text_element(&raw, b"v", &namespaces)?,
5038 "c:v",
5039 )?;
5040 boundary = 1;
5041 } else {
5042 raw_children.push(boundary, raw);
5043 }
5044 }
5045 event @ (Event::Text(_)
5046 | Event::CData(_)
5047 | Event::Comment(_)
5048 | Event::PI(_)
5049 | Event::GeneralRef(_)) => {
5050 raw_children.push(boundary, capture_event(event)?);
5051 }
5052 Event::End(end) if matches_local_name(end.name().as_ref(), b"pt") => {
5053 let (value, value_markup) = value
5054 .ok_or_else(|| ChartError::MissingElement("c:v".to_owned()))?;
5055 return Ok((
5056 index,
5057 value,
5058 PointMarkup {
5059 raw_attributes,
5060 raw_children: raw_children_in_schema_order(&raw_children, 1),
5061 value: value_markup,
5062 },
5063 ));
5064 }
5065 Event::Eof => return Err(missing_end("c:pt")),
5066 _ => {}
5067 }
5068 inner.clear();
5069 }
5070 }
5071 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pt") => {
5072 return Err(ChartError::MissingElement("c:v".to_owned()));
5073 }
5074 Event::Start(element) | Event::Empty(element) => {
5075 return Err(ChartError::UnexpectedElement(element_name(&element)));
5076 }
5077 Event::Eof => return Err(ChartError::MissingElement("c:pt".to_owned())),
5078 _ => {}
5079 }
5080 buffer.clear();
5081 }
5082}
5083
5084fn parse_text_element(
5085 xml: &[u8],
5086 local: &[u8],
5087 inherited: &NamespaceBindings,
5088) -> Result<(String, TextMarkup)> {
5089 let mut reader = Reader::from_reader(xml);
5090 let mut buffer = Vec::new();
5091 loop {
5092 match reader
5093 .read_event_into(&mut buffer)
5094 .map_err(OxmlError::from)?
5095 {
5096 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
5097 typed_rewrite_bindings(&element, inherited)?;
5098 let raw_attributes = capture_attributes(&element)?;
5099 let mut value = String::new();
5100 let mut raw_children = OrderedRawChildren::default();
5101 let mut seen_text = false;
5102 let mut inner = Vec::new();
5103 loop {
5104 match reader
5105 .read_event_into(&mut inner)
5106 .map_err(OxmlError::from)?
5107 {
5108 Event::Text(text) => {
5109 value.push_str(&decode_plain(&text));
5110 seen_text = true;
5111 }
5112 Event::CData(text) => {
5113 let decoded =
5114 text.decode().map_err(|error| ChartError::InvalidValue {
5115 element: format!("c:{}", String::from_utf8_lossy(local)),
5116 value: error.to_string(),
5117 })?;
5118 value.push_str(&decoded);
5119 seen_text = true;
5120 }
5121 Event::GeneralRef(reference) => {
5122 value.push_str(&resolve_entity(&reference));
5123 seen_text = true;
5124 }
5125 event @ (Event::Comment(_) | Event::PI(_)) => {
5126 raw_children.push(usize::from(seen_text), capture_event(event)?);
5127 }
5128 Event::Start(child) | Event::Empty(child) => {
5129 return Err(ChartError::UnexpectedElement(element_name(&child)));
5130 }
5131 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
5132 return Ok((
5133 value,
5134 TextMarkup {
5135 raw_attributes,
5136 raw_children: raw_children_in_schema_order(&raw_children, 1),
5137 },
5138 ));
5139 }
5140 Event::Eof => {
5141 return Err(missing_end(&format!(
5142 "c:{}",
5143 String::from_utf8_lossy(local)
5144 )));
5145 }
5146 _ => {}
5147 }
5148 inner.clear();
5149 }
5150 }
5151 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
5152 typed_rewrite_bindings(&element, inherited)?;
5153 return Ok((
5154 String::new(),
5155 TextMarkup {
5156 raw_attributes: capture_attributes(&element)?,
5157 raw_children: OrderedRawChildren::default(),
5158 },
5159 ));
5160 }
5161 Event::Start(element) | Event::Empty(element) => {
5162 return Err(ChartError::UnexpectedElement(element_name(&element)));
5163 }
5164 Event::Eof => {
5165 return Err(ChartError::MissingElement(format!(
5166 "c:{}",
5167 String::from_utf8_lossy(local)
5168 )));
5169 }
5170 _ => {}
5171 }
5172 buffer.clear();
5173 }
5174}
5175
5176struct ParsedWrapper {
5177 choice: Option<(Vec<u8>, Vec<u8>)>,
5178 markup: WrapperMarkup,
5179 namespaces: NamespaceBindings,
5180}
5181
5182fn parse_wrapper(
5183 xml: &[u8],
5184 local: &[u8],
5185 choices: &[&[u8]],
5186 inherited: &NamespaceBindings,
5187) -> Result<ParsedWrapper> {
5188 let mut reader = Reader::from_reader(xml);
5189 let mut buffer = Vec::new();
5190 loop {
5191 match reader
5192 .read_event_into(&mut buffer)
5193 .map_err(OxmlError::from)?
5194 {
5195 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
5196 let namespaces = typed_rewrite_bindings(&element, inherited)?;
5197 let raw_attributes = capture_attributes(&element)?;
5198 let mut choice = None;
5199 let mut raw_children = OrderedRawChildren::default();
5200 let mut boundary = 0usize;
5201 let mut inner = Vec::new();
5202 loop {
5203 match reader
5204 .read_event_into(&mut inner)
5205 .map_err(OxmlError::from)?
5206 {
5207 Event::Start(child) => {
5208 let name = chart_child_local(&child, &namespaces)?;
5209 let raw = capture_element(&mut reader, &child)?;
5210 if let Some(name) =
5211 name.filter(|name| choices.contains(&name.as_slice()))
5212 {
5213 if choice.is_some() {
5214 return Err(ChartError::DuplicateElement(format!(
5215 "c:{} reference",
5216 String::from_utf8_lossy(local)
5217 )));
5218 }
5219 choice = Some((name, raw));
5220 boundary = 1;
5221 } else {
5222 raw_children.push(boundary, raw);
5223 }
5224 }
5225 Event::Empty(child) => {
5226 let name = chart_child_local(&child, &namespaces)?;
5227 let raw = capture_empty_element(&child)?;
5228 if let Some(name) =
5229 name.filter(|name| choices.contains(&name.as_slice()))
5230 {
5231 if choice.is_some() {
5232 return Err(ChartError::DuplicateElement(format!(
5233 "c:{} reference",
5234 String::from_utf8_lossy(local)
5235 )));
5236 }
5237 choice = Some((name, raw));
5238 boundary = 1;
5239 } else {
5240 raw_children.push(boundary, raw);
5241 }
5242 }
5243 event @ (Event::Text(_)
5244 | Event::CData(_)
5245 | Event::Comment(_)
5246 | Event::PI(_)
5247 | Event::GeneralRef(_)) => {
5248 raw_children.push(boundary, capture_event(event)?);
5249 }
5250 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
5251 return Ok(ParsedWrapper {
5252 choice,
5253 markup: WrapperMarkup {
5254 raw_attributes,
5255 raw_children: raw_children_in_schema_order(&raw_children, 1),
5256 },
5257 namespaces,
5258 });
5259 }
5260 Event::Eof => {
5261 return Err(missing_end(&format!(
5262 "c:{}",
5263 String::from_utf8_lossy(local)
5264 )));
5265 }
5266 _ => {}
5267 }
5268 inner.clear();
5269 }
5270 }
5271 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
5272 let namespaces = chart_bindings(inherited, &element)?;
5273 return Ok(ParsedWrapper {
5274 choice: None,
5275 markup: WrapperMarkup {
5276 raw_attributes: capture_attributes(&element)?,
5277 raw_children: OrderedRawChildren::default(),
5278 },
5279 namespaces,
5280 });
5281 }
5282 Event::Start(element) | Event::Empty(element) => {
5283 return Err(ChartError::UnexpectedElement(element_name(&element)));
5284 }
5285 Event::Eof => {
5286 return Err(ChartError::MissingElement(format!(
5287 "c:{}",
5288 String::from_utf8_lossy(local)
5289 )));
5290 }
5291 _ => {}
5292 }
5293 buffer.clear();
5294 }
5295}
5296
5297fn write_string_cache(
5298 writer: &mut Writer<Vec<u8>>,
5299 values: &[String],
5300 markup: &ReferenceMarkup,
5301) -> Result<()> {
5302 let (declared_count, indexes) = cache_layout(markup, values.len())?;
5303 let mut start = BytesStart::new("c:strCache");
5304 push_attributes(&mut start, &markup.cache_attributes);
5305 writer
5306 .write_event(Event::Start(start))
5307 .map_err(OxmlError::from)?;
5308 emit_raw(writer, markup.cache_children.at(0))?;
5309 write_scalar(
5310 writer,
5311 "c:ptCount",
5312 &declared_count.to_string(),
5313 Some(&markup.point_count),
5314 )?;
5315 emit_cache_point_raw(writer, markup, 1, values.len(), 0)?;
5316 for (position, value) in values.iter().enumerate() {
5317 let default_markup = PointMarkup::default();
5318 write_point(
5319 writer,
5320 indexes[position],
5321 value,
5322 markup.points.get(position).unwrap_or(&default_markup),
5323 )?;
5324 emit_cache_point_raw(writer, markup, 1, values.len(), position + 1)?;
5325 }
5326 writer
5327 .write_event(Event::End(BytesEnd::new("c:strCache")))
5328 .map_err(OxmlError::from)?;
5329 Ok(())
5330}
5331
5332fn write_numeric_cache(
5333 writer: &mut Writer<Vec<u8>>,
5334 data: &NumericData,
5335 markup: &ReferenceMarkup,
5336) -> Result<()> {
5337 let (declared_count, indexes) = cache_layout(markup, data.values.len())?;
5338 let mut start = BytesStart::new("c:numCache");
5339 push_attributes(&mut start, &markup.cache_attributes);
5340 writer
5341 .write_event(Event::Start(start))
5342 .map_err(OxmlError::from)?;
5343 emit_raw(writer, markup.cache_children.at(0))?;
5344 if !markup.format_code_omitted || data.format_code != "General" {
5345 let default_format_markup = TextMarkup::default();
5346 write_text(
5347 writer,
5348 "c:formatCode",
5349 &data.format_code,
5350 markup
5351 .format_code
5352 .as_ref()
5353 .unwrap_or(&default_format_markup),
5354 )?;
5355 }
5356 emit_raw(writer, markup.cache_children.at(1))?;
5357 write_scalar(
5358 writer,
5359 "c:ptCount",
5360 &declared_count.to_string(),
5361 Some(&markup.point_count),
5362 )?;
5363 emit_cache_point_raw(writer, markup, 2, data.values.len(), 0)?;
5364 for (position, value) in data.values.iter().enumerate() {
5365 let default_markup = PointMarkup::default();
5366 write_point(
5367 writer,
5368 indexes[position],
5369 &value.to_string(),
5370 markup.points.get(position).unwrap_or(&default_markup),
5371 )?;
5372 emit_cache_point_raw(writer, markup, 2, data.values.len(), position + 1)?;
5373 }
5374 writer
5375 .write_event(Event::End(BytesEnd::new("c:numCache")))
5376 .map_err(OxmlError::from)?;
5377 Ok(())
5378}
5379
5380fn emit_cache_point_raw(
5381 writer: &mut Writer<Vec<u8>>,
5382 markup: &ReferenceMarkup,
5383 point_base: usize,
5384 current_count: usize,
5385 completed_points: usize,
5386) -> Result<()> {
5387 let original_count = markup.points.len();
5388 if current_count == 0 {
5389 for boundary in point_base..=point_base + original_count {
5390 emit_raw(writer, markup.cache_children.at(boundary))?;
5391 }
5392 } else if completed_points == 0 {
5393 if original_count > 0 {
5394 emit_raw(writer, markup.cache_children.at(point_base))?;
5395 }
5396 } else if completed_points < current_count {
5397 if completed_points < original_count {
5398 emit_raw(
5399 writer,
5400 markup.cache_children.at(point_base + completed_points),
5401 )?;
5402 }
5403 } else {
5404 let first_tail = if original_count == 0 {
5405 point_base
5406 } else {
5407 point_base + current_count.min(original_count)
5408 };
5409 for boundary in first_tail..=point_base + original_count {
5410 emit_raw(writer, markup.cache_children.at(boundary))?;
5411 }
5412 }
5413 Ok(())
5414}
5415
5416fn write_point(
5417 writer: &mut Writer<Vec<u8>>,
5418 index: u32,
5419 value: &str,
5420 markup: &PointMarkup,
5421) -> Result<()> {
5422 let index = index.to_string();
5423 let mut start = BytesStart::new("c:pt");
5424 start.push_attribute(("idx", index.as_str()));
5425 push_attributes(&mut start, &markup.raw_attributes);
5426 writer
5427 .write_event(Event::Start(start))
5428 .map_err(OxmlError::from)?;
5429 emit_raw(writer, markup.raw_children.at(0))?;
5430 write_text(writer, "c:v", value, &markup.value)?;
5431 emit_raw(writer, markup.raw_children.at(1))?;
5432 writer
5433 .write_event(Event::End(BytesEnd::new("c:pt")))
5434 .map_err(OxmlError::from)?;
5435 Ok(())
5436}
5437
5438fn cache_layout(markup: &ReferenceMarkup, value_count: usize) -> Result<(u32, Vec<u32>)> {
5439 if markup.point_indexes.len() == value_count
5440 && let Some(declared) = markup.declared_point_count
5441 {
5442 return Ok((declared, markup.point_indexes.clone()));
5443 }
5444 let declared = u32::try_from(value_count).map_err(|_| ChartError::InvalidValue {
5445 element: "c:ptCount".to_owned(),
5446 value: value_count.to_string(),
5447 })?;
5448 Ok((declared, (0..declared).collect()))
5449}
5450
5451fn write_text(
5452 writer: &mut Writer<Vec<u8>>,
5453 tag: &str,
5454 value: &str,
5455 markup: &TextMarkup,
5456) -> Result<()> {
5457 let mut start = BytesStart::new(tag);
5458 push_attributes(&mut start, &markup.raw_attributes);
5459 writer
5460 .write_event(Event::Start(start))
5461 .map_err(OxmlError::from)?;
5462 emit_raw(writer, markup.raw_children.at(0))?;
5463 writer
5464 .write_event(Event::Text(BytesText::new(value)))
5465 .map_err(OxmlError::from)?;
5466 emit_raw(writer, markup.raw_children.at(1))?;
5467 writer
5468 .write_event(Event::End(BytesEnd::new(tag)))
5469 .map_err(OxmlError::from)?;
5470 Ok(())
5471}
5472
5473fn write_wrapper_start(
5474 writer: &mut Writer<Vec<u8>>,
5475 tag: &str,
5476 markup: &WrapperMarkup,
5477) -> Result<()> {
5478 let mut start = BytesStart::new(tag);
5479 push_attributes(&mut start, &markup.raw_attributes);
5480 writer
5481 .write_event(Event::Start(start))
5482 .map_err(OxmlError::from)?;
5483 emit_raw(writer, markup.raw_children.at(0))
5484}
5485
5486fn write_wrapper_end(
5487 writer: &mut Writer<Vec<u8>>,
5488 tag: &str,
5489 markup: &WrapperMarkup,
5490) -> Result<()> {
5491 emit_raw(writer, markup.raw_children.at(1))?;
5492 writer
5493 .write_event(Event::End(BytesEnd::new(tag)))
5494 .map_err(OxmlError::from)?;
5495 Ok(())
5496}
5497
5498fn validate_formula(formula: &str, element: &str) -> Result<()> {
5499 if formula.trim().is_empty() {
5500 return Err(ChartError::InvalidValue {
5501 element: element.to_owned(),
5502 value: formula.to_owned(),
5503 });
5504 }
5505 validate_xml_text(formula, element)
5506}
5507
5508fn validate_format_code(format_code: &str) -> Result<()> {
5509 if format_code.is_empty() {
5510 return Err(ChartError::InvalidValue {
5511 element: "c:formatCode".to_owned(),
5512 value: format_code.to_owned(),
5513 });
5514 }
5515 validate_xml_text(format_code, "c:formatCode")
5516}
5517
5518fn validate_xml_text(value: &str, element: &str) -> Result<()> {
5519 if value.chars().all(|character| {
5520 matches!(
5521 character,
5522 '\u{9}' | '\u{A}' | '\u{D}' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}'
5523 )
5524 }) {
5525 return Ok(());
5526 }
5527 Err(ChartError::InvalidValue {
5528 element: element.to_owned(),
5529 value: value.to_owned(),
5530 })
5531}
5532
5533fn validate_numeric_values(values: &[f64]) -> Result<()> {
5534 if let Some(value) = values.iter().find(|value| !value.is_finite()) {
5535 return Err(ChartError::InvalidValue {
5536 element: "c:v".to_owned(),
5537 value: value.to_string(),
5538 });
5539 }
5540 Ok(())
5541}
5542
5543fn validate_point_count(count: usize, element: &str) -> Result<()> {
5544 u32::try_from(count)
5545 .map(|_| ())
5546 .map_err(|_| ChartError::InvalidValue {
5547 element: element.to_owned(),
5548 value: count.to_string(),
5549 })
5550}
5551
5552fn parse_u32_scalar(xml: &[u8], local: &str) -> Result<(u32, ScalarMarkup)> {
5553 let (value, markup) = scalar_value(xml, local)?;
5554 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
5555 let parsed = value
5556 .parse::<u32>()
5557 .map_err(|_| invalid_attribute(local, "val", value))?;
5558 Ok((parsed, markup))
5559}
5560
5561fn required_u32_attribute(element: &BytesStart<'_>, local: &str, attribute: &[u8]) -> Result<u32> {
5562 let value = attribute_value(element, attribute)?.ok_or_else(|| {
5563 invalid_attribute(
5564 local,
5565 &String::from_utf8_lossy(attribute),
5566 "<missing>".to_owned(),
5567 )
5568 })?;
5569 value
5570 .parse::<u32>()
5571 .map_err(|_| invalid_attribute(local, &String::from_utf8_lossy(attribute), value))
5572}
5573
5574fn chart_root_prefix(element: &BytesStart<'_>) -> Result<Vec<u8>> {
5575 let name = element.name();
5576 let qualified = name.as_ref();
5577 let prefix = qualified
5578 .iter()
5579 .position(|byte| *byte == b':')
5580 .map(|position| qualified[..position].to_vec())
5581 .unwrap_or_default();
5582 let declaration = if prefix.is_empty() {
5583 b"xmlns".to_vec()
5584 } else {
5585 let mut declaration = b"xmlns:".to_vec();
5586 declaration.extend_from_slice(&prefix);
5587 declaration
5588 };
5589 if let Some(namespace) = attribute_value(element, &declaration)?
5590 && namespace != C_NS
5591 {
5592 return Err(invalid_attribute(
5593 &element_name(element),
5594 "namespace",
5595 namespace,
5596 ));
5597 }
5598 reject_conflicting_prefix(element, b"c", C_NS)?;
5599 Ok(prefix)
5600}
5601
5602fn typed_rewrite_bindings(
5603 element: &BytesStart<'_>,
5604 inherited: &NamespaceBindings,
5605) -> Result<NamespaceBindings> {
5606 chart_root_prefix(element)?;
5607 if !element_is_in_namespace(element, C_NS, inherited)? {
5608 return Err(ChartError::UnexpectedElement(element_name(element)));
5609 }
5610 let bindings = chart_bindings(inherited, element)?;
5611 require_fixed_namespace(&bindings, b"c", C_NS, element)?;
5612 Ok(bindings)
5613}
5614
5615fn chart_namespace_defaults() -> NamespaceBindings {
5616 vec![
5617 (b"c".to_vec(), C_NS.to_owned()),
5618 (b"a".to_vec(), A_NS.to_owned()),
5619 (b"r".to_vec(), R_NS.to_owned()),
5620 ]
5621}
5622
5623fn chart_bindings(
5624 inherited: &NamespaceBindings,
5625 element: &BytesStart<'_>,
5626) -> Result<NamespaceBindings> {
5627 let mut bindings = chart_namespace_defaults();
5628 for (prefix, namespace) in inherited
5629 .iter()
5630 .cloned()
5631 .chain(namespace_bindings(element)?)
5632 {
5633 upsert_namespace_binding(&mut bindings, prefix, namespace);
5634 }
5635 Ok(bindings)
5636}
5637
5638fn root_chart_bindings(
5639 xml: &[u8],
5640 local: &[u8],
5641 inherited: &NamespaceBindings,
5642) -> Result<NamespaceBindings> {
5643 let mut reader = Reader::from_reader(xml);
5644 let mut buffer = Vec::new();
5645 loop {
5646 match reader
5647 .read_event_into(&mut buffer)
5648 .map_err(OxmlError::from)?
5649 {
5650 Event::Start(element) | Event::Empty(element)
5651 if matches_local_name(element.name().as_ref(), local) =>
5652 {
5653 if !element_is_in_namespace(&element, C_NS, inherited)? {
5654 return Err(ChartError::UnexpectedElement(element_name(&element)));
5655 }
5656 return chart_bindings(inherited, &element);
5657 }
5658 Event::Start(element) | Event::Empty(element) => {
5659 return Err(ChartError::UnexpectedElement(element_name(&element)));
5660 }
5661 Event::Eof => {
5662 return Err(ChartError::MissingElement(format!(
5663 "c:{}",
5664 String::from_utf8_lossy(local)
5665 )));
5666 }
5667 _ => {}
5668 }
5669 buffer.clear();
5670 }
5671}
5672
5673fn chart_child_local(
5674 element: &BytesStart<'_>,
5675 inherited: &NamespaceBindings,
5676) -> Result<Option<Vec<u8>>> {
5677 Ok(element_is_in_namespace(element, C_NS, inherited)?
5678 .then(|| local_name(element.name().as_ref()).to_vec()))
5679}
5680
5681fn capture_fixed_root_attributes(
5682 start: &BytesStart<'_>,
5683 fixed: &[&str],
5684) -> Result<(XmlAttributes, XmlAttributes)> {
5685 let mut attributes = Vec::new();
5686 let mut namespaces = Vec::new();
5687 for attribute in start.attributes() {
5688 let attribute = attribute.map_err(OxmlError::from)?;
5689 let name = std::str::from_utf8(attribute.key.as_ref())
5690 .map_err(OxmlError::from)?
5691 .to_owned();
5692 let value = attribute
5693 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
5694 .map_err(OxmlError::from)?
5695 .into_owned();
5696 if fixed.contains(&name.as_str()) {
5697 continue;
5698 }
5699 if name == "xmlns" || name.starts_with("xmlns:") {
5700 namespaces.push((name, value));
5701 } else {
5702 attributes.push((name, value));
5703 }
5704 }
5705 Ok((attributes, namespaces))
5706}
5707
5708fn require_fixed_namespace(
5709 bindings: &NamespaceBindings,
5710 prefix: &[u8],
5711 expected: &str,
5712 element: &BytesStart<'_>,
5713) -> Result<()> {
5714 let actual = bindings
5715 .iter()
5716 .find(|(candidate, _)| candidate.as_slice() == prefix)
5717 .map(|(_, namespace)| namespace.as_str());
5718 if actual == Some(expected) {
5719 return Ok(());
5720 }
5721 Err(invalid_attribute(
5722 &element_name(element),
5723 &format!("xmlns:{}", String::from_utf8_lossy(prefix)),
5724 actual.unwrap_or("<missing>").to_owned(),
5725 ))
5726}
5727
5728fn standalone_namespace_declarations(bindings: &NamespaceBindings) -> Result<XmlAttributes> {
5729 bindings
5730 .iter()
5731 .filter(|(prefix, _)| prefix != b"c" && prefix != b"a" && prefix != b"r")
5732 .map(|(prefix, namespace)| {
5733 let prefix = std::str::from_utf8(prefix).map_err(OxmlError::from)?;
5734 let name = if prefix.is_empty() {
5735 "xmlns".to_owned()
5736 } else {
5737 format!("xmlns:{prefix}")
5738 };
5739 Ok((name, namespace.clone()))
5740 })
5741 .collect()
5742}
5743
5744fn capture_fixed_attributes(start: &BytesStart<'_>, fixed: &[&str]) -> Result<XmlAttributes> {
5745 let mut attributes = Vec::new();
5746 for attribute in start.attributes() {
5747 let attribute = attribute.map_err(OxmlError::from)?;
5748 let name = std::str::from_utf8(attribute.key.as_ref())
5749 .map_err(OxmlError::from)?
5750 .to_owned();
5751 if fixed.contains(&name.as_str()) {
5752 continue;
5753 }
5754 let value = attribute
5755 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
5756 .map_err(OxmlError::from)?
5757 .into_owned();
5758 attributes.push((name, value));
5759 }
5760 Ok(attributes)
5761}
5762
5763#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
5768pub struct AxisId(i64);
5769
5770impl AxisId {
5771 pub fn new(value: i64) -> Result<Self> {
5772 if (i64::from(i32::MIN)..=i64::from(u32::MAX)).contains(&value) {
5773 Ok(Self(value))
5774 } else {
5775 Err(ChartError::InvalidValue {
5776 element: "c:axId".to_owned(),
5777 value: value.to_string(),
5778 })
5779 }
5780 }
5781
5782 pub const fn value(self) -> i64 {
5783 self.0
5784 }
5785}
5786
5787#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5789pub enum BarDirection {
5790 Bar,
5791 Column,
5792}
5793
5794impl BarDirection {
5795 fn parse(value: &str) -> Option<Self> {
5796 match value {
5797 "bar" => Some(Self::Bar),
5798 "col" => Some(Self::Column),
5799 _ => None,
5800 }
5801 }
5802
5803 const fn as_str(self) -> &'static str {
5804 match self {
5805 Self::Bar => "bar",
5806 Self::Column => "col",
5807 }
5808 }
5809}
5810
5811#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5813pub enum BarGrouping {
5814 Clustered,
5815 PercentStacked,
5816 Stacked,
5817 Standard,
5818}
5819
5820impl BarGrouping {
5821 fn parse(value: &str) -> Option<Self> {
5822 match value {
5823 "clustered" => Some(Self::Clustered),
5824 "percentStacked" => Some(Self::PercentStacked),
5825 "stacked" => Some(Self::Stacked),
5826 "standard" => Some(Self::Standard),
5827 _ => None,
5828 }
5829 }
5830
5831 const fn as_str(self) -> &'static str {
5832 match self {
5833 Self::Clustered => "clustered",
5834 Self::PercentStacked => "percentStacked",
5835 Self::Stacked => "stacked",
5836 Self::Standard => "standard",
5837 }
5838 }
5839}
5840
5841#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5843pub enum Grouping {
5844 PercentStacked,
5845 Stacked,
5846 Standard,
5847}
5848
5849impl Grouping {
5850 fn parse(value: &str) -> Option<Self> {
5851 match value {
5852 "percentStacked" => Some(Self::PercentStacked),
5853 "stacked" => Some(Self::Stacked),
5854 "standard" => Some(Self::Standard),
5855 _ => None,
5856 }
5857 }
5858
5859 const fn as_str(self) -> &'static str {
5860 match self {
5861 Self::PercentStacked => "percentStacked",
5862 Self::Stacked => "stacked",
5863 Self::Standard => "standard",
5864 }
5865 }
5866}
5867
5868#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5870pub enum ScatterStyle {
5871 Line,
5872 LineMarker,
5873 Marker,
5874 None,
5875 Smooth,
5876 SmoothMarker,
5877}
5878
5879impl ScatterStyle {
5880 fn parse(value: &str) -> Option<Self> {
5881 match value {
5882 "line" => Some(Self::Line),
5883 "lineMarker" => Some(Self::LineMarker),
5884 "marker" => Some(Self::Marker),
5885 "none" => Some(Self::None),
5886 "smooth" => Some(Self::Smooth),
5887 "smoothMarker" => Some(Self::SmoothMarker),
5888 _ => None,
5889 }
5890 }
5891
5892 const fn as_str(self) -> &'static str {
5893 match self {
5894 Self::Line => "line",
5895 Self::LineMarker => "lineMarker",
5896 Self::Marker => "marker",
5897 Self::None => "none",
5898 Self::Smooth => "smooth",
5899 Self::SmoothMarker => "smoothMarker",
5900 }
5901 }
5902}
5903
5904#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5906pub enum RadarStyle {
5907 Filled,
5908 Marker,
5909 Standard,
5910}
5911
5912impl RadarStyle {
5913 fn parse(value: &str) -> Option<Self> {
5914 match value {
5915 "filled" => Some(Self::Filled),
5916 "marker" => Some(Self::Marker),
5917 "standard" => Some(Self::Standard),
5918 _ => None,
5919 }
5920 }
5921
5922 const fn as_str(self) -> &'static str {
5923 match self {
5924 Self::Filled => "filled",
5925 Self::Marker => "marker",
5926 Self::Standard => "standard",
5927 }
5928 }
5929}
5930
5931#[derive(Clone, Debug, PartialEq)]
5933pub enum Plot {
5934 Bar {
5935 direction: BarDirection,
5936 grouping: BarGrouping,
5937 gap_width: u16,
5938 overlap: i8,
5939 series: Vec<Series>,
5940 data_labels: Option<CT_DLbls>,
5941 axis_ids: [AxisId; 2],
5942 },
5943 Line {
5944 grouping: Grouping,
5945 marker: bool,
5946 smooth: bool,
5947 series: Vec<Series>,
5948 data_labels: Option<CT_DLbls>,
5949 axis_ids: [AxisId; 2],
5950 },
5951 Pie {
5952 first_slice_angle: u16,
5953 series: Vec<Series>,
5954 data_labels: Option<CT_DLbls>,
5955 },
5956 Doughnut {
5957 first_slice_angle: u16,
5958 hole_size: u8,
5959 series: Vec<Series>,
5960 data_labels: Option<CT_DLbls>,
5961 },
5962 Area {
5963 grouping: Grouping,
5964 series: Vec<Series>,
5965 data_labels: Option<CT_DLbls>,
5966 axis_ids: [AxisId; 2],
5967 },
5968 Scatter {
5969 style: ScatterStyle,
5970 series: Vec<Series>,
5971 data_labels: Option<CT_DLbls>,
5972 axis_ids: [AxisId; 2],
5973 },
5974 Radar {
5975 style: RadarStyle,
5976 series: Vec<Series>,
5977 data_labels: Option<CT_DLbls>,
5978 axis_ids: [AxisId; 2],
5979 },
5980}
5981
5982impl Plot {
5983 pub fn bar(
5984 direction: BarDirection,
5985 grouping: BarGrouping,
5986 series: Vec<Series>,
5987 axis_ids: [AxisId; 2],
5988 ) -> Result<Self> {
5989 let plot = Self::Bar {
5990 direction,
5991 grouping,
5992 gap_width: 150,
5993 overlap: 0,
5994 series,
5995 data_labels: None,
5996 axis_ids,
5997 };
5998 plot.validate()?;
5999 Ok(plot)
6000 }
6001
6002 pub fn line(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
6003 let plot = Self::Line {
6004 grouping,
6005 marker: false,
6006 smooth: false,
6007 series,
6008 data_labels: None,
6009 axis_ids,
6010 };
6011 plot.validate()?;
6012 Ok(plot)
6013 }
6014
6015 pub fn pie(series: Vec<Series>) -> Result<Self> {
6016 let plot = Self::Pie {
6017 first_slice_angle: 0,
6018 series,
6019 data_labels: None,
6020 };
6021 plot.validate()?;
6022 Ok(plot)
6023 }
6024
6025 pub fn doughnut(series: Vec<Series>) -> Result<Self> {
6026 let plot = Self::Doughnut {
6027 first_slice_angle: 0,
6028 hole_size: 50,
6029 series,
6030 data_labels: None,
6031 };
6032 plot.validate()?;
6033 Ok(plot)
6034 }
6035
6036 pub fn area(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
6037 let plot = Self::Area {
6038 grouping,
6039 series,
6040 data_labels: None,
6041 axis_ids,
6042 };
6043 plot.validate()?;
6044 Ok(plot)
6045 }
6046
6047 pub fn scatter(
6048 style: ScatterStyle,
6049 series: Vec<Series>,
6050 axis_ids: [AxisId; 2],
6051 ) -> Result<Self> {
6052 let plot = Self::Scatter {
6053 style,
6054 series,
6055 data_labels: None,
6056 axis_ids,
6057 };
6058 plot.validate()?;
6059 Ok(plot)
6060 }
6061
6062 pub fn radar(style: RadarStyle, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
6063 let plot = Self::Radar {
6064 style,
6065 series,
6066 data_labels: None,
6067 axis_ids,
6068 };
6069 plot.validate()?;
6070 Ok(plot)
6071 }
6072
6073 fn axis_ids(&self) -> Option<[AxisId; 2]> {
6074 match self {
6075 Self::Bar { axis_ids, .. }
6076 | Self::Line { axis_ids, .. }
6077 | Self::Area { axis_ids, .. }
6078 | Self::Scatter { axis_ids, .. }
6079 | Self::Radar { axis_ids, .. } => Some(*axis_ids),
6080 Self::Pie { .. } | Self::Doughnut { .. } => None,
6081 }
6082 }
6083
6084 fn validate(&self) -> Result<()> {
6085 let (series, labels, axis_ids) = match self {
6086 Self::Bar {
6087 gap_width,
6088 overlap,
6089 series,
6090 data_labels,
6091 axis_ids,
6092 ..
6093 } => {
6094 if *gap_width > 500 {
6095 return Err(ChartError::InvalidValue {
6096 element: "c:gapWidth".to_owned(),
6097 value: gap_width.to_string(),
6098 });
6099 }
6100 if !(-100..=100).contains(overlap) {
6101 return Err(ChartError::InvalidValue {
6102 element: "c:overlap".to_owned(),
6103 value: overlap.to_string(),
6104 });
6105 }
6106 (series, data_labels, Some(axis_ids))
6107 }
6108 Self::Line {
6109 series,
6110 data_labels,
6111 axis_ids,
6112 ..
6113 } => (series, data_labels, Some(axis_ids)),
6114 Self::Pie {
6115 first_slice_angle,
6116 series,
6117 data_labels,
6118 } => {
6119 if *first_slice_angle > 360 {
6120 return Err(ChartError::InvalidValue {
6121 element: "c:firstSliceAng".to_owned(),
6122 value: first_slice_angle.to_string(),
6123 });
6124 }
6125 (series, data_labels, None)
6126 }
6127 Self::Doughnut {
6128 first_slice_angle,
6129 hole_size,
6130 series,
6131 data_labels,
6132 } => {
6133 if *first_slice_angle > 360 {
6134 return Err(ChartError::InvalidValue {
6135 element: "c:firstSliceAng".to_owned(),
6136 value: first_slice_angle.to_string(),
6137 });
6138 }
6139 if !(10..=90).contains(hole_size) {
6140 return Err(ChartError::InvalidValue {
6141 element: "c:holeSize".to_owned(),
6142 value: hole_size.to_string(),
6143 });
6144 }
6145 (series, data_labels, None)
6146 }
6147 Self::Area {
6148 series,
6149 data_labels,
6150 axis_ids,
6151 ..
6152 }
6153 | Self::Radar {
6154 series,
6155 data_labels,
6156 axis_ids,
6157 ..
6158 } => (series, data_labels, Some(axis_ids)),
6159 Self::Scatter {
6160 series,
6161 data_labels,
6162 axis_ids,
6163 ..
6164 } => {
6165 for item in series {
6166 if !matches!(item.categories, Some(AxisData::Numeric(_))) {
6167 return Err(ChartError::InvalidValue {
6168 element: "c:xVal".to_owned(),
6169 value: "scatter series requires numeric x values".to_owned(),
6170 });
6171 }
6172 }
6173 (series, data_labels, Some(axis_ids))
6174 }
6175 };
6176 if series.is_empty() {
6177 return Err(ChartError::MissingElement("c:ser".to_owned()));
6178 }
6179 let scatter_plot = matches!(self, Self::Scatter { .. });
6180 for item in series {
6181 if item.bubble_size.is_some() || item.opaque_bubble_size {
6182 return Err(ChartError::InvalidValue {
6183 element: "c:bubbleSize".to_owned(),
6184 value: "bubble size is only valid in an opaque bubble plot".to_owned(),
6185 });
6186 }
6187 if item.uses_scatter_wrappers && !scatter_plot {
6188 return Err(ChartError::InvalidValue {
6189 element: "c:ser".to_owned(),
6190 value: "x/y wrappers are only valid in scatter plots".to_owned(),
6191 });
6192 }
6193 if scatter_plot && item.parsed_from_xml && !item.uses_scatter_wrappers {
6194 return Err(ChartError::InvalidValue {
6195 element: "c:ser".to_owned(),
6196 value: "scatter plots require xVal/yVal wrappers".to_owned(),
6197 });
6198 }
6199 }
6200 if let Some(axis_ids) = axis_ids {
6201 if axis_ids[0] == axis_ids[1] {
6202 return Err(ChartError::DuplicateElement(format!(
6203 "c:axId {}",
6204 axis_ids[0].value()
6205 )));
6206 }
6207 for id in axis_ids {
6208 AxisId::new(id.value())?;
6209 }
6210 }
6211 if let Some(labels) = labels {
6212 labels.validate()?;
6213 }
6214 for item in series {
6215 item.values.validate()?;
6216 }
6217 Ok(())
6218 }
6219}
6220
6221#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
6223pub enum AxisKind {
6224 Category,
6225 Value,
6226 Date,
6227 Series,
6228}
6229
6230impl AxisKind {
6231 fn parse(local: &[u8]) -> Option<Self> {
6232 match local {
6233 b"catAx" => Some(Self::Category),
6234 b"valAx" => Some(Self::Value),
6235 b"dateAx" => Some(Self::Date),
6236 b"serAx" => Some(Self::Series),
6237 _ => None,
6238 }
6239 }
6240
6241 const fn local(self) -> &'static str {
6242 match self {
6243 Self::Category => "catAx",
6244 Self::Value => "valAx",
6245 Self::Date => "dateAx",
6246 Self::Series => "serAx",
6247 }
6248 }
6249}
6250
6251#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6253pub enum Orientation {
6254 #[default]
6255 MinMax,
6256 MaxMin,
6257}
6258
6259impl Orientation {
6260 fn parse(value: &str) -> Option<Self> {
6261 match value {
6262 "minMax" => Some(Self::MinMax),
6263 "maxMin" => Some(Self::MaxMin),
6264 _ => None,
6265 }
6266 }
6267
6268 const fn as_str(self) -> &'static str {
6269 match self {
6270 Self::MinMax => "minMax",
6271 Self::MaxMin => "maxMin",
6272 }
6273 }
6274}
6275
6276#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6278pub enum AxisPosition {
6279 Bottom,
6280 Left,
6281 Right,
6282 Top,
6283}
6284
6285impl AxisPosition {
6286 fn parse(value: &str) -> Option<Self> {
6287 match value {
6288 "b" => Some(Self::Bottom),
6289 "l" => Some(Self::Left),
6290 "r" => Some(Self::Right),
6291 "t" => Some(Self::Top),
6292 _ => None,
6293 }
6294 }
6295
6296 const fn as_str(self) -> &'static str {
6297 match self {
6298 Self::Bottom => "b",
6299 Self::Left => "l",
6300 Self::Right => "r",
6301 Self::Top => "t",
6302 }
6303 }
6304}
6305
6306#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6308pub enum TickMark {
6309 Cross,
6310 Inside,
6311 #[default]
6312 None,
6313 Outside,
6314}
6315
6316impl TickMark {
6317 fn parse(value: &str) -> Option<Self> {
6318 match value {
6319 "cross" => Some(Self::Cross),
6320 "in" => Some(Self::Inside),
6321 "none" => Some(Self::None),
6322 "out" => Some(Self::Outside),
6323 _ => None,
6324 }
6325 }
6326
6327 const fn as_str(self) -> &'static str {
6328 match self {
6329 Self::Cross => "cross",
6330 Self::Inside => "in",
6331 Self::None => "none",
6332 Self::Outside => "out",
6333 }
6334 }
6335}
6336
6337#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6339pub enum TickLabelPosition {
6340 High,
6341 Low,
6342 #[default]
6343 NextTo,
6344 None,
6345}
6346
6347impl TickLabelPosition {
6348 fn parse(value: &str) -> Option<Self> {
6349 match value {
6350 "high" => Some(Self::High),
6351 "low" => Some(Self::Low),
6352 "nextTo" => Some(Self::NextTo),
6353 "none" => Some(Self::None),
6354 _ => None,
6355 }
6356 }
6357
6358 const fn as_str(self) -> &'static str {
6359 match self {
6360 Self::High => "high",
6361 Self::Low => "low",
6362 Self::NextTo => "nextTo",
6363 Self::None => "none",
6364 }
6365 }
6366}
6367
6368#[derive(Clone, Debug)]
6370pub struct Scaling {
6371 pub log_base: Option<f64>,
6372 pub orientation: Orientation,
6373 pub maximum: Option<f64>,
6374 pub minimum: Option<f64>,
6375 log_base_markup: Option<ScalarMarkup>,
6376 orientation_markup: Option<ScalarMarkup>,
6377 maximum_markup: Option<ScalarMarkup>,
6378 minimum_markup: Option<ScalarMarkup>,
6379 raw_attributes: XmlAttributes,
6380 raw_children: OrderedRawChildren,
6381}
6382
6383impl PartialEq for Scaling {
6384 fn eq(&self, other: &Self) -> bool {
6385 self.log_base == other.log_base
6386 && self.orientation == other.orientation
6387 && self.maximum == other.maximum
6388 && self.minimum == other.minimum
6389 && optional_scalar_markup_eq(&self.log_base_markup, &other.log_base_markup)
6390 && optional_scalar_markup_eq(&self.orientation_markup, &other.orientation_markup)
6391 && optional_scalar_markup_eq(&self.maximum_markup, &other.maximum_markup)
6392 && optional_scalar_markup_eq(&self.minimum_markup, &other.minimum_markup)
6393 && self.raw_attributes == other.raw_attributes
6394 && self.raw_children == other.raw_children
6395 }
6396}
6397
6398impl Default for Scaling {
6399 fn default() -> Self {
6400 Self {
6401 log_base: None,
6402 orientation: Orientation::MinMax,
6403 maximum: None,
6404 minimum: None,
6405 log_base_markup: None,
6406 orientation_markup: None,
6407 maximum_markup: None,
6408 minimum_markup: None,
6409 raw_attributes: Vec::new(),
6410 raw_children: OrderedRawChildren::default(),
6411 }
6412 }
6413}
6414
6415#[derive(Clone, Debug, Default, PartialEq)]
6417pub struct ChartLines {
6418 pub sp_pr: Option<CT_ShapeProperties>,
6419 raw_attributes: XmlAttributes,
6420 raw_children: OrderedRawChildren,
6421}
6422
6423#[derive(Clone, Debug, Eq, PartialEq)]
6425pub struct NumberFormat {
6426 pub format_code: String,
6427 pub source_linked: bool,
6428 raw_attributes: XmlAttributes,
6429 raw_content: Vec<Vec<u8>>,
6430}
6431
6432#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6434pub enum DataLabelPosition {
6435 BestFit,
6436 Bottom,
6437 Center,
6438 InsideBase,
6439 InsideEnd,
6440 Left,
6441 OutsideEnd,
6442 Right,
6443 Top,
6444}
6445
6446impl DataLabelPosition {
6447 fn parse(value: &str) -> Option<Self> {
6448 match value {
6449 "bestFit" => Some(Self::BestFit),
6450 "b" => Some(Self::Bottom),
6451 "ctr" => Some(Self::Center),
6452 "inBase" => Some(Self::InsideBase),
6453 "inEnd" => Some(Self::InsideEnd),
6454 "l" => Some(Self::Left),
6455 "outEnd" => Some(Self::OutsideEnd),
6456 "r" => Some(Self::Right),
6457 "t" => Some(Self::Top),
6458 _ => None,
6459 }
6460 }
6461
6462 const fn as_str(self) -> &'static str {
6463 match self {
6464 Self::BestFit => "bestFit",
6465 Self::Bottom => "b",
6466 Self::Center => "ctr",
6467 Self::InsideBase => "inBase",
6468 Self::InsideEnd => "inEnd",
6469 Self::Left => "l",
6470 Self::OutsideEnd => "outEnd",
6471 Self::Right => "r",
6472 Self::Top => "t",
6473 }
6474 }
6475}
6476
6477#[derive(Clone, Debug, Default, PartialEq)]
6479pub struct CT_DLbls {
6480 pub number_format: Option<NumberFormat>,
6481 pub position: Option<DataLabelPosition>,
6482 pub separator: Option<String>,
6483 pub show_legend_key: bool,
6484 pub show_value: bool,
6485 pub show_category_name: bool,
6486 pub show_series_name: bool,
6487 pub show_percent: bool,
6488 pub show_bubble_size: bool,
6489 position_markup: Option<ScalarMarkup>,
6490 show_legend_key_markup: Option<ScalarMarkup>,
6491 show_value_markup: Option<ScalarMarkup>,
6492 show_category_name_markup: Option<ScalarMarkup>,
6493 show_series_name_markup: Option<ScalarMarkup>,
6494 show_percent_markup: Option<ScalarMarkup>,
6495 show_bubble_size_markup: Option<ScalarMarkup>,
6496 separator_markup: Option<TextMarkup>,
6497 raw_attributes: XmlAttributes,
6498 namespace_declarations: XmlAttributes,
6499 raw_children: OrderedRawChildren,
6500}
6501
6502#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6504pub enum DispBlanksAs {
6505 #[default]
6506 Gap,
6507 Zero,
6508 Span,
6509}
6510
6511impl DispBlanksAs {
6512 fn parse(value: &str) -> Option<Self> {
6513 match value {
6514 "gap" => Some(Self::Gap),
6515 "zero" => Some(Self::Zero),
6516 "span" => Some(Self::Span),
6517 _ => None,
6518 }
6519 }
6520
6521 const fn as_str(self) -> &'static str {
6522 match self {
6523 Self::Gap => "gap",
6524 Self::Zero => "zero",
6525 Self::Span => "span",
6526 }
6527 }
6528}
6529
6530#[derive(Clone, Debug, Default, Eq, PartialEq)]
6532pub struct CT_Title {
6533 text: Option<CT_TextBody>,
6534 raw_attributes: Vec<(String, String)>,
6535 raw_children: OrderedRawChildren,
6536}
6537
6538#[derive(Clone, Debug, Default, PartialEq)]
6540struct PlotMarkup {
6541 raw_attributes: XmlAttributes,
6542 raw_children: OrderedRawChildren,
6543 direction: Option<ScalarMarkup>,
6544 grouping: Option<ScalarMarkup>,
6545 gap_width: Option<ScalarMarkup>,
6546 overlap: Option<ScalarMarkup>,
6547 marker: Option<ScalarMarkup>,
6548 smooth: Option<ScalarMarkup>,
6549 first_slice_angle: Option<ScalarMarkup>,
6550 hole_size: Option<ScalarMarkup>,
6551 style: Option<ScalarMarkup>,
6552 axis_ids: Vec<AxisIdMarkup>,
6553 original_series_keys: Vec<(u32, u32)>,
6554 original_axis_ids: Vec<AxisId>,
6555 parsed_bar: Option<bool>,
6556 parsed_remaining: Option<&'static str>,
6557}
6558
6559#[derive(Clone, Debug, PartialEq)]
6561pub struct CT_PlotArea {
6562 raw_attributes: Vec<(String, String)>,
6563 raw_children: OrderedRawChildren,
6564 namespace_bindings: NamespaceBindings,
6565 plots: Option<Vec<Plot>>,
6566 plot_markup: Vec<PlotMarkup>,
6567 axes: Vec<Axis>,
6568}
6569
6570#[derive(Clone, Debug, Default, Eq, PartialEq)]
6572pub struct CT_Legend {
6573 raw_attributes: Vec<(String, String)>,
6574 raw_children: OrderedRawChildren,
6575}
6576
6577#[derive(Clone, Debug, PartialEq)]
6578struct AxisIdMarkup {
6579 scalar: ScalarMarkup,
6580 parsed: AxisId,
6581 lexical: String,
6582}
6583
6584#[derive(Clone, Debug)]
6586pub struct Axis {
6587 pub kind: AxisKind,
6588 pub id: AxisId,
6589 pub scaling: Scaling,
6590 pub deleted: bool,
6591 pub position: AxisPosition,
6592 pub major_gridlines: Option<ChartLines>,
6593 pub minor_gridlines: Option<ChartLines>,
6594 pub title: Option<CT_Title>,
6595 pub number_format: Option<NumberFormat>,
6596 pub major_tick_mark: TickMark,
6597 pub minor_tick_mark: TickMark,
6598 pub tick_label_position: TickLabelPosition,
6599 pub sp_pr: Option<CT_ShapeProperties>,
6600 pub tx_pr: Option<CT_TextBody>,
6601 pub cross_axis: AxisId,
6602 parsed_kind: Option<AxisKind>,
6603 id_markup: AxisIdMarkup,
6604 delete_markup: Option<ScalarMarkup>,
6605 position_markup: ScalarMarkup,
6606 major_tick_mark_markup: Option<ScalarMarkup>,
6607 minor_tick_mark_markup: Option<ScalarMarkup>,
6608 tick_label_position_markup: Option<ScalarMarkup>,
6609 cross_axis_markup: AxisIdMarkup,
6610 raw_attributes: XmlAttributes,
6611 namespace_declarations: XmlAttributes,
6612 raw_children: OrderedRawChildren,
6613}
6614
6615impl PartialEq for Axis {
6616 fn eq(&self, other: &Self) -> bool {
6617 self.kind == other.kind
6618 && self.id == other.id
6619 && axis_id_markup_eq(&self.id_markup, &other.id_markup)
6620 && self.scaling == other.scaling
6621 && self.deleted == other.deleted
6622 && optional_scalar_markup_eq(&self.delete_markup, &other.delete_markup)
6623 && self.position == other.position
6624 && self.position_markup == other.position_markup
6625 && self.major_gridlines == other.major_gridlines
6626 && self.minor_gridlines == other.minor_gridlines
6627 && self.title == other.title
6628 && self.number_format == other.number_format
6629 && self.major_tick_mark == other.major_tick_mark
6630 && optional_scalar_markup_eq(
6631 &self.major_tick_mark_markup,
6632 &other.major_tick_mark_markup,
6633 )
6634 && self.minor_tick_mark == other.minor_tick_mark
6635 && optional_scalar_markup_eq(
6636 &self.minor_tick_mark_markup,
6637 &other.minor_tick_mark_markup,
6638 )
6639 && self.tick_label_position == other.tick_label_position
6640 && optional_scalar_markup_eq(
6641 &self.tick_label_position_markup,
6642 &other.tick_label_position_markup,
6643 )
6644 && self.sp_pr == other.sp_pr
6645 && self.tx_pr == other.tx_pr
6646 && self.cross_axis == other.cross_axis
6647 && axis_id_markup_eq(&self.cross_axis_markup, &other.cross_axis_markup)
6648 && self.raw_attributes == other.raw_attributes
6649 && self.namespace_declarations == other.namespace_declarations
6650 && self.raw_children == other.raw_children
6651 }
6652}
6653
6654impl Axis {
6655 pub fn new(kind: AxisKind, id: AxisId, position: AxisPosition, cross_axis: AxisId) -> Self {
6656 Self {
6657 kind,
6658 id,
6659 scaling: Scaling::default(),
6660 deleted: false,
6661 position,
6662 major_gridlines: None,
6663 minor_gridlines: None,
6664 title: None,
6665 number_format: None,
6666 major_tick_mark: TickMark::None,
6667 minor_tick_mark: TickMark::None,
6668 tick_label_position: TickLabelPosition::NextTo,
6669 sp_pr: None,
6670 tx_pr: None,
6671 cross_axis,
6672 parsed_kind: None,
6673 id_markup: default_axis_id_markup(id),
6674 delete_markup: None,
6675 position_markup: ScalarMarkup::default(),
6676 major_tick_mark_markup: None,
6677 minor_tick_mark_markup: None,
6678 tick_label_position_markup: None,
6679 cross_axis_markup: default_axis_id_markup(cross_axis),
6680 raw_attributes: Vec::new(),
6681 namespace_declarations: Vec::new(),
6682 raw_children: OrderedRawChildren::default(),
6683 }
6684 }
6685
6686 pub fn set_deleted(&mut self, deleted: bool) {
6688 self.deleted = deleted;
6689 self.delete_markup.get_or_insert_with(ScalarMarkup::default);
6690 }
6691
6692 pub fn from_xml(xml: &[u8]) -> Result<Self> {
6693 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
6694 }
6695
6696 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
6697 let mut reader = Reader::from_reader(xml);
6698 let mut buffer = Vec::new();
6699 loop {
6700 match reader
6701 .read_event_into(&mut buffer)
6702 .map_err(OxmlError::from)?
6703 {
6704 Event::Start(element) => {
6705 let name = element.name();
6706 let local = local_name(name.as_ref());
6707 let Some(kind) = AxisKind::parse(local) else {
6708 return Err(ChartError::UnexpectedElement(element_name(&element)));
6709 };
6710 chart_root_prefix(&element)?;
6711 if !element_is_in_namespace(&element, C_NS, inherited)? {
6712 return Err(ChartError::UnexpectedElement(element_name(&element)));
6713 }
6714 return Self::from_element(&mut reader, &element, inherited, kind);
6715 }
6716 Event::Empty(element) => {
6717 let name = element.name();
6718 let local = local_name(name.as_ref());
6719 if AxisKind::parse(local).is_some() {
6720 return Err(ChartError::MissingElement("c:axId".to_owned()));
6721 }
6722 return Err(ChartError::UnexpectedElement(element_name(&element)));
6723 }
6724 Event::Eof => return Err(ChartError::MissingElement("ChartML axis".to_owned())),
6725 _ => {}
6726 }
6727 buffer.clear();
6728 }
6729 }
6730
6731 fn from_element(
6732 reader: &mut Reader<&[u8]>,
6733 start: &BytesStart<'_>,
6734 inherited: &NamespaceBindings,
6735 kind: AxisKind,
6736 ) -> Result<Self> {
6737 reject_conflicting_prefix(start, b"a", A_NS)?;
6738 reject_conflicting_prefix(start, b"r", R_NS)?;
6739 let namespaces = chart_bindings(inherited, start)?;
6740 require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
6741 require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
6742 require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
6743 let (raw_attributes, _) =
6744 capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
6745 let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
6746 let mut state = AxisParseState::default();
6747 let mut buffer = Vec::new();
6748 loop {
6749 match reader
6750 .read_event_into(&mut buffer)
6751 .map_err(OxmlError::from)?
6752 {
6753 Event::Start(element) => {
6754 let name = chart_child_local(&element, &namespaces)?;
6755 let raw = capture_element(reader, &element)?;
6756 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
6757 }
6758 Event::Empty(element) => {
6759 let name = chart_child_local(&element, &namespaces)?;
6760 let raw = capture_empty_element(&element)?;
6761 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
6762 }
6763 event @ (Event::Text(_)
6764 | Event::CData(_)
6765 | Event::Comment(_)
6766 | Event::PI(_)
6767 | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
6768 Event::End(element)
6769 if matches_local_name(element.name().as_ref(), kind.local().as_bytes()) =>
6770 {
6771 break;
6772 }
6773 Event::Eof => return Err(missing_end(&format!("c:{}", kind.local()))),
6774 _ => {}
6775 }
6776 buffer.clear();
6777 }
6778 state.finish(kind, raw_attributes, namespace_declarations)
6779 }
6780
6781 pub fn to_xml(&self) -> Result<Vec<u8>> {
6782 self.validate()?;
6783 let mut writer = Writer::new(Vec::new());
6784 let root = format!("c:{}", self.kind.local());
6785 let mut start = BytesStart::new(&root);
6786 start.push_attribute(("xmlns:c", C_NS));
6787 start.push_attribute(("xmlns:a", A_NS));
6788 start.push_attribute(("xmlns:r", R_NS));
6789 push_attributes(&mut start, &self.namespace_declarations);
6790 push_attributes(&mut start, &self.raw_attributes);
6791 writer
6792 .write_event(Event::Start(start))
6793 .map_err(OxmlError::from)?;
6794 emit_raw(&mut writer, self.raw_children.at(0))?;
6795 write_axis_id(&mut writer, "c:axId", self.id, &self.id_markup)?;
6796 emit_raw(&mut writer, self.raw_children.at(1))?;
6797 self.scaling.write_xml(&mut writer)?;
6798 emit_raw(&mut writer, self.raw_children.at(2))?;
6799 if self.deleted || self.delete_markup.is_some() {
6800 write_scalar(
6801 &mut writer,
6802 "c:delete",
6803 bool_lexical(self.deleted),
6804 self.delete_markup.as_ref(),
6805 )?;
6806 }
6807 emit_raw(&mut writer, self.raw_children.at(3))?;
6808 write_scalar(
6809 &mut writer,
6810 "c:axPos",
6811 self.position.as_str(),
6812 Some(&self.position_markup),
6813 )?;
6814 emit_raw(&mut writer, self.raw_children.at(4))?;
6815 if let Some(lines) = &self.major_gridlines {
6816 lines.write_xml(&mut writer, "c:majorGridlines")?;
6817 }
6818 emit_raw(&mut writer, self.raw_children.at(5))?;
6819 if let Some(lines) = &self.minor_gridlines {
6820 lines.write_xml(&mut writer, "c:minorGridlines")?;
6821 }
6822 emit_raw(&mut writer, self.raw_children.at(6))?;
6823 if let Some(title) = &self.title {
6824 title.write_xml(&mut writer)?;
6825 }
6826 emit_raw(&mut writer, self.raw_children.at(7))?;
6827 if let Some(number_format) = &self.number_format {
6828 number_format.write_xml(&mut writer)?;
6829 }
6830 emit_raw(&mut writer, self.raw_children.at(8))?;
6831 if self.major_tick_mark != TickMark::None || self.major_tick_mark_markup.is_some() {
6832 write_scalar(
6833 &mut writer,
6834 "c:majorTickMark",
6835 self.major_tick_mark.as_str(),
6836 self.major_tick_mark_markup.as_ref(),
6837 )?;
6838 }
6839 emit_raw(&mut writer, self.raw_children.at(9))?;
6840 if self.minor_tick_mark != TickMark::None || self.minor_tick_mark_markup.is_some() {
6841 write_scalar(
6842 &mut writer,
6843 "c:minorTickMark",
6844 self.minor_tick_mark.as_str(),
6845 self.minor_tick_mark_markup.as_ref(),
6846 )?;
6847 }
6848 emit_raw(&mut writer, self.raw_children.at(10))?;
6849 if self.tick_label_position != TickLabelPosition::NextTo
6850 || self.tick_label_position_markup.is_some()
6851 {
6852 write_scalar(
6853 &mut writer,
6854 "c:tickLblPos",
6855 self.tick_label_position.as_str(),
6856 self.tick_label_position_markup.as_ref(),
6857 )?;
6858 }
6859 emit_raw(&mut writer, self.raw_children.at(11))?;
6860 if let Some(properties) = &self.sp_pr {
6861 properties.write_xml_as(&mut writer, "c:spPr")?;
6862 }
6863 emit_raw(&mut writer, self.raw_children.at(12))?;
6864 if let Some(text) = &self.tx_pr {
6865 text.write_xml_as(&mut writer, "c:txPr")?;
6866 }
6867 emit_raw(&mut writer, self.raw_children.at(13))?;
6868 write_axis_id(
6869 &mut writer,
6870 "c:crossAx",
6871 self.cross_axis,
6872 &self.cross_axis_markup,
6873 )?;
6874 emit_raw(&mut writer, self.raw_children.at(14))?;
6875 writer
6876 .write_event(Event::End(BytesEnd::new(&root)))
6877 .map_err(OxmlError::from)?;
6878 Ok(writer.into_inner())
6879 }
6880
6881 pub fn raw_children(&self) -> &OrderedRawChildren {
6882 &self.raw_children
6883 }
6884
6885 fn validate(&self) -> Result<()> {
6886 if let Some(parsed_kind) = self.parsed_kind
6887 && self.kind != parsed_kind
6888 {
6889 return Err(ChartError::InvalidValue {
6890 element: "ChartML axis root".to_owned(),
6891 value: format!(
6892 "parsed c:{} cannot be relabelled c:{}",
6893 parsed_kind.local(),
6894 self.kind.local()
6895 ),
6896 });
6897 }
6898 AxisId::new(self.id.value())?;
6899 AxisId::new(self.cross_axis.value())?;
6900 self.scaling.validate()?;
6901 if let Some(number_format) = &self.number_format {
6902 number_format.validate()?;
6903 }
6904 Ok(())
6905 }
6906}
6907
6908#[derive(Default)]
6909struct AxisParseState {
6910 id: Option<(AxisId, AxisIdMarkup)>,
6911 scaling: Option<Scaling>,
6912 deleted: Option<(bool, ScalarMarkup)>,
6913 position: Option<(AxisPosition, ScalarMarkup)>,
6914 major_gridlines: Option<ChartLines>,
6915 minor_gridlines: Option<ChartLines>,
6916 title: Option<CT_Title>,
6917 number_format: Option<NumberFormat>,
6918 major_tick_mark: Option<(TickMark, ScalarMarkup)>,
6919 minor_tick_mark: Option<(TickMark, ScalarMarkup)>,
6920 tick_label_position: Option<(TickLabelPosition, ScalarMarkup)>,
6921 sp_pr: Option<CT_ShapeProperties>,
6922 tx_pr: Option<CT_TextBody>,
6923 cross_axis: Option<(AxisId, AxisIdMarkup)>,
6924 raw_children: OrderedRawChildren,
6925 boundary: usize,
6926}
6927
6928impl AxisParseState {
6929 fn capture_event(&mut self, raw: Vec<u8>) {
6930 self.raw_children.push(self.boundary, raw);
6931 }
6932
6933 fn parse_child(
6934 &mut self,
6935 name: &[u8],
6936 raw: Vec<u8>,
6937 namespaces: &NamespaceBindings,
6938 ) -> Result<()> {
6939 match name {
6940 b"axId" => {
6941 set_once(&mut self.id, parse_axis_id_scalar(&raw, "axId")?, "c:axId")?;
6942 self.boundary = self.boundary.max(1);
6943 }
6944 b"scaling" => {
6945 set_once(
6946 &mut self.scaling,
6947 Scaling::from_xml(&raw, namespaces)?,
6948 "c:scaling",
6949 )?;
6950 self.boundary = self.boundary.max(2);
6951 }
6952 b"delete" => {
6953 set_once(
6954 &mut self.deleted,
6955 parse_bool_value(&raw, "delete")?,
6956 "c:delete",
6957 )?;
6958 self.boundary = self.boundary.max(3);
6959 }
6960 b"axPos" => {
6961 set_once(&mut self.position, parse_axis_position(&raw)?, "c:axPos")?;
6962 self.boundary = self.boundary.max(4);
6963 }
6964 b"majorGridlines" => {
6965 set_once(
6966 &mut self.major_gridlines,
6967 ChartLines::from_xml(&raw, b"majorGridlines", namespaces)?,
6968 "c:majorGridlines",
6969 )?;
6970 self.boundary = self.boundary.max(5);
6971 }
6972 b"minorGridlines" => {
6973 set_once(
6974 &mut self.minor_gridlines,
6975 ChartLines::from_xml(&raw, b"minorGridlines", namespaces)?,
6976 "c:minorGridlines",
6977 )?;
6978 self.boundary = self.boundary.max(6);
6979 }
6980 b"title" => {
6981 set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
6982 self.boundary = self.boundary.max(7);
6983 }
6984 b"numFmt" => {
6985 set_once(
6986 &mut self.number_format,
6987 NumberFormat::from_xml(&raw)?,
6988 "c:numFmt",
6989 )?;
6990 self.boundary = self.boundary.max(8);
6991 }
6992 b"majorTickMark" => {
6993 set_once(
6994 &mut self.major_tick_mark,
6995 parse_tick_mark(&raw, "majorTickMark")?,
6996 "c:majorTickMark",
6997 )?;
6998 self.boundary = self.boundary.max(9);
6999 }
7000 b"minorTickMark" => {
7001 set_once(
7002 &mut self.minor_tick_mark,
7003 parse_tick_mark(&raw, "minorTickMark")?,
7004 "c:minorTickMark",
7005 )?;
7006 self.boundary = self.boundary.max(10);
7007 }
7008 b"tickLblPos" => {
7009 set_once(
7010 &mut self.tick_label_position,
7011 parse_tick_label_position(&raw)?,
7012 "c:tickLblPos",
7013 )?;
7014 self.boundary = self.boundary.max(11);
7015 }
7016 b"spPr" => {
7017 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
7018 let properties = CT_ShapeProperties::from_xml(&raw)?;
7019 let mut writer = Writer::new(Vec::new());
7020 properties.write_xml_as(&mut writer, "c:spPr")?;
7021 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
7022 set_once(&mut self.sp_pr, properties, "c:spPr")?;
7023 self.boundary = self.boundary.max(12);
7024 }
7025 b"txPr" => {
7026 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
7027 let text = CT_TextBody::from_xml_as(&raw, b"txPr")?;
7028 let mut writer = Writer::new(Vec::new());
7029 text.write_xml_as(&mut writer, "c:txPr")?;
7030 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
7031 set_once(&mut self.tx_pr, text, "c:txPr")?;
7032 self.boundary = self.boundary.max(13);
7033 }
7034 b"crossAx" => {
7035 set_once(
7036 &mut self.cross_axis,
7037 parse_axis_id_scalar(&raw, "crossAx")?,
7038 "c:crossAx",
7039 )?;
7040 self.boundary = self.boundary.max(14);
7041 }
7042 _ => self.raw_children.push(self.boundary, raw),
7043 }
7044 Ok(())
7045 }
7046
7047 fn finish(
7048 self,
7049 kind: AxisKind,
7050 raw_attributes: XmlAttributes,
7051 namespace_declarations: XmlAttributes,
7052 ) -> Result<Axis> {
7053 let (id, id_markup) = self
7054 .id
7055 .ok_or_else(|| ChartError::MissingElement("c:axId".to_owned()))?;
7056 let scaling = self
7057 .scaling
7058 .ok_or_else(|| ChartError::MissingElement("c:scaling".to_owned()))?;
7059 let (position, position_markup) = self
7060 .position
7061 .ok_or_else(|| ChartError::MissingElement("c:axPos".to_owned()))?;
7062 let (cross_axis, cross_axis_markup) = self
7063 .cross_axis
7064 .ok_or_else(|| ChartError::MissingElement("c:crossAx".to_owned()))?;
7065 let (deleted, delete_markup) = self
7066 .deleted
7067 .map(|(value, markup)| (value, Some(markup)))
7068 .unwrap_or((false, None));
7069 let (major_tick_mark, major_tick_mark_markup) = self
7070 .major_tick_mark
7071 .map(|(value, markup)| (value, Some(markup)))
7072 .unwrap_or((TickMark::None, None));
7073 let (minor_tick_mark, minor_tick_mark_markup) = self
7074 .minor_tick_mark
7075 .map(|(value, markup)| (value, Some(markup)))
7076 .unwrap_or((TickMark::None, None));
7077 let (tick_label_position, tick_label_position_markup) = self
7078 .tick_label_position
7079 .map(|(value, markup)| (value, Some(markup)))
7080 .unwrap_or((TickLabelPosition::NextTo, None));
7081 let axis = Axis {
7082 kind,
7083 id,
7084 scaling,
7085 deleted,
7086 position,
7087 major_gridlines: self.major_gridlines,
7088 minor_gridlines: self.minor_gridlines,
7089 title: self.title,
7090 number_format: self.number_format,
7091 major_tick_mark,
7092 minor_tick_mark,
7093 tick_label_position,
7094 sp_pr: self.sp_pr,
7095 tx_pr: self.tx_pr,
7096 cross_axis,
7097 parsed_kind: Some(kind),
7098 id_markup,
7099 delete_markup,
7100 position_markup,
7101 major_tick_mark_markup,
7102 minor_tick_mark_markup,
7103 tick_label_position_markup,
7104 cross_axis_markup,
7105 raw_attributes,
7106 namespace_declarations,
7107 raw_children: raw_children_in_schema_order(&self.raw_children, 14),
7108 };
7109 axis.validate()?;
7110 Ok(axis)
7111 }
7112}
7113
7114impl Scaling {
7115 fn from_xml(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7116 let mut reader = Reader::from_reader(xml);
7117 let mut buffer = Vec::new();
7118 loop {
7119 match reader
7120 .read_event_into(&mut buffer)
7121 .map_err(OxmlError::from)?
7122 {
7123 Event::Start(element)
7124 if matches_local_name(element.name().as_ref(), b"scaling") =>
7125 {
7126 let namespaces = typed_rewrite_bindings(&element, inherited)?;
7127 let raw_attributes = capture_attributes(&element)?;
7128 let mut log_base = None;
7129 let mut orientation = None;
7130 let mut maximum = None;
7131 let mut minimum = None;
7132 let mut raw_children = OrderedRawChildren::default();
7133 let mut boundary = 0usize;
7134 let mut inner = Vec::new();
7135 loop {
7136 match reader
7137 .read_event_into(&mut inner)
7138 .map_err(OxmlError::from)?
7139 {
7140 Event::Start(child) => {
7141 let name = chart_child_local(&child, &namespaces)?;
7142 let raw = capture_element(&mut reader, &child)?;
7143 parse_scaling_child(
7144 name.as_deref().unwrap_or_default(),
7145 raw,
7146 &mut log_base,
7147 &mut orientation,
7148 &mut maximum,
7149 &mut minimum,
7150 &mut raw_children,
7151 &mut boundary,
7152 )?;
7153 }
7154 Event::Empty(child) => {
7155 let name = chart_child_local(&child, &namespaces)?;
7156 let raw = capture_empty_element(&child)?;
7157 parse_scaling_child(
7158 name.as_deref().unwrap_or_default(),
7159 raw,
7160 &mut log_base,
7161 &mut orientation,
7162 &mut maximum,
7163 &mut minimum,
7164 &mut raw_children,
7165 &mut boundary,
7166 )?;
7167 }
7168 event @ (Event::Text(_)
7169 | Event::CData(_)
7170 | Event::Comment(_)
7171 | Event::PI(_)
7172 | Event::GeneralRef(_)) => {
7173 raw_children.push(boundary, capture_event(event)?);
7174 }
7175 Event::End(end)
7176 if matches_local_name(end.name().as_ref(), b"scaling") =>
7177 {
7178 let (log_base, log_base_markup) = optional_markup(log_base);
7179 let (orientation, orientation_markup) = orientation
7180 .map(|(value, markup)| (value, Some(markup)))
7181 .unwrap_or((Orientation::MinMax, None));
7182 let (maximum, maximum_markup) = optional_markup(maximum);
7183 let (minimum, minimum_markup) = optional_markup(minimum);
7184 let scaling = Self {
7185 log_base,
7186 orientation,
7187 maximum,
7188 minimum,
7189 log_base_markup,
7190 orientation_markup,
7191 maximum_markup,
7192 minimum_markup,
7193 raw_attributes,
7194 raw_children: raw_children_in_schema_order(&raw_children, 4),
7195 };
7196 scaling.validate()?;
7197 return Ok(scaling);
7198 }
7199 Event::Eof => return Err(missing_end("c:scaling")),
7200 _ => {}
7201 }
7202 inner.clear();
7203 }
7204 }
7205 Event::Empty(element)
7206 if matches_local_name(element.name().as_ref(), b"scaling") =>
7207 {
7208 typed_rewrite_bindings(&element, inherited)?;
7209 return Ok(Self {
7210 raw_attributes: capture_attributes(&element)?,
7211 ..Self::default()
7212 });
7213 }
7214 Event::Start(element) | Event::Empty(element) => {
7215 return Err(ChartError::UnexpectedElement(element_name(&element)));
7216 }
7217 Event::Eof => return Err(ChartError::MissingElement("c:scaling".to_owned())),
7218 _ => {}
7219 }
7220 buffer.clear();
7221 }
7222 }
7223
7224 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7225 self.validate()?;
7226 let mut start = BytesStart::new("c:scaling");
7227 push_attributes(&mut start, &self.raw_attributes);
7228 writer
7229 .write_event(Event::Start(start))
7230 .map_err(OxmlError::from)?;
7231 emit_raw(writer, self.raw_children.at(0))?;
7232 if let Some(value) = self.log_base {
7233 write_scalar(
7234 writer,
7235 "c:logBase",
7236 &value.to_string(),
7237 self.log_base_markup.as_ref(),
7238 )?;
7239 }
7240 emit_raw(writer, self.raw_children.at(1))?;
7241 if self.orientation != Orientation::MinMax || self.orientation_markup.is_some() {
7242 write_scalar(
7243 writer,
7244 "c:orientation",
7245 self.orientation.as_str(),
7246 self.orientation_markup.as_ref(),
7247 )?;
7248 }
7249 emit_raw(writer, self.raw_children.at(2))?;
7250 if let Some(value) = self.maximum {
7251 write_scalar(
7252 writer,
7253 "c:max",
7254 &value.to_string(),
7255 self.maximum_markup.as_ref(),
7256 )?;
7257 }
7258 emit_raw(writer, self.raw_children.at(3))?;
7259 if let Some(value) = self.minimum {
7260 write_scalar(
7261 writer,
7262 "c:min",
7263 &value.to_string(),
7264 self.minimum_markup.as_ref(),
7265 )?;
7266 }
7267 emit_raw(writer, self.raw_children.at(4))?;
7268 writer
7269 .write_event(Event::End(BytesEnd::new("c:scaling")))
7270 .map_err(OxmlError::from)?;
7271 Ok(())
7272 }
7273
7274 fn validate(&self) -> Result<()> {
7275 if let Some(value) = self.log_base
7276 && (!value.is_finite() || !(2.0..=1000.0).contains(&value))
7277 {
7278 return Err(ChartError::InvalidValue {
7279 element: "c:logBase".to_owned(),
7280 value: value.to_string(),
7281 });
7282 }
7283 for (element, value) in [("c:max", self.maximum), ("c:min", self.minimum)] {
7284 if let Some(value) = value
7285 && !value.is_finite()
7286 {
7287 return Err(ChartError::InvalidValue {
7288 element: element.to_owned(),
7289 value: value.to_string(),
7290 });
7291 }
7292 }
7293 if let (Some(minimum), Some(maximum)) = (self.minimum, self.maximum)
7294 && minimum > maximum
7295 {
7296 return Err(ChartError::InvalidValue {
7297 element: "c:scaling".to_owned(),
7298 value: format!("minimum {minimum} exceeds maximum {maximum}"),
7299 });
7300 }
7301 Ok(())
7302 }
7303}
7304
7305#[allow(clippy::too_many_arguments)]
7306fn parse_scaling_child(
7307 name: &[u8],
7308 raw: Vec<u8>,
7309 log_base: &mut Option<(f64, ScalarMarkup)>,
7310 orientation: &mut Option<(Orientation, ScalarMarkup)>,
7311 maximum: &mut Option<(f64, ScalarMarkup)>,
7312 minimum: &mut Option<(f64, ScalarMarkup)>,
7313 raw_children: &mut OrderedRawChildren,
7314 boundary: &mut usize,
7315) -> Result<()> {
7316 match name {
7317 b"logBase" => {
7318 set_once(log_base, parse_f64_scalar(&raw, "logBase")?, "c:logBase")?;
7319 *boundary = (*boundary).max(1);
7320 }
7321 b"orientation" => {
7322 let (value, markup) = scalar_value(&raw, "orientation")?;
7323 let value = value
7324 .ok_or_else(|| invalid_attribute("orientation", "val", "<missing>".to_owned()))?;
7325 let parsed = Orientation::parse(&value)
7326 .ok_or_else(|| invalid_attribute("orientation", "val", value))?;
7327 set_once(orientation, (parsed, markup), "c:orientation")?;
7328 *boundary = (*boundary).max(2);
7329 }
7330 b"max" => {
7331 set_once(maximum, parse_f64_scalar(&raw, "max")?, "c:max")?;
7332 *boundary = (*boundary).max(3);
7333 }
7334 b"min" => {
7335 set_once(minimum, parse_f64_scalar(&raw, "min")?, "c:min")?;
7336 *boundary = (*boundary).max(4);
7337 }
7338 _ => raw_children.push(*boundary, raw),
7339 }
7340 Ok(())
7341}
7342
7343impl ChartLines {
7344 fn from_xml(xml: &[u8], local: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7345 let mut reader = Reader::from_reader(xml);
7346 let mut buffer = Vec::new();
7347 loop {
7348 match reader
7349 .read_event_into(&mut buffer)
7350 .map_err(OxmlError::from)?
7351 {
7352 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
7353 let namespaces = typed_rewrite_bindings(&element, inherited)?;
7354 let raw_attributes = capture_attributes(&element)?;
7355 let mut sp_pr = None;
7356 let mut raw_children = OrderedRawChildren::default();
7357 let mut boundary = 0usize;
7358 let mut inner = Vec::new();
7359 loop {
7360 match reader
7361 .read_event_into(&mut inner)
7362 .map_err(OxmlError::from)?
7363 {
7364 Event::Start(child) => {
7365 let name = chart_child_local(&child, &namespaces)?;
7366 let raw = capture_element(&mut reader, &child)?;
7367 if name.as_deref() == Some(b"spPr") {
7368 parse_chart_lines_shape(&raw, &namespaces, &mut sp_pr)?;
7369 boundary = 1;
7370 } else {
7371 raw_children.push(boundary, raw);
7372 }
7373 }
7374 Event::Empty(child) => {
7375 let name = chart_child_local(&child, &namespaces)?;
7376 let raw = capture_empty_element(&child)?;
7377 if name.as_deref() == Some(b"spPr") {
7378 parse_chart_lines_shape(&raw, &namespaces, &mut sp_pr)?;
7379 boundary = 1;
7380 } else {
7381 raw_children.push(boundary, raw);
7382 }
7383 }
7384 event @ (Event::Text(_)
7385 | Event::CData(_)
7386 | Event::Comment(_)
7387 | Event::PI(_)
7388 | Event::GeneralRef(_)) => {
7389 raw_children.push(boundary, capture_event(event)?);
7390 }
7391 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
7392 return Ok(Self {
7393 sp_pr,
7394 raw_attributes,
7395 raw_children: raw_children_in_schema_order(&raw_children, 1),
7396 });
7397 }
7398 Event::Eof => {
7399 return Err(missing_end(&format!(
7400 "c:{}",
7401 String::from_utf8_lossy(local)
7402 )));
7403 }
7404 _ => {}
7405 }
7406 inner.clear();
7407 }
7408 }
7409 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
7410 typed_rewrite_bindings(&element, inherited)?;
7411 return Ok(Self {
7412 sp_pr: None,
7413 raw_attributes: capture_attributes(&element)?,
7414 raw_children: OrderedRawChildren::default(),
7415 });
7416 }
7417 Event::Start(element) | Event::Empty(element) => {
7418 return Err(ChartError::UnexpectedElement(element_name(&element)));
7419 }
7420 Event::Eof => {
7421 return Err(ChartError::MissingElement(format!(
7422 "c:{}",
7423 String::from_utf8_lossy(local)
7424 )));
7425 }
7426 _ => {}
7427 }
7428 buffer.clear();
7429 }
7430 }
7431
7432 fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
7433 let mut start = BytesStart::new(tag);
7434 push_attributes(&mut start, &self.raw_attributes);
7435 if self.sp_pr.is_none() && self.raw_children.is_empty() {
7436 writer
7437 .write_event(Event::Empty(start))
7438 .map_err(OxmlError::from)?;
7439 return Ok(());
7440 }
7441 writer
7442 .write_event(Event::Start(start))
7443 .map_err(OxmlError::from)?;
7444 emit_raw(writer, self.raw_children.at(0))?;
7445 if let Some(properties) = &self.sp_pr {
7446 properties.write_xml_as(writer, "c:spPr")?;
7447 }
7448 emit_raw(writer, self.raw_children.at(1))?;
7449 writer
7450 .write_event(Event::End(BytesEnd::new(tag)))
7451 .map_err(OxmlError::from)?;
7452 Ok(())
7453 }
7454}
7455
7456fn parse_chart_lines_shape(
7457 raw: &[u8],
7458 namespaces: &NamespaceBindings,
7459 slot: &mut Option<CT_ShapeProperties>,
7460) -> Result<()> {
7461 reject_conflicting_prefix_in_xml(raw, b"a", A_NS)?;
7462 let properties = CT_ShapeProperties::from_xml(raw)?;
7463 let mut writer = Writer::new(Vec::new());
7464 properties.write_xml_as(&mut writer, "c:spPr")?;
7465 reject_rewritten_foreign_elements(raw, &writer.into_inner(), namespaces, b"spPr")?;
7466 set_once(slot, properties, "c:spPr")
7467}
7468
7469impl NumberFormat {
7470 pub fn new(format_code: String, source_linked: bool) -> Result<Self> {
7471 let number_format = Self {
7472 format_code,
7473 source_linked,
7474 raw_attributes: Vec::new(),
7475 raw_content: Vec::new(),
7476 };
7477 number_format.validate()?;
7478 Ok(number_format)
7479 }
7480
7481 fn from_xml(xml: &[u8]) -> Result<Self> {
7482 let mut reader = Reader::from_reader(xml);
7483 let mut buffer = Vec::new();
7484 loop {
7485 match reader
7486 .read_event_into(&mut buffer)
7487 .map_err(OxmlError::from)?
7488 {
7489 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
7490 return Self::from_element(&element, Vec::new());
7491 }
7492 Event::Start(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
7493 let mut raw_content = Vec::new();
7494 let mut inner = Vec::new();
7495 loop {
7496 match reader
7497 .read_event_into(&mut inner)
7498 .map_err(OxmlError::from)?
7499 {
7500 Event::End(end)
7501 if matches_local_name(end.name().as_ref(), b"numFmt") =>
7502 {
7503 return Self::from_element(&element, raw_content);
7504 }
7505 event @ (Event::Text(_)
7506 | Event::CData(_)
7507 | Event::Comment(_)
7508 | Event::PI(_)
7509 | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
7510 Event::Eof => return Err(missing_end("c:numFmt")),
7511 _ => return Err(ChartError::UnexpectedElement("c:numFmt".to_owned())),
7512 }
7513 inner.clear();
7514 }
7515 }
7516 Event::Start(element) | Event::Empty(element) => {
7517 return Err(ChartError::UnexpectedElement(element_name(&element)));
7518 }
7519 Event::Eof => return Err(ChartError::MissingElement("c:numFmt".to_owned())),
7520 _ => {}
7521 }
7522 buffer.clear();
7523 }
7524 }
7525
7526 fn from_element(element: &BytesStart<'_>, raw_content: Vec<Vec<u8>>) -> Result<Self> {
7527 reject_conflicting_prefix(element, b"c", C_NS)?;
7528 let format_code = attribute_value(element, b"formatCode")?
7529 .ok_or_else(|| invalid_attribute("numFmt", "formatCode", "<missing>".to_owned()))?;
7530 let source_linked = attribute_value(element, b"sourceLinked")?
7531 .ok_or_else(|| invalid_attribute("numFmt", "sourceLinked", "<missing>".to_owned()))?;
7532 let source_linked = parse_bool_lexical("numFmt", "sourceLinked", &source_linked)?;
7533 let number_format = Self {
7534 format_code,
7535 source_linked,
7536 raw_attributes: capture_attributes_excluding(
7537 element,
7538 &[b"formatCode", b"sourceLinked"],
7539 )?,
7540 raw_content,
7541 };
7542 number_format.validate()?;
7543 Ok(number_format)
7544 }
7545
7546 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7547 self.validate()?;
7548 let mut start = BytesStart::new("c:numFmt");
7549 start.push_attribute(("formatCode", self.format_code.as_str()));
7550 start.push_attribute(("sourceLinked", bool_lexical(self.source_linked)));
7551 push_attributes(&mut start, &self.raw_attributes);
7552 if self.raw_content.is_empty() {
7553 writer
7554 .write_event(Event::Empty(start))
7555 .map_err(OxmlError::from)?;
7556 return Ok(());
7557 }
7558 writer
7559 .write_event(Event::Start(start))
7560 .map_err(OxmlError::from)?;
7561 for raw in &self.raw_content {
7562 writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
7563 }
7564 writer
7565 .write_event(Event::End(BytesEnd::new("c:numFmt")))
7566 .map_err(OxmlError::from)?;
7567 Ok(())
7568 }
7569
7570 pub fn format_value(&self, value: f64) -> Result<String> {
7572 if !value.is_finite() {
7573 return Err(ChartError::InvalidValue {
7574 element: "c:numFmt value".to_owned(),
7575 value: value.to_string(),
7576 });
7577 }
7578 if self.format_code == "General" {
7579 return Ok(value.to_string());
7580 }
7581
7582 let (numeric_code, percentage) = self
7583 .format_code
7584 .strip_suffix('%')
7585 .map(|code| (code, true))
7586 .unwrap_or((&self.format_code, false));
7587 let precision = if numeric_code == "0" {
7588 Some(0)
7589 } else if let Some(decimals) = numeric_code.strip_prefix("0.")
7590 && !decimals.is_empty()
7591 && decimals.bytes().all(|byte| byte == b'0')
7592 {
7593 Some(decimals.len())
7594 } else {
7595 None
7596 };
7597 let Some(precision) = precision else {
7598 return Err(ChartError::InvalidValue {
7599 element: "c:numFmt/@formatCode projection".to_owned(),
7600 value: self.format_code.clone(),
7601 });
7602 };
7603 let projected = if percentage { value * 100.0 } else { value };
7604 if !projected.is_finite() {
7605 return Err(ChartError::InvalidValue {
7606 element: "c:numFmt value".to_owned(),
7607 value: projected.to_string(),
7608 });
7609 }
7610 let mut formatted = format!("{projected:.precision$}");
7611 if percentage {
7612 formatted.push('%');
7613 }
7614 Ok(formatted)
7615 }
7616
7617 fn validate(&self) -> Result<()> {
7618 if self.format_code.is_empty() {
7619 return Err(ChartError::InvalidValue {
7620 element: "c:numFmt/@formatCode".to_owned(),
7621 value: self.format_code.clone(),
7622 });
7623 }
7624 validate_xml_text(&self.format_code, "c:numFmt/@formatCode")
7625 }
7626}
7627
7628impl CT_DLbls {
7629 pub fn from_xml(xml: &[u8]) -> Result<Self> {
7630 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
7631 }
7632
7633 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7634 let mut reader = Reader::from_reader(xml);
7635 let mut buffer = Vec::new();
7636 loop {
7637 match reader
7638 .read_event_into(&mut buffer)
7639 .map_err(OxmlError::from)?
7640 {
7641 Event::Start(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7642 chart_root_prefix(&element)?;
7643 if !element_is_in_namespace(&element, C_NS, inherited)? {
7644 return Err(ChartError::UnexpectedElement(element_name(&element)));
7645 }
7646 return Self::from_element(&mut reader, &element, inherited);
7647 }
7648 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7649 chart_root_prefix(&element)?;
7650 let namespaces = chart_bindings(inherited, &element)?;
7651 if !element_is_in_namespace(&element, C_NS, inherited)? {
7652 return Err(ChartError::UnexpectedElement(element_name(&element)));
7653 }
7654 let (raw_attributes, _) = capture_fixed_root_attributes(
7655 &element,
7656 &["xmlns:c", "xmlns:a", "xmlns:r"],
7657 )?;
7658 return Ok(Self {
7659 raw_attributes,
7660 namespace_declarations: standalone_namespace_declarations(&namespaces)?,
7661 ..Self::default()
7662 });
7663 }
7664 Event::Start(element) | Event::Empty(element) => {
7665 return Err(ChartError::UnexpectedElement(element_name(&element)));
7666 }
7667 Event::Eof => return Err(ChartError::MissingElement("c:dLbls".to_owned())),
7668 _ => {}
7669 }
7670 buffer.clear();
7671 }
7672 }
7673
7674 fn from_element(
7675 reader: &mut Reader<&[u8]>,
7676 start: &BytesStart<'_>,
7677 inherited: &NamespaceBindings,
7678 ) -> Result<Self> {
7679 reject_conflicting_prefix(start, b"a", A_NS)?;
7680 let namespaces = chart_bindings(inherited, start)?;
7681 require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
7682 let (raw_attributes, _) =
7683 capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
7684 let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
7685 let mut state = DataLabelsParseState::new(namespaces);
7686 let mut buffer = Vec::new();
7687 loop {
7688 match reader
7689 .read_event_into(&mut buffer)
7690 .map_err(OxmlError::from)?
7691 {
7692 Event::Start(element) => {
7693 let name = chart_child_local(&element, &state.namespaces)?;
7694 let raw = capture_element(reader, &element)?;
7695 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
7696 }
7697 Event::Empty(element) => {
7698 let name = chart_child_local(&element, &state.namespaces)?;
7699 let raw = capture_empty_element(&element)?;
7700 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
7701 }
7702 event @ (Event::Text(_)
7703 | Event::CData(_)
7704 | Event::Comment(_)
7705 | Event::PI(_)
7706 | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
7707 Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7708 return state.finish(raw_attributes, namespace_declarations);
7709 }
7710 Event::Eof => return Err(missing_end("c:dLbls")),
7711 _ => {}
7712 }
7713 buffer.clear();
7714 }
7715 }
7716
7717 pub fn to_xml(&self) -> Result<Vec<u8>> {
7718 let mut writer = Writer::new(Vec::new());
7719 self.write_xml(&mut writer, true)?;
7720 Ok(writer.into_inner())
7721 }
7722
7723 fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
7724 self.validate()?;
7725 let mut start = BytesStart::new("c:dLbls");
7726 if standalone {
7727 start.push_attribute(("xmlns:c", C_NS));
7728 start.push_attribute(("xmlns:a", A_NS));
7729 start.push_attribute(("xmlns:r", R_NS));
7730 }
7731 push_attributes(&mut start, &self.namespace_declarations);
7732 push_attributes(&mut start, &self.raw_attributes);
7733 writer
7734 .write_event(Event::Start(start))
7735 .map_err(OxmlError::from)?;
7736 emit_raw(writer, self.raw_children.at(0))?;
7737 if let Some(number_format) = &self.number_format {
7738 number_format.write_xml(writer)?;
7739 }
7740 emit_raw(writer, self.raw_children.at(1))?;
7741 if let Some(position) = self.position {
7742 write_scalar(
7743 writer,
7744 "c:dLblPos",
7745 position.as_str(),
7746 self.position_markup.as_ref(),
7747 )?;
7748 }
7749 emit_raw(writer, self.raw_children.at(2))?;
7750 write_optional_bool(
7751 writer,
7752 "c:showLegendKey",
7753 self.show_legend_key,
7754 self.show_legend_key_markup.as_ref(),
7755 )?;
7756 emit_raw(writer, self.raw_children.at(3))?;
7757 write_optional_bool(
7758 writer,
7759 "c:showVal",
7760 self.show_value,
7761 self.show_value_markup.as_ref(),
7762 )?;
7763 emit_raw(writer, self.raw_children.at(4))?;
7764 write_optional_bool(
7765 writer,
7766 "c:showCatName",
7767 self.show_category_name,
7768 self.show_category_name_markup.as_ref(),
7769 )?;
7770 emit_raw(writer, self.raw_children.at(5))?;
7771 write_optional_bool(
7772 writer,
7773 "c:showSerName",
7774 self.show_series_name,
7775 self.show_series_name_markup.as_ref(),
7776 )?;
7777 emit_raw(writer, self.raw_children.at(6))?;
7778 write_optional_bool(
7779 writer,
7780 "c:showPercent",
7781 self.show_percent,
7782 self.show_percent_markup.as_ref(),
7783 )?;
7784 emit_raw(writer, self.raw_children.at(7))?;
7785 write_optional_bool(
7786 writer,
7787 "c:showBubbleSize",
7788 self.show_bubble_size,
7789 self.show_bubble_size_markup.as_ref(),
7790 )?;
7791 emit_raw(writer, self.raw_children.at(8))?;
7792 if let Some(separator) = &self.separator {
7793 write_text(
7794 writer,
7795 "c:separator",
7796 separator,
7797 self.separator_markup
7798 .as_ref()
7799 .unwrap_or(&TextMarkup::default()),
7800 )?;
7801 }
7802 emit_raw(writer, self.raw_children.at(9))?;
7803 writer
7804 .write_event(Event::End(BytesEnd::new("c:dLbls")))
7805 .map_err(OxmlError::from)?;
7806 Ok(())
7807 }
7808
7809 fn validate(&self) -> Result<()> {
7810 if let Some(number_format) = &self.number_format {
7811 number_format.validate()?;
7812 }
7813 if let Some(separator) = &self.separator {
7814 validate_xml_text(separator, "c:separator")?;
7815 }
7816 Ok(())
7817 }
7818
7819 pub fn raw_children(&self) -> &OrderedRawChildren {
7820 &self.raw_children
7821 }
7822}
7823
7824struct DataLabelsParseState {
7825 namespaces: NamespaceBindings,
7826 number_format: Option<NumberFormat>,
7827 position: Option<(DataLabelPosition, ScalarMarkup)>,
7828 show_legend_key: Option<(bool, ScalarMarkup)>,
7829 show_value: Option<(bool, ScalarMarkup)>,
7830 show_category_name: Option<(bool, ScalarMarkup)>,
7831 show_series_name: Option<(bool, ScalarMarkup)>,
7832 show_percent: Option<(bool, ScalarMarkup)>,
7833 show_bubble_size: Option<(bool, ScalarMarkup)>,
7834 separator: Option<(String, TextMarkup)>,
7835 raw_children: OrderedRawChildren,
7836 boundary: usize,
7837}
7838
7839impl DataLabelsParseState {
7840 fn new(namespaces: NamespaceBindings) -> Self {
7841 Self {
7842 namespaces,
7843 number_format: None,
7844 position: None,
7845 show_legend_key: None,
7846 show_value: None,
7847 show_category_name: None,
7848 show_series_name: None,
7849 show_percent: None,
7850 show_bubble_size: None,
7851 separator: None,
7852 raw_children: OrderedRawChildren::default(),
7853 boundary: 0,
7854 }
7855 }
7856
7857 fn capture_event(&mut self, raw: Vec<u8>) {
7858 self.raw_children.push(self.boundary, raw);
7859 }
7860
7861 fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
7862 match name {
7863 b"numFmt" => {
7864 set_once(
7865 &mut self.number_format,
7866 NumberFormat::from_xml(&raw)?,
7867 "c:numFmt",
7868 )?;
7869 self.boundary = self.boundary.max(1);
7870 }
7871 b"dLblPos" => {
7872 let (value, markup) = scalar_value(&raw, "dLblPos")?;
7873 let lexical = value
7874 .ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
7875 let position = DataLabelPosition::parse(&lexical)
7876 .ok_or_else(|| invalid_attribute("dLblPos", "val", lexical))?;
7877 set_once(&mut self.position, (position, markup), "c:dLblPos")?;
7878 self.boundary = self.boundary.max(2);
7879 }
7880 b"showLegendKey" => {
7881 set_once(
7882 &mut self.show_legend_key,
7883 parse_bool_value(&raw, "showLegendKey")?,
7884 "c:showLegendKey",
7885 )?;
7886 self.boundary = self.boundary.max(3);
7887 }
7888 b"showVal" => {
7889 set_once(
7890 &mut self.show_value,
7891 parse_bool_value(&raw, "showVal")?,
7892 "c:showVal",
7893 )?;
7894 self.boundary = self.boundary.max(4);
7895 }
7896 b"showCatName" => {
7897 set_once(
7898 &mut self.show_category_name,
7899 parse_bool_value(&raw, "showCatName")?,
7900 "c:showCatName",
7901 )?;
7902 self.boundary = self.boundary.max(5);
7903 }
7904 b"showSerName" => {
7905 set_once(
7906 &mut self.show_series_name,
7907 parse_bool_value(&raw, "showSerName")?,
7908 "c:showSerName",
7909 )?;
7910 self.boundary = self.boundary.max(6);
7911 }
7912 b"showPercent" => {
7913 set_once(
7914 &mut self.show_percent,
7915 parse_bool_value(&raw, "showPercent")?,
7916 "c:showPercent",
7917 )?;
7918 self.boundary = self.boundary.max(7);
7919 }
7920 b"showBubbleSize" => {
7921 set_once(
7922 &mut self.show_bubble_size,
7923 parse_bool_value(&raw, "showBubbleSize")?,
7924 "c:showBubbleSize",
7925 )?;
7926 self.boundary = self.boundary.max(8);
7927 }
7928 b"separator" => {
7929 set_once(
7930 &mut self.separator,
7931 parse_text_element(&raw, b"separator", &self.namespaces)?,
7932 "c:separator",
7933 )?;
7934 self.boundary = self.boundary.max(9);
7935 }
7936 _ => {
7937 let boundary = data_labels_raw_boundary(name, self.boundary);
7938 self.raw_children.push(boundary, raw);
7939 self.boundary = self.boundary.max(boundary);
7940 }
7941 }
7942 Ok(())
7943 }
7944
7945 fn finish(
7946 self,
7947 raw_attributes: XmlAttributes,
7948 namespace_declarations: XmlAttributes,
7949 ) -> Result<CT_DLbls> {
7950 let (position, position_markup) = optional_markup(self.position);
7951 let (show_legend_key, show_legend_key_markup) = optional_bool_markup(self.show_legend_key);
7952 let (show_value, show_value_markup) = optional_bool_markup(self.show_value);
7953 let (show_category_name, show_category_name_markup) =
7954 optional_bool_markup(self.show_category_name);
7955 let (show_series_name, show_series_name_markup) =
7956 optional_bool_markup(self.show_series_name);
7957 let (show_percent, show_percent_markup) = optional_bool_markup(self.show_percent);
7958 let (show_bubble_size, show_bubble_size_markup) =
7959 optional_bool_markup(self.show_bubble_size);
7960 let (separator, separator_markup) = self
7961 .separator
7962 .map(|(value, markup)| (Some(value), Some(markup)))
7963 .unwrap_or((None, None));
7964 let labels = CT_DLbls {
7965 number_format: self.number_format,
7966 position,
7967 separator,
7968 show_legend_key,
7969 show_value,
7970 show_category_name,
7971 show_series_name,
7972 show_percent,
7973 show_bubble_size,
7974 position_markup,
7975 show_legend_key_markup,
7976 show_value_markup,
7977 show_category_name_markup,
7978 show_series_name_markup,
7979 show_percent_markup,
7980 show_bubble_size_markup,
7981 separator_markup,
7982 raw_attributes,
7983 namespace_declarations,
7984 raw_children: raw_children_in_schema_order(&self.raw_children, 9),
7985 };
7986 labels.validate()?;
7987 Ok(labels)
7988 }
7989}
7990
7991fn data_labels_raw_boundary(name: &[u8], current: usize) -> usize {
7992 match name {
7993 b"dLbl" | b"delete" => 0,
7994 b"spPr" | b"txPr" => 1,
7995 b"showLeaderLines" | b"leaderLines" | b"extLst" => 9,
7996 _ => current,
7997 }
7998}
7999
8000fn optional_bool_markup(value: Option<(bool, ScalarMarkup)>) -> (bool, Option<ScalarMarkup>) {
8001 value
8002 .map(|(value, markup)| (value, Some(markup)))
8003 .unwrap_or((false, None))
8004}
8005
8006fn write_optional_bool(
8007 writer: &mut Writer<Vec<u8>>,
8008 tag: &str,
8009 value: bool,
8010 markup: Option<&ScalarMarkup>,
8011) -> Result<()> {
8012 if value || markup.is_some() {
8013 write_scalar(writer, tag, bool_lexical(value), markup)?;
8014 }
8015 Ok(())
8016}
8017
8018fn default_axis_id_markup(id: AxisId) -> AxisIdMarkup {
8019 AxisIdMarkup {
8020 scalar: ScalarMarkup::default(),
8021 parsed: id,
8022 lexical: id.value().to_string(),
8023 }
8024}
8025
8026fn parse_axis_id_scalar(xml: &[u8], local: &str) -> Result<(AxisId, AxisIdMarkup)> {
8027 let (value, scalar) = scalar_value(xml, local)?;
8028 let lexical = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8029 let parsed = lexical
8030 .parse::<i64>()
8031 .map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
8032 let id = AxisId::new(parsed).map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
8033 Ok((
8034 id,
8035 AxisIdMarkup {
8036 scalar,
8037 parsed: id,
8038 lexical,
8039 },
8040 ))
8041}
8042
8043fn write_axis_id<W: Write>(
8044 writer: &mut Writer<W>,
8045 tag: &str,
8046 id: AxisId,
8047 markup: &AxisIdMarkup,
8048) -> Result<()> {
8049 AxisId::new(id.value())?;
8050 let normalized;
8051 let value = if id == markup.parsed {
8052 markup.lexical.as_str()
8053 } else {
8054 normalized = id.value().to_string();
8055 normalized.as_str()
8056 };
8057 write_scalar(writer, tag, value, Some(&markup.scalar))
8058}
8059
8060fn parse_f64_scalar(xml: &[u8], local: &str) -> Result<(f64, ScalarMarkup)> {
8061 let (value, markup) = scalar_value(xml, local)?;
8062 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8063 let parsed = value
8064 .parse::<f64>()
8065 .map_err(|_| invalid_attribute(local, "val", value.clone()))?;
8066 if !parsed.is_finite() {
8067 return Err(invalid_attribute(local, "val", value));
8068 }
8069 Ok((parsed, markup))
8070}
8071
8072fn parse_axis_position(xml: &[u8]) -> Result<(AxisPosition, ScalarMarkup)> {
8073 let (value, markup) = scalar_value(xml, "axPos")?;
8074 let value = value.ok_or_else(|| invalid_attribute("axPos", "val", "<missing>".to_owned()))?;
8075 let parsed =
8076 AxisPosition::parse(&value).ok_or_else(|| invalid_attribute("axPos", "val", value))?;
8077 Ok((parsed, markup))
8078}
8079
8080fn parse_tick_mark(xml: &[u8], local: &str) -> Result<(TickMark, ScalarMarkup)> {
8081 let (value, markup) = scalar_value(xml, local)?;
8082 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8083 let parsed = TickMark::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
8084 Ok((parsed, markup))
8085}
8086
8087fn parse_tick_label_position(xml: &[u8]) -> Result<(TickLabelPosition, ScalarMarkup)> {
8088 let (value, markup) = scalar_value(xml, "tickLblPos")?;
8089 let value =
8090 value.ok_or_else(|| invalid_attribute("tickLblPos", "val", "<missing>".to_owned()))?;
8091 let parsed = TickLabelPosition::parse(&value)
8092 .ok_or_else(|| invalid_attribute("tickLblPos", "val", value))?;
8093 Ok((parsed, markup))
8094}
8095
8096fn optional_markup<T>(value: Option<(T, ScalarMarkup)>) -> (Option<T>, Option<ScalarMarkup>) {
8097 value
8098 .map(|(value, markup)| (Some(value), Some(markup)))
8099 .unwrap_or((None, None))
8100}
8101
8102fn optional_scalar_markup_eq(left: &Option<ScalarMarkup>, right: &Option<ScalarMarkup>) -> bool {
8103 let empty = ScalarMarkup::default();
8104 left.as_ref().unwrap_or(&empty) == right.as_ref().unwrap_or(&empty)
8105}
8106
8107fn axis_id_markup_eq(left: &AxisIdMarkup, right: &AxisIdMarkup) -> bool {
8108 left.scalar == right.scalar
8109}
8110
8111const fn bool_lexical(value: bool) -> &'static str {
8112 if value { "1" } else { "0" }
8113}
8114
8115fn parse_bool_lexical(element: &str, attribute: &str, value: &str) -> Result<bool> {
8116 match value {
8117 "1" | "true" => Ok(true),
8118 "0" | "false" => Ok(false),
8119 _ => Err(invalid_attribute(element, attribute, value.to_owned())),
8120 }
8121}
8122
8123impl CT_Title {
8124 fn plain_text(text: &str) -> Self {
8125 let mut body = CT_TextBody::new();
8126 body.set_text(text);
8127 let mut raw_children = OrderedRawChildren::default();
8128 raw_children.push(1, br#"<c:layout/><c:overlay val="0"/>"#.to_vec());
8129 Self {
8130 text: Some(body),
8131 raw_children,
8132 ..Self::default()
8133 }
8134 }
8135
8136 fn from_xml(xml: &[u8]) -> Result<Self> {
8137 let (raw_attributes, raw_children) = parse_raw_shell(xml, b"title", "c:title")?;
8138 Ok(Self {
8139 text: None,
8140 raw_attributes,
8141 raw_children,
8142 })
8143 }
8144
8145 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8146 if self.text.is_none() {
8147 return write_raw_shell(writer, "c:title", &self.raw_attributes, &self.raw_children);
8148 }
8149 let mut start = BytesStart::new("c:title");
8150 push_attributes(&mut start, &self.raw_attributes);
8151 writer
8152 .write_event(Event::Start(start))
8153 .map_err(OxmlError::from)?;
8154 emit_raw(writer, self.raw_children.at(0))?;
8155 writer
8156 .write_event(Event::Start(BytesStart::new("c:tx")))
8157 .map_err(OxmlError::from)?;
8158 if let Some(text) = &self.text {
8159 text.write_xml_as(writer, "c:rich")?;
8160 }
8161 writer
8162 .write_event(Event::End(BytesEnd::new("c:tx")))
8163 .map_err(OxmlError::from)?;
8164 emit_raw(writer, self.raw_children.at(1))?;
8165 writer
8166 .write_event(Event::End(BytesEnd::new("c:title")))
8167 .map_err(OxmlError::from)?;
8168 Ok(())
8169 }
8170
8171 pub fn raw_children(&self) -> &OrderedRawChildren {
8172 &self.raw_children
8173 }
8174}
8175
8176impl CT_PlotArea {
8177 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
8178 let (raw_attributes, captured_children) = parse_raw_shell(xml, b"plotArea", "c:plotArea")?;
8179 let namespace_bindings = root_chart_bindings(xml, b"plotArea", inherited)?;
8180 let direct_roots: Vec<_> = captured_children
8181 .at(0)
8182 .filter_map(|raw| chart_root_local(raw, &namespace_bindings).transpose())
8183 .collect::<Result<_>>()?;
8184 let plot_roots: Vec<_> = direct_roots
8185 .iter()
8186 .filter(|local| is_plot_container(local))
8187 .cloned()
8188 .collect();
8189 let typed_local = (plot_roots.len() == 1
8190 && matches!(
8191 plot_roots[0].as_slice(),
8192 b"barChart"
8193 | b"lineChart"
8194 | b"pieChart"
8195 | b"doughnutChart"
8196 | b"areaChart"
8197 | b"scatterChart"
8198 | b"radarChart"
8199 ))
8200 .then(|| plot_roots[0].clone());
8201
8202 let Some(typed_local) = typed_local else {
8203 return Ok(Self {
8204 raw_attributes,
8205 raw_children: captured_children,
8206 namespace_bindings,
8207 plots: None,
8208 plot_markup: Vec::new(),
8209 axes: Vec::new(),
8210 });
8211 };
8212
8213 let mut raw_children = OrderedRawChildren::default();
8214 let mut plots = Vec::new();
8215 let mut plot_markup = Vec::new();
8216 let mut axes = Vec::new();
8217 let mut boundary = 0usize;
8218 for raw in captured_children.at(0) {
8219 let local = chart_root_local(raw, &namespace_bindings)?;
8220 if local.as_deref() == Some(typed_local.as_slice()) {
8221 let (plot, markup) = parse_plot(raw, &namespace_bindings)?;
8222 plots.push(plot);
8223 plot_markup.push(markup);
8224 boundary = 1;
8225 } else if local.as_deref().and_then(AxisKind::parse).is_some() {
8226 axes.push(Axis::from_xml_with_namespaces(raw, &namespace_bindings)?);
8227 boundary = axes.len() + 1;
8228 } else {
8229 raw_children.push(boundary, raw.to_vec());
8230 }
8231 }
8232 let plot_area = Self {
8233 raw_attributes,
8234 raw_children,
8235 namespace_bindings,
8236 plots: Some(plots),
8237 plot_markup,
8238 axes,
8239 };
8240 plot_area.validate_typed()?;
8241 Ok(plot_area)
8242 }
8243
8244 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8245 let Some(plots) = &self.plots else {
8246 return write_raw_shell(
8247 writer,
8248 "c:plotArea",
8249 &self.raw_attributes,
8250 &self.raw_children,
8251 );
8252 };
8253 self.validate_typed()?;
8254 let mut start = BytesStart::new("c:plotArea");
8255 push_attributes(&mut start, &self.raw_attributes);
8256 writer
8257 .write_event(Event::Start(start))
8258 .map_err(OxmlError::from)?;
8259 emit_raw(writer, self.raw_children.at(0))?;
8260 for (index, plot) in plots.iter().enumerate() {
8261 write_plot(
8262 writer,
8263 plot,
8264 self.plot_markup
8265 .get(index)
8266 .unwrap_or(&PlotMarkup::default()),
8267 )?;
8268 }
8269 emit_raw(writer, self.raw_children.at(1))?;
8270 for (index, axis) in self.axes.iter().enumerate() {
8271 writer
8272 .get_mut()
8273 .write_all(&axis.to_xml()?)
8274 .map_err(OxmlError::from)?;
8275 emit_raw(writer, self.raw_children.at(index + 2))?;
8276 }
8277 writer
8278 .write_event(Event::End(BytesEnd::new("c:plotArea")))
8279 .map_err(OxmlError::from)?;
8280 Ok(())
8281 }
8282
8283 pub fn raw_children(&self) -> &OrderedRawChildren {
8284 &self.raw_children
8285 }
8286
8287 pub fn new(plots: Vec<Plot>, axes: Vec<Axis>) -> Result<Self> {
8289 let plot_markup = plots
8290 .iter()
8291 .map(|plot| {
8292 let mut markup = PlotMarkup::default();
8293 if matches!(plot, Plot::Line { .. }) {
8294 markup.marker = Some(ScalarMarkup::default());
8295 markup.smooth = Some(ScalarMarkup::default());
8296 }
8297 if matches!(plot, Plot::Doughnut { .. }) {
8298 markup.hole_size = Some(ScalarMarkup::default());
8299 }
8300 markup
8301 })
8302 .collect();
8303 let plot_area = Self {
8304 raw_attributes: Vec::new(),
8305 raw_children: OrderedRawChildren::default(),
8306 namespace_bindings: chart_namespace_defaults(),
8307 plot_markup,
8308 plots: Some(plots),
8309 axes,
8310 };
8311 plot_area.validate_typed()?;
8312 Ok(plot_area)
8313 }
8314
8315 pub fn plots(&self) -> Result<&[Plot]> {
8317 self.plots
8318 .as_deref()
8319 .ok_or_else(|| ChartError::InvalidValue {
8320 element: "c:plotArea".to_owned(),
8321 value: "unsupported or combination plot area is opaque".to_owned(),
8322 })
8323 }
8324
8325 pub fn plots_mut(&mut self) -> Result<&mut [Plot]> {
8327 self.plots
8328 .as_deref_mut()
8329 .ok_or_else(|| ChartError::InvalidValue {
8330 element: "c:plotArea".to_owned(),
8331 value: "cannot combine typed plots with an opaque plot choice".to_owned(),
8332 })
8333 }
8334
8335 pub fn series(&self) -> Result<Vec<Series>> {
8337 if let Some(plots) = &self.plots {
8338 return Ok(plots
8339 .iter()
8340 .flat_map(|plot| match plot {
8341 Plot::Bar { series, .. }
8342 | Plot::Line { series, .. }
8343 | Plot::Pie { series, .. }
8344 | Plot::Doughnut { series, .. }
8345 | Plot::Area { series, .. }
8346 | Plot::Scatter { series, .. }
8347 | Plot::Radar { series, .. } => series.clone(),
8348 })
8349 .collect());
8350 }
8351 let mut series = Vec::new();
8352 for raw in self.raw_children.at(0) {
8353 parse_plot_series(raw, &self.namespace_bindings, &mut series)?;
8354 }
8355 Ok(series)
8356 }
8357
8358 pub fn axes(&self) -> Result<Vec<Axis>> {
8360 if self.plots.is_some() {
8361 self.validate_typed()?;
8362 return Ok(self.axes.clone());
8363 }
8364 let mut axes = Vec::new();
8365 for raw in self.raw_children.at(0) {
8366 if let Some(axis) = parse_plot_axis(raw, &self.namespace_bindings)? {
8367 axes.push(axis);
8368 }
8369 }
8370 validate_axis_pairs(&axes)?;
8371 Ok(axes)
8372 }
8373
8374 fn validate_typed(&self) -> Result<()> {
8375 let plots = self
8376 .plots
8377 .as_ref()
8378 .ok_or_else(|| ChartError::InvalidValue {
8379 element: "c:plotArea".to_owned(),
8380 value: "opaque plot area has no typed validation view".to_owned(),
8381 })?;
8382 if plots.len() != 1 {
8383 return Err(ChartError::InvalidValue {
8384 element: "c:plotArea".to_owned(),
8385 value: "typed plot area requires exactly one plot family".to_owned(),
8386 });
8387 }
8388 validate_axis_pairs(&self.axes)?;
8389 for plot in plots {
8390 plot.validate()?;
8391 if let Some(axis_ids) = plot.axis_ids() {
8392 for id in axis_ids {
8393 if !self.axes.iter().any(|axis| axis.id == id) {
8394 return Err(ChartError::InvalidValue {
8395 element: "c:axId".to_owned(),
8396 value: format!("plot references missing axis {}", id.value()),
8397 });
8398 }
8399 }
8400 } else if !self.axes.is_empty() {
8401 return Err(ChartError::InvalidValue {
8402 element: "c:plotArea".to_owned(),
8403 value: "pie and doughnut plot areas must not own axes".to_owned(),
8404 });
8405 }
8406 if plot.axis_ids().is_some() && self.axes.len() != 2 {
8407 return Err(ChartError::InvalidValue {
8408 element: "c:plotArea".to_owned(),
8409 value: format!(
8410 "axis-owned plot requires exactly 2 axes, found {}",
8411 self.axes.len()
8412 ),
8413 });
8414 }
8415 }
8416 Ok(())
8417 }
8418}
8419
8420impl Default for CT_PlotArea {
8421 fn default() -> Self {
8422 Self {
8423 raw_attributes: Vec::new(),
8424 raw_children: OrderedRawChildren::default(),
8425 namespace_bindings: chart_namespace_defaults(),
8426 plots: Some(Vec::new()),
8427 plot_markup: Vec::new(),
8428 axes: Vec::new(),
8429 }
8430 }
8431}
8432
8433fn chart_root_local(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Vec<u8>>> {
8434 let mut reader = Reader::from_reader(xml);
8435 let mut buffer = Vec::new();
8436 loop {
8437 match reader
8438 .read_event_into(&mut buffer)
8439 .map_err(OxmlError::from)?
8440 {
8441 Event::Start(element) | Event::Empty(element) => {
8442 return Ok(element_is_in_namespace(&element, C_NS, inherited)?
8443 .then(|| local_name(element.name().as_ref()).to_vec()));
8444 }
8445 Event::Eof => return Ok(None),
8446 _ => {}
8447 }
8448 buffer.clear();
8449 }
8450}
8451
8452fn is_plot_container(local: &[u8]) -> bool {
8453 matches!(
8454 local,
8455 b"areaChart"
8456 | b"area3DChart"
8457 | b"barChart"
8458 | b"bar3DChart"
8459 | b"bubbleChart"
8460 | b"doughnutChart"
8461 | b"lineChart"
8462 | b"line3DChart"
8463 | b"ofPieChart"
8464 | b"pieChart"
8465 | b"pie3DChart"
8466 | b"radarChart"
8467 | b"scatterChart"
8468 | b"stockChart"
8469 | b"surfaceChart"
8470 | b"surface3DChart"
8471 )
8472}
8473
8474fn parse_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8475 let local = chart_root_local(xml, inherited)?
8476 .ok_or_else(|| ChartError::MissingElement("c:barChart or c:lineChart".to_owned()))?;
8477 match local.as_slice() {
8478 b"barChart" => parse_bar_plot(xml, inherited),
8479 b"lineChart" => parse_line_plot(xml, inherited),
8480 b"pieChart" | b"doughnutChart" | b"areaChart" | b"scatterChart" | b"radarChart" => {
8481 parse_remaining_plot(xml, inherited, &local)
8482 }
8483 _ => Err(ChartError::UnexpectedElement(format!(
8484 "c:{}",
8485 String::from_utf8_lossy(&local)
8486 ))),
8487 }
8488}
8489
8490fn parse_remaining_plot(
8491 xml: &[u8],
8492 inherited: &NamespaceBindings,
8493 local: &[u8],
8494) -> Result<(Plot, PlotMarkup)> {
8495 let root = String::from_utf8_lossy(local).into_owned();
8496 let mut reader = Reader::from_reader(xml);
8497 let mut buffer = Vec::new();
8498 loop {
8499 match reader
8500 .read_event_into(&mut buffer)
8501 .map_err(OxmlError::from)?
8502 {
8503 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
8504 let namespaces = typed_rewrite_bindings(&element, inherited)?;
8505 let (mut raw_attributes, namespace_declarations) =
8506 capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8507 raw_attributes.splice(0..0, namespace_declarations);
8508 let mut grouping = None;
8509 let mut style = None;
8510 let mut first_slice_angle = None;
8511 let mut hole_size = None;
8512 let mut series = Vec::new();
8513 let mut data_labels = None;
8514 let mut axis_ids = Vec::new();
8515 let mut raw_children = OrderedRawChildren::default();
8516 let mut boundary = 0usize;
8517 let mut inner = Vec::new();
8518 loop {
8519 match reader
8520 .read_event_into(&mut inner)
8521 .map_err(OxmlError::from)?
8522 {
8523 Event::Start(child) => {
8524 let name = chart_child_local(&child, &namespaces)?;
8525 let raw = capture_element(&mut reader, &child)?;
8526 parse_remaining_plot_child(
8527 local,
8528 name.as_deref().unwrap_or_default(),
8529 raw,
8530 &namespaces,
8531 &mut grouping,
8532 &mut style,
8533 &mut first_slice_angle,
8534 &mut hole_size,
8535 &mut series,
8536 &mut data_labels,
8537 &mut axis_ids,
8538 &mut raw_children,
8539 &mut boundary,
8540 )?;
8541 }
8542 Event::Empty(child) => {
8543 let name = chart_child_local(&child, &namespaces)?;
8544 let raw = capture_empty_element(&child)?;
8545 parse_remaining_plot_child(
8546 local,
8547 name.as_deref().unwrap_or_default(),
8548 raw,
8549 &namespaces,
8550 &mut grouping,
8551 &mut style,
8552 &mut first_slice_angle,
8553 &mut hole_size,
8554 &mut series,
8555 &mut data_labels,
8556 &mut axis_ids,
8557 &mut raw_children,
8558 &mut boundary,
8559 )?;
8560 }
8561 event @ (Event::Text(_)
8562 | Event::CData(_)
8563 | Event::Comment(_)
8564 | Event::PI(_)
8565 | Event::GeneralRef(_)) => {
8566 raw_children.push(boundary, capture_event(event)?);
8567 }
8568 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
8569 let needs_axes =
8570 matches!(local, b"areaChart" | b"scatterChart" | b"radarChart");
8571 if needs_axes && axis_ids.len() != 2 {
8572 return Err(ChartError::InvalidValue {
8573 element: format!("c:{root}/c:axId"),
8574 value: format!("expected 2, found {}", axis_ids.len()),
8575 });
8576 }
8577 if !needs_axes && !axis_ids.is_empty() {
8578 return Err(ChartError::InvalidValue {
8579 element: format!("c:{root}/c:axId"),
8580 value: "axis-free plot contains axis references".to_owned(),
8581 });
8582 }
8583 let parsed_ids = needs_axes.then(|| [axis_ids[0].0, axis_ids[1].0]);
8584 let original_series_keys = series
8585 .iter()
8586 .map(|item| (item.index, item.order))
8587 .collect::<Vec<_>>();
8588 let plot = match local {
8589 b"pieChart" => Plot::Pie {
8590 first_slice_angle: first_slice_angle
8591 .as_ref()
8592 .map_or(0, |item| item.0),
8593 series,
8594 data_labels,
8595 },
8596 b"doughnutChart" => Plot::Doughnut {
8597 first_slice_angle: first_slice_angle
8598 .as_ref()
8599 .map_or(0, |item| item.0),
8600 hole_size: hole_size.as_ref().map_or(50, |item| item.0),
8601 series,
8602 data_labels,
8603 },
8604 b"areaChart" => Plot::Area {
8605 grouping: grouping
8606 .as_ref()
8607 .ok_or_else(|| {
8608 ChartError::MissingElement("c:grouping".to_owned())
8609 })?
8610 .0,
8611 series,
8612 data_labels,
8613 axis_ids: parsed_ids.ok_or_else(|| {
8614 ChartError::MissingElement("c:areaChart/c:axId".to_owned())
8615 })?,
8616 },
8617 b"scatterChart" => {
8618 let lexical = style
8619 .as_ref()
8620 .ok_or_else(|| {
8621 ChartError::MissingElement("c:scatterStyle".to_owned())
8622 })?
8623 .0
8624 .as_str();
8625 Plot::Scatter {
8626 style: ScatterStyle::parse(lexical).ok_or_else(|| {
8627 invalid_attribute(
8628 "scatterStyle",
8629 "val",
8630 lexical.to_owned(),
8631 )
8632 })?,
8633 series,
8634 data_labels,
8635 axis_ids: parsed_ids.ok_or_else(|| {
8636 ChartError::MissingElement(
8637 "c:scatterChart/c:axId".to_owned(),
8638 )
8639 })?,
8640 }
8641 }
8642 b"radarChart" => {
8643 let lexical = style
8644 .as_ref()
8645 .ok_or_else(|| {
8646 ChartError::MissingElement("c:radarStyle".to_owned())
8647 })?
8648 .0
8649 .as_str();
8650 Plot::Radar {
8651 style: RadarStyle::parse(lexical).ok_or_else(|| {
8652 invalid_attribute(
8653 "radarStyle",
8654 "val",
8655 lexical.to_owned(),
8656 )
8657 })?,
8658 series,
8659 data_labels,
8660 axis_ids: parsed_ids.ok_or_else(|| {
8661 ChartError::MissingElement(
8662 "c:radarChart/c:axId".to_owned(),
8663 )
8664 })?,
8665 }
8666 }
8667 _ => {
8668 return Err(ChartError::UnexpectedElement(format!(
8669 "c:{}",
8670 String::from_utf8_lossy(local)
8671 )));
8672 }
8673 };
8674 plot.validate()?;
8675 let final_boundary = boundary.max(original_series_keys.len() + 6);
8676 return Ok((
8677 plot,
8678 PlotMarkup {
8679 raw_attributes,
8680 raw_children: raw_children_in_schema_order(
8681 &raw_children,
8682 final_boundary,
8683 ),
8684 grouping: grouping.map(|item| item.1),
8685 first_slice_angle: first_slice_angle.map(|item| item.1),
8686 hole_size: hole_size.map(|item| item.1),
8687 style: style.map(|item| item.1),
8688 axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8689 original_series_keys,
8690 original_axis_ids: parsed_ids
8691 .map_or_else(Vec::new, |ids| ids.to_vec()),
8692 parsed_remaining: match local {
8693 b"pieChart" => Some("pieChart"),
8694 b"doughnutChart" => Some("doughnutChart"),
8695 b"areaChart" => Some("areaChart"),
8696 b"scatterChart" => Some("scatterChart"),
8697 b"radarChart" => Some("radarChart"),
8698 _ => None,
8699 },
8700 ..PlotMarkup::default()
8701 },
8702 ));
8703 }
8704 Event::Eof => return Err(missing_end(&format!("c:{root}"))),
8705 _ => {}
8706 }
8707 inner.clear();
8708 }
8709 }
8710 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
8711 return Err(ChartError::MissingElement("c:ser".to_owned()));
8712 }
8713 Event::Start(element) | Event::Empty(element) => {
8714 return Err(ChartError::UnexpectedElement(element_name(&element)));
8715 }
8716 Event::Eof => return Err(ChartError::MissingElement(format!("c:{root}"))),
8717 _ => {}
8718 }
8719 buffer.clear();
8720 }
8721}
8722
8723#[allow(clippy::too_many_arguments)]
8724fn parse_remaining_plot_child(
8725 root: &[u8],
8726 name: &[u8],
8727 raw: Vec<u8>,
8728 namespaces: &NamespaceBindings,
8729 grouping: &mut Option<(Grouping, ScalarMarkup)>,
8730 style: &mut Option<(String, ScalarMarkup)>,
8731 first_slice_angle: &mut Option<(u16, ScalarMarkup)>,
8732 hole_size: &mut Option<(u8, ScalarMarkup)>,
8733 series: &mut Vec<Series>,
8734 data_labels: &mut Option<CT_DLbls>,
8735 axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
8736 raw_children: &mut OrderedRawChildren,
8737 boundary: &mut usize,
8738) -> Result<()> {
8739 let property_first = matches!(root, b"areaChart" | b"scatterChart" | b"radarChart");
8740 let base = usize::from(property_first);
8741 match name {
8742 b"grouping" if root == b"areaChart" => {
8743 let (value, markup) = required_scalar(&raw, "grouping")?;
8744 let parsed = Grouping::parse(&value)
8745 .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
8746 set_once(grouping, (parsed, markup), "c:grouping")?;
8747 *boundary = (*boundary).max(1);
8748 }
8749 b"scatterStyle" if root == b"scatterChart" => {
8750 let (value, markup) = required_scalar(&raw, "scatterStyle")?;
8751 if ScatterStyle::parse(&value).is_none() {
8752 return Err(invalid_attribute("scatterStyle", "val", value));
8753 }
8754 set_once(style, (value, markup), "c:scatterStyle")?;
8755 *boundary = (*boundary).max(1);
8756 }
8757 b"radarStyle" if root == b"radarChart" => {
8758 let (value, markup) = required_scalar(&raw, "radarStyle")?;
8759 if RadarStyle::parse(&value).is_none() {
8760 return Err(invalid_attribute("radarStyle", "val", value));
8761 }
8762 set_once(style, (value, markup), "c:radarStyle")?;
8763 *boundary = (*boundary).max(1);
8764 }
8765 b"ser" => {
8766 series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
8767 *boundary = (*boundary).max(base + series.len());
8768 }
8769 b"dLbls" => {
8770 set_once(
8771 data_labels,
8772 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
8773 "c:dLbls",
8774 )?;
8775 *boundary = (*boundary).max(base + series.len() + 1);
8776 }
8777 b"firstSliceAng" if matches!(root, b"pieChart" | b"doughnutChart") => {
8778 let (value, markup) = required_scalar(&raw, "firstSliceAng")?;
8779 let parsed = value
8780 .parse::<u16>()
8781 .map_err(|_| invalid_attribute("firstSliceAng", "val", value.clone()))?;
8782 if parsed > 360 {
8783 return Err(invalid_attribute("firstSliceAng", "val", value));
8784 }
8785 set_once(first_slice_angle, (parsed, markup), "c:firstSliceAng")?;
8786 *boundary = (*boundary).max(series.len() + 2);
8787 }
8788 b"holeSize" if root == b"doughnutChart" => {
8789 let (value, markup) = required_scalar(&raw, "holeSize")?;
8790 let parsed = value
8791 .parse::<u8>()
8792 .map_err(|_| invalid_attribute("holeSize", "val", value.clone()))?;
8793 if !(10..=90).contains(&parsed) {
8794 return Err(invalid_attribute("holeSize", "val", value));
8795 }
8796 set_once(hole_size, (parsed, markup), "c:holeSize")?;
8797 *boundary = (*boundary).max(series.len() + 3);
8798 }
8799 b"axId" if property_first => {
8800 if axis_ids.len() == 2 {
8801 return Err(ChartError::DuplicateElement("c:axId".to_owned()));
8802 }
8803 axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
8804 *boundary = (*boundary).max(base + series.len() + 1 + axis_ids.len());
8805 }
8806 b"axId" => {
8807 return Err(ChartError::InvalidValue {
8808 element: "c:axId".to_owned(),
8809 value: "axis-free plot contains axis reference".to_owned(),
8810 });
8811 }
8812 _ => {
8813 let raw_boundary = match (root, name) {
8814 (_, b"varyColors") => base,
8815 (b"pieChart", b"extLst") => series.len() + 2,
8816 (b"doughnutChart", b"extLst") => series.len() + 3,
8817 (b"areaChart" | b"scatterChart" | b"radarChart", b"dropLines") => {
8818 base + series.len() + 1
8819 }
8820 (b"areaChart" | b"scatterChart" | b"radarChart", b"extLst") => {
8821 base + series.len() + 3
8822 }
8823 _ => *boundary,
8824 };
8825 raw_children.push(raw_boundary, raw);
8826 *boundary = (*boundary).max(raw_boundary);
8827 }
8828 }
8829 Ok(())
8830}
8831
8832fn parse_bar_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8833 let mut reader = Reader::from_reader(xml);
8834 let mut buffer = Vec::new();
8835 loop {
8836 match reader
8837 .read_event_into(&mut buffer)
8838 .map_err(OxmlError::from)?
8839 {
8840 Event::Start(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
8841 let namespaces = typed_rewrite_bindings(&element, inherited)?;
8842 let (mut raw_attributes, namespace_declarations) =
8843 capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8844 raw_attributes.splice(0..0, namespace_declarations);
8845 let mut direction = None;
8846 let mut grouping = None;
8847 let mut gap_width = None;
8848 let mut overlap = None;
8849 let mut series = Vec::new();
8850 let mut data_labels = None;
8851 let mut axis_ids = Vec::new();
8852 let mut raw_children = OrderedRawChildren::default();
8853 let mut boundary = 0usize;
8854 let mut inner = Vec::new();
8855 loop {
8856 match reader
8857 .read_event_into(&mut inner)
8858 .map_err(OxmlError::from)?
8859 {
8860 Event::Start(child) => {
8861 let name = chart_child_local(&child, &namespaces)?;
8862 let raw = capture_element(&mut reader, &child)?;
8863 parse_bar_child(
8864 name.as_deref().unwrap_or_default(),
8865 raw,
8866 &namespaces,
8867 &mut direction,
8868 &mut grouping,
8869 &mut gap_width,
8870 &mut overlap,
8871 &mut series,
8872 &mut data_labels,
8873 &mut axis_ids,
8874 &mut raw_children,
8875 &mut boundary,
8876 )?;
8877 }
8878 Event::Empty(child) => {
8879 let name = chart_child_local(&child, &namespaces)?;
8880 let raw = capture_empty_element(&child)?;
8881 parse_bar_child(
8882 name.as_deref().unwrap_or_default(),
8883 raw,
8884 &namespaces,
8885 &mut direction,
8886 &mut grouping,
8887 &mut gap_width,
8888 &mut overlap,
8889 &mut series,
8890 &mut data_labels,
8891 &mut axis_ids,
8892 &mut raw_children,
8893 &mut boundary,
8894 )?;
8895 }
8896 event @ (Event::Text(_)
8897 | Event::CData(_)
8898 | Event::Comment(_)
8899 | Event::PI(_)
8900 | Event::GeneralRef(_)) => {
8901 raw_children.push(boundary, capture_event(event)?);
8902 }
8903 Event::End(end) if matches_local_name(end.name().as_ref(), b"barChart") => {
8904 let (direction, direction_markup) = direction
8905 .ok_or_else(|| ChartError::MissingElement("c:barDir".to_owned()))?;
8906 let (grouping, grouping_markup) = grouping.ok_or_else(|| {
8907 ChartError::MissingElement("c:grouping".to_owned())
8908 })?;
8909 if axis_ids.len() != 2 {
8910 return Err(ChartError::InvalidValue {
8911 element: "c:barChart/c:axId".to_owned(),
8912 value: format!("expected 2, found {}", axis_ids.len()),
8913 });
8914 }
8915 let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
8916 let original_series_keys: Vec<(u32, u32)> =
8917 series.iter().map(|item| (item.index, item.order)).collect();
8918 let plot = Plot::Bar {
8919 direction,
8920 grouping,
8921 gap_width: gap_width.as_ref().map_or(150, |item| item.0),
8922 overlap: overlap.as_ref().map_or(0, |item| item.0),
8923 series,
8924 data_labels,
8925 axis_ids: parsed_ids,
8926 };
8927 plot.validate()?;
8928 return Ok((
8929 plot,
8930 PlotMarkup {
8931 raw_attributes,
8932 raw_children: raw_children_in_schema_order(
8933 &raw_children,
8934 original_series_keys.len() + 7,
8935 ),
8936 direction: Some(direction_markup),
8937 grouping: Some(grouping_markup),
8938 gap_width: gap_width.map(|item| item.1),
8939 overlap: overlap.map(|item| item.1),
8940 marker: None,
8941 smooth: None,
8942 first_slice_angle: None,
8943 hole_size: None,
8944 style: None,
8945 axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8946 original_series_keys,
8947 original_axis_ids: parsed_ids.to_vec(),
8948 parsed_bar: Some(true),
8949 parsed_remaining: None,
8950 },
8951 ));
8952 }
8953 Event::Eof => return Err(missing_end("c:barChart")),
8954 _ => {}
8955 }
8956 inner.clear();
8957 }
8958 }
8959 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
8960 return Err(ChartError::MissingElement("c:barDir".to_owned()));
8961 }
8962 Event::Start(element) | Event::Empty(element) => {
8963 return Err(ChartError::UnexpectedElement(element_name(&element)));
8964 }
8965 Event::Eof => return Err(ChartError::MissingElement("c:barChart".to_owned())),
8966 _ => {}
8967 }
8968 buffer.clear();
8969 }
8970}
8971
8972#[allow(clippy::too_many_arguments)]
8973fn parse_bar_child(
8974 name: &[u8],
8975 raw: Vec<u8>,
8976 namespaces: &NamespaceBindings,
8977 direction: &mut Option<(BarDirection, ScalarMarkup)>,
8978 grouping: &mut Option<(BarGrouping, ScalarMarkup)>,
8979 gap_width: &mut Option<(u16, ScalarMarkup)>,
8980 overlap: &mut Option<(i8, ScalarMarkup)>,
8981 series: &mut Vec<Series>,
8982 data_labels: &mut Option<CT_DLbls>,
8983 axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
8984 raw_children: &mut OrderedRawChildren,
8985 boundary: &mut usize,
8986) -> Result<()> {
8987 match name {
8988 b"barDir" => {
8989 let (value, markup) = required_scalar(&raw, "barDir")?;
8990 let value = BarDirection::parse(&value)
8991 .ok_or_else(|| invalid_attribute("barDir", "val", value))?;
8992 set_once(direction, (value, markup), "c:barDir")?;
8993 *boundary = (*boundary).max(1);
8994 }
8995 b"grouping" => {
8996 let (value, markup) = required_scalar(&raw, "grouping")?;
8997 let value = BarGrouping::parse(&value)
8998 .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
8999 set_once(grouping, (value, markup), "c:grouping")?;
9000 *boundary = (*boundary).max(2);
9001 }
9002 b"ser" => {
9003 series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
9004 *boundary = (*boundary).max(series.len() + 2);
9005 }
9006 b"dLbls" => {
9007 set_once(
9008 data_labels,
9009 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
9010 "c:dLbls",
9011 )?;
9012 *boundary = (*boundary).max(series.len() + 3);
9013 }
9014 b"gapWidth" => {
9015 let (value, markup) = required_scalar(&raw, "gapWidth")?;
9016 let parsed = value
9017 .parse::<u16>()
9018 .map_err(|_| invalid_attribute("gapWidth", "val", value.clone()))?;
9019 if parsed > 500 {
9020 return Err(invalid_attribute("gapWidth", "val", value));
9021 }
9022 set_once(gap_width, (parsed, markup), "c:gapWidth")?;
9023 *boundary = (*boundary).max(series.len() + 4);
9024 }
9025 b"overlap" => {
9026 let (value, markup) = required_scalar(&raw, "overlap")?;
9027 let parsed = value
9028 .parse::<i8>()
9029 .map_err(|_| invalid_attribute("overlap", "val", value.clone()))?;
9030 if !(-100..=100).contains(&parsed) {
9031 return Err(invalid_attribute("overlap", "val", value));
9032 }
9033 set_once(overlap, (parsed, markup), "c:overlap")?;
9034 *boundary = (*boundary).max(series.len() + 5);
9035 }
9036 b"axId" => {
9037 if axis_ids.len() == 2 {
9038 return Err(ChartError::DuplicateElement("c:axId".to_owned()));
9039 }
9040 axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
9041 *boundary = (*boundary).max(series.len() + 5 + axis_ids.len());
9042 }
9043 _ => raw_children.push(*boundary, raw),
9044 }
9045 Ok(())
9046}
9047
9048fn parse_line_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
9049 let mut reader = Reader::from_reader(xml);
9050 let mut buffer = Vec::new();
9051 loop {
9052 match reader
9053 .read_event_into(&mut buffer)
9054 .map_err(OxmlError::from)?
9055 {
9056 Event::Start(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
9057 let namespaces = typed_rewrite_bindings(&element, inherited)?;
9058 let (mut raw_attributes, namespace_declarations) =
9059 capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
9060 raw_attributes.splice(0..0, namespace_declarations);
9061 let mut grouping = None;
9062 let mut marker = None;
9063 let mut smooth = None;
9064 let mut series = Vec::new();
9065 let mut data_labels = None;
9066 let mut axis_ids = Vec::new();
9067 let mut raw_children = OrderedRawChildren::default();
9068 let mut boundary = 0usize;
9069 let mut inner = Vec::new();
9070 loop {
9071 match reader
9072 .read_event_into(&mut inner)
9073 .map_err(OxmlError::from)?
9074 {
9075 Event::Start(child) => {
9076 let name = chart_child_local(&child, &namespaces)?;
9077 let raw = capture_element(&mut reader, &child)?;
9078 parse_line_child(
9079 name.as_deref().unwrap_or_default(),
9080 raw,
9081 &namespaces,
9082 &mut grouping,
9083 &mut marker,
9084 &mut smooth,
9085 &mut series,
9086 &mut data_labels,
9087 &mut axis_ids,
9088 &mut raw_children,
9089 &mut boundary,
9090 )?;
9091 }
9092 Event::Empty(child) => {
9093 let name = chart_child_local(&child, &namespaces)?;
9094 let raw = capture_empty_element(&child)?;
9095 parse_line_child(
9096 name.as_deref().unwrap_or_default(),
9097 raw,
9098 &namespaces,
9099 &mut grouping,
9100 &mut marker,
9101 &mut smooth,
9102 &mut series,
9103 &mut data_labels,
9104 &mut axis_ids,
9105 &mut raw_children,
9106 &mut boundary,
9107 )?;
9108 }
9109 event @ (Event::Text(_)
9110 | Event::CData(_)
9111 | Event::Comment(_)
9112 | Event::PI(_)
9113 | Event::GeneralRef(_)) => {
9114 raw_children.push(boundary, capture_event(event)?);
9115 }
9116 Event::End(end)
9117 if matches_local_name(end.name().as_ref(), b"lineChart") =>
9118 {
9119 let (grouping, grouping_markup) = grouping.ok_or_else(|| {
9120 ChartError::MissingElement("c:grouping".to_owned())
9121 })?;
9122 if axis_ids.len() != 2 {
9123 return Err(ChartError::InvalidValue {
9124 element: "c:lineChart/c:axId".to_owned(),
9125 value: format!("expected 2, found {}", axis_ids.len()),
9126 });
9127 }
9128 let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
9129 let original_series_keys: Vec<(u32, u32)> =
9130 series.iter().map(|item| (item.index, item.order)).collect();
9131 let plot = Plot::Line {
9132 grouping,
9133 marker: marker.as_ref().is_some_and(|item| item.0),
9134 smooth: smooth.as_ref().is_some_and(|item| item.0),
9135 series,
9136 data_labels,
9137 axis_ids: parsed_ids,
9138 };
9139 plot.validate()?;
9140 return Ok((
9141 plot,
9142 PlotMarkup {
9143 raw_attributes,
9144 raw_children: raw_children_in_schema_order(
9145 &raw_children,
9146 original_series_keys.len() + 6,
9147 ),
9148 direction: None,
9149 grouping: Some(grouping_markup),
9150 gap_width: None,
9151 overlap: None,
9152 marker: marker.map(|item| item.1),
9153 smooth: smooth.map(|item| item.1),
9154 first_slice_angle: None,
9155 hole_size: None,
9156 style: None,
9157 axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
9158 original_series_keys,
9159 original_axis_ids: parsed_ids.to_vec(),
9160 parsed_bar: Some(false),
9161 parsed_remaining: None,
9162 },
9163 ));
9164 }
9165 Event::Eof => return Err(missing_end("c:lineChart")),
9166 _ => {}
9167 }
9168 inner.clear();
9169 }
9170 }
9171 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
9172 return Err(ChartError::MissingElement("c:grouping".to_owned()));
9173 }
9174 Event::Start(element) | Event::Empty(element) => {
9175 return Err(ChartError::UnexpectedElement(element_name(&element)));
9176 }
9177 Event::Eof => return Err(ChartError::MissingElement("c:lineChart".to_owned())),
9178 _ => {}
9179 }
9180 buffer.clear();
9181 }
9182}
9183
9184#[allow(clippy::too_many_arguments)]
9185fn parse_line_child(
9186 name: &[u8],
9187 raw: Vec<u8>,
9188 namespaces: &NamespaceBindings,
9189 grouping: &mut Option<(Grouping, ScalarMarkup)>,
9190 marker: &mut Option<(bool, ScalarMarkup)>,
9191 smooth: &mut Option<(bool, ScalarMarkup)>,
9192 series: &mut Vec<Series>,
9193 data_labels: &mut Option<CT_DLbls>,
9194 axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
9195 raw_children: &mut OrderedRawChildren,
9196 boundary: &mut usize,
9197) -> Result<()> {
9198 match name {
9199 b"grouping" => {
9200 let (value, markup) = required_scalar(&raw, "grouping")?;
9201 let value = Grouping::parse(&value)
9202 .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
9203 set_once(grouping, (value, markup), "c:grouping")?;
9204 *boundary = (*boundary).max(1);
9205 }
9206 b"ser" => {
9207 series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
9208 *boundary = (*boundary).max(series.len() + 1);
9209 }
9210 b"dLbls" => {
9211 set_once(
9212 data_labels,
9213 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
9214 "c:dLbls",
9215 )?;
9216 *boundary = (*boundary).max(series.len() + 2);
9217 }
9218 b"marker" => {
9219 set_once(marker, parse_bool_value(&raw, "marker")?, "c:marker")?;
9220 *boundary = (*boundary).max(series.len() + 3);
9221 }
9222 b"smooth" => {
9223 set_once(smooth, parse_bool_value(&raw, "smooth")?, "c:smooth")?;
9224 *boundary = (*boundary).max(series.len() + 4);
9225 }
9226 b"axId" => {
9227 if axis_ids.len() == 2 {
9228 return Err(ChartError::DuplicateElement("c:axId".to_owned()));
9229 }
9230 axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
9231 *boundary = (*boundary).max(series.len() + 4 + axis_ids.len());
9232 }
9233 _ => raw_children.push(*boundary, raw),
9234 }
9235 Ok(())
9236}
9237
9238fn required_scalar(xml: &[u8], local: &str) -> Result<(String, ScalarMarkup)> {
9239 let (value, markup) = scalar_value(xml, local)?;
9240 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
9241 Ok((value, markup))
9242}
9243
9244fn write_plot<W: Write>(writer: &mut Writer<W>, plot: &Plot, markup: &PlotMarkup) -> Result<()> {
9245 plot.validate()?;
9246 if let Some(parsed_bar) = markup.parsed_bar
9247 && parsed_bar != matches!(plot, Plot::Bar { .. })
9248 {
9249 return Err(ChartError::InvalidValue {
9250 element: "c:plotArea".to_owned(),
9251 value: "a parsed plot family cannot be replaced while preserved payload remains"
9252 .to_owned(),
9253 });
9254 }
9255 let current_remaining = match plot {
9256 Plot::Pie { .. } => Some("pieChart"),
9257 Plot::Doughnut { .. } => Some("doughnutChart"),
9258 Plot::Area { .. } => Some("areaChart"),
9259 Plot::Scatter { .. } => Some("scatterChart"),
9260 Plot::Radar { .. } => Some("radarChart"),
9261 Plot::Bar { .. } | Plot::Line { .. } => None,
9262 };
9263 if let Some(parsed) = markup.parsed_remaining
9264 && current_remaining != Some(parsed)
9265 {
9266 return Err(ChartError::InvalidValue {
9267 element: "c:plotArea".to_owned(),
9268 value: "a parsed plot family cannot be replaced while preserved payload remains"
9269 .to_owned(),
9270 });
9271 }
9272 match plot {
9273 Plot::Bar {
9274 direction,
9275 grouping,
9276 gap_width,
9277 overlap,
9278 series,
9279 data_labels,
9280 axis_ids,
9281 } => {
9282 let mut start = BytesStart::new("c:barChart");
9283 push_attributes(&mut start, &markup.raw_attributes);
9284 writer
9285 .write_event(Event::Start(start))
9286 .map_err(OxmlError::from)?;
9287 emit_raw(writer, markup.raw_children.at(0))?;
9288 write_scalar(
9289 writer,
9290 "c:barDir",
9291 direction.as_str(),
9292 markup.direction.as_ref(),
9293 )?;
9294 emit_raw(writer, markup.raw_children.at(1))?;
9295 write_scalar(
9296 writer,
9297 "c:grouping",
9298 grouping.as_str(),
9299 markup.grouping.as_ref(),
9300 )?;
9301 let original_to_current =
9302 series_original_to_current(&markup.original_series_keys, series);
9303 emit_repeated_raw(
9304 writer,
9305 &markup.raw_children,
9306 2,
9307 &original_to_current,
9308 series.len(),
9309 0,
9310 )?;
9311 for (index, item) in series.iter().enumerate() {
9312 writer
9313 .get_mut()
9314 .write_all(&item.to_xml()?)
9315 .map_err(OxmlError::from)?;
9316 emit_repeated_raw(
9317 writer,
9318 &markup.raw_children,
9319 2,
9320 &original_to_current,
9321 series.len(),
9322 index + 1,
9323 )?;
9324 }
9325 if let Some(labels) = data_labels {
9326 writer
9327 .get_mut()
9328 .write_all(&labels.to_xml()?)
9329 .map_err(OxmlError::from)?;
9330 }
9331 let trailing = markup.original_series_keys.len();
9332 emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9333 if *gap_width != 150 || markup.gap_width.is_some() {
9334 write_scalar(
9335 writer,
9336 "c:gapWidth",
9337 &gap_width.to_string(),
9338 markup.gap_width.as_ref(),
9339 )?;
9340 }
9341 emit_raw(writer, markup.raw_children.at(trailing + 4))?;
9342 if *overlap != 0 || markup.overlap.is_some() {
9343 write_scalar(
9344 writer,
9345 "c:overlap",
9346 &overlap.to_string(),
9347 markup.overlap.as_ref(),
9348 )?;
9349 }
9350 let axis_original_to_current =
9351 axis_original_to_current(&markup.original_axis_ids, axis_ids);
9352 let axis_current_to_original =
9353 invert_original_to_current(&axis_original_to_current, axis_ids.len());
9354 emit_repeated_raw(
9355 writer,
9356 &markup.raw_children,
9357 trailing + 5,
9358 &axis_original_to_current,
9359 axis_ids.len(),
9360 0,
9361 )?;
9362 for (index, id) in axis_ids.iter().enumerate() {
9363 let default_markup = default_axis_id_markup(*id);
9364 let original_markup = axis_current_to_original[index]
9365 .and_then(|original| markup.axis_ids.get(original));
9366 write_axis_id(
9367 writer,
9368 "c:axId",
9369 *id,
9370 original_markup.unwrap_or(&default_markup),
9371 )?;
9372 emit_repeated_raw(
9373 writer,
9374 &markup.raw_children,
9375 trailing + 5,
9376 &axis_original_to_current,
9377 axis_ids.len(),
9378 index + 1,
9379 )?;
9380 }
9381 writer
9382 .write_event(Event::End(BytesEnd::new("c:barChart")))
9383 .map_err(OxmlError::from)?;
9384 }
9385 Plot::Line {
9386 grouping,
9387 marker,
9388 smooth,
9389 series,
9390 data_labels,
9391 axis_ids,
9392 } => {
9393 let mut start = BytesStart::new("c:lineChart");
9394 push_attributes(&mut start, &markup.raw_attributes);
9395 writer
9396 .write_event(Event::Start(start))
9397 .map_err(OxmlError::from)?;
9398 emit_raw(writer, markup.raw_children.at(0))?;
9399 write_scalar(
9400 writer,
9401 "c:grouping",
9402 grouping.as_str(),
9403 markup.grouping.as_ref(),
9404 )?;
9405 let original_to_current =
9406 series_original_to_current(&markup.original_series_keys, series);
9407 emit_repeated_raw(
9408 writer,
9409 &markup.raw_children,
9410 1,
9411 &original_to_current,
9412 series.len(),
9413 0,
9414 )?;
9415 for (index, item) in series.iter().enumerate() {
9416 writer
9417 .get_mut()
9418 .write_all(&item.to_xml()?)
9419 .map_err(OxmlError::from)?;
9420 emit_repeated_raw(
9421 writer,
9422 &markup.raw_children,
9423 1,
9424 &original_to_current,
9425 series.len(),
9426 index + 1,
9427 )?;
9428 }
9429 if let Some(labels) = data_labels {
9430 writer
9431 .get_mut()
9432 .write_all(&labels.to_xml()?)
9433 .map_err(OxmlError::from)?;
9434 }
9435 let trailing = markup.original_series_keys.len();
9436 emit_raw(writer, markup.raw_children.at(trailing + 2))?;
9437 if *marker || markup.marker.is_some() {
9438 write_scalar(
9439 writer,
9440 "c:marker",
9441 bool_lexical(*marker),
9442 markup.marker.as_ref(),
9443 )?;
9444 }
9445 emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9446 if *smooth || markup.smooth.is_some() {
9447 write_scalar(
9448 writer,
9449 "c:smooth",
9450 bool_lexical(*smooth),
9451 markup.smooth.as_ref(),
9452 )?;
9453 }
9454 let axis_original_to_current =
9455 axis_original_to_current(&markup.original_axis_ids, axis_ids);
9456 let axis_current_to_original =
9457 invert_original_to_current(&axis_original_to_current, axis_ids.len());
9458 emit_repeated_raw(
9459 writer,
9460 &markup.raw_children,
9461 trailing + 4,
9462 &axis_original_to_current,
9463 axis_ids.len(),
9464 0,
9465 )?;
9466 for (index, id) in axis_ids.iter().enumerate() {
9467 let default_markup = default_axis_id_markup(*id);
9468 let original_markup = axis_current_to_original[index]
9469 .and_then(|original| markup.axis_ids.get(original));
9470 write_axis_id(
9471 writer,
9472 "c:axId",
9473 *id,
9474 original_markup.unwrap_or(&default_markup),
9475 )?;
9476 emit_repeated_raw(
9477 writer,
9478 &markup.raw_children,
9479 trailing + 4,
9480 &axis_original_to_current,
9481 axis_ids.len(),
9482 index + 1,
9483 )?;
9484 }
9485 writer
9486 .write_event(Event::End(BytesEnd::new("c:lineChart")))
9487 .map_err(OxmlError::from)?;
9488 }
9489 remaining => {
9490 let (
9491 root,
9492 property,
9493 series,
9494 data_labels,
9495 axis_ids,
9496 first_slice_angle,
9497 hole_size,
9498 scatter,
9499 ) = match remaining {
9500 Plot::Pie {
9501 first_slice_angle,
9502 series,
9503 data_labels,
9504 } => (
9505 "pieChart",
9506 None,
9507 series,
9508 data_labels,
9509 None,
9510 Some(*first_slice_angle),
9511 None,
9512 false,
9513 ),
9514 Plot::Doughnut {
9515 first_slice_angle,
9516 hole_size,
9517 series,
9518 data_labels,
9519 } => (
9520 "doughnutChart",
9521 None,
9522 series,
9523 data_labels,
9524 None,
9525 Some(*first_slice_angle),
9526 Some(*hole_size),
9527 false,
9528 ),
9529 Plot::Area {
9530 grouping,
9531 series,
9532 data_labels,
9533 axis_ids,
9534 } => (
9535 "areaChart",
9536 Some(("c:grouping", grouping.as_str(), markup.grouping.as_ref())),
9537 series,
9538 data_labels,
9539 Some(axis_ids),
9540 None,
9541 None,
9542 false,
9543 ),
9544 Plot::Scatter {
9545 style,
9546 series,
9547 data_labels,
9548 axis_ids,
9549 } => (
9550 "scatterChart",
9551 Some(("c:scatterStyle", style.as_str(), markup.style.as_ref())),
9552 series,
9553 data_labels,
9554 Some(axis_ids),
9555 None,
9556 None,
9557 true,
9558 ),
9559 Plot::Radar {
9560 style,
9561 series,
9562 data_labels,
9563 axis_ids,
9564 } => (
9565 "radarChart",
9566 Some(("c:radarStyle", style.as_str(), markup.style.as_ref())),
9567 series,
9568 data_labels,
9569 Some(axis_ids),
9570 None,
9571 None,
9572 false,
9573 ),
9574 Plot::Bar { .. } | Plot::Line { .. } => unreachable!("handled above"),
9575 };
9576 let tag = format!("c:{root}");
9577 let mut start = BytesStart::new(&tag);
9578 push_attributes(&mut start, &markup.raw_attributes);
9579 writer
9580 .write_event(Event::Start(start))
9581 .map_err(OxmlError::from)?;
9582 let base = usize::from(property.is_some());
9583 if let Some((name, value, scalar_markup)) = property {
9584 emit_raw(writer, markup.raw_children.at(0))?;
9585 write_scalar(writer, name, value, scalar_markup)?;
9586 }
9587 let original_to_current =
9588 series_original_to_current(&markup.original_series_keys, series);
9589 emit_repeated_raw(
9590 writer,
9591 &markup.raw_children,
9592 base,
9593 &original_to_current,
9594 series.len(),
9595 0,
9596 )?;
9597 for (index, item) in series.iter().enumerate() {
9598 writer
9599 .get_mut()
9600 .write_all(&item.to_xml_for_plot(scatter)?)
9601 .map_err(OxmlError::from)?;
9602 emit_repeated_raw(
9603 writer,
9604 &markup.raw_children,
9605 base,
9606 &original_to_current,
9607 series.len(),
9608 index + 1,
9609 )?;
9610 }
9611 if let Some(labels) = data_labels {
9612 writer
9613 .get_mut()
9614 .write_all(&labels.to_xml()?)
9615 .map_err(OxmlError::from)?;
9616 }
9617 let trailing = base + markup.original_series_keys.len();
9618 if let Some(axis_ids) = axis_ids {
9619 let axis_original_to_current =
9620 axis_original_to_current(&markup.original_axis_ids, axis_ids);
9621 let axis_current_to_original =
9622 invert_original_to_current(&axis_original_to_current, axis_ids.len());
9623 emit_repeated_raw(
9624 writer,
9625 &markup.raw_children,
9626 trailing + 1,
9627 &axis_original_to_current,
9628 axis_ids.len(),
9629 0,
9630 )?;
9631 for (index, id) in axis_ids.iter().enumerate() {
9632 let default_markup = default_axis_id_markup(*id);
9633 let original_markup = axis_current_to_original[index]
9634 .and_then(|original| markup.axis_ids.get(original));
9635 write_axis_id(
9636 writer,
9637 "c:axId",
9638 *id,
9639 original_markup.unwrap_or(&default_markup),
9640 )?;
9641 emit_repeated_raw(
9642 writer,
9643 &markup.raw_children,
9644 trailing + 1,
9645 &axis_original_to_current,
9646 axis_ids.len(),
9647 index + 1,
9648 )?;
9649 }
9650 } else {
9651 emit_raw(writer, markup.raw_children.at(trailing + 1))?;
9652 if let Some(angle) = first_slice_angle {
9653 if angle != 0 || markup.first_slice_angle.is_some() {
9654 write_scalar(
9655 writer,
9656 "c:firstSliceAng",
9657 &angle.to_string(),
9658 markup.first_slice_angle.as_ref(),
9659 )?;
9660 }
9661 emit_raw(writer, markup.raw_children.at(trailing + 2))?;
9662 }
9663 if let Some(size) = hole_size {
9664 if size != 50 || markup.hole_size.is_some() {
9665 write_scalar(
9666 writer,
9667 "c:holeSize",
9668 &size.to_string(),
9669 markup.hole_size.as_ref(),
9670 )?;
9671 }
9672 emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9673 }
9674 }
9675 writer
9676 .write_event(Event::End(BytesEnd::new(&tag)))
9677 .map_err(OxmlError::from)?;
9678 }
9679 }
9680 Ok(())
9681}
9682
9683fn series_original_to_current(original: &[(u32, u32)], current: &[Series]) -> Vec<Option<usize>> {
9684 let mut used = vec![false; current.len()];
9685 let mut matches = vec![None; original.len()];
9686 for original_index in 0..original.len().min(current.len()) {
9687 if original[original_index]
9688 == (current[original_index].index, current[original_index].order)
9689 {
9690 matches[original_index] = Some(original_index);
9691 used[original_index] = true;
9692 }
9693 }
9694 for (original_index, key) in original.iter().enumerate() {
9695 if matches[original_index].is_some() {
9696 continue;
9697 }
9698 if let Some(current_index) = current
9699 .iter()
9700 .enumerate()
9701 .position(|(index, item)| !used[index] && (item.index, item.order) == *key)
9702 {
9703 matches[original_index] = Some(current_index);
9704 used[current_index] = true;
9705 }
9706 }
9707 match_unidentified_by_position(&mut matches, &mut used);
9708 matches
9709}
9710
9711fn axis_original_to_current(original: &[AxisId], current: &[AxisId]) -> Vec<Option<usize>> {
9712 let mut used = vec![false; current.len()];
9713 let mut matches = vec![None; original.len()];
9714 for (original_index, id) in original.iter().enumerate() {
9715 if let Some(current_index) = current
9716 .iter()
9717 .enumerate()
9718 .position(|(index, current_id)| !used[index] && current_id == id)
9719 {
9720 matches[original_index] = Some(current_index);
9721 used[current_index] = true;
9722 }
9723 }
9724 match_unidentified_by_position(&mut matches, &mut used);
9725 matches
9726}
9727
9728fn match_unidentified_by_position(matches: &mut [Option<usize>], used: &mut [bool]) {
9729 for original_index in 0..matches.len() {
9730 if matches[original_index].is_none() && original_index < used.len() && !used[original_index]
9731 {
9732 matches[original_index] = Some(original_index);
9733 used[original_index] = true;
9734 }
9735 }
9736 for matched in matches.iter_mut().filter(|matched| matched.is_none()) {
9737 if let Some(current_index) = used.iter().position(|used| !used) {
9738 *matched = Some(current_index);
9739 used[current_index] = true;
9740 }
9741 }
9742}
9743
9744fn invert_original_to_current(
9745 original_to_current: &[Option<usize>],
9746 current_len: usize,
9747) -> Vec<Option<usize>> {
9748 let mut current_to_original = vec![None; current_len];
9749 for (original, current) in original_to_current.iter().enumerate() {
9750 if let Some(current) = current {
9751 current_to_original[*current] = Some(original);
9752 }
9753 }
9754 current_to_original
9755}
9756
9757fn emit_repeated_raw<W: Write>(
9758 writer: &mut Writer<W>,
9759 raw_children: &OrderedRawChildren,
9760 offset: usize,
9761 original_to_current: &[Option<usize>],
9762 current_len: usize,
9763 current_boundary: usize,
9764) -> Result<()> {
9765 if current_boundary == 0 {
9766 emit_raw(writer, raw_children.at(offset))?;
9767 }
9768 for original_boundary in 1..=original_to_current.len() {
9769 let effective = original_to_current
9770 .iter()
9771 .skip(original_boundary)
9772 .flatten()
9773 .copied()
9774 .next()
9775 .unwrap_or(current_len);
9776 if effective == current_boundary {
9777 emit_raw(writer, raw_children.at(offset + original_boundary))?;
9778 }
9779 }
9780 Ok(())
9781}
9782
9783fn parse_plot_axis(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Axis>> {
9784 let mut reader = Reader::from_reader(xml);
9785 let mut buffer = Vec::new();
9786 loop {
9787 match reader
9788 .read_event_into(&mut buffer)
9789 .map_err(OxmlError::from)?
9790 {
9791 Event::Start(element) | Event::Empty(element) => {
9792 if !element_is_in_namespace(&element, C_NS, inherited)? {
9793 return Ok(None);
9794 }
9795 let name = element.name();
9796 if AxisKind::parse(local_name(name.as_ref())).is_none() {
9797 return Ok(None);
9798 }
9799 return Axis::from_xml_with_namespaces(xml, inherited).map(Some);
9800 }
9801 Event::Eof => return Ok(None),
9802 _ => {}
9803 }
9804 buffer.clear();
9805 }
9806}
9807
9808fn validate_axis_pairs(axes: &[Axis]) -> Result<()> {
9809 for (index, axis) in axes.iter().enumerate() {
9810 if axes[..index].iter().any(|other| other.id == axis.id) {
9811 return Err(ChartError::DuplicateElement(format!(
9812 "c:axId {}",
9813 axis.id.value()
9814 )));
9815 }
9816 }
9817 for axis in axes {
9818 if axis.id == axis.cross_axis {
9819 return Err(ChartError::InvalidValue {
9820 element: "c:crossAx".to_owned(),
9821 value: format!("axis {} crosses itself", axis.id.value()),
9822 });
9823 }
9824 let crossed = axes
9825 .iter()
9826 .find(|candidate| candidate.id == axis.cross_axis)
9827 .ok_or_else(|| ChartError::InvalidValue {
9828 element: "c:crossAx".to_owned(),
9829 value: format!(
9830 "axis {} references missing axis {}",
9831 axis.id.value(),
9832 axis.cross_axis.value()
9833 ),
9834 })?;
9835 if crossed.cross_axis != axis.id {
9836 return Err(ChartError::InvalidValue {
9837 element: "c:crossAx".to_owned(),
9838 value: format!(
9839 "axis {} references {}, which references {}",
9840 axis.id.value(),
9841 crossed.id.value(),
9842 crossed.cross_axis.value()
9843 ),
9844 });
9845 }
9846 }
9847 Ok(())
9848}
9849
9850fn parse_plot_series(
9851 xml: &[u8],
9852 inherited: &NamespaceBindings,
9853 series: &mut Vec<Series>,
9854) -> Result<()> {
9855 let mut reader = Reader::from_reader(xml);
9856 let mut buffer = Vec::new();
9857 loop {
9858 match reader
9859 .read_event_into(&mut buffer)
9860 .map_err(OxmlError::from)?
9861 {
9862 Event::Start(element) => {
9863 if !element_is_in_namespace(&element, C_NS, inherited)? {
9864 return Ok(());
9865 }
9866 let local = local_name(element.name().as_ref()).to_vec();
9867 if !is_supported_series_plot(&local) {
9868 return Ok(());
9869 }
9870 let namespaces = chart_bindings(inherited, &element)?;
9871 let mut inner = Vec::new();
9872 loop {
9873 match reader
9874 .read_event_into(&mut inner)
9875 .map_err(OxmlError::from)?
9876 {
9877 Event::Start(child) => {
9878 let name = chart_child_local(&child, &namespaces)?;
9879 let raw = capture_element(&mut reader, &child)?;
9880 if name.as_deref() == Some(b"ser") {
9881 series.push(Series::from_xml_with_namespaces(&raw, &namespaces)?);
9882 }
9883 }
9884 Event::Empty(child) => {
9885 let name = chart_child_local(&child, &namespaces)?;
9886 if name.as_deref() == Some(b"ser") {
9887 return Err(ChartError::MissingElement("c:ser/c:idx".to_owned()));
9888 }
9889 }
9890 Event::End(end) if matches_local_name(end.name().as_ref(), &local) => {
9891 return Ok(());
9892 }
9893 Event::Eof => {
9894 return Err(missing_end(&format!(
9895 "c:{}",
9896 String::from_utf8_lossy(&local)
9897 )));
9898 }
9899 _ => {}
9900 }
9901 inner.clear();
9902 }
9903 }
9904 Event::Empty(_) | Event::Eof => return Ok(()),
9905 _ => {}
9906 }
9907 buffer.clear();
9908 }
9909}
9910
9911fn is_supported_series_plot(local: &[u8]) -> bool {
9912 matches!(
9913 local,
9914 b"areaChart"
9915 | b"area3DChart"
9916 | b"barChart"
9917 | b"bar3DChart"
9918 | b"doughnutChart"
9919 | b"lineChart"
9920 | b"line3DChart"
9921 | b"ofPieChart"
9922 | b"pieChart"
9923 | b"pie3DChart"
9924 | b"radarChart"
9925 | b"stockChart"
9926 | b"surfaceChart"
9927 | b"surface3DChart"
9928 )
9929}
9930
9931impl CT_Legend {
9932 fn from_xml(xml: &[u8]) -> Result<Self> {
9933 let (raw_attributes, raw_children) = parse_raw_shell(xml, b"legend", "c:legend")?;
9934 Ok(Self {
9935 raw_attributes,
9936 raw_children,
9937 })
9938 }
9939
9940 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
9941 write_raw_shell(writer, "c:legend", &self.raw_attributes, &self.raw_children)
9942 }
9943
9944 pub fn raw_children(&self) -> &OrderedRawChildren {
9945 &self.raw_children
9946 }
9947}
9948
9949fn parse_raw_shell(
9950 xml: &[u8],
9951 local: &[u8],
9952 tag: &str,
9953) -> Result<(XmlAttributes, OrderedRawChildren)> {
9954 let mut reader = Reader::from_reader(xml);
9955 let mut buffer = Vec::new();
9956 loop {
9957 match reader
9958 .read_event_into(&mut buffer)
9959 .map_err(OxmlError::from)?
9960 {
9961 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
9962 reject_conflicting_prefix(&element, b"c", C_NS)?;
9963 let raw_attributes = capture_attributes(&element)?;
9964 let mut raw_children = OrderedRawChildren::default();
9965 let mut child_buffer = Vec::new();
9966 loop {
9967 match reader
9968 .read_event_into(&mut child_buffer)
9969 .map_err(OxmlError::from)?
9970 {
9971 Event::Start(child) => {
9972 raw_children.push(0, capture_element(&mut reader, &child)?)
9973 }
9974 Event::Empty(child) => raw_children.push(0, capture_empty_element(&child)?),
9975 event @ (Event::Text(_)
9976 | Event::CData(_)
9977 | Event::Comment(_)
9978 | Event::PI(_)
9979 | Event::GeneralRef(_)) => {
9980 raw_children.push(0, capture_event(event)?);
9981 }
9982 Event::End(end) if matches_local_name(end.name().as_ref(), local) => break,
9983 Event::Eof => return Err(missing_end(tag)),
9984 _ => {}
9985 }
9986 child_buffer.clear();
9987 }
9988 return Ok((raw_attributes, raw_children));
9989 }
9990 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
9991 reject_conflicting_prefix(&element, b"c", C_NS)?;
9992 return Ok((capture_attributes(&element)?, OrderedRawChildren::default()));
9993 }
9994 Event::Start(element) | Event::Empty(element) => {
9995 return Err(ChartError::UnexpectedElement(element_name(&element)));
9996 }
9997 Event::Eof => return Err(ChartError::MissingElement(tag.to_owned())),
9998 _ => {}
9999 }
10000 buffer.clear();
10001 }
10002}
10003
10004fn write_raw_shell<W: Write>(
10005 writer: &mut Writer<W>,
10006 tag: &str,
10007 raw_attributes: &XmlAttributes,
10008 raw_children: &OrderedRawChildren,
10009) -> Result<()> {
10010 let mut start = BytesStart::new(tag);
10011 push_attributes(&mut start, raw_attributes);
10012 if raw_children.is_empty() {
10013 writer
10014 .write_event(Event::Empty(start))
10015 .map_err(OxmlError::from)?;
10016 return Ok(());
10017 }
10018 writer
10019 .write_event(Event::Start(start))
10020 .map_err(OxmlError::from)?;
10021 emit_raw(writer, raw_children.at(0))?;
10022 writer
10023 .write_event(Event::End(BytesEnd::new(tag)))
10024 .map_err(OxmlError::from)?;
10025 Ok(())
10026}
10027
10028#[derive(Clone, Debug, PartialEq)]
10030pub struct CT_Chart {
10031 pub title: Option<CT_Title>,
10032 pub auto_title_deleted: bool,
10033 pub plot_area: CT_PlotArea,
10034 pub legend: Option<CT_Legend>,
10035 pub plot_vis_only: bool,
10036 pub disp_blanks_as: DispBlanksAs,
10037 auto_title_deleted_markup: Option<ScalarMarkup>,
10038 plot_vis_only_markup: Option<ScalarMarkup>,
10039 disp_blanks_as_markup: Option<ScalarMarkup>,
10040 raw_attributes: Vec<(String, String)>,
10041 raw_children: OrderedRawChildren,
10042}
10043
10044impl CT_Chart {
10045 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
10046 let mut reader = Reader::from_reader(xml);
10047 let mut buffer = Vec::new();
10048 loop {
10049 match reader
10050 .read_event_into(&mut buffer)
10051 .map_err(OxmlError::from)?
10052 {
10053 Event::Start(element) if matches_local_name(element.name().as_ref(), b"chart") => {
10054 reject_conflicting_prefix(&element, b"c", C_NS)?;
10055 return Self::from_element(&mut reader, &element, inherited);
10056 }
10057 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"chart") => {
10058 return Err(ChartError::MissingElement("c:plotArea".to_owned()));
10059 }
10060 Event::Start(element) | Event::Empty(element) => {
10061 return Err(ChartError::UnexpectedElement(element_name(&element)));
10062 }
10063 Event::Eof => return Err(ChartError::MissingElement("c:chart".to_owned())),
10064 _ => {}
10065 }
10066 buffer.clear();
10067 }
10068 }
10069
10070 fn from_element(
10071 reader: &mut Reader<&[u8]>,
10072 start: &BytesStart<'_>,
10073 inherited: &NamespaceBindings,
10074 ) -> Result<Self> {
10075 let namespaces = bindings_with_local(inherited, start)?;
10076 let raw_attributes = capture_attributes(start)?;
10077 let mut state = ChartParseState::default();
10078 let mut buffer = Vec::new();
10079
10080 loop {
10081 match reader
10082 .read_event_into(&mut buffer)
10083 .map_err(OxmlError::from)?
10084 {
10085 Event::Start(element) => {
10086 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10087 local_name(element.name().as_ref()).to_vec()
10088 } else {
10089 Vec::new()
10090 };
10091 let raw = capture_element(reader, &element)?;
10092 state.parse_child(&name, raw, &namespaces)?;
10093 }
10094 Event::Empty(element) => {
10095 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10096 local_name(element.name().as_ref()).to_vec()
10097 } else {
10098 Vec::new()
10099 };
10100 let raw = capture_empty_element(&element)?;
10101 state.parse_child(&name, raw, &namespaces)?;
10102 }
10103 event @ (Event::Text(_)
10104 | Event::CData(_)
10105 | Event::Comment(_)
10106 | Event::PI(_)
10107 | Event::GeneralRef(_)) => {
10108 state.capture_event(capture_event(event)?);
10109 }
10110 Event::End(element) if matches_local_name(element.name().as_ref(), b"chart") => {
10111 break;
10112 }
10113 Event::Eof => return Err(missing_end("c:chart")),
10114 _ => {}
10115 }
10116 buffer.clear();
10117 }
10118
10119 state.finish(raw_attributes)
10120 }
10121
10122 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
10123 let mut start = BytesStart::new("c:chart");
10124 push_attributes(&mut start, &self.raw_attributes);
10125 writer
10126 .write_event(Event::Start(start))
10127 .map_err(OxmlError::from)?;
10128 emit_raw(writer, self.raw_children.at(0))?;
10129 if let Some(title) = &self.title {
10130 title.write_xml(writer)?;
10131 }
10132 emit_raw(writer, self.raw_children.at(1))?;
10133 if self.auto_title_deleted_markup.is_some() || self.auto_title_deleted {
10134 write_scalar(
10135 writer,
10136 "c:autoTitleDeleted",
10137 if self.auto_title_deleted { "1" } else { "0" },
10138 self.auto_title_deleted_markup.as_ref(),
10139 )?;
10140 }
10141 for raw_boundary in 2..8 {
10142 emit_raw(writer, self.raw_children.at(raw_boundary))?;
10143 }
10144 self.plot_area.write_xml(writer)?;
10145 emit_raw(writer, self.raw_children.at(8))?;
10146 if let Some(legend) = &self.legend {
10147 legend.write_xml(writer)?;
10148 }
10149 emit_raw(writer, self.raw_children.at(9))?;
10150 if self.plot_vis_only_markup.is_some() || !self.plot_vis_only {
10151 write_scalar(
10152 writer,
10153 "c:plotVisOnly",
10154 if self.plot_vis_only { "1" } else { "0" },
10155 self.plot_vis_only_markup.as_ref(),
10156 )?;
10157 }
10158 emit_raw(writer, self.raw_children.at(10))?;
10159 if self.disp_blanks_as_markup.is_some() || self.disp_blanks_as != DispBlanksAs::Gap {
10160 write_scalar(
10161 writer,
10162 "c:dispBlanksAs",
10163 self.disp_blanks_as.as_str(),
10164 self.disp_blanks_as_markup.as_ref(),
10165 )?;
10166 }
10167 for raw_boundary in 11..=13 {
10168 emit_raw(writer, self.raw_children.at(raw_boundary))?;
10169 }
10170 writer
10171 .write_event(Event::End(BytesEnd::new("c:chart")))
10172 .map_err(OxmlError::from)?;
10173 Ok(())
10174 }
10175
10176 pub fn raw_children(&self) -> &OrderedRawChildren {
10177 &self.raw_children
10178 }
10179}
10180
10181#[derive(Default)]
10182struct ChartParseState {
10183 title: Option<CT_Title>,
10184 auto_title_deleted: Option<(bool, ScalarMarkup)>,
10185 plot_area: Option<CT_PlotArea>,
10186 legend: Option<CT_Legend>,
10187 plot_vis_only: Option<(bool, ScalarMarkup)>,
10188 disp_blanks_as: Option<(DispBlanksAs, ScalarMarkup)>,
10189 raw_children: OrderedRawChildren,
10190 boundary: usize,
10191}
10192
10193impl ChartParseState {
10194 fn capture_event(&mut self, raw: Vec<u8>) {
10195 self.raw_children.push(self.boundary, raw);
10196 }
10197
10198 fn parse_child(
10199 &mut self,
10200 name: &[u8],
10201 raw: Vec<u8>,
10202 namespaces: &NamespaceBindings,
10203 ) -> Result<()> {
10204 match name {
10205 b"title" => {
10206 set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
10207 self.boundary = self.boundary.max(1);
10208 }
10209 b"autoTitleDeleted" => {
10210 set_once(
10211 &mut self.auto_title_deleted,
10212 parse_bool_value(&raw, "autoTitleDeleted")?,
10213 "c:autoTitleDeleted",
10214 )?;
10215 self.boundary = self.boundary.max(2);
10216 }
10217 b"plotArea" => {
10218 set_once(
10219 &mut self.plot_area,
10220 CT_PlotArea::from_xml_with_namespaces(&raw, namespaces)?,
10221 "c:plotArea",
10222 )?;
10223 self.boundary = self.boundary.max(8);
10224 }
10225 b"legend" => {
10226 set_once(&mut self.legend, CT_Legend::from_xml(&raw)?, "c:legend")?;
10227 self.boundary = self.boundary.max(9);
10228 }
10229 b"plotVisOnly" => {
10230 set_once(
10231 &mut self.plot_vis_only,
10232 parse_bool_value(&raw, "plotVisOnly")?,
10233 "c:plotVisOnly",
10234 )?;
10235 self.boundary = self.boundary.max(10);
10236 }
10237 b"dispBlanksAs" => {
10238 set_once(
10239 &mut self.disp_blanks_as,
10240 parse_enum_value(&raw, "dispBlanksAs")?,
10241 "c:dispBlanksAs",
10242 )?;
10243 self.boundary = self.boundary.max(11);
10244 }
10245 _ => {
10246 self.raw_children
10247 .push(chart_raw_slot(name).unwrap_or(self.boundary), raw);
10248 self.boundary = self.boundary.max(chart_raw_boundary_after(name));
10249 }
10250 }
10251 Ok(())
10252 }
10253
10254 fn finish(self, raw_attributes: XmlAttributes) -> Result<CT_Chart> {
10255 let (auto_title_deleted, auto_title_deleted_markup) = self
10256 .auto_title_deleted
10257 .map(|(value, markup)| (value, Some(markup)))
10258 .unwrap_or((false, None));
10259 let (plot_vis_only, plot_vis_only_markup) = self
10260 .plot_vis_only
10261 .map(|(value, markup)| (value, Some(markup)))
10262 .unwrap_or((true, None));
10263 let (disp_blanks_as, disp_blanks_as_markup) = self
10264 .disp_blanks_as
10265 .map(|(value, markup)| (value, Some(markup)))
10266 .unwrap_or((DispBlanksAs::Gap, None));
10267 Ok(CT_Chart {
10268 title: self.title,
10269 auto_title_deleted,
10270 plot_area: self
10271 .plot_area
10272 .ok_or_else(|| ChartError::MissingElement("c:plotArea".to_owned()))?,
10273 legend: self.legend,
10274 plot_vis_only,
10275 disp_blanks_as,
10276 auto_title_deleted_markup,
10277 plot_vis_only_markup,
10278 disp_blanks_as_markup,
10279 raw_attributes,
10280 raw_children: raw_children_in_schema_order(&self.raw_children, 13),
10281 })
10282 }
10283}
10284
10285fn chart_raw_slot(name: &[u8]) -> Option<usize> {
10286 match name {
10287 b"pivotFmts" => Some(2),
10288 b"view3D" => Some(3),
10289 b"floor" => Some(4),
10290 b"sideWall" => Some(5),
10291 b"backWall" => Some(6),
10292 b"showDLblsOverMax" | b"extLst" => Some(11),
10295 _ => None,
10296 }
10297}
10298
10299fn chart_raw_boundary_after(name: &[u8]) -> usize {
10300 match name {
10301 b"title" => 1,
10302 b"autoTitleDeleted" => 2,
10303 b"pivotFmts" => 3,
10304 b"view3D" => 4,
10305 b"floor" => 5,
10306 b"sideWall" => 6,
10307 b"backWall" => 7,
10308 b"plotArea" => 8,
10309 b"legend" => 9,
10310 b"plotVisOnly" => 10,
10311 b"dispBlanksAs" => 11,
10312 b"showDLblsOverMax" | b"extLst" => 11,
10313 _ => 0,
10314 }
10315}
10316
10317#[derive(Clone, Debug, PartialEq)]
10319pub struct CT_ChartSpace {
10320 pub chart: CT_Chart,
10321 pub sp_pr: Option<CT_ShapeProperties>,
10322 pub tx_pr: Option<CT_TextBody>,
10323 raw_attributes: Vec<(String, String)>,
10324 namespace_declarations: Vec<(String, String)>,
10325 raw_children: OrderedRawChildren,
10326}
10327
10328impl CT_ChartSpace {
10329 pub fn from_xml(xml: &[u8]) -> Result<Self> {
10331 let mut reader = Reader::from_reader(xml);
10332 let mut buffer = Vec::new();
10333 loop {
10334 match reader
10335 .read_event_into(&mut buffer)
10336 .map_err(OxmlError::from)?
10337 {
10338 Event::Start(element)
10339 if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10340 {
10341 validate_chart_space_namespace(&element)?;
10342 return Self::from_element(&mut reader, &element);
10343 }
10344 Event::Empty(element)
10345 if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10346 {
10347 validate_chart_space_namespace(&element)?;
10348 return Err(ChartError::MissingElement("c:chart".to_owned()));
10349 }
10350 Event::Start(element) | Event::Empty(element) => {
10351 return Err(ChartError::UnexpectedElement(element_name(&element)));
10352 }
10353 Event::Eof => return Err(ChartError::MissingElement("c:chartSpace".to_owned())),
10354 _ => {}
10355 }
10356 buffer.clear();
10357 }
10358 }
10359
10360 fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
10361 let namespaces = namespace_bindings(start)?;
10362 let (raw_attributes, namespace_declarations) = capture_root_attributes(start)?;
10363 let mut state = ChartSpaceParseState::default();
10364 let mut buffer = Vec::new();
10365
10366 loop {
10367 match reader
10368 .read_event_into(&mut buffer)
10369 .map_err(OxmlError::from)?
10370 {
10371 Event::Start(element) => {
10372 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10373 local_name(element.name().as_ref()).to_vec()
10374 } else {
10375 Vec::new()
10376 };
10377 let raw = capture_element(reader, &element)?;
10378 state.parse_child(&name, raw, &namespaces)?;
10379 }
10380 Event::Empty(element) => {
10381 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10382 local_name(element.name().as_ref()).to_vec()
10383 } else {
10384 Vec::new()
10385 };
10386 let raw = capture_empty_element(&element)?;
10387 state.parse_child(&name, raw, &namespaces)?;
10388 }
10389 event @ (Event::Text(_)
10390 | Event::CData(_)
10391 | Event::Comment(_)
10392 | Event::PI(_)
10393 | Event::GeneralRef(_)) => {
10394 state.capture_event(capture_event(event)?);
10395 }
10396 Event::End(element)
10397 if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10398 {
10399 break;
10400 }
10401 Event::Eof => return Err(missing_end("c:chartSpace")),
10402 _ => {}
10403 }
10404 buffer.clear();
10405 }
10406
10407 state.finish(raw_attributes, namespace_declarations)
10408 }
10409
10410 pub fn to_xml(&self) -> Result<Vec<u8>> {
10412 let mut writer = Writer::new(Vec::new());
10413 let mut start = BytesStart::new("c:chartSpace");
10414 start.push_attribute(("xmlns:c", C_NS));
10415 start.push_attribute(("xmlns:a", A_NS));
10416 start.push_attribute(("xmlns:r", R_NS));
10417 push_attributes(&mut start, &self.namespace_declarations);
10418 push_attributes(&mut start, &self.raw_attributes);
10419 writer
10420 .write_event(Event::Start(start))
10421 .map_err(OxmlError::from)?;
10422 for raw_boundary in 0..8 {
10423 emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
10424 }
10425 self.chart.write_xml(&mut writer)?;
10426 emit_raw(&mut writer, self.raw_children.at(8))?;
10427 if let Some(properties) = &self.sp_pr {
10428 properties.write_xml_as(&mut writer, "c:spPr")?;
10429 }
10430 emit_raw(&mut writer, self.raw_children.at(9))?;
10431 if let Some(text) = &self.tx_pr {
10432 text.write_xml_as(&mut writer, "c:txPr")?;
10433 }
10434 for raw_boundary in 10..=14 {
10435 emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
10436 }
10437 writer
10438 .write_event(Event::End(BytesEnd::new("c:chartSpace")))
10439 .map_err(OxmlError::from)?;
10440 Ok(writer.into_inner())
10441 }
10442
10443 pub fn raw_children(&self) -> &OrderedRawChildren {
10444 &self.raw_children
10445 }
10446}
10447
10448#[derive(Default)]
10449struct ChartSpaceParseState {
10450 chart: Option<CT_Chart>,
10451 sp_pr: Option<CT_ShapeProperties>,
10452 tx_pr: Option<CT_TextBody>,
10453 raw_children: OrderedRawChildren,
10454 boundary: usize,
10455}
10456
10457impl ChartSpaceParseState {
10458 fn capture_event(&mut self, raw: Vec<u8>) {
10459 self.raw_children.push(self.boundary, raw);
10460 }
10461
10462 fn parse_child(
10463 &mut self,
10464 name: &[u8],
10465 raw: Vec<u8>,
10466 namespaces: &NamespaceBindings,
10467 ) -> Result<()> {
10468 match name {
10469 b"chart" => {
10470 set_once(
10471 &mut self.chart,
10472 CT_Chart::from_xml_with_namespaces(&raw, namespaces)?,
10473 "c:chart",
10474 )?;
10475 self.boundary = self.boundary.max(8);
10476 }
10477 b"spPr" => {
10478 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
10479 let properties = CT_ShapeProperties::from_xml(&raw)?;
10480 let mut writer = Writer::new(Vec::new());
10481 properties.write_xml_as(&mut writer, "c:spPr")?;
10482 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
10483 set_once(&mut self.sp_pr, properties, "c:spPr")?;
10484 self.boundary = self.boundary.max(9);
10485 }
10486 b"txPr" => {
10487 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
10488 let text = CT_TextBody::from_xml_as(&raw, b"txPr")?;
10489 let mut writer = Writer::new(Vec::new());
10490 text.write_xml_as(&mut writer, "c:txPr")?;
10491 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
10492 set_once(&mut self.tx_pr, text, "c:txPr")?;
10493 self.boundary = self.boundary.max(10);
10494 }
10495 _ => {
10496 self.raw_children
10497 .push(chart_space_raw_slot(name).unwrap_or(self.boundary), raw);
10498 self.boundary = self.boundary.max(chart_space_raw_boundary_after(name));
10499 }
10500 }
10501 Ok(())
10502 }
10503
10504 fn finish(
10505 self,
10506 raw_attributes: XmlAttributes,
10507 namespace_declarations: XmlAttributes,
10508 ) -> Result<CT_ChartSpace> {
10509 Ok(CT_ChartSpace {
10510 chart: self
10511 .chart
10512 .ok_or_else(|| ChartError::MissingElement("c:chart".to_owned()))?,
10513 sp_pr: self.sp_pr,
10514 tx_pr: self.tx_pr,
10515 raw_attributes,
10516 namespace_declarations,
10517 raw_children: raw_children_in_schema_order(&self.raw_children, 14),
10518 })
10519 }
10520}
10521
10522fn raw_children_in_schema_order(
10523 raw_children: &OrderedRawChildren,
10524 final_boundary: usize,
10525) -> OrderedRawChildren {
10526 let mut ordered = OrderedRawChildren::default();
10527 for boundary in 0..=final_boundary {
10528 for raw in raw_children.at(boundary) {
10529 ordered.push(boundary, raw.to_vec());
10530 }
10531 }
10532 ordered
10533}
10534
10535fn chart_space_raw_slot(name: &[u8]) -> Option<usize> {
10536 match name {
10537 b"date1904" => Some(0),
10538 b"lang" => Some(1),
10539 b"roundedCorners" => Some(2),
10540 b"style" => Some(3),
10541 b"clrMapOvr" => Some(4),
10542 b"pivotSource" => Some(5),
10543 b"protection" => Some(6),
10544 b"externalData" => Some(10),
10545 b"printSettings" => Some(11),
10546 b"userShapes" => Some(12),
10547 b"extLst" => Some(13),
10548 _ => None,
10549 }
10550}
10551
10552fn chart_space_raw_boundary_after(name: &[u8]) -> usize {
10553 match name {
10554 b"date1904" => 1,
10555 b"lang" => 2,
10556 b"roundedCorners" => 3,
10557 b"style" => 4,
10558 b"clrMapOvr" => 5,
10559 b"pivotSource" => 6,
10560 b"protection" => 7,
10561 b"chart" => 8,
10562 b"spPr" => 9,
10563 b"txPr" => 10,
10564 b"externalData" => 11,
10565 b"printSettings" => 12,
10566 b"userShapes" => 13,
10567 b"extLst" => 14,
10568 _ => 0,
10569 }
10570}
10571
10572fn parse_bool_value(xml: &[u8], local: &str) -> Result<(bool, ScalarMarkup)> {
10573 let (value, markup) = scalar_value(xml, local)?;
10574 let value = match value.as_deref().unwrap_or("1") {
10575 "1" | "true" => true,
10576 "0" | "false" => false,
10577 invalid => return Err(invalid_attribute(local, "val", invalid.to_owned())),
10578 };
10579 Ok((value, markup))
10580}
10581
10582fn parse_enum_value(xml: &[u8], local: &str) -> Result<(DispBlanksAs, ScalarMarkup)> {
10583 let (value, markup) = scalar_value(xml, local)?;
10584 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
10585 let parsed =
10586 DispBlanksAs::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
10587 Ok((parsed, markup))
10588}
10589
10590fn scalar_value(xml: &[u8], expected: &str) -> Result<(Option<String>, ScalarMarkup)> {
10591 let mut reader = Reader::from_reader(xml);
10592 let mut buffer = Vec::new();
10593 loop {
10594 match reader
10595 .read_event_into(&mut buffer)
10596 .map_err(OxmlError::from)?
10597 {
10598 Event::Empty(element)
10599 if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
10600 {
10601 reject_conflicting_prefix(&element, b"c", C_NS)?;
10602 return Ok((
10603 attribute_value(&element, b"val")?,
10604 ScalarMarkup {
10605 raw_attributes: capture_attributes_except(&element, b"val")?,
10606 raw_content: Vec::new(),
10607 },
10608 ));
10609 }
10610 Event::Start(element)
10611 if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
10612 {
10613 reject_conflicting_prefix(&element, b"c", C_NS)?;
10614 let value = attribute_value(&element, b"val")?;
10615 let raw_attributes = capture_attributes_except(&element, b"val")?;
10616 let mut raw_content = Vec::new();
10617 let mut inner = Vec::new();
10618 loop {
10619 match reader
10620 .read_event_into(&mut inner)
10621 .map_err(OxmlError::from)?
10622 {
10623 Event::End(end)
10624 if matches_local_name(end.name().as_ref(), expected.as_bytes()) =>
10625 {
10626 return Ok((
10627 value,
10628 ScalarMarkup {
10629 raw_attributes,
10630 raw_content,
10631 },
10632 ));
10633 }
10634 event @ (Event::Text(_)
10635 | Event::CData(_)
10636 | Event::Comment(_)
10637 | Event::PI(_)
10638 | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
10639 Event::Eof => return Err(missing_end(expected)),
10640 _ => return Err(ChartError::UnexpectedElement(expected.to_owned())),
10641 }
10642 inner.clear();
10643 }
10644 }
10645 Event::Start(element) | Event::Empty(element) => {
10646 return Err(ChartError::UnexpectedElement(element_name(&element)));
10647 }
10648 Event::Eof => return Err(ChartError::MissingElement(expected.to_owned())),
10649 _ => {}
10650 }
10651 buffer.clear();
10652 }
10653}
10654
10655fn set_once<T>(slot: &mut Option<T>, value: T, name: &str) -> Result<()> {
10656 if slot.is_some() {
10657 return Err(ChartError::DuplicateElement(name.to_owned()));
10658 }
10659 *slot = Some(value);
10660 Ok(())
10661}
10662
10663fn mark_once(seen: &mut bool, name: &str) -> Result<()> {
10664 if *seen {
10665 return Err(ChartError::DuplicateElement(name.to_owned()));
10666 }
10667 *seen = true;
10668 Ok(())
10669}
10670
10671fn validate_chart_space_namespace(element: &BytesStart<'_>) -> Result<()> {
10672 let name = element.name();
10673 let qualified = name.as_ref();
10674 let declaration = match qualified.iter().position(|byte| *byte == b':') {
10675 Some(position) => {
10676 let mut declaration = b"xmlns:".to_vec();
10677 declaration.extend_from_slice(&qualified[..position]);
10678 declaration
10679 }
10680 None => b"xmlns".to_vec(),
10681 };
10682 let namespace = attribute_value(element, &declaration)?
10683 .ok_or_else(|| invalid_attribute("chartSpace", "namespace", "<missing>".to_owned()))?;
10684 if namespace != C_NS {
10685 return Err(invalid_attribute("chartSpace", "namespace", namespace));
10686 }
10687 reject_conflicting_prefix(element, b"c", C_NS)?;
10688 reject_conflicting_prefix(element, b"a", A_NS)?;
10689 reject_conflicting_prefix(element, b"r", R_NS)
10690}
10691
10692fn reject_conflicting_prefix_in_xml(xml: &[u8], prefix: &[u8], namespace: &str) -> Result<()> {
10693 let mut reader = Reader::from_reader(xml);
10694 let mut buffer = Vec::new();
10695 loop {
10696 match reader
10697 .read_event_into(&mut buffer)
10698 .map_err(OxmlError::from)?
10699 {
10700 Event::Start(element) | Event::Empty(element) => {
10701 return reject_conflicting_prefix(&element, prefix, namespace);
10702 }
10703 Event::Eof => return Err(ChartError::MissingElement("XML root".to_owned())),
10704 _ => {}
10705 }
10706 buffer.clear();
10707 }
10708}
10709
10710fn reject_rewritten_foreign_elements(
10711 original: &[u8],
10712 written: &[u8],
10713 inherited: &NamespaceBindings,
10714 root_local: &[u8],
10715) -> Result<()> {
10716 let original = foreign_element_regions(original, inherited, root_local)?;
10717 let mut written_namespaces = inherited.clone();
10718 written_namespaces.retain(|(prefix, _)| prefix != b"a");
10719 written_namespaces.push((b"a".to_vec(), A_NS.to_owned()));
10720 let written = foreign_element_regions(written, &written_namespaces, root_local)?;
10721 if original != written {
10722 return Err(invalid_attribute(
10723 &String::from_utf8_lossy(root_local),
10724 "namespace",
10725 "foreign DrawingML content would be converted to typed content".to_owned(),
10726 ));
10727 }
10728 Ok(())
10729}
10730
10731fn foreign_element_regions(
10732 xml: &[u8],
10733 inherited: &NamespaceBindings,
10734 root_local: &[u8],
10735) -> Result<Vec<Vec<u8>>> {
10736 let mut reader = Reader::from_reader(xml);
10737 let mut buffer = Vec::new();
10738 let root_namespaces = loop {
10739 match reader
10740 .read_event_into(&mut buffer)
10741 .map_err(OxmlError::from)?
10742 {
10743 Event::Start(element) if matches_local_name(element.name().as_ref(), root_local) => {
10744 break bindings_with_local(inherited, &element)?;
10745 }
10746 Event::Empty(element) if matches_local_name(element.name().as_ref(), root_local) => {
10747 return Ok(Vec::new());
10748 }
10749 Event::Start(element) | Event::Empty(element) => {
10750 return Err(ChartError::UnexpectedElement(element_name(&element)));
10751 }
10752 Event::Eof => {
10753 return Err(ChartError::MissingElement(
10754 String::from_utf8_lossy(root_local).into_owned(),
10755 ));
10756 }
10757 _ => {}
10758 }
10759 buffer.clear();
10760 };
10761
10762 let mut namespace_stack = vec![root_namespaces];
10763 let mut foreign = Vec::new();
10764 let mut child_buffer = Vec::new();
10765 loop {
10766 match reader
10767 .read_event_into(&mut child_buffer)
10768 .map_err(OxmlError::from)?
10769 {
10770 Event::Start(element) => {
10771 let namespaces = namespace_stack.last().expect("root namespace binding");
10772 if element_is_in_namespace(&element, A_NS, namespaces)? {
10773 namespace_stack.push(bindings_with_local(namespaces, &element)?);
10774 } else {
10775 foreign.push(capture_element(&mut reader, &element)?);
10776 }
10777 }
10778 Event::Empty(element) => {
10779 let namespaces = namespace_stack.last().expect("root namespace binding");
10780 if !element_is_in_namespace(&element, A_NS, namespaces)? {
10781 foreign.push(capture_empty_element(&element)?);
10782 }
10783 }
10784 Event::End(element) if matches_local_name(element.name().as_ref(), root_local) => {
10785 return Ok(foreign);
10786 }
10787 Event::End(_) => {
10788 namespace_stack.pop();
10789 }
10790 Event::Eof => {
10791 return Err(missing_end(&String::from_utf8_lossy(root_local)));
10792 }
10793 _ => {}
10794 }
10795 child_buffer.clear();
10796 }
10797}
10798
10799fn reject_conflicting_prefix(
10800 element: &BytesStart<'_>,
10801 prefix: &[u8],
10802 namespace: &str,
10803) -> Result<()> {
10804 let mut key = b"xmlns:".to_vec();
10805 key.extend_from_slice(prefix);
10806 if let Some(value) = attribute_value(element, &key)?
10807 && value != namespace
10808 {
10809 return Err(invalid_attribute(
10810 &element_name(element),
10811 &String::from_utf8_lossy(&key),
10812 value,
10813 ));
10814 }
10815 Ok(())
10816}
10817
10818fn namespace_bindings(element: &BytesStart<'_>) -> Result<NamespaceBindings> {
10819 let mut bindings = Vec::new();
10820 for attribute in element.attributes() {
10821 let attribute = attribute.map_err(OxmlError::from)?;
10822 let key = attribute.key.as_ref();
10823 let prefix = if key == b"xmlns" {
10824 Vec::new()
10825 } else if let Some(prefix) = key.strip_prefix(b"xmlns:") {
10826 prefix.to_vec()
10827 } else {
10828 continue;
10829 };
10830 let namespace = attribute
10831 .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
10832 .map_err(OxmlError::from)?
10833 .into_owned();
10834 bindings.push((prefix, namespace));
10835 }
10836 Ok(bindings)
10837}
10838
10839fn bindings_with_local(
10840 inherited: &NamespaceBindings,
10841 element: &BytesStart<'_>,
10842) -> Result<NamespaceBindings> {
10843 let mut bindings = inherited.clone();
10844 for (prefix, namespace) in namespace_bindings(element)? {
10845 upsert_namespace_binding(&mut bindings, prefix, namespace);
10846 }
10847 Ok(bindings)
10848}
10849
10850fn upsert_namespace_binding(bindings: &mut NamespaceBindings, prefix: Vec<u8>, namespace: String) {
10851 if let Some((_, current)) = bindings.iter_mut().find(|(current, _)| current == &prefix) {
10852 *current = namespace;
10853 } else {
10854 bindings.push((prefix, namespace));
10855 }
10856}
10857
10858fn element_is_in_namespace(
10859 element: &BytesStart<'_>,
10860 expected: &str,
10861 inherited: &NamespaceBindings,
10862) -> Result<bool> {
10863 let name = element.name();
10864 let qualified = name.as_ref();
10865 let prefix = qualified
10866 .iter()
10867 .position(|byte| *byte == b':')
10868 .map(|position| &qualified[..position])
10869 .unwrap_or_default();
10870 let local = namespace_bindings(element)?;
10871 let namespace = local
10872 .iter()
10873 .rev()
10874 .chain(inherited.iter().rev())
10875 .find(|(candidate, _)| candidate.as_slice() == prefix)
10876 .map(|(_, namespace)| namespace.as_str());
10877 Ok(namespace == Some(expected))
10878}
10879
10880fn attribute_value(element: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
10881 for attribute in element.attributes() {
10882 let attribute = attribute.map_err(OxmlError::from)?;
10883 if attribute.key.as_ref() == name {
10884 return Ok(Some(
10885 attribute
10886 .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
10887 .map_err(OxmlError::from)?
10888 .into_owned(),
10889 ));
10890 }
10891 }
10892 Ok(None)
10893}
10894
10895fn capture_root_attributes(start: &BytesStart<'_>) -> Result<RootAttributes> {
10896 let mut attributes = Vec::new();
10897 let mut namespaces = Vec::new();
10898 for attribute in start.attributes() {
10899 let attribute = attribute.map_err(OxmlError::from)?;
10900 let name = std::str::from_utf8(attribute.key.as_ref())
10901 .map_err(OxmlError::from)?
10902 .to_owned();
10903 let value = attribute
10904 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10905 .map_err(OxmlError::from)?
10906 .into_owned();
10907 if name == "xmlns:c" || name == "xmlns:a" || name == "xmlns:r" {
10908 continue;
10909 }
10910 if name == "xmlns" || name.starts_with("xmlns:") {
10911 namespaces.push((name, value));
10912 } else {
10913 attributes.push((name, value));
10914 }
10915 }
10916 Ok((attributes, namespaces))
10917}
10918
10919fn capture_attributes(start: &BytesStart<'_>) -> Result<XmlAttributes> {
10920 let mut attributes = Vec::new();
10921 for attribute in start.attributes() {
10922 let attribute = attribute.map_err(OxmlError::from)?;
10923 attributes.push((
10924 std::str::from_utf8(attribute.key.as_ref())
10925 .map_err(OxmlError::from)?
10926 .to_owned(),
10927 attribute
10928 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10929 .map_err(OxmlError::from)?
10930 .into_owned(),
10931 ));
10932 }
10933 Ok(attributes)
10934}
10935
10936fn capture_attributes_except(start: &BytesStart<'_>, excluded: &[u8]) -> Result<XmlAttributes> {
10937 let mut attributes = Vec::new();
10938 for attribute in start.attributes() {
10939 let attribute = attribute.map_err(OxmlError::from)?;
10940 if attribute.key.as_ref() == excluded {
10941 continue;
10942 }
10943 attributes.push((
10944 std::str::from_utf8(attribute.key.as_ref())
10945 .map_err(OxmlError::from)?
10946 .to_owned(),
10947 attribute
10948 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10949 .map_err(OxmlError::from)?
10950 .into_owned(),
10951 ));
10952 }
10953 Ok(attributes)
10954}
10955
10956fn capture_attributes_excluding(
10957 start: &BytesStart<'_>,
10958 excluded: &[&[u8]],
10959) -> Result<XmlAttributes> {
10960 let mut attributes = Vec::new();
10961 for attribute in start.attributes() {
10962 let attribute = attribute.map_err(OxmlError::from)?;
10963 if excluded.contains(&attribute.key.as_ref()) {
10964 continue;
10965 }
10966 attributes.push((
10967 std::str::from_utf8(attribute.key.as_ref())
10968 .map_err(OxmlError::from)?
10969 .to_owned(),
10970 attribute
10971 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10972 .map_err(OxmlError::from)?
10973 .into_owned(),
10974 ));
10975 }
10976 Ok(attributes)
10977}
10978
10979fn push_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
10980 for (name, value) in attributes {
10981 start.push_attribute((name.as_str(), value.as_str()));
10982 }
10983}
10984
10985fn write_scalar<W: Write>(
10986 writer: &mut Writer<W>,
10987 name: &str,
10988 value: &str,
10989 markup: Option<&ScalarMarkup>,
10990) -> Result<()> {
10991 let mut start = BytesStart::new(name);
10992 start.push_attribute(("val", value));
10993 if let Some(markup) = markup {
10994 push_attributes(&mut start, &markup.raw_attributes);
10995 }
10996 let has_content = markup.is_some_and(|markup| !markup.raw_content.is_empty());
10997 if !has_content {
10998 writer
10999 .write_event(Event::Empty(start))
11000 .map_err(OxmlError::from)?;
11001 return Ok(());
11002 }
11003 writer
11004 .write_event(Event::Start(start))
11005 .map_err(OxmlError::from)?;
11006 if let Some(markup) = markup {
11007 for raw in &markup.raw_content {
11008 writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
11009 }
11010 }
11011 writer
11012 .write_event(Event::End(BytesEnd::new(name)))
11013 .map_err(OxmlError::from)?;
11014 Ok(())
11015}
11016
11017fn capture_event(event: Event<'_>) -> Result<Vec<u8>> {
11018 let mut writer = Writer::new(Vec::new());
11019 writer
11020 .write_event(event.into_owned())
11021 .map_err(OxmlError::from)?;
11022 Ok(writer.into_inner())
11023}
11024
11025fn emit_raw<'a, W: Write>(
11026 writer: &mut Writer<W>,
11027 children: impl Iterator<Item = &'a [u8]>,
11028) -> Result<()> {
11029 for child in children {
11030 writer.get_mut().write_all(child).map_err(OxmlError::from)?;
11031 }
11032 Ok(())
11033}
11034
11035fn invalid_attribute(element: &str, attribute: &str, value: String) -> ChartError {
11036 ChartError::InvalidAttribute {
11037 element: element.to_owned(),
11038 attribute: attribute.to_owned(),
11039 value,
11040 }
11041}
11042
11043fn element_name(element: &BytesStart<'_>) -> String {
11044 String::from_utf8_lossy(element.name().as_ref()).into_owned()
11045}
11046
11047fn missing_end(element: &str) -> ChartError {
11048 ChartError::Xml(OxmlError::MissingElement(format!("closing {element}")))
11049}
11050
11051#[cfg(test)]
11052mod tests {
11053 use std::collections::HashSet;
11054 use std::fs;
11055 use std::path::{Path, PathBuf};
11056 use std::process::Command;
11057
11058 use oxml_core::raw_xml::{capture_element, capture_empty_element};
11059 use oxml_drawing::color::{ColorMap, ColorMapSlot, RgbColor, ThemeColorSlot};
11060 use oxml_drawing::theme::CT_OfficeStyleSheet;
11061 use oxml_layout::{
11062 Color, FontManager, LayoutResult, PageFrame, Paint, PathCommand, Point, PositionedElement,
11063 Rect,
11064 };
11065 use oxml_opc::OpcPackage;
11066 use quick_xml::Reader;
11067 use quick_xml::events::Event;
11068
11069 use super::{
11070 A_NS, Axis, AxisData, AxisId, AxisKind, AxisPosition, BarDirection, BarGrouping, C_NS,
11071 CT_ChartSpace, CT_DLbls, CT_ShapeProperties, CT_TextBody, ChartData, ChartGeometry,
11072 ChartKind, DataLabelPosition, DispBlanksAs, Domain, Grouping, NumberFormat, NumericData,
11073 Orientation, Plot, R_NS, ScatterStyle, Series, StringRef, TickLabelPosition, TickMark,
11074 authored_chart_parts, capture_event, local_name, matches_local_name, nice_number_scale,
11075 render_chart as render_chart_with_theme, render_geometry as render_geometry_with_theme,
11076 };
11077
11078 const MANIFEST: &str = include_str!("../../../scripts/pptx-corpus-manifest.tsv");
11079 const EXPECTED_DECKS: usize = 50;
11080 const LIBREOFFICE_VERSION: &str =
11081 "LibreOffice 26.2.5.2 cd7284b4cbbfeb507e630c1aac019f4157393acb";
11082 const PDFTOTEXT_VERSION: &str = "pdftotext version 26.01.0";
11083 const PDFTOPPM_VERSION: &str = "pdftoppm version 26.01.0";
11084 const PLOT_RENDER_NORMALIZED_MAE_THRESHOLD: f64 = 0.0;
11085
11086 #[test]
11087 fn authored_charts_emit_portable_viewer_defaults() {
11088 let data = ChartData {
11089 categories: vec!["August".to_owned(), "September".to_owned()],
11090 series: vec![("Price".to_owned(), vec![12.5, 19.2])],
11091 number_format: Some(r"0.##\%".to_owned()),
11092 category_axis_title: Some("Month".to_owned()),
11093 value_axis_title: Some("Change".to_owned()),
11094 palette: vec![
11095 RgbColor::parse("2B6FE3").unwrap(),
11096 RgbColor::parse("F0761F").unwrap(),
11097 ],
11098 };
11099 let (line, line_workbook) = authored_chart_parts(ChartKind::Line, &data, "rId1").unwrap();
11100 let line = std::str::from_utf8(&line).unwrap();
11101 assert_eq!(line.matches(r#"<c:delete val="0"/>"#).count(), 2);
11102 assert!(line.contains(r#"<c:numFmt formatCode="0.##\%" sourceLinked="0"/>"#));
11103 assert!(line.contains("<a:t>Month</a:t>"));
11104 assert!(line.contains("<a:t>Change</a:t>"));
11105 assert!(line.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
11106 let assert_portable_line = |xml: &str| {
11107 let titles = xml
11108 .split("<c:title>")
11109 .skip(1)
11110 .map(|tail| tail.split_once("</c:title>").unwrap().0)
11111 .collect::<Vec<_>>();
11112 assert_eq!(titles.len(), 2);
11113 for title in titles {
11114 let text = title.find("<c:tx>").unwrap();
11115 let layout = title.find("<c:layout/>").unwrap();
11116 let overlay = title.find(r#"<c:overlay val="0"/>"#).unwrap();
11117 assert!(text < layout && layout < overlay);
11118 }
11119
11120 let line_plot = xml
11121 .split_once("<c:lineChart>")
11122 .unwrap()
11123 .1
11124 .split_once("</c:lineChart>")
11125 .unwrap()
11126 .0;
11127 assert_eq!(line_plot.matches(r#"<c:marker val="0"/>"#).count(), 1);
11128 assert_eq!(line_plot.matches(r#"<c:smooth val="0"/>"#).count(), 1);
11129 let marker = line_plot.find(r#"<c:marker val="0"/>"#).unwrap();
11130 let smooth = line_plot.find(r#"<c:smooth val="0"/>"#).unwrap();
11131 let axis_id = line_plot.find("<c:axId").unwrap();
11132 assert!(marker < smooth && smooth < axis_id);
11133 };
11134 assert_portable_line(line);
11135 for axis in line.split("<c:crossAx").take(2) {
11136 let scaling = axis.rfind("<c:scaling").unwrap();
11137 let deleted = axis.rfind("<c:delete").unwrap();
11138 let position = axis.rfind("<c:axPos").unwrap();
11139 assert!(scaling < deleted && deleted < position);
11140 }
11141
11142 let parsed = CT_ChartSpace::from_xml(line.as_bytes()).unwrap();
11143 let rewritten = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
11144 assert_portable_line(&rewritten);
11145 assert!(rewritten.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
11146 assert!(rewritten.contains(r#"<c:externalData r:id="rId1">"#));
11147
11148 for kind in [
11149 ChartKind::Bar,
11150 ChartKind::Pie,
11151 ChartKind::Doughnut,
11152 ChartKind::Area,
11153 ChartKind::Scatter,
11154 ChartKind::Radar,
11155 ] {
11156 let mut kind_data = data.clone();
11157 if kind == ChartKind::Scatter {
11158 kind_data.categories = vec!["1".to_owned(), "2".to_owned()];
11159 }
11160 let (chart, workbook) = authored_chart_parts(kind, &kind_data, "rId1").unwrap();
11161 let chart = std::str::from_utf8(&chart).unwrap();
11162 assert!(!chart.contains(r#"<c:marker val="0"/>"#));
11163 assert!(!chart.contains(r#"<c:smooth val="0"/>"#));
11164 if kind != ChartKind::Scatter {
11165 assert_eq!(workbook, line_workbook);
11166 }
11167 if matches!(kind, ChartKind::Pie | ChartKind::Doughnut) {
11168 assert!(chart.contains("<c:legend"));
11169 assert!(chart.contains(r#"<c:showPercent val="1"/>"#));
11170 }
11171 if kind == ChartKind::Doughnut {
11172 assert!(chart.contains(r#"<c:holeSize val="50"/>"#));
11173 }
11174 if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) {
11175 assert_eq!(chart.matches("<c:dPt>").count(), 2);
11176 assert!(chart.contains(
11177 r#"<c:dPt><c:idx val="0"/><c:spPr><a:solidFill><a:srgbClr val="2B6FE3"/>"#
11178 ));
11179 assert!(chart.contains(
11180 r#"<c:dPt><c:idx val="1"/><c:spPr><a:solidFill><a:srgbClr val="F0761F"/>"#
11181 ));
11182 }
11183
11184 let parsed = CT_ChartSpace::from_xml(chart.as_bytes()).unwrap();
11185 let rewritten = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
11186 assert_eq!(
11187 rewritten.matches("<c:dPt>").count(),
11188 usize::from(matches!(
11189 kind,
11190 ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut
11191 )) * 2
11192 );
11193 assert!(rewritten.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
11194 if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) {
11195 assert!(rewritten.contains(r#"<a:srgbClr val="F0761F"/>"#));
11196 }
11197 }
11198 }
11199
11200 #[test]
11201 fn optional_numeric_cache_format_remains_omitted() {
11202 let xml = br#"<c:numRef xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:f>Sheet1!$B$2:$B$3</c:f><c:numCache><c:ptCount val="2"/><c:pt idx="0"><c:v>12.5</c:v></c:pt><c:pt idx="1"><c:v>19.2</c:v></c:pt></c:numCache></c:numRef>"#;
11203 let mut parsed = NumericData::from_xml(xml).unwrap();
11204 assert_eq!(parsed.format_code, "General");
11205 let written = parsed.to_xml().unwrap();
11206 assert!(!String::from_utf8_lossy(&written).contains("formatCode"));
11207 assert_eq!(NumericData::from_xml(&written).unwrap(), parsed);
11208
11209 parsed.format_code = "0.00".to_owned();
11210 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
11211 assert!(written.contains("<c:formatCode>0.00</c:formatCode>"));
11212 }
11213
11214 fn render_geometry(chart: &super::CT_Chart, bounds: Rect) -> super::Result<ChartGeometry> {
11215 render_geometry_with_theme(
11216 chart,
11217 bounds,
11218 &CT_OfficeStyleSheet::office_default(),
11219 &ColorMap::default(),
11220 )
11221 }
11222
11223 fn render_chart(
11224 chart: &super::CT_Chart,
11225 bounds: Rect,
11226 fonts: &mut FontManager,
11227 ) -> super::Result<oxml_layout::GroupElement> {
11228 render_chart_with_theme(
11229 chart,
11230 bounds,
11231 &CT_OfficeStyleSheet::office_default(),
11232 &ColorMap::default(),
11233 fonts,
11234 )
11235 }
11236
11237 #[test]
11238 fn bar_chart_rasterises_at_computed_positions() {
11239 let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11240 let geometry = render_geometry(
11241 &chart.chart,
11242 Rect {
11243 x: 0.0,
11244 y: 0.0,
11245 width: 200.0,
11246 height: 140.0,
11247 },
11248 )
11249 .unwrap();
11250 assert_eq!(
11251 geometry.plot_bounds,
11252 Rect {
11253 x: 36.0,
11254 y: 12.0,
11255 width: 152.0,
11256 height: 100.0,
11257 }
11258 );
11259 let bounds = path_bounds(&geometry);
11260 assert_eq!(bounds.len(), 2);
11261 assert_rect_near(
11262 bounds[0],
11263 Rect {
11264 x: 58.8,
11265 y: 62.0,
11266 width: 30.4,
11267 height: 50.0,
11268 },
11269 );
11270 assert_rect_near(
11271 bounds[1],
11272 Rect {
11273 x: 134.8,
11274 y: 12.0,
11275 width: 30.4,
11276 height: 100.0,
11277 },
11278 );
11279
11280 let layout = LayoutResult::new(
11281 vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
11282 Vec::new(),
11283 None,
11284 Vec::new(),
11285 );
11286 let png = oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap();
11287 let pixmap = tiny_skia::Pixmap::decode_png(&png).unwrap();
11288 for (x, y) in [(74, 87), (150, 37)] {
11289 let pixel = pixmap.pixel(x, y).unwrap();
11290 assert_ne!((pixel.red(), pixel.green(), pixel.blue()), (255, 255, 255));
11291 }
11292 }
11293
11294 #[test]
11295 fn unstyled_four_series_use_accent_one_through_four() {
11296 let chart = four_series_bar_chart();
11297 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11298 let colours = path_colours(&geometry);
11299
11300 assert_eq!(
11301 &colours[..4],
11302 &[
11303 Color::from_hex("156082"),
11304 Color::from_hex("E97132"),
11305 Color::from_hex("196B24"),
11306 Color::from_hex("0F9ED5"),
11307 ]
11308 );
11309 }
11310
11311 #[test]
11312 fn direct_series_solid_colour_overrides_theme_accent() {
11313 for (paint, expected) in [
11314 (
11315 "<a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill>",
11316 Color::from_hex("123456"),
11317 ),
11318 (
11319 "<a:ln><a:solidFill><a:srgbClr val=\"654321\"/></a:solidFill></a:ln>",
11320 Color::from_hex("654321"),
11321 ),
11322 (
11323 "<a:noFill/>",
11324 Color {
11325 r: 0.0,
11326 g: 0.0,
11327 b: 0.0,
11328 a: 0.0,
11329 },
11330 ),
11331 ] {
11332 let styled =
11333 plot_series(0).replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
11334 let plot = bar_plot("").replace(&plot_series(0), &styled);
11335 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11336 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11337
11338 assert_eq!(path_colours(&geometry)[0], expected);
11339 }
11340
11341 let styled = plot_series(0).replace(
11342 "<q:cat>",
11343 "<q:spPr><a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill></q:spPr><q:cat>",
11344 );
11345 let plot = bar_plot("").replace(&plot_series(0), &styled);
11346 let xml = chart_with_plot(&plot).replace(
11347 "</q:plotArea></q:chart>",
11348 "</q:plotArea><q:legend/></q:chart>",
11349 );
11350 let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
11351 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
11352 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
11353 assert!(group.children.iter().any(|element| matches!(
11354 element,
11355 PositionedElement::Path(path)
11356 if path.fill == Some(Paint::Solid(Color::from_hex("123456")))
11357 )));
11358
11359 let mut theme = CT_OfficeStyleSheet::office_default();
11360 let preset = oxml_drawing::fill::Fill::from_xml(
11361 br#"<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:prstClr val="red"/></a:solidFill>"#,
11362 )
11363 .unwrap();
11364 let oxml_drawing::fill::Fill::Solid(preset) = preset else {
11365 panic!("expected solid fill");
11366 };
11367 theme.theme_elements.color_scheme.hyperlink = preset.color.unwrap();
11368 let geometry =
11369 render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &ColorMap::default())
11370 .unwrap();
11371 assert_eq!(path_colours(&geometry)[0], Color::from_hex("123456"));
11372
11373 for (paint, expected_alpha) in [
11374 ("<a:noFill/>", 0.0),
11375 (
11376 "<a:solidFill><a:srgbClr val=\"123456\"><a:alpha val=\"50000\"/></a:srgbClr></a:solidFill>",
11377 (128.0 / 255.0) * 0.55,
11378 ),
11379 ] {
11380 let (_, area, _) = remaining_plot_fixtures()
11381 .into_iter()
11382 .find(|(kind, _, _)| *kind == "areaChart")
11383 .unwrap();
11384 let styled_area = area.replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
11385 let chart =
11386 CT_ChartSpace::from_xml(chart_with_optional_axes(&styled_area, true).as_bytes())
11387 .unwrap();
11388 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11389 let actual_alpha = path_colours(&geometry)[0].a;
11390
11391 assert!((actual_alpha - expected_alpha).abs() < 1.0e-12);
11392 }
11393 }
11394
11395 #[test]
11396 fn series_accent_cycle_repeats_after_six() {
11397 let chart = seven_series_bar_chart();
11398 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11399 let colours = path_colours(&geometry);
11400
11401 assert_eq!(colours[0], Color::from_hex("156082"));
11402 assert_eq!(colours[6], colours[0]);
11403 }
11404
11405 #[test]
11406 fn series_colours_honor_colour_map_and_transform_order() {
11407 let styled = plot_series(0).replace(
11408 "<q:cat>",
11409 "<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>",
11410 );
11411 let plot = bar_plot("").replace(&plot_series(0), &styled);
11412 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11413 let color_map =
11414 ColorMap::default().with_overrides(&[(ColorMapSlot::Accent1, ThemeColorSlot::Accent4)]);
11415 let mut theme = CT_OfficeStyleSheet::office_default();
11416 let theme_fill = oxml_drawing::fill::Fill::from_xml(
11417 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>"#,
11418 )
11419 .unwrap();
11420 let oxml_drawing::fill::Fill::Solid(theme_fill) = theme_fill else {
11421 panic!("expected solid fill");
11422 };
11423 let theme_colour = theme_fill.color.unwrap();
11424 theme.theme_elements.color_scheme.accent4 = theme_colour.clone();
11425 let geometry =
11426 render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &color_map).unwrap();
11427 let series_fill = oxml_drawing::fill::Fill::from_xml(
11428 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>"#,
11429 )
11430 .unwrap();
11431 let oxml_drawing::fill::Fill::Solid(series_fill) = series_fill else {
11432 panic!("expected solid fill");
11433 };
11434 let mut transforms = theme_colour.transforms().to_vec();
11435 transforms.extend_from_slice(series_fill.color.as_ref().unwrap().transforms());
11436 let expected = oxml_drawing::color::apply_color_transforms(
11437 RgbColor::parse("336699").unwrap(),
11438 &transforms,
11439 );
11440
11441 assert_eq!(path_colours(&geometry)[0], resolved_layout_colour(expected));
11442 }
11443
11444 #[test]
11445 fn chart_colours_do_not_use_word_tint_shade() {
11446 let styled = plot_series(0).replace(
11447 "<q:cat>",
11448 "<q:spPr><a:solidFill><a:srgbClr val=\"808080\"><a:tint val=\"50000\"/></a:srgbClr></a:solidFill></q:spPr><q:cat>",
11449 );
11450 let plot = bar_plot("").replace(&plot_series(0), &styled);
11451 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11452 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11453
11454 assert_eq!(
11455 path_colours(&geometry)[0],
11456 Color {
11457 r: 205.0 / 255.0,
11458 g: 205.0 / 255.0,
11459 b: 205.0 / 255.0,
11460 a: 1.0,
11461 }
11462 );
11463 }
11464
11465 #[test]
11466 fn unsupported_direct_series_paint_is_contextual() {
11467 let styled = plot_series(0).replace(
11468 "<q:cat>",
11469 "<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>",
11470 );
11471 let plot = bar_plot("").replace(&plot_series(0), &styled);
11472 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11473 let error = render_geometry(&chart.chart, chart_bounds()).unwrap_err();
11474
11475 assert!(error.to_string().contains("c:ser[0] direct fill paint"));
11476 assert!(error.to_string().contains("gradient"));
11477 }
11478
11479 #[test]
11480 fn resolved_chart_palette_raster_is_deterministic() {
11481 let raster = || {
11482 let chart = four_series_bar_chart();
11483 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
11484 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
11485 let layout = LayoutResult::new(
11486 vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
11487 fonts.all_font_data(),
11488 None,
11489 Vec::new(),
11490 );
11491 oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
11492 };
11493 let chart = four_series_bar_chart();
11494 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11495
11496 assert_eq!(path_colours(&geometry)[0], Color::from_hex("156082"));
11497 assert_eq!(raster(), raster());
11498 }
11499
11500 #[test]
11501 fn bar_geometry_handles_direction_grouping_gap_and_overlap() {
11502 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11503 let Plot::Bar {
11504 overlap, series, ..
11505 } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11506 else {
11507 panic!("expected bar plot");
11508 };
11509 let mut second = series[0].clone();
11510 second.index = 1;
11511 second.order = 1;
11512 second.values.values = vec![2.0, 1.0];
11513 series.push(second);
11514
11515 *overlap = 25;
11516 let clustered = render_geometry(&chart.chart, chart_bounds()).unwrap();
11517 let clustered_bounds = path_bounds(&clustered);
11518 let clustered_width = 30.4 / 1.75;
11519 let clustered_advance = clustered_width * 0.75;
11520 assert_rects_near(
11521 &clustered_bounds,
11522 &[
11523 Rect {
11524 x: 58.8,
11525 y: 62.0,
11526 width: clustered_width,
11527 height: 50.0,
11528 },
11529 Rect {
11530 x: 134.8,
11531 y: 12.0,
11532 width: clustered_width,
11533 height: 100.0,
11534 },
11535 Rect {
11536 x: 58.8 + clustered_advance,
11537 y: 12.0,
11538 width: clustered_width,
11539 height: 100.0,
11540 },
11541 Rect {
11542 x: 134.8 + clustered_advance,
11543 y: 62.0,
11544 width: clustered_width,
11545 height: 50.0,
11546 },
11547 ],
11548 );
11549
11550 let Plot::Bar { direction, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
11551 unreachable!()
11552 };
11553 *direction = BarDirection::Bar;
11554 let horizontal = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11555 let horizontal_width = 20.0 / 1.75;
11556 let horizontal_advance = horizontal_width * 0.75;
11557 assert_rects_near(
11558 &horizontal,
11559 &[
11560 Rect {
11561 x: 36.0,
11562 y: 27.0,
11563 width: 76.0,
11564 height: horizontal_width,
11565 },
11566 Rect {
11567 x: 36.0,
11568 y: 77.0,
11569 width: 152.0,
11570 height: horizontal_width,
11571 },
11572 Rect {
11573 x: 36.0,
11574 y: 27.0 + horizontal_advance,
11575 width: 152.0,
11576 height: horizontal_width,
11577 },
11578 Rect {
11579 x: 36.0,
11580 y: 77.0 + horizontal_advance,
11581 width: 76.0,
11582 height: horizontal_width,
11583 },
11584 ],
11585 );
11586
11587 let Plot::Bar {
11588 direction,
11589 grouping,
11590 ..
11591 } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11592 else {
11593 unreachable!()
11594 };
11595 *direction = BarDirection::Column;
11596 *grouping = BarGrouping::Stacked;
11597 let stacked = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11598 assert_rects_near(
11599 &stacked,
11600 &[
11601 Rect {
11602 x: 58.8,
11603 y: 112.0 - 100.0 / 3.0,
11604 width: 30.4,
11605 height: 100.0 / 3.0,
11606 },
11607 Rect {
11608 x: 134.8,
11609 y: 112.0 - 200.0 / 3.0,
11610 width: 30.4,
11611 height: 200.0 / 3.0,
11612 },
11613 Rect {
11614 x: 58.8,
11615 y: 12.0,
11616 width: 30.4,
11617 height: 200.0 / 3.0,
11618 },
11619 Rect {
11620 x: 134.8,
11621 y: 12.0,
11622 width: 30.4,
11623 height: 100.0 / 3.0,
11624 },
11625 ],
11626 );
11627
11628 let Plot::Bar {
11629 grouping, series, ..
11630 } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11631 else {
11632 unreachable!()
11633 };
11634 *grouping = BarGrouping::PercentStacked;
11635 series[1].values.values = vec![2.0, 2.0];
11636 let percent = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11637 assert_rects_near(
11638 &percent,
11639 &[
11640 Rect {
11641 x: 58.8,
11642 y: 112.0 - 100.0 / 3.0,
11643 width: 30.4,
11644 height: 100.0 / 3.0,
11645 },
11646 Rect {
11647 x: 134.8,
11648 y: 62.0,
11649 width: 30.4,
11650 height: 50.0,
11651 },
11652 Rect {
11653 x: 58.8,
11654 y: 12.0,
11655 width: 30.4,
11656 height: 200.0 / 3.0,
11657 },
11658 Rect {
11659 x: 134.8,
11660 y: 12.0,
11661 width: 30.4,
11662 height: 50.0,
11663 },
11664 ],
11665 );
11666 }
11667
11668 #[test]
11669 fn line_scatter_and_radar_emit_paths_and_markers() {
11670 let line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
11671 let line_geometry = render_geometry(&line.chart, chart_bounds()).unwrap();
11672 assert_path_points(
11673 path_commands(&line_geometry)[0],
11674 &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
11675 false,
11676 );
11677 assert_rects_near(
11678 &path_bounds(&line_geometry)[1..],
11679 &[
11680 Rect {
11681 x: 71.5,
11682 y: 109.5,
11683 width: 5.0,
11684 height: 5.0,
11685 },
11686 Rect {
11687 x: 147.5,
11688 y: 9.5,
11689 width: 5.0,
11690 height: 5.0,
11691 },
11692 ],
11693 );
11694
11695 let (scatter_plot, _) = remaining_plot_fixtures()
11696 .into_iter()
11697 .find(|(kind, _, _)| *kind == "scatterChart")
11698 .map(|(_, plot, axes)| (plot, axes))
11699 .unwrap();
11700 let mut scatter =
11701 CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
11702 .unwrap();
11703 let Plot::Scatter { style, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
11704 else {
11705 unreachable!()
11706 };
11707 *style = ScatterStyle::LineMarker;
11708 let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
11709 assert_path_points(
11710 path_commands(&scatter_geometry)[0],
11711 &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
11712 false,
11713 );
11714 assert_rects_near(
11715 &path_bounds(&scatter_geometry)[1..],
11716 &[
11717 Rect {
11718 x: 33.5,
11719 y: 109.5,
11720 width: 5.0,
11721 height: 5.0,
11722 },
11723 Rect {
11724 x: 185.5,
11725 y: 9.5,
11726 width: 5.0,
11727 height: 5.0,
11728 },
11729 ],
11730 );
11731
11732 let (radar_plot, _) = remaining_plot_fixtures()
11733 .into_iter()
11734 .find(|(kind, _, _)| *kind == "radarChart")
11735 .map(|(_, plot, axes)| (plot, axes))
11736 .unwrap();
11737 let radar = CT_ChartSpace::from_xml(chart_with_optional_axes(&radar_plot, true).as_bytes())
11738 .unwrap();
11739 let radar_geometry = render_geometry(&radar.chart, chart_bounds()).unwrap();
11740 assert_path_points(
11741 path_commands(&radar_geometry)[0],
11742 &[Point { x: 112.0, y: 37.0 }, Point { x: 112.0, y: 112.0 }],
11743 true,
11744 );
11745 assert_rects_near(
11746 &path_bounds(&radar_geometry)[1..],
11747 &[
11748 Rect {
11749 x: 109.5,
11750 y: 34.5,
11751 width: 5.0,
11752 height: 5.0,
11753 },
11754 Rect {
11755 x: 109.5,
11756 y: 109.5,
11757 width: 5.0,
11758 height: 5.0,
11759 },
11760 ],
11761 );
11762 }
11763
11764 #[test]
11765 fn pie_doughnut_and_area_emit_closed_paths() {
11766 let fixtures = remaining_plot_fixtures();
11767 let pie_plot = fixtures
11768 .iter()
11769 .find(|(kind, _, _)| *kind == "pieChart")
11770 .unwrap();
11771 let pie =
11772 CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot.1, pie_plot.2).as_bytes())
11773 .unwrap();
11774 let pie_geometry = render_geometry(&pie.chart, chart_bounds()).unwrap();
11775 let pie_commands = path_commands(&pie_geometry);
11776 assert_eq!(pie_commands.len(), 2);
11777 let [
11778 PathCommand::MoveTo(center),
11779 PathCommand::LineTo(start),
11780 ..,
11781 PathCommand::CurveTo { to: end, .. },
11782 PathCommand::Close,
11783 ] = pie_commands[0]
11784 else {
11785 panic!("expected closed cubic pie wedge");
11786 };
11787 assert_point_near(*center, Point { x: 112.0, y: 62.0 });
11788 assert_point_near(
11789 *start,
11790 Point {
11791 x: 137.0,
11792 y: 62.0 - 25.0 * 3.0_f64.sqrt(),
11793 },
11794 );
11795 assert_point_near(
11796 *end,
11797 Point {
11798 x: 137.0,
11799 y: 62.0 + 25.0 * 3.0_f64.sqrt(),
11800 },
11801 );
11802
11803 let doughnut_plot = fixtures
11804 .iter()
11805 .find(|(kind, _, _)| *kind == "doughnutChart")
11806 .unwrap();
11807 let doughnut = CT_ChartSpace::from_xml(
11808 chart_with_optional_axes(&doughnut_plot.1, doughnut_plot.2).as_bytes(),
11809 )
11810 .unwrap();
11811 let doughnut_geometry = render_geometry(&doughnut.chart, chart_bounds()).unwrap();
11812 let doughnut_commands = path_commands(&doughnut_geometry);
11813 assert_eq!(doughnut_commands.len(), 2);
11814 let commands = doughnut_commands[0];
11815 assert_eq!(commands.len(), 7);
11816 let PathCommand::MoveTo(outer_start) = commands[0] else {
11817 panic!("expected doughnut outer start");
11818 };
11819 let PathCommand::LineTo(inner_end) = commands[3] else {
11820 panic!("expected doughnut inner-radius join");
11821 };
11822 let PathCommand::CurveTo {
11823 to: inner_start, ..
11824 } = commands[5]
11825 else {
11826 panic!("expected reverse inner arc");
11827 };
11828 let diagonal = 25.0 * 2.0_f64.sqrt();
11829 assert_point_near(
11830 outer_start,
11831 Point {
11832 x: 112.0 + diagonal,
11833 y: 62.0 - diagonal,
11834 },
11835 );
11836 assert_point_near(
11837 inner_end,
11838 Point {
11839 x: 112.0 + 30.0 * 75.0_f64.to_radians().cos(),
11840 y: 62.0 + 30.0 * 75.0_f64.to_radians().sin(),
11841 },
11842 );
11843 let inner_diagonal = 15.0 * 2.0_f64.sqrt();
11844 assert_point_near(
11845 inner_start,
11846 Point {
11847 x: 112.0 + inner_diagonal,
11848 y: 62.0 - inner_diagonal,
11849 },
11850 );
11851 assert_eq!(commands.last(), Some(&PathCommand::Close));
11852
11853 let area_plot = fixtures
11854 .iter()
11855 .find(|(kind, _, _)| *kind == "areaChart")
11856 .unwrap();
11857 let mut area =
11858 CT_ChartSpace::from_xml(chart_with_optional_axes(&area_plot.1, area_plot.2).as_bytes())
11859 .unwrap();
11860 let area_geometry = render_geometry(&area.chart, chart_bounds()).unwrap();
11861 assert_path_points(
11862 path_commands(&area_geometry)[0],
11863 &[
11864 Point { x: 74.0, y: 62.0 },
11865 Point { x: 150.0, y: 12.0 },
11866 Point { x: 150.0, y: 112.0 },
11867 Point { x: 74.0, y: 112.0 },
11868 ],
11869 true,
11870 );
11871
11872 let Plot::Area {
11873 grouping, series, ..
11874 } = &mut area.chart.plot_area.plots_mut().unwrap()[0]
11875 else {
11876 unreachable!()
11877 };
11878 *grouping = Grouping::Stacked;
11879 let mut second = series[0].clone();
11880 second.index = 1;
11881 second.order = 1;
11882 second.values.values = vec![2.0, 1.0];
11883 series.push(second);
11884 let stacked_area = render_geometry(&area.chart, chart_bounds()).unwrap();
11885 assert_path_points(
11886 path_commands(&stacked_area)[1],
11887 &[
11888 Point { x: 74.0, y: 12.0 },
11889 Point { x: 150.0, y: 12.0 },
11890 Point {
11891 x: 150.0,
11892 y: 112.0 - 200.0 / 3.0,
11893 },
11894 Point {
11895 x: 74.0,
11896 y: 112.0 - 100.0 / 3.0,
11897 },
11898 ],
11899 true,
11900 );
11901 }
11902
11903 #[test]
11904 fn sparse_cache_indexes_preserve_slots_and_scatter_pairing() {
11905 let series = sparse_category_series();
11906 let bar = format!(
11907 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>"#
11908 );
11909 let bar = CT_ChartSpace::from_xml(chart_with_plot(&bar).as_bytes()).unwrap();
11910 assert_rects_near(
11911 &path_bounds(&render_geometry(&bar.chart, chart_bounds()).unwrap()),
11912 &[
11913 Rect {
11914 x: 51.2,
11915 y: 62.0,
11916 width: 304.0 / 15.0,
11917 height: 50.0,
11918 },
11919 Rect {
11920 x: 36.0 + 304.0 / 3.0 + 15.2,
11921 y: 12.0,
11922 width: 304.0 / 15.0,
11923 height: 100.0,
11924 },
11925 ],
11926 );
11927
11928 let line = format!(
11929 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>"#
11930 );
11931 let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line).as_bytes()).unwrap();
11932 let first = Point {
11933 x: 36.0 + 76.0 / 3.0,
11934 y: 62.0,
11935 };
11936 let missing = Point { x: 112.0, y: 112.0 };
11937 let last = Point {
11938 x: 36.0 + 380.0 / 3.0,
11939 y: 12.0,
11940 };
11941 let gap = render_geometry(&line.chart, chart_bounds()).unwrap();
11942 let gap_commands = path_commands(&gap);
11943 assert_eq!(gap_commands.len(), 2);
11944 assert_path_points(
11945 gap_commands[0],
11946 &[Point {
11947 x: first.x,
11948 y: 112.0,
11949 }],
11950 false,
11951 );
11952 assert_path_points(gap_commands[1], &[last], false);
11953 line.chart.disp_blanks_as = DispBlanksAs::Zero;
11954 assert_path_points(
11955 path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
11956 &[first, missing, last],
11957 false,
11958 );
11959 line.chart.disp_blanks_as = DispBlanksAs::Span;
11960 assert_path_points(
11961 path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
11962 &[
11963 Point {
11964 x: first.x,
11965 y: 112.0,
11966 },
11967 last,
11968 ],
11969 false,
11970 );
11971
11972 let area = format!(
11973 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:areaChart>"#
11974 );
11975 let mut area = CT_ChartSpace::from_xml(chart_with_plot(&area).as_bytes()).unwrap();
11976 let area_gap = render_geometry(&area.chart, chart_bounds()).unwrap();
11977 let area_gap_commands = path_commands(&area_gap);
11978 assert_eq!(area_gap_commands.len(), 2);
11979 assert_path_points(
11980 area_gap_commands[0],
11981 &[
11982 first,
11983 Point {
11984 x: first.x,
11985 y: 112.0,
11986 },
11987 ],
11988 true,
11989 );
11990 assert_path_points(
11991 area_gap_commands[1],
11992 &[
11993 last,
11994 Point {
11995 x: last.x,
11996 y: 112.0,
11997 },
11998 ],
11999 true,
12000 );
12001 area.chart.disp_blanks_as = DispBlanksAs::Zero;
12002 assert_path_points(
12003 path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
12004 &[
12005 first,
12006 missing,
12007 last,
12008 Point {
12009 x: last.x,
12010 y: 112.0,
12011 },
12012 missing,
12013 Point {
12014 x: first.x,
12015 y: 112.0,
12016 },
12017 ],
12018 true,
12019 );
12020 area.chart.disp_blanks_as = DispBlanksAs::Span;
12021 assert_path_points(
12022 path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
12023 &[
12024 first,
12025 last,
12026 Point {
12027 x: last.x,
12028 y: 112.0,
12029 },
12030 Point {
12031 x: first.x,
12032 y: 112.0,
12033 },
12034 ],
12035 true,
12036 );
12037
12038 let radar = format!(
12039 r#"<q:radarChart><q:radarStyle val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#
12040 );
12041 let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
12042 assert_path_points(
12043 path_commands(&render_geometry(&radar.chart, chart_bounds()).unwrap())[0],
12044 &[
12045 Point { x: 112.0, y: 37.0 },
12046 Point {
12047 x: 112.0 - 25.0 * 3.0_f64.sqrt(),
12048 y: 87.0,
12049 },
12050 ],
12051 true,
12052 );
12053
12054 let scatter = format!(
12055 r#"<q:scatterChart><q:scatterStyle val="lineMarker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:scatterChart>"#,
12056 sparse_scatter_series()
12057 );
12058 let mut scatter = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
12059 let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12060 assert_eq!(geometry_children(&scatter_geometry).len(), 4);
12061 let gap_commands = path_commands(&scatter_geometry);
12062 assert_path_points(gap_commands[0], &[Point { x: 36.0, y: 112.0 }], false);
12063 assert_path_points(gap_commands[1], &[Point { x: 188.0, y: 12.0 }], false);
12064
12065 scatter.chart.disp_blanks_as = DispBlanksAs::Zero;
12066 let zero = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12067 assert_path_points(
12068 path_commands(&zero)[0],
12069 &[
12070 Point { x: 74.0, y: 87.0 },
12071 Point { x: 36.0, y: 62.0 },
12072 Point { x: 150.0, y: 112.0 },
12073 Point { x: 188.0, y: 12.0 },
12074 ],
12075 false,
12076 );
12077
12078 scatter.chart.disp_blanks_as = DispBlanksAs::Span;
12079 assert_path_points(
12080 path_commands(&render_geometry(&scatter.chart, chart_bounds()).unwrap())[0],
12081 &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
12082 false,
12083 );
12084 }
12085
12086 #[test]
12087 fn sparse_scatter_domains_and_labels_use_only_matched_points() {
12088 let scatter = format!(
12089 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>"#,
12090 sparse_scatter_series()
12091 );
12092 let chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
12093 let plot = &chart.chart.plot_area.plots().unwrap()[0];
12094 assert_eq!(
12095 super::scatter_domains(plot, DispBlanksAs::Gap).unwrap(),
12096 (
12097 Domain {
12098 min: 10.0,
12099 max: 40.0,
12100 },
12101 Domain {
12102 min: 100.0,
12103 max: 400.0,
12104 },
12105 )
12106 );
12107 let axes = chart.chart.plot_area.axes().unwrap();
12108 let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
12109 let anchors =
12110 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12111 assert_eq!(
12112 anchors.keys().copied().collect::<Vec<_>>(),
12113 vec![(0, 0), (0, 3)]
12114 );
12115 assert_eq!(anchors[&(0, 0)].center, Point { x: 0.0, y: 140.0 });
12116 assert_eq!(anchors[&(0, 3)].center, Point { x: 200.0, y: 0.0 });
12117 }
12118
12119 #[test]
12120 fn sparse_bubble_size_labels_resolve_logical_indexes() {
12121 let scatter = format!(
12122 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>"#,
12123 sparse_scatter_series()
12124 );
12125 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
12126 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12127 let Plot::Scatter { series, .. } = plot else {
12128 panic!("expected scatter plot");
12129 };
12130 series[0].bubble_size = Some(
12131 NumericData::from_xml(
12132 format!(
12133 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>"#
12134 )
12135 .as_bytes(),
12136 )
12137 .unwrap(),
12138 );
12139 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12140 let mut rendered = Vec::new();
12141 super::render_data_labels(
12142 plot,
12143 chart_bounds(),
12144 DispBlanksAs::Gap,
12145 super::GeometryOptions::default(),
12146 &mut fonts,
12147 &mut rendered,
12148 )
12149 .unwrap();
12150 let text: Vec<_> = rendered
12151 .iter()
12152 .filter_map(|element| match element {
12153 PositionedElement::Text(run) => Some(run.text.as_str()),
12154 _ => None,
12155 })
12156 .collect();
12157 assert_eq!(text, vec!["5", "9"]);
12158 }
12159
12160 #[test]
12161 fn geometry_rejects_invalid_bounds_and_opaque_plots() {
12162 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12163 for bounds in [
12164 Rect {
12165 x: 0.0,
12166 y: 0.0,
12167 width: f64::NAN,
12168 height: 100.0,
12169 },
12170 Rect {
12171 x: 0.0,
12172 y: 0.0,
12173 width: 0.0,
12174 height: 100.0,
12175 },
12176 Rect {
12177 x: 0.0,
12178 y: 0.0,
12179 width: 40.0,
12180 height: 30.0,
12181 },
12182 ] {
12183 assert!(render_geometry(&chart.chart, bounds).is_err());
12184 }
12185
12186 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12187 unreachable!()
12188 };
12189 series[0].values.values.clear();
12190 assert!(render_geometry(&chart.chart, chart_bounds()).is_err());
12191
12192 let opaque = CT_ChartSpace::from_xml(
12193 chart_with_plot(r#"<q:bar3DChart><q:barDir val="col"/></q:bar3DChart>"#).as_bytes(),
12194 )
12195 .unwrap();
12196 let error = render_geometry(&opaque.chart, chart_bounds()).unwrap_err();
12197 assert!(error.to_string().contains("unsupported or combination"));
12198
12199 let (scatter_plot, _) = remaining_plot_fixtures()
12200 .into_iter()
12201 .find(|(kind, _, _)| *kind == "scatterChart")
12202 .map(|(_, plot, axes)| (plot, axes))
12203 .unwrap();
12204 let mut scatter =
12205 CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
12206 .unwrap();
12207 for invalid in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
12208 let Plot::Scatter { series, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
12209 else {
12210 unreachable!()
12211 };
12212 let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
12213 unreachable!()
12214 };
12215 x_values.values[0] = invalid;
12216 let error = render_geometry(&scatter.chart, chart_bounds()).unwrap_err();
12217 assert!(error.to_string().contains("xVal"));
12218 }
12219 }
12220
12221 #[test]
12222 fn finite_extremes_never_produce_nonfinite_geometry() {
12223 let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
12224 let Plot::Line { series, .. } = &mut line.chart.plot_area.plots_mut().unwrap()[0] else {
12225 unreachable!()
12226 };
12227 series[0].values.values = vec![-f64::MAX, f64::MAX];
12228 let extreme_line = render_geometry(&line.chart, chart_bounds()).unwrap();
12229 assert_path_points(
12230 path_commands(&extreme_line)[0],
12231 &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
12232 false,
12233 );
12234
12235 let mut stacked =
12236 CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12237 let Plot::Bar {
12238 grouping, series, ..
12239 } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
12240 else {
12241 unreachable!()
12242 };
12243 *grouping = BarGrouping::Stacked;
12244 series[0].values.values = vec![f64::MAX, 1.0];
12245 let mut second = series[0].clone();
12246 second.index = 1;
12247 second.order = 1;
12248 series.push(second);
12249 let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
12250 assert!(error.to_string().contains("stacked value total"));
12251
12252 let Plot::Bar { grouping, .. } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
12253 else {
12254 unreachable!()
12255 };
12256 *grouping = BarGrouping::PercentStacked;
12257 let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
12258 assert!(error.to_string().contains("stacked percentage total"));
12259
12260 let (pie_plot, _) = remaining_plot_fixtures()
12261 .into_iter()
12262 .find(|(kind, _, _)| *kind == "pieChart")
12263 .map(|(_, plot, axes)| (plot, axes))
12264 .unwrap();
12265 let mut pie =
12266 CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot, false).as_bytes()).unwrap();
12267 let Plot::Pie { series, .. } = &mut pie.chart.plot_area.plots_mut().unwrap()[0] else {
12268 unreachable!()
12269 };
12270 series[0].values.values = vec![f64::MAX, f64::MAX];
12271 let error = render_geometry(&pie.chart, chart_bounds()).unwrap_err();
12272 assert!(error.to_string().contains("pie value total"));
12273
12274 let (scatter_plot, _) = remaining_plot_fixtures()
12275 .into_iter()
12276 .find(|(kind, _, _)| *kind == "scatterChart")
12277 .map(|(_, plot, axes)| (plot, axes))
12278 .unwrap();
12279 let mut scatter =
12280 CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
12281 .unwrap();
12282 let Plot::Scatter { style, series, .. } =
12283 &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
12284 else {
12285 unreachable!()
12286 };
12287 *style = ScatterStyle::Line;
12288 let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
12289 unreachable!()
12290 };
12291 x_values.values = vec![-f64::MAX, f64::MAX];
12292 series[0].values.values = vec![-f64::MAX, f64::MAX];
12293 let extreme_scatter = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12294 assert_path_points(
12295 path_commands(&extreme_scatter)[0],
12296 &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
12297 false,
12298 );
12299 }
12300
12301 #[test]
12302 fn geometry_is_backend_neutral_and_deterministic() {
12303 let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12304 let first = render_geometry(&chart.chart, chart_bounds()).unwrap();
12305 let second = render_geometry(&chart.chart, chart_bounds()).unwrap();
12306 assert_eq!(
12307 format!("{:?}", first.elements),
12308 format!("{:?}", second.elements)
12309 );
12310
12311 let raster = |geometry: ChartGeometry| {
12312 let layout = LayoutResult::new(
12313 vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
12314 Vec::new(),
12315 None,
12316 Vec::new(),
12317 );
12318 oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
12319 };
12320 assert_eq!(raster(first), raster(second));
12321 }
12322
12323 #[test]
12324 fn zero_to_one_hundred_axis_uses_expected_ticks() {
12325 let scale = nice_number_scale(
12326 Domain {
12327 min: 0.0,
12328 max: 100.0,
12329 },
12330 None,
12331 None,
12332 6,
12333 )
12334 .unwrap();
12335 assert_eq!(scale.ticks, vec![0.0, 20.0, 40.0, 60.0, 80.0, 100.0]);
12336 }
12337
12338 #[test]
12339 fn nice_number_ticks_cover_unpinned_extents() {
12340 for domain in [
12341 Domain {
12342 min: 3.0,
12343 max: 97.0,
12344 },
12345 Domain {
12346 min: -97.0,
12347 max: -3.0,
12348 },
12349 Domain {
12350 min: -3.5,
12351 max: 8.25,
12352 },
12353 Domain {
12354 min: 0.125,
12355 max: 0.875,
12356 },
12357 Domain {
12358 min: 1.0e12,
12359 max: 7.0e12,
12360 },
12361 Domain {
12362 min: -f64::MAX,
12363 max: f64::MAX,
12364 },
12365 Domain {
12366 min: f64::from_bits(1),
12367 max: f64::from_bits(5),
12368 },
12369 ] {
12370 let scale = nice_number_scale(domain, None, None, 6).unwrap();
12371 assert!(scale.domain.min <= domain.min);
12372 assert!(scale.domain.max >= domain.max);
12373 assert!(scale.ticks.iter().all(|tick| tick.is_finite()));
12374 assert!(scale.ticks.windows(2).all(|pair| pair[0] < pair[1]));
12375 }
12376
12377 let constant = nice_number_scale(Domain { min: 4.0, max: 4.0 }, None, None, 6).unwrap();
12378 assert!(constant.domain.min < 4.0 && constant.domain.max > 4.0);
12379 }
12380
12381 #[test]
12382 fn one_sided_explicit_scale_beyond_data_derives_a_valid_opposite_bound() {
12383 let minimum = nice_number_scale(
12384 Domain {
12385 min: 0.0,
12386 max: 10.0,
12387 },
12388 Some(20.0),
12389 None,
12390 6,
12391 )
12392 .unwrap();
12393 assert_eq!(minimum.domain.min, 20.0);
12394 assert!(minimum.domain.max > 20.0);
12395 assert!(minimum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
12396
12397 let maximum = nice_number_scale(
12398 Domain {
12399 min: 0.0,
12400 max: 10.0,
12401 },
12402 None,
12403 Some(-20.0),
12404 6,
12405 )
12406 .unwrap();
12407 assert_eq!(maximum.domain.max, -20.0);
12408 assert!(maximum.domain.min < -20.0);
12409 assert!(maximum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
12410 }
12411
12412 #[test]
12413 fn standard_line_domain_does_not_force_zero() {
12414 let mut chart =
12415 CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
12416 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12417 let Plot::Line { series, .. } = plot else {
12418 panic!("expected line plot");
12419 };
12420 series[0].values.values = vec![1_000.0, 1_010.0];
12421 assert_eq!(
12422 super::plot_value_domain(plot, DispBlanksAs::Gap).unwrap(),
12423 Some(Domain {
12424 min: 1_000.0,
12425 max: 1_010.0,
12426 })
12427 );
12428 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12429 assert_path_points(
12430 path_commands(&geometry)[0],
12431 &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
12432 false,
12433 );
12434 }
12435
12436 #[test]
12437 fn explicit_scaling_bounds_and_orientation_are_honored() {
12438 let scale = nice_number_scale(
12439 Domain {
12440 min: 25.0,
12441 max: 75.0,
12442 },
12443 Some(0.0),
12444 Some(100.0),
12445 6,
12446 )
12447 .unwrap();
12448 assert_eq!(
12449 scale.domain,
12450 Domain {
12451 min: 0.0,
12452 max: 100.0
12453 }
12454 );
12455 assert_eq!(
12456 super::map_y_oriented(0.0, scale.domain, chart_bounds(), Orientation::MaxMin).unwrap(),
12457 0.0
12458 );
12459 assert_eq!(
12460 super::map_y_oriented(100.0, scale.domain, chart_bounds(), Orientation::MaxMin)
12461 .unwrap(),
12462 140.0
12463 );
12464
12465 let mut chart = labelled_bar_chart();
12466 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12467 panic!("expected bar plot");
12468 };
12469 series[0].values.values = vec![25.0, 75.0];
12470 let value_axis = chart
12471 .chart
12472 .plot_area
12473 .axes
12474 .iter_mut()
12475 .find(|axis| axis.kind == AxisKind::Value)
12476 .unwrap();
12477 value_axis.scaling.orientation = Orientation::MaxMin;
12478 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12479 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12480 let plot = group
12481 .children
12482 .iter()
12483 .find_map(|element| match element {
12484 PositionedElement::Group(group) => Some(group),
12485 _ => None,
12486 })
12487 .unwrap();
12488 let bars: Vec<_> = plot
12489 .children
12490 .iter()
12491 .filter_map(|element| match element {
12492 PositionedElement::Path(path) => path.path.bounds(),
12493 _ => None,
12494 })
12495 .collect();
12496 assert_eq!(bars.len(), 2);
12497 assert!((bars[0].y - 12.0).abs() < 1.0e-9);
12498 assert!((bars[0].height - 25.0).abs() < 1.0e-9);
12499 assert!((bars[1].height - 75.0).abs() < 1.0e-9);
12500 }
12501
12502 #[test]
12503 fn family_label_anchors_share_rendered_geometry() {
12504 let mut bar = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12505 let bar_plot = &mut bar.chart.plot_area.plots_mut().unwrap()[0];
12506 let Plot::Bar { series, .. } = bar_plot else {
12507 panic!("expected bar plot");
12508 };
12509 let mut second = series[0].clone();
12510 second.index = 1;
12511 second.order = 1;
12512 series.push(second);
12513 let anchors = super::plot_label_anchors(
12514 bar_plot,
12515 chart_bounds(),
12516 DispBlanksAs::Gap,
12517 super::GeometryOptions::default(),
12518 )
12519 .unwrap();
12520 assert_point_near(anchors[&(0, 0)].center, Point { x: 40.0, y: 105.0 });
12521 assert_point_near(anchors[&(1, 0)].center, Point { x: 60.0, y: 105.0 });
12522
12523 let pie = CT_ChartSpace::from_xml(
12524 chart_with_optional_axes(
12525 &remaining_plot_fixtures()
12526 .into_iter()
12527 .find(|(kind, _, _)| *kind == "pieChart")
12528 .unwrap()
12529 .1,
12530 false,
12531 )
12532 .as_bytes(),
12533 )
12534 .unwrap();
12535 let Plot::Pie {
12536 first_slice_angle,
12537 series,
12538 ..
12539 } = &pie.chart.plot_area.plots().unwrap()[0]
12540 else {
12541 panic!("expected pie plot");
12542 };
12543 let slices = super::pie_slices(*first_slice_angle, None, series, chart_bounds()).unwrap();
12544 assert_eq!(slices.len(), 2);
12545 let first_span = std::f64::consts::TAU / 3.0;
12546 let first_midpoint = f64::from(*first_slice_angle).to_radians()
12547 - std::f64::consts::FRAC_PI_2
12548 + first_span / 2.0;
12549 assert_point_near(
12550 slices[0].anchor,
12551 super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, first_midpoint),
12552 );
12553 let second_midpoint = first_midpoint + std::f64::consts::PI;
12554 assert_point_near(
12555 slices[1].anchor,
12556 super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, second_midpoint),
12557 );
12558
12559 let radar = format!(
12560 r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12561 plot_series(0)
12562 );
12563 let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
12564 let Plot::Radar { series, .. } = &radar.chart.plot_area.plots().unwrap()[0] else {
12565 panic!("expected radar plot");
12566 };
12567 let radar_points =
12568 super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
12569 .unwrap();
12570 assert_eq!(radar_points[0][0], (0, Point { x: 100.0, y: 35.0 }));
12571 assert_eq!(radar_points[0][1], (1, Point { x: 100.0, y: 140.0 }));
12572 }
12573
12574 #[test]
12575 fn radar_annotations_use_spokes_perimeter_labels_and_radial_gridlines() {
12576 let radar_plot = format!(
12577 r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12578 sparse_category_series()
12579 );
12580 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12581 for axis in &mut chart.chart.plot_area.axes {
12582 axis.major_tick_mark = TickMark::Cross;
12583 if axis.kind == AxisKind::Value {
12584 axis.major_gridlines = Some(super::ChartLines::default());
12585 }
12586 }
12587 let plot = &chart.chart.plot_area.plots().unwrap()[0];
12588 let axes = chart.chart.plot_area.axes().unwrap();
12589 let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
12590 let center = Point { x: 112.0, y: 62.0 };
12591 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12592 let annotations =
12593 super::render_axes_and_labels(plot, &axes, bounds, DispBlanksAs::Gap, &mut fonts)
12594 .unwrap();
12595 assert_eq!(annotations.axis_lines.len(), 4);
12596 for spoke in annotations.axis_lines.iter().take(3) {
12597 let PositionedElement::Path(spoke) = spoke else {
12598 panic!("radar spoke must be a path");
12599 };
12600 assert_eq!(
12601 spoke.path.commands.first(),
12602 Some(&PathCommand::MoveTo(center))
12603 );
12604 }
12605 assert!(!annotations.gridlines.is_empty());
12606 for gridline in &annotations.gridlines {
12607 let PositionedElement::Path(gridline) = gridline else {
12608 panic!("radar gridline must be a path");
12609 };
12610 assert_eq!(gridline.path.commands.last(), Some(&PathCommand::Close));
12611 let radii: Vec<_> = gridline
12612 .path
12613 .commands
12614 .iter()
12615 .filter_map(|command| match command {
12616 PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
12617 Some((point.x - center.x).hypot(point.y - center.y))
12618 }
12619 _ => None,
12620 })
12621 .collect();
12622 assert_eq!(radii.len(), 3);
12623 assert!(
12624 radii
12625 .windows(2)
12626 .all(|pair| (pair[0] - pair[1]).abs() < 1.0e-9)
12627 );
12628 }
12629 let text: Vec<_> = annotations
12630 .labels
12631 .iter()
12632 .filter_map(|element| match element {
12633 PositionedElement::Text(run) => Some(run.text.as_str()),
12634 _ => None,
12635 })
12636 .collect();
12637 assert!(text.contains(&"North"));
12638 assert!(text.contains(&"West"));
12639 }
12640
12641 #[test]
12642 fn radar_tick_label_positions_are_distinct() {
12643 let radar_plot = format!(
12644 r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12645 plot_series(0)
12646 );
12647 let chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12648 let plot = &chart.chart.plot_area.plots().unwrap()[0];
12649 let axes = chart.chart.plot_area.axes().unwrap();
12650 let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
12651 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12652
12653 let category_axis = axes
12654 .iter()
12655 .find(|axis| axis.kind == AxisKind::Category)
12656 .unwrap();
12657 for (position, expected) in [
12658 (TickLabelPosition::High, Point { x: 108.0, y: 0.0 }),
12659 (TickLabelPosition::Low, Point { x: 108.0, y: 25.0 }),
12660 (TickLabelPosition::NextTo, Point { x: 108.0, y: 5.0 }),
12661 ] {
12662 let mut axis = category_axis.clone();
12663 axis.tick_label_position = position;
12664 let annotations =
12665 super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
12666 .unwrap();
12667 let north = annotations
12668 .labels
12669 .iter()
12670 .find_map(|element| match element {
12671 PositionedElement::Text(run) if run.text == "North" => Some(run),
12672 _ => None,
12673 })
12674 .unwrap();
12675 assert_point_near(north.origin, expected);
12676 }
12677 let mut hidden_category = category_axis.clone();
12678 hidden_category.tick_label_position = TickLabelPosition::None;
12679 let annotations = super::render_axes_and_labels(
12680 plot,
12681 &[hidden_category],
12682 bounds,
12683 DispBlanksAs::Gap,
12684 &mut fonts,
12685 )
12686 .unwrap();
12687 assert!(annotations.labels.is_empty());
12688
12689 let narrow_bounds = Rect {
12690 x: 36.0,
12691 y: 12.0,
12692 width: 24.0,
12693 height: 100.0,
12694 };
12695 let narrow_center = Point { x: 48.0, y: 62.0 };
12696 assert_eq!(
12697 super::radar_category_label_origin(
12698 narrow_bounds,
12699 narrow_center,
12700 12.0,
12701 1,
12702 4,
12703 TickLabelPosition::High,
12704 ),
12705 Some(Point { x: 72.0, y: 65.0 })
12706 );
12707 assert_eq!(
12708 super::radar_category_label_origin(
12709 narrow_bounds,
12710 narrow_center,
12711 12.0,
12712 1,
12713 4,
12714 TickLabelPosition::NextTo,
12715 ),
12716 Some(Point { x: 66.0, y: 65.0 })
12717 );
12718
12719 let value_axis = axes
12720 .iter()
12721 .find(|axis| axis.kind == AxisKind::Value)
12722 .unwrap();
12723 for (position, expected) in [
12724 (TickLabelPosition::High, Point { x: 166.0, y: 65.0 }),
12725 (TickLabelPosition::Low, Point { x: 32.0, y: 65.0 }),
12726 (TickLabelPosition::NextTo, Point { x: 116.0, y: 65.0 }),
12727 ] {
12728 let mut axis = value_axis.clone();
12729 axis.tick_label_position = position;
12730 let annotations =
12731 super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
12732 .unwrap();
12733 let zero = annotations
12734 .labels
12735 .iter()
12736 .find_map(|element| match element {
12737 PositionedElement::Text(run) if run.text == "0" => Some(run),
12738 _ => None,
12739 })
12740 .unwrap();
12741 assert_point_near(zero.origin, expected);
12742 }
12743 let mut hidden_value = value_axis.clone();
12744 hidden_value.tick_label_position = TickLabelPosition::None;
12745 let annotations = super::render_axes_and_labels(
12746 plot,
12747 &[hidden_value],
12748 bounds,
12749 DispBlanksAs::Gap,
12750 &mut fonts,
12751 )
12752 .unwrap();
12753 assert!(annotations.labels.is_empty());
12754 }
12755
12756 #[test]
12757 fn radar_negative_and_mixed_sign_domains_preserve_values() {
12758 let radar_plot = format!(
12759 r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12760 plot_series(0)
12761 );
12762 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12763 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12764 let Plot::Radar { series, .. } = plot else {
12765 panic!("expected radar plot");
12766 };
12767 series[0].values.values = vec![-10.0, -5.0];
12768 let points =
12769 super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
12770 .unwrap();
12771 assert_eq!(points[0][0], (0, Point { x: 100.0, y: 70.0 }));
12772 assert_eq!(points[0][1], (1, Point { x: 100.0, y: 105.0 }));
12773
12774 series[0].values.values = vec![-5.0, 10.0];
12775 let options = super::GeometryOptions {
12776 value: Some(super::AxisMapping {
12777 domain: Domain {
12778 min: -10.0,
12779 max: 10.0,
12780 },
12781 orientation: Orientation::MinMax,
12782 }),
12783 ..super::GeometryOptions::default()
12784 };
12785 let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
12786 assert_eq!(points[0][0], (0, Point { x: 100.0, y: 52.5 }));
12787 assert_eq!(points[0][1], (1, Point { x: 100.0, y: 140.0 }));
12788 }
12789
12790 #[test]
12791 fn radar_explicit_bounds_suppress_out_of_range_points_and_labels() {
12792 let radar_plot = format!(
12793 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>"#,
12794 plot_series(0)
12795 );
12796 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12797 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12798 let Plot::Radar { series, .. } = plot else {
12799 panic!("expected radar plot");
12800 };
12801 series[0].values.values = vec![25.0, 75.0];
12802 let options = super::GeometryOptions {
12803 value: Some(super::AxisMapping {
12804 domain: Domain {
12805 min: 50.0,
12806 max: 100.0,
12807 },
12808 orientation: Orientation::MinMax,
12809 }),
12810 ..super::GeometryOptions::default()
12811 };
12812 let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
12813 assert_eq!(points[0], vec![(1, Point { x: 100.0, y: 105.0 })]);
12814 let anchors =
12815 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12816 assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 1)]);
12817
12818 let Plot::Radar { series, .. } = plot else {
12819 panic!("expected radar plot");
12820 };
12821 series[0].values.values = vec![75.0, 125.0];
12822 let reversed = super::GeometryOptions {
12823 value: Some(super::AxisMapping {
12824 domain: Domain {
12825 min: 50.0,
12826 max: 100.0,
12827 },
12828 orientation: Orientation::MaxMin,
12829 }),
12830 ..super::GeometryOptions::default()
12831 };
12832 let points = super::radar_series_points(series, chart_bounds(), reversed).unwrap();
12833 assert_eq!(points[0], vec![(0, Point { x: 100.0, y: 35.0 })]);
12834 let anchors =
12835 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
12836 assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
12837
12838 let value_axis = chart
12839 .chart
12840 .plot_area
12841 .axes
12842 .iter_mut()
12843 .find(|axis| axis.kind == AxisKind::Value)
12844 .unwrap();
12845 value_axis.scaling.minimum = Some(50.0);
12846 value_axis.scaling.maximum = Some(100.0);
12847 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12848 let Plot::Radar { series, .. } = plot else {
12849 panic!("expected radar plot");
12850 };
12851 series[0].values.values = vec![25.0, 25.0];
12852 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12853 let rendered = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12854 assert!(rendered.children.iter().any(|element| matches!(
12855 element,
12856 PositionedElement::Group(group) if group.clip.is_some() && group.children.is_empty()
12857 )));
12858 assert!(
12859 rendered
12860 .children
12861 .iter()
12862 .any(|element| matches!(element, PositionedElement::Path(_)))
12863 );
12864 assert!(!rendered.children.iter().any(|element| matches!(
12865 element,
12866 PositionedElement::Text(run) if run.text == "25"
12867 )));
12868 }
12869
12870 #[test]
12871 fn inside_and_outside_label_positions_follow_family_geometry() {
12872 let mut horizontal =
12873 CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12874 let plot = &mut horizontal.chart.plot_area.plots_mut().unwrap()[0];
12875 let Plot::Bar { direction, .. } = plot else {
12876 panic!("expected bar plot");
12877 };
12878 *direction = BarDirection::Bar;
12879 let anchors = super::plot_label_anchors(
12880 plot,
12881 chart_bounds(),
12882 DispBlanksAs::Gap,
12883 super::GeometryOptions::default(),
12884 )
12885 .unwrap();
12886 let anchor = anchors[&(0, 0)];
12887 assert_point_near(
12888 anchor.origin(Some(DataLabelPosition::InsideBase)),
12889 Point { x: 5.0, y: 35.0 },
12890 );
12891 assert_point_near(
12892 anchor.origin(Some(DataLabelPosition::InsideEnd)),
12893 Point { x: 95.0, y: 35.0 },
12894 );
12895 assert_point_near(
12896 anchor.origin(Some(DataLabelPosition::OutsideEnd)),
12897 Point { x: 105.0, y: 35.0 },
12898 );
12899
12900 let mut negative =
12901 CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12902 let plot = &mut negative.chart.plot_area.plots_mut().unwrap()[0];
12903 let Plot::Bar { series, .. } = plot else {
12904 panic!("expected bar plot");
12905 };
12906 series[0].values.values = vec![-1.0, -2.0];
12907 let anchors = super::plot_label_anchors(
12908 plot,
12909 chart_bounds(),
12910 DispBlanksAs::Gap,
12911 super::GeometryOptions::default(),
12912 )
12913 .unwrap();
12914 assert_point_near(
12915 anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12916 Point { x: 50.0, y: 75.0 },
12917 );
12918 let reversed = super::GeometryOptions {
12919 value: Some(super::AxisMapping {
12920 domain: Domain {
12921 min: -2.0,
12922 max: 0.0,
12923 },
12924 orientation: Orientation::MaxMin,
12925 }),
12926 ..super::GeometryOptions::default()
12927 };
12928 let anchors =
12929 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
12930 assert_point_near(
12931 anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12932 Point { x: 50.0, y: 65.0 },
12933 );
12934
12935 let pie = CT_ChartSpace::from_xml(
12936 chart_with_optional_axes(
12937 &remaining_plot_fixtures()
12938 .into_iter()
12939 .find(|(kind, _, _)| *kind == "pieChart")
12940 .unwrap()
12941 .1,
12942 false,
12943 )
12944 .as_bytes(),
12945 )
12946 .unwrap();
12947 let Plot::Pie { series, .. } = &pie.chart.plot_area.plots().unwrap()[0] else {
12948 panic!("expected pie plot");
12949 };
12950 let pie_slice = &super::pie_slices(30, None, series, chart_bounds()).unwrap()[0];
12951 assert_point_near(
12952 super::LabelAnchor::segment(pie_slice.anchor, pie_slice.base, pie_slice.end)
12953 .origin(Some(DataLabelPosition::OutsideEnd)),
12954 Point { x: 175.0, y: 70.0 },
12955 );
12956 let doughnut_slice = &super::pie_slices(30, Some(60), series, chart_bounds()).unwrap()[0];
12957 assert_point_near(
12958 super::LabelAnchor::segment(
12959 doughnut_slice.anchor,
12960 doughnut_slice.base,
12961 doughnut_slice.end,
12962 )
12963 .origin(Some(DataLabelPosition::InsideBase)),
12964 Point { x: 147.0, y: 70.0 },
12965 );
12966
12967 let short = super::LabelAnchor::segment(
12968 Point { x: 2.0, y: 0.0 },
12969 Point { x: 0.0, y: 0.0 },
12970 Point { x: 4.0, y: 0.0 },
12971 );
12972 assert_eq!(
12973 short.origin(Some(DataLabelPosition::InsideBase)),
12974 Point { x: 2.0, y: 0.0 }
12975 );
12976 assert_eq!(
12977 short.origin(Some(DataLabelPosition::InsideEnd)),
12978 Point { x: 2.0, y: 0.0 }
12979 );
12980 let thin_bounds = Rect {
12981 x: 0.0,
12982 y: 0.0,
12983 width: 80.0,
12984 height: 80.0,
12985 };
12986 let thin = &super::pie_slices(30, Some(90), series, thin_bounds).unwrap()[0];
12987 let thin = super::LabelAnchor::segment(thin.anchor, thin.base, thin.end);
12988 assert_point_near(
12989 thin.origin(Some(DataLabelPosition::InsideBase)),
12990 Point { x: 78.0, y: 40.0 },
12991 );
12992 assert_point_near(
12993 thin.origin(Some(DataLabelPosition::InsideEnd)),
12994 Point { x: 78.0, y: 40.0 },
12995 );
12996 }
12997
12998 #[test]
12999 fn degenerate_bar_and_radar_anchors_preserve_family_direction() {
13000 let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13001 let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
13002 let Plot::Bar { series, .. } = plot else {
13003 panic!("expected bar plot");
13004 };
13005 series[0].values.values = vec![80.0, 125.0];
13006 for (direction, orientation, boundary, outside) in [
13007 (
13008 BarDirection::Column,
13009 Orientation::MinMax,
13010 Point { x: 50.0, y: 140.0 },
13011 Point { x: 50.0, y: 135.0 },
13012 ),
13013 (
13014 BarDirection::Column,
13015 Orientation::MaxMin,
13016 Point { x: 50.0, y: 0.0 },
13017 Point { x: 50.0, y: 5.0 },
13018 ),
13019 (
13020 BarDirection::Bar,
13021 Orientation::MinMax,
13022 Point { x: 0.0, y: 35.0 },
13023 Point { x: 5.0, y: 35.0 },
13024 ),
13025 (
13026 BarDirection::Bar,
13027 Orientation::MaxMin,
13028 Point { x: 200.0, y: 35.0 },
13029 Point { x: 195.0, y: 35.0 },
13030 ),
13031 ] {
13032 let Plot::Bar {
13033 direction: plot_direction,
13034 ..
13035 } = plot
13036 else {
13037 panic!("expected bar plot");
13038 };
13039 *plot_direction = direction;
13040 let options = super::GeometryOptions {
13041 value: Some(super::AxisMapping {
13042 domain: Domain {
13043 min: 80.0,
13044 max: 100.0,
13045 },
13046 orientation,
13047 }),
13048 ..super::GeometryOptions::default()
13049 };
13050 let anchors =
13051 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options)
13052 .unwrap();
13053 let anchor = anchors[&(0, 0)];
13054 assert_point_near(anchor.center, boundary);
13055 assert_point_near(anchor.base, boundary);
13056 assert_point_near(anchor.end, boundary);
13057 assert_point_near(anchor.origin(Some(DataLabelPosition::InsideBase)), boundary);
13058 assert_point_near(anchor.origin(Some(DataLabelPosition::InsideEnd)), boundary);
13059 assert_point_near(anchor.origin(Some(DataLabelPosition::OutsideEnd)), outside);
13060 }
13061
13062 let radar_plot = format!(
13063 r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
13064 plot_series(0)
13065 );
13066 let mut radar = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
13067 let plot = &mut radar.chart.plot_area.plots_mut().unwrap()[0];
13068 let Plot::Radar { series, .. } = plot else {
13069 panic!("expected radar plot");
13070 };
13071 series[0].values.values = vec![0.0, 0.0, 0.0, 0.0];
13072 let anchors = super::plot_label_anchors(
13073 plot,
13074 chart_bounds(),
13075 DispBlanksAs::Gap,
13076 super::GeometryOptions::default(),
13077 )
13078 .unwrap();
13079 let center = Point { x: 100.0, y: 70.0 };
13080 for logical_index in 0..4 {
13081 assert_point_near(
13082 anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideBase)),
13083 center,
13084 );
13085 assert_point_near(
13086 anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideEnd)),
13087 center,
13088 );
13089 }
13090 assert_point_near(
13091 anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
13092 Point { x: 100.0, y: 65.0 },
13093 );
13094 assert_point_near(
13095 anchors[&(0, 1)].origin(Some(DataLabelPosition::OutsideEnd)),
13096 Point { x: 105.0, y: 70.0 },
13097 );
13098 assert_point_near(
13099 anchors[&(0, 2)].origin(Some(DataLabelPosition::OutsideEnd)),
13100 Point { x: 100.0, y: 75.0 },
13101 );
13102 assert_point_near(
13103 anchors[&(0, 3)].origin(Some(DataLabelPosition::OutsideEnd)),
13104 Point { x: 95.0, y: 70.0 },
13105 );
13106 }
13107
13108 #[test]
13109 fn labels_outside_explicit_axis_bounds_are_suppressed() {
13110 let mut chart =
13111 CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
13112 {
13113 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13114 let Plot::Line { series, .. } = plot else {
13115 panic!("expected line plot");
13116 };
13117 series[0].values.values = vec![25.0, 75.0];
13118 }
13119 let value_axis = chart
13120 .chart
13121 .plot_area
13122 .axes
13123 .iter_mut()
13124 .find(|axis| axis.kind == AxisKind::Value)
13125 .unwrap();
13126 value_axis.scaling.minimum = Some(50.0);
13127 value_axis.scaling.maximum = Some(100.0);
13128 value_axis.scaling.orientation = Orientation::MaxMin;
13129 let plot = &chart.chart.plot_area.plots().unwrap()[0];
13130 let axes = chart.chart.plot_area.axes().unwrap();
13131 let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
13132 let anchors =
13133 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
13134 assert!(!anchors.contains_key(&(0, 0)));
13135 assert_point_near(anchors[&(0, 1)].center, Point { x: 150.0, y: 70.0 });
13136
13137 let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13138 let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
13139 let Plot::Bar { series, .. } = plot else {
13140 panic!("expected bar plot");
13141 };
13142 series[0].values.values = vec![90.0, 125.0];
13143 let options = super::GeometryOptions {
13144 value: Some(super::AxisMapping {
13145 domain: Domain {
13146 min: 80.0,
13147 max: 100.0,
13148 },
13149 orientation: Orientation::MinMax,
13150 }),
13151 ..super::GeometryOptions::default()
13152 };
13153 let anchors =
13154 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
13155 assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
13156 assert_point_near(anchors[&(0, 0)].base, Point { x: 50.0, y: 140.0 });
13157 assert_point_near(anchors[&(0, 0)].end, Point { x: 50.0, y: 70.0 });
13158 assert_point_near(anchors[&(0, 0)].center, Point { x: 50.0, y: 105.0 });
13159 assert_point_near(
13160 anchors[&(0, 0)].origin(Some(DataLabelPosition::InsideBase)),
13161 Point { x: 50.0, y: 135.0 },
13162 );
13163 }
13164
13165 #[test]
13166 fn zero_bars_render_and_emit_requested_value_labels() {
13167 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13168 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13169 let Plot::Bar { series, .. } = plot else {
13170 panic!("expected bar plot");
13171 };
13172 series[0].values.values = vec![0.0, 0.0];
13173 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
13174 assert_eq!(path_commands(&geometry).len(), 2);
13175 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13176 render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13177 let plot = &chart.chart.plot_area.plots().unwrap()[0];
13178 let mut labels = Vec::new();
13179 super::render_data_labels(
13180 plot,
13181 chart_bounds(),
13182 DispBlanksAs::Gap,
13183 super::GeometryOptions::default(),
13184 &mut fonts,
13185 &mut labels,
13186 )
13187 .unwrap();
13188 let text: Vec<_> = labels
13189 .iter()
13190 .filter_map(|element| match element {
13191 PositionedElement::Text(run) => Some(run.text.as_str()),
13192 _ => None,
13193 })
13194 .collect();
13195 assert_eq!(text, vec!["0", "0"]);
13196 }
13197
13198 #[test]
13199 fn axes_gridlines_and_tick_marks_follow_model_state() {
13200 let chart = labelled_bar_chart();
13201 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13202 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13203 let ranks: Vec<_> = group
13204 .children
13205 .iter()
13206 .map(|element| match element {
13207 PositionedElement::Group(group) => {
13208 assert!(group.clip.is_some());
13209 1
13210 }
13211 PositionedElement::Path(path)
13212 if path
13213 .stroke
13214 .as_ref()
13215 .is_some_and(|stroke| stroke.width == 0.5) =>
13216 {
13217 0
13218 }
13219 PositionedElement::Path(path) if path.fill.is_some() => 4,
13220 PositionedElement::Path(path) => {
13221 let bounds = path.path.bounds().expect("axis or tick bounds");
13222 assert_eq!(path.stroke.as_ref().map(|stroke| stroke.width), Some(1.0));
13223 if bounds.width > 4.0 || bounds.height > 4.0 {
13224 2
13225 } else {
13226 3
13227 }
13228 }
13229 PositionedElement::Text(_) => 5,
13230 _ => panic!("unexpected labelled chart element"),
13231 })
13232 .collect();
13233 assert!(ranks.windows(2).all(|pair| pair[0] <= pair[1]), "{ranks:?}");
13234 for required in 0..=5 {
13235 assert!(
13236 ranks.contains(&required),
13237 "missing z-order rank {required}: {ranks:?}"
13238 );
13239 }
13240 assert_eq!(ranks.iter().filter(|rank| **rank == 1).count(), 1);
13241 assert_eq!(ranks.iter().filter(|rank| **rank == 2).count(), 2);
13242
13243 let mut deleted = chart.clone();
13244 for axis in &mut deleted.chart.plot_area.axes {
13245 axis.deleted = true;
13246 }
13247 let deleted_group = render_chart(&deleted.chart, chart_bounds(), &mut fonts).unwrap();
13248 assert_eq!(
13249 deleted_group
13250 .children
13251 .iter()
13252 .filter(|element| {
13253 matches!(
13254 element,
13255 PositionedElement::Path(path) if path.stroke.is_some()
13256 )
13257 })
13258 .count(),
13259 0
13260 );
13261 }
13262
13263 #[test]
13264 fn category_ticks_cover_unlabelled_and_sparse_logical_slots() {
13265 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13266 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
13267 panic!("expected bar plot");
13268 };
13269 series[0].categories = None;
13270 let plot = &chart.chart.plot_area.plots().unwrap()[0];
13271 let mut category_axis = chart
13272 .chart
13273 .plot_area
13274 .axes()
13275 .unwrap()
13276 .into_iter()
13277 .find(|axis| axis.kind == AxisKind::Category)
13278 .unwrap();
13279 category_axis.major_gridlines = Some(super::ChartLines::default());
13280 category_axis.major_tick_mark = TickMark::Cross;
13281 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13282 let annotations = super::render_axes_and_labels(
13283 plot,
13284 &[category_axis],
13285 chart_bounds(),
13286 DispBlanksAs::Gap,
13287 &mut fonts,
13288 )
13289 .unwrap();
13290 assert_eq!(annotations.gridlines.len(), 2);
13291 assert_eq!(annotations.ticks.len(), 2);
13292 assert!(annotations.labels.is_empty());
13293
13294 let sparse_plot = format!(
13295 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>"#,
13296 sparse_category_series()
13297 );
13298 let sparse = CT_ChartSpace::from_xml(chart_with_plot(&sparse_plot).as_bytes()).unwrap();
13299 let plot = &sparse.chart.plot_area.plots().unwrap()[0];
13300 let mut category_axis = sparse
13301 .chart
13302 .plot_area
13303 .axes()
13304 .unwrap()
13305 .into_iter()
13306 .find(|axis| axis.kind == AxisKind::Category)
13307 .unwrap();
13308 category_axis.major_gridlines = Some(super::ChartLines::default());
13309 category_axis.major_tick_mark = TickMark::Cross;
13310 let annotations = super::render_axes_and_labels(
13311 plot,
13312 &[category_axis],
13313 chart_bounds(),
13314 DispBlanksAs::Gap,
13315 &mut fonts,
13316 )
13317 .unwrap();
13318 assert_eq!(annotations.gridlines.len(), 3);
13319 assert_eq!(annotations.ticks.len(), 3);
13320 assert_eq!(annotations.labels.len(), 2);
13321
13322 let oversized_plot = sparse_plot.replacen(
13323 r#"<q:ptCount val="3"/>"#,
13324 r#"<q:ptCount val="4294967295"/>"#,
13325 1,
13326 );
13327 let oversized =
13328 CT_ChartSpace::from_xml(chart_with_plot(&oversized_plot).as_bytes()).unwrap();
13329 let plot = &oversized.chart.plot_area.plots().unwrap()[0];
13330 let category_axis = oversized
13331 .chart
13332 .plot_area
13333 .axes()
13334 .unwrap()
13335 .into_iter()
13336 .find(|axis| axis.kind == AxisKind::Category)
13337 .unwrap();
13338 let result = super::render_axes_and_labels(
13339 plot,
13340 &[category_axis],
13341 chart_bounds(),
13342 DispBlanksAs::Gap,
13343 &mut fonts,
13344 );
13345 let error = match result {
13346 Err(error) => error,
13347 Ok(_) => panic!("oversized category annotations rendered"),
13348 };
13349 assert!(
13350 error
13351 .to_string()
13352 .contains("logical category count 4294967295")
13353 );
13354 assert!(error.to_string().contains("exceeds 16384"));
13355 }
13356
13357 #[test]
13358 fn unsupported_numeric_category_formats_return_projection_errors() {
13359 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13360 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
13361 panic!("expected bar plot");
13362 };
13363 series[0].categories = Some(AxisData::Numeric(
13364 NumericData::new(
13365 "Sheet1!$A$2:$A$3".to_owned(),
13366 "General".to_owned(),
13367 vec![1.0, 2.0],
13368 )
13369 .unwrap(),
13370 ));
13371 let plot = &chart.chart.plot_area.plots().unwrap()[0];
13372 let mut category_axis = chart
13373 .chart
13374 .plot_area
13375 .axes()
13376 .unwrap()
13377 .into_iter()
13378 .find(|axis| axis.kind == AxisKind::Category)
13379 .unwrap();
13380 category_axis.number_format = Some(NumberFormat::new("0.00".to_owned(), false).unwrap());
13381 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13382 let annotations = super::render_axes_and_labels(
13383 plot,
13384 &[category_axis.clone()],
13385 chart_bounds(),
13386 DispBlanksAs::Gap,
13387 &mut fonts,
13388 )
13389 .unwrap();
13390 let text: Vec<_> = annotations
13391 .labels
13392 .iter()
13393 .filter_map(|element| match element {
13394 PositionedElement::Text(run) => Some(run.text.as_str()),
13395 _ => None,
13396 })
13397 .collect();
13398 assert_eq!(text, vec!["1.00", "2.00"]);
13399
13400 category_axis.number_format = Some(NumberFormat::new("#,##0".to_owned(), false).unwrap());
13401 let result = super::render_axes_and_labels(
13402 plot,
13403 &[category_axis],
13404 chart_bounds(),
13405 DispBlanksAs::Gap,
13406 &mut fonts,
13407 );
13408 let error = match result {
13409 Err(error) => error,
13410 Ok(_) => panic!("unsupported category-axis format rendered"),
13411 };
13412 assert!(
13413 error
13414 .to_string()
13415 .contains("c:numFmt/@formatCode projection")
13416 );
13417 assert!(error.to_string().contains("#,##0"));
13418 }
13419
13420 #[test]
13421 fn labels_and_legend_shape_with_deterministic_fonts() {
13422 let chart = labelled_bar_chart();
13423 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13424 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13425 let labels: Vec<_> = group
13426 .children
13427 .iter()
13428 .filter_map(|element| match element {
13429 PositionedElement::Text(run) => Some(run),
13430 _ => None,
13431 })
13432 .collect();
13433 for text in ["North", "South", "20", "Sales", "100"] {
13434 assert!(labels.iter().any(|run| run.text == text), "missing {text}");
13435 }
13436 let styled_tick = labels.iter().find(|run| run.text == "20").unwrap();
13437 assert_eq!(styled_tick.font_size, 12.0);
13438 assert!(styled_tick.bold);
13439 assert!(
13440 labels
13441 .iter()
13442 .all(|run| !run.glyph_ids.is_empty() && run.glyph_ids.len() == run.advances.len())
13443 );
13444 }
13445
13446 #[test]
13447 fn labelled_chart_raster_is_deterministic() {
13448 let raster = || {
13449 let chart = labelled_bar_chart();
13450 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13451 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13452 let layout = LayoutResult::new(
13453 vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
13454 fonts.all_font_data(),
13455 None,
13456 Vec::new(),
13457 );
13458 oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
13459 };
13460 assert_eq!(raster(), raster());
13461 }
13462
13463 fn labelled_bar_chart() -> CT_ChartSpace {
13464 let series = plot_series(0)
13465 .replace(
13466 "<q:cat>",
13467 "<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>",
13468 )
13469 .replace("<q:v>1</q:v>", "<q:v>0</q:v>")
13470 .replace("<q:v>2</q:v>", "<q:v>100</q:v>");
13471 let plot = format!(
13472 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>"#
13473 );
13474 let xml = chart_with_plot(&plot)
13475 .replacen(
13476 "<q:scaling/>",
13477 "<q:scaling><q:orientation val=\"minMax\"/></q:scaling>",
13478 1,
13479 )
13480 .replacen(
13481 "<q:scaling/>",
13482 "<q:scaling><q:max val=\"100\"/><q:min val=\"0\"/></q:scaling>",
13483 1,
13484 )
13485 .replace(
13486 "<q:axPos val=\"b\"/>",
13487 "<q:axPos val=\"b\"/><q:majorTickMark val=\"cross\"/>",
13488 )
13489 .replace(
13490 "<q:axPos val=\"l\"/>",
13491 "<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>",
13492 )
13493 .replace("</q:plotArea></q:chart>", "</q:plotArea><q:legend/></q:chart>");
13494 CT_ChartSpace::from_xml(xml.as_bytes()).unwrap()
13495 }
13496
13497 fn chart_bounds() -> Rect {
13498 Rect {
13499 x: 0.0,
13500 y: 0.0,
13501 width: 200.0,
13502 height: 140.0,
13503 }
13504 }
13505
13506 fn four_series_bar_chart() -> CT_ChartSpace {
13507 let series = (0..4).map(plot_series).collect::<String>();
13508 let plot = bar_plot("").replace(&plot_series(0), &series);
13509 CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
13510 }
13511
13512 fn seven_series_bar_chart() -> CT_ChartSpace {
13513 let series = (0..7).map(plot_series).collect::<String>();
13514 let plot = bar_plot("").replace(&plot_series(0), &series);
13515 CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
13516 }
13517
13518 fn path_colours(geometry: &ChartGeometry) -> Vec<Color> {
13519 let mut colours = geometry_children(geometry)
13520 .iter()
13521 .filter_map(|element| match element {
13522 PositionedElement::Path(path) => match path
13523 .fill
13524 .as_ref()
13525 .or_else(|| path.stroke.as_ref().map(|stroke| &stroke.paint))
13526 {
13527 Some(Paint::Solid(colour)) => Some(*colour),
13528 _ => None,
13529 },
13530 _ => None,
13531 })
13532 .collect::<Vec<_>>();
13533 colours.dedup_by(|left, right| left == right);
13534 colours
13535 }
13536
13537 fn resolved_layout_colour(colour: oxml_drawing::color::ResolvedColor) -> Color {
13538 Color {
13539 r: f64::from(colour.red) / 255.0,
13540 g: f64::from(colour.green) / 255.0,
13541 b: f64::from(colour.blue) / 255.0,
13542 a: f64::from(colour.alpha) / 255.0,
13543 }
13544 }
13545
13546 fn geometry_children(geometry: &ChartGeometry) -> &[PositionedElement] {
13547 let [PositionedElement::Group(group)] = geometry.elements.as_slice() else {
13548 panic!("chart geometry must contain one group");
13549 };
13550 &group.children
13551 }
13552
13553 fn path_commands(geometry: &ChartGeometry) -> Vec<&[PathCommand]> {
13554 geometry_children(geometry)
13555 .iter()
13556 .map(|element| {
13557 let PositionedElement::Path(path) = element else {
13558 panic!("chart geometry child must be a path");
13559 };
13560 path.path.commands.as_slice()
13561 })
13562 .collect()
13563 }
13564
13565 fn path_bounds(geometry: &ChartGeometry) -> Vec<Rect> {
13566 geometry_children(geometry)
13567 .iter()
13568 .map(|element| {
13569 let PositionedElement::Path(path) = element else {
13570 panic!("chart geometry child must be a path");
13571 };
13572 path.path.bounds().expect("chart path has bounds")
13573 })
13574 .collect()
13575 }
13576
13577 fn assert_rect_near(actual: Rect, expected: Rect) {
13578 assert_near(actual.x, expected.x);
13579 assert_near(actual.y, expected.y);
13580 assert_near(actual.width, expected.width);
13581 assert_near(actual.height, expected.height);
13582 }
13583
13584 fn assert_rects_near(actual: &[Rect], expected: &[Rect]) {
13585 assert_eq!(actual.len(), expected.len());
13586 for (actual, expected) in actual.iter().copied().zip(expected.iter().copied()) {
13587 assert_rect_near(actual, expected);
13588 }
13589 }
13590
13591 fn assert_path_points(commands: &[PathCommand], expected: &[Point], closed: bool) {
13592 let points: Vec<_> = commands
13593 .iter()
13594 .filter_map(|command| match command {
13595 PathCommand::MoveTo(point) | PathCommand::LineTo(point) => Some(*point),
13596 PathCommand::Close => None,
13597 PathCommand::CurveTo { .. } => panic!("expected polygonal path"),
13598 })
13599 .collect();
13600 assert_eq!(points.len(), expected.len());
13601 for (actual, expected) in points.into_iter().zip(expected.iter().copied()) {
13602 assert_point_near(actual, expected);
13603 }
13604 assert_eq!(commands.last() == Some(&PathCommand::Close), closed);
13605 }
13606
13607 fn assert_point_near(actual: Point, expected: Point) {
13608 assert_near(actual.x, expected.x);
13609 assert_near(actual.y, expected.y);
13610 }
13611
13612 fn assert_near(actual: f64, expected: f64) {
13613 assert!(
13614 (actual - expected).abs() < 1e-9,
13615 "expected {expected}, got {actual}"
13616 );
13617 }
13618
13619 #[test]
13620 fn remaining_v1_plots_round_trip_and_render() {
13621 for (kind, plot, axes) in remaining_plot_fixtures() {
13622 let xml = chart_with_optional_axes(&plot, axes);
13623 let parsed = CT_ChartSpace::from_xml(xml.as_bytes())
13624 .unwrap_or_else(|error| panic!("{kind}: parse failed: {error}"));
13625 assert_eq!(parsed.chart.plot_area.plots().unwrap().len(), 1);
13626 assert_eq!(
13627 parsed.chart.plot_area.axes().unwrap().len(),
13628 usize::from(axes) * 2
13629 );
13630 let written = parsed
13631 .to_xml()
13632 .unwrap_or_else(|error| panic!("{kind}: write failed: {error}"));
13633 assert_eq!(
13634 parsed,
13635 CT_ChartSpace::from_xml(&written)
13636 .unwrap_or_else(|error| panic!("{kind}: reparse failed: {error}"))
13637 );
13638 }
13639 if let Some(corpus) = require_or_skip_corpus() {
13640 verify_remaining_plot_viewer_gate(&corpus);
13641 }
13642 }
13643
13644 #[test]
13645 fn remaining_plot_families_write_fixed_prefixes_in_schema_order() {
13646 for (kind, plot, axes) in remaining_plot_fixtures() {
13647 let parsed =
13648 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
13649 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13650 assert!(written.contains(&format!("<c:{kind}")));
13651 assert!(!written.contains(&format!("<q:{kind}")));
13652 let series = written.find("<c:ser").unwrap();
13653 let labels = written.find("<c:dLbls").unwrap();
13654 assert!(series < labels, "{kind}: series must precede labels");
13655 if matches!(kind, "areaChart" | "scatterChart" | "radarChart") {
13656 let property = match kind {
13657 "areaChart" => "<c:grouping",
13658 "scatterChart" => "<c:scatterStyle",
13659 "radarChart" => "<c:radarStyle",
13660 _ => unreachable!(),
13661 };
13662 assert!(written.find(property).unwrap() < series);
13663 assert!(labels < written.find("<c:axId").unwrap());
13664 } else {
13665 assert!(labels < written.find("<c:firstSliceAng").unwrap());
13666 }
13667 if kind == "doughnutChart" {
13668 assert!(
13669 written.find("<c:firstSliceAng").unwrap()
13670 < written.find("<c:holeSize").unwrap()
13671 );
13672 }
13673 }
13674 }
13675
13676 #[test]
13677 fn scatter_series_map_numeric_categories_and_values_to_x_and_y() {
13678 let plot = remaining_plot_fixtures()
13679 .into_iter()
13680 .find(|(kind, _, _)| *kind == "scatterChart")
13681 .unwrap()
13682 .1;
13683 let mut parsed =
13684 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, true).as_bytes()).unwrap();
13685 let standalone_series = {
13686 let Plot::Scatter { series, .. } = &mut parsed.chart.plot_area.plots_mut().unwrap()[0]
13687 else {
13688 panic!("expected scatter plot");
13689 };
13690 let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
13691 panic!("expected numeric x values");
13692 };
13693 x_values.values[0] = 3.5;
13694 series[0].values.values[0] = 7.5;
13695 series[0].clone()
13696 };
13697 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13698 assert!(written.contains("<c:xVal>"));
13699 assert!(written.contains("<c:yVal>"));
13700 assert!(!written.contains("<c:cat>"));
13701 assert!(!written.contains("<c:val>"));
13702 assert!(written.contains("<c:v>3.5</c:v>"));
13703 assert!(written.contains("<c:v>7.5</c:v>"));
13704 let standalone = String::from_utf8(standalone_series.to_xml().unwrap()).unwrap();
13705 assert!(standalone.contains("<c:xVal>"));
13706 assert!(standalone.contains("<c:yVal>"));
13707 assert!(!standalone.contains("<c:cat>"));
13708 assert!(!standalone.contains("<c:val>"));
13709 }
13710
13711 #[test]
13712 fn malformed_remaining_plots_return_errors_without_panicking() {
13713 let series = plot_series(0);
13714 let scatter = numeric_plot_series(0);
13715 let opaque_bubble = series.replace(
13716 "</q:val></q:ser>",
13717 "</q:val><q:bubbleSize><x:opaque/></q:bubbleSize></q:ser>",
13718 );
13719 let cases = [
13720 r#"<q:pieChart/>"#.to_owned(),
13721 format!(r#"<q:pieChart>{series}<q:firstSliceAng val="361"/></q:pieChart>"#),
13722 format!(
13723 r#"<q:pieChart>{series}<q:firstSliceAng val="1"/><q:firstSliceAng val="2"/></q:pieChart>"#
13724 ),
13725 format!(r#"<q:doughnutChart>{series}<q:holeSize val="9"/></q:doughnutChart>"#),
13726 format!(
13727 r#"<q:doughnutChart>{series}<q:holeSize val="50"/><q:holeSize val="60"/></q:doughnutChart>"#
13728 ),
13729 format!(
13730 r#"<q:areaChart><q:grouping val="clustered"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
13731 ),
13732 format!(
13733 r#"<q:areaChart><q:grouping val="standard"/><q:grouping val="stacked"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
13734 ),
13735 format!(
13736 r#"<q:scatterChart><q:scatterStyle val="dots"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13737 ),
13738 format!(
13739 r#"<q:scatterChart><q:scatterStyle val="marker"/><q:scatterStyle val="line"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13740 ),
13741 format!(
13742 r#"<q:radarChart><q:radarStyle val="smooth"/>{series}<q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
13743 ),
13744 format!(
13745 r#"<q:radarChart><q:radarStyle val="marker"/>{series}<q:dLbls/><q:dLbls/><q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
13746 ),
13747 format!(r#"<q:pieChart>{series}<q:axId val="1"/></q:pieChart>"#),
13748 format!(
13749 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/></q:areaChart>"#
13750 ),
13751 r#"<q:areaChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:areaChart>"#.to_owned(),
13752 format!(
13753 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/><q:axId val="2"/><q:axId val="3"/></q:areaChart>"#
13754 ),
13755 format!(
13756 r#"<q:scatterChart><q:scatterStyle val="marker"/>{series}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13757 ),
13758 format!(
13759 r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13760 scatter
13761 .replace("<q:yVal>", "<q:val>")
13762 .replace("</q:yVal>", "</q:val>")
13763 ),
13764 format!(
13765 r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13766 scatter_without_x(0)
13767 ),
13768 format!(
13769 r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13770 scatter_without_y(0)
13771 ),
13772 format!(r#"<q:pieChart>{opaque_bubble}</q:pieChart>"#),
13773 ];
13774 for plot in cases {
13775 let xml = chart_with_optional_axes(&plot, false);
13776 let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
13777 assert!(result.is_ok(), "parser panicked for {plot}");
13778 assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
13779 }
13780
13781 let missing_axis = format!(
13782 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="7"/></q:areaChart>"#
13783 );
13784 let missing_axis =
13785 CT_ChartSpace::from_xml(chart_with_optional_axes(&missing_axis, true).as_bytes())
13786 .unwrap_err();
13787 assert!(missing_axis.to_string().contains("missing axis 7"));
13788
13789 let values =
13790 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
13791 let mut bubble_series = Series::new(0, 0, values.clone());
13792 bubble_series.bubble_size = Some(values);
13793 assert!(Plot::pie(vec![bubble_series]).is_err());
13794 }
13795
13796 #[test]
13797 fn unsupported_plot_families_and_children_remain_byte_preserved() {
13798 for raw in [
13799 r#"<q:pie3DChart x:keep="3d"><q:ser><x:opaque/></q:ser></q:pie3DChart>"#,
13800 r#"<q:ofPieChart x:keep="of"><x:opaque/></q:ofPieChart>"#,
13801 r#"<q:stockChart x:keep="stock"><x:opaque/></q:stockChart>"#,
13802 r#"<q:surfaceChart x:keep="surface"><x:opaque/></q:surfaceChart>"#,
13803 r#"<q:bubbleChart x:keep="bubble"><x:opaque/></q:bubbleChart>"#,
13804 ] {
13805 let parsed =
13806 CT_ChartSpace::from_xml(chart_with_optional_axes(raw, false).as_bytes()).unwrap();
13807 assert!(parsed.chart.plot_area.plots().is_err());
13808 let written = parsed.to_xml().unwrap();
13809 assert!(
13810 written
13811 .windows(raw.len())
13812 .any(|window| window == raw.as_bytes())
13813 );
13814 }
13815
13816 let pie = remaining_plot_fixtures()[0].1.clone();
13817 let preserved = r#"<!--point--><q:dPt x:id="1"><q:explosion val="7"/><x:tail/></q:dPt>"#;
13818 let plot = pie.replace("<q:dLbls>", &format!("{preserved}<q:dLbls>"));
13819 let parsed =
13820 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, false).as_bytes()).unwrap();
13821 let written = parsed.to_xml().unwrap();
13822 assert!(
13823 written
13824 .windows(preserved.len())
13825 .any(|window| window == preserved.as_bytes())
13826 );
13827
13828 for (kind, mut plot, axes) in [
13829 ("pieChart", remaining_plot_fixtures()[0].1.clone(), false),
13830 ("areaChart", remaining_plot_fixtures()[2].1.clone(), true),
13831 ] {
13832 plot = plot.replace(r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#, "");
13833 if kind == "pieChart" {
13834 plot = plot.replace(
13835 "</q:extLst></q:pieChart>",
13836 "</q:extLst><!--after-ext--></q:pieChart>",
13837 );
13838 } else {
13839 plot = plot.replace(
13840 "</q:dropLines><q:axId",
13841 "</q:dropLines><!--after-drop-lines--><q:axId",
13842 );
13843 }
13844 let mut parsed =
13845 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
13846 let labels =
13847 CT_DLbls::from_xml(format!(r#"<c:dLbls xmlns:c="{C_NS}"/>"#).as_bytes()).unwrap();
13848 match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
13849 Plot::Pie { data_labels, .. } | Plot::Area { data_labels, .. } => {
13850 *data_labels = Some(labels)
13851 }
13852 _ => panic!("expected pie or area"),
13853 }
13854 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13855 assert!(
13856 written.find("<c:dLbls").unwrap() < written.find("<q:extLst").unwrap(),
13857 "{kind}: inserted labels must precede the preserved extension"
13858 );
13859 if kind == "areaChart" {
13860 assert!(written.find("<c:dLbls").unwrap() < written.find("<q:dropLines").unwrap());
13861 assert!(
13862 written.find("<q:dropLines").unwrap()
13863 < written.find("<!--after-drop-lines-->").unwrap()
13864 );
13865 } else {
13866 assert!(
13867 written.find("<q:extLst").unwrap() < written.find("<!--after-ext-->").unwrap()
13868 );
13869 }
13870 }
13871 }
13872
13873 #[test]
13874 fn every_supported_corpus_plot_round_trips_structurally() {
13875 let Some(corpus) = require_or_skip_corpus() else {
13876 return;
13877 };
13878 verify_fetched_corpus(&corpus);
13879 let mut pie_count = 0usize;
13880 let mut bar_count = 0usize;
13881 let mut line_count = 0usize;
13882 for path in manifest_paths() {
13883 let package = OpcPackage::open(corpus.join(path)).unwrap();
13884 for (part, xml) in &package.parts {
13885 if !is_chart_part(part) {
13886 continue;
13887 }
13888 let parsed = CT_ChartSpace::from_xml(xml).unwrap();
13889 if let Ok(plots) = parsed.chart.plot_area.plots() {
13890 for plot in plots {
13891 match plot {
13892 Plot::Pie { .. } => pie_count += 1,
13893 Plot::Bar { .. } => bar_count += 1,
13894 Plot::Line { .. } => line_count += 1,
13895 _ => {}
13896 }
13897 }
13898 }
13899 let written = parsed.to_xml().unwrap();
13900 assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
13901 }
13902 }
13903 assert_eq!(pie_count, 1, "typed corpus pie coverage changed");
13904 assert_eq!(bar_count, 11, "typed corpus bar coverage changed");
13905 assert_eq!(line_count, 2, "typed corpus line coverage changed");
13906 }
13907
13908 fn standalone_axis(local: &str, children: &str) -> String {
13909 format!(
13910 r#"<c:{local} xmlns:c="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}">{children}</c:{local}>"#
13911 )
13912 }
13913
13914 fn minimal_axis(local: &str, id: &str, position: &str, cross_axis: &str) -> String {
13915 format!(
13916 r#"<c:{local}><c:axId val="{id}"/><c:scaling/><c:axPos val="{position}"/><c:crossAx val="{cross_axis}"/></c:{local}>"#
13917 )
13918 }
13919
13920 fn chart_with_axes(axes: &[String]) -> String {
13921 format!(
13922 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>"#,
13923 axes.join("")
13924 )
13925 }
13926
13927 #[test]
13928 fn data_labels_write_fixed_prefixes_in_schema_order() {
13929 let xml = format!(
13930 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>"#
13931 );
13932 let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
13933 assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0%");
13934 assert_eq!(parsed.position, Some(DataLabelPosition::OutsideEnd));
13935 assert_eq!(parsed.separator.as_deref(), Some(" / "));
13936 assert!(parsed.show_legend_key);
13937 assert!(parsed.show_value);
13938 assert!(!parsed.show_category_name);
13939 assert!(parsed.show_series_name);
13940 assert!(!parsed.show_percent);
13941 assert!(parsed.show_bubble_size);
13942
13943 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13944 assert!(written.starts_with(&format!(
13945 "<c:dLbls xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
13946 )));
13947 let mut cursor = 0usize;
13948 for tag in [
13949 "<q:dLbl",
13950 "<c:numFmt",
13951 "<q:spPr",
13952 "<q:txPr",
13953 "<c:dLblPos",
13954 "<c:showLegendKey",
13955 "<c:showVal",
13956 "<c:showCatName",
13957 "<c:showSerName",
13958 "<c:showPercent",
13959 "<c:showBubbleSize",
13960 "<c:separator",
13961 "<q:leaderLines",
13962 "<q:extLst",
13963 ] {
13964 let position = written[cursor..]
13965 .find(tag)
13966 .unwrap_or_else(|| panic!("missing ordered data-label tag {tag}"));
13967 cursor += position + tag.len();
13968 }
13969 assert_eq!(parsed, CT_DLbls::from_xml(written.as_bytes()).unwrap());
13970 }
13971
13972 #[test]
13973 fn percentage_formatted_label_renders_with_correct_text() {
13974 let Some(corpus) = require_or_skip_corpus() else {
13975 return;
13976 };
13977 verify_fetched_corpus(&corpus);
13978 assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
13979 assert_command_version("pdftotext", &["-v"], PDFTOTEXT_VERSION);
13980
13981 let source = corpus.join("bar-chart.pptx");
13982 let mut package = OpcPackage::open(&source).expect("open percentage-label source deck");
13983 let chart_part = "/ppt/charts/chart1.xml";
13984 let original_parts = package.parts.clone();
13985 let chart_xml = package
13986 .get_part(chart_part)
13987 .expect("bar chart part")
13988 .to_vec();
13989 let original_series = first_series_xml(&chart_xml).expect("bar chart series");
13990 let mut series = Series::from_xml(&original_series).expect("parse bar chart series");
13991 series.values.values.fill(0.25);
13992 let labels = CT_DLbls {
13993 number_format: Some(NumberFormat::new("0%".to_owned(), false).unwrap()),
13994 position: Some(DataLabelPosition::OutsideEnd),
13995 show_value: true,
13996 ..CT_DLbls::default()
13997 };
13998 series.data_labels = Some(labels);
13999 let replacement = series.to_xml().expect("serialize typed percentage labels");
14000 let candidate_chart = replace_once(&chart_xml, &original_series, &replacement);
14001 package.set_part(chart_part, candidate_chart);
14002 for (part, bytes) in &original_parts {
14003 if part == chart_part {
14004 assert_ne!(package.parts[part], *bytes);
14005 } else {
14006 assert_eq!(
14007 package.parts[part], *bytes,
14008 "unexpected changed part {part}"
14009 );
14010 }
14011 }
14012
14013 let temp_root =
14014 std::env::temp_dir().join(format!("oxml-chart-f123-label-gate-{}", std::process::id()));
14015 let profile = std::env::temp_dir().join(format!(
14016 "oxml-chart-f123-label-profile-{}",
14017 std::process::id()
14018 ));
14019 if temp_root.exists() {
14020 fs::remove_dir_all(&temp_root).expect("remove stale F-123 evidence");
14021 }
14022 if profile.exists() {
14023 fs::remove_dir_all(&profile).expect("remove stale F-123 profile");
14024 }
14025 fs::create_dir_all(&temp_root).expect("create F-123 evidence root");
14026 let unbound_candidate = temp_root.join("f123-percentage-label.pptx");
14027 package
14028 .save(&unbound_candidate)
14029 .expect("save percentage-label candidate");
14030 let candidate_sha = sha256(&unbound_candidate);
14031 let evidence_dir = temp_root.join(&candidate_sha);
14032 fs::create_dir(&evidence_dir).expect("create SHA-bound evidence directory");
14033 let candidate = evidence_dir.join("f123-percentage-label.pptx");
14034 fs::rename(&unbound_candidate, &candidate).expect("bind candidate to evidence SHA");
14035 assert_eq!(sha256(&candidate), candidate_sha);
14036
14037 let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
14038 let conversion = Command::new("soffice")
14039 .args([
14040 "--headless",
14041 &profile_argument,
14042 "--convert-to",
14043 "pdf:impress_pdf_Export",
14044 "--outdir",
14045 ])
14046 .arg(&evidence_dir)
14047 .arg(&candidate)
14048 .output()
14049 .expect("run pinned LibreOffice percentage-label gate");
14050 assert!(
14051 conversion.status.success(),
14052 "LibreOffice conversion failed: {}",
14053 String::from_utf8_lossy(&conversion.stderr)
14054 );
14055 let pdf = evidence_dir.join("f123-percentage-label.pdf");
14056 assert!(
14057 pdf.is_file(),
14058 "LibreOffice did not create {}",
14059 pdf.display()
14060 );
14061 let text_path = evidence_dir.join("f123-percentage-label.txt");
14062 let extraction = Command::new("pdftotext")
14063 .arg(&pdf)
14064 .arg(&text_path)
14065 .output()
14066 .expect("run pinned Poppler percentage-label extraction");
14067 assert!(
14068 extraction.status.success(),
14069 "pdftotext failed: {}",
14070 String::from_utf8_lossy(&extraction.stderr)
14071 );
14072 let extracted = fs::read_to_string(&text_path).expect("read percentage-label text");
14073 assert!(
14074 extracted.contains("25%"),
14075 "SHA-bound viewer text lacks 25%: {extracted:?}"
14076 );
14077 for artifact in [&candidate, &pdf, &text_path] {
14078 assert_eq!(
14079 artifact.parent(),
14080 Some(evidence_dir.as_path()),
14081 "evidence escaped candidate SHA"
14082 );
14083 }
14084 eprintln!("F-123 candidate/render/text evidence SHA-256 {candidate_sha}, extracted 25%");
14085 fs::remove_dir_all(&temp_root).expect("remove F-123 evidence");
14086 if profile.exists() {
14087 fs::remove_dir_all(&profile).expect("remove F-123 profile");
14088 }
14089 }
14090
14091 #[test]
14092 fn common_number_formats_project_cached_values_deterministically() {
14093 for (code, value, expected) in [
14094 ("General", 0.25, "0.25"),
14095 ("0", 12.6, "13"),
14096 ("0.0", 12.64, "12.6"),
14097 ("0.00", -12.645, "-12.64"),
14098 ("0%", 0.25, "25%"),
14099 ("0.0%", 0.256, "25.6%"),
14100 ("0.00%", 0.256, "25.60%"),
14101 ] {
14102 let format = NumberFormat::new(code.to_owned(), false).unwrap();
14103 assert_eq!(format.format_value(value).unwrap(), expected, "{code}");
14104 }
14105 }
14106
14107 #[test]
14108 fn malformed_data_labels_and_number_formats_return_errors_without_panicking() {
14109 let cases = [
14110 format!(
14111 r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="1"/><c:showVal val="0"/></c:dLbls>"#
14112 ),
14113 format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="yes"/></c:dLbls>"#),
14114 format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:dLblPos val="middle"/></c:dLbls>"#),
14115 format!(
14116 r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="" sourceLinked="1"/></c:dLbls>"#
14117 ),
14118 format!(
14119 r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="0" sourceLinked="maybe"/></c:dLbls>"#
14120 ),
14121 ];
14122 for xml in cases {
14123 let result = std::panic::catch_unwind(|| CT_DLbls::from_xml(xml.as_bytes()));
14124 assert!(result.is_ok(), "data-label parser panicked for {xml}");
14125 assert!(result.unwrap().is_err(), "malformed labels parsed: {xml}");
14126 }
14127
14128 let invalid_code = NumberFormat::new("bad\u{0}code".to_owned(), false);
14129 assert!(invalid_code.is_err());
14130 let unsupported = NumberFormat::new("#,##0".to_owned(), false).unwrap();
14131 assert!(unsupported.format_value(12.0).is_err());
14132 let general = NumberFormat::new("General".to_owned(), false).unwrap();
14133 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
14134 assert!(general.format_value(value).is_err());
14135 }
14136
14137 let invalid_separator = CT_DLbls {
14138 separator: Some("bad\u{0}separator".to_owned()),
14139 ..CT_DLbls::default()
14140 };
14141 assert!(invalid_separator.to_xml().is_err());
14142 }
14143
14144 #[test]
14145 fn data_labels_preserve_point_overrides_and_extensions_byte_for_byte() {
14146 let xml = format!(
14147 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>"#
14148 );
14149 let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
14150 let written = parsed.to_xml().unwrap();
14151 for raw in [
14152 br#"<!--before-->"#.as_slice(),
14153 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(),
14154 br#"<?after-point?>"#.as_slice(),
14155 br#"<q:spPr><x:collection-shape/></q:spPr>"#.as_slice(),
14156 br#"<q:txPr><x:collection-text/></q:txPr>"#.as_slice(),
14157 br#"<q:showLeaderLines val="1"/>"#.as_slice(),
14158 br#"<q:leaderLines><q:spPr><x:leader-shape/></q:spPr></q:leaderLines>"#.as_slice(),
14159 br#"<q:extLst><q:ext uri="collection"><x:tail/></q:ext></q:extLst>"#.as_slice(),
14160 br#"<!--after-->"#.as_slice(),
14161 ] {
14162 assert!(
14163 written.windows(raw.len()).any(|window| window == raw),
14164 "preserved data-label bytes changed: {}",
14165 String::from_utf8_lossy(raw)
14166 );
14167 }
14168 assert_eq!(parsed, CT_DLbls::from_xml(&written).unwrap());
14169 }
14170
14171 #[test]
14172 fn every_corpus_data_label_collection_round_trips_structurally() {
14173 let Some(corpus) = require_or_skip_corpus() else {
14174 return;
14175 };
14176 verify_fetched_corpus(&corpus);
14177 let mut label_collections = 0usize;
14178 let mut axis_number_formats = 0usize;
14179 for path in manifest_paths() {
14180 let package = OpcPackage::open(corpus.join(path))
14181 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14182 for (part, xml) in &package.parts {
14183 if !is_chart_part(part) {
14184 continue;
14185 }
14186 let chart = CT_ChartSpace::from_xml(xml)
14187 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14188 for series in chart.chart.plot_area.series().unwrap_or_else(|error| {
14189 panic!("{path} {part}: series projection failed: {error}")
14190 }) {
14191 if let Some(labels) = series.data_labels {
14192 let written = labels.to_xml().unwrap_or_else(|error| {
14193 panic!("{path} {part}: label write failed: {error}")
14194 });
14195 let reparsed = CT_DLbls::from_xml(&written).unwrap_or_else(|error| {
14196 panic!("{path} {part}: written labels parse failed: {error}")
14197 });
14198 assert_eq!(labels, reparsed, "{path} {part}: labels changed");
14199 label_collections += 1;
14200 }
14201 }
14202 for axis in chart.chart.plot_area.axes().unwrap_or_else(|error| {
14203 panic!("{path} {part}: axis projection failed: {error}")
14204 }) {
14205 if axis.number_format.is_some() {
14206 let written = axis.to_xml().unwrap_or_else(|error| {
14207 panic!("{path} {part}: axis write failed: {error}")
14208 });
14209 let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
14210 panic!("{path} {part}: written axis parse failed: {error}")
14211 });
14212 assert_eq!(axis, reparsed, "{path} {part}: axis changed");
14213 axis_number_formats += 1;
14214 }
14215 }
14216 }
14217 }
14218 assert!(label_collections > 0, "the pinned corpus has no c:dLbls");
14219 assert!(
14220 axis_number_formats > 0,
14221 "the pinned corpus has no axis c:numFmt"
14222 );
14223 eprintln!(
14224 "ChartML data-label corpus gate checked {label_collections} collections and {axis_number_formats} axis number formats"
14225 );
14226 }
14227
14228 fn full_axis(local: &str, position: &str) -> String {
14229 format!(
14230 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}>"#
14231 )
14232 }
14233
14234 #[test]
14235 fn axis_id_pairs_are_reciprocal() {
14236 assert_eq!(
14237 AxisId::new(i64::from(i32::MIN)).unwrap().value(),
14238 i64::from(i32::MIN)
14239 );
14240 assert_eq!(
14241 AxisId::new(i64::from(u32::MAX)).unwrap().value(),
14242 i64::from(u32::MAX)
14243 );
14244 assert!(AxisId::new(i64::from(i32::MIN) - 1).is_err());
14245 assert!(AxisId::new(i64::from(u32::MAX) + 1).is_err());
14246
14247 let valid = chart_with_axes(&[
14248 minimal_axis("catAx", "-1884094432", "b", "-1884097184"),
14249 minimal_axis("valAx", "-1884097184", "l", "-1884094432"),
14250 ]);
14251 let axes = CT_ChartSpace::from_xml(valid.as_bytes())
14252 .unwrap()
14253 .chart
14254 .plot_area
14255 .axes()
14256 .unwrap();
14257 assert_eq!(axes.len(), 2);
14258 assert_eq!(axes[0].id.value(), -1_884_094_432);
14259 assert_eq!(axes[0].cross_axis, axes[1].id);
14260 assert_eq!(axes[1].cross_axis, axes[0].id);
14261
14262 let invalid_sets = [
14263 vec![
14264 minimal_axis("catAx", "1", "b", "2"),
14265 minimal_axis("valAx", "1", "l", "2"),
14266 ],
14267 vec![minimal_axis("catAx", "1", "b", "1")],
14268 vec![minimal_axis("catAx", "1", "b", "2")],
14269 vec![
14270 minimal_axis("catAx", "1", "b", "2"),
14271 minimal_axis("valAx", "2", "l", "3"),
14272 minimal_axis("serAx", "3", "r", "2"),
14273 ],
14274 ];
14275 for axes in invalid_sets {
14276 let chart = CT_ChartSpace::from_xml(chart_with_axes(&axes).as_bytes()).unwrap();
14277 assert!(chart.chart.plot_area.axes().is_err());
14278 }
14279
14280 let empty = CT_ChartSpace::from_xml(chart_with_axes(&[]).as_bytes()).unwrap();
14281 assert!(empty.chart.plot_area.axes().unwrap().is_empty());
14282 }
14283
14284 #[test]
14285 fn all_axis_forms_write_fixed_prefixes_in_schema_order() {
14286 for (local, kind, position) in [
14287 ("catAx", AxisKind::Category, AxisPosition::Bottom),
14288 ("valAx", AxisKind::Value, AxisPosition::Left),
14289 ("dateAx", AxisKind::Date, AxisPosition::Top),
14290 ("serAx", AxisKind::Series, AxisPosition::Right),
14291 ] {
14292 let xml = full_axis(local, position.as_str());
14293 let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
14294 assert_eq!(parsed.kind, kind);
14295 assert_eq!(parsed.id.value(), -1_125_255_920);
14296 assert_eq!(parsed.scaling.log_base, Some(10.0));
14297 assert_eq!(parsed.scaling.orientation, Orientation::MaxMin);
14298 assert_eq!(parsed.scaling.maximum, Some(100.0));
14299 assert_eq!(parsed.scaling.minimum, Some(-10.0));
14300 assert!(parsed.deleted);
14301 assert_eq!(parsed.position, position);
14302 assert!(parsed.major_gridlines.is_some());
14303 assert!(parsed.minor_gridlines.is_some());
14304 assert!(parsed.title.is_some());
14305 assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0");
14306 assert_eq!(parsed.major_tick_mark, TickMark::Cross);
14307 assert_eq!(parsed.minor_tick_mark, TickMark::Inside);
14308 assert_eq!(parsed.tick_label_position, TickLabelPosition::High);
14309 assert!(parsed.sp_pr.is_some());
14310 assert!(parsed.tx_pr.is_some());
14311 assert_eq!(parsed.cross_axis.value(), -1_004_303_696);
14312
14313 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14314 assert!(written.starts_with(&format!(
14315 "<c:{local} xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
14316 )));
14317 assert!(written.contains("<c:axId val=\"-1125255920\""));
14318 let mut cursor = 0usize;
14319 for tag in [
14320 "<c:axId",
14321 "<c:scaling",
14322 "<c:delete",
14323 "<c:axPos",
14324 "<c:majorGridlines",
14325 "<c:minorGridlines",
14326 "<c:title",
14327 "<c:numFmt",
14328 "<c:majorTickMark",
14329 "<c:minorTickMark",
14330 "<c:tickLblPos",
14331 "<c:spPr",
14332 "<c:txPr",
14333 "<c:crossAx",
14334 ] {
14335 let position = written[cursor..]
14336 .find(tag)
14337 .unwrap_or_else(|| panic!("missing ordered axis tag {tag}"));
14338 cursor += position + tag.len();
14339 }
14340 assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
14341 }
14342
14343 let mut constructed = Axis::new(
14344 AxisKind::Value,
14345 AxisId::new(1).unwrap(),
14346 AxisPosition::Left,
14347 AxisId::new(2).unwrap(),
14348 );
14349 assert_eq!(
14350 constructed,
14351 Axis::from_xml(&constructed.to_xml().unwrap()).unwrap()
14352 );
14353 constructed.kind = AxisKind::Category;
14354 constructed.id = AxisId::new(3).unwrap();
14355 constructed.cross_axis = AxisId::new(4).unwrap();
14356 constructed.scaling.minimum = Some(0.0);
14357 constructed.scaling.maximum = Some(10.0);
14358 let written = constructed.to_xml().unwrap();
14359 assert!(written.starts_with(b"<c:catAx"));
14360 assert_eq!(constructed, Axis::from_xml(&written).unwrap());
14361 }
14362
14363 #[test]
14364 fn axis_equality_normalizes_ids_without_losing_lexical_preservation() {
14365 let padded = Axis::from_xml(
14366 standalone_axis(
14367 "catAx",
14368 "<c:axId val=\"01\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"002\"/>",
14369 )
14370 .as_bytes(),
14371 )
14372 .unwrap();
14373 let normalized = Axis::from_xml(
14374 standalone_axis(
14375 "catAx",
14376 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14377 )
14378 .as_bytes(),
14379 )
14380 .unwrap();
14381 assert_eq!(padded, normalized);
14382
14383 let preserved = String::from_utf8(padded.to_xml().unwrap()).unwrap();
14384 assert!(preserved.contains("<c:axId val=\"01\""));
14385 assert!(preserved.contains("<c:crossAx val=\"002\""));
14386
14387 let mut mutated = padded;
14388 mutated.id = AxisId::new(3).unwrap();
14389 mutated.cross_axis = AxisId::new(4).unwrap();
14390 let rewritten = String::from_utf8(mutated.to_xml().unwrap()).unwrap();
14391 assert!(rewritten.contains("<c:axId val=\"3\""));
14392 assert!(rewritten.contains("<c:crossAx val=\"4\""));
14393 assert!(!rewritten.contains("val=\"01\""));
14394 assert!(!rewritten.contains("val=\"002\""));
14395 }
14396
14397 #[test]
14398 fn malformed_axis_values_return_errors_without_panicking() {
14399 let cases = [
14400 standalone_axis(
14401 "catAx",
14402 "<c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14403 ),
14404 standalone_axis(
14405 "catAx",
14406 "<c:axId val=\"1\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14407 ),
14408 standalone_axis(
14409 "catAx",
14410 "<c:axId val=\"1\"/><c:scaling/><c:crossAx val=\"2\"/>",
14411 ),
14412 standalone_axis(
14413 "catAx",
14414 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/>",
14415 ),
14416 standalone_axis(
14417 "catAx",
14418 "<c:axId val=\"-2147483649\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14419 ),
14420 standalone_axis(
14421 "catAx",
14422 "<c:axId val=\"4294967296\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14423 ),
14424 standalone_axis(
14425 "catAx",
14426 "<c:axId val=\"1\"/><c:scaling><c:logBase val=\"1\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14427 ),
14428 standalone_axis(
14429 "catAx",
14430 "<c:axId val=\"1\"/><c:scaling><c:max val=\"NaN\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14431 ),
14432 standalone_axis(
14433 "catAx",
14434 "<c:axId val=\"1\"/><c:scaling><c:max val=\"1\"/><c:min val=\"2\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14435 ),
14436 standalone_axis(
14437 "catAx",
14438 "<c:axId val=\"1\"/><c:scaling><c:orientation val=\"sideways\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14439 ),
14440 standalone_axis(
14441 "catAx",
14442 "<c:axId val=\"1\"/><c:scaling/><c:delete val=\"yes\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14443 ),
14444 standalone_axis(
14445 "catAx",
14446 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"middle\"/><c:crossAx val=\"2\"/>",
14447 ),
14448 standalone_axis(
14449 "catAx",
14450 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:majorTickMark val=\"near\"/><c:crossAx val=\"2\"/>",
14451 ),
14452 standalone_axis(
14453 "catAx",
14454 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:tickLblPos val=\"middle\"/><c:crossAx val=\"2\"/>",
14455 ),
14456 standalone_axis(
14457 "catAx",
14458 "<c:axId val=\"1\"/><c:axId val=\"2\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14459 ),
14460 standalone_axis(
14461 "catAx",
14462 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt sourceLinked=\"1\"/><c:crossAx val=\"2\"/>",
14463 ),
14464 standalone_axis(
14465 "catAx",
14466 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt formatCode=\"0\" sourceLinked=\"maybe\"/><c:crossAx val=\"2\"/>",
14467 ),
14468 ];
14469 for xml in cases {
14470 let result = std::panic::catch_unwind(|| Axis::from_xml(xml.as_bytes()));
14471 assert!(result.is_ok(), "axis parser panicked for {xml}");
14472 assert!(result.unwrap().is_err(), "malformed axis parsed: {xml}");
14473 }
14474
14475 let mut axis = Axis::new(
14476 AxisKind::Value,
14477 AxisId::new(1).unwrap(),
14478 AxisPosition::Left,
14479 AxisId::new(2).unwrap(),
14480 );
14481 axis.scaling.maximum = Some(f64::INFINITY);
14482 assert!(axis.to_xml().is_err());
14483 }
14484
14485 #[test]
14486 fn axes_preserve_unmodelled_children_byte_for_byte() {
14487 let xml = format!(
14488 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>"#
14489 );
14490 let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
14491 let mut relabelled = parsed.clone();
14492 relabelled.kind = AxisKind::Value;
14493 assert!(
14494 relabelled.to_xml().is_err(),
14495 "a parsed category axis must not retain its tail under a value-axis root"
14496 );
14497 let written = parsed.to_xml().unwrap();
14498 for raw in [
14499 br#"<!--before-->"#.as_slice(),
14500 br#"<?after-id?>"#.as_slice(),
14501 br#"<!--scale-->"#.as_slice(),
14502 br#"<q:extLst><q:ext uri="scale"><x:data/></q:ext></q:extLst>"#.as_slice(),
14503 br#"<x:grid/>"#.as_slice(),
14504 br#"<x:title/>"#.as_slice(),
14505 br#"<q:crosses val="autoZero"/>"#.as_slice(),
14506 br#"<q:auto val="1"><x:auto/></q:auto>"#.as_slice(),
14507 br#"<q:extLst><q:ext uri="axis"><x:tail/></q:ext></q:extLst>"#.as_slice(),
14508 br#"<!--after-->"#.as_slice(),
14509 ] {
14510 assert!(
14511 written.windows(raw.len()).any(|window| window == raw),
14512 "preserved axis bytes changed: {}",
14513 String::from_utf8_lossy(raw)
14514 );
14515 }
14516 let written = String::from_utf8(written).unwrap();
14517 assert!(written.starts_with("<c:catAx"));
14518 assert!(written.contains("x:keep=\"axis\""));
14519 assert!(written.contains("x:keep=\"id\""));
14520 assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
14521 }
14522
14523 #[test]
14524 fn every_corpus_axis_round_trips_structurally() {
14525 let Some(corpus) = require_or_skip_corpus() else {
14526 return;
14527 };
14528 verify_fetched_corpus(&corpus);
14529 let mut axis_count = 0usize;
14530 let mut chart_parts = 0usize;
14531 let mut kinds = HashSet::new();
14532 for path in manifest_paths() {
14533 let package = OpcPackage::open(corpus.join(path))
14534 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14535 for (part, xml) in &package.parts {
14536 if !is_chart_part(part) {
14537 continue;
14538 }
14539 let chart = CT_ChartSpace::from_xml(xml)
14540 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14541 let axes = chart.chart.plot_area.axes().unwrap_or_else(|error| {
14542 panic!("{path} {part}: axis projection failed: {error}")
14543 });
14544 for axis in axes {
14545 kinds.insert(axis.kind);
14546 let written = axis.to_xml().unwrap_or_else(|error| {
14547 panic!("{path} {part}: axis write failed: {error}")
14548 });
14549 let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
14550 panic!("{path} {part}: written axis parse failed: {error}")
14551 });
14552 assert_eq!(axis, reparsed, "{path} {part}: axis model changed");
14553 axis_count += 1;
14554 }
14555 chart_parts += 1;
14556 }
14557 }
14558 assert!(
14559 axis_count > 0,
14560 "the pinned corpus contained no ChartML axes"
14561 );
14562 assert!(kinds.contains(&AxisKind::Category));
14563 assert!(kinds.contains(&AxisKind::Value));
14564
14565 for kind in ["dateAx", "serAx"] {
14566 let parsed = Axis::from_xml(
14567 standalone_axis(
14568 kind,
14569 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14570 )
14571 .as_bytes(),
14572 )
14573 .unwrap();
14574 assert_eq!(parsed, Axis::from_xml(&parsed.to_xml().unwrap()).unwrap());
14575 }
14576 eprintln!(
14577 "ChartML axis corpus gate checked {axis_count} axes across {chart_parts} chart parts"
14578 );
14579 }
14580
14581 #[test]
14582 fn series_formula_and_cache_are_consistent_with_one_source() {
14583 let values = NumericData::new(
14584 "'Sales 24'!$B$2:$B$4".to_owned(),
14585 "0.0".to_owned(),
14586 vec![4.25, 8.5, 17.0],
14587 )
14588 .unwrap();
14589 let mut series = Series::new(3, 1, values);
14590 series.categories = Some(AxisData::String(
14591 StringRef::new(
14592 "'Sales 24'!$A$2:$A$4".to_owned(),
14593 vec!["North".to_owned(), "South".to_owned(), "West".to_owned()],
14594 )
14595 .unwrap(),
14596 ));
14597
14598 let written = String::from_utf8(series.to_xml().unwrap()).unwrap();
14599 assert!(written.contains("<c:f>'Sales 24'!$B$2:$B$4</c:f>"));
14600 assert!(written.contains("<c:formatCode>0.0</c:formatCode>"));
14601 assert_eq!(written.matches("<c:ptCount val=\"3\"").count(), 2);
14602 for (index, value) in ["4.25", "8.5", "17"].iter().enumerate() {
14603 assert!(written.contains(&format!("<c:pt idx=\"{index}\"><c:v>{value}</c:v></c:pt>")));
14604 }
14605 let reparsed = Series::from_xml(written.as_bytes()).unwrap();
14606 assert_eq!(reparsed.index, series.index);
14607 assert_eq!(reparsed.order, series.order);
14608 match (&reparsed.categories, &series.categories) {
14609 (Some(AxisData::String(left)), Some(AxisData::String(right))) => {
14610 assert_eq!(left.formula, right.formula);
14611 assert_eq!(left.values, right.values);
14612 }
14613 _ => panic!("expected string categories"),
14614 }
14615 assert_eq!(reparsed.values.formula, series.values.formula);
14616 assert_eq!(reparsed.values.format_code, series.values.format_code);
14617 assert_eq!(reparsed.values.values, series.values.values);
14618 }
14619
14620 #[test]
14621 fn string_and_numeric_references_write_fixed_prefixes_in_schema_order() {
14622 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>"#;
14623 let string_ref = StringRef::from_xml(string_xml).unwrap();
14624 let written = String::from_utf8(string_ref.to_xml().unwrap()).unwrap();
14625 assert!(written.starts_with("<c:strRef xmlns:c="));
14626 assert!(written.find("<c:f>").unwrap() < written.find("<c:strCache>").unwrap());
14627 assert!(written.find("<c:ptCount").unwrap() < written.find("<c:pt idx=").unwrap());
14628
14629 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>"#;
14630 let numeric = NumericData::from_xml(numeric_xml).unwrap();
14631 let written = String::from_utf8(numeric.to_xml().unwrap()).unwrap();
14632 let positions: Vec<_> = [
14633 "<c:f>",
14634 "<c:numCache>",
14635 "<c:formatCode>",
14636 "<c:ptCount",
14637 "<c:pt idx=",
14638 ]
14639 .iter()
14640 .map(|tag| written.find(tag).unwrap())
14641 .collect();
14642 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14643 assert_eq!(numeric, NumericData::from_xml(written.as_bytes()).unwrap());
14644 }
14645
14646 #[test]
14647 fn mixed_chartml_aliases_resolve_by_namespace_uri() {
14648 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>"#;
14649 let parsed = Series::from_xml(xml).unwrap();
14650 assert_eq!(parsed.index, 4);
14651 assert_eq!(parsed.order, 2);
14652 assert_eq!(parsed.values.values, vec![1.5, 2.5]);
14653 assert!(matches!(parsed.categories, Some(AxisData::String(_))));
14654 assert_eq!(parsed, Series::from_xml(&parsed.to_xml().unwrap()).unwrap());
14655 }
14656
14657 #[test]
14658 fn plot_area_series_ignores_inherited_foreign_plot_aliases() {
14659 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>"#;
14660 let chart = CT_ChartSpace::from_xml(xml).unwrap();
14661 let series = chart.chart.plot_area.series().unwrap();
14662 assert_eq!(series.len(), 1);
14663 assert_eq!(series[0].index, 1);
14664 let written = String::from_utf8(series[0].to_xml().unwrap()).unwrap();
14665 assert!(
14666 written.contains(r#"xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart""#)
14667 );
14668 assert!(written.contains(r#"xmlns:x="urn:producer""#));
14669 assert!(written.contains("<q:marker><x:data/></q:marker>"));
14670 assert_eq!(series[0], Series::from_xml(written.as_bytes()).unwrap());
14671
14672 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>"#;
14673 let chart = CT_ChartSpace::from_xml(conflicting).unwrap();
14674 assert!(chart.chart.plot_area.series().is_err());
14675 }
14676
14677 #[test]
14678 fn malformed_series_and_cache_values_return_errors_without_panicking() {
14679 let cases: &[&[u8]] = &[
14680 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>"#,
14681 br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/></c:ser>"#,
14682 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>"#,
14683 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>"#,
14684 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>"#,
14685 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>"#,
14686 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>"#,
14687 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>"#,
14688 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>"#,
14689 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>"#,
14690 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>"#,
14691 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>"#,
14692 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>"#,
14693 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>"#,
14694 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>"#,
14695 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>"#,
14696 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>"#,
14697 ];
14698 for xml in cases {
14699 let result = std::panic::catch_unwind(|| Series::from_xml(xml));
14700 assert!(result.is_ok(), "series parser panicked");
14701 assert!(
14702 result.unwrap().is_err(),
14703 "malformed series parsed: {}",
14704 String::from_utf8_lossy(xml)
14705 );
14706 }
14707 assert!(
14708 NumericData::new(
14709 "S!$A$1".to_owned(),
14710 "General".to_owned(),
14711 vec![f64::INFINITY]
14712 )
14713 .is_err()
14714 );
14715 }
14716
14717 #[test]
14718 fn chart_text_rejects_xml_forbidden_characters_and_escapes_metacharacters() {
14719 assert!(StringRef::new("S!$A$1\u{1}".to_owned(), Vec::new()).is_err());
14720 assert!(StringRef::new("S!$A$1".to_owned(), vec!["invalid\u{1}".to_owned()]).is_err());
14721 assert!(
14722 NumericData::new("S!$A$1\u{1}".to_owned(), "General".to_owned(), Vec::new()).is_err()
14723 );
14724 assert!(
14725 NumericData::new("S!$A$1".to_owned(), "General\u{1}".to_owned(), Vec::new()).is_err()
14726 );
14727
14728 let mut strings = StringRef::new("S!$A$1".to_owned(), vec!["valid".to_owned()]).unwrap();
14729 strings.formula = "invalid\u{1}".to_owned();
14730 assert!(strings.to_xml().is_err());
14731 strings.formula = "S!$A$1".to_owned();
14732 strings.values[0] = "invalid\u{1}".to_owned();
14733 assert!(strings.to_xml().is_err());
14734
14735 let mut numbers =
14736 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap();
14737 numbers.formula = "invalid\u{1}".to_owned();
14738 assert!(numbers.to_xml().is_err());
14739 numbers.formula = "S!$A$1".to_owned();
14740 numbers.format_code = "invalid\u{1}".to_owned();
14741 assert!(numbers.to_xml().is_err());
14742
14743 let strings =
14744 StringRef::new("S!$A$1&".to_owned(), vec!["North < West & East".to_owned()]).unwrap();
14745 let written = String::from_utf8(strings.to_xml().unwrap()).unwrap();
14746 assert!(written.contains("S!$A$1&"));
14747 assert!(written.contains("North < West & East"));
14748 }
14749
14750 #[test]
14751 fn series_preserves_unmodelled_children_byte_for_byte() {
14752 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>"#;
14753 let parsed = Series::from_xml(xml).unwrap();
14754 let written = parsed.to_xml().unwrap();
14755 for raw in [
14756 br#"<!--before-->"#.as_slice(),
14757 br#"<x:between/>"#.as_slice(),
14758 br#"<q:dPt x:id="one"><q:idx val="0"/></q:dPt>"#.as_slice(),
14759 br#"<q:trendline x:id="trend"/>"#.as_slice(),
14760 br#"<q:extLst><q:ext uri="keep"><x:data/></q:ext></q:extLst>"#.as_slice(),
14761 br#"<!--point-->"#.as_slice(),
14762 br#"<x:cacheExt/>"#.as_slice(),
14763 ] {
14764 assert!(
14765 written.windows(raw.len()).any(|window| window == raw),
14766 "preserved series bytes changed: {}",
14767 String::from_utf8_lossy(raw)
14768 );
14769 }
14770 let written = String::from_utf8(written.clone()).unwrap();
14771 assert!(written.contains(r#"x:keep="series""#));
14772 assert!(written.contains(r#"x:keep="formula""#));
14773 assert!(written.contains("<c:dLbls"));
14774 assert!(written.contains(r#"x:id="labels""#));
14775 assert!(parsed.data_labels.is_some());
14776 assert_eq!(parsed, Series::from_xml(written.as_bytes()).unwrap());
14777 }
14778
14779 #[test]
14780 fn public_series_edits_do_not_duplicate_or_drop_preserved_payloads() {
14781 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>"#;
14782 let parsed = Series::from_xml(opaque_xml).unwrap();
14783
14784 let mut edited = parsed.clone();
14785 edited.name = Some(StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap());
14786 assert!(edited.to_xml().is_err());
14787
14788 let mut edited = parsed.clone();
14789 edited.categories = Some(AxisData::Numeric(
14790 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap(),
14791 ));
14792 assert!(edited.to_xml().is_err());
14793
14794 let mut edited = parsed;
14795 edited.bubble_size =
14796 Some(NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
14797 assert!(edited.to_xml().is_err());
14798
14799 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>"#;
14800 let parsed = Series::from_xml(cache_xml).unwrap();
14801 let tail = "<c:extLst><c:ext uri=\"keep\"><x:data/></c:ext></c:extLst>";
14802
14803 let mut shortened = parsed.clone();
14804 shortened.values.values.truncate(1);
14805 let written = String::from_utf8(shortened.to_xml().unwrap()).unwrap();
14806 assert!(!written.contains("<c:pt idx=\"1\""));
14807 assert!(written.find("<c:pt idx=\"0\"").unwrap() < written.find(tail).unwrap());
14808 assert!(Series::from_xml(written.as_bytes()).is_ok());
14809
14810 let mut grown = parsed;
14811 grown.values.values.push(3.0);
14812 let written = String::from_utf8(grown.to_xml().unwrap()).unwrap();
14813 assert!(written.find("<c:pt idx=\"2\"").unwrap() < written.find(tail).unwrap());
14814 assert!(Series::from_xml(written.as_bytes()).is_ok());
14815
14816 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>"#;
14817 let mut edited = Series::from_xml(ordered_xml).unwrap();
14818 edited.name = Some(StringRef::new("S!$B$1".to_owned(), Vec::new()).unwrap());
14819 edited.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
14820 edited.categories = Some(AxisData::String(
14821 StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap(),
14822 ));
14823 edited.bubble_size =
14824 Some(NumericData::new("S!$C$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
14825 let written = String::from_utf8(edited.to_xml().unwrap()).unwrap();
14826 let positions: Vec<_> = [
14827 "<c:tx>",
14828 "<c:spPr",
14829 "<c:marker>",
14830 "<c:cat>",
14831 "<c:val>",
14832 "<c:bubbleSize>",
14833 "<c:extLst>",
14834 ]
14835 .iter()
14836 .map(|tag| written.find(tag).unwrap())
14837 .collect();
14838 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14839 assert!(Series::from_xml(written.as_bytes()).is_ok());
14840 }
14841
14842 #[test]
14843 fn every_corpus_series_round_trips_structurally() {
14844 let Some(corpus) = require_or_skip_corpus() else {
14845 return;
14846 };
14847 verify_fetched_corpus(&corpus);
14848 let mut series_count = 0usize;
14849 let mut chart_parts = 0usize;
14850 for path in manifest_paths() {
14851 let package = OpcPackage::open(corpus.join(path))
14852 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14853 for (part, xml) in &package.parts {
14854 if !is_chart_part(part) {
14855 continue;
14856 }
14857 let chart = CT_ChartSpace::from_xml(xml)
14858 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14859 let parsed_series =
14860 chart.chart.plot_area.series().unwrap_or_else(|error| {
14861 panic!("{path} {part}: series parse failed: {error}")
14862 });
14863 for series in parsed_series {
14864 let written = series.to_xml().unwrap_or_else(|error| {
14865 panic!("{path} {part}: series write failed: {error}")
14866 });
14867 let reparsed = Series::from_xml(&written).unwrap_or_else(|error| {
14868 panic!("{path} {part}: written series parse failed: {error}")
14869 });
14870 assert_eq!(series, reparsed, "{path} {part}: series model changed");
14871 series_count += 1;
14872 }
14873 chart_parts += 1;
14874 }
14875 }
14876 assert!(
14877 series_count > 0,
14878 "the pinned corpus contained no supported series"
14879 );
14880 eprintln!(
14881 "ChartML series corpus gate checked {series_count} series across {chart_parts} chart parts"
14882 );
14883 }
14884
14885 #[test]
14886 fn chart_space_reads_aliases_and_writes_fixed_prefixes_in_schema_order() {
14887 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>"#;
14888 let parsed = CT_ChartSpace::from_xml(xml).unwrap();
14889 assert!(!parsed.chart.auto_title_deleted);
14890 assert!(!parsed.chart.plot_vis_only);
14891 assert_eq!(parsed.chart.disp_blanks_as, DispBlanksAs::Span);
14892 assert!(parsed.chart.title.is_some());
14893 assert!(parsed.chart.legend.is_some());
14894 assert!(parsed.sp_pr.is_some());
14895 assert!(parsed.tx_pr.is_some());
14896
14897 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14898 assert!(written.starts_with(
14899 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""#
14900 ));
14901 for tag in [
14902 "<c:chart>",
14903 "<c:title>",
14904 "<c:plotArea>",
14905 "<c:legend>",
14906 "<c:plotVisOnly",
14907 "<c:dispBlanksAs",
14908 "<c:spPr>",
14909 "<c:txPr>",
14910 ] {
14911 assert!(written.contains(tag), "missing fixed-prefix tag {tag}");
14912 }
14913 let positions: Vec<_> = [
14914 "<c:title>",
14915 "<c:plotArea>",
14916 "<c:legend>",
14917 "<c:plotVisOnly",
14918 "<c:dispBlanksAs",
14919 "<c:spPr>",
14920 "<c:txPr>",
14921 ]
14922 .iter()
14923 .map(|tag| written.find(tag).unwrap())
14924 .collect();
14925 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14926 assert!(written.contains("<a:solidFill><a:srgbClr"));
14927 assert!(written.contains("<a:bodyPr/><a:p/>"));
14928 assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
14929 }
14930
14931 #[test]
14932 fn public_core_edits_keep_preserved_children_in_their_schema_slots() {
14933 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>"#;
14934 let mut parsed = CT_ChartSpace::from_xml(xml).unwrap();
14935 parsed.chart.auto_title_deleted = true;
14936 parsed.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
14937 parsed.tx_pr =
14938 Some(CT_TextBody::from_xml(br#"<a:txBody><a:bodyPr/><a:p/></a:txBody>"#).unwrap());
14939 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14940 let positions: Vec<_> = [
14941 "<c:autoTitleDeleted",
14942 "<c:pivotFmts>",
14943 "<c:plotArea",
14944 "<c:spPr",
14945 "<c:txPr",
14946 "<c:externalData",
14947 ]
14948 .iter()
14949 .map(|tag| written.find(tag).unwrap())
14950 .collect();
14951 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14952 assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
14953 }
14954
14955 #[test]
14956 fn malformed_core_chart_values_return_errors_without_panicking() {
14957 let cases: &[&[u8]] = &[
14958 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"/>"#,
14959 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart/></c:chartSpace>"#,
14960 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:plotArea/></c:chart></c:chartSpace>"#,
14961 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:autoTitleDeleted val="yes"/><c:plotArea/></c:chart></c:chartSpace>"#,
14962 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:dispBlanksAs val="empty"/></c:chart></c:chartSpace>"#,
14963 br#"<c:chartSpace xmlns:c="urn:wrong"><c:chart><c:plotArea/></c:chart></c:chartSpace>"#,
14964 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:foreign"><x:chart><x:plotArea/></x:chart></c:chartSpace>"#,
14965 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>"#,
14966 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:title/><c:title/><c:plotArea/></c:chart></c:chartSpace>"#,
14967 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>"#,
14968 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>"#,
14969 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>"#,
14970 ];
14971 for xml in cases {
14972 let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml));
14973 assert!(result.is_ok(), "ChartML parser panicked");
14974 assert!(
14975 result.unwrap().is_err(),
14976 "malformed ChartML parsed: {}",
14977 String::from_utf8_lossy(xml)
14978 );
14979 }
14980 }
14981
14982 #[test]
14983 fn point_label_overrides_render_without_changing_preserved_xml() {
14984 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>"#;
14985 let plot = bar_plot("").replace(
14986 r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
14987 &format!(
14988 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>"#
14989 ),
14990 );
14991 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
14992 let plot = &chart.chart.plot_area.plots().unwrap()[0];
14993 let Plot::Bar { data_labels, .. } = plot else {
14994 panic!("expected bar plot");
14995 };
14996 let labels = data_labels.as_ref().unwrap();
14997 let written = labels.to_xml().unwrap();
14998 assert!(
14999 written
15000 .windows(raw_overrides.len())
15001 .any(|window| window == raw_overrides.as_bytes())
15002 );
15003 let overrides = super::point_label_overrides(labels).unwrap();
15004 assert_eq!(overrides.keys().copied().collect::<Vec<_>>(), vec![1]);
15005
15006 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
15007 let mut rendered = Vec::new();
15008 super::render_data_labels(
15009 plot,
15010 chart_bounds(),
15011 DispBlanksAs::Gap,
15012 super::GeometryOptions::default(),
15013 &mut fonts,
15014 &mut rendered,
15015 )
15016 .unwrap();
15017 let labels: Vec<_> = rendered
15018 .iter()
15019 .filter_map(|element| match element {
15020 PositionedElement::Text(run) => Some(run),
15021 _ => None,
15022 })
15023 .collect();
15024 assert_eq!(labels.len(), 2);
15025 assert_eq!(labels[0].text, "North, 1");
15026 assert_eq!(labels[1].text, "South / 2.0");
15027 assert_point_near(labels[0].origin, Point { x: 50.0, y: 105.0 });
15028 assert_point_near(labels[1].origin, Point { x: 158.0, y: 70.0 });
15029 assert_eq!(
15030 CT_DLbls::from_xml(&written).unwrap().to_xml().unwrap(),
15031 written
15032 );
15033 }
15034
15035 #[test]
15036 fn malformed_point_label_overrides_return_contextual_errors() {
15037 let cases = [
15038 r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:showVal val="1"/></q:dLbl></q:dLbls>"#,
15039 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>"#,
15040 r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="point"/></q:dLbl></q:dLbls>"#,
15041 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>"#,
15042 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>"#,
15043 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>"#,
15044 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>"#,
15045 ];
15046 for xml in cases {
15047 let labels = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
15048 let result = std::panic::catch_unwind(|| super::point_label_overrides(&labels));
15049 assert!(result.is_ok(), "point-label parser panicked for {xml}");
15050 assert!(
15051 result.unwrap().is_err(),
15052 "malformed point labels parsed: {xml}"
15053 );
15054 }
15055
15056 let foreign = format!(
15057 r#"<q:dLbls xmlns:q="{C_NS}" xmlns:x="urn:foreign"><x:dLbl><x:idx val="0"/></x:dLbl></q:dLbls>"#
15058 );
15059 let labels = CT_DLbls::from_xml(foreign.as_bytes()).unwrap();
15060 assert!(super::point_label_overrides(&labels).unwrap().is_empty());
15061 }
15062
15063 #[test]
15064 fn percentage_label_aggregation_rejects_nonfinite_totals() {
15065 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
15066 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
15067 let Plot::Bar { series, .. } = plot else {
15068 panic!("expected bar plot");
15069 };
15070 series[0].values.values = vec![f64::MAX, f64::MAX];
15071 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
15072 super::render_data_labels(
15073 plot,
15074 chart_bounds(),
15075 DispBlanksAs::Gap,
15076 super::GeometryOptions::default(),
15077 &mut fonts,
15078 &mut Vec::new(),
15079 )
15080 .expect("value-only labels do not need a percentage total");
15081 let Plot::Bar { data_labels, .. } = plot else {
15082 panic!("expected bar plot");
15083 };
15084 data_labels.as_mut().unwrap().show_percent = true;
15085 let error = super::render_data_labels(
15086 plot,
15087 chart_bounds(),
15088 DispBlanksAs::Gap,
15089 super::GeometryOptions::default(),
15090 &mut fonts,
15091 &mut Vec::new(),
15092 )
15093 .unwrap_err();
15094 assert!(error.to_string().contains("data-label percentage total"));
15095
15096 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>"#;
15097 let plot_xml = bar_plot("").replace(
15098 r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
15099 &format!(
15100 r#"<q:dLbls>{overrides}<q:showVal val="1"/><q:showPercent val="1"/></q:dLbls>"#
15101 ),
15102 );
15103 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
15104 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
15105 let Plot::Bar { series, .. } = plot else {
15106 panic!("expected bar plot");
15107 };
15108 series[0].values.values = vec![f64::MAX, f64::MAX];
15109 super::render_data_labels(
15110 plot,
15111 chart_bounds(),
15112 DispBlanksAs::Gap,
15113 super::GeometryOptions::default(),
15114 &mut fonts,
15115 &mut Vec::new(),
15116 )
15117 .expect("effective point overrides disable percentage aggregation");
15118 }
15119
15120 #[test]
15121 fn percentage_labels_use_effective_number_format_precision() {
15122 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
15123 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
15124 let Plot::Bar {
15125 series,
15126 data_labels,
15127 ..
15128 } = plot
15129 else {
15130 panic!("expected bar plot");
15131 };
15132 series[0].values.values = vec![1.0, 2.0];
15133 let labels = data_labels.as_mut().unwrap();
15134 labels.show_value = false;
15135 labels.show_percent = true;
15136 labels.number_format = Some(NumberFormat::new("0.00%".to_owned(), false).unwrap());
15137 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
15138 let mut rendered = Vec::new();
15139 super::render_data_labels(
15140 plot,
15141 chart_bounds(),
15142 DispBlanksAs::Gap,
15143 super::GeometryOptions::default(),
15144 &mut fonts,
15145 &mut rendered,
15146 )
15147 .unwrap();
15148 let text: Vec<_> = rendered
15149 .iter()
15150 .filter_map(|element| match element {
15151 PositionedElement::Text(run) => Some(run.text.as_str()),
15152 _ => None,
15153 })
15154 .collect();
15155 assert_eq!(text, vec!["33.33%", "66.67%"]);
15156
15157 let point_override = r#"<q:dLbl><q:idx val="0"/><q:numFmt formatCode="0.00%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbl>"#;
15158 let plot_xml = bar_plot("").replace(
15159 r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
15160 &format!(
15161 r#"<q:dLbls>{point_override}<q:numFmt formatCode="0%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbls>"#
15162 ),
15163 );
15164 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
15165 let plot = &chart.chart.plot_area.plots().unwrap()[0];
15166 let mut rendered = Vec::new();
15167 super::render_data_labels(
15168 plot,
15169 chart_bounds(),
15170 DispBlanksAs::Gap,
15171 super::GeometryOptions::default(),
15172 &mut fonts,
15173 &mut rendered,
15174 )
15175 .unwrap();
15176 let text: Vec<_> = rendered
15177 .iter()
15178 .filter_map(|element| match element {
15179 PositionedElement::Text(run) => Some(run.text.as_str()),
15180 _ => None,
15181 })
15182 .collect();
15183 assert_eq!(text, vec!["33.33%", "67%"]);
15184 }
15185
15186 #[test]
15187 fn core_chart_shells_preserve_unmodelled_children_byte_for_byte() {
15188 let preserved = [
15189 br#"<x:root x:id="1"><x:nested>one & two</x:nested><!--root--></x:root>"#
15190 .as_slice(),
15191 br#"<x:title x:id="2"><x:nested><![CDATA[title]]></x:nested></x:title>"#.as_slice(),
15192 br#"<x:plot x:id="3"><x:nested>plot</x:nested></x:plot>"#.as_slice(),
15193 br#"<x:legend x:id="4"><x:nested>legend</x:nested></x:legend>"#.as_slice(),
15194 br#"<x:extension x:id="5"><x:nested>extension</x:nested></x:extension>"#.as_slice(),
15195 br#"<!--space-comment-->"#.as_slice(),
15196 br#"<?space processing?>"#.as_slice(),
15197 br#"<!--chart-comment-->"#.as_slice(),
15198 br#"<?title processing?>"#.as_slice(),
15199 br#"<!--flag-comment-->"#.as_slice(),
15200 br#"<?flag processing?>"#.as_slice(),
15201 br#"<x:producerShape/>"#.as_slice(),
15202 br#"<x:producerText/>"#.as_slice(),
15203 ];
15204 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>"#;
15205 let parsed = CT_ChartSpace::from_xml(xml).unwrap();
15206 let written = parsed.to_xml().unwrap();
15207 for raw in preserved {
15208 assert!(
15209 written.windows(raw.len()).any(|window| window == raw),
15210 "preserved subtree changed: {}",
15211 String::from_utf8_lossy(raw)
15212 );
15213 }
15214 let written_text = String::from_utf8(written.clone()).unwrap();
15215 assert!(written_text.contains(r#"x:keep="one & two""#));
15216 assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
15217 }
15218
15219 #[test]
15220 fn every_corpus_chart_part_round_trips_structurally() {
15221 let Some(corpus) = require_or_skip_corpus() else {
15222 return;
15223 };
15224 verify_fetched_corpus(&corpus);
15225 let mut chart_parts = 0usize;
15226 let mut decks_with_charts = HashSet::new();
15227 for path in manifest_paths() {
15228 let package = OpcPackage::open(corpus.join(path))
15229 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
15230 for (part, xml) in &package.parts {
15231 if !is_chart_part(part) {
15232 continue;
15233 }
15234 let parsed = CT_ChartSpace::from_xml(xml)
15235 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
15236 let written = parsed
15237 .to_xml()
15238 .unwrap_or_else(|error| panic!("{path} {part}: write failed: {error}"));
15239 let original_preserved = preserved_chartml_regions(xml)
15240 .unwrap_or_else(|error| panic!("{path} {part}: original scan failed: {error}"));
15241 let written_preserved = preserved_chartml_regions(&written)
15242 .unwrap_or_else(|error| panic!("{path} {part}: written scan failed: {error}"));
15243 assert_eq!(
15244 original_preserved.len(),
15245 written_preserved.len(),
15246 "{path} {part}: preserved ChartML region count changed"
15247 );
15248 for (index, (original, written)) in original_preserved
15249 .iter()
15250 .zip(&written_preserved)
15251 .enumerate()
15252 {
15253 assert_eq!(
15254 original.parent_path, written.parent_path,
15255 "{path} {part}: preserved region {index} parent changed"
15256 );
15257 assert_eq!(
15258 original.boundary, written.boundary,
15259 "{path} {part}: preserved region {index} boundary changed"
15260 );
15261 assert_eq!(
15262 original.sibling_order, written.sibling_order,
15263 "{path} {part}: preserved region {index} order changed"
15264 );
15265 assert_eq!(
15266 original.xml, written.xml,
15267 "{path} {part}: preserved region {index} bytes changed"
15268 );
15269 }
15270 let reparsed = CT_ChartSpace::from_xml(&written)
15271 .unwrap_or_else(|error| panic!("{path} {part}: written parse failed: {error}"));
15272 assert_eq!(parsed, reparsed, "{path} {part}: core model changed");
15273 chart_parts += 1;
15274 decks_with_charts.insert(path);
15275 }
15276 }
15277 assert!(
15278 chart_parts > 0,
15279 "the pinned corpus contained no chart parts"
15280 );
15281 eprintln!(
15282 "ChartML corpus gate checked {chart_parts} chart parts across {} decks",
15283 decks_with_charts.len()
15284 );
15285 }
15286
15287 #[derive(Debug, Eq, PartialEq)]
15288 struct PreservedChartRegion {
15289 parent_path: Vec<Vec<u8>>,
15290 boundary: usize,
15291 sibling_order: usize,
15292 xml: Vec<u8>,
15293 }
15294
15295 struct PreservationFrame {
15296 local_name: Vec<u8>,
15297 boundary: usize,
15298 next_sibling: usize,
15299 }
15300
15301 fn preserved_chartml_regions(xml: &[u8]) -> Result<Vec<PreservedChartRegion>, String> {
15302 let mut reader = Reader::from_reader(xml);
15303 let mut buffer = Vec::new();
15304 let mut parents: Vec<PreservationFrame> = Vec::new();
15305 let mut preserved = Vec::new();
15306 loop {
15307 match reader.read_event_into(&mut buffer) {
15308 Ok(Event::Start(element)) => {
15309 let child = local_name(element.name().as_ref()).to_vec();
15310 if parents.is_empty() && child == b"chartSpace" {
15311 parents.push(PreservationFrame {
15312 local_name: child,
15313 boundary: 0,
15314 next_sibling: 0,
15315 });
15316 } else if let Some(parent) = parents.last()
15317 && is_opaque_chartml_child(&parent.local_name, &child)
15318 {
15319 let boundary =
15320 preserved_child_boundary(&parent.local_name, &child, parent.boundary);
15321 let xml = capture_element(&mut reader, &element)
15322 .map_err(|error| error.to_string())?;
15323 record_preserved_region(&mut parents, &mut preserved, boundary, xml);
15324 advance_preservation_boundary(&mut parents, &child);
15325 } else {
15326 advance_preservation_boundary(&mut parents, &child);
15327 if is_chartml_preservation_parent(&child) {
15328 parents.push(PreservationFrame {
15329 local_name: child,
15330 boundary: 0,
15331 next_sibling: 0,
15332 });
15333 } else {
15334 capture_element(&mut reader, &element)
15335 .map_err(|error| error.to_string())?;
15336 }
15337 }
15338 }
15339 Ok(Event::Empty(element)) => {
15340 let name = element.name();
15341 let child = local_name(name.as_ref()).to_vec();
15342 if let Some(parent) = parents.last()
15343 && is_opaque_chartml_child(&parent.local_name, &child)
15344 {
15345 let boundary =
15346 preserved_child_boundary(&parent.local_name, &child, parent.boundary);
15347 let xml =
15348 capture_empty_element(&element).map_err(|error| error.to_string())?;
15349 record_preserved_region(&mut parents, &mut preserved, boundary, xml);
15350 }
15351 advance_preservation_boundary(&mut parents, &child);
15352 }
15353 Ok(
15354 event @ (Event::Comment(_)
15355 | Event::PI(_)
15356 | Event::CData(_)
15357 | Event::GeneralRef(_)),
15358 ) if !parents.is_empty() => {
15359 let boundary = parents.last().expect("preservation parent").boundary;
15360 let xml = capture_event(event).map_err(|error| error.to_string())?;
15361 record_preserved_region(&mut parents, &mut preserved, boundary, xml);
15362 }
15363 Ok(Event::End(_)) => {
15364 parents.pop();
15365 }
15366 Ok(Event::Eof) => break,
15367 Ok(_) => {}
15368 Err(error) => return Err(error.to_string()),
15369 }
15370 buffer.clear();
15371 }
15372 Ok(preserved)
15373 }
15374
15375 fn record_preserved_region(
15376 parents: &mut [PreservationFrame],
15377 preserved: &mut Vec<PreservedChartRegion>,
15378 boundary: usize,
15379 xml: Vec<u8>,
15380 ) {
15381 let sibling_order = parents.last().expect("preservation parent").next_sibling;
15382 parents
15383 .last_mut()
15384 .expect("preservation parent")
15385 .next_sibling += 1;
15386 preserved.push(PreservedChartRegion {
15387 parent_path: parents
15388 .iter()
15389 .map(|parent| parent.local_name.clone())
15390 .collect(),
15391 boundary,
15392 sibling_order,
15393 xml,
15394 });
15395 }
15396
15397 fn advance_preservation_boundary(parents: &mut [PreservationFrame], child: &[u8]) {
15398 let Some(parent) = parents.last_mut() else {
15399 return;
15400 };
15401 parent.boundary = parent
15402 .boundary
15403 .max(preserved_boundary_after(&parent.local_name, child));
15404 }
15405
15406 fn preserved_child_boundary(parent: &[u8], child: &[u8], current: usize) -> usize {
15407 match parent {
15408 b"chartSpace" => match child {
15409 b"date1904" => 0,
15410 b"lang" => 1,
15411 b"roundedCorners" => 2,
15412 b"style" => 3,
15413 b"clrMapOvr" => 4,
15414 b"pivotSource" => 5,
15415 b"protection" => 6,
15416 b"externalData" => 10,
15417 b"printSettings" => 11,
15418 b"userShapes" => 12,
15419 b"extLst" => 13,
15420 _ => current,
15421 },
15422 b"chart" => match child {
15423 b"pivotFmts" => 2,
15424 b"view3D" => 3,
15425 b"floor" => 4,
15426 b"sideWall" => 5,
15427 b"backWall" => 6,
15428 b"showDLblsOverMax" | b"extLst" => 11,
15429 _ => current,
15430 },
15431 _ => current,
15432 }
15433 }
15434
15435 fn preserved_boundary_after(parent: &[u8], child: &[u8]) -> usize {
15436 match parent {
15437 b"chartSpace" => match child {
15438 b"date1904" => 1,
15439 b"lang" => 2,
15440 b"roundedCorners" => 3,
15441 b"style" => 4,
15442 b"clrMapOvr" => 5,
15443 b"pivotSource" => 6,
15444 b"protection" => 7,
15445 b"chart" => 8,
15446 b"spPr" => 9,
15447 b"txPr" => 10,
15448 b"externalData" => 11,
15449 b"printSettings" => 12,
15450 b"userShapes" => 13,
15451 b"extLst" => 14,
15452 _ => 0,
15453 },
15454 b"chart" => match child {
15455 b"title" => 1,
15456 b"autoTitleDeleted" => 2,
15457 b"pivotFmts" => 3,
15458 b"view3D" => 4,
15459 b"floor" => 5,
15460 b"sideWall" => 6,
15461 b"backWall" => 7,
15462 b"plotArea" => 8,
15463 b"legend" => 9,
15464 b"plotVisOnly" => 10,
15465 b"dispBlanksAs" => 11,
15466 b"showDLblsOverMax" | b"extLst" => 11,
15467 _ => 0,
15468 },
15469 b"pieChart" => match child {
15470 b"ser" => 1,
15471 b"dLbls" => 2,
15472 b"firstSliceAng" => 3,
15473 _ => 0,
15474 },
15475 b"doughnutChart" => match child {
15476 b"ser" => 1,
15477 b"dLbls" => 2,
15478 b"firstSliceAng" => 3,
15479 b"holeSize" => 4,
15480 _ => 0,
15481 },
15482 b"areaChart" => match child {
15483 b"grouping" => 1,
15484 b"ser" => 2,
15485 b"dLbls" => 3,
15486 b"axId" => 4,
15487 _ => 0,
15488 },
15489 b"scatterChart" | b"radarChart" => match child {
15490 b"scatterStyle" | b"radarStyle" => 1,
15491 b"ser" => 2,
15492 b"dLbls" => 3,
15493 b"axId" => 4,
15494 _ => 0,
15495 },
15496 _ => 0,
15497 }
15498 }
15499
15500 fn is_opaque_chartml_child(parent: &[u8], child: &[u8]) -> bool {
15501 match parent {
15502 b"chartSpace" => !matches!(child, b"chart" | b"spPr" | b"txPr"),
15503 b"chart" => !matches!(
15504 child,
15505 b"title"
15506 | b"autoTitleDeleted"
15507 | b"plotArea"
15508 | b"legend"
15509 | b"plotVisOnly"
15510 | b"dispBlanksAs"
15511 ),
15512 b"plotArea" => !matches!(
15513 child,
15514 b"barChart"
15515 | b"lineChart"
15516 | b"pieChart"
15517 | b"doughnutChart"
15518 | b"areaChart"
15519 | b"scatterChart"
15520 | b"radarChart"
15521 | b"catAx"
15522 | b"valAx"
15523 | b"dateAx"
15524 | b"serAx"
15525 ),
15526 b"pieChart" => !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng"),
15527 b"doughnutChart" => {
15528 !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng" | b"holeSize")
15529 }
15530 b"areaChart" => !matches!(child, b"grouping" | b"ser" | b"dLbls" | b"axId"),
15531 b"scatterChart" => !matches!(child, b"scatterStyle" | b"ser" | b"dLbls" | b"axId"),
15532 b"radarChart" => !matches!(child, b"radarStyle" | b"ser" | b"dLbls" | b"axId"),
15533 b"title" | b"legend" => true,
15534 _ => false,
15535 }
15536 }
15537
15538 fn is_chartml_preservation_parent(parent: &[u8]) -> bool {
15539 matches!(
15540 parent,
15541 b"chartSpace"
15542 | b"chart"
15543 | b"title"
15544 | b"plotArea"
15545 | b"pieChart"
15546 | b"doughnutChart"
15547 | b"areaChart"
15548 | b"scatterChart"
15549 | b"radarChart"
15550 | b"legend"
15551 | b"autoTitleDeleted"
15552 | b"plotVisOnly"
15553 | b"dispBlanksAs"
15554 )
15555 }
15556
15557 #[test]
15558 fn oxml_chart_is_an_explicit_publication_candidate() {
15559 let manifest = include_str!("../Cargo.toml");
15560 assert!(manifest.contains("name = \"oxml-chart\""));
15561 assert!(manifest.contains("version = \"0.14.0\""));
15562 assert!(manifest.contains("publish = true"));
15563 for dependency in [
15564 "oxml-core.workspace",
15565 "oxml-drawing.workspace",
15566 "quick-xml.workspace",
15567 ] {
15568 assert!(manifest.contains(dependency));
15569 }
15570 assert!(!manifest.contains("rpptx.workspace"));
15571 assert!(!manifest.contains("rdocx.workspace"));
15572 }
15573
15574 fn first_series_xml(xml: &[u8]) -> Option<Vec<u8>> {
15575 let mut reader = Reader::from_reader(xml);
15576 let mut buffer = Vec::new();
15577 loop {
15578 match reader.read_event_into(&mut buffer).ok()? {
15579 Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
15580 return capture_element(&mut reader, &element).ok();
15581 }
15582 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
15583 return capture_empty_element(&element).ok();
15584 }
15585 Event::Eof => return None,
15586 _ => {}
15587 }
15588 buffer.clear();
15589 }
15590 }
15591
15592 fn replace_once(haystack: &[u8], needle: &[u8], replacement: &[u8]) -> Vec<u8> {
15593 let offset = haystack
15594 .windows(needle.len())
15595 .position(|window| window == needle)
15596 .expect("serialized source series occurs in chart part");
15597 assert!(
15598 !haystack[offset + needle.len()..]
15599 .windows(needle.len())
15600 .any(|window| window == needle),
15601 "source series unexpectedly occurs more than once"
15602 );
15603 let mut result = Vec::with_capacity(haystack.len() - needle.len() + replacement.len());
15604 result.extend_from_slice(&haystack[..offset]);
15605 result.extend_from_slice(replacement);
15606 result.extend_from_slice(&haystack[offset + needle.len()..]);
15607 result
15608 }
15609
15610 fn assert_command_version(program: &str, arguments: &[&str], expected: &str) {
15611 let output = Command::new(program)
15612 .args(arguments)
15613 .output()
15614 .unwrap_or_else(|error| panic!("run {program} version check: {error}"));
15615 assert!(
15616 output.status.success(),
15617 "{program} version check failed: {}",
15618 String::from_utf8_lossy(&output.stderr)
15619 );
15620 let stdout = String::from_utf8(output.stdout).expect("version stdout is UTF-8");
15621 let stderr = String::from_utf8(output.stderr).expect("version stderr is UTF-8");
15622 let actual = if stdout.trim().is_empty() {
15623 stderr.trim()
15624 } else {
15625 stdout.trim()
15626 };
15627 assert_eq!(
15628 actual.lines().next(),
15629 Some(expected),
15630 "{program} version drift"
15631 );
15632 }
15633
15634 fn sha256(path: &Path) -> String {
15635 let output = Command::new("shasum")
15636 .args(["-a", "256"])
15637 .arg(path)
15638 .output()
15639 .unwrap_or_else(|error| panic!("{}: run shasum: {error}", path.display()));
15640 assert!(
15641 output.status.success(),
15642 "{}: shasum failed: {}",
15643 path.display(),
15644 String::from_utf8_lossy(&output.stderr)
15645 );
15646 String::from_utf8(output.stdout)
15647 .expect("shasum output is UTF-8")
15648 .split_whitespace()
15649 .next()
15650 .expect("shasum digest")
15651 .to_owned()
15652 }
15653
15654 fn workspace_root() -> PathBuf {
15655 Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
15656 }
15657
15658 fn corpus_dir() -> PathBuf {
15659 std::env::var_os("RDOCX_PPTX_CORPUS_DIR")
15660 .map(PathBuf::from)
15661 .unwrap_or_else(|| workspace_root().join("corpus/pptx"))
15662 }
15663
15664 fn require_or_skip_corpus() -> Option<PathBuf> {
15665 let corpus = corpus_dir();
15666 if corpus.is_dir() {
15667 return Some(corpus);
15668 }
15669 assert_ne!(
15670 std::env::var_os("RDOCX_PPTX_CORPUS_REQUIRED").as_deref(),
15671 Some(std::ffi::OsStr::new("1")),
15672 "the pinned corpus is required but {} does not exist",
15673 corpus.display()
15674 );
15675 eprintln!(
15676 "ChartML corpus gate skipped because {} is absent",
15677 corpus.display()
15678 );
15679 None
15680 }
15681
15682 fn verify_fetched_corpus(corpus: &Path) {
15683 let status = Command::new("python3")
15684 .arg(workspace_root().join("scripts/fetch_pptx_corpus.py"))
15685 .arg("--check")
15686 .env("RDOCX_PPTX_CORPUS_DIR", corpus)
15687 .status()
15688 .expect("run corpus verifier");
15689 assert!(status.success(), "pinned corpus verification failed");
15690 }
15691
15692 fn manifest_paths() -> Vec<&'static str> {
15693 let mut lines = MANIFEST.lines();
15694 assert_eq!(lines.next(), Some("path\tproducer\tsha256\turl"));
15695 let paths: Vec<_> = lines
15696 .map(|line| line.split('\t').next().expect("manifest path"))
15697 .collect();
15698 assert_eq!(paths.len(), EXPECTED_DECKS);
15699 paths
15700 }
15701
15702 fn is_chart_part(part: &str) -> bool {
15703 let Some(stem) = part
15704 .strip_prefix("/ppt/charts/chart")
15705 .and_then(|name| name.strip_suffix(".xml"))
15706 else {
15707 return false;
15708 };
15709 !stem.is_empty() && stem.bytes().all(|byte| byte.is_ascii_digit())
15710 }
15711
15712 fn plot_series(index: u32) -> String {
15713 format!(
15714 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>"#
15715 )
15716 }
15717
15718 fn sparse_category_series() -> &'static str {
15719 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>"#
15720 }
15721
15722 fn sparse_scatter_series() -> &'static str {
15723 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>"#
15724 }
15725
15726 fn chart_with_plot(plot: &str) -> String {
15727 format!(
15728 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>"#
15729 )
15730 }
15731
15732 fn chart_with_optional_axes(plot: &str, axes: bool) -> String {
15733 let axis_xml = if axes {
15734 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>"#
15735 } else {
15736 ""
15737 };
15738 format!(
15739 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>"#
15740 )
15741 }
15742
15743 fn numeric_plot_series(index: u32) -> String {
15744 format!(
15745 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>"#
15746 )
15747 }
15748
15749 fn scatter_without_x(index: u32) -> String {
15750 format!(
15751 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>"#
15752 )
15753 }
15754
15755 fn scatter_without_y(index: u32) -> String {
15756 format!(
15757 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>"#
15758 )
15759 }
15760
15761 fn remaining_plot_fixtures() -> Vec<(&'static str, String, bool)> {
15762 let series = plot_series(0);
15763 let scatter = numeric_plot_series(0);
15764 vec![
15765 (
15766 "pieChart",
15767 format!(
15768 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>"#
15769 ),
15770 false,
15771 ),
15772 (
15773 "doughnutChart",
15774 format!(
15775 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>"#
15776 ),
15777 false,
15778 ),
15779 (
15780 "areaChart",
15781 format!(
15782 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>"#
15783 ),
15784 true,
15785 ),
15786 (
15787 "scatterChart",
15788 format!(
15789 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>"#
15790 ),
15791 true,
15792 ),
15793 (
15794 "radarChart",
15795 format!(
15796 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>"#
15797 ),
15798 true,
15799 ),
15800 ]
15801 }
15802
15803 fn bar_plot(extra: &str) -> String {
15804 format!(
15805 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>"#,
15806 plot_series(0)
15807 )
15808 }
15809
15810 fn line_plot(extra: &str) -> String {
15811 format!(
15812 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>"#,
15813 plot_series(0)
15814 )
15815 }
15816
15817 #[test]
15818 fn bar_and_line_plots_round_trip_and_render() {
15819 for (xml, is_bar) in [
15820 (chart_with_plot(&bar_plot("")), true),
15821 (chart_with_plot(&line_plot("")), false),
15822 ] {
15823 let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15824 let plots = parsed.chart.plot_area.plots().unwrap();
15825 assert_eq!(plots.len(), 1);
15826 assert_eq!(matches!(plots[0], Plot::Bar { .. }), is_bar);
15827 assert_eq!(matches!(plots[0], Plot::Line { .. }), !is_bar);
15828 assert_eq!(parsed.chart.plot_area.axes().unwrap().len(), 2);
15829 let written = parsed.to_xml().unwrap();
15830 let reparsed = CT_ChartSpace::from_xml(&written).unwrap();
15831 assert_eq!(parsed, reparsed);
15832 }
15833 if let Some(corpus) = require_or_skip_corpus() {
15834 verify_bar_and_line_viewer_gate(&corpus);
15835 }
15836 }
15837
15838 #[test]
15839 fn ppm_parser_preserves_whitespace_valued_first_pixels() {
15840 for first_pixel in *b" \n\r\t" {
15841 let mut ppm = b"P6\n1 1\n255\n".to_vec();
15842 ppm.extend_from_slice(&[first_pixel, 0x7f, 0xff]);
15843 let (width, height, pixels) = ppm_pixels(&ppm);
15844 assert_eq!((width, height), (1, 1));
15845 assert_eq!(pixels, [first_pixel, 0x7f, 0xff]);
15846 }
15847
15848 let ppm = b"P6\r\n1 1\r\n255\r\n\x7f\xff";
15849 assert_eq!(ppm_pixels(ppm).2, [b'\n', 0x7f, 0xff]);
15850 }
15851
15852 #[test]
15853 fn bar_and_line_plots_write_fixed_prefixes_in_schema_order() {
15854 for xml in [
15855 chart_with_plot(&bar_plot(r#"<q:extLst><x:bar-tail/></q:extLst>"#)),
15856 chart_with_plot(&line_plot(r#"<q:extLst><x:line-tail/></q:extLst>"#)),
15857 ] {
15858 let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15859 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15860 assert!(written.contains("<c:barChart") || written.contains("<c:lineChart"));
15861 assert!(!written.contains("<q:barChart"));
15862 assert!(!written.contains("<q:lineChart"));
15863 if written.contains("<c:barChart") {
15864 let positions: Vec<_> = [
15865 "<c:barDir",
15866 "<c:grouping",
15867 "<q:varyColors",
15868 "<c:ser",
15869 "<c:dLbls",
15870 "<c:gapWidth",
15871 "<c:overlap",
15872 "<q:serLines",
15873 "<c:axId",
15874 "<q:extLst",
15875 ]
15876 .iter()
15877 .map(|tag| written.find(tag).unwrap())
15878 .collect();
15879 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15880 } else {
15881 let positions: Vec<_> = [
15882 "<c:grouping",
15883 "<q:varyColors",
15884 "<c:ser",
15885 "<c:dLbls",
15886 "<q:dropLines",
15887 "<c:marker",
15888 "<c:smooth",
15889 "<c:axId",
15890 "<q:extLst",
15891 ]
15892 .iter()
15893 .map(|tag| written.find(tag).unwrap())
15894 .collect();
15895 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15896 }
15897 assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
15898 }
15899
15900 for plot in [bar_plot(""), line_plot("")] {
15901 let first = plot_series(0);
15902 let with_series_boundary = plot.replacen(
15903 &first,
15904 &format!(
15905 "{first}<!--between-series--><?series-boundary?>\n {}",
15906 plot_series(1)
15907 ),
15908 1,
15909 );
15910 let with_axis_boundary = with_series_boundary.replacen(
15911 r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
15912 r#"<q:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
15913 <q:axId val="-1884097184"/>"#,
15914 1,
15915 );
15916 let parsed =
15917 CT_ChartSpace::from_xml(chart_with_plot(&with_axis_boundary).as_bytes()).unwrap();
15918 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15919 assert!(written.contains("</c:ser><!--between-series--><?series-boundary?>\n <c:ser"));
15920 assert!(written.contains(
15921 r#"<c:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
15922 <c:axId val="-1884097184"/>"#
15923 ));
15924 assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
15925 }
15926 }
15927
15928 #[test]
15929 fn malformed_bar_and_line_plots_return_errors_without_panicking() {
15930 let valid_series = plot_series(0);
15931 let cases = [
15932 format!(r#"<q:barChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15933 format!(r#"<q:barChart><q:barDir val="diagonal"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15934 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>"#),
15935 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>"#),
15936 format!(r#"<q:lineChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
15937 format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:marker val="maybe"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
15938 r#"<q:lineChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#.to_owned(),
15939 format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:axId val="1"/><q:axId val="1"/></q:lineChart>"#),
15940 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>"#),
15941 format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15942 ];
15943 for plot in cases {
15944 let xml = chart_with_plot(&plot);
15945 let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
15946 assert!(result.is_ok(), "plot parser panicked for {plot}");
15947 assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
15948 }
15949
15950 let missing_axis = format!(
15951 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>"#,
15952 bar_plot("")
15953 );
15954 assert!(CT_ChartSpace::from_xml(missing_axis.as_bytes()).is_err());
15955
15956 let values =
15957 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
15958 let series = vec![Series::new(0, 0, values)];
15959 let ids = [AxisId::new(1).unwrap(), AxisId::new(2).unwrap()];
15960 let mut invalid =
15961 Plot::bar(BarDirection::Column, BarGrouping::Clustered, series, ids).unwrap();
15962 if let Plot::Bar { gap_width, .. } = &mut invalid {
15963 *gap_width = 501;
15964 }
15965 assert!(invalid.validate().is_err());
15966 }
15967
15968 #[test]
15969 fn unsupported_and_combo_plots_remain_byte_preserved() {
15970 let bar = bar_plot("");
15971 let line = line_plot("");
15972 let combo = chart_with_plot(&format!("<!--before-->{bar}<?between?>{line}<!--after-->"));
15973 let parsed = CT_ChartSpace::from_xml(combo.as_bytes()).unwrap();
15974 assert!(parsed.chart.plot_area.plots().is_err());
15975 let written = parsed.to_xml().unwrap();
15976 for raw in [
15977 bar.as_bytes(),
15978 line.as_bytes(),
15979 br#"<!--before-->"#.as_slice(),
15980 br#"<?between?>"#.as_slice(),
15981 br#"<!--after-->"#.as_slice(),
15982 ] {
15983 assert!(written.windows(raw.len()).any(|window| window == raw));
15984 }
15985
15986 let unsupported = chart_with_plot(
15987 r#"<q:bar3DChart x:keep="three"><q:barDir val="col"/><q:ser><x:opaque/></q:ser></q:bar3DChart>"#,
15988 );
15989 let parsed = CT_ChartSpace::from_xml(unsupported.as_bytes()).unwrap();
15990 assert!(parsed.chart.plot_area.plots().is_err());
15991 assert!(
15992 String::from_utf8(parsed.to_xml().unwrap())
15993 .unwrap()
15994 .contains(r#"<q:bar3DChart x:keep="three">"#)
15995 );
15996 }
15997
15998 #[test]
15999 fn public_plot_edits_preserve_axes_and_unselected_payloads() {
16000 let xml = chart_with_plot(&bar_plot(
16001 r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#,
16002 ));
16003 let mut parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16004 let axes_before = parsed.chart.plot_area.axes().unwrap();
16005 match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
16006 Plot::Bar {
16007 gap_width,
16008 overlap,
16009 series,
16010 ..
16011 } => {
16012 *gap_width = 225;
16013 *overlap = -25;
16014 series[0].index = 7;
16015 series[0].values.values[0] = 9.0;
16016 }
16017 _ => panic!("expected bar plot"),
16018 }
16019 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
16020 assert!(written.contains(r#"<c:gapWidth val="225"/>"#));
16021 assert!(written.contains(r#"<c:overlap val="-25"/>"#));
16022 assert!(written.contains("<c:v>9</c:v>"));
16023 assert!(written.contains("<q:varyColors val=\"1\"/>"));
16024 assert!(written.find("<q:varyColors").unwrap() < written.find("<c:ser").unwrap());
16025 assert!(written.contains("<q:serLines><x:line/></q:serLines>"));
16026 assert!(written.contains(r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#));
16027 let reparsed = CT_ChartSpace::from_xml(written.as_bytes()).unwrap();
16028 assert_eq!(axes_before, reparsed.chart.plot_area.axes().unwrap());
16029
16030 let first = plot_series(0);
16031 let colliding_series =
16032 bar_plot("").replacen(&first, &format!("{first}{}", plot_series(1)), 1);
16033 let mut collision =
16034 CT_ChartSpace::from_xml(chart_with_plot(&colliding_series).as_bytes()).unwrap();
16035 if let Plot::Bar { series, .. } = &mut collision.chart.plot_area.plots_mut().unwrap()[0] {
16036 series[0].index = series[1].index;
16037 series[0].order = series[1].order;
16038 }
16039 let collision = String::from_utf8(collision.to_xml().unwrap()).unwrap();
16040 assert!(collision.find("<q:varyColors").unwrap() < collision.find("<c:ser").unwrap());
16041
16042 let mut inserted = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16043 if let Plot::Bar { series, .. } = &mut inserted.chart.plot_area.plots_mut().unwrap()[0] {
16044 let mut new_series = series[0].clone();
16045 new_series.index = 99;
16046 new_series.order = 99;
16047 series.insert(0, new_series);
16048 }
16049 let inserted = String::from_utf8(inserted.to_xml().unwrap()).unwrap();
16050 assert!(inserted.find("<q:varyColors").unwrap() < inserted.find("<c:ser").unwrap());
16051
16052 let axis_payload = bar_plot("").replacen(
16053 r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
16054 r#"<q:axId val="-1884094432" x:slot="category"/><!--axis-anchor--><q:axId val="-1884097184" x:slot="value"/>"#,
16055 1,
16056 );
16057 let mut swapped =
16058 CT_ChartSpace::from_xml(chart_with_plot(&axis_payload).as_bytes()).unwrap();
16059 if let Plot::Bar { axis_ids, .. } = &mut swapped.chart.plot_area.plots_mut().unwrap()[0] {
16060 axis_ids.swap(0, 1);
16061 }
16062 let swapped = String::from_utf8(swapped.to_xml().unwrap()).unwrap();
16063 let anchor = swapped.find("<!--axis-anchor-->").unwrap();
16064 let value_axis = swapped
16065 .find(r#"<c:axId val="-1884097184" x:slot="value"/>"#)
16066 .unwrap();
16067 let category_axis = swapped
16068 .find(r#"<c:axId val="-1884094432" x:slot="category"/>"#)
16069 .unwrap();
16070 let series_lines = swapped.find("<q:serLines").unwrap();
16071 assert!(series_lines < value_axis && series_lines < category_axis);
16072 assert!(anchor < value_axis && value_axis < category_axis);
16073
16074 let mut replaced = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16075 let Plot::Bar {
16076 series, axis_ids, ..
16077 } = &replaced.chart.plot_area.plots().unwrap()[0]
16078 else {
16079 panic!("expected bar plot");
16080 };
16081 let line = Plot::line(Grouping::Standard, series.clone(), *axis_ids).unwrap();
16082 replaced.chart.plot_area.plots_mut().unwrap()[0] = line;
16083 assert!(replaced.to_xml().is_err());
16084 }
16085
16086 #[test]
16087 fn every_corpus_bar_and_line_plot_round_trips_structurally() {
16088 let Some(corpus) = require_or_skip_corpus() else {
16089 return;
16090 };
16091 verify_fetched_corpus(&corpus);
16092 let mut typed_bar_count = 0usize;
16093 let mut typed_line_count = 0usize;
16094 let mut opaque_bar_count = 0usize;
16095 let mut opaque_line_count = 0usize;
16096 let mut opaque_combo_count = 0usize;
16097 for path in manifest_paths() {
16098 let package = OpcPackage::open(corpus.join(path))
16099 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
16100 for (part, xml) in &package.parts {
16101 if !is_chart_part(part) {
16102 continue;
16103 }
16104 let chart = CT_ChartSpace::from_xml(xml)
16105 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
16106 match chart.chart.plot_area.plots() {
16107 Ok(plots) => {
16108 for plot in plots {
16109 match plot {
16110 Plot::Bar { .. } => typed_bar_count += 1,
16111 Plot::Line { .. } => typed_line_count += 1,
16112 _ => {}
16113 }
16114 }
16115 }
16116 Err(_) => {
16117 let mut preserved_plot_count = 0usize;
16118 for raw in chart.chart.plot_area.raw_children.at(0) {
16119 if let Ok(Some(local)) = super::chart_root_local(
16120 raw,
16121 &chart.chart.plot_area.namespace_bindings,
16122 ) {
16123 match local.as_slice() {
16124 b"barChart" => {
16125 opaque_bar_count += 1;
16126 preserved_plot_count += 1;
16127 }
16128 b"lineChart" => {
16129 opaque_line_count += 1;
16130 preserved_plot_count += 1;
16131 }
16132 _ => {}
16133 }
16134 }
16135 }
16136 if preserved_plot_count > 1 {
16137 opaque_combo_count += 1;
16138 }
16139 }
16140 }
16141 let written = chart
16142 .to_xml()
16143 .unwrap_or_else(|error| panic!("{path} {part}: chart write failed: {error}"));
16144 let reparsed = CT_ChartSpace::from_xml(&written).unwrap_or_else(|error| {
16145 panic!("{path} {part}: written chart parse failed: {error}")
16146 });
16147 assert_eq!(chart, reparsed, "{path} {part}: chart model changed");
16148 }
16149 }
16150 assert_eq!(typed_bar_count, 11, "typed corpus bar coverage changed");
16151 assert_eq!(typed_line_count, 2, "typed corpus line coverage changed");
16152 assert_eq!(opaque_bar_count, 1, "preserved combo bar coverage changed");
16153 assert_eq!(
16154 opaque_line_count, 1,
16155 "preserved combo line coverage changed"
16156 );
16157 assert_eq!(opaque_combo_count, 1, "preserved combo coverage changed");
16158 assert_eq!(typed_bar_count + opaque_bar_count, 12);
16159 assert_eq!(typed_line_count + opaque_line_count, 3);
16160 eprintln!(
16161 "ChartML plot corpus gate checked {typed_bar_count} typed bar, {typed_line_count} typed line, and one preserved bar-line combination"
16162 );
16163 }
16164
16165 fn verify_remaining_plot_viewer_gate(corpus: &Path) {
16166 verify_fetched_corpus(corpus);
16167 assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
16168 assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
16169 let source = corpus.join("bar-chart.pptx");
16170 let source_sha = sha256(&source);
16171 let temp_root = std::env::temp_dir().join(format!(
16172 "oxml-chart-f122-viewer-gate-{}",
16173 std::process::id()
16174 ));
16175 if temp_root.exists() {
16176 fs::remove_dir_all(&temp_root).expect("remove stale F-122 evidence");
16177 }
16178 fs::create_dir_all(&temp_root).expect("create F-122 evidence root");
16179
16180 for (kind, plot, axes) in remaining_plot_fixtures() {
16181 let mut package = OpcPackage::open(&source).expect("open F-122 viewer source");
16182 package.set_part(
16183 "/ppt/charts/chart1.xml",
16184 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes())
16185 .expect("parse F-122 viewer chart")
16186 .to_xml()
16187 .expect("serialize F-122 viewer chart"),
16188 );
16189 let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
16190 package.save(&unbound).expect("save F-122 viewer candidate");
16191 let candidate_sha = sha256(&unbound);
16192 let evidence = temp_root.join(format!("{source_sha}-{candidate_sha}"));
16193 fs::create_dir(&evidence).expect("create SHA-bound F-122 evidence directory");
16194 let candidate = evidence.join(format!("{kind}-candidate.pptx"));
16195 fs::rename(&unbound, &candidate).expect("bind F-122 candidate to SHA");
16196 assert_eq!(sha256(&candidate), candidate_sha);
16197 let render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
16198 let bytes = fs::read(&render).expect("read F-122 viewer PPM");
16199 let (width, height, pixels) = ppm_pixels(&bytes);
16200 let left = width / 5;
16201 let right = width * 4 / 5;
16202 let top = height * 3 / 20;
16203 let bottom = height * 17 / 20;
16204 let mut nonblank = 0usize;
16205 for y in top..bottom {
16206 for x in left..right {
16207 let offset = (y * width + x) * 3;
16208 if pixels[offset..offset + 3]
16209 .iter()
16210 .any(|channel| *channel < 245)
16211 {
16212 nonblank += 1;
16213 }
16214 }
16215 }
16216 const CHART_RECTANGLE_NONBLANK_THRESHOLD: usize = 1_000;
16217 assert!(
16218 nonblank >= CHART_RECTANGLE_NONBLANK_THRESHOLD,
16219 "{kind}: chart rectangle [{left},{top}) to [{right},{bottom}) contains {nonblank} nonblank pixels, below {CHART_RECTANGLE_NONBLANK_THRESHOLD}"
16220 );
16221 eprintln!(
16222 "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}",
16223 sha256(&render)
16224 );
16225 }
16226 fs::remove_dir_all(&temp_root).expect("remove F-122 viewer evidence");
16227 }
16228
16229 fn verify_bar_and_line_viewer_gate(corpus: &Path) {
16230 verify_fetched_corpus(corpus);
16231 assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
16232 assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
16233 let temp_root = std::env::temp_dir().join(format!(
16234 "oxml-chart-f121-viewer-gate-{}",
16235 std::process::id()
16236 ));
16237 if temp_root.exists() {
16238 fs::remove_dir_all(&temp_root).expect("remove stale F-121 evidence");
16239 }
16240 fs::create_dir_all(&temp_root).expect("create F-121 evidence root");
16241
16242 for (kind, deck) in [("bar", "bar-chart.pptx"), ("line", "line-chart.pptx")] {
16243 let source = corpus.join(deck);
16244 let original_sha = sha256(&source);
16245 let mut package = OpcPackage::open(&source)
16246 .unwrap_or_else(|error| panic!("{deck}: open viewer source: {error}"));
16247 let chart_part = "/ppt/charts/chart1.xml";
16248 let original_parts = package.parts.clone();
16249 let parsed = CT_ChartSpace::from_xml(
16250 package
16251 .get_part(chart_part)
16252 .unwrap_or_else(|| panic!("{deck}: missing {chart_part}")),
16253 )
16254 .unwrap_or_else(|error| panic!("{deck}: parse viewer chart: {error}"));
16255 assert!(
16256 parsed.chart.plot_area.plots().is_ok(),
16257 "{deck}: plot is opaque"
16258 );
16259 package.set_part(
16260 chart_part,
16261 parsed
16262 .to_xml()
16263 .unwrap_or_else(|error| panic!("{deck}: serialize viewer chart: {error}")),
16264 );
16265 for (part, bytes) in &original_parts {
16266 if part == chart_part {
16267 assert_ne!(package.parts[part], *bytes, "{deck}: chart did not rewrite");
16268 } else {
16269 assert_eq!(package.parts[part], *bytes, "{deck}: changed part {part}");
16270 }
16271 }
16272
16273 let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
16274 package
16275 .save(&unbound)
16276 .unwrap_or_else(|error| panic!("{deck}: save viewer candidate: {error}"));
16277 let candidate_sha = sha256(&unbound);
16278 let evidence = temp_root.join(format!("{original_sha}-{candidate_sha}"));
16279 fs::create_dir(&evidence).expect("create SHA-bound F-121 evidence directory");
16280 let original = evidence.join(format!("{kind}-original.pptx"));
16281 let candidate = evidence.join(format!("{kind}-candidate.pptx"));
16282 fs::copy(&source, &original).expect("copy SHA-bound viewer original");
16283 fs::rename(&unbound, &candidate).expect("bind viewer candidate to SHA");
16284 assert_eq!(sha256(&original), original_sha);
16285 assert_eq!(sha256(&candidate), candidate_sha);
16286
16287 let original_render = render_deck_to_ppm(&original, &evidence, kind, "original");
16288 let candidate_render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
16289 let original_bytes = fs::read(&original_render).expect("read original PPM");
16290 let candidate_bytes = fs::read(&candidate_render).expect("read candidate PPM");
16291 let normalized_mae = normalized_ppm_mae(&original_bytes, &candidate_bytes);
16292 assert!(
16293 normalized_mae <= PLOT_RENDER_NORMALIZED_MAE_THRESHOLD,
16294 "{deck}: normalized RGB MAE {normalized_mae:.8} exceeds {:.8}",
16295 PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
16296 );
16297 eprintln!(
16298 "F-121 {kind} viewer gate original deck {original_sha}, candidate deck {candidate_sha}, original render {}, candidate render {}, normalized RGB MAE {normalized_mae:.8} <= {:.8}",
16299 sha256(&original_render),
16300 sha256(&candidate_render),
16301 PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
16302 );
16303 }
16304 fs::remove_dir_all(&temp_root).expect("remove F-121 viewer evidence");
16305 }
16306
16307 fn render_deck_to_ppm(deck: &Path, evidence: &Path, kind: &str, side: &str) -> PathBuf {
16308 let profile = std::env::temp_dir().join(format!(
16309 "oxml-chart-f121-{kind}-{side}-profile-{}",
16310 std::process::id()
16311 ));
16312 if profile.exists() {
16313 fs::remove_dir_all(&profile).expect("remove stale F-121 viewer profile");
16314 }
16315 let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
16316 let conversion = Command::new("soffice")
16317 .args([
16318 "--headless",
16319 &profile_argument,
16320 "--convert-to",
16321 "pdf:impress_pdf_Export",
16322 "--outdir",
16323 ])
16324 .arg(evidence)
16325 .arg(deck)
16326 .output()
16327 .expect("run pinned LibreOffice F-121 viewer gate");
16328 assert!(
16329 conversion.status.success(),
16330 "{}: LibreOffice conversion failed: {}",
16331 deck.display(),
16332 String::from_utf8_lossy(&conversion.stderr)
16333 );
16334 let pdf = deck.with_extension("pdf");
16335 assert!(
16336 pdf.is_file(),
16337 "LibreOffice did not create {}",
16338 pdf.display()
16339 );
16340 let prefix = evidence.join(format!("{kind}-{side}"));
16341 let raster = Command::new("pdftoppm")
16342 .args(["-f", "1", "-singlefile", "-r", "150"])
16343 .arg(&pdf)
16344 .arg(&prefix)
16345 .output()
16346 .expect("run pinned Poppler F-121 raster gate");
16347 assert!(
16348 raster.status.success(),
16349 "{}: pdftoppm failed: {}",
16350 pdf.display(),
16351 String::from_utf8_lossy(&raster.stderr)
16352 );
16353 if profile.exists() {
16354 fs::remove_dir_all(&profile).expect("remove F-121 viewer profile");
16355 }
16356 prefix.with_extension("ppm")
16357 }
16358
16359 fn normalized_ppm_mae(left: &[u8], right: &[u8]) -> f64 {
16360 let (left_width, left_height, left_pixels) = ppm_pixels(left);
16361 let (right_width, right_height, right_pixels) = ppm_pixels(right);
16362 assert_eq!((left_width, left_height), (right_width, right_height));
16363 assert_eq!(left_pixels.len(), right_pixels.len());
16364 let difference: u64 = left_pixels
16365 .iter()
16366 .zip(right_pixels)
16367 .map(|(left, right)| u64::from(left.abs_diff(*right)))
16368 .sum();
16369 difference as f64 / (left_pixels.len() as f64 * 255.0)
16370 }
16371
16372 fn ppm_pixels(bytes: &[u8]) -> (usize, usize, &[u8]) {
16373 let mut cursor = 0usize;
16374 let mut tokens = Vec::new();
16375 while tokens.len() < 4 {
16376 while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
16377 cursor += 1;
16378 }
16379 if cursor < bytes.len() && bytes[cursor] == b'#' {
16380 while cursor < bytes.len() && bytes[cursor] != b'\n' {
16381 cursor += 1;
16382 }
16383 continue;
16384 }
16385 let start = cursor;
16386 while cursor < bytes.len() && !bytes[cursor].is_ascii_whitespace() {
16387 cursor += 1;
16388 }
16389 tokens.push(std::str::from_utf8(&bytes[start..cursor]).expect("PPM header UTF-8"));
16390 }
16391 assert_eq!(tokens[0], "P6");
16392 let width = tokens[1].parse::<usize>().expect("PPM width");
16393 let height = tokens[2].parse::<usize>().expect("PPM height");
16394 assert_eq!(tokens[3], "255");
16395 assert!(
16396 cursor < bytes.len() && bytes[cursor].is_ascii_whitespace(),
16397 "PPM header must end with whitespace"
16398 );
16399 cursor += 1;
16400 let pixels = &bytes[cursor..];
16401 assert_eq!(pixels.len(), width * height * 3, "PPM pixel length");
16402 (width, height, pixels)
16403 }
16404}