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 tab_aligned: None,
1421 })
1422}
1423
1424fn chart_text_properties(body: &CT_TextBody) -> Option<&CT_TextCharacterProperties> {
1425 body.paragraphs()
1426 .first()
1427 .and_then(|paragraph| paragraph.properties.as_ref())
1428 .and_then(|properties| properties.default_run_properties.as_ref())
1429}
1430
1431fn stroked_segment(start: Point, end: Point, color: Color, width: f64) -> PositionedElement {
1432 PositionedElement::Path(PathElement {
1433 path: Path {
1434 commands: vec![PathCommand::MoveTo(start), PathCommand::LineTo(end)],
1435 fill_rule: FillRule::NonZero,
1436 },
1437 fill: None,
1438 stroke: Some(Stroke::new(Paint::Solid(color), width)),
1439 })
1440}
1441
1442fn plot_series(plot: &Plot) -> &[Series] {
1443 match plot {
1444 Plot::Bar { series, .. }
1445 | Plot::Line { series, .. }
1446 | Plot::Pie { series, .. }
1447 | Plot::Doughnut { series, .. }
1448 | Plot::Area { series, .. }
1449 | Plot::Scatter { series, .. }
1450 | Plot::Radar { series, .. } => series,
1451 }
1452}
1453
1454fn plot_data_labels(plot: &Plot) -> Option<&CT_DLbls> {
1455 match plot {
1456 Plot::Bar { data_labels, .. }
1457 | Plot::Line { data_labels, .. }
1458 | Plot::Pie { data_labels, .. }
1459 | Plot::Doughnut { data_labels, .. }
1460 | Plot::Area { data_labels, .. }
1461 | Plot::Scatter { data_labels, .. }
1462 | Plot::Radar { data_labels, .. } => data_labels.as_ref(),
1463 }
1464}
1465
1466fn series_categories(
1467 series: &Series,
1468 number_format: Option<&NumberFormat>,
1469) -> Result<Vec<(usize, String)>> {
1470 let Some(categories) = &series.categories else {
1471 return Ok(Vec::new());
1472 };
1473 match categories {
1474 AxisData::String(data) => {
1475 let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
1476 indexes
1477 .into_iter()
1478 .zip(&data.values)
1479 .map(|(index, value)| {
1480 usize::try_from(index)
1481 .map(|index| (index, value.clone()))
1482 .map_err(|_| ChartError::InvalidValue {
1483 element: "c:cat/c:pt/@idx".to_owned(),
1484 value: index.to_string(),
1485 })
1486 })
1487 .collect()
1488 }
1489 AxisData::Numeric(data) => {
1490 let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
1491 let cache_format;
1492 let format = if let Some(format) = number_format {
1493 format
1494 } else {
1495 cache_format = NumberFormat::new(data.format_code.clone(), false)?;
1496 &cache_format
1497 };
1498 indexes
1499 .into_iter()
1500 .zip(&data.values)
1501 .map(|(index, value)| {
1502 let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
1503 element: "c:cat/c:pt/@idx".to_owned(),
1504 value: index.to_string(),
1505 })?;
1506 let text = format.format_value(*value)?;
1507 Ok((index, text))
1508 })
1509 .collect()
1510 }
1511 }
1512}
1513
1514fn series_name(series: &Series, index: usize) -> String {
1515 series
1516 .name
1517 .as_ref()
1518 .and_then(|name| name.values.first())
1519 .cloned()
1520 .unwrap_or_else(|| format!("Series {}", index + 1))
1521}
1522
1523fn render_data_labels(
1524 plot: &Plot,
1525 bounds: Rect,
1526 blanks: DispBlanksAs,
1527 options: GeometryOptions,
1528 fonts: &mut FontManager,
1529 elements: &mut Vec<PositionedElement>,
1530) -> Result<()> {
1531 let plot_labels = plot_data_labels(plot);
1532 let anchors = plot_label_anchors(plot, bounds, blanks, options)?;
1533 for (series_index, series) in plot_series(plot).iter().enumerate() {
1534 let Some(labels) = series.data_labels.as_ref().or(plot_labels) else {
1535 continue;
1536 };
1537 let overrides = point_label_overrides(labels)?;
1538 let mut categories = None;
1539 let mut bubble_sizes = None;
1540 let (_, values) = logical_numeric_values(&series.values)?;
1541 let mut percentage_total = None;
1542 for (logical_index, value) in values {
1543 let point_override = overrides.get(&logical_index);
1544 if point_override.is_some_and(|point| point.deleted == Some(true)) {
1545 continue;
1546 }
1547 let effective_labels = point_override
1548 .map(|point| point.apply_to(labels))
1549 .unwrap_or_else(|| labels.clone());
1550 let labels = &effective_labels;
1551 let Some(anchor) = anchors.get(&(series_index, logical_index)).copied() else {
1552 continue;
1553 };
1554 let mut fields = Vec::new();
1555 if labels.show_series_name {
1556 fields.push(series_name(series, series_index));
1557 }
1558 if labels.show_category_name {
1559 if categories.is_none() {
1560 categories = Some(
1561 series_categories(series, None)?
1562 .into_iter()
1563 .collect::<BTreeMap<_, _>>(),
1564 );
1565 }
1566 if let Some(category) = categories
1567 .as_ref()
1568 .and_then(|categories| categories.get(&logical_index))
1569 {
1570 fields.push(category.clone());
1571 }
1572 }
1573 if labels.show_value {
1574 fields.push(format_axis_value(labels.number_format.as_ref(), value)?);
1575 }
1576 if labels.show_percent {
1577 let total = if let Some(total) = percentage_total {
1578 total
1579 } else {
1580 let total = series
1581 .values
1582 .values
1583 .iter()
1584 .copied()
1585 .filter(|value| *value > 0.0)
1586 .try_fold(0.0, |total, value| {
1587 checked_geometry_sum(total, value, "data-label percentage total")
1588 })?;
1589 percentage_total = Some(total);
1590 total
1591 };
1592 if total > 0.0 {
1593 let text = if let Some(format) = &labels.number_format {
1594 format.format_value(value / total)?
1595 } else {
1596 NumberFormat::new("0%".to_owned(), false)?.format_value(value / total)?
1597 };
1598 fields.push(text);
1599 }
1600 }
1601 if labels.show_bubble_size {
1602 if bubble_sizes.is_none() {
1603 bubble_sizes = Some(
1604 series
1605 .bubble_size
1606 .as_ref()
1607 .map(logical_numeric_values)
1608 .transpose()?
1609 .map(|(_, values)| values.into_iter().collect::<BTreeMap<_, _>>())
1610 .unwrap_or_default(),
1611 );
1612 }
1613 if let Some(value) = bubble_sizes
1614 .as_ref()
1615 .and_then(|sizes| sizes.get(&logical_index))
1616 {
1617 fields.push(format_axis_value(labels.number_format.as_ref(), *value)?);
1618 }
1619 }
1620 if fields.is_empty() {
1621 continue;
1622 }
1623 let separator = labels.separator.as_deref().unwrap_or(", ");
1624 let text = fields.join(separator);
1625 let origin = anchor.origin(labels.position);
1626 elements.push(PositionedElement::Text(shape_label(fonts, &text, origin)?));
1627 }
1628 }
1629 Ok(())
1630}
1631
1632#[derive(Clone, Debug, Default)]
1633struct PointLabelOverride {
1634 index: Option<usize>,
1635 deleted: Option<bool>,
1636 number_format: Option<NumberFormat>,
1637 position: Option<DataLabelPosition>,
1638 separator: Option<String>,
1639 show_legend_key: Option<bool>,
1640 show_value: Option<bool>,
1641 show_category_name: Option<bool>,
1642 show_series_name: Option<bool>,
1643 show_percent: Option<bool>,
1644 show_bubble_size: Option<bool>,
1645}
1646
1647impl PointLabelOverride {
1648 fn apply_to(&self, labels: &CT_DLbls) -> CT_DLbls {
1649 let mut effective = labels.clone();
1650 if let Some(value) = &self.number_format {
1651 effective.number_format = Some(value.clone());
1652 }
1653 if let Some(value) = self.position {
1654 effective.position = Some(value);
1655 }
1656 if let Some(value) = &self.separator {
1657 effective.separator = Some(value.clone());
1658 }
1659 if let Some(value) = self.show_legend_key {
1660 effective.show_legend_key = value;
1661 }
1662 if let Some(value) = self.show_value {
1663 effective.show_value = value;
1664 }
1665 if let Some(value) = self.show_category_name {
1666 effective.show_category_name = value;
1667 }
1668 if let Some(value) = self.show_series_name {
1669 effective.show_series_name = value;
1670 }
1671 if let Some(value) = self.show_percent {
1672 effective.show_percent = value;
1673 }
1674 if let Some(value) = self.show_bubble_size {
1675 effective.show_bubble_size = value;
1676 }
1677 effective
1678 }
1679}
1680
1681fn point_label_overrides(labels: &CT_DLbls) -> Result<BTreeMap<usize, PointLabelOverride>> {
1682 let mut namespaces = chart_namespace_defaults();
1683 for (name, namespace) in &labels.namespace_declarations {
1684 let prefix = if name == "xmlns" {
1685 Vec::new()
1686 } else if let Some(prefix) = name.strip_prefix("xmlns:") {
1687 prefix.as_bytes().to_vec()
1688 } else {
1689 continue;
1690 };
1691 upsert_namespace_binding(&mut namespaces, prefix, namespace.clone());
1692 }
1693 let mut overrides = BTreeMap::new();
1694 for raw in labels.raw_children.at(0) {
1695 if chart_root_local(raw, &namespaces)?.as_deref() != Some(b"dLbl") {
1696 continue;
1697 }
1698 let point = parse_point_label_override(raw, &namespaces)?;
1699 let index = point
1700 .index
1701 .ok_or_else(|| ChartError::MissingElement("c:dLbl/c:idx".to_owned()))?;
1702 if overrides.insert(index, point).is_some() {
1703 return Err(ChartError::DuplicateElement(format!(
1704 "c:dLbl[c:idx={index}]"
1705 )));
1706 }
1707 }
1708 Ok(overrides)
1709}
1710
1711fn parse_point_label_override(
1712 xml: &[u8],
1713 inherited: &NamespaceBindings,
1714) -> Result<PointLabelOverride> {
1715 let mut reader = Reader::from_reader(xml);
1716 let mut buffer = Vec::new();
1717 let mut namespaces = None;
1718 let mut state = PointLabelOverride::default();
1719 let mut order = 0;
1720 loop {
1721 match reader
1722 .read_event_into(&mut buffer)
1723 .map_err(OxmlError::from)?
1724 {
1725 Event::Start(element) if namespaces.is_none() => {
1726 if !matches_local_name(element.name().as_ref(), b"dLbl")
1727 || !element_is_in_namespace(&element, C_NS, inherited)?
1728 {
1729 return Err(ChartError::UnexpectedElement(element_name(&element)));
1730 }
1731 namespaces = Some(chart_bindings(inherited, &element)?);
1732 }
1733 Event::Start(element) => {
1734 let bindings = namespaces
1735 .as_ref()
1736 .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
1737 let name = chart_child_local(&element, bindings)?;
1738 let raw = capture_element(&mut reader, &element)?;
1739 parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
1740 }
1741 Event::Empty(element) => {
1742 let bindings = namespaces
1743 .as_ref()
1744 .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
1745 let name = chart_child_local(&element, bindings)?;
1746 let raw = capture_empty_element(&element)?;
1747 parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
1748 }
1749 Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbl") => {
1750 if state.index.is_none() {
1751 return Err(ChartError::MissingElement("c:dLbl/c:idx".to_owned()));
1752 }
1753 return Ok(state);
1754 }
1755 Event::Eof => return Err(missing_end("c:dLbl")),
1756 _ => {}
1757 }
1758 buffer.clear();
1759 }
1760}
1761
1762fn parse_point_label_child(
1763 state: &mut PointLabelOverride,
1764 name: Option<&[u8]>,
1765 raw: &[u8],
1766 namespaces: &NamespaceBindings,
1767 order: &mut usize,
1768) -> Result<()> {
1769 let Some(name) = name else {
1770 return Ok(());
1771 };
1772 let rank = match name {
1773 b"idx" => 0,
1774 b"delete" => 1,
1775 b"layout" => 2,
1776 b"tx" => 3,
1777 b"numFmt" => 4,
1778 b"spPr" => 5,
1779 b"txPr" => 6,
1780 b"dLblPos" => 7,
1781 b"showLegendKey" => 8,
1782 b"showVal" => 9,
1783 b"showCatName" => 10,
1784 b"showSerName" => 11,
1785 b"showPercent" => 12,
1786 b"showBubbleSize" => 13,
1787 b"separator" => 14,
1788 b"showLeaderLines" => 15,
1789 b"leaderLines" => 16,
1790 b"extLst" => 17,
1791 _ => return Ok(()),
1792 };
1793 if rank < *order {
1794 return Err(ChartError::InvalidValue {
1795 element: "c:dLbl".to_owned(),
1796 value: format!("c:{} is out of schema order", String::from_utf8_lossy(name)),
1797 });
1798 }
1799 *order = rank;
1800 match name {
1801 b"idx" => {
1802 let (index, _) = parse_u32_scalar(raw, "idx")?;
1803 let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
1804 element: "c:dLbl/c:idx".to_owned(),
1805 value: index.to_string(),
1806 })?;
1807 set_once(&mut state.index, index, "c:dLbl/c:idx")?;
1808 }
1809 b"delete" => set_once(
1810 &mut state.deleted,
1811 parse_bool_value(raw, "delete")?.0,
1812 "c:dLbl/c:delete",
1813 )?,
1814 b"numFmt" => set_once(
1815 &mut state.number_format,
1816 NumberFormat::from_xml(raw)?,
1817 "c:dLbl/c:numFmt",
1818 )?,
1819 b"dLblPos" => {
1820 let (value, _) = scalar_value(raw, "dLblPos")?;
1821 let value =
1822 value.ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
1823 let position = DataLabelPosition::parse(&value)
1824 .ok_or_else(|| invalid_attribute("dLblPos", "val", value))?;
1825 set_once(&mut state.position, position, "c:dLbl/c:dLblPos")?;
1826 }
1827 b"showLegendKey" => set_once(
1828 &mut state.show_legend_key,
1829 parse_bool_value(raw, "showLegendKey")?.0,
1830 "c:dLbl/c:showLegendKey",
1831 )?,
1832 b"showVal" => set_once(
1833 &mut state.show_value,
1834 parse_bool_value(raw, "showVal")?.0,
1835 "c:dLbl/c:showVal",
1836 )?,
1837 b"showCatName" => set_once(
1838 &mut state.show_category_name,
1839 parse_bool_value(raw, "showCatName")?.0,
1840 "c:dLbl/c:showCatName",
1841 )?,
1842 b"showSerName" => set_once(
1843 &mut state.show_series_name,
1844 parse_bool_value(raw, "showSerName")?.0,
1845 "c:dLbl/c:showSerName",
1846 )?,
1847 b"showPercent" => set_once(
1848 &mut state.show_percent,
1849 parse_bool_value(raw, "showPercent")?.0,
1850 "c:dLbl/c:showPercent",
1851 )?,
1852 b"showBubbleSize" => set_once(
1853 &mut state.show_bubble_size,
1854 parse_bool_value(raw, "showBubbleSize")?.0,
1855 "c:dLbl/c:showBubbleSize",
1856 )?,
1857 b"separator" => set_once(
1858 &mut state.separator,
1859 parse_text_element(raw, b"separator", namespaces)?.0,
1860 "c:dLbl/c:separator",
1861 )?,
1862 _ => {}
1863 }
1864 Ok(())
1865}
1866
1867#[derive(Clone, Copy, Debug)]
1868struct LabelAnchor {
1869 center: Point,
1870 base: Point,
1871 end: Point,
1872 direction: Point,
1873}
1874
1875impl LabelAnchor {
1876 const fn point(center: Point) -> Self {
1877 Self {
1878 center,
1879 base: center,
1880 end: center,
1881 direction: Point { x: 0.0, y: 0.0 },
1882 }
1883 }
1884
1885 const fn segment(center: Point, base: Point, end: Point) -> Self {
1886 Self {
1887 center,
1888 base,
1889 end,
1890 direction: Point {
1891 x: end.x - base.x,
1892 y: end.y - base.y,
1893 },
1894 }
1895 }
1896
1897 const fn directed_segment(center: Point, base: Point, end: Point, direction: Point) -> Self {
1898 Self {
1899 center,
1900 base,
1901 end,
1902 direction,
1903 }
1904 }
1905
1906 fn origin(self, position: Option<DataLabelPosition>) -> Point {
1907 let mut origin = self.center;
1908 match position.unwrap_or(DataLabelPosition::BestFit) {
1909 DataLabelPosition::Bottom => origin.y += 8.0,
1910 DataLabelPosition::Top => origin.y -= 5.0,
1911 DataLabelPosition::Left => origin.x -= 8.0,
1912 DataLabelPosition::Right => origin.x += 8.0,
1913 DataLabelPosition::InsideBase => {
1914 origin = self.along_segment(self.base, 5.0, true).unwrap_or(Point {
1915 x: origin.x,
1916 y: origin.y + 8.0,
1917 });
1918 }
1919 DataLabelPosition::InsideEnd => {
1920 origin = self.along_segment(self.end, -5.0, true).unwrap_or(origin);
1921 }
1922 DataLabelPosition::OutsideEnd => {
1923 origin = self.along_segment(self.end, 5.0, false).unwrap_or(Point {
1924 x: origin.x,
1925 y: origin.y - 5.0,
1926 });
1927 }
1928 DataLabelPosition::BestFit | DataLabelPosition::Center => {}
1929 }
1930 origin
1931 }
1932
1933 fn along_segment(self, origin: Point, distance: f64, clamp_inside: bool) -> Option<Point> {
1934 let dx = self.end.x - self.base.x;
1935 let dy = self.end.y - self.base.y;
1936 let length = dx.hypot(dy);
1937 let (dx, dy, direction_length) = if length.is_finite() && length > 0.0 {
1938 (dx, dy, length)
1939 } else {
1940 let direction_length = self.direction.x.hypot(self.direction.y);
1941 if !direction_length.is_finite() || direction_length == 0.0 {
1942 return None;
1943 }
1944 (self.direction.x, self.direction.y, direction_length)
1945 };
1946 let distance = if clamp_inside {
1947 distance.clamp(-length / 2.0, length / 2.0)
1948 } else {
1949 distance
1950 };
1951 Some(Point {
1952 x: origin.x + dx / direction_length * distance,
1953 y: origin.y + dy / direction_length * distance,
1954 })
1955 }
1956}
1957
1958fn plot_label_anchors(
1959 plot: &Plot,
1960 bounds: Rect,
1961 blanks: DispBlanksAs,
1962 options: GeometryOptions,
1963) -> Result<BTreeMap<(usize, usize), LabelAnchor>> {
1964 let mut anchors = BTreeMap::new();
1965 match plot {
1966 Plot::Bar {
1967 direction,
1968 grouping,
1969 gap_width,
1970 overlap,
1971 series,
1972 ..
1973 } => {
1974 for bar in bar_rectangles(
1975 *direction, *grouping, *gap_width, *overlap, series, bounds, options,
1976 )? {
1977 if !point_in_rect(bar.end, bounds) {
1978 continue;
1979 }
1980 let visible_base = Point {
1981 x: bar.base.x.clamp(bounds.x, bounds.x + bounds.width),
1982 y: bar.base.y.clamp(bounds.y, bounds.y + bounds.height),
1983 };
1984 let visible_end = Point {
1985 x: bar.end.x.clamp(bounds.x, bounds.x + bounds.width),
1986 y: bar.end.y.clamp(bounds.y, bounds.y + bounds.height),
1987 };
1988 anchors.insert(
1989 (bar.series_index, bar.logical_index),
1990 LabelAnchor::directed_segment(
1991 Point {
1992 x: (visible_base.x + visible_end.x) / 2.0,
1993 y: (visible_base.y + visible_end.y) / 2.0,
1994 },
1995 visible_base,
1996 visible_end,
1997 Point {
1998 x: bar.end.x - bar.base.x,
1999 y: bar.end.y - bar.base.y,
2000 },
2001 ),
2002 );
2003 }
2004 }
2005 Plot::Line {
2006 grouping, series, ..
2007 }
2008 | Plot::Area {
2009 grouping, series, ..
2010 } => {
2011 for (series_index, points) in
2012 category_label_points(*grouping, series, bounds, blanks, options)?
2013 .into_iter()
2014 .enumerate()
2015 {
2016 for (logical_index, point) in points {
2017 anchors.insert((series_index, logical_index), LabelAnchor::point(point));
2018 }
2019 }
2020 }
2021 Plot::Scatter { series, .. } => {
2022 let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
2023 let x_mapping = options.scatter_x.unwrap_or(AxisMapping {
2024 domain: x_domain,
2025 orientation: Orientation::MinMax,
2026 });
2027 let y_mapping = options.scatter_y.unwrap_or(AxisMapping {
2028 domain: y_domain,
2029 orientation: Orientation::MinMax,
2030 });
2031 for (series_index, series) in series.iter().enumerate() {
2032 let indexed = matched_scatter_points(series)?;
2033 let points = scatter_points(
2034 &indexed,
2035 x_mapping.domain,
2036 y_mapping.domain,
2037 bounds,
2038 x_mapping.orientation,
2039 y_mapping.orientation,
2040 )?;
2041 for ((logical_index, _, _), point) in indexed.into_iter().zip(points) {
2042 anchors.insert((series_index, logical_index), LabelAnchor::point(point));
2043 }
2044 }
2045 }
2046 Plot::Radar { series, .. } => {
2047 let center = Point {
2048 x: bounds.x + bounds.width / 2.0,
2049 y: bounds.y + bounds.height / 2.0,
2050 };
2051 let category_count = series_category_count(series)?;
2052 for (series_index, points) in radar_series_points(series, bounds, options)?
2053 .into_iter()
2054 .enumerate()
2055 {
2056 for (logical_index, point) in points {
2057 let spoke = radial_point(
2058 center,
2059 1.0,
2060 oriented_category_index(
2061 logical_index,
2062 category_count,
2063 options.category_orientation,
2064 ),
2065 category_count,
2066 );
2067 anchors.insert(
2068 (series_index, logical_index),
2069 LabelAnchor::directed_segment(
2070 point,
2071 center,
2072 point,
2073 Point {
2074 x: spoke.x - center.x,
2075 y: spoke.y - center.y,
2076 },
2077 ),
2078 );
2079 }
2080 }
2081 }
2082 Plot::Pie {
2083 first_slice_angle,
2084 series,
2085 ..
2086 } => {
2087 for slice in pie_slices(*first_slice_angle, None, series, bounds)? {
2088 anchors.insert(
2089 (slice.series_index, slice.logical_index),
2090 LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2091 );
2092 }
2093 }
2094 Plot::Doughnut {
2095 first_slice_angle,
2096 hole_size,
2097 series,
2098 ..
2099 } => {
2100 for slice in pie_slices(*first_slice_angle, Some(*hole_size), series, bounds)? {
2101 anchors.insert(
2102 (slice.series_index, slice.logical_index),
2103 LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2104 );
2105 }
2106 }
2107 }
2108 anchors.retain(|_, anchor| point_in_rect(anchor.end, bounds));
2109 Ok(anchors)
2110}
2111
2112fn category_label_points(
2113 grouping: Grouping,
2114 series: &[Series],
2115 bounds: Rect,
2116 blanks: DispBlanksAs,
2117 options: GeometryOptions,
2118) -> Result<Vec<Vec<(usize, Point)>>> {
2119 validate_series_values(series)?;
2120 let logical_series = logical_series_values(series)?;
2121 let category_count = series_category_count(series)?;
2122 let calculated_series = if blanks == DispBlanksAs::Zero {
2123 zero_filled_series(&logical_series, category_count)
2124 } else {
2125 logical_series.clone()
2126 };
2127 let layers = stacked_series_bounds(
2128 &calculated_series,
2129 grouping != Grouping::Standard,
2130 grouping == Grouping::PercentStacked,
2131 )?;
2132 let domain = options
2133 .value
2134 .map(|mapping| mapping.domain)
2135 .unwrap_or(domain_from_layers(&layers)?);
2136 let value_orientation = options
2137 .value
2138 .map(|mapping| mapping.orientation)
2139 .unwrap_or(Orientation::MinMax);
2140 let mut point_series = Vec::with_capacity(layers.len());
2141 for (series_index, layer) in layers.iter().enumerate() {
2142 let present: BTreeSet<_> = logical_series[series_index]
2143 .1
2144 .iter()
2145 .map(|(index, _)| *index)
2146 .collect();
2147 let indexed: Vec<_> = layer
2148 .iter()
2149 .filter(|(index, _, _)| present.contains(index))
2150 .map(|(index, _, upper)| (*index, *upper))
2151 .collect();
2152 let points = category_points(
2153 &indexed,
2154 category_count,
2155 domain,
2156 bounds,
2157 options.category_orientation,
2158 value_orientation,
2159 )?;
2160 point_series.push(
2161 indexed
2162 .into_iter()
2163 .map(|(index, _)| index)
2164 .zip(points)
2165 .collect(),
2166 );
2167 }
2168 Ok(point_series)
2169}
2170
2171fn point_in_rect(point: Point, bounds: Rect) -> bool {
2172 const EPSILON: f64 = 1.0e-9;
2173 point.x >= bounds.x - EPSILON
2174 && point.x <= bounds.x + bounds.width + EPSILON
2175 && point.y >= bounds.y - EPSILON
2176 && point.y <= bounds.y + bounds.height + EPSILON
2177}
2178
2179fn resolve_series_colours(
2180 plot: &Plot,
2181 theme: &CT_OfficeStyleSheet,
2182 color_map: &ColorMap,
2183) -> Result<Vec<Color>> {
2184 let lookup = theme_colour_lookup(theme);
2185 plot_series(plot)
2186 .iter()
2187 .enumerate()
2188 .map(|(index, series)| {
2189 if let Some(properties) = series.sp_pr.as_ref() {
2190 if let Some(fill) = properties.fill.as_ref() {
2191 return resolve_series_paint(fill, index, "fill", theme, color_map, &lookup);
2192 }
2193 if let Some(fill) = properties.line.as_ref().and_then(|line| line.fill.as_ref()) {
2194 return resolve_series_paint(fill, index, "line", theme, color_map, &lookup);
2195 }
2196 }
2197 resolve_series_accent(index, theme, color_map, &lookup)
2198 })
2199 .collect()
2200}
2201
2202fn resolve_series_paint(
2203 fill: &Fill,
2204 series_index: usize,
2205 source: &str,
2206 theme: &CT_OfficeStyleSheet,
2207 color_map: &ColorMap,
2208 lookup: &[(&str, RgbColor)],
2209) -> Result<Color> {
2210 match fill {
2211 Fill::NoFill(_) => Ok(Color {
2212 r: 0.0,
2213 g: 0.0,
2214 b: 0.0,
2215 a: 0.0,
2216 }),
2217 Fill::Solid(solid) => {
2218 let choice = solid
2219 .color
2220 .as_ref()
2221 .ok_or_else(|| ChartError::InvalidValue {
2222 element: format!("c:ser[{series_index}] direct {source} paint"),
2223 value: "solid fill has no colour choice".to_owned(),
2224 })?;
2225 resolve_direct_chart_colour(choice, theme, color_map, lookup, || {
2226 format!("c:ser[{series_index}] direct {source} paint")
2227 })
2228 }
2229 Fill::Gradient(_) => unsupported_series_paint(series_index, source, "gradient"),
2230 Fill::Pattern(_) => unsupported_series_paint(series_index, source, "pattern"),
2231 Fill::Blip(_) => unsupported_series_paint(series_index, source, "picture"),
2232 }
2233}
2234
2235fn resolve_direct_chart_colour(
2236 choice: &ColorChoice,
2237 theme: &CT_OfficeStyleSheet,
2238 color_map: &ColorMap,
2239 lookup: &[(&str, RgbColor)],
2240 element: impl FnOnce() -> String,
2241) -> Result<Color> {
2242 let ColorChoice::Scheme {
2243 value, transforms, ..
2244 } = choice
2245 else {
2246 return resolve_chart_colour(choice, color_map, lookup, element);
2247 };
2248 let theme_slot =
2249 mapped_theme_slot(value, color_map).ok_or_else(|| ChartError::InvalidValue {
2250 element: element(),
2251 value: format!("no concrete theme slot is available for scheme colour {value}"),
2252 })?;
2253 let theme_choice = theme.theme_elements.color_scheme.color(theme_slot);
2254 let base = concrete_theme_colour(theme_choice).ok_or_else(|| ChartError::InvalidValue {
2255 element: format!("a:clrScheme/a:{}", theme_slot.as_str()),
2256 value: "selected theme slot has no concrete sRGB or system fallback colour".to_owned(),
2257 })?;
2258 let mut combined = theme_choice.transforms().to_vec();
2259 combined.extend_from_slice(transforms);
2260 Ok(resolved_layout_colour(apply_color_transforms(
2261 base, &combined,
2262 )))
2263}
2264
2265fn mapped_theme_slot(value: &str, color_map: &ColorMap) -> Option<ThemeColorSlot> {
2266 let mapped = match value {
2267 "bg1" => color_map.theme_slot(ColorMapSlot::Background1),
2268 "tx1" => color_map.theme_slot(ColorMapSlot::Text1),
2269 "bg2" => color_map.theme_slot(ColorMapSlot::Background2),
2270 "tx2" => color_map.theme_slot(ColorMapSlot::Text2),
2271 "accent1" => color_map.theme_slot(ColorMapSlot::Accent1),
2272 "accent2" => color_map.theme_slot(ColorMapSlot::Accent2),
2273 "accent3" => color_map.theme_slot(ColorMapSlot::Accent3),
2274 "accent4" => color_map.theme_slot(ColorMapSlot::Accent4),
2275 "accent5" => color_map.theme_slot(ColorMapSlot::Accent5),
2276 "accent6" => color_map.theme_slot(ColorMapSlot::Accent6),
2277 "hlink" => color_map.theme_slot(ColorMapSlot::Hyperlink),
2278 "folHlink" => color_map.theme_slot(ColorMapSlot::FollowedHyperlink),
2279 "dk1" => ThemeColorSlot::Dark1,
2280 "lt1" => ThemeColorSlot::Light1,
2281 "dk2" => ThemeColorSlot::Dark2,
2282 "lt2" => ThemeColorSlot::Light2,
2283 _ => return None,
2284 };
2285 Some(mapped)
2286}
2287
2288fn unsupported_series_paint(series_index: usize, source: &str, kind: &str) -> Result<Color> {
2289 Err(ChartError::InvalidValue {
2290 element: format!("c:ser[{series_index}] direct {source} paint"),
2291 value: format!("unsupported {kind} paint"),
2292 })
2293}
2294
2295fn resolve_series_accent(
2296 series_index: usize,
2297 theme: &CT_OfficeStyleSheet,
2298 color_map: &ColorMap,
2299 lookup: &[(&str, RgbColor)],
2300) -> Result<Color> {
2301 const ACCENTS: [ColorMapSlot; 6] = [
2302 ColorMapSlot::Accent1,
2303 ColorMapSlot::Accent2,
2304 ColorMapSlot::Accent3,
2305 ColorMapSlot::Accent4,
2306 ColorMapSlot::Accent5,
2307 ColorMapSlot::Accent6,
2308 ];
2309 let semantic_slot = ACCENTS[series_index % ACCENTS.len()];
2310 let theme_slot = color_map.theme_slot(semantic_slot);
2311 let choice = theme.theme_elements.color_scheme.color(theme_slot);
2312 resolve_chart_colour(choice, color_map, lookup, || {
2313 format!("c:ser[{series_index}] theme accent")
2314 })
2315}
2316
2317fn resolve_chart_colour(
2318 choice: &ColorChoice,
2319 color_map: &ColorMap,
2320 lookup: &[(&str, RgbColor)],
2321 element: impl FnOnce() -> String,
2322) -> Result<Color> {
2323 resolve_color(choice, color_map, lookup)
2324 .map(resolved_layout_colour)
2325 .map_err(|error| ChartError::InvalidValue {
2326 element: element(),
2327 value: error.to_string(),
2328 })
2329}
2330
2331fn theme_colour_lookup(theme: &CT_OfficeStyleSheet) -> Vec<(&'static str, RgbColor)> {
2332 theme
2333 .theme_elements
2334 .color_scheme
2335 .iter()
2336 .filter_map(|(slot, choice)| {
2337 concrete_theme_colour(choice).map(|colour| (slot.as_str(), colour))
2338 })
2339 .collect()
2340}
2341
2342fn concrete_theme_colour(choice: &ColorChoice) -> Option<RgbColor> {
2343 match choice {
2344 ColorChoice::Srgb { value, .. } => Some(*value),
2345 ColorChoice::System { last_color, .. } => *last_color,
2346 ColorChoice::Scheme { .. } | ColorChoice::Preset { .. } => None,
2347 }
2348}
2349
2350fn resolved_layout_colour(colour: ResolvedColor) -> Color {
2351 Color {
2352 r: f64::from(colour.red) / 255.0,
2353 g: f64::from(colour.green) / 255.0,
2354 b: f64::from(colour.blue) / 255.0,
2355 a: f64::from(colour.alpha) / 255.0,
2356 }
2357}
2358
2359fn render_legend(
2360 chart: &CT_Chart,
2361 plot: &Plot,
2362 bounds: Rect,
2363 fonts: &mut FontManager,
2364 series_colours: &[Color],
2365 paths: &mut Vec<PositionedElement>,
2366 labels: &mut Vec<PositionedElement>,
2367) -> Result<()> {
2368 if chart.legend.is_none() {
2369 return Ok(());
2370 }
2371 let x = bounds.x + bounds.width - 58.0;
2372 for (index, (series, colour)) in plot_series(plot).iter().zip(series_colours).enumerate() {
2373 let y = bounds.y + 6.0 + index as f64 * 13.0;
2374 paths.push(filled_path(
2375 Path::rect(Rect {
2376 x,
2377 y,
2378 width: 7.0,
2379 height: 7.0,
2380 }),
2381 *colour,
2382 ));
2383 labels.push(PositionedElement::Text(shape_label(
2384 fonts,
2385 &series_name(series, index),
2386 Point {
2387 x: x + 10.0,
2388 y: y + 7.0,
2389 },
2390 )?));
2391 }
2392 Ok(())
2393}
2394
2395fn validate_geometry_coordinates(elements: &[PositionedElement]) -> Result<()> {
2396 fn finite_point(point: Point) -> bool {
2397 point.x.is_finite() && point.y.is_finite()
2398 }
2399
2400 for element in elements {
2401 match element {
2402 PositionedElement::Path(path) => {
2403 for command in &path.path.commands {
2404 let finite = match command {
2405 PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
2406 finite_point(*point)
2407 }
2408 PathCommand::CurveTo { c1, c2, to } => {
2409 finite_point(*c1) && finite_point(*c2) && finite_point(*to)
2410 }
2411 PathCommand::Close => true,
2412 };
2413 if !finite {
2414 return Err(ChartError::InvalidValue {
2415 element: "chart geometry coordinate".to_owned(),
2416 value: "generated path contains a nonfinite point".to_owned(),
2417 });
2418 }
2419 }
2420 }
2421 PositionedElement::Group(group) => validate_geometry_coordinates(&group.children)?,
2422 _ => {}
2423 }
2424 }
2425 Ok(())
2426}
2427
2428fn render_plot_geometry(
2429 plot: &Plot,
2430 bounds: Rect,
2431 blanks: DispBlanksAs,
2432 options: GeometryOptions,
2433 series_colours: &[Color],
2434) -> Result<Vec<PositionedElement>> {
2435 match plot {
2436 Plot::Bar {
2437 direction,
2438 grouping,
2439 gap_width,
2440 overlap,
2441 series,
2442 ..
2443 } => render_bar_geometry(
2444 *direction,
2445 *grouping,
2446 *gap_width,
2447 *overlap,
2448 series,
2449 bounds,
2450 options,
2451 series_colours,
2452 ),
2453 Plot::Line {
2454 grouping,
2455 marker,
2456 series,
2457 ..
2458 } => render_line_geometry(
2459 *grouping,
2460 *marker,
2461 series,
2462 bounds,
2463 blanks,
2464 options,
2465 series_colours,
2466 ),
2467 Plot::Pie {
2468 first_slice_angle,
2469 series,
2470 ..
2471 } => render_pie_geometry(*first_slice_angle, None, series, bounds, series_colours),
2472 Plot::Doughnut {
2473 first_slice_angle,
2474 hole_size,
2475 series,
2476 ..
2477 } => render_pie_geometry(
2478 *first_slice_angle,
2479 Some(*hole_size),
2480 series,
2481 bounds,
2482 series_colours,
2483 ),
2484 Plot::Area {
2485 grouping, series, ..
2486 } => render_area_geometry(*grouping, series, bounds, blanks, options, series_colours),
2487 Plot::Scatter { style, series, .. } => {
2488 render_scatter_geometry(*style, series, bounds, blanks, options, series_colours)
2489 }
2490 Plot::Radar { style, series, .. } => {
2491 render_radar_geometry(*style, series, bounds, options, series_colours)
2492 }
2493 }
2494}
2495
2496#[allow(clippy::too_many_arguments)]
2497fn render_bar_geometry(
2498 direction: BarDirection,
2499 grouping: BarGrouping,
2500 gap_width: u16,
2501 overlap: i8,
2502 series: &[Series],
2503 bounds: Rect,
2504 options: GeometryOptions,
2505 series_colours: &[Color],
2506) -> Result<Vec<PositionedElement>> {
2507 let bars = bar_rectangles(
2508 direction, grouping, gap_width, overlap, series, bounds, options,
2509 )?;
2510 bars.into_iter()
2511 .map(|bar| {
2512 Ok(filled_path(
2513 Path::rect(bar.rect),
2514 series_colour(series_colours, bar.series_index)?,
2515 ))
2516 })
2517 .collect()
2518}
2519
2520#[derive(Clone, Copy, Debug)]
2521struct BarRectangle {
2522 series_index: usize,
2523 logical_index: usize,
2524 rect: Rect,
2525 base: Point,
2526 end: Point,
2527}
2528
2529fn bar_rectangles(
2530 direction: BarDirection,
2531 grouping: BarGrouping,
2532 gap_width: u16,
2533 overlap: i8,
2534 series: &[Series],
2535 bounds: Rect,
2536 options: GeometryOptions,
2537) -> Result<Vec<BarRectangle>> {
2538 validate_series_values(series)?;
2539 let logical_series = logical_series_values(series)?;
2540 let category_count = series_category_count(series)?;
2541 let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
2542 let percent = grouping == BarGrouping::PercentStacked;
2543 let layers = stacked_series_bounds(&logical_series, stacked, percent)?;
2544 let domain = options
2545 .value
2546 .map(|mapping| mapping.domain)
2547 .unwrap_or(domain_from_layers(&layers)?);
2548 let value_orientation = options
2549 .value
2550 .map(|mapping| mapping.orientation)
2551 .unwrap_or(Orientation::MinMax);
2552 let category_extent = if direction == BarDirection::Column {
2553 bounds.width
2554 } else {
2555 bounds.height
2556 };
2557 let slot = category_extent / category_count as f64;
2558 let cluster_extent = slot * 100.0 / (100.0 + f64::from(gap_width));
2559 let bars_per_cluster = if stacked { 1 } else { series.len() };
2560 let overlap_fraction = if stacked {
2561 0.0
2562 } else {
2563 f64::from(overlap) / 100.0
2564 };
2565 let divisor =
2566 bars_per_cluster as f64 - (bars_per_cluster.saturating_sub(1)) as f64 * overlap_fraction;
2567 let bar_extent = cluster_extent / divisor;
2568 let advance = bar_extent * (1.0 - overlap_fraction);
2569 let mut bars = Vec::new();
2570 for (series_index, layer) in layers.iter().enumerate() {
2571 for &(logical_index, start, end) in layer {
2572 let category = oriented_category_index(
2573 logical_index,
2574 category_count,
2575 options.category_orientation,
2576 );
2577 let cluster_start = category as f64 / category_count as f64 * category_extent
2578 + (slot - cluster_extent) / 2.0;
2579 let category_start = if stacked {
2580 cluster_start
2581 } else {
2582 cluster_start + series_index as f64 * advance
2583 };
2584 let (rect, base, end) = if direction == BarDirection::Column {
2585 let y0 = map_y_oriented(start, domain, bounds, value_orientation)?;
2586 let y1 = map_y_oriented(end, domain, bounds, value_orientation)?;
2587 let center_x = bounds.x + category_start + bar_extent / 2.0;
2588 (
2589 Rect {
2590 x: bounds.x + category_start,
2591 y: y0.min(y1),
2592 width: bar_extent,
2593 height: (y1 - y0).abs(),
2594 },
2595 Point { x: center_x, y: y0 },
2596 Point { x: center_x, y: y1 },
2597 )
2598 } else {
2599 let x0 = map_x_oriented(start, domain, bounds, value_orientation)?;
2600 let x1 = map_x_oriented(end, domain, bounds, value_orientation)?;
2601 let center_y = bounds.y + category_start + bar_extent / 2.0;
2602 (
2603 Rect {
2604 x: x0.min(x1),
2605 y: bounds.y + category_start,
2606 width: (x1 - x0).abs(),
2607 height: bar_extent,
2608 },
2609 Point { x: x0, y: center_y },
2610 Point { x: x1, y: center_y },
2611 )
2612 };
2613 bars.push(BarRectangle {
2614 series_index,
2615 logical_index,
2616 rect,
2617 base,
2618 end,
2619 });
2620 }
2621 }
2622 Ok(bars)
2623}
2624
2625fn render_line_geometry(
2626 grouping: Grouping,
2627 marker: bool,
2628 series: &[Series],
2629 bounds: Rect,
2630 blanks: DispBlanksAs,
2631 options: GeometryOptions,
2632 series_colours: &[Color],
2633) -> Result<Vec<PositionedElement>> {
2634 validate_series_values(series)?;
2635 let logical_series = logical_series_values(series)?;
2636 let category_count = series_category_count(series)?;
2637 let calculated_series = if blanks == DispBlanksAs::Zero {
2638 zero_filled_series(&logical_series, category_count)
2639 } else {
2640 logical_series.clone()
2641 };
2642 let stacked = grouping != Grouping::Standard;
2643 let percent = grouping == Grouping::PercentStacked;
2644 let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
2645 let domain = options
2646 .value
2647 .map(|mapping| mapping.domain)
2648 .unwrap_or(domain_from_layers(&layers)?);
2649 let value_orientation = options
2650 .value
2651 .map(|mapping| mapping.orientation)
2652 .unwrap_or(Orientation::MinMax);
2653 let mut elements = Vec::new();
2654 for (series_index, layer) in layers.iter().enumerate() {
2655 let colour = series_colour(series_colours, series_index)?;
2656 let indexed: Vec<_> = layer
2657 .iter()
2658 .map(|(index, _, upper)| (*index, *upper))
2659 .collect();
2660 let indexes: Vec<_> = indexed.iter().map(|(index, _)| *index).collect();
2661 for (start, end) in contiguous_ranges(&indexes, blanks) {
2662 let points = category_points(
2663 &indexed[start..end],
2664 category_count,
2665 domain,
2666 bounds,
2667 options.category_orientation,
2668 value_orientation,
2669 )?;
2670 elements.push(stroked_path(polyline_path(&points, false), colour));
2671 }
2672 if marker {
2673 let present: BTreeSet<_> = logical_series[series_index]
2674 .1
2675 .iter()
2676 .map(|(index, _)| *index)
2677 .collect();
2678 let marker_values: Vec<_> = indexed
2679 .iter()
2680 .copied()
2681 .filter(|(index, _)| present.contains(index))
2682 .collect();
2683 let marker_points = category_points(
2684 &marker_values,
2685 category_count,
2686 domain,
2687 bounds,
2688 options.category_orientation,
2689 value_orientation,
2690 )?;
2691 push_markers(&mut elements, &marker_points, colour);
2692 }
2693 }
2694 Ok(elements)
2695}
2696
2697fn render_area_geometry(
2698 grouping: Grouping,
2699 series: &[Series],
2700 bounds: Rect,
2701 blanks: DispBlanksAs,
2702 options: GeometryOptions,
2703 series_colours: &[Color],
2704) -> Result<Vec<PositionedElement>> {
2705 validate_series_values(series)?;
2706 let logical_series = logical_series_values(series)?;
2707 let category_count = series_category_count(series)?;
2708 let calculated_series = if blanks == DispBlanksAs::Zero {
2709 zero_filled_series(&logical_series, category_count)
2710 } else {
2711 logical_series
2712 };
2713 let stacked = grouping != Grouping::Standard;
2714 let percent = grouping == Grouping::PercentStacked;
2715 let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
2716 let domain = options
2717 .value
2718 .map(|mapping| mapping.domain)
2719 .unwrap_or(domain_from_layers(&layers)?);
2720 let value_orientation = options
2721 .value
2722 .map(|mapping| mapping.orientation)
2723 .unwrap_or(Orientation::MinMax);
2724 let mut elements = Vec::new();
2725 for (series_index, layer) in layers.iter().enumerate() {
2726 let colour = series_colour(series_colours, series_index)?;
2727 let indexes: Vec<_> = layer.iter().map(|(index, _, _)| *index).collect();
2728 for (start, end) in contiguous_ranges(&indexes, blanks) {
2729 let segment = &layer[start..end];
2730 let top_values: Vec<_> = segment
2731 .iter()
2732 .map(|(index, _, upper)| (*index, *upper))
2733 .collect();
2734 let lower_values: Vec<_> = segment
2735 .iter()
2736 .map(|(index, lower, _)| (*index, *lower))
2737 .collect();
2738 let top = category_points(
2739 &top_values,
2740 category_count,
2741 domain,
2742 bounds,
2743 options.category_orientation,
2744 value_orientation,
2745 )?;
2746 let mut bottom = category_points(
2747 &lower_values,
2748 category_count,
2749 domain,
2750 bounds,
2751 options.category_orientation,
2752 value_orientation,
2753 )?;
2754 bottom.reverse();
2755 let mut commands = Vec::with_capacity(top.len() + bottom.len() + 2);
2756 commands.push(PathCommand::MoveTo(top[0]));
2757 commands.extend(top.iter().skip(1).copied().map(PathCommand::LineTo));
2758 commands.extend(bottom.into_iter().map(PathCommand::LineTo));
2759 commands.push(PathCommand::Close);
2760 elements.push(filled_and_stroked_path(
2761 Path {
2762 commands,
2763 fill_rule: FillRule::NonZero,
2764 },
2765 colour,
2766 ));
2767 }
2768 }
2769 Ok(elements)
2770}
2771
2772fn render_scatter_geometry(
2773 style: ScatterStyle,
2774 series: &[Series],
2775 bounds: Rect,
2776 blanks: DispBlanksAs,
2777 options: GeometryOptions,
2778 series_colours: &[Color],
2779) -> Result<Vec<PositionedElement>> {
2780 validate_series_values(series)?;
2781 let mut paired_series = Vec::with_capacity(series.len());
2782 let mut marker_series = Vec::with_capacity(series.len());
2783 let mut all_x = Vec::new();
2784 let mut all_y = Vec::new();
2785 for item in series {
2786 let markers = matched_scatter_points(item)?;
2787 let paired = scatter_render_points(item, blanks)?;
2788 all_x.extend(paired.iter().map(|(_, x, _)| *x));
2789 all_y.extend(paired.iter().map(|(_, _, y)| *y));
2790 paired_series.push(paired);
2791 marker_series.push(markers);
2792 }
2793 let x_domain = options
2794 .scatter_x
2795 .map(|mapping| mapping.domain)
2796 .unwrap_or(data_domain(&all_x, false)?);
2797 let y_domain = options
2798 .scatter_y
2799 .map(|mapping| mapping.domain)
2800 .unwrap_or(data_domain(&all_y, false)?);
2801 let x_orientation = options
2802 .scatter_x
2803 .map(|mapping| mapping.orientation)
2804 .unwrap_or(Orientation::MinMax);
2805 let y_orientation = options
2806 .scatter_y
2807 .map(|mapping| mapping.orientation)
2808 .unwrap_or(Orientation::MinMax);
2809 let draw_line = matches!(
2810 style,
2811 ScatterStyle::Line
2812 | ScatterStyle::LineMarker
2813 | ScatterStyle::Smooth
2814 | ScatterStyle::SmoothMarker
2815 );
2816 let draw_marker = matches!(
2817 style,
2818 ScatterStyle::Marker | ScatterStyle::LineMarker | ScatterStyle::SmoothMarker
2819 );
2820 let mut elements = Vec::new();
2821 for (series_index, paired) in paired_series.iter().enumerate() {
2822 let colour = series_colour(series_colours, series_index)?;
2823 if draw_line {
2824 let indexes: Vec<_> = paired.iter().map(|(index, _, _)| *index).collect();
2825 for (start, end) in contiguous_ranges(&indexes, blanks) {
2826 let points = scatter_points(
2827 &paired[start..end],
2828 x_domain,
2829 y_domain,
2830 bounds,
2831 x_orientation,
2832 y_orientation,
2833 )?;
2834 elements.push(stroked_path(polyline_path(&points, false), colour));
2835 }
2836 }
2837 if draw_marker {
2838 let marker_points = scatter_points(
2839 &marker_series[series_index],
2840 x_domain,
2841 y_domain,
2842 bounds,
2843 x_orientation,
2844 y_orientation,
2845 )?;
2846 push_markers(&mut elements, &marker_points, colour);
2847 }
2848 }
2849 Ok(elements)
2850}
2851
2852type ScatterPoint = (usize, f64, f64);
2853
2854struct ScatterPointMaps {
2855 x_count: usize,
2856 y_count: usize,
2857 x_by_index: BTreeMap<usize, f64>,
2858 y_by_index: BTreeMap<usize, f64>,
2859}
2860
2861fn scatter_point_maps(series: &Series) -> Result<ScatterPointMaps> {
2862 let Some(AxisData::Numeric(x_values)) = &series.categories else {
2863 return Err(ChartError::InvalidValue {
2864 element: "c:xVal".to_owned(),
2865 value: "scatter series requires numeric cached x values".to_owned(),
2866 });
2867 };
2868 validate_finite(&x_values.values, "c:xVal/c:numCache")?;
2869 let (x_count, x_points) = logical_numeric_values(x_values)?;
2870 let (y_count, y_points) = logical_numeric_values(&series.values)?;
2871 Ok(ScatterPointMaps {
2872 x_count,
2873 y_count,
2874 x_by_index: x_points.into_iter().collect(),
2875 y_by_index: y_points.into_iter().collect(),
2876 })
2877}
2878
2879fn matched_scatter_points(series: &Series) -> Result<Vec<ScatterPoint>> {
2880 let points = scatter_point_maps(series)?;
2881 Ok(points
2882 .y_by_index
2883 .iter()
2884 .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
2885 .collect())
2886}
2887
2888fn scatter_render_points(series: &Series, blanks: DispBlanksAs) -> Result<Vec<ScatterPoint>> {
2889 let points = scatter_point_maps(series)?;
2890 if blanks != DispBlanksAs::Zero {
2891 return Ok(points
2892 .y_by_index
2893 .iter()
2894 .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
2895 .collect());
2896 }
2897 let count = points.x_count.max(points.y_count);
2898 let present = points
2899 .x_by_index
2900 .keys()
2901 .chain(points.y_by_index.keys())
2902 .copied()
2903 .collect();
2904 Ok(zero_control_indexes(count, &present)
2905 .into_iter()
2906 .map(|index| {
2907 (
2908 index,
2909 points.x_by_index.get(&index).copied().unwrap_or(0.0),
2910 points.y_by_index.get(&index).copied().unwrap_or(0.0),
2911 )
2912 })
2913 .collect())
2914}
2915
2916fn scatter_points(
2917 values: &[(usize, f64, f64)],
2918 x_domain: Domain,
2919 y_domain: Domain,
2920 bounds: Rect,
2921 x_orientation: Orientation,
2922 y_orientation: Orientation,
2923) -> Result<Vec<Point>> {
2924 values
2925 .iter()
2926 .map(|(_, x, y)| {
2927 Ok(Point {
2928 x: map_x_oriented(*x, x_domain, bounds, x_orientation)?,
2929 y: map_y_oriented(*y, y_domain, bounds, y_orientation)?,
2930 })
2931 })
2932 .collect()
2933}
2934
2935fn render_radar_geometry(
2936 style: RadarStyle,
2937 series: &[Series],
2938 bounds: Rect,
2939 options: GeometryOptions,
2940 series_colours: &[Color],
2941) -> Result<Vec<PositionedElement>> {
2942 let point_series = radar_series_points(series, bounds, options)?;
2943 let mut elements = Vec::new();
2944 for (series_index, points) in point_series.iter().enumerate() {
2945 let colour = series_colour(series_colours, series_index)?;
2946 let plain_points: Vec<_> = points.iter().map(|(_, point)| *point).collect();
2947 if plain_points.is_empty() {
2948 continue;
2949 }
2950 let path = polyline_path(&plain_points, true);
2951 if style == RadarStyle::Filled {
2952 elements.push(filled_and_stroked_path(path, colour));
2953 } else {
2954 elements.push(stroked_path(path, colour));
2955 }
2956 if style == RadarStyle::Marker {
2957 push_markers(&mut elements, &plain_points, colour);
2958 }
2959 }
2960 Ok(elements)
2961}
2962
2963fn radar_series_points(
2964 series: &[Series],
2965 bounds: Rect,
2966 options: GeometryOptions,
2967) -> Result<Vec<Vec<(usize, Point)>>> {
2968 validate_series_values(series)?;
2969 let logical_series = logical_series_values(series)?;
2970 let category_count = series_category_count(series)?;
2971 let values = logical_series
2972 .iter()
2973 .flat_map(|(_, points)| points.iter().map(|(_, value)| *value))
2974 .collect::<Vec<_>>();
2975 let mapping = options.value.unwrap_or(AxisMapping {
2976 domain: data_domain(&values, true)?,
2977 orientation: Orientation::MinMax,
2978 });
2979 let center = Point {
2980 x: bounds.x + bounds.width / 2.0,
2981 y: bounds.y + bounds.height / 2.0,
2982 };
2983 let radius = bounds.width.min(bounds.height) / 2.0;
2984 let mut point_series = Vec::with_capacity(logical_series.len());
2985 for (_, logical) in &logical_series {
2986 let mut points = Vec::with_capacity(logical.len());
2987 for (category, value) in logical {
2988 if *value < mapping.domain.min || *value > mapping.domain.max {
2989 continue;
2990 }
2991 points.push((
2992 *category,
2993 radial_point(
2994 center,
2995 normalized_value_oriented(*value, mapping.domain, mapping.orientation)?
2996 * radius,
2997 oriented_category_index(
2998 *category,
2999 category_count,
3000 options.category_orientation,
3001 ),
3002 category_count,
3003 ),
3004 ));
3005 }
3006 point_series.push(points);
3007 }
3008 Ok(point_series)
3009}
3010
3011fn render_pie_geometry(
3012 first_slice_angle: u16,
3013 hole_size: Option<u8>,
3014 series: &[Series],
3015 bounds: Rect,
3016 series_colours: &[Color],
3017) -> Result<Vec<PositionedElement>> {
3018 pie_slices(first_slice_angle, hole_size, series, bounds)?
3019 .into_iter()
3020 .map(|slice| {
3021 Ok(filled_path(
3022 slice.path,
3023 series_colour(series_colours, slice.series_index)?,
3024 ))
3025 })
3026 .collect()
3027}
3028
3029struct PieSliceGeometry {
3030 series_index: usize,
3031 logical_index: usize,
3032 path: Path,
3033 anchor: Point,
3034 base: Point,
3035 end: Point,
3036}
3037
3038fn pie_slices(
3039 first_slice_angle: u16,
3040 hole_size: Option<u8>,
3041 series: &[Series],
3042 bounds: Rect,
3043) -> Result<Vec<PieSliceGeometry>> {
3044 validate_series_values(series)?;
3045 let center = Point {
3046 x: bounds.x + bounds.width / 2.0,
3047 y: bounds.y + bounds.height / 2.0,
3048 };
3049 let radius = bounds.width.min(bounds.height) / 2.0;
3050 let mut elements = Vec::new();
3051 for (series_index, item) in series.iter().enumerate() {
3052 let (_, logical) = logical_numeric_values(&item.values)?;
3053 let total = logical
3054 .iter()
3055 .map(|(_, value)| *value)
3056 .filter(|value| *value > 0.0)
3057 .try_fold(0.0, |total, value| {
3058 checked_geometry_sum(total, value, "pie value total")
3059 })?;
3060 if total <= 0.0 {
3061 continue;
3062 }
3063 let mut angle = f64::from(first_slice_angle).to_radians() - std::f64::consts::FRAC_PI_2;
3064 for (logical_index, value) in logical {
3065 if value <= 0.0 {
3066 continue;
3067 }
3068 let next = angle + value / total * std::f64::consts::TAU;
3069 let path = if let Some(hole_size) = hole_size {
3070 doughnut_wedge(
3071 center,
3072 radius,
3073 radius * f64::from(hole_size) / 100.0,
3074 angle,
3075 next,
3076 )
3077 } else {
3078 pie_wedge(center, radius, angle, next)
3079 };
3080 let inner_radius = hole_size
3081 .map(|hole| radius * f64::from(hole) / 100.0)
3082 .unwrap_or(0.0);
3083 let anchor_radius = hole_size
3084 .map(|_| (radius + inner_radius) / 2.0)
3085 .unwrap_or(radius * 2.0 / 3.0);
3086 let midpoint = (angle + next) / 2.0;
3087 elements.push(PieSliceGeometry {
3088 series_index,
3089 logical_index,
3090 path,
3091 anchor: circle_point(center, anchor_radius, midpoint),
3092 base: circle_point(center, inner_radius, midpoint),
3093 end: circle_point(center, radius, midpoint),
3094 });
3095 angle = next;
3096 }
3097 }
3098 Ok(elements)
3099}
3100
3101fn validate_series_values(series: &[Series]) -> Result<()> {
3102 if series.is_empty() {
3103 return Err(ChartError::MissingElement("c:ser".to_owned()));
3104 }
3105 for item in series {
3106 validate_finite(&item.values.values, "c:val/c:numCache")?;
3107 }
3108 Ok(())
3109}
3110
3111fn validate_finite(values: &[f64], element: &str) -> Result<()> {
3112 if let Some(value) = values.iter().find(|value| !value.is_finite()) {
3113 return Err(ChartError::InvalidValue {
3114 element: element.to_owned(),
3115 value: value.to_string(),
3116 });
3117 }
3118 Ok(())
3119}
3120
3121type IndexedValues = Vec<(usize, f64)>;
3122type LogicalSeries = (usize, IndexedValues);
3123type GeometryLayer = Vec<(usize, f64, f64)>;
3124
3125fn zero_control_indexes(count: usize, present: &BTreeSet<usize>) -> BTreeSet<usize> {
3126 let mut controls = present.clone();
3127 if count == 0 {
3128 return controls;
3129 }
3130 controls.insert(0);
3131 controls.insert(count - 1);
3132 for index in present {
3133 if *index > 0 {
3134 controls.insert(*index - 1);
3135 }
3136 if index + 1 < count {
3137 controls.insert(index + 1);
3138 }
3139 }
3140 controls
3141}
3142
3143fn zero_filled_series(series: &[LogicalSeries], count: usize) -> Vec<LogicalSeries> {
3144 let present = series
3145 .iter()
3146 .flat_map(|(_, points)| points.iter().map(|(index, _)| *index))
3147 .collect();
3148 let controls = zero_control_indexes(count, &present);
3149 series
3150 .iter()
3151 .map(|(declared, points)| {
3152 let values: BTreeMap<_, _> = points.iter().copied().collect();
3153 let filled = controls
3154 .iter()
3155 .map(|index| (*index, values.get(index).copied().unwrap_or(0.0)))
3156 .collect();
3157 (*declared, filled)
3158 })
3159 .collect()
3160}
3161
3162fn contiguous_ranges(indexes: &[usize], blanks: DispBlanksAs) -> Vec<(usize, usize)> {
3163 if indexes.is_empty() {
3164 return Vec::new();
3165 }
3166 if blanks != DispBlanksAs::Gap {
3167 return vec![(0, indexes.len())];
3168 }
3169 let mut ranges = Vec::new();
3170 let mut start = 0usize;
3171 for position in 1..indexes.len() {
3172 if indexes[position - 1].checked_add(1) != Some(indexes[position]) {
3173 ranges.push((start, position));
3174 start = position;
3175 }
3176 }
3177 ranges.push((start, indexes.len()));
3178 ranges
3179}
3180
3181fn logical_numeric_values(data: &NumericData) -> Result<LogicalSeries> {
3182 validate_finite(&data.values, "c:numCache")?;
3183 let (declared, indexes) = cache_layout(&data.markup, data.values.len())?;
3184 let declared = usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
3185 element: "c:ptCount".to_owned(),
3186 value: declared.to_string(),
3187 })?;
3188 let points = indexes
3189 .into_iter()
3190 .zip(data.values.iter().copied())
3191 .map(|(index, value)| {
3192 usize::try_from(index)
3193 .map(|index| (index, value))
3194 .map_err(|_| ChartError::InvalidValue {
3195 element: "c:pt/@idx".to_owned(),
3196 value: index.to_string(),
3197 })
3198 })
3199 .collect::<Result<Vec<_>>>()?;
3200 Ok((declared, points))
3201}
3202
3203fn logical_series_values(series: &[Series]) -> Result<Vec<LogicalSeries>> {
3204 series
3205 .iter()
3206 .map(|item| logical_numeric_values(&item.values))
3207 .collect()
3208}
3209
3210fn reference_logical_count(markup: &ReferenceMarkup, value_count: usize) -> Result<usize> {
3211 let (declared, _) = cache_layout(markup, value_count)?;
3212 usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
3213 element: "c:ptCount".to_owned(),
3214 value: declared.to_string(),
3215 })
3216}
3217
3218fn series_category_count(series: &[Series]) -> Result<usize> {
3219 let mut count = 0usize;
3220 for item in series {
3221 count = count.max(reference_logical_count(
3222 &item.values.markup,
3223 item.values.values.len(),
3224 )?);
3225 if let Some(categories) = &item.categories {
3226 let category_count = match categories {
3227 AxisData::String(data) => reference_logical_count(&data.markup, data.values.len())?,
3228 AxisData::Numeric(data) => {
3229 reference_logical_count(&data.markup, data.values.len())?
3230 }
3231 };
3232 count = count.max(category_count);
3233 }
3234 }
3235 if count == 0 {
3236 return Err(ChartError::InvalidValue {
3237 element: "c:ser".to_owned(),
3238 value: "cached series contain no points".to_owned(),
3239 });
3240 }
3241 Ok(count)
3242}
3243
3244fn stacked_series_bounds(
3245 series: &[LogicalSeries],
3246 stacked: bool,
3247 percent: bool,
3248) -> Result<Vec<GeometryLayer>> {
3249 let mut positive_totals = BTreeMap::<usize, f64>::new();
3250 let mut negative_totals = BTreeMap::<usize, f64>::new();
3251 if percent {
3252 for (_, points) in series {
3253 for &(index, value) in points {
3254 let totals = if value >= 0.0 {
3255 &mut positive_totals
3256 } else {
3257 &mut negative_totals
3258 };
3259 let total = totals.entry(index).or_default();
3260 *total = checked_geometry_sum(*total, value.abs(), "stacked percentage total")?;
3261 }
3262 }
3263 }
3264 let mut positive = BTreeMap::<usize, f64>::new();
3265 let mut negative = BTreeMap::<usize, f64>::new();
3266 let mut layers = Vec::with_capacity(series.len());
3267 for (_, points) in series {
3268 let mut layer = Vec::with_capacity(points.len());
3269 for &(index, original) in points {
3270 let total = if original >= 0.0 {
3271 positive_totals.get(&index).copied().unwrap_or(0.0)
3272 } else {
3273 negative_totals.get(&index).copied().unwrap_or(0.0)
3274 };
3275 let value = if percent && total != 0.0 {
3276 original / total
3277 } else {
3278 original
3279 };
3280 if stacked {
3281 let accumulators = if value >= 0.0 {
3282 &mut positive
3283 } else {
3284 &mut negative
3285 };
3286 let accumulator = accumulators.entry(index).or_default();
3287 let lower = *accumulator;
3288 *accumulator = checked_geometry_sum(*accumulator, value, "stacked value total")?;
3289 layer.push((index, lower, *accumulator));
3290 } else {
3291 layer.push((index, 0.0, value));
3292 }
3293 }
3294 layers.push(layer);
3295 }
3296 Ok(layers)
3297}
3298
3299fn checked_geometry_sum(current: f64, value: f64, context: &str) -> Result<f64> {
3300 let sum = current + value;
3301 if sum.is_finite() {
3302 Ok(sum)
3303 } else {
3304 Err(ChartError::InvalidValue {
3305 element: "chart geometry aggregate".to_owned(),
3306 value: format!("{context} is nonfinite"),
3307 })
3308 }
3309}
3310
3311#[derive(Clone, Copy, Debug, PartialEq)]
3312struct Domain {
3313 min: f64,
3314 max: f64,
3315}
3316
3317fn nice_number_scale(
3318 extent: Domain,
3319 pinned_minimum: Option<f64>,
3320 pinned_maximum: Option<f64>,
3321 target_tick_count: usize,
3322) -> Result<AxisScale> {
3323 if target_tick_count < 2
3324 || !extent.min.is_finite()
3325 || !extent.max.is_finite()
3326 || pinned_minimum.is_some_and(|value| !value.is_finite())
3327 || pinned_maximum.is_some_and(|value| !value.is_finite())
3328 {
3329 return Err(ChartError::InvalidValue {
3330 element: "chart axis scale".to_owned(),
3331 value: "scale bounds and target tick count must be finite and valid".to_owned(),
3332 });
3333 }
3334
3335 let mut data_min = extent.min.min(extent.max);
3336 let mut data_max = extent.min.max(extent.max);
3337 if data_min == data_max {
3338 let expanded = expand_constant_domain(data_min)?;
3339 data_min = expanded.min;
3340 data_max = expanded.max;
3341 }
3342 let requested_min = match (pinned_minimum, pinned_maximum) {
3343 (Some(minimum), _) => minimum,
3344 (None, Some(maximum)) if data_min >= maximum => {
3345 extend_bound(maximum, stable_positive_step(data_min, data_max, 3)?, false)?
3346 }
3347 (None, _) => data_min,
3348 };
3349 let requested_max = match (pinned_minimum, pinned_maximum) {
3350 (_, Some(maximum)) => maximum,
3351 (Some(minimum), None) if data_max <= minimum => {
3352 extend_bound(minimum, stable_positive_step(data_min, data_max, 3)?, true)?
3353 }
3354 (_, None) => data_max,
3355 };
3356 if requested_min >= requested_max {
3357 return Err(ChartError::InvalidValue {
3358 element: "c:scaling".to_owned(),
3359 value: format!("minimum {requested_min} is not less than maximum {requested_max}"),
3360 });
3361 }
3362
3363 let raw_step = stable_positive_step(requested_min, requested_max, target_tick_count)?;
3364 let step = nice_step(raw_step)?;
3365 let rounded_minimum = (requested_min / step).floor() * step;
3366 let rounded_maximum = (requested_max / step).ceil() * step;
3367 let lower_fallback = pinned_minimum.is_none() && !rounded_minimum.is_finite();
3368 let upper_fallback = pinned_maximum.is_none() && !rounded_maximum.is_finite();
3369 let minimum = pinned_minimum.unwrap_or(if rounded_minimum.is_finite() {
3370 rounded_minimum
3371 } else {
3372 requested_min
3373 });
3374 let maximum = pinned_maximum.unwrap_or(if rounded_maximum.is_finite() {
3375 rounded_maximum
3376 } else {
3377 requested_max
3378 });
3379 if !minimum.is_finite() || !maximum.is_finite() || minimum >= maximum {
3380 return Err(ChartError::InvalidValue {
3381 element: "chart axis scale".to_owned(),
3382 value: "nice-number bounds are nonfinite or empty".to_owned(),
3383 });
3384 }
3385
3386 let first = (minimum / step).ceil();
3387 let last = (maximum / step).floor();
3388 let tick_count = if last >= first {
3389 (last - first).min(1024.0) as usize + 1
3390 } else {
3391 0
3392 };
3393 let mut ticks = Vec::with_capacity(tick_count);
3394 for index in 0..tick_count {
3395 let tick = (first + index as f64) * step;
3396 if tick.is_finite() && tick >= minimum && tick <= maximum {
3397 ticks.push(clean_tick(tick, step));
3398 }
3399 }
3400 if lower_fallback && ticks.first().is_none_or(|tick| *tick > minimum) {
3401 ticks.insert(0, minimum);
3402 }
3403 if upper_fallback && ticks.last().is_none_or(|tick| *tick < maximum) {
3404 ticks.push(maximum);
3405 }
3406 if ticks.len() < 2 {
3407 ticks.extend([minimum, maximum]);
3408 ticks.sort_by(f64::total_cmp);
3409 }
3410 ticks.dedup_by(|left, right| *left == *right);
3411 if ticks.len() < 2 || !ticks.windows(2).all(|pair| pair[0] < pair[1]) {
3412 return Err(ChartError::InvalidValue {
3413 element: "chart axis scale".to_owned(),
3414 value: "scale cannot produce two increasing finite ticks".to_owned(),
3415 });
3416 }
3417 Ok(AxisScale {
3418 domain: Domain {
3419 min: minimum,
3420 max: maximum,
3421 },
3422 ticks,
3423 })
3424}
3425
3426const MIN_POSITIVE_SUBNORMAL: f64 = f64::from_bits(1);
3427
3428fn stable_positive_step(minimum: f64, maximum: f64, target_tick_count: usize) -> Result<f64> {
3429 let scale = minimum.abs().max(maximum.abs());
3430 let scale = if scale == 0.0 {
3431 MIN_POSITIVE_SUBNORMAL
3432 } else {
3433 scale
3434 };
3435 let normalized =
3436 (maximum / scale - minimum / scale) / target_tick_count.saturating_sub(1) as f64;
3437 let step = normalized * scale;
3438 if normalized.is_finite() && normalized > 0.0 && step.is_finite() {
3439 Ok(step.max(MIN_POSITIVE_SUBNORMAL))
3440 } else {
3441 Err(ChartError::InvalidValue {
3442 element: "chart axis scale".to_owned(),
3443 value: "scale step is nonfinite or empty".to_owned(),
3444 })
3445 }
3446}
3447
3448fn nice_step(raw_step: f64) -> Result<f64> {
3449 if !raw_step.is_finite() || raw_step <= 0.0 {
3450 return Err(ChartError::InvalidValue {
3451 element: "chart axis scale".to_owned(),
3452 value: format!("invalid raw tick step {raw_step}"),
3453 });
3454 }
3455 let magnitude = 10.0_f64
3456 .powf(raw_step.log10().floor())
3457 .max(MIN_POSITIVE_SUBNORMAL);
3458 let fraction = raw_step / magnitude;
3459 let multiplier = if fraction <= 1.0 {
3460 1.0
3461 } else if fraction <= 2.0 {
3462 2.0
3463 } else if fraction <= 5.0 {
3464 5.0
3465 } else {
3466 10.0
3467 };
3468 let step = (multiplier * magnitude).max(MIN_POSITIVE_SUBNORMAL);
3469 if step.is_finite() && step > 0.0 {
3470 Ok(step)
3471 } else {
3472 Err(ChartError::InvalidValue {
3473 element: "chart axis scale".to_owned(),
3474 value: "nice-number step is nonfinite".to_owned(),
3475 })
3476 }
3477}
3478
3479fn expand_constant_domain(value: f64) -> Result<Domain> {
3480 if value == 0.0 {
3481 return Ok(Domain {
3482 min: -1.0,
3483 max: 1.0,
3484 });
3485 }
3486 let padding = (value.abs() * 0.5).max(MIN_POSITIVE_SUBNORMAL);
3487 let minimum = value - padding;
3488 let maximum = value + padding;
3489 if minimum.is_finite() && maximum.is_finite() && minimum < maximum {
3490 return Ok(Domain {
3491 min: minimum,
3492 max: maximum,
3493 });
3494 }
3495 let domain = if value.is_sign_positive() {
3496 Domain {
3497 min: 0.0,
3498 max: value,
3499 }
3500 } else {
3501 Domain {
3502 min: value,
3503 max: 0.0,
3504 }
3505 };
3506 if domain.min < domain.max {
3507 Ok(domain)
3508 } else {
3509 Err(ChartError::InvalidValue {
3510 element: "chart axis scale".to_owned(),
3511 value: format!("constant value {value} has no finite renderable extent"),
3512 })
3513 }
3514}
3515
3516fn extend_bound(bound: f64, span: f64, positive: bool) -> Result<f64> {
3517 let candidate = if positive { bound + span } else { bound - span };
3518 let extended = if candidate.is_finite()
3519 && if positive {
3520 candidate > bound
3521 } else {
3522 candidate < bound
3523 } {
3524 candidate
3525 } else if positive {
3526 bound.next_up()
3527 } else {
3528 bound.next_down()
3529 };
3530 if extended.is_finite()
3531 && if positive {
3532 extended > bound
3533 } else {
3534 extended < bound
3535 }
3536 {
3537 Ok(extended)
3538 } else {
3539 Err(ChartError::InvalidValue {
3540 element: "c:scaling".to_owned(),
3541 value: format!("one-sided bound {bound} leaves no finite automatic extent"),
3542 })
3543 }
3544}
3545
3546fn clean_tick(value: f64, step: f64) -> f64 {
3547 if value.abs() <= step.abs() * 1.0e-12 {
3548 0.0
3549 } else {
3550 value
3551 }
3552}
3553
3554fn domain_from_layers(layers: &[GeometryLayer]) -> Result<Domain> {
3555 let values = layers
3556 .iter()
3557 .flatten()
3558 .flat_map(|(_, lower, upper)| [*lower, *upper]);
3559 let mut min = 0.0_f64;
3560 let mut max = 0.0_f64;
3561 for value in values {
3562 if !value.is_finite() {
3563 return Err(ChartError::InvalidValue {
3564 element: "chart geometry domain".to_owned(),
3565 value: "aggregate produced a nonfinite value".to_owned(),
3566 });
3567 }
3568 min = min.min(value);
3569 max = max.max(value);
3570 }
3571 if min == max {
3572 max = 1.0;
3573 }
3574 Ok(Domain { min, max })
3575}
3576
3577fn data_domain(values: &[f64], include_zero: bool) -> Result<Domain> {
3578 validate_finite(values, "chart geometry cache")?;
3579 let Some(first) = values.first().copied() else {
3580 return Err(ChartError::InvalidValue {
3581 element: "c:numCache".to_owned(),
3582 value: "cached series contain no points".to_owned(),
3583 });
3584 };
3585 let mut min = first;
3586 let mut max = first;
3587 for value in values.iter().copied() {
3588 min = min.min(value);
3589 max = max.max(value);
3590 }
3591 if include_zero {
3592 min = min.min(0.0);
3593 max = max.max(0.0);
3594 }
3595 if min == max {
3596 if !include_zero {
3597 return expand_constant_domain(min);
3598 }
3599 if min > 0.0 {
3600 min = 0.0;
3601 } else if max < 0.0 {
3602 max = 0.0;
3603 } else {
3604 max = 1.0;
3605 }
3606 }
3607 Ok(Domain { min, max })
3608}
3609
3610fn normalized_value(value: f64, domain: Domain) -> Result<f64> {
3611 let scale = value
3612 .abs()
3613 .max(domain.min.abs())
3614 .max(domain.max.abs())
3615 .max(1.0);
3616 let value = value / scale;
3617 let min = domain.min / scale;
3618 let max = domain.max / scale;
3619 let range = max - min;
3620 let normalized = (value - min) / range;
3621 if !range.is_finite() || range <= 0.0 || !normalized.is_finite() {
3622 return Err(ChartError::InvalidValue {
3623 element: "chart geometry domain".to_owned(),
3624 value: format!("cannot map finite domain {} to {}", domain.min, domain.max),
3625 });
3626 }
3627 Ok(normalized)
3628}
3629
3630fn normalized_value_oriented(value: f64, domain: Domain, orientation: Orientation) -> Result<f64> {
3631 let normalized = normalized_value(value, domain)?;
3632 Ok(if orientation == Orientation::MaxMin {
3633 1.0 - normalized
3634 } else {
3635 normalized
3636 })
3637}
3638
3639fn map_x_oriented(
3640 value: f64,
3641 domain: Domain,
3642 bounds: Rect,
3643 orientation: Orientation,
3644) -> Result<f64> {
3645 let coordinate =
3646 bounds.x + normalized_value_oriented(value, domain, orientation)? * bounds.width;
3647 if coordinate.is_finite() {
3648 Ok(coordinate)
3649 } else {
3650 Err(ChartError::InvalidValue {
3651 element: "chart geometry coordinate".to_owned(),
3652 value: "x coordinate is nonfinite".to_owned(),
3653 })
3654 }
3655}
3656
3657fn map_y_oriented(
3658 value: f64,
3659 domain: Domain,
3660 bounds: Rect,
3661 orientation: Orientation,
3662) -> Result<f64> {
3663 let coordinate = bounds.y + bounds.height
3664 - normalized_value_oriented(value, domain, orientation)? * bounds.height;
3665 if coordinate.is_finite() {
3666 Ok(coordinate)
3667 } else {
3668 Err(ChartError::InvalidValue {
3669 element: "chart geometry coordinate".to_owned(),
3670 value: "y coordinate is nonfinite".to_owned(),
3671 })
3672 }
3673}
3674
3675fn oriented_category_index(index: usize, count: usize, orientation: Orientation) -> usize {
3676 if orientation == Orientation::MaxMin && index < count {
3677 count - index - 1
3678 } else {
3679 index
3680 }
3681}
3682
3683fn category_points(
3684 values: &[(usize, f64)],
3685 count: usize,
3686 domain: Domain,
3687 bounds: Rect,
3688 category_orientation: Orientation,
3689 value_orientation: Orientation,
3690) -> Result<Vec<Point>> {
3691 values
3692 .iter()
3693 .map(|(index, value)| {
3694 let index = oriented_category_index(*index, count, category_orientation);
3695 let x = bounds.x + (index as f64 + 0.5) / count as f64 * bounds.width;
3696 let y = map_y_oriented(*value, domain, bounds, value_orientation)?;
3697 if !x.is_finite() {
3698 return Err(ChartError::InvalidValue {
3699 element: "chart geometry coordinate".to_owned(),
3700 value: "category x coordinate is nonfinite".to_owned(),
3701 });
3702 }
3703 Ok(Point { x, y })
3704 })
3705 .collect()
3706}
3707
3708fn polyline_path(points: &[Point], close: bool) -> Path {
3709 let mut commands = Vec::with_capacity(points.len() + usize::from(close));
3710 if let Some(first) = points.first() {
3711 commands.push(PathCommand::MoveTo(*first));
3712 commands.extend(points.iter().skip(1).copied().map(PathCommand::LineTo));
3713 if close {
3714 commands.push(PathCommand::Close);
3715 }
3716 }
3717 Path {
3718 commands,
3719 fill_rule: FillRule::NonZero,
3720 }
3721}
3722
3723fn pie_wedge(center: Point, radius: f64, start: f64, end: f64) -> Path {
3724 let first = circle_point(center, radius, start);
3725 let mut commands = vec![PathCommand::MoveTo(center), PathCommand::LineTo(first)];
3726 append_arc(&mut commands, center, radius, start, end);
3727 commands.push(PathCommand::Close);
3728 Path {
3729 commands,
3730 fill_rule: FillRule::NonZero,
3731 }
3732}
3733
3734fn doughnut_wedge(
3735 center: Point,
3736 outer_radius: f64,
3737 inner_radius: f64,
3738 start: f64,
3739 end: f64,
3740) -> Path {
3741 let outer_start = circle_point(center, outer_radius, start);
3742 let inner_end = circle_point(center, inner_radius, end);
3743 let mut commands = vec![PathCommand::MoveTo(outer_start)];
3744 append_arc(&mut commands, center, outer_radius, start, end);
3745 commands.push(PathCommand::LineTo(inner_end));
3746 append_arc(&mut commands, center, inner_radius, end, start);
3747 commands.push(PathCommand::Close);
3748 Path {
3749 commands,
3750 fill_rule: FillRule::NonZero,
3751 }
3752}
3753
3754fn append_arc(commands: &mut Vec<PathCommand>, center: Point, radius: f64, start: f64, end: f64) {
3755 let segments = ((end - start).abs() / std::f64::consts::FRAC_PI_2)
3756 .ceil()
3757 .max(1.0) as usize;
3758 let sweep = (end - start) / segments as f64;
3759 for segment in 0..segments {
3760 let angle = start + segment as f64 * sweep;
3761 let next = angle + sweep;
3762 let factor = 4.0 / 3.0 * (sweep / 4.0).tan();
3763 let from = circle_point(center, radius, angle);
3764 let to = circle_point(center, radius, next);
3765 commands.push(PathCommand::CurveTo {
3766 c1: Point {
3767 x: from.x - factor * radius * angle.sin(),
3768 y: from.y + factor * radius * angle.cos(),
3769 },
3770 c2: Point {
3771 x: to.x + factor * radius * next.sin(),
3772 y: to.y - factor * radius * next.cos(),
3773 },
3774 to,
3775 });
3776 }
3777}
3778
3779fn circle_point(center: Point, radius: f64, angle: f64) -> Point {
3780 Point {
3781 x: center.x + radius * angle.cos(),
3782 y: center.y + radius * angle.sin(),
3783 }
3784}
3785
3786fn radial_point(center: Point, radius: f64, index: usize, count: usize) -> Point {
3787 circle_point(
3788 center,
3789 radius,
3790 index as f64 * std::f64::consts::TAU / count as f64 - std::f64::consts::FRAC_PI_2,
3791 )
3792}
3793
3794fn marker_path(center: Point) -> Path {
3795 const RADIUS: f64 = 2.5;
3796 const KAPPA: f64 = 0.552_284_749_830_793_6;
3797 let control = RADIUS * KAPPA;
3798 Path {
3799 commands: vec![
3800 PathCommand::MoveTo(Point {
3801 x: center.x + RADIUS,
3802 y: center.y,
3803 }),
3804 PathCommand::CurveTo {
3805 c1: Point {
3806 x: center.x + RADIUS,
3807 y: center.y + control,
3808 },
3809 c2: Point {
3810 x: center.x + control,
3811 y: center.y + RADIUS,
3812 },
3813 to: Point {
3814 x: center.x,
3815 y: center.y + RADIUS,
3816 },
3817 },
3818 PathCommand::CurveTo {
3819 c1: Point {
3820 x: center.x - control,
3821 y: center.y + RADIUS,
3822 },
3823 c2: Point {
3824 x: center.x - RADIUS,
3825 y: center.y + control,
3826 },
3827 to: Point {
3828 x: center.x - RADIUS,
3829 y: center.y,
3830 },
3831 },
3832 PathCommand::CurveTo {
3833 c1: Point {
3834 x: center.x - RADIUS,
3835 y: center.y - control,
3836 },
3837 c2: Point {
3838 x: center.x - control,
3839 y: center.y - RADIUS,
3840 },
3841 to: Point {
3842 x: center.x,
3843 y: center.y - RADIUS,
3844 },
3845 },
3846 PathCommand::CurveTo {
3847 c1: Point {
3848 x: center.x + control,
3849 y: center.y - RADIUS,
3850 },
3851 c2: Point {
3852 x: center.x + RADIUS,
3853 y: center.y - control,
3854 },
3855 to: Point {
3856 x: center.x + RADIUS,
3857 y: center.y,
3858 },
3859 },
3860 PathCommand::Close,
3861 ],
3862 fill_rule: FillRule::NonZero,
3863 }
3864}
3865
3866fn push_markers(elements: &mut Vec<PositionedElement>, points: &[Point], colour: Color) {
3867 for point in points {
3868 elements.push(filled_path(marker_path(*point), colour));
3869 }
3870}
3871
3872fn series_colour(series_colours: &[Color], index: usize) -> Result<Color> {
3873 series_colours
3874 .get(index)
3875 .copied()
3876 .ok_or_else(|| ChartError::InvalidValue {
3877 element: "chart series palette".to_owned(),
3878 value: format!("missing resolved colour for series {index}"),
3879 })
3880}
3881
3882fn filled_path(path: Path, color: Color) -> PositionedElement {
3883 PositionedElement::Path(PathElement {
3884 path,
3885 fill: Some(Paint::Solid(color)),
3886 stroke: None,
3887 })
3888}
3889
3890fn stroked_path(path: Path, color: Color) -> PositionedElement {
3891 PositionedElement::Path(PathElement {
3892 path,
3893 fill: None,
3894 stroke: Some(Stroke::new(Paint::Solid(color), 1.5)),
3895 })
3896}
3897
3898fn filled_and_stroked_path(path: Path, color: Color) -> PositionedElement {
3899 PositionedElement::Path(PathElement {
3900 path,
3901 fill: Some(Paint::Solid(Color {
3902 a: color.a * 0.55,
3903 ..color
3904 })),
3905 stroke: Some(Stroke::new(Paint::Solid(color), 1.0)),
3906 })
3907}
3908
3909type NamespaceBindings = Vec<(Vec<u8>, String)>;
3910type XmlAttributes = Vec<(String, String)>;
3911type RootAttributes = (XmlAttributes, XmlAttributes);
3912
3913#[derive(Clone, Debug, Default, Eq, PartialEq)]
3914struct ScalarMarkup {
3915 raw_attributes: XmlAttributes,
3916 raw_content: Vec<Vec<u8>>,
3917}
3918
3919#[derive(Clone, Debug, Default, Eq, PartialEq)]
3920struct TextMarkup {
3921 raw_attributes: XmlAttributes,
3922 raw_children: OrderedRawChildren,
3923}
3924
3925#[derive(Clone, Debug, Default, Eq, PartialEq)]
3926struct PointMarkup {
3927 raw_attributes: XmlAttributes,
3928 raw_children: OrderedRawChildren,
3929 value: TextMarkup,
3930}
3931
3932#[derive(Clone, Debug, Default, Eq, PartialEq)]
3933struct ReferenceMarkup {
3934 raw_attributes: XmlAttributes,
3935 raw_children: OrderedRawChildren,
3936 formula: TextMarkup,
3937 cache_attributes: XmlAttributes,
3938 cache_children: OrderedRawChildren,
3939 format_code: Option<TextMarkup>,
3940 format_code_omitted: bool,
3941 point_count: ScalarMarkup,
3942 declared_point_count: Option<u32>,
3943 point_indexes: Vec<u32>,
3944 points: Vec<PointMarkup>,
3945}
3946
3947#[derive(Clone, Debug, Default, Eq, PartialEq)]
3948struct WrapperMarkup {
3949 raw_attributes: XmlAttributes,
3950 raw_children: OrderedRawChildren,
3951}
3952
3953#[derive(Clone, Debug, PartialEq)]
3955pub struct StringRef {
3956 pub formula: String,
3957 pub values: Vec<String>,
3958 markup: ReferenceMarkup,
3959}
3960
3961impl StringRef {
3962 pub fn new(formula: String, values: Vec<String>) -> Result<Self> {
3963 let reference = Self {
3964 formula,
3965 values,
3966 markup: ReferenceMarkup::default(),
3967 };
3968 reference.validate()?;
3969 Ok(reference)
3970 }
3971
3972 pub fn from_xml(xml: &[u8]) -> Result<Self> {
3973 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
3974 }
3975
3976 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
3977 let parsed = parse_reference(xml, b"strRef", b"strCache", false, inherited)?;
3978 let values = parsed.values;
3979 let reference = Self {
3980 formula: parsed.formula,
3981 values,
3982 markup: parsed.markup,
3983 };
3984 reference.validate()?;
3985 Ok(reference)
3986 }
3987
3988 pub fn to_xml(&self) -> Result<Vec<u8>> {
3989 self.validate()?;
3990 let mut writer = Writer::new(Vec::new());
3991 self.write_xml(&mut writer, true)?;
3992 Ok(writer.into_inner())
3993 }
3994
3995 fn validate(&self) -> Result<()> {
3996 validate_formula(&self.formula, "c:strRef/c:f")?;
3997 for value in &self.values {
3998 validate_xml_text(value, "c:strCache/c:pt/c:v")?;
3999 }
4000 validate_point_count(self.values.len(), "c:strCache")
4001 }
4002
4003 fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
4004 self.validate()?;
4005 let mut start = BytesStart::new("c:strRef");
4006 if standalone {
4007 start.push_attribute(("xmlns:c", C_NS));
4008 }
4009 push_attributes(&mut start, &self.markup.raw_attributes);
4010 writer
4011 .write_event(Event::Start(start))
4012 .map_err(OxmlError::from)?;
4013 emit_raw(writer, self.markup.raw_children.at(0))?;
4014 write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
4015 emit_raw(writer, self.markup.raw_children.at(1))?;
4016 write_string_cache(writer, &self.values, &self.markup)?;
4017 emit_raw(writer, self.markup.raw_children.at(2))?;
4018 writer
4019 .write_event(Event::End(BytesEnd::new("c:strRef")))
4020 .map_err(OxmlError::from)?;
4021 Ok(())
4022 }
4023}
4024
4025#[derive(Clone, Debug, PartialEq)]
4027pub struct NumericData {
4028 pub formula: String,
4029 pub format_code: String,
4030 pub values: Vec<f64>,
4031 markup: ReferenceMarkup,
4032}
4033
4034impl NumericData {
4035 pub fn new(formula: String, format_code: String, values: Vec<f64>) -> Result<Self> {
4036 validate_formula(&formula, "c:numRef/c:f")?;
4037 validate_format_code(&format_code)?;
4038 validate_numeric_values(&values)?;
4039 validate_point_count(values.len(), "c:numCache")?;
4040 Ok(Self {
4041 formula,
4042 format_code,
4043 values,
4044 markup: ReferenceMarkup::default(),
4045 })
4046 }
4047
4048 pub fn from_xml(xml: &[u8]) -> Result<Self> {
4049 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
4050 }
4051
4052 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
4053 let parsed = parse_reference(xml, b"numRef", b"numCache", true, inherited)?;
4054 let format_code = parsed.format_code.unwrap_or_else(|| "General".to_owned());
4055 let mut values = Vec::with_capacity(parsed.values.len());
4056 for value in parsed.values {
4057 let number = value.parse::<f64>().map_err(|_| ChartError::InvalidValue {
4058 element: "c:v".to_owned(),
4059 value: value.clone(),
4060 })?;
4061 if !number.is_finite() {
4062 return Err(ChartError::InvalidValue {
4063 element: "c:v".to_owned(),
4064 value,
4065 });
4066 }
4067 values.push(number);
4068 }
4069 let data = Self {
4070 formula: parsed.formula,
4071 format_code,
4072 values,
4073 markup: parsed.markup,
4074 };
4075 data.validate()?;
4076 Ok(data)
4077 }
4078
4079 pub fn to_xml(&self) -> Result<Vec<u8>> {
4080 self.validate()?;
4081 let mut writer = Writer::new(Vec::new());
4082 self.write_xml(&mut writer, true)?;
4083 Ok(writer.into_inner())
4084 }
4085
4086 fn validate(&self) -> Result<()> {
4087 validate_formula(&self.formula, "c:numRef/c:f")?;
4088 validate_format_code(&self.format_code)?;
4089 validate_numeric_values(&self.values)?;
4090 validate_point_count(self.values.len(), "c:numCache")
4091 }
4092
4093 fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
4094 self.validate()?;
4095 let mut start = BytesStart::new("c:numRef");
4096 if standalone {
4097 start.push_attribute(("xmlns:c", C_NS));
4098 }
4099 push_attributes(&mut start, &self.markup.raw_attributes);
4100 writer
4101 .write_event(Event::Start(start))
4102 .map_err(OxmlError::from)?;
4103 emit_raw(writer, self.markup.raw_children.at(0))?;
4104 write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
4105 emit_raw(writer, self.markup.raw_children.at(1))?;
4106 write_numeric_cache(writer, self, &self.markup)?;
4107 emit_raw(writer, self.markup.raw_children.at(2))?;
4108 writer
4109 .write_event(Event::End(BytesEnd::new("c:numRef")))
4110 .map_err(OxmlError::from)?;
4111 Ok(())
4112 }
4113}
4114
4115#[derive(Clone, Debug, PartialEq)]
4116pub enum AxisData {
4117 String(StringRef),
4118 Numeric(NumericData),
4119}
4120
4121#[derive(Clone, Debug, PartialEq)]
4122struct DataPoint {
4123 index: u32,
4124 sp_pr: CT_ShapeProperties,
4125}
4126
4127impl DataPoint {
4128 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
4129 writer
4130 .write_event(Event::Start(BytesStart::new("c:dPt")))
4131 .map_err(OxmlError::from)?;
4132 write_scalar(writer, "c:idx", &self.index.to_string(), None)?;
4133 self.sp_pr.write_xml_as(writer, "c:spPr")?;
4134 writer
4135 .write_event(Event::End(BytesEnd::new("c:dPt")))
4136 .map_err(OxmlError::from)?;
4137 Ok(())
4138 }
4139}
4140
4141#[derive(Clone, Debug, PartialEq)]
4143pub struct Series {
4144 pub index: u32,
4145 pub order: u32,
4146 pub name: Option<StringRef>,
4147 pub categories: Option<AxisData>,
4148 pub values: NumericData,
4149 pub bubble_size: Option<NumericData>,
4150 pub sp_pr: Option<CT_ShapeProperties>,
4151 pub data_labels: Option<CT_DLbls>,
4152 authored_data_points: Vec<DataPoint>,
4153 index_markup: ScalarMarkup,
4154 order_markup: ScalarMarkup,
4155 name_markup: Option<WrapperMarkup>,
4156 categories_markup: Option<WrapperMarkup>,
4157 values_markup: WrapperMarkup,
4158 bubble_size_markup: Option<WrapperMarkup>,
4159 opaque_name: bool,
4160 opaque_categories: bool,
4161 opaque_bubble_size: bool,
4162 uses_scatter_wrappers: bool,
4163 parsed_from_xml: bool,
4164 raw_attributes: XmlAttributes,
4165 namespace_declarations: XmlAttributes,
4166 raw_children: OrderedRawChildren,
4167}
4168
4169impl Series {
4170 pub fn new(index: u32, order: u32, values: NumericData) -> Self {
4171 Self {
4172 index,
4173 order,
4174 name: None,
4175 categories: None,
4176 values,
4177 bubble_size: None,
4178 sp_pr: None,
4179 data_labels: None,
4180 authored_data_points: Vec::new(),
4181 index_markup: ScalarMarkup::default(),
4182 order_markup: ScalarMarkup::default(),
4183 name_markup: None,
4184 categories_markup: None,
4185 values_markup: WrapperMarkup::default(),
4186 bubble_size_markup: None,
4187 opaque_name: false,
4188 opaque_categories: false,
4189 opaque_bubble_size: false,
4190 uses_scatter_wrappers: false,
4191 parsed_from_xml: false,
4192 raw_attributes: Vec::new(),
4193 namespace_declarations: Vec::new(),
4194 raw_children: OrderedRawChildren::default(),
4195 }
4196 }
4197
4198 pub fn from_xml(xml: &[u8]) -> Result<Self> {
4199 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
4200 }
4201
4202 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
4203 let mut reader = Reader::from_reader(xml);
4204 let mut buffer = Vec::new();
4205 loop {
4206 match reader
4207 .read_event_into(&mut buffer)
4208 .map_err(OxmlError::from)?
4209 {
4210 Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4211 chart_root_prefix(&element)?;
4212 if !element_is_in_namespace(&element, C_NS, inherited)? {
4213 return Err(ChartError::UnexpectedElement(element_name(&element)));
4214 }
4215 return Self::from_element(&mut reader, &element, inherited);
4216 }
4217 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4218 return Err(ChartError::MissingElement("c:idx".to_owned()));
4219 }
4220 Event::Start(element) | Event::Empty(element) => {
4221 return Err(ChartError::UnexpectedElement(element_name(&element)));
4222 }
4223 Event::Eof => return Err(ChartError::MissingElement("c:ser".to_owned())),
4224 _ => {}
4225 }
4226 buffer.clear();
4227 }
4228 }
4229
4230 fn from_element(
4231 reader: &mut Reader<&[u8]>,
4232 start: &BytesStart<'_>,
4233 inherited: &NamespaceBindings,
4234 ) -> Result<Self> {
4235 reject_conflicting_prefix(start, b"a", A_NS)?;
4236 let namespaces = chart_bindings(inherited, start)?;
4237 require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
4238 require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
4239 require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
4240 let (raw_attributes, _) =
4241 capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
4242 let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
4243 let mut state = SeriesParseState::default();
4244 let mut buffer = Vec::new();
4245 loop {
4246 match reader
4247 .read_event_into(&mut buffer)
4248 .map_err(OxmlError::from)?
4249 {
4250 Event::Start(element) => {
4251 let name = chart_child_local(&element, &namespaces)?;
4252 let raw = capture_element(reader, &element)?;
4253 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
4254 }
4255 Event::Empty(element) => {
4256 let name = chart_child_local(&element, &namespaces)?;
4257 let raw = capture_empty_element(&element)?;
4258 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
4259 }
4260 event @ (Event::Text(_)
4261 | Event::CData(_)
4262 | Event::Comment(_)
4263 | Event::PI(_)
4264 | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
4265 Event::End(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4266 break;
4267 }
4268 Event::Eof => return Err(missing_end("c:ser")),
4269 _ => {}
4270 }
4271 buffer.clear();
4272 }
4273 state.finish(raw_attributes, namespace_declarations)
4274 }
4275
4276 pub fn to_xml(&self) -> Result<Vec<u8>> {
4277 self.to_xml_for_plot(self.uses_scatter_wrappers)
4278 }
4279
4280 fn to_xml_for_plot(&self, scatter: bool) -> Result<Vec<u8>> {
4281 if self.opaque_name && self.name.is_some() {
4282 return Err(ChartError::DuplicateElement("c:tx".to_owned()));
4283 }
4284 if self.opaque_categories && self.categories.is_some() {
4285 return Err(ChartError::DuplicateElement("c:cat".to_owned()));
4286 }
4287 if self.opaque_bubble_size && self.bubble_size.is_some() {
4288 return Err(ChartError::DuplicateElement("c:bubbleSize".to_owned()));
4289 }
4290 self.values.validate()?;
4291 if let Some(name) = &self.name {
4292 name.validate()?;
4293 }
4294 if let Some(categories) = &self.categories {
4295 if scatter && !matches!(categories, AxisData::Numeric(_)) {
4296 return Err(ChartError::InvalidValue {
4297 element: "c:xVal".to_owned(),
4298 value: "scatter x values must use a numeric cache".to_owned(),
4299 });
4300 }
4301 match categories {
4302 AxisData::String(reference) => reference.validate()?,
4303 AxisData::Numeric(reference) => reference.validate()?,
4304 }
4305 }
4306 if scatter && self.categories.is_none() {
4307 return Err(ChartError::MissingElement("c:xVal".to_owned()));
4308 }
4309 if let Some(size) = &self.bubble_size {
4310 size.validate()?;
4311 }
4312 if let Some(labels) = &self.data_labels {
4313 labels.validate()?;
4314 }
4315 let mut writer = Writer::new(Vec::new());
4316 let mut start = BytesStart::new("c:ser");
4317 start.push_attribute(("xmlns:c", C_NS));
4318 start.push_attribute(("xmlns:a", A_NS));
4319 start.push_attribute(("xmlns:r", R_NS));
4320 push_attributes(&mut start, &self.namespace_declarations);
4321 push_attributes(&mut start, &self.raw_attributes);
4322 writer
4323 .write_event(Event::Start(start))
4324 .map_err(OxmlError::from)?;
4325 emit_raw(&mut writer, self.raw_children.at(0))?;
4326 write_scalar(
4327 &mut writer,
4328 "c:idx",
4329 &self.index.to_string(),
4330 Some(&self.index_markup),
4331 )?;
4332 emit_raw(&mut writer, self.raw_children.at(1))?;
4333 write_scalar(
4334 &mut writer,
4335 "c:order",
4336 &self.order.to_string(),
4337 Some(&self.order_markup),
4338 )?;
4339 emit_raw(&mut writer, self.raw_children.at(2))?;
4340 if let Some(name) = &self.name {
4341 write_wrapper_start(
4342 &mut writer,
4343 "c:tx",
4344 self.name_markup
4345 .as_ref()
4346 .unwrap_or(&WrapperMarkup::default()),
4347 )?;
4348 name.write_xml(&mut writer, false)?;
4349 write_wrapper_end(
4350 &mut writer,
4351 "c:tx",
4352 self.name_markup
4353 .as_ref()
4354 .unwrap_or(&WrapperMarkup::default()),
4355 )?;
4356 }
4357 emit_raw(&mut writer, self.raw_children.at(3))?;
4358 if let Some(properties) = &self.sp_pr {
4359 properties.write_xml_as(&mut writer, "c:spPr")?;
4360 }
4361 emit_raw(&mut writer, self.raw_children.at(4))?;
4362 for point in &self.authored_data_points {
4363 point.write_xml(&mut writer)?;
4364 }
4365 if let Some(labels) = &self.data_labels {
4366 labels.write_xml(&mut writer, false)?;
4367 }
4368 emit_raw(&mut writer, self.raw_children.at(5))?;
4369 if let Some(categories) = &self.categories {
4370 let default_markup = WrapperMarkup::default();
4371 let markup = self.categories_markup.as_ref().unwrap_or(&default_markup);
4372 write_wrapper_start(
4373 &mut writer,
4374 if scatter { "c:xVal" } else { "c:cat" },
4375 markup,
4376 )?;
4377 match categories {
4378 AxisData::String(reference) => reference.write_xml(&mut writer, false)?,
4379 AxisData::Numeric(reference) => reference.write_xml(&mut writer, false)?,
4380 }
4381 write_wrapper_end(
4382 &mut writer,
4383 if scatter { "c:xVal" } else { "c:cat" },
4384 markup,
4385 )?;
4386 }
4387 emit_raw(&mut writer, self.raw_children.at(6))?;
4388 write_wrapper_start(
4389 &mut writer,
4390 if scatter { "c:yVal" } else { "c:val" },
4391 &self.values_markup,
4392 )?;
4393 self.values.write_xml(&mut writer, false)?;
4394 write_wrapper_end(
4395 &mut writer,
4396 if scatter { "c:yVal" } else { "c:val" },
4397 &self.values_markup,
4398 )?;
4399 emit_raw(&mut writer, self.raw_children.at(7))?;
4400 if let Some(size) = &self.bubble_size {
4401 let default_markup = WrapperMarkup::default();
4402 let markup = self.bubble_size_markup.as_ref().unwrap_or(&default_markup);
4403 write_wrapper_start(&mut writer, "c:bubbleSize", markup)?;
4404 size.write_xml(&mut writer, false)?;
4405 write_wrapper_end(&mut writer, "c:bubbleSize", markup)?;
4406 }
4407 emit_raw(&mut writer, self.raw_children.at(8))?;
4408 writer
4409 .write_event(Event::End(BytesEnd::new("c:ser")))
4410 .map_err(OxmlError::from)?;
4411 Ok(writer.into_inner())
4412 }
4413
4414 pub fn raw_children(&self) -> &OrderedRawChildren {
4415 &self.raw_children
4416 }
4417}
4418
4419#[derive(Default)]
4420struct SeriesParseState {
4421 index: Option<(u32, ScalarMarkup)>,
4422 order: Option<(u32, ScalarMarkup)>,
4423 name: Option<(StringRef, WrapperMarkup)>,
4424 categories: Option<(AxisData, WrapperMarkup)>,
4425 values: Option<(NumericData, WrapperMarkup)>,
4426 bubble_size: Option<(NumericData, WrapperMarkup)>,
4427 data_labels: Option<CT_DLbls>,
4428 name_seen: bool,
4429 categories_seen: bool,
4430 bubble_size_seen: bool,
4431 opaque_name: bool,
4432 opaque_categories: bool,
4433 opaque_bubble_size: bool,
4434 saw_standard_wrapper: bool,
4435 saw_scatter_wrapper: bool,
4436 sp_pr: Option<CT_ShapeProperties>,
4437 raw_children: OrderedRawChildren,
4438 boundary: usize,
4439}
4440
4441impl SeriesParseState {
4442 fn capture_event(&mut self, raw: Vec<u8>) {
4443 self.raw_children.push(self.boundary, raw);
4444 }
4445
4446 fn parse_child(
4447 &mut self,
4448 name: &[u8],
4449 raw: Vec<u8>,
4450 namespaces: &NamespaceBindings,
4451 ) -> Result<()> {
4452 match name {
4453 b"idx" => {
4454 set_once(&mut self.index, parse_u32_scalar(&raw, "idx")?, "c:idx")?;
4455 self.boundary = self.boundary.max(1);
4456 }
4457 b"order" => {
4458 set_once(&mut self.order, parse_u32_scalar(&raw, "order")?, "c:order")?;
4459 self.boundary = self.boundary.max(2);
4460 }
4461 b"tx" => {
4462 mark_once(&mut self.name_seen, "c:tx")?;
4463 let parsed = parse_wrapper(&raw, b"tx", &[b"strRef"], namespaces)?;
4464 if let Some((_, reference)) = parsed.choice {
4465 self.name = Some((
4466 StringRef::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4467 parsed.markup,
4468 ));
4469 } else {
4470 self.opaque_name = true;
4471 self.raw_children.push(2, raw);
4472 }
4473 self.boundary = self.boundary.max(3);
4474 }
4475 b"spPr" => {
4476 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
4477 let properties = CT_ShapeProperties::from_xml(&raw)?;
4478 let mut writer = Writer::new(Vec::new());
4479 properties.write_xml_as(&mut writer, "c:spPr")?;
4480 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
4481 set_once(&mut self.sp_pr, properties, "c:spPr")?;
4482 self.boundary = self.boundary.max(4);
4483 }
4484 b"dLbls" => {
4485 set_once(
4486 &mut self.data_labels,
4487 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
4488 "c:dLbls",
4489 )?;
4490 self.boundary = self.boundary.max(5);
4491 }
4492 b"cat" => {
4493 self.saw_standard_wrapper = true;
4494 mark_once(&mut self.categories_seen, "c:cat")?;
4495 let parsed = parse_wrapper(&raw, b"cat", &[b"strRef", b"numRef"], namespaces)?;
4496 if let Some((choice, reference)) = parsed.choice {
4497 let categories = if choice == b"strRef" {
4498 AxisData::String(StringRef::from_xml_with_namespaces(
4499 &reference,
4500 &parsed.namespaces,
4501 )?)
4502 } else {
4503 AxisData::Numeric(NumericData::from_xml_with_namespaces(
4504 &reference,
4505 &parsed.namespaces,
4506 )?)
4507 };
4508 self.categories = Some((categories, parsed.markup));
4509 } else {
4510 self.opaque_categories = true;
4511 self.raw_children.push(5, raw);
4512 }
4513 self.boundary = self.boundary.max(6);
4514 }
4515 b"xVal" => {
4516 self.saw_scatter_wrapper = true;
4517 mark_once(&mut self.categories_seen, "c:xVal")?;
4518 let parsed = parse_wrapper(&raw, b"xVal", &[b"numRef"], namespaces)?;
4519 let (_, reference) = parsed
4520 .choice
4521 .ok_or_else(|| ChartError::MissingElement("c:xVal/c:numRef".to_owned()))?;
4522 self.categories = Some((
4523 AxisData::Numeric(NumericData::from_xml_with_namespaces(
4524 &reference,
4525 &parsed.namespaces,
4526 )?),
4527 parsed.markup,
4528 ));
4529 self.boundary = self.boundary.max(6);
4530 }
4531 b"val" | b"yVal" => {
4532 if name == b"yVal" {
4533 self.saw_scatter_wrapper = true;
4534 } else {
4535 self.saw_standard_wrapper = true;
4536 }
4537 let wrapper = if name == b"yVal" {
4538 b"yVal".as_slice()
4539 } else {
4540 b"val".as_slice()
4541 };
4542 let parsed = parse_wrapper(&raw, wrapper, &[b"numRef"], namespaces)?;
4543 let (_, reference) = parsed.choice.ok_or_else(|| {
4544 ChartError::MissingElement(format!(
4545 "c:{}/c:numRef",
4546 String::from_utf8_lossy(wrapper)
4547 ))
4548 })?;
4549 set_once(
4550 &mut self.values,
4551 (
4552 NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4553 parsed.markup,
4554 ),
4555 "c:val or c:yVal",
4556 )?;
4557 self.boundary = self.boundary.max(7);
4558 }
4559 b"bubbleSize" => {
4560 mark_once(&mut self.bubble_size_seen, "c:bubbleSize")?;
4561 let parsed = parse_wrapper(&raw, b"bubbleSize", &[b"numRef"], namespaces)?;
4562 if let Some((_, reference)) = parsed.choice {
4563 self.bubble_size = Some((
4564 NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4565 parsed.markup,
4566 ));
4567 } else {
4568 self.opaque_bubble_size = true;
4569 self.raw_children.push(7, raw);
4570 }
4571 self.boundary = self.boundary.max(8);
4572 }
4573 _ => {
4574 let boundary = series_raw_boundary(name, self.boundary);
4575 self.raw_children.push(boundary, raw);
4576 self.boundary = self.boundary.max(boundary);
4577 }
4578 }
4579 Ok(())
4580 }
4581
4582 fn finish(
4583 self,
4584 raw_attributes: XmlAttributes,
4585 namespace_declarations: XmlAttributes,
4586 ) -> Result<Series> {
4587 if self.saw_standard_wrapper && self.saw_scatter_wrapper {
4588 return Err(ChartError::InvalidValue {
4589 element: "c:ser".to_owned(),
4590 value: "category/value and x/y wrappers cannot be combined".to_owned(),
4591 });
4592 }
4593 let (index, index_markup) = self
4594 .index
4595 .ok_or_else(|| ChartError::MissingElement("c:idx".to_owned()))?;
4596 let (order, order_markup) = self
4597 .order
4598 .ok_or_else(|| ChartError::MissingElement("c:order".to_owned()))?;
4599 let (values, values_markup) = self
4600 .values
4601 .ok_or_else(|| ChartError::MissingElement("c:val".to_owned()))?;
4602 let (name, name_markup) = self
4603 .name
4604 .map(|(value, markup)| (Some(value), Some(markup)))
4605 .unwrap_or((None, None));
4606 let (categories, categories_markup) = self
4607 .categories
4608 .map(|(value, markup)| (Some(value), Some(markup)))
4609 .unwrap_or((None, None));
4610 let (bubble_size, bubble_size_markup) = self
4611 .bubble_size
4612 .map(|(value, markup)| (Some(value), Some(markup)))
4613 .unwrap_or((None, None));
4614 Ok(Series {
4615 index,
4616 order,
4617 name,
4618 categories,
4619 values,
4620 bubble_size,
4621 sp_pr: self.sp_pr,
4622 data_labels: self.data_labels,
4623 authored_data_points: Vec::new(),
4624 index_markup,
4625 order_markup,
4626 name_markup,
4627 categories_markup,
4628 values_markup,
4629 bubble_size_markup,
4630 opaque_name: self.opaque_name,
4631 opaque_categories: self.opaque_categories,
4632 opaque_bubble_size: self.opaque_bubble_size,
4633 uses_scatter_wrappers: self.saw_scatter_wrapper,
4634 parsed_from_xml: true,
4635 raw_attributes,
4636 namespace_declarations,
4637 raw_children: raw_children_in_schema_order(&self.raw_children, 8),
4638 })
4639 }
4640}
4641
4642fn series_raw_boundary(name: &[u8], current: usize) -> usize {
4643 match name {
4644 b"marker" | b"invertIfNegative" | b"pictureOptions" | b"explosion" | b"dPt" => 4,
4645 b"trendline" | b"errBars" => 5,
4646 b"shape" | b"smooth" => 7,
4647 b"extLst" => 8,
4648 _ => current,
4649 }
4650}
4651
4652struct ParsedReference {
4653 formula: String,
4654 format_code: Option<String>,
4655 values: Vec<String>,
4656 markup: ReferenceMarkup,
4657}
4658
4659fn parse_reference(
4660 xml: &[u8],
4661 reference_local: &[u8],
4662 cache_local: &[u8],
4663 numeric: bool,
4664 inherited: &NamespaceBindings,
4665) -> Result<ParsedReference> {
4666 let mut reader = Reader::from_reader(xml);
4667 let mut buffer = Vec::new();
4668 loop {
4669 match reader
4670 .read_event_into(&mut buffer)
4671 .map_err(OxmlError::from)?
4672 {
4673 Event::Start(element)
4674 if matches_local_name(element.name().as_ref(), reference_local) =>
4675 {
4676 let namespaces = typed_rewrite_bindings(&element, inherited)?;
4677 let raw_attributes = capture_fixed_attributes(&element, &["xmlns:c"])?;
4678 let mut formula: Option<(String, TextMarkup)> = None;
4679 let mut cache: Option<(Option<String>, Vec<String>, CacheMarkup)> = None;
4680 let mut raw_children = OrderedRawChildren::default();
4681 let mut boundary = 0usize;
4682 let mut inner = Vec::new();
4683 loop {
4684 match reader
4685 .read_event_into(&mut inner)
4686 .map_err(OxmlError::from)?
4687 {
4688 Event::Start(child) => {
4689 let name = chart_child_local(&child, &namespaces)?;
4690 let raw = capture_element(&mut reader, &child)?;
4691 match name.as_deref().unwrap_or_default() {
4692 b"f" => {
4693 set_once(
4694 &mut formula,
4695 parse_text_element(&raw, b"f", &namespaces)?,
4696 "c:f",
4697 )?;
4698 boundary = boundary.max(1);
4699 }
4700 name if name == cache_local => {
4701 set_once(
4702 &mut cache,
4703 parse_cache(&raw, cache_local, numeric, &namespaces)?,
4704 &format!("c:{}", String::from_utf8_lossy(cache_local)),
4705 )?;
4706 boundary = boundary.max(2);
4707 }
4708 _ => raw_children.push(boundary, raw),
4709 }
4710 }
4711 Event::Empty(child) => {
4712 let name = chart_child_local(&child, &namespaces)?;
4713 let raw = capture_empty_element(&child)?;
4714 match name.as_deref().unwrap_or_default() {
4715 b"f" => {
4716 set_once(
4717 &mut formula,
4718 parse_text_element(&raw, b"f", &namespaces)?,
4719 "c:f",
4720 )?;
4721 boundary = boundary.max(1);
4722 }
4723 name if name == cache_local => {
4724 set_once(
4725 &mut cache,
4726 parse_cache(&raw, cache_local, numeric, &namespaces)?,
4727 &format!("c:{}", String::from_utf8_lossy(cache_local)),
4728 )?;
4729 boundary = boundary.max(2);
4730 }
4731 _ => raw_children.push(boundary, raw),
4732 }
4733 }
4734 event @ (Event::Text(_)
4735 | Event::CData(_)
4736 | Event::Comment(_)
4737 | Event::PI(_)
4738 | Event::GeneralRef(_)) => {
4739 raw_children.push(boundary, capture_event(event)?);
4740 }
4741 Event::End(end)
4742 if matches_local_name(end.name().as_ref(), reference_local) =>
4743 {
4744 let (formula, formula_markup) = formula
4745 .ok_or_else(|| ChartError::MissingElement("c:f".to_owned()))?;
4746 validate_formula(&formula, "c:f")?;
4747 let (format_code, values, cache_markup) = cache.ok_or_else(|| {
4748 ChartError::MissingElement(format!(
4749 "c:{}",
4750 String::from_utf8_lossy(cache_local)
4751 ))
4752 })?;
4753 let markup = ReferenceMarkup {
4754 raw_attributes,
4755 raw_children: raw_children_in_schema_order(&raw_children, 2),
4756 formula: formula_markup,
4757 cache_attributes: cache_markup.raw_attributes,
4758 cache_children: cache_markup.raw_children,
4759 format_code: cache_markup.format_code,
4760 format_code_omitted: cache_markup.format_code_omitted,
4761 point_count: cache_markup.point_count,
4762 declared_point_count: cache_markup.declared_point_count,
4763 point_indexes: cache_markup.point_indexes,
4764 points: cache_markup.points,
4765 };
4766 return Ok(ParsedReference {
4767 formula,
4768 format_code,
4769 values,
4770 markup,
4771 });
4772 }
4773 Event::Eof => {
4774 return Err(missing_end(&format!(
4775 "c:{}",
4776 String::from_utf8_lossy(reference_local)
4777 )));
4778 }
4779 _ => {}
4780 }
4781 inner.clear();
4782 }
4783 }
4784 Event::Empty(element)
4785 if matches_local_name(element.name().as_ref(), reference_local) =>
4786 {
4787 return Err(ChartError::MissingElement("c:f".to_owned()));
4788 }
4789 Event::Start(element) | Event::Empty(element) => {
4790 return Err(ChartError::UnexpectedElement(element_name(&element)));
4791 }
4792 Event::Eof => {
4793 return Err(ChartError::MissingElement(format!(
4794 "c:{}",
4795 String::from_utf8_lossy(reference_local)
4796 )));
4797 }
4798 _ => {}
4799 }
4800 buffer.clear();
4801 }
4802}
4803
4804struct CacheMarkup {
4805 raw_attributes: XmlAttributes,
4806 raw_children: OrderedRawChildren,
4807 format_code: Option<TextMarkup>,
4808 format_code_omitted: bool,
4809 point_count: ScalarMarkup,
4810 declared_point_count: Option<u32>,
4811 point_indexes: Vec<u32>,
4812 points: Vec<PointMarkup>,
4813}
4814
4815fn parse_cache(
4816 xml: &[u8],
4817 cache_local: &[u8],
4818 numeric: bool,
4819 inherited: &NamespaceBindings,
4820) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
4821 let mut reader = Reader::from_reader(xml);
4822 let mut buffer = Vec::new();
4823 loop {
4824 match reader
4825 .read_event_into(&mut buffer)
4826 .map_err(OxmlError::from)?
4827 {
4828 Event::Start(element) if matches_local_name(element.name().as_ref(), cache_local) => {
4829 let namespaces = typed_rewrite_bindings(&element, inherited)?;
4830 let raw_attributes = capture_attributes(&element)?;
4831 let mut state = CacheParseState::new(numeric, namespaces);
4832 let mut inner = Vec::new();
4833 loop {
4834 match reader
4835 .read_event_into(&mut inner)
4836 .map_err(OxmlError::from)?
4837 {
4838 Event::Start(child) => {
4839 let name = chart_child_local(&child, &state.namespaces)?;
4840 let raw = capture_element(&mut reader, &child)?;
4841 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
4842 }
4843 Event::Empty(child) => {
4844 let name = chart_child_local(&child, &state.namespaces)?;
4845 let raw = capture_empty_element(&child)?;
4846 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
4847 }
4848 event @ (Event::Text(_)
4849 | Event::CData(_)
4850 | Event::Comment(_)
4851 | Event::PI(_)
4852 | Event::GeneralRef(_)) => {
4853 state.capture_event(capture_event(event)?);
4854 }
4855 Event::End(end) if matches_local_name(end.name().as_ref(), cache_local) => {
4856 return state.finish(raw_attributes);
4857 }
4858 Event::Eof => {
4859 return Err(missing_end(&format!(
4860 "c:{}",
4861 String::from_utf8_lossy(cache_local)
4862 )));
4863 }
4864 _ => {}
4865 }
4866 inner.clear();
4867 }
4868 }
4869 Event::Empty(element) if matches_local_name(element.name().as_ref(), cache_local) => {
4870 return Err(ChartError::MissingElement("c:ptCount".to_owned()));
4871 }
4872 Event::Start(element) | Event::Empty(element) => {
4873 return Err(ChartError::UnexpectedElement(element_name(&element)));
4874 }
4875 Event::Eof => {
4876 return Err(ChartError::MissingElement(format!(
4877 "c:{}",
4878 String::from_utf8_lossy(cache_local)
4879 )));
4880 }
4881 _ => {}
4882 }
4883 buffer.clear();
4884 }
4885}
4886
4887struct CacheParseState {
4888 numeric: bool,
4889 namespaces: NamespaceBindings,
4890 point_base: usize,
4891 boundary: usize,
4892 format_code: Option<(String, TextMarkup)>,
4893 point_count: Option<(u32, ScalarMarkup)>,
4894 points: Vec<(u32, String, PointMarkup)>,
4895 raw_children: OrderedRawChildren,
4896}
4897
4898impl CacheParseState {
4899 fn new(numeric: bool, namespaces: NamespaceBindings) -> Self {
4900 Self {
4901 numeric,
4902 namespaces,
4903 point_base: usize::from(numeric) + 1,
4904 boundary: 0,
4905 format_code: None,
4906 point_count: None,
4907 points: Vec::new(),
4908 raw_children: OrderedRawChildren::default(),
4909 }
4910 }
4911
4912 fn capture_event(&mut self, raw: Vec<u8>) {
4913 self.raw_children.push(self.boundary, raw);
4914 }
4915
4916 fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
4917 match name {
4918 b"formatCode" if self.numeric => {
4919 set_once(
4920 &mut self.format_code,
4921 parse_text_element(&raw, b"formatCode", &self.namespaces)?,
4922 "c:formatCode",
4923 )?;
4924 self.boundary = self.boundary.max(1);
4925 }
4926 b"ptCount" => {
4927 set_once(
4928 &mut self.point_count,
4929 parse_u32_scalar(&raw, "ptCount")?,
4930 "c:ptCount",
4931 )?;
4932 self.boundary = self.boundary.max(self.point_base);
4933 }
4934 b"pt" => {
4935 self.points.push(parse_point(&raw, &self.namespaces)?);
4936 self.boundary = self.boundary.max(self.point_base + self.points.len());
4937 }
4938 _ => self.raw_children.push(self.boundary, raw),
4939 }
4940 Ok(())
4941 }
4942
4943 fn finish(
4944 self,
4945 raw_attributes: XmlAttributes,
4946 ) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
4947 let (declared, point_count_markup) = self
4948 .point_count
4949 .ok_or_else(|| ChartError::MissingElement("c:ptCount".to_owned()))?;
4950 let actual = u32::try_from(self.points.len()).map_err(|_| ChartError::InvalidValue {
4951 element: "c:ptCount".to_owned(),
4952 value: self.points.len().to_string(),
4953 })?;
4954 if declared < actual {
4955 return Err(invalid_attribute("ptCount", "val", declared.to_string()));
4956 }
4957 let mut values = Vec::with_capacity(self.points.len());
4958 let mut point_indexes = Vec::with_capacity(self.points.len());
4959 let mut point_markup = Vec::with_capacity(self.points.len());
4960 let mut previous = None;
4961 for (index, value, markup) in self.points {
4962 if index >= declared || previous.is_some_and(|last| index <= last) {
4963 return Err(invalid_attribute("pt", "idx", index.to_string()));
4964 }
4965 previous = Some(index);
4966 values.push(value);
4967 point_indexes.push(index);
4968 point_markup.push(markup);
4969 }
4970 let (format_code, format_markup) = self
4971 .format_code
4972 .map(|(value, markup)| (Some(value), Some(markup)))
4973 .unwrap_or((None, None));
4974 let format_code_omitted = self.numeric && format_code.is_none();
4975 Ok((
4976 format_code,
4977 values,
4978 CacheMarkup {
4979 raw_attributes,
4980 raw_children: raw_children_in_schema_order(
4981 &self.raw_children,
4982 self.point_base + point_markup.len(),
4983 ),
4984 format_code: format_markup,
4985 format_code_omitted,
4986 point_count: point_count_markup,
4987 declared_point_count: Some(declared),
4988 point_indexes,
4989 points: point_markup,
4990 },
4991 ))
4992 }
4993}
4994
4995fn parse_point(xml: &[u8], inherited: &NamespaceBindings) -> Result<(u32, String, PointMarkup)> {
4996 let mut reader = Reader::from_reader(xml);
4997 let mut buffer = Vec::new();
4998 loop {
4999 match reader
5000 .read_event_into(&mut buffer)
5001 .map_err(OxmlError::from)?
5002 {
5003 Event::Start(element) if matches_local_name(element.name().as_ref(), b"pt") => {
5004 let namespaces = typed_rewrite_bindings(&element, inherited)?;
5005 let index = required_u32_attribute(&element, "pt", b"idx")?;
5006 let raw_attributes = capture_attributes_except(&element, b"idx")?;
5007 let mut value: Option<(String, TextMarkup)> = None;
5008 let mut raw_children = OrderedRawChildren::default();
5009 let mut boundary = 0usize;
5010 let mut inner = Vec::new();
5011 loop {
5012 match reader
5013 .read_event_into(&mut inner)
5014 .map_err(OxmlError::from)?
5015 {
5016 Event::Start(child) => {
5017 let name = chart_child_local(&child, &namespaces)?;
5018 let raw = capture_element(&mut reader, &child)?;
5019 if name.as_deref() == Some(b"v") {
5020 set_once(
5021 &mut value,
5022 parse_text_element(&raw, b"v", &namespaces)?,
5023 "c:v",
5024 )?;
5025 boundary = 1;
5026 } else {
5027 raw_children.push(boundary, raw);
5028 }
5029 }
5030 Event::Empty(child) => {
5031 let name = chart_child_local(&child, &namespaces)?;
5032 let raw = capture_empty_element(&child)?;
5033 if name.as_deref() == Some(b"v") {
5034 set_once(
5035 &mut value,
5036 parse_text_element(&raw, b"v", &namespaces)?,
5037 "c:v",
5038 )?;
5039 boundary = 1;
5040 } else {
5041 raw_children.push(boundary, raw);
5042 }
5043 }
5044 event @ (Event::Text(_)
5045 | Event::CData(_)
5046 | Event::Comment(_)
5047 | Event::PI(_)
5048 | Event::GeneralRef(_)) => {
5049 raw_children.push(boundary, capture_event(event)?);
5050 }
5051 Event::End(end) if matches_local_name(end.name().as_ref(), b"pt") => {
5052 let (value, value_markup) = value
5053 .ok_or_else(|| ChartError::MissingElement("c:v".to_owned()))?;
5054 return Ok((
5055 index,
5056 value,
5057 PointMarkup {
5058 raw_attributes,
5059 raw_children: raw_children_in_schema_order(&raw_children, 1),
5060 value: value_markup,
5061 },
5062 ));
5063 }
5064 Event::Eof => return Err(missing_end("c:pt")),
5065 _ => {}
5066 }
5067 inner.clear();
5068 }
5069 }
5070 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pt") => {
5071 return Err(ChartError::MissingElement("c:v".to_owned()));
5072 }
5073 Event::Start(element) | Event::Empty(element) => {
5074 return Err(ChartError::UnexpectedElement(element_name(&element)));
5075 }
5076 Event::Eof => return Err(ChartError::MissingElement("c:pt".to_owned())),
5077 _ => {}
5078 }
5079 buffer.clear();
5080 }
5081}
5082
5083fn parse_text_element(
5084 xml: &[u8],
5085 local: &[u8],
5086 inherited: &NamespaceBindings,
5087) -> Result<(String, TextMarkup)> {
5088 let mut reader = Reader::from_reader(xml);
5089 let mut buffer = Vec::new();
5090 loop {
5091 match reader
5092 .read_event_into(&mut buffer)
5093 .map_err(OxmlError::from)?
5094 {
5095 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
5096 typed_rewrite_bindings(&element, inherited)?;
5097 let raw_attributes = capture_attributes(&element)?;
5098 let mut value = String::new();
5099 let mut raw_children = OrderedRawChildren::default();
5100 let mut seen_text = false;
5101 let mut inner = Vec::new();
5102 loop {
5103 match reader
5104 .read_event_into(&mut inner)
5105 .map_err(OxmlError::from)?
5106 {
5107 Event::Text(text) => {
5108 value.push_str(&decode_plain(&text));
5109 seen_text = true;
5110 }
5111 Event::CData(text) => {
5112 let decoded =
5113 text.decode().map_err(|error| ChartError::InvalidValue {
5114 element: format!("c:{}", String::from_utf8_lossy(local)),
5115 value: error.to_string(),
5116 })?;
5117 value.push_str(&decoded);
5118 seen_text = true;
5119 }
5120 Event::GeneralRef(reference) => {
5121 value.push_str(&resolve_entity(&reference));
5122 seen_text = true;
5123 }
5124 event @ (Event::Comment(_) | Event::PI(_)) => {
5125 raw_children.push(usize::from(seen_text), capture_event(event)?);
5126 }
5127 Event::Start(child) | Event::Empty(child) => {
5128 return Err(ChartError::UnexpectedElement(element_name(&child)));
5129 }
5130 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
5131 return Ok((
5132 value,
5133 TextMarkup {
5134 raw_attributes,
5135 raw_children: raw_children_in_schema_order(&raw_children, 1),
5136 },
5137 ));
5138 }
5139 Event::Eof => {
5140 return Err(missing_end(&format!(
5141 "c:{}",
5142 String::from_utf8_lossy(local)
5143 )));
5144 }
5145 _ => {}
5146 }
5147 inner.clear();
5148 }
5149 }
5150 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
5151 typed_rewrite_bindings(&element, inherited)?;
5152 return Ok((
5153 String::new(),
5154 TextMarkup {
5155 raw_attributes: capture_attributes(&element)?,
5156 raw_children: OrderedRawChildren::default(),
5157 },
5158 ));
5159 }
5160 Event::Start(element) | Event::Empty(element) => {
5161 return Err(ChartError::UnexpectedElement(element_name(&element)));
5162 }
5163 Event::Eof => {
5164 return Err(ChartError::MissingElement(format!(
5165 "c:{}",
5166 String::from_utf8_lossy(local)
5167 )));
5168 }
5169 _ => {}
5170 }
5171 buffer.clear();
5172 }
5173}
5174
5175struct ParsedWrapper {
5176 choice: Option<(Vec<u8>, Vec<u8>)>,
5177 markup: WrapperMarkup,
5178 namespaces: NamespaceBindings,
5179}
5180
5181fn parse_wrapper(
5182 xml: &[u8],
5183 local: &[u8],
5184 choices: &[&[u8]],
5185 inherited: &NamespaceBindings,
5186) -> Result<ParsedWrapper> {
5187 let mut reader = Reader::from_reader(xml);
5188 let mut buffer = Vec::new();
5189 loop {
5190 match reader
5191 .read_event_into(&mut buffer)
5192 .map_err(OxmlError::from)?
5193 {
5194 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
5195 let namespaces = typed_rewrite_bindings(&element, inherited)?;
5196 let raw_attributes = capture_attributes(&element)?;
5197 let mut choice = None;
5198 let mut raw_children = OrderedRawChildren::default();
5199 let mut boundary = 0usize;
5200 let mut inner = Vec::new();
5201 loop {
5202 match reader
5203 .read_event_into(&mut inner)
5204 .map_err(OxmlError::from)?
5205 {
5206 Event::Start(child) => {
5207 let name = chart_child_local(&child, &namespaces)?;
5208 let raw = capture_element(&mut reader, &child)?;
5209 if let Some(name) =
5210 name.filter(|name| choices.contains(&name.as_slice()))
5211 {
5212 if choice.is_some() {
5213 return Err(ChartError::DuplicateElement(format!(
5214 "c:{} reference",
5215 String::from_utf8_lossy(local)
5216 )));
5217 }
5218 choice = Some((name, raw));
5219 boundary = 1;
5220 } else {
5221 raw_children.push(boundary, raw);
5222 }
5223 }
5224 Event::Empty(child) => {
5225 let name = chart_child_local(&child, &namespaces)?;
5226 let raw = capture_empty_element(&child)?;
5227 if let Some(name) =
5228 name.filter(|name| choices.contains(&name.as_slice()))
5229 {
5230 if choice.is_some() {
5231 return Err(ChartError::DuplicateElement(format!(
5232 "c:{} reference",
5233 String::from_utf8_lossy(local)
5234 )));
5235 }
5236 choice = Some((name, raw));
5237 boundary = 1;
5238 } else {
5239 raw_children.push(boundary, raw);
5240 }
5241 }
5242 event @ (Event::Text(_)
5243 | Event::CData(_)
5244 | Event::Comment(_)
5245 | Event::PI(_)
5246 | Event::GeneralRef(_)) => {
5247 raw_children.push(boundary, capture_event(event)?);
5248 }
5249 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
5250 return Ok(ParsedWrapper {
5251 choice,
5252 markup: WrapperMarkup {
5253 raw_attributes,
5254 raw_children: raw_children_in_schema_order(&raw_children, 1),
5255 },
5256 namespaces,
5257 });
5258 }
5259 Event::Eof => {
5260 return Err(missing_end(&format!(
5261 "c:{}",
5262 String::from_utf8_lossy(local)
5263 )));
5264 }
5265 _ => {}
5266 }
5267 inner.clear();
5268 }
5269 }
5270 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
5271 let namespaces = chart_bindings(inherited, &element)?;
5272 return Ok(ParsedWrapper {
5273 choice: None,
5274 markup: WrapperMarkup {
5275 raw_attributes: capture_attributes(&element)?,
5276 raw_children: OrderedRawChildren::default(),
5277 },
5278 namespaces,
5279 });
5280 }
5281 Event::Start(element) | Event::Empty(element) => {
5282 return Err(ChartError::UnexpectedElement(element_name(&element)));
5283 }
5284 Event::Eof => {
5285 return Err(ChartError::MissingElement(format!(
5286 "c:{}",
5287 String::from_utf8_lossy(local)
5288 )));
5289 }
5290 _ => {}
5291 }
5292 buffer.clear();
5293 }
5294}
5295
5296fn write_string_cache(
5297 writer: &mut Writer<Vec<u8>>,
5298 values: &[String],
5299 markup: &ReferenceMarkup,
5300) -> Result<()> {
5301 let (declared_count, indexes) = cache_layout(markup, values.len())?;
5302 let mut start = BytesStart::new("c:strCache");
5303 push_attributes(&mut start, &markup.cache_attributes);
5304 writer
5305 .write_event(Event::Start(start))
5306 .map_err(OxmlError::from)?;
5307 emit_raw(writer, markup.cache_children.at(0))?;
5308 write_scalar(
5309 writer,
5310 "c:ptCount",
5311 &declared_count.to_string(),
5312 Some(&markup.point_count),
5313 )?;
5314 emit_cache_point_raw(writer, markup, 1, values.len(), 0)?;
5315 for (position, value) in values.iter().enumerate() {
5316 let default_markup = PointMarkup::default();
5317 write_point(
5318 writer,
5319 indexes[position],
5320 value,
5321 markup.points.get(position).unwrap_or(&default_markup),
5322 )?;
5323 emit_cache_point_raw(writer, markup, 1, values.len(), position + 1)?;
5324 }
5325 writer
5326 .write_event(Event::End(BytesEnd::new("c:strCache")))
5327 .map_err(OxmlError::from)?;
5328 Ok(())
5329}
5330
5331fn write_numeric_cache(
5332 writer: &mut Writer<Vec<u8>>,
5333 data: &NumericData,
5334 markup: &ReferenceMarkup,
5335) -> Result<()> {
5336 let (declared_count, indexes) = cache_layout(markup, data.values.len())?;
5337 let mut start = BytesStart::new("c:numCache");
5338 push_attributes(&mut start, &markup.cache_attributes);
5339 writer
5340 .write_event(Event::Start(start))
5341 .map_err(OxmlError::from)?;
5342 emit_raw(writer, markup.cache_children.at(0))?;
5343 if !markup.format_code_omitted || data.format_code != "General" {
5344 let default_format_markup = TextMarkup::default();
5345 write_text(
5346 writer,
5347 "c:formatCode",
5348 &data.format_code,
5349 markup
5350 .format_code
5351 .as_ref()
5352 .unwrap_or(&default_format_markup),
5353 )?;
5354 }
5355 emit_raw(writer, markup.cache_children.at(1))?;
5356 write_scalar(
5357 writer,
5358 "c:ptCount",
5359 &declared_count.to_string(),
5360 Some(&markup.point_count),
5361 )?;
5362 emit_cache_point_raw(writer, markup, 2, data.values.len(), 0)?;
5363 for (position, value) in data.values.iter().enumerate() {
5364 let default_markup = PointMarkup::default();
5365 write_point(
5366 writer,
5367 indexes[position],
5368 &value.to_string(),
5369 markup.points.get(position).unwrap_or(&default_markup),
5370 )?;
5371 emit_cache_point_raw(writer, markup, 2, data.values.len(), position + 1)?;
5372 }
5373 writer
5374 .write_event(Event::End(BytesEnd::new("c:numCache")))
5375 .map_err(OxmlError::from)?;
5376 Ok(())
5377}
5378
5379fn emit_cache_point_raw(
5380 writer: &mut Writer<Vec<u8>>,
5381 markup: &ReferenceMarkup,
5382 point_base: usize,
5383 current_count: usize,
5384 completed_points: usize,
5385) -> Result<()> {
5386 let original_count = markup.points.len();
5387 if current_count == 0 {
5388 for boundary in point_base..=point_base + original_count {
5389 emit_raw(writer, markup.cache_children.at(boundary))?;
5390 }
5391 } else if completed_points == 0 {
5392 if original_count > 0 {
5393 emit_raw(writer, markup.cache_children.at(point_base))?;
5394 }
5395 } else if completed_points < current_count {
5396 if completed_points < original_count {
5397 emit_raw(
5398 writer,
5399 markup.cache_children.at(point_base + completed_points),
5400 )?;
5401 }
5402 } else {
5403 let first_tail = if original_count == 0 {
5404 point_base
5405 } else {
5406 point_base + current_count.min(original_count)
5407 };
5408 for boundary in first_tail..=point_base + original_count {
5409 emit_raw(writer, markup.cache_children.at(boundary))?;
5410 }
5411 }
5412 Ok(())
5413}
5414
5415fn write_point(
5416 writer: &mut Writer<Vec<u8>>,
5417 index: u32,
5418 value: &str,
5419 markup: &PointMarkup,
5420) -> Result<()> {
5421 let index = index.to_string();
5422 let mut start = BytesStart::new("c:pt");
5423 start.push_attribute(("idx", index.as_str()));
5424 push_attributes(&mut start, &markup.raw_attributes);
5425 writer
5426 .write_event(Event::Start(start))
5427 .map_err(OxmlError::from)?;
5428 emit_raw(writer, markup.raw_children.at(0))?;
5429 write_text(writer, "c:v", value, &markup.value)?;
5430 emit_raw(writer, markup.raw_children.at(1))?;
5431 writer
5432 .write_event(Event::End(BytesEnd::new("c:pt")))
5433 .map_err(OxmlError::from)?;
5434 Ok(())
5435}
5436
5437fn cache_layout(markup: &ReferenceMarkup, value_count: usize) -> Result<(u32, Vec<u32>)> {
5438 if markup.point_indexes.len() == value_count
5439 && let Some(declared) = markup.declared_point_count
5440 {
5441 return Ok((declared, markup.point_indexes.clone()));
5442 }
5443 let declared = u32::try_from(value_count).map_err(|_| ChartError::InvalidValue {
5444 element: "c:ptCount".to_owned(),
5445 value: value_count.to_string(),
5446 })?;
5447 Ok((declared, (0..declared).collect()))
5448}
5449
5450fn write_text(
5451 writer: &mut Writer<Vec<u8>>,
5452 tag: &str,
5453 value: &str,
5454 markup: &TextMarkup,
5455) -> Result<()> {
5456 let mut start = BytesStart::new(tag);
5457 push_attributes(&mut start, &markup.raw_attributes);
5458 writer
5459 .write_event(Event::Start(start))
5460 .map_err(OxmlError::from)?;
5461 emit_raw(writer, markup.raw_children.at(0))?;
5462 writer
5463 .write_event(Event::Text(BytesText::new(value)))
5464 .map_err(OxmlError::from)?;
5465 emit_raw(writer, markup.raw_children.at(1))?;
5466 writer
5467 .write_event(Event::End(BytesEnd::new(tag)))
5468 .map_err(OxmlError::from)?;
5469 Ok(())
5470}
5471
5472fn write_wrapper_start(
5473 writer: &mut Writer<Vec<u8>>,
5474 tag: &str,
5475 markup: &WrapperMarkup,
5476) -> Result<()> {
5477 let mut start = BytesStart::new(tag);
5478 push_attributes(&mut start, &markup.raw_attributes);
5479 writer
5480 .write_event(Event::Start(start))
5481 .map_err(OxmlError::from)?;
5482 emit_raw(writer, markup.raw_children.at(0))
5483}
5484
5485fn write_wrapper_end(
5486 writer: &mut Writer<Vec<u8>>,
5487 tag: &str,
5488 markup: &WrapperMarkup,
5489) -> Result<()> {
5490 emit_raw(writer, markup.raw_children.at(1))?;
5491 writer
5492 .write_event(Event::End(BytesEnd::new(tag)))
5493 .map_err(OxmlError::from)?;
5494 Ok(())
5495}
5496
5497fn validate_formula(formula: &str, element: &str) -> Result<()> {
5498 if formula.trim().is_empty() {
5499 return Err(ChartError::InvalidValue {
5500 element: element.to_owned(),
5501 value: formula.to_owned(),
5502 });
5503 }
5504 validate_xml_text(formula, element)
5505}
5506
5507fn validate_format_code(format_code: &str) -> Result<()> {
5508 if format_code.is_empty() {
5509 return Err(ChartError::InvalidValue {
5510 element: "c:formatCode".to_owned(),
5511 value: format_code.to_owned(),
5512 });
5513 }
5514 validate_xml_text(format_code, "c:formatCode")
5515}
5516
5517fn validate_xml_text(value: &str, element: &str) -> Result<()> {
5518 if value.chars().all(|character| {
5519 matches!(
5520 character,
5521 '\u{9}' | '\u{A}' | '\u{D}' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}'
5522 )
5523 }) {
5524 return Ok(());
5525 }
5526 Err(ChartError::InvalidValue {
5527 element: element.to_owned(),
5528 value: value.to_owned(),
5529 })
5530}
5531
5532fn validate_numeric_values(values: &[f64]) -> Result<()> {
5533 if let Some(value) = values.iter().find(|value| !value.is_finite()) {
5534 return Err(ChartError::InvalidValue {
5535 element: "c:v".to_owned(),
5536 value: value.to_string(),
5537 });
5538 }
5539 Ok(())
5540}
5541
5542fn validate_point_count(count: usize, element: &str) -> Result<()> {
5543 u32::try_from(count)
5544 .map(|_| ())
5545 .map_err(|_| ChartError::InvalidValue {
5546 element: element.to_owned(),
5547 value: count.to_string(),
5548 })
5549}
5550
5551fn parse_u32_scalar(xml: &[u8], local: &str) -> Result<(u32, ScalarMarkup)> {
5552 let (value, markup) = scalar_value(xml, local)?;
5553 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
5554 let parsed = value
5555 .parse::<u32>()
5556 .map_err(|_| invalid_attribute(local, "val", value))?;
5557 Ok((parsed, markup))
5558}
5559
5560fn required_u32_attribute(element: &BytesStart<'_>, local: &str, attribute: &[u8]) -> Result<u32> {
5561 let value = attribute_value(element, attribute)?.ok_or_else(|| {
5562 invalid_attribute(
5563 local,
5564 &String::from_utf8_lossy(attribute),
5565 "<missing>".to_owned(),
5566 )
5567 })?;
5568 value
5569 .parse::<u32>()
5570 .map_err(|_| invalid_attribute(local, &String::from_utf8_lossy(attribute), value))
5571}
5572
5573fn chart_root_prefix(element: &BytesStart<'_>) -> Result<Vec<u8>> {
5574 let name = element.name();
5575 let qualified = name.as_ref();
5576 let prefix = qualified
5577 .iter()
5578 .position(|byte| *byte == b':')
5579 .map(|position| qualified[..position].to_vec())
5580 .unwrap_or_default();
5581 let declaration = if prefix.is_empty() {
5582 b"xmlns".to_vec()
5583 } else {
5584 let mut declaration = b"xmlns:".to_vec();
5585 declaration.extend_from_slice(&prefix);
5586 declaration
5587 };
5588 if let Some(namespace) = attribute_value(element, &declaration)?
5589 && namespace != C_NS
5590 {
5591 return Err(invalid_attribute(
5592 &element_name(element),
5593 "namespace",
5594 namespace,
5595 ));
5596 }
5597 reject_conflicting_prefix(element, b"c", C_NS)?;
5598 Ok(prefix)
5599}
5600
5601fn typed_rewrite_bindings(
5602 element: &BytesStart<'_>,
5603 inherited: &NamespaceBindings,
5604) -> Result<NamespaceBindings> {
5605 chart_root_prefix(element)?;
5606 if !element_is_in_namespace(element, C_NS, inherited)? {
5607 return Err(ChartError::UnexpectedElement(element_name(element)));
5608 }
5609 let bindings = chart_bindings(inherited, element)?;
5610 require_fixed_namespace(&bindings, b"c", C_NS, element)?;
5611 Ok(bindings)
5612}
5613
5614fn chart_namespace_defaults() -> NamespaceBindings {
5615 vec![
5616 (b"c".to_vec(), C_NS.to_owned()),
5617 (b"a".to_vec(), A_NS.to_owned()),
5618 (b"r".to_vec(), R_NS.to_owned()),
5619 ]
5620}
5621
5622fn chart_bindings(
5623 inherited: &NamespaceBindings,
5624 element: &BytesStart<'_>,
5625) -> Result<NamespaceBindings> {
5626 let mut bindings = chart_namespace_defaults();
5627 for (prefix, namespace) in inherited
5628 .iter()
5629 .cloned()
5630 .chain(namespace_bindings(element)?)
5631 {
5632 upsert_namespace_binding(&mut bindings, prefix, namespace);
5633 }
5634 Ok(bindings)
5635}
5636
5637fn root_chart_bindings(
5638 xml: &[u8],
5639 local: &[u8],
5640 inherited: &NamespaceBindings,
5641) -> Result<NamespaceBindings> {
5642 let mut reader = Reader::from_reader(xml);
5643 let mut buffer = Vec::new();
5644 loop {
5645 match reader
5646 .read_event_into(&mut buffer)
5647 .map_err(OxmlError::from)?
5648 {
5649 Event::Start(element) | Event::Empty(element)
5650 if matches_local_name(element.name().as_ref(), local) =>
5651 {
5652 if !element_is_in_namespace(&element, C_NS, inherited)? {
5653 return Err(ChartError::UnexpectedElement(element_name(&element)));
5654 }
5655 return chart_bindings(inherited, &element);
5656 }
5657 Event::Start(element) | Event::Empty(element) => {
5658 return Err(ChartError::UnexpectedElement(element_name(&element)));
5659 }
5660 Event::Eof => {
5661 return Err(ChartError::MissingElement(format!(
5662 "c:{}",
5663 String::from_utf8_lossy(local)
5664 )));
5665 }
5666 _ => {}
5667 }
5668 buffer.clear();
5669 }
5670}
5671
5672fn chart_child_local(
5673 element: &BytesStart<'_>,
5674 inherited: &NamespaceBindings,
5675) -> Result<Option<Vec<u8>>> {
5676 Ok(element_is_in_namespace(element, C_NS, inherited)?
5677 .then(|| local_name(element.name().as_ref()).to_vec()))
5678}
5679
5680fn capture_fixed_root_attributes(
5681 start: &BytesStart<'_>,
5682 fixed: &[&str],
5683) -> Result<(XmlAttributes, XmlAttributes)> {
5684 let mut attributes = Vec::new();
5685 let mut namespaces = Vec::new();
5686 for attribute in start.attributes() {
5687 let attribute = attribute.map_err(OxmlError::from)?;
5688 let name = std::str::from_utf8(attribute.key.as_ref())
5689 .map_err(OxmlError::from)?
5690 .to_owned();
5691 let value = attribute
5692 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
5693 .map_err(OxmlError::from)?
5694 .into_owned();
5695 if fixed.contains(&name.as_str()) {
5696 continue;
5697 }
5698 if name == "xmlns" || name.starts_with("xmlns:") {
5699 namespaces.push((name, value));
5700 } else {
5701 attributes.push((name, value));
5702 }
5703 }
5704 Ok((attributes, namespaces))
5705}
5706
5707fn require_fixed_namespace(
5708 bindings: &NamespaceBindings,
5709 prefix: &[u8],
5710 expected: &str,
5711 element: &BytesStart<'_>,
5712) -> Result<()> {
5713 let actual = bindings
5714 .iter()
5715 .find(|(candidate, _)| candidate.as_slice() == prefix)
5716 .map(|(_, namespace)| namespace.as_str());
5717 if actual == Some(expected) {
5718 return Ok(());
5719 }
5720 Err(invalid_attribute(
5721 &element_name(element),
5722 &format!("xmlns:{}", String::from_utf8_lossy(prefix)),
5723 actual.unwrap_or("<missing>").to_owned(),
5724 ))
5725}
5726
5727fn standalone_namespace_declarations(bindings: &NamespaceBindings) -> Result<XmlAttributes> {
5728 bindings
5729 .iter()
5730 .filter(|(prefix, _)| prefix != b"c" && prefix != b"a" && prefix != b"r")
5731 .map(|(prefix, namespace)| {
5732 let prefix = std::str::from_utf8(prefix).map_err(OxmlError::from)?;
5733 let name = if prefix.is_empty() {
5734 "xmlns".to_owned()
5735 } else {
5736 format!("xmlns:{prefix}")
5737 };
5738 Ok((name, namespace.clone()))
5739 })
5740 .collect()
5741}
5742
5743fn capture_fixed_attributes(start: &BytesStart<'_>, fixed: &[&str]) -> Result<XmlAttributes> {
5744 let mut attributes = Vec::new();
5745 for attribute in start.attributes() {
5746 let attribute = attribute.map_err(OxmlError::from)?;
5747 let name = std::str::from_utf8(attribute.key.as_ref())
5748 .map_err(OxmlError::from)?
5749 .to_owned();
5750 if fixed.contains(&name.as_str()) {
5751 continue;
5752 }
5753 let value = attribute
5754 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
5755 .map_err(OxmlError::from)?
5756 .into_owned();
5757 attributes.push((name, value));
5758 }
5759 Ok(attributes)
5760}
5761
5762#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
5767pub struct AxisId(i64);
5768
5769impl AxisId {
5770 pub fn new(value: i64) -> Result<Self> {
5771 if (i64::from(i32::MIN)..=i64::from(u32::MAX)).contains(&value) {
5772 Ok(Self(value))
5773 } else {
5774 Err(ChartError::InvalidValue {
5775 element: "c:axId".to_owned(),
5776 value: value.to_string(),
5777 })
5778 }
5779 }
5780
5781 pub const fn value(self) -> i64 {
5782 self.0
5783 }
5784}
5785
5786#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5788pub enum BarDirection {
5789 Bar,
5790 Column,
5791}
5792
5793impl BarDirection {
5794 fn parse(value: &str) -> Option<Self> {
5795 match value {
5796 "bar" => Some(Self::Bar),
5797 "col" => Some(Self::Column),
5798 _ => None,
5799 }
5800 }
5801
5802 const fn as_str(self) -> &'static str {
5803 match self {
5804 Self::Bar => "bar",
5805 Self::Column => "col",
5806 }
5807 }
5808}
5809
5810#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5812pub enum BarGrouping {
5813 Clustered,
5814 PercentStacked,
5815 Stacked,
5816 Standard,
5817}
5818
5819impl BarGrouping {
5820 fn parse(value: &str) -> Option<Self> {
5821 match value {
5822 "clustered" => Some(Self::Clustered),
5823 "percentStacked" => Some(Self::PercentStacked),
5824 "stacked" => Some(Self::Stacked),
5825 "standard" => Some(Self::Standard),
5826 _ => None,
5827 }
5828 }
5829
5830 const fn as_str(self) -> &'static str {
5831 match self {
5832 Self::Clustered => "clustered",
5833 Self::PercentStacked => "percentStacked",
5834 Self::Stacked => "stacked",
5835 Self::Standard => "standard",
5836 }
5837 }
5838}
5839
5840#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5842pub enum Grouping {
5843 PercentStacked,
5844 Stacked,
5845 Standard,
5846}
5847
5848impl Grouping {
5849 fn parse(value: &str) -> Option<Self> {
5850 match value {
5851 "percentStacked" => Some(Self::PercentStacked),
5852 "stacked" => Some(Self::Stacked),
5853 "standard" => Some(Self::Standard),
5854 _ => None,
5855 }
5856 }
5857
5858 const fn as_str(self) -> &'static str {
5859 match self {
5860 Self::PercentStacked => "percentStacked",
5861 Self::Stacked => "stacked",
5862 Self::Standard => "standard",
5863 }
5864 }
5865}
5866
5867#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5869pub enum ScatterStyle {
5870 Line,
5871 LineMarker,
5872 Marker,
5873 None,
5874 Smooth,
5875 SmoothMarker,
5876}
5877
5878impl ScatterStyle {
5879 fn parse(value: &str) -> Option<Self> {
5880 match value {
5881 "line" => Some(Self::Line),
5882 "lineMarker" => Some(Self::LineMarker),
5883 "marker" => Some(Self::Marker),
5884 "none" => Some(Self::None),
5885 "smooth" => Some(Self::Smooth),
5886 "smoothMarker" => Some(Self::SmoothMarker),
5887 _ => None,
5888 }
5889 }
5890
5891 const fn as_str(self) -> &'static str {
5892 match self {
5893 Self::Line => "line",
5894 Self::LineMarker => "lineMarker",
5895 Self::Marker => "marker",
5896 Self::None => "none",
5897 Self::Smooth => "smooth",
5898 Self::SmoothMarker => "smoothMarker",
5899 }
5900 }
5901}
5902
5903#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5905pub enum RadarStyle {
5906 Filled,
5907 Marker,
5908 Standard,
5909}
5910
5911impl RadarStyle {
5912 fn parse(value: &str) -> Option<Self> {
5913 match value {
5914 "filled" => Some(Self::Filled),
5915 "marker" => Some(Self::Marker),
5916 "standard" => Some(Self::Standard),
5917 _ => None,
5918 }
5919 }
5920
5921 const fn as_str(self) -> &'static str {
5922 match self {
5923 Self::Filled => "filled",
5924 Self::Marker => "marker",
5925 Self::Standard => "standard",
5926 }
5927 }
5928}
5929
5930#[derive(Clone, Debug, PartialEq)]
5932pub enum Plot {
5933 Bar {
5934 direction: BarDirection,
5935 grouping: BarGrouping,
5936 gap_width: u16,
5937 overlap: i8,
5938 series: Vec<Series>,
5939 data_labels: Option<CT_DLbls>,
5940 axis_ids: [AxisId; 2],
5941 },
5942 Line {
5943 grouping: Grouping,
5944 marker: bool,
5945 smooth: bool,
5946 series: Vec<Series>,
5947 data_labels: Option<CT_DLbls>,
5948 axis_ids: [AxisId; 2],
5949 },
5950 Pie {
5951 first_slice_angle: u16,
5952 series: Vec<Series>,
5953 data_labels: Option<CT_DLbls>,
5954 },
5955 Doughnut {
5956 first_slice_angle: u16,
5957 hole_size: u8,
5958 series: Vec<Series>,
5959 data_labels: Option<CT_DLbls>,
5960 },
5961 Area {
5962 grouping: Grouping,
5963 series: Vec<Series>,
5964 data_labels: Option<CT_DLbls>,
5965 axis_ids: [AxisId; 2],
5966 },
5967 Scatter {
5968 style: ScatterStyle,
5969 series: Vec<Series>,
5970 data_labels: Option<CT_DLbls>,
5971 axis_ids: [AxisId; 2],
5972 },
5973 Radar {
5974 style: RadarStyle,
5975 series: Vec<Series>,
5976 data_labels: Option<CT_DLbls>,
5977 axis_ids: [AxisId; 2],
5978 },
5979}
5980
5981impl Plot {
5982 pub fn bar(
5983 direction: BarDirection,
5984 grouping: BarGrouping,
5985 series: Vec<Series>,
5986 axis_ids: [AxisId; 2],
5987 ) -> Result<Self> {
5988 let plot = Self::Bar {
5989 direction,
5990 grouping,
5991 gap_width: 150,
5992 overlap: 0,
5993 series,
5994 data_labels: None,
5995 axis_ids,
5996 };
5997 plot.validate()?;
5998 Ok(plot)
5999 }
6000
6001 pub fn line(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
6002 let plot = Self::Line {
6003 grouping,
6004 marker: false,
6005 smooth: false,
6006 series,
6007 data_labels: None,
6008 axis_ids,
6009 };
6010 plot.validate()?;
6011 Ok(plot)
6012 }
6013
6014 pub fn pie(series: Vec<Series>) -> Result<Self> {
6015 let plot = Self::Pie {
6016 first_slice_angle: 0,
6017 series,
6018 data_labels: None,
6019 };
6020 plot.validate()?;
6021 Ok(plot)
6022 }
6023
6024 pub fn doughnut(series: Vec<Series>) -> Result<Self> {
6025 let plot = Self::Doughnut {
6026 first_slice_angle: 0,
6027 hole_size: 50,
6028 series,
6029 data_labels: None,
6030 };
6031 plot.validate()?;
6032 Ok(plot)
6033 }
6034
6035 pub fn area(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
6036 let plot = Self::Area {
6037 grouping,
6038 series,
6039 data_labels: None,
6040 axis_ids,
6041 };
6042 plot.validate()?;
6043 Ok(plot)
6044 }
6045
6046 pub fn scatter(
6047 style: ScatterStyle,
6048 series: Vec<Series>,
6049 axis_ids: [AxisId; 2],
6050 ) -> Result<Self> {
6051 let plot = Self::Scatter {
6052 style,
6053 series,
6054 data_labels: None,
6055 axis_ids,
6056 };
6057 plot.validate()?;
6058 Ok(plot)
6059 }
6060
6061 pub fn radar(style: RadarStyle, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
6062 let plot = Self::Radar {
6063 style,
6064 series,
6065 data_labels: None,
6066 axis_ids,
6067 };
6068 plot.validate()?;
6069 Ok(plot)
6070 }
6071
6072 fn axis_ids(&self) -> Option<[AxisId; 2]> {
6073 match self {
6074 Self::Bar { axis_ids, .. }
6075 | Self::Line { axis_ids, .. }
6076 | Self::Area { axis_ids, .. }
6077 | Self::Scatter { axis_ids, .. }
6078 | Self::Radar { axis_ids, .. } => Some(*axis_ids),
6079 Self::Pie { .. } | Self::Doughnut { .. } => None,
6080 }
6081 }
6082
6083 fn validate(&self) -> Result<()> {
6084 let (series, labels, axis_ids) = match self {
6085 Self::Bar {
6086 gap_width,
6087 overlap,
6088 series,
6089 data_labels,
6090 axis_ids,
6091 ..
6092 } => {
6093 if *gap_width > 500 {
6094 return Err(ChartError::InvalidValue {
6095 element: "c:gapWidth".to_owned(),
6096 value: gap_width.to_string(),
6097 });
6098 }
6099 if !(-100..=100).contains(overlap) {
6100 return Err(ChartError::InvalidValue {
6101 element: "c:overlap".to_owned(),
6102 value: overlap.to_string(),
6103 });
6104 }
6105 (series, data_labels, Some(axis_ids))
6106 }
6107 Self::Line {
6108 series,
6109 data_labels,
6110 axis_ids,
6111 ..
6112 } => (series, data_labels, Some(axis_ids)),
6113 Self::Pie {
6114 first_slice_angle,
6115 series,
6116 data_labels,
6117 } => {
6118 if *first_slice_angle > 360 {
6119 return Err(ChartError::InvalidValue {
6120 element: "c:firstSliceAng".to_owned(),
6121 value: first_slice_angle.to_string(),
6122 });
6123 }
6124 (series, data_labels, None)
6125 }
6126 Self::Doughnut {
6127 first_slice_angle,
6128 hole_size,
6129 series,
6130 data_labels,
6131 } => {
6132 if *first_slice_angle > 360 {
6133 return Err(ChartError::InvalidValue {
6134 element: "c:firstSliceAng".to_owned(),
6135 value: first_slice_angle.to_string(),
6136 });
6137 }
6138 if !(10..=90).contains(hole_size) {
6139 return Err(ChartError::InvalidValue {
6140 element: "c:holeSize".to_owned(),
6141 value: hole_size.to_string(),
6142 });
6143 }
6144 (series, data_labels, None)
6145 }
6146 Self::Area {
6147 series,
6148 data_labels,
6149 axis_ids,
6150 ..
6151 }
6152 | Self::Radar {
6153 series,
6154 data_labels,
6155 axis_ids,
6156 ..
6157 } => (series, data_labels, Some(axis_ids)),
6158 Self::Scatter {
6159 series,
6160 data_labels,
6161 axis_ids,
6162 ..
6163 } => {
6164 for item in series {
6165 if !matches!(item.categories, Some(AxisData::Numeric(_))) {
6166 return Err(ChartError::InvalidValue {
6167 element: "c:xVal".to_owned(),
6168 value: "scatter series requires numeric x values".to_owned(),
6169 });
6170 }
6171 }
6172 (series, data_labels, Some(axis_ids))
6173 }
6174 };
6175 if series.is_empty() {
6176 return Err(ChartError::MissingElement("c:ser".to_owned()));
6177 }
6178 let scatter_plot = matches!(self, Self::Scatter { .. });
6179 for item in series {
6180 if item.bubble_size.is_some() || item.opaque_bubble_size {
6181 return Err(ChartError::InvalidValue {
6182 element: "c:bubbleSize".to_owned(),
6183 value: "bubble size is only valid in an opaque bubble plot".to_owned(),
6184 });
6185 }
6186 if item.uses_scatter_wrappers && !scatter_plot {
6187 return Err(ChartError::InvalidValue {
6188 element: "c:ser".to_owned(),
6189 value: "x/y wrappers are only valid in scatter plots".to_owned(),
6190 });
6191 }
6192 if scatter_plot && item.parsed_from_xml && !item.uses_scatter_wrappers {
6193 return Err(ChartError::InvalidValue {
6194 element: "c:ser".to_owned(),
6195 value: "scatter plots require xVal/yVal wrappers".to_owned(),
6196 });
6197 }
6198 }
6199 if let Some(axis_ids) = axis_ids {
6200 if axis_ids[0] == axis_ids[1] {
6201 return Err(ChartError::DuplicateElement(format!(
6202 "c:axId {}",
6203 axis_ids[0].value()
6204 )));
6205 }
6206 for id in axis_ids {
6207 AxisId::new(id.value())?;
6208 }
6209 }
6210 if let Some(labels) = labels {
6211 labels.validate()?;
6212 }
6213 for item in series {
6214 item.values.validate()?;
6215 }
6216 Ok(())
6217 }
6218}
6219
6220#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
6222pub enum AxisKind {
6223 Category,
6224 Value,
6225 Date,
6226 Series,
6227}
6228
6229impl AxisKind {
6230 fn parse(local: &[u8]) -> Option<Self> {
6231 match local {
6232 b"catAx" => Some(Self::Category),
6233 b"valAx" => Some(Self::Value),
6234 b"dateAx" => Some(Self::Date),
6235 b"serAx" => Some(Self::Series),
6236 _ => None,
6237 }
6238 }
6239
6240 const fn local(self) -> &'static str {
6241 match self {
6242 Self::Category => "catAx",
6243 Self::Value => "valAx",
6244 Self::Date => "dateAx",
6245 Self::Series => "serAx",
6246 }
6247 }
6248}
6249
6250#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6252pub enum Orientation {
6253 #[default]
6254 MinMax,
6255 MaxMin,
6256}
6257
6258impl Orientation {
6259 fn parse(value: &str) -> Option<Self> {
6260 match value {
6261 "minMax" => Some(Self::MinMax),
6262 "maxMin" => Some(Self::MaxMin),
6263 _ => None,
6264 }
6265 }
6266
6267 const fn as_str(self) -> &'static str {
6268 match self {
6269 Self::MinMax => "minMax",
6270 Self::MaxMin => "maxMin",
6271 }
6272 }
6273}
6274
6275#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6277pub enum AxisPosition {
6278 Bottom,
6279 Left,
6280 Right,
6281 Top,
6282}
6283
6284impl AxisPosition {
6285 fn parse(value: &str) -> Option<Self> {
6286 match value {
6287 "b" => Some(Self::Bottom),
6288 "l" => Some(Self::Left),
6289 "r" => Some(Self::Right),
6290 "t" => Some(Self::Top),
6291 _ => None,
6292 }
6293 }
6294
6295 const fn as_str(self) -> &'static str {
6296 match self {
6297 Self::Bottom => "b",
6298 Self::Left => "l",
6299 Self::Right => "r",
6300 Self::Top => "t",
6301 }
6302 }
6303}
6304
6305#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6307pub enum TickMark {
6308 Cross,
6309 Inside,
6310 #[default]
6311 None,
6312 Outside,
6313}
6314
6315impl TickMark {
6316 fn parse(value: &str) -> Option<Self> {
6317 match value {
6318 "cross" => Some(Self::Cross),
6319 "in" => Some(Self::Inside),
6320 "none" => Some(Self::None),
6321 "out" => Some(Self::Outside),
6322 _ => None,
6323 }
6324 }
6325
6326 const fn as_str(self) -> &'static str {
6327 match self {
6328 Self::Cross => "cross",
6329 Self::Inside => "in",
6330 Self::None => "none",
6331 Self::Outside => "out",
6332 }
6333 }
6334}
6335
6336#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6338pub enum TickLabelPosition {
6339 High,
6340 Low,
6341 #[default]
6342 NextTo,
6343 None,
6344}
6345
6346impl TickLabelPosition {
6347 fn parse(value: &str) -> Option<Self> {
6348 match value {
6349 "high" => Some(Self::High),
6350 "low" => Some(Self::Low),
6351 "nextTo" => Some(Self::NextTo),
6352 "none" => Some(Self::None),
6353 _ => None,
6354 }
6355 }
6356
6357 const fn as_str(self) -> &'static str {
6358 match self {
6359 Self::High => "high",
6360 Self::Low => "low",
6361 Self::NextTo => "nextTo",
6362 Self::None => "none",
6363 }
6364 }
6365}
6366
6367#[derive(Clone, Debug)]
6369pub struct Scaling {
6370 pub log_base: Option<f64>,
6371 pub orientation: Orientation,
6372 pub maximum: Option<f64>,
6373 pub minimum: Option<f64>,
6374 log_base_markup: Option<ScalarMarkup>,
6375 orientation_markup: Option<ScalarMarkup>,
6376 maximum_markup: Option<ScalarMarkup>,
6377 minimum_markup: Option<ScalarMarkup>,
6378 raw_attributes: XmlAttributes,
6379 raw_children: OrderedRawChildren,
6380}
6381
6382impl PartialEq for Scaling {
6383 fn eq(&self, other: &Self) -> bool {
6384 self.log_base == other.log_base
6385 && self.orientation == other.orientation
6386 && self.maximum == other.maximum
6387 && self.minimum == other.minimum
6388 && optional_scalar_markup_eq(&self.log_base_markup, &other.log_base_markup)
6389 && optional_scalar_markup_eq(&self.orientation_markup, &other.orientation_markup)
6390 && optional_scalar_markup_eq(&self.maximum_markup, &other.maximum_markup)
6391 && optional_scalar_markup_eq(&self.minimum_markup, &other.minimum_markup)
6392 && self.raw_attributes == other.raw_attributes
6393 && self.raw_children == other.raw_children
6394 }
6395}
6396
6397impl Default for Scaling {
6398 fn default() -> Self {
6399 Self {
6400 log_base: None,
6401 orientation: Orientation::MinMax,
6402 maximum: None,
6403 minimum: None,
6404 log_base_markup: None,
6405 orientation_markup: None,
6406 maximum_markup: None,
6407 minimum_markup: None,
6408 raw_attributes: Vec::new(),
6409 raw_children: OrderedRawChildren::default(),
6410 }
6411 }
6412}
6413
6414#[derive(Clone, Debug, Default, PartialEq)]
6416pub struct ChartLines {
6417 pub sp_pr: Option<CT_ShapeProperties>,
6418 raw_attributes: XmlAttributes,
6419 raw_children: OrderedRawChildren,
6420}
6421
6422#[derive(Clone, Debug, Eq, PartialEq)]
6424pub struct NumberFormat {
6425 pub format_code: String,
6426 pub source_linked: bool,
6427 raw_attributes: XmlAttributes,
6428 raw_content: Vec<Vec<u8>>,
6429}
6430
6431#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6433pub enum DataLabelPosition {
6434 BestFit,
6435 Bottom,
6436 Center,
6437 InsideBase,
6438 InsideEnd,
6439 Left,
6440 OutsideEnd,
6441 Right,
6442 Top,
6443}
6444
6445impl DataLabelPosition {
6446 fn parse(value: &str) -> Option<Self> {
6447 match value {
6448 "bestFit" => Some(Self::BestFit),
6449 "b" => Some(Self::Bottom),
6450 "ctr" => Some(Self::Center),
6451 "inBase" => Some(Self::InsideBase),
6452 "inEnd" => Some(Self::InsideEnd),
6453 "l" => Some(Self::Left),
6454 "outEnd" => Some(Self::OutsideEnd),
6455 "r" => Some(Self::Right),
6456 "t" => Some(Self::Top),
6457 _ => None,
6458 }
6459 }
6460
6461 const fn as_str(self) -> &'static str {
6462 match self {
6463 Self::BestFit => "bestFit",
6464 Self::Bottom => "b",
6465 Self::Center => "ctr",
6466 Self::InsideBase => "inBase",
6467 Self::InsideEnd => "inEnd",
6468 Self::Left => "l",
6469 Self::OutsideEnd => "outEnd",
6470 Self::Right => "r",
6471 Self::Top => "t",
6472 }
6473 }
6474}
6475
6476#[derive(Clone, Debug, Default, PartialEq)]
6478pub struct CT_DLbls {
6479 pub number_format: Option<NumberFormat>,
6480 pub position: Option<DataLabelPosition>,
6481 pub separator: Option<String>,
6482 pub show_legend_key: bool,
6483 pub show_value: bool,
6484 pub show_category_name: bool,
6485 pub show_series_name: bool,
6486 pub show_percent: bool,
6487 pub show_bubble_size: bool,
6488 position_markup: Option<ScalarMarkup>,
6489 show_legend_key_markup: Option<ScalarMarkup>,
6490 show_value_markup: Option<ScalarMarkup>,
6491 show_category_name_markup: Option<ScalarMarkup>,
6492 show_series_name_markup: Option<ScalarMarkup>,
6493 show_percent_markup: Option<ScalarMarkup>,
6494 show_bubble_size_markup: Option<ScalarMarkup>,
6495 separator_markup: Option<TextMarkup>,
6496 raw_attributes: XmlAttributes,
6497 namespace_declarations: XmlAttributes,
6498 raw_children: OrderedRawChildren,
6499}
6500
6501#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6503pub enum DispBlanksAs {
6504 #[default]
6505 Gap,
6506 Zero,
6507 Span,
6508}
6509
6510impl DispBlanksAs {
6511 fn parse(value: &str) -> Option<Self> {
6512 match value {
6513 "gap" => Some(Self::Gap),
6514 "zero" => Some(Self::Zero),
6515 "span" => Some(Self::Span),
6516 _ => None,
6517 }
6518 }
6519
6520 const fn as_str(self) -> &'static str {
6521 match self {
6522 Self::Gap => "gap",
6523 Self::Zero => "zero",
6524 Self::Span => "span",
6525 }
6526 }
6527}
6528
6529#[derive(Clone, Debug, Default, Eq, PartialEq)]
6531pub struct CT_Title {
6532 text: Option<CT_TextBody>,
6533 raw_attributes: Vec<(String, String)>,
6534 raw_children: OrderedRawChildren,
6535}
6536
6537#[derive(Clone, Debug, Default, PartialEq)]
6539struct PlotMarkup {
6540 raw_attributes: XmlAttributes,
6541 raw_children: OrderedRawChildren,
6542 direction: Option<ScalarMarkup>,
6543 grouping: Option<ScalarMarkup>,
6544 gap_width: Option<ScalarMarkup>,
6545 overlap: Option<ScalarMarkup>,
6546 marker: Option<ScalarMarkup>,
6547 smooth: Option<ScalarMarkup>,
6548 first_slice_angle: Option<ScalarMarkup>,
6549 hole_size: Option<ScalarMarkup>,
6550 style: Option<ScalarMarkup>,
6551 axis_ids: Vec<AxisIdMarkup>,
6552 original_series_keys: Vec<(u32, u32)>,
6553 original_axis_ids: Vec<AxisId>,
6554 parsed_bar: Option<bool>,
6555 parsed_remaining: Option<&'static str>,
6556}
6557
6558#[derive(Clone, Debug, PartialEq)]
6560pub struct CT_PlotArea {
6561 raw_attributes: Vec<(String, String)>,
6562 raw_children: OrderedRawChildren,
6563 namespace_bindings: NamespaceBindings,
6564 plots: Option<Vec<Plot>>,
6565 plot_markup: Vec<PlotMarkup>,
6566 axes: Vec<Axis>,
6567}
6568
6569#[derive(Clone, Debug, Default, Eq, PartialEq)]
6571pub struct CT_Legend {
6572 raw_attributes: Vec<(String, String)>,
6573 raw_children: OrderedRawChildren,
6574}
6575
6576#[derive(Clone, Debug, PartialEq)]
6577struct AxisIdMarkup {
6578 scalar: ScalarMarkup,
6579 parsed: AxisId,
6580 lexical: String,
6581}
6582
6583#[derive(Clone, Debug)]
6585pub struct Axis {
6586 pub kind: AxisKind,
6587 pub id: AxisId,
6588 pub scaling: Scaling,
6589 pub deleted: bool,
6590 pub position: AxisPosition,
6591 pub major_gridlines: Option<ChartLines>,
6592 pub minor_gridlines: Option<ChartLines>,
6593 pub title: Option<CT_Title>,
6594 pub number_format: Option<NumberFormat>,
6595 pub major_tick_mark: TickMark,
6596 pub minor_tick_mark: TickMark,
6597 pub tick_label_position: TickLabelPosition,
6598 pub sp_pr: Option<CT_ShapeProperties>,
6599 pub tx_pr: Option<CT_TextBody>,
6600 pub cross_axis: AxisId,
6601 parsed_kind: Option<AxisKind>,
6602 id_markup: AxisIdMarkup,
6603 delete_markup: Option<ScalarMarkup>,
6604 position_markup: ScalarMarkup,
6605 major_tick_mark_markup: Option<ScalarMarkup>,
6606 minor_tick_mark_markup: Option<ScalarMarkup>,
6607 tick_label_position_markup: Option<ScalarMarkup>,
6608 cross_axis_markup: AxisIdMarkup,
6609 raw_attributes: XmlAttributes,
6610 namespace_declarations: XmlAttributes,
6611 raw_children: OrderedRawChildren,
6612}
6613
6614impl PartialEq for Axis {
6615 fn eq(&self, other: &Self) -> bool {
6616 self.kind == other.kind
6617 && self.id == other.id
6618 && axis_id_markup_eq(&self.id_markup, &other.id_markup)
6619 && self.scaling == other.scaling
6620 && self.deleted == other.deleted
6621 && optional_scalar_markup_eq(&self.delete_markup, &other.delete_markup)
6622 && self.position == other.position
6623 && self.position_markup == other.position_markup
6624 && self.major_gridlines == other.major_gridlines
6625 && self.minor_gridlines == other.minor_gridlines
6626 && self.title == other.title
6627 && self.number_format == other.number_format
6628 && self.major_tick_mark == other.major_tick_mark
6629 && optional_scalar_markup_eq(
6630 &self.major_tick_mark_markup,
6631 &other.major_tick_mark_markup,
6632 )
6633 && self.minor_tick_mark == other.minor_tick_mark
6634 && optional_scalar_markup_eq(
6635 &self.minor_tick_mark_markup,
6636 &other.minor_tick_mark_markup,
6637 )
6638 && self.tick_label_position == other.tick_label_position
6639 && optional_scalar_markup_eq(
6640 &self.tick_label_position_markup,
6641 &other.tick_label_position_markup,
6642 )
6643 && self.sp_pr == other.sp_pr
6644 && self.tx_pr == other.tx_pr
6645 && self.cross_axis == other.cross_axis
6646 && axis_id_markup_eq(&self.cross_axis_markup, &other.cross_axis_markup)
6647 && self.raw_attributes == other.raw_attributes
6648 && self.namespace_declarations == other.namespace_declarations
6649 && self.raw_children == other.raw_children
6650 }
6651}
6652
6653impl Axis {
6654 pub fn new(kind: AxisKind, id: AxisId, position: AxisPosition, cross_axis: AxisId) -> Self {
6655 Self {
6656 kind,
6657 id,
6658 scaling: Scaling::default(),
6659 deleted: false,
6660 position,
6661 major_gridlines: None,
6662 minor_gridlines: None,
6663 title: None,
6664 number_format: None,
6665 major_tick_mark: TickMark::None,
6666 minor_tick_mark: TickMark::None,
6667 tick_label_position: TickLabelPosition::NextTo,
6668 sp_pr: None,
6669 tx_pr: None,
6670 cross_axis,
6671 parsed_kind: None,
6672 id_markup: default_axis_id_markup(id),
6673 delete_markup: None,
6674 position_markup: ScalarMarkup::default(),
6675 major_tick_mark_markup: None,
6676 minor_tick_mark_markup: None,
6677 tick_label_position_markup: None,
6678 cross_axis_markup: default_axis_id_markup(cross_axis),
6679 raw_attributes: Vec::new(),
6680 namespace_declarations: Vec::new(),
6681 raw_children: OrderedRawChildren::default(),
6682 }
6683 }
6684
6685 pub fn set_deleted(&mut self, deleted: bool) {
6687 self.deleted = deleted;
6688 self.delete_markup.get_or_insert_with(ScalarMarkup::default);
6689 }
6690
6691 pub fn from_xml(xml: &[u8]) -> Result<Self> {
6692 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
6693 }
6694
6695 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
6696 let mut reader = Reader::from_reader(xml);
6697 let mut buffer = Vec::new();
6698 loop {
6699 match reader
6700 .read_event_into(&mut buffer)
6701 .map_err(OxmlError::from)?
6702 {
6703 Event::Start(element) => {
6704 let name = element.name();
6705 let local = local_name(name.as_ref());
6706 let Some(kind) = AxisKind::parse(local) else {
6707 return Err(ChartError::UnexpectedElement(element_name(&element)));
6708 };
6709 chart_root_prefix(&element)?;
6710 if !element_is_in_namespace(&element, C_NS, inherited)? {
6711 return Err(ChartError::UnexpectedElement(element_name(&element)));
6712 }
6713 return Self::from_element(&mut reader, &element, inherited, kind);
6714 }
6715 Event::Empty(element) => {
6716 let name = element.name();
6717 let local = local_name(name.as_ref());
6718 if AxisKind::parse(local).is_some() {
6719 return Err(ChartError::MissingElement("c:axId".to_owned()));
6720 }
6721 return Err(ChartError::UnexpectedElement(element_name(&element)));
6722 }
6723 Event::Eof => return Err(ChartError::MissingElement("ChartML axis".to_owned())),
6724 _ => {}
6725 }
6726 buffer.clear();
6727 }
6728 }
6729
6730 fn from_element(
6731 reader: &mut Reader<&[u8]>,
6732 start: &BytesStart<'_>,
6733 inherited: &NamespaceBindings,
6734 kind: AxisKind,
6735 ) -> Result<Self> {
6736 reject_conflicting_prefix(start, b"a", A_NS)?;
6737 reject_conflicting_prefix(start, b"r", R_NS)?;
6738 let namespaces = chart_bindings(inherited, start)?;
6739 require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
6740 require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
6741 require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
6742 let (raw_attributes, _) =
6743 capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
6744 let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
6745 let mut state = AxisParseState::default();
6746 let mut buffer = Vec::new();
6747 loop {
6748 match reader
6749 .read_event_into(&mut buffer)
6750 .map_err(OxmlError::from)?
6751 {
6752 Event::Start(element) => {
6753 let name = chart_child_local(&element, &namespaces)?;
6754 let raw = capture_element(reader, &element)?;
6755 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
6756 }
6757 Event::Empty(element) => {
6758 let name = chart_child_local(&element, &namespaces)?;
6759 let raw = capture_empty_element(&element)?;
6760 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
6761 }
6762 event @ (Event::Text(_)
6763 | Event::CData(_)
6764 | Event::Comment(_)
6765 | Event::PI(_)
6766 | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
6767 Event::End(element)
6768 if matches_local_name(element.name().as_ref(), kind.local().as_bytes()) =>
6769 {
6770 break;
6771 }
6772 Event::Eof => return Err(missing_end(&format!("c:{}", kind.local()))),
6773 _ => {}
6774 }
6775 buffer.clear();
6776 }
6777 state.finish(kind, raw_attributes, namespace_declarations)
6778 }
6779
6780 pub fn to_xml(&self) -> Result<Vec<u8>> {
6781 self.validate()?;
6782 let mut writer = Writer::new(Vec::new());
6783 let root = format!("c:{}", self.kind.local());
6784 let mut start = BytesStart::new(&root);
6785 start.push_attribute(("xmlns:c", C_NS));
6786 start.push_attribute(("xmlns:a", A_NS));
6787 start.push_attribute(("xmlns:r", R_NS));
6788 push_attributes(&mut start, &self.namespace_declarations);
6789 push_attributes(&mut start, &self.raw_attributes);
6790 writer
6791 .write_event(Event::Start(start))
6792 .map_err(OxmlError::from)?;
6793 emit_raw(&mut writer, self.raw_children.at(0))?;
6794 write_axis_id(&mut writer, "c:axId", self.id, &self.id_markup)?;
6795 emit_raw(&mut writer, self.raw_children.at(1))?;
6796 self.scaling.write_xml(&mut writer)?;
6797 emit_raw(&mut writer, self.raw_children.at(2))?;
6798 if self.deleted || self.delete_markup.is_some() {
6799 write_scalar(
6800 &mut writer,
6801 "c:delete",
6802 bool_lexical(self.deleted),
6803 self.delete_markup.as_ref(),
6804 )?;
6805 }
6806 emit_raw(&mut writer, self.raw_children.at(3))?;
6807 write_scalar(
6808 &mut writer,
6809 "c:axPos",
6810 self.position.as_str(),
6811 Some(&self.position_markup),
6812 )?;
6813 emit_raw(&mut writer, self.raw_children.at(4))?;
6814 if let Some(lines) = &self.major_gridlines {
6815 lines.write_xml(&mut writer, "c:majorGridlines")?;
6816 }
6817 emit_raw(&mut writer, self.raw_children.at(5))?;
6818 if let Some(lines) = &self.minor_gridlines {
6819 lines.write_xml(&mut writer, "c:minorGridlines")?;
6820 }
6821 emit_raw(&mut writer, self.raw_children.at(6))?;
6822 if let Some(title) = &self.title {
6823 title.write_xml(&mut writer)?;
6824 }
6825 emit_raw(&mut writer, self.raw_children.at(7))?;
6826 if let Some(number_format) = &self.number_format {
6827 number_format.write_xml(&mut writer)?;
6828 }
6829 emit_raw(&mut writer, self.raw_children.at(8))?;
6830 if self.major_tick_mark != TickMark::None || self.major_tick_mark_markup.is_some() {
6831 write_scalar(
6832 &mut writer,
6833 "c:majorTickMark",
6834 self.major_tick_mark.as_str(),
6835 self.major_tick_mark_markup.as_ref(),
6836 )?;
6837 }
6838 emit_raw(&mut writer, self.raw_children.at(9))?;
6839 if self.minor_tick_mark != TickMark::None || self.minor_tick_mark_markup.is_some() {
6840 write_scalar(
6841 &mut writer,
6842 "c:minorTickMark",
6843 self.minor_tick_mark.as_str(),
6844 self.minor_tick_mark_markup.as_ref(),
6845 )?;
6846 }
6847 emit_raw(&mut writer, self.raw_children.at(10))?;
6848 if self.tick_label_position != TickLabelPosition::NextTo
6849 || self.tick_label_position_markup.is_some()
6850 {
6851 write_scalar(
6852 &mut writer,
6853 "c:tickLblPos",
6854 self.tick_label_position.as_str(),
6855 self.tick_label_position_markup.as_ref(),
6856 )?;
6857 }
6858 emit_raw(&mut writer, self.raw_children.at(11))?;
6859 if let Some(properties) = &self.sp_pr {
6860 properties.write_xml_as(&mut writer, "c:spPr")?;
6861 }
6862 emit_raw(&mut writer, self.raw_children.at(12))?;
6863 if let Some(text) = &self.tx_pr {
6864 text.write_xml_as(&mut writer, "c:txPr")?;
6865 }
6866 emit_raw(&mut writer, self.raw_children.at(13))?;
6867 write_axis_id(
6868 &mut writer,
6869 "c:crossAx",
6870 self.cross_axis,
6871 &self.cross_axis_markup,
6872 )?;
6873 emit_raw(&mut writer, self.raw_children.at(14))?;
6874 writer
6875 .write_event(Event::End(BytesEnd::new(&root)))
6876 .map_err(OxmlError::from)?;
6877 Ok(writer.into_inner())
6878 }
6879
6880 pub fn raw_children(&self) -> &OrderedRawChildren {
6881 &self.raw_children
6882 }
6883
6884 fn validate(&self) -> Result<()> {
6885 if let Some(parsed_kind) = self.parsed_kind
6886 && self.kind != parsed_kind
6887 {
6888 return Err(ChartError::InvalidValue {
6889 element: "ChartML axis root".to_owned(),
6890 value: format!(
6891 "parsed c:{} cannot be relabelled c:{}",
6892 parsed_kind.local(),
6893 self.kind.local()
6894 ),
6895 });
6896 }
6897 AxisId::new(self.id.value())?;
6898 AxisId::new(self.cross_axis.value())?;
6899 self.scaling.validate()?;
6900 if let Some(number_format) = &self.number_format {
6901 number_format.validate()?;
6902 }
6903 Ok(())
6904 }
6905}
6906
6907#[derive(Default)]
6908struct AxisParseState {
6909 id: Option<(AxisId, AxisIdMarkup)>,
6910 scaling: Option<Scaling>,
6911 deleted: Option<(bool, ScalarMarkup)>,
6912 position: Option<(AxisPosition, ScalarMarkup)>,
6913 major_gridlines: Option<ChartLines>,
6914 minor_gridlines: Option<ChartLines>,
6915 title: Option<CT_Title>,
6916 number_format: Option<NumberFormat>,
6917 major_tick_mark: Option<(TickMark, ScalarMarkup)>,
6918 minor_tick_mark: Option<(TickMark, ScalarMarkup)>,
6919 tick_label_position: Option<(TickLabelPosition, ScalarMarkup)>,
6920 sp_pr: Option<CT_ShapeProperties>,
6921 tx_pr: Option<CT_TextBody>,
6922 cross_axis: Option<(AxisId, AxisIdMarkup)>,
6923 raw_children: OrderedRawChildren,
6924 boundary: usize,
6925}
6926
6927impl AxisParseState {
6928 fn capture_event(&mut self, raw: Vec<u8>) {
6929 self.raw_children.push(self.boundary, raw);
6930 }
6931
6932 fn parse_child(
6933 &mut self,
6934 name: &[u8],
6935 raw: Vec<u8>,
6936 namespaces: &NamespaceBindings,
6937 ) -> Result<()> {
6938 match name {
6939 b"axId" => {
6940 set_once(&mut self.id, parse_axis_id_scalar(&raw, "axId")?, "c:axId")?;
6941 self.boundary = self.boundary.max(1);
6942 }
6943 b"scaling" => {
6944 set_once(
6945 &mut self.scaling,
6946 Scaling::from_xml(&raw, namespaces)?,
6947 "c:scaling",
6948 )?;
6949 self.boundary = self.boundary.max(2);
6950 }
6951 b"delete" => {
6952 set_once(
6953 &mut self.deleted,
6954 parse_bool_value(&raw, "delete")?,
6955 "c:delete",
6956 )?;
6957 self.boundary = self.boundary.max(3);
6958 }
6959 b"axPos" => {
6960 set_once(&mut self.position, parse_axis_position(&raw)?, "c:axPos")?;
6961 self.boundary = self.boundary.max(4);
6962 }
6963 b"majorGridlines" => {
6964 set_once(
6965 &mut self.major_gridlines,
6966 ChartLines::from_xml(&raw, b"majorGridlines", namespaces)?,
6967 "c:majorGridlines",
6968 )?;
6969 self.boundary = self.boundary.max(5);
6970 }
6971 b"minorGridlines" => {
6972 set_once(
6973 &mut self.minor_gridlines,
6974 ChartLines::from_xml(&raw, b"minorGridlines", namespaces)?,
6975 "c:minorGridlines",
6976 )?;
6977 self.boundary = self.boundary.max(6);
6978 }
6979 b"title" => {
6980 set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
6981 self.boundary = self.boundary.max(7);
6982 }
6983 b"numFmt" => {
6984 set_once(
6985 &mut self.number_format,
6986 NumberFormat::from_xml(&raw)?,
6987 "c:numFmt",
6988 )?;
6989 self.boundary = self.boundary.max(8);
6990 }
6991 b"majorTickMark" => {
6992 set_once(
6993 &mut self.major_tick_mark,
6994 parse_tick_mark(&raw, "majorTickMark")?,
6995 "c:majorTickMark",
6996 )?;
6997 self.boundary = self.boundary.max(9);
6998 }
6999 b"minorTickMark" => {
7000 set_once(
7001 &mut self.minor_tick_mark,
7002 parse_tick_mark(&raw, "minorTickMark")?,
7003 "c:minorTickMark",
7004 )?;
7005 self.boundary = self.boundary.max(10);
7006 }
7007 b"tickLblPos" => {
7008 set_once(
7009 &mut self.tick_label_position,
7010 parse_tick_label_position(&raw)?,
7011 "c:tickLblPos",
7012 )?;
7013 self.boundary = self.boundary.max(11);
7014 }
7015 b"spPr" => {
7016 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
7017 let properties = CT_ShapeProperties::from_xml(&raw)?;
7018 let mut writer = Writer::new(Vec::new());
7019 properties.write_xml_as(&mut writer, "c:spPr")?;
7020 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
7021 set_once(&mut self.sp_pr, properties, "c:spPr")?;
7022 self.boundary = self.boundary.max(12);
7023 }
7024 b"txPr" => {
7025 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
7026 let text = CT_TextBody::from_xml_as(&raw, b"txPr")?;
7027 let mut writer = Writer::new(Vec::new());
7028 text.write_xml_as(&mut writer, "c:txPr")?;
7029 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
7030 set_once(&mut self.tx_pr, text, "c:txPr")?;
7031 self.boundary = self.boundary.max(13);
7032 }
7033 b"crossAx" => {
7034 set_once(
7035 &mut self.cross_axis,
7036 parse_axis_id_scalar(&raw, "crossAx")?,
7037 "c:crossAx",
7038 )?;
7039 self.boundary = self.boundary.max(14);
7040 }
7041 _ => self.raw_children.push(self.boundary, raw),
7042 }
7043 Ok(())
7044 }
7045
7046 fn finish(
7047 self,
7048 kind: AxisKind,
7049 raw_attributes: XmlAttributes,
7050 namespace_declarations: XmlAttributes,
7051 ) -> Result<Axis> {
7052 let (id, id_markup) = self
7053 .id
7054 .ok_or_else(|| ChartError::MissingElement("c:axId".to_owned()))?;
7055 let scaling = self
7056 .scaling
7057 .ok_or_else(|| ChartError::MissingElement("c:scaling".to_owned()))?;
7058 let (position, position_markup) = self
7059 .position
7060 .ok_or_else(|| ChartError::MissingElement("c:axPos".to_owned()))?;
7061 let (cross_axis, cross_axis_markup) = self
7062 .cross_axis
7063 .ok_or_else(|| ChartError::MissingElement("c:crossAx".to_owned()))?;
7064 let (deleted, delete_markup) = self
7065 .deleted
7066 .map(|(value, markup)| (value, Some(markup)))
7067 .unwrap_or((false, None));
7068 let (major_tick_mark, major_tick_mark_markup) = self
7069 .major_tick_mark
7070 .map(|(value, markup)| (value, Some(markup)))
7071 .unwrap_or((TickMark::None, None));
7072 let (minor_tick_mark, minor_tick_mark_markup) = self
7073 .minor_tick_mark
7074 .map(|(value, markup)| (value, Some(markup)))
7075 .unwrap_or((TickMark::None, None));
7076 let (tick_label_position, tick_label_position_markup) = self
7077 .tick_label_position
7078 .map(|(value, markup)| (value, Some(markup)))
7079 .unwrap_or((TickLabelPosition::NextTo, None));
7080 let axis = Axis {
7081 kind,
7082 id,
7083 scaling,
7084 deleted,
7085 position,
7086 major_gridlines: self.major_gridlines,
7087 minor_gridlines: self.minor_gridlines,
7088 title: self.title,
7089 number_format: self.number_format,
7090 major_tick_mark,
7091 minor_tick_mark,
7092 tick_label_position,
7093 sp_pr: self.sp_pr,
7094 tx_pr: self.tx_pr,
7095 cross_axis,
7096 parsed_kind: Some(kind),
7097 id_markup,
7098 delete_markup,
7099 position_markup,
7100 major_tick_mark_markup,
7101 minor_tick_mark_markup,
7102 tick_label_position_markup,
7103 cross_axis_markup,
7104 raw_attributes,
7105 namespace_declarations,
7106 raw_children: raw_children_in_schema_order(&self.raw_children, 14),
7107 };
7108 axis.validate()?;
7109 Ok(axis)
7110 }
7111}
7112
7113impl Scaling {
7114 fn from_xml(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7115 let mut reader = Reader::from_reader(xml);
7116 let mut buffer = Vec::new();
7117 loop {
7118 match reader
7119 .read_event_into(&mut buffer)
7120 .map_err(OxmlError::from)?
7121 {
7122 Event::Start(element)
7123 if matches_local_name(element.name().as_ref(), b"scaling") =>
7124 {
7125 let namespaces = typed_rewrite_bindings(&element, inherited)?;
7126 let raw_attributes = capture_attributes(&element)?;
7127 let mut log_base = None;
7128 let mut orientation = None;
7129 let mut maximum = None;
7130 let mut minimum = None;
7131 let mut raw_children = OrderedRawChildren::default();
7132 let mut boundary = 0usize;
7133 let mut inner = Vec::new();
7134 loop {
7135 match reader
7136 .read_event_into(&mut inner)
7137 .map_err(OxmlError::from)?
7138 {
7139 Event::Start(child) => {
7140 let name = chart_child_local(&child, &namespaces)?;
7141 let raw = capture_element(&mut reader, &child)?;
7142 parse_scaling_child(
7143 name.as_deref().unwrap_or_default(),
7144 raw,
7145 &mut log_base,
7146 &mut orientation,
7147 &mut maximum,
7148 &mut minimum,
7149 &mut raw_children,
7150 &mut boundary,
7151 )?;
7152 }
7153 Event::Empty(child) => {
7154 let name = chart_child_local(&child, &namespaces)?;
7155 let raw = capture_empty_element(&child)?;
7156 parse_scaling_child(
7157 name.as_deref().unwrap_or_default(),
7158 raw,
7159 &mut log_base,
7160 &mut orientation,
7161 &mut maximum,
7162 &mut minimum,
7163 &mut raw_children,
7164 &mut boundary,
7165 )?;
7166 }
7167 event @ (Event::Text(_)
7168 | Event::CData(_)
7169 | Event::Comment(_)
7170 | Event::PI(_)
7171 | Event::GeneralRef(_)) => {
7172 raw_children.push(boundary, capture_event(event)?);
7173 }
7174 Event::End(end)
7175 if matches_local_name(end.name().as_ref(), b"scaling") =>
7176 {
7177 let (log_base, log_base_markup) = optional_markup(log_base);
7178 let (orientation, orientation_markup) = orientation
7179 .map(|(value, markup)| (value, Some(markup)))
7180 .unwrap_or((Orientation::MinMax, None));
7181 let (maximum, maximum_markup) = optional_markup(maximum);
7182 let (minimum, minimum_markup) = optional_markup(minimum);
7183 let scaling = Self {
7184 log_base,
7185 orientation,
7186 maximum,
7187 minimum,
7188 log_base_markup,
7189 orientation_markup,
7190 maximum_markup,
7191 minimum_markup,
7192 raw_attributes,
7193 raw_children: raw_children_in_schema_order(&raw_children, 4),
7194 };
7195 scaling.validate()?;
7196 return Ok(scaling);
7197 }
7198 Event::Eof => return Err(missing_end("c:scaling")),
7199 _ => {}
7200 }
7201 inner.clear();
7202 }
7203 }
7204 Event::Empty(element)
7205 if matches_local_name(element.name().as_ref(), b"scaling") =>
7206 {
7207 typed_rewrite_bindings(&element, inherited)?;
7208 return Ok(Self {
7209 raw_attributes: capture_attributes(&element)?,
7210 ..Self::default()
7211 });
7212 }
7213 Event::Start(element) | Event::Empty(element) => {
7214 return Err(ChartError::UnexpectedElement(element_name(&element)));
7215 }
7216 Event::Eof => return Err(ChartError::MissingElement("c:scaling".to_owned())),
7217 _ => {}
7218 }
7219 buffer.clear();
7220 }
7221 }
7222
7223 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7224 self.validate()?;
7225 let mut start = BytesStart::new("c:scaling");
7226 push_attributes(&mut start, &self.raw_attributes);
7227 writer
7228 .write_event(Event::Start(start))
7229 .map_err(OxmlError::from)?;
7230 emit_raw(writer, self.raw_children.at(0))?;
7231 if let Some(value) = self.log_base {
7232 write_scalar(
7233 writer,
7234 "c:logBase",
7235 &value.to_string(),
7236 self.log_base_markup.as_ref(),
7237 )?;
7238 }
7239 emit_raw(writer, self.raw_children.at(1))?;
7240 if self.orientation != Orientation::MinMax || self.orientation_markup.is_some() {
7241 write_scalar(
7242 writer,
7243 "c:orientation",
7244 self.orientation.as_str(),
7245 self.orientation_markup.as_ref(),
7246 )?;
7247 }
7248 emit_raw(writer, self.raw_children.at(2))?;
7249 if let Some(value) = self.maximum {
7250 write_scalar(
7251 writer,
7252 "c:max",
7253 &value.to_string(),
7254 self.maximum_markup.as_ref(),
7255 )?;
7256 }
7257 emit_raw(writer, self.raw_children.at(3))?;
7258 if let Some(value) = self.minimum {
7259 write_scalar(
7260 writer,
7261 "c:min",
7262 &value.to_string(),
7263 self.minimum_markup.as_ref(),
7264 )?;
7265 }
7266 emit_raw(writer, self.raw_children.at(4))?;
7267 writer
7268 .write_event(Event::End(BytesEnd::new("c:scaling")))
7269 .map_err(OxmlError::from)?;
7270 Ok(())
7271 }
7272
7273 fn validate(&self) -> Result<()> {
7274 if let Some(value) = self.log_base
7275 && (!value.is_finite() || !(2.0..=1000.0).contains(&value))
7276 {
7277 return Err(ChartError::InvalidValue {
7278 element: "c:logBase".to_owned(),
7279 value: value.to_string(),
7280 });
7281 }
7282 for (element, value) in [("c:max", self.maximum), ("c:min", self.minimum)] {
7283 if let Some(value) = value
7284 && !value.is_finite()
7285 {
7286 return Err(ChartError::InvalidValue {
7287 element: element.to_owned(),
7288 value: value.to_string(),
7289 });
7290 }
7291 }
7292 if let (Some(minimum), Some(maximum)) = (self.minimum, self.maximum)
7293 && minimum > maximum
7294 {
7295 return Err(ChartError::InvalidValue {
7296 element: "c:scaling".to_owned(),
7297 value: format!("minimum {minimum} exceeds maximum {maximum}"),
7298 });
7299 }
7300 Ok(())
7301 }
7302}
7303
7304#[allow(clippy::too_many_arguments)]
7305fn parse_scaling_child(
7306 name: &[u8],
7307 raw: Vec<u8>,
7308 log_base: &mut Option<(f64, ScalarMarkup)>,
7309 orientation: &mut Option<(Orientation, ScalarMarkup)>,
7310 maximum: &mut Option<(f64, ScalarMarkup)>,
7311 minimum: &mut Option<(f64, ScalarMarkup)>,
7312 raw_children: &mut OrderedRawChildren,
7313 boundary: &mut usize,
7314) -> Result<()> {
7315 match name {
7316 b"logBase" => {
7317 set_once(log_base, parse_f64_scalar(&raw, "logBase")?, "c:logBase")?;
7318 *boundary = (*boundary).max(1);
7319 }
7320 b"orientation" => {
7321 let (value, markup) = scalar_value(&raw, "orientation")?;
7322 let value = value
7323 .ok_or_else(|| invalid_attribute("orientation", "val", "<missing>".to_owned()))?;
7324 let parsed = Orientation::parse(&value)
7325 .ok_or_else(|| invalid_attribute("orientation", "val", value))?;
7326 set_once(orientation, (parsed, markup), "c:orientation")?;
7327 *boundary = (*boundary).max(2);
7328 }
7329 b"max" => {
7330 set_once(maximum, parse_f64_scalar(&raw, "max")?, "c:max")?;
7331 *boundary = (*boundary).max(3);
7332 }
7333 b"min" => {
7334 set_once(minimum, parse_f64_scalar(&raw, "min")?, "c:min")?;
7335 *boundary = (*boundary).max(4);
7336 }
7337 _ => raw_children.push(*boundary, raw),
7338 }
7339 Ok(())
7340}
7341
7342impl ChartLines {
7343 fn from_xml(xml: &[u8], local: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7344 let mut reader = Reader::from_reader(xml);
7345 let mut buffer = Vec::new();
7346 loop {
7347 match reader
7348 .read_event_into(&mut buffer)
7349 .map_err(OxmlError::from)?
7350 {
7351 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
7352 let namespaces = typed_rewrite_bindings(&element, inherited)?;
7353 let raw_attributes = capture_attributes(&element)?;
7354 let mut sp_pr = None;
7355 let mut raw_children = OrderedRawChildren::default();
7356 let mut boundary = 0usize;
7357 let mut inner = Vec::new();
7358 loop {
7359 match reader
7360 .read_event_into(&mut inner)
7361 .map_err(OxmlError::from)?
7362 {
7363 Event::Start(child) => {
7364 let name = chart_child_local(&child, &namespaces)?;
7365 let raw = capture_element(&mut reader, &child)?;
7366 if name.as_deref() == Some(b"spPr") {
7367 parse_chart_lines_shape(&raw, &namespaces, &mut sp_pr)?;
7368 boundary = 1;
7369 } else {
7370 raw_children.push(boundary, raw);
7371 }
7372 }
7373 Event::Empty(child) => {
7374 let name = chart_child_local(&child, &namespaces)?;
7375 let raw = capture_empty_element(&child)?;
7376 if name.as_deref() == Some(b"spPr") {
7377 parse_chart_lines_shape(&raw, &namespaces, &mut sp_pr)?;
7378 boundary = 1;
7379 } else {
7380 raw_children.push(boundary, raw);
7381 }
7382 }
7383 event @ (Event::Text(_)
7384 | Event::CData(_)
7385 | Event::Comment(_)
7386 | Event::PI(_)
7387 | Event::GeneralRef(_)) => {
7388 raw_children.push(boundary, capture_event(event)?);
7389 }
7390 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
7391 return Ok(Self {
7392 sp_pr,
7393 raw_attributes,
7394 raw_children: raw_children_in_schema_order(&raw_children, 1),
7395 });
7396 }
7397 Event::Eof => {
7398 return Err(missing_end(&format!(
7399 "c:{}",
7400 String::from_utf8_lossy(local)
7401 )));
7402 }
7403 _ => {}
7404 }
7405 inner.clear();
7406 }
7407 }
7408 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
7409 typed_rewrite_bindings(&element, inherited)?;
7410 return Ok(Self {
7411 sp_pr: None,
7412 raw_attributes: capture_attributes(&element)?,
7413 raw_children: OrderedRawChildren::default(),
7414 });
7415 }
7416 Event::Start(element) | Event::Empty(element) => {
7417 return Err(ChartError::UnexpectedElement(element_name(&element)));
7418 }
7419 Event::Eof => {
7420 return Err(ChartError::MissingElement(format!(
7421 "c:{}",
7422 String::from_utf8_lossy(local)
7423 )));
7424 }
7425 _ => {}
7426 }
7427 buffer.clear();
7428 }
7429 }
7430
7431 fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
7432 let mut start = BytesStart::new(tag);
7433 push_attributes(&mut start, &self.raw_attributes);
7434 if self.sp_pr.is_none() && self.raw_children.is_empty() {
7435 writer
7436 .write_event(Event::Empty(start))
7437 .map_err(OxmlError::from)?;
7438 return Ok(());
7439 }
7440 writer
7441 .write_event(Event::Start(start))
7442 .map_err(OxmlError::from)?;
7443 emit_raw(writer, self.raw_children.at(0))?;
7444 if let Some(properties) = &self.sp_pr {
7445 properties.write_xml_as(writer, "c:spPr")?;
7446 }
7447 emit_raw(writer, self.raw_children.at(1))?;
7448 writer
7449 .write_event(Event::End(BytesEnd::new(tag)))
7450 .map_err(OxmlError::from)?;
7451 Ok(())
7452 }
7453}
7454
7455fn parse_chart_lines_shape(
7456 raw: &[u8],
7457 namespaces: &NamespaceBindings,
7458 slot: &mut Option<CT_ShapeProperties>,
7459) -> Result<()> {
7460 reject_conflicting_prefix_in_xml(raw, b"a", A_NS)?;
7461 let properties = CT_ShapeProperties::from_xml(raw)?;
7462 let mut writer = Writer::new(Vec::new());
7463 properties.write_xml_as(&mut writer, "c:spPr")?;
7464 reject_rewritten_foreign_elements(raw, &writer.into_inner(), namespaces, b"spPr")?;
7465 set_once(slot, properties, "c:spPr")
7466}
7467
7468impl NumberFormat {
7469 pub fn new(format_code: String, source_linked: bool) -> Result<Self> {
7470 let number_format = Self {
7471 format_code,
7472 source_linked,
7473 raw_attributes: Vec::new(),
7474 raw_content: Vec::new(),
7475 };
7476 number_format.validate()?;
7477 Ok(number_format)
7478 }
7479
7480 fn from_xml(xml: &[u8]) -> Result<Self> {
7481 let mut reader = Reader::from_reader(xml);
7482 let mut buffer = Vec::new();
7483 loop {
7484 match reader
7485 .read_event_into(&mut buffer)
7486 .map_err(OxmlError::from)?
7487 {
7488 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
7489 return Self::from_element(&element, Vec::new());
7490 }
7491 Event::Start(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
7492 let mut raw_content = Vec::new();
7493 let mut inner = Vec::new();
7494 loop {
7495 match reader
7496 .read_event_into(&mut inner)
7497 .map_err(OxmlError::from)?
7498 {
7499 Event::End(end)
7500 if matches_local_name(end.name().as_ref(), b"numFmt") =>
7501 {
7502 return Self::from_element(&element, raw_content);
7503 }
7504 event @ (Event::Text(_)
7505 | Event::CData(_)
7506 | Event::Comment(_)
7507 | Event::PI(_)
7508 | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
7509 Event::Eof => return Err(missing_end("c:numFmt")),
7510 _ => return Err(ChartError::UnexpectedElement("c:numFmt".to_owned())),
7511 }
7512 inner.clear();
7513 }
7514 }
7515 Event::Start(element) | Event::Empty(element) => {
7516 return Err(ChartError::UnexpectedElement(element_name(&element)));
7517 }
7518 Event::Eof => return Err(ChartError::MissingElement("c:numFmt".to_owned())),
7519 _ => {}
7520 }
7521 buffer.clear();
7522 }
7523 }
7524
7525 fn from_element(element: &BytesStart<'_>, raw_content: Vec<Vec<u8>>) -> Result<Self> {
7526 reject_conflicting_prefix(element, b"c", C_NS)?;
7527 let format_code = attribute_value(element, b"formatCode")?
7528 .ok_or_else(|| invalid_attribute("numFmt", "formatCode", "<missing>".to_owned()))?;
7529 let source_linked = attribute_value(element, b"sourceLinked")?
7530 .ok_or_else(|| invalid_attribute("numFmt", "sourceLinked", "<missing>".to_owned()))?;
7531 let source_linked = parse_bool_lexical("numFmt", "sourceLinked", &source_linked)?;
7532 let number_format = Self {
7533 format_code,
7534 source_linked,
7535 raw_attributes: capture_attributes_excluding(
7536 element,
7537 &[b"formatCode", b"sourceLinked"],
7538 )?,
7539 raw_content,
7540 };
7541 number_format.validate()?;
7542 Ok(number_format)
7543 }
7544
7545 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7546 self.validate()?;
7547 let mut start = BytesStart::new("c:numFmt");
7548 start.push_attribute(("formatCode", self.format_code.as_str()));
7549 start.push_attribute(("sourceLinked", bool_lexical(self.source_linked)));
7550 push_attributes(&mut start, &self.raw_attributes);
7551 if self.raw_content.is_empty() {
7552 writer
7553 .write_event(Event::Empty(start))
7554 .map_err(OxmlError::from)?;
7555 return Ok(());
7556 }
7557 writer
7558 .write_event(Event::Start(start))
7559 .map_err(OxmlError::from)?;
7560 for raw in &self.raw_content {
7561 writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
7562 }
7563 writer
7564 .write_event(Event::End(BytesEnd::new("c:numFmt")))
7565 .map_err(OxmlError::from)?;
7566 Ok(())
7567 }
7568
7569 pub fn format_value(&self, value: f64) -> Result<String> {
7571 if !value.is_finite() {
7572 return Err(ChartError::InvalidValue {
7573 element: "c:numFmt value".to_owned(),
7574 value: value.to_string(),
7575 });
7576 }
7577 if self.format_code == "General" {
7578 return Ok(value.to_string());
7579 }
7580
7581 let (numeric_code, percentage) = self
7582 .format_code
7583 .strip_suffix('%')
7584 .map(|code| (code, true))
7585 .unwrap_or((&self.format_code, false));
7586 let precision = if numeric_code == "0" {
7587 Some(0)
7588 } else if let Some(decimals) = numeric_code.strip_prefix("0.")
7589 && !decimals.is_empty()
7590 && decimals.bytes().all(|byte| byte == b'0')
7591 {
7592 Some(decimals.len())
7593 } else {
7594 None
7595 };
7596 let Some(precision) = precision else {
7597 return Err(ChartError::InvalidValue {
7598 element: "c:numFmt/@formatCode projection".to_owned(),
7599 value: self.format_code.clone(),
7600 });
7601 };
7602 let projected = if percentage { value * 100.0 } else { value };
7603 if !projected.is_finite() {
7604 return Err(ChartError::InvalidValue {
7605 element: "c:numFmt value".to_owned(),
7606 value: projected.to_string(),
7607 });
7608 }
7609 let mut formatted = format!("{projected:.precision$}");
7610 if percentage {
7611 formatted.push('%');
7612 }
7613 Ok(formatted)
7614 }
7615
7616 fn validate(&self) -> Result<()> {
7617 if self.format_code.is_empty() {
7618 return Err(ChartError::InvalidValue {
7619 element: "c:numFmt/@formatCode".to_owned(),
7620 value: self.format_code.clone(),
7621 });
7622 }
7623 validate_xml_text(&self.format_code, "c:numFmt/@formatCode")
7624 }
7625}
7626
7627impl CT_DLbls {
7628 pub fn from_xml(xml: &[u8]) -> Result<Self> {
7629 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
7630 }
7631
7632 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7633 let mut reader = Reader::from_reader(xml);
7634 let mut buffer = Vec::new();
7635 loop {
7636 match reader
7637 .read_event_into(&mut buffer)
7638 .map_err(OxmlError::from)?
7639 {
7640 Event::Start(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7641 chart_root_prefix(&element)?;
7642 if !element_is_in_namespace(&element, C_NS, inherited)? {
7643 return Err(ChartError::UnexpectedElement(element_name(&element)));
7644 }
7645 return Self::from_element(&mut reader, &element, inherited);
7646 }
7647 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7648 chart_root_prefix(&element)?;
7649 let namespaces = chart_bindings(inherited, &element)?;
7650 if !element_is_in_namespace(&element, C_NS, inherited)? {
7651 return Err(ChartError::UnexpectedElement(element_name(&element)));
7652 }
7653 let (raw_attributes, _) = capture_fixed_root_attributes(
7654 &element,
7655 &["xmlns:c", "xmlns:a", "xmlns:r"],
7656 )?;
7657 return Ok(Self {
7658 raw_attributes,
7659 namespace_declarations: standalone_namespace_declarations(&namespaces)?,
7660 ..Self::default()
7661 });
7662 }
7663 Event::Start(element) | Event::Empty(element) => {
7664 return Err(ChartError::UnexpectedElement(element_name(&element)));
7665 }
7666 Event::Eof => return Err(ChartError::MissingElement("c:dLbls".to_owned())),
7667 _ => {}
7668 }
7669 buffer.clear();
7670 }
7671 }
7672
7673 fn from_element(
7674 reader: &mut Reader<&[u8]>,
7675 start: &BytesStart<'_>,
7676 inherited: &NamespaceBindings,
7677 ) -> Result<Self> {
7678 reject_conflicting_prefix(start, b"a", A_NS)?;
7679 let namespaces = chart_bindings(inherited, start)?;
7680 require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
7681 let (raw_attributes, _) =
7682 capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
7683 let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
7684 let mut state = DataLabelsParseState::new(namespaces);
7685 let mut buffer = Vec::new();
7686 loop {
7687 match reader
7688 .read_event_into(&mut buffer)
7689 .map_err(OxmlError::from)?
7690 {
7691 Event::Start(element) => {
7692 let name = chart_child_local(&element, &state.namespaces)?;
7693 let raw = capture_element(reader, &element)?;
7694 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
7695 }
7696 Event::Empty(element) => {
7697 let name = chart_child_local(&element, &state.namespaces)?;
7698 let raw = capture_empty_element(&element)?;
7699 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
7700 }
7701 event @ (Event::Text(_)
7702 | Event::CData(_)
7703 | Event::Comment(_)
7704 | Event::PI(_)
7705 | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
7706 Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7707 return state.finish(raw_attributes, namespace_declarations);
7708 }
7709 Event::Eof => return Err(missing_end("c:dLbls")),
7710 _ => {}
7711 }
7712 buffer.clear();
7713 }
7714 }
7715
7716 pub fn to_xml(&self) -> Result<Vec<u8>> {
7717 let mut writer = Writer::new(Vec::new());
7718 self.write_xml(&mut writer, true)?;
7719 Ok(writer.into_inner())
7720 }
7721
7722 fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
7723 self.validate()?;
7724 let mut start = BytesStart::new("c:dLbls");
7725 if standalone {
7726 start.push_attribute(("xmlns:c", C_NS));
7727 start.push_attribute(("xmlns:a", A_NS));
7728 start.push_attribute(("xmlns:r", R_NS));
7729 }
7730 push_attributes(&mut start, &self.namespace_declarations);
7731 push_attributes(&mut start, &self.raw_attributes);
7732 writer
7733 .write_event(Event::Start(start))
7734 .map_err(OxmlError::from)?;
7735 emit_raw(writer, self.raw_children.at(0))?;
7736 if let Some(number_format) = &self.number_format {
7737 number_format.write_xml(writer)?;
7738 }
7739 emit_raw(writer, self.raw_children.at(1))?;
7740 if let Some(position) = self.position {
7741 write_scalar(
7742 writer,
7743 "c:dLblPos",
7744 position.as_str(),
7745 self.position_markup.as_ref(),
7746 )?;
7747 }
7748 emit_raw(writer, self.raw_children.at(2))?;
7749 write_optional_bool(
7750 writer,
7751 "c:showLegendKey",
7752 self.show_legend_key,
7753 self.show_legend_key_markup.as_ref(),
7754 )?;
7755 emit_raw(writer, self.raw_children.at(3))?;
7756 write_optional_bool(
7757 writer,
7758 "c:showVal",
7759 self.show_value,
7760 self.show_value_markup.as_ref(),
7761 )?;
7762 emit_raw(writer, self.raw_children.at(4))?;
7763 write_optional_bool(
7764 writer,
7765 "c:showCatName",
7766 self.show_category_name,
7767 self.show_category_name_markup.as_ref(),
7768 )?;
7769 emit_raw(writer, self.raw_children.at(5))?;
7770 write_optional_bool(
7771 writer,
7772 "c:showSerName",
7773 self.show_series_name,
7774 self.show_series_name_markup.as_ref(),
7775 )?;
7776 emit_raw(writer, self.raw_children.at(6))?;
7777 write_optional_bool(
7778 writer,
7779 "c:showPercent",
7780 self.show_percent,
7781 self.show_percent_markup.as_ref(),
7782 )?;
7783 emit_raw(writer, self.raw_children.at(7))?;
7784 write_optional_bool(
7785 writer,
7786 "c:showBubbleSize",
7787 self.show_bubble_size,
7788 self.show_bubble_size_markup.as_ref(),
7789 )?;
7790 emit_raw(writer, self.raw_children.at(8))?;
7791 if let Some(separator) = &self.separator {
7792 write_text(
7793 writer,
7794 "c:separator",
7795 separator,
7796 self.separator_markup
7797 .as_ref()
7798 .unwrap_or(&TextMarkup::default()),
7799 )?;
7800 }
7801 emit_raw(writer, self.raw_children.at(9))?;
7802 writer
7803 .write_event(Event::End(BytesEnd::new("c:dLbls")))
7804 .map_err(OxmlError::from)?;
7805 Ok(())
7806 }
7807
7808 fn validate(&self) -> Result<()> {
7809 if let Some(number_format) = &self.number_format {
7810 number_format.validate()?;
7811 }
7812 if let Some(separator) = &self.separator {
7813 validate_xml_text(separator, "c:separator")?;
7814 }
7815 Ok(())
7816 }
7817
7818 pub fn raw_children(&self) -> &OrderedRawChildren {
7819 &self.raw_children
7820 }
7821}
7822
7823struct DataLabelsParseState {
7824 namespaces: NamespaceBindings,
7825 number_format: Option<NumberFormat>,
7826 position: Option<(DataLabelPosition, ScalarMarkup)>,
7827 show_legend_key: Option<(bool, ScalarMarkup)>,
7828 show_value: Option<(bool, ScalarMarkup)>,
7829 show_category_name: Option<(bool, ScalarMarkup)>,
7830 show_series_name: Option<(bool, ScalarMarkup)>,
7831 show_percent: Option<(bool, ScalarMarkup)>,
7832 show_bubble_size: Option<(bool, ScalarMarkup)>,
7833 separator: Option<(String, TextMarkup)>,
7834 raw_children: OrderedRawChildren,
7835 boundary: usize,
7836}
7837
7838impl DataLabelsParseState {
7839 fn new(namespaces: NamespaceBindings) -> Self {
7840 Self {
7841 namespaces,
7842 number_format: None,
7843 position: None,
7844 show_legend_key: None,
7845 show_value: None,
7846 show_category_name: None,
7847 show_series_name: None,
7848 show_percent: None,
7849 show_bubble_size: None,
7850 separator: None,
7851 raw_children: OrderedRawChildren::default(),
7852 boundary: 0,
7853 }
7854 }
7855
7856 fn capture_event(&mut self, raw: Vec<u8>) {
7857 self.raw_children.push(self.boundary, raw);
7858 }
7859
7860 fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
7861 match name {
7862 b"numFmt" => {
7863 set_once(
7864 &mut self.number_format,
7865 NumberFormat::from_xml(&raw)?,
7866 "c:numFmt",
7867 )?;
7868 self.boundary = self.boundary.max(1);
7869 }
7870 b"dLblPos" => {
7871 let (value, markup) = scalar_value(&raw, "dLblPos")?;
7872 let lexical = value
7873 .ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
7874 let position = DataLabelPosition::parse(&lexical)
7875 .ok_or_else(|| invalid_attribute("dLblPos", "val", lexical))?;
7876 set_once(&mut self.position, (position, markup), "c:dLblPos")?;
7877 self.boundary = self.boundary.max(2);
7878 }
7879 b"showLegendKey" => {
7880 set_once(
7881 &mut self.show_legend_key,
7882 parse_bool_value(&raw, "showLegendKey")?,
7883 "c:showLegendKey",
7884 )?;
7885 self.boundary = self.boundary.max(3);
7886 }
7887 b"showVal" => {
7888 set_once(
7889 &mut self.show_value,
7890 parse_bool_value(&raw, "showVal")?,
7891 "c:showVal",
7892 )?;
7893 self.boundary = self.boundary.max(4);
7894 }
7895 b"showCatName" => {
7896 set_once(
7897 &mut self.show_category_name,
7898 parse_bool_value(&raw, "showCatName")?,
7899 "c:showCatName",
7900 )?;
7901 self.boundary = self.boundary.max(5);
7902 }
7903 b"showSerName" => {
7904 set_once(
7905 &mut self.show_series_name,
7906 parse_bool_value(&raw, "showSerName")?,
7907 "c:showSerName",
7908 )?;
7909 self.boundary = self.boundary.max(6);
7910 }
7911 b"showPercent" => {
7912 set_once(
7913 &mut self.show_percent,
7914 parse_bool_value(&raw, "showPercent")?,
7915 "c:showPercent",
7916 )?;
7917 self.boundary = self.boundary.max(7);
7918 }
7919 b"showBubbleSize" => {
7920 set_once(
7921 &mut self.show_bubble_size,
7922 parse_bool_value(&raw, "showBubbleSize")?,
7923 "c:showBubbleSize",
7924 )?;
7925 self.boundary = self.boundary.max(8);
7926 }
7927 b"separator" => {
7928 set_once(
7929 &mut self.separator,
7930 parse_text_element(&raw, b"separator", &self.namespaces)?,
7931 "c:separator",
7932 )?;
7933 self.boundary = self.boundary.max(9);
7934 }
7935 _ => {
7936 let boundary = data_labels_raw_boundary(name, self.boundary);
7937 self.raw_children.push(boundary, raw);
7938 self.boundary = self.boundary.max(boundary);
7939 }
7940 }
7941 Ok(())
7942 }
7943
7944 fn finish(
7945 self,
7946 raw_attributes: XmlAttributes,
7947 namespace_declarations: XmlAttributes,
7948 ) -> Result<CT_DLbls> {
7949 let (position, position_markup) = optional_markup(self.position);
7950 let (show_legend_key, show_legend_key_markup) = optional_bool_markup(self.show_legend_key);
7951 let (show_value, show_value_markup) = optional_bool_markup(self.show_value);
7952 let (show_category_name, show_category_name_markup) =
7953 optional_bool_markup(self.show_category_name);
7954 let (show_series_name, show_series_name_markup) =
7955 optional_bool_markup(self.show_series_name);
7956 let (show_percent, show_percent_markup) = optional_bool_markup(self.show_percent);
7957 let (show_bubble_size, show_bubble_size_markup) =
7958 optional_bool_markup(self.show_bubble_size);
7959 let (separator, separator_markup) = self
7960 .separator
7961 .map(|(value, markup)| (Some(value), Some(markup)))
7962 .unwrap_or((None, None));
7963 let labels = CT_DLbls {
7964 number_format: self.number_format,
7965 position,
7966 separator,
7967 show_legend_key,
7968 show_value,
7969 show_category_name,
7970 show_series_name,
7971 show_percent,
7972 show_bubble_size,
7973 position_markup,
7974 show_legend_key_markup,
7975 show_value_markup,
7976 show_category_name_markup,
7977 show_series_name_markup,
7978 show_percent_markup,
7979 show_bubble_size_markup,
7980 separator_markup,
7981 raw_attributes,
7982 namespace_declarations,
7983 raw_children: raw_children_in_schema_order(&self.raw_children, 9),
7984 };
7985 labels.validate()?;
7986 Ok(labels)
7987 }
7988}
7989
7990fn data_labels_raw_boundary(name: &[u8], current: usize) -> usize {
7991 match name {
7992 b"dLbl" | b"delete" => 0,
7993 b"spPr" | b"txPr" => 1,
7994 b"showLeaderLines" | b"leaderLines" | b"extLst" => 9,
7995 _ => current,
7996 }
7997}
7998
7999fn optional_bool_markup(value: Option<(bool, ScalarMarkup)>) -> (bool, Option<ScalarMarkup>) {
8000 value
8001 .map(|(value, markup)| (value, Some(markup)))
8002 .unwrap_or((false, None))
8003}
8004
8005fn write_optional_bool(
8006 writer: &mut Writer<Vec<u8>>,
8007 tag: &str,
8008 value: bool,
8009 markup: Option<&ScalarMarkup>,
8010) -> Result<()> {
8011 if value || markup.is_some() {
8012 write_scalar(writer, tag, bool_lexical(value), markup)?;
8013 }
8014 Ok(())
8015}
8016
8017fn default_axis_id_markup(id: AxisId) -> AxisIdMarkup {
8018 AxisIdMarkup {
8019 scalar: ScalarMarkup::default(),
8020 parsed: id,
8021 lexical: id.value().to_string(),
8022 }
8023}
8024
8025fn parse_axis_id_scalar(xml: &[u8], local: &str) -> Result<(AxisId, AxisIdMarkup)> {
8026 let (value, scalar) = scalar_value(xml, local)?;
8027 let lexical = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8028 let parsed = lexical
8029 .parse::<i64>()
8030 .map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
8031 let id = AxisId::new(parsed).map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
8032 Ok((
8033 id,
8034 AxisIdMarkup {
8035 scalar,
8036 parsed: id,
8037 lexical,
8038 },
8039 ))
8040}
8041
8042fn write_axis_id<W: Write>(
8043 writer: &mut Writer<W>,
8044 tag: &str,
8045 id: AxisId,
8046 markup: &AxisIdMarkup,
8047) -> Result<()> {
8048 AxisId::new(id.value())?;
8049 let normalized;
8050 let value = if id == markup.parsed {
8051 markup.lexical.as_str()
8052 } else {
8053 normalized = id.value().to_string();
8054 normalized.as_str()
8055 };
8056 write_scalar(writer, tag, value, Some(&markup.scalar))
8057}
8058
8059fn parse_f64_scalar(xml: &[u8], local: &str) -> Result<(f64, ScalarMarkup)> {
8060 let (value, markup) = scalar_value(xml, local)?;
8061 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8062 let parsed = value
8063 .parse::<f64>()
8064 .map_err(|_| invalid_attribute(local, "val", value.clone()))?;
8065 if !parsed.is_finite() {
8066 return Err(invalid_attribute(local, "val", value));
8067 }
8068 Ok((parsed, markup))
8069}
8070
8071fn parse_axis_position(xml: &[u8]) -> Result<(AxisPosition, ScalarMarkup)> {
8072 let (value, markup) = scalar_value(xml, "axPos")?;
8073 let value = value.ok_or_else(|| invalid_attribute("axPos", "val", "<missing>".to_owned()))?;
8074 let parsed =
8075 AxisPosition::parse(&value).ok_or_else(|| invalid_attribute("axPos", "val", value))?;
8076 Ok((parsed, markup))
8077}
8078
8079fn parse_tick_mark(xml: &[u8], local: &str) -> Result<(TickMark, ScalarMarkup)> {
8080 let (value, markup) = scalar_value(xml, local)?;
8081 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8082 let parsed = TickMark::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
8083 Ok((parsed, markup))
8084}
8085
8086fn parse_tick_label_position(xml: &[u8]) -> Result<(TickLabelPosition, ScalarMarkup)> {
8087 let (value, markup) = scalar_value(xml, "tickLblPos")?;
8088 let value =
8089 value.ok_or_else(|| invalid_attribute("tickLblPos", "val", "<missing>".to_owned()))?;
8090 let parsed = TickLabelPosition::parse(&value)
8091 .ok_or_else(|| invalid_attribute("tickLblPos", "val", value))?;
8092 Ok((parsed, markup))
8093}
8094
8095fn optional_markup<T>(value: Option<(T, ScalarMarkup)>) -> (Option<T>, Option<ScalarMarkup>) {
8096 value
8097 .map(|(value, markup)| (Some(value), Some(markup)))
8098 .unwrap_or((None, None))
8099}
8100
8101fn optional_scalar_markup_eq(left: &Option<ScalarMarkup>, right: &Option<ScalarMarkup>) -> bool {
8102 let empty = ScalarMarkup::default();
8103 left.as_ref().unwrap_or(&empty) == right.as_ref().unwrap_or(&empty)
8104}
8105
8106fn axis_id_markup_eq(left: &AxisIdMarkup, right: &AxisIdMarkup) -> bool {
8107 left.scalar == right.scalar
8108}
8109
8110const fn bool_lexical(value: bool) -> &'static str {
8111 if value { "1" } else { "0" }
8112}
8113
8114fn parse_bool_lexical(element: &str, attribute: &str, value: &str) -> Result<bool> {
8115 match value {
8116 "1" | "true" => Ok(true),
8117 "0" | "false" => Ok(false),
8118 _ => Err(invalid_attribute(element, attribute, value.to_owned())),
8119 }
8120}
8121
8122impl CT_Title {
8123 fn plain_text(text: &str) -> Self {
8124 let mut body = CT_TextBody::new();
8125 body.set_text(text);
8126 let mut raw_children = OrderedRawChildren::default();
8127 raw_children.push(1, br#"<c:layout/><c:overlay val="0"/>"#.to_vec());
8128 Self {
8129 text: Some(body),
8130 raw_children,
8131 ..Self::default()
8132 }
8133 }
8134
8135 fn from_xml(xml: &[u8]) -> Result<Self> {
8136 let (raw_attributes, raw_children) = parse_raw_shell(xml, b"title", "c:title")?;
8137 Ok(Self {
8138 text: None,
8139 raw_attributes,
8140 raw_children,
8141 })
8142 }
8143
8144 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8145 if self.text.is_none() {
8146 return write_raw_shell(writer, "c:title", &self.raw_attributes, &self.raw_children);
8147 }
8148 let mut start = BytesStart::new("c:title");
8149 push_attributes(&mut start, &self.raw_attributes);
8150 writer
8151 .write_event(Event::Start(start))
8152 .map_err(OxmlError::from)?;
8153 emit_raw(writer, self.raw_children.at(0))?;
8154 writer
8155 .write_event(Event::Start(BytesStart::new("c:tx")))
8156 .map_err(OxmlError::from)?;
8157 if let Some(text) = &self.text {
8158 text.write_xml_as(writer, "c:rich")?;
8159 }
8160 writer
8161 .write_event(Event::End(BytesEnd::new("c:tx")))
8162 .map_err(OxmlError::from)?;
8163 emit_raw(writer, self.raw_children.at(1))?;
8164 writer
8165 .write_event(Event::End(BytesEnd::new("c:title")))
8166 .map_err(OxmlError::from)?;
8167 Ok(())
8168 }
8169
8170 pub fn raw_children(&self) -> &OrderedRawChildren {
8171 &self.raw_children
8172 }
8173}
8174
8175impl CT_PlotArea {
8176 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
8177 let (raw_attributes, captured_children) = parse_raw_shell(xml, b"plotArea", "c:plotArea")?;
8178 let namespace_bindings = root_chart_bindings(xml, b"plotArea", inherited)?;
8179 let direct_roots: Vec<_> = captured_children
8180 .at(0)
8181 .filter_map(|raw| chart_root_local(raw, &namespace_bindings).transpose())
8182 .collect::<Result<_>>()?;
8183 let plot_roots: Vec<_> = direct_roots
8184 .iter()
8185 .filter(|local| is_plot_container(local))
8186 .cloned()
8187 .collect();
8188 let typed_local = (plot_roots.len() == 1
8189 && matches!(
8190 plot_roots[0].as_slice(),
8191 b"barChart"
8192 | b"lineChart"
8193 | b"pieChart"
8194 | b"doughnutChart"
8195 | b"areaChart"
8196 | b"scatterChart"
8197 | b"radarChart"
8198 ))
8199 .then(|| plot_roots[0].clone());
8200
8201 let Some(typed_local) = typed_local else {
8202 return Ok(Self {
8203 raw_attributes,
8204 raw_children: captured_children,
8205 namespace_bindings,
8206 plots: None,
8207 plot_markup: Vec::new(),
8208 axes: Vec::new(),
8209 });
8210 };
8211
8212 let mut raw_children = OrderedRawChildren::default();
8213 let mut plots = Vec::new();
8214 let mut plot_markup = Vec::new();
8215 let mut axes = Vec::new();
8216 let mut boundary = 0usize;
8217 for raw in captured_children.at(0) {
8218 let local = chart_root_local(raw, &namespace_bindings)?;
8219 if local.as_deref() == Some(typed_local.as_slice()) {
8220 let (plot, markup) = parse_plot(raw, &namespace_bindings)?;
8221 plots.push(plot);
8222 plot_markup.push(markup);
8223 boundary = 1;
8224 } else if local.as_deref().and_then(AxisKind::parse).is_some() {
8225 axes.push(Axis::from_xml_with_namespaces(raw, &namespace_bindings)?);
8226 boundary = axes.len() + 1;
8227 } else {
8228 raw_children.push(boundary, raw.to_vec());
8229 }
8230 }
8231 let plot_area = Self {
8232 raw_attributes,
8233 raw_children,
8234 namespace_bindings,
8235 plots: Some(plots),
8236 plot_markup,
8237 axes,
8238 };
8239 plot_area.validate_typed()?;
8240 Ok(plot_area)
8241 }
8242
8243 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8244 let Some(plots) = &self.plots else {
8245 return write_raw_shell(
8246 writer,
8247 "c:plotArea",
8248 &self.raw_attributes,
8249 &self.raw_children,
8250 );
8251 };
8252 self.validate_typed()?;
8253 let mut start = BytesStart::new("c:plotArea");
8254 push_attributes(&mut start, &self.raw_attributes);
8255 writer
8256 .write_event(Event::Start(start))
8257 .map_err(OxmlError::from)?;
8258 emit_raw(writer, self.raw_children.at(0))?;
8259 for (index, plot) in plots.iter().enumerate() {
8260 write_plot(
8261 writer,
8262 plot,
8263 self.plot_markup
8264 .get(index)
8265 .unwrap_or(&PlotMarkup::default()),
8266 )?;
8267 }
8268 emit_raw(writer, self.raw_children.at(1))?;
8269 for (index, axis) in self.axes.iter().enumerate() {
8270 writer
8271 .get_mut()
8272 .write_all(&axis.to_xml()?)
8273 .map_err(OxmlError::from)?;
8274 emit_raw(writer, self.raw_children.at(index + 2))?;
8275 }
8276 writer
8277 .write_event(Event::End(BytesEnd::new("c:plotArea")))
8278 .map_err(OxmlError::from)?;
8279 Ok(())
8280 }
8281
8282 pub fn raw_children(&self) -> &OrderedRawChildren {
8283 &self.raw_children
8284 }
8285
8286 pub fn new(plots: Vec<Plot>, axes: Vec<Axis>) -> Result<Self> {
8288 let plot_markup = plots
8289 .iter()
8290 .map(|plot| {
8291 let mut markup = PlotMarkup::default();
8292 if matches!(plot, Plot::Line { .. }) {
8293 markup.marker = Some(ScalarMarkup::default());
8294 markup.smooth = Some(ScalarMarkup::default());
8295 }
8296 if matches!(plot, Plot::Doughnut { .. }) {
8297 markup.hole_size = Some(ScalarMarkup::default());
8298 }
8299 markup
8300 })
8301 .collect();
8302 let plot_area = Self {
8303 raw_attributes: Vec::new(),
8304 raw_children: OrderedRawChildren::default(),
8305 namespace_bindings: chart_namespace_defaults(),
8306 plot_markup,
8307 plots: Some(plots),
8308 axes,
8309 };
8310 plot_area.validate_typed()?;
8311 Ok(plot_area)
8312 }
8313
8314 pub fn plots(&self) -> Result<&[Plot]> {
8316 self.plots
8317 .as_deref()
8318 .ok_or_else(|| ChartError::InvalidValue {
8319 element: "c:plotArea".to_owned(),
8320 value: "unsupported or combination plot area is opaque".to_owned(),
8321 })
8322 }
8323
8324 pub fn plots_mut(&mut self) -> Result<&mut [Plot]> {
8326 self.plots
8327 .as_deref_mut()
8328 .ok_or_else(|| ChartError::InvalidValue {
8329 element: "c:plotArea".to_owned(),
8330 value: "cannot combine typed plots with an opaque plot choice".to_owned(),
8331 })
8332 }
8333
8334 pub fn series(&self) -> Result<Vec<Series>> {
8336 if let Some(plots) = &self.plots {
8337 return Ok(plots
8338 .iter()
8339 .flat_map(|plot| match plot {
8340 Plot::Bar { series, .. }
8341 | Plot::Line { series, .. }
8342 | Plot::Pie { series, .. }
8343 | Plot::Doughnut { series, .. }
8344 | Plot::Area { series, .. }
8345 | Plot::Scatter { series, .. }
8346 | Plot::Radar { series, .. } => series.clone(),
8347 })
8348 .collect());
8349 }
8350 let mut series = Vec::new();
8351 for raw in self.raw_children.at(0) {
8352 parse_plot_series(raw, &self.namespace_bindings, &mut series)?;
8353 }
8354 Ok(series)
8355 }
8356
8357 pub fn axes(&self) -> Result<Vec<Axis>> {
8359 if self.plots.is_some() {
8360 self.validate_typed()?;
8361 return Ok(self.axes.clone());
8362 }
8363 let mut axes = Vec::new();
8364 for raw in self.raw_children.at(0) {
8365 if let Some(axis) = parse_plot_axis(raw, &self.namespace_bindings)? {
8366 axes.push(axis);
8367 }
8368 }
8369 validate_axis_pairs(&axes)?;
8370 Ok(axes)
8371 }
8372
8373 fn validate_typed(&self) -> Result<()> {
8374 let plots = self
8375 .plots
8376 .as_ref()
8377 .ok_or_else(|| ChartError::InvalidValue {
8378 element: "c:plotArea".to_owned(),
8379 value: "opaque plot area has no typed validation view".to_owned(),
8380 })?;
8381 if plots.len() != 1 {
8382 return Err(ChartError::InvalidValue {
8383 element: "c:plotArea".to_owned(),
8384 value: "typed plot area requires exactly one plot family".to_owned(),
8385 });
8386 }
8387 validate_axis_pairs(&self.axes)?;
8388 for plot in plots {
8389 plot.validate()?;
8390 if let Some(axis_ids) = plot.axis_ids() {
8391 for id in axis_ids {
8392 if !self.axes.iter().any(|axis| axis.id == id) {
8393 return Err(ChartError::InvalidValue {
8394 element: "c:axId".to_owned(),
8395 value: format!("plot references missing axis {}", id.value()),
8396 });
8397 }
8398 }
8399 } else if !self.axes.is_empty() {
8400 return Err(ChartError::InvalidValue {
8401 element: "c:plotArea".to_owned(),
8402 value: "pie and doughnut plot areas must not own axes".to_owned(),
8403 });
8404 }
8405 if plot.axis_ids().is_some() && self.axes.len() != 2 {
8406 return Err(ChartError::InvalidValue {
8407 element: "c:plotArea".to_owned(),
8408 value: format!(
8409 "axis-owned plot requires exactly 2 axes, found {}",
8410 self.axes.len()
8411 ),
8412 });
8413 }
8414 }
8415 Ok(())
8416 }
8417}
8418
8419impl Default for CT_PlotArea {
8420 fn default() -> Self {
8421 Self {
8422 raw_attributes: Vec::new(),
8423 raw_children: OrderedRawChildren::default(),
8424 namespace_bindings: chart_namespace_defaults(),
8425 plots: Some(Vec::new()),
8426 plot_markup: Vec::new(),
8427 axes: Vec::new(),
8428 }
8429 }
8430}
8431
8432fn chart_root_local(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Vec<u8>>> {
8433 let mut reader = Reader::from_reader(xml);
8434 let mut buffer = Vec::new();
8435 loop {
8436 match reader
8437 .read_event_into(&mut buffer)
8438 .map_err(OxmlError::from)?
8439 {
8440 Event::Start(element) | Event::Empty(element) => {
8441 return Ok(element_is_in_namespace(&element, C_NS, inherited)?
8442 .then(|| local_name(element.name().as_ref()).to_vec()));
8443 }
8444 Event::Eof => return Ok(None),
8445 _ => {}
8446 }
8447 buffer.clear();
8448 }
8449}
8450
8451fn is_plot_container(local: &[u8]) -> bool {
8452 matches!(
8453 local,
8454 b"areaChart"
8455 | b"area3DChart"
8456 | b"barChart"
8457 | b"bar3DChart"
8458 | b"bubbleChart"
8459 | b"doughnutChart"
8460 | b"lineChart"
8461 | b"line3DChart"
8462 | b"ofPieChart"
8463 | b"pieChart"
8464 | b"pie3DChart"
8465 | b"radarChart"
8466 | b"scatterChart"
8467 | b"stockChart"
8468 | b"surfaceChart"
8469 | b"surface3DChart"
8470 )
8471}
8472
8473fn parse_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8474 let local = chart_root_local(xml, inherited)?
8475 .ok_or_else(|| ChartError::MissingElement("c:barChart or c:lineChart".to_owned()))?;
8476 match local.as_slice() {
8477 b"barChart" => parse_bar_plot(xml, inherited),
8478 b"lineChart" => parse_line_plot(xml, inherited),
8479 b"pieChart" | b"doughnutChart" | b"areaChart" | b"scatterChart" | b"radarChart" => {
8480 parse_remaining_plot(xml, inherited, &local)
8481 }
8482 _ => Err(ChartError::UnexpectedElement(format!(
8483 "c:{}",
8484 String::from_utf8_lossy(&local)
8485 ))),
8486 }
8487}
8488
8489fn parse_remaining_plot(
8490 xml: &[u8],
8491 inherited: &NamespaceBindings,
8492 local: &[u8],
8493) -> Result<(Plot, PlotMarkup)> {
8494 let root = String::from_utf8_lossy(local).into_owned();
8495 let mut reader = Reader::from_reader(xml);
8496 let mut buffer = Vec::new();
8497 loop {
8498 match reader
8499 .read_event_into(&mut buffer)
8500 .map_err(OxmlError::from)?
8501 {
8502 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
8503 let namespaces = typed_rewrite_bindings(&element, inherited)?;
8504 let (mut raw_attributes, namespace_declarations) =
8505 capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8506 raw_attributes.splice(0..0, namespace_declarations);
8507 let mut grouping = None;
8508 let mut style = None;
8509 let mut first_slice_angle = None;
8510 let mut hole_size = None;
8511 let mut series = Vec::new();
8512 let mut data_labels = None;
8513 let mut axis_ids = Vec::new();
8514 let mut raw_children = OrderedRawChildren::default();
8515 let mut boundary = 0usize;
8516 let mut inner = Vec::new();
8517 loop {
8518 match reader
8519 .read_event_into(&mut inner)
8520 .map_err(OxmlError::from)?
8521 {
8522 Event::Start(child) => {
8523 let name = chart_child_local(&child, &namespaces)?;
8524 let raw = capture_element(&mut reader, &child)?;
8525 parse_remaining_plot_child(
8526 local,
8527 name.as_deref().unwrap_or_default(),
8528 raw,
8529 &namespaces,
8530 &mut grouping,
8531 &mut style,
8532 &mut first_slice_angle,
8533 &mut hole_size,
8534 &mut series,
8535 &mut data_labels,
8536 &mut axis_ids,
8537 &mut raw_children,
8538 &mut boundary,
8539 )?;
8540 }
8541 Event::Empty(child) => {
8542 let name = chart_child_local(&child, &namespaces)?;
8543 let raw = capture_empty_element(&child)?;
8544 parse_remaining_plot_child(
8545 local,
8546 name.as_deref().unwrap_or_default(),
8547 raw,
8548 &namespaces,
8549 &mut grouping,
8550 &mut style,
8551 &mut first_slice_angle,
8552 &mut hole_size,
8553 &mut series,
8554 &mut data_labels,
8555 &mut axis_ids,
8556 &mut raw_children,
8557 &mut boundary,
8558 )?;
8559 }
8560 event @ (Event::Text(_)
8561 | Event::CData(_)
8562 | Event::Comment(_)
8563 | Event::PI(_)
8564 | Event::GeneralRef(_)) => {
8565 raw_children.push(boundary, capture_event(event)?);
8566 }
8567 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
8568 let needs_axes =
8569 matches!(local, b"areaChart" | b"scatterChart" | b"radarChart");
8570 if needs_axes && axis_ids.len() != 2 {
8571 return Err(ChartError::InvalidValue {
8572 element: format!("c:{root}/c:axId"),
8573 value: format!("expected 2, found {}", axis_ids.len()),
8574 });
8575 }
8576 if !needs_axes && !axis_ids.is_empty() {
8577 return Err(ChartError::InvalidValue {
8578 element: format!("c:{root}/c:axId"),
8579 value: "axis-free plot contains axis references".to_owned(),
8580 });
8581 }
8582 let parsed_ids = needs_axes.then(|| [axis_ids[0].0, axis_ids[1].0]);
8583 let original_series_keys = series
8584 .iter()
8585 .map(|item| (item.index, item.order))
8586 .collect::<Vec<_>>();
8587 let plot = match local {
8588 b"pieChart" => Plot::Pie {
8589 first_slice_angle: first_slice_angle
8590 .as_ref()
8591 .map_or(0, |item| item.0),
8592 series,
8593 data_labels,
8594 },
8595 b"doughnutChart" => Plot::Doughnut {
8596 first_slice_angle: first_slice_angle
8597 .as_ref()
8598 .map_or(0, |item| item.0),
8599 hole_size: hole_size.as_ref().map_or(50, |item| item.0),
8600 series,
8601 data_labels,
8602 },
8603 b"areaChart" => Plot::Area {
8604 grouping: grouping
8605 .as_ref()
8606 .ok_or_else(|| {
8607 ChartError::MissingElement("c:grouping".to_owned())
8608 })?
8609 .0,
8610 series,
8611 data_labels,
8612 axis_ids: parsed_ids.ok_or_else(|| {
8613 ChartError::MissingElement("c:areaChart/c:axId".to_owned())
8614 })?,
8615 },
8616 b"scatterChart" => {
8617 let lexical = style
8618 .as_ref()
8619 .ok_or_else(|| {
8620 ChartError::MissingElement("c:scatterStyle".to_owned())
8621 })?
8622 .0
8623 .as_str();
8624 Plot::Scatter {
8625 style: ScatterStyle::parse(lexical).ok_or_else(|| {
8626 invalid_attribute(
8627 "scatterStyle",
8628 "val",
8629 lexical.to_owned(),
8630 )
8631 })?,
8632 series,
8633 data_labels,
8634 axis_ids: parsed_ids.ok_or_else(|| {
8635 ChartError::MissingElement(
8636 "c:scatterChart/c:axId".to_owned(),
8637 )
8638 })?,
8639 }
8640 }
8641 b"radarChart" => {
8642 let lexical = style
8643 .as_ref()
8644 .ok_or_else(|| {
8645 ChartError::MissingElement("c:radarStyle".to_owned())
8646 })?
8647 .0
8648 .as_str();
8649 Plot::Radar {
8650 style: RadarStyle::parse(lexical).ok_or_else(|| {
8651 invalid_attribute(
8652 "radarStyle",
8653 "val",
8654 lexical.to_owned(),
8655 )
8656 })?,
8657 series,
8658 data_labels,
8659 axis_ids: parsed_ids.ok_or_else(|| {
8660 ChartError::MissingElement(
8661 "c:radarChart/c:axId".to_owned(),
8662 )
8663 })?,
8664 }
8665 }
8666 _ => {
8667 return Err(ChartError::UnexpectedElement(format!(
8668 "c:{}",
8669 String::from_utf8_lossy(local)
8670 )));
8671 }
8672 };
8673 plot.validate()?;
8674 let final_boundary = boundary.max(original_series_keys.len() + 6);
8675 return Ok((
8676 plot,
8677 PlotMarkup {
8678 raw_attributes,
8679 raw_children: raw_children_in_schema_order(
8680 &raw_children,
8681 final_boundary,
8682 ),
8683 grouping: grouping.map(|item| item.1),
8684 first_slice_angle: first_slice_angle.map(|item| item.1),
8685 hole_size: hole_size.map(|item| item.1),
8686 style: style.map(|item| item.1),
8687 axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8688 original_series_keys,
8689 original_axis_ids: parsed_ids
8690 .map_or_else(Vec::new, |ids| ids.to_vec()),
8691 parsed_remaining: match local {
8692 b"pieChart" => Some("pieChart"),
8693 b"doughnutChart" => Some("doughnutChart"),
8694 b"areaChart" => Some("areaChart"),
8695 b"scatterChart" => Some("scatterChart"),
8696 b"radarChart" => Some("radarChart"),
8697 _ => None,
8698 },
8699 ..PlotMarkup::default()
8700 },
8701 ));
8702 }
8703 Event::Eof => return Err(missing_end(&format!("c:{root}"))),
8704 _ => {}
8705 }
8706 inner.clear();
8707 }
8708 }
8709 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
8710 return Err(ChartError::MissingElement("c:ser".to_owned()));
8711 }
8712 Event::Start(element) | Event::Empty(element) => {
8713 return Err(ChartError::UnexpectedElement(element_name(&element)));
8714 }
8715 Event::Eof => return Err(ChartError::MissingElement(format!("c:{root}"))),
8716 _ => {}
8717 }
8718 buffer.clear();
8719 }
8720}
8721
8722#[allow(clippy::too_many_arguments)]
8723fn parse_remaining_plot_child(
8724 root: &[u8],
8725 name: &[u8],
8726 raw: Vec<u8>,
8727 namespaces: &NamespaceBindings,
8728 grouping: &mut Option<(Grouping, ScalarMarkup)>,
8729 style: &mut Option<(String, ScalarMarkup)>,
8730 first_slice_angle: &mut Option<(u16, ScalarMarkup)>,
8731 hole_size: &mut Option<(u8, ScalarMarkup)>,
8732 series: &mut Vec<Series>,
8733 data_labels: &mut Option<CT_DLbls>,
8734 axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
8735 raw_children: &mut OrderedRawChildren,
8736 boundary: &mut usize,
8737) -> Result<()> {
8738 let property_first = matches!(root, b"areaChart" | b"scatterChart" | b"radarChart");
8739 let base = usize::from(property_first);
8740 match name {
8741 b"grouping" if root == b"areaChart" => {
8742 let (value, markup) = required_scalar(&raw, "grouping")?;
8743 let parsed = Grouping::parse(&value)
8744 .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
8745 set_once(grouping, (parsed, markup), "c:grouping")?;
8746 *boundary = (*boundary).max(1);
8747 }
8748 b"scatterStyle" if root == b"scatterChart" => {
8749 let (value, markup) = required_scalar(&raw, "scatterStyle")?;
8750 if ScatterStyle::parse(&value).is_none() {
8751 return Err(invalid_attribute("scatterStyle", "val", value));
8752 }
8753 set_once(style, (value, markup), "c:scatterStyle")?;
8754 *boundary = (*boundary).max(1);
8755 }
8756 b"radarStyle" if root == b"radarChart" => {
8757 let (value, markup) = required_scalar(&raw, "radarStyle")?;
8758 if RadarStyle::parse(&value).is_none() {
8759 return Err(invalid_attribute("radarStyle", "val", value));
8760 }
8761 set_once(style, (value, markup), "c:radarStyle")?;
8762 *boundary = (*boundary).max(1);
8763 }
8764 b"ser" => {
8765 series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
8766 *boundary = (*boundary).max(base + series.len());
8767 }
8768 b"dLbls" => {
8769 set_once(
8770 data_labels,
8771 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
8772 "c:dLbls",
8773 )?;
8774 *boundary = (*boundary).max(base + series.len() + 1);
8775 }
8776 b"firstSliceAng" if matches!(root, b"pieChart" | b"doughnutChart") => {
8777 let (value, markup) = required_scalar(&raw, "firstSliceAng")?;
8778 let parsed = value
8779 .parse::<u16>()
8780 .map_err(|_| invalid_attribute("firstSliceAng", "val", value.clone()))?;
8781 if parsed > 360 {
8782 return Err(invalid_attribute("firstSliceAng", "val", value));
8783 }
8784 set_once(first_slice_angle, (parsed, markup), "c:firstSliceAng")?;
8785 *boundary = (*boundary).max(series.len() + 2);
8786 }
8787 b"holeSize" if root == b"doughnutChart" => {
8788 let (value, markup) = required_scalar(&raw, "holeSize")?;
8789 let parsed = value
8790 .parse::<u8>()
8791 .map_err(|_| invalid_attribute("holeSize", "val", value.clone()))?;
8792 if !(10..=90).contains(&parsed) {
8793 return Err(invalid_attribute("holeSize", "val", value));
8794 }
8795 set_once(hole_size, (parsed, markup), "c:holeSize")?;
8796 *boundary = (*boundary).max(series.len() + 3);
8797 }
8798 b"axId" if property_first => {
8799 if axis_ids.len() == 2 {
8800 return Err(ChartError::DuplicateElement("c:axId".to_owned()));
8801 }
8802 axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
8803 *boundary = (*boundary).max(base + series.len() + 1 + axis_ids.len());
8804 }
8805 b"axId" => {
8806 return Err(ChartError::InvalidValue {
8807 element: "c:axId".to_owned(),
8808 value: "axis-free plot contains axis reference".to_owned(),
8809 });
8810 }
8811 _ => {
8812 let raw_boundary = match (root, name) {
8813 (_, b"varyColors") => base,
8814 (b"pieChart", b"extLst") => series.len() + 2,
8815 (b"doughnutChart", b"extLst") => series.len() + 3,
8816 (b"areaChart" | b"scatterChart" | b"radarChart", b"dropLines") => {
8817 base + series.len() + 1
8818 }
8819 (b"areaChart" | b"scatterChart" | b"radarChart", b"extLst") => {
8820 base + series.len() + 3
8821 }
8822 _ => *boundary,
8823 };
8824 raw_children.push(raw_boundary, raw);
8825 *boundary = (*boundary).max(raw_boundary);
8826 }
8827 }
8828 Ok(())
8829}
8830
8831fn parse_bar_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8832 let mut reader = Reader::from_reader(xml);
8833 let mut buffer = Vec::new();
8834 loop {
8835 match reader
8836 .read_event_into(&mut buffer)
8837 .map_err(OxmlError::from)?
8838 {
8839 Event::Start(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
8840 let namespaces = typed_rewrite_bindings(&element, inherited)?;
8841 let (mut raw_attributes, namespace_declarations) =
8842 capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8843 raw_attributes.splice(0..0, namespace_declarations);
8844 let mut direction = None;
8845 let mut grouping = None;
8846 let mut gap_width = None;
8847 let mut overlap = None;
8848 let mut series = Vec::new();
8849 let mut data_labels = None;
8850 let mut axis_ids = Vec::new();
8851 let mut raw_children = OrderedRawChildren::default();
8852 let mut boundary = 0usize;
8853 let mut inner = Vec::new();
8854 loop {
8855 match reader
8856 .read_event_into(&mut inner)
8857 .map_err(OxmlError::from)?
8858 {
8859 Event::Start(child) => {
8860 let name = chart_child_local(&child, &namespaces)?;
8861 let raw = capture_element(&mut reader, &child)?;
8862 parse_bar_child(
8863 name.as_deref().unwrap_or_default(),
8864 raw,
8865 &namespaces,
8866 &mut direction,
8867 &mut grouping,
8868 &mut gap_width,
8869 &mut overlap,
8870 &mut series,
8871 &mut data_labels,
8872 &mut axis_ids,
8873 &mut raw_children,
8874 &mut boundary,
8875 )?;
8876 }
8877 Event::Empty(child) => {
8878 let name = chart_child_local(&child, &namespaces)?;
8879 let raw = capture_empty_element(&child)?;
8880 parse_bar_child(
8881 name.as_deref().unwrap_or_default(),
8882 raw,
8883 &namespaces,
8884 &mut direction,
8885 &mut grouping,
8886 &mut gap_width,
8887 &mut overlap,
8888 &mut series,
8889 &mut data_labels,
8890 &mut axis_ids,
8891 &mut raw_children,
8892 &mut boundary,
8893 )?;
8894 }
8895 event @ (Event::Text(_)
8896 | Event::CData(_)
8897 | Event::Comment(_)
8898 | Event::PI(_)
8899 | Event::GeneralRef(_)) => {
8900 raw_children.push(boundary, capture_event(event)?);
8901 }
8902 Event::End(end) if matches_local_name(end.name().as_ref(), b"barChart") => {
8903 let (direction, direction_markup) = direction
8904 .ok_or_else(|| ChartError::MissingElement("c:barDir".to_owned()))?;
8905 let (grouping, grouping_markup) = grouping.ok_or_else(|| {
8906 ChartError::MissingElement("c:grouping".to_owned())
8907 })?;
8908 if axis_ids.len() != 2 {
8909 return Err(ChartError::InvalidValue {
8910 element: "c:barChart/c:axId".to_owned(),
8911 value: format!("expected 2, found {}", axis_ids.len()),
8912 });
8913 }
8914 let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
8915 let original_series_keys: Vec<(u32, u32)> =
8916 series.iter().map(|item| (item.index, item.order)).collect();
8917 let plot = Plot::Bar {
8918 direction,
8919 grouping,
8920 gap_width: gap_width.as_ref().map_or(150, |item| item.0),
8921 overlap: overlap.as_ref().map_or(0, |item| item.0),
8922 series,
8923 data_labels,
8924 axis_ids: parsed_ids,
8925 };
8926 plot.validate()?;
8927 return Ok((
8928 plot,
8929 PlotMarkup {
8930 raw_attributes,
8931 raw_children: raw_children_in_schema_order(
8932 &raw_children,
8933 original_series_keys.len() + 7,
8934 ),
8935 direction: Some(direction_markup),
8936 grouping: Some(grouping_markup),
8937 gap_width: gap_width.map(|item| item.1),
8938 overlap: overlap.map(|item| item.1),
8939 marker: None,
8940 smooth: None,
8941 first_slice_angle: None,
8942 hole_size: None,
8943 style: None,
8944 axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8945 original_series_keys,
8946 original_axis_ids: parsed_ids.to_vec(),
8947 parsed_bar: Some(true),
8948 parsed_remaining: None,
8949 },
8950 ));
8951 }
8952 Event::Eof => return Err(missing_end("c:barChart")),
8953 _ => {}
8954 }
8955 inner.clear();
8956 }
8957 }
8958 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
8959 return Err(ChartError::MissingElement("c:barDir".to_owned()));
8960 }
8961 Event::Start(element) | Event::Empty(element) => {
8962 return Err(ChartError::UnexpectedElement(element_name(&element)));
8963 }
8964 Event::Eof => return Err(ChartError::MissingElement("c:barChart".to_owned())),
8965 _ => {}
8966 }
8967 buffer.clear();
8968 }
8969}
8970
8971#[allow(clippy::too_many_arguments)]
8972fn parse_bar_child(
8973 name: &[u8],
8974 raw: Vec<u8>,
8975 namespaces: &NamespaceBindings,
8976 direction: &mut Option<(BarDirection, ScalarMarkup)>,
8977 grouping: &mut Option<(BarGrouping, ScalarMarkup)>,
8978 gap_width: &mut Option<(u16, ScalarMarkup)>,
8979 overlap: &mut Option<(i8, ScalarMarkup)>,
8980 series: &mut Vec<Series>,
8981 data_labels: &mut Option<CT_DLbls>,
8982 axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
8983 raw_children: &mut OrderedRawChildren,
8984 boundary: &mut usize,
8985) -> Result<()> {
8986 match name {
8987 b"barDir" => {
8988 let (value, markup) = required_scalar(&raw, "barDir")?;
8989 let value = BarDirection::parse(&value)
8990 .ok_or_else(|| invalid_attribute("barDir", "val", value))?;
8991 set_once(direction, (value, markup), "c:barDir")?;
8992 *boundary = (*boundary).max(1);
8993 }
8994 b"grouping" => {
8995 let (value, markup) = required_scalar(&raw, "grouping")?;
8996 let value = BarGrouping::parse(&value)
8997 .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
8998 set_once(grouping, (value, markup), "c:grouping")?;
8999 *boundary = (*boundary).max(2);
9000 }
9001 b"ser" => {
9002 series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
9003 *boundary = (*boundary).max(series.len() + 2);
9004 }
9005 b"dLbls" => {
9006 set_once(
9007 data_labels,
9008 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
9009 "c:dLbls",
9010 )?;
9011 *boundary = (*boundary).max(series.len() + 3);
9012 }
9013 b"gapWidth" => {
9014 let (value, markup) = required_scalar(&raw, "gapWidth")?;
9015 let parsed = value
9016 .parse::<u16>()
9017 .map_err(|_| invalid_attribute("gapWidth", "val", value.clone()))?;
9018 if parsed > 500 {
9019 return Err(invalid_attribute("gapWidth", "val", value));
9020 }
9021 set_once(gap_width, (parsed, markup), "c:gapWidth")?;
9022 *boundary = (*boundary).max(series.len() + 4);
9023 }
9024 b"overlap" => {
9025 let (value, markup) = required_scalar(&raw, "overlap")?;
9026 let parsed = value
9027 .parse::<i8>()
9028 .map_err(|_| invalid_attribute("overlap", "val", value.clone()))?;
9029 if !(-100..=100).contains(&parsed) {
9030 return Err(invalid_attribute("overlap", "val", value));
9031 }
9032 set_once(overlap, (parsed, markup), "c:overlap")?;
9033 *boundary = (*boundary).max(series.len() + 5);
9034 }
9035 b"axId" => {
9036 if axis_ids.len() == 2 {
9037 return Err(ChartError::DuplicateElement("c:axId".to_owned()));
9038 }
9039 axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
9040 *boundary = (*boundary).max(series.len() + 5 + axis_ids.len());
9041 }
9042 _ => raw_children.push(*boundary, raw),
9043 }
9044 Ok(())
9045}
9046
9047fn parse_line_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
9048 let mut reader = Reader::from_reader(xml);
9049 let mut buffer = Vec::new();
9050 loop {
9051 match reader
9052 .read_event_into(&mut buffer)
9053 .map_err(OxmlError::from)?
9054 {
9055 Event::Start(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
9056 let namespaces = typed_rewrite_bindings(&element, inherited)?;
9057 let (mut raw_attributes, namespace_declarations) =
9058 capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
9059 raw_attributes.splice(0..0, namespace_declarations);
9060 let mut grouping = None;
9061 let mut marker = None;
9062 let mut smooth = None;
9063 let mut series = Vec::new();
9064 let mut data_labels = None;
9065 let mut axis_ids = Vec::new();
9066 let mut raw_children = OrderedRawChildren::default();
9067 let mut boundary = 0usize;
9068 let mut inner = Vec::new();
9069 loop {
9070 match reader
9071 .read_event_into(&mut inner)
9072 .map_err(OxmlError::from)?
9073 {
9074 Event::Start(child) => {
9075 let name = chart_child_local(&child, &namespaces)?;
9076 let raw = capture_element(&mut reader, &child)?;
9077 parse_line_child(
9078 name.as_deref().unwrap_or_default(),
9079 raw,
9080 &namespaces,
9081 &mut grouping,
9082 &mut marker,
9083 &mut smooth,
9084 &mut series,
9085 &mut data_labels,
9086 &mut axis_ids,
9087 &mut raw_children,
9088 &mut boundary,
9089 )?;
9090 }
9091 Event::Empty(child) => {
9092 let name = chart_child_local(&child, &namespaces)?;
9093 let raw = capture_empty_element(&child)?;
9094 parse_line_child(
9095 name.as_deref().unwrap_or_default(),
9096 raw,
9097 &namespaces,
9098 &mut grouping,
9099 &mut marker,
9100 &mut smooth,
9101 &mut series,
9102 &mut data_labels,
9103 &mut axis_ids,
9104 &mut raw_children,
9105 &mut boundary,
9106 )?;
9107 }
9108 event @ (Event::Text(_)
9109 | Event::CData(_)
9110 | Event::Comment(_)
9111 | Event::PI(_)
9112 | Event::GeneralRef(_)) => {
9113 raw_children.push(boundary, capture_event(event)?);
9114 }
9115 Event::End(end)
9116 if matches_local_name(end.name().as_ref(), b"lineChart") =>
9117 {
9118 let (grouping, grouping_markup) = grouping.ok_or_else(|| {
9119 ChartError::MissingElement("c:grouping".to_owned())
9120 })?;
9121 if axis_ids.len() != 2 {
9122 return Err(ChartError::InvalidValue {
9123 element: "c:lineChart/c:axId".to_owned(),
9124 value: format!("expected 2, found {}", axis_ids.len()),
9125 });
9126 }
9127 let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
9128 let original_series_keys: Vec<(u32, u32)> =
9129 series.iter().map(|item| (item.index, item.order)).collect();
9130 let plot = Plot::Line {
9131 grouping,
9132 marker: marker.as_ref().is_some_and(|item| item.0),
9133 smooth: smooth.as_ref().is_some_and(|item| item.0),
9134 series,
9135 data_labels,
9136 axis_ids: parsed_ids,
9137 };
9138 plot.validate()?;
9139 return Ok((
9140 plot,
9141 PlotMarkup {
9142 raw_attributes,
9143 raw_children: raw_children_in_schema_order(
9144 &raw_children,
9145 original_series_keys.len() + 6,
9146 ),
9147 direction: None,
9148 grouping: Some(grouping_markup),
9149 gap_width: None,
9150 overlap: None,
9151 marker: marker.map(|item| item.1),
9152 smooth: smooth.map(|item| item.1),
9153 first_slice_angle: None,
9154 hole_size: None,
9155 style: None,
9156 axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
9157 original_series_keys,
9158 original_axis_ids: parsed_ids.to_vec(),
9159 parsed_bar: Some(false),
9160 parsed_remaining: None,
9161 },
9162 ));
9163 }
9164 Event::Eof => return Err(missing_end("c:lineChart")),
9165 _ => {}
9166 }
9167 inner.clear();
9168 }
9169 }
9170 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
9171 return Err(ChartError::MissingElement("c:grouping".to_owned()));
9172 }
9173 Event::Start(element) | Event::Empty(element) => {
9174 return Err(ChartError::UnexpectedElement(element_name(&element)));
9175 }
9176 Event::Eof => return Err(ChartError::MissingElement("c:lineChart".to_owned())),
9177 _ => {}
9178 }
9179 buffer.clear();
9180 }
9181}
9182
9183#[allow(clippy::too_many_arguments)]
9184fn parse_line_child(
9185 name: &[u8],
9186 raw: Vec<u8>,
9187 namespaces: &NamespaceBindings,
9188 grouping: &mut Option<(Grouping, ScalarMarkup)>,
9189 marker: &mut Option<(bool, ScalarMarkup)>,
9190 smooth: &mut Option<(bool, ScalarMarkup)>,
9191 series: &mut Vec<Series>,
9192 data_labels: &mut Option<CT_DLbls>,
9193 axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
9194 raw_children: &mut OrderedRawChildren,
9195 boundary: &mut usize,
9196) -> Result<()> {
9197 match name {
9198 b"grouping" => {
9199 let (value, markup) = required_scalar(&raw, "grouping")?;
9200 let value = Grouping::parse(&value)
9201 .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
9202 set_once(grouping, (value, markup), "c:grouping")?;
9203 *boundary = (*boundary).max(1);
9204 }
9205 b"ser" => {
9206 series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
9207 *boundary = (*boundary).max(series.len() + 1);
9208 }
9209 b"dLbls" => {
9210 set_once(
9211 data_labels,
9212 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
9213 "c:dLbls",
9214 )?;
9215 *boundary = (*boundary).max(series.len() + 2);
9216 }
9217 b"marker" => {
9218 set_once(marker, parse_bool_value(&raw, "marker")?, "c:marker")?;
9219 *boundary = (*boundary).max(series.len() + 3);
9220 }
9221 b"smooth" => {
9222 set_once(smooth, parse_bool_value(&raw, "smooth")?, "c:smooth")?;
9223 *boundary = (*boundary).max(series.len() + 4);
9224 }
9225 b"axId" => {
9226 if axis_ids.len() == 2 {
9227 return Err(ChartError::DuplicateElement("c:axId".to_owned()));
9228 }
9229 axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
9230 *boundary = (*boundary).max(series.len() + 4 + axis_ids.len());
9231 }
9232 _ => raw_children.push(*boundary, raw),
9233 }
9234 Ok(())
9235}
9236
9237fn required_scalar(xml: &[u8], local: &str) -> Result<(String, ScalarMarkup)> {
9238 let (value, markup) = scalar_value(xml, local)?;
9239 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
9240 Ok((value, markup))
9241}
9242
9243fn write_plot<W: Write>(writer: &mut Writer<W>, plot: &Plot, markup: &PlotMarkup) -> Result<()> {
9244 plot.validate()?;
9245 if let Some(parsed_bar) = markup.parsed_bar
9246 && parsed_bar != matches!(plot, Plot::Bar { .. })
9247 {
9248 return Err(ChartError::InvalidValue {
9249 element: "c:plotArea".to_owned(),
9250 value: "a parsed plot family cannot be replaced while preserved payload remains"
9251 .to_owned(),
9252 });
9253 }
9254 let current_remaining = match plot {
9255 Plot::Pie { .. } => Some("pieChart"),
9256 Plot::Doughnut { .. } => Some("doughnutChart"),
9257 Plot::Area { .. } => Some("areaChart"),
9258 Plot::Scatter { .. } => Some("scatterChart"),
9259 Plot::Radar { .. } => Some("radarChart"),
9260 Plot::Bar { .. } | Plot::Line { .. } => None,
9261 };
9262 if let Some(parsed) = markup.parsed_remaining
9263 && current_remaining != Some(parsed)
9264 {
9265 return Err(ChartError::InvalidValue {
9266 element: "c:plotArea".to_owned(),
9267 value: "a parsed plot family cannot be replaced while preserved payload remains"
9268 .to_owned(),
9269 });
9270 }
9271 match plot {
9272 Plot::Bar {
9273 direction,
9274 grouping,
9275 gap_width,
9276 overlap,
9277 series,
9278 data_labels,
9279 axis_ids,
9280 } => {
9281 let mut start = BytesStart::new("c:barChart");
9282 push_attributes(&mut start, &markup.raw_attributes);
9283 writer
9284 .write_event(Event::Start(start))
9285 .map_err(OxmlError::from)?;
9286 emit_raw(writer, markup.raw_children.at(0))?;
9287 write_scalar(
9288 writer,
9289 "c:barDir",
9290 direction.as_str(),
9291 markup.direction.as_ref(),
9292 )?;
9293 emit_raw(writer, markup.raw_children.at(1))?;
9294 write_scalar(
9295 writer,
9296 "c:grouping",
9297 grouping.as_str(),
9298 markup.grouping.as_ref(),
9299 )?;
9300 let original_to_current =
9301 series_original_to_current(&markup.original_series_keys, series);
9302 emit_repeated_raw(
9303 writer,
9304 &markup.raw_children,
9305 2,
9306 &original_to_current,
9307 series.len(),
9308 0,
9309 )?;
9310 for (index, item) in series.iter().enumerate() {
9311 writer
9312 .get_mut()
9313 .write_all(&item.to_xml()?)
9314 .map_err(OxmlError::from)?;
9315 emit_repeated_raw(
9316 writer,
9317 &markup.raw_children,
9318 2,
9319 &original_to_current,
9320 series.len(),
9321 index + 1,
9322 )?;
9323 }
9324 if let Some(labels) = data_labels {
9325 writer
9326 .get_mut()
9327 .write_all(&labels.to_xml()?)
9328 .map_err(OxmlError::from)?;
9329 }
9330 let trailing = markup.original_series_keys.len();
9331 emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9332 if *gap_width != 150 || markup.gap_width.is_some() {
9333 write_scalar(
9334 writer,
9335 "c:gapWidth",
9336 &gap_width.to_string(),
9337 markup.gap_width.as_ref(),
9338 )?;
9339 }
9340 emit_raw(writer, markup.raw_children.at(trailing + 4))?;
9341 if *overlap != 0 || markup.overlap.is_some() {
9342 write_scalar(
9343 writer,
9344 "c:overlap",
9345 &overlap.to_string(),
9346 markup.overlap.as_ref(),
9347 )?;
9348 }
9349 let axis_original_to_current =
9350 axis_original_to_current(&markup.original_axis_ids, axis_ids);
9351 let axis_current_to_original =
9352 invert_original_to_current(&axis_original_to_current, axis_ids.len());
9353 emit_repeated_raw(
9354 writer,
9355 &markup.raw_children,
9356 trailing + 5,
9357 &axis_original_to_current,
9358 axis_ids.len(),
9359 0,
9360 )?;
9361 for (index, id) in axis_ids.iter().enumerate() {
9362 let default_markup = default_axis_id_markup(*id);
9363 let original_markup = axis_current_to_original[index]
9364 .and_then(|original| markup.axis_ids.get(original));
9365 write_axis_id(
9366 writer,
9367 "c:axId",
9368 *id,
9369 original_markup.unwrap_or(&default_markup),
9370 )?;
9371 emit_repeated_raw(
9372 writer,
9373 &markup.raw_children,
9374 trailing + 5,
9375 &axis_original_to_current,
9376 axis_ids.len(),
9377 index + 1,
9378 )?;
9379 }
9380 writer
9381 .write_event(Event::End(BytesEnd::new("c:barChart")))
9382 .map_err(OxmlError::from)?;
9383 }
9384 Plot::Line {
9385 grouping,
9386 marker,
9387 smooth,
9388 series,
9389 data_labels,
9390 axis_ids,
9391 } => {
9392 let mut start = BytesStart::new("c:lineChart");
9393 push_attributes(&mut start, &markup.raw_attributes);
9394 writer
9395 .write_event(Event::Start(start))
9396 .map_err(OxmlError::from)?;
9397 emit_raw(writer, markup.raw_children.at(0))?;
9398 write_scalar(
9399 writer,
9400 "c:grouping",
9401 grouping.as_str(),
9402 markup.grouping.as_ref(),
9403 )?;
9404 let original_to_current =
9405 series_original_to_current(&markup.original_series_keys, series);
9406 emit_repeated_raw(
9407 writer,
9408 &markup.raw_children,
9409 1,
9410 &original_to_current,
9411 series.len(),
9412 0,
9413 )?;
9414 for (index, item) in series.iter().enumerate() {
9415 writer
9416 .get_mut()
9417 .write_all(&item.to_xml()?)
9418 .map_err(OxmlError::from)?;
9419 emit_repeated_raw(
9420 writer,
9421 &markup.raw_children,
9422 1,
9423 &original_to_current,
9424 series.len(),
9425 index + 1,
9426 )?;
9427 }
9428 if let Some(labels) = data_labels {
9429 writer
9430 .get_mut()
9431 .write_all(&labels.to_xml()?)
9432 .map_err(OxmlError::from)?;
9433 }
9434 let trailing = markup.original_series_keys.len();
9435 emit_raw(writer, markup.raw_children.at(trailing + 2))?;
9436 if *marker || markup.marker.is_some() {
9437 write_scalar(
9438 writer,
9439 "c:marker",
9440 bool_lexical(*marker),
9441 markup.marker.as_ref(),
9442 )?;
9443 }
9444 emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9445 if *smooth || markup.smooth.is_some() {
9446 write_scalar(
9447 writer,
9448 "c:smooth",
9449 bool_lexical(*smooth),
9450 markup.smooth.as_ref(),
9451 )?;
9452 }
9453 let axis_original_to_current =
9454 axis_original_to_current(&markup.original_axis_ids, axis_ids);
9455 let axis_current_to_original =
9456 invert_original_to_current(&axis_original_to_current, axis_ids.len());
9457 emit_repeated_raw(
9458 writer,
9459 &markup.raw_children,
9460 trailing + 4,
9461 &axis_original_to_current,
9462 axis_ids.len(),
9463 0,
9464 )?;
9465 for (index, id) in axis_ids.iter().enumerate() {
9466 let default_markup = default_axis_id_markup(*id);
9467 let original_markup = axis_current_to_original[index]
9468 .and_then(|original| markup.axis_ids.get(original));
9469 write_axis_id(
9470 writer,
9471 "c:axId",
9472 *id,
9473 original_markup.unwrap_or(&default_markup),
9474 )?;
9475 emit_repeated_raw(
9476 writer,
9477 &markup.raw_children,
9478 trailing + 4,
9479 &axis_original_to_current,
9480 axis_ids.len(),
9481 index + 1,
9482 )?;
9483 }
9484 writer
9485 .write_event(Event::End(BytesEnd::new("c:lineChart")))
9486 .map_err(OxmlError::from)?;
9487 }
9488 remaining => {
9489 let (
9490 root,
9491 property,
9492 series,
9493 data_labels,
9494 axis_ids,
9495 first_slice_angle,
9496 hole_size,
9497 scatter,
9498 ) = match remaining {
9499 Plot::Pie {
9500 first_slice_angle,
9501 series,
9502 data_labels,
9503 } => (
9504 "pieChart",
9505 None,
9506 series,
9507 data_labels,
9508 None,
9509 Some(*first_slice_angle),
9510 None,
9511 false,
9512 ),
9513 Plot::Doughnut {
9514 first_slice_angle,
9515 hole_size,
9516 series,
9517 data_labels,
9518 } => (
9519 "doughnutChart",
9520 None,
9521 series,
9522 data_labels,
9523 None,
9524 Some(*first_slice_angle),
9525 Some(*hole_size),
9526 false,
9527 ),
9528 Plot::Area {
9529 grouping,
9530 series,
9531 data_labels,
9532 axis_ids,
9533 } => (
9534 "areaChart",
9535 Some(("c:grouping", grouping.as_str(), markup.grouping.as_ref())),
9536 series,
9537 data_labels,
9538 Some(axis_ids),
9539 None,
9540 None,
9541 false,
9542 ),
9543 Plot::Scatter {
9544 style,
9545 series,
9546 data_labels,
9547 axis_ids,
9548 } => (
9549 "scatterChart",
9550 Some(("c:scatterStyle", style.as_str(), markup.style.as_ref())),
9551 series,
9552 data_labels,
9553 Some(axis_ids),
9554 None,
9555 None,
9556 true,
9557 ),
9558 Plot::Radar {
9559 style,
9560 series,
9561 data_labels,
9562 axis_ids,
9563 } => (
9564 "radarChart",
9565 Some(("c:radarStyle", style.as_str(), markup.style.as_ref())),
9566 series,
9567 data_labels,
9568 Some(axis_ids),
9569 None,
9570 None,
9571 false,
9572 ),
9573 Plot::Bar { .. } | Plot::Line { .. } => unreachable!("handled above"),
9574 };
9575 let tag = format!("c:{root}");
9576 let mut start = BytesStart::new(&tag);
9577 push_attributes(&mut start, &markup.raw_attributes);
9578 writer
9579 .write_event(Event::Start(start))
9580 .map_err(OxmlError::from)?;
9581 let base = usize::from(property.is_some());
9582 if let Some((name, value, scalar_markup)) = property {
9583 emit_raw(writer, markup.raw_children.at(0))?;
9584 write_scalar(writer, name, value, scalar_markup)?;
9585 }
9586 let original_to_current =
9587 series_original_to_current(&markup.original_series_keys, series);
9588 emit_repeated_raw(
9589 writer,
9590 &markup.raw_children,
9591 base,
9592 &original_to_current,
9593 series.len(),
9594 0,
9595 )?;
9596 for (index, item) in series.iter().enumerate() {
9597 writer
9598 .get_mut()
9599 .write_all(&item.to_xml_for_plot(scatter)?)
9600 .map_err(OxmlError::from)?;
9601 emit_repeated_raw(
9602 writer,
9603 &markup.raw_children,
9604 base,
9605 &original_to_current,
9606 series.len(),
9607 index + 1,
9608 )?;
9609 }
9610 if let Some(labels) = data_labels {
9611 writer
9612 .get_mut()
9613 .write_all(&labels.to_xml()?)
9614 .map_err(OxmlError::from)?;
9615 }
9616 let trailing = base + markup.original_series_keys.len();
9617 if let Some(axis_ids) = axis_ids {
9618 let axis_original_to_current =
9619 axis_original_to_current(&markup.original_axis_ids, axis_ids);
9620 let axis_current_to_original =
9621 invert_original_to_current(&axis_original_to_current, axis_ids.len());
9622 emit_repeated_raw(
9623 writer,
9624 &markup.raw_children,
9625 trailing + 1,
9626 &axis_original_to_current,
9627 axis_ids.len(),
9628 0,
9629 )?;
9630 for (index, id) in axis_ids.iter().enumerate() {
9631 let default_markup = default_axis_id_markup(*id);
9632 let original_markup = axis_current_to_original[index]
9633 .and_then(|original| markup.axis_ids.get(original));
9634 write_axis_id(
9635 writer,
9636 "c:axId",
9637 *id,
9638 original_markup.unwrap_or(&default_markup),
9639 )?;
9640 emit_repeated_raw(
9641 writer,
9642 &markup.raw_children,
9643 trailing + 1,
9644 &axis_original_to_current,
9645 axis_ids.len(),
9646 index + 1,
9647 )?;
9648 }
9649 } else {
9650 emit_raw(writer, markup.raw_children.at(trailing + 1))?;
9651 if let Some(angle) = first_slice_angle {
9652 if angle != 0 || markup.first_slice_angle.is_some() {
9653 write_scalar(
9654 writer,
9655 "c:firstSliceAng",
9656 &angle.to_string(),
9657 markup.first_slice_angle.as_ref(),
9658 )?;
9659 }
9660 emit_raw(writer, markup.raw_children.at(trailing + 2))?;
9661 }
9662 if let Some(size) = hole_size {
9663 if size != 50 || markup.hole_size.is_some() {
9664 write_scalar(
9665 writer,
9666 "c:holeSize",
9667 &size.to_string(),
9668 markup.hole_size.as_ref(),
9669 )?;
9670 }
9671 emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9672 }
9673 }
9674 writer
9675 .write_event(Event::End(BytesEnd::new(&tag)))
9676 .map_err(OxmlError::from)?;
9677 }
9678 }
9679 Ok(())
9680}
9681
9682fn series_original_to_current(original: &[(u32, u32)], current: &[Series]) -> Vec<Option<usize>> {
9683 let mut used = vec![false; current.len()];
9684 let mut matches = vec![None; original.len()];
9685 for original_index in 0..original.len().min(current.len()) {
9686 if original[original_index]
9687 == (current[original_index].index, current[original_index].order)
9688 {
9689 matches[original_index] = Some(original_index);
9690 used[original_index] = true;
9691 }
9692 }
9693 for (original_index, key) in original.iter().enumerate() {
9694 if matches[original_index].is_some() {
9695 continue;
9696 }
9697 if let Some(current_index) = current
9698 .iter()
9699 .enumerate()
9700 .position(|(index, item)| !used[index] && (item.index, item.order) == *key)
9701 {
9702 matches[original_index] = Some(current_index);
9703 used[current_index] = true;
9704 }
9705 }
9706 match_unidentified_by_position(&mut matches, &mut used);
9707 matches
9708}
9709
9710fn axis_original_to_current(original: &[AxisId], current: &[AxisId]) -> Vec<Option<usize>> {
9711 let mut used = vec![false; current.len()];
9712 let mut matches = vec![None; original.len()];
9713 for (original_index, id) in original.iter().enumerate() {
9714 if let Some(current_index) = current
9715 .iter()
9716 .enumerate()
9717 .position(|(index, current_id)| !used[index] && current_id == id)
9718 {
9719 matches[original_index] = Some(current_index);
9720 used[current_index] = true;
9721 }
9722 }
9723 match_unidentified_by_position(&mut matches, &mut used);
9724 matches
9725}
9726
9727fn match_unidentified_by_position(matches: &mut [Option<usize>], used: &mut [bool]) {
9728 for original_index in 0..matches.len() {
9729 if matches[original_index].is_none() && original_index < used.len() && !used[original_index]
9730 {
9731 matches[original_index] = Some(original_index);
9732 used[original_index] = true;
9733 }
9734 }
9735 for matched in matches.iter_mut().filter(|matched| matched.is_none()) {
9736 if let Some(current_index) = used.iter().position(|used| !used) {
9737 *matched = Some(current_index);
9738 used[current_index] = true;
9739 }
9740 }
9741}
9742
9743fn invert_original_to_current(
9744 original_to_current: &[Option<usize>],
9745 current_len: usize,
9746) -> Vec<Option<usize>> {
9747 let mut current_to_original = vec![None; current_len];
9748 for (original, current) in original_to_current.iter().enumerate() {
9749 if let Some(current) = current {
9750 current_to_original[*current] = Some(original);
9751 }
9752 }
9753 current_to_original
9754}
9755
9756fn emit_repeated_raw<W: Write>(
9757 writer: &mut Writer<W>,
9758 raw_children: &OrderedRawChildren,
9759 offset: usize,
9760 original_to_current: &[Option<usize>],
9761 current_len: usize,
9762 current_boundary: usize,
9763) -> Result<()> {
9764 if current_boundary == 0 {
9765 emit_raw(writer, raw_children.at(offset))?;
9766 }
9767 for original_boundary in 1..=original_to_current.len() {
9768 let effective = original_to_current
9769 .iter()
9770 .skip(original_boundary)
9771 .flatten()
9772 .copied()
9773 .next()
9774 .unwrap_or(current_len);
9775 if effective == current_boundary {
9776 emit_raw(writer, raw_children.at(offset + original_boundary))?;
9777 }
9778 }
9779 Ok(())
9780}
9781
9782fn parse_plot_axis(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Axis>> {
9783 let mut reader = Reader::from_reader(xml);
9784 let mut buffer = Vec::new();
9785 loop {
9786 match reader
9787 .read_event_into(&mut buffer)
9788 .map_err(OxmlError::from)?
9789 {
9790 Event::Start(element) | Event::Empty(element) => {
9791 if !element_is_in_namespace(&element, C_NS, inherited)? {
9792 return Ok(None);
9793 }
9794 let name = element.name();
9795 if AxisKind::parse(local_name(name.as_ref())).is_none() {
9796 return Ok(None);
9797 }
9798 return Axis::from_xml_with_namespaces(xml, inherited).map(Some);
9799 }
9800 Event::Eof => return Ok(None),
9801 _ => {}
9802 }
9803 buffer.clear();
9804 }
9805}
9806
9807fn validate_axis_pairs(axes: &[Axis]) -> Result<()> {
9808 for (index, axis) in axes.iter().enumerate() {
9809 if axes[..index].iter().any(|other| other.id == axis.id) {
9810 return Err(ChartError::DuplicateElement(format!(
9811 "c:axId {}",
9812 axis.id.value()
9813 )));
9814 }
9815 }
9816 for axis in axes {
9817 if axis.id == axis.cross_axis {
9818 return Err(ChartError::InvalidValue {
9819 element: "c:crossAx".to_owned(),
9820 value: format!("axis {} crosses itself", axis.id.value()),
9821 });
9822 }
9823 let crossed = axes
9824 .iter()
9825 .find(|candidate| candidate.id == axis.cross_axis)
9826 .ok_or_else(|| ChartError::InvalidValue {
9827 element: "c:crossAx".to_owned(),
9828 value: format!(
9829 "axis {} references missing axis {}",
9830 axis.id.value(),
9831 axis.cross_axis.value()
9832 ),
9833 })?;
9834 if crossed.cross_axis != axis.id {
9835 return Err(ChartError::InvalidValue {
9836 element: "c:crossAx".to_owned(),
9837 value: format!(
9838 "axis {} references {}, which references {}",
9839 axis.id.value(),
9840 crossed.id.value(),
9841 crossed.cross_axis.value()
9842 ),
9843 });
9844 }
9845 }
9846 Ok(())
9847}
9848
9849fn parse_plot_series(
9850 xml: &[u8],
9851 inherited: &NamespaceBindings,
9852 series: &mut Vec<Series>,
9853) -> Result<()> {
9854 let mut reader = Reader::from_reader(xml);
9855 let mut buffer = Vec::new();
9856 loop {
9857 match reader
9858 .read_event_into(&mut buffer)
9859 .map_err(OxmlError::from)?
9860 {
9861 Event::Start(element) => {
9862 if !element_is_in_namespace(&element, C_NS, inherited)? {
9863 return Ok(());
9864 }
9865 let local = local_name(element.name().as_ref()).to_vec();
9866 if !is_supported_series_plot(&local) {
9867 return Ok(());
9868 }
9869 let namespaces = chart_bindings(inherited, &element)?;
9870 let mut inner = Vec::new();
9871 loop {
9872 match reader
9873 .read_event_into(&mut inner)
9874 .map_err(OxmlError::from)?
9875 {
9876 Event::Start(child) => {
9877 let name = chart_child_local(&child, &namespaces)?;
9878 let raw = capture_element(&mut reader, &child)?;
9879 if name.as_deref() == Some(b"ser") {
9880 series.push(Series::from_xml_with_namespaces(&raw, &namespaces)?);
9881 }
9882 }
9883 Event::Empty(child) => {
9884 let name = chart_child_local(&child, &namespaces)?;
9885 if name.as_deref() == Some(b"ser") {
9886 return Err(ChartError::MissingElement("c:ser/c:idx".to_owned()));
9887 }
9888 }
9889 Event::End(end) if matches_local_name(end.name().as_ref(), &local) => {
9890 return Ok(());
9891 }
9892 Event::Eof => {
9893 return Err(missing_end(&format!(
9894 "c:{}",
9895 String::from_utf8_lossy(&local)
9896 )));
9897 }
9898 _ => {}
9899 }
9900 inner.clear();
9901 }
9902 }
9903 Event::Empty(_) | Event::Eof => return Ok(()),
9904 _ => {}
9905 }
9906 buffer.clear();
9907 }
9908}
9909
9910fn is_supported_series_plot(local: &[u8]) -> bool {
9911 matches!(
9912 local,
9913 b"areaChart"
9914 | b"area3DChart"
9915 | b"barChart"
9916 | b"bar3DChart"
9917 | b"doughnutChart"
9918 | b"lineChart"
9919 | b"line3DChart"
9920 | b"ofPieChart"
9921 | b"pieChart"
9922 | b"pie3DChart"
9923 | b"radarChart"
9924 | b"stockChart"
9925 | b"surfaceChart"
9926 | b"surface3DChart"
9927 )
9928}
9929
9930impl CT_Legend {
9931 fn from_xml(xml: &[u8]) -> Result<Self> {
9932 let (raw_attributes, raw_children) = parse_raw_shell(xml, b"legend", "c:legend")?;
9933 Ok(Self {
9934 raw_attributes,
9935 raw_children,
9936 })
9937 }
9938
9939 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
9940 write_raw_shell(writer, "c:legend", &self.raw_attributes, &self.raw_children)
9941 }
9942
9943 pub fn raw_children(&self) -> &OrderedRawChildren {
9944 &self.raw_children
9945 }
9946}
9947
9948fn parse_raw_shell(
9949 xml: &[u8],
9950 local: &[u8],
9951 tag: &str,
9952) -> Result<(XmlAttributes, OrderedRawChildren)> {
9953 let mut reader = Reader::from_reader(xml);
9954 let mut buffer = Vec::new();
9955 loop {
9956 match reader
9957 .read_event_into(&mut buffer)
9958 .map_err(OxmlError::from)?
9959 {
9960 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
9961 reject_conflicting_prefix(&element, b"c", C_NS)?;
9962 let raw_attributes = capture_attributes(&element)?;
9963 let mut raw_children = OrderedRawChildren::default();
9964 let mut child_buffer = Vec::new();
9965 loop {
9966 match reader
9967 .read_event_into(&mut child_buffer)
9968 .map_err(OxmlError::from)?
9969 {
9970 Event::Start(child) => {
9971 raw_children.push(0, capture_element(&mut reader, &child)?)
9972 }
9973 Event::Empty(child) => raw_children.push(0, capture_empty_element(&child)?),
9974 event @ (Event::Text(_)
9975 | Event::CData(_)
9976 | Event::Comment(_)
9977 | Event::PI(_)
9978 | Event::GeneralRef(_)) => {
9979 raw_children.push(0, capture_event(event)?);
9980 }
9981 Event::End(end) if matches_local_name(end.name().as_ref(), local) => break,
9982 Event::Eof => return Err(missing_end(tag)),
9983 _ => {}
9984 }
9985 child_buffer.clear();
9986 }
9987 return Ok((raw_attributes, raw_children));
9988 }
9989 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
9990 reject_conflicting_prefix(&element, b"c", C_NS)?;
9991 return Ok((capture_attributes(&element)?, OrderedRawChildren::default()));
9992 }
9993 Event::Start(element) | Event::Empty(element) => {
9994 return Err(ChartError::UnexpectedElement(element_name(&element)));
9995 }
9996 Event::Eof => return Err(ChartError::MissingElement(tag.to_owned())),
9997 _ => {}
9998 }
9999 buffer.clear();
10000 }
10001}
10002
10003fn write_raw_shell<W: Write>(
10004 writer: &mut Writer<W>,
10005 tag: &str,
10006 raw_attributes: &XmlAttributes,
10007 raw_children: &OrderedRawChildren,
10008) -> Result<()> {
10009 let mut start = BytesStart::new(tag);
10010 push_attributes(&mut start, raw_attributes);
10011 if raw_children.is_empty() {
10012 writer
10013 .write_event(Event::Empty(start))
10014 .map_err(OxmlError::from)?;
10015 return Ok(());
10016 }
10017 writer
10018 .write_event(Event::Start(start))
10019 .map_err(OxmlError::from)?;
10020 emit_raw(writer, raw_children.at(0))?;
10021 writer
10022 .write_event(Event::End(BytesEnd::new(tag)))
10023 .map_err(OxmlError::from)?;
10024 Ok(())
10025}
10026
10027#[derive(Clone, Debug, PartialEq)]
10029pub struct CT_Chart {
10030 pub title: Option<CT_Title>,
10031 pub auto_title_deleted: bool,
10032 pub plot_area: CT_PlotArea,
10033 pub legend: Option<CT_Legend>,
10034 pub plot_vis_only: bool,
10035 pub disp_blanks_as: DispBlanksAs,
10036 auto_title_deleted_markup: Option<ScalarMarkup>,
10037 plot_vis_only_markup: Option<ScalarMarkup>,
10038 disp_blanks_as_markup: Option<ScalarMarkup>,
10039 raw_attributes: Vec<(String, String)>,
10040 raw_children: OrderedRawChildren,
10041}
10042
10043impl CT_Chart {
10044 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
10045 let mut reader = Reader::from_reader(xml);
10046 let mut buffer = Vec::new();
10047 loop {
10048 match reader
10049 .read_event_into(&mut buffer)
10050 .map_err(OxmlError::from)?
10051 {
10052 Event::Start(element) if matches_local_name(element.name().as_ref(), b"chart") => {
10053 reject_conflicting_prefix(&element, b"c", C_NS)?;
10054 return Self::from_element(&mut reader, &element, inherited);
10055 }
10056 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"chart") => {
10057 return Err(ChartError::MissingElement("c:plotArea".to_owned()));
10058 }
10059 Event::Start(element) | Event::Empty(element) => {
10060 return Err(ChartError::UnexpectedElement(element_name(&element)));
10061 }
10062 Event::Eof => return Err(ChartError::MissingElement("c:chart".to_owned())),
10063 _ => {}
10064 }
10065 buffer.clear();
10066 }
10067 }
10068
10069 fn from_element(
10070 reader: &mut Reader<&[u8]>,
10071 start: &BytesStart<'_>,
10072 inherited: &NamespaceBindings,
10073 ) -> Result<Self> {
10074 let namespaces = bindings_with_local(inherited, start)?;
10075 let raw_attributes = capture_attributes(start)?;
10076 let mut state = ChartParseState::default();
10077 let mut buffer = Vec::new();
10078
10079 loop {
10080 match reader
10081 .read_event_into(&mut buffer)
10082 .map_err(OxmlError::from)?
10083 {
10084 Event::Start(element) => {
10085 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10086 local_name(element.name().as_ref()).to_vec()
10087 } else {
10088 Vec::new()
10089 };
10090 let raw = capture_element(reader, &element)?;
10091 state.parse_child(&name, raw, &namespaces)?;
10092 }
10093 Event::Empty(element) => {
10094 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10095 local_name(element.name().as_ref()).to_vec()
10096 } else {
10097 Vec::new()
10098 };
10099 let raw = capture_empty_element(&element)?;
10100 state.parse_child(&name, raw, &namespaces)?;
10101 }
10102 event @ (Event::Text(_)
10103 | Event::CData(_)
10104 | Event::Comment(_)
10105 | Event::PI(_)
10106 | Event::GeneralRef(_)) => {
10107 state.capture_event(capture_event(event)?);
10108 }
10109 Event::End(element) if matches_local_name(element.name().as_ref(), b"chart") => {
10110 break;
10111 }
10112 Event::Eof => return Err(missing_end("c:chart")),
10113 _ => {}
10114 }
10115 buffer.clear();
10116 }
10117
10118 state.finish(raw_attributes)
10119 }
10120
10121 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
10122 let mut start = BytesStart::new("c:chart");
10123 push_attributes(&mut start, &self.raw_attributes);
10124 writer
10125 .write_event(Event::Start(start))
10126 .map_err(OxmlError::from)?;
10127 emit_raw(writer, self.raw_children.at(0))?;
10128 if let Some(title) = &self.title {
10129 title.write_xml(writer)?;
10130 }
10131 emit_raw(writer, self.raw_children.at(1))?;
10132 if self.auto_title_deleted_markup.is_some() || self.auto_title_deleted {
10133 write_scalar(
10134 writer,
10135 "c:autoTitleDeleted",
10136 if self.auto_title_deleted { "1" } else { "0" },
10137 self.auto_title_deleted_markup.as_ref(),
10138 )?;
10139 }
10140 for raw_boundary in 2..8 {
10141 emit_raw(writer, self.raw_children.at(raw_boundary))?;
10142 }
10143 self.plot_area.write_xml(writer)?;
10144 emit_raw(writer, self.raw_children.at(8))?;
10145 if let Some(legend) = &self.legend {
10146 legend.write_xml(writer)?;
10147 }
10148 emit_raw(writer, self.raw_children.at(9))?;
10149 if self.plot_vis_only_markup.is_some() || !self.plot_vis_only {
10150 write_scalar(
10151 writer,
10152 "c:plotVisOnly",
10153 if self.plot_vis_only { "1" } else { "0" },
10154 self.plot_vis_only_markup.as_ref(),
10155 )?;
10156 }
10157 emit_raw(writer, self.raw_children.at(10))?;
10158 if self.disp_blanks_as_markup.is_some() || self.disp_blanks_as != DispBlanksAs::Gap {
10159 write_scalar(
10160 writer,
10161 "c:dispBlanksAs",
10162 self.disp_blanks_as.as_str(),
10163 self.disp_blanks_as_markup.as_ref(),
10164 )?;
10165 }
10166 for raw_boundary in 11..=13 {
10167 emit_raw(writer, self.raw_children.at(raw_boundary))?;
10168 }
10169 writer
10170 .write_event(Event::End(BytesEnd::new("c:chart")))
10171 .map_err(OxmlError::from)?;
10172 Ok(())
10173 }
10174
10175 pub fn raw_children(&self) -> &OrderedRawChildren {
10176 &self.raw_children
10177 }
10178}
10179
10180#[derive(Default)]
10181struct ChartParseState {
10182 title: Option<CT_Title>,
10183 auto_title_deleted: Option<(bool, ScalarMarkup)>,
10184 plot_area: Option<CT_PlotArea>,
10185 legend: Option<CT_Legend>,
10186 plot_vis_only: Option<(bool, ScalarMarkup)>,
10187 disp_blanks_as: Option<(DispBlanksAs, ScalarMarkup)>,
10188 raw_children: OrderedRawChildren,
10189 boundary: usize,
10190}
10191
10192impl ChartParseState {
10193 fn capture_event(&mut self, raw: Vec<u8>) {
10194 self.raw_children.push(self.boundary, raw);
10195 }
10196
10197 fn parse_child(
10198 &mut self,
10199 name: &[u8],
10200 raw: Vec<u8>,
10201 namespaces: &NamespaceBindings,
10202 ) -> Result<()> {
10203 match name {
10204 b"title" => {
10205 set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
10206 self.boundary = self.boundary.max(1);
10207 }
10208 b"autoTitleDeleted" => {
10209 set_once(
10210 &mut self.auto_title_deleted,
10211 parse_bool_value(&raw, "autoTitleDeleted")?,
10212 "c:autoTitleDeleted",
10213 )?;
10214 self.boundary = self.boundary.max(2);
10215 }
10216 b"plotArea" => {
10217 set_once(
10218 &mut self.plot_area,
10219 CT_PlotArea::from_xml_with_namespaces(&raw, namespaces)?,
10220 "c:plotArea",
10221 )?;
10222 self.boundary = self.boundary.max(8);
10223 }
10224 b"legend" => {
10225 set_once(&mut self.legend, CT_Legend::from_xml(&raw)?, "c:legend")?;
10226 self.boundary = self.boundary.max(9);
10227 }
10228 b"plotVisOnly" => {
10229 set_once(
10230 &mut self.plot_vis_only,
10231 parse_bool_value(&raw, "plotVisOnly")?,
10232 "c:plotVisOnly",
10233 )?;
10234 self.boundary = self.boundary.max(10);
10235 }
10236 b"dispBlanksAs" => {
10237 set_once(
10238 &mut self.disp_blanks_as,
10239 parse_enum_value(&raw, "dispBlanksAs")?,
10240 "c:dispBlanksAs",
10241 )?;
10242 self.boundary = self.boundary.max(11);
10243 }
10244 _ => {
10245 self.raw_children
10246 .push(chart_raw_slot(name).unwrap_or(self.boundary), raw);
10247 self.boundary = self.boundary.max(chart_raw_boundary_after(name));
10248 }
10249 }
10250 Ok(())
10251 }
10252
10253 fn finish(self, raw_attributes: XmlAttributes) -> Result<CT_Chart> {
10254 let (auto_title_deleted, auto_title_deleted_markup) = self
10255 .auto_title_deleted
10256 .map(|(value, markup)| (value, Some(markup)))
10257 .unwrap_or((false, None));
10258 let (plot_vis_only, plot_vis_only_markup) = self
10259 .plot_vis_only
10260 .map(|(value, markup)| (value, Some(markup)))
10261 .unwrap_or((true, None));
10262 let (disp_blanks_as, disp_blanks_as_markup) = self
10263 .disp_blanks_as
10264 .map(|(value, markup)| (value, Some(markup)))
10265 .unwrap_or((DispBlanksAs::Gap, None));
10266 Ok(CT_Chart {
10267 title: self.title,
10268 auto_title_deleted,
10269 plot_area: self
10270 .plot_area
10271 .ok_or_else(|| ChartError::MissingElement("c:plotArea".to_owned()))?,
10272 legend: self.legend,
10273 plot_vis_only,
10274 disp_blanks_as,
10275 auto_title_deleted_markup,
10276 plot_vis_only_markup,
10277 disp_blanks_as_markup,
10278 raw_attributes,
10279 raw_children: raw_children_in_schema_order(&self.raw_children, 13),
10280 })
10281 }
10282}
10283
10284fn chart_raw_slot(name: &[u8]) -> Option<usize> {
10285 match name {
10286 b"pivotFmts" => Some(2),
10287 b"view3D" => Some(3),
10288 b"floor" => Some(4),
10289 b"sideWall" => Some(5),
10290 b"backWall" => Some(6),
10291 b"showDLblsOverMax" | b"extLst" => Some(11),
10294 _ => None,
10295 }
10296}
10297
10298fn chart_raw_boundary_after(name: &[u8]) -> usize {
10299 match name {
10300 b"title" => 1,
10301 b"autoTitleDeleted" => 2,
10302 b"pivotFmts" => 3,
10303 b"view3D" => 4,
10304 b"floor" => 5,
10305 b"sideWall" => 6,
10306 b"backWall" => 7,
10307 b"plotArea" => 8,
10308 b"legend" => 9,
10309 b"plotVisOnly" => 10,
10310 b"dispBlanksAs" => 11,
10311 b"showDLblsOverMax" | b"extLst" => 11,
10312 _ => 0,
10313 }
10314}
10315
10316#[derive(Clone, Debug, PartialEq)]
10318pub struct CT_ChartSpace {
10319 pub chart: CT_Chart,
10320 pub sp_pr: Option<CT_ShapeProperties>,
10321 pub tx_pr: Option<CT_TextBody>,
10322 raw_attributes: Vec<(String, String)>,
10323 namespace_declarations: Vec<(String, String)>,
10324 raw_children: OrderedRawChildren,
10325}
10326
10327impl CT_ChartSpace {
10328 pub fn from_xml(xml: &[u8]) -> Result<Self> {
10330 let mut reader = Reader::from_reader(xml);
10331 let mut buffer = Vec::new();
10332 loop {
10333 match reader
10334 .read_event_into(&mut buffer)
10335 .map_err(OxmlError::from)?
10336 {
10337 Event::Start(element)
10338 if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10339 {
10340 validate_chart_space_namespace(&element)?;
10341 return Self::from_element(&mut reader, &element);
10342 }
10343 Event::Empty(element)
10344 if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10345 {
10346 validate_chart_space_namespace(&element)?;
10347 return Err(ChartError::MissingElement("c:chart".to_owned()));
10348 }
10349 Event::Start(element) | Event::Empty(element) => {
10350 return Err(ChartError::UnexpectedElement(element_name(&element)));
10351 }
10352 Event::Eof => return Err(ChartError::MissingElement("c:chartSpace".to_owned())),
10353 _ => {}
10354 }
10355 buffer.clear();
10356 }
10357 }
10358
10359 fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
10360 let namespaces = namespace_bindings(start)?;
10361 let (raw_attributes, namespace_declarations) = capture_root_attributes(start)?;
10362 let mut state = ChartSpaceParseState::default();
10363 let mut buffer = Vec::new();
10364
10365 loop {
10366 match reader
10367 .read_event_into(&mut buffer)
10368 .map_err(OxmlError::from)?
10369 {
10370 Event::Start(element) => {
10371 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10372 local_name(element.name().as_ref()).to_vec()
10373 } else {
10374 Vec::new()
10375 };
10376 let raw = capture_element(reader, &element)?;
10377 state.parse_child(&name, raw, &namespaces)?;
10378 }
10379 Event::Empty(element) => {
10380 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10381 local_name(element.name().as_ref()).to_vec()
10382 } else {
10383 Vec::new()
10384 };
10385 let raw = capture_empty_element(&element)?;
10386 state.parse_child(&name, raw, &namespaces)?;
10387 }
10388 event @ (Event::Text(_)
10389 | Event::CData(_)
10390 | Event::Comment(_)
10391 | Event::PI(_)
10392 | Event::GeneralRef(_)) => {
10393 state.capture_event(capture_event(event)?);
10394 }
10395 Event::End(element)
10396 if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10397 {
10398 break;
10399 }
10400 Event::Eof => return Err(missing_end("c:chartSpace")),
10401 _ => {}
10402 }
10403 buffer.clear();
10404 }
10405
10406 state.finish(raw_attributes, namespace_declarations)
10407 }
10408
10409 pub fn to_xml(&self) -> Result<Vec<u8>> {
10411 let mut writer = Writer::new(Vec::new());
10412 let mut start = BytesStart::new("c:chartSpace");
10413 start.push_attribute(("xmlns:c", C_NS));
10414 start.push_attribute(("xmlns:a", A_NS));
10415 start.push_attribute(("xmlns:r", R_NS));
10416 push_attributes(&mut start, &self.namespace_declarations);
10417 push_attributes(&mut start, &self.raw_attributes);
10418 writer
10419 .write_event(Event::Start(start))
10420 .map_err(OxmlError::from)?;
10421 for raw_boundary in 0..8 {
10422 emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
10423 }
10424 self.chart.write_xml(&mut writer)?;
10425 emit_raw(&mut writer, self.raw_children.at(8))?;
10426 if let Some(properties) = &self.sp_pr {
10427 properties.write_xml_as(&mut writer, "c:spPr")?;
10428 }
10429 emit_raw(&mut writer, self.raw_children.at(9))?;
10430 if let Some(text) = &self.tx_pr {
10431 text.write_xml_as(&mut writer, "c:txPr")?;
10432 }
10433 for raw_boundary in 10..=14 {
10434 emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
10435 }
10436 writer
10437 .write_event(Event::End(BytesEnd::new("c:chartSpace")))
10438 .map_err(OxmlError::from)?;
10439 Ok(writer.into_inner())
10440 }
10441
10442 pub fn raw_children(&self) -> &OrderedRawChildren {
10443 &self.raw_children
10444 }
10445}
10446
10447#[derive(Default)]
10448struct ChartSpaceParseState {
10449 chart: Option<CT_Chart>,
10450 sp_pr: Option<CT_ShapeProperties>,
10451 tx_pr: Option<CT_TextBody>,
10452 raw_children: OrderedRawChildren,
10453 boundary: usize,
10454}
10455
10456impl ChartSpaceParseState {
10457 fn capture_event(&mut self, raw: Vec<u8>) {
10458 self.raw_children.push(self.boundary, raw);
10459 }
10460
10461 fn parse_child(
10462 &mut self,
10463 name: &[u8],
10464 raw: Vec<u8>,
10465 namespaces: &NamespaceBindings,
10466 ) -> Result<()> {
10467 match name {
10468 b"chart" => {
10469 set_once(
10470 &mut self.chart,
10471 CT_Chart::from_xml_with_namespaces(&raw, namespaces)?,
10472 "c:chart",
10473 )?;
10474 self.boundary = self.boundary.max(8);
10475 }
10476 b"spPr" => {
10477 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
10478 let properties = CT_ShapeProperties::from_xml(&raw)?;
10479 let mut writer = Writer::new(Vec::new());
10480 properties.write_xml_as(&mut writer, "c:spPr")?;
10481 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
10482 set_once(&mut self.sp_pr, properties, "c:spPr")?;
10483 self.boundary = self.boundary.max(9);
10484 }
10485 b"txPr" => {
10486 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
10487 let text = CT_TextBody::from_xml_as(&raw, b"txPr")?;
10488 let mut writer = Writer::new(Vec::new());
10489 text.write_xml_as(&mut writer, "c:txPr")?;
10490 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
10491 set_once(&mut self.tx_pr, text, "c:txPr")?;
10492 self.boundary = self.boundary.max(10);
10493 }
10494 _ => {
10495 self.raw_children
10496 .push(chart_space_raw_slot(name).unwrap_or(self.boundary), raw);
10497 self.boundary = self.boundary.max(chart_space_raw_boundary_after(name));
10498 }
10499 }
10500 Ok(())
10501 }
10502
10503 fn finish(
10504 self,
10505 raw_attributes: XmlAttributes,
10506 namespace_declarations: XmlAttributes,
10507 ) -> Result<CT_ChartSpace> {
10508 Ok(CT_ChartSpace {
10509 chart: self
10510 .chart
10511 .ok_or_else(|| ChartError::MissingElement("c:chart".to_owned()))?,
10512 sp_pr: self.sp_pr,
10513 tx_pr: self.tx_pr,
10514 raw_attributes,
10515 namespace_declarations,
10516 raw_children: raw_children_in_schema_order(&self.raw_children, 14),
10517 })
10518 }
10519}
10520
10521fn raw_children_in_schema_order(
10522 raw_children: &OrderedRawChildren,
10523 final_boundary: usize,
10524) -> OrderedRawChildren {
10525 let mut ordered = OrderedRawChildren::default();
10526 for boundary in 0..=final_boundary {
10527 for raw in raw_children.at(boundary) {
10528 ordered.push(boundary, raw.to_vec());
10529 }
10530 }
10531 ordered
10532}
10533
10534fn chart_space_raw_slot(name: &[u8]) -> Option<usize> {
10535 match name {
10536 b"date1904" => Some(0),
10537 b"lang" => Some(1),
10538 b"roundedCorners" => Some(2),
10539 b"style" => Some(3),
10540 b"clrMapOvr" => Some(4),
10541 b"pivotSource" => Some(5),
10542 b"protection" => Some(6),
10543 b"externalData" => Some(10),
10544 b"printSettings" => Some(11),
10545 b"userShapes" => Some(12),
10546 b"extLst" => Some(13),
10547 _ => None,
10548 }
10549}
10550
10551fn chart_space_raw_boundary_after(name: &[u8]) -> usize {
10552 match name {
10553 b"date1904" => 1,
10554 b"lang" => 2,
10555 b"roundedCorners" => 3,
10556 b"style" => 4,
10557 b"clrMapOvr" => 5,
10558 b"pivotSource" => 6,
10559 b"protection" => 7,
10560 b"chart" => 8,
10561 b"spPr" => 9,
10562 b"txPr" => 10,
10563 b"externalData" => 11,
10564 b"printSettings" => 12,
10565 b"userShapes" => 13,
10566 b"extLst" => 14,
10567 _ => 0,
10568 }
10569}
10570
10571fn parse_bool_value(xml: &[u8], local: &str) -> Result<(bool, ScalarMarkup)> {
10572 let (value, markup) = scalar_value(xml, local)?;
10573 let value = match value.as_deref().unwrap_or("1") {
10574 "1" | "true" => true,
10575 "0" | "false" => false,
10576 invalid => return Err(invalid_attribute(local, "val", invalid.to_owned())),
10577 };
10578 Ok((value, markup))
10579}
10580
10581fn parse_enum_value(xml: &[u8], local: &str) -> Result<(DispBlanksAs, ScalarMarkup)> {
10582 let (value, markup) = scalar_value(xml, local)?;
10583 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
10584 let parsed =
10585 DispBlanksAs::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
10586 Ok((parsed, markup))
10587}
10588
10589fn scalar_value(xml: &[u8], expected: &str) -> Result<(Option<String>, ScalarMarkup)> {
10590 let mut reader = Reader::from_reader(xml);
10591 let mut buffer = Vec::new();
10592 loop {
10593 match reader
10594 .read_event_into(&mut buffer)
10595 .map_err(OxmlError::from)?
10596 {
10597 Event::Empty(element)
10598 if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
10599 {
10600 reject_conflicting_prefix(&element, b"c", C_NS)?;
10601 return Ok((
10602 attribute_value(&element, b"val")?,
10603 ScalarMarkup {
10604 raw_attributes: capture_attributes_except(&element, b"val")?,
10605 raw_content: Vec::new(),
10606 },
10607 ));
10608 }
10609 Event::Start(element)
10610 if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
10611 {
10612 reject_conflicting_prefix(&element, b"c", C_NS)?;
10613 let value = attribute_value(&element, b"val")?;
10614 let raw_attributes = capture_attributes_except(&element, b"val")?;
10615 let mut raw_content = Vec::new();
10616 let mut inner = Vec::new();
10617 loop {
10618 match reader
10619 .read_event_into(&mut inner)
10620 .map_err(OxmlError::from)?
10621 {
10622 Event::End(end)
10623 if matches_local_name(end.name().as_ref(), expected.as_bytes()) =>
10624 {
10625 return Ok((
10626 value,
10627 ScalarMarkup {
10628 raw_attributes,
10629 raw_content,
10630 },
10631 ));
10632 }
10633 event @ (Event::Text(_)
10634 | Event::CData(_)
10635 | Event::Comment(_)
10636 | Event::PI(_)
10637 | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
10638 Event::Eof => return Err(missing_end(expected)),
10639 _ => return Err(ChartError::UnexpectedElement(expected.to_owned())),
10640 }
10641 inner.clear();
10642 }
10643 }
10644 Event::Start(element) | Event::Empty(element) => {
10645 return Err(ChartError::UnexpectedElement(element_name(&element)));
10646 }
10647 Event::Eof => return Err(ChartError::MissingElement(expected.to_owned())),
10648 _ => {}
10649 }
10650 buffer.clear();
10651 }
10652}
10653
10654fn set_once<T>(slot: &mut Option<T>, value: T, name: &str) -> Result<()> {
10655 if slot.is_some() {
10656 return Err(ChartError::DuplicateElement(name.to_owned()));
10657 }
10658 *slot = Some(value);
10659 Ok(())
10660}
10661
10662fn mark_once(seen: &mut bool, name: &str) -> Result<()> {
10663 if *seen {
10664 return Err(ChartError::DuplicateElement(name.to_owned()));
10665 }
10666 *seen = true;
10667 Ok(())
10668}
10669
10670fn validate_chart_space_namespace(element: &BytesStart<'_>) -> Result<()> {
10671 let name = element.name();
10672 let qualified = name.as_ref();
10673 let declaration = match qualified.iter().position(|byte| *byte == b':') {
10674 Some(position) => {
10675 let mut declaration = b"xmlns:".to_vec();
10676 declaration.extend_from_slice(&qualified[..position]);
10677 declaration
10678 }
10679 None => b"xmlns".to_vec(),
10680 };
10681 let namespace = attribute_value(element, &declaration)?
10682 .ok_or_else(|| invalid_attribute("chartSpace", "namespace", "<missing>".to_owned()))?;
10683 if namespace != C_NS {
10684 return Err(invalid_attribute("chartSpace", "namespace", namespace));
10685 }
10686 reject_conflicting_prefix(element, b"c", C_NS)?;
10687 reject_conflicting_prefix(element, b"a", A_NS)?;
10688 reject_conflicting_prefix(element, b"r", R_NS)
10689}
10690
10691fn reject_conflicting_prefix_in_xml(xml: &[u8], prefix: &[u8], namespace: &str) -> Result<()> {
10692 let mut reader = Reader::from_reader(xml);
10693 let mut buffer = Vec::new();
10694 loop {
10695 match reader
10696 .read_event_into(&mut buffer)
10697 .map_err(OxmlError::from)?
10698 {
10699 Event::Start(element) | Event::Empty(element) => {
10700 return reject_conflicting_prefix(&element, prefix, namespace);
10701 }
10702 Event::Eof => return Err(ChartError::MissingElement("XML root".to_owned())),
10703 _ => {}
10704 }
10705 buffer.clear();
10706 }
10707}
10708
10709fn reject_rewritten_foreign_elements(
10710 original: &[u8],
10711 written: &[u8],
10712 inherited: &NamespaceBindings,
10713 root_local: &[u8],
10714) -> Result<()> {
10715 let original = foreign_element_regions(original, inherited, root_local)?;
10716 let mut written_namespaces = inherited.clone();
10717 written_namespaces.retain(|(prefix, _)| prefix != b"a");
10718 written_namespaces.push((b"a".to_vec(), A_NS.to_owned()));
10719 let written = foreign_element_regions(written, &written_namespaces, root_local)?;
10720 if original != written {
10721 return Err(invalid_attribute(
10722 &String::from_utf8_lossy(root_local),
10723 "namespace",
10724 "foreign DrawingML content would be converted to typed content".to_owned(),
10725 ));
10726 }
10727 Ok(())
10728}
10729
10730fn foreign_element_regions(
10731 xml: &[u8],
10732 inherited: &NamespaceBindings,
10733 root_local: &[u8],
10734) -> Result<Vec<Vec<u8>>> {
10735 let mut reader = Reader::from_reader(xml);
10736 let mut buffer = Vec::new();
10737 let root_namespaces = loop {
10738 match reader
10739 .read_event_into(&mut buffer)
10740 .map_err(OxmlError::from)?
10741 {
10742 Event::Start(element) if matches_local_name(element.name().as_ref(), root_local) => {
10743 break bindings_with_local(inherited, &element)?;
10744 }
10745 Event::Empty(element) if matches_local_name(element.name().as_ref(), root_local) => {
10746 return Ok(Vec::new());
10747 }
10748 Event::Start(element) | Event::Empty(element) => {
10749 return Err(ChartError::UnexpectedElement(element_name(&element)));
10750 }
10751 Event::Eof => {
10752 return Err(ChartError::MissingElement(
10753 String::from_utf8_lossy(root_local).into_owned(),
10754 ));
10755 }
10756 _ => {}
10757 }
10758 buffer.clear();
10759 };
10760
10761 let mut namespace_stack = vec![root_namespaces];
10762 let mut foreign = Vec::new();
10763 let mut child_buffer = Vec::new();
10764 loop {
10765 match reader
10766 .read_event_into(&mut child_buffer)
10767 .map_err(OxmlError::from)?
10768 {
10769 Event::Start(element) => {
10770 let namespaces = namespace_stack.last().expect("root namespace binding");
10771 if element_is_in_namespace(&element, A_NS, namespaces)? {
10772 namespace_stack.push(bindings_with_local(namespaces, &element)?);
10773 } else {
10774 foreign.push(capture_element(&mut reader, &element)?);
10775 }
10776 }
10777 Event::Empty(element) => {
10778 let namespaces = namespace_stack.last().expect("root namespace binding");
10779 if !element_is_in_namespace(&element, A_NS, namespaces)? {
10780 foreign.push(capture_empty_element(&element)?);
10781 }
10782 }
10783 Event::End(element) if matches_local_name(element.name().as_ref(), root_local) => {
10784 return Ok(foreign);
10785 }
10786 Event::End(_) => {
10787 namespace_stack.pop();
10788 }
10789 Event::Eof => {
10790 return Err(missing_end(&String::from_utf8_lossy(root_local)));
10791 }
10792 _ => {}
10793 }
10794 child_buffer.clear();
10795 }
10796}
10797
10798fn reject_conflicting_prefix(
10799 element: &BytesStart<'_>,
10800 prefix: &[u8],
10801 namespace: &str,
10802) -> Result<()> {
10803 let mut key = b"xmlns:".to_vec();
10804 key.extend_from_slice(prefix);
10805 if let Some(value) = attribute_value(element, &key)?
10806 && value != namespace
10807 {
10808 return Err(invalid_attribute(
10809 &element_name(element),
10810 &String::from_utf8_lossy(&key),
10811 value,
10812 ));
10813 }
10814 Ok(())
10815}
10816
10817fn namespace_bindings(element: &BytesStart<'_>) -> Result<NamespaceBindings> {
10818 let mut bindings = Vec::new();
10819 for attribute in element.attributes() {
10820 let attribute = attribute.map_err(OxmlError::from)?;
10821 let key = attribute.key.as_ref();
10822 let prefix = if key == b"xmlns" {
10823 Vec::new()
10824 } else if let Some(prefix) = key.strip_prefix(b"xmlns:") {
10825 prefix.to_vec()
10826 } else {
10827 continue;
10828 };
10829 let namespace = attribute
10830 .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
10831 .map_err(OxmlError::from)?
10832 .into_owned();
10833 bindings.push((prefix, namespace));
10834 }
10835 Ok(bindings)
10836}
10837
10838fn bindings_with_local(
10839 inherited: &NamespaceBindings,
10840 element: &BytesStart<'_>,
10841) -> Result<NamespaceBindings> {
10842 let mut bindings = inherited.clone();
10843 for (prefix, namespace) in namespace_bindings(element)? {
10844 upsert_namespace_binding(&mut bindings, prefix, namespace);
10845 }
10846 Ok(bindings)
10847}
10848
10849fn upsert_namespace_binding(bindings: &mut NamespaceBindings, prefix: Vec<u8>, namespace: String) {
10850 if let Some((_, current)) = bindings.iter_mut().find(|(current, _)| current == &prefix) {
10851 *current = namespace;
10852 } else {
10853 bindings.push((prefix, namespace));
10854 }
10855}
10856
10857fn element_is_in_namespace(
10858 element: &BytesStart<'_>,
10859 expected: &str,
10860 inherited: &NamespaceBindings,
10861) -> Result<bool> {
10862 let name = element.name();
10863 let qualified = name.as_ref();
10864 let prefix = qualified
10865 .iter()
10866 .position(|byte| *byte == b':')
10867 .map(|position| &qualified[..position])
10868 .unwrap_or_default();
10869 let local = namespace_bindings(element)?;
10870 let namespace = local
10871 .iter()
10872 .rev()
10873 .chain(inherited.iter().rev())
10874 .find(|(candidate, _)| candidate.as_slice() == prefix)
10875 .map(|(_, namespace)| namespace.as_str());
10876 Ok(namespace == Some(expected))
10877}
10878
10879fn attribute_value(element: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
10880 for attribute in element.attributes() {
10881 let attribute = attribute.map_err(OxmlError::from)?;
10882 if attribute.key.as_ref() == name {
10883 return Ok(Some(
10884 attribute
10885 .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
10886 .map_err(OxmlError::from)?
10887 .into_owned(),
10888 ));
10889 }
10890 }
10891 Ok(None)
10892}
10893
10894fn capture_root_attributes(start: &BytesStart<'_>) -> Result<RootAttributes> {
10895 let mut attributes = Vec::new();
10896 let mut namespaces = Vec::new();
10897 for attribute in start.attributes() {
10898 let attribute = attribute.map_err(OxmlError::from)?;
10899 let name = std::str::from_utf8(attribute.key.as_ref())
10900 .map_err(OxmlError::from)?
10901 .to_owned();
10902 let value = attribute
10903 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10904 .map_err(OxmlError::from)?
10905 .into_owned();
10906 if name == "xmlns:c" || name == "xmlns:a" || name == "xmlns:r" {
10907 continue;
10908 }
10909 if name == "xmlns" || name.starts_with("xmlns:") {
10910 namespaces.push((name, value));
10911 } else {
10912 attributes.push((name, value));
10913 }
10914 }
10915 Ok((attributes, namespaces))
10916}
10917
10918fn capture_attributes(start: &BytesStart<'_>) -> Result<XmlAttributes> {
10919 let mut attributes = Vec::new();
10920 for attribute in start.attributes() {
10921 let attribute = attribute.map_err(OxmlError::from)?;
10922 attributes.push((
10923 std::str::from_utf8(attribute.key.as_ref())
10924 .map_err(OxmlError::from)?
10925 .to_owned(),
10926 attribute
10927 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10928 .map_err(OxmlError::from)?
10929 .into_owned(),
10930 ));
10931 }
10932 Ok(attributes)
10933}
10934
10935fn capture_attributes_except(start: &BytesStart<'_>, excluded: &[u8]) -> Result<XmlAttributes> {
10936 let mut attributes = Vec::new();
10937 for attribute in start.attributes() {
10938 let attribute = attribute.map_err(OxmlError::from)?;
10939 if attribute.key.as_ref() == excluded {
10940 continue;
10941 }
10942 attributes.push((
10943 std::str::from_utf8(attribute.key.as_ref())
10944 .map_err(OxmlError::from)?
10945 .to_owned(),
10946 attribute
10947 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10948 .map_err(OxmlError::from)?
10949 .into_owned(),
10950 ));
10951 }
10952 Ok(attributes)
10953}
10954
10955fn capture_attributes_excluding(
10956 start: &BytesStart<'_>,
10957 excluded: &[&[u8]],
10958) -> Result<XmlAttributes> {
10959 let mut attributes = Vec::new();
10960 for attribute in start.attributes() {
10961 let attribute = attribute.map_err(OxmlError::from)?;
10962 if excluded.contains(&attribute.key.as_ref()) {
10963 continue;
10964 }
10965 attributes.push((
10966 std::str::from_utf8(attribute.key.as_ref())
10967 .map_err(OxmlError::from)?
10968 .to_owned(),
10969 attribute
10970 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10971 .map_err(OxmlError::from)?
10972 .into_owned(),
10973 ));
10974 }
10975 Ok(attributes)
10976}
10977
10978fn push_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
10979 for (name, value) in attributes {
10980 start.push_attribute((name.as_str(), value.as_str()));
10981 }
10982}
10983
10984fn write_scalar<W: Write>(
10985 writer: &mut Writer<W>,
10986 name: &str,
10987 value: &str,
10988 markup: Option<&ScalarMarkup>,
10989) -> Result<()> {
10990 let mut start = BytesStart::new(name);
10991 start.push_attribute(("val", value));
10992 if let Some(markup) = markup {
10993 push_attributes(&mut start, &markup.raw_attributes);
10994 }
10995 let has_content = markup.is_some_and(|markup| !markup.raw_content.is_empty());
10996 if !has_content {
10997 writer
10998 .write_event(Event::Empty(start))
10999 .map_err(OxmlError::from)?;
11000 return Ok(());
11001 }
11002 writer
11003 .write_event(Event::Start(start))
11004 .map_err(OxmlError::from)?;
11005 if let Some(markup) = markup {
11006 for raw in &markup.raw_content {
11007 writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
11008 }
11009 }
11010 writer
11011 .write_event(Event::End(BytesEnd::new(name)))
11012 .map_err(OxmlError::from)?;
11013 Ok(())
11014}
11015
11016fn capture_event(event: Event<'_>) -> Result<Vec<u8>> {
11017 let mut writer = Writer::new(Vec::new());
11018 writer
11019 .write_event(event.into_owned())
11020 .map_err(OxmlError::from)?;
11021 Ok(writer.into_inner())
11022}
11023
11024fn emit_raw<'a, W: Write>(
11025 writer: &mut Writer<W>,
11026 children: impl Iterator<Item = &'a [u8]>,
11027) -> Result<()> {
11028 for child in children {
11029 writer.get_mut().write_all(child).map_err(OxmlError::from)?;
11030 }
11031 Ok(())
11032}
11033
11034fn invalid_attribute(element: &str, attribute: &str, value: String) -> ChartError {
11035 ChartError::InvalidAttribute {
11036 element: element.to_owned(),
11037 attribute: attribute.to_owned(),
11038 value,
11039 }
11040}
11041
11042fn element_name(element: &BytesStart<'_>) -> String {
11043 String::from_utf8_lossy(element.name().as_ref()).into_owned()
11044}
11045
11046fn missing_end(element: &str) -> ChartError {
11047 ChartError::Xml(OxmlError::MissingElement(format!("closing {element}")))
11048}
11049
11050#[cfg(test)]
11051mod tests {
11052 use std::collections::HashSet;
11053 use std::fs;
11054 use std::path::{Path, PathBuf};
11055 use std::process::Command;
11056
11057 use oxml_core::raw_xml::{capture_element, capture_empty_element};
11058 use oxml_drawing::color::{ColorMap, ColorMapSlot, RgbColor, ThemeColorSlot};
11059 use oxml_drawing::theme::CT_OfficeStyleSheet;
11060 use oxml_layout::{
11061 Color, FontManager, LayoutResult, PageFrame, Paint, PathCommand, Point, PositionedElement,
11062 Rect,
11063 };
11064 use oxml_opc::OpcPackage;
11065 use quick_xml::Reader;
11066 use quick_xml::events::Event;
11067
11068 use super::{
11069 A_NS, Axis, AxisData, AxisId, AxisKind, AxisPosition, BarDirection, BarGrouping, C_NS,
11070 CT_ChartSpace, CT_DLbls, CT_ShapeProperties, CT_TextBody, ChartData, ChartGeometry,
11071 ChartKind, DataLabelPosition, DispBlanksAs, Domain, Grouping, NumberFormat, NumericData,
11072 Orientation, Plot, R_NS, ScatterStyle, Series, StringRef, TickLabelPosition, TickMark,
11073 authored_chart_parts, capture_event, local_name, matches_local_name, nice_number_scale,
11074 render_chart as render_chart_with_theme, render_geometry as render_geometry_with_theme,
11075 };
11076
11077 const MANIFEST: &str = include_str!("../../../scripts/pptx-corpus-manifest.tsv");
11078 const EXPECTED_DECKS: usize = 50;
11079 const LIBREOFFICE_VERSION: &str =
11080 "LibreOffice 26.2.5.2 cd7284b4cbbfeb507e630c1aac019f4157393acb";
11081 const PDFTOTEXT_VERSION: &str = "pdftotext version 26.01.0";
11082 const PDFTOPPM_VERSION: &str = "pdftoppm version 26.01.0";
11083 const PLOT_RENDER_NORMALIZED_MAE_THRESHOLD: f64 = 0.0;
11084
11085 #[test]
11086 fn authored_charts_emit_portable_viewer_defaults() {
11087 let data = ChartData {
11088 categories: vec!["August".to_owned(), "September".to_owned()],
11089 series: vec![("Price".to_owned(), vec![12.5, 19.2])],
11090 number_format: Some(r"0.##\%".to_owned()),
11091 category_axis_title: Some("Month".to_owned()),
11092 value_axis_title: Some("Change".to_owned()),
11093 palette: vec![
11094 RgbColor::parse("2B6FE3").unwrap(),
11095 RgbColor::parse("F0761F").unwrap(),
11096 ],
11097 };
11098 let (line, line_workbook) = authored_chart_parts(ChartKind::Line, &data, "rId1").unwrap();
11099 let line = std::str::from_utf8(&line).unwrap();
11100 assert_eq!(line.matches(r#"<c:delete val="0"/>"#).count(), 2);
11101 assert!(line.contains(r#"<c:numFmt formatCode="0.##\%" sourceLinked="0"/>"#));
11102 assert!(line.contains("<a:t>Month</a:t>"));
11103 assert!(line.contains("<a:t>Change</a:t>"));
11104 assert!(line.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
11105 let assert_portable_line = |xml: &str| {
11106 let titles = xml
11107 .split("<c:title>")
11108 .skip(1)
11109 .map(|tail| tail.split_once("</c:title>").unwrap().0)
11110 .collect::<Vec<_>>();
11111 assert_eq!(titles.len(), 2);
11112 for title in titles {
11113 let text = title.find("<c:tx>").unwrap();
11114 let layout = title.find("<c:layout/>").unwrap();
11115 let overlay = title.find(r#"<c:overlay val="0"/>"#).unwrap();
11116 assert!(text < layout && layout < overlay);
11117 }
11118
11119 let line_plot = xml
11120 .split_once("<c:lineChart>")
11121 .unwrap()
11122 .1
11123 .split_once("</c:lineChart>")
11124 .unwrap()
11125 .0;
11126 assert_eq!(line_plot.matches(r#"<c:marker val="0"/>"#).count(), 1);
11127 assert_eq!(line_plot.matches(r#"<c:smooth val="0"/>"#).count(), 1);
11128 let marker = line_plot.find(r#"<c:marker val="0"/>"#).unwrap();
11129 let smooth = line_plot.find(r#"<c:smooth val="0"/>"#).unwrap();
11130 let axis_id = line_plot.find("<c:axId").unwrap();
11131 assert!(marker < smooth && smooth < axis_id);
11132 };
11133 assert_portable_line(line);
11134 for axis in line.split("<c:crossAx").take(2) {
11135 let scaling = axis.rfind("<c:scaling").unwrap();
11136 let deleted = axis.rfind("<c:delete").unwrap();
11137 let position = axis.rfind("<c:axPos").unwrap();
11138 assert!(scaling < deleted && deleted < position);
11139 }
11140
11141 let parsed = CT_ChartSpace::from_xml(line.as_bytes()).unwrap();
11142 let rewritten = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
11143 assert_portable_line(&rewritten);
11144 assert!(rewritten.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
11145 assert!(rewritten.contains(r#"<c:externalData r:id="rId1">"#));
11146
11147 for kind in [
11148 ChartKind::Bar,
11149 ChartKind::Pie,
11150 ChartKind::Doughnut,
11151 ChartKind::Area,
11152 ChartKind::Scatter,
11153 ChartKind::Radar,
11154 ] {
11155 let mut kind_data = data.clone();
11156 if kind == ChartKind::Scatter {
11157 kind_data.categories = vec!["1".to_owned(), "2".to_owned()];
11158 }
11159 let (chart, workbook) = authored_chart_parts(kind, &kind_data, "rId1").unwrap();
11160 let chart = std::str::from_utf8(&chart).unwrap();
11161 assert!(!chart.contains(r#"<c:marker val="0"/>"#));
11162 assert!(!chart.contains(r#"<c:smooth val="0"/>"#));
11163 if kind != ChartKind::Scatter {
11164 assert_eq!(workbook, line_workbook);
11165 }
11166 if matches!(kind, ChartKind::Pie | ChartKind::Doughnut) {
11167 assert!(chart.contains("<c:legend"));
11168 assert!(chart.contains(r#"<c:showPercent val="1"/>"#));
11169 }
11170 if kind == ChartKind::Doughnut {
11171 assert!(chart.contains(r#"<c:holeSize val="50"/>"#));
11172 }
11173 if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) {
11174 assert_eq!(chart.matches("<c:dPt>").count(), 2);
11175 assert!(chart.contains(
11176 r#"<c:dPt><c:idx val="0"/><c:spPr><a:solidFill><a:srgbClr val="2B6FE3"/>"#
11177 ));
11178 assert!(chart.contains(
11179 r#"<c:dPt><c:idx val="1"/><c:spPr><a:solidFill><a:srgbClr val="F0761F"/>"#
11180 ));
11181 }
11182
11183 let parsed = CT_ChartSpace::from_xml(chart.as_bytes()).unwrap();
11184 let rewritten = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
11185 assert_eq!(
11186 rewritten.matches("<c:dPt>").count(),
11187 usize::from(matches!(
11188 kind,
11189 ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut
11190 )) * 2
11191 );
11192 assert!(rewritten.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
11193 if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) {
11194 assert!(rewritten.contains(r#"<a:srgbClr val="F0761F"/>"#));
11195 }
11196 }
11197 }
11198
11199 #[test]
11200 fn optional_numeric_cache_format_remains_omitted() {
11201 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>"#;
11202 let mut parsed = NumericData::from_xml(xml).unwrap();
11203 assert_eq!(parsed.format_code, "General");
11204 let written = parsed.to_xml().unwrap();
11205 assert!(!String::from_utf8_lossy(&written).contains("formatCode"));
11206 assert_eq!(NumericData::from_xml(&written).unwrap(), parsed);
11207
11208 parsed.format_code = "0.00".to_owned();
11209 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
11210 assert!(written.contains("<c:formatCode>0.00</c:formatCode>"));
11211 }
11212
11213 fn render_geometry(chart: &super::CT_Chart, bounds: Rect) -> super::Result<ChartGeometry> {
11214 render_geometry_with_theme(
11215 chart,
11216 bounds,
11217 &CT_OfficeStyleSheet::office_default(),
11218 &ColorMap::default(),
11219 )
11220 }
11221
11222 fn render_chart(
11223 chart: &super::CT_Chart,
11224 bounds: Rect,
11225 fonts: &mut FontManager,
11226 ) -> super::Result<oxml_layout::GroupElement> {
11227 render_chart_with_theme(
11228 chart,
11229 bounds,
11230 &CT_OfficeStyleSheet::office_default(),
11231 &ColorMap::default(),
11232 fonts,
11233 )
11234 }
11235
11236 #[test]
11237 fn bar_chart_rasterises_at_computed_positions() {
11238 let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11239 let geometry = render_geometry(
11240 &chart.chart,
11241 Rect {
11242 x: 0.0,
11243 y: 0.0,
11244 width: 200.0,
11245 height: 140.0,
11246 },
11247 )
11248 .unwrap();
11249 assert_eq!(
11250 geometry.plot_bounds,
11251 Rect {
11252 x: 36.0,
11253 y: 12.0,
11254 width: 152.0,
11255 height: 100.0,
11256 }
11257 );
11258 let bounds = path_bounds(&geometry);
11259 assert_eq!(bounds.len(), 2);
11260 assert_rect_near(
11261 bounds[0],
11262 Rect {
11263 x: 58.8,
11264 y: 62.0,
11265 width: 30.4,
11266 height: 50.0,
11267 },
11268 );
11269 assert_rect_near(
11270 bounds[1],
11271 Rect {
11272 x: 134.8,
11273 y: 12.0,
11274 width: 30.4,
11275 height: 100.0,
11276 },
11277 );
11278
11279 let layout = LayoutResult::new(
11280 vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
11281 Vec::new(),
11282 None,
11283 Vec::new(),
11284 );
11285 let png = oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap();
11286 let pixmap = tiny_skia::Pixmap::decode_png(&png).unwrap();
11287 for (x, y) in [(74, 87), (150, 37)] {
11288 let pixel = pixmap.pixel(x, y).unwrap();
11289 assert_ne!((pixel.red(), pixel.green(), pixel.blue()), (255, 255, 255));
11290 }
11291 }
11292
11293 #[test]
11294 fn unstyled_four_series_use_accent_one_through_four() {
11295 let chart = four_series_bar_chart();
11296 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11297 let colours = path_colours(&geometry);
11298
11299 assert_eq!(
11300 &colours[..4],
11301 &[
11302 Color::from_hex("156082"),
11303 Color::from_hex("E97132"),
11304 Color::from_hex("196B24"),
11305 Color::from_hex("0F9ED5"),
11306 ]
11307 );
11308 }
11309
11310 #[test]
11311 fn direct_series_solid_colour_overrides_theme_accent() {
11312 for (paint, expected) in [
11313 (
11314 "<a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill>",
11315 Color::from_hex("123456"),
11316 ),
11317 (
11318 "<a:ln><a:solidFill><a:srgbClr val=\"654321\"/></a:solidFill></a:ln>",
11319 Color::from_hex("654321"),
11320 ),
11321 (
11322 "<a:noFill/>",
11323 Color {
11324 r: 0.0,
11325 g: 0.0,
11326 b: 0.0,
11327 a: 0.0,
11328 },
11329 ),
11330 ] {
11331 let styled =
11332 plot_series(0).replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
11333 let plot = bar_plot("").replace(&plot_series(0), &styled);
11334 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11335 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11336
11337 assert_eq!(path_colours(&geometry)[0], expected);
11338 }
11339
11340 let styled = plot_series(0).replace(
11341 "<q:cat>",
11342 "<q:spPr><a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill></q:spPr><q:cat>",
11343 );
11344 let plot = bar_plot("").replace(&plot_series(0), &styled);
11345 let xml = chart_with_plot(&plot).replace(
11346 "</q:plotArea></q:chart>",
11347 "</q:plotArea><q:legend/></q:chart>",
11348 );
11349 let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
11350 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
11351 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
11352 assert!(group.children.iter().any(|element| matches!(
11353 element,
11354 PositionedElement::Path(path)
11355 if path.fill == Some(Paint::Solid(Color::from_hex("123456")))
11356 )));
11357
11358 let mut theme = CT_OfficeStyleSheet::office_default();
11359 let preset = oxml_drawing::fill::Fill::from_xml(
11360 br#"<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:prstClr val="red"/></a:solidFill>"#,
11361 )
11362 .unwrap();
11363 let oxml_drawing::fill::Fill::Solid(preset) = preset else {
11364 panic!("expected solid fill");
11365 };
11366 theme.theme_elements.color_scheme.hyperlink = preset.color.unwrap();
11367 let geometry =
11368 render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &ColorMap::default())
11369 .unwrap();
11370 assert_eq!(path_colours(&geometry)[0], Color::from_hex("123456"));
11371
11372 for (paint, expected_alpha) in [
11373 ("<a:noFill/>", 0.0),
11374 (
11375 "<a:solidFill><a:srgbClr val=\"123456\"><a:alpha val=\"50000\"/></a:srgbClr></a:solidFill>",
11376 (128.0 / 255.0) * 0.55,
11377 ),
11378 ] {
11379 let (_, area, _) = remaining_plot_fixtures()
11380 .into_iter()
11381 .find(|(kind, _, _)| *kind == "areaChart")
11382 .unwrap();
11383 let styled_area = area.replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
11384 let chart =
11385 CT_ChartSpace::from_xml(chart_with_optional_axes(&styled_area, true).as_bytes())
11386 .unwrap();
11387 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11388 let actual_alpha = path_colours(&geometry)[0].a;
11389
11390 assert!((actual_alpha - expected_alpha).abs() < 1.0e-12);
11391 }
11392 }
11393
11394 #[test]
11395 fn series_accent_cycle_repeats_after_six() {
11396 let chart = seven_series_bar_chart();
11397 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11398 let colours = path_colours(&geometry);
11399
11400 assert_eq!(colours[0], Color::from_hex("156082"));
11401 assert_eq!(colours[6], colours[0]);
11402 }
11403
11404 #[test]
11405 fn series_colours_honor_colour_map_and_transform_order() {
11406 let styled = plot_series(0).replace(
11407 "<q:cat>",
11408 "<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>",
11409 );
11410 let plot = bar_plot("").replace(&plot_series(0), &styled);
11411 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11412 let color_map =
11413 ColorMap::default().with_overrides(&[(ColorMapSlot::Accent1, ThemeColorSlot::Accent4)]);
11414 let mut theme = CT_OfficeStyleSheet::office_default();
11415 let theme_fill = oxml_drawing::fill::Fill::from_xml(
11416 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>"#,
11417 )
11418 .unwrap();
11419 let oxml_drawing::fill::Fill::Solid(theme_fill) = theme_fill else {
11420 panic!("expected solid fill");
11421 };
11422 let theme_colour = theme_fill.color.unwrap();
11423 theme.theme_elements.color_scheme.accent4 = theme_colour.clone();
11424 let geometry =
11425 render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &color_map).unwrap();
11426 let series_fill = oxml_drawing::fill::Fill::from_xml(
11427 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>"#,
11428 )
11429 .unwrap();
11430 let oxml_drawing::fill::Fill::Solid(series_fill) = series_fill else {
11431 panic!("expected solid fill");
11432 };
11433 let mut transforms = theme_colour.transforms().to_vec();
11434 transforms.extend_from_slice(series_fill.color.as_ref().unwrap().transforms());
11435 let expected = oxml_drawing::color::apply_color_transforms(
11436 RgbColor::parse("336699").unwrap(),
11437 &transforms,
11438 );
11439
11440 assert_eq!(path_colours(&geometry)[0], resolved_layout_colour(expected));
11441 }
11442
11443 #[test]
11444 fn chart_colours_do_not_use_word_tint_shade() {
11445 let styled = plot_series(0).replace(
11446 "<q:cat>",
11447 "<q:spPr><a:solidFill><a:srgbClr val=\"808080\"><a:tint val=\"50000\"/></a:srgbClr></a:solidFill></q:spPr><q:cat>",
11448 );
11449 let plot = bar_plot("").replace(&plot_series(0), &styled);
11450 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11451 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11452
11453 assert_eq!(
11454 path_colours(&geometry)[0],
11455 Color {
11456 r: 205.0 / 255.0,
11457 g: 205.0 / 255.0,
11458 b: 205.0 / 255.0,
11459 a: 1.0,
11460 }
11461 );
11462 }
11463
11464 #[test]
11465 fn unsupported_direct_series_paint_is_contextual() {
11466 let styled = plot_series(0).replace(
11467 "<q:cat>",
11468 "<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>",
11469 );
11470 let plot = bar_plot("").replace(&plot_series(0), &styled);
11471 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11472 let error = render_geometry(&chart.chart, chart_bounds()).unwrap_err();
11473
11474 assert!(error.to_string().contains("c:ser[0] direct fill paint"));
11475 assert!(error.to_string().contains("gradient"));
11476 }
11477
11478 #[test]
11479 fn resolved_chart_palette_raster_is_deterministic() {
11480 let raster = || {
11481 let chart = four_series_bar_chart();
11482 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
11483 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
11484 let layout = LayoutResult::new(
11485 vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
11486 fonts.all_font_data(),
11487 None,
11488 Vec::new(),
11489 );
11490 oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
11491 };
11492 let chart = four_series_bar_chart();
11493 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11494
11495 assert_eq!(path_colours(&geometry)[0], Color::from_hex("156082"));
11496 assert_eq!(raster(), raster());
11497 }
11498
11499 #[test]
11500 fn bar_geometry_handles_direction_grouping_gap_and_overlap() {
11501 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11502 let Plot::Bar {
11503 overlap, series, ..
11504 } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11505 else {
11506 panic!("expected bar plot");
11507 };
11508 let mut second = series[0].clone();
11509 second.index = 1;
11510 second.order = 1;
11511 second.values.values = vec![2.0, 1.0];
11512 series.push(second);
11513
11514 *overlap = 25;
11515 let clustered = render_geometry(&chart.chart, chart_bounds()).unwrap();
11516 let clustered_bounds = path_bounds(&clustered);
11517 let clustered_width = 30.4 / 1.75;
11518 let clustered_advance = clustered_width * 0.75;
11519 assert_rects_near(
11520 &clustered_bounds,
11521 &[
11522 Rect {
11523 x: 58.8,
11524 y: 62.0,
11525 width: clustered_width,
11526 height: 50.0,
11527 },
11528 Rect {
11529 x: 134.8,
11530 y: 12.0,
11531 width: clustered_width,
11532 height: 100.0,
11533 },
11534 Rect {
11535 x: 58.8 + clustered_advance,
11536 y: 12.0,
11537 width: clustered_width,
11538 height: 100.0,
11539 },
11540 Rect {
11541 x: 134.8 + clustered_advance,
11542 y: 62.0,
11543 width: clustered_width,
11544 height: 50.0,
11545 },
11546 ],
11547 );
11548
11549 let Plot::Bar { direction, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
11550 unreachable!()
11551 };
11552 *direction = BarDirection::Bar;
11553 let horizontal = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11554 let horizontal_width = 20.0 / 1.75;
11555 let horizontal_advance = horizontal_width * 0.75;
11556 assert_rects_near(
11557 &horizontal,
11558 &[
11559 Rect {
11560 x: 36.0,
11561 y: 27.0,
11562 width: 76.0,
11563 height: horizontal_width,
11564 },
11565 Rect {
11566 x: 36.0,
11567 y: 77.0,
11568 width: 152.0,
11569 height: horizontal_width,
11570 },
11571 Rect {
11572 x: 36.0,
11573 y: 27.0 + horizontal_advance,
11574 width: 152.0,
11575 height: horizontal_width,
11576 },
11577 Rect {
11578 x: 36.0,
11579 y: 77.0 + horizontal_advance,
11580 width: 76.0,
11581 height: horizontal_width,
11582 },
11583 ],
11584 );
11585
11586 let Plot::Bar {
11587 direction,
11588 grouping,
11589 ..
11590 } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11591 else {
11592 unreachable!()
11593 };
11594 *direction = BarDirection::Column;
11595 *grouping = BarGrouping::Stacked;
11596 let stacked = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11597 assert_rects_near(
11598 &stacked,
11599 &[
11600 Rect {
11601 x: 58.8,
11602 y: 112.0 - 100.0 / 3.0,
11603 width: 30.4,
11604 height: 100.0 / 3.0,
11605 },
11606 Rect {
11607 x: 134.8,
11608 y: 112.0 - 200.0 / 3.0,
11609 width: 30.4,
11610 height: 200.0 / 3.0,
11611 },
11612 Rect {
11613 x: 58.8,
11614 y: 12.0,
11615 width: 30.4,
11616 height: 200.0 / 3.0,
11617 },
11618 Rect {
11619 x: 134.8,
11620 y: 12.0,
11621 width: 30.4,
11622 height: 100.0 / 3.0,
11623 },
11624 ],
11625 );
11626
11627 let Plot::Bar {
11628 grouping, series, ..
11629 } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11630 else {
11631 unreachable!()
11632 };
11633 *grouping = BarGrouping::PercentStacked;
11634 series[1].values.values = vec![2.0, 2.0];
11635 let percent = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11636 assert_rects_near(
11637 &percent,
11638 &[
11639 Rect {
11640 x: 58.8,
11641 y: 112.0 - 100.0 / 3.0,
11642 width: 30.4,
11643 height: 100.0 / 3.0,
11644 },
11645 Rect {
11646 x: 134.8,
11647 y: 62.0,
11648 width: 30.4,
11649 height: 50.0,
11650 },
11651 Rect {
11652 x: 58.8,
11653 y: 12.0,
11654 width: 30.4,
11655 height: 200.0 / 3.0,
11656 },
11657 Rect {
11658 x: 134.8,
11659 y: 12.0,
11660 width: 30.4,
11661 height: 50.0,
11662 },
11663 ],
11664 );
11665 }
11666
11667 #[test]
11668 fn line_scatter_and_radar_emit_paths_and_markers() {
11669 let line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
11670 let line_geometry = render_geometry(&line.chart, chart_bounds()).unwrap();
11671 assert_path_points(
11672 path_commands(&line_geometry)[0],
11673 &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
11674 false,
11675 );
11676 assert_rects_near(
11677 &path_bounds(&line_geometry)[1..],
11678 &[
11679 Rect {
11680 x: 71.5,
11681 y: 109.5,
11682 width: 5.0,
11683 height: 5.0,
11684 },
11685 Rect {
11686 x: 147.5,
11687 y: 9.5,
11688 width: 5.0,
11689 height: 5.0,
11690 },
11691 ],
11692 );
11693
11694 let (scatter_plot, _) = remaining_plot_fixtures()
11695 .into_iter()
11696 .find(|(kind, _, _)| *kind == "scatterChart")
11697 .map(|(_, plot, axes)| (plot, axes))
11698 .unwrap();
11699 let mut scatter =
11700 CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
11701 .unwrap();
11702 let Plot::Scatter { style, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
11703 else {
11704 unreachable!()
11705 };
11706 *style = ScatterStyle::LineMarker;
11707 let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
11708 assert_path_points(
11709 path_commands(&scatter_geometry)[0],
11710 &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
11711 false,
11712 );
11713 assert_rects_near(
11714 &path_bounds(&scatter_geometry)[1..],
11715 &[
11716 Rect {
11717 x: 33.5,
11718 y: 109.5,
11719 width: 5.0,
11720 height: 5.0,
11721 },
11722 Rect {
11723 x: 185.5,
11724 y: 9.5,
11725 width: 5.0,
11726 height: 5.0,
11727 },
11728 ],
11729 );
11730
11731 let (radar_plot, _) = remaining_plot_fixtures()
11732 .into_iter()
11733 .find(|(kind, _, _)| *kind == "radarChart")
11734 .map(|(_, plot, axes)| (plot, axes))
11735 .unwrap();
11736 let radar = CT_ChartSpace::from_xml(chart_with_optional_axes(&radar_plot, true).as_bytes())
11737 .unwrap();
11738 let radar_geometry = render_geometry(&radar.chart, chart_bounds()).unwrap();
11739 assert_path_points(
11740 path_commands(&radar_geometry)[0],
11741 &[Point { x: 112.0, y: 37.0 }, Point { x: 112.0, y: 112.0 }],
11742 true,
11743 );
11744 assert_rects_near(
11745 &path_bounds(&radar_geometry)[1..],
11746 &[
11747 Rect {
11748 x: 109.5,
11749 y: 34.5,
11750 width: 5.0,
11751 height: 5.0,
11752 },
11753 Rect {
11754 x: 109.5,
11755 y: 109.5,
11756 width: 5.0,
11757 height: 5.0,
11758 },
11759 ],
11760 );
11761 }
11762
11763 #[test]
11764 fn pie_doughnut_and_area_emit_closed_paths() {
11765 let fixtures = remaining_plot_fixtures();
11766 let pie_plot = fixtures
11767 .iter()
11768 .find(|(kind, _, _)| *kind == "pieChart")
11769 .unwrap();
11770 let pie =
11771 CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot.1, pie_plot.2).as_bytes())
11772 .unwrap();
11773 let pie_geometry = render_geometry(&pie.chart, chart_bounds()).unwrap();
11774 let pie_commands = path_commands(&pie_geometry);
11775 assert_eq!(pie_commands.len(), 2);
11776 let [
11777 PathCommand::MoveTo(center),
11778 PathCommand::LineTo(start),
11779 ..,
11780 PathCommand::CurveTo { to: end, .. },
11781 PathCommand::Close,
11782 ] = pie_commands[0]
11783 else {
11784 panic!("expected closed cubic pie wedge");
11785 };
11786 assert_point_near(*center, Point { x: 112.0, y: 62.0 });
11787 assert_point_near(
11788 *start,
11789 Point {
11790 x: 137.0,
11791 y: 62.0 - 25.0 * 3.0_f64.sqrt(),
11792 },
11793 );
11794 assert_point_near(
11795 *end,
11796 Point {
11797 x: 137.0,
11798 y: 62.0 + 25.0 * 3.0_f64.sqrt(),
11799 },
11800 );
11801
11802 let doughnut_plot = fixtures
11803 .iter()
11804 .find(|(kind, _, _)| *kind == "doughnutChart")
11805 .unwrap();
11806 let doughnut = CT_ChartSpace::from_xml(
11807 chart_with_optional_axes(&doughnut_plot.1, doughnut_plot.2).as_bytes(),
11808 )
11809 .unwrap();
11810 let doughnut_geometry = render_geometry(&doughnut.chart, chart_bounds()).unwrap();
11811 let doughnut_commands = path_commands(&doughnut_geometry);
11812 assert_eq!(doughnut_commands.len(), 2);
11813 let commands = doughnut_commands[0];
11814 assert_eq!(commands.len(), 7);
11815 let PathCommand::MoveTo(outer_start) = commands[0] else {
11816 panic!("expected doughnut outer start");
11817 };
11818 let PathCommand::LineTo(inner_end) = commands[3] else {
11819 panic!("expected doughnut inner-radius join");
11820 };
11821 let PathCommand::CurveTo {
11822 to: inner_start, ..
11823 } = commands[5]
11824 else {
11825 panic!("expected reverse inner arc");
11826 };
11827 let diagonal = 25.0 * 2.0_f64.sqrt();
11828 assert_point_near(
11829 outer_start,
11830 Point {
11831 x: 112.0 + diagonal,
11832 y: 62.0 - diagonal,
11833 },
11834 );
11835 assert_point_near(
11836 inner_end,
11837 Point {
11838 x: 112.0 + 30.0 * 75.0_f64.to_radians().cos(),
11839 y: 62.0 + 30.0 * 75.0_f64.to_radians().sin(),
11840 },
11841 );
11842 let inner_diagonal = 15.0 * 2.0_f64.sqrt();
11843 assert_point_near(
11844 inner_start,
11845 Point {
11846 x: 112.0 + inner_diagonal,
11847 y: 62.0 - inner_diagonal,
11848 },
11849 );
11850 assert_eq!(commands.last(), Some(&PathCommand::Close));
11851
11852 let area_plot = fixtures
11853 .iter()
11854 .find(|(kind, _, _)| *kind == "areaChart")
11855 .unwrap();
11856 let mut area =
11857 CT_ChartSpace::from_xml(chart_with_optional_axes(&area_plot.1, area_plot.2).as_bytes())
11858 .unwrap();
11859 let area_geometry = render_geometry(&area.chart, chart_bounds()).unwrap();
11860 assert_path_points(
11861 path_commands(&area_geometry)[0],
11862 &[
11863 Point { x: 74.0, y: 62.0 },
11864 Point { x: 150.0, y: 12.0 },
11865 Point { x: 150.0, y: 112.0 },
11866 Point { x: 74.0, y: 112.0 },
11867 ],
11868 true,
11869 );
11870
11871 let Plot::Area {
11872 grouping, series, ..
11873 } = &mut area.chart.plot_area.plots_mut().unwrap()[0]
11874 else {
11875 unreachable!()
11876 };
11877 *grouping = Grouping::Stacked;
11878 let mut second = series[0].clone();
11879 second.index = 1;
11880 second.order = 1;
11881 second.values.values = vec![2.0, 1.0];
11882 series.push(second);
11883 let stacked_area = render_geometry(&area.chart, chart_bounds()).unwrap();
11884 assert_path_points(
11885 path_commands(&stacked_area)[1],
11886 &[
11887 Point { x: 74.0, y: 12.0 },
11888 Point { x: 150.0, y: 12.0 },
11889 Point {
11890 x: 150.0,
11891 y: 112.0 - 200.0 / 3.0,
11892 },
11893 Point {
11894 x: 74.0,
11895 y: 112.0 - 100.0 / 3.0,
11896 },
11897 ],
11898 true,
11899 );
11900 }
11901
11902 #[test]
11903 fn sparse_cache_indexes_preserve_slots_and_scatter_pairing() {
11904 let series = sparse_category_series();
11905 let bar = format!(
11906 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>"#
11907 );
11908 let bar = CT_ChartSpace::from_xml(chart_with_plot(&bar).as_bytes()).unwrap();
11909 assert_rects_near(
11910 &path_bounds(&render_geometry(&bar.chart, chart_bounds()).unwrap()),
11911 &[
11912 Rect {
11913 x: 51.2,
11914 y: 62.0,
11915 width: 304.0 / 15.0,
11916 height: 50.0,
11917 },
11918 Rect {
11919 x: 36.0 + 304.0 / 3.0 + 15.2,
11920 y: 12.0,
11921 width: 304.0 / 15.0,
11922 height: 100.0,
11923 },
11924 ],
11925 );
11926
11927 let line = format!(
11928 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>"#
11929 );
11930 let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line).as_bytes()).unwrap();
11931 let first = Point {
11932 x: 36.0 + 76.0 / 3.0,
11933 y: 62.0,
11934 };
11935 let missing = Point { x: 112.0, y: 112.0 };
11936 let last = Point {
11937 x: 36.0 + 380.0 / 3.0,
11938 y: 12.0,
11939 };
11940 let gap = render_geometry(&line.chart, chart_bounds()).unwrap();
11941 let gap_commands = path_commands(&gap);
11942 assert_eq!(gap_commands.len(), 2);
11943 assert_path_points(
11944 gap_commands[0],
11945 &[Point {
11946 x: first.x,
11947 y: 112.0,
11948 }],
11949 false,
11950 );
11951 assert_path_points(gap_commands[1], &[last], false);
11952 line.chart.disp_blanks_as = DispBlanksAs::Zero;
11953 assert_path_points(
11954 path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
11955 &[first, missing, last],
11956 false,
11957 );
11958 line.chart.disp_blanks_as = DispBlanksAs::Span;
11959 assert_path_points(
11960 path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
11961 &[
11962 Point {
11963 x: first.x,
11964 y: 112.0,
11965 },
11966 last,
11967 ],
11968 false,
11969 );
11970
11971 let area = format!(
11972 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:areaChart>"#
11973 );
11974 let mut area = CT_ChartSpace::from_xml(chart_with_plot(&area).as_bytes()).unwrap();
11975 let area_gap = render_geometry(&area.chart, chart_bounds()).unwrap();
11976 let area_gap_commands = path_commands(&area_gap);
11977 assert_eq!(area_gap_commands.len(), 2);
11978 assert_path_points(
11979 area_gap_commands[0],
11980 &[
11981 first,
11982 Point {
11983 x: first.x,
11984 y: 112.0,
11985 },
11986 ],
11987 true,
11988 );
11989 assert_path_points(
11990 area_gap_commands[1],
11991 &[
11992 last,
11993 Point {
11994 x: last.x,
11995 y: 112.0,
11996 },
11997 ],
11998 true,
11999 );
12000 area.chart.disp_blanks_as = DispBlanksAs::Zero;
12001 assert_path_points(
12002 path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
12003 &[
12004 first,
12005 missing,
12006 last,
12007 Point {
12008 x: last.x,
12009 y: 112.0,
12010 },
12011 missing,
12012 Point {
12013 x: first.x,
12014 y: 112.0,
12015 },
12016 ],
12017 true,
12018 );
12019 area.chart.disp_blanks_as = DispBlanksAs::Span;
12020 assert_path_points(
12021 path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
12022 &[
12023 first,
12024 last,
12025 Point {
12026 x: last.x,
12027 y: 112.0,
12028 },
12029 Point {
12030 x: first.x,
12031 y: 112.0,
12032 },
12033 ],
12034 true,
12035 );
12036
12037 let radar = format!(
12038 r#"<q:radarChart><q:radarStyle val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#
12039 );
12040 let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
12041 assert_path_points(
12042 path_commands(&render_geometry(&radar.chart, chart_bounds()).unwrap())[0],
12043 &[
12044 Point { x: 112.0, y: 37.0 },
12045 Point {
12046 x: 112.0 - 25.0 * 3.0_f64.sqrt(),
12047 y: 87.0,
12048 },
12049 ],
12050 true,
12051 );
12052
12053 let scatter = format!(
12054 r#"<q:scatterChart><q:scatterStyle val="lineMarker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:scatterChart>"#,
12055 sparse_scatter_series()
12056 );
12057 let mut scatter = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
12058 let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12059 assert_eq!(geometry_children(&scatter_geometry).len(), 4);
12060 let gap_commands = path_commands(&scatter_geometry);
12061 assert_path_points(gap_commands[0], &[Point { x: 36.0, y: 112.0 }], false);
12062 assert_path_points(gap_commands[1], &[Point { x: 188.0, y: 12.0 }], false);
12063
12064 scatter.chart.disp_blanks_as = DispBlanksAs::Zero;
12065 let zero = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12066 assert_path_points(
12067 path_commands(&zero)[0],
12068 &[
12069 Point { x: 74.0, y: 87.0 },
12070 Point { x: 36.0, y: 62.0 },
12071 Point { x: 150.0, y: 112.0 },
12072 Point { x: 188.0, y: 12.0 },
12073 ],
12074 false,
12075 );
12076
12077 scatter.chart.disp_blanks_as = DispBlanksAs::Span;
12078 assert_path_points(
12079 path_commands(&render_geometry(&scatter.chart, chart_bounds()).unwrap())[0],
12080 &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
12081 false,
12082 );
12083 }
12084
12085 #[test]
12086 fn sparse_scatter_domains_and_labels_use_only_matched_points() {
12087 let scatter = format!(
12088 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>"#,
12089 sparse_scatter_series()
12090 );
12091 let chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
12092 let plot = &chart.chart.plot_area.plots().unwrap()[0];
12093 assert_eq!(
12094 super::scatter_domains(plot, DispBlanksAs::Gap).unwrap(),
12095 (
12096 Domain {
12097 min: 10.0,
12098 max: 40.0,
12099 },
12100 Domain {
12101 min: 100.0,
12102 max: 400.0,
12103 },
12104 )
12105 );
12106 let axes = chart.chart.plot_area.axes().unwrap();
12107 let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
12108 let anchors =
12109 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12110 assert_eq!(
12111 anchors.keys().copied().collect::<Vec<_>>(),
12112 vec![(0, 0), (0, 3)]
12113 );
12114 assert_eq!(anchors[&(0, 0)].center, Point { x: 0.0, y: 140.0 });
12115 assert_eq!(anchors[&(0, 3)].center, Point { x: 200.0, y: 0.0 });
12116 }
12117
12118 #[test]
12119 fn sparse_bubble_size_labels_resolve_logical_indexes() {
12120 let scatter = format!(
12121 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>"#,
12122 sparse_scatter_series()
12123 );
12124 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
12125 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12126 let Plot::Scatter { series, .. } = plot else {
12127 panic!("expected scatter plot");
12128 };
12129 series[0].bubble_size = Some(
12130 NumericData::from_xml(
12131 format!(
12132 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>"#
12133 )
12134 .as_bytes(),
12135 )
12136 .unwrap(),
12137 );
12138 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12139 let mut rendered = Vec::new();
12140 super::render_data_labels(
12141 plot,
12142 chart_bounds(),
12143 DispBlanksAs::Gap,
12144 super::GeometryOptions::default(),
12145 &mut fonts,
12146 &mut rendered,
12147 )
12148 .unwrap();
12149 let text: Vec<_> = rendered
12150 .iter()
12151 .filter_map(|element| match element {
12152 PositionedElement::Text(run) => Some(run.text.as_str()),
12153 _ => None,
12154 })
12155 .collect();
12156 assert_eq!(text, vec!["5", "9"]);
12157 }
12158
12159 #[test]
12160 fn geometry_rejects_invalid_bounds_and_opaque_plots() {
12161 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12162 for bounds in [
12163 Rect {
12164 x: 0.0,
12165 y: 0.0,
12166 width: f64::NAN,
12167 height: 100.0,
12168 },
12169 Rect {
12170 x: 0.0,
12171 y: 0.0,
12172 width: 0.0,
12173 height: 100.0,
12174 },
12175 Rect {
12176 x: 0.0,
12177 y: 0.0,
12178 width: 40.0,
12179 height: 30.0,
12180 },
12181 ] {
12182 assert!(render_geometry(&chart.chart, bounds).is_err());
12183 }
12184
12185 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12186 unreachable!()
12187 };
12188 series[0].values.values.clear();
12189 assert!(render_geometry(&chart.chart, chart_bounds()).is_err());
12190
12191 let opaque = CT_ChartSpace::from_xml(
12192 chart_with_plot(r#"<q:bar3DChart><q:barDir val="col"/></q:bar3DChart>"#).as_bytes(),
12193 )
12194 .unwrap();
12195 let error = render_geometry(&opaque.chart, chart_bounds()).unwrap_err();
12196 assert!(error.to_string().contains("unsupported or combination"));
12197
12198 let (scatter_plot, _) = remaining_plot_fixtures()
12199 .into_iter()
12200 .find(|(kind, _, _)| *kind == "scatterChart")
12201 .map(|(_, plot, axes)| (plot, axes))
12202 .unwrap();
12203 let mut scatter =
12204 CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
12205 .unwrap();
12206 for invalid in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
12207 let Plot::Scatter { series, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
12208 else {
12209 unreachable!()
12210 };
12211 let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
12212 unreachable!()
12213 };
12214 x_values.values[0] = invalid;
12215 let error = render_geometry(&scatter.chart, chart_bounds()).unwrap_err();
12216 assert!(error.to_string().contains("xVal"));
12217 }
12218 }
12219
12220 #[test]
12221 fn finite_extremes_never_produce_nonfinite_geometry() {
12222 let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
12223 let Plot::Line { series, .. } = &mut line.chart.plot_area.plots_mut().unwrap()[0] else {
12224 unreachable!()
12225 };
12226 series[0].values.values = vec![-f64::MAX, f64::MAX];
12227 let extreme_line = render_geometry(&line.chart, chart_bounds()).unwrap();
12228 assert_path_points(
12229 path_commands(&extreme_line)[0],
12230 &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
12231 false,
12232 );
12233
12234 let mut stacked =
12235 CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12236 let Plot::Bar {
12237 grouping, series, ..
12238 } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
12239 else {
12240 unreachable!()
12241 };
12242 *grouping = BarGrouping::Stacked;
12243 series[0].values.values = vec![f64::MAX, 1.0];
12244 let mut second = series[0].clone();
12245 second.index = 1;
12246 second.order = 1;
12247 series.push(second);
12248 let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
12249 assert!(error.to_string().contains("stacked value total"));
12250
12251 let Plot::Bar { grouping, .. } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
12252 else {
12253 unreachable!()
12254 };
12255 *grouping = BarGrouping::PercentStacked;
12256 let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
12257 assert!(error.to_string().contains("stacked percentage total"));
12258
12259 let (pie_plot, _) = remaining_plot_fixtures()
12260 .into_iter()
12261 .find(|(kind, _, _)| *kind == "pieChart")
12262 .map(|(_, plot, axes)| (plot, axes))
12263 .unwrap();
12264 let mut pie =
12265 CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot, false).as_bytes()).unwrap();
12266 let Plot::Pie { series, .. } = &mut pie.chart.plot_area.plots_mut().unwrap()[0] else {
12267 unreachable!()
12268 };
12269 series[0].values.values = vec![f64::MAX, f64::MAX];
12270 let error = render_geometry(&pie.chart, chart_bounds()).unwrap_err();
12271 assert!(error.to_string().contains("pie value total"));
12272
12273 let (scatter_plot, _) = remaining_plot_fixtures()
12274 .into_iter()
12275 .find(|(kind, _, _)| *kind == "scatterChart")
12276 .map(|(_, plot, axes)| (plot, axes))
12277 .unwrap();
12278 let mut scatter =
12279 CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
12280 .unwrap();
12281 let Plot::Scatter { style, series, .. } =
12282 &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
12283 else {
12284 unreachable!()
12285 };
12286 *style = ScatterStyle::Line;
12287 let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
12288 unreachable!()
12289 };
12290 x_values.values = vec![-f64::MAX, f64::MAX];
12291 series[0].values.values = vec![-f64::MAX, f64::MAX];
12292 let extreme_scatter = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12293 assert_path_points(
12294 path_commands(&extreme_scatter)[0],
12295 &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
12296 false,
12297 );
12298 }
12299
12300 #[test]
12301 fn geometry_is_backend_neutral_and_deterministic() {
12302 let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12303 let first = render_geometry(&chart.chart, chart_bounds()).unwrap();
12304 let second = render_geometry(&chart.chart, chart_bounds()).unwrap();
12305 assert_eq!(
12306 format!("{:?}", first.elements),
12307 format!("{:?}", second.elements)
12308 );
12309
12310 let raster = |geometry: ChartGeometry| {
12311 let layout = LayoutResult::new(
12312 vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
12313 Vec::new(),
12314 None,
12315 Vec::new(),
12316 );
12317 oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
12318 };
12319 assert_eq!(raster(first), raster(second));
12320 }
12321
12322 #[test]
12323 fn zero_to_one_hundred_axis_uses_expected_ticks() {
12324 let scale = nice_number_scale(
12325 Domain {
12326 min: 0.0,
12327 max: 100.0,
12328 },
12329 None,
12330 None,
12331 6,
12332 )
12333 .unwrap();
12334 assert_eq!(scale.ticks, vec![0.0, 20.0, 40.0, 60.0, 80.0, 100.0]);
12335 }
12336
12337 #[test]
12338 fn nice_number_ticks_cover_unpinned_extents() {
12339 for domain in [
12340 Domain {
12341 min: 3.0,
12342 max: 97.0,
12343 },
12344 Domain {
12345 min: -97.0,
12346 max: -3.0,
12347 },
12348 Domain {
12349 min: -3.5,
12350 max: 8.25,
12351 },
12352 Domain {
12353 min: 0.125,
12354 max: 0.875,
12355 },
12356 Domain {
12357 min: 1.0e12,
12358 max: 7.0e12,
12359 },
12360 Domain {
12361 min: -f64::MAX,
12362 max: f64::MAX,
12363 },
12364 Domain {
12365 min: f64::from_bits(1),
12366 max: f64::from_bits(5),
12367 },
12368 ] {
12369 let scale = nice_number_scale(domain, None, None, 6).unwrap();
12370 assert!(scale.domain.min <= domain.min);
12371 assert!(scale.domain.max >= domain.max);
12372 assert!(scale.ticks.iter().all(|tick| tick.is_finite()));
12373 assert!(scale.ticks.windows(2).all(|pair| pair[0] < pair[1]));
12374 }
12375
12376 let constant = nice_number_scale(Domain { min: 4.0, max: 4.0 }, None, None, 6).unwrap();
12377 assert!(constant.domain.min < 4.0 && constant.domain.max > 4.0);
12378 }
12379
12380 #[test]
12381 fn one_sided_explicit_scale_beyond_data_derives_a_valid_opposite_bound() {
12382 let minimum = nice_number_scale(
12383 Domain {
12384 min: 0.0,
12385 max: 10.0,
12386 },
12387 Some(20.0),
12388 None,
12389 6,
12390 )
12391 .unwrap();
12392 assert_eq!(minimum.domain.min, 20.0);
12393 assert!(minimum.domain.max > 20.0);
12394 assert!(minimum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
12395
12396 let maximum = nice_number_scale(
12397 Domain {
12398 min: 0.0,
12399 max: 10.0,
12400 },
12401 None,
12402 Some(-20.0),
12403 6,
12404 )
12405 .unwrap();
12406 assert_eq!(maximum.domain.max, -20.0);
12407 assert!(maximum.domain.min < -20.0);
12408 assert!(maximum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
12409 }
12410
12411 #[test]
12412 fn standard_line_domain_does_not_force_zero() {
12413 let mut chart =
12414 CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
12415 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12416 let Plot::Line { series, .. } = plot else {
12417 panic!("expected line plot");
12418 };
12419 series[0].values.values = vec![1_000.0, 1_010.0];
12420 assert_eq!(
12421 super::plot_value_domain(plot, DispBlanksAs::Gap).unwrap(),
12422 Some(Domain {
12423 min: 1_000.0,
12424 max: 1_010.0,
12425 })
12426 );
12427 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12428 assert_path_points(
12429 path_commands(&geometry)[0],
12430 &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
12431 false,
12432 );
12433 }
12434
12435 #[test]
12436 fn explicit_scaling_bounds_and_orientation_are_honored() {
12437 let scale = nice_number_scale(
12438 Domain {
12439 min: 25.0,
12440 max: 75.0,
12441 },
12442 Some(0.0),
12443 Some(100.0),
12444 6,
12445 )
12446 .unwrap();
12447 assert_eq!(
12448 scale.domain,
12449 Domain {
12450 min: 0.0,
12451 max: 100.0
12452 }
12453 );
12454 assert_eq!(
12455 super::map_y_oriented(0.0, scale.domain, chart_bounds(), Orientation::MaxMin).unwrap(),
12456 0.0
12457 );
12458 assert_eq!(
12459 super::map_y_oriented(100.0, scale.domain, chart_bounds(), Orientation::MaxMin)
12460 .unwrap(),
12461 140.0
12462 );
12463
12464 let mut chart = labelled_bar_chart();
12465 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12466 panic!("expected bar plot");
12467 };
12468 series[0].values.values = vec![25.0, 75.0];
12469 let value_axis = chart
12470 .chart
12471 .plot_area
12472 .axes
12473 .iter_mut()
12474 .find(|axis| axis.kind == AxisKind::Value)
12475 .unwrap();
12476 value_axis.scaling.orientation = Orientation::MaxMin;
12477 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12478 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12479 let plot = group
12480 .children
12481 .iter()
12482 .find_map(|element| match element {
12483 PositionedElement::Group(group) => Some(group),
12484 _ => None,
12485 })
12486 .unwrap();
12487 let bars: Vec<_> = plot
12488 .children
12489 .iter()
12490 .filter_map(|element| match element {
12491 PositionedElement::Path(path) => path.path.bounds(),
12492 _ => None,
12493 })
12494 .collect();
12495 assert_eq!(bars.len(), 2);
12496 assert!((bars[0].y - 12.0).abs() < 1.0e-9);
12497 assert!((bars[0].height - 25.0).abs() < 1.0e-9);
12498 assert!((bars[1].height - 75.0).abs() < 1.0e-9);
12499 }
12500
12501 #[test]
12502 fn family_label_anchors_share_rendered_geometry() {
12503 let mut bar = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12504 let bar_plot = &mut bar.chart.plot_area.plots_mut().unwrap()[0];
12505 let Plot::Bar { series, .. } = bar_plot else {
12506 panic!("expected bar plot");
12507 };
12508 let mut second = series[0].clone();
12509 second.index = 1;
12510 second.order = 1;
12511 series.push(second);
12512 let anchors = super::plot_label_anchors(
12513 bar_plot,
12514 chart_bounds(),
12515 DispBlanksAs::Gap,
12516 super::GeometryOptions::default(),
12517 )
12518 .unwrap();
12519 assert_point_near(anchors[&(0, 0)].center, Point { x: 40.0, y: 105.0 });
12520 assert_point_near(anchors[&(1, 0)].center, Point { x: 60.0, y: 105.0 });
12521
12522 let pie = CT_ChartSpace::from_xml(
12523 chart_with_optional_axes(
12524 &remaining_plot_fixtures()
12525 .into_iter()
12526 .find(|(kind, _, _)| *kind == "pieChart")
12527 .unwrap()
12528 .1,
12529 false,
12530 )
12531 .as_bytes(),
12532 )
12533 .unwrap();
12534 let Plot::Pie {
12535 first_slice_angle,
12536 series,
12537 ..
12538 } = &pie.chart.plot_area.plots().unwrap()[0]
12539 else {
12540 panic!("expected pie plot");
12541 };
12542 let slices = super::pie_slices(*first_slice_angle, None, series, chart_bounds()).unwrap();
12543 assert_eq!(slices.len(), 2);
12544 let first_span = std::f64::consts::TAU / 3.0;
12545 let first_midpoint = f64::from(*first_slice_angle).to_radians()
12546 - std::f64::consts::FRAC_PI_2
12547 + first_span / 2.0;
12548 assert_point_near(
12549 slices[0].anchor,
12550 super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, first_midpoint),
12551 );
12552 let second_midpoint = first_midpoint + std::f64::consts::PI;
12553 assert_point_near(
12554 slices[1].anchor,
12555 super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, second_midpoint),
12556 );
12557
12558 let radar = format!(
12559 r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12560 plot_series(0)
12561 );
12562 let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
12563 let Plot::Radar { series, .. } = &radar.chart.plot_area.plots().unwrap()[0] else {
12564 panic!("expected radar plot");
12565 };
12566 let radar_points =
12567 super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
12568 .unwrap();
12569 assert_eq!(radar_points[0][0], (0, Point { x: 100.0, y: 35.0 }));
12570 assert_eq!(radar_points[0][1], (1, Point { x: 100.0, y: 140.0 }));
12571 }
12572
12573 #[test]
12574 fn radar_annotations_use_spokes_perimeter_labels_and_radial_gridlines() {
12575 let radar_plot = format!(
12576 r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12577 sparse_category_series()
12578 );
12579 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12580 for axis in &mut chart.chart.plot_area.axes {
12581 axis.major_tick_mark = TickMark::Cross;
12582 if axis.kind == AxisKind::Value {
12583 axis.major_gridlines = Some(super::ChartLines::default());
12584 }
12585 }
12586 let plot = &chart.chart.plot_area.plots().unwrap()[0];
12587 let axes = chart.chart.plot_area.axes().unwrap();
12588 let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
12589 let center = Point { x: 112.0, y: 62.0 };
12590 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12591 let annotations =
12592 super::render_axes_and_labels(plot, &axes, bounds, DispBlanksAs::Gap, &mut fonts)
12593 .unwrap();
12594 assert_eq!(annotations.axis_lines.len(), 4);
12595 for spoke in annotations.axis_lines.iter().take(3) {
12596 let PositionedElement::Path(spoke) = spoke else {
12597 panic!("radar spoke must be a path");
12598 };
12599 assert_eq!(
12600 spoke.path.commands.first(),
12601 Some(&PathCommand::MoveTo(center))
12602 );
12603 }
12604 assert!(!annotations.gridlines.is_empty());
12605 for gridline in &annotations.gridlines {
12606 let PositionedElement::Path(gridline) = gridline else {
12607 panic!("radar gridline must be a path");
12608 };
12609 assert_eq!(gridline.path.commands.last(), Some(&PathCommand::Close));
12610 let radii: Vec<_> = gridline
12611 .path
12612 .commands
12613 .iter()
12614 .filter_map(|command| match command {
12615 PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
12616 Some((point.x - center.x).hypot(point.y - center.y))
12617 }
12618 _ => None,
12619 })
12620 .collect();
12621 assert_eq!(radii.len(), 3);
12622 assert!(
12623 radii
12624 .windows(2)
12625 .all(|pair| (pair[0] - pair[1]).abs() < 1.0e-9)
12626 );
12627 }
12628 let text: Vec<_> = annotations
12629 .labels
12630 .iter()
12631 .filter_map(|element| match element {
12632 PositionedElement::Text(run) => Some(run.text.as_str()),
12633 _ => None,
12634 })
12635 .collect();
12636 assert!(text.contains(&"North"));
12637 assert!(text.contains(&"West"));
12638 }
12639
12640 #[test]
12641 fn radar_tick_label_positions_are_distinct() {
12642 let radar_plot = format!(
12643 r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12644 plot_series(0)
12645 );
12646 let chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12647 let plot = &chart.chart.plot_area.plots().unwrap()[0];
12648 let axes = chart.chart.plot_area.axes().unwrap();
12649 let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
12650 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12651
12652 let category_axis = axes
12653 .iter()
12654 .find(|axis| axis.kind == AxisKind::Category)
12655 .unwrap();
12656 for (position, expected) in [
12657 (TickLabelPosition::High, Point { x: 108.0, y: 0.0 }),
12658 (TickLabelPosition::Low, Point { x: 108.0, y: 25.0 }),
12659 (TickLabelPosition::NextTo, Point { x: 108.0, y: 5.0 }),
12660 ] {
12661 let mut axis = category_axis.clone();
12662 axis.tick_label_position = position;
12663 let annotations =
12664 super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
12665 .unwrap();
12666 let north = annotations
12667 .labels
12668 .iter()
12669 .find_map(|element| match element {
12670 PositionedElement::Text(run) if run.text == "North" => Some(run),
12671 _ => None,
12672 })
12673 .unwrap();
12674 assert_point_near(north.origin, expected);
12675 }
12676 let mut hidden_category = category_axis.clone();
12677 hidden_category.tick_label_position = TickLabelPosition::None;
12678 let annotations = super::render_axes_and_labels(
12679 plot,
12680 &[hidden_category],
12681 bounds,
12682 DispBlanksAs::Gap,
12683 &mut fonts,
12684 )
12685 .unwrap();
12686 assert!(annotations.labels.is_empty());
12687
12688 let narrow_bounds = Rect {
12689 x: 36.0,
12690 y: 12.0,
12691 width: 24.0,
12692 height: 100.0,
12693 };
12694 let narrow_center = Point { x: 48.0, y: 62.0 };
12695 assert_eq!(
12696 super::radar_category_label_origin(
12697 narrow_bounds,
12698 narrow_center,
12699 12.0,
12700 1,
12701 4,
12702 TickLabelPosition::High,
12703 ),
12704 Some(Point { x: 72.0, y: 65.0 })
12705 );
12706 assert_eq!(
12707 super::radar_category_label_origin(
12708 narrow_bounds,
12709 narrow_center,
12710 12.0,
12711 1,
12712 4,
12713 TickLabelPosition::NextTo,
12714 ),
12715 Some(Point { x: 66.0, y: 65.0 })
12716 );
12717
12718 let value_axis = axes
12719 .iter()
12720 .find(|axis| axis.kind == AxisKind::Value)
12721 .unwrap();
12722 for (position, expected) in [
12723 (TickLabelPosition::High, Point { x: 166.0, y: 65.0 }),
12724 (TickLabelPosition::Low, Point { x: 32.0, y: 65.0 }),
12725 (TickLabelPosition::NextTo, Point { x: 116.0, y: 65.0 }),
12726 ] {
12727 let mut axis = value_axis.clone();
12728 axis.tick_label_position = position;
12729 let annotations =
12730 super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
12731 .unwrap();
12732 let zero = annotations
12733 .labels
12734 .iter()
12735 .find_map(|element| match element {
12736 PositionedElement::Text(run) if run.text == "0" => Some(run),
12737 _ => None,
12738 })
12739 .unwrap();
12740 assert_point_near(zero.origin, expected);
12741 }
12742 let mut hidden_value = value_axis.clone();
12743 hidden_value.tick_label_position = TickLabelPosition::None;
12744 let annotations = super::render_axes_and_labels(
12745 plot,
12746 &[hidden_value],
12747 bounds,
12748 DispBlanksAs::Gap,
12749 &mut fonts,
12750 )
12751 .unwrap();
12752 assert!(annotations.labels.is_empty());
12753 }
12754
12755 #[test]
12756 fn radar_negative_and_mixed_sign_domains_preserve_values() {
12757 let radar_plot = format!(
12758 r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12759 plot_series(0)
12760 );
12761 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12762 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12763 let Plot::Radar { series, .. } = plot else {
12764 panic!("expected radar plot");
12765 };
12766 series[0].values.values = vec![-10.0, -5.0];
12767 let points =
12768 super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
12769 .unwrap();
12770 assert_eq!(points[0][0], (0, Point { x: 100.0, y: 70.0 }));
12771 assert_eq!(points[0][1], (1, Point { x: 100.0, y: 105.0 }));
12772
12773 series[0].values.values = vec![-5.0, 10.0];
12774 let options = super::GeometryOptions {
12775 value: Some(super::AxisMapping {
12776 domain: Domain {
12777 min: -10.0,
12778 max: 10.0,
12779 },
12780 orientation: Orientation::MinMax,
12781 }),
12782 ..super::GeometryOptions::default()
12783 };
12784 let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
12785 assert_eq!(points[0][0], (0, Point { x: 100.0, y: 52.5 }));
12786 assert_eq!(points[0][1], (1, Point { x: 100.0, y: 140.0 }));
12787 }
12788
12789 #[test]
12790 fn radar_explicit_bounds_suppress_out_of_range_points_and_labels() {
12791 let radar_plot = format!(
12792 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>"#,
12793 plot_series(0)
12794 );
12795 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12796 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12797 let Plot::Radar { series, .. } = plot else {
12798 panic!("expected radar plot");
12799 };
12800 series[0].values.values = vec![25.0, 75.0];
12801 let options = super::GeometryOptions {
12802 value: Some(super::AxisMapping {
12803 domain: Domain {
12804 min: 50.0,
12805 max: 100.0,
12806 },
12807 orientation: Orientation::MinMax,
12808 }),
12809 ..super::GeometryOptions::default()
12810 };
12811 let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
12812 assert_eq!(points[0], vec![(1, Point { x: 100.0, y: 105.0 })]);
12813 let anchors =
12814 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12815 assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 1)]);
12816
12817 let Plot::Radar { series, .. } = plot else {
12818 panic!("expected radar plot");
12819 };
12820 series[0].values.values = vec![75.0, 125.0];
12821 let reversed = super::GeometryOptions {
12822 value: Some(super::AxisMapping {
12823 domain: Domain {
12824 min: 50.0,
12825 max: 100.0,
12826 },
12827 orientation: Orientation::MaxMin,
12828 }),
12829 ..super::GeometryOptions::default()
12830 };
12831 let points = super::radar_series_points(series, chart_bounds(), reversed).unwrap();
12832 assert_eq!(points[0], vec![(0, Point { x: 100.0, y: 35.0 })]);
12833 let anchors =
12834 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
12835 assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
12836
12837 let value_axis = chart
12838 .chart
12839 .plot_area
12840 .axes
12841 .iter_mut()
12842 .find(|axis| axis.kind == AxisKind::Value)
12843 .unwrap();
12844 value_axis.scaling.minimum = Some(50.0);
12845 value_axis.scaling.maximum = Some(100.0);
12846 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12847 let Plot::Radar { series, .. } = plot else {
12848 panic!("expected radar plot");
12849 };
12850 series[0].values.values = vec![25.0, 25.0];
12851 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12852 let rendered = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12853 assert!(rendered.children.iter().any(|element| matches!(
12854 element,
12855 PositionedElement::Group(group) if group.clip.is_some() && group.children.is_empty()
12856 )));
12857 assert!(
12858 rendered
12859 .children
12860 .iter()
12861 .any(|element| matches!(element, PositionedElement::Path(_)))
12862 );
12863 assert!(!rendered.children.iter().any(|element| matches!(
12864 element,
12865 PositionedElement::Text(run) if run.text == "25"
12866 )));
12867 }
12868
12869 #[test]
12870 fn inside_and_outside_label_positions_follow_family_geometry() {
12871 let mut horizontal =
12872 CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12873 let plot = &mut horizontal.chart.plot_area.plots_mut().unwrap()[0];
12874 let Plot::Bar { direction, .. } = plot else {
12875 panic!("expected bar plot");
12876 };
12877 *direction = BarDirection::Bar;
12878 let anchors = super::plot_label_anchors(
12879 plot,
12880 chart_bounds(),
12881 DispBlanksAs::Gap,
12882 super::GeometryOptions::default(),
12883 )
12884 .unwrap();
12885 let anchor = anchors[&(0, 0)];
12886 assert_point_near(
12887 anchor.origin(Some(DataLabelPosition::InsideBase)),
12888 Point { x: 5.0, y: 35.0 },
12889 );
12890 assert_point_near(
12891 anchor.origin(Some(DataLabelPosition::InsideEnd)),
12892 Point { x: 95.0, y: 35.0 },
12893 );
12894 assert_point_near(
12895 anchor.origin(Some(DataLabelPosition::OutsideEnd)),
12896 Point { x: 105.0, y: 35.0 },
12897 );
12898
12899 let mut negative =
12900 CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12901 let plot = &mut negative.chart.plot_area.plots_mut().unwrap()[0];
12902 let Plot::Bar { series, .. } = plot else {
12903 panic!("expected bar plot");
12904 };
12905 series[0].values.values = vec![-1.0, -2.0];
12906 let anchors = super::plot_label_anchors(
12907 plot,
12908 chart_bounds(),
12909 DispBlanksAs::Gap,
12910 super::GeometryOptions::default(),
12911 )
12912 .unwrap();
12913 assert_point_near(
12914 anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12915 Point { x: 50.0, y: 75.0 },
12916 );
12917 let reversed = super::GeometryOptions {
12918 value: Some(super::AxisMapping {
12919 domain: Domain {
12920 min: -2.0,
12921 max: 0.0,
12922 },
12923 orientation: Orientation::MaxMin,
12924 }),
12925 ..super::GeometryOptions::default()
12926 };
12927 let anchors =
12928 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
12929 assert_point_near(
12930 anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12931 Point { x: 50.0, y: 65.0 },
12932 );
12933
12934 let pie = CT_ChartSpace::from_xml(
12935 chart_with_optional_axes(
12936 &remaining_plot_fixtures()
12937 .into_iter()
12938 .find(|(kind, _, _)| *kind == "pieChart")
12939 .unwrap()
12940 .1,
12941 false,
12942 )
12943 .as_bytes(),
12944 )
12945 .unwrap();
12946 let Plot::Pie { series, .. } = &pie.chart.plot_area.plots().unwrap()[0] else {
12947 panic!("expected pie plot");
12948 };
12949 let pie_slice = &super::pie_slices(30, None, series, chart_bounds()).unwrap()[0];
12950 assert_point_near(
12951 super::LabelAnchor::segment(pie_slice.anchor, pie_slice.base, pie_slice.end)
12952 .origin(Some(DataLabelPosition::OutsideEnd)),
12953 Point { x: 175.0, y: 70.0 },
12954 );
12955 let doughnut_slice = &super::pie_slices(30, Some(60), series, chart_bounds()).unwrap()[0];
12956 assert_point_near(
12957 super::LabelAnchor::segment(
12958 doughnut_slice.anchor,
12959 doughnut_slice.base,
12960 doughnut_slice.end,
12961 )
12962 .origin(Some(DataLabelPosition::InsideBase)),
12963 Point { x: 147.0, y: 70.0 },
12964 );
12965
12966 let short = super::LabelAnchor::segment(
12967 Point { x: 2.0, y: 0.0 },
12968 Point { x: 0.0, y: 0.0 },
12969 Point { x: 4.0, y: 0.0 },
12970 );
12971 assert_eq!(
12972 short.origin(Some(DataLabelPosition::InsideBase)),
12973 Point { x: 2.0, y: 0.0 }
12974 );
12975 assert_eq!(
12976 short.origin(Some(DataLabelPosition::InsideEnd)),
12977 Point { x: 2.0, y: 0.0 }
12978 );
12979 let thin_bounds = Rect {
12980 x: 0.0,
12981 y: 0.0,
12982 width: 80.0,
12983 height: 80.0,
12984 };
12985 let thin = &super::pie_slices(30, Some(90), series, thin_bounds).unwrap()[0];
12986 let thin = super::LabelAnchor::segment(thin.anchor, thin.base, thin.end);
12987 assert_point_near(
12988 thin.origin(Some(DataLabelPosition::InsideBase)),
12989 Point { x: 78.0, y: 40.0 },
12990 );
12991 assert_point_near(
12992 thin.origin(Some(DataLabelPosition::InsideEnd)),
12993 Point { x: 78.0, y: 40.0 },
12994 );
12995 }
12996
12997 #[test]
12998 fn degenerate_bar_and_radar_anchors_preserve_family_direction() {
12999 let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13000 let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
13001 let Plot::Bar { series, .. } = plot else {
13002 panic!("expected bar plot");
13003 };
13004 series[0].values.values = vec![80.0, 125.0];
13005 for (direction, orientation, boundary, outside) in [
13006 (
13007 BarDirection::Column,
13008 Orientation::MinMax,
13009 Point { x: 50.0, y: 140.0 },
13010 Point { x: 50.0, y: 135.0 },
13011 ),
13012 (
13013 BarDirection::Column,
13014 Orientation::MaxMin,
13015 Point { x: 50.0, y: 0.0 },
13016 Point { x: 50.0, y: 5.0 },
13017 ),
13018 (
13019 BarDirection::Bar,
13020 Orientation::MinMax,
13021 Point { x: 0.0, y: 35.0 },
13022 Point { x: 5.0, y: 35.0 },
13023 ),
13024 (
13025 BarDirection::Bar,
13026 Orientation::MaxMin,
13027 Point { x: 200.0, y: 35.0 },
13028 Point { x: 195.0, y: 35.0 },
13029 ),
13030 ] {
13031 let Plot::Bar {
13032 direction: plot_direction,
13033 ..
13034 } = plot
13035 else {
13036 panic!("expected bar plot");
13037 };
13038 *plot_direction = direction;
13039 let options = super::GeometryOptions {
13040 value: Some(super::AxisMapping {
13041 domain: Domain {
13042 min: 80.0,
13043 max: 100.0,
13044 },
13045 orientation,
13046 }),
13047 ..super::GeometryOptions::default()
13048 };
13049 let anchors =
13050 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options)
13051 .unwrap();
13052 let anchor = anchors[&(0, 0)];
13053 assert_point_near(anchor.center, boundary);
13054 assert_point_near(anchor.base, boundary);
13055 assert_point_near(anchor.end, boundary);
13056 assert_point_near(anchor.origin(Some(DataLabelPosition::InsideBase)), boundary);
13057 assert_point_near(anchor.origin(Some(DataLabelPosition::InsideEnd)), boundary);
13058 assert_point_near(anchor.origin(Some(DataLabelPosition::OutsideEnd)), outside);
13059 }
13060
13061 let radar_plot = format!(
13062 r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
13063 plot_series(0)
13064 );
13065 let mut radar = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
13066 let plot = &mut radar.chart.plot_area.plots_mut().unwrap()[0];
13067 let Plot::Radar { series, .. } = plot else {
13068 panic!("expected radar plot");
13069 };
13070 series[0].values.values = vec![0.0, 0.0, 0.0, 0.0];
13071 let anchors = super::plot_label_anchors(
13072 plot,
13073 chart_bounds(),
13074 DispBlanksAs::Gap,
13075 super::GeometryOptions::default(),
13076 )
13077 .unwrap();
13078 let center = Point { x: 100.0, y: 70.0 };
13079 for logical_index in 0..4 {
13080 assert_point_near(
13081 anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideBase)),
13082 center,
13083 );
13084 assert_point_near(
13085 anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideEnd)),
13086 center,
13087 );
13088 }
13089 assert_point_near(
13090 anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
13091 Point { x: 100.0, y: 65.0 },
13092 );
13093 assert_point_near(
13094 anchors[&(0, 1)].origin(Some(DataLabelPosition::OutsideEnd)),
13095 Point { x: 105.0, y: 70.0 },
13096 );
13097 assert_point_near(
13098 anchors[&(0, 2)].origin(Some(DataLabelPosition::OutsideEnd)),
13099 Point { x: 100.0, y: 75.0 },
13100 );
13101 assert_point_near(
13102 anchors[&(0, 3)].origin(Some(DataLabelPosition::OutsideEnd)),
13103 Point { x: 95.0, y: 70.0 },
13104 );
13105 }
13106
13107 #[test]
13108 fn labels_outside_explicit_axis_bounds_are_suppressed() {
13109 let mut chart =
13110 CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
13111 {
13112 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13113 let Plot::Line { series, .. } = plot else {
13114 panic!("expected line plot");
13115 };
13116 series[0].values.values = vec![25.0, 75.0];
13117 }
13118 let value_axis = chart
13119 .chart
13120 .plot_area
13121 .axes
13122 .iter_mut()
13123 .find(|axis| axis.kind == AxisKind::Value)
13124 .unwrap();
13125 value_axis.scaling.minimum = Some(50.0);
13126 value_axis.scaling.maximum = Some(100.0);
13127 value_axis.scaling.orientation = Orientation::MaxMin;
13128 let plot = &chart.chart.plot_area.plots().unwrap()[0];
13129 let axes = chart.chart.plot_area.axes().unwrap();
13130 let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
13131 let anchors =
13132 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
13133 assert!(!anchors.contains_key(&(0, 0)));
13134 assert_point_near(anchors[&(0, 1)].center, Point { x: 150.0, y: 70.0 });
13135
13136 let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13137 let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
13138 let Plot::Bar { series, .. } = plot else {
13139 panic!("expected bar plot");
13140 };
13141 series[0].values.values = vec![90.0, 125.0];
13142 let options = super::GeometryOptions {
13143 value: Some(super::AxisMapping {
13144 domain: Domain {
13145 min: 80.0,
13146 max: 100.0,
13147 },
13148 orientation: Orientation::MinMax,
13149 }),
13150 ..super::GeometryOptions::default()
13151 };
13152 let anchors =
13153 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
13154 assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
13155 assert_point_near(anchors[&(0, 0)].base, Point { x: 50.0, y: 140.0 });
13156 assert_point_near(anchors[&(0, 0)].end, Point { x: 50.0, y: 70.0 });
13157 assert_point_near(anchors[&(0, 0)].center, Point { x: 50.0, y: 105.0 });
13158 assert_point_near(
13159 anchors[&(0, 0)].origin(Some(DataLabelPosition::InsideBase)),
13160 Point { x: 50.0, y: 135.0 },
13161 );
13162 }
13163
13164 #[test]
13165 fn zero_bars_render_and_emit_requested_value_labels() {
13166 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13167 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13168 let Plot::Bar { series, .. } = plot else {
13169 panic!("expected bar plot");
13170 };
13171 series[0].values.values = vec![0.0, 0.0];
13172 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
13173 assert_eq!(path_commands(&geometry).len(), 2);
13174 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13175 render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13176 let plot = &chart.chart.plot_area.plots().unwrap()[0];
13177 let mut labels = Vec::new();
13178 super::render_data_labels(
13179 plot,
13180 chart_bounds(),
13181 DispBlanksAs::Gap,
13182 super::GeometryOptions::default(),
13183 &mut fonts,
13184 &mut labels,
13185 )
13186 .unwrap();
13187 let text: Vec<_> = labels
13188 .iter()
13189 .filter_map(|element| match element {
13190 PositionedElement::Text(run) => Some(run.text.as_str()),
13191 _ => None,
13192 })
13193 .collect();
13194 assert_eq!(text, vec!["0", "0"]);
13195 }
13196
13197 #[test]
13198 fn axes_gridlines_and_tick_marks_follow_model_state() {
13199 let chart = labelled_bar_chart();
13200 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13201 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13202 let ranks: Vec<_> = group
13203 .children
13204 .iter()
13205 .map(|element| match element {
13206 PositionedElement::Group(group) => {
13207 assert!(group.clip.is_some());
13208 1
13209 }
13210 PositionedElement::Path(path)
13211 if path
13212 .stroke
13213 .as_ref()
13214 .is_some_and(|stroke| stroke.width == 0.5) =>
13215 {
13216 0
13217 }
13218 PositionedElement::Path(path) if path.fill.is_some() => 4,
13219 PositionedElement::Path(path) => {
13220 let bounds = path.path.bounds().expect("axis or tick bounds");
13221 assert_eq!(path.stroke.as_ref().map(|stroke| stroke.width), Some(1.0));
13222 if bounds.width > 4.0 || bounds.height > 4.0 {
13223 2
13224 } else {
13225 3
13226 }
13227 }
13228 PositionedElement::Text(_) => 5,
13229 _ => panic!("unexpected labelled chart element"),
13230 })
13231 .collect();
13232 assert!(ranks.windows(2).all(|pair| pair[0] <= pair[1]), "{ranks:?}");
13233 for required in 0..=5 {
13234 assert!(
13235 ranks.contains(&required),
13236 "missing z-order rank {required}: {ranks:?}"
13237 );
13238 }
13239 assert_eq!(ranks.iter().filter(|rank| **rank == 1).count(), 1);
13240 assert_eq!(ranks.iter().filter(|rank| **rank == 2).count(), 2);
13241
13242 let mut deleted = chart.clone();
13243 for axis in &mut deleted.chart.plot_area.axes {
13244 axis.deleted = true;
13245 }
13246 let deleted_group = render_chart(&deleted.chart, chart_bounds(), &mut fonts).unwrap();
13247 assert_eq!(
13248 deleted_group
13249 .children
13250 .iter()
13251 .filter(|element| {
13252 matches!(
13253 element,
13254 PositionedElement::Path(path) if path.stroke.is_some()
13255 )
13256 })
13257 .count(),
13258 0
13259 );
13260 }
13261
13262 #[test]
13263 fn category_ticks_cover_unlabelled_and_sparse_logical_slots() {
13264 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13265 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
13266 panic!("expected bar plot");
13267 };
13268 series[0].categories = None;
13269 let plot = &chart.chart.plot_area.plots().unwrap()[0];
13270 let mut category_axis = chart
13271 .chart
13272 .plot_area
13273 .axes()
13274 .unwrap()
13275 .into_iter()
13276 .find(|axis| axis.kind == AxisKind::Category)
13277 .unwrap();
13278 category_axis.major_gridlines = Some(super::ChartLines::default());
13279 category_axis.major_tick_mark = TickMark::Cross;
13280 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13281 let annotations = super::render_axes_and_labels(
13282 plot,
13283 &[category_axis],
13284 chart_bounds(),
13285 DispBlanksAs::Gap,
13286 &mut fonts,
13287 )
13288 .unwrap();
13289 assert_eq!(annotations.gridlines.len(), 2);
13290 assert_eq!(annotations.ticks.len(), 2);
13291 assert!(annotations.labels.is_empty());
13292
13293 let sparse_plot = format!(
13294 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>"#,
13295 sparse_category_series()
13296 );
13297 let sparse = CT_ChartSpace::from_xml(chart_with_plot(&sparse_plot).as_bytes()).unwrap();
13298 let plot = &sparse.chart.plot_area.plots().unwrap()[0];
13299 let mut category_axis = sparse
13300 .chart
13301 .plot_area
13302 .axes()
13303 .unwrap()
13304 .into_iter()
13305 .find(|axis| axis.kind == AxisKind::Category)
13306 .unwrap();
13307 category_axis.major_gridlines = Some(super::ChartLines::default());
13308 category_axis.major_tick_mark = TickMark::Cross;
13309 let annotations = super::render_axes_and_labels(
13310 plot,
13311 &[category_axis],
13312 chart_bounds(),
13313 DispBlanksAs::Gap,
13314 &mut fonts,
13315 )
13316 .unwrap();
13317 assert_eq!(annotations.gridlines.len(), 3);
13318 assert_eq!(annotations.ticks.len(), 3);
13319 assert_eq!(annotations.labels.len(), 2);
13320
13321 let oversized_plot = sparse_plot.replacen(
13322 r#"<q:ptCount val="3"/>"#,
13323 r#"<q:ptCount val="4294967295"/>"#,
13324 1,
13325 );
13326 let oversized =
13327 CT_ChartSpace::from_xml(chart_with_plot(&oversized_plot).as_bytes()).unwrap();
13328 let plot = &oversized.chart.plot_area.plots().unwrap()[0];
13329 let category_axis = oversized
13330 .chart
13331 .plot_area
13332 .axes()
13333 .unwrap()
13334 .into_iter()
13335 .find(|axis| axis.kind == AxisKind::Category)
13336 .unwrap();
13337 let result = super::render_axes_and_labels(
13338 plot,
13339 &[category_axis],
13340 chart_bounds(),
13341 DispBlanksAs::Gap,
13342 &mut fonts,
13343 );
13344 let error = match result {
13345 Err(error) => error,
13346 Ok(_) => panic!("oversized category annotations rendered"),
13347 };
13348 assert!(
13349 error
13350 .to_string()
13351 .contains("logical category count 4294967295")
13352 );
13353 assert!(error.to_string().contains("exceeds 16384"));
13354 }
13355
13356 #[test]
13357 fn unsupported_numeric_category_formats_return_projection_errors() {
13358 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13359 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
13360 panic!("expected bar plot");
13361 };
13362 series[0].categories = Some(AxisData::Numeric(
13363 NumericData::new(
13364 "Sheet1!$A$2:$A$3".to_owned(),
13365 "General".to_owned(),
13366 vec![1.0, 2.0],
13367 )
13368 .unwrap(),
13369 ));
13370 let plot = &chart.chart.plot_area.plots().unwrap()[0];
13371 let mut category_axis = chart
13372 .chart
13373 .plot_area
13374 .axes()
13375 .unwrap()
13376 .into_iter()
13377 .find(|axis| axis.kind == AxisKind::Category)
13378 .unwrap();
13379 category_axis.number_format = Some(NumberFormat::new("0.00".to_owned(), false).unwrap());
13380 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13381 let annotations = super::render_axes_and_labels(
13382 plot,
13383 &[category_axis.clone()],
13384 chart_bounds(),
13385 DispBlanksAs::Gap,
13386 &mut fonts,
13387 )
13388 .unwrap();
13389 let text: Vec<_> = annotations
13390 .labels
13391 .iter()
13392 .filter_map(|element| match element {
13393 PositionedElement::Text(run) => Some(run.text.as_str()),
13394 _ => None,
13395 })
13396 .collect();
13397 assert_eq!(text, vec!["1.00", "2.00"]);
13398
13399 category_axis.number_format = Some(NumberFormat::new("#,##0".to_owned(), false).unwrap());
13400 let result = super::render_axes_and_labels(
13401 plot,
13402 &[category_axis],
13403 chart_bounds(),
13404 DispBlanksAs::Gap,
13405 &mut fonts,
13406 );
13407 let error = match result {
13408 Err(error) => error,
13409 Ok(_) => panic!("unsupported category-axis format rendered"),
13410 };
13411 assert!(
13412 error
13413 .to_string()
13414 .contains("c:numFmt/@formatCode projection")
13415 );
13416 assert!(error.to_string().contains("#,##0"));
13417 }
13418
13419 #[test]
13420 fn labels_and_legend_shape_with_deterministic_fonts() {
13421 let chart = labelled_bar_chart();
13422 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13423 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13424 let labels: Vec<_> = group
13425 .children
13426 .iter()
13427 .filter_map(|element| match element {
13428 PositionedElement::Text(run) => Some(run),
13429 _ => None,
13430 })
13431 .collect();
13432 for text in ["North", "South", "20", "Sales", "100"] {
13433 assert!(labels.iter().any(|run| run.text == text), "missing {text}");
13434 }
13435 let styled_tick = labels.iter().find(|run| run.text == "20").unwrap();
13436 assert_eq!(styled_tick.font_size, 12.0);
13437 assert!(styled_tick.bold);
13438 assert!(
13439 labels
13440 .iter()
13441 .all(|run| !run.glyph_ids.is_empty() && run.glyph_ids.len() == run.advances.len())
13442 );
13443 }
13444
13445 #[test]
13446 fn labelled_chart_raster_is_deterministic() {
13447 let raster = || {
13448 let chart = labelled_bar_chart();
13449 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13450 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13451 let layout = LayoutResult::new(
13452 vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
13453 fonts.all_font_data(),
13454 None,
13455 Vec::new(),
13456 );
13457 oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
13458 };
13459 assert_eq!(raster(), raster());
13460 }
13461
13462 fn labelled_bar_chart() -> CT_ChartSpace {
13463 let series = plot_series(0)
13464 .replace(
13465 "<q:cat>",
13466 "<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>",
13467 )
13468 .replace("<q:v>1</q:v>", "<q:v>0</q:v>")
13469 .replace("<q:v>2</q:v>", "<q:v>100</q:v>");
13470 let plot = format!(
13471 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>"#
13472 );
13473 let xml = chart_with_plot(&plot)
13474 .replacen(
13475 "<q:scaling/>",
13476 "<q:scaling><q:orientation val=\"minMax\"/></q:scaling>",
13477 1,
13478 )
13479 .replacen(
13480 "<q:scaling/>",
13481 "<q:scaling><q:max val=\"100\"/><q:min val=\"0\"/></q:scaling>",
13482 1,
13483 )
13484 .replace(
13485 "<q:axPos val=\"b\"/>",
13486 "<q:axPos val=\"b\"/><q:majorTickMark val=\"cross\"/>",
13487 )
13488 .replace(
13489 "<q:axPos val=\"l\"/>",
13490 "<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>",
13491 )
13492 .replace("</q:plotArea></q:chart>", "</q:plotArea><q:legend/></q:chart>");
13493 CT_ChartSpace::from_xml(xml.as_bytes()).unwrap()
13494 }
13495
13496 fn chart_bounds() -> Rect {
13497 Rect {
13498 x: 0.0,
13499 y: 0.0,
13500 width: 200.0,
13501 height: 140.0,
13502 }
13503 }
13504
13505 fn four_series_bar_chart() -> CT_ChartSpace {
13506 let series = (0..4).map(plot_series).collect::<String>();
13507 let plot = bar_plot("").replace(&plot_series(0), &series);
13508 CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
13509 }
13510
13511 fn seven_series_bar_chart() -> CT_ChartSpace {
13512 let series = (0..7).map(plot_series).collect::<String>();
13513 let plot = bar_plot("").replace(&plot_series(0), &series);
13514 CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
13515 }
13516
13517 fn path_colours(geometry: &ChartGeometry) -> Vec<Color> {
13518 let mut colours = geometry_children(geometry)
13519 .iter()
13520 .filter_map(|element| match element {
13521 PositionedElement::Path(path) => match path
13522 .fill
13523 .as_ref()
13524 .or_else(|| path.stroke.as_ref().map(|stroke| &stroke.paint))
13525 {
13526 Some(Paint::Solid(colour)) => Some(*colour),
13527 _ => None,
13528 },
13529 _ => None,
13530 })
13531 .collect::<Vec<_>>();
13532 colours.dedup_by(|left, right| left == right);
13533 colours
13534 }
13535
13536 fn resolved_layout_colour(colour: oxml_drawing::color::ResolvedColor) -> Color {
13537 Color {
13538 r: f64::from(colour.red) / 255.0,
13539 g: f64::from(colour.green) / 255.0,
13540 b: f64::from(colour.blue) / 255.0,
13541 a: f64::from(colour.alpha) / 255.0,
13542 }
13543 }
13544
13545 fn geometry_children(geometry: &ChartGeometry) -> &[PositionedElement] {
13546 let [PositionedElement::Group(group)] = geometry.elements.as_slice() else {
13547 panic!("chart geometry must contain one group");
13548 };
13549 &group.children
13550 }
13551
13552 fn path_commands(geometry: &ChartGeometry) -> Vec<&[PathCommand]> {
13553 geometry_children(geometry)
13554 .iter()
13555 .map(|element| {
13556 let PositionedElement::Path(path) = element else {
13557 panic!("chart geometry child must be a path");
13558 };
13559 path.path.commands.as_slice()
13560 })
13561 .collect()
13562 }
13563
13564 fn path_bounds(geometry: &ChartGeometry) -> Vec<Rect> {
13565 geometry_children(geometry)
13566 .iter()
13567 .map(|element| {
13568 let PositionedElement::Path(path) = element else {
13569 panic!("chart geometry child must be a path");
13570 };
13571 path.path.bounds().expect("chart path has bounds")
13572 })
13573 .collect()
13574 }
13575
13576 fn assert_rect_near(actual: Rect, expected: Rect) {
13577 assert_near(actual.x, expected.x);
13578 assert_near(actual.y, expected.y);
13579 assert_near(actual.width, expected.width);
13580 assert_near(actual.height, expected.height);
13581 }
13582
13583 fn assert_rects_near(actual: &[Rect], expected: &[Rect]) {
13584 assert_eq!(actual.len(), expected.len());
13585 for (actual, expected) in actual.iter().copied().zip(expected.iter().copied()) {
13586 assert_rect_near(actual, expected);
13587 }
13588 }
13589
13590 fn assert_path_points(commands: &[PathCommand], expected: &[Point], closed: bool) {
13591 let points: Vec<_> = commands
13592 .iter()
13593 .filter_map(|command| match command {
13594 PathCommand::MoveTo(point) | PathCommand::LineTo(point) => Some(*point),
13595 PathCommand::Close => None,
13596 PathCommand::CurveTo { .. } => panic!("expected polygonal path"),
13597 })
13598 .collect();
13599 assert_eq!(points.len(), expected.len());
13600 for (actual, expected) in points.into_iter().zip(expected.iter().copied()) {
13601 assert_point_near(actual, expected);
13602 }
13603 assert_eq!(commands.last() == Some(&PathCommand::Close), closed);
13604 }
13605
13606 fn assert_point_near(actual: Point, expected: Point) {
13607 assert_near(actual.x, expected.x);
13608 assert_near(actual.y, expected.y);
13609 }
13610
13611 fn assert_near(actual: f64, expected: f64) {
13612 assert!(
13613 (actual - expected).abs() < 1e-9,
13614 "expected {expected}, got {actual}"
13615 );
13616 }
13617
13618 #[test]
13619 fn remaining_v1_plots_round_trip_and_render() {
13620 for (kind, plot, axes) in remaining_plot_fixtures() {
13621 let xml = chart_with_optional_axes(&plot, axes);
13622 let parsed = CT_ChartSpace::from_xml(xml.as_bytes())
13623 .unwrap_or_else(|error| panic!("{kind}: parse failed: {error}"));
13624 assert_eq!(parsed.chart.plot_area.plots().unwrap().len(), 1);
13625 assert_eq!(
13626 parsed.chart.plot_area.axes().unwrap().len(),
13627 usize::from(axes) * 2
13628 );
13629 let written = parsed
13630 .to_xml()
13631 .unwrap_or_else(|error| panic!("{kind}: write failed: {error}"));
13632 assert_eq!(
13633 parsed,
13634 CT_ChartSpace::from_xml(&written)
13635 .unwrap_or_else(|error| panic!("{kind}: reparse failed: {error}"))
13636 );
13637 }
13638 if let Some(corpus) = require_or_skip_corpus() {
13639 verify_remaining_plot_viewer_gate(&corpus);
13640 }
13641 }
13642
13643 #[test]
13644 fn remaining_plot_families_write_fixed_prefixes_in_schema_order() {
13645 for (kind, plot, axes) in remaining_plot_fixtures() {
13646 let parsed =
13647 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
13648 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13649 assert!(written.contains(&format!("<c:{kind}")));
13650 assert!(!written.contains(&format!("<q:{kind}")));
13651 let series = written.find("<c:ser").unwrap();
13652 let labels = written.find("<c:dLbls").unwrap();
13653 assert!(series < labels, "{kind}: series must precede labels");
13654 if matches!(kind, "areaChart" | "scatterChart" | "radarChart") {
13655 let property = match kind {
13656 "areaChart" => "<c:grouping",
13657 "scatterChart" => "<c:scatterStyle",
13658 "radarChart" => "<c:radarStyle",
13659 _ => unreachable!(),
13660 };
13661 assert!(written.find(property).unwrap() < series);
13662 assert!(labels < written.find("<c:axId").unwrap());
13663 } else {
13664 assert!(labels < written.find("<c:firstSliceAng").unwrap());
13665 }
13666 if kind == "doughnutChart" {
13667 assert!(
13668 written.find("<c:firstSliceAng").unwrap()
13669 < written.find("<c:holeSize").unwrap()
13670 );
13671 }
13672 }
13673 }
13674
13675 #[test]
13676 fn scatter_series_map_numeric_categories_and_values_to_x_and_y() {
13677 let plot = remaining_plot_fixtures()
13678 .into_iter()
13679 .find(|(kind, _, _)| *kind == "scatterChart")
13680 .unwrap()
13681 .1;
13682 let mut parsed =
13683 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, true).as_bytes()).unwrap();
13684 let standalone_series = {
13685 let Plot::Scatter { series, .. } = &mut parsed.chart.plot_area.plots_mut().unwrap()[0]
13686 else {
13687 panic!("expected scatter plot");
13688 };
13689 let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
13690 panic!("expected numeric x values");
13691 };
13692 x_values.values[0] = 3.5;
13693 series[0].values.values[0] = 7.5;
13694 series[0].clone()
13695 };
13696 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13697 assert!(written.contains("<c:xVal>"));
13698 assert!(written.contains("<c:yVal>"));
13699 assert!(!written.contains("<c:cat>"));
13700 assert!(!written.contains("<c:val>"));
13701 assert!(written.contains("<c:v>3.5</c:v>"));
13702 assert!(written.contains("<c:v>7.5</c:v>"));
13703 let standalone = String::from_utf8(standalone_series.to_xml().unwrap()).unwrap();
13704 assert!(standalone.contains("<c:xVal>"));
13705 assert!(standalone.contains("<c:yVal>"));
13706 assert!(!standalone.contains("<c:cat>"));
13707 assert!(!standalone.contains("<c:val>"));
13708 }
13709
13710 #[test]
13711 fn malformed_remaining_plots_return_errors_without_panicking() {
13712 let series = plot_series(0);
13713 let scatter = numeric_plot_series(0);
13714 let opaque_bubble = series.replace(
13715 "</q:val></q:ser>",
13716 "</q:val><q:bubbleSize><x:opaque/></q:bubbleSize></q:ser>",
13717 );
13718 let cases = [
13719 r#"<q:pieChart/>"#.to_owned(),
13720 format!(r#"<q:pieChart>{series}<q:firstSliceAng val="361"/></q:pieChart>"#),
13721 format!(
13722 r#"<q:pieChart>{series}<q:firstSliceAng val="1"/><q:firstSliceAng val="2"/></q:pieChart>"#
13723 ),
13724 format!(r#"<q:doughnutChart>{series}<q:holeSize val="9"/></q:doughnutChart>"#),
13725 format!(
13726 r#"<q:doughnutChart>{series}<q:holeSize val="50"/><q:holeSize val="60"/></q:doughnutChart>"#
13727 ),
13728 format!(
13729 r#"<q:areaChart><q:grouping val="clustered"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
13730 ),
13731 format!(
13732 r#"<q:areaChart><q:grouping val="standard"/><q:grouping val="stacked"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
13733 ),
13734 format!(
13735 r#"<q:scatterChart><q:scatterStyle val="dots"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13736 ),
13737 format!(
13738 r#"<q:scatterChart><q:scatterStyle val="marker"/><q:scatterStyle val="line"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13739 ),
13740 format!(
13741 r#"<q:radarChart><q:radarStyle val="smooth"/>{series}<q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
13742 ),
13743 format!(
13744 r#"<q:radarChart><q:radarStyle val="marker"/>{series}<q:dLbls/><q:dLbls/><q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
13745 ),
13746 format!(r#"<q:pieChart>{series}<q:axId val="1"/></q:pieChart>"#),
13747 format!(
13748 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/></q:areaChart>"#
13749 ),
13750 r#"<q:areaChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:areaChart>"#.to_owned(),
13751 format!(
13752 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/><q:axId val="2"/><q:axId val="3"/></q:areaChart>"#
13753 ),
13754 format!(
13755 r#"<q:scatterChart><q:scatterStyle val="marker"/>{series}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13756 ),
13757 format!(
13758 r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13759 scatter
13760 .replace("<q:yVal>", "<q:val>")
13761 .replace("</q:yVal>", "</q:val>")
13762 ),
13763 format!(
13764 r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13765 scatter_without_x(0)
13766 ),
13767 format!(
13768 r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13769 scatter_without_y(0)
13770 ),
13771 format!(r#"<q:pieChart>{opaque_bubble}</q:pieChart>"#),
13772 ];
13773 for plot in cases {
13774 let xml = chart_with_optional_axes(&plot, false);
13775 let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
13776 assert!(result.is_ok(), "parser panicked for {plot}");
13777 assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
13778 }
13779
13780 let missing_axis = format!(
13781 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="7"/></q:areaChart>"#
13782 );
13783 let missing_axis =
13784 CT_ChartSpace::from_xml(chart_with_optional_axes(&missing_axis, true).as_bytes())
13785 .unwrap_err();
13786 assert!(missing_axis.to_string().contains("missing axis 7"));
13787
13788 let values =
13789 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
13790 let mut bubble_series = Series::new(0, 0, values.clone());
13791 bubble_series.bubble_size = Some(values);
13792 assert!(Plot::pie(vec![bubble_series]).is_err());
13793 }
13794
13795 #[test]
13796 fn unsupported_plot_families_and_children_remain_byte_preserved() {
13797 for raw in [
13798 r#"<q:pie3DChart x:keep="3d"><q:ser><x:opaque/></q:ser></q:pie3DChart>"#,
13799 r#"<q:ofPieChart x:keep="of"><x:opaque/></q:ofPieChart>"#,
13800 r#"<q:stockChart x:keep="stock"><x:opaque/></q:stockChart>"#,
13801 r#"<q:surfaceChart x:keep="surface"><x:opaque/></q:surfaceChart>"#,
13802 r#"<q:bubbleChart x:keep="bubble"><x:opaque/></q:bubbleChart>"#,
13803 ] {
13804 let parsed =
13805 CT_ChartSpace::from_xml(chart_with_optional_axes(raw, false).as_bytes()).unwrap();
13806 assert!(parsed.chart.plot_area.plots().is_err());
13807 let written = parsed.to_xml().unwrap();
13808 assert!(
13809 written
13810 .windows(raw.len())
13811 .any(|window| window == raw.as_bytes())
13812 );
13813 }
13814
13815 let pie = remaining_plot_fixtures()[0].1.clone();
13816 let preserved = r#"<!--point--><q:dPt x:id="1"><q:explosion val="7"/><x:tail/></q:dPt>"#;
13817 let plot = pie.replace("<q:dLbls>", &format!("{preserved}<q:dLbls>"));
13818 let parsed =
13819 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, false).as_bytes()).unwrap();
13820 let written = parsed.to_xml().unwrap();
13821 assert!(
13822 written
13823 .windows(preserved.len())
13824 .any(|window| window == preserved.as_bytes())
13825 );
13826
13827 for (kind, mut plot, axes) in [
13828 ("pieChart", remaining_plot_fixtures()[0].1.clone(), false),
13829 ("areaChart", remaining_plot_fixtures()[2].1.clone(), true),
13830 ] {
13831 plot = plot.replace(r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#, "");
13832 if kind == "pieChart" {
13833 plot = plot.replace(
13834 "</q:extLst></q:pieChart>",
13835 "</q:extLst><!--after-ext--></q:pieChart>",
13836 );
13837 } else {
13838 plot = plot.replace(
13839 "</q:dropLines><q:axId",
13840 "</q:dropLines><!--after-drop-lines--><q:axId",
13841 );
13842 }
13843 let mut parsed =
13844 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
13845 let labels =
13846 CT_DLbls::from_xml(format!(r#"<c:dLbls xmlns:c="{C_NS}"/>"#).as_bytes()).unwrap();
13847 match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
13848 Plot::Pie { data_labels, .. } | Plot::Area { data_labels, .. } => {
13849 *data_labels = Some(labels)
13850 }
13851 _ => panic!("expected pie or area"),
13852 }
13853 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13854 assert!(
13855 written.find("<c:dLbls").unwrap() < written.find("<q:extLst").unwrap(),
13856 "{kind}: inserted labels must precede the preserved extension"
13857 );
13858 if kind == "areaChart" {
13859 assert!(written.find("<c:dLbls").unwrap() < written.find("<q:dropLines").unwrap());
13860 assert!(
13861 written.find("<q:dropLines").unwrap()
13862 < written.find("<!--after-drop-lines-->").unwrap()
13863 );
13864 } else {
13865 assert!(
13866 written.find("<q:extLst").unwrap() < written.find("<!--after-ext-->").unwrap()
13867 );
13868 }
13869 }
13870 }
13871
13872 #[test]
13873 fn every_supported_corpus_plot_round_trips_structurally() {
13874 let Some(corpus) = require_or_skip_corpus() else {
13875 return;
13876 };
13877 verify_fetched_corpus(&corpus);
13878 let mut pie_count = 0usize;
13879 let mut bar_count = 0usize;
13880 let mut line_count = 0usize;
13881 for path in manifest_paths() {
13882 let package = OpcPackage::open(corpus.join(path)).unwrap();
13883 for (part, xml) in &package.parts {
13884 if !is_chart_part(part) {
13885 continue;
13886 }
13887 let parsed = CT_ChartSpace::from_xml(xml).unwrap();
13888 if let Ok(plots) = parsed.chart.plot_area.plots() {
13889 for plot in plots {
13890 match plot {
13891 Plot::Pie { .. } => pie_count += 1,
13892 Plot::Bar { .. } => bar_count += 1,
13893 Plot::Line { .. } => line_count += 1,
13894 _ => {}
13895 }
13896 }
13897 }
13898 let written = parsed.to_xml().unwrap();
13899 assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
13900 }
13901 }
13902 assert_eq!(pie_count, 1, "typed corpus pie coverage changed");
13903 assert_eq!(bar_count, 11, "typed corpus bar coverage changed");
13904 assert_eq!(line_count, 2, "typed corpus line coverage changed");
13905 }
13906
13907 fn standalone_axis(local: &str, children: &str) -> String {
13908 format!(
13909 r#"<c:{local} xmlns:c="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}">{children}</c:{local}>"#
13910 )
13911 }
13912
13913 fn minimal_axis(local: &str, id: &str, position: &str, cross_axis: &str) -> String {
13914 format!(
13915 r#"<c:{local}><c:axId val="{id}"/><c:scaling/><c:axPos val="{position}"/><c:crossAx val="{cross_axis}"/></c:{local}>"#
13916 )
13917 }
13918
13919 fn chart_with_axes(axes: &[String]) -> String {
13920 format!(
13921 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>"#,
13922 axes.join("")
13923 )
13924 }
13925
13926 #[test]
13927 fn data_labels_write_fixed_prefixes_in_schema_order() {
13928 let xml = format!(
13929 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>"#
13930 );
13931 let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
13932 assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0%");
13933 assert_eq!(parsed.position, Some(DataLabelPosition::OutsideEnd));
13934 assert_eq!(parsed.separator.as_deref(), Some(" / "));
13935 assert!(parsed.show_legend_key);
13936 assert!(parsed.show_value);
13937 assert!(!parsed.show_category_name);
13938 assert!(parsed.show_series_name);
13939 assert!(!parsed.show_percent);
13940 assert!(parsed.show_bubble_size);
13941
13942 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13943 assert!(written.starts_with(&format!(
13944 "<c:dLbls xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
13945 )));
13946 let mut cursor = 0usize;
13947 for tag in [
13948 "<q:dLbl",
13949 "<c:numFmt",
13950 "<q:spPr",
13951 "<q:txPr",
13952 "<c:dLblPos",
13953 "<c:showLegendKey",
13954 "<c:showVal",
13955 "<c:showCatName",
13956 "<c:showSerName",
13957 "<c:showPercent",
13958 "<c:showBubbleSize",
13959 "<c:separator",
13960 "<q:leaderLines",
13961 "<q:extLst",
13962 ] {
13963 let position = written[cursor..]
13964 .find(tag)
13965 .unwrap_or_else(|| panic!("missing ordered data-label tag {tag}"));
13966 cursor += position + tag.len();
13967 }
13968 assert_eq!(parsed, CT_DLbls::from_xml(written.as_bytes()).unwrap());
13969 }
13970
13971 #[test]
13972 fn percentage_formatted_label_renders_with_correct_text() {
13973 let Some(corpus) = require_or_skip_corpus() else {
13974 return;
13975 };
13976 verify_fetched_corpus(&corpus);
13977 assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
13978 assert_command_version("pdftotext", &["-v"], PDFTOTEXT_VERSION);
13979
13980 let source = corpus.join("bar-chart.pptx");
13981 let mut package = OpcPackage::open(&source).expect("open percentage-label source deck");
13982 let chart_part = "/ppt/charts/chart1.xml";
13983 let original_parts = package.parts.clone();
13984 let chart_xml = package
13985 .get_part(chart_part)
13986 .expect("bar chart part")
13987 .to_vec();
13988 let original_series = first_series_xml(&chart_xml).expect("bar chart series");
13989 let mut series = Series::from_xml(&original_series).expect("parse bar chart series");
13990 series.values.values.fill(0.25);
13991 let labels = CT_DLbls {
13992 number_format: Some(NumberFormat::new("0%".to_owned(), false).unwrap()),
13993 position: Some(DataLabelPosition::OutsideEnd),
13994 show_value: true,
13995 ..CT_DLbls::default()
13996 };
13997 series.data_labels = Some(labels);
13998 let replacement = series.to_xml().expect("serialize typed percentage labels");
13999 let candidate_chart = replace_once(&chart_xml, &original_series, &replacement);
14000 package.set_part(chart_part, candidate_chart);
14001 for (part, bytes) in &original_parts {
14002 if part == chart_part {
14003 assert_ne!(package.parts[part], *bytes);
14004 } else {
14005 assert_eq!(
14006 package.parts[part], *bytes,
14007 "unexpected changed part {part}"
14008 );
14009 }
14010 }
14011
14012 let temp_root =
14013 std::env::temp_dir().join(format!("oxml-chart-f123-label-gate-{}", std::process::id()));
14014 let profile = std::env::temp_dir().join(format!(
14015 "oxml-chart-f123-label-profile-{}",
14016 std::process::id()
14017 ));
14018 if temp_root.exists() {
14019 fs::remove_dir_all(&temp_root).expect("remove stale F-123 evidence");
14020 }
14021 if profile.exists() {
14022 fs::remove_dir_all(&profile).expect("remove stale F-123 profile");
14023 }
14024 fs::create_dir_all(&temp_root).expect("create F-123 evidence root");
14025 let unbound_candidate = temp_root.join("f123-percentage-label.pptx");
14026 package
14027 .save(&unbound_candidate)
14028 .expect("save percentage-label candidate");
14029 let candidate_sha = sha256(&unbound_candidate);
14030 let evidence_dir = temp_root.join(&candidate_sha);
14031 fs::create_dir(&evidence_dir).expect("create SHA-bound evidence directory");
14032 let candidate = evidence_dir.join("f123-percentage-label.pptx");
14033 fs::rename(&unbound_candidate, &candidate).expect("bind candidate to evidence SHA");
14034 assert_eq!(sha256(&candidate), candidate_sha);
14035
14036 let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
14037 let conversion = Command::new("soffice")
14038 .args([
14039 "--headless",
14040 &profile_argument,
14041 "--convert-to",
14042 "pdf:impress_pdf_Export",
14043 "--outdir",
14044 ])
14045 .arg(&evidence_dir)
14046 .arg(&candidate)
14047 .output()
14048 .expect("run pinned LibreOffice percentage-label gate");
14049 assert!(
14050 conversion.status.success(),
14051 "LibreOffice conversion failed: {}",
14052 String::from_utf8_lossy(&conversion.stderr)
14053 );
14054 let pdf = evidence_dir.join("f123-percentage-label.pdf");
14055 assert!(
14056 pdf.is_file(),
14057 "LibreOffice did not create {}",
14058 pdf.display()
14059 );
14060 let text_path = evidence_dir.join("f123-percentage-label.txt");
14061 let extraction = Command::new("pdftotext")
14062 .arg(&pdf)
14063 .arg(&text_path)
14064 .output()
14065 .expect("run pinned Poppler percentage-label extraction");
14066 assert!(
14067 extraction.status.success(),
14068 "pdftotext failed: {}",
14069 String::from_utf8_lossy(&extraction.stderr)
14070 );
14071 let extracted = fs::read_to_string(&text_path).expect("read percentage-label text");
14072 assert!(
14073 extracted.contains("25%"),
14074 "SHA-bound viewer text lacks 25%: {extracted:?}"
14075 );
14076 for artifact in [&candidate, &pdf, &text_path] {
14077 assert_eq!(
14078 artifact.parent(),
14079 Some(evidence_dir.as_path()),
14080 "evidence escaped candidate SHA"
14081 );
14082 }
14083 eprintln!("F-123 candidate/render/text evidence SHA-256 {candidate_sha}, extracted 25%");
14084 fs::remove_dir_all(&temp_root).expect("remove F-123 evidence");
14085 if profile.exists() {
14086 fs::remove_dir_all(&profile).expect("remove F-123 profile");
14087 }
14088 }
14089
14090 #[test]
14091 fn common_number_formats_project_cached_values_deterministically() {
14092 for (code, value, expected) in [
14093 ("General", 0.25, "0.25"),
14094 ("0", 12.6, "13"),
14095 ("0.0", 12.64, "12.6"),
14096 ("0.00", -12.645, "-12.64"),
14097 ("0%", 0.25, "25%"),
14098 ("0.0%", 0.256, "25.6%"),
14099 ("0.00%", 0.256, "25.60%"),
14100 ] {
14101 let format = NumberFormat::new(code.to_owned(), false).unwrap();
14102 assert_eq!(format.format_value(value).unwrap(), expected, "{code}");
14103 }
14104 }
14105
14106 #[test]
14107 fn malformed_data_labels_and_number_formats_return_errors_without_panicking() {
14108 let cases = [
14109 format!(
14110 r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="1"/><c:showVal val="0"/></c:dLbls>"#
14111 ),
14112 format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="yes"/></c:dLbls>"#),
14113 format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:dLblPos val="middle"/></c:dLbls>"#),
14114 format!(
14115 r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="" sourceLinked="1"/></c:dLbls>"#
14116 ),
14117 format!(
14118 r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="0" sourceLinked="maybe"/></c:dLbls>"#
14119 ),
14120 ];
14121 for xml in cases {
14122 let result = std::panic::catch_unwind(|| CT_DLbls::from_xml(xml.as_bytes()));
14123 assert!(result.is_ok(), "data-label parser panicked for {xml}");
14124 assert!(result.unwrap().is_err(), "malformed labels parsed: {xml}");
14125 }
14126
14127 let invalid_code = NumberFormat::new("bad\u{0}code".to_owned(), false);
14128 assert!(invalid_code.is_err());
14129 let unsupported = NumberFormat::new("#,##0".to_owned(), false).unwrap();
14130 assert!(unsupported.format_value(12.0).is_err());
14131 let general = NumberFormat::new("General".to_owned(), false).unwrap();
14132 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
14133 assert!(general.format_value(value).is_err());
14134 }
14135
14136 let invalid_separator = CT_DLbls {
14137 separator: Some("bad\u{0}separator".to_owned()),
14138 ..CT_DLbls::default()
14139 };
14140 assert!(invalid_separator.to_xml().is_err());
14141 }
14142
14143 #[test]
14144 fn data_labels_preserve_point_overrides_and_extensions_byte_for_byte() {
14145 let xml = format!(
14146 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>"#
14147 );
14148 let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
14149 let written = parsed.to_xml().unwrap();
14150 for raw in [
14151 br#"<!--before-->"#.as_slice(),
14152 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(),
14153 br#"<?after-point?>"#.as_slice(),
14154 br#"<q:spPr><x:collection-shape/></q:spPr>"#.as_slice(),
14155 br#"<q:txPr><x:collection-text/></q:txPr>"#.as_slice(),
14156 br#"<q:showLeaderLines val="1"/>"#.as_slice(),
14157 br#"<q:leaderLines><q:spPr><x:leader-shape/></q:spPr></q:leaderLines>"#.as_slice(),
14158 br#"<q:extLst><q:ext uri="collection"><x:tail/></q:ext></q:extLst>"#.as_slice(),
14159 br#"<!--after-->"#.as_slice(),
14160 ] {
14161 assert!(
14162 written.windows(raw.len()).any(|window| window == raw),
14163 "preserved data-label bytes changed: {}",
14164 String::from_utf8_lossy(raw)
14165 );
14166 }
14167 assert_eq!(parsed, CT_DLbls::from_xml(&written).unwrap());
14168 }
14169
14170 #[test]
14171 fn every_corpus_data_label_collection_round_trips_structurally() {
14172 let Some(corpus) = require_or_skip_corpus() else {
14173 return;
14174 };
14175 verify_fetched_corpus(&corpus);
14176 let mut label_collections = 0usize;
14177 let mut axis_number_formats = 0usize;
14178 for path in manifest_paths() {
14179 let package = OpcPackage::open(corpus.join(path))
14180 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14181 for (part, xml) in &package.parts {
14182 if !is_chart_part(part) {
14183 continue;
14184 }
14185 let chart = CT_ChartSpace::from_xml(xml)
14186 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14187 for series in chart.chart.plot_area.series().unwrap_or_else(|error| {
14188 panic!("{path} {part}: series projection failed: {error}")
14189 }) {
14190 if let Some(labels) = series.data_labels {
14191 let written = labels.to_xml().unwrap_or_else(|error| {
14192 panic!("{path} {part}: label write failed: {error}")
14193 });
14194 let reparsed = CT_DLbls::from_xml(&written).unwrap_or_else(|error| {
14195 panic!("{path} {part}: written labels parse failed: {error}")
14196 });
14197 assert_eq!(labels, reparsed, "{path} {part}: labels changed");
14198 label_collections += 1;
14199 }
14200 }
14201 for axis in chart.chart.plot_area.axes().unwrap_or_else(|error| {
14202 panic!("{path} {part}: axis projection failed: {error}")
14203 }) {
14204 if axis.number_format.is_some() {
14205 let written = axis.to_xml().unwrap_or_else(|error| {
14206 panic!("{path} {part}: axis write failed: {error}")
14207 });
14208 let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
14209 panic!("{path} {part}: written axis parse failed: {error}")
14210 });
14211 assert_eq!(axis, reparsed, "{path} {part}: axis changed");
14212 axis_number_formats += 1;
14213 }
14214 }
14215 }
14216 }
14217 assert!(label_collections > 0, "the pinned corpus has no c:dLbls");
14218 assert!(
14219 axis_number_formats > 0,
14220 "the pinned corpus has no axis c:numFmt"
14221 );
14222 eprintln!(
14223 "ChartML data-label corpus gate checked {label_collections} collections and {axis_number_formats} axis number formats"
14224 );
14225 }
14226
14227 fn full_axis(local: &str, position: &str) -> String {
14228 format!(
14229 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}>"#
14230 )
14231 }
14232
14233 #[test]
14234 fn axis_id_pairs_are_reciprocal() {
14235 assert_eq!(
14236 AxisId::new(i64::from(i32::MIN)).unwrap().value(),
14237 i64::from(i32::MIN)
14238 );
14239 assert_eq!(
14240 AxisId::new(i64::from(u32::MAX)).unwrap().value(),
14241 i64::from(u32::MAX)
14242 );
14243 assert!(AxisId::new(i64::from(i32::MIN) - 1).is_err());
14244 assert!(AxisId::new(i64::from(u32::MAX) + 1).is_err());
14245
14246 let valid = chart_with_axes(&[
14247 minimal_axis("catAx", "-1884094432", "b", "-1884097184"),
14248 minimal_axis("valAx", "-1884097184", "l", "-1884094432"),
14249 ]);
14250 let axes = CT_ChartSpace::from_xml(valid.as_bytes())
14251 .unwrap()
14252 .chart
14253 .plot_area
14254 .axes()
14255 .unwrap();
14256 assert_eq!(axes.len(), 2);
14257 assert_eq!(axes[0].id.value(), -1_884_094_432);
14258 assert_eq!(axes[0].cross_axis, axes[1].id);
14259 assert_eq!(axes[1].cross_axis, axes[0].id);
14260
14261 let invalid_sets = [
14262 vec![
14263 minimal_axis("catAx", "1", "b", "2"),
14264 minimal_axis("valAx", "1", "l", "2"),
14265 ],
14266 vec![minimal_axis("catAx", "1", "b", "1")],
14267 vec![minimal_axis("catAx", "1", "b", "2")],
14268 vec![
14269 minimal_axis("catAx", "1", "b", "2"),
14270 minimal_axis("valAx", "2", "l", "3"),
14271 minimal_axis("serAx", "3", "r", "2"),
14272 ],
14273 ];
14274 for axes in invalid_sets {
14275 let chart = CT_ChartSpace::from_xml(chart_with_axes(&axes).as_bytes()).unwrap();
14276 assert!(chart.chart.plot_area.axes().is_err());
14277 }
14278
14279 let empty = CT_ChartSpace::from_xml(chart_with_axes(&[]).as_bytes()).unwrap();
14280 assert!(empty.chart.plot_area.axes().unwrap().is_empty());
14281 }
14282
14283 #[test]
14284 fn all_axis_forms_write_fixed_prefixes_in_schema_order() {
14285 for (local, kind, position) in [
14286 ("catAx", AxisKind::Category, AxisPosition::Bottom),
14287 ("valAx", AxisKind::Value, AxisPosition::Left),
14288 ("dateAx", AxisKind::Date, AxisPosition::Top),
14289 ("serAx", AxisKind::Series, AxisPosition::Right),
14290 ] {
14291 let xml = full_axis(local, position.as_str());
14292 let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
14293 assert_eq!(parsed.kind, kind);
14294 assert_eq!(parsed.id.value(), -1_125_255_920);
14295 assert_eq!(parsed.scaling.log_base, Some(10.0));
14296 assert_eq!(parsed.scaling.orientation, Orientation::MaxMin);
14297 assert_eq!(parsed.scaling.maximum, Some(100.0));
14298 assert_eq!(parsed.scaling.minimum, Some(-10.0));
14299 assert!(parsed.deleted);
14300 assert_eq!(parsed.position, position);
14301 assert!(parsed.major_gridlines.is_some());
14302 assert!(parsed.minor_gridlines.is_some());
14303 assert!(parsed.title.is_some());
14304 assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0");
14305 assert_eq!(parsed.major_tick_mark, TickMark::Cross);
14306 assert_eq!(parsed.minor_tick_mark, TickMark::Inside);
14307 assert_eq!(parsed.tick_label_position, TickLabelPosition::High);
14308 assert!(parsed.sp_pr.is_some());
14309 assert!(parsed.tx_pr.is_some());
14310 assert_eq!(parsed.cross_axis.value(), -1_004_303_696);
14311
14312 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14313 assert!(written.starts_with(&format!(
14314 "<c:{local} xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
14315 )));
14316 assert!(written.contains("<c:axId val=\"-1125255920\""));
14317 let mut cursor = 0usize;
14318 for tag in [
14319 "<c:axId",
14320 "<c:scaling",
14321 "<c:delete",
14322 "<c:axPos",
14323 "<c:majorGridlines",
14324 "<c:minorGridlines",
14325 "<c:title",
14326 "<c:numFmt",
14327 "<c:majorTickMark",
14328 "<c:minorTickMark",
14329 "<c:tickLblPos",
14330 "<c:spPr",
14331 "<c:txPr",
14332 "<c:crossAx",
14333 ] {
14334 let position = written[cursor..]
14335 .find(tag)
14336 .unwrap_or_else(|| panic!("missing ordered axis tag {tag}"));
14337 cursor += position + tag.len();
14338 }
14339 assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
14340 }
14341
14342 let mut constructed = Axis::new(
14343 AxisKind::Value,
14344 AxisId::new(1).unwrap(),
14345 AxisPosition::Left,
14346 AxisId::new(2).unwrap(),
14347 );
14348 assert_eq!(
14349 constructed,
14350 Axis::from_xml(&constructed.to_xml().unwrap()).unwrap()
14351 );
14352 constructed.kind = AxisKind::Category;
14353 constructed.id = AxisId::new(3).unwrap();
14354 constructed.cross_axis = AxisId::new(4).unwrap();
14355 constructed.scaling.minimum = Some(0.0);
14356 constructed.scaling.maximum = Some(10.0);
14357 let written = constructed.to_xml().unwrap();
14358 assert!(written.starts_with(b"<c:catAx"));
14359 assert_eq!(constructed, Axis::from_xml(&written).unwrap());
14360 }
14361
14362 #[test]
14363 fn axis_equality_normalizes_ids_without_losing_lexical_preservation() {
14364 let padded = Axis::from_xml(
14365 standalone_axis(
14366 "catAx",
14367 "<c:axId val=\"01\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"002\"/>",
14368 )
14369 .as_bytes(),
14370 )
14371 .unwrap();
14372 let normalized = Axis::from_xml(
14373 standalone_axis(
14374 "catAx",
14375 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14376 )
14377 .as_bytes(),
14378 )
14379 .unwrap();
14380 assert_eq!(padded, normalized);
14381
14382 let preserved = String::from_utf8(padded.to_xml().unwrap()).unwrap();
14383 assert!(preserved.contains("<c:axId val=\"01\""));
14384 assert!(preserved.contains("<c:crossAx val=\"002\""));
14385
14386 let mut mutated = padded;
14387 mutated.id = AxisId::new(3).unwrap();
14388 mutated.cross_axis = AxisId::new(4).unwrap();
14389 let rewritten = String::from_utf8(mutated.to_xml().unwrap()).unwrap();
14390 assert!(rewritten.contains("<c:axId val=\"3\""));
14391 assert!(rewritten.contains("<c:crossAx val=\"4\""));
14392 assert!(!rewritten.contains("val=\"01\""));
14393 assert!(!rewritten.contains("val=\"002\""));
14394 }
14395
14396 #[test]
14397 fn malformed_axis_values_return_errors_without_panicking() {
14398 let cases = [
14399 standalone_axis(
14400 "catAx",
14401 "<c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14402 ),
14403 standalone_axis(
14404 "catAx",
14405 "<c:axId val=\"1\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14406 ),
14407 standalone_axis(
14408 "catAx",
14409 "<c:axId val=\"1\"/><c:scaling/><c:crossAx val=\"2\"/>",
14410 ),
14411 standalone_axis(
14412 "catAx",
14413 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/>",
14414 ),
14415 standalone_axis(
14416 "catAx",
14417 "<c:axId val=\"-2147483649\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14418 ),
14419 standalone_axis(
14420 "catAx",
14421 "<c:axId val=\"4294967296\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14422 ),
14423 standalone_axis(
14424 "catAx",
14425 "<c:axId val=\"1\"/><c:scaling><c:logBase val=\"1\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14426 ),
14427 standalone_axis(
14428 "catAx",
14429 "<c:axId val=\"1\"/><c:scaling><c:max val=\"NaN\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14430 ),
14431 standalone_axis(
14432 "catAx",
14433 "<c:axId val=\"1\"/><c:scaling><c:max val=\"1\"/><c:min val=\"2\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14434 ),
14435 standalone_axis(
14436 "catAx",
14437 "<c:axId val=\"1\"/><c:scaling><c:orientation val=\"sideways\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14438 ),
14439 standalone_axis(
14440 "catAx",
14441 "<c:axId val=\"1\"/><c:scaling/><c:delete val=\"yes\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14442 ),
14443 standalone_axis(
14444 "catAx",
14445 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"middle\"/><c:crossAx val=\"2\"/>",
14446 ),
14447 standalone_axis(
14448 "catAx",
14449 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:majorTickMark val=\"near\"/><c:crossAx val=\"2\"/>",
14450 ),
14451 standalone_axis(
14452 "catAx",
14453 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:tickLblPos val=\"middle\"/><c:crossAx val=\"2\"/>",
14454 ),
14455 standalone_axis(
14456 "catAx",
14457 "<c:axId val=\"1\"/><c:axId val=\"2\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14458 ),
14459 standalone_axis(
14460 "catAx",
14461 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt sourceLinked=\"1\"/><c:crossAx val=\"2\"/>",
14462 ),
14463 standalone_axis(
14464 "catAx",
14465 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt formatCode=\"0\" sourceLinked=\"maybe\"/><c:crossAx val=\"2\"/>",
14466 ),
14467 ];
14468 for xml in cases {
14469 let result = std::panic::catch_unwind(|| Axis::from_xml(xml.as_bytes()));
14470 assert!(result.is_ok(), "axis parser panicked for {xml}");
14471 assert!(result.unwrap().is_err(), "malformed axis parsed: {xml}");
14472 }
14473
14474 let mut axis = Axis::new(
14475 AxisKind::Value,
14476 AxisId::new(1).unwrap(),
14477 AxisPosition::Left,
14478 AxisId::new(2).unwrap(),
14479 );
14480 axis.scaling.maximum = Some(f64::INFINITY);
14481 assert!(axis.to_xml().is_err());
14482 }
14483
14484 #[test]
14485 fn axes_preserve_unmodelled_children_byte_for_byte() {
14486 let xml = format!(
14487 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>"#
14488 );
14489 let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
14490 let mut relabelled = parsed.clone();
14491 relabelled.kind = AxisKind::Value;
14492 assert!(
14493 relabelled.to_xml().is_err(),
14494 "a parsed category axis must not retain its tail under a value-axis root"
14495 );
14496 let written = parsed.to_xml().unwrap();
14497 for raw in [
14498 br#"<!--before-->"#.as_slice(),
14499 br#"<?after-id?>"#.as_slice(),
14500 br#"<!--scale-->"#.as_slice(),
14501 br#"<q:extLst><q:ext uri="scale"><x:data/></q:ext></q:extLst>"#.as_slice(),
14502 br#"<x:grid/>"#.as_slice(),
14503 br#"<x:title/>"#.as_slice(),
14504 br#"<q:crosses val="autoZero"/>"#.as_slice(),
14505 br#"<q:auto val="1"><x:auto/></q:auto>"#.as_slice(),
14506 br#"<q:extLst><q:ext uri="axis"><x:tail/></q:ext></q:extLst>"#.as_slice(),
14507 br#"<!--after-->"#.as_slice(),
14508 ] {
14509 assert!(
14510 written.windows(raw.len()).any(|window| window == raw),
14511 "preserved axis bytes changed: {}",
14512 String::from_utf8_lossy(raw)
14513 );
14514 }
14515 let written = String::from_utf8(written).unwrap();
14516 assert!(written.starts_with("<c:catAx"));
14517 assert!(written.contains("x:keep=\"axis\""));
14518 assert!(written.contains("x:keep=\"id\""));
14519 assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
14520 }
14521
14522 #[test]
14523 fn every_corpus_axis_round_trips_structurally() {
14524 let Some(corpus) = require_or_skip_corpus() else {
14525 return;
14526 };
14527 verify_fetched_corpus(&corpus);
14528 let mut axis_count = 0usize;
14529 let mut chart_parts = 0usize;
14530 let mut kinds = HashSet::new();
14531 for path in manifest_paths() {
14532 let package = OpcPackage::open(corpus.join(path))
14533 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14534 for (part, xml) in &package.parts {
14535 if !is_chart_part(part) {
14536 continue;
14537 }
14538 let chart = CT_ChartSpace::from_xml(xml)
14539 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14540 let axes = chart.chart.plot_area.axes().unwrap_or_else(|error| {
14541 panic!("{path} {part}: axis projection failed: {error}")
14542 });
14543 for axis in axes {
14544 kinds.insert(axis.kind);
14545 let written = axis.to_xml().unwrap_or_else(|error| {
14546 panic!("{path} {part}: axis write failed: {error}")
14547 });
14548 let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
14549 panic!("{path} {part}: written axis parse failed: {error}")
14550 });
14551 assert_eq!(axis, reparsed, "{path} {part}: axis model changed");
14552 axis_count += 1;
14553 }
14554 chart_parts += 1;
14555 }
14556 }
14557 assert!(
14558 axis_count > 0,
14559 "the pinned corpus contained no ChartML axes"
14560 );
14561 assert!(kinds.contains(&AxisKind::Category));
14562 assert!(kinds.contains(&AxisKind::Value));
14563
14564 for kind in ["dateAx", "serAx"] {
14565 let parsed = Axis::from_xml(
14566 standalone_axis(
14567 kind,
14568 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14569 )
14570 .as_bytes(),
14571 )
14572 .unwrap();
14573 assert_eq!(parsed, Axis::from_xml(&parsed.to_xml().unwrap()).unwrap());
14574 }
14575 eprintln!(
14576 "ChartML axis corpus gate checked {axis_count} axes across {chart_parts} chart parts"
14577 );
14578 }
14579
14580 #[test]
14581 fn series_formula_and_cache_are_consistent_with_one_source() {
14582 let values = NumericData::new(
14583 "'Sales 24'!$B$2:$B$4".to_owned(),
14584 "0.0".to_owned(),
14585 vec![4.25, 8.5, 17.0],
14586 )
14587 .unwrap();
14588 let mut series = Series::new(3, 1, values);
14589 series.categories = Some(AxisData::String(
14590 StringRef::new(
14591 "'Sales 24'!$A$2:$A$4".to_owned(),
14592 vec!["North".to_owned(), "South".to_owned(), "West".to_owned()],
14593 )
14594 .unwrap(),
14595 ));
14596
14597 let written = String::from_utf8(series.to_xml().unwrap()).unwrap();
14598 assert!(written.contains("<c:f>'Sales 24'!$B$2:$B$4</c:f>"));
14599 assert!(written.contains("<c:formatCode>0.0</c:formatCode>"));
14600 assert_eq!(written.matches("<c:ptCount val=\"3\"").count(), 2);
14601 for (index, value) in ["4.25", "8.5", "17"].iter().enumerate() {
14602 assert!(written.contains(&format!("<c:pt idx=\"{index}\"><c:v>{value}</c:v></c:pt>")));
14603 }
14604 let reparsed = Series::from_xml(written.as_bytes()).unwrap();
14605 assert_eq!(reparsed.index, series.index);
14606 assert_eq!(reparsed.order, series.order);
14607 match (&reparsed.categories, &series.categories) {
14608 (Some(AxisData::String(left)), Some(AxisData::String(right))) => {
14609 assert_eq!(left.formula, right.formula);
14610 assert_eq!(left.values, right.values);
14611 }
14612 _ => panic!("expected string categories"),
14613 }
14614 assert_eq!(reparsed.values.formula, series.values.formula);
14615 assert_eq!(reparsed.values.format_code, series.values.format_code);
14616 assert_eq!(reparsed.values.values, series.values.values);
14617 }
14618
14619 #[test]
14620 fn string_and_numeric_references_write_fixed_prefixes_in_schema_order() {
14621 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>"#;
14622 let string_ref = StringRef::from_xml(string_xml).unwrap();
14623 let written = String::from_utf8(string_ref.to_xml().unwrap()).unwrap();
14624 assert!(written.starts_with("<c:strRef xmlns:c="));
14625 assert!(written.find("<c:f>").unwrap() < written.find("<c:strCache>").unwrap());
14626 assert!(written.find("<c:ptCount").unwrap() < written.find("<c:pt idx=").unwrap());
14627
14628 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>"#;
14629 let numeric = NumericData::from_xml(numeric_xml).unwrap();
14630 let written = String::from_utf8(numeric.to_xml().unwrap()).unwrap();
14631 let positions: Vec<_> = [
14632 "<c:f>",
14633 "<c:numCache>",
14634 "<c:formatCode>",
14635 "<c:ptCount",
14636 "<c:pt idx=",
14637 ]
14638 .iter()
14639 .map(|tag| written.find(tag).unwrap())
14640 .collect();
14641 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14642 assert_eq!(numeric, NumericData::from_xml(written.as_bytes()).unwrap());
14643 }
14644
14645 #[test]
14646 fn mixed_chartml_aliases_resolve_by_namespace_uri() {
14647 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>"#;
14648 let parsed = Series::from_xml(xml).unwrap();
14649 assert_eq!(parsed.index, 4);
14650 assert_eq!(parsed.order, 2);
14651 assert_eq!(parsed.values.values, vec![1.5, 2.5]);
14652 assert!(matches!(parsed.categories, Some(AxisData::String(_))));
14653 assert_eq!(parsed, Series::from_xml(&parsed.to_xml().unwrap()).unwrap());
14654 }
14655
14656 #[test]
14657 fn plot_area_series_ignores_inherited_foreign_plot_aliases() {
14658 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>"#;
14659 let chart = CT_ChartSpace::from_xml(xml).unwrap();
14660 let series = chart.chart.plot_area.series().unwrap();
14661 assert_eq!(series.len(), 1);
14662 assert_eq!(series[0].index, 1);
14663 let written = String::from_utf8(series[0].to_xml().unwrap()).unwrap();
14664 assert!(
14665 written.contains(r#"xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart""#)
14666 );
14667 assert!(written.contains(r#"xmlns:x="urn:producer""#));
14668 assert!(written.contains("<q:marker><x:data/></q:marker>"));
14669 assert_eq!(series[0], Series::from_xml(written.as_bytes()).unwrap());
14670
14671 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>"#;
14672 let chart = CT_ChartSpace::from_xml(conflicting).unwrap();
14673 assert!(chart.chart.plot_area.series().is_err());
14674 }
14675
14676 #[test]
14677 fn malformed_series_and_cache_values_return_errors_without_panicking() {
14678 let cases: &[&[u8]] = &[
14679 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>"#,
14680 br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/></c:ser>"#,
14681 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>"#,
14682 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>"#,
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>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>"#,
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:$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>"#,
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</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>"#,
14686 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>"#,
14687 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>"#,
14688 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>"#,
14689 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>"#,
14690 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>"#,
14691 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>"#,
14692 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>"#,
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><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>"#,
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 xmlns:c="urn:producer" val="0"/></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 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>"#,
14696 ];
14697 for xml in cases {
14698 let result = std::panic::catch_unwind(|| Series::from_xml(xml));
14699 assert!(result.is_ok(), "series parser panicked");
14700 assert!(
14701 result.unwrap().is_err(),
14702 "malformed series parsed: {}",
14703 String::from_utf8_lossy(xml)
14704 );
14705 }
14706 assert!(
14707 NumericData::new(
14708 "S!$A$1".to_owned(),
14709 "General".to_owned(),
14710 vec![f64::INFINITY]
14711 )
14712 .is_err()
14713 );
14714 }
14715
14716 #[test]
14717 fn chart_text_rejects_xml_forbidden_characters_and_escapes_metacharacters() {
14718 assert!(StringRef::new("S!$A$1\u{1}".to_owned(), Vec::new()).is_err());
14719 assert!(StringRef::new("S!$A$1".to_owned(), vec!["invalid\u{1}".to_owned()]).is_err());
14720 assert!(
14721 NumericData::new("S!$A$1\u{1}".to_owned(), "General".to_owned(), Vec::new()).is_err()
14722 );
14723 assert!(
14724 NumericData::new("S!$A$1".to_owned(), "General\u{1}".to_owned(), Vec::new()).is_err()
14725 );
14726
14727 let mut strings = StringRef::new("S!$A$1".to_owned(), vec!["valid".to_owned()]).unwrap();
14728 strings.formula = "invalid\u{1}".to_owned();
14729 assert!(strings.to_xml().is_err());
14730 strings.formula = "S!$A$1".to_owned();
14731 strings.values[0] = "invalid\u{1}".to_owned();
14732 assert!(strings.to_xml().is_err());
14733
14734 let mut numbers =
14735 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap();
14736 numbers.formula = "invalid\u{1}".to_owned();
14737 assert!(numbers.to_xml().is_err());
14738 numbers.formula = "S!$A$1".to_owned();
14739 numbers.format_code = "invalid\u{1}".to_owned();
14740 assert!(numbers.to_xml().is_err());
14741
14742 let strings =
14743 StringRef::new("S!$A$1&".to_owned(), vec!["North < West & East".to_owned()]).unwrap();
14744 let written = String::from_utf8(strings.to_xml().unwrap()).unwrap();
14745 assert!(written.contains("S!$A$1&"));
14746 assert!(written.contains("North < West & East"));
14747 }
14748
14749 #[test]
14750 fn series_preserves_unmodelled_children_byte_for_byte() {
14751 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>"#;
14752 let parsed = Series::from_xml(xml).unwrap();
14753 let written = parsed.to_xml().unwrap();
14754 for raw in [
14755 br#"<!--before-->"#.as_slice(),
14756 br#"<x:between/>"#.as_slice(),
14757 br#"<q:dPt x:id="one"><q:idx val="0"/></q:dPt>"#.as_slice(),
14758 br#"<q:trendline x:id="trend"/>"#.as_slice(),
14759 br#"<q:extLst><q:ext uri="keep"><x:data/></q:ext></q:extLst>"#.as_slice(),
14760 br#"<!--point-->"#.as_slice(),
14761 br#"<x:cacheExt/>"#.as_slice(),
14762 ] {
14763 assert!(
14764 written.windows(raw.len()).any(|window| window == raw),
14765 "preserved series bytes changed: {}",
14766 String::from_utf8_lossy(raw)
14767 );
14768 }
14769 let written = String::from_utf8(written.clone()).unwrap();
14770 assert!(written.contains(r#"x:keep="series""#));
14771 assert!(written.contains(r#"x:keep="formula""#));
14772 assert!(written.contains("<c:dLbls"));
14773 assert!(written.contains(r#"x:id="labels""#));
14774 assert!(parsed.data_labels.is_some());
14775 assert_eq!(parsed, Series::from_xml(written.as_bytes()).unwrap());
14776 }
14777
14778 #[test]
14779 fn public_series_edits_do_not_duplicate_or_drop_preserved_payloads() {
14780 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>"#;
14781 let parsed = Series::from_xml(opaque_xml).unwrap();
14782
14783 let mut edited = parsed.clone();
14784 edited.name = Some(StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap());
14785 assert!(edited.to_xml().is_err());
14786
14787 let mut edited = parsed.clone();
14788 edited.categories = Some(AxisData::Numeric(
14789 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap(),
14790 ));
14791 assert!(edited.to_xml().is_err());
14792
14793 let mut edited = parsed;
14794 edited.bubble_size =
14795 Some(NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
14796 assert!(edited.to_xml().is_err());
14797
14798 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>"#;
14799 let parsed = Series::from_xml(cache_xml).unwrap();
14800 let tail = "<c:extLst><c:ext uri=\"keep\"><x:data/></c:ext></c:extLst>";
14801
14802 let mut shortened = parsed.clone();
14803 shortened.values.values.truncate(1);
14804 let written = String::from_utf8(shortened.to_xml().unwrap()).unwrap();
14805 assert!(!written.contains("<c:pt idx=\"1\""));
14806 assert!(written.find("<c:pt idx=\"0\"").unwrap() < written.find(tail).unwrap());
14807 assert!(Series::from_xml(written.as_bytes()).is_ok());
14808
14809 let mut grown = parsed;
14810 grown.values.values.push(3.0);
14811 let written = String::from_utf8(grown.to_xml().unwrap()).unwrap();
14812 assert!(written.find("<c:pt idx=\"2\"").unwrap() < written.find(tail).unwrap());
14813 assert!(Series::from_xml(written.as_bytes()).is_ok());
14814
14815 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>"#;
14816 let mut edited = Series::from_xml(ordered_xml).unwrap();
14817 edited.name = Some(StringRef::new("S!$B$1".to_owned(), Vec::new()).unwrap());
14818 edited.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
14819 edited.categories = Some(AxisData::String(
14820 StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap(),
14821 ));
14822 edited.bubble_size =
14823 Some(NumericData::new("S!$C$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
14824 let written = String::from_utf8(edited.to_xml().unwrap()).unwrap();
14825 let positions: Vec<_> = [
14826 "<c:tx>",
14827 "<c:spPr",
14828 "<c:marker>",
14829 "<c:cat>",
14830 "<c:val>",
14831 "<c:bubbleSize>",
14832 "<c:extLst>",
14833 ]
14834 .iter()
14835 .map(|tag| written.find(tag).unwrap())
14836 .collect();
14837 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14838 assert!(Series::from_xml(written.as_bytes()).is_ok());
14839 }
14840
14841 #[test]
14842 fn every_corpus_series_round_trips_structurally() {
14843 let Some(corpus) = require_or_skip_corpus() else {
14844 return;
14845 };
14846 verify_fetched_corpus(&corpus);
14847 let mut series_count = 0usize;
14848 let mut chart_parts = 0usize;
14849 for path in manifest_paths() {
14850 let package = OpcPackage::open(corpus.join(path))
14851 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14852 for (part, xml) in &package.parts {
14853 if !is_chart_part(part) {
14854 continue;
14855 }
14856 let chart = CT_ChartSpace::from_xml(xml)
14857 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14858 let parsed_series =
14859 chart.chart.plot_area.series().unwrap_or_else(|error| {
14860 panic!("{path} {part}: series parse failed: {error}")
14861 });
14862 for series in parsed_series {
14863 let written = series.to_xml().unwrap_or_else(|error| {
14864 panic!("{path} {part}: series write failed: {error}")
14865 });
14866 let reparsed = Series::from_xml(&written).unwrap_or_else(|error| {
14867 panic!("{path} {part}: written series parse failed: {error}")
14868 });
14869 assert_eq!(series, reparsed, "{path} {part}: series model changed");
14870 series_count += 1;
14871 }
14872 chart_parts += 1;
14873 }
14874 }
14875 assert!(
14876 series_count > 0,
14877 "the pinned corpus contained no supported series"
14878 );
14879 eprintln!(
14880 "ChartML series corpus gate checked {series_count} series across {chart_parts} chart parts"
14881 );
14882 }
14883
14884 #[test]
14885 fn chart_space_reads_aliases_and_writes_fixed_prefixes_in_schema_order() {
14886 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>"#;
14887 let parsed = CT_ChartSpace::from_xml(xml).unwrap();
14888 assert!(!parsed.chart.auto_title_deleted);
14889 assert!(!parsed.chart.plot_vis_only);
14890 assert_eq!(parsed.chart.disp_blanks_as, DispBlanksAs::Span);
14891 assert!(parsed.chart.title.is_some());
14892 assert!(parsed.chart.legend.is_some());
14893 assert!(parsed.sp_pr.is_some());
14894 assert!(parsed.tx_pr.is_some());
14895
14896 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14897 assert!(written.starts_with(
14898 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""#
14899 ));
14900 for tag in [
14901 "<c:chart>",
14902 "<c:title>",
14903 "<c:plotArea>",
14904 "<c:legend>",
14905 "<c:plotVisOnly",
14906 "<c:dispBlanksAs",
14907 "<c:spPr>",
14908 "<c:txPr>",
14909 ] {
14910 assert!(written.contains(tag), "missing fixed-prefix tag {tag}");
14911 }
14912 let positions: Vec<_> = [
14913 "<c:title>",
14914 "<c:plotArea>",
14915 "<c:legend>",
14916 "<c:plotVisOnly",
14917 "<c:dispBlanksAs",
14918 "<c:spPr>",
14919 "<c:txPr>",
14920 ]
14921 .iter()
14922 .map(|tag| written.find(tag).unwrap())
14923 .collect();
14924 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14925 assert!(written.contains("<a:solidFill><a:srgbClr"));
14926 assert!(written.contains("<a:bodyPr/><a:p/>"));
14927 assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
14928 }
14929
14930 #[test]
14931 fn public_core_edits_keep_preserved_children_in_their_schema_slots() {
14932 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>"#;
14933 let mut parsed = CT_ChartSpace::from_xml(xml).unwrap();
14934 parsed.chart.auto_title_deleted = true;
14935 parsed.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
14936 parsed.tx_pr =
14937 Some(CT_TextBody::from_xml(br#"<a:txBody><a:bodyPr/><a:p/></a:txBody>"#).unwrap());
14938 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14939 let positions: Vec<_> = [
14940 "<c:autoTitleDeleted",
14941 "<c:pivotFmts>",
14942 "<c:plotArea",
14943 "<c:spPr",
14944 "<c:txPr",
14945 "<c:externalData",
14946 ]
14947 .iter()
14948 .map(|tag| written.find(tag).unwrap())
14949 .collect();
14950 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14951 assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
14952 }
14953
14954 #[test]
14955 fn malformed_core_chart_values_return_errors_without_panicking() {
14956 let cases: &[&[u8]] = &[
14957 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"/>"#,
14958 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart/></c:chartSpace>"#,
14959 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:plotArea/></c:chart></c:chartSpace>"#,
14960 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:autoTitleDeleted val="yes"/><c:plotArea/></c:chart></c:chartSpace>"#,
14961 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:dispBlanksAs val="empty"/></c:chart></c:chartSpace>"#,
14962 br#"<c:chartSpace xmlns:c="urn:wrong"><c:chart><c:plotArea/></c:chart></c:chartSpace>"#,
14963 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:foreign"><x:chart><x:plotArea/></x:chart></c:chartSpace>"#,
14964 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>"#,
14965 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:title/><c:title/><c:plotArea/></c:chart></c:chartSpace>"#,
14966 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>"#,
14967 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>"#,
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:spPr><a:solidFill><x:srgbClr val="112233"/></a:solidFill></c:spPr></c:chartSpace>"#,
14969 ];
14970 for xml in cases {
14971 let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml));
14972 assert!(result.is_ok(), "ChartML parser panicked");
14973 assert!(
14974 result.unwrap().is_err(),
14975 "malformed ChartML parsed: {}",
14976 String::from_utf8_lossy(xml)
14977 );
14978 }
14979 }
14980
14981 #[test]
14982 fn point_label_overrides_render_without_changing_preserved_xml() {
14983 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>"#;
14984 let plot = bar_plot("").replace(
14985 r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
14986 &format!(
14987 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>"#
14988 ),
14989 );
14990 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
14991 let plot = &chart.chart.plot_area.plots().unwrap()[0];
14992 let Plot::Bar { data_labels, .. } = plot else {
14993 panic!("expected bar plot");
14994 };
14995 let labels = data_labels.as_ref().unwrap();
14996 let written = labels.to_xml().unwrap();
14997 assert!(
14998 written
14999 .windows(raw_overrides.len())
15000 .any(|window| window == raw_overrides.as_bytes())
15001 );
15002 let overrides = super::point_label_overrides(labels).unwrap();
15003 assert_eq!(overrides.keys().copied().collect::<Vec<_>>(), vec![1]);
15004
15005 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
15006 let mut rendered = Vec::new();
15007 super::render_data_labels(
15008 plot,
15009 chart_bounds(),
15010 DispBlanksAs::Gap,
15011 super::GeometryOptions::default(),
15012 &mut fonts,
15013 &mut rendered,
15014 )
15015 .unwrap();
15016 let labels: Vec<_> = rendered
15017 .iter()
15018 .filter_map(|element| match element {
15019 PositionedElement::Text(run) => Some(run),
15020 _ => None,
15021 })
15022 .collect();
15023 assert_eq!(labels.len(), 2);
15024 assert_eq!(labels[0].text, "North, 1");
15025 assert_eq!(labels[1].text, "South / 2.0");
15026 assert_point_near(labels[0].origin, Point { x: 50.0, y: 105.0 });
15027 assert_point_near(labels[1].origin, Point { x: 158.0, y: 70.0 });
15028 assert_eq!(
15029 CT_DLbls::from_xml(&written).unwrap().to_xml().unwrap(),
15030 written
15031 );
15032 }
15033
15034 #[test]
15035 fn malformed_point_label_overrides_return_contextual_errors() {
15036 let cases = [
15037 r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:showVal val="1"/></q:dLbl></q:dLbls>"#,
15038 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>"#,
15039 r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="point"/></q:dLbl></q:dLbls>"#,
15040 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>"#,
15041 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>"#,
15042 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>"#,
15043 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>"#,
15044 ];
15045 for xml in cases {
15046 let labels = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
15047 let result = std::panic::catch_unwind(|| super::point_label_overrides(&labels));
15048 assert!(result.is_ok(), "point-label parser panicked for {xml}");
15049 assert!(
15050 result.unwrap().is_err(),
15051 "malformed point labels parsed: {xml}"
15052 );
15053 }
15054
15055 let foreign = format!(
15056 r#"<q:dLbls xmlns:q="{C_NS}" xmlns:x="urn:foreign"><x:dLbl><x:idx val="0"/></x:dLbl></q:dLbls>"#
15057 );
15058 let labels = CT_DLbls::from_xml(foreign.as_bytes()).unwrap();
15059 assert!(super::point_label_overrides(&labels).unwrap().is_empty());
15060 }
15061
15062 #[test]
15063 fn percentage_label_aggregation_rejects_nonfinite_totals() {
15064 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
15065 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
15066 let Plot::Bar { series, .. } = plot else {
15067 panic!("expected bar plot");
15068 };
15069 series[0].values.values = vec![f64::MAX, f64::MAX];
15070 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
15071 super::render_data_labels(
15072 plot,
15073 chart_bounds(),
15074 DispBlanksAs::Gap,
15075 super::GeometryOptions::default(),
15076 &mut fonts,
15077 &mut Vec::new(),
15078 )
15079 .expect("value-only labels do not need a percentage total");
15080 let Plot::Bar { data_labels, .. } = plot else {
15081 panic!("expected bar plot");
15082 };
15083 data_labels.as_mut().unwrap().show_percent = true;
15084 let error = super::render_data_labels(
15085 plot,
15086 chart_bounds(),
15087 DispBlanksAs::Gap,
15088 super::GeometryOptions::default(),
15089 &mut fonts,
15090 &mut Vec::new(),
15091 )
15092 .unwrap_err();
15093 assert!(error.to_string().contains("data-label percentage total"));
15094
15095 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>"#;
15096 let plot_xml = bar_plot("").replace(
15097 r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
15098 &format!(
15099 r#"<q:dLbls>{overrides}<q:showVal val="1"/><q:showPercent val="1"/></q:dLbls>"#
15100 ),
15101 );
15102 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
15103 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
15104 let Plot::Bar { series, .. } = plot else {
15105 panic!("expected bar plot");
15106 };
15107 series[0].values.values = vec![f64::MAX, f64::MAX];
15108 super::render_data_labels(
15109 plot,
15110 chart_bounds(),
15111 DispBlanksAs::Gap,
15112 super::GeometryOptions::default(),
15113 &mut fonts,
15114 &mut Vec::new(),
15115 )
15116 .expect("effective point overrides disable percentage aggregation");
15117 }
15118
15119 #[test]
15120 fn percentage_labels_use_effective_number_format_precision() {
15121 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
15122 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
15123 let Plot::Bar {
15124 series,
15125 data_labels,
15126 ..
15127 } = plot
15128 else {
15129 panic!("expected bar plot");
15130 };
15131 series[0].values.values = vec![1.0, 2.0];
15132 let labels = data_labels.as_mut().unwrap();
15133 labels.show_value = false;
15134 labels.show_percent = true;
15135 labels.number_format = Some(NumberFormat::new("0.00%".to_owned(), false).unwrap());
15136 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
15137 let mut rendered = Vec::new();
15138 super::render_data_labels(
15139 plot,
15140 chart_bounds(),
15141 DispBlanksAs::Gap,
15142 super::GeometryOptions::default(),
15143 &mut fonts,
15144 &mut rendered,
15145 )
15146 .unwrap();
15147 let text: Vec<_> = rendered
15148 .iter()
15149 .filter_map(|element| match element {
15150 PositionedElement::Text(run) => Some(run.text.as_str()),
15151 _ => None,
15152 })
15153 .collect();
15154 assert_eq!(text, vec!["33.33%", "66.67%"]);
15155
15156 let point_override = r#"<q:dLbl><q:idx val="0"/><q:numFmt formatCode="0.00%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbl>"#;
15157 let plot_xml = bar_plot("").replace(
15158 r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
15159 &format!(
15160 r#"<q:dLbls>{point_override}<q:numFmt formatCode="0%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbls>"#
15161 ),
15162 );
15163 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
15164 let plot = &chart.chart.plot_area.plots().unwrap()[0];
15165 let mut rendered = Vec::new();
15166 super::render_data_labels(
15167 plot,
15168 chart_bounds(),
15169 DispBlanksAs::Gap,
15170 super::GeometryOptions::default(),
15171 &mut fonts,
15172 &mut rendered,
15173 )
15174 .unwrap();
15175 let text: Vec<_> = rendered
15176 .iter()
15177 .filter_map(|element| match element {
15178 PositionedElement::Text(run) => Some(run.text.as_str()),
15179 _ => None,
15180 })
15181 .collect();
15182 assert_eq!(text, vec!["33.33%", "67%"]);
15183 }
15184
15185 #[test]
15186 fn core_chart_shells_preserve_unmodelled_children_byte_for_byte() {
15187 let preserved = [
15188 br#"<x:root x:id="1"><x:nested>one & two</x:nested><!--root--></x:root>"#
15189 .as_slice(),
15190 br#"<x:title x:id="2"><x:nested><![CDATA[title]]></x:nested></x:title>"#.as_slice(),
15191 br#"<x:plot x:id="3"><x:nested>plot</x:nested></x:plot>"#.as_slice(),
15192 br#"<x:legend x:id="4"><x:nested>legend</x:nested></x:legend>"#.as_slice(),
15193 br#"<x:extension x:id="5"><x:nested>extension</x:nested></x:extension>"#.as_slice(),
15194 br#"<!--space-comment-->"#.as_slice(),
15195 br#"<?space processing?>"#.as_slice(),
15196 br#"<!--chart-comment-->"#.as_slice(),
15197 br#"<?title processing?>"#.as_slice(),
15198 br#"<!--flag-comment-->"#.as_slice(),
15199 br#"<?flag processing?>"#.as_slice(),
15200 br#"<x:producerShape/>"#.as_slice(),
15201 br#"<x:producerText/>"#.as_slice(),
15202 ];
15203 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>"#;
15204 let parsed = CT_ChartSpace::from_xml(xml).unwrap();
15205 let written = parsed.to_xml().unwrap();
15206 for raw in preserved {
15207 assert!(
15208 written.windows(raw.len()).any(|window| window == raw),
15209 "preserved subtree changed: {}",
15210 String::from_utf8_lossy(raw)
15211 );
15212 }
15213 let written_text = String::from_utf8(written.clone()).unwrap();
15214 assert!(written_text.contains(r#"x:keep="one & two""#));
15215 assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
15216 }
15217
15218 #[test]
15219 fn every_corpus_chart_part_round_trips_structurally() {
15220 let Some(corpus) = require_or_skip_corpus() else {
15221 return;
15222 };
15223 verify_fetched_corpus(&corpus);
15224 let mut chart_parts = 0usize;
15225 let mut decks_with_charts = HashSet::new();
15226 for path in manifest_paths() {
15227 let package = OpcPackage::open(corpus.join(path))
15228 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
15229 for (part, xml) in &package.parts {
15230 if !is_chart_part(part) {
15231 continue;
15232 }
15233 let parsed = CT_ChartSpace::from_xml(xml)
15234 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
15235 let written = parsed
15236 .to_xml()
15237 .unwrap_or_else(|error| panic!("{path} {part}: write failed: {error}"));
15238 let original_preserved = preserved_chartml_regions(xml)
15239 .unwrap_or_else(|error| panic!("{path} {part}: original scan failed: {error}"));
15240 let written_preserved = preserved_chartml_regions(&written)
15241 .unwrap_or_else(|error| panic!("{path} {part}: written scan failed: {error}"));
15242 assert_eq!(
15243 original_preserved.len(),
15244 written_preserved.len(),
15245 "{path} {part}: preserved ChartML region count changed"
15246 );
15247 for (index, (original, written)) in original_preserved
15248 .iter()
15249 .zip(&written_preserved)
15250 .enumerate()
15251 {
15252 assert_eq!(
15253 original.parent_path, written.parent_path,
15254 "{path} {part}: preserved region {index} parent changed"
15255 );
15256 assert_eq!(
15257 original.boundary, written.boundary,
15258 "{path} {part}: preserved region {index} boundary changed"
15259 );
15260 assert_eq!(
15261 original.sibling_order, written.sibling_order,
15262 "{path} {part}: preserved region {index} order changed"
15263 );
15264 assert_eq!(
15265 original.xml, written.xml,
15266 "{path} {part}: preserved region {index} bytes changed"
15267 );
15268 }
15269 let reparsed = CT_ChartSpace::from_xml(&written)
15270 .unwrap_or_else(|error| panic!("{path} {part}: written parse failed: {error}"));
15271 assert_eq!(parsed, reparsed, "{path} {part}: core model changed");
15272 chart_parts += 1;
15273 decks_with_charts.insert(path);
15274 }
15275 }
15276 assert!(
15277 chart_parts > 0,
15278 "the pinned corpus contained no chart parts"
15279 );
15280 eprintln!(
15281 "ChartML corpus gate checked {chart_parts} chart parts across {} decks",
15282 decks_with_charts.len()
15283 );
15284 }
15285
15286 #[derive(Debug, Eq, PartialEq)]
15287 struct PreservedChartRegion {
15288 parent_path: Vec<Vec<u8>>,
15289 boundary: usize,
15290 sibling_order: usize,
15291 xml: Vec<u8>,
15292 }
15293
15294 struct PreservationFrame {
15295 local_name: Vec<u8>,
15296 boundary: usize,
15297 next_sibling: usize,
15298 }
15299
15300 fn preserved_chartml_regions(xml: &[u8]) -> Result<Vec<PreservedChartRegion>, String> {
15301 let mut reader = Reader::from_reader(xml);
15302 let mut buffer = Vec::new();
15303 let mut parents: Vec<PreservationFrame> = Vec::new();
15304 let mut preserved = Vec::new();
15305 loop {
15306 match reader.read_event_into(&mut buffer) {
15307 Ok(Event::Start(element)) => {
15308 let child = local_name(element.name().as_ref()).to_vec();
15309 if parents.is_empty() && child == b"chartSpace" {
15310 parents.push(PreservationFrame {
15311 local_name: child,
15312 boundary: 0,
15313 next_sibling: 0,
15314 });
15315 } else if let Some(parent) = parents.last()
15316 && is_opaque_chartml_child(&parent.local_name, &child)
15317 {
15318 let boundary =
15319 preserved_child_boundary(&parent.local_name, &child, parent.boundary);
15320 let xml = capture_element(&mut reader, &element)
15321 .map_err(|error| error.to_string())?;
15322 record_preserved_region(&mut parents, &mut preserved, boundary, xml);
15323 advance_preservation_boundary(&mut parents, &child);
15324 } else {
15325 advance_preservation_boundary(&mut parents, &child);
15326 if is_chartml_preservation_parent(&child) {
15327 parents.push(PreservationFrame {
15328 local_name: child,
15329 boundary: 0,
15330 next_sibling: 0,
15331 });
15332 } else {
15333 capture_element(&mut reader, &element)
15334 .map_err(|error| error.to_string())?;
15335 }
15336 }
15337 }
15338 Ok(Event::Empty(element)) => {
15339 let name = element.name();
15340 let child = local_name(name.as_ref()).to_vec();
15341 if let Some(parent) = parents.last()
15342 && is_opaque_chartml_child(&parent.local_name, &child)
15343 {
15344 let boundary =
15345 preserved_child_boundary(&parent.local_name, &child, parent.boundary);
15346 let xml =
15347 capture_empty_element(&element).map_err(|error| error.to_string())?;
15348 record_preserved_region(&mut parents, &mut preserved, boundary, xml);
15349 }
15350 advance_preservation_boundary(&mut parents, &child);
15351 }
15352 Ok(
15353 event @ (Event::Comment(_)
15354 | Event::PI(_)
15355 | Event::CData(_)
15356 | Event::GeneralRef(_)),
15357 ) if !parents.is_empty() => {
15358 let boundary = parents.last().expect("preservation parent").boundary;
15359 let xml = capture_event(event).map_err(|error| error.to_string())?;
15360 record_preserved_region(&mut parents, &mut preserved, boundary, xml);
15361 }
15362 Ok(Event::End(_)) => {
15363 parents.pop();
15364 }
15365 Ok(Event::Eof) => break,
15366 Ok(_) => {}
15367 Err(error) => return Err(error.to_string()),
15368 }
15369 buffer.clear();
15370 }
15371 Ok(preserved)
15372 }
15373
15374 fn record_preserved_region(
15375 parents: &mut [PreservationFrame],
15376 preserved: &mut Vec<PreservedChartRegion>,
15377 boundary: usize,
15378 xml: Vec<u8>,
15379 ) {
15380 let sibling_order = parents.last().expect("preservation parent").next_sibling;
15381 parents
15382 .last_mut()
15383 .expect("preservation parent")
15384 .next_sibling += 1;
15385 preserved.push(PreservedChartRegion {
15386 parent_path: parents
15387 .iter()
15388 .map(|parent| parent.local_name.clone())
15389 .collect(),
15390 boundary,
15391 sibling_order,
15392 xml,
15393 });
15394 }
15395
15396 fn advance_preservation_boundary(parents: &mut [PreservationFrame], child: &[u8]) {
15397 let Some(parent) = parents.last_mut() else {
15398 return;
15399 };
15400 parent.boundary = parent
15401 .boundary
15402 .max(preserved_boundary_after(&parent.local_name, child));
15403 }
15404
15405 fn preserved_child_boundary(parent: &[u8], child: &[u8], current: usize) -> usize {
15406 match parent {
15407 b"chartSpace" => match child {
15408 b"date1904" => 0,
15409 b"lang" => 1,
15410 b"roundedCorners" => 2,
15411 b"style" => 3,
15412 b"clrMapOvr" => 4,
15413 b"pivotSource" => 5,
15414 b"protection" => 6,
15415 b"externalData" => 10,
15416 b"printSettings" => 11,
15417 b"userShapes" => 12,
15418 b"extLst" => 13,
15419 _ => current,
15420 },
15421 b"chart" => match child {
15422 b"pivotFmts" => 2,
15423 b"view3D" => 3,
15424 b"floor" => 4,
15425 b"sideWall" => 5,
15426 b"backWall" => 6,
15427 b"showDLblsOverMax" | b"extLst" => 11,
15428 _ => current,
15429 },
15430 _ => current,
15431 }
15432 }
15433
15434 fn preserved_boundary_after(parent: &[u8], child: &[u8]) -> usize {
15435 match parent {
15436 b"chartSpace" => match child {
15437 b"date1904" => 1,
15438 b"lang" => 2,
15439 b"roundedCorners" => 3,
15440 b"style" => 4,
15441 b"clrMapOvr" => 5,
15442 b"pivotSource" => 6,
15443 b"protection" => 7,
15444 b"chart" => 8,
15445 b"spPr" => 9,
15446 b"txPr" => 10,
15447 b"externalData" => 11,
15448 b"printSettings" => 12,
15449 b"userShapes" => 13,
15450 b"extLst" => 14,
15451 _ => 0,
15452 },
15453 b"chart" => match child {
15454 b"title" => 1,
15455 b"autoTitleDeleted" => 2,
15456 b"pivotFmts" => 3,
15457 b"view3D" => 4,
15458 b"floor" => 5,
15459 b"sideWall" => 6,
15460 b"backWall" => 7,
15461 b"plotArea" => 8,
15462 b"legend" => 9,
15463 b"plotVisOnly" => 10,
15464 b"dispBlanksAs" => 11,
15465 b"showDLblsOverMax" | b"extLst" => 11,
15466 _ => 0,
15467 },
15468 b"pieChart" => match child {
15469 b"ser" => 1,
15470 b"dLbls" => 2,
15471 b"firstSliceAng" => 3,
15472 _ => 0,
15473 },
15474 b"doughnutChart" => match child {
15475 b"ser" => 1,
15476 b"dLbls" => 2,
15477 b"firstSliceAng" => 3,
15478 b"holeSize" => 4,
15479 _ => 0,
15480 },
15481 b"areaChart" => match child {
15482 b"grouping" => 1,
15483 b"ser" => 2,
15484 b"dLbls" => 3,
15485 b"axId" => 4,
15486 _ => 0,
15487 },
15488 b"scatterChart" | b"radarChart" => match child {
15489 b"scatterStyle" | b"radarStyle" => 1,
15490 b"ser" => 2,
15491 b"dLbls" => 3,
15492 b"axId" => 4,
15493 _ => 0,
15494 },
15495 _ => 0,
15496 }
15497 }
15498
15499 fn is_opaque_chartml_child(parent: &[u8], child: &[u8]) -> bool {
15500 match parent {
15501 b"chartSpace" => !matches!(child, b"chart" | b"spPr" | b"txPr"),
15502 b"chart" => !matches!(
15503 child,
15504 b"title"
15505 | b"autoTitleDeleted"
15506 | b"plotArea"
15507 | b"legend"
15508 | b"plotVisOnly"
15509 | b"dispBlanksAs"
15510 ),
15511 b"plotArea" => !matches!(
15512 child,
15513 b"barChart"
15514 | b"lineChart"
15515 | b"pieChart"
15516 | b"doughnutChart"
15517 | b"areaChart"
15518 | b"scatterChart"
15519 | b"radarChart"
15520 | b"catAx"
15521 | b"valAx"
15522 | b"dateAx"
15523 | b"serAx"
15524 ),
15525 b"pieChart" => !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng"),
15526 b"doughnutChart" => {
15527 !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng" | b"holeSize")
15528 }
15529 b"areaChart" => !matches!(child, b"grouping" | b"ser" | b"dLbls" | b"axId"),
15530 b"scatterChart" => !matches!(child, b"scatterStyle" | b"ser" | b"dLbls" | b"axId"),
15531 b"radarChart" => !matches!(child, b"radarStyle" | b"ser" | b"dLbls" | b"axId"),
15532 b"title" | b"legend" => true,
15533 _ => false,
15534 }
15535 }
15536
15537 fn is_chartml_preservation_parent(parent: &[u8]) -> bool {
15538 matches!(
15539 parent,
15540 b"chartSpace"
15541 | b"chart"
15542 | b"title"
15543 | b"plotArea"
15544 | b"pieChart"
15545 | b"doughnutChart"
15546 | b"areaChart"
15547 | b"scatterChart"
15548 | b"radarChart"
15549 | b"legend"
15550 | b"autoTitleDeleted"
15551 | b"plotVisOnly"
15552 | b"dispBlanksAs"
15553 )
15554 }
15555
15556 #[test]
15557 fn oxml_chart_is_an_explicit_publication_candidate() {
15558 let manifest = include_str!("../Cargo.toml");
15559 assert!(manifest.contains("name = \"oxml-chart\""));
15560 assert!(manifest.contains("version = \"0.13.1\""));
15561 assert!(manifest.contains("publish = true"));
15562 for dependency in [
15563 "oxml-core.workspace",
15564 "oxml-drawing.workspace",
15565 "quick-xml.workspace",
15566 ] {
15567 assert!(manifest.contains(dependency));
15568 }
15569 assert!(!manifest.contains("rpptx.workspace"));
15570 assert!(!manifest.contains("rdocx.workspace"));
15571 }
15572
15573 fn first_series_xml(xml: &[u8]) -> Option<Vec<u8>> {
15574 let mut reader = Reader::from_reader(xml);
15575 let mut buffer = Vec::new();
15576 loop {
15577 match reader.read_event_into(&mut buffer).ok()? {
15578 Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
15579 return capture_element(&mut reader, &element).ok();
15580 }
15581 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
15582 return capture_empty_element(&element).ok();
15583 }
15584 Event::Eof => return None,
15585 _ => {}
15586 }
15587 buffer.clear();
15588 }
15589 }
15590
15591 fn replace_once(haystack: &[u8], needle: &[u8], replacement: &[u8]) -> Vec<u8> {
15592 let offset = haystack
15593 .windows(needle.len())
15594 .position(|window| window == needle)
15595 .expect("serialized source series occurs in chart part");
15596 assert!(
15597 !haystack[offset + needle.len()..]
15598 .windows(needle.len())
15599 .any(|window| window == needle),
15600 "source series unexpectedly occurs more than once"
15601 );
15602 let mut result = Vec::with_capacity(haystack.len() - needle.len() + replacement.len());
15603 result.extend_from_slice(&haystack[..offset]);
15604 result.extend_from_slice(replacement);
15605 result.extend_from_slice(&haystack[offset + needle.len()..]);
15606 result
15607 }
15608
15609 fn assert_command_version(program: &str, arguments: &[&str], expected: &str) {
15610 let output = Command::new(program)
15611 .args(arguments)
15612 .output()
15613 .unwrap_or_else(|error| panic!("run {program} version check: {error}"));
15614 assert!(
15615 output.status.success(),
15616 "{program} version check failed: {}",
15617 String::from_utf8_lossy(&output.stderr)
15618 );
15619 let stdout = String::from_utf8(output.stdout).expect("version stdout is UTF-8");
15620 let stderr = String::from_utf8(output.stderr).expect("version stderr is UTF-8");
15621 let actual = if stdout.trim().is_empty() {
15622 stderr.trim()
15623 } else {
15624 stdout.trim()
15625 };
15626 assert_eq!(
15627 actual.lines().next(),
15628 Some(expected),
15629 "{program} version drift"
15630 );
15631 }
15632
15633 fn sha256(path: &Path) -> String {
15634 let output = Command::new("shasum")
15635 .args(["-a", "256"])
15636 .arg(path)
15637 .output()
15638 .unwrap_or_else(|error| panic!("{}: run shasum: {error}", path.display()));
15639 assert!(
15640 output.status.success(),
15641 "{}: shasum failed: {}",
15642 path.display(),
15643 String::from_utf8_lossy(&output.stderr)
15644 );
15645 String::from_utf8(output.stdout)
15646 .expect("shasum output is UTF-8")
15647 .split_whitespace()
15648 .next()
15649 .expect("shasum digest")
15650 .to_owned()
15651 }
15652
15653 fn workspace_root() -> PathBuf {
15654 Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
15655 }
15656
15657 fn corpus_dir() -> PathBuf {
15658 std::env::var_os("RDOCX_PPTX_CORPUS_DIR")
15659 .map(PathBuf::from)
15660 .unwrap_or_else(|| workspace_root().join("corpus/pptx"))
15661 }
15662
15663 fn require_or_skip_corpus() -> Option<PathBuf> {
15664 let corpus = corpus_dir();
15665 if corpus.is_dir() {
15666 return Some(corpus);
15667 }
15668 assert_ne!(
15669 std::env::var_os("RDOCX_PPTX_CORPUS_REQUIRED").as_deref(),
15670 Some(std::ffi::OsStr::new("1")),
15671 "the pinned corpus is required but {} does not exist",
15672 corpus.display()
15673 );
15674 eprintln!(
15675 "ChartML corpus gate skipped because {} is absent",
15676 corpus.display()
15677 );
15678 None
15679 }
15680
15681 fn verify_fetched_corpus(corpus: &Path) {
15682 let status = Command::new("python3")
15683 .arg(workspace_root().join("scripts/fetch_pptx_corpus.py"))
15684 .arg("--check")
15685 .env("RDOCX_PPTX_CORPUS_DIR", corpus)
15686 .status()
15687 .expect("run corpus verifier");
15688 assert!(status.success(), "pinned corpus verification failed");
15689 }
15690
15691 fn manifest_paths() -> Vec<&'static str> {
15692 let mut lines = MANIFEST.lines();
15693 assert_eq!(lines.next(), Some("path\tproducer\tsha256\turl"));
15694 let paths: Vec<_> = lines
15695 .map(|line| line.split('\t').next().expect("manifest path"))
15696 .collect();
15697 assert_eq!(paths.len(), EXPECTED_DECKS);
15698 paths
15699 }
15700
15701 fn is_chart_part(part: &str) -> bool {
15702 let Some(stem) = part
15703 .strip_prefix("/ppt/charts/chart")
15704 .and_then(|name| name.strip_suffix(".xml"))
15705 else {
15706 return false;
15707 };
15708 !stem.is_empty() && stem.bytes().all(|byte| byte.is_ascii_digit())
15709 }
15710
15711 fn plot_series(index: u32) -> String {
15712 format!(
15713 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>"#
15714 )
15715 }
15716
15717 fn sparse_category_series() -> &'static str {
15718 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>"#
15719 }
15720
15721 fn sparse_scatter_series() -> &'static str {
15722 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>"#
15723 }
15724
15725 fn chart_with_plot(plot: &str) -> String {
15726 format!(
15727 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>"#
15728 )
15729 }
15730
15731 fn chart_with_optional_axes(plot: &str, axes: bool) -> String {
15732 let axis_xml = if axes {
15733 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>"#
15734 } else {
15735 ""
15736 };
15737 format!(
15738 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>"#
15739 )
15740 }
15741
15742 fn numeric_plot_series(index: u32) -> String {
15743 format!(
15744 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>"#
15745 )
15746 }
15747
15748 fn scatter_without_x(index: u32) -> String {
15749 format!(
15750 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>"#
15751 )
15752 }
15753
15754 fn scatter_without_y(index: u32) -> String {
15755 format!(
15756 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>"#
15757 )
15758 }
15759
15760 fn remaining_plot_fixtures() -> Vec<(&'static str, String, bool)> {
15761 let series = plot_series(0);
15762 let scatter = numeric_plot_series(0);
15763 vec![
15764 (
15765 "pieChart",
15766 format!(
15767 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>"#
15768 ),
15769 false,
15770 ),
15771 (
15772 "doughnutChart",
15773 format!(
15774 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>"#
15775 ),
15776 false,
15777 ),
15778 (
15779 "areaChart",
15780 format!(
15781 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>"#
15782 ),
15783 true,
15784 ),
15785 (
15786 "scatterChart",
15787 format!(
15788 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>"#
15789 ),
15790 true,
15791 ),
15792 (
15793 "radarChart",
15794 format!(
15795 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>"#
15796 ),
15797 true,
15798 ),
15799 ]
15800 }
15801
15802 fn bar_plot(extra: &str) -> String {
15803 format!(
15804 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>"#,
15805 plot_series(0)
15806 )
15807 }
15808
15809 fn line_plot(extra: &str) -> String {
15810 format!(
15811 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>"#,
15812 plot_series(0)
15813 )
15814 }
15815
15816 #[test]
15817 fn bar_and_line_plots_round_trip_and_render() {
15818 for (xml, is_bar) in [
15819 (chart_with_plot(&bar_plot("")), true),
15820 (chart_with_plot(&line_plot("")), false),
15821 ] {
15822 let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15823 let plots = parsed.chart.plot_area.plots().unwrap();
15824 assert_eq!(plots.len(), 1);
15825 assert_eq!(matches!(plots[0], Plot::Bar { .. }), is_bar);
15826 assert_eq!(matches!(plots[0], Plot::Line { .. }), !is_bar);
15827 assert_eq!(parsed.chart.plot_area.axes().unwrap().len(), 2);
15828 let written = parsed.to_xml().unwrap();
15829 let reparsed = CT_ChartSpace::from_xml(&written).unwrap();
15830 assert_eq!(parsed, reparsed);
15831 }
15832 if let Some(corpus) = require_or_skip_corpus() {
15833 verify_bar_and_line_viewer_gate(&corpus);
15834 }
15835 }
15836
15837 #[test]
15838 fn ppm_parser_preserves_whitespace_valued_first_pixels() {
15839 for first_pixel in *b" \n\r\t" {
15840 let mut ppm = b"P6\n1 1\n255\n".to_vec();
15841 ppm.extend_from_slice(&[first_pixel, 0x7f, 0xff]);
15842 let (width, height, pixels) = ppm_pixels(&ppm);
15843 assert_eq!((width, height), (1, 1));
15844 assert_eq!(pixels, [first_pixel, 0x7f, 0xff]);
15845 }
15846
15847 let ppm = b"P6\r\n1 1\r\n255\r\n\x7f\xff";
15848 assert_eq!(ppm_pixels(ppm).2, [b'\n', 0x7f, 0xff]);
15849 }
15850
15851 #[test]
15852 fn bar_and_line_plots_write_fixed_prefixes_in_schema_order() {
15853 for xml in [
15854 chart_with_plot(&bar_plot(r#"<q:extLst><x:bar-tail/></q:extLst>"#)),
15855 chart_with_plot(&line_plot(r#"<q:extLst><x:line-tail/></q:extLst>"#)),
15856 ] {
15857 let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15858 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15859 assert!(written.contains("<c:barChart") || written.contains("<c:lineChart"));
15860 assert!(!written.contains("<q:barChart"));
15861 assert!(!written.contains("<q:lineChart"));
15862 if written.contains("<c:barChart") {
15863 let positions: Vec<_> = [
15864 "<c:barDir",
15865 "<c:grouping",
15866 "<q:varyColors",
15867 "<c:ser",
15868 "<c:dLbls",
15869 "<c:gapWidth",
15870 "<c:overlap",
15871 "<q:serLines",
15872 "<c:axId",
15873 "<q:extLst",
15874 ]
15875 .iter()
15876 .map(|tag| written.find(tag).unwrap())
15877 .collect();
15878 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15879 } else {
15880 let positions: Vec<_> = [
15881 "<c:grouping",
15882 "<q:varyColors",
15883 "<c:ser",
15884 "<c:dLbls",
15885 "<q:dropLines",
15886 "<c:marker",
15887 "<c:smooth",
15888 "<c:axId",
15889 "<q:extLst",
15890 ]
15891 .iter()
15892 .map(|tag| written.find(tag).unwrap())
15893 .collect();
15894 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15895 }
15896 assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
15897 }
15898
15899 for plot in [bar_plot(""), line_plot("")] {
15900 let first = plot_series(0);
15901 let with_series_boundary = plot.replacen(
15902 &first,
15903 &format!(
15904 "{first}<!--between-series--><?series-boundary?>\n {}",
15905 plot_series(1)
15906 ),
15907 1,
15908 );
15909 let with_axis_boundary = with_series_boundary.replacen(
15910 r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
15911 r#"<q:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
15912 <q:axId val="-1884097184"/>"#,
15913 1,
15914 );
15915 let parsed =
15916 CT_ChartSpace::from_xml(chart_with_plot(&with_axis_boundary).as_bytes()).unwrap();
15917 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15918 assert!(written.contains("</c:ser><!--between-series--><?series-boundary?>\n <c:ser"));
15919 assert!(written.contains(
15920 r#"<c:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
15921 <c:axId val="-1884097184"/>"#
15922 ));
15923 assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
15924 }
15925 }
15926
15927 #[test]
15928 fn malformed_bar_and_line_plots_return_errors_without_panicking() {
15929 let valid_series = plot_series(0);
15930 let cases = [
15931 format!(r#"<q:barChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15932 format!(r#"<q:barChart><q:barDir val="diagonal"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15933 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>"#),
15934 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>"#),
15935 format!(r#"<q:lineChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
15936 format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:marker val="maybe"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
15937 r#"<q:lineChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#.to_owned(),
15938 format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:axId val="1"/><q:axId val="1"/></q:lineChart>"#),
15939 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>"#),
15940 format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15941 ];
15942 for plot in cases {
15943 let xml = chart_with_plot(&plot);
15944 let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
15945 assert!(result.is_ok(), "plot parser panicked for {plot}");
15946 assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
15947 }
15948
15949 let missing_axis = format!(
15950 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>"#,
15951 bar_plot("")
15952 );
15953 assert!(CT_ChartSpace::from_xml(missing_axis.as_bytes()).is_err());
15954
15955 let values =
15956 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
15957 let series = vec![Series::new(0, 0, values)];
15958 let ids = [AxisId::new(1).unwrap(), AxisId::new(2).unwrap()];
15959 let mut invalid =
15960 Plot::bar(BarDirection::Column, BarGrouping::Clustered, series, ids).unwrap();
15961 if let Plot::Bar { gap_width, .. } = &mut invalid {
15962 *gap_width = 501;
15963 }
15964 assert!(invalid.validate().is_err());
15965 }
15966
15967 #[test]
15968 fn unsupported_and_combo_plots_remain_byte_preserved() {
15969 let bar = bar_plot("");
15970 let line = line_plot("");
15971 let combo = chart_with_plot(&format!("<!--before-->{bar}<?between?>{line}<!--after-->"));
15972 let parsed = CT_ChartSpace::from_xml(combo.as_bytes()).unwrap();
15973 assert!(parsed.chart.plot_area.plots().is_err());
15974 let written = parsed.to_xml().unwrap();
15975 for raw in [
15976 bar.as_bytes(),
15977 line.as_bytes(),
15978 br#"<!--before-->"#.as_slice(),
15979 br#"<?between?>"#.as_slice(),
15980 br#"<!--after-->"#.as_slice(),
15981 ] {
15982 assert!(written.windows(raw.len()).any(|window| window == raw));
15983 }
15984
15985 let unsupported = chart_with_plot(
15986 r#"<q:bar3DChart x:keep="three"><q:barDir val="col"/><q:ser><x:opaque/></q:ser></q:bar3DChart>"#,
15987 );
15988 let parsed = CT_ChartSpace::from_xml(unsupported.as_bytes()).unwrap();
15989 assert!(parsed.chart.plot_area.plots().is_err());
15990 assert!(
15991 String::from_utf8(parsed.to_xml().unwrap())
15992 .unwrap()
15993 .contains(r#"<q:bar3DChart x:keep="three">"#)
15994 );
15995 }
15996
15997 #[test]
15998 fn public_plot_edits_preserve_axes_and_unselected_payloads() {
15999 let xml = chart_with_plot(&bar_plot(
16000 r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#,
16001 ));
16002 let mut parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16003 let axes_before = parsed.chart.plot_area.axes().unwrap();
16004 match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
16005 Plot::Bar {
16006 gap_width,
16007 overlap,
16008 series,
16009 ..
16010 } => {
16011 *gap_width = 225;
16012 *overlap = -25;
16013 series[0].index = 7;
16014 series[0].values.values[0] = 9.0;
16015 }
16016 _ => panic!("expected bar plot"),
16017 }
16018 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
16019 assert!(written.contains(r#"<c:gapWidth val="225"/>"#));
16020 assert!(written.contains(r#"<c:overlap val="-25"/>"#));
16021 assert!(written.contains("<c:v>9</c:v>"));
16022 assert!(written.contains("<q:varyColors val=\"1\"/>"));
16023 assert!(written.find("<q:varyColors").unwrap() < written.find("<c:ser").unwrap());
16024 assert!(written.contains("<q:serLines><x:line/></q:serLines>"));
16025 assert!(written.contains(r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#));
16026 let reparsed = CT_ChartSpace::from_xml(written.as_bytes()).unwrap();
16027 assert_eq!(axes_before, reparsed.chart.plot_area.axes().unwrap());
16028
16029 let first = plot_series(0);
16030 let colliding_series =
16031 bar_plot("").replacen(&first, &format!("{first}{}", plot_series(1)), 1);
16032 let mut collision =
16033 CT_ChartSpace::from_xml(chart_with_plot(&colliding_series).as_bytes()).unwrap();
16034 if let Plot::Bar { series, .. } = &mut collision.chart.plot_area.plots_mut().unwrap()[0] {
16035 series[0].index = series[1].index;
16036 series[0].order = series[1].order;
16037 }
16038 let collision = String::from_utf8(collision.to_xml().unwrap()).unwrap();
16039 assert!(collision.find("<q:varyColors").unwrap() < collision.find("<c:ser").unwrap());
16040
16041 let mut inserted = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16042 if let Plot::Bar { series, .. } = &mut inserted.chart.plot_area.plots_mut().unwrap()[0] {
16043 let mut new_series = series[0].clone();
16044 new_series.index = 99;
16045 new_series.order = 99;
16046 series.insert(0, new_series);
16047 }
16048 let inserted = String::from_utf8(inserted.to_xml().unwrap()).unwrap();
16049 assert!(inserted.find("<q:varyColors").unwrap() < inserted.find("<c:ser").unwrap());
16050
16051 let axis_payload = bar_plot("").replacen(
16052 r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
16053 r#"<q:axId val="-1884094432" x:slot="category"/><!--axis-anchor--><q:axId val="-1884097184" x:slot="value"/>"#,
16054 1,
16055 );
16056 let mut swapped =
16057 CT_ChartSpace::from_xml(chart_with_plot(&axis_payload).as_bytes()).unwrap();
16058 if let Plot::Bar { axis_ids, .. } = &mut swapped.chart.plot_area.plots_mut().unwrap()[0] {
16059 axis_ids.swap(0, 1);
16060 }
16061 let swapped = String::from_utf8(swapped.to_xml().unwrap()).unwrap();
16062 let anchor = swapped.find("<!--axis-anchor-->").unwrap();
16063 let value_axis = swapped
16064 .find(r#"<c:axId val="-1884097184" x:slot="value"/>"#)
16065 .unwrap();
16066 let category_axis = swapped
16067 .find(r#"<c:axId val="-1884094432" x:slot="category"/>"#)
16068 .unwrap();
16069 let series_lines = swapped.find("<q:serLines").unwrap();
16070 assert!(series_lines < value_axis && series_lines < category_axis);
16071 assert!(anchor < value_axis && value_axis < category_axis);
16072
16073 let mut replaced = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16074 let Plot::Bar {
16075 series, axis_ids, ..
16076 } = &replaced.chart.plot_area.plots().unwrap()[0]
16077 else {
16078 panic!("expected bar plot");
16079 };
16080 let line = Plot::line(Grouping::Standard, series.clone(), *axis_ids).unwrap();
16081 replaced.chart.plot_area.plots_mut().unwrap()[0] = line;
16082 assert!(replaced.to_xml().is_err());
16083 }
16084
16085 #[test]
16086 fn every_corpus_bar_and_line_plot_round_trips_structurally() {
16087 let Some(corpus) = require_or_skip_corpus() else {
16088 return;
16089 };
16090 verify_fetched_corpus(&corpus);
16091 let mut typed_bar_count = 0usize;
16092 let mut typed_line_count = 0usize;
16093 let mut opaque_bar_count = 0usize;
16094 let mut opaque_line_count = 0usize;
16095 let mut opaque_combo_count = 0usize;
16096 for path in manifest_paths() {
16097 let package = OpcPackage::open(corpus.join(path))
16098 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
16099 for (part, xml) in &package.parts {
16100 if !is_chart_part(part) {
16101 continue;
16102 }
16103 let chart = CT_ChartSpace::from_xml(xml)
16104 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
16105 match chart.chart.plot_area.plots() {
16106 Ok(plots) => {
16107 for plot in plots {
16108 match plot {
16109 Plot::Bar { .. } => typed_bar_count += 1,
16110 Plot::Line { .. } => typed_line_count += 1,
16111 _ => {}
16112 }
16113 }
16114 }
16115 Err(_) => {
16116 let mut preserved_plot_count = 0usize;
16117 for raw in chart.chart.plot_area.raw_children.at(0) {
16118 if let Ok(Some(local)) = super::chart_root_local(
16119 raw,
16120 &chart.chart.plot_area.namespace_bindings,
16121 ) {
16122 match local.as_slice() {
16123 b"barChart" => {
16124 opaque_bar_count += 1;
16125 preserved_plot_count += 1;
16126 }
16127 b"lineChart" => {
16128 opaque_line_count += 1;
16129 preserved_plot_count += 1;
16130 }
16131 _ => {}
16132 }
16133 }
16134 }
16135 if preserved_plot_count > 1 {
16136 opaque_combo_count += 1;
16137 }
16138 }
16139 }
16140 let written = chart
16141 .to_xml()
16142 .unwrap_or_else(|error| panic!("{path} {part}: chart write failed: {error}"));
16143 let reparsed = CT_ChartSpace::from_xml(&written).unwrap_or_else(|error| {
16144 panic!("{path} {part}: written chart parse failed: {error}")
16145 });
16146 assert_eq!(chart, reparsed, "{path} {part}: chart model changed");
16147 }
16148 }
16149 assert_eq!(typed_bar_count, 11, "typed corpus bar coverage changed");
16150 assert_eq!(typed_line_count, 2, "typed corpus line coverage changed");
16151 assert_eq!(opaque_bar_count, 1, "preserved combo bar coverage changed");
16152 assert_eq!(
16153 opaque_line_count, 1,
16154 "preserved combo line coverage changed"
16155 );
16156 assert_eq!(opaque_combo_count, 1, "preserved combo coverage changed");
16157 assert_eq!(typed_bar_count + opaque_bar_count, 12);
16158 assert_eq!(typed_line_count + opaque_line_count, 3);
16159 eprintln!(
16160 "ChartML plot corpus gate checked {typed_bar_count} typed bar, {typed_line_count} typed line, and one preserved bar-line combination"
16161 );
16162 }
16163
16164 fn verify_remaining_plot_viewer_gate(corpus: &Path) {
16165 verify_fetched_corpus(corpus);
16166 assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
16167 assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
16168 let source = corpus.join("bar-chart.pptx");
16169 let source_sha = sha256(&source);
16170 let temp_root = std::env::temp_dir().join(format!(
16171 "oxml-chart-f122-viewer-gate-{}",
16172 std::process::id()
16173 ));
16174 if temp_root.exists() {
16175 fs::remove_dir_all(&temp_root).expect("remove stale F-122 evidence");
16176 }
16177 fs::create_dir_all(&temp_root).expect("create F-122 evidence root");
16178
16179 for (kind, plot, axes) in remaining_plot_fixtures() {
16180 let mut package = OpcPackage::open(&source).expect("open F-122 viewer source");
16181 package.set_part(
16182 "/ppt/charts/chart1.xml",
16183 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes())
16184 .expect("parse F-122 viewer chart")
16185 .to_xml()
16186 .expect("serialize F-122 viewer chart"),
16187 );
16188 let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
16189 package.save(&unbound).expect("save F-122 viewer candidate");
16190 let candidate_sha = sha256(&unbound);
16191 let evidence = temp_root.join(format!("{source_sha}-{candidate_sha}"));
16192 fs::create_dir(&evidence).expect("create SHA-bound F-122 evidence directory");
16193 let candidate = evidence.join(format!("{kind}-candidate.pptx"));
16194 fs::rename(&unbound, &candidate).expect("bind F-122 candidate to SHA");
16195 assert_eq!(sha256(&candidate), candidate_sha);
16196 let render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
16197 let bytes = fs::read(&render).expect("read F-122 viewer PPM");
16198 let (width, height, pixels) = ppm_pixels(&bytes);
16199 let left = width / 5;
16200 let right = width * 4 / 5;
16201 let top = height * 3 / 20;
16202 let bottom = height * 17 / 20;
16203 let mut nonblank = 0usize;
16204 for y in top..bottom {
16205 for x in left..right {
16206 let offset = (y * width + x) * 3;
16207 if pixels[offset..offset + 3]
16208 .iter()
16209 .any(|channel| *channel < 245)
16210 {
16211 nonblank += 1;
16212 }
16213 }
16214 }
16215 const CHART_RECTANGLE_NONBLANK_THRESHOLD: usize = 1_000;
16216 assert!(
16217 nonblank >= CHART_RECTANGLE_NONBLANK_THRESHOLD,
16218 "{kind}: chart rectangle [{left},{top}) to [{right},{bottom}) contains {nonblank} nonblank pixels, below {CHART_RECTANGLE_NONBLANK_THRESHOLD}"
16219 );
16220 eprintln!(
16221 "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}",
16222 sha256(&render)
16223 );
16224 }
16225 fs::remove_dir_all(&temp_root).expect("remove F-122 viewer evidence");
16226 }
16227
16228 fn verify_bar_and_line_viewer_gate(corpus: &Path) {
16229 verify_fetched_corpus(corpus);
16230 assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
16231 assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
16232 let temp_root = std::env::temp_dir().join(format!(
16233 "oxml-chart-f121-viewer-gate-{}",
16234 std::process::id()
16235 ));
16236 if temp_root.exists() {
16237 fs::remove_dir_all(&temp_root).expect("remove stale F-121 evidence");
16238 }
16239 fs::create_dir_all(&temp_root).expect("create F-121 evidence root");
16240
16241 for (kind, deck) in [("bar", "bar-chart.pptx"), ("line", "line-chart.pptx")] {
16242 let source = corpus.join(deck);
16243 let original_sha = sha256(&source);
16244 let mut package = OpcPackage::open(&source)
16245 .unwrap_or_else(|error| panic!("{deck}: open viewer source: {error}"));
16246 let chart_part = "/ppt/charts/chart1.xml";
16247 let original_parts = package.parts.clone();
16248 let parsed = CT_ChartSpace::from_xml(
16249 package
16250 .get_part(chart_part)
16251 .unwrap_or_else(|| panic!("{deck}: missing {chart_part}")),
16252 )
16253 .unwrap_or_else(|error| panic!("{deck}: parse viewer chart: {error}"));
16254 assert!(
16255 parsed.chart.plot_area.plots().is_ok(),
16256 "{deck}: plot is opaque"
16257 );
16258 package.set_part(
16259 chart_part,
16260 parsed
16261 .to_xml()
16262 .unwrap_or_else(|error| panic!("{deck}: serialize viewer chart: {error}")),
16263 );
16264 for (part, bytes) in &original_parts {
16265 if part == chart_part {
16266 assert_ne!(package.parts[part], *bytes, "{deck}: chart did not rewrite");
16267 } else {
16268 assert_eq!(package.parts[part], *bytes, "{deck}: changed part {part}");
16269 }
16270 }
16271
16272 let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
16273 package
16274 .save(&unbound)
16275 .unwrap_or_else(|error| panic!("{deck}: save viewer candidate: {error}"));
16276 let candidate_sha = sha256(&unbound);
16277 let evidence = temp_root.join(format!("{original_sha}-{candidate_sha}"));
16278 fs::create_dir(&evidence).expect("create SHA-bound F-121 evidence directory");
16279 let original = evidence.join(format!("{kind}-original.pptx"));
16280 let candidate = evidence.join(format!("{kind}-candidate.pptx"));
16281 fs::copy(&source, &original).expect("copy SHA-bound viewer original");
16282 fs::rename(&unbound, &candidate).expect("bind viewer candidate to SHA");
16283 assert_eq!(sha256(&original), original_sha);
16284 assert_eq!(sha256(&candidate), candidate_sha);
16285
16286 let original_render = render_deck_to_ppm(&original, &evidence, kind, "original");
16287 let candidate_render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
16288 let original_bytes = fs::read(&original_render).expect("read original PPM");
16289 let candidate_bytes = fs::read(&candidate_render).expect("read candidate PPM");
16290 let normalized_mae = normalized_ppm_mae(&original_bytes, &candidate_bytes);
16291 assert!(
16292 normalized_mae <= PLOT_RENDER_NORMALIZED_MAE_THRESHOLD,
16293 "{deck}: normalized RGB MAE {normalized_mae:.8} exceeds {:.8}",
16294 PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
16295 );
16296 eprintln!(
16297 "F-121 {kind} viewer gate original deck {original_sha}, candidate deck {candidate_sha}, original render {}, candidate render {}, normalized RGB MAE {normalized_mae:.8} <= {:.8}",
16298 sha256(&original_render),
16299 sha256(&candidate_render),
16300 PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
16301 );
16302 }
16303 fs::remove_dir_all(&temp_root).expect("remove F-121 viewer evidence");
16304 }
16305
16306 fn render_deck_to_ppm(deck: &Path, evidence: &Path, kind: &str, side: &str) -> PathBuf {
16307 let profile = std::env::temp_dir().join(format!(
16308 "oxml-chart-f121-{kind}-{side}-profile-{}",
16309 std::process::id()
16310 ));
16311 if profile.exists() {
16312 fs::remove_dir_all(&profile).expect("remove stale F-121 viewer profile");
16313 }
16314 let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
16315 let conversion = Command::new("soffice")
16316 .args([
16317 "--headless",
16318 &profile_argument,
16319 "--convert-to",
16320 "pdf:impress_pdf_Export",
16321 "--outdir",
16322 ])
16323 .arg(evidence)
16324 .arg(deck)
16325 .output()
16326 .expect("run pinned LibreOffice F-121 viewer gate");
16327 assert!(
16328 conversion.status.success(),
16329 "{}: LibreOffice conversion failed: {}",
16330 deck.display(),
16331 String::from_utf8_lossy(&conversion.stderr)
16332 );
16333 let pdf = deck.with_extension("pdf");
16334 assert!(
16335 pdf.is_file(),
16336 "LibreOffice did not create {}",
16337 pdf.display()
16338 );
16339 let prefix = evidence.join(format!("{kind}-{side}"));
16340 let raster = Command::new("pdftoppm")
16341 .args(["-f", "1", "-singlefile", "-r", "150"])
16342 .arg(&pdf)
16343 .arg(&prefix)
16344 .output()
16345 .expect("run pinned Poppler F-121 raster gate");
16346 assert!(
16347 raster.status.success(),
16348 "{}: pdftoppm failed: {}",
16349 pdf.display(),
16350 String::from_utf8_lossy(&raster.stderr)
16351 );
16352 if profile.exists() {
16353 fs::remove_dir_all(&profile).expect("remove F-121 viewer profile");
16354 }
16355 prefix.with_extension("ppm")
16356 }
16357
16358 fn normalized_ppm_mae(left: &[u8], right: &[u8]) -> f64 {
16359 let (left_width, left_height, left_pixels) = ppm_pixels(left);
16360 let (right_width, right_height, right_pixels) = ppm_pixels(right);
16361 assert_eq!((left_width, left_height), (right_width, right_height));
16362 assert_eq!(left_pixels.len(), right_pixels.len());
16363 let difference: u64 = left_pixels
16364 .iter()
16365 .zip(right_pixels)
16366 .map(|(left, right)| u64::from(left.abs_diff(*right)))
16367 .sum();
16368 difference as f64 / (left_pixels.len() as f64 * 255.0)
16369 }
16370
16371 fn ppm_pixels(bytes: &[u8]) -> (usize, usize, &[u8]) {
16372 let mut cursor = 0usize;
16373 let mut tokens = Vec::new();
16374 while tokens.len() < 4 {
16375 while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
16376 cursor += 1;
16377 }
16378 if cursor < bytes.len() && bytes[cursor] == b'#' {
16379 while cursor < bytes.len() && bytes[cursor] != b'\n' {
16380 cursor += 1;
16381 }
16382 continue;
16383 }
16384 let start = cursor;
16385 while cursor < bytes.len() && !bytes[cursor].is_ascii_whitespace() {
16386 cursor += 1;
16387 }
16388 tokens.push(std::str::from_utf8(&bytes[start..cursor]).expect("PPM header UTF-8"));
16389 }
16390 assert_eq!(tokens[0], "P6");
16391 let width = tokens[1].parse::<usize>().expect("PPM width");
16392 let height = tokens[2].parse::<usize>().expect("PPM height");
16393 assert_eq!(tokens[3], "255");
16394 assert!(
16395 cursor < bytes.len() && bytes[cursor].is_ascii_whitespace(),
16396 "PPM header must end with whitespace"
16397 );
16398 cursor += 1;
16399 let pixels = &bytes[cursor..];
16400 assert_eq!(pixels.len(), width * height * 3, "PPM pixel length");
16401 (width, height, pixels)
16402 }
16403}