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::{ChartCellValue, ChartRange, ChartRangeUpdate, 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
291pub fn read_chart_data(chart_xml: &[u8]) -> Result<(ChartKind, ChartData)> {
295 let space = CT_ChartSpace::from_xml(chart_xml)?;
296 let [plot] = space.chart.plot_area.plots()? else {
297 return Err(invalid_authoring_value(
298 "chart data projection requires one supported plot",
299 ));
300 };
301 let (kind, series) = match plot {
302 Plot::Bar { series, .. } => (ChartKind::Bar, series),
303 Plot::Line { series, .. } => (ChartKind::Line, series),
304 Plot::Pie { series, .. } => (ChartKind::Pie, series),
305 Plot::Doughnut { series, .. } => (ChartKind::Doughnut, series),
306 Plot::Area { series, .. } => (ChartKind::Area, series),
307 Plot::Scatter { series, .. } => (ChartKind::Scatter, series),
308 Plot::Radar { series, .. } => (ChartKind::Radar, series),
309 };
310 let first = series
311 .first()
312 .ok_or_else(|| invalid_authoring_value("chart has no series"))?;
313 let categories = chart_category_labels(first)?;
314 let mut projected = Vec::with_capacity(series.len());
315 for item in series {
316 if chart_category_labels(item)? != categories {
317 return Err(invalid_authoring_value(
318 "chart series use different category or X vectors",
319 ));
320 }
321 let name = match &item.name {
322 Some(name) => name.values.first().cloned().unwrap_or_default(),
323 None if item.opaque_name => chart_literal_name(item)?,
324 None => String::new(),
325 };
326 projected.push((name, chart_dense_numbers(&item.values)?));
327 }
328 Ok((
329 kind,
330 ChartData {
331 categories,
332 series: projected,
333 number_format: (first.values.format_code != "General")
334 .then(|| first.values.format_code.clone()),
335 ..ChartData::default()
336 },
337 ))
338}
339
340fn chart_dense_numbers(data: &NumericData) -> Result<Vec<f64>> {
341 let (count, indexes) = cache_layout(&data.markup, data.values.len())?;
342 let count = chart_projection_count(count)?;
343 let mut values = vec![f64::NAN; count];
344 for (index, value) in indexes.into_iter().zip(&data.values) {
345 values[index as usize] = *value;
346 }
347 Ok(values)
348}
349
350fn chart_projection_count(count: u32) -> Result<usize> {
351 if count > 1_048_576 {
352 return Err(invalid_attribute("ptCount", "val", count.to_string()));
353 }
354 Ok(count as usize)
355}
356
357fn chart_category_labels(series: &Series) -> Result<Vec<String>> {
358 let (markup, labels) = match &series.categories {
359 Some(AxisData::String(data)) => (&data.markup, data.values.clone()),
360 Some(AxisData::Numeric(data)) => (
361 &data.markup,
362 data.values.iter().map(|value| value.to_string()).collect(),
363 ),
364 None if series.opaque_categories => {
365 return Err(invalid_authoring_value(
366 "unsupported literal or multilevel category projection",
367 ));
368 }
369 None => return Ok(Vec::new()),
370 };
371 let (count, indexes) = cache_layout(markup, labels.len())?;
372 let mut values = vec![String::new(); chart_projection_count(count)?];
373 for (index, value) in indexes.into_iter().zip(labels) {
374 values[index as usize] = value;
375 }
376 Ok(values)
377}
378
379fn chart_literal_name(series: &Series) -> Result<String> {
380 for raw in series.raw_children.at(2) {
381 let inherited = series
382 .namespace_declarations
383 .iter()
384 .filter_map(|(name, value)| {
385 let prefix = if name == "xmlns" {
386 ""
387 } else {
388 name.strip_prefix("xmlns:")?
389 };
390 Some((prefix.as_bytes().to_vec(), value.clone()))
391 })
392 .collect::<NamespaceBindings>();
393 let mut reader = Reader::from_reader(raw);
394 let mut buffer = Vec::new();
395 let mut namespaces = inherited;
396 let mut depth = 0usize;
397 loop {
398 match reader
399 .read_event_into(&mut buffer)
400 .map_err(OxmlError::from)?
401 {
402 Event::Start(element) => {
403 if depth == 0 {
404 if chart_child_local(&element, &namespaces)?.as_deref() != Some(b"tx") {
405 break;
406 }
407 namespaces = chart_bindings(&namespaces, &element)?;
408 }
409 if depth == 1
410 && chart_child_local(&element, &namespaces)?.as_deref() == Some(b"v")
411 {
412 let value = capture_element(&mut reader, &element)?;
413 return Ok(parse_text_element(&value, b"v", &namespaces)?.0);
414 }
415 depth += 1;
416 }
417 Event::End(_) => {
418 depth = depth.saturating_sub(1);
419 }
420 Event::Eof => break,
421 _ => {}
422 }
423 buffer.clear();
424 }
425 }
426 Err(invalid_authoring_value(
427 "unsupported series name projection",
428 ))
429}
430
431pub fn chart_workbook_relationship_id(chart_xml: &[u8]) -> Result<Option<String>> {
433 use quick_xml::name::ResolveResult;
434 use quick_xml::reader::NsReader;
435 CT_ChartSpace::from_xml(chart_xml)?;
436 let mut reader = NsReader::from_reader(chart_xml);
437 let mut buffer = Vec::new();
438 let mut depth = 0usize;
439 let mut result = None;
440 loop {
441 let (namespace, event) = reader
442 .read_resolved_event_into(&mut buffer)
443 .map_err(OxmlError::from)?;
444 let empty = matches!(&event, Event::Empty(_));
445 match (namespace, event) {
446 (namespace, Event::Start(element) | Event::Empty(element)) => {
447 if depth == 1
448 && matches!(namespace, ResolveResult::Bound(value) if value.as_ref() == C_NS.as_bytes())
449 && element.local_name().as_ref() == b"externalData"
450 {
451 if result.is_some() {
452 return Err(ChartError::DuplicateElement("c:externalData".into()));
453 }
454 for attribute in element.attributes() {
455 let attribute = attribute.map_err(OxmlError::from)?;
456 let (namespace, local) = reader.resolver().resolve_attribute(attribute.key);
457 if matches!(namespace, ResolveResult::Bound(value) if value.as_ref() == R_NS.as_bytes())
458 && local.as_ref() == b"id"
459 {
460 result = Some(
461 attribute
462 .decoded_and_normalized_value(
463 XmlVersion::Implicit1_0,
464 reader.decoder(),
465 )
466 .map_err(OxmlError::from)?
467 .into_owned(),
468 );
469 }
470 }
471 if result.is_none() {
472 return Err(ChartError::MissingElement("c:externalData/@r:id".into()));
473 }
474 }
475 if !empty {
476 depth += 1;
477 }
478 }
479 (_, Event::End(_)) => {
480 depth = depth.saturating_sub(1);
481 }
482 (_, Event::DocType(_)) => {
483 return Err(invalid_authoring_value(
484 "DOCTYPE is unsupported in chart data",
485 ));
486 }
487 (_, Event::Eof) => break,
488 _ => {}
489 }
490 buffer.clear();
491 }
492 Ok(result)
493}
494
495#[derive(Debug)]
496struct ChartSourceNode {
497 local: Vec<u8>,
498 qualified: bool,
499 parent: Option<usize>,
500 start: usize,
501 end: usize,
502}
503
504fn chart_source_nodes(xml: &[u8]) -> Result<Vec<ChartSourceNode>> {
505 use quick_xml::name::ResolveResult;
506 use quick_xml::reader::NsReader;
507 let mut reader = NsReader::from_reader(xml);
508 let mut nodes: Vec<ChartSourceNode> = Vec::new();
509 let mut stack = Vec::new();
510 let mut buffer = Vec::new();
511 loop {
512 let start = reader.buffer_position() as usize;
513 let event = reader
514 .read_event_into(&mut buffer)
515 .map_err(OxmlError::from)?;
516 let end = reader.buffer_position() as usize;
517 let empty = matches!(&event, Event::Empty(_));
518 match event {
519 Event::Start(element) | Event::Empty(element) => {
520 let (namespace, local) = reader.resolver().resolve_element(element.name());
521 let qualified = matches!(namespace,ResolveResult::Bound(value) if value.as_ref()==C_NS.as_bytes());
522 let local = local.as_ref().to_vec();
523 let index = nodes.len();
524 if index >= 1_000_000 {
525 return Err(invalid_authoring_value("chart XML node limit"));
526 }
527 nodes.push(ChartSourceNode {
528 local,
529 qualified,
530 parent: stack.last().copied(),
531 start,
532 end,
533 });
534 if !empty {
535 stack.push(index);
536 }
537 }
538 Event::End(_) => {
539 let index = stack
540 .pop()
541 .ok_or_else(|| invalid_authoring_value("unbalanced chart source"))?;
542 nodes[index].end = end;
543 }
544 Event::DocType(_) => {
545 return Err(invalid_authoring_value(
546 "DOCTYPE is unsupported in chart source",
547 ));
548 }
549 Event::Eof => break,
550 _ => {}
551 }
552 buffer.clear();
553 }
554 if !stack.is_empty() {
555 return Err(invalid_authoring_value("truncated chart source"));
556 }
557 Ok(nodes)
558}
559
560fn chart_child_node(nodes: &[ChartSourceNode], parent: usize, local: &[u8]) -> Result<usize> {
561 let found = nodes
562 .iter()
563 .enumerate()
564 .filter(|(_, node)| node.parent == Some(parent) && node.qualified && node.local == local)
565 .map(|(index, _)| index)
566 .collect::<Vec<_>>();
567 let [index] = found.as_slice() else {
568 return Err(invalid_authoring_value(format!(
569 "chart data requires one owned {}",
570 String::from_utf8_lossy(local)
571 )));
572 };
573 Ok(*index)
574}
575
576fn chart_source_series(xml: &[u8]) -> Result<Vec<Series>> {
577 CT_ChartSpace::from_xml(xml)?.chart.plot_area.series()
578}
579
580fn resized_chart_range(original: &ChartRange, count: usize) -> Result<ChartRange> {
581 let mut range = original.clone();
582 range.last_row = range
583 .first_row
584 .checked_add(
585 count
586 .checked_sub(1)
587 .ok_or_else(|| invalid_authoring_value("empty chart data range"))?,
588 )
589 .ok_or_else(|| invalid_authoring_value("chart data row overflow"))?;
590 range.formula().map_err(invalid_authoring_value)?;
591 Ok(range)
592}
593
594fn replaced_string_reference(
595 original: &StringRef,
596 formula: String,
597 values: Vec<String>,
598) -> StringRef {
599 let mut result = original.clone();
600 result.formula = formula;
601 result.values = values;
602 result.markup.declared_point_count = None;
603 result.markup.point_indexes.clear();
604 result
605}
606
607fn replaced_numeric_reference(
608 original: &NumericData,
609 formula: String,
610 values: Vec<f64>,
611 number_format: Option<&str>,
612) -> NumericData {
613 let mut result = original.clone();
614 result.formula = formula;
615 result.values = values;
616 if let Some(format) = number_format {
617 result.format_code = format.to_owned();
618 }
619 result.markup.declared_point_count = None;
620 result.markup.point_indexes.clear();
621 result
622}
623
624pub fn replace_chart_data(
628 chart_xml: &[u8],
629 workbook: &[u8],
630 data: &ChartData,
631) -> Result<(Vec<u8>, Vec<u8>)> {
632 let (kind, current) = read_chart_data(chart_xml)?;
633 validate_chart_data(kind, data)?;
634 if data.category_axis_title.is_some()
635 || data.value_axis_title.is_some()
636 || !data.palette.is_empty()
637 {
638 return Err(invalid_authoring_value(
639 "replace_data changes chart data, not axis titles or palette",
640 ));
641 }
642 let workbook_id = chart_workbook_relationship_id(chart_xml)?
643 .ok_or_else(|| invalid_authoring_value("chart has no embedded workbook reference"))?;
644 let old_series = chart_source_series(chart_xml)?;
645 let first = old_series
646 .first()
647 .ok_or_else(|| invalid_authoring_value("chart has no editable series"))?;
648 let first_name = first
649 .name
650 .as_ref()
651 .ok_or_else(|| invalid_authoring_value("series title needs an owned workbook formula"))?;
652 let name_origin =
653 ChartRange::from_formula(&first_name.formula).map_err(invalid_authoring_value)?;
654 let values_origin =
655 ChartRange::from_formula(&first.values.formula).map_err(invalid_authoring_value)?;
656 if name_origin.first_row != name_origin.last_row || name_origin.sheet != values_origin.sheet {
657 return Err(invalid_authoring_value(
658 "series title and values must own one embedded worksheet",
659 ));
660 }
661 let category_origin = match first.categories.as_ref() {
662 Some(AxisData::String(reference)) => ChartRange::from_formula(&reference.formula),
663 Some(AxisData::Numeric(reference)) => ChartRange::from_formula(&reference.formula),
664 None => {
665 return Err(invalid_authoring_value(
666 "categories need an owned workbook formula",
667 ));
668 }
669 }
670 .map_err(invalid_authoring_value)?;
671 if category_origin.sheet != values_origin.sheet {
672 return Err(invalid_authoring_value(
673 "chart data must own one embedded worksheet",
674 ));
675 }
676 for series in &old_series {
678 for formula in [
679 series.name.as_ref().map(|r| r.formula.as_str()),
680 Some(series.values.formula.as_str()),
681 series.categories.as_ref().map(|r| match r {
682 AxisData::String(r) => r.formula.as_str(),
683 AxisData::Numeric(r) => r.formula.as_str(),
684 }),
685 ] {
686 let formula = formula.ok_or_else(|| {
687 invalid_authoring_value("series needs ordinary formula-backed data")
688 })?;
689 let range = ChartRange::from_formula(formula).map_err(invalid_authoring_value)?;
690 if range.sheet != values_origin.sheet {
691 return Err(invalid_authoring_value(
692 "series formula refers to a different worksheet",
693 ));
694 }
695 }
696 }
697 if data.categories == current.categories
698 && data.series == current.series
699 && data.number_format.as_ref().is_none_or(|format| {
700 Some(format) == current.number_format.as_ref()
701 || (format == "General" && current.number_format.is_none())
702 })
703 {
704 oxml_sml::chart_unused_columns(workbook, &values_origin.sheet, 0)
705 .map_err(invalid_authoring_value)?;
706 return Ok((chart_xml.to_vec(), workbook.to_vec()));
707 }
708 let nodes = chart_source_nodes(chart_xml)?;
709 let chart = chart_child_node(&nodes, 0, b"chart")?;
710 let plot_area = chart_child_node(&nodes, chart, b"plotArea")?;
711 let plot_local = match kind {
712 ChartKind::Bar => b"barChart".as_slice(),
713 ChartKind::Line => b"lineChart",
714 ChartKind::Pie => b"pieChart",
715 ChartKind::Doughnut => b"doughnutChart",
716 ChartKind::Area => b"areaChart",
717 ChartKind::Scatter => b"scatterChart",
718 ChartKind::Radar => b"radarChart",
719 };
720 let plot = chart_child_node(&nodes, plot_area, plot_local)?;
721 let series_nodes = nodes
722 .iter()
723 .enumerate()
724 .filter(|(_, node)| node.parent == Some(plot) && node.qualified && node.local == b"ser")
725 .map(|(index, _)| index)
726 .collect::<Vec<_>>();
727 if series_nodes.len() != old_series.len() {
728 return Err(invalid_authoring_value(
729 "editable series source inventory disagrees",
730 ));
731 }
732 let mut updates = Vec::new();
733 let mut patches = Vec::new();
734 for (ordinal, (old, &source_index)) in old_series.iter().zip(&series_nodes).enumerate() {
735 let old_name = old
736 .name
737 .as_ref()
738 .ok_or_else(|| invalid_authoring_value("series title needs formula-backed data"))?;
739 let old_name_range =
740 ChartRange::from_formula(&old_name.formula).map_err(invalid_authoring_value)?;
741 if old_name_range.first_row != old_name_range.last_row {
742 return Err(invalid_authoring_value(
743 "series title formula must own one cell",
744 ));
745 }
746 let old_value_range =
747 ChartRange::from_formula(&old.values.formula).map_err(invalid_authoring_value)?;
748 let categories = old
749 .categories
750 .as_ref()
751 .ok_or_else(|| invalid_authoring_value("series categories are missing"))?;
752 let old_category_range = ChartRange::from_formula(match categories {
753 AxisData::String(r) => &r.formula,
754 AxisData::Numeric(r) => &r.formula,
755 })
756 .map_err(invalid_authoring_value)?;
757 let Some((name, values)) = data.series.get(ordinal) else {
758 for original in [old_name_range, old_value_range, old_category_range] {
759 updates.push(ChartRangeUpdate {
760 number_format: None,
761 original: Some(original),
762 target: None,
763 values: Vec::new(),
764 });
765 }
766 patches.push((
767 nodes[source_index].start,
768 nodes[source_index].end,
769 Vec::new(),
770 ));
771 continue;
772 };
773 let new_value_range = resized_chart_range(&old_value_range, values.len())?;
774 let new_category_range = resized_chart_range(&old_category_range, data.categories.len())?;
775 updates.push(ChartRangeUpdate {
776 number_format: None,
777 original: Some(old_name_range.clone()),
778 target: Some(old_name_range.clone()),
779 values: vec![ChartCellValue::Text(name.clone())],
780 });
781 updates.push(ChartRangeUpdate {
782 number_format: Some(
783 data.number_format
784 .as_deref()
785 .unwrap_or(&old.values.format_code)
786 .to_owned(),
787 ),
788 original: Some(old_value_range),
789 target: Some(new_value_range.clone()),
790 values: values.iter().copied().map(ChartCellValue::Number).collect(),
791 });
792 let category_cells = if matches!(categories, AxisData::Numeric(_)) {
793 data.categories
794 .iter()
795 .map(|value| {
796 value
797 .parse::<f64>()
798 .map(ChartCellValue::Number)
799 .map_err(invalid_authoring_value)
800 })
801 .collect::<Result<Vec<_>>>()?
802 } else {
803 data.categories
804 .iter()
805 .cloned()
806 .map(ChartCellValue::Text)
807 .collect()
808 };
809 updates.push(ChartRangeUpdate {
810 number_format: None,
811 original: Some(old_category_range),
812 target: Some(new_category_range.clone()),
813 values: category_cells,
814 });
815 let tx = chart_child_node(&nodes, source_index, b"tx")?;
816 let name_ref = chart_child_node(&nodes, tx, b"strRef")?;
817 let name_reference = replaced_string_reference(
818 old_name,
819 old_name_range.formula().map_err(invalid_authoring_value)?,
820 vec![name.clone()],
821 );
822 patches.push((
823 nodes[name_ref].start,
824 nodes[name_ref].end,
825 name_reference.to_xml()?,
826 ));
827 let value_wrapper = chart_child_node(
828 &nodes,
829 source_index,
830 if kind == ChartKind::Scatter {
831 b"yVal"
832 } else {
833 b"val"
834 },
835 )?;
836 let value_ref = chart_child_node(&nodes, value_wrapper, b"numRef")?;
837 let value_reference = replaced_numeric_reference(
838 &old.values,
839 new_value_range.formula().map_err(invalid_authoring_value)?,
840 values.clone(),
841 data.number_format.as_deref(),
842 );
843 patches.push((
844 nodes[value_ref].start,
845 nodes[value_ref].end,
846 value_reference.to_xml()?,
847 ));
848 let category_wrapper = chart_child_node(
849 &nodes,
850 source_index,
851 if kind == ChartKind::Scatter {
852 b"xVal"
853 } else {
854 b"cat"
855 },
856 )?;
857 let (local, bytes) = match categories {
858 AxisData::String(reference) => (
859 b"strRef".as_slice(),
860 replaced_string_reference(
861 reference,
862 new_category_range
863 .formula()
864 .map_err(invalid_authoring_value)?,
865 data.categories.clone(),
866 )
867 .to_xml()?,
868 ),
869 AxisData::Numeric(reference) => (
870 b"numRef".as_slice(),
871 replaced_numeric_reference(
872 reference,
873 new_category_range
874 .formula()
875 .map_err(invalid_authoring_value)?,
876 data.categories
877 .iter()
878 .map(|v| v.parse::<f64>().map_err(invalid_authoring_value))
879 .collect::<Result<Vec<_>>>()?,
880 None,
881 )
882 .to_xml()?,
883 ),
884 };
885 let category_ref = chart_child_node(&nodes, category_wrapper, local)?;
886 patches.push((nodes[category_ref].start, nodes[category_ref].end, bytes));
887 }
888 let added = data.series.len().saturating_sub(old_series.len());
889 if added > 0 {
890 let free = oxml_sml::chart_unused_columns(workbook, &values_origin.sheet, added)
891 .map_err(invalid_authoring_value)?;
892 let (default_xml, _) = authored_chart_parts(kind, data, &workbook_id)?;
893 let defaults = chart_source_series(&default_xml)?;
894 let max_index = old_series.iter().map(|s| s.index).max().unwrap();
895 let max_order = old_series.iter().map(|s| s.order).max().unwrap();
896 let mut bytes = Vec::new();
897 for (offset, column) in free.into_iter().enumerate() {
898 let ordinal = old_series.len() + offset;
899 let (name, values) = &data.series[ordinal];
900 let mut series = defaults[ordinal].clone();
901 let step = u32::try_from(offset + 1).map_err(invalid_authoring_value)?;
902 series.index = max_index
903 .checked_add(step)
904 .ok_or_else(|| invalid_authoring_value("chart series index overflow"))?;
905 series.order = max_order
906 .checked_add(step)
907 .ok_or_else(|| invalid_authoring_value("chart series order overflow"))?;
908 let mut title = name_origin.clone();
909 title.column = column;
910 let mut numbers = resized_chart_range(&values_origin, values.len())?;
911 numbers.column = column;
912 let category = resized_chart_range(&category_origin, data.categories.len())?;
913 series.name = Some(StringRef::new(
914 title.formula().map_err(invalid_authoring_value)?,
915 vec![name.clone()],
916 )?);
917 series.values = NumericData::new(
918 numbers.formula().map_err(invalid_authoring_value)?,
919 data.number_format
920 .as_deref()
921 .unwrap_or(&first.values.format_code)
922 .to_owned(),
923 values.clone(),
924 )?;
925 series.categories = Some(if kind == ChartKind::Scatter {
926 AxisData::Numeric(NumericData::new(
927 category.formula().map_err(invalid_authoring_value)?,
928 "General".into(),
929 data.categories
930 .iter()
931 .map(|v| v.parse::<f64>().map_err(invalid_authoring_value))
932 .collect::<Result<Vec<_>>>()?,
933 )?)
934 } else {
935 AxisData::String(StringRef::new(
936 category.formula().map_err(invalid_authoring_value)?,
937 data.categories.clone(),
938 )?)
939 });
940 updates.push(ChartRangeUpdate {
941 number_format: None,
942 original: None,
943 target: Some(title),
944 values: vec![ChartCellValue::Text(name.clone())],
945 });
946 updates.push(ChartRangeUpdate {
947 number_format: Some(
948 data.number_format
949 .as_deref()
950 .unwrap_or(&first.values.format_code)
951 .to_owned(),
952 ),
953 original: None,
954 target: Some(numbers),
955 values: values.iter().copied().map(ChartCellValue::Number).collect(),
956 });
957 bytes.extend(series.to_xml_for_plot(kind == ChartKind::Scatter)?);
958 }
959 let position = nodes[*series_nodes.last().unwrap()].end;
960 patches.push((position, position, bytes));
961 }
962 let updated_workbook =
963 oxml_sml::merge_chart_ranges(workbook, &updates).map_err(invalid_authoring_value)?;
964 patches.sort_by_key(|(start, end, _)| (*start, *end));
965 for pair in patches.windows(2) {
966 if pair[0].1 > pair[1].0 {
967 return Err(invalid_authoring_value(
968 "overlapping chart data source edits",
969 ));
970 }
971 }
972 let mut updated_chart = chart_xml.to_vec();
973 for (start, end, bytes) in patches.into_iter().rev() {
974 updated_chart.splice(start..end, bytes);
975 }
976 let (_, projected) = read_chart_data(&updated_chart)?;
977 if projected.categories != data.categories || projected.series != data.series {
978 return Err(invalid_authoring_value(
979 "replacement chart cache projection disagrees with requested data",
980 ));
981 }
982 Ok((updated_chart, updated_workbook))
983}
984
985fn validate_chart_data(kind: ChartKind, data: &ChartData) -> Result<()> {
986 if data.categories.is_empty() {
987 return Err(invalid_authoring_value("at least one category is required"));
988 }
989 if data.series.is_empty() {
990 return Err(invalid_authoring_value("at least one series is required"));
991 }
992 if matches!(kind, ChartKind::Pie | ChartKind::Doughnut) && data.series.len() != 1 {
993 return Err(invalid_authoring_value(
994 "pie and doughnut charts require exactly one series",
995 ));
996 }
997 for (name, values) in &data.series {
998 if values.len() != data.categories.len() {
999 return Err(invalid_authoring_value(format!(
1000 "series {name:?} has {} values for {} categories",
1001 values.len(),
1002 data.categories.len()
1003 )));
1004 }
1005 if values.iter().any(|value| !value.is_finite()) {
1006 return Err(invalid_authoring_value(format!(
1007 "series {name:?} contains a nonfinite value"
1008 )));
1009 }
1010 }
1011 if kind == ChartKind::Scatter {
1012 for category in &data.categories {
1013 if category
1014 .parse::<f64>()
1015 .ok()
1016 .is_none_or(|value| !value.is_finite())
1017 {
1018 return Err(invalid_authoring_value(format!(
1019 "scatter category {category:?} is not a finite number"
1020 )));
1021 }
1022 }
1023 }
1024 if let Some(format) = &data.number_format {
1025 NumericData::new("Sheet1!$A$2".to_owned(), format.clone(), vec![0.0])?;
1026 }
1027 Ok(())
1028}
1029
1030fn invalid_authoring_value(error: impl fmt::Display) -> ChartError {
1031 ChartError::InvalidValue {
1032 element: "chart authoring".to_owned(),
1033 value: error.to_string(),
1034 }
1035}
1036
1037fn category_value_axes(category: AxisId, value: AxisId, data: &ChartData) -> Result<Vec<Axis>> {
1038 authored_axes(
1039 [
1040 Axis::new(AxisKind::Category, category, AxisPosition::Bottom, value),
1041 Axis::new(AxisKind::Value, value, AxisPosition::Left, category),
1042 ],
1043 data,
1044 )
1045}
1046
1047fn authored_axes(mut axes: [Axis; 2], data: &ChartData) -> Result<Vec<Axis>> {
1048 for axis in &mut axes {
1049 axis.set_deleted(false);
1050 }
1051 axes[0].title = data
1052 .category_axis_title
1053 .as_deref()
1054 .map(CT_Title::plain_text);
1055 axes[1].title = data.value_axis_title.as_deref().map(CT_Title::plain_text);
1056 axes[1].number_format = data
1057 .number_format
1058 .as_ref()
1059 .map(|format| NumberFormat::new(format.clone(), false))
1060 .transpose()?;
1061 Ok(axes.into())
1062}
1063
1064fn apply_palette(kind: ChartKind, data: &ChartData, series: &mut [Series]) {
1065 if data.palette.is_empty() {
1066 return;
1067 }
1068 for (index, item) in series.iter_mut().enumerate() {
1069 item.sp_pr = Some(series_shape(kind, data.palette[index % data.palette.len()]));
1070 }
1071 if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) && series.len() == 1 {
1072 series[0].authored_data_points = data
1073 .categories
1074 .iter()
1075 .enumerate()
1076 .map(|(index, _)| DataPoint {
1077 index: index as u32,
1078 sp_pr: series_shape(kind, data.palette[index % data.palette.len()]),
1079 })
1080 .collect();
1081 }
1082}
1083
1084fn series_shape(kind: ChartKind, color: RgbColor) -> CT_ShapeProperties {
1085 let mut solid = SolidFill::default();
1086 solid.color = Some(ColorChoice::srgb(color));
1087 let fill = Fill::Solid(solid);
1088 let mut properties = CT_ShapeProperties::default();
1089 if matches!(
1090 kind,
1091 ChartKind::Line | ChartKind::Scatter | ChartKind::Radar
1092 ) {
1093 let mut line = CT_LineProperties::default();
1094 line.fill = Some(fill);
1095 properties.line = Some(line);
1096 } else {
1097 properties.fill = Some(fill);
1098 }
1099 properties
1100}
1101
1102fn spreadsheet_column_name(mut index: usize) -> String {
1103 let mut name = Vec::new();
1104 index += 1;
1105 while index != 0 {
1106 let remainder = (index - 1) % 26;
1107 name.push((b'A' + remainder as u8) as char);
1108 index = (index - 1) / 26;
1109 }
1110 name.iter().rev().collect()
1111}
1112
1113#[derive(Clone, Debug)]
1115pub struct ChartGeometry {
1116 pub plot_bounds: Rect,
1117 pub elements: Vec<PositionedElement>,
1118}
1119
1120#[derive(Clone, Debug)]
1121struct AxisScale {
1122 domain: Domain,
1123 ticks: Vec<f64>,
1124}
1125
1126#[derive(Clone, Copy, Debug)]
1127struct AxisMapping {
1128 domain: Domain,
1129 orientation: Orientation,
1130}
1131
1132#[derive(Clone, Copy, Debug, Default)]
1133struct GeometryOptions {
1134 category_orientation: Orientation,
1135 value: Option<AxisMapping>,
1136 scatter_x: Option<AxisMapping>,
1137 scatter_y: Option<AxisMapping>,
1138}
1139
1140#[derive(Default)]
1141struct ChartAnnotations {
1142 gridlines: Vec<PositionedElement>,
1143 axis_lines: Vec<PositionedElement>,
1144 ticks: Vec<PositionedElement>,
1145 labels: Vec<PositionedElement>,
1146}
1147
1148pub fn render_chart(
1150 chart: &CT_Chart,
1151 bounds: Rect,
1152 theme: &CT_OfficeStyleSheet,
1153 color_map: &ColorMap,
1154 fonts: &mut FontManager,
1155) -> Result<GroupElement> {
1156 let plot_bounds = geometry_plot_bounds(bounds)?;
1157 let plots = chart.plot_area.plots()?;
1158 let plot = plots
1159 .first()
1160 .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
1161 plot.validate()?;
1162 let series_colours = resolve_series_colours(plot, theme, color_map)?;
1163 let axes = chart.plot_area.axes()?;
1164 let options = geometry_options(plot, &axes, chart.disp_blanks_as)?;
1165 let plot_children = render_plot_geometry(
1166 plot,
1167 plot_bounds,
1168 chart.disp_blanks_as,
1169 options,
1170 &series_colours,
1171 )?;
1172 if plot_children.is_empty() && !plot_can_render_without_marks(plot) {
1173 return Err(ChartError::InvalidValue {
1174 element: "c:plotArea".to_owned(),
1175 value: "plot has no renderable cached data".to_owned(),
1176 });
1177 }
1178
1179 let mut chart_annotations =
1180 render_axes_and_labels(plot, &axes, plot_bounds, chart.disp_blanks_as, fonts)?;
1181 let mut annotations = Vec::new();
1182 render_data_labels(
1183 plot,
1184 plot_bounds,
1185 chart.disp_blanks_as,
1186 options,
1187 fonts,
1188 &mut chart_annotations.labels,
1189 )?;
1190 render_legend(
1191 chart,
1192 plot,
1193 plot_bounds,
1194 fonts,
1195 &series_colours,
1196 &mut annotations,
1197 &mut chart_annotations.labels,
1198 )?;
1199
1200 validate_geometry_coordinates(&chart_annotations.gridlines)?;
1201 validate_geometry_coordinates(&plot_children)?;
1202 validate_geometry_coordinates(&chart_annotations.axis_lines)?;
1203 validate_geometry_coordinates(&chart_annotations.ticks)?;
1204 validate_geometry_coordinates(&annotations)?;
1205 let mut children = chart_annotations.gridlines;
1206 children.push(PositionedElement::Group(GroupElement {
1207 transform: Transform::IDENTITY,
1208 clip: Some(Path::rect(plot_bounds)),
1209 opacity: 1.0,
1210 effects: Vec::new(),
1211 children: plot_children,
1212 }));
1213 children.extend(chart_annotations.axis_lines);
1214 children.extend(chart_annotations.ticks);
1215 children.extend(annotations);
1216 children.extend(chart_annotations.labels);
1217 Ok(GroupElement {
1218 transform: Transform::IDENTITY,
1219 clip: None,
1220 opacity: 1.0,
1221 effects: Vec::new(),
1222 children,
1223 })
1224}
1225
1226pub fn render_chart_placeholder(bounds: Rect, fonts: &mut FontManager) -> Result<GroupElement> {
1228 if !bounds.width.is_finite()
1229 || !bounds.height.is_finite()
1230 || bounds.width <= 0.0
1231 || bounds.height <= 0.0
1232 {
1233 return Err(ChartError::InvalidValue {
1234 element: "chart fallback bounds".to_owned(),
1235 value: "width and height must be finite and positive".to_owned(),
1236 });
1237 }
1238 let label = shape_label(
1239 fonts,
1240 "Unsupported chart",
1241 Point {
1242 x: 4.0,
1243 y: (bounds.height / 2.0).max(9.0),
1244 },
1245 )?;
1246 Ok(GroupElement {
1247 transform: Transform::IDENTITY,
1248 clip: Some(Path::rect(Rect {
1249 x: 0.0,
1250 y: 0.0,
1251 width: bounds.width,
1252 height: bounds.height,
1253 })),
1254 opacity: 1.0,
1255 effects: Vec::new(),
1256 children: vec![PositionedElement::Text(label)],
1257 })
1258}
1259
1260pub fn render_geometry(
1262 chart: &CT_Chart,
1263 bounds: Rect,
1264 theme: &CT_OfficeStyleSheet,
1265 color_map: &ColorMap,
1266) -> Result<ChartGeometry> {
1267 let plot_bounds = geometry_plot_bounds(bounds)?;
1268 let plots = chart.plot_area.plots()?;
1269 let plot = plots
1270 .first()
1271 .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
1272 plot.validate()?;
1273 let series_colours = resolve_series_colours(plot, theme, color_map)?;
1274 let children = render_plot_geometry(
1275 plot,
1276 plot_bounds,
1277 chart.disp_blanks_as,
1278 geometry_options(plot, &[], chart.disp_blanks_as)?,
1279 &series_colours,
1280 )?;
1281 if children.is_empty() && !plot_can_render_without_marks(plot) {
1282 return Err(ChartError::InvalidValue {
1283 element: "c:plotArea".to_owned(),
1284 value: "plot has no renderable cached data".to_owned(),
1285 });
1286 }
1287 validate_geometry_coordinates(&children)?;
1288 Ok(ChartGeometry {
1289 plot_bounds,
1290 elements: vec![PositionedElement::Group(GroupElement {
1291 transform: Transform::IDENTITY,
1292 clip: None,
1293 opacity: 1.0,
1294 effects: Vec::new(),
1295 children,
1296 })],
1297 })
1298}
1299
1300fn plot_can_render_without_marks(plot: &Plot) -> bool {
1301 matches!(
1302 plot,
1303 Plot::Radar { series, .. }
1304 if series
1305 .iter()
1306 .any(|series| !series.values.values.is_empty())
1307 )
1308}
1309
1310fn geometry_plot_bounds(bounds: Rect) -> Result<Rect> {
1311 if ![bounds.x, bounds.y, bounds.width, bounds.height]
1312 .into_iter()
1313 .all(f64::is_finite)
1314 || bounds.width <= 0.0
1315 || bounds.height <= 0.0
1316 || !(bounds.x + bounds.width).is_finite()
1317 || !(bounds.y + bounds.height).is_finite()
1318 {
1319 return Err(ChartError::InvalidValue {
1320 element: "chart geometry bounds".to_owned(),
1321 value: format!(
1322 "x={}, y={}, width={}, height={}",
1323 bounds.x, bounds.y, bounds.width, bounds.height
1324 ),
1325 });
1326 }
1327 let plot_bounds = Rect {
1328 x: bounds.x + PLOT_MARGIN_LEFT,
1329 y: bounds.y + PLOT_MARGIN_TOP,
1330 width: bounds.width - PLOT_MARGIN_LEFT - PLOT_MARGIN_RIGHT,
1331 height: bounds.height - PLOT_MARGIN_TOP - PLOT_MARGIN_BOTTOM,
1332 };
1333 if ![
1334 plot_bounds.x,
1335 plot_bounds.y,
1336 plot_bounds.width,
1337 plot_bounds.height,
1338 plot_bounds.x + plot_bounds.width,
1339 plot_bounds.y + plot_bounds.height,
1340 ]
1341 .into_iter()
1342 .all(f64::is_finite)
1343 || plot_bounds.width <= 0.0
1344 || plot_bounds.height <= 0.0
1345 {
1346 return Err(ChartError::InvalidValue {
1347 element: "chart geometry bounds".to_owned(),
1348 value: "chart is too small for the fixed plot margins".to_owned(),
1349 });
1350 }
1351 Ok(plot_bounds)
1352}
1353
1354fn geometry_options(plot: &Plot, axes: &[Axis], blanks: DispBlanksAs) -> Result<GeometryOptions> {
1355 let mut options = GeometryOptions::default();
1356 if let Some(category_axis) = axes.iter().find(|axis| axis.kind != AxisKind::Value) {
1357 options.category_orientation = category_axis.scaling.orientation;
1358 }
1359
1360 if let Plot::Scatter { axis_ids, .. } = plot {
1361 let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
1362 if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[0]) {
1363 options.scatter_x = Some(axis_mapping(axis, x_domain)?);
1364 }
1365 if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[1]) {
1366 options.scatter_y = Some(axis_mapping(axis, y_domain)?);
1367 }
1368 } else if let Some(domain) = plot_value_domain(plot, blanks)? {
1369 options.value = Some(
1370 axes.iter()
1371 .find(|axis| axis.kind == AxisKind::Value)
1372 .map(|axis| axis_mapping(axis, domain))
1373 .transpose()?
1374 .unwrap_or(AxisMapping {
1375 domain,
1376 orientation: Orientation::MinMax,
1377 }),
1378 );
1379 }
1380 Ok(options)
1381}
1382
1383fn axis_mapping(axis: &Axis, extent: Domain) -> Result<AxisMapping> {
1384 if axis.scaling.log_base.is_some() {
1385 return Err(ChartError::InvalidValue {
1386 element: "c:scaling/c:logBase".to_owned(),
1387 value: "native chart rendering currently supports linear axes".to_owned(),
1388 });
1389 }
1390 let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
1391 Ok(AxisMapping {
1392 domain: scale.domain,
1393 orientation: axis.scaling.orientation,
1394 })
1395}
1396
1397fn plot_value_domain(plot: &Plot, blanks: DispBlanksAs) -> Result<Option<Domain>> {
1398 let domain = match plot {
1399 Plot::Bar {
1400 grouping, series, ..
1401 } => {
1402 let logical = logical_series_values(series)?;
1403 let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
1404 let layers =
1405 stacked_series_bounds(&logical, stacked, *grouping == BarGrouping::PercentStacked)?;
1406 domain_from_layers(&layers)?
1407 }
1408 Plot::Line {
1409 grouping, series, ..
1410 } => {
1411 let logical = logical_series_values(series)?;
1412 if *grouping == Grouping::Standard {
1413 let values = if blanks == DispBlanksAs::Zero {
1414 zero_filled_series(&logical, series_category_count(series)?)
1415 .into_iter()
1416 .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
1417 .collect::<Vec<_>>()
1418 } else {
1419 logical
1420 .into_iter()
1421 .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
1422 .collect::<Vec<_>>()
1423 };
1424 data_domain(&values, false)?
1425 } else {
1426 let layers =
1427 stacked_series_bounds(&logical, true, *grouping == Grouping::PercentStacked)?;
1428 domain_from_layers(&layers)?
1429 }
1430 }
1431 Plot::Area {
1432 grouping, series, ..
1433 } => {
1434 let logical = logical_series_values(series)?;
1435 let layers = stacked_series_bounds(
1436 &logical,
1437 *grouping != Grouping::Standard,
1438 *grouping == Grouping::PercentStacked,
1439 )?;
1440 domain_from_layers(&layers)?
1441 }
1442 Plot::Radar { series, .. } => {
1443 let values = series
1444 .iter()
1445 .flat_map(|series| series.values.values.iter().copied())
1446 .collect::<Vec<_>>();
1447 data_domain(&values, true)?
1448 }
1449 Plot::Pie { .. } | Plot::Doughnut { .. } | Plot::Scatter { .. } => return Ok(None),
1450 };
1451 Ok(Some(domain))
1452}
1453
1454fn scatter_domains(plot: &Plot, blanks: DispBlanksAs) -> Result<(Domain, Domain)> {
1455 let Plot::Scatter { series, .. } = plot else {
1456 return Err(ChartError::UnexpectedElement("non-scatter plot".to_owned()));
1457 };
1458 let mut x_values = Vec::new();
1459 let mut y_values = Vec::new();
1460 for series in series {
1461 for (_, x, y) in scatter_render_points(series, blanks)? {
1462 x_values.push(x);
1463 y_values.push(y);
1464 }
1465 }
1466 Ok((
1467 data_domain(&x_values, false)?,
1468 data_domain(&y_values, false)?,
1469 ))
1470}
1471
1472fn render_axes_and_labels(
1473 plot: &Plot,
1474 axes: &[Axis],
1475 bounds: Rect,
1476 blanks: DispBlanksAs,
1477 fonts: &mut FontManager,
1478) -> Result<ChartAnnotations> {
1479 if matches!(plot, Plot::Radar { .. }) {
1480 return render_radar_axes_and_labels(plot, axes, bounds, blanks, fonts);
1481 }
1482 let mut annotations = ChartAnnotations::default();
1483 let series = plot_series(plot);
1484 let category_count = if series.is_empty() {
1485 1
1486 } else {
1487 series_category_count(series)?
1488 };
1489 for axis in axes {
1490 if axis.deleted {
1491 continue;
1492 }
1493 axis_line(&mut annotations.axis_lines, bounds, axis.position);
1494 if axis.kind == AxisKind::Value {
1495 let extent = axis_extent(plot, axis, blanks)?;
1496 let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
1497 for tick in &scale.ticks {
1498 let coordinate = axis_tick_coordinate(
1499 *tick,
1500 scale.domain,
1501 bounds,
1502 axis.position,
1503 axis.scaling.orientation,
1504 )?;
1505 if axis.major_gridlines.is_some() {
1506 major_gridline(
1507 &mut annotations.gridlines,
1508 bounds,
1509 axis.position,
1510 coordinate,
1511 );
1512 }
1513 axis_tick(
1514 &mut annotations.ticks,
1515 bounds,
1516 axis.position,
1517 coordinate,
1518 axis.major_tick_mark,
1519 );
1520 if axis.tick_label_position != TickLabelPosition::None {
1521 let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
1522 let origin = tick_label_origin(
1523 bounds,
1524 axis.position,
1525 axis.tick_label_position,
1526 coordinate,
1527 );
1528 annotations
1529 .labels
1530 .push(PositionedElement::Text(shape_label_with_properties(
1531 fonts,
1532 &text,
1533 origin,
1534 axis.tx_pr.as_ref(),
1535 )?));
1536 }
1537 }
1538 } else {
1539 let count = category_count;
1540 if count > MAX_CATEGORY_ANNOTATIONS {
1541 return Err(ChartError::InvalidValue {
1542 element: "c:catAx annotations".to_owned(),
1543 value: format!(
1544 "logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"
1545 ),
1546 });
1547 }
1548 let category_labels: BTreeMap<_, _> =
1549 if axis.tick_label_position != TickLabelPosition::None {
1550 series
1551 .first()
1552 .map(|series| series_categories(series, axis.number_format.as_ref()))
1553 .transpose()?
1554 .unwrap_or_default()
1555 .into_iter()
1556 .collect()
1557 } else {
1558 BTreeMap::new()
1559 };
1560 for logical_index in 0..count {
1561 let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
1562 let fraction = (index as f64 + 0.5) / count as f64;
1563 let coordinate =
1564 if matches!(axis.position, AxisPosition::Bottom | AxisPosition::Top) {
1565 bounds.x + fraction * bounds.width
1566 } else {
1567 bounds.y + fraction * bounds.height
1568 };
1569 if axis.major_gridlines.is_some() {
1570 major_gridline(
1571 &mut annotations.gridlines,
1572 bounds,
1573 axis.position,
1574 coordinate,
1575 );
1576 }
1577 axis_tick(
1578 &mut annotations.ticks,
1579 bounds,
1580 axis.position,
1581 coordinate,
1582 axis.major_tick_mark,
1583 );
1584 if axis.tick_label_position != TickLabelPosition::None
1585 && let Some(text) = category_labels.get(&logical_index)
1586 {
1587 let origin = tick_label_origin(
1588 bounds,
1589 axis.position,
1590 axis.tick_label_position,
1591 coordinate,
1592 );
1593 annotations
1594 .labels
1595 .push(PositionedElement::Text(shape_label_with_properties(
1596 fonts,
1597 text,
1598 origin,
1599 axis.tx_pr.as_ref(),
1600 )?));
1601 }
1602 }
1603 }
1604 }
1605 Ok(annotations)
1606}
1607
1608fn render_radar_axes_and_labels(
1609 plot: &Plot,
1610 axes: &[Axis],
1611 bounds: Rect,
1612 blanks: DispBlanksAs,
1613 fonts: &mut FontManager,
1614) -> Result<ChartAnnotations> {
1615 let mut annotations = ChartAnnotations::default();
1616 let series = plot_series(plot);
1617 let count = series_category_count(series)?;
1618 if count > MAX_CATEGORY_ANNOTATIONS {
1619 return Err(ChartError::InvalidValue {
1620 element: "c:radarChart annotations".to_owned(),
1621 value: format!("logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"),
1622 });
1623 }
1624 let center = Point {
1625 x: bounds.x + bounds.width / 2.0,
1626 y: bounds.y + bounds.height / 2.0,
1627 };
1628 let radius = bounds.width.min(bounds.height) / 2.0;
1629 for axis in axes {
1630 if axis.deleted {
1631 continue;
1632 }
1633 if axis.kind == AxisKind::Value {
1634 let scale = nice_number_scale(
1635 axis_extent(plot, axis, blanks)?,
1636 axis.scaling.minimum,
1637 axis.scaling.maximum,
1638 6,
1639 )?;
1640 annotations.axis_lines.push(stroked_segment(
1641 center,
1642 radial_point(center, radius, 0, count),
1643 Color::BLACK,
1644 1.0,
1645 ));
1646 for tick in &scale.ticks {
1647 let distance =
1648 normalized_value_oriented(*tick, scale.domain, axis.scaling.orientation)?
1649 * radius;
1650 if axis.major_gridlines.is_some() && distance > 0.0 {
1651 let points: Vec<_> = (0..count)
1652 .map(|index| radial_point(center, distance, index, count))
1653 .collect();
1654 annotations
1655 .gridlines
1656 .push(PositionedElement::Path(PathElement {
1657 path: polyline_path(&points, true),
1658 fill: None,
1659 stroke: Some(Stroke::new(
1660 Paint::Solid(Color {
1661 r: 0.82,
1662 g: 0.82,
1663 b: 0.82,
1664 a: 1.0,
1665 }),
1666 0.5,
1667 )),
1668 }));
1669 }
1670 radar_value_tick(
1671 &mut annotations.ticks,
1672 center,
1673 distance,
1674 axis.major_tick_mark,
1675 );
1676 let origin = match axis.tick_label_position {
1677 TickLabelPosition::High => Some(Point {
1678 x: center.x + radius + 4.0,
1679 y: center.y - distance + 3.0,
1680 }),
1681 TickLabelPosition::Low => Some(Point {
1682 x: center.x - radius - 30.0,
1683 y: center.y - distance + 3.0,
1684 }),
1685 TickLabelPosition::NextTo => Some(Point {
1686 x: center.x + 4.0,
1687 y: center.y - distance + 3.0,
1688 }),
1689 TickLabelPosition::None => None,
1690 };
1691 if let Some(origin) = origin {
1692 let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
1693 annotations
1694 .labels
1695 .push(PositionedElement::Text(shape_label_with_properties(
1696 fonts,
1697 &text,
1698 origin,
1699 axis.tx_pr.as_ref(),
1700 )?));
1701 }
1702 }
1703 } else {
1704 let category_labels: BTreeMap<_, _> =
1705 if axis.tick_label_position != TickLabelPosition::None {
1706 series
1707 .first()
1708 .map(|series| series_categories(series, axis.number_format.as_ref()))
1709 .transpose()?
1710 .unwrap_or_default()
1711 .into_iter()
1712 .collect()
1713 } else {
1714 BTreeMap::new()
1715 };
1716 for logical_index in 0..count {
1717 let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
1718 let end = radial_point(center, radius, index, count);
1719 annotations
1720 .axis_lines
1721 .push(stroked_segment(center, end, Color::BLACK, 1.0));
1722 radar_category_tick(
1723 &mut annotations.ticks,
1724 center,
1725 radius,
1726 index,
1727 count,
1728 axis.major_tick_mark,
1729 );
1730 if let Some(text) = category_labels.get(&logical_index) {
1731 let Some(origin) = radar_category_label_origin(
1732 bounds,
1733 center,
1734 radius,
1735 index,
1736 count,
1737 axis.tick_label_position,
1738 ) else {
1739 continue;
1740 };
1741 annotations
1742 .labels
1743 .push(PositionedElement::Text(shape_label_with_properties(
1744 fonts,
1745 text,
1746 origin,
1747 axis.tx_pr.as_ref(),
1748 )?));
1749 }
1750 }
1751 }
1752 }
1753 Ok(annotations)
1754}
1755
1756fn radar_category_label_origin(
1757 bounds: Rect,
1758 center: Point,
1759 radius: f64,
1760 index: usize,
1761 count: usize,
1762 position: TickLabelPosition,
1763) -> Option<Point> {
1764 let label_radius = match position {
1765 TickLabelPosition::High => radius + 18.0,
1766 TickLabelPosition::Low => (radius - 10.0).max(0.0),
1767 TickLabelPosition::NextTo => radius + 10.0,
1768 TickLabelPosition::None => return None,
1769 };
1770 let label = radial_point(center, label_radius, index, count);
1771 let mut origin = Point {
1772 x: label.x - 4.0,
1773 y: label.y + 3.0,
1774 };
1775 if position == TickLabelPosition::High {
1776 origin.x = origin.x.clamp(
1777 bounds.x - PLOT_MARGIN_LEFT,
1778 bounds.x + bounds.width + PLOT_MARGIN_RIGHT,
1779 );
1780 origin.y = origin.y.clamp(
1781 bounds.y - PLOT_MARGIN_TOP,
1782 bounds.y + bounds.height + PLOT_MARGIN_BOTTOM,
1783 );
1784 }
1785 Some(origin)
1786}
1787
1788fn radar_value_tick(
1789 elements: &mut Vec<PositionedElement>,
1790 center: Point,
1791 distance: f64,
1792 mark: TickMark,
1793) {
1794 let (left, right) = match mark {
1795 TickMark::None => return,
1796 TickMark::Inside => (4.0, 0.0),
1797 TickMark::Outside => (0.0, 4.0),
1798 TickMark::Cross => (2.0, 2.0),
1799 };
1800 let y = center.y - distance;
1801 elements.push(stroked_segment(
1802 Point {
1803 x: center.x - left,
1804 y,
1805 },
1806 Point {
1807 x: center.x + right,
1808 y,
1809 },
1810 Color::BLACK,
1811 1.0,
1812 ));
1813}
1814
1815fn radar_category_tick(
1816 elements: &mut Vec<PositionedElement>,
1817 center: Point,
1818 radius: f64,
1819 index: usize,
1820 count: usize,
1821 mark: TickMark,
1822) {
1823 let (inside, outside) = match mark {
1824 TickMark::None => return,
1825 TickMark::Inside => (4.0, 0.0),
1826 TickMark::Outside => (0.0, 4.0),
1827 TickMark::Cross => (2.0, 2.0),
1828 };
1829 elements.push(stroked_segment(
1830 radial_point(center, radius - inside, index, count),
1831 radial_point(center, radius + outside, index, count),
1832 Color::BLACK,
1833 1.0,
1834 ));
1835}
1836
1837fn axis_extent(plot: &Plot, axis: &Axis, blanks: DispBlanksAs) -> Result<Domain> {
1838 if let Plot::Scatter { axis_ids, .. } = plot {
1839 let (x, y) = scatter_domains(plot, blanks)?;
1840 return Ok(if axis.id == axis_ids[0] { x } else { y });
1841 }
1842 plot_value_domain(plot, blanks)?.ok_or_else(|| ChartError::InvalidValue {
1843 element: "chart axis".to_owned(),
1844 value: "axis-free plot supplied an axis".to_owned(),
1845 })
1846}
1847
1848fn axis_tick_coordinate(
1849 value: f64,
1850 domain: Domain,
1851 bounds: Rect,
1852 position: AxisPosition,
1853 orientation: Orientation,
1854) -> Result<f64> {
1855 if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
1856 map_x_oriented(value, domain, bounds, orientation)
1857 } else {
1858 map_y_oriented(value, domain, bounds, orientation)
1859 }
1860}
1861
1862fn axis_line(elements: &mut Vec<PositionedElement>, bounds: Rect, position: AxisPosition) {
1863 let (start, end) = match position {
1864 AxisPosition::Bottom => (
1865 Point {
1866 x: bounds.x,
1867 y: bounds.y + bounds.height,
1868 },
1869 Point {
1870 x: bounds.x + bounds.width,
1871 y: bounds.y + bounds.height,
1872 },
1873 ),
1874 AxisPosition::Top => (
1875 Point {
1876 x: bounds.x,
1877 y: bounds.y,
1878 },
1879 Point {
1880 x: bounds.x + bounds.width,
1881 y: bounds.y,
1882 },
1883 ),
1884 AxisPosition::Left => (
1885 Point {
1886 x: bounds.x,
1887 y: bounds.y,
1888 },
1889 Point {
1890 x: bounds.x,
1891 y: bounds.y + bounds.height,
1892 },
1893 ),
1894 AxisPosition::Right => (
1895 Point {
1896 x: bounds.x + bounds.width,
1897 y: bounds.y,
1898 },
1899 Point {
1900 x: bounds.x + bounds.width,
1901 y: bounds.y + bounds.height,
1902 },
1903 ),
1904 };
1905 elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
1906}
1907
1908fn major_gridline(
1909 elements: &mut Vec<PositionedElement>,
1910 bounds: Rect,
1911 position: AxisPosition,
1912 coordinate: f64,
1913) {
1914 let (start, end) = if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
1915 (
1916 Point {
1917 x: coordinate,
1918 y: bounds.y,
1919 },
1920 Point {
1921 x: coordinate,
1922 y: bounds.y + bounds.height,
1923 },
1924 )
1925 } else {
1926 (
1927 Point {
1928 x: bounds.x,
1929 y: coordinate,
1930 },
1931 Point {
1932 x: bounds.x + bounds.width,
1933 y: coordinate,
1934 },
1935 )
1936 };
1937 elements.push(stroked_segment(
1938 start,
1939 end,
1940 Color {
1941 r: 0.82,
1942 g: 0.82,
1943 b: 0.82,
1944 a: 1.0,
1945 },
1946 0.5,
1947 ));
1948}
1949
1950fn axis_tick(
1951 elements: &mut Vec<PositionedElement>,
1952 bounds: Rect,
1953 position: AxisPosition,
1954 coordinate: f64,
1955 mark: TickMark,
1956) {
1957 let (inside, outside) = match mark {
1958 TickMark::None => return,
1959 TickMark::Inside => (4.0, 0.0),
1960 TickMark::Outside => (0.0, 4.0),
1961 TickMark::Cross => (2.0, 2.0),
1962 };
1963 let (start, end) = match position {
1964 AxisPosition::Bottom => (
1965 Point {
1966 x: coordinate,
1967 y: bounds.y + bounds.height - inside,
1968 },
1969 Point {
1970 x: coordinate,
1971 y: bounds.y + bounds.height + outside,
1972 },
1973 ),
1974 AxisPosition::Top => (
1975 Point {
1976 x: coordinate,
1977 y: bounds.y + inside,
1978 },
1979 Point {
1980 x: coordinate,
1981 y: bounds.y - outside,
1982 },
1983 ),
1984 AxisPosition::Left => (
1985 Point {
1986 x: bounds.x + inside,
1987 y: coordinate,
1988 },
1989 Point {
1990 x: bounds.x - outside,
1991 y: coordinate,
1992 },
1993 ),
1994 AxisPosition::Right => (
1995 Point {
1996 x: bounds.x + bounds.width - inside,
1997 y: coordinate,
1998 },
1999 Point {
2000 x: bounds.x + bounds.width + outside,
2001 y: coordinate,
2002 },
2003 ),
2004 };
2005 elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
2006}
2007
2008fn tick_label_origin(
2009 bounds: Rect,
2010 axis_position: AxisPosition,
2011 label_position: TickLabelPosition,
2012 coordinate: f64,
2013) -> Point {
2014 let side = match label_position {
2015 TickLabelPosition::NextTo => axis_position,
2016 TickLabelPosition::High => {
2017 if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
2018 AxisPosition::Top
2019 } else {
2020 AxisPosition::Right
2021 }
2022 }
2023 TickLabelPosition::Low => {
2024 if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
2025 AxisPosition::Bottom
2026 } else {
2027 AxisPosition::Left
2028 }
2029 }
2030 TickLabelPosition::None => axis_position,
2031 };
2032 match side {
2033 AxisPosition::Bottom => Point {
2034 x: coordinate - 4.0,
2035 y: bounds.y + bounds.height + 12.0,
2036 },
2037 AxisPosition::Top => Point {
2038 x: coordinate - 4.0,
2039 y: bounds.y - 4.0,
2040 },
2041 AxisPosition::Left => Point {
2042 x: bounds.x - 30.0,
2043 y: coordinate + 3.0,
2044 },
2045 AxisPosition::Right => Point {
2046 x: bounds.x + bounds.width + 4.0,
2047 y: coordinate + 3.0,
2048 },
2049 }
2050}
2051
2052fn format_axis_value(format: Option<&NumberFormat>, value: f64) -> Result<String> {
2053 if let Some(format) = format {
2054 format.format_value(value)
2055 } else {
2056 NumberFormat::new("General".to_owned(), false)?.format_value(value)
2057 }
2058}
2059
2060fn shape_label(fonts: &mut FontManager, text: &str, origin: Point) -> Result<GlyphRun> {
2061 shape_label_with_properties(fonts, text, origin, None)
2062}
2063
2064fn shape_label_with_properties(
2065 fonts: &mut FontManager,
2066 text: &str,
2067 origin: Point,
2068 properties: Option<&CT_TextBody>,
2069) -> Result<GlyphRun> {
2070 const FAMILY: &str = "Carlito";
2071 const SIZE: f64 = 9.0;
2072 let properties = properties.and_then(chart_text_properties);
2073 let family = properties
2074 .and_then(|properties| properties.latin.as_ref())
2075 .map(|font| font.typeface.as_str())
2076 .unwrap_or(FAMILY);
2077 let size = properties
2078 .and_then(|properties| properties.font_size)
2079 .map(|size| f64::from(size) / 100.0)
2080 .unwrap_or(SIZE);
2081 let bold = properties
2082 .and_then(|properties| properties.bold)
2083 .unwrap_or(false);
2084 let italic = properties
2085 .and_then(|properties| properties.italic)
2086 .unwrap_or(false);
2087 let font_id = fonts
2088 .resolve_font_for_text(Some(family), bold, italic, text)
2089 .map_err(|error| ChartError::InvalidValue {
2090 element: "chart label font".to_owned(),
2091 value: error.to_string(),
2092 })?;
2093 let shaped =
2094 fonts
2095 .shape_text(font_id, text, size)
2096 .map_err(|error| ChartError::InvalidValue {
2097 element: "chart label shaping".to_owned(),
2098 value: error.to_string(),
2099 })?;
2100 Ok(GlyphRun {
2101 origin,
2102 font_id,
2103 font_size: size,
2104 glyph_ids: shaped.glyph_ids,
2105 advances: shaped.advances,
2106 text: text.to_owned(),
2107 source: None,
2108 color: Color::BLACK,
2109 bold,
2110 italic,
2111 field_kind: None,
2112 field_source: None,
2113 note: None,
2114 note_reference_source: None,
2115 tab_aligned: None,
2116 })
2117}
2118
2119fn chart_text_properties(body: &CT_TextBody) -> Option<&CT_TextCharacterProperties> {
2120 body.paragraphs()
2121 .first()
2122 .and_then(|paragraph| paragraph.properties.as_ref())
2123 .and_then(|properties| properties.default_run_properties.as_ref())
2124}
2125
2126fn stroked_segment(start: Point, end: Point, color: Color, width: f64) -> PositionedElement {
2127 PositionedElement::Path(PathElement {
2128 path: Path {
2129 commands: vec![PathCommand::MoveTo(start), PathCommand::LineTo(end)],
2130 fill_rule: FillRule::NonZero,
2131 },
2132 fill: None,
2133 stroke: Some(Stroke::new(Paint::Solid(color), width)),
2134 })
2135}
2136
2137fn plot_series(plot: &Plot) -> &[Series] {
2138 match plot {
2139 Plot::Bar { series, .. }
2140 | Plot::Line { series, .. }
2141 | Plot::Pie { series, .. }
2142 | Plot::Doughnut { series, .. }
2143 | Plot::Area { series, .. }
2144 | Plot::Scatter { series, .. }
2145 | Plot::Radar { series, .. } => series,
2146 }
2147}
2148
2149fn plot_data_labels(plot: &Plot) -> Option<&CT_DLbls> {
2150 match plot {
2151 Plot::Bar { data_labels, .. }
2152 | Plot::Line { data_labels, .. }
2153 | Plot::Pie { data_labels, .. }
2154 | Plot::Doughnut { data_labels, .. }
2155 | Plot::Area { data_labels, .. }
2156 | Plot::Scatter { data_labels, .. }
2157 | Plot::Radar { data_labels, .. } => data_labels.as_ref(),
2158 }
2159}
2160
2161fn series_categories(
2162 series: &Series,
2163 number_format: Option<&NumberFormat>,
2164) -> Result<Vec<(usize, String)>> {
2165 let Some(categories) = &series.categories else {
2166 return Ok(Vec::new());
2167 };
2168 match categories {
2169 AxisData::String(data) => {
2170 let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
2171 indexes
2172 .into_iter()
2173 .zip(&data.values)
2174 .map(|(index, value)| {
2175 usize::try_from(index)
2176 .map(|index| (index, value.clone()))
2177 .map_err(|_| ChartError::InvalidValue {
2178 element: "c:cat/c:pt/@idx".to_owned(),
2179 value: index.to_string(),
2180 })
2181 })
2182 .collect()
2183 }
2184 AxisData::Numeric(data) => {
2185 let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
2186 let cache_format;
2187 let format = if let Some(format) = number_format {
2188 format
2189 } else {
2190 cache_format = NumberFormat::new(data.format_code.clone(), false)?;
2191 &cache_format
2192 };
2193 indexes
2194 .into_iter()
2195 .zip(&data.values)
2196 .map(|(index, value)| {
2197 let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
2198 element: "c:cat/c:pt/@idx".to_owned(),
2199 value: index.to_string(),
2200 })?;
2201 let text = format.format_value(*value)?;
2202 Ok((index, text))
2203 })
2204 .collect()
2205 }
2206 }
2207}
2208
2209fn series_name(series: &Series, index: usize) -> String {
2210 series
2211 .name
2212 .as_ref()
2213 .and_then(|name| name.values.first())
2214 .cloned()
2215 .unwrap_or_else(|| format!("Series {}", index + 1))
2216}
2217
2218fn render_data_labels(
2219 plot: &Plot,
2220 bounds: Rect,
2221 blanks: DispBlanksAs,
2222 options: GeometryOptions,
2223 fonts: &mut FontManager,
2224 elements: &mut Vec<PositionedElement>,
2225) -> Result<()> {
2226 let plot_labels = plot_data_labels(plot);
2227 let anchors = plot_label_anchors(plot, bounds, blanks, options)?;
2228 for (series_index, series) in plot_series(plot).iter().enumerate() {
2229 let Some(labels) = series.data_labels.as_ref().or(plot_labels) else {
2230 continue;
2231 };
2232 let overrides = point_label_overrides(labels)?;
2233 let mut categories = None;
2234 let mut bubble_sizes = None;
2235 let (_, values) = logical_numeric_values(&series.values)?;
2236 let mut percentage_total = None;
2237 for (logical_index, value) in values {
2238 let point_override = overrides.get(&logical_index);
2239 if point_override.is_some_and(|point| point.deleted == Some(true)) {
2240 continue;
2241 }
2242 let effective_labels = point_override
2243 .map(|point| point.apply_to(labels))
2244 .unwrap_or_else(|| labels.clone());
2245 let labels = &effective_labels;
2246 let Some(anchor) = anchors.get(&(series_index, logical_index)).copied() else {
2247 continue;
2248 };
2249 let mut fields = Vec::new();
2250 if labels.show_series_name {
2251 fields.push(series_name(series, series_index));
2252 }
2253 if labels.show_category_name {
2254 if categories.is_none() {
2255 categories = Some(
2256 series_categories(series, None)?
2257 .into_iter()
2258 .collect::<BTreeMap<_, _>>(),
2259 );
2260 }
2261 if let Some(category) = categories
2262 .as_ref()
2263 .and_then(|categories| categories.get(&logical_index))
2264 {
2265 fields.push(category.clone());
2266 }
2267 }
2268 if labels.show_value {
2269 fields.push(format_axis_value(labels.number_format.as_ref(), value)?);
2270 }
2271 if labels.show_percent {
2272 let total = if let Some(total) = percentage_total {
2273 total
2274 } else {
2275 let total = series
2276 .values
2277 .values
2278 .iter()
2279 .copied()
2280 .filter(|value| *value > 0.0)
2281 .try_fold(0.0, |total, value| {
2282 checked_geometry_sum(total, value, "data-label percentage total")
2283 })?;
2284 percentage_total = Some(total);
2285 total
2286 };
2287 if total > 0.0 {
2288 let text = if let Some(format) = &labels.number_format {
2289 format.format_value(value / total)?
2290 } else {
2291 NumberFormat::new("0%".to_owned(), false)?.format_value(value / total)?
2292 };
2293 fields.push(text);
2294 }
2295 }
2296 if labels.show_bubble_size {
2297 if bubble_sizes.is_none() {
2298 bubble_sizes = Some(
2299 series
2300 .bubble_size
2301 .as_ref()
2302 .map(logical_numeric_values)
2303 .transpose()?
2304 .map(|(_, values)| values.into_iter().collect::<BTreeMap<_, _>>())
2305 .unwrap_or_default(),
2306 );
2307 }
2308 if let Some(value) = bubble_sizes
2309 .as_ref()
2310 .and_then(|sizes| sizes.get(&logical_index))
2311 {
2312 fields.push(format_axis_value(labels.number_format.as_ref(), *value)?);
2313 }
2314 }
2315 if fields.is_empty() {
2316 continue;
2317 }
2318 let separator = labels.separator.as_deref().unwrap_or(", ");
2319 let text = fields.join(separator);
2320 let origin = anchor.origin(labels.position);
2321 elements.push(PositionedElement::Text(shape_label(fonts, &text, origin)?));
2322 }
2323 }
2324 Ok(())
2325}
2326
2327#[derive(Clone, Debug, Default)]
2328struct PointLabelOverride {
2329 index: Option<usize>,
2330 deleted: Option<bool>,
2331 number_format: Option<NumberFormat>,
2332 position: Option<DataLabelPosition>,
2333 separator: Option<String>,
2334 show_legend_key: Option<bool>,
2335 show_value: Option<bool>,
2336 show_category_name: Option<bool>,
2337 show_series_name: Option<bool>,
2338 show_percent: Option<bool>,
2339 show_bubble_size: Option<bool>,
2340}
2341
2342impl PointLabelOverride {
2343 fn apply_to(&self, labels: &CT_DLbls) -> CT_DLbls {
2344 let mut effective = labels.clone();
2345 if let Some(value) = &self.number_format {
2346 effective.number_format = Some(value.clone());
2347 }
2348 if let Some(value) = self.position {
2349 effective.position = Some(value);
2350 }
2351 if let Some(value) = &self.separator {
2352 effective.separator = Some(value.clone());
2353 }
2354 if let Some(value) = self.show_legend_key {
2355 effective.show_legend_key = value;
2356 }
2357 if let Some(value) = self.show_value {
2358 effective.show_value = value;
2359 }
2360 if let Some(value) = self.show_category_name {
2361 effective.show_category_name = value;
2362 }
2363 if let Some(value) = self.show_series_name {
2364 effective.show_series_name = value;
2365 }
2366 if let Some(value) = self.show_percent {
2367 effective.show_percent = value;
2368 }
2369 if let Some(value) = self.show_bubble_size {
2370 effective.show_bubble_size = value;
2371 }
2372 effective
2373 }
2374}
2375
2376fn point_label_overrides(labels: &CT_DLbls) -> Result<BTreeMap<usize, PointLabelOverride>> {
2377 let mut namespaces = chart_namespace_defaults();
2378 for (name, namespace) in &labels.namespace_declarations {
2379 let prefix = if name == "xmlns" {
2380 Vec::new()
2381 } else if let Some(prefix) = name.strip_prefix("xmlns:") {
2382 prefix.as_bytes().to_vec()
2383 } else {
2384 continue;
2385 };
2386 upsert_namespace_binding(&mut namespaces, prefix, namespace.clone());
2387 }
2388 let mut overrides = BTreeMap::new();
2389 for raw in labels.raw_children.at(0) {
2390 if chart_root_local(raw, &namespaces)?.as_deref() != Some(b"dLbl") {
2391 continue;
2392 }
2393 let point = parse_point_label_override(raw, &namespaces)?;
2394 let index = point
2395 .index
2396 .ok_or_else(|| ChartError::MissingElement("c:dLbl/c:idx".to_owned()))?;
2397 if overrides.insert(index, point).is_some() {
2398 return Err(ChartError::DuplicateElement(format!(
2399 "c:dLbl[c:idx={index}]"
2400 )));
2401 }
2402 }
2403 Ok(overrides)
2404}
2405
2406fn parse_point_label_override(
2407 xml: &[u8],
2408 inherited: &NamespaceBindings,
2409) -> Result<PointLabelOverride> {
2410 let mut reader = Reader::from_reader(xml);
2411 let mut buffer = Vec::new();
2412 let mut namespaces = None;
2413 let mut state = PointLabelOverride::default();
2414 let mut order = 0;
2415 loop {
2416 match reader
2417 .read_event_into(&mut buffer)
2418 .map_err(OxmlError::from)?
2419 {
2420 Event::Start(element) if namespaces.is_none() => {
2421 if !matches_local_name(element.name().as_ref(), b"dLbl")
2422 || !element_is_in_namespace(&element, C_NS, inherited)?
2423 {
2424 return Err(ChartError::UnexpectedElement(element_name(&element)));
2425 }
2426 namespaces = Some(chart_bindings(inherited, &element)?);
2427 }
2428 Event::Start(element) => {
2429 let bindings = namespaces
2430 .as_ref()
2431 .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
2432 let name = chart_child_local(&element, bindings)?;
2433 let raw = capture_element(&mut reader, &element)?;
2434 parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
2435 }
2436 Event::Empty(element) => {
2437 let bindings = namespaces
2438 .as_ref()
2439 .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
2440 let name = chart_child_local(&element, bindings)?;
2441 let raw = capture_empty_element(&element)?;
2442 parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
2443 }
2444 Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbl") => {
2445 if state.index.is_none() {
2446 return Err(ChartError::MissingElement("c:dLbl/c:idx".to_owned()));
2447 }
2448 return Ok(state);
2449 }
2450 Event::Eof => return Err(missing_end("c:dLbl")),
2451 _ => {}
2452 }
2453 buffer.clear();
2454 }
2455}
2456
2457fn parse_point_label_child(
2458 state: &mut PointLabelOverride,
2459 name: Option<&[u8]>,
2460 raw: &[u8],
2461 namespaces: &NamespaceBindings,
2462 order: &mut usize,
2463) -> Result<()> {
2464 let Some(name) = name else {
2465 return Ok(());
2466 };
2467 let rank = match name {
2468 b"idx" => 0,
2469 b"delete" => 1,
2470 b"layout" => 2,
2471 b"tx" => 3,
2472 b"numFmt" => 4,
2473 b"spPr" => 5,
2474 b"txPr" => 6,
2475 b"dLblPos" => 7,
2476 b"showLegendKey" => 8,
2477 b"showVal" => 9,
2478 b"showCatName" => 10,
2479 b"showSerName" => 11,
2480 b"showPercent" => 12,
2481 b"showBubbleSize" => 13,
2482 b"separator" => 14,
2483 b"showLeaderLines" => 15,
2484 b"leaderLines" => 16,
2485 b"extLst" => 17,
2486 _ => return Ok(()),
2487 };
2488 if rank < *order {
2489 return Err(ChartError::InvalidValue {
2490 element: "c:dLbl".to_owned(),
2491 value: format!("c:{} is out of schema order", String::from_utf8_lossy(name)),
2492 });
2493 }
2494 *order = rank;
2495 match name {
2496 b"idx" => {
2497 let (index, _) = parse_u32_scalar(raw, "idx")?;
2498 let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
2499 element: "c:dLbl/c:idx".to_owned(),
2500 value: index.to_string(),
2501 })?;
2502 set_once(&mut state.index, index, "c:dLbl/c:idx")?;
2503 }
2504 b"delete" => set_once(
2505 &mut state.deleted,
2506 parse_bool_value(raw, "delete")?.0,
2507 "c:dLbl/c:delete",
2508 )?,
2509 b"numFmt" => set_once(
2510 &mut state.number_format,
2511 NumberFormat::from_xml(raw)?,
2512 "c:dLbl/c:numFmt",
2513 )?,
2514 b"dLblPos" => {
2515 let (value, _) = scalar_value(raw, "dLblPos")?;
2516 let value =
2517 value.ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
2518 let position = DataLabelPosition::parse(&value)
2519 .ok_or_else(|| invalid_attribute("dLblPos", "val", value))?;
2520 set_once(&mut state.position, position, "c:dLbl/c:dLblPos")?;
2521 }
2522 b"showLegendKey" => set_once(
2523 &mut state.show_legend_key,
2524 parse_bool_value(raw, "showLegendKey")?.0,
2525 "c:dLbl/c:showLegendKey",
2526 )?,
2527 b"showVal" => set_once(
2528 &mut state.show_value,
2529 parse_bool_value(raw, "showVal")?.0,
2530 "c:dLbl/c:showVal",
2531 )?,
2532 b"showCatName" => set_once(
2533 &mut state.show_category_name,
2534 parse_bool_value(raw, "showCatName")?.0,
2535 "c:dLbl/c:showCatName",
2536 )?,
2537 b"showSerName" => set_once(
2538 &mut state.show_series_name,
2539 parse_bool_value(raw, "showSerName")?.0,
2540 "c:dLbl/c:showSerName",
2541 )?,
2542 b"showPercent" => set_once(
2543 &mut state.show_percent,
2544 parse_bool_value(raw, "showPercent")?.0,
2545 "c:dLbl/c:showPercent",
2546 )?,
2547 b"showBubbleSize" => set_once(
2548 &mut state.show_bubble_size,
2549 parse_bool_value(raw, "showBubbleSize")?.0,
2550 "c:dLbl/c:showBubbleSize",
2551 )?,
2552 b"separator" => set_once(
2553 &mut state.separator,
2554 parse_text_element(raw, b"separator", namespaces)?.0,
2555 "c:dLbl/c:separator",
2556 )?,
2557 _ => {}
2558 }
2559 Ok(())
2560}
2561
2562#[derive(Clone, Copy, Debug)]
2563struct LabelAnchor {
2564 center: Point,
2565 base: Point,
2566 end: Point,
2567 direction: Point,
2568}
2569
2570impl LabelAnchor {
2571 const fn point(center: Point) -> Self {
2572 Self {
2573 center,
2574 base: center,
2575 end: center,
2576 direction: Point { x: 0.0, y: 0.0 },
2577 }
2578 }
2579
2580 const fn segment(center: Point, base: Point, end: Point) -> Self {
2581 Self {
2582 center,
2583 base,
2584 end,
2585 direction: Point {
2586 x: end.x - base.x,
2587 y: end.y - base.y,
2588 },
2589 }
2590 }
2591
2592 const fn directed_segment(center: Point, base: Point, end: Point, direction: Point) -> Self {
2593 Self {
2594 center,
2595 base,
2596 end,
2597 direction,
2598 }
2599 }
2600
2601 fn origin(self, position: Option<DataLabelPosition>) -> Point {
2602 let mut origin = self.center;
2603 match position.unwrap_or(DataLabelPosition::BestFit) {
2604 DataLabelPosition::Bottom => origin.y += 8.0,
2605 DataLabelPosition::Top => origin.y -= 5.0,
2606 DataLabelPosition::Left => origin.x -= 8.0,
2607 DataLabelPosition::Right => origin.x += 8.0,
2608 DataLabelPosition::InsideBase => {
2609 origin = self.along_segment(self.base, 5.0, true).unwrap_or(Point {
2610 x: origin.x,
2611 y: origin.y + 8.0,
2612 });
2613 }
2614 DataLabelPosition::InsideEnd => {
2615 origin = self.along_segment(self.end, -5.0, true).unwrap_or(origin);
2616 }
2617 DataLabelPosition::OutsideEnd => {
2618 origin = self.along_segment(self.end, 5.0, false).unwrap_or(Point {
2619 x: origin.x,
2620 y: origin.y - 5.0,
2621 });
2622 }
2623 DataLabelPosition::BestFit | DataLabelPosition::Center => {}
2624 }
2625 origin
2626 }
2627
2628 fn along_segment(self, origin: Point, distance: f64, clamp_inside: bool) -> Option<Point> {
2629 let dx = self.end.x - self.base.x;
2630 let dy = self.end.y - self.base.y;
2631 let length = dx.hypot(dy);
2632 let (dx, dy, direction_length) = if length.is_finite() && length > 0.0 {
2633 (dx, dy, length)
2634 } else {
2635 let direction_length = self.direction.x.hypot(self.direction.y);
2636 if !direction_length.is_finite() || direction_length == 0.0 {
2637 return None;
2638 }
2639 (self.direction.x, self.direction.y, direction_length)
2640 };
2641 let distance = if clamp_inside {
2642 distance.clamp(-length / 2.0, length / 2.0)
2643 } else {
2644 distance
2645 };
2646 Some(Point {
2647 x: origin.x + dx / direction_length * distance,
2648 y: origin.y + dy / direction_length * distance,
2649 })
2650 }
2651}
2652
2653fn plot_label_anchors(
2654 plot: &Plot,
2655 bounds: Rect,
2656 blanks: DispBlanksAs,
2657 options: GeometryOptions,
2658) -> Result<BTreeMap<(usize, usize), LabelAnchor>> {
2659 let mut anchors = BTreeMap::new();
2660 match plot {
2661 Plot::Bar {
2662 direction,
2663 grouping,
2664 gap_width,
2665 overlap,
2666 series,
2667 ..
2668 } => {
2669 for bar in bar_rectangles(
2670 *direction, *grouping, *gap_width, *overlap, series, bounds, options,
2671 )? {
2672 if !point_in_rect(bar.end, bounds) {
2673 continue;
2674 }
2675 let visible_base = Point {
2676 x: bar.base.x.clamp(bounds.x, bounds.x + bounds.width),
2677 y: bar.base.y.clamp(bounds.y, bounds.y + bounds.height),
2678 };
2679 let visible_end = Point {
2680 x: bar.end.x.clamp(bounds.x, bounds.x + bounds.width),
2681 y: bar.end.y.clamp(bounds.y, bounds.y + bounds.height),
2682 };
2683 anchors.insert(
2684 (bar.series_index, bar.logical_index),
2685 LabelAnchor::directed_segment(
2686 Point {
2687 x: (visible_base.x + visible_end.x) / 2.0,
2688 y: (visible_base.y + visible_end.y) / 2.0,
2689 },
2690 visible_base,
2691 visible_end,
2692 Point {
2693 x: bar.end.x - bar.base.x,
2694 y: bar.end.y - bar.base.y,
2695 },
2696 ),
2697 );
2698 }
2699 }
2700 Plot::Line {
2701 grouping, series, ..
2702 }
2703 | Plot::Area {
2704 grouping, series, ..
2705 } => {
2706 for (series_index, points) in
2707 category_label_points(*grouping, series, bounds, blanks, options)?
2708 .into_iter()
2709 .enumerate()
2710 {
2711 for (logical_index, point) in points {
2712 anchors.insert((series_index, logical_index), LabelAnchor::point(point));
2713 }
2714 }
2715 }
2716 Plot::Scatter { series, .. } => {
2717 let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
2718 let x_mapping = options.scatter_x.unwrap_or(AxisMapping {
2719 domain: x_domain,
2720 orientation: Orientation::MinMax,
2721 });
2722 let y_mapping = options.scatter_y.unwrap_or(AxisMapping {
2723 domain: y_domain,
2724 orientation: Orientation::MinMax,
2725 });
2726 for (series_index, series) in series.iter().enumerate() {
2727 let indexed = matched_scatter_points(series)?;
2728 let points = scatter_points(
2729 &indexed,
2730 x_mapping.domain,
2731 y_mapping.domain,
2732 bounds,
2733 x_mapping.orientation,
2734 y_mapping.orientation,
2735 )?;
2736 for ((logical_index, _, _), point) in indexed.into_iter().zip(points) {
2737 anchors.insert((series_index, logical_index), LabelAnchor::point(point));
2738 }
2739 }
2740 }
2741 Plot::Radar { series, .. } => {
2742 let center = Point {
2743 x: bounds.x + bounds.width / 2.0,
2744 y: bounds.y + bounds.height / 2.0,
2745 };
2746 let category_count = series_category_count(series)?;
2747 for (series_index, points) in radar_series_points(series, bounds, options)?
2748 .into_iter()
2749 .enumerate()
2750 {
2751 for (logical_index, point) in points {
2752 let spoke = radial_point(
2753 center,
2754 1.0,
2755 oriented_category_index(
2756 logical_index,
2757 category_count,
2758 options.category_orientation,
2759 ),
2760 category_count,
2761 );
2762 anchors.insert(
2763 (series_index, logical_index),
2764 LabelAnchor::directed_segment(
2765 point,
2766 center,
2767 point,
2768 Point {
2769 x: spoke.x - center.x,
2770 y: spoke.y - center.y,
2771 },
2772 ),
2773 );
2774 }
2775 }
2776 }
2777 Plot::Pie {
2778 first_slice_angle,
2779 series,
2780 ..
2781 } => {
2782 for slice in pie_slices(*first_slice_angle, None, series, bounds)? {
2783 anchors.insert(
2784 (slice.series_index, slice.logical_index),
2785 LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2786 );
2787 }
2788 }
2789 Plot::Doughnut {
2790 first_slice_angle,
2791 hole_size,
2792 series,
2793 ..
2794 } => {
2795 for slice in pie_slices(*first_slice_angle, Some(*hole_size), series, bounds)? {
2796 anchors.insert(
2797 (slice.series_index, slice.logical_index),
2798 LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2799 );
2800 }
2801 }
2802 }
2803 anchors.retain(|_, anchor| point_in_rect(anchor.end, bounds));
2804 Ok(anchors)
2805}
2806
2807fn category_label_points(
2808 grouping: Grouping,
2809 series: &[Series],
2810 bounds: Rect,
2811 blanks: DispBlanksAs,
2812 options: GeometryOptions,
2813) -> Result<Vec<Vec<(usize, Point)>>> {
2814 validate_series_values(series)?;
2815 let logical_series = logical_series_values(series)?;
2816 let category_count = series_category_count(series)?;
2817 let calculated_series = if blanks == DispBlanksAs::Zero {
2818 zero_filled_series(&logical_series, category_count)
2819 } else {
2820 logical_series.clone()
2821 };
2822 let layers = stacked_series_bounds(
2823 &calculated_series,
2824 grouping != Grouping::Standard,
2825 grouping == Grouping::PercentStacked,
2826 )?;
2827 let domain = options
2828 .value
2829 .map(|mapping| mapping.domain)
2830 .unwrap_or(domain_from_layers(&layers)?);
2831 let value_orientation = options
2832 .value
2833 .map(|mapping| mapping.orientation)
2834 .unwrap_or(Orientation::MinMax);
2835 let mut point_series = Vec::with_capacity(layers.len());
2836 for (series_index, layer) in layers.iter().enumerate() {
2837 let present: BTreeSet<_> = logical_series[series_index]
2838 .1
2839 .iter()
2840 .map(|(index, _)| *index)
2841 .collect();
2842 let indexed: Vec<_> = layer
2843 .iter()
2844 .filter(|(index, _, _)| present.contains(index))
2845 .map(|(index, _, upper)| (*index, *upper))
2846 .collect();
2847 let points = category_points(
2848 &indexed,
2849 category_count,
2850 domain,
2851 bounds,
2852 options.category_orientation,
2853 value_orientation,
2854 )?;
2855 point_series.push(
2856 indexed
2857 .into_iter()
2858 .map(|(index, _)| index)
2859 .zip(points)
2860 .collect(),
2861 );
2862 }
2863 Ok(point_series)
2864}
2865
2866fn point_in_rect(point: Point, bounds: Rect) -> bool {
2867 const EPSILON: f64 = 1.0e-9;
2868 point.x >= bounds.x - EPSILON
2869 && point.x <= bounds.x + bounds.width + EPSILON
2870 && point.y >= bounds.y - EPSILON
2871 && point.y <= bounds.y + bounds.height + EPSILON
2872}
2873
2874fn resolve_series_colours(
2875 plot: &Plot,
2876 theme: &CT_OfficeStyleSheet,
2877 color_map: &ColorMap,
2878) -> Result<Vec<Color>> {
2879 let lookup = theme_colour_lookup(theme);
2880 plot_series(plot)
2881 .iter()
2882 .enumerate()
2883 .map(|(index, series)| {
2884 if let Some(properties) = series.sp_pr.as_ref() {
2885 if let Some(fill) = properties.fill.as_ref() {
2886 return resolve_series_paint(fill, index, "fill", theme, color_map, &lookup);
2887 }
2888 if let Some(fill) = properties.line.as_ref().and_then(|line| line.fill.as_ref()) {
2889 return resolve_series_paint(fill, index, "line", theme, color_map, &lookup);
2890 }
2891 }
2892 resolve_series_accent(index, theme, color_map, &lookup)
2893 })
2894 .collect()
2895}
2896
2897fn resolve_series_paint(
2898 fill: &Fill,
2899 series_index: usize,
2900 source: &str,
2901 theme: &CT_OfficeStyleSheet,
2902 color_map: &ColorMap,
2903 lookup: &[(&str, RgbColor)],
2904) -> Result<Color> {
2905 match fill {
2906 Fill::NoFill(_) => Ok(Color {
2907 r: 0.0,
2908 g: 0.0,
2909 b: 0.0,
2910 a: 0.0,
2911 }),
2912 Fill::Solid(solid) => {
2913 let choice = solid
2914 .color
2915 .as_ref()
2916 .ok_or_else(|| ChartError::InvalidValue {
2917 element: format!("c:ser[{series_index}] direct {source} paint"),
2918 value: "solid fill has no colour choice".to_owned(),
2919 })?;
2920 resolve_direct_chart_colour(choice, theme, color_map, lookup, || {
2921 format!("c:ser[{series_index}] direct {source} paint")
2922 })
2923 }
2924 Fill::Gradient(_) => unsupported_series_paint(series_index, source, "gradient"),
2925 Fill::Pattern(_) => unsupported_series_paint(series_index, source, "pattern"),
2926 Fill::Blip(_) => unsupported_series_paint(series_index, source, "picture"),
2927 }
2928}
2929
2930fn resolve_direct_chart_colour(
2931 choice: &ColorChoice,
2932 theme: &CT_OfficeStyleSheet,
2933 color_map: &ColorMap,
2934 lookup: &[(&str, RgbColor)],
2935 element: impl FnOnce() -> String,
2936) -> Result<Color> {
2937 let ColorChoice::Scheme {
2938 value, transforms, ..
2939 } = choice
2940 else {
2941 return resolve_chart_colour(choice, color_map, lookup, element);
2942 };
2943 let theme_slot =
2944 mapped_theme_slot(value, color_map).ok_or_else(|| ChartError::InvalidValue {
2945 element: element(),
2946 value: format!("no concrete theme slot is available for scheme colour {value}"),
2947 })?;
2948 let theme_choice = theme.theme_elements.color_scheme.color(theme_slot);
2949 let base = concrete_theme_colour(theme_choice).ok_or_else(|| ChartError::InvalidValue {
2950 element: format!("a:clrScheme/a:{}", theme_slot.as_str()),
2951 value: "selected theme slot has no concrete sRGB or system fallback colour".to_owned(),
2952 })?;
2953 let mut combined = theme_choice.transforms().to_vec();
2954 combined.extend_from_slice(transforms);
2955 Ok(resolved_layout_colour(apply_color_transforms(
2956 base, &combined,
2957 )))
2958}
2959
2960fn mapped_theme_slot(value: &str, color_map: &ColorMap) -> Option<ThemeColorSlot> {
2961 let mapped = match value {
2962 "bg1" => color_map.theme_slot(ColorMapSlot::Background1),
2963 "tx1" => color_map.theme_slot(ColorMapSlot::Text1),
2964 "bg2" => color_map.theme_slot(ColorMapSlot::Background2),
2965 "tx2" => color_map.theme_slot(ColorMapSlot::Text2),
2966 "accent1" => color_map.theme_slot(ColorMapSlot::Accent1),
2967 "accent2" => color_map.theme_slot(ColorMapSlot::Accent2),
2968 "accent3" => color_map.theme_slot(ColorMapSlot::Accent3),
2969 "accent4" => color_map.theme_slot(ColorMapSlot::Accent4),
2970 "accent5" => color_map.theme_slot(ColorMapSlot::Accent5),
2971 "accent6" => color_map.theme_slot(ColorMapSlot::Accent6),
2972 "hlink" => color_map.theme_slot(ColorMapSlot::Hyperlink),
2973 "folHlink" => color_map.theme_slot(ColorMapSlot::FollowedHyperlink),
2974 "dk1" => ThemeColorSlot::Dark1,
2975 "lt1" => ThemeColorSlot::Light1,
2976 "dk2" => ThemeColorSlot::Dark2,
2977 "lt2" => ThemeColorSlot::Light2,
2978 _ => return None,
2979 };
2980 Some(mapped)
2981}
2982
2983fn unsupported_series_paint(series_index: usize, source: &str, kind: &str) -> Result<Color> {
2984 Err(ChartError::InvalidValue {
2985 element: format!("c:ser[{series_index}] direct {source} paint"),
2986 value: format!("unsupported {kind} paint"),
2987 })
2988}
2989
2990fn resolve_series_accent(
2991 series_index: usize,
2992 theme: &CT_OfficeStyleSheet,
2993 color_map: &ColorMap,
2994 lookup: &[(&str, RgbColor)],
2995) -> Result<Color> {
2996 const ACCENTS: [ColorMapSlot; 6] = [
2997 ColorMapSlot::Accent1,
2998 ColorMapSlot::Accent2,
2999 ColorMapSlot::Accent3,
3000 ColorMapSlot::Accent4,
3001 ColorMapSlot::Accent5,
3002 ColorMapSlot::Accent6,
3003 ];
3004 let semantic_slot = ACCENTS[series_index % ACCENTS.len()];
3005 let theme_slot = color_map.theme_slot(semantic_slot);
3006 let choice = theme.theme_elements.color_scheme.color(theme_slot);
3007 resolve_chart_colour(choice, color_map, lookup, || {
3008 format!("c:ser[{series_index}] theme accent")
3009 })
3010}
3011
3012fn resolve_chart_colour(
3013 choice: &ColorChoice,
3014 color_map: &ColorMap,
3015 lookup: &[(&str, RgbColor)],
3016 element: impl FnOnce() -> String,
3017) -> Result<Color> {
3018 resolve_color(choice, color_map, lookup)
3019 .map(resolved_layout_colour)
3020 .map_err(|error| ChartError::InvalidValue {
3021 element: element(),
3022 value: error.to_string(),
3023 })
3024}
3025
3026fn theme_colour_lookup(theme: &CT_OfficeStyleSheet) -> Vec<(&'static str, RgbColor)> {
3027 theme
3028 .theme_elements
3029 .color_scheme
3030 .iter()
3031 .filter_map(|(slot, choice)| {
3032 concrete_theme_colour(choice).map(|colour| (slot.as_str(), colour))
3033 })
3034 .collect()
3035}
3036
3037fn concrete_theme_colour(choice: &ColorChoice) -> Option<RgbColor> {
3038 match choice {
3039 ColorChoice::Srgb { value, .. } => Some(*value),
3040 ColorChoice::System { last_color, .. } => *last_color,
3041 ColorChoice::Scheme { .. } | ColorChoice::Preset { .. } => None,
3042 }
3043}
3044
3045fn resolved_layout_colour(colour: ResolvedColor) -> Color {
3046 Color {
3047 r: f64::from(colour.red) / 255.0,
3048 g: f64::from(colour.green) / 255.0,
3049 b: f64::from(colour.blue) / 255.0,
3050 a: f64::from(colour.alpha) / 255.0,
3051 }
3052}
3053
3054fn render_legend(
3055 chart: &CT_Chart,
3056 plot: &Plot,
3057 bounds: Rect,
3058 fonts: &mut FontManager,
3059 series_colours: &[Color],
3060 paths: &mut Vec<PositionedElement>,
3061 labels: &mut Vec<PositionedElement>,
3062) -> Result<()> {
3063 if chart.legend.is_none() {
3064 return Ok(());
3065 }
3066 let x = bounds.x + bounds.width - 58.0;
3067 for (index, (series, colour)) in plot_series(plot).iter().zip(series_colours).enumerate() {
3068 let y = bounds.y + 6.0 + index as f64 * 13.0;
3069 paths.push(filled_path(
3070 Path::rect(Rect {
3071 x,
3072 y,
3073 width: 7.0,
3074 height: 7.0,
3075 }),
3076 *colour,
3077 ));
3078 labels.push(PositionedElement::Text(shape_label(
3079 fonts,
3080 &series_name(series, index),
3081 Point {
3082 x: x + 10.0,
3083 y: y + 7.0,
3084 },
3085 )?));
3086 }
3087 Ok(())
3088}
3089
3090fn validate_geometry_coordinates(elements: &[PositionedElement]) -> Result<()> {
3091 fn finite_point(point: Point) -> bool {
3092 point.x.is_finite() && point.y.is_finite()
3093 }
3094
3095 for element in elements {
3096 match element {
3097 PositionedElement::Path(path) => {
3098 for command in &path.path.commands {
3099 let finite = match command {
3100 PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
3101 finite_point(*point)
3102 }
3103 PathCommand::CurveTo { c1, c2, to } => {
3104 finite_point(*c1) && finite_point(*c2) && finite_point(*to)
3105 }
3106 PathCommand::Close => true,
3107 };
3108 if !finite {
3109 return Err(ChartError::InvalidValue {
3110 element: "chart geometry coordinate".to_owned(),
3111 value: "generated path contains a nonfinite point".to_owned(),
3112 });
3113 }
3114 }
3115 }
3116 PositionedElement::Group(group) => validate_geometry_coordinates(&group.children)?,
3117 _ => {}
3118 }
3119 }
3120 Ok(())
3121}
3122
3123fn render_plot_geometry(
3124 plot: &Plot,
3125 bounds: Rect,
3126 blanks: DispBlanksAs,
3127 options: GeometryOptions,
3128 series_colours: &[Color],
3129) -> Result<Vec<PositionedElement>> {
3130 match plot {
3131 Plot::Bar {
3132 direction,
3133 grouping,
3134 gap_width,
3135 overlap,
3136 series,
3137 ..
3138 } => render_bar_geometry(
3139 *direction,
3140 *grouping,
3141 *gap_width,
3142 *overlap,
3143 series,
3144 bounds,
3145 options,
3146 series_colours,
3147 ),
3148 Plot::Line {
3149 grouping,
3150 marker,
3151 series,
3152 ..
3153 } => render_line_geometry(
3154 *grouping,
3155 *marker,
3156 series,
3157 bounds,
3158 blanks,
3159 options,
3160 series_colours,
3161 ),
3162 Plot::Pie {
3163 first_slice_angle,
3164 series,
3165 ..
3166 } => render_pie_geometry(*first_slice_angle, None, series, bounds, series_colours),
3167 Plot::Doughnut {
3168 first_slice_angle,
3169 hole_size,
3170 series,
3171 ..
3172 } => render_pie_geometry(
3173 *first_slice_angle,
3174 Some(*hole_size),
3175 series,
3176 bounds,
3177 series_colours,
3178 ),
3179 Plot::Area {
3180 grouping, series, ..
3181 } => render_area_geometry(*grouping, series, bounds, blanks, options, series_colours),
3182 Plot::Scatter { style, series, .. } => {
3183 render_scatter_geometry(*style, series, bounds, blanks, options, series_colours)
3184 }
3185 Plot::Radar { style, series, .. } => {
3186 render_radar_geometry(*style, series, bounds, options, series_colours)
3187 }
3188 }
3189}
3190
3191#[allow(clippy::too_many_arguments)]
3192fn render_bar_geometry(
3193 direction: BarDirection,
3194 grouping: BarGrouping,
3195 gap_width: u16,
3196 overlap: i8,
3197 series: &[Series],
3198 bounds: Rect,
3199 options: GeometryOptions,
3200 series_colours: &[Color],
3201) -> Result<Vec<PositionedElement>> {
3202 let bars = bar_rectangles(
3203 direction, grouping, gap_width, overlap, series, bounds, options,
3204 )?;
3205 bars.into_iter()
3206 .map(|bar| {
3207 Ok(filled_path(
3208 Path::rect(bar.rect),
3209 series_colour(series_colours, bar.series_index)?,
3210 ))
3211 })
3212 .collect()
3213}
3214
3215#[derive(Clone, Copy, Debug)]
3216struct BarRectangle {
3217 series_index: usize,
3218 logical_index: usize,
3219 rect: Rect,
3220 base: Point,
3221 end: Point,
3222}
3223
3224fn bar_rectangles(
3225 direction: BarDirection,
3226 grouping: BarGrouping,
3227 gap_width: u16,
3228 overlap: i8,
3229 series: &[Series],
3230 bounds: Rect,
3231 options: GeometryOptions,
3232) -> Result<Vec<BarRectangle>> {
3233 validate_series_values(series)?;
3234 let logical_series = logical_series_values(series)?;
3235 let category_count = series_category_count(series)?;
3236 let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
3237 let percent = grouping == BarGrouping::PercentStacked;
3238 let layers = stacked_series_bounds(&logical_series, stacked, percent)?;
3239 let domain = options
3240 .value
3241 .map(|mapping| mapping.domain)
3242 .unwrap_or(domain_from_layers(&layers)?);
3243 let value_orientation = options
3244 .value
3245 .map(|mapping| mapping.orientation)
3246 .unwrap_or(Orientation::MinMax);
3247 let category_extent = if direction == BarDirection::Column {
3248 bounds.width
3249 } else {
3250 bounds.height
3251 };
3252 let slot = category_extent / category_count as f64;
3253 let cluster_extent = slot * 100.0 / (100.0 + f64::from(gap_width));
3254 let bars_per_cluster = if stacked { 1 } else { series.len() };
3255 let overlap_fraction = if stacked {
3256 0.0
3257 } else {
3258 f64::from(overlap) / 100.0
3259 };
3260 let divisor =
3261 bars_per_cluster as f64 - (bars_per_cluster.saturating_sub(1)) as f64 * overlap_fraction;
3262 let bar_extent = cluster_extent / divisor;
3263 let advance = bar_extent * (1.0 - overlap_fraction);
3264 let mut bars = Vec::new();
3265 for (series_index, layer) in layers.iter().enumerate() {
3266 for &(logical_index, start, end) in layer {
3267 let category = oriented_category_index(
3268 logical_index,
3269 category_count,
3270 options.category_orientation,
3271 );
3272 let cluster_start = category as f64 / category_count as f64 * category_extent
3273 + (slot - cluster_extent) / 2.0;
3274 let category_start = if stacked {
3275 cluster_start
3276 } else {
3277 cluster_start + series_index as f64 * advance
3278 };
3279 let (rect, base, end) = if direction == BarDirection::Column {
3280 let y0 = map_y_oriented(start, domain, bounds, value_orientation)?;
3281 let y1 = map_y_oriented(end, domain, bounds, value_orientation)?;
3282 let center_x = bounds.x + category_start + bar_extent / 2.0;
3283 (
3284 Rect {
3285 x: bounds.x + category_start,
3286 y: y0.min(y1),
3287 width: bar_extent,
3288 height: (y1 - y0).abs(),
3289 },
3290 Point { x: center_x, y: y0 },
3291 Point { x: center_x, y: y1 },
3292 )
3293 } else {
3294 let x0 = map_x_oriented(start, domain, bounds, value_orientation)?;
3295 let x1 = map_x_oriented(end, domain, bounds, value_orientation)?;
3296 let center_y = bounds.y + category_start + bar_extent / 2.0;
3297 (
3298 Rect {
3299 x: x0.min(x1),
3300 y: bounds.y + category_start,
3301 width: (x1 - x0).abs(),
3302 height: bar_extent,
3303 },
3304 Point { x: x0, y: center_y },
3305 Point { x: x1, y: center_y },
3306 )
3307 };
3308 bars.push(BarRectangle {
3309 series_index,
3310 logical_index,
3311 rect,
3312 base,
3313 end,
3314 });
3315 }
3316 }
3317 Ok(bars)
3318}
3319
3320fn render_line_geometry(
3321 grouping: Grouping,
3322 marker: bool,
3323 series: &[Series],
3324 bounds: Rect,
3325 blanks: DispBlanksAs,
3326 options: GeometryOptions,
3327 series_colours: &[Color],
3328) -> Result<Vec<PositionedElement>> {
3329 validate_series_values(series)?;
3330 let logical_series = logical_series_values(series)?;
3331 let category_count = series_category_count(series)?;
3332 let calculated_series = if blanks == DispBlanksAs::Zero {
3333 zero_filled_series(&logical_series, category_count)
3334 } else {
3335 logical_series.clone()
3336 };
3337 let stacked = grouping != Grouping::Standard;
3338 let percent = grouping == Grouping::PercentStacked;
3339 let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
3340 let domain = options
3341 .value
3342 .map(|mapping| mapping.domain)
3343 .unwrap_or(domain_from_layers(&layers)?);
3344 let value_orientation = options
3345 .value
3346 .map(|mapping| mapping.orientation)
3347 .unwrap_or(Orientation::MinMax);
3348 let mut elements = Vec::new();
3349 for (series_index, layer) in layers.iter().enumerate() {
3350 let colour = series_colour(series_colours, series_index)?;
3351 let indexed: Vec<_> = layer
3352 .iter()
3353 .map(|(index, _, upper)| (*index, *upper))
3354 .collect();
3355 let indexes: Vec<_> = indexed.iter().map(|(index, _)| *index).collect();
3356 for (start, end) in contiguous_ranges(&indexes, blanks) {
3357 let points = category_points(
3358 &indexed[start..end],
3359 category_count,
3360 domain,
3361 bounds,
3362 options.category_orientation,
3363 value_orientation,
3364 )?;
3365 elements.push(stroked_path(polyline_path(&points, false), colour));
3366 }
3367 if marker {
3368 let present: BTreeSet<_> = logical_series[series_index]
3369 .1
3370 .iter()
3371 .map(|(index, _)| *index)
3372 .collect();
3373 let marker_values: Vec<_> = indexed
3374 .iter()
3375 .copied()
3376 .filter(|(index, _)| present.contains(index))
3377 .collect();
3378 let marker_points = category_points(
3379 &marker_values,
3380 category_count,
3381 domain,
3382 bounds,
3383 options.category_orientation,
3384 value_orientation,
3385 )?;
3386 push_markers(&mut elements, &marker_points, colour);
3387 }
3388 }
3389 Ok(elements)
3390}
3391
3392fn render_area_geometry(
3393 grouping: Grouping,
3394 series: &[Series],
3395 bounds: Rect,
3396 blanks: DispBlanksAs,
3397 options: GeometryOptions,
3398 series_colours: &[Color],
3399) -> Result<Vec<PositionedElement>> {
3400 validate_series_values(series)?;
3401 let logical_series = logical_series_values(series)?;
3402 let category_count = series_category_count(series)?;
3403 let calculated_series = if blanks == DispBlanksAs::Zero {
3404 zero_filled_series(&logical_series, category_count)
3405 } else {
3406 logical_series
3407 };
3408 let stacked = grouping != Grouping::Standard;
3409 let percent = grouping == Grouping::PercentStacked;
3410 let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
3411 let domain = options
3412 .value
3413 .map(|mapping| mapping.domain)
3414 .unwrap_or(domain_from_layers(&layers)?);
3415 let value_orientation = options
3416 .value
3417 .map(|mapping| mapping.orientation)
3418 .unwrap_or(Orientation::MinMax);
3419 let mut elements = Vec::new();
3420 for (series_index, layer) in layers.iter().enumerate() {
3421 let colour = series_colour(series_colours, series_index)?;
3422 let indexes: Vec<_> = layer.iter().map(|(index, _, _)| *index).collect();
3423 for (start, end) in contiguous_ranges(&indexes, blanks) {
3424 let segment = &layer[start..end];
3425 let top_values: Vec<_> = segment
3426 .iter()
3427 .map(|(index, _, upper)| (*index, *upper))
3428 .collect();
3429 let lower_values: Vec<_> = segment
3430 .iter()
3431 .map(|(index, lower, _)| (*index, *lower))
3432 .collect();
3433 let top = category_points(
3434 &top_values,
3435 category_count,
3436 domain,
3437 bounds,
3438 options.category_orientation,
3439 value_orientation,
3440 )?;
3441 let mut bottom = category_points(
3442 &lower_values,
3443 category_count,
3444 domain,
3445 bounds,
3446 options.category_orientation,
3447 value_orientation,
3448 )?;
3449 bottom.reverse();
3450 let mut commands = Vec::with_capacity(top.len() + bottom.len() + 2);
3451 commands.push(PathCommand::MoveTo(top[0]));
3452 commands.extend(top.iter().skip(1).copied().map(PathCommand::LineTo));
3453 commands.extend(bottom.into_iter().map(PathCommand::LineTo));
3454 commands.push(PathCommand::Close);
3455 elements.push(filled_and_stroked_path(
3456 Path {
3457 commands,
3458 fill_rule: FillRule::NonZero,
3459 },
3460 colour,
3461 ));
3462 }
3463 }
3464 Ok(elements)
3465}
3466
3467fn render_scatter_geometry(
3468 style: ScatterStyle,
3469 series: &[Series],
3470 bounds: Rect,
3471 blanks: DispBlanksAs,
3472 options: GeometryOptions,
3473 series_colours: &[Color],
3474) -> Result<Vec<PositionedElement>> {
3475 validate_series_values(series)?;
3476 let mut paired_series = Vec::with_capacity(series.len());
3477 let mut marker_series = Vec::with_capacity(series.len());
3478 let mut all_x = Vec::new();
3479 let mut all_y = Vec::new();
3480 for item in series {
3481 let markers = matched_scatter_points(item)?;
3482 let paired = scatter_render_points(item, blanks)?;
3483 all_x.extend(paired.iter().map(|(_, x, _)| *x));
3484 all_y.extend(paired.iter().map(|(_, _, y)| *y));
3485 paired_series.push(paired);
3486 marker_series.push(markers);
3487 }
3488 let x_domain = options
3489 .scatter_x
3490 .map(|mapping| mapping.domain)
3491 .unwrap_or(data_domain(&all_x, false)?);
3492 let y_domain = options
3493 .scatter_y
3494 .map(|mapping| mapping.domain)
3495 .unwrap_or(data_domain(&all_y, false)?);
3496 let x_orientation = options
3497 .scatter_x
3498 .map(|mapping| mapping.orientation)
3499 .unwrap_or(Orientation::MinMax);
3500 let y_orientation = options
3501 .scatter_y
3502 .map(|mapping| mapping.orientation)
3503 .unwrap_or(Orientation::MinMax);
3504 let draw_line = matches!(
3505 style,
3506 ScatterStyle::Line
3507 | ScatterStyle::LineMarker
3508 | ScatterStyle::Smooth
3509 | ScatterStyle::SmoothMarker
3510 );
3511 let draw_marker = matches!(
3512 style,
3513 ScatterStyle::Marker | ScatterStyle::LineMarker | ScatterStyle::SmoothMarker
3514 );
3515 let mut elements = Vec::new();
3516 for (series_index, paired) in paired_series.iter().enumerate() {
3517 let colour = series_colour(series_colours, series_index)?;
3518 if draw_line {
3519 let indexes: Vec<_> = paired.iter().map(|(index, _, _)| *index).collect();
3520 for (start, end) in contiguous_ranges(&indexes, blanks) {
3521 let points = scatter_points(
3522 &paired[start..end],
3523 x_domain,
3524 y_domain,
3525 bounds,
3526 x_orientation,
3527 y_orientation,
3528 )?;
3529 elements.push(stroked_path(polyline_path(&points, false), colour));
3530 }
3531 }
3532 if draw_marker {
3533 let marker_points = scatter_points(
3534 &marker_series[series_index],
3535 x_domain,
3536 y_domain,
3537 bounds,
3538 x_orientation,
3539 y_orientation,
3540 )?;
3541 push_markers(&mut elements, &marker_points, colour);
3542 }
3543 }
3544 Ok(elements)
3545}
3546
3547type ScatterPoint = (usize, f64, f64);
3548
3549struct ScatterPointMaps {
3550 x_count: usize,
3551 y_count: usize,
3552 x_by_index: BTreeMap<usize, f64>,
3553 y_by_index: BTreeMap<usize, f64>,
3554}
3555
3556fn scatter_point_maps(series: &Series) -> Result<ScatterPointMaps> {
3557 let Some(AxisData::Numeric(x_values)) = &series.categories else {
3558 return Err(ChartError::InvalidValue {
3559 element: "c:xVal".to_owned(),
3560 value: "scatter series requires numeric cached x values".to_owned(),
3561 });
3562 };
3563 validate_finite(&x_values.values, "c:xVal/c:numCache")?;
3564 let (x_count, x_points) = logical_numeric_values(x_values)?;
3565 let (y_count, y_points) = logical_numeric_values(&series.values)?;
3566 Ok(ScatterPointMaps {
3567 x_count,
3568 y_count,
3569 x_by_index: x_points.into_iter().collect(),
3570 y_by_index: y_points.into_iter().collect(),
3571 })
3572}
3573
3574fn matched_scatter_points(series: &Series) -> Result<Vec<ScatterPoint>> {
3575 let points = scatter_point_maps(series)?;
3576 Ok(points
3577 .y_by_index
3578 .iter()
3579 .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
3580 .collect())
3581}
3582
3583fn scatter_render_points(series: &Series, blanks: DispBlanksAs) -> Result<Vec<ScatterPoint>> {
3584 let points = scatter_point_maps(series)?;
3585 if blanks != DispBlanksAs::Zero {
3586 return Ok(points
3587 .y_by_index
3588 .iter()
3589 .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
3590 .collect());
3591 }
3592 let count = points.x_count.max(points.y_count);
3593 let present = points
3594 .x_by_index
3595 .keys()
3596 .chain(points.y_by_index.keys())
3597 .copied()
3598 .collect();
3599 Ok(zero_control_indexes(count, &present)
3600 .into_iter()
3601 .map(|index| {
3602 (
3603 index,
3604 points.x_by_index.get(&index).copied().unwrap_or(0.0),
3605 points.y_by_index.get(&index).copied().unwrap_or(0.0),
3606 )
3607 })
3608 .collect())
3609}
3610
3611fn scatter_points(
3612 values: &[(usize, f64, f64)],
3613 x_domain: Domain,
3614 y_domain: Domain,
3615 bounds: Rect,
3616 x_orientation: Orientation,
3617 y_orientation: Orientation,
3618) -> Result<Vec<Point>> {
3619 values
3620 .iter()
3621 .map(|(_, x, y)| {
3622 Ok(Point {
3623 x: map_x_oriented(*x, x_domain, bounds, x_orientation)?,
3624 y: map_y_oriented(*y, y_domain, bounds, y_orientation)?,
3625 })
3626 })
3627 .collect()
3628}
3629
3630fn render_radar_geometry(
3631 style: RadarStyle,
3632 series: &[Series],
3633 bounds: Rect,
3634 options: GeometryOptions,
3635 series_colours: &[Color],
3636) -> Result<Vec<PositionedElement>> {
3637 let point_series = radar_series_points(series, bounds, options)?;
3638 let mut elements = Vec::new();
3639 for (series_index, points) in point_series.iter().enumerate() {
3640 let colour = series_colour(series_colours, series_index)?;
3641 let plain_points: Vec<_> = points.iter().map(|(_, point)| *point).collect();
3642 if plain_points.is_empty() {
3643 continue;
3644 }
3645 let path = polyline_path(&plain_points, true);
3646 if style == RadarStyle::Filled {
3647 elements.push(filled_and_stroked_path(path, colour));
3648 } else {
3649 elements.push(stroked_path(path, colour));
3650 }
3651 if style == RadarStyle::Marker {
3652 push_markers(&mut elements, &plain_points, colour);
3653 }
3654 }
3655 Ok(elements)
3656}
3657
3658fn radar_series_points(
3659 series: &[Series],
3660 bounds: Rect,
3661 options: GeometryOptions,
3662) -> Result<Vec<Vec<(usize, Point)>>> {
3663 validate_series_values(series)?;
3664 let logical_series = logical_series_values(series)?;
3665 let category_count = series_category_count(series)?;
3666 let values = logical_series
3667 .iter()
3668 .flat_map(|(_, points)| points.iter().map(|(_, value)| *value))
3669 .collect::<Vec<_>>();
3670 let mapping = options.value.unwrap_or(AxisMapping {
3671 domain: data_domain(&values, true)?,
3672 orientation: Orientation::MinMax,
3673 });
3674 let center = Point {
3675 x: bounds.x + bounds.width / 2.0,
3676 y: bounds.y + bounds.height / 2.0,
3677 };
3678 let radius = bounds.width.min(bounds.height) / 2.0;
3679 let mut point_series = Vec::with_capacity(logical_series.len());
3680 for (_, logical) in &logical_series {
3681 let mut points = Vec::with_capacity(logical.len());
3682 for (category, value) in logical {
3683 if *value < mapping.domain.min || *value > mapping.domain.max {
3684 continue;
3685 }
3686 points.push((
3687 *category,
3688 radial_point(
3689 center,
3690 normalized_value_oriented(*value, mapping.domain, mapping.orientation)?
3691 * radius,
3692 oriented_category_index(
3693 *category,
3694 category_count,
3695 options.category_orientation,
3696 ),
3697 category_count,
3698 ),
3699 ));
3700 }
3701 point_series.push(points);
3702 }
3703 Ok(point_series)
3704}
3705
3706fn render_pie_geometry(
3707 first_slice_angle: u16,
3708 hole_size: Option<u8>,
3709 series: &[Series],
3710 bounds: Rect,
3711 series_colours: &[Color],
3712) -> Result<Vec<PositionedElement>> {
3713 pie_slices(first_slice_angle, hole_size, series, bounds)?
3714 .into_iter()
3715 .map(|slice| {
3716 Ok(filled_path(
3717 slice.path,
3718 series_colour(series_colours, slice.series_index)?,
3719 ))
3720 })
3721 .collect()
3722}
3723
3724struct PieSliceGeometry {
3725 series_index: usize,
3726 logical_index: usize,
3727 path: Path,
3728 anchor: Point,
3729 base: Point,
3730 end: Point,
3731}
3732
3733fn pie_slices(
3734 first_slice_angle: u16,
3735 hole_size: Option<u8>,
3736 series: &[Series],
3737 bounds: Rect,
3738) -> Result<Vec<PieSliceGeometry>> {
3739 validate_series_values(series)?;
3740 let center = Point {
3741 x: bounds.x + bounds.width / 2.0,
3742 y: bounds.y + bounds.height / 2.0,
3743 };
3744 let radius = bounds.width.min(bounds.height) / 2.0;
3745 let mut elements = Vec::new();
3746 for (series_index, item) in series.iter().enumerate() {
3747 let (_, logical) = logical_numeric_values(&item.values)?;
3748 let total = logical
3749 .iter()
3750 .map(|(_, value)| *value)
3751 .filter(|value| *value > 0.0)
3752 .try_fold(0.0, |total, value| {
3753 checked_geometry_sum(total, value, "pie value total")
3754 })?;
3755 if total <= 0.0 {
3756 continue;
3757 }
3758 let mut angle = f64::from(first_slice_angle).to_radians() - std::f64::consts::FRAC_PI_2;
3759 for (logical_index, value) in logical {
3760 if value <= 0.0 {
3761 continue;
3762 }
3763 let next = angle + value / total * std::f64::consts::TAU;
3764 let path = if let Some(hole_size) = hole_size {
3765 doughnut_wedge(
3766 center,
3767 radius,
3768 radius * f64::from(hole_size) / 100.0,
3769 angle,
3770 next,
3771 )
3772 } else {
3773 pie_wedge(center, radius, angle, next)
3774 };
3775 let inner_radius = hole_size
3776 .map(|hole| radius * f64::from(hole) / 100.0)
3777 .unwrap_or(0.0);
3778 let anchor_radius = hole_size
3779 .map(|_| (radius + inner_radius) / 2.0)
3780 .unwrap_or(radius * 2.0 / 3.0);
3781 let midpoint = (angle + next) / 2.0;
3782 elements.push(PieSliceGeometry {
3783 series_index,
3784 logical_index,
3785 path,
3786 anchor: circle_point(center, anchor_radius, midpoint),
3787 base: circle_point(center, inner_radius, midpoint),
3788 end: circle_point(center, radius, midpoint),
3789 });
3790 angle = next;
3791 }
3792 }
3793 Ok(elements)
3794}
3795
3796fn validate_series_values(series: &[Series]) -> Result<()> {
3797 if series.is_empty() {
3798 return Err(ChartError::MissingElement("c:ser".to_owned()));
3799 }
3800 for item in series {
3801 validate_finite(&item.values.values, "c:val/c:numCache")?;
3802 }
3803 Ok(())
3804}
3805
3806fn validate_finite(values: &[f64], element: &str) -> Result<()> {
3807 if let Some(value) = values.iter().find(|value| !value.is_finite()) {
3808 return Err(ChartError::InvalidValue {
3809 element: element.to_owned(),
3810 value: value.to_string(),
3811 });
3812 }
3813 Ok(())
3814}
3815
3816type IndexedValues = Vec<(usize, f64)>;
3817type LogicalSeries = (usize, IndexedValues);
3818type GeometryLayer = Vec<(usize, f64, f64)>;
3819
3820fn zero_control_indexes(count: usize, present: &BTreeSet<usize>) -> BTreeSet<usize> {
3821 let mut controls = present.clone();
3822 if count == 0 {
3823 return controls;
3824 }
3825 controls.insert(0);
3826 controls.insert(count - 1);
3827 for index in present {
3828 if *index > 0 {
3829 controls.insert(*index - 1);
3830 }
3831 if index + 1 < count {
3832 controls.insert(index + 1);
3833 }
3834 }
3835 controls
3836}
3837
3838fn zero_filled_series(series: &[LogicalSeries], count: usize) -> Vec<LogicalSeries> {
3839 let present = series
3840 .iter()
3841 .flat_map(|(_, points)| points.iter().map(|(index, _)| *index))
3842 .collect();
3843 let controls = zero_control_indexes(count, &present);
3844 series
3845 .iter()
3846 .map(|(declared, points)| {
3847 let values: BTreeMap<_, _> = points.iter().copied().collect();
3848 let filled = controls
3849 .iter()
3850 .map(|index| (*index, values.get(index).copied().unwrap_or(0.0)))
3851 .collect();
3852 (*declared, filled)
3853 })
3854 .collect()
3855}
3856
3857fn contiguous_ranges(indexes: &[usize], blanks: DispBlanksAs) -> Vec<(usize, usize)> {
3858 if indexes.is_empty() {
3859 return Vec::new();
3860 }
3861 if blanks != DispBlanksAs::Gap {
3862 return vec![(0, indexes.len())];
3863 }
3864 let mut ranges = Vec::new();
3865 let mut start = 0usize;
3866 for position in 1..indexes.len() {
3867 if indexes[position - 1].checked_add(1) != Some(indexes[position]) {
3868 ranges.push((start, position));
3869 start = position;
3870 }
3871 }
3872 ranges.push((start, indexes.len()));
3873 ranges
3874}
3875
3876fn logical_numeric_values(data: &NumericData) -> Result<LogicalSeries> {
3877 validate_finite(&data.values, "c:numCache")?;
3878 let (declared, indexes) = cache_layout(&data.markup, data.values.len())?;
3879 let declared = usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
3880 element: "c:ptCount".to_owned(),
3881 value: declared.to_string(),
3882 })?;
3883 let points = indexes
3884 .into_iter()
3885 .zip(data.values.iter().copied())
3886 .map(|(index, value)| {
3887 usize::try_from(index)
3888 .map(|index| (index, value))
3889 .map_err(|_| ChartError::InvalidValue {
3890 element: "c:pt/@idx".to_owned(),
3891 value: index.to_string(),
3892 })
3893 })
3894 .collect::<Result<Vec<_>>>()?;
3895 Ok((declared, points))
3896}
3897
3898fn logical_series_values(series: &[Series]) -> Result<Vec<LogicalSeries>> {
3899 series
3900 .iter()
3901 .map(|item| logical_numeric_values(&item.values))
3902 .collect()
3903}
3904
3905fn reference_logical_count(markup: &ReferenceMarkup, value_count: usize) -> Result<usize> {
3906 let (declared, _) = cache_layout(markup, value_count)?;
3907 usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
3908 element: "c:ptCount".to_owned(),
3909 value: declared.to_string(),
3910 })
3911}
3912
3913fn series_category_count(series: &[Series]) -> Result<usize> {
3914 let mut count = 0usize;
3915 for item in series {
3916 count = count.max(reference_logical_count(
3917 &item.values.markup,
3918 item.values.values.len(),
3919 )?);
3920 if let Some(categories) = &item.categories {
3921 let category_count = match categories {
3922 AxisData::String(data) => reference_logical_count(&data.markup, data.values.len())?,
3923 AxisData::Numeric(data) => {
3924 reference_logical_count(&data.markup, data.values.len())?
3925 }
3926 };
3927 count = count.max(category_count);
3928 }
3929 }
3930 if count == 0 {
3931 return Err(ChartError::InvalidValue {
3932 element: "c:ser".to_owned(),
3933 value: "cached series contain no points".to_owned(),
3934 });
3935 }
3936 Ok(count)
3937}
3938
3939fn stacked_series_bounds(
3940 series: &[LogicalSeries],
3941 stacked: bool,
3942 percent: bool,
3943) -> Result<Vec<GeometryLayer>> {
3944 let mut positive_totals = BTreeMap::<usize, f64>::new();
3945 let mut negative_totals = BTreeMap::<usize, f64>::new();
3946 if percent {
3947 for (_, points) in series {
3948 for &(index, value) in points {
3949 let totals = if value >= 0.0 {
3950 &mut positive_totals
3951 } else {
3952 &mut negative_totals
3953 };
3954 let total = totals.entry(index).or_default();
3955 *total = checked_geometry_sum(*total, value.abs(), "stacked percentage total")?;
3956 }
3957 }
3958 }
3959 let mut positive = BTreeMap::<usize, f64>::new();
3960 let mut negative = BTreeMap::<usize, f64>::new();
3961 let mut layers = Vec::with_capacity(series.len());
3962 for (_, points) in series {
3963 let mut layer = Vec::with_capacity(points.len());
3964 for &(index, original) in points {
3965 let total = if original >= 0.0 {
3966 positive_totals.get(&index).copied().unwrap_or(0.0)
3967 } else {
3968 negative_totals.get(&index).copied().unwrap_or(0.0)
3969 };
3970 let value = if percent && total != 0.0 {
3971 original / total
3972 } else {
3973 original
3974 };
3975 if stacked {
3976 let accumulators = if value >= 0.0 {
3977 &mut positive
3978 } else {
3979 &mut negative
3980 };
3981 let accumulator = accumulators.entry(index).or_default();
3982 let lower = *accumulator;
3983 *accumulator = checked_geometry_sum(*accumulator, value, "stacked value total")?;
3984 layer.push((index, lower, *accumulator));
3985 } else {
3986 layer.push((index, 0.0, value));
3987 }
3988 }
3989 layers.push(layer);
3990 }
3991 Ok(layers)
3992}
3993
3994fn checked_geometry_sum(current: f64, value: f64, context: &str) -> Result<f64> {
3995 let sum = current + value;
3996 if sum.is_finite() {
3997 Ok(sum)
3998 } else {
3999 Err(ChartError::InvalidValue {
4000 element: "chart geometry aggregate".to_owned(),
4001 value: format!("{context} is nonfinite"),
4002 })
4003 }
4004}
4005
4006#[derive(Clone, Copy, Debug, PartialEq)]
4007struct Domain {
4008 min: f64,
4009 max: f64,
4010}
4011
4012fn nice_number_scale(
4013 extent: Domain,
4014 pinned_minimum: Option<f64>,
4015 pinned_maximum: Option<f64>,
4016 target_tick_count: usize,
4017) -> Result<AxisScale> {
4018 if target_tick_count < 2
4019 || !extent.min.is_finite()
4020 || !extent.max.is_finite()
4021 || pinned_minimum.is_some_and(|value| !value.is_finite())
4022 || pinned_maximum.is_some_and(|value| !value.is_finite())
4023 {
4024 return Err(ChartError::InvalidValue {
4025 element: "chart axis scale".to_owned(),
4026 value: "scale bounds and target tick count must be finite and valid".to_owned(),
4027 });
4028 }
4029
4030 let mut data_min = extent.min.min(extent.max);
4031 let mut data_max = extent.min.max(extent.max);
4032 if data_min == data_max {
4033 let expanded = expand_constant_domain(data_min)?;
4034 data_min = expanded.min;
4035 data_max = expanded.max;
4036 }
4037 let requested_min = match (pinned_minimum, pinned_maximum) {
4038 (Some(minimum), _) => minimum,
4039 (None, Some(maximum)) if data_min >= maximum => {
4040 extend_bound(maximum, stable_positive_step(data_min, data_max, 3)?, false)?
4041 }
4042 (None, _) => data_min,
4043 };
4044 let requested_max = match (pinned_minimum, pinned_maximum) {
4045 (_, Some(maximum)) => maximum,
4046 (Some(minimum), None) if data_max <= minimum => {
4047 extend_bound(minimum, stable_positive_step(data_min, data_max, 3)?, true)?
4048 }
4049 (_, None) => data_max,
4050 };
4051 if requested_min >= requested_max {
4052 return Err(ChartError::InvalidValue {
4053 element: "c:scaling".to_owned(),
4054 value: format!("minimum {requested_min} is not less than maximum {requested_max}"),
4055 });
4056 }
4057
4058 let raw_step = stable_positive_step(requested_min, requested_max, target_tick_count)?;
4059 let step = nice_step(raw_step)?;
4060 let rounded_minimum = (requested_min / step).floor() * step;
4061 let rounded_maximum = (requested_max / step).ceil() * step;
4062 let lower_fallback = pinned_minimum.is_none() && !rounded_minimum.is_finite();
4063 let upper_fallback = pinned_maximum.is_none() && !rounded_maximum.is_finite();
4064 let minimum = pinned_minimum.unwrap_or(if rounded_minimum.is_finite() {
4065 rounded_minimum
4066 } else {
4067 requested_min
4068 });
4069 let maximum = pinned_maximum.unwrap_or(if rounded_maximum.is_finite() {
4070 rounded_maximum
4071 } else {
4072 requested_max
4073 });
4074 if !minimum.is_finite() || !maximum.is_finite() || minimum >= maximum {
4075 return Err(ChartError::InvalidValue {
4076 element: "chart axis scale".to_owned(),
4077 value: "nice-number bounds are nonfinite or empty".to_owned(),
4078 });
4079 }
4080
4081 let first = (minimum / step).ceil();
4082 let last = (maximum / step).floor();
4083 let tick_count = if last >= first {
4084 (last - first).min(1024.0) as usize + 1
4085 } else {
4086 0
4087 };
4088 let mut ticks = Vec::with_capacity(tick_count);
4089 for index in 0..tick_count {
4090 let tick = (first + index as f64) * step;
4091 if tick.is_finite() && tick >= minimum && tick <= maximum {
4092 ticks.push(clean_tick(tick, step));
4093 }
4094 }
4095 if lower_fallback && ticks.first().is_none_or(|tick| *tick > minimum) {
4096 ticks.insert(0, minimum);
4097 }
4098 if upper_fallback && ticks.last().is_none_or(|tick| *tick < maximum) {
4099 ticks.push(maximum);
4100 }
4101 if ticks.len() < 2 {
4102 ticks.extend([minimum, maximum]);
4103 ticks.sort_by(f64::total_cmp);
4104 }
4105 ticks.dedup_by(|left, right| *left == *right);
4106 if ticks.len() < 2 || !ticks.windows(2).all(|pair| pair[0] < pair[1]) {
4107 return Err(ChartError::InvalidValue {
4108 element: "chart axis scale".to_owned(),
4109 value: "scale cannot produce two increasing finite ticks".to_owned(),
4110 });
4111 }
4112 Ok(AxisScale {
4113 domain: Domain {
4114 min: minimum,
4115 max: maximum,
4116 },
4117 ticks,
4118 })
4119}
4120
4121const MIN_POSITIVE_SUBNORMAL: f64 = f64::from_bits(1);
4122
4123fn stable_positive_step(minimum: f64, maximum: f64, target_tick_count: usize) -> Result<f64> {
4124 let scale = minimum.abs().max(maximum.abs());
4125 let scale = if scale == 0.0 {
4126 MIN_POSITIVE_SUBNORMAL
4127 } else {
4128 scale
4129 };
4130 let normalized =
4131 (maximum / scale - minimum / scale) / target_tick_count.saturating_sub(1) as f64;
4132 let step = normalized * scale;
4133 if normalized.is_finite() && normalized > 0.0 && step.is_finite() {
4134 Ok(step.max(MIN_POSITIVE_SUBNORMAL))
4135 } else {
4136 Err(ChartError::InvalidValue {
4137 element: "chart axis scale".to_owned(),
4138 value: "scale step is nonfinite or empty".to_owned(),
4139 })
4140 }
4141}
4142
4143fn nice_step(raw_step: f64) -> Result<f64> {
4144 if !raw_step.is_finite() || raw_step <= 0.0 {
4145 return Err(ChartError::InvalidValue {
4146 element: "chart axis scale".to_owned(),
4147 value: format!("invalid raw tick step {raw_step}"),
4148 });
4149 }
4150 let magnitude = 10.0_f64
4151 .powf(raw_step.log10().floor())
4152 .max(MIN_POSITIVE_SUBNORMAL);
4153 let fraction = raw_step / magnitude;
4154 let multiplier = if fraction <= 1.0 {
4155 1.0
4156 } else if fraction <= 2.0 {
4157 2.0
4158 } else if fraction <= 5.0 {
4159 5.0
4160 } else {
4161 10.0
4162 };
4163 let step = (multiplier * magnitude).max(MIN_POSITIVE_SUBNORMAL);
4164 if step.is_finite() && step > 0.0 {
4165 Ok(step)
4166 } else {
4167 Err(ChartError::InvalidValue {
4168 element: "chart axis scale".to_owned(),
4169 value: "nice-number step is nonfinite".to_owned(),
4170 })
4171 }
4172}
4173
4174fn expand_constant_domain(value: f64) -> Result<Domain> {
4175 if value == 0.0 {
4176 return Ok(Domain {
4177 min: -1.0,
4178 max: 1.0,
4179 });
4180 }
4181 let padding = (value.abs() * 0.5).max(MIN_POSITIVE_SUBNORMAL);
4182 let minimum = value - padding;
4183 let maximum = value + padding;
4184 if minimum.is_finite() && maximum.is_finite() && minimum < maximum {
4185 return Ok(Domain {
4186 min: minimum,
4187 max: maximum,
4188 });
4189 }
4190 let domain = if value.is_sign_positive() {
4191 Domain {
4192 min: 0.0,
4193 max: value,
4194 }
4195 } else {
4196 Domain {
4197 min: value,
4198 max: 0.0,
4199 }
4200 };
4201 if domain.min < domain.max {
4202 Ok(domain)
4203 } else {
4204 Err(ChartError::InvalidValue {
4205 element: "chart axis scale".to_owned(),
4206 value: format!("constant value {value} has no finite renderable extent"),
4207 })
4208 }
4209}
4210
4211fn extend_bound(bound: f64, span: f64, positive: bool) -> Result<f64> {
4212 let candidate = if positive { bound + span } else { bound - span };
4213 let extended = if candidate.is_finite()
4214 && if positive {
4215 candidate > bound
4216 } else {
4217 candidate < bound
4218 } {
4219 candidate
4220 } else if positive {
4221 bound.next_up()
4222 } else {
4223 bound.next_down()
4224 };
4225 if extended.is_finite()
4226 && if positive {
4227 extended > bound
4228 } else {
4229 extended < bound
4230 }
4231 {
4232 Ok(extended)
4233 } else {
4234 Err(ChartError::InvalidValue {
4235 element: "c:scaling".to_owned(),
4236 value: format!("one-sided bound {bound} leaves no finite automatic extent"),
4237 })
4238 }
4239}
4240
4241fn clean_tick(value: f64, step: f64) -> f64 {
4242 if value.abs() <= step.abs() * 1.0e-12 {
4243 0.0
4244 } else {
4245 value
4246 }
4247}
4248
4249fn domain_from_layers(layers: &[GeometryLayer]) -> Result<Domain> {
4250 let values = layers
4251 .iter()
4252 .flatten()
4253 .flat_map(|(_, lower, upper)| [*lower, *upper]);
4254 let mut min = 0.0_f64;
4255 let mut max = 0.0_f64;
4256 for value in values {
4257 if !value.is_finite() {
4258 return Err(ChartError::InvalidValue {
4259 element: "chart geometry domain".to_owned(),
4260 value: "aggregate produced a nonfinite value".to_owned(),
4261 });
4262 }
4263 min = min.min(value);
4264 max = max.max(value);
4265 }
4266 if min == max {
4267 max = 1.0;
4268 }
4269 Ok(Domain { min, max })
4270}
4271
4272fn data_domain(values: &[f64], include_zero: bool) -> Result<Domain> {
4273 validate_finite(values, "chart geometry cache")?;
4274 let Some(first) = values.first().copied() else {
4275 return Err(ChartError::InvalidValue {
4276 element: "c:numCache".to_owned(),
4277 value: "cached series contain no points".to_owned(),
4278 });
4279 };
4280 let mut min = first;
4281 let mut max = first;
4282 for value in values.iter().copied() {
4283 min = min.min(value);
4284 max = max.max(value);
4285 }
4286 if include_zero {
4287 min = min.min(0.0);
4288 max = max.max(0.0);
4289 }
4290 if min == max {
4291 if !include_zero {
4292 return expand_constant_domain(min);
4293 }
4294 if min > 0.0 {
4295 min = 0.0;
4296 } else if max < 0.0 {
4297 max = 0.0;
4298 } else {
4299 max = 1.0;
4300 }
4301 }
4302 Ok(Domain { min, max })
4303}
4304
4305fn normalized_value(value: f64, domain: Domain) -> Result<f64> {
4306 let scale = value
4307 .abs()
4308 .max(domain.min.abs())
4309 .max(domain.max.abs())
4310 .max(1.0);
4311 let value = value / scale;
4312 let min = domain.min / scale;
4313 let max = domain.max / scale;
4314 let range = max - min;
4315 let normalized = (value - min) / range;
4316 if !range.is_finite() || range <= 0.0 || !normalized.is_finite() {
4317 return Err(ChartError::InvalidValue {
4318 element: "chart geometry domain".to_owned(),
4319 value: format!("cannot map finite domain {} to {}", domain.min, domain.max),
4320 });
4321 }
4322 Ok(normalized)
4323}
4324
4325fn normalized_value_oriented(value: f64, domain: Domain, orientation: Orientation) -> Result<f64> {
4326 let normalized = normalized_value(value, domain)?;
4327 Ok(if orientation == Orientation::MaxMin {
4328 1.0 - normalized
4329 } else {
4330 normalized
4331 })
4332}
4333
4334fn map_x_oriented(
4335 value: f64,
4336 domain: Domain,
4337 bounds: Rect,
4338 orientation: Orientation,
4339) -> Result<f64> {
4340 let coordinate =
4341 bounds.x + normalized_value_oriented(value, domain, orientation)? * bounds.width;
4342 if coordinate.is_finite() {
4343 Ok(coordinate)
4344 } else {
4345 Err(ChartError::InvalidValue {
4346 element: "chart geometry coordinate".to_owned(),
4347 value: "x coordinate is nonfinite".to_owned(),
4348 })
4349 }
4350}
4351
4352fn map_y_oriented(
4353 value: f64,
4354 domain: Domain,
4355 bounds: Rect,
4356 orientation: Orientation,
4357) -> Result<f64> {
4358 let coordinate = bounds.y + bounds.height
4359 - normalized_value_oriented(value, domain, orientation)? * bounds.height;
4360 if coordinate.is_finite() {
4361 Ok(coordinate)
4362 } else {
4363 Err(ChartError::InvalidValue {
4364 element: "chart geometry coordinate".to_owned(),
4365 value: "y coordinate is nonfinite".to_owned(),
4366 })
4367 }
4368}
4369
4370fn oriented_category_index(index: usize, count: usize, orientation: Orientation) -> usize {
4371 if orientation == Orientation::MaxMin && index < count {
4372 count - index - 1
4373 } else {
4374 index
4375 }
4376}
4377
4378fn category_points(
4379 values: &[(usize, f64)],
4380 count: usize,
4381 domain: Domain,
4382 bounds: Rect,
4383 category_orientation: Orientation,
4384 value_orientation: Orientation,
4385) -> Result<Vec<Point>> {
4386 values
4387 .iter()
4388 .map(|(index, value)| {
4389 let index = oriented_category_index(*index, count, category_orientation);
4390 let x = bounds.x + (index as f64 + 0.5) / count as f64 * bounds.width;
4391 let y = map_y_oriented(*value, domain, bounds, value_orientation)?;
4392 if !x.is_finite() {
4393 return Err(ChartError::InvalidValue {
4394 element: "chart geometry coordinate".to_owned(),
4395 value: "category x coordinate is nonfinite".to_owned(),
4396 });
4397 }
4398 Ok(Point { x, y })
4399 })
4400 .collect()
4401}
4402
4403fn polyline_path(points: &[Point], close: bool) -> Path {
4404 let mut commands = Vec::with_capacity(points.len() + usize::from(close));
4405 if let Some(first) = points.first() {
4406 commands.push(PathCommand::MoveTo(*first));
4407 commands.extend(points.iter().skip(1).copied().map(PathCommand::LineTo));
4408 if close {
4409 commands.push(PathCommand::Close);
4410 }
4411 }
4412 Path {
4413 commands,
4414 fill_rule: FillRule::NonZero,
4415 }
4416}
4417
4418fn pie_wedge(center: Point, radius: f64, start: f64, end: f64) -> Path {
4419 let first = circle_point(center, radius, start);
4420 let mut commands = vec![PathCommand::MoveTo(center), PathCommand::LineTo(first)];
4421 append_arc(&mut commands, center, radius, start, end);
4422 commands.push(PathCommand::Close);
4423 Path {
4424 commands,
4425 fill_rule: FillRule::NonZero,
4426 }
4427}
4428
4429fn doughnut_wedge(
4430 center: Point,
4431 outer_radius: f64,
4432 inner_radius: f64,
4433 start: f64,
4434 end: f64,
4435) -> Path {
4436 let outer_start = circle_point(center, outer_radius, start);
4437 let inner_end = circle_point(center, inner_radius, end);
4438 let mut commands = vec![PathCommand::MoveTo(outer_start)];
4439 append_arc(&mut commands, center, outer_radius, start, end);
4440 commands.push(PathCommand::LineTo(inner_end));
4441 append_arc(&mut commands, center, inner_radius, end, start);
4442 commands.push(PathCommand::Close);
4443 Path {
4444 commands,
4445 fill_rule: FillRule::NonZero,
4446 }
4447}
4448
4449fn append_arc(commands: &mut Vec<PathCommand>, center: Point, radius: f64, start: f64, end: f64) {
4450 let segments = ((end - start).abs() / std::f64::consts::FRAC_PI_2)
4451 .ceil()
4452 .max(1.0) as usize;
4453 let sweep = (end - start) / segments as f64;
4454 for segment in 0..segments {
4455 let angle = start + segment as f64 * sweep;
4456 let next = angle + sweep;
4457 let factor = 4.0 / 3.0 * (sweep / 4.0).tan();
4458 let from = circle_point(center, radius, angle);
4459 let to = circle_point(center, radius, next);
4460 commands.push(PathCommand::CurveTo {
4461 c1: Point {
4462 x: from.x - factor * radius * angle.sin(),
4463 y: from.y + factor * radius * angle.cos(),
4464 },
4465 c2: Point {
4466 x: to.x + factor * radius * next.sin(),
4467 y: to.y - factor * radius * next.cos(),
4468 },
4469 to,
4470 });
4471 }
4472}
4473
4474fn circle_point(center: Point, radius: f64, angle: f64) -> Point {
4475 Point {
4476 x: center.x + radius * angle.cos(),
4477 y: center.y + radius * angle.sin(),
4478 }
4479}
4480
4481fn radial_point(center: Point, radius: f64, index: usize, count: usize) -> Point {
4482 circle_point(
4483 center,
4484 radius,
4485 index as f64 * std::f64::consts::TAU / count as f64 - std::f64::consts::FRAC_PI_2,
4486 )
4487}
4488
4489fn marker_path(center: Point) -> Path {
4490 const RADIUS: f64 = 2.5;
4491 const KAPPA: f64 = 0.552_284_749_830_793_6;
4492 let control = RADIUS * KAPPA;
4493 Path {
4494 commands: vec![
4495 PathCommand::MoveTo(Point {
4496 x: center.x + RADIUS,
4497 y: center.y,
4498 }),
4499 PathCommand::CurveTo {
4500 c1: Point {
4501 x: center.x + RADIUS,
4502 y: center.y + control,
4503 },
4504 c2: Point {
4505 x: center.x + control,
4506 y: center.y + RADIUS,
4507 },
4508 to: Point {
4509 x: center.x,
4510 y: center.y + RADIUS,
4511 },
4512 },
4513 PathCommand::CurveTo {
4514 c1: Point {
4515 x: center.x - control,
4516 y: center.y + RADIUS,
4517 },
4518 c2: Point {
4519 x: center.x - RADIUS,
4520 y: center.y + control,
4521 },
4522 to: Point {
4523 x: center.x - RADIUS,
4524 y: center.y,
4525 },
4526 },
4527 PathCommand::CurveTo {
4528 c1: Point {
4529 x: center.x - RADIUS,
4530 y: center.y - control,
4531 },
4532 c2: Point {
4533 x: center.x - control,
4534 y: center.y - RADIUS,
4535 },
4536 to: Point {
4537 x: center.x,
4538 y: center.y - RADIUS,
4539 },
4540 },
4541 PathCommand::CurveTo {
4542 c1: Point {
4543 x: center.x + control,
4544 y: center.y - RADIUS,
4545 },
4546 c2: Point {
4547 x: center.x + RADIUS,
4548 y: center.y - control,
4549 },
4550 to: Point {
4551 x: center.x + RADIUS,
4552 y: center.y,
4553 },
4554 },
4555 PathCommand::Close,
4556 ],
4557 fill_rule: FillRule::NonZero,
4558 }
4559}
4560
4561fn push_markers(elements: &mut Vec<PositionedElement>, points: &[Point], colour: Color) {
4562 for point in points {
4563 elements.push(filled_path(marker_path(*point), colour));
4564 }
4565}
4566
4567fn series_colour(series_colours: &[Color], index: usize) -> Result<Color> {
4568 series_colours
4569 .get(index)
4570 .copied()
4571 .ok_or_else(|| ChartError::InvalidValue {
4572 element: "chart series palette".to_owned(),
4573 value: format!("missing resolved colour for series {index}"),
4574 })
4575}
4576
4577fn filled_path(path: Path, color: Color) -> PositionedElement {
4578 PositionedElement::Path(PathElement {
4579 path,
4580 fill: Some(Paint::Solid(color)),
4581 stroke: None,
4582 })
4583}
4584
4585fn stroked_path(path: Path, color: Color) -> PositionedElement {
4586 PositionedElement::Path(PathElement {
4587 path,
4588 fill: None,
4589 stroke: Some(Stroke::new(Paint::Solid(color), 1.5)),
4590 })
4591}
4592
4593fn filled_and_stroked_path(path: Path, color: Color) -> PositionedElement {
4594 PositionedElement::Path(PathElement {
4595 path,
4596 fill: Some(Paint::Solid(Color {
4597 a: color.a * 0.55,
4598 ..color
4599 })),
4600 stroke: Some(Stroke::new(Paint::Solid(color), 1.0)),
4601 })
4602}
4603
4604type NamespaceBindings = Vec<(Vec<u8>, String)>;
4605type XmlAttributes = Vec<(String, String)>;
4606type RootAttributes = (XmlAttributes, XmlAttributes);
4607
4608#[derive(Clone, Debug, Default, Eq, PartialEq)]
4609struct ScalarMarkup {
4610 raw_attributes: XmlAttributes,
4611 raw_content: Vec<Vec<u8>>,
4612}
4613
4614#[derive(Clone, Debug, Default, Eq, PartialEq)]
4615struct TextMarkup {
4616 raw_attributes: XmlAttributes,
4617 raw_children: OrderedRawChildren,
4618}
4619
4620#[derive(Clone, Debug, Default, Eq, PartialEq)]
4621struct PointMarkup {
4622 raw_attributes: XmlAttributes,
4623 raw_children: OrderedRawChildren,
4624 value: TextMarkup,
4625}
4626
4627#[derive(Clone, Debug, Default, Eq, PartialEq)]
4628struct ReferenceMarkup {
4629 raw_attributes: XmlAttributes,
4630 raw_children: OrderedRawChildren,
4631 formula: TextMarkup,
4632 cache_attributes: XmlAttributes,
4633 cache_children: OrderedRawChildren,
4634 format_code: Option<TextMarkup>,
4635 format_code_omitted: bool,
4636 point_count: ScalarMarkup,
4637 declared_point_count: Option<u32>,
4638 point_indexes: Vec<u32>,
4639 points: Vec<PointMarkup>,
4640}
4641
4642#[derive(Clone, Debug, Default, Eq, PartialEq)]
4643struct WrapperMarkup {
4644 raw_attributes: XmlAttributes,
4645 raw_children: OrderedRawChildren,
4646}
4647
4648#[derive(Clone, Debug, PartialEq)]
4650pub struct StringRef {
4651 pub formula: String,
4652 pub values: Vec<String>,
4653 markup: ReferenceMarkup,
4654}
4655
4656impl StringRef {
4657 pub fn new(formula: String, values: Vec<String>) -> Result<Self> {
4658 let reference = Self {
4659 formula,
4660 values,
4661 markup: ReferenceMarkup::default(),
4662 };
4663 reference.validate()?;
4664 Ok(reference)
4665 }
4666
4667 pub fn from_xml(xml: &[u8]) -> Result<Self> {
4668 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
4669 }
4670
4671 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
4672 let parsed = parse_reference(xml, b"strRef", b"strCache", false, inherited)?;
4673 let values = parsed.values;
4674 let reference = Self {
4675 formula: parsed.formula,
4676 values,
4677 markup: parsed.markup,
4678 };
4679 reference.validate()?;
4680 Ok(reference)
4681 }
4682
4683 pub fn to_xml(&self) -> Result<Vec<u8>> {
4684 self.validate()?;
4685 let mut writer = Writer::new(Vec::new());
4686 self.write_xml(&mut writer, true)?;
4687 Ok(writer.into_inner())
4688 }
4689
4690 fn validate(&self) -> Result<()> {
4691 validate_formula(&self.formula, "c:strRef/c:f")?;
4692 for value in &self.values {
4693 validate_xml_text(value, "c:strCache/c:pt/c:v")?;
4694 }
4695 validate_point_count(self.values.len(), "c:strCache")
4696 }
4697
4698 fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
4699 self.validate()?;
4700 let mut start = BytesStart::new("c:strRef");
4701 if standalone {
4702 start.push_attribute(("xmlns:c", C_NS));
4703 }
4704 push_attributes(&mut start, &self.markup.raw_attributes);
4705 writer
4706 .write_event(Event::Start(start))
4707 .map_err(OxmlError::from)?;
4708 emit_raw(writer, self.markup.raw_children.at(0))?;
4709 write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
4710 emit_raw(writer, self.markup.raw_children.at(1))?;
4711 write_string_cache(writer, &self.values, &self.markup)?;
4712 emit_raw(writer, self.markup.raw_children.at(2))?;
4713 writer
4714 .write_event(Event::End(BytesEnd::new("c:strRef")))
4715 .map_err(OxmlError::from)?;
4716 Ok(())
4717 }
4718}
4719
4720#[derive(Clone, Debug, PartialEq)]
4722pub struct NumericData {
4723 pub formula: String,
4724 pub format_code: String,
4725 pub values: Vec<f64>,
4726 markup: ReferenceMarkup,
4727}
4728
4729impl NumericData {
4730 pub fn new(formula: String, format_code: String, values: Vec<f64>) -> Result<Self> {
4731 validate_formula(&formula, "c:numRef/c:f")?;
4732 validate_format_code(&format_code)?;
4733 validate_numeric_values(&values)?;
4734 validate_point_count(values.len(), "c:numCache")?;
4735 Ok(Self {
4736 formula,
4737 format_code,
4738 values,
4739 markup: ReferenceMarkup::default(),
4740 })
4741 }
4742
4743 pub fn from_xml(xml: &[u8]) -> Result<Self> {
4744 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
4745 }
4746
4747 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
4748 let parsed = parse_reference(xml, b"numRef", b"numCache", true, inherited)?;
4749 let format_code = parsed.format_code.unwrap_or_else(|| "General".to_owned());
4750 let mut values = Vec::with_capacity(parsed.values.len());
4751 for value in parsed.values {
4752 let number = value.parse::<f64>().map_err(|_| ChartError::InvalidValue {
4753 element: "c:v".to_owned(),
4754 value: value.clone(),
4755 })?;
4756 if !number.is_finite() {
4757 return Err(ChartError::InvalidValue {
4758 element: "c:v".to_owned(),
4759 value,
4760 });
4761 }
4762 values.push(number);
4763 }
4764 let data = Self {
4765 formula: parsed.formula,
4766 format_code,
4767 values,
4768 markup: parsed.markup,
4769 };
4770 data.validate()?;
4771 Ok(data)
4772 }
4773
4774 pub fn to_xml(&self) -> Result<Vec<u8>> {
4775 self.validate()?;
4776 let mut writer = Writer::new(Vec::new());
4777 self.write_xml(&mut writer, true)?;
4778 Ok(writer.into_inner())
4779 }
4780
4781 fn validate(&self) -> Result<()> {
4782 validate_formula(&self.formula, "c:numRef/c:f")?;
4783 validate_format_code(&self.format_code)?;
4784 validate_numeric_values(&self.values)?;
4785 validate_point_count(self.values.len(), "c:numCache")
4786 }
4787
4788 fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
4789 self.validate()?;
4790 let mut start = BytesStart::new("c:numRef");
4791 if standalone {
4792 start.push_attribute(("xmlns:c", C_NS));
4793 }
4794 push_attributes(&mut start, &self.markup.raw_attributes);
4795 writer
4796 .write_event(Event::Start(start))
4797 .map_err(OxmlError::from)?;
4798 emit_raw(writer, self.markup.raw_children.at(0))?;
4799 write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
4800 emit_raw(writer, self.markup.raw_children.at(1))?;
4801 write_numeric_cache(writer, self, &self.markup)?;
4802 emit_raw(writer, self.markup.raw_children.at(2))?;
4803 writer
4804 .write_event(Event::End(BytesEnd::new("c:numRef")))
4805 .map_err(OxmlError::from)?;
4806 Ok(())
4807 }
4808}
4809
4810#[derive(Clone, Debug, PartialEq)]
4811pub enum AxisData {
4812 String(StringRef),
4813 Numeric(NumericData),
4814}
4815
4816#[derive(Clone, Debug, PartialEq)]
4817struct DataPoint {
4818 index: u32,
4819 sp_pr: CT_ShapeProperties,
4820}
4821
4822impl DataPoint {
4823 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
4824 writer
4825 .write_event(Event::Start(BytesStart::new("c:dPt")))
4826 .map_err(OxmlError::from)?;
4827 write_scalar(writer, "c:idx", &self.index.to_string(), None)?;
4828 self.sp_pr.write_xml_as(writer, "c:spPr")?;
4829 writer
4830 .write_event(Event::End(BytesEnd::new("c:dPt")))
4831 .map_err(OxmlError::from)?;
4832 Ok(())
4833 }
4834}
4835
4836#[derive(Clone, Debug, PartialEq)]
4838pub struct Series {
4839 pub index: u32,
4840 pub order: u32,
4841 pub name: Option<StringRef>,
4842 pub categories: Option<AxisData>,
4843 pub values: NumericData,
4844 pub bubble_size: Option<NumericData>,
4845 pub sp_pr: Option<CT_ShapeProperties>,
4846 pub data_labels: Option<CT_DLbls>,
4847 authored_data_points: Vec<DataPoint>,
4848 index_markup: ScalarMarkup,
4849 order_markup: ScalarMarkup,
4850 name_markup: Option<WrapperMarkup>,
4851 categories_markup: Option<WrapperMarkup>,
4852 values_markup: WrapperMarkup,
4853 bubble_size_markup: Option<WrapperMarkup>,
4854 opaque_name: bool,
4855 opaque_categories: bool,
4856 opaque_bubble_size: bool,
4857 uses_scatter_wrappers: bool,
4858 parsed_from_xml: bool,
4859 raw_attributes: XmlAttributes,
4860 namespace_declarations: XmlAttributes,
4861 raw_children: OrderedRawChildren,
4862}
4863
4864impl Series {
4865 pub fn new(index: u32, order: u32, values: NumericData) -> Self {
4866 Self {
4867 index,
4868 order,
4869 name: None,
4870 categories: None,
4871 values,
4872 bubble_size: None,
4873 sp_pr: None,
4874 data_labels: None,
4875 authored_data_points: Vec::new(),
4876 index_markup: ScalarMarkup::default(),
4877 order_markup: ScalarMarkup::default(),
4878 name_markup: None,
4879 categories_markup: None,
4880 values_markup: WrapperMarkup::default(),
4881 bubble_size_markup: None,
4882 opaque_name: false,
4883 opaque_categories: false,
4884 opaque_bubble_size: false,
4885 uses_scatter_wrappers: false,
4886 parsed_from_xml: false,
4887 raw_attributes: Vec::new(),
4888 namespace_declarations: Vec::new(),
4889 raw_children: OrderedRawChildren::default(),
4890 }
4891 }
4892
4893 pub fn from_xml(xml: &[u8]) -> Result<Self> {
4894 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
4895 }
4896
4897 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
4898 let mut reader = Reader::from_reader(xml);
4899 let mut buffer = Vec::new();
4900 loop {
4901 match reader
4902 .read_event_into(&mut buffer)
4903 .map_err(OxmlError::from)?
4904 {
4905 Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4906 chart_root_prefix(&element)?;
4907 if !element_is_in_namespace(&element, C_NS, inherited)? {
4908 return Err(ChartError::UnexpectedElement(element_name(&element)));
4909 }
4910 return Self::from_element(&mut reader, &element, inherited);
4911 }
4912 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4913 return Err(ChartError::MissingElement("c:idx".to_owned()));
4914 }
4915 Event::Start(element) | Event::Empty(element) => {
4916 return Err(ChartError::UnexpectedElement(element_name(&element)));
4917 }
4918 Event::Eof => return Err(ChartError::MissingElement("c:ser".to_owned())),
4919 _ => {}
4920 }
4921 buffer.clear();
4922 }
4923 }
4924
4925 fn from_element(
4926 reader: &mut Reader<&[u8]>,
4927 start: &BytesStart<'_>,
4928 inherited: &NamespaceBindings,
4929 ) -> Result<Self> {
4930 reject_conflicting_prefix(start, b"a", A_NS)?;
4931 let namespaces = chart_bindings(inherited, start)?;
4932 require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
4933 require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
4934 require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
4935 let (raw_attributes, _) =
4936 capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
4937 let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
4938 let mut state = SeriesParseState::default();
4939 let mut buffer = Vec::new();
4940 loop {
4941 match reader
4942 .read_event_into(&mut buffer)
4943 .map_err(OxmlError::from)?
4944 {
4945 Event::Start(element) => {
4946 let name = chart_child_local(&element, &namespaces)?;
4947 let raw = capture_element(reader, &element)?;
4948 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
4949 }
4950 Event::Empty(element) => {
4951 let name = chart_child_local(&element, &namespaces)?;
4952 let raw = capture_empty_element(&element)?;
4953 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
4954 }
4955 event @ (Event::Text(_)
4956 | Event::CData(_)
4957 | Event::Comment(_)
4958 | Event::PI(_)
4959 | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
4960 Event::End(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4961 break;
4962 }
4963 Event::Eof => return Err(missing_end("c:ser")),
4964 _ => {}
4965 }
4966 buffer.clear();
4967 }
4968 state.finish(raw_attributes, namespace_declarations)
4969 }
4970
4971 pub fn to_xml(&self) -> Result<Vec<u8>> {
4972 self.to_xml_for_plot(self.uses_scatter_wrappers)
4973 }
4974
4975 fn to_xml_for_plot(&self, scatter: bool) -> Result<Vec<u8>> {
4976 if self.opaque_name && self.name.is_some() {
4977 return Err(ChartError::DuplicateElement("c:tx".to_owned()));
4978 }
4979 if self.opaque_categories && self.categories.is_some() {
4980 return Err(ChartError::DuplicateElement("c:cat".to_owned()));
4981 }
4982 if self.opaque_bubble_size && self.bubble_size.is_some() {
4983 return Err(ChartError::DuplicateElement("c:bubbleSize".to_owned()));
4984 }
4985 self.values.validate()?;
4986 if let Some(name) = &self.name {
4987 name.validate()?;
4988 }
4989 if let Some(categories) = &self.categories {
4990 if scatter && !matches!(categories, AxisData::Numeric(_)) {
4991 return Err(ChartError::InvalidValue {
4992 element: "c:xVal".to_owned(),
4993 value: "scatter x values must use a numeric cache".to_owned(),
4994 });
4995 }
4996 match categories {
4997 AxisData::String(reference) => reference.validate()?,
4998 AxisData::Numeric(reference) => reference.validate()?,
4999 }
5000 }
5001 if scatter && self.categories.is_none() {
5002 return Err(ChartError::MissingElement("c:xVal".to_owned()));
5003 }
5004 if let Some(size) = &self.bubble_size {
5005 size.validate()?;
5006 }
5007 if let Some(labels) = &self.data_labels {
5008 labels.validate()?;
5009 }
5010 let mut writer = Writer::new(Vec::new());
5011 let mut start = BytesStart::new("c:ser");
5012 start.push_attribute(("xmlns:c", C_NS));
5013 start.push_attribute(("xmlns:a", A_NS));
5014 start.push_attribute(("xmlns:r", R_NS));
5015 push_attributes(&mut start, &self.namespace_declarations);
5016 push_attributes(&mut start, &self.raw_attributes);
5017 writer
5018 .write_event(Event::Start(start))
5019 .map_err(OxmlError::from)?;
5020 emit_raw(&mut writer, self.raw_children.at(0))?;
5021 write_scalar(
5022 &mut writer,
5023 "c:idx",
5024 &self.index.to_string(),
5025 Some(&self.index_markup),
5026 )?;
5027 emit_raw(&mut writer, self.raw_children.at(1))?;
5028 write_scalar(
5029 &mut writer,
5030 "c:order",
5031 &self.order.to_string(),
5032 Some(&self.order_markup),
5033 )?;
5034 emit_raw(&mut writer, self.raw_children.at(2))?;
5035 if let Some(name) = &self.name {
5036 write_wrapper_start(
5037 &mut writer,
5038 "c:tx",
5039 self.name_markup
5040 .as_ref()
5041 .unwrap_or(&WrapperMarkup::default()),
5042 )?;
5043 name.write_xml(&mut writer, false)?;
5044 write_wrapper_end(
5045 &mut writer,
5046 "c:tx",
5047 self.name_markup
5048 .as_ref()
5049 .unwrap_or(&WrapperMarkup::default()),
5050 )?;
5051 }
5052 emit_raw(&mut writer, self.raw_children.at(3))?;
5053 if let Some(properties) = &self.sp_pr {
5054 properties.write_xml_as(&mut writer, "c:spPr")?;
5055 }
5056 emit_raw(&mut writer, self.raw_children.at(4))?;
5057 for point in &self.authored_data_points {
5058 point.write_xml(&mut writer)?;
5059 }
5060 if let Some(labels) = &self.data_labels {
5061 labels.write_xml(&mut writer, false)?;
5062 }
5063 emit_raw(&mut writer, self.raw_children.at(5))?;
5064 if let Some(categories) = &self.categories {
5065 let default_markup = WrapperMarkup::default();
5066 let markup = self.categories_markup.as_ref().unwrap_or(&default_markup);
5067 write_wrapper_start(
5068 &mut writer,
5069 if scatter { "c:xVal" } else { "c:cat" },
5070 markup,
5071 )?;
5072 match categories {
5073 AxisData::String(reference) => reference.write_xml(&mut writer, false)?,
5074 AxisData::Numeric(reference) => reference.write_xml(&mut writer, false)?,
5075 }
5076 write_wrapper_end(
5077 &mut writer,
5078 if scatter { "c:xVal" } else { "c:cat" },
5079 markup,
5080 )?;
5081 }
5082 emit_raw(&mut writer, self.raw_children.at(6))?;
5083 write_wrapper_start(
5084 &mut writer,
5085 if scatter { "c:yVal" } else { "c:val" },
5086 &self.values_markup,
5087 )?;
5088 self.values.write_xml(&mut writer, false)?;
5089 write_wrapper_end(
5090 &mut writer,
5091 if scatter { "c:yVal" } else { "c:val" },
5092 &self.values_markup,
5093 )?;
5094 emit_raw(&mut writer, self.raw_children.at(7))?;
5095 if let Some(size) = &self.bubble_size {
5096 let default_markup = WrapperMarkup::default();
5097 let markup = self.bubble_size_markup.as_ref().unwrap_or(&default_markup);
5098 write_wrapper_start(&mut writer, "c:bubbleSize", markup)?;
5099 size.write_xml(&mut writer, false)?;
5100 write_wrapper_end(&mut writer, "c:bubbleSize", markup)?;
5101 }
5102 emit_raw(&mut writer, self.raw_children.at(8))?;
5103 writer
5104 .write_event(Event::End(BytesEnd::new("c:ser")))
5105 .map_err(OxmlError::from)?;
5106 Ok(writer.into_inner())
5107 }
5108
5109 pub fn raw_children(&self) -> &OrderedRawChildren {
5110 &self.raw_children
5111 }
5112}
5113
5114#[derive(Default)]
5115struct SeriesParseState {
5116 index: Option<(u32, ScalarMarkup)>,
5117 order: Option<(u32, ScalarMarkup)>,
5118 name: Option<(StringRef, WrapperMarkup)>,
5119 categories: Option<(AxisData, WrapperMarkup)>,
5120 values: Option<(NumericData, WrapperMarkup)>,
5121 bubble_size: Option<(NumericData, WrapperMarkup)>,
5122 data_labels: Option<CT_DLbls>,
5123 name_seen: bool,
5124 categories_seen: bool,
5125 bubble_size_seen: bool,
5126 opaque_name: bool,
5127 opaque_categories: bool,
5128 opaque_bubble_size: bool,
5129 saw_standard_wrapper: bool,
5130 saw_scatter_wrapper: bool,
5131 sp_pr: Option<CT_ShapeProperties>,
5132 raw_children: OrderedRawChildren,
5133 boundary: usize,
5134}
5135
5136impl SeriesParseState {
5137 fn capture_event(&mut self, raw: Vec<u8>) {
5138 self.raw_children.push(self.boundary, raw);
5139 }
5140
5141 fn parse_child(
5142 &mut self,
5143 name: &[u8],
5144 raw: Vec<u8>,
5145 namespaces: &NamespaceBindings,
5146 ) -> Result<()> {
5147 match name {
5148 b"idx" => {
5149 set_once(&mut self.index, parse_u32_scalar(&raw, "idx")?, "c:idx")?;
5150 self.boundary = self.boundary.max(1);
5151 }
5152 b"order" => {
5153 set_once(&mut self.order, parse_u32_scalar(&raw, "order")?, "c:order")?;
5154 self.boundary = self.boundary.max(2);
5155 }
5156 b"tx" => {
5157 mark_once(&mut self.name_seen, "c:tx")?;
5158 let parsed = parse_wrapper(&raw, b"tx", &[b"strRef"], namespaces)?;
5159 if let Some((_, reference)) = parsed.choice {
5160 self.name = Some((
5161 StringRef::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
5162 parsed.markup,
5163 ));
5164 } else {
5165 self.opaque_name = true;
5166 self.raw_children.push(2, raw);
5167 }
5168 self.boundary = self.boundary.max(3);
5169 }
5170 b"spPr" => {
5171 parse_chart_shape_properties(&raw, namespaces, &mut self.sp_pr)?;
5172 self.boundary = self.boundary.max(4);
5173 }
5174 b"dLbls" => {
5175 set_once(
5176 &mut self.data_labels,
5177 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
5178 "c:dLbls",
5179 )?;
5180 self.boundary = self.boundary.max(5);
5181 }
5182 b"cat" => {
5183 self.saw_standard_wrapper = true;
5184 mark_once(&mut self.categories_seen, "c:cat")?;
5185 let parsed = parse_wrapper(&raw, b"cat", &[b"strRef", b"numRef"], namespaces)?;
5186 if let Some((choice, reference)) = parsed.choice {
5187 let categories = if choice == b"strRef" {
5188 AxisData::String(StringRef::from_xml_with_namespaces(
5189 &reference,
5190 &parsed.namespaces,
5191 )?)
5192 } else {
5193 AxisData::Numeric(NumericData::from_xml_with_namespaces(
5194 &reference,
5195 &parsed.namespaces,
5196 )?)
5197 };
5198 self.categories = Some((categories, parsed.markup));
5199 } else {
5200 self.opaque_categories = true;
5201 self.raw_children.push(5, raw);
5202 }
5203 self.boundary = self.boundary.max(6);
5204 }
5205 b"xVal" => {
5206 self.saw_scatter_wrapper = true;
5207 mark_once(&mut self.categories_seen, "c:xVal")?;
5208 let parsed = parse_wrapper(&raw, b"xVal", &[b"numRef"], namespaces)?;
5209 let (_, reference) = parsed
5210 .choice
5211 .ok_or_else(|| ChartError::MissingElement("c:xVal/c:numRef".to_owned()))?;
5212 self.categories = Some((
5213 AxisData::Numeric(NumericData::from_xml_with_namespaces(
5214 &reference,
5215 &parsed.namespaces,
5216 )?),
5217 parsed.markup,
5218 ));
5219 self.boundary = self.boundary.max(6);
5220 }
5221 b"val" | b"yVal" => {
5222 if name == b"yVal" {
5223 self.saw_scatter_wrapper = true;
5224 } else {
5225 self.saw_standard_wrapper = true;
5226 }
5227 let wrapper = if name == b"yVal" {
5228 b"yVal".as_slice()
5229 } else {
5230 b"val".as_slice()
5231 };
5232 let parsed = parse_wrapper(&raw, wrapper, &[b"numRef"], namespaces)?;
5233 let (_, reference) = parsed.choice.ok_or_else(|| {
5234 ChartError::MissingElement(format!(
5235 "c:{}/c:numRef",
5236 String::from_utf8_lossy(wrapper)
5237 ))
5238 })?;
5239 set_once(
5240 &mut self.values,
5241 (
5242 NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
5243 parsed.markup,
5244 ),
5245 "c:val or c:yVal",
5246 )?;
5247 self.boundary = self.boundary.max(7);
5248 }
5249 b"bubbleSize" => {
5250 mark_once(&mut self.bubble_size_seen, "c:bubbleSize")?;
5251 let parsed = parse_wrapper(&raw, b"bubbleSize", &[b"numRef"], namespaces)?;
5252 if let Some((_, reference)) = parsed.choice {
5253 self.bubble_size = Some((
5254 NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
5255 parsed.markup,
5256 ));
5257 } else {
5258 self.opaque_bubble_size = true;
5259 self.raw_children.push(7, raw);
5260 }
5261 self.boundary = self.boundary.max(8);
5262 }
5263 _ => {
5264 let boundary = series_raw_boundary(name, self.boundary);
5265 self.raw_children.push(boundary, raw);
5266 self.boundary = self.boundary.max(boundary);
5267 }
5268 }
5269 Ok(())
5270 }
5271
5272 fn finish(
5273 self,
5274 raw_attributes: XmlAttributes,
5275 namespace_declarations: XmlAttributes,
5276 ) -> Result<Series> {
5277 if self.saw_standard_wrapper && self.saw_scatter_wrapper {
5278 return Err(ChartError::InvalidValue {
5279 element: "c:ser".to_owned(),
5280 value: "category/value and x/y wrappers cannot be combined".to_owned(),
5281 });
5282 }
5283 let (index, index_markup) = self
5284 .index
5285 .ok_or_else(|| ChartError::MissingElement("c:idx".to_owned()))?;
5286 let (order, order_markup) = self
5287 .order
5288 .ok_or_else(|| ChartError::MissingElement("c:order".to_owned()))?;
5289 let (values, values_markup) = self
5290 .values
5291 .ok_or_else(|| ChartError::MissingElement("c:val".to_owned()))?;
5292 let (name, name_markup) = self
5293 .name
5294 .map(|(value, markup)| (Some(value), Some(markup)))
5295 .unwrap_or((None, None));
5296 let (categories, categories_markup) = self
5297 .categories
5298 .map(|(value, markup)| (Some(value), Some(markup)))
5299 .unwrap_or((None, None));
5300 let (bubble_size, bubble_size_markup) = self
5301 .bubble_size
5302 .map(|(value, markup)| (Some(value), Some(markup)))
5303 .unwrap_or((None, None));
5304 Ok(Series {
5305 index,
5306 order,
5307 name,
5308 categories,
5309 values,
5310 bubble_size,
5311 sp_pr: self.sp_pr,
5312 data_labels: self.data_labels,
5313 authored_data_points: Vec::new(),
5314 index_markup,
5315 order_markup,
5316 name_markup,
5317 categories_markup,
5318 values_markup,
5319 bubble_size_markup,
5320 opaque_name: self.opaque_name,
5321 opaque_categories: self.opaque_categories,
5322 opaque_bubble_size: self.opaque_bubble_size,
5323 uses_scatter_wrappers: self.saw_scatter_wrapper,
5324 parsed_from_xml: true,
5325 raw_attributes,
5326 namespace_declarations,
5327 raw_children: raw_children_in_schema_order(&self.raw_children, 8),
5328 })
5329 }
5330}
5331
5332fn series_raw_boundary(name: &[u8], current: usize) -> usize {
5333 match name {
5334 b"marker" | b"invertIfNegative" | b"pictureOptions" | b"explosion" | b"dPt" => 4,
5335 b"trendline" | b"errBars" => 5,
5336 b"shape" | b"smooth" => 7,
5337 b"extLst" => 8,
5338 _ => current,
5339 }
5340}
5341
5342struct ParsedReference {
5343 formula: String,
5344 format_code: Option<String>,
5345 values: Vec<String>,
5346 markup: ReferenceMarkup,
5347}
5348
5349fn parse_reference(
5350 xml: &[u8],
5351 reference_local: &[u8],
5352 cache_local: &[u8],
5353 numeric: bool,
5354 inherited: &NamespaceBindings,
5355) -> Result<ParsedReference> {
5356 let mut reader = Reader::from_reader(xml);
5357 let mut buffer = Vec::new();
5358 loop {
5359 match reader
5360 .read_event_into(&mut buffer)
5361 .map_err(OxmlError::from)?
5362 {
5363 Event::Start(element)
5364 if matches_local_name(element.name().as_ref(), reference_local) =>
5365 {
5366 let namespaces = typed_rewrite_bindings(&element, inherited)?;
5367 let raw_attributes = capture_fixed_attributes(&element, &["xmlns:c"])?;
5368 let mut formula: Option<(String, TextMarkup)> = None;
5369 let mut cache: Option<(Option<String>, Vec<String>, CacheMarkup)> = None;
5370 let mut raw_children = OrderedRawChildren::default();
5371 let mut boundary = 0usize;
5372 let mut inner = Vec::new();
5373 loop {
5374 match reader
5375 .read_event_into(&mut inner)
5376 .map_err(OxmlError::from)?
5377 {
5378 Event::Start(child) => {
5379 let name = chart_child_local(&child, &namespaces)?;
5380 let raw = capture_element(&mut reader, &child)?;
5381 match name.as_deref().unwrap_or_default() {
5382 b"f" => {
5383 set_once(
5384 &mut formula,
5385 parse_text_element(&raw, b"f", &namespaces)?,
5386 "c:f",
5387 )?;
5388 boundary = boundary.max(1);
5389 }
5390 name if name == cache_local => {
5391 set_once(
5392 &mut cache,
5393 parse_cache(&raw, cache_local, numeric, &namespaces)?,
5394 &format!("c:{}", String::from_utf8_lossy(cache_local)),
5395 )?;
5396 boundary = boundary.max(2);
5397 }
5398 _ => raw_children.push(boundary, raw),
5399 }
5400 }
5401 Event::Empty(child) => {
5402 let name = chart_child_local(&child, &namespaces)?;
5403 let raw = capture_empty_element(&child)?;
5404 match name.as_deref().unwrap_or_default() {
5405 b"f" => {
5406 set_once(
5407 &mut formula,
5408 parse_text_element(&raw, b"f", &namespaces)?,
5409 "c:f",
5410 )?;
5411 boundary = boundary.max(1);
5412 }
5413 name if name == cache_local => {
5414 set_once(
5415 &mut cache,
5416 parse_cache(&raw, cache_local, numeric, &namespaces)?,
5417 &format!("c:{}", String::from_utf8_lossy(cache_local)),
5418 )?;
5419 boundary = boundary.max(2);
5420 }
5421 _ => raw_children.push(boundary, raw),
5422 }
5423 }
5424 event @ (Event::Text(_)
5425 | Event::CData(_)
5426 | Event::Comment(_)
5427 | Event::PI(_)
5428 | Event::GeneralRef(_)) => {
5429 raw_children.push(boundary, capture_event(event)?);
5430 }
5431 Event::End(end)
5432 if matches_local_name(end.name().as_ref(), reference_local) =>
5433 {
5434 let (formula, formula_markup) = formula
5435 .ok_or_else(|| ChartError::MissingElement("c:f".to_owned()))?;
5436 validate_formula(&formula, "c:f")?;
5437 let (format_code, values, cache_markup) = cache.ok_or_else(|| {
5438 ChartError::MissingElement(format!(
5439 "c:{}",
5440 String::from_utf8_lossy(cache_local)
5441 ))
5442 })?;
5443 let markup = ReferenceMarkup {
5444 raw_attributes,
5445 raw_children: raw_children_in_schema_order(&raw_children, 2),
5446 formula: formula_markup,
5447 cache_attributes: cache_markup.raw_attributes,
5448 cache_children: cache_markup.raw_children,
5449 format_code: cache_markup.format_code,
5450 format_code_omitted: cache_markup.format_code_omitted,
5451 point_count: cache_markup.point_count,
5452 declared_point_count: cache_markup.declared_point_count,
5453 point_indexes: cache_markup.point_indexes,
5454 points: cache_markup.points,
5455 };
5456 return Ok(ParsedReference {
5457 formula,
5458 format_code,
5459 values,
5460 markup,
5461 });
5462 }
5463 Event::Eof => {
5464 return Err(missing_end(&format!(
5465 "c:{}",
5466 String::from_utf8_lossy(reference_local)
5467 )));
5468 }
5469 _ => {}
5470 }
5471 inner.clear();
5472 }
5473 }
5474 Event::Empty(element)
5475 if matches_local_name(element.name().as_ref(), reference_local) =>
5476 {
5477 return Err(ChartError::MissingElement("c:f".to_owned()));
5478 }
5479 Event::Start(element) | Event::Empty(element) => {
5480 return Err(ChartError::UnexpectedElement(element_name(&element)));
5481 }
5482 Event::Eof => {
5483 return Err(ChartError::MissingElement(format!(
5484 "c:{}",
5485 String::from_utf8_lossy(reference_local)
5486 )));
5487 }
5488 _ => {}
5489 }
5490 buffer.clear();
5491 }
5492}
5493
5494struct CacheMarkup {
5495 raw_attributes: XmlAttributes,
5496 raw_children: OrderedRawChildren,
5497 format_code: Option<TextMarkup>,
5498 format_code_omitted: bool,
5499 point_count: ScalarMarkup,
5500 declared_point_count: Option<u32>,
5501 point_indexes: Vec<u32>,
5502 points: Vec<PointMarkup>,
5503}
5504
5505fn parse_cache(
5506 xml: &[u8],
5507 cache_local: &[u8],
5508 numeric: bool,
5509 inherited: &NamespaceBindings,
5510) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
5511 let mut reader = Reader::from_reader(xml);
5512 let mut buffer = Vec::new();
5513 loop {
5514 match reader
5515 .read_event_into(&mut buffer)
5516 .map_err(OxmlError::from)?
5517 {
5518 Event::Start(element) if matches_local_name(element.name().as_ref(), cache_local) => {
5519 let namespaces = typed_rewrite_bindings(&element, inherited)?;
5520 let raw_attributes = capture_attributes(&element)?;
5521 let mut state = CacheParseState::new(numeric, namespaces);
5522 let mut inner = Vec::new();
5523 loop {
5524 match reader
5525 .read_event_into(&mut inner)
5526 .map_err(OxmlError::from)?
5527 {
5528 Event::Start(child) => {
5529 let name = chart_child_local(&child, &state.namespaces)?;
5530 let raw = capture_element(&mut reader, &child)?;
5531 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
5532 }
5533 Event::Empty(child) => {
5534 let name = chart_child_local(&child, &state.namespaces)?;
5535 let raw = capture_empty_element(&child)?;
5536 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
5537 }
5538 event @ (Event::Text(_)
5539 | Event::CData(_)
5540 | Event::Comment(_)
5541 | Event::PI(_)
5542 | Event::GeneralRef(_)) => {
5543 state.capture_event(capture_event(event)?);
5544 }
5545 Event::End(end) if matches_local_name(end.name().as_ref(), cache_local) => {
5546 return state.finish(raw_attributes);
5547 }
5548 Event::Eof => {
5549 return Err(missing_end(&format!(
5550 "c:{}",
5551 String::from_utf8_lossy(cache_local)
5552 )));
5553 }
5554 _ => {}
5555 }
5556 inner.clear();
5557 }
5558 }
5559 Event::Empty(element) if matches_local_name(element.name().as_ref(), cache_local) => {
5560 return Err(ChartError::MissingElement("c:ptCount".to_owned()));
5561 }
5562 Event::Start(element) | Event::Empty(element) => {
5563 return Err(ChartError::UnexpectedElement(element_name(&element)));
5564 }
5565 Event::Eof => {
5566 return Err(ChartError::MissingElement(format!(
5567 "c:{}",
5568 String::from_utf8_lossy(cache_local)
5569 )));
5570 }
5571 _ => {}
5572 }
5573 buffer.clear();
5574 }
5575}
5576
5577struct CacheParseState {
5578 numeric: bool,
5579 namespaces: NamespaceBindings,
5580 point_base: usize,
5581 boundary: usize,
5582 format_code: Option<(String, TextMarkup)>,
5583 point_count: Option<(u32, ScalarMarkup)>,
5584 points: Vec<(u32, String, PointMarkup)>,
5585 raw_children: OrderedRawChildren,
5586}
5587
5588impl CacheParseState {
5589 fn new(numeric: bool, namespaces: NamespaceBindings) -> Self {
5590 Self {
5591 numeric,
5592 namespaces,
5593 point_base: usize::from(numeric) + 1,
5594 boundary: 0,
5595 format_code: None,
5596 point_count: None,
5597 points: Vec::new(),
5598 raw_children: OrderedRawChildren::default(),
5599 }
5600 }
5601
5602 fn capture_event(&mut self, raw: Vec<u8>) {
5603 self.raw_children.push(self.boundary, raw);
5604 }
5605
5606 fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
5607 match name {
5608 b"formatCode" if self.numeric => {
5609 set_once(
5610 &mut self.format_code,
5611 parse_text_element(&raw, b"formatCode", &self.namespaces)?,
5612 "c:formatCode",
5613 )?;
5614 self.boundary = self.boundary.max(1);
5615 }
5616 b"ptCount" => {
5617 set_once(
5618 &mut self.point_count,
5619 parse_u32_scalar(&raw, "ptCount")?,
5620 "c:ptCount",
5621 )?;
5622 self.boundary = self.boundary.max(self.point_base);
5623 }
5624 b"pt" => {
5625 self.points.push(parse_point(&raw, &self.namespaces)?);
5626 self.boundary = self.boundary.max(self.point_base + self.points.len());
5627 }
5628 _ => self.raw_children.push(self.boundary, raw),
5629 }
5630 Ok(())
5631 }
5632
5633 fn finish(
5634 self,
5635 raw_attributes: XmlAttributes,
5636 ) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
5637 let (declared, point_count_markup) = self
5638 .point_count
5639 .ok_or_else(|| ChartError::MissingElement("c:ptCount".to_owned()))?;
5640 let actual = u32::try_from(self.points.len()).map_err(|_| ChartError::InvalidValue {
5641 element: "c:ptCount".to_owned(),
5642 value: self.points.len().to_string(),
5643 })?;
5644 if declared < actual {
5645 return Err(invalid_attribute("ptCount", "val", declared.to_string()));
5646 }
5647 let mut values = Vec::with_capacity(self.points.len());
5648 let mut point_indexes = Vec::with_capacity(self.points.len());
5649 let mut point_markup = Vec::with_capacity(self.points.len());
5650 let mut previous = None;
5651 for (index, value, markup) in self.points {
5652 if index >= declared || previous.is_some_and(|last| index <= last) {
5653 return Err(invalid_attribute("pt", "idx", index.to_string()));
5654 }
5655 previous = Some(index);
5656 values.push(value);
5657 point_indexes.push(index);
5658 point_markup.push(markup);
5659 }
5660 let (format_code, format_markup) = self
5661 .format_code
5662 .map(|(value, markup)| (Some(value), Some(markup)))
5663 .unwrap_or((None, None));
5664 let format_code_omitted = self.numeric && format_code.is_none();
5665 Ok((
5666 format_code,
5667 values,
5668 CacheMarkup {
5669 raw_attributes,
5670 raw_children: raw_children_in_schema_order(
5671 &self.raw_children,
5672 self.point_base + point_markup.len(),
5673 ),
5674 format_code: format_markup,
5675 format_code_omitted,
5676 point_count: point_count_markup,
5677 declared_point_count: Some(declared),
5678 point_indexes,
5679 points: point_markup,
5680 },
5681 ))
5682 }
5683}
5684
5685fn parse_point(xml: &[u8], inherited: &NamespaceBindings) -> Result<(u32, String, PointMarkup)> {
5686 let mut reader = Reader::from_reader(xml);
5687 let mut buffer = Vec::new();
5688 loop {
5689 match reader
5690 .read_event_into(&mut buffer)
5691 .map_err(OxmlError::from)?
5692 {
5693 Event::Start(element) if matches_local_name(element.name().as_ref(), b"pt") => {
5694 let namespaces = typed_rewrite_bindings(&element, inherited)?;
5695 let index = required_u32_attribute(&element, "pt", b"idx")?;
5696 let raw_attributes = capture_attributes_except(&element, b"idx")?;
5697 let mut value: Option<(String, TextMarkup)> = None;
5698 let mut raw_children = OrderedRawChildren::default();
5699 let mut boundary = 0usize;
5700 let mut inner = Vec::new();
5701 loop {
5702 match reader
5703 .read_event_into(&mut inner)
5704 .map_err(OxmlError::from)?
5705 {
5706 Event::Start(child) => {
5707 let name = chart_child_local(&child, &namespaces)?;
5708 let raw = capture_element(&mut reader, &child)?;
5709 if name.as_deref() == Some(b"v") {
5710 set_once(
5711 &mut value,
5712 parse_text_element(&raw, b"v", &namespaces)?,
5713 "c:v",
5714 )?;
5715 boundary = 1;
5716 } else {
5717 raw_children.push(boundary, raw);
5718 }
5719 }
5720 Event::Empty(child) => {
5721 let name = chart_child_local(&child, &namespaces)?;
5722 let raw = capture_empty_element(&child)?;
5723 if name.as_deref() == Some(b"v") {
5724 set_once(
5725 &mut value,
5726 parse_text_element(&raw, b"v", &namespaces)?,
5727 "c:v",
5728 )?;
5729 boundary = 1;
5730 } else {
5731 raw_children.push(boundary, raw);
5732 }
5733 }
5734 event @ (Event::Text(_)
5735 | Event::CData(_)
5736 | Event::Comment(_)
5737 | Event::PI(_)
5738 | Event::GeneralRef(_)) => {
5739 raw_children.push(boundary, capture_event(event)?);
5740 }
5741 Event::End(end) if matches_local_name(end.name().as_ref(), b"pt") => {
5742 let (value, value_markup) = value
5743 .ok_or_else(|| ChartError::MissingElement("c:v".to_owned()))?;
5744 return Ok((
5745 index,
5746 value,
5747 PointMarkup {
5748 raw_attributes,
5749 raw_children: raw_children_in_schema_order(&raw_children, 1),
5750 value: value_markup,
5751 },
5752 ));
5753 }
5754 Event::Eof => return Err(missing_end("c:pt")),
5755 _ => {}
5756 }
5757 inner.clear();
5758 }
5759 }
5760 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pt") => {
5761 return Err(ChartError::MissingElement("c:v".to_owned()));
5762 }
5763 Event::Start(element) | Event::Empty(element) => {
5764 return Err(ChartError::UnexpectedElement(element_name(&element)));
5765 }
5766 Event::Eof => return Err(ChartError::MissingElement("c:pt".to_owned())),
5767 _ => {}
5768 }
5769 buffer.clear();
5770 }
5771}
5772
5773fn parse_text_element(
5774 xml: &[u8],
5775 local: &[u8],
5776 inherited: &NamespaceBindings,
5777) -> Result<(String, TextMarkup)> {
5778 let mut reader = Reader::from_reader(xml);
5779 let mut buffer = Vec::new();
5780 loop {
5781 match reader
5782 .read_event_into(&mut buffer)
5783 .map_err(OxmlError::from)?
5784 {
5785 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
5786 typed_rewrite_bindings(&element, inherited)?;
5787 let raw_attributes = capture_attributes(&element)?;
5788 let mut value = String::new();
5789 let mut raw_children = OrderedRawChildren::default();
5790 let mut seen_text = false;
5791 let mut inner = Vec::new();
5792 loop {
5793 match reader
5794 .read_event_into(&mut inner)
5795 .map_err(OxmlError::from)?
5796 {
5797 Event::Text(text) => {
5798 value.push_str(&decode_plain(&text));
5799 seen_text = true;
5800 }
5801 Event::CData(text) => {
5802 let decoded =
5803 text.decode().map_err(|error| ChartError::InvalidValue {
5804 element: format!("c:{}", String::from_utf8_lossy(local)),
5805 value: error.to_string(),
5806 })?;
5807 value.push_str(&decoded);
5808 seen_text = true;
5809 }
5810 Event::GeneralRef(reference) => {
5811 value.push_str(&resolve_entity(&reference));
5812 seen_text = true;
5813 }
5814 event @ (Event::Comment(_) | Event::PI(_)) => {
5815 raw_children.push(usize::from(seen_text), capture_event(event)?);
5816 }
5817 Event::Start(child) | Event::Empty(child) => {
5818 return Err(ChartError::UnexpectedElement(element_name(&child)));
5819 }
5820 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
5821 return Ok((
5822 value,
5823 TextMarkup {
5824 raw_attributes,
5825 raw_children: raw_children_in_schema_order(&raw_children, 1),
5826 },
5827 ));
5828 }
5829 Event::Eof => {
5830 return Err(missing_end(&format!(
5831 "c:{}",
5832 String::from_utf8_lossy(local)
5833 )));
5834 }
5835 _ => {}
5836 }
5837 inner.clear();
5838 }
5839 }
5840 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
5841 typed_rewrite_bindings(&element, inherited)?;
5842 return Ok((
5843 String::new(),
5844 TextMarkup {
5845 raw_attributes: capture_attributes(&element)?,
5846 raw_children: OrderedRawChildren::default(),
5847 },
5848 ));
5849 }
5850 Event::Start(element) | Event::Empty(element) => {
5851 return Err(ChartError::UnexpectedElement(element_name(&element)));
5852 }
5853 Event::Eof => {
5854 return Err(ChartError::MissingElement(format!(
5855 "c:{}",
5856 String::from_utf8_lossy(local)
5857 )));
5858 }
5859 _ => {}
5860 }
5861 buffer.clear();
5862 }
5863}
5864
5865struct ParsedWrapper {
5866 choice: Option<(Vec<u8>, Vec<u8>)>,
5867 markup: WrapperMarkup,
5868 namespaces: NamespaceBindings,
5869}
5870
5871fn parse_wrapper(
5872 xml: &[u8],
5873 local: &[u8],
5874 choices: &[&[u8]],
5875 inherited: &NamespaceBindings,
5876) -> Result<ParsedWrapper> {
5877 let mut reader = Reader::from_reader(xml);
5878 let mut buffer = Vec::new();
5879 loop {
5880 match reader
5881 .read_event_into(&mut buffer)
5882 .map_err(OxmlError::from)?
5883 {
5884 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
5885 let namespaces = typed_rewrite_bindings(&element, inherited)?;
5886 let raw_attributes = capture_attributes(&element)?;
5887 let mut choice = None;
5888 let mut raw_children = OrderedRawChildren::default();
5889 let mut boundary = 0usize;
5890 let mut inner = Vec::new();
5891 loop {
5892 match reader
5893 .read_event_into(&mut inner)
5894 .map_err(OxmlError::from)?
5895 {
5896 Event::Start(child) => {
5897 let name = chart_child_local(&child, &namespaces)?;
5898 let raw = capture_element(&mut reader, &child)?;
5899 if let Some(name) =
5900 name.filter(|name| choices.contains(&name.as_slice()))
5901 {
5902 if choice.is_some() {
5903 return Err(ChartError::DuplicateElement(format!(
5904 "c:{} reference",
5905 String::from_utf8_lossy(local)
5906 )));
5907 }
5908 choice = Some((name, raw));
5909 boundary = 1;
5910 } else {
5911 raw_children.push(boundary, raw);
5912 }
5913 }
5914 Event::Empty(child) => {
5915 let name = chart_child_local(&child, &namespaces)?;
5916 let raw = capture_empty_element(&child)?;
5917 if let Some(name) =
5918 name.filter(|name| choices.contains(&name.as_slice()))
5919 {
5920 if choice.is_some() {
5921 return Err(ChartError::DuplicateElement(format!(
5922 "c:{} reference",
5923 String::from_utf8_lossy(local)
5924 )));
5925 }
5926 choice = Some((name, raw));
5927 boundary = 1;
5928 } else {
5929 raw_children.push(boundary, raw);
5930 }
5931 }
5932 event @ (Event::Text(_)
5933 | Event::CData(_)
5934 | Event::Comment(_)
5935 | Event::PI(_)
5936 | Event::GeneralRef(_)) => {
5937 raw_children.push(boundary, capture_event(event)?);
5938 }
5939 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
5940 return Ok(ParsedWrapper {
5941 choice,
5942 markup: WrapperMarkup {
5943 raw_attributes,
5944 raw_children: raw_children_in_schema_order(&raw_children, 1),
5945 },
5946 namespaces,
5947 });
5948 }
5949 Event::Eof => {
5950 return Err(missing_end(&format!(
5951 "c:{}",
5952 String::from_utf8_lossy(local)
5953 )));
5954 }
5955 _ => {}
5956 }
5957 inner.clear();
5958 }
5959 }
5960 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
5961 let namespaces = chart_bindings(inherited, &element)?;
5962 return Ok(ParsedWrapper {
5963 choice: None,
5964 markup: WrapperMarkup {
5965 raw_attributes: capture_attributes(&element)?,
5966 raw_children: OrderedRawChildren::default(),
5967 },
5968 namespaces,
5969 });
5970 }
5971 Event::Start(element) | Event::Empty(element) => {
5972 return Err(ChartError::UnexpectedElement(element_name(&element)));
5973 }
5974 Event::Eof => {
5975 return Err(ChartError::MissingElement(format!(
5976 "c:{}",
5977 String::from_utf8_lossy(local)
5978 )));
5979 }
5980 _ => {}
5981 }
5982 buffer.clear();
5983 }
5984}
5985
5986fn write_string_cache(
5987 writer: &mut Writer<Vec<u8>>,
5988 values: &[String],
5989 markup: &ReferenceMarkup,
5990) -> Result<()> {
5991 let (declared_count, indexes) = cache_layout(markup, values.len())?;
5992 let mut start = BytesStart::new("c:strCache");
5993 push_attributes(&mut start, &markup.cache_attributes);
5994 writer
5995 .write_event(Event::Start(start))
5996 .map_err(OxmlError::from)?;
5997 emit_raw(writer, markup.cache_children.at(0))?;
5998 write_scalar(
5999 writer,
6000 "c:ptCount",
6001 &declared_count.to_string(),
6002 Some(&markup.point_count),
6003 )?;
6004 emit_cache_point_raw(writer, markup, 1, values.len(), 0)?;
6005 for (position, value) in values.iter().enumerate() {
6006 let default_markup = PointMarkup::default();
6007 write_point(
6008 writer,
6009 indexes[position],
6010 value,
6011 markup.points.get(position).unwrap_or(&default_markup),
6012 )?;
6013 emit_cache_point_raw(writer, markup, 1, values.len(), position + 1)?;
6014 }
6015 writer
6016 .write_event(Event::End(BytesEnd::new("c:strCache")))
6017 .map_err(OxmlError::from)?;
6018 Ok(())
6019}
6020
6021fn write_numeric_cache(
6022 writer: &mut Writer<Vec<u8>>,
6023 data: &NumericData,
6024 markup: &ReferenceMarkup,
6025) -> Result<()> {
6026 let (declared_count, indexes) = cache_layout(markup, data.values.len())?;
6027 let mut start = BytesStart::new("c:numCache");
6028 push_attributes(&mut start, &markup.cache_attributes);
6029 writer
6030 .write_event(Event::Start(start))
6031 .map_err(OxmlError::from)?;
6032 emit_raw(writer, markup.cache_children.at(0))?;
6033 if !markup.format_code_omitted || data.format_code != "General" {
6034 let default_format_markup = TextMarkup::default();
6035 write_text(
6036 writer,
6037 "c:formatCode",
6038 &data.format_code,
6039 markup
6040 .format_code
6041 .as_ref()
6042 .unwrap_or(&default_format_markup),
6043 )?;
6044 }
6045 emit_raw(writer, markup.cache_children.at(1))?;
6046 write_scalar(
6047 writer,
6048 "c:ptCount",
6049 &declared_count.to_string(),
6050 Some(&markup.point_count),
6051 )?;
6052 emit_cache_point_raw(writer, markup, 2, data.values.len(), 0)?;
6053 for (position, value) in data.values.iter().enumerate() {
6054 let default_markup = PointMarkup::default();
6055 write_point(
6056 writer,
6057 indexes[position],
6058 &value.to_string(),
6059 markup.points.get(position).unwrap_or(&default_markup),
6060 )?;
6061 emit_cache_point_raw(writer, markup, 2, data.values.len(), position + 1)?;
6062 }
6063 writer
6064 .write_event(Event::End(BytesEnd::new("c:numCache")))
6065 .map_err(OxmlError::from)?;
6066 Ok(())
6067}
6068
6069fn emit_cache_point_raw(
6070 writer: &mut Writer<Vec<u8>>,
6071 markup: &ReferenceMarkup,
6072 point_base: usize,
6073 current_count: usize,
6074 completed_points: usize,
6075) -> Result<()> {
6076 let original_count = markup.points.len();
6077 if current_count == 0 {
6078 for boundary in point_base..=point_base + original_count {
6079 emit_raw(writer, markup.cache_children.at(boundary))?;
6080 }
6081 } else if completed_points == 0 {
6082 if original_count > 0 {
6083 emit_raw(writer, markup.cache_children.at(point_base))?;
6084 }
6085 } else if completed_points < current_count {
6086 if completed_points < original_count {
6087 emit_raw(
6088 writer,
6089 markup.cache_children.at(point_base + completed_points),
6090 )?;
6091 }
6092 } else {
6093 let first_tail = if original_count == 0 {
6094 point_base
6095 } else {
6096 point_base + current_count.min(original_count)
6097 };
6098 for boundary in first_tail..=point_base + original_count {
6099 emit_raw(writer, markup.cache_children.at(boundary))?;
6100 }
6101 }
6102 Ok(())
6103}
6104
6105fn write_point(
6106 writer: &mut Writer<Vec<u8>>,
6107 index: u32,
6108 value: &str,
6109 markup: &PointMarkup,
6110) -> Result<()> {
6111 let index = index.to_string();
6112 let mut start = BytesStart::new("c:pt");
6113 start.push_attribute(("idx", index.as_str()));
6114 push_attributes(&mut start, &markup.raw_attributes);
6115 writer
6116 .write_event(Event::Start(start))
6117 .map_err(OxmlError::from)?;
6118 emit_raw(writer, markup.raw_children.at(0))?;
6119 write_text(writer, "c:v", value, &markup.value)?;
6120 emit_raw(writer, markup.raw_children.at(1))?;
6121 writer
6122 .write_event(Event::End(BytesEnd::new("c:pt")))
6123 .map_err(OxmlError::from)?;
6124 Ok(())
6125}
6126
6127fn cache_layout(markup: &ReferenceMarkup, value_count: usize) -> Result<(u32, Vec<u32>)> {
6128 if markup.point_indexes.len() == value_count
6129 && let Some(declared) = markup.declared_point_count
6130 {
6131 return Ok((declared, markup.point_indexes.clone()));
6132 }
6133 let declared = u32::try_from(value_count).map_err(|_| ChartError::InvalidValue {
6134 element: "c:ptCount".to_owned(),
6135 value: value_count.to_string(),
6136 })?;
6137 Ok((declared, (0..declared).collect()))
6138}
6139
6140fn write_text(
6141 writer: &mut Writer<Vec<u8>>,
6142 tag: &str,
6143 value: &str,
6144 markup: &TextMarkup,
6145) -> Result<()> {
6146 let mut start = BytesStart::new(tag);
6147 push_attributes(&mut start, &markup.raw_attributes);
6148 writer
6149 .write_event(Event::Start(start))
6150 .map_err(OxmlError::from)?;
6151 emit_raw(writer, markup.raw_children.at(0))?;
6152 writer
6153 .write_event(Event::Text(BytesText::new(value)))
6154 .map_err(OxmlError::from)?;
6155 emit_raw(writer, markup.raw_children.at(1))?;
6156 writer
6157 .write_event(Event::End(BytesEnd::new(tag)))
6158 .map_err(OxmlError::from)?;
6159 Ok(())
6160}
6161
6162fn write_wrapper_start(
6163 writer: &mut Writer<Vec<u8>>,
6164 tag: &str,
6165 markup: &WrapperMarkup,
6166) -> Result<()> {
6167 let mut start = BytesStart::new(tag);
6168 push_attributes(&mut start, &markup.raw_attributes);
6169 writer
6170 .write_event(Event::Start(start))
6171 .map_err(OxmlError::from)?;
6172 emit_raw(writer, markup.raw_children.at(0))
6173}
6174
6175fn write_wrapper_end(
6176 writer: &mut Writer<Vec<u8>>,
6177 tag: &str,
6178 markup: &WrapperMarkup,
6179) -> Result<()> {
6180 emit_raw(writer, markup.raw_children.at(1))?;
6181 writer
6182 .write_event(Event::End(BytesEnd::new(tag)))
6183 .map_err(OxmlError::from)?;
6184 Ok(())
6185}
6186
6187fn validate_formula(formula: &str, element: &str) -> Result<()> {
6188 if formula.trim().is_empty() {
6189 return Err(ChartError::InvalidValue {
6190 element: element.to_owned(),
6191 value: formula.to_owned(),
6192 });
6193 }
6194 validate_xml_text(formula, element)
6195}
6196
6197fn validate_format_code(format_code: &str) -> Result<()> {
6198 if format_code.is_empty() {
6199 return Err(ChartError::InvalidValue {
6200 element: "c:formatCode".to_owned(),
6201 value: format_code.to_owned(),
6202 });
6203 }
6204 validate_xml_text(format_code, "c:formatCode")
6205}
6206
6207fn validate_xml_text(value: &str, element: &str) -> Result<()> {
6208 if value.chars().all(|character| {
6209 matches!(
6210 character,
6211 '\u{9}' | '\u{A}' | '\u{D}' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}'
6212 )
6213 }) {
6214 return Ok(());
6215 }
6216 Err(ChartError::InvalidValue {
6217 element: element.to_owned(),
6218 value: value.to_owned(),
6219 })
6220}
6221
6222fn validate_numeric_values(values: &[f64]) -> Result<()> {
6223 if let Some(value) = values.iter().find(|value| !value.is_finite()) {
6224 return Err(ChartError::InvalidValue {
6225 element: "c:v".to_owned(),
6226 value: value.to_string(),
6227 });
6228 }
6229 Ok(())
6230}
6231
6232fn validate_point_count(count: usize, element: &str) -> Result<()> {
6233 u32::try_from(count)
6234 .map(|_| ())
6235 .map_err(|_| ChartError::InvalidValue {
6236 element: element.to_owned(),
6237 value: count.to_string(),
6238 })
6239}
6240
6241fn parse_u32_scalar(xml: &[u8], local: &str) -> Result<(u32, ScalarMarkup)> {
6242 let (value, markup) = scalar_value(xml, local)?;
6243 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
6244 let parsed = value
6245 .parse::<u32>()
6246 .map_err(|_| invalid_attribute(local, "val", value))?;
6247 Ok((parsed, markup))
6248}
6249
6250fn required_u32_attribute(element: &BytesStart<'_>, local: &str, attribute: &[u8]) -> Result<u32> {
6251 let value = attribute_value(element, attribute)?.ok_or_else(|| {
6252 invalid_attribute(
6253 local,
6254 &String::from_utf8_lossy(attribute),
6255 "<missing>".to_owned(),
6256 )
6257 })?;
6258 value
6259 .parse::<u32>()
6260 .map_err(|_| invalid_attribute(local, &String::from_utf8_lossy(attribute), value))
6261}
6262
6263fn chart_root_prefix(element: &BytesStart<'_>) -> Result<Vec<u8>> {
6264 let name = element.name();
6265 let qualified = name.as_ref();
6266 let prefix = qualified
6267 .iter()
6268 .position(|byte| *byte == b':')
6269 .map(|position| qualified[..position].to_vec())
6270 .unwrap_or_default();
6271 let declaration = if prefix.is_empty() {
6272 b"xmlns".to_vec()
6273 } else {
6274 let mut declaration = b"xmlns:".to_vec();
6275 declaration.extend_from_slice(&prefix);
6276 declaration
6277 };
6278 if let Some(namespace) = attribute_value(element, &declaration)?
6279 && namespace != C_NS
6280 {
6281 return Err(invalid_attribute(
6282 &element_name(element),
6283 "namespace",
6284 namespace,
6285 ));
6286 }
6287 reject_conflicting_prefix(element, b"c", C_NS)?;
6288 Ok(prefix)
6289}
6290
6291fn typed_rewrite_bindings(
6292 element: &BytesStart<'_>,
6293 inherited: &NamespaceBindings,
6294) -> Result<NamespaceBindings> {
6295 chart_root_prefix(element)?;
6296 if !element_is_in_namespace(element, C_NS, inherited)? {
6297 return Err(ChartError::UnexpectedElement(element_name(element)));
6298 }
6299 let bindings = chart_bindings(inherited, element)?;
6300 require_fixed_namespace(&bindings, b"c", C_NS, element)?;
6301 Ok(bindings)
6302}
6303
6304fn chart_namespace_defaults() -> NamespaceBindings {
6305 vec![
6306 (b"c".to_vec(), C_NS.to_owned()),
6307 (b"a".to_vec(), A_NS.to_owned()),
6308 (b"r".to_vec(), R_NS.to_owned()),
6309 ]
6310}
6311
6312fn chart_bindings(
6313 inherited: &NamespaceBindings,
6314 element: &BytesStart<'_>,
6315) -> Result<NamespaceBindings> {
6316 let mut bindings = chart_namespace_defaults();
6317 for (prefix, namespace) in inherited
6318 .iter()
6319 .cloned()
6320 .chain(namespace_bindings(element)?)
6321 {
6322 upsert_namespace_binding(&mut bindings, prefix, namespace);
6323 }
6324 Ok(bindings)
6325}
6326
6327fn root_chart_bindings(
6328 xml: &[u8],
6329 local: &[u8],
6330 inherited: &NamespaceBindings,
6331) -> Result<NamespaceBindings> {
6332 let mut reader = Reader::from_reader(xml);
6333 let mut buffer = Vec::new();
6334 loop {
6335 match reader
6336 .read_event_into(&mut buffer)
6337 .map_err(OxmlError::from)?
6338 {
6339 Event::Start(element) | Event::Empty(element)
6340 if matches_local_name(element.name().as_ref(), local) =>
6341 {
6342 if !element_is_in_namespace(&element, C_NS, inherited)? {
6343 return Err(ChartError::UnexpectedElement(element_name(&element)));
6344 }
6345 return chart_bindings(inherited, &element);
6346 }
6347 Event::Start(element) | Event::Empty(element) => {
6348 return Err(ChartError::UnexpectedElement(element_name(&element)));
6349 }
6350 Event::Eof => {
6351 return Err(ChartError::MissingElement(format!(
6352 "c:{}",
6353 String::from_utf8_lossy(local)
6354 )));
6355 }
6356 _ => {}
6357 }
6358 buffer.clear();
6359 }
6360}
6361
6362fn chart_child_local(
6363 element: &BytesStart<'_>,
6364 inherited: &NamespaceBindings,
6365) -> Result<Option<Vec<u8>>> {
6366 Ok(element_is_in_namespace(element, C_NS, inherited)?
6367 .then(|| local_name(element.name().as_ref()).to_vec()))
6368}
6369
6370fn capture_fixed_root_attributes(
6371 start: &BytesStart<'_>,
6372 fixed: &[&str],
6373) -> Result<(XmlAttributes, XmlAttributes)> {
6374 let mut attributes = Vec::new();
6375 let mut namespaces = Vec::new();
6376 for attribute in start.attributes() {
6377 let attribute = attribute.map_err(OxmlError::from)?;
6378 let name = std::str::from_utf8(attribute.key.as_ref())
6379 .map_err(OxmlError::from)?
6380 .to_owned();
6381 let value = attribute
6382 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
6383 .map_err(OxmlError::from)?
6384 .into_owned();
6385 if fixed.contains(&name.as_str()) {
6386 continue;
6387 }
6388 if name == "xmlns" || name.starts_with("xmlns:") {
6389 namespaces.push((name, value));
6390 } else {
6391 attributes.push((name, value));
6392 }
6393 }
6394 Ok((attributes, namespaces))
6395}
6396
6397fn require_fixed_namespace(
6398 bindings: &NamespaceBindings,
6399 prefix: &[u8],
6400 expected: &str,
6401 element: &BytesStart<'_>,
6402) -> Result<()> {
6403 let actual = bindings
6404 .iter()
6405 .find(|(candidate, _)| candidate.as_slice() == prefix)
6406 .map(|(_, namespace)| namespace.as_str());
6407 if actual == Some(expected) {
6408 return Ok(());
6409 }
6410 Err(invalid_attribute(
6411 &element_name(element),
6412 &format!("xmlns:{}", String::from_utf8_lossy(prefix)),
6413 actual.unwrap_or("<missing>").to_owned(),
6414 ))
6415}
6416
6417fn standalone_namespace_declarations(bindings: &NamespaceBindings) -> Result<XmlAttributes> {
6418 bindings
6419 .iter()
6420 .filter(|(prefix, _)| prefix != b"c" && prefix != b"a" && prefix != b"r")
6421 .map(|(prefix, namespace)| {
6422 let prefix = std::str::from_utf8(prefix).map_err(OxmlError::from)?;
6423 let name = if prefix.is_empty() {
6424 "xmlns".to_owned()
6425 } else {
6426 format!("xmlns:{prefix}")
6427 };
6428 Ok((name, namespace.clone()))
6429 })
6430 .collect()
6431}
6432
6433fn capture_fixed_attributes(start: &BytesStart<'_>, fixed: &[&str]) -> Result<XmlAttributes> {
6434 let mut attributes = Vec::new();
6435 for attribute in start.attributes() {
6436 let attribute = attribute.map_err(OxmlError::from)?;
6437 let name = std::str::from_utf8(attribute.key.as_ref())
6438 .map_err(OxmlError::from)?
6439 .to_owned();
6440 if fixed.contains(&name.as_str()) {
6441 continue;
6442 }
6443 let value = attribute
6444 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
6445 .map_err(OxmlError::from)?
6446 .into_owned();
6447 attributes.push((name, value));
6448 }
6449 Ok(attributes)
6450}
6451
6452#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
6457pub struct AxisId(i64);
6458
6459impl AxisId {
6460 pub fn new(value: i64) -> Result<Self> {
6461 if (i64::from(i32::MIN)..=i64::from(u32::MAX)).contains(&value) {
6462 Ok(Self(value))
6463 } else {
6464 Err(ChartError::InvalidValue {
6465 element: "c:axId".to_owned(),
6466 value: value.to_string(),
6467 })
6468 }
6469 }
6470
6471 pub const fn value(self) -> i64 {
6472 self.0
6473 }
6474}
6475
6476#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6478pub enum BarDirection {
6479 Bar,
6480 Column,
6481}
6482
6483impl BarDirection {
6484 fn parse(value: &str) -> Option<Self> {
6485 match value {
6486 "bar" => Some(Self::Bar),
6487 "col" => Some(Self::Column),
6488 _ => None,
6489 }
6490 }
6491
6492 const fn as_str(self) -> &'static str {
6493 match self {
6494 Self::Bar => "bar",
6495 Self::Column => "col",
6496 }
6497 }
6498}
6499
6500#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6502pub enum BarGrouping {
6503 Clustered,
6504 PercentStacked,
6505 Stacked,
6506 Standard,
6507}
6508
6509impl BarGrouping {
6510 fn parse(value: &str) -> Option<Self> {
6511 match value {
6512 "clustered" => Some(Self::Clustered),
6513 "percentStacked" => Some(Self::PercentStacked),
6514 "stacked" => Some(Self::Stacked),
6515 "standard" => Some(Self::Standard),
6516 _ => None,
6517 }
6518 }
6519
6520 const fn as_str(self) -> &'static str {
6521 match self {
6522 Self::Clustered => "clustered",
6523 Self::PercentStacked => "percentStacked",
6524 Self::Stacked => "stacked",
6525 Self::Standard => "standard",
6526 }
6527 }
6528}
6529
6530#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6532pub enum Grouping {
6533 PercentStacked,
6534 Stacked,
6535 Standard,
6536}
6537
6538impl Grouping {
6539 fn parse(value: &str) -> Option<Self> {
6540 match value {
6541 "percentStacked" => Some(Self::PercentStacked),
6542 "stacked" => Some(Self::Stacked),
6543 "standard" => Some(Self::Standard),
6544 _ => None,
6545 }
6546 }
6547
6548 const fn as_str(self) -> &'static str {
6549 match self {
6550 Self::PercentStacked => "percentStacked",
6551 Self::Stacked => "stacked",
6552 Self::Standard => "standard",
6553 }
6554 }
6555}
6556
6557#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6559pub enum ScatterStyle {
6560 Line,
6561 LineMarker,
6562 Marker,
6563 None,
6564 Smooth,
6565 SmoothMarker,
6566}
6567
6568impl ScatterStyle {
6569 fn parse(value: &str) -> Option<Self> {
6570 match value {
6571 "line" => Some(Self::Line),
6572 "lineMarker" => Some(Self::LineMarker),
6573 "marker" => Some(Self::Marker),
6574 "none" => Some(Self::None),
6575 "smooth" => Some(Self::Smooth),
6576 "smoothMarker" => Some(Self::SmoothMarker),
6577 _ => None,
6578 }
6579 }
6580
6581 const fn as_str(self) -> &'static str {
6582 match self {
6583 Self::Line => "line",
6584 Self::LineMarker => "lineMarker",
6585 Self::Marker => "marker",
6586 Self::None => "none",
6587 Self::Smooth => "smooth",
6588 Self::SmoothMarker => "smoothMarker",
6589 }
6590 }
6591}
6592
6593#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6595pub enum RadarStyle {
6596 Filled,
6597 Marker,
6598 Standard,
6599}
6600
6601impl RadarStyle {
6602 fn parse(value: &str) -> Option<Self> {
6603 match value {
6604 "filled" => Some(Self::Filled),
6605 "marker" => Some(Self::Marker),
6606 "standard" => Some(Self::Standard),
6607 _ => None,
6608 }
6609 }
6610
6611 const fn as_str(self) -> &'static str {
6612 match self {
6613 Self::Filled => "filled",
6614 Self::Marker => "marker",
6615 Self::Standard => "standard",
6616 }
6617 }
6618}
6619
6620#[derive(Clone, Debug, PartialEq)]
6622pub enum Plot {
6623 Bar {
6624 direction: BarDirection,
6625 grouping: BarGrouping,
6626 gap_width: u16,
6627 overlap: i8,
6628 series: Vec<Series>,
6629 data_labels: Option<CT_DLbls>,
6630 axis_ids: [AxisId; 2],
6631 },
6632 Line {
6633 grouping: Grouping,
6634 marker: bool,
6635 smooth: bool,
6636 series: Vec<Series>,
6637 data_labels: Option<CT_DLbls>,
6638 axis_ids: [AxisId; 2],
6639 },
6640 Pie {
6641 first_slice_angle: u16,
6642 series: Vec<Series>,
6643 data_labels: Option<CT_DLbls>,
6644 },
6645 Doughnut {
6646 first_slice_angle: u16,
6647 hole_size: u8,
6648 series: Vec<Series>,
6649 data_labels: Option<CT_DLbls>,
6650 },
6651 Area {
6652 grouping: Grouping,
6653 series: Vec<Series>,
6654 data_labels: Option<CT_DLbls>,
6655 axis_ids: [AxisId; 2],
6656 },
6657 Scatter {
6658 style: ScatterStyle,
6659 series: Vec<Series>,
6660 data_labels: Option<CT_DLbls>,
6661 axis_ids: [AxisId; 2],
6662 },
6663 Radar {
6664 style: RadarStyle,
6665 series: Vec<Series>,
6666 data_labels: Option<CT_DLbls>,
6667 axis_ids: [AxisId; 2],
6668 },
6669}
6670
6671impl Plot {
6672 pub fn bar(
6673 direction: BarDirection,
6674 grouping: BarGrouping,
6675 series: Vec<Series>,
6676 axis_ids: [AxisId; 2],
6677 ) -> Result<Self> {
6678 let plot = Self::Bar {
6679 direction,
6680 grouping,
6681 gap_width: 150,
6682 overlap: 0,
6683 series,
6684 data_labels: None,
6685 axis_ids,
6686 };
6687 plot.validate()?;
6688 Ok(plot)
6689 }
6690
6691 pub fn line(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
6692 let plot = Self::Line {
6693 grouping,
6694 marker: false,
6695 smooth: false,
6696 series,
6697 data_labels: None,
6698 axis_ids,
6699 };
6700 plot.validate()?;
6701 Ok(plot)
6702 }
6703
6704 pub fn pie(series: Vec<Series>) -> Result<Self> {
6705 let plot = Self::Pie {
6706 first_slice_angle: 0,
6707 series,
6708 data_labels: None,
6709 };
6710 plot.validate()?;
6711 Ok(plot)
6712 }
6713
6714 pub fn doughnut(series: Vec<Series>) -> Result<Self> {
6715 let plot = Self::Doughnut {
6716 first_slice_angle: 0,
6717 hole_size: 50,
6718 series,
6719 data_labels: None,
6720 };
6721 plot.validate()?;
6722 Ok(plot)
6723 }
6724
6725 pub fn area(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
6726 let plot = Self::Area {
6727 grouping,
6728 series,
6729 data_labels: None,
6730 axis_ids,
6731 };
6732 plot.validate()?;
6733 Ok(plot)
6734 }
6735
6736 pub fn scatter(
6737 style: ScatterStyle,
6738 series: Vec<Series>,
6739 axis_ids: [AxisId; 2],
6740 ) -> Result<Self> {
6741 let plot = Self::Scatter {
6742 style,
6743 series,
6744 data_labels: None,
6745 axis_ids,
6746 };
6747 plot.validate()?;
6748 Ok(plot)
6749 }
6750
6751 pub fn radar(style: RadarStyle, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
6752 let plot = Self::Radar {
6753 style,
6754 series,
6755 data_labels: None,
6756 axis_ids,
6757 };
6758 plot.validate()?;
6759 Ok(plot)
6760 }
6761
6762 fn axis_ids(&self) -> Option<[AxisId; 2]> {
6763 match self {
6764 Self::Bar { axis_ids, .. }
6765 | Self::Line { axis_ids, .. }
6766 | Self::Area { axis_ids, .. }
6767 | Self::Scatter { axis_ids, .. }
6768 | Self::Radar { axis_ids, .. } => Some(*axis_ids),
6769 Self::Pie { .. } | Self::Doughnut { .. } => None,
6770 }
6771 }
6772
6773 fn validate(&self) -> Result<()> {
6774 let (series, labels, axis_ids) = match self {
6775 Self::Bar {
6776 gap_width,
6777 overlap,
6778 series,
6779 data_labels,
6780 axis_ids,
6781 ..
6782 } => {
6783 if *gap_width > 500 {
6784 return Err(ChartError::InvalidValue {
6785 element: "c:gapWidth".to_owned(),
6786 value: gap_width.to_string(),
6787 });
6788 }
6789 if !(-100..=100).contains(overlap) {
6790 return Err(ChartError::InvalidValue {
6791 element: "c:overlap".to_owned(),
6792 value: overlap.to_string(),
6793 });
6794 }
6795 (series, data_labels, Some(axis_ids))
6796 }
6797 Self::Line {
6798 series,
6799 data_labels,
6800 axis_ids,
6801 ..
6802 } => (series, data_labels, Some(axis_ids)),
6803 Self::Pie {
6804 first_slice_angle,
6805 series,
6806 data_labels,
6807 } => {
6808 if *first_slice_angle > 360 {
6809 return Err(ChartError::InvalidValue {
6810 element: "c:firstSliceAng".to_owned(),
6811 value: first_slice_angle.to_string(),
6812 });
6813 }
6814 (series, data_labels, None)
6815 }
6816 Self::Doughnut {
6817 first_slice_angle,
6818 hole_size,
6819 series,
6820 data_labels,
6821 } => {
6822 if *first_slice_angle > 360 {
6823 return Err(ChartError::InvalidValue {
6824 element: "c:firstSliceAng".to_owned(),
6825 value: first_slice_angle.to_string(),
6826 });
6827 }
6828 if !(10..=90).contains(hole_size) {
6829 return Err(ChartError::InvalidValue {
6830 element: "c:holeSize".to_owned(),
6831 value: hole_size.to_string(),
6832 });
6833 }
6834 (series, data_labels, None)
6835 }
6836 Self::Area {
6837 series,
6838 data_labels,
6839 axis_ids,
6840 ..
6841 }
6842 | Self::Radar {
6843 series,
6844 data_labels,
6845 axis_ids,
6846 ..
6847 } => (series, data_labels, Some(axis_ids)),
6848 Self::Scatter {
6849 series,
6850 data_labels,
6851 axis_ids,
6852 ..
6853 } => {
6854 for item in series {
6855 if !matches!(item.categories, Some(AxisData::Numeric(_))) {
6856 return Err(ChartError::InvalidValue {
6857 element: "c:xVal".to_owned(),
6858 value: "scatter series requires numeric x values".to_owned(),
6859 });
6860 }
6861 }
6862 (series, data_labels, Some(axis_ids))
6863 }
6864 };
6865 if series.is_empty() {
6866 return Err(ChartError::MissingElement("c:ser".to_owned()));
6867 }
6868 let scatter_plot = matches!(self, Self::Scatter { .. });
6869 for item in series {
6870 if item.bubble_size.is_some() || item.opaque_bubble_size {
6871 return Err(ChartError::InvalidValue {
6872 element: "c:bubbleSize".to_owned(),
6873 value: "bubble size is only valid in an opaque bubble plot".to_owned(),
6874 });
6875 }
6876 if item.uses_scatter_wrappers && !scatter_plot {
6877 return Err(ChartError::InvalidValue {
6878 element: "c:ser".to_owned(),
6879 value: "x/y wrappers are only valid in scatter plots".to_owned(),
6880 });
6881 }
6882 if scatter_plot && item.parsed_from_xml && !item.uses_scatter_wrappers {
6883 return Err(ChartError::InvalidValue {
6884 element: "c:ser".to_owned(),
6885 value: "scatter plots require xVal/yVal wrappers".to_owned(),
6886 });
6887 }
6888 }
6889 if let Some(axis_ids) = axis_ids {
6890 if axis_ids[0] == axis_ids[1] {
6891 return Err(ChartError::DuplicateElement(format!(
6892 "c:axId {}",
6893 axis_ids[0].value()
6894 )));
6895 }
6896 for id in axis_ids {
6897 AxisId::new(id.value())?;
6898 }
6899 }
6900 if let Some(labels) = labels {
6901 labels.validate()?;
6902 }
6903 for item in series {
6904 item.values.validate()?;
6905 }
6906 Ok(())
6907 }
6908}
6909
6910#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
6912pub enum AxisKind {
6913 Category,
6914 Value,
6915 Date,
6916 Series,
6917}
6918
6919impl AxisKind {
6920 fn parse(local: &[u8]) -> Option<Self> {
6921 match local {
6922 b"catAx" => Some(Self::Category),
6923 b"valAx" => Some(Self::Value),
6924 b"dateAx" => Some(Self::Date),
6925 b"serAx" => Some(Self::Series),
6926 _ => None,
6927 }
6928 }
6929
6930 const fn local(self) -> &'static str {
6931 match self {
6932 Self::Category => "catAx",
6933 Self::Value => "valAx",
6934 Self::Date => "dateAx",
6935 Self::Series => "serAx",
6936 }
6937 }
6938}
6939
6940#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6942pub enum Orientation {
6943 #[default]
6944 MinMax,
6945 MaxMin,
6946}
6947
6948impl Orientation {
6949 fn parse(value: &str) -> Option<Self> {
6950 match value {
6951 "minMax" => Some(Self::MinMax),
6952 "maxMin" => Some(Self::MaxMin),
6953 _ => None,
6954 }
6955 }
6956
6957 const fn as_str(self) -> &'static str {
6958 match self {
6959 Self::MinMax => "minMax",
6960 Self::MaxMin => "maxMin",
6961 }
6962 }
6963}
6964
6965#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6967pub enum AxisPosition {
6968 Bottom,
6969 Left,
6970 Right,
6971 Top,
6972}
6973
6974impl AxisPosition {
6975 fn parse(value: &str) -> Option<Self> {
6976 match value {
6977 "b" => Some(Self::Bottom),
6978 "l" => Some(Self::Left),
6979 "r" => Some(Self::Right),
6980 "t" => Some(Self::Top),
6981 _ => None,
6982 }
6983 }
6984
6985 const fn as_str(self) -> &'static str {
6986 match self {
6987 Self::Bottom => "b",
6988 Self::Left => "l",
6989 Self::Right => "r",
6990 Self::Top => "t",
6991 }
6992 }
6993}
6994
6995#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6997pub enum TickMark {
6998 Cross,
6999 Inside,
7000 #[default]
7001 None,
7002 Outside,
7003}
7004
7005impl TickMark {
7006 fn parse(value: &str) -> Option<Self> {
7007 match value {
7008 "cross" => Some(Self::Cross),
7009 "in" => Some(Self::Inside),
7010 "none" => Some(Self::None),
7011 "out" => Some(Self::Outside),
7012 _ => None,
7013 }
7014 }
7015
7016 const fn as_str(self) -> &'static str {
7017 match self {
7018 Self::Cross => "cross",
7019 Self::Inside => "in",
7020 Self::None => "none",
7021 Self::Outside => "out",
7022 }
7023 }
7024}
7025
7026#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
7028pub enum TickLabelPosition {
7029 High,
7030 Low,
7031 #[default]
7032 NextTo,
7033 None,
7034}
7035
7036impl TickLabelPosition {
7037 fn parse(value: &str) -> Option<Self> {
7038 match value {
7039 "high" => Some(Self::High),
7040 "low" => Some(Self::Low),
7041 "nextTo" => Some(Self::NextTo),
7042 "none" => Some(Self::None),
7043 _ => None,
7044 }
7045 }
7046
7047 const fn as_str(self) -> &'static str {
7048 match self {
7049 Self::High => "high",
7050 Self::Low => "low",
7051 Self::NextTo => "nextTo",
7052 Self::None => "none",
7053 }
7054 }
7055}
7056
7057#[derive(Clone, Debug)]
7059pub struct Scaling {
7060 pub log_base: Option<f64>,
7061 pub orientation: Orientation,
7062 pub maximum: Option<f64>,
7063 pub minimum: Option<f64>,
7064 log_base_markup: Option<ScalarMarkup>,
7065 orientation_markup: Option<ScalarMarkup>,
7066 maximum_markup: Option<ScalarMarkup>,
7067 minimum_markup: Option<ScalarMarkup>,
7068 raw_attributes: XmlAttributes,
7069 raw_children: OrderedRawChildren,
7070}
7071
7072impl PartialEq for Scaling {
7073 fn eq(&self, other: &Self) -> bool {
7074 self.log_base == other.log_base
7075 && self.orientation == other.orientation
7076 && self.maximum == other.maximum
7077 && self.minimum == other.minimum
7078 && optional_scalar_markup_eq(&self.log_base_markup, &other.log_base_markup)
7079 && optional_scalar_markup_eq(&self.orientation_markup, &other.orientation_markup)
7080 && optional_scalar_markup_eq(&self.maximum_markup, &other.maximum_markup)
7081 && optional_scalar_markup_eq(&self.minimum_markup, &other.minimum_markup)
7082 && self.raw_attributes == other.raw_attributes
7083 && self.raw_children == other.raw_children
7084 }
7085}
7086
7087impl Default for Scaling {
7088 fn default() -> Self {
7089 Self {
7090 log_base: None,
7091 orientation: Orientation::MinMax,
7092 maximum: None,
7093 minimum: None,
7094 log_base_markup: None,
7095 orientation_markup: None,
7096 maximum_markup: None,
7097 minimum_markup: None,
7098 raw_attributes: Vec::new(),
7099 raw_children: OrderedRawChildren::default(),
7100 }
7101 }
7102}
7103
7104#[derive(Clone, Debug, Default, PartialEq)]
7106pub struct ChartLines {
7107 pub sp_pr: Option<CT_ShapeProperties>,
7108 raw_attributes: XmlAttributes,
7109 raw_children: OrderedRawChildren,
7110}
7111
7112#[derive(Clone, Debug, Eq, PartialEq)]
7114pub struct NumberFormat {
7115 pub format_code: String,
7116 pub source_linked: bool,
7117 raw_attributes: XmlAttributes,
7118 raw_content: Vec<Vec<u8>>,
7119}
7120
7121#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7123pub enum DataLabelPosition {
7124 BestFit,
7125 Bottom,
7126 Center,
7127 InsideBase,
7128 InsideEnd,
7129 Left,
7130 OutsideEnd,
7131 Right,
7132 Top,
7133}
7134
7135impl DataLabelPosition {
7136 fn parse(value: &str) -> Option<Self> {
7137 match value {
7138 "bestFit" => Some(Self::BestFit),
7139 "b" => Some(Self::Bottom),
7140 "ctr" => Some(Self::Center),
7141 "inBase" => Some(Self::InsideBase),
7142 "inEnd" => Some(Self::InsideEnd),
7143 "l" => Some(Self::Left),
7144 "outEnd" => Some(Self::OutsideEnd),
7145 "r" => Some(Self::Right),
7146 "t" => Some(Self::Top),
7147 _ => None,
7148 }
7149 }
7150
7151 const fn as_str(self) -> &'static str {
7152 match self {
7153 Self::BestFit => "bestFit",
7154 Self::Bottom => "b",
7155 Self::Center => "ctr",
7156 Self::InsideBase => "inBase",
7157 Self::InsideEnd => "inEnd",
7158 Self::Left => "l",
7159 Self::OutsideEnd => "outEnd",
7160 Self::Right => "r",
7161 Self::Top => "t",
7162 }
7163 }
7164}
7165
7166#[derive(Clone, Debug, Default, PartialEq)]
7168pub struct CT_DLbls {
7169 pub number_format: Option<NumberFormat>,
7170 pub position: Option<DataLabelPosition>,
7171 pub separator: Option<String>,
7172 pub show_legend_key: bool,
7173 pub show_value: bool,
7174 pub show_category_name: bool,
7175 pub show_series_name: bool,
7176 pub show_percent: bool,
7177 pub show_bubble_size: bool,
7178 position_markup: Option<ScalarMarkup>,
7179 show_legend_key_markup: Option<ScalarMarkup>,
7180 show_value_markup: Option<ScalarMarkup>,
7181 show_category_name_markup: Option<ScalarMarkup>,
7182 show_series_name_markup: Option<ScalarMarkup>,
7183 show_percent_markup: Option<ScalarMarkup>,
7184 show_bubble_size_markup: Option<ScalarMarkup>,
7185 separator_markup: Option<TextMarkup>,
7186 raw_attributes: XmlAttributes,
7187 namespace_declarations: XmlAttributes,
7188 raw_children: OrderedRawChildren,
7189}
7190
7191#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
7193pub enum DispBlanksAs {
7194 #[default]
7195 Gap,
7196 Zero,
7197 Span,
7198}
7199
7200impl DispBlanksAs {
7201 fn parse(value: &str) -> Option<Self> {
7202 match value {
7203 "gap" => Some(Self::Gap),
7204 "zero" => Some(Self::Zero),
7205 "span" => Some(Self::Span),
7206 _ => None,
7207 }
7208 }
7209
7210 const fn as_str(self) -> &'static str {
7211 match self {
7212 Self::Gap => "gap",
7213 Self::Zero => "zero",
7214 Self::Span => "span",
7215 }
7216 }
7217}
7218
7219#[derive(Clone, Debug, Default, Eq, PartialEq)]
7221pub struct CT_Title {
7222 text: Option<CT_TextBody>,
7223 raw_attributes: Vec<(String, String)>,
7224 raw_children: OrderedRawChildren,
7225}
7226
7227#[derive(Clone, Debug, Default, PartialEq)]
7229struct PlotMarkup {
7230 raw_attributes: XmlAttributes,
7231 raw_children: OrderedRawChildren,
7232 direction: Option<ScalarMarkup>,
7233 grouping: Option<ScalarMarkup>,
7234 gap_width: Option<ScalarMarkup>,
7235 overlap: Option<ScalarMarkup>,
7236 marker: Option<ScalarMarkup>,
7237 smooth: Option<ScalarMarkup>,
7238 first_slice_angle: Option<ScalarMarkup>,
7239 hole_size: Option<ScalarMarkup>,
7240 style: Option<ScalarMarkup>,
7241 axis_ids: Vec<AxisIdMarkup>,
7242 original_series_keys: Vec<(u32, u32)>,
7243 original_axis_ids: Vec<AxisId>,
7244 parsed_bar: Option<bool>,
7245 parsed_remaining: Option<&'static str>,
7246}
7247
7248#[derive(Clone, Debug, PartialEq)]
7250pub struct CT_PlotArea {
7251 raw_attributes: Vec<(String, String)>,
7252 raw_children: OrderedRawChildren,
7253 namespace_bindings: NamespaceBindings,
7254 plots: Option<Vec<Plot>>,
7255 plot_markup: Vec<PlotMarkup>,
7256 axes: Vec<Axis>,
7257}
7258
7259#[derive(Clone, Debug, Default, Eq, PartialEq)]
7261pub struct CT_Legend {
7262 raw_attributes: Vec<(String, String)>,
7263 raw_children: OrderedRawChildren,
7264}
7265
7266#[derive(Clone, Debug, PartialEq)]
7267struct AxisIdMarkup {
7268 scalar: ScalarMarkup,
7269 parsed: AxisId,
7270 lexical: String,
7271}
7272
7273#[derive(Clone, Debug)]
7275pub struct Axis {
7276 pub kind: AxisKind,
7277 pub id: AxisId,
7278 pub scaling: Scaling,
7279 pub deleted: bool,
7280 pub position: AxisPosition,
7281 pub major_gridlines: Option<ChartLines>,
7282 pub minor_gridlines: Option<ChartLines>,
7283 pub title: Option<CT_Title>,
7284 pub number_format: Option<NumberFormat>,
7285 pub major_tick_mark: TickMark,
7286 pub minor_tick_mark: TickMark,
7287 pub tick_label_position: TickLabelPosition,
7288 pub sp_pr: Option<CT_ShapeProperties>,
7289 pub tx_pr: Option<CT_TextBody>,
7290 pub cross_axis: AxisId,
7291 parsed_kind: Option<AxisKind>,
7292 id_markup: AxisIdMarkup,
7293 delete_markup: Option<ScalarMarkup>,
7294 position_markup: ScalarMarkup,
7295 major_tick_mark_markup: Option<ScalarMarkup>,
7296 minor_tick_mark_markup: Option<ScalarMarkup>,
7297 tick_label_position_markup: Option<ScalarMarkup>,
7298 cross_axis_markup: AxisIdMarkup,
7299 raw_attributes: XmlAttributes,
7300 namespace_declarations: XmlAttributes,
7301 raw_children: OrderedRawChildren,
7302}
7303
7304impl PartialEq for Axis {
7305 fn eq(&self, other: &Self) -> bool {
7306 self.kind == other.kind
7307 && self.id == other.id
7308 && axis_id_markup_eq(&self.id_markup, &other.id_markup)
7309 && self.scaling == other.scaling
7310 && self.deleted == other.deleted
7311 && optional_scalar_markup_eq(&self.delete_markup, &other.delete_markup)
7312 && self.position == other.position
7313 && self.position_markup == other.position_markup
7314 && self.major_gridlines == other.major_gridlines
7315 && self.minor_gridlines == other.minor_gridlines
7316 && self.title == other.title
7317 && self.number_format == other.number_format
7318 && self.major_tick_mark == other.major_tick_mark
7319 && optional_scalar_markup_eq(
7320 &self.major_tick_mark_markup,
7321 &other.major_tick_mark_markup,
7322 )
7323 && self.minor_tick_mark == other.minor_tick_mark
7324 && optional_scalar_markup_eq(
7325 &self.minor_tick_mark_markup,
7326 &other.minor_tick_mark_markup,
7327 )
7328 && self.tick_label_position == other.tick_label_position
7329 && optional_scalar_markup_eq(
7330 &self.tick_label_position_markup,
7331 &other.tick_label_position_markup,
7332 )
7333 && self.sp_pr == other.sp_pr
7334 && self.tx_pr == other.tx_pr
7335 && self.cross_axis == other.cross_axis
7336 && axis_id_markup_eq(&self.cross_axis_markup, &other.cross_axis_markup)
7337 && self.raw_attributes == other.raw_attributes
7338 && self.namespace_declarations == other.namespace_declarations
7339 && self.raw_children == other.raw_children
7340 }
7341}
7342
7343impl Axis {
7344 pub fn new(kind: AxisKind, id: AxisId, position: AxisPosition, cross_axis: AxisId) -> Self {
7345 Self {
7346 kind,
7347 id,
7348 scaling: Scaling::default(),
7349 deleted: false,
7350 position,
7351 major_gridlines: None,
7352 minor_gridlines: None,
7353 title: None,
7354 number_format: None,
7355 major_tick_mark: TickMark::None,
7356 minor_tick_mark: TickMark::None,
7357 tick_label_position: TickLabelPosition::NextTo,
7358 sp_pr: None,
7359 tx_pr: None,
7360 cross_axis,
7361 parsed_kind: None,
7362 id_markup: default_axis_id_markup(id),
7363 delete_markup: None,
7364 position_markup: ScalarMarkup::default(),
7365 major_tick_mark_markup: None,
7366 minor_tick_mark_markup: None,
7367 tick_label_position_markup: None,
7368 cross_axis_markup: default_axis_id_markup(cross_axis),
7369 raw_attributes: Vec::new(),
7370 namespace_declarations: Vec::new(),
7371 raw_children: OrderedRawChildren::default(),
7372 }
7373 }
7374
7375 pub fn set_deleted(&mut self, deleted: bool) {
7377 self.deleted = deleted;
7378 self.delete_markup.get_or_insert_with(ScalarMarkup::default);
7379 }
7380
7381 pub fn from_xml(xml: &[u8]) -> Result<Self> {
7382 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
7383 }
7384
7385 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7386 let mut reader = Reader::from_reader(xml);
7387 let mut buffer = Vec::new();
7388 loop {
7389 match reader
7390 .read_event_into(&mut buffer)
7391 .map_err(OxmlError::from)?
7392 {
7393 Event::Start(element) => {
7394 let name = element.name();
7395 let local = local_name(name.as_ref());
7396 let Some(kind) = AxisKind::parse(local) else {
7397 return Err(ChartError::UnexpectedElement(element_name(&element)));
7398 };
7399 chart_root_prefix(&element)?;
7400 if !element_is_in_namespace(&element, C_NS, inherited)? {
7401 return Err(ChartError::UnexpectedElement(element_name(&element)));
7402 }
7403 return Self::from_element(&mut reader, &element, inherited, kind);
7404 }
7405 Event::Empty(element) => {
7406 let name = element.name();
7407 let local = local_name(name.as_ref());
7408 if AxisKind::parse(local).is_some() {
7409 return Err(ChartError::MissingElement("c:axId".to_owned()));
7410 }
7411 return Err(ChartError::UnexpectedElement(element_name(&element)));
7412 }
7413 Event::Eof => return Err(ChartError::MissingElement("ChartML axis".to_owned())),
7414 _ => {}
7415 }
7416 buffer.clear();
7417 }
7418 }
7419
7420 fn from_element(
7421 reader: &mut Reader<&[u8]>,
7422 start: &BytesStart<'_>,
7423 inherited: &NamespaceBindings,
7424 kind: AxisKind,
7425 ) -> Result<Self> {
7426 reject_conflicting_prefix(start, b"a", A_NS)?;
7427 reject_conflicting_prefix(start, b"r", R_NS)?;
7428 let namespaces = chart_bindings(inherited, start)?;
7429 require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
7430 require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
7431 require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
7432 let (raw_attributes, _) =
7433 capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
7434 let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
7435 let mut state = AxisParseState::default();
7436 let mut buffer = Vec::new();
7437 loop {
7438 match reader
7439 .read_event_into(&mut buffer)
7440 .map_err(OxmlError::from)?
7441 {
7442 Event::Start(element) => {
7443 let name = chart_child_local(&element, &namespaces)?;
7444 let raw = capture_element(reader, &element)?;
7445 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
7446 }
7447 Event::Empty(element) => {
7448 let name = chart_child_local(&element, &namespaces)?;
7449 let raw = capture_empty_element(&element)?;
7450 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
7451 }
7452 event @ (Event::Text(_)
7453 | Event::CData(_)
7454 | Event::Comment(_)
7455 | Event::PI(_)
7456 | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
7457 Event::End(element)
7458 if matches_local_name(element.name().as_ref(), kind.local().as_bytes()) =>
7459 {
7460 break;
7461 }
7462 Event::Eof => return Err(missing_end(&format!("c:{}", kind.local()))),
7463 _ => {}
7464 }
7465 buffer.clear();
7466 }
7467 state.finish(kind, raw_attributes, namespace_declarations)
7468 }
7469
7470 pub fn to_xml(&self) -> Result<Vec<u8>> {
7471 self.validate()?;
7472 let mut writer = Writer::new(Vec::new());
7473 let root = format!("c:{}", self.kind.local());
7474 let mut start = BytesStart::new(&root);
7475 start.push_attribute(("xmlns:c", C_NS));
7476 start.push_attribute(("xmlns:a", A_NS));
7477 start.push_attribute(("xmlns:r", R_NS));
7478 push_attributes(&mut start, &self.namespace_declarations);
7479 push_attributes(&mut start, &self.raw_attributes);
7480 writer
7481 .write_event(Event::Start(start))
7482 .map_err(OxmlError::from)?;
7483 emit_raw(&mut writer, self.raw_children.at(0))?;
7484 write_axis_id(&mut writer, "c:axId", self.id, &self.id_markup)?;
7485 emit_raw(&mut writer, self.raw_children.at(1))?;
7486 self.scaling.write_xml(&mut writer)?;
7487 emit_raw(&mut writer, self.raw_children.at(2))?;
7488 if self.deleted || self.delete_markup.is_some() {
7489 write_scalar(
7490 &mut writer,
7491 "c:delete",
7492 bool_lexical(self.deleted),
7493 self.delete_markup.as_ref(),
7494 )?;
7495 }
7496 emit_raw(&mut writer, self.raw_children.at(3))?;
7497 write_scalar(
7498 &mut writer,
7499 "c:axPos",
7500 self.position.as_str(),
7501 Some(&self.position_markup),
7502 )?;
7503 emit_raw(&mut writer, self.raw_children.at(4))?;
7504 if let Some(lines) = &self.major_gridlines {
7505 lines.write_xml(&mut writer, "c:majorGridlines")?;
7506 }
7507 emit_raw(&mut writer, self.raw_children.at(5))?;
7508 if let Some(lines) = &self.minor_gridlines {
7509 lines.write_xml(&mut writer, "c:minorGridlines")?;
7510 }
7511 emit_raw(&mut writer, self.raw_children.at(6))?;
7512 if let Some(title) = &self.title {
7513 title.write_xml(&mut writer)?;
7514 }
7515 emit_raw(&mut writer, self.raw_children.at(7))?;
7516 if let Some(number_format) = &self.number_format {
7517 number_format.write_xml(&mut writer)?;
7518 }
7519 emit_raw(&mut writer, self.raw_children.at(8))?;
7520 if self.major_tick_mark != TickMark::None || self.major_tick_mark_markup.is_some() {
7521 write_scalar(
7522 &mut writer,
7523 "c:majorTickMark",
7524 self.major_tick_mark.as_str(),
7525 self.major_tick_mark_markup.as_ref(),
7526 )?;
7527 }
7528 emit_raw(&mut writer, self.raw_children.at(9))?;
7529 if self.minor_tick_mark != TickMark::None || self.minor_tick_mark_markup.is_some() {
7530 write_scalar(
7531 &mut writer,
7532 "c:minorTickMark",
7533 self.minor_tick_mark.as_str(),
7534 self.minor_tick_mark_markup.as_ref(),
7535 )?;
7536 }
7537 emit_raw(&mut writer, self.raw_children.at(10))?;
7538 if self.tick_label_position != TickLabelPosition::NextTo
7539 || self.tick_label_position_markup.is_some()
7540 {
7541 write_scalar(
7542 &mut writer,
7543 "c:tickLblPos",
7544 self.tick_label_position.as_str(),
7545 self.tick_label_position_markup.as_ref(),
7546 )?;
7547 }
7548 emit_raw(&mut writer, self.raw_children.at(11))?;
7549 if let Some(properties) = &self.sp_pr {
7550 properties.write_xml_as(&mut writer, "c:spPr")?;
7551 }
7552 emit_raw(&mut writer, self.raw_children.at(12))?;
7553 if let Some(text) = &self.tx_pr {
7554 text.write_xml_as(&mut writer, "c:txPr")?;
7555 }
7556 emit_raw(&mut writer, self.raw_children.at(13))?;
7557 write_axis_id(
7558 &mut writer,
7559 "c:crossAx",
7560 self.cross_axis,
7561 &self.cross_axis_markup,
7562 )?;
7563 emit_raw(&mut writer, self.raw_children.at(14))?;
7564 writer
7565 .write_event(Event::End(BytesEnd::new(&root)))
7566 .map_err(OxmlError::from)?;
7567 Ok(writer.into_inner())
7568 }
7569
7570 pub fn raw_children(&self) -> &OrderedRawChildren {
7571 &self.raw_children
7572 }
7573
7574 fn validate(&self) -> Result<()> {
7575 if let Some(parsed_kind) = self.parsed_kind
7576 && self.kind != parsed_kind
7577 {
7578 return Err(ChartError::InvalidValue {
7579 element: "ChartML axis root".to_owned(),
7580 value: format!(
7581 "parsed c:{} cannot be relabelled c:{}",
7582 parsed_kind.local(),
7583 self.kind.local()
7584 ),
7585 });
7586 }
7587 AxisId::new(self.id.value())?;
7588 AxisId::new(self.cross_axis.value())?;
7589 self.scaling.validate()?;
7590 if let Some(number_format) = &self.number_format {
7591 number_format.validate()?;
7592 }
7593 Ok(())
7594 }
7595}
7596
7597#[derive(Default)]
7598struct AxisParseState {
7599 id: Option<(AxisId, AxisIdMarkup)>,
7600 scaling: Option<Scaling>,
7601 deleted: Option<(bool, ScalarMarkup)>,
7602 position: Option<(AxisPosition, ScalarMarkup)>,
7603 major_gridlines: Option<ChartLines>,
7604 minor_gridlines: Option<ChartLines>,
7605 title: Option<CT_Title>,
7606 number_format: Option<NumberFormat>,
7607 major_tick_mark: Option<(TickMark, ScalarMarkup)>,
7608 minor_tick_mark: Option<(TickMark, ScalarMarkup)>,
7609 tick_label_position: Option<(TickLabelPosition, ScalarMarkup)>,
7610 sp_pr: Option<CT_ShapeProperties>,
7611 tx_pr: Option<CT_TextBody>,
7612 cross_axis: Option<(AxisId, AxisIdMarkup)>,
7613 raw_children: OrderedRawChildren,
7614 boundary: usize,
7615}
7616
7617impl AxisParseState {
7618 fn capture_event(&mut self, raw: Vec<u8>) {
7619 self.raw_children.push(self.boundary, raw);
7620 }
7621
7622 fn parse_child(
7623 &mut self,
7624 name: &[u8],
7625 raw: Vec<u8>,
7626 namespaces: &NamespaceBindings,
7627 ) -> Result<()> {
7628 match name {
7629 b"axId" => {
7630 set_once(&mut self.id, parse_axis_id_scalar(&raw, "axId")?, "c:axId")?;
7631 self.boundary = self.boundary.max(1);
7632 }
7633 b"scaling" => {
7634 set_once(
7635 &mut self.scaling,
7636 Scaling::from_xml(&raw, namespaces)?,
7637 "c:scaling",
7638 )?;
7639 self.boundary = self.boundary.max(2);
7640 }
7641 b"delete" => {
7642 set_once(
7643 &mut self.deleted,
7644 parse_bool_value(&raw, "delete")?,
7645 "c:delete",
7646 )?;
7647 self.boundary = self.boundary.max(3);
7648 }
7649 b"axPos" => {
7650 set_once(&mut self.position, parse_axis_position(&raw)?, "c:axPos")?;
7651 self.boundary = self.boundary.max(4);
7652 }
7653 b"majorGridlines" => {
7654 set_once(
7655 &mut self.major_gridlines,
7656 ChartLines::from_xml(&raw, b"majorGridlines", namespaces)?,
7657 "c:majorGridlines",
7658 )?;
7659 self.boundary = self.boundary.max(5);
7660 }
7661 b"minorGridlines" => {
7662 set_once(
7663 &mut self.minor_gridlines,
7664 ChartLines::from_xml(&raw, b"minorGridlines", namespaces)?,
7665 "c:minorGridlines",
7666 )?;
7667 self.boundary = self.boundary.max(6);
7668 }
7669 b"title" => {
7670 set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
7671 self.boundary = self.boundary.max(7);
7672 }
7673 b"numFmt" => {
7674 set_once(
7675 &mut self.number_format,
7676 NumberFormat::from_xml(&raw)?,
7677 "c:numFmt",
7678 )?;
7679 self.boundary = self.boundary.max(8);
7680 }
7681 b"majorTickMark" => {
7682 set_once(
7683 &mut self.major_tick_mark,
7684 parse_tick_mark(&raw, "majorTickMark")?,
7685 "c:majorTickMark",
7686 )?;
7687 self.boundary = self.boundary.max(9);
7688 }
7689 b"minorTickMark" => {
7690 set_once(
7691 &mut self.minor_tick_mark,
7692 parse_tick_mark(&raw, "minorTickMark")?,
7693 "c:minorTickMark",
7694 )?;
7695 self.boundary = self.boundary.max(10);
7696 }
7697 b"tickLblPos" => {
7698 set_once(
7699 &mut self.tick_label_position,
7700 parse_tick_label_position(&raw)?,
7701 "c:tickLblPos",
7702 )?;
7703 self.boundary = self.boundary.max(11);
7704 }
7705 b"spPr" => {
7706 parse_chart_shape_properties(&raw, namespaces, &mut self.sp_pr)?;
7707 self.boundary = self.boundary.max(12);
7708 }
7709 b"txPr" => {
7710 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
7711 let inherited = namespaces
7712 .iter()
7713 .map(|(prefix, uri)| {
7714 Ok((
7715 std::str::from_utf8(prefix)
7716 .map_err(OxmlError::from)?
7717 .to_owned(),
7718 uri.clone(),
7719 ))
7720 })
7721 .collect::<Result<Vec<_>>>()?;
7722 let text = CT_TextBody::from_xml_as_with_namespaces(&raw, b"txPr", &inherited)?;
7723 let mut writer = Writer::new(Vec::new());
7724 text.write_xml_as(&mut writer, "c:txPr")?;
7725 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
7726 set_once(&mut self.tx_pr, text, "c:txPr")?;
7727 self.boundary = self.boundary.max(13);
7728 }
7729 b"crossAx" => {
7730 set_once(
7731 &mut self.cross_axis,
7732 parse_axis_id_scalar(&raw, "crossAx")?,
7733 "c:crossAx",
7734 )?;
7735 self.boundary = self.boundary.max(14);
7736 }
7737 _ => self.raw_children.push(self.boundary, raw),
7738 }
7739 Ok(())
7740 }
7741
7742 fn finish(
7743 self,
7744 kind: AxisKind,
7745 raw_attributes: XmlAttributes,
7746 namespace_declarations: XmlAttributes,
7747 ) -> Result<Axis> {
7748 let (id, id_markup) = self
7749 .id
7750 .ok_or_else(|| ChartError::MissingElement("c:axId".to_owned()))?;
7751 let scaling = self
7752 .scaling
7753 .ok_or_else(|| ChartError::MissingElement("c:scaling".to_owned()))?;
7754 let (position, position_markup) = self
7755 .position
7756 .ok_or_else(|| ChartError::MissingElement("c:axPos".to_owned()))?;
7757 let (cross_axis, cross_axis_markup) = self
7758 .cross_axis
7759 .ok_or_else(|| ChartError::MissingElement("c:crossAx".to_owned()))?;
7760 let (deleted, delete_markup) = self
7761 .deleted
7762 .map(|(value, markup)| (value, Some(markup)))
7763 .unwrap_or((false, None));
7764 let (major_tick_mark, major_tick_mark_markup) = self
7765 .major_tick_mark
7766 .map(|(value, markup)| (value, Some(markup)))
7767 .unwrap_or((TickMark::None, None));
7768 let (minor_tick_mark, minor_tick_mark_markup) = self
7769 .minor_tick_mark
7770 .map(|(value, markup)| (value, Some(markup)))
7771 .unwrap_or((TickMark::None, None));
7772 let (tick_label_position, tick_label_position_markup) = self
7773 .tick_label_position
7774 .map(|(value, markup)| (value, Some(markup)))
7775 .unwrap_or((TickLabelPosition::NextTo, None));
7776 let axis = Axis {
7777 kind,
7778 id,
7779 scaling,
7780 deleted,
7781 position,
7782 major_gridlines: self.major_gridlines,
7783 minor_gridlines: self.minor_gridlines,
7784 title: self.title,
7785 number_format: self.number_format,
7786 major_tick_mark,
7787 minor_tick_mark,
7788 tick_label_position,
7789 sp_pr: self.sp_pr,
7790 tx_pr: self.tx_pr,
7791 cross_axis,
7792 parsed_kind: Some(kind),
7793 id_markup,
7794 delete_markup,
7795 position_markup,
7796 major_tick_mark_markup,
7797 minor_tick_mark_markup,
7798 tick_label_position_markup,
7799 cross_axis_markup,
7800 raw_attributes,
7801 namespace_declarations,
7802 raw_children: raw_children_in_schema_order(&self.raw_children, 14),
7803 };
7804 axis.validate()?;
7805 Ok(axis)
7806 }
7807}
7808
7809impl Scaling {
7810 fn from_xml(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7811 let mut reader = Reader::from_reader(xml);
7812 let mut buffer = Vec::new();
7813 loop {
7814 match reader
7815 .read_event_into(&mut buffer)
7816 .map_err(OxmlError::from)?
7817 {
7818 Event::Start(element)
7819 if matches_local_name(element.name().as_ref(), b"scaling") =>
7820 {
7821 let namespaces = typed_rewrite_bindings(&element, inherited)?;
7822 let raw_attributes = capture_attributes(&element)?;
7823 let mut log_base = None;
7824 let mut orientation = None;
7825 let mut maximum = None;
7826 let mut minimum = None;
7827 let mut raw_children = OrderedRawChildren::default();
7828 let mut boundary = 0usize;
7829 let mut inner = Vec::new();
7830 loop {
7831 match reader
7832 .read_event_into(&mut inner)
7833 .map_err(OxmlError::from)?
7834 {
7835 Event::Start(child) => {
7836 let name = chart_child_local(&child, &namespaces)?;
7837 let raw = capture_element(&mut reader, &child)?;
7838 parse_scaling_child(
7839 name.as_deref().unwrap_or_default(),
7840 raw,
7841 &mut log_base,
7842 &mut orientation,
7843 &mut maximum,
7844 &mut minimum,
7845 &mut raw_children,
7846 &mut boundary,
7847 )?;
7848 }
7849 Event::Empty(child) => {
7850 let name = chart_child_local(&child, &namespaces)?;
7851 let raw = capture_empty_element(&child)?;
7852 parse_scaling_child(
7853 name.as_deref().unwrap_or_default(),
7854 raw,
7855 &mut log_base,
7856 &mut orientation,
7857 &mut maximum,
7858 &mut minimum,
7859 &mut raw_children,
7860 &mut boundary,
7861 )?;
7862 }
7863 event @ (Event::Text(_)
7864 | Event::CData(_)
7865 | Event::Comment(_)
7866 | Event::PI(_)
7867 | Event::GeneralRef(_)) => {
7868 raw_children.push(boundary, capture_event(event)?);
7869 }
7870 Event::End(end)
7871 if matches_local_name(end.name().as_ref(), b"scaling") =>
7872 {
7873 let (log_base, log_base_markup) = optional_markup(log_base);
7874 let (orientation, orientation_markup) = orientation
7875 .map(|(value, markup)| (value, Some(markup)))
7876 .unwrap_or((Orientation::MinMax, None));
7877 let (maximum, maximum_markup) = optional_markup(maximum);
7878 let (minimum, minimum_markup) = optional_markup(minimum);
7879 let scaling = Self {
7880 log_base,
7881 orientation,
7882 maximum,
7883 minimum,
7884 log_base_markup,
7885 orientation_markup,
7886 maximum_markup,
7887 minimum_markup,
7888 raw_attributes,
7889 raw_children: raw_children_in_schema_order(&raw_children, 4),
7890 };
7891 scaling.validate()?;
7892 return Ok(scaling);
7893 }
7894 Event::Eof => return Err(missing_end("c:scaling")),
7895 _ => {}
7896 }
7897 inner.clear();
7898 }
7899 }
7900 Event::Empty(element)
7901 if matches_local_name(element.name().as_ref(), b"scaling") =>
7902 {
7903 typed_rewrite_bindings(&element, inherited)?;
7904 return Ok(Self {
7905 raw_attributes: capture_attributes(&element)?,
7906 ..Self::default()
7907 });
7908 }
7909 Event::Start(element) | Event::Empty(element) => {
7910 return Err(ChartError::UnexpectedElement(element_name(&element)));
7911 }
7912 Event::Eof => return Err(ChartError::MissingElement("c:scaling".to_owned())),
7913 _ => {}
7914 }
7915 buffer.clear();
7916 }
7917 }
7918
7919 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7920 self.validate()?;
7921 let mut start = BytesStart::new("c:scaling");
7922 push_attributes(&mut start, &self.raw_attributes);
7923 writer
7924 .write_event(Event::Start(start))
7925 .map_err(OxmlError::from)?;
7926 emit_raw(writer, self.raw_children.at(0))?;
7927 if let Some(value) = self.log_base {
7928 write_scalar(
7929 writer,
7930 "c:logBase",
7931 &value.to_string(),
7932 self.log_base_markup.as_ref(),
7933 )?;
7934 }
7935 emit_raw(writer, self.raw_children.at(1))?;
7936 if self.orientation != Orientation::MinMax || self.orientation_markup.is_some() {
7937 write_scalar(
7938 writer,
7939 "c:orientation",
7940 self.orientation.as_str(),
7941 self.orientation_markup.as_ref(),
7942 )?;
7943 }
7944 emit_raw(writer, self.raw_children.at(2))?;
7945 if let Some(value) = self.maximum {
7946 write_scalar(
7947 writer,
7948 "c:max",
7949 &value.to_string(),
7950 self.maximum_markup.as_ref(),
7951 )?;
7952 }
7953 emit_raw(writer, self.raw_children.at(3))?;
7954 if let Some(value) = self.minimum {
7955 write_scalar(
7956 writer,
7957 "c:min",
7958 &value.to_string(),
7959 self.minimum_markup.as_ref(),
7960 )?;
7961 }
7962 emit_raw(writer, self.raw_children.at(4))?;
7963 writer
7964 .write_event(Event::End(BytesEnd::new("c:scaling")))
7965 .map_err(OxmlError::from)?;
7966 Ok(())
7967 }
7968
7969 fn validate(&self) -> Result<()> {
7970 if let Some(value) = self.log_base
7971 && (!value.is_finite() || !(2.0..=1000.0).contains(&value))
7972 {
7973 return Err(ChartError::InvalidValue {
7974 element: "c:logBase".to_owned(),
7975 value: value.to_string(),
7976 });
7977 }
7978 for (element, value) in [("c:max", self.maximum), ("c:min", self.minimum)] {
7979 if let Some(value) = value
7980 && !value.is_finite()
7981 {
7982 return Err(ChartError::InvalidValue {
7983 element: element.to_owned(),
7984 value: value.to_string(),
7985 });
7986 }
7987 }
7988 if let (Some(minimum), Some(maximum)) = (self.minimum, self.maximum)
7989 && minimum > maximum
7990 {
7991 return Err(ChartError::InvalidValue {
7992 element: "c:scaling".to_owned(),
7993 value: format!("minimum {minimum} exceeds maximum {maximum}"),
7994 });
7995 }
7996 Ok(())
7997 }
7998}
7999
8000#[allow(clippy::too_many_arguments)]
8001fn parse_scaling_child(
8002 name: &[u8],
8003 raw: Vec<u8>,
8004 log_base: &mut Option<(f64, ScalarMarkup)>,
8005 orientation: &mut Option<(Orientation, ScalarMarkup)>,
8006 maximum: &mut Option<(f64, ScalarMarkup)>,
8007 minimum: &mut Option<(f64, ScalarMarkup)>,
8008 raw_children: &mut OrderedRawChildren,
8009 boundary: &mut usize,
8010) -> Result<()> {
8011 match name {
8012 b"logBase" => {
8013 set_once(log_base, parse_f64_scalar(&raw, "logBase")?, "c:logBase")?;
8014 *boundary = (*boundary).max(1);
8015 }
8016 b"orientation" => {
8017 let (value, markup) = scalar_value(&raw, "orientation")?;
8018 let value = value
8019 .ok_or_else(|| invalid_attribute("orientation", "val", "<missing>".to_owned()))?;
8020 let parsed = Orientation::parse(&value)
8021 .ok_or_else(|| invalid_attribute("orientation", "val", value))?;
8022 set_once(orientation, (parsed, markup), "c:orientation")?;
8023 *boundary = (*boundary).max(2);
8024 }
8025 b"max" => {
8026 set_once(maximum, parse_f64_scalar(&raw, "max")?, "c:max")?;
8027 *boundary = (*boundary).max(3);
8028 }
8029 b"min" => {
8030 set_once(minimum, parse_f64_scalar(&raw, "min")?, "c:min")?;
8031 *boundary = (*boundary).max(4);
8032 }
8033 _ => raw_children.push(*boundary, raw),
8034 }
8035 Ok(())
8036}
8037
8038impl ChartLines {
8039 fn from_xml(xml: &[u8], local: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
8040 let mut reader = Reader::from_reader(xml);
8041 let mut buffer = Vec::new();
8042 loop {
8043 match reader
8044 .read_event_into(&mut buffer)
8045 .map_err(OxmlError::from)?
8046 {
8047 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
8048 let namespaces = typed_rewrite_bindings(&element, inherited)?;
8049 let raw_attributes = capture_attributes(&element)?;
8050 let mut sp_pr = None;
8051 let mut raw_children = OrderedRawChildren::default();
8052 let mut boundary = 0usize;
8053 let mut inner = Vec::new();
8054 loop {
8055 match reader
8056 .read_event_into(&mut inner)
8057 .map_err(OxmlError::from)?
8058 {
8059 Event::Start(child) => {
8060 let name = chart_child_local(&child, &namespaces)?;
8061 let raw = capture_element(&mut reader, &child)?;
8062 if name.as_deref() == Some(b"spPr") {
8063 parse_chart_shape_properties(&raw, &namespaces, &mut sp_pr)?;
8064 boundary = 1;
8065 } else {
8066 raw_children.push(boundary, raw);
8067 }
8068 }
8069 Event::Empty(child) => {
8070 let name = chart_child_local(&child, &namespaces)?;
8071 let raw = capture_empty_element(&child)?;
8072 if name.as_deref() == Some(b"spPr") {
8073 parse_chart_shape_properties(&raw, &namespaces, &mut sp_pr)?;
8074 boundary = 1;
8075 } else {
8076 raw_children.push(boundary, raw);
8077 }
8078 }
8079 event @ (Event::Text(_)
8080 | Event::CData(_)
8081 | Event::Comment(_)
8082 | Event::PI(_)
8083 | Event::GeneralRef(_)) => {
8084 raw_children.push(boundary, capture_event(event)?);
8085 }
8086 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
8087 return Ok(Self {
8088 sp_pr,
8089 raw_attributes,
8090 raw_children: raw_children_in_schema_order(&raw_children, 1),
8091 });
8092 }
8093 Event::Eof => {
8094 return Err(missing_end(&format!(
8095 "c:{}",
8096 String::from_utf8_lossy(local)
8097 )));
8098 }
8099 _ => {}
8100 }
8101 inner.clear();
8102 }
8103 }
8104 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
8105 typed_rewrite_bindings(&element, inherited)?;
8106 return Ok(Self {
8107 sp_pr: None,
8108 raw_attributes: capture_attributes(&element)?,
8109 raw_children: OrderedRawChildren::default(),
8110 });
8111 }
8112 Event::Start(element) | Event::Empty(element) => {
8113 return Err(ChartError::UnexpectedElement(element_name(&element)));
8114 }
8115 Event::Eof => {
8116 return Err(ChartError::MissingElement(format!(
8117 "c:{}",
8118 String::from_utf8_lossy(local)
8119 )));
8120 }
8121 _ => {}
8122 }
8123 buffer.clear();
8124 }
8125 }
8126
8127 fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
8128 let mut start = BytesStart::new(tag);
8129 push_attributes(&mut start, &self.raw_attributes);
8130 if self.sp_pr.is_none() && self.raw_children.is_empty() {
8131 writer
8132 .write_event(Event::Empty(start))
8133 .map_err(OxmlError::from)?;
8134 return Ok(());
8135 }
8136 writer
8137 .write_event(Event::Start(start))
8138 .map_err(OxmlError::from)?;
8139 emit_raw(writer, self.raw_children.at(0))?;
8140 if let Some(properties) = &self.sp_pr {
8141 properties.write_xml_as(writer, "c:spPr")?;
8142 }
8143 emit_raw(writer, self.raw_children.at(1))?;
8144 writer
8145 .write_event(Event::End(BytesEnd::new(tag)))
8146 .map_err(OxmlError::from)?;
8147 Ok(())
8148 }
8149}
8150
8151fn parse_chart_shape_properties(
8152 raw: &[u8],
8153 namespaces: &NamespaceBindings,
8154 slot: &mut Option<CT_ShapeProperties>,
8155) -> Result<()> {
8156 reject_conflicting_prefix_in_xml(raw, b"a", A_NS)?;
8157 let inherited = namespaces
8158 .iter()
8159 .map(|(prefix, uri)| {
8160 Ok((
8161 std::str::from_utf8(prefix)
8162 .map_err(OxmlError::from)?
8163 .to_owned(),
8164 uri.clone(),
8165 ))
8166 })
8167 .collect::<Result<Vec<_>>>()?;
8168 let properties = CT_ShapeProperties::from_xml_with_inherited_namespaces(raw, &inherited)?;
8169 let mut writer = Writer::new(Vec::new());
8170 properties.write_xml_as(&mut writer, "c:spPr")?;
8171 reject_rewritten_foreign_elements(raw, &writer.into_inner(), namespaces, b"spPr")?;
8172 set_once(slot, properties, "c:spPr")
8173}
8174
8175impl NumberFormat {
8176 pub fn new(format_code: String, source_linked: bool) -> Result<Self> {
8177 let number_format = Self {
8178 format_code,
8179 source_linked,
8180 raw_attributes: Vec::new(),
8181 raw_content: Vec::new(),
8182 };
8183 number_format.validate()?;
8184 Ok(number_format)
8185 }
8186
8187 fn from_xml(xml: &[u8]) -> Result<Self> {
8188 let mut reader = Reader::from_reader(xml);
8189 let mut buffer = Vec::new();
8190 loop {
8191 match reader
8192 .read_event_into(&mut buffer)
8193 .map_err(OxmlError::from)?
8194 {
8195 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
8196 return Self::from_element(&element, Vec::new());
8197 }
8198 Event::Start(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
8199 let mut raw_content = Vec::new();
8200 let mut inner = Vec::new();
8201 loop {
8202 match reader
8203 .read_event_into(&mut inner)
8204 .map_err(OxmlError::from)?
8205 {
8206 Event::End(end)
8207 if matches_local_name(end.name().as_ref(), b"numFmt") =>
8208 {
8209 return Self::from_element(&element, raw_content);
8210 }
8211 event @ (Event::Text(_)
8212 | Event::CData(_)
8213 | Event::Comment(_)
8214 | Event::PI(_)
8215 | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
8216 Event::Eof => return Err(missing_end("c:numFmt")),
8217 _ => return Err(ChartError::UnexpectedElement("c:numFmt".to_owned())),
8218 }
8219 inner.clear();
8220 }
8221 }
8222 Event::Start(element) | Event::Empty(element) => {
8223 return Err(ChartError::UnexpectedElement(element_name(&element)));
8224 }
8225 Event::Eof => return Err(ChartError::MissingElement("c:numFmt".to_owned())),
8226 _ => {}
8227 }
8228 buffer.clear();
8229 }
8230 }
8231
8232 fn from_element(element: &BytesStart<'_>, raw_content: Vec<Vec<u8>>) -> Result<Self> {
8233 reject_conflicting_prefix(element, b"c", C_NS)?;
8234 let format_code = attribute_value(element, b"formatCode")?
8235 .ok_or_else(|| invalid_attribute("numFmt", "formatCode", "<missing>".to_owned()))?;
8236 let source_linked = attribute_value(element, b"sourceLinked")?
8237 .ok_or_else(|| invalid_attribute("numFmt", "sourceLinked", "<missing>".to_owned()))?;
8238 let source_linked = parse_bool_lexical("numFmt", "sourceLinked", &source_linked)?;
8239 let number_format = Self {
8240 format_code,
8241 source_linked,
8242 raw_attributes: capture_attributes_excluding(
8243 element,
8244 &[b"formatCode", b"sourceLinked"],
8245 )?,
8246 raw_content,
8247 };
8248 number_format.validate()?;
8249 Ok(number_format)
8250 }
8251
8252 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8253 self.validate()?;
8254 let mut start = BytesStart::new("c:numFmt");
8255 start.push_attribute(("formatCode", self.format_code.as_str()));
8256 start.push_attribute(("sourceLinked", bool_lexical(self.source_linked)));
8257 push_attributes(&mut start, &self.raw_attributes);
8258 if self.raw_content.is_empty() {
8259 writer
8260 .write_event(Event::Empty(start))
8261 .map_err(OxmlError::from)?;
8262 return Ok(());
8263 }
8264 writer
8265 .write_event(Event::Start(start))
8266 .map_err(OxmlError::from)?;
8267 for raw in &self.raw_content {
8268 writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
8269 }
8270 writer
8271 .write_event(Event::End(BytesEnd::new("c:numFmt")))
8272 .map_err(OxmlError::from)?;
8273 Ok(())
8274 }
8275
8276 pub fn format_value(&self, value: f64) -> Result<String> {
8278 if !value.is_finite() {
8279 return Err(ChartError::InvalidValue {
8280 element: "c:numFmt value".to_owned(),
8281 value: value.to_string(),
8282 });
8283 }
8284 if self.format_code == "General" {
8285 return Ok(value.to_string());
8286 }
8287
8288 let (numeric_code, percentage) = self
8289 .format_code
8290 .strip_suffix('%')
8291 .map(|code| (code, true))
8292 .unwrap_or((&self.format_code, false));
8293 let precision = if numeric_code == "0" {
8294 Some(0)
8295 } else if let Some(decimals) = numeric_code.strip_prefix("0.")
8296 && !decimals.is_empty()
8297 && decimals.bytes().all(|byte| byte == b'0')
8298 {
8299 Some(decimals.len())
8300 } else {
8301 None
8302 };
8303 let Some(precision) = precision else {
8304 return Err(ChartError::InvalidValue {
8305 element: "c:numFmt/@formatCode projection".to_owned(),
8306 value: self.format_code.clone(),
8307 });
8308 };
8309 let projected = if percentage { value * 100.0 } else { value };
8310 if !projected.is_finite() {
8311 return Err(ChartError::InvalidValue {
8312 element: "c:numFmt value".to_owned(),
8313 value: projected.to_string(),
8314 });
8315 }
8316 let mut formatted = format!("{projected:.precision$}");
8317 if percentage {
8318 formatted.push('%');
8319 }
8320 Ok(formatted)
8321 }
8322
8323 fn validate(&self) -> Result<()> {
8324 if self.format_code.is_empty() {
8325 return Err(ChartError::InvalidValue {
8326 element: "c:numFmt/@formatCode".to_owned(),
8327 value: self.format_code.clone(),
8328 });
8329 }
8330 validate_xml_text(&self.format_code, "c:numFmt/@formatCode")
8331 }
8332}
8333
8334impl CT_DLbls {
8335 pub fn from_xml(xml: &[u8]) -> Result<Self> {
8336 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
8337 }
8338
8339 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
8340 let mut reader = Reader::from_reader(xml);
8341 let mut buffer = Vec::new();
8342 loop {
8343 match reader
8344 .read_event_into(&mut buffer)
8345 .map_err(OxmlError::from)?
8346 {
8347 Event::Start(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
8348 chart_root_prefix(&element)?;
8349 if !element_is_in_namespace(&element, C_NS, inherited)? {
8350 return Err(ChartError::UnexpectedElement(element_name(&element)));
8351 }
8352 return Self::from_element(&mut reader, &element, inherited);
8353 }
8354 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
8355 chart_root_prefix(&element)?;
8356 let namespaces = chart_bindings(inherited, &element)?;
8357 if !element_is_in_namespace(&element, C_NS, inherited)? {
8358 return Err(ChartError::UnexpectedElement(element_name(&element)));
8359 }
8360 let (raw_attributes, _) = capture_fixed_root_attributes(
8361 &element,
8362 &["xmlns:c", "xmlns:a", "xmlns:r"],
8363 )?;
8364 return Ok(Self {
8365 raw_attributes,
8366 namespace_declarations: standalone_namespace_declarations(&namespaces)?,
8367 ..Self::default()
8368 });
8369 }
8370 Event::Start(element) | Event::Empty(element) => {
8371 return Err(ChartError::UnexpectedElement(element_name(&element)));
8372 }
8373 Event::Eof => return Err(ChartError::MissingElement("c:dLbls".to_owned())),
8374 _ => {}
8375 }
8376 buffer.clear();
8377 }
8378 }
8379
8380 fn from_element(
8381 reader: &mut Reader<&[u8]>,
8382 start: &BytesStart<'_>,
8383 inherited: &NamespaceBindings,
8384 ) -> Result<Self> {
8385 reject_conflicting_prefix(start, b"a", A_NS)?;
8386 let namespaces = chart_bindings(inherited, start)?;
8387 require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
8388 let (raw_attributes, _) =
8389 capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8390 let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
8391 let mut state = DataLabelsParseState::new(namespaces);
8392 let mut buffer = Vec::new();
8393 loop {
8394 match reader
8395 .read_event_into(&mut buffer)
8396 .map_err(OxmlError::from)?
8397 {
8398 Event::Start(element) => {
8399 let name = chart_child_local(&element, &state.namespaces)?;
8400 let raw = capture_element(reader, &element)?;
8401 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
8402 }
8403 Event::Empty(element) => {
8404 let name = chart_child_local(&element, &state.namespaces)?;
8405 let raw = capture_empty_element(&element)?;
8406 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
8407 }
8408 event @ (Event::Text(_)
8409 | Event::CData(_)
8410 | Event::Comment(_)
8411 | Event::PI(_)
8412 | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
8413 Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
8414 return state.finish(raw_attributes, namespace_declarations);
8415 }
8416 Event::Eof => return Err(missing_end("c:dLbls")),
8417 _ => {}
8418 }
8419 buffer.clear();
8420 }
8421 }
8422
8423 pub fn to_xml(&self) -> Result<Vec<u8>> {
8424 let mut writer = Writer::new(Vec::new());
8425 self.write_xml(&mut writer, true)?;
8426 Ok(writer.into_inner())
8427 }
8428
8429 fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
8430 self.validate()?;
8431 let mut start = BytesStart::new("c:dLbls");
8432 if standalone {
8433 start.push_attribute(("xmlns:c", C_NS));
8434 start.push_attribute(("xmlns:a", A_NS));
8435 start.push_attribute(("xmlns:r", R_NS));
8436 }
8437 push_attributes(&mut start, &self.namespace_declarations);
8438 push_attributes(&mut start, &self.raw_attributes);
8439 writer
8440 .write_event(Event::Start(start))
8441 .map_err(OxmlError::from)?;
8442 emit_raw(writer, self.raw_children.at(0))?;
8443 if let Some(number_format) = &self.number_format {
8444 number_format.write_xml(writer)?;
8445 }
8446 emit_raw(writer, self.raw_children.at(1))?;
8447 if let Some(position) = self.position {
8448 write_scalar(
8449 writer,
8450 "c:dLblPos",
8451 position.as_str(),
8452 self.position_markup.as_ref(),
8453 )?;
8454 }
8455 emit_raw(writer, self.raw_children.at(2))?;
8456 write_optional_bool(
8457 writer,
8458 "c:showLegendKey",
8459 self.show_legend_key,
8460 self.show_legend_key_markup.as_ref(),
8461 )?;
8462 emit_raw(writer, self.raw_children.at(3))?;
8463 write_optional_bool(
8464 writer,
8465 "c:showVal",
8466 self.show_value,
8467 self.show_value_markup.as_ref(),
8468 )?;
8469 emit_raw(writer, self.raw_children.at(4))?;
8470 write_optional_bool(
8471 writer,
8472 "c:showCatName",
8473 self.show_category_name,
8474 self.show_category_name_markup.as_ref(),
8475 )?;
8476 emit_raw(writer, self.raw_children.at(5))?;
8477 write_optional_bool(
8478 writer,
8479 "c:showSerName",
8480 self.show_series_name,
8481 self.show_series_name_markup.as_ref(),
8482 )?;
8483 emit_raw(writer, self.raw_children.at(6))?;
8484 write_optional_bool(
8485 writer,
8486 "c:showPercent",
8487 self.show_percent,
8488 self.show_percent_markup.as_ref(),
8489 )?;
8490 emit_raw(writer, self.raw_children.at(7))?;
8491 write_optional_bool(
8492 writer,
8493 "c:showBubbleSize",
8494 self.show_bubble_size,
8495 self.show_bubble_size_markup.as_ref(),
8496 )?;
8497 emit_raw(writer, self.raw_children.at(8))?;
8498 if let Some(separator) = &self.separator {
8499 write_text(
8500 writer,
8501 "c:separator",
8502 separator,
8503 self.separator_markup
8504 .as_ref()
8505 .unwrap_or(&TextMarkup::default()),
8506 )?;
8507 }
8508 emit_raw(writer, self.raw_children.at(9))?;
8509 writer
8510 .write_event(Event::End(BytesEnd::new("c:dLbls")))
8511 .map_err(OxmlError::from)?;
8512 Ok(())
8513 }
8514
8515 fn validate(&self) -> Result<()> {
8516 if let Some(number_format) = &self.number_format {
8517 number_format.validate()?;
8518 }
8519 if let Some(separator) = &self.separator {
8520 validate_xml_text(separator, "c:separator")?;
8521 }
8522 Ok(())
8523 }
8524
8525 pub fn raw_children(&self) -> &OrderedRawChildren {
8526 &self.raw_children
8527 }
8528}
8529
8530struct DataLabelsParseState {
8531 namespaces: NamespaceBindings,
8532 number_format: Option<NumberFormat>,
8533 position: Option<(DataLabelPosition, ScalarMarkup)>,
8534 show_legend_key: Option<(bool, ScalarMarkup)>,
8535 show_value: Option<(bool, ScalarMarkup)>,
8536 show_category_name: Option<(bool, ScalarMarkup)>,
8537 show_series_name: Option<(bool, ScalarMarkup)>,
8538 show_percent: Option<(bool, ScalarMarkup)>,
8539 show_bubble_size: Option<(bool, ScalarMarkup)>,
8540 separator: Option<(String, TextMarkup)>,
8541 raw_children: OrderedRawChildren,
8542 boundary: usize,
8543}
8544
8545impl DataLabelsParseState {
8546 fn new(namespaces: NamespaceBindings) -> Self {
8547 Self {
8548 namespaces,
8549 number_format: None,
8550 position: None,
8551 show_legend_key: None,
8552 show_value: None,
8553 show_category_name: None,
8554 show_series_name: None,
8555 show_percent: None,
8556 show_bubble_size: None,
8557 separator: None,
8558 raw_children: OrderedRawChildren::default(),
8559 boundary: 0,
8560 }
8561 }
8562
8563 fn capture_event(&mut self, raw: Vec<u8>) {
8564 self.raw_children.push(self.boundary, raw);
8565 }
8566
8567 fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
8568 match name {
8569 b"numFmt" => {
8570 set_once(
8571 &mut self.number_format,
8572 NumberFormat::from_xml(&raw)?,
8573 "c:numFmt",
8574 )?;
8575 self.boundary = self.boundary.max(1);
8576 }
8577 b"dLblPos" => {
8578 let (value, markup) = scalar_value(&raw, "dLblPos")?;
8579 let lexical = value
8580 .ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
8581 let position = DataLabelPosition::parse(&lexical)
8582 .ok_or_else(|| invalid_attribute("dLblPos", "val", lexical))?;
8583 set_once(&mut self.position, (position, markup), "c:dLblPos")?;
8584 self.boundary = self.boundary.max(2);
8585 }
8586 b"showLegendKey" => {
8587 set_once(
8588 &mut self.show_legend_key,
8589 parse_bool_value(&raw, "showLegendKey")?,
8590 "c:showLegendKey",
8591 )?;
8592 self.boundary = self.boundary.max(3);
8593 }
8594 b"showVal" => {
8595 set_once(
8596 &mut self.show_value,
8597 parse_bool_value(&raw, "showVal")?,
8598 "c:showVal",
8599 )?;
8600 self.boundary = self.boundary.max(4);
8601 }
8602 b"showCatName" => {
8603 set_once(
8604 &mut self.show_category_name,
8605 parse_bool_value(&raw, "showCatName")?,
8606 "c:showCatName",
8607 )?;
8608 self.boundary = self.boundary.max(5);
8609 }
8610 b"showSerName" => {
8611 set_once(
8612 &mut self.show_series_name,
8613 parse_bool_value(&raw, "showSerName")?,
8614 "c:showSerName",
8615 )?;
8616 self.boundary = self.boundary.max(6);
8617 }
8618 b"showPercent" => {
8619 set_once(
8620 &mut self.show_percent,
8621 parse_bool_value(&raw, "showPercent")?,
8622 "c:showPercent",
8623 )?;
8624 self.boundary = self.boundary.max(7);
8625 }
8626 b"showBubbleSize" => {
8627 set_once(
8628 &mut self.show_bubble_size,
8629 parse_bool_value(&raw, "showBubbleSize")?,
8630 "c:showBubbleSize",
8631 )?;
8632 self.boundary = self.boundary.max(8);
8633 }
8634 b"separator" => {
8635 set_once(
8636 &mut self.separator,
8637 parse_text_element(&raw, b"separator", &self.namespaces)?,
8638 "c:separator",
8639 )?;
8640 self.boundary = self.boundary.max(9);
8641 }
8642 _ => {
8643 let boundary = data_labels_raw_boundary(name, self.boundary);
8644 self.raw_children.push(boundary, raw);
8645 self.boundary = self.boundary.max(boundary);
8646 }
8647 }
8648 Ok(())
8649 }
8650
8651 fn finish(
8652 self,
8653 raw_attributes: XmlAttributes,
8654 namespace_declarations: XmlAttributes,
8655 ) -> Result<CT_DLbls> {
8656 let (position, position_markup) = optional_markup(self.position);
8657 let (show_legend_key, show_legend_key_markup) = optional_bool_markup(self.show_legend_key);
8658 let (show_value, show_value_markup) = optional_bool_markup(self.show_value);
8659 let (show_category_name, show_category_name_markup) =
8660 optional_bool_markup(self.show_category_name);
8661 let (show_series_name, show_series_name_markup) =
8662 optional_bool_markup(self.show_series_name);
8663 let (show_percent, show_percent_markup) = optional_bool_markup(self.show_percent);
8664 let (show_bubble_size, show_bubble_size_markup) =
8665 optional_bool_markup(self.show_bubble_size);
8666 let (separator, separator_markup) = self
8667 .separator
8668 .map(|(value, markup)| (Some(value), Some(markup)))
8669 .unwrap_or((None, None));
8670 let labels = CT_DLbls {
8671 number_format: self.number_format,
8672 position,
8673 separator,
8674 show_legend_key,
8675 show_value,
8676 show_category_name,
8677 show_series_name,
8678 show_percent,
8679 show_bubble_size,
8680 position_markup,
8681 show_legend_key_markup,
8682 show_value_markup,
8683 show_category_name_markup,
8684 show_series_name_markup,
8685 show_percent_markup,
8686 show_bubble_size_markup,
8687 separator_markup,
8688 raw_attributes,
8689 namespace_declarations,
8690 raw_children: raw_children_in_schema_order(&self.raw_children, 9),
8691 };
8692 labels.validate()?;
8693 Ok(labels)
8694 }
8695}
8696
8697fn data_labels_raw_boundary(name: &[u8], current: usize) -> usize {
8698 match name {
8699 b"dLbl" | b"delete" => 0,
8700 b"spPr" | b"txPr" => 1,
8701 b"showLeaderLines" | b"leaderLines" | b"extLst" => 9,
8702 _ => current,
8703 }
8704}
8705
8706fn optional_bool_markup(value: Option<(bool, ScalarMarkup)>) -> (bool, Option<ScalarMarkup>) {
8707 value
8708 .map(|(value, markup)| (value, Some(markup)))
8709 .unwrap_or((false, None))
8710}
8711
8712fn write_optional_bool(
8713 writer: &mut Writer<Vec<u8>>,
8714 tag: &str,
8715 value: bool,
8716 markup: Option<&ScalarMarkup>,
8717) -> Result<()> {
8718 if value || markup.is_some() {
8719 write_scalar(writer, tag, bool_lexical(value), markup)?;
8720 }
8721 Ok(())
8722}
8723
8724fn default_axis_id_markup(id: AxisId) -> AxisIdMarkup {
8725 AxisIdMarkup {
8726 scalar: ScalarMarkup::default(),
8727 parsed: id,
8728 lexical: id.value().to_string(),
8729 }
8730}
8731
8732fn parse_axis_id_scalar(xml: &[u8], local: &str) -> Result<(AxisId, AxisIdMarkup)> {
8733 let (value, scalar) = scalar_value(xml, local)?;
8734 let lexical = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8735 let parsed = lexical
8736 .parse::<i64>()
8737 .map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
8738 let id = AxisId::new(parsed).map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
8739 Ok((
8740 id,
8741 AxisIdMarkup {
8742 scalar,
8743 parsed: id,
8744 lexical,
8745 },
8746 ))
8747}
8748
8749fn write_axis_id<W: Write>(
8750 writer: &mut Writer<W>,
8751 tag: &str,
8752 id: AxisId,
8753 markup: &AxisIdMarkup,
8754) -> Result<()> {
8755 AxisId::new(id.value())?;
8756 let normalized;
8757 let value = if id == markup.parsed {
8758 markup.lexical.as_str()
8759 } else {
8760 normalized = id.value().to_string();
8761 normalized.as_str()
8762 };
8763 write_scalar(writer, tag, value, Some(&markup.scalar))
8764}
8765
8766fn parse_f64_scalar(xml: &[u8], local: &str) -> Result<(f64, ScalarMarkup)> {
8767 let (value, markup) = scalar_value(xml, local)?;
8768 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8769 let parsed = value
8770 .parse::<f64>()
8771 .map_err(|_| invalid_attribute(local, "val", value.clone()))?;
8772 if !parsed.is_finite() {
8773 return Err(invalid_attribute(local, "val", value));
8774 }
8775 Ok((parsed, markup))
8776}
8777
8778fn parse_axis_position(xml: &[u8]) -> Result<(AxisPosition, ScalarMarkup)> {
8779 let (value, markup) = scalar_value(xml, "axPos")?;
8780 let value = value.ok_or_else(|| invalid_attribute("axPos", "val", "<missing>".to_owned()))?;
8781 let parsed =
8782 AxisPosition::parse(&value).ok_or_else(|| invalid_attribute("axPos", "val", value))?;
8783 Ok((parsed, markup))
8784}
8785
8786fn parse_tick_mark(xml: &[u8], local: &str) -> Result<(TickMark, ScalarMarkup)> {
8787 let (value, markup) = scalar_value(xml, local)?;
8788 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8789 let parsed = TickMark::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
8790 Ok((parsed, markup))
8791}
8792
8793fn parse_tick_label_position(xml: &[u8]) -> Result<(TickLabelPosition, ScalarMarkup)> {
8794 let (value, markup) = scalar_value(xml, "tickLblPos")?;
8795 let value =
8796 value.ok_or_else(|| invalid_attribute("tickLblPos", "val", "<missing>".to_owned()))?;
8797 let parsed = TickLabelPosition::parse(&value)
8798 .ok_or_else(|| invalid_attribute("tickLblPos", "val", value))?;
8799 Ok((parsed, markup))
8800}
8801
8802fn optional_markup<T>(value: Option<(T, ScalarMarkup)>) -> (Option<T>, Option<ScalarMarkup>) {
8803 value
8804 .map(|(value, markup)| (Some(value), Some(markup)))
8805 .unwrap_or((None, None))
8806}
8807
8808fn optional_scalar_markup_eq(left: &Option<ScalarMarkup>, right: &Option<ScalarMarkup>) -> bool {
8809 let empty = ScalarMarkup::default();
8810 left.as_ref().unwrap_or(&empty) == right.as_ref().unwrap_or(&empty)
8811}
8812
8813fn axis_id_markup_eq(left: &AxisIdMarkup, right: &AxisIdMarkup) -> bool {
8814 left.scalar == right.scalar
8815}
8816
8817const fn bool_lexical(value: bool) -> &'static str {
8818 if value { "1" } else { "0" }
8819}
8820
8821fn parse_bool_lexical(element: &str, attribute: &str, value: &str) -> Result<bool> {
8822 match value {
8823 "1" | "true" => Ok(true),
8824 "0" | "false" => Ok(false),
8825 _ => Err(invalid_attribute(element, attribute, value.to_owned())),
8826 }
8827}
8828
8829impl CT_Title {
8830 fn plain_text(text: &str) -> Self {
8831 let mut body = CT_TextBody::new();
8832 body.set_text(text);
8833 let mut raw_children = OrderedRawChildren::default();
8834 raw_children.push(1, br#"<c:layout/><c:overlay val="0"/>"#.to_vec());
8835 Self {
8836 text: Some(body),
8837 raw_children,
8838 ..Self::default()
8839 }
8840 }
8841
8842 fn from_xml(xml: &[u8]) -> Result<Self> {
8843 let (raw_attributes, raw_children) = parse_raw_shell(xml, b"title", "c:title")?;
8844 Ok(Self {
8845 text: None,
8846 raw_attributes,
8847 raw_children,
8848 })
8849 }
8850
8851 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8852 if self.text.is_none() {
8853 return write_raw_shell(writer, "c:title", &self.raw_attributes, &self.raw_children);
8854 }
8855 let mut start = BytesStart::new("c:title");
8856 push_attributes(&mut start, &self.raw_attributes);
8857 writer
8858 .write_event(Event::Start(start))
8859 .map_err(OxmlError::from)?;
8860 emit_raw(writer, self.raw_children.at(0))?;
8861 writer
8862 .write_event(Event::Start(BytesStart::new("c:tx")))
8863 .map_err(OxmlError::from)?;
8864 if let Some(text) = &self.text {
8865 text.write_xml_as(writer, "c:rich")?;
8866 }
8867 writer
8868 .write_event(Event::End(BytesEnd::new("c:tx")))
8869 .map_err(OxmlError::from)?;
8870 emit_raw(writer, self.raw_children.at(1))?;
8871 writer
8872 .write_event(Event::End(BytesEnd::new("c:title")))
8873 .map_err(OxmlError::from)?;
8874 Ok(())
8875 }
8876
8877 pub fn raw_children(&self) -> &OrderedRawChildren {
8878 &self.raw_children
8879 }
8880}
8881
8882impl CT_PlotArea {
8883 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
8884 let (raw_attributes, captured_children) = parse_raw_shell(xml, b"plotArea", "c:plotArea")?;
8885 let namespace_bindings = root_chart_bindings(xml, b"plotArea", inherited)?;
8886 let direct_roots: Vec<_> = captured_children
8887 .at(0)
8888 .filter_map(|raw| chart_root_local(raw, &namespace_bindings).transpose())
8889 .collect::<Result<_>>()?;
8890 let plot_roots: Vec<_> = direct_roots
8891 .iter()
8892 .filter(|local| is_plot_container(local))
8893 .cloned()
8894 .collect();
8895 let typed_local = (plot_roots.len() == 1
8896 && matches!(
8897 plot_roots[0].as_slice(),
8898 b"barChart"
8899 | b"lineChart"
8900 | b"pieChart"
8901 | b"doughnutChart"
8902 | b"areaChart"
8903 | b"scatterChart"
8904 | b"radarChart"
8905 ))
8906 .then(|| plot_roots[0].clone());
8907
8908 let Some(typed_local) = typed_local else {
8909 return Ok(Self {
8910 raw_attributes,
8911 raw_children: captured_children,
8912 namespace_bindings,
8913 plots: None,
8914 plot_markup: Vec::new(),
8915 axes: Vec::new(),
8916 });
8917 };
8918
8919 let mut raw_children = OrderedRawChildren::default();
8920 let mut plots = Vec::new();
8921 let mut plot_markup = Vec::new();
8922 let mut axes = Vec::new();
8923 let mut boundary = 0usize;
8924 for raw in captured_children.at(0) {
8925 let local = chart_root_local(raw, &namespace_bindings)?;
8926 if local.as_deref() == Some(typed_local.as_slice()) {
8927 let (plot, markup) = parse_plot(raw, &namespace_bindings)?;
8928 plots.push(plot);
8929 plot_markup.push(markup);
8930 boundary = 1;
8931 } else if local.as_deref().and_then(AxisKind::parse).is_some() {
8932 axes.push(Axis::from_xml_with_namespaces(raw, &namespace_bindings)?);
8933 boundary = axes.len() + 1;
8934 } else {
8935 raw_children.push(boundary, raw.to_vec());
8936 }
8937 }
8938 let plot_area = Self {
8939 raw_attributes,
8940 raw_children,
8941 namespace_bindings,
8942 plots: Some(plots),
8943 plot_markup,
8944 axes,
8945 };
8946 plot_area.validate_typed()?;
8947 Ok(plot_area)
8948 }
8949
8950 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8951 let Some(plots) = &self.plots else {
8952 return write_raw_shell(
8953 writer,
8954 "c:plotArea",
8955 &self.raw_attributes,
8956 &self.raw_children,
8957 );
8958 };
8959 self.validate_typed()?;
8960 let mut start = BytesStart::new("c:plotArea");
8961 push_attributes(&mut start, &self.raw_attributes);
8962 writer
8963 .write_event(Event::Start(start))
8964 .map_err(OxmlError::from)?;
8965 emit_raw(writer, self.raw_children.at(0))?;
8966 for (index, plot) in plots.iter().enumerate() {
8967 write_plot(
8968 writer,
8969 plot,
8970 self.plot_markup
8971 .get(index)
8972 .unwrap_or(&PlotMarkup::default()),
8973 )?;
8974 }
8975 emit_raw(writer, self.raw_children.at(1))?;
8976 for (index, axis) in self.axes.iter().enumerate() {
8977 writer
8978 .get_mut()
8979 .write_all(&axis.to_xml()?)
8980 .map_err(OxmlError::from)?;
8981 emit_raw(writer, self.raw_children.at(index + 2))?;
8982 }
8983 writer
8984 .write_event(Event::End(BytesEnd::new("c:plotArea")))
8985 .map_err(OxmlError::from)?;
8986 Ok(())
8987 }
8988
8989 pub fn raw_children(&self) -> &OrderedRawChildren {
8990 &self.raw_children
8991 }
8992
8993 pub fn new(plots: Vec<Plot>, axes: Vec<Axis>) -> Result<Self> {
8995 let plot_markup = plots
8996 .iter()
8997 .map(|plot| {
8998 let mut markup = PlotMarkup::default();
8999 if matches!(plot, Plot::Line { .. }) {
9000 markup.marker = Some(ScalarMarkup::default());
9001 markup.smooth = Some(ScalarMarkup::default());
9002 }
9003 if matches!(plot, Plot::Doughnut { .. }) {
9004 markup.hole_size = Some(ScalarMarkup::default());
9005 }
9006 markup
9007 })
9008 .collect();
9009 let plot_area = Self {
9010 raw_attributes: Vec::new(),
9011 raw_children: OrderedRawChildren::default(),
9012 namespace_bindings: chart_namespace_defaults(),
9013 plot_markup,
9014 plots: Some(plots),
9015 axes,
9016 };
9017 plot_area.validate_typed()?;
9018 Ok(plot_area)
9019 }
9020
9021 pub fn plots(&self) -> Result<&[Plot]> {
9023 self.plots
9024 .as_deref()
9025 .ok_or_else(|| ChartError::InvalidValue {
9026 element: "c:plotArea".to_owned(),
9027 value: "unsupported or combination plot area is opaque".to_owned(),
9028 })
9029 }
9030
9031 pub fn plots_mut(&mut self) -> Result<&mut [Plot]> {
9033 self.plots
9034 .as_deref_mut()
9035 .ok_or_else(|| ChartError::InvalidValue {
9036 element: "c:plotArea".to_owned(),
9037 value: "cannot combine typed plots with an opaque plot choice".to_owned(),
9038 })
9039 }
9040
9041 pub fn series(&self) -> Result<Vec<Series>> {
9043 if let Some(plots) = &self.plots {
9044 return Ok(plots
9045 .iter()
9046 .flat_map(|plot| match plot {
9047 Plot::Bar { series, .. }
9048 | Plot::Line { series, .. }
9049 | Plot::Pie { series, .. }
9050 | Plot::Doughnut { series, .. }
9051 | Plot::Area { series, .. }
9052 | Plot::Scatter { series, .. }
9053 | Plot::Radar { series, .. } => series.clone(),
9054 })
9055 .collect());
9056 }
9057 let mut series = Vec::new();
9058 for raw in self.raw_children.at(0) {
9059 parse_plot_series(raw, &self.namespace_bindings, &mut series)?;
9060 }
9061 Ok(series)
9062 }
9063
9064 pub fn axes(&self) -> Result<Vec<Axis>> {
9066 if self.plots.is_some() {
9067 self.validate_typed()?;
9068 return Ok(self.axes.clone());
9069 }
9070 let mut axes = Vec::new();
9071 for raw in self.raw_children.at(0) {
9072 if let Some(axis) = parse_plot_axis(raw, &self.namespace_bindings)? {
9073 axes.push(axis);
9074 }
9075 }
9076 validate_axis_pairs(&axes)?;
9077 Ok(axes)
9078 }
9079
9080 fn validate_typed(&self) -> Result<()> {
9081 let plots = self
9082 .plots
9083 .as_ref()
9084 .ok_or_else(|| ChartError::InvalidValue {
9085 element: "c:plotArea".to_owned(),
9086 value: "opaque plot area has no typed validation view".to_owned(),
9087 })?;
9088 if plots.len() != 1 {
9089 return Err(ChartError::InvalidValue {
9090 element: "c:plotArea".to_owned(),
9091 value: "typed plot area requires exactly one plot family".to_owned(),
9092 });
9093 }
9094 validate_axis_pairs(&self.axes)?;
9095 for plot in plots {
9096 plot.validate()?;
9097 if let Some(axis_ids) = plot.axis_ids() {
9098 for id in axis_ids {
9099 if !self.axes.iter().any(|axis| axis.id == id) {
9100 return Err(ChartError::InvalidValue {
9101 element: "c:axId".to_owned(),
9102 value: format!("plot references missing axis {}", id.value()),
9103 });
9104 }
9105 }
9106 } else if !self.axes.is_empty() {
9107 return Err(ChartError::InvalidValue {
9108 element: "c:plotArea".to_owned(),
9109 value: "pie and doughnut plot areas must not own axes".to_owned(),
9110 });
9111 }
9112 if plot.axis_ids().is_some() && self.axes.len() != 2 {
9113 return Err(ChartError::InvalidValue {
9114 element: "c:plotArea".to_owned(),
9115 value: format!(
9116 "axis-owned plot requires exactly 2 axes, found {}",
9117 self.axes.len()
9118 ),
9119 });
9120 }
9121 }
9122 Ok(())
9123 }
9124}
9125
9126impl Default for CT_PlotArea {
9127 fn default() -> Self {
9128 Self {
9129 raw_attributes: Vec::new(),
9130 raw_children: OrderedRawChildren::default(),
9131 namespace_bindings: chart_namespace_defaults(),
9132 plots: Some(Vec::new()),
9133 plot_markup: Vec::new(),
9134 axes: Vec::new(),
9135 }
9136 }
9137}
9138
9139fn chart_root_local(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Vec<u8>>> {
9140 let mut reader = Reader::from_reader(xml);
9141 let mut buffer = Vec::new();
9142 loop {
9143 match reader
9144 .read_event_into(&mut buffer)
9145 .map_err(OxmlError::from)?
9146 {
9147 Event::Start(element) | Event::Empty(element) => {
9148 return Ok(element_is_in_namespace(&element, C_NS, inherited)?
9149 .then(|| local_name(element.name().as_ref()).to_vec()));
9150 }
9151 Event::Eof => return Ok(None),
9152 _ => {}
9153 }
9154 buffer.clear();
9155 }
9156}
9157
9158fn is_plot_container(local: &[u8]) -> bool {
9159 matches!(
9160 local,
9161 b"areaChart"
9162 | b"area3DChart"
9163 | b"barChart"
9164 | b"bar3DChart"
9165 | b"bubbleChart"
9166 | b"doughnutChart"
9167 | b"lineChart"
9168 | b"line3DChart"
9169 | b"ofPieChart"
9170 | b"pieChart"
9171 | b"pie3DChart"
9172 | b"radarChart"
9173 | b"scatterChart"
9174 | b"stockChart"
9175 | b"surfaceChart"
9176 | b"surface3DChart"
9177 )
9178}
9179
9180fn parse_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
9181 let local = chart_root_local(xml, inherited)?
9182 .ok_or_else(|| ChartError::MissingElement("c:barChart or c:lineChart".to_owned()))?;
9183 match local.as_slice() {
9184 b"barChart" => parse_bar_plot(xml, inherited),
9185 b"lineChart" => parse_line_plot(xml, inherited),
9186 b"pieChart" | b"doughnutChart" | b"areaChart" | b"scatterChart" | b"radarChart" => {
9187 parse_remaining_plot(xml, inherited, &local)
9188 }
9189 _ => Err(ChartError::UnexpectedElement(format!(
9190 "c:{}",
9191 String::from_utf8_lossy(&local)
9192 ))),
9193 }
9194}
9195
9196fn parse_remaining_plot(
9197 xml: &[u8],
9198 inherited: &NamespaceBindings,
9199 local: &[u8],
9200) -> Result<(Plot, PlotMarkup)> {
9201 let root = String::from_utf8_lossy(local).into_owned();
9202 let mut reader = Reader::from_reader(xml);
9203 let mut buffer = Vec::new();
9204 loop {
9205 match reader
9206 .read_event_into(&mut buffer)
9207 .map_err(OxmlError::from)?
9208 {
9209 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
9210 let namespaces = typed_rewrite_bindings(&element, inherited)?;
9211 let (mut raw_attributes, namespace_declarations) =
9212 capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
9213 raw_attributes.splice(0..0, namespace_declarations);
9214 let mut grouping = None;
9215 let mut style = None;
9216 let mut first_slice_angle = None;
9217 let mut hole_size = None;
9218 let mut series = Vec::new();
9219 let mut data_labels = None;
9220 let mut axis_ids = Vec::new();
9221 let mut raw_children = OrderedRawChildren::default();
9222 let mut boundary = 0usize;
9223 let mut inner = Vec::new();
9224 loop {
9225 match reader
9226 .read_event_into(&mut inner)
9227 .map_err(OxmlError::from)?
9228 {
9229 Event::Start(child) => {
9230 let name = chart_child_local(&child, &namespaces)?;
9231 let raw = capture_element(&mut reader, &child)?;
9232 parse_remaining_plot_child(
9233 local,
9234 name.as_deref().unwrap_or_default(),
9235 raw,
9236 &namespaces,
9237 &mut grouping,
9238 &mut style,
9239 &mut first_slice_angle,
9240 &mut hole_size,
9241 &mut series,
9242 &mut data_labels,
9243 &mut axis_ids,
9244 &mut raw_children,
9245 &mut boundary,
9246 )?;
9247 }
9248 Event::Empty(child) => {
9249 let name = chart_child_local(&child, &namespaces)?;
9250 let raw = capture_empty_element(&child)?;
9251 parse_remaining_plot_child(
9252 local,
9253 name.as_deref().unwrap_or_default(),
9254 raw,
9255 &namespaces,
9256 &mut grouping,
9257 &mut style,
9258 &mut first_slice_angle,
9259 &mut hole_size,
9260 &mut series,
9261 &mut data_labels,
9262 &mut axis_ids,
9263 &mut raw_children,
9264 &mut boundary,
9265 )?;
9266 }
9267 event @ (Event::Text(_)
9268 | Event::CData(_)
9269 | Event::Comment(_)
9270 | Event::PI(_)
9271 | Event::GeneralRef(_)) => {
9272 raw_children.push(boundary, capture_event(event)?);
9273 }
9274 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
9275 let needs_axes =
9276 matches!(local, b"areaChart" | b"scatterChart" | b"radarChart");
9277 if needs_axes && axis_ids.len() != 2 {
9278 return Err(ChartError::InvalidValue {
9279 element: format!("c:{root}/c:axId"),
9280 value: format!("expected 2, found {}", axis_ids.len()),
9281 });
9282 }
9283 if !needs_axes && !axis_ids.is_empty() {
9284 return Err(ChartError::InvalidValue {
9285 element: format!("c:{root}/c:axId"),
9286 value: "axis-free plot contains axis references".to_owned(),
9287 });
9288 }
9289 let parsed_ids = needs_axes.then(|| [axis_ids[0].0, axis_ids[1].0]);
9290 let original_series_keys = series
9291 .iter()
9292 .map(|item| (item.index, item.order))
9293 .collect::<Vec<_>>();
9294 let plot = match local {
9295 b"pieChart" => Plot::Pie {
9296 first_slice_angle: first_slice_angle
9297 .as_ref()
9298 .map_or(0, |item| item.0),
9299 series,
9300 data_labels,
9301 },
9302 b"doughnutChart" => Plot::Doughnut {
9303 first_slice_angle: first_slice_angle
9304 .as_ref()
9305 .map_or(0, |item| item.0),
9306 hole_size: hole_size.as_ref().map_or(50, |item| item.0),
9307 series,
9308 data_labels,
9309 },
9310 b"areaChart" => Plot::Area {
9311 grouping: grouping
9312 .as_ref()
9313 .ok_or_else(|| {
9314 ChartError::MissingElement("c:grouping".to_owned())
9315 })?
9316 .0,
9317 series,
9318 data_labels,
9319 axis_ids: parsed_ids.ok_or_else(|| {
9320 ChartError::MissingElement("c:areaChart/c:axId".to_owned())
9321 })?,
9322 },
9323 b"scatterChart" => {
9324 let lexical = style
9325 .as_ref()
9326 .ok_or_else(|| {
9327 ChartError::MissingElement("c:scatterStyle".to_owned())
9328 })?
9329 .0
9330 .as_str();
9331 Plot::Scatter {
9332 style: ScatterStyle::parse(lexical).ok_or_else(|| {
9333 invalid_attribute(
9334 "scatterStyle",
9335 "val",
9336 lexical.to_owned(),
9337 )
9338 })?,
9339 series,
9340 data_labels,
9341 axis_ids: parsed_ids.ok_or_else(|| {
9342 ChartError::MissingElement(
9343 "c:scatterChart/c:axId".to_owned(),
9344 )
9345 })?,
9346 }
9347 }
9348 b"radarChart" => {
9349 let lexical = style
9350 .as_ref()
9351 .ok_or_else(|| {
9352 ChartError::MissingElement("c:radarStyle".to_owned())
9353 })?
9354 .0
9355 .as_str();
9356 Plot::Radar {
9357 style: RadarStyle::parse(lexical).ok_or_else(|| {
9358 invalid_attribute(
9359 "radarStyle",
9360 "val",
9361 lexical.to_owned(),
9362 )
9363 })?,
9364 series,
9365 data_labels,
9366 axis_ids: parsed_ids.ok_or_else(|| {
9367 ChartError::MissingElement(
9368 "c:radarChart/c:axId".to_owned(),
9369 )
9370 })?,
9371 }
9372 }
9373 _ => {
9374 return Err(ChartError::UnexpectedElement(format!(
9375 "c:{}",
9376 String::from_utf8_lossy(local)
9377 )));
9378 }
9379 };
9380 plot.validate()?;
9381 let final_boundary = boundary.max(original_series_keys.len() + 6);
9382 return Ok((
9383 plot,
9384 PlotMarkup {
9385 raw_attributes,
9386 raw_children: raw_children_in_schema_order(
9387 &raw_children,
9388 final_boundary,
9389 ),
9390 grouping: grouping.map(|item| item.1),
9391 first_slice_angle: first_slice_angle.map(|item| item.1),
9392 hole_size: hole_size.map(|item| item.1),
9393 style: style.map(|item| item.1),
9394 axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
9395 original_series_keys,
9396 original_axis_ids: parsed_ids
9397 .map_or_else(Vec::new, |ids| ids.to_vec()),
9398 parsed_remaining: match local {
9399 b"pieChart" => Some("pieChart"),
9400 b"doughnutChart" => Some("doughnutChart"),
9401 b"areaChart" => Some("areaChart"),
9402 b"scatterChart" => Some("scatterChart"),
9403 b"radarChart" => Some("radarChart"),
9404 _ => None,
9405 },
9406 ..PlotMarkup::default()
9407 },
9408 ));
9409 }
9410 Event::Eof => return Err(missing_end(&format!("c:{root}"))),
9411 _ => {}
9412 }
9413 inner.clear();
9414 }
9415 }
9416 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
9417 return Err(ChartError::MissingElement("c:ser".to_owned()));
9418 }
9419 Event::Start(element) | Event::Empty(element) => {
9420 return Err(ChartError::UnexpectedElement(element_name(&element)));
9421 }
9422 Event::Eof => return Err(ChartError::MissingElement(format!("c:{root}"))),
9423 _ => {}
9424 }
9425 buffer.clear();
9426 }
9427}
9428
9429#[allow(clippy::too_many_arguments)]
9430fn parse_remaining_plot_child(
9431 root: &[u8],
9432 name: &[u8],
9433 raw: Vec<u8>,
9434 namespaces: &NamespaceBindings,
9435 grouping: &mut Option<(Grouping, ScalarMarkup)>,
9436 style: &mut Option<(String, ScalarMarkup)>,
9437 first_slice_angle: &mut Option<(u16, ScalarMarkup)>,
9438 hole_size: &mut Option<(u8, ScalarMarkup)>,
9439 series: &mut Vec<Series>,
9440 data_labels: &mut Option<CT_DLbls>,
9441 axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
9442 raw_children: &mut OrderedRawChildren,
9443 boundary: &mut usize,
9444) -> Result<()> {
9445 let property_first = matches!(root, b"areaChart" | b"scatterChart" | b"radarChart");
9446 let base = usize::from(property_first);
9447 match name {
9448 b"grouping" if root == b"areaChart" => {
9449 let (value, markup) = required_scalar(&raw, "grouping")?;
9450 let parsed = Grouping::parse(&value)
9451 .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
9452 set_once(grouping, (parsed, markup), "c:grouping")?;
9453 *boundary = (*boundary).max(1);
9454 }
9455 b"scatterStyle" if root == b"scatterChart" => {
9456 let (value, markup) = required_scalar(&raw, "scatterStyle")?;
9457 if ScatterStyle::parse(&value).is_none() {
9458 return Err(invalid_attribute("scatterStyle", "val", value));
9459 }
9460 set_once(style, (value, markup), "c:scatterStyle")?;
9461 *boundary = (*boundary).max(1);
9462 }
9463 b"radarStyle" if root == b"radarChart" => {
9464 let (value, markup) = required_scalar(&raw, "radarStyle")?;
9465 if RadarStyle::parse(&value).is_none() {
9466 return Err(invalid_attribute("radarStyle", "val", value));
9467 }
9468 set_once(style, (value, markup), "c:radarStyle")?;
9469 *boundary = (*boundary).max(1);
9470 }
9471 b"ser" => {
9472 series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
9473 *boundary = (*boundary).max(base + series.len());
9474 }
9475 b"dLbls" => {
9476 set_once(
9477 data_labels,
9478 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
9479 "c:dLbls",
9480 )?;
9481 *boundary = (*boundary).max(base + series.len() + 1);
9482 }
9483 b"firstSliceAng" if matches!(root, b"pieChart" | b"doughnutChart") => {
9484 let (value, markup) = required_scalar(&raw, "firstSliceAng")?;
9485 let parsed = value
9486 .parse::<u16>()
9487 .map_err(|_| invalid_attribute("firstSliceAng", "val", value.clone()))?;
9488 if parsed > 360 {
9489 return Err(invalid_attribute("firstSliceAng", "val", value));
9490 }
9491 set_once(first_slice_angle, (parsed, markup), "c:firstSliceAng")?;
9492 *boundary = (*boundary).max(series.len() + 2);
9493 }
9494 b"holeSize" if root == b"doughnutChart" => {
9495 let (value, markup) = required_scalar(&raw, "holeSize")?;
9496 let parsed = value
9497 .parse::<u8>()
9498 .map_err(|_| invalid_attribute("holeSize", "val", value.clone()))?;
9499 if !(10..=90).contains(&parsed) {
9500 return Err(invalid_attribute("holeSize", "val", value));
9501 }
9502 set_once(hole_size, (parsed, markup), "c:holeSize")?;
9503 *boundary = (*boundary).max(series.len() + 3);
9504 }
9505 b"axId" if property_first => {
9506 if axis_ids.len() == 2 {
9507 return Err(ChartError::DuplicateElement("c:axId".to_owned()));
9508 }
9509 axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
9510 *boundary = (*boundary).max(base + series.len() + 1 + axis_ids.len());
9511 }
9512 b"axId" => {
9513 return Err(ChartError::InvalidValue {
9514 element: "c:axId".to_owned(),
9515 value: "axis-free plot contains axis reference".to_owned(),
9516 });
9517 }
9518 _ => {
9519 let raw_boundary = match (root, name) {
9520 (_, b"varyColors") => base,
9521 (b"pieChart", b"extLst") => series.len() + 2,
9522 (b"doughnutChart", b"extLst") => series.len() + 3,
9523 (b"areaChart" | b"scatterChart" | b"radarChart", b"dropLines") => {
9524 base + series.len() + 1
9525 }
9526 (b"areaChart" | b"scatterChart" | b"radarChart", b"extLst") => {
9527 base + series.len() + 3
9528 }
9529 _ => *boundary,
9530 };
9531 raw_children.push(raw_boundary, raw);
9532 *boundary = (*boundary).max(raw_boundary);
9533 }
9534 }
9535 Ok(())
9536}
9537
9538fn parse_bar_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
9539 let mut reader = Reader::from_reader(xml);
9540 let mut buffer = Vec::new();
9541 loop {
9542 match reader
9543 .read_event_into(&mut buffer)
9544 .map_err(OxmlError::from)?
9545 {
9546 Event::Start(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
9547 let namespaces = typed_rewrite_bindings(&element, inherited)?;
9548 let (mut raw_attributes, namespace_declarations) =
9549 capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
9550 raw_attributes.splice(0..0, namespace_declarations);
9551 let mut direction = None;
9552 let mut grouping = None;
9553 let mut gap_width = None;
9554 let mut overlap = None;
9555 let mut series = Vec::new();
9556 let mut data_labels = None;
9557 let mut axis_ids = Vec::new();
9558 let mut raw_children = OrderedRawChildren::default();
9559 let mut boundary = 0usize;
9560 let mut inner = Vec::new();
9561 loop {
9562 match reader
9563 .read_event_into(&mut inner)
9564 .map_err(OxmlError::from)?
9565 {
9566 Event::Start(child) => {
9567 let name = chart_child_local(&child, &namespaces)?;
9568 let raw = capture_element(&mut reader, &child)?;
9569 parse_bar_child(
9570 name.as_deref().unwrap_or_default(),
9571 raw,
9572 &namespaces,
9573 &mut direction,
9574 &mut grouping,
9575 &mut gap_width,
9576 &mut overlap,
9577 &mut series,
9578 &mut data_labels,
9579 &mut axis_ids,
9580 &mut raw_children,
9581 &mut boundary,
9582 )?;
9583 }
9584 Event::Empty(child) => {
9585 let name = chart_child_local(&child, &namespaces)?;
9586 let raw = capture_empty_element(&child)?;
9587 parse_bar_child(
9588 name.as_deref().unwrap_or_default(),
9589 raw,
9590 &namespaces,
9591 &mut direction,
9592 &mut grouping,
9593 &mut gap_width,
9594 &mut overlap,
9595 &mut series,
9596 &mut data_labels,
9597 &mut axis_ids,
9598 &mut raw_children,
9599 &mut boundary,
9600 )?;
9601 }
9602 event @ (Event::Text(_)
9603 | Event::CData(_)
9604 | Event::Comment(_)
9605 | Event::PI(_)
9606 | Event::GeneralRef(_)) => {
9607 raw_children.push(boundary, capture_event(event)?);
9608 }
9609 Event::End(end) if matches_local_name(end.name().as_ref(), b"barChart") => {
9610 let (direction, direction_markup) = direction
9611 .ok_or_else(|| ChartError::MissingElement("c:barDir".to_owned()))?;
9612 let (grouping, grouping_markup) = grouping.ok_or_else(|| {
9613 ChartError::MissingElement("c:grouping".to_owned())
9614 })?;
9615 if axis_ids.len() != 2 {
9616 return Err(ChartError::InvalidValue {
9617 element: "c:barChart/c:axId".to_owned(),
9618 value: format!("expected 2, found {}", axis_ids.len()),
9619 });
9620 }
9621 let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
9622 let original_series_keys: Vec<(u32, u32)> =
9623 series.iter().map(|item| (item.index, item.order)).collect();
9624 let plot = Plot::Bar {
9625 direction,
9626 grouping,
9627 gap_width: gap_width.as_ref().map_or(150, |item| item.0),
9628 overlap: overlap.as_ref().map_or(0, |item| item.0),
9629 series,
9630 data_labels,
9631 axis_ids: parsed_ids,
9632 };
9633 plot.validate()?;
9634 return Ok((
9635 plot,
9636 PlotMarkup {
9637 raw_attributes,
9638 raw_children: raw_children_in_schema_order(
9639 &raw_children,
9640 original_series_keys.len() + 7,
9641 ),
9642 direction: Some(direction_markup),
9643 grouping: Some(grouping_markup),
9644 gap_width: gap_width.map(|item| item.1),
9645 overlap: overlap.map(|item| item.1),
9646 marker: None,
9647 smooth: None,
9648 first_slice_angle: None,
9649 hole_size: None,
9650 style: None,
9651 axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
9652 original_series_keys,
9653 original_axis_ids: parsed_ids.to_vec(),
9654 parsed_bar: Some(true),
9655 parsed_remaining: None,
9656 },
9657 ));
9658 }
9659 Event::Eof => return Err(missing_end("c:barChart")),
9660 _ => {}
9661 }
9662 inner.clear();
9663 }
9664 }
9665 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
9666 return Err(ChartError::MissingElement("c:barDir".to_owned()));
9667 }
9668 Event::Start(element) | Event::Empty(element) => {
9669 return Err(ChartError::UnexpectedElement(element_name(&element)));
9670 }
9671 Event::Eof => return Err(ChartError::MissingElement("c:barChart".to_owned())),
9672 _ => {}
9673 }
9674 buffer.clear();
9675 }
9676}
9677
9678#[allow(clippy::too_many_arguments)]
9679fn parse_bar_child(
9680 name: &[u8],
9681 raw: Vec<u8>,
9682 namespaces: &NamespaceBindings,
9683 direction: &mut Option<(BarDirection, ScalarMarkup)>,
9684 grouping: &mut Option<(BarGrouping, ScalarMarkup)>,
9685 gap_width: &mut Option<(u16, ScalarMarkup)>,
9686 overlap: &mut Option<(i8, ScalarMarkup)>,
9687 series: &mut Vec<Series>,
9688 data_labels: &mut Option<CT_DLbls>,
9689 axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
9690 raw_children: &mut OrderedRawChildren,
9691 boundary: &mut usize,
9692) -> Result<()> {
9693 match name {
9694 b"barDir" => {
9695 let (value, markup) = required_scalar(&raw, "barDir")?;
9696 let value = BarDirection::parse(&value)
9697 .ok_or_else(|| invalid_attribute("barDir", "val", value))?;
9698 set_once(direction, (value, markup), "c:barDir")?;
9699 *boundary = (*boundary).max(1);
9700 }
9701 b"grouping" => {
9702 let (value, markup) = required_scalar(&raw, "grouping")?;
9703 let value = BarGrouping::parse(&value)
9704 .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
9705 set_once(grouping, (value, markup), "c:grouping")?;
9706 *boundary = (*boundary).max(2);
9707 }
9708 b"ser" => {
9709 series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
9710 *boundary = (*boundary).max(series.len() + 2);
9711 }
9712 b"dLbls" => {
9713 set_once(
9714 data_labels,
9715 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
9716 "c:dLbls",
9717 )?;
9718 *boundary = (*boundary).max(series.len() + 3);
9719 }
9720 b"gapWidth" => {
9721 let (value, markup) = required_scalar(&raw, "gapWidth")?;
9722 let parsed = value
9723 .parse::<u16>()
9724 .map_err(|_| invalid_attribute("gapWidth", "val", value.clone()))?;
9725 if parsed > 500 {
9726 return Err(invalid_attribute("gapWidth", "val", value));
9727 }
9728 set_once(gap_width, (parsed, markup), "c:gapWidth")?;
9729 *boundary = (*boundary).max(series.len() + 4);
9730 }
9731 b"overlap" => {
9732 let (value, markup) = required_scalar(&raw, "overlap")?;
9733 let parsed = value
9734 .parse::<i8>()
9735 .map_err(|_| invalid_attribute("overlap", "val", value.clone()))?;
9736 if !(-100..=100).contains(&parsed) {
9737 return Err(invalid_attribute("overlap", "val", value));
9738 }
9739 set_once(overlap, (parsed, markup), "c:overlap")?;
9740 *boundary = (*boundary).max(series.len() + 5);
9741 }
9742 b"axId" => {
9743 if axis_ids.len() == 2 {
9744 return Err(ChartError::DuplicateElement("c:axId".to_owned()));
9745 }
9746 axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
9747 *boundary = (*boundary).max(series.len() + 5 + axis_ids.len());
9748 }
9749 _ => raw_children.push(*boundary, raw),
9750 }
9751 Ok(())
9752}
9753
9754fn parse_line_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
9755 let mut reader = Reader::from_reader(xml);
9756 let mut buffer = Vec::new();
9757 loop {
9758 match reader
9759 .read_event_into(&mut buffer)
9760 .map_err(OxmlError::from)?
9761 {
9762 Event::Start(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
9763 let namespaces = typed_rewrite_bindings(&element, inherited)?;
9764 let (mut raw_attributes, namespace_declarations) =
9765 capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
9766 raw_attributes.splice(0..0, namespace_declarations);
9767 let mut grouping = None;
9768 let mut marker = None;
9769 let mut smooth = None;
9770 let mut series = Vec::new();
9771 let mut data_labels = None;
9772 let mut axis_ids = Vec::new();
9773 let mut raw_children = OrderedRawChildren::default();
9774 let mut boundary = 0usize;
9775 let mut inner = Vec::new();
9776 loop {
9777 match reader
9778 .read_event_into(&mut inner)
9779 .map_err(OxmlError::from)?
9780 {
9781 Event::Start(child) => {
9782 let name = chart_child_local(&child, &namespaces)?;
9783 let raw = capture_element(&mut reader, &child)?;
9784 parse_line_child(
9785 name.as_deref().unwrap_or_default(),
9786 raw,
9787 &namespaces,
9788 &mut grouping,
9789 &mut marker,
9790 &mut smooth,
9791 &mut series,
9792 &mut data_labels,
9793 &mut axis_ids,
9794 &mut raw_children,
9795 &mut boundary,
9796 )?;
9797 }
9798 Event::Empty(child) => {
9799 let name = chart_child_local(&child, &namespaces)?;
9800 let raw = capture_empty_element(&child)?;
9801 parse_line_child(
9802 name.as_deref().unwrap_or_default(),
9803 raw,
9804 &namespaces,
9805 &mut grouping,
9806 &mut marker,
9807 &mut smooth,
9808 &mut series,
9809 &mut data_labels,
9810 &mut axis_ids,
9811 &mut raw_children,
9812 &mut boundary,
9813 )?;
9814 }
9815 event @ (Event::Text(_)
9816 | Event::CData(_)
9817 | Event::Comment(_)
9818 | Event::PI(_)
9819 | Event::GeneralRef(_)) => {
9820 raw_children.push(boundary, capture_event(event)?);
9821 }
9822 Event::End(end)
9823 if matches_local_name(end.name().as_ref(), b"lineChart") =>
9824 {
9825 let (grouping, grouping_markup) = grouping.ok_or_else(|| {
9826 ChartError::MissingElement("c:grouping".to_owned())
9827 })?;
9828 if axis_ids.len() != 2 {
9829 return Err(ChartError::InvalidValue {
9830 element: "c:lineChart/c:axId".to_owned(),
9831 value: format!("expected 2, found {}", axis_ids.len()),
9832 });
9833 }
9834 let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
9835 let original_series_keys: Vec<(u32, u32)> =
9836 series.iter().map(|item| (item.index, item.order)).collect();
9837 let plot = Plot::Line {
9838 grouping,
9839 marker: marker.as_ref().is_some_and(|item| item.0),
9840 smooth: smooth.as_ref().is_some_and(|item| item.0),
9841 series,
9842 data_labels,
9843 axis_ids: parsed_ids,
9844 };
9845 plot.validate()?;
9846 return Ok((
9847 plot,
9848 PlotMarkup {
9849 raw_attributes,
9850 raw_children: raw_children_in_schema_order(
9851 &raw_children,
9852 original_series_keys.len() + 6,
9853 ),
9854 direction: None,
9855 grouping: Some(grouping_markup),
9856 gap_width: None,
9857 overlap: None,
9858 marker: marker.map(|item| item.1),
9859 smooth: smooth.map(|item| item.1),
9860 first_slice_angle: None,
9861 hole_size: None,
9862 style: None,
9863 axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
9864 original_series_keys,
9865 original_axis_ids: parsed_ids.to_vec(),
9866 parsed_bar: Some(false),
9867 parsed_remaining: None,
9868 },
9869 ));
9870 }
9871 Event::Eof => return Err(missing_end("c:lineChart")),
9872 _ => {}
9873 }
9874 inner.clear();
9875 }
9876 }
9877 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
9878 return Err(ChartError::MissingElement("c:grouping".to_owned()));
9879 }
9880 Event::Start(element) | Event::Empty(element) => {
9881 return Err(ChartError::UnexpectedElement(element_name(&element)));
9882 }
9883 Event::Eof => return Err(ChartError::MissingElement("c:lineChart".to_owned())),
9884 _ => {}
9885 }
9886 buffer.clear();
9887 }
9888}
9889
9890#[allow(clippy::too_many_arguments)]
9891fn parse_line_child(
9892 name: &[u8],
9893 raw: Vec<u8>,
9894 namespaces: &NamespaceBindings,
9895 grouping: &mut Option<(Grouping, ScalarMarkup)>,
9896 marker: &mut Option<(bool, ScalarMarkup)>,
9897 smooth: &mut Option<(bool, ScalarMarkup)>,
9898 series: &mut Vec<Series>,
9899 data_labels: &mut Option<CT_DLbls>,
9900 axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
9901 raw_children: &mut OrderedRawChildren,
9902 boundary: &mut usize,
9903) -> Result<()> {
9904 match name {
9905 b"grouping" => {
9906 let (value, markup) = required_scalar(&raw, "grouping")?;
9907 let value = Grouping::parse(&value)
9908 .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
9909 set_once(grouping, (value, markup), "c:grouping")?;
9910 *boundary = (*boundary).max(1);
9911 }
9912 b"ser" => {
9913 series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
9914 *boundary = (*boundary).max(series.len() + 1);
9915 }
9916 b"dLbls" => {
9917 set_once(
9918 data_labels,
9919 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
9920 "c:dLbls",
9921 )?;
9922 *boundary = (*boundary).max(series.len() + 2);
9923 }
9924 b"marker" => {
9925 set_once(marker, parse_bool_value(&raw, "marker")?, "c:marker")?;
9926 *boundary = (*boundary).max(series.len() + 3);
9927 }
9928 b"smooth" => {
9929 set_once(smooth, parse_bool_value(&raw, "smooth")?, "c:smooth")?;
9930 *boundary = (*boundary).max(series.len() + 4);
9931 }
9932 b"axId" => {
9933 if axis_ids.len() == 2 {
9934 return Err(ChartError::DuplicateElement("c:axId".to_owned()));
9935 }
9936 axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
9937 *boundary = (*boundary).max(series.len() + 4 + axis_ids.len());
9938 }
9939 _ => raw_children.push(*boundary, raw),
9940 }
9941 Ok(())
9942}
9943
9944fn required_scalar(xml: &[u8], local: &str) -> Result<(String, ScalarMarkup)> {
9945 let (value, markup) = scalar_value(xml, local)?;
9946 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
9947 Ok((value, markup))
9948}
9949
9950fn write_plot<W: Write>(writer: &mut Writer<W>, plot: &Plot, markup: &PlotMarkup) -> Result<()> {
9951 plot.validate()?;
9952 if let Some(parsed_bar) = markup.parsed_bar
9953 && parsed_bar != matches!(plot, Plot::Bar { .. })
9954 {
9955 return Err(ChartError::InvalidValue {
9956 element: "c:plotArea".to_owned(),
9957 value: "a parsed plot family cannot be replaced while preserved payload remains"
9958 .to_owned(),
9959 });
9960 }
9961 let current_remaining = match plot {
9962 Plot::Pie { .. } => Some("pieChart"),
9963 Plot::Doughnut { .. } => Some("doughnutChart"),
9964 Plot::Area { .. } => Some("areaChart"),
9965 Plot::Scatter { .. } => Some("scatterChart"),
9966 Plot::Radar { .. } => Some("radarChart"),
9967 Plot::Bar { .. } | Plot::Line { .. } => None,
9968 };
9969 if let Some(parsed) = markup.parsed_remaining
9970 && current_remaining != Some(parsed)
9971 {
9972 return Err(ChartError::InvalidValue {
9973 element: "c:plotArea".to_owned(),
9974 value: "a parsed plot family cannot be replaced while preserved payload remains"
9975 .to_owned(),
9976 });
9977 }
9978 match plot {
9979 Plot::Bar {
9980 direction,
9981 grouping,
9982 gap_width,
9983 overlap,
9984 series,
9985 data_labels,
9986 axis_ids,
9987 } => {
9988 let mut start = BytesStart::new("c:barChart");
9989 push_attributes(&mut start, &markup.raw_attributes);
9990 writer
9991 .write_event(Event::Start(start))
9992 .map_err(OxmlError::from)?;
9993 emit_raw(writer, markup.raw_children.at(0))?;
9994 write_scalar(
9995 writer,
9996 "c:barDir",
9997 direction.as_str(),
9998 markup.direction.as_ref(),
9999 )?;
10000 emit_raw(writer, markup.raw_children.at(1))?;
10001 write_scalar(
10002 writer,
10003 "c:grouping",
10004 grouping.as_str(),
10005 markup.grouping.as_ref(),
10006 )?;
10007 let original_to_current =
10008 series_original_to_current(&markup.original_series_keys, series);
10009 emit_repeated_raw(
10010 writer,
10011 &markup.raw_children,
10012 2,
10013 &original_to_current,
10014 series.len(),
10015 0,
10016 )?;
10017 for (index, item) in series.iter().enumerate() {
10018 writer
10019 .get_mut()
10020 .write_all(&item.to_xml()?)
10021 .map_err(OxmlError::from)?;
10022 emit_repeated_raw(
10023 writer,
10024 &markup.raw_children,
10025 2,
10026 &original_to_current,
10027 series.len(),
10028 index + 1,
10029 )?;
10030 }
10031 if let Some(labels) = data_labels {
10032 writer
10033 .get_mut()
10034 .write_all(&labels.to_xml()?)
10035 .map_err(OxmlError::from)?;
10036 }
10037 let trailing = markup.original_series_keys.len();
10038 emit_raw(writer, markup.raw_children.at(trailing + 3))?;
10039 if *gap_width != 150 || markup.gap_width.is_some() {
10040 write_scalar(
10041 writer,
10042 "c:gapWidth",
10043 &gap_width.to_string(),
10044 markup.gap_width.as_ref(),
10045 )?;
10046 }
10047 emit_raw(writer, markup.raw_children.at(trailing + 4))?;
10048 if *overlap != 0 || markup.overlap.is_some() {
10049 write_scalar(
10050 writer,
10051 "c:overlap",
10052 &overlap.to_string(),
10053 markup.overlap.as_ref(),
10054 )?;
10055 }
10056 let axis_original_to_current =
10057 axis_original_to_current(&markup.original_axis_ids, axis_ids);
10058 let axis_current_to_original =
10059 invert_original_to_current(&axis_original_to_current, axis_ids.len());
10060 emit_repeated_raw(
10061 writer,
10062 &markup.raw_children,
10063 trailing + 5,
10064 &axis_original_to_current,
10065 axis_ids.len(),
10066 0,
10067 )?;
10068 for (index, id) in axis_ids.iter().enumerate() {
10069 let default_markup = default_axis_id_markup(*id);
10070 let original_markup = axis_current_to_original[index]
10071 .and_then(|original| markup.axis_ids.get(original));
10072 write_axis_id(
10073 writer,
10074 "c:axId",
10075 *id,
10076 original_markup.unwrap_or(&default_markup),
10077 )?;
10078 emit_repeated_raw(
10079 writer,
10080 &markup.raw_children,
10081 trailing + 5,
10082 &axis_original_to_current,
10083 axis_ids.len(),
10084 index + 1,
10085 )?;
10086 }
10087 writer
10088 .write_event(Event::End(BytesEnd::new("c:barChart")))
10089 .map_err(OxmlError::from)?;
10090 }
10091 Plot::Line {
10092 grouping,
10093 marker,
10094 smooth,
10095 series,
10096 data_labels,
10097 axis_ids,
10098 } => {
10099 let mut start = BytesStart::new("c:lineChart");
10100 push_attributes(&mut start, &markup.raw_attributes);
10101 writer
10102 .write_event(Event::Start(start))
10103 .map_err(OxmlError::from)?;
10104 emit_raw(writer, markup.raw_children.at(0))?;
10105 write_scalar(
10106 writer,
10107 "c:grouping",
10108 grouping.as_str(),
10109 markup.grouping.as_ref(),
10110 )?;
10111 let original_to_current =
10112 series_original_to_current(&markup.original_series_keys, series);
10113 emit_repeated_raw(
10114 writer,
10115 &markup.raw_children,
10116 1,
10117 &original_to_current,
10118 series.len(),
10119 0,
10120 )?;
10121 for (index, item) in series.iter().enumerate() {
10122 writer
10123 .get_mut()
10124 .write_all(&item.to_xml()?)
10125 .map_err(OxmlError::from)?;
10126 emit_repeated_raw(
10127 writer,
10128 &markup.raw_children,
10129 1,
10130 &original_to_current,
10131 series.len(),
10132 index + 1,
10133 )?;
10134 }
10135 if let Some(labels) = data_labels {
10136 writer
10137 .get_mut()
10138 .write_all(&labels.to_xml()?)
10139 .map_err(OxmlError::from)?;
10140 }
10141 let trailing = markup.original_series_keys.len();
10142 emit_raw(writer, markup.raw_children.at(trailing + 2))?;
10143 if *marker || markup.marker.is_some() {
10144 write_scalar(
10145 writer,
10146 "c:marker",
10147 bool_lexical(*marker),
10148 markup.marker.as_ref(),
10149 )?;
10150 }
10151 emit_raw(writer, markup.raw_children.at(trailing + 3))?;
10152 if *smooth || markup.smooth.is_some() {
10153 write_scalar(
10154 writer,
10155 "c:smooth",
10156 bool_lexical(*smooth),
10157 markup.smooth.as_ref(),
10158 )?;
10159 }
10160 let axis_original_to_current =
10161 axis_original_to_current(&markup.original_axis_ids, axis_ids);
10162 let axis_current_to_original =
10163 invert_original_to_current(&axis_original_to_current, axis_ids.len());
10164 emit_repeated_raw(
10165 writer,
10166 &markup.raw_children,
10167 trailing + 4,
10168 &axis_original_to_current,
10169 axis_ids.len(),
10170 0,
10171 )?;
10172 for (index, id) in axis_ids.iter().enumerate() {
10173 let default_markup = default_axis_id_markup(*id);
10174 let original_markup = axis_current_to_original[index]
10175 .and_then(|original| markup.axis_ids.get(original));
10176 write_axis_id(
10177 writer,
10178 "c:axId",
10179 *id,
10180 original_markup.unwrap_or(&default_markup),
10181 )?;
10182 emit_repeated_raw(
10183 writer,
10184 &markup.raw_children,
10185 trailing + 4,
10186 &axis_original_to_current,
10187 axis_ids.len(),
10188 index + 1,
10189 )?;
10190 }
10191 writer
10192 .write_event(Event::End(BytesEnd::new("c:lineChart")))
10193 .map_err(OxmlError::from)?;
10194 }
10195 remaining => {
10196 let (
10197 root,
10198 property,
10199 series,
10200 data_labels,
10201 axis_ids,
10202 first_slice_angle,
10203 hole_size,
10204 scatter,
10205 ) = match remaining {
10206 Plot::Pie {
10207 first_slice_angle,
10208 series,
10209 data_labels,
10210 } => (
10211 "pieChart",
10212 None,
10213 series,
10214 data_labels,
10215 None,
10216 Some(*first_slice_angle),
10217 None,
10218 false,
10219 ),
10220 Plot::Doughnut {
10221 first_slice_angle,
10222 hole_size,
10223 series,
10224 data_labels,
10225 } => (
10226 "doughnutChart",
10227 None,
10228 series,
10229 data_labels,
10230 None,
10231 Some(*first_slice_angle),
10232 Some(*hole_size),
10233 false,
10234 ),
10235 Plot::Area {
10236 grouping,
10237 series,
10238 data_labels,
10239 axis_ids,
10240 } => (
10241 "areaChart",
10242 Some(("c:grouping", grouping.as_str(), markup.grouping.as_ref())),
10243 series,
10244 data_labels,
10245 Some(axis_ids),
10246 None,
10247 None,
10248 false,
10249 ),
10250 Plot::Scatter {
10251 style,
10252 series,
10253 data_labels,
10254 axis_ids,
10255 } => (
10256 "scatterChart",
10257 Some(("c:scatterStyle", style.as_str(), markup.style.as_ref())),
10258 series,
10259 data_labels,
10260 Some(axis_ids),
10261 None,
10262 None,
10263 true,
10264 ),
10265 Plot::Radar {
10266 style,
10267 series,
10268 data_labels,
10269 axis_ids,
10270 } => (
10271 "radarChart",
10272 Some(("c:radarStyle", style.as_str(), markup.style.as_ref())),
10273 series,
10274 data_labels,
10275 Some(axis_ids),
10276 None,
10277 None,
10278 false,
10279 ),
10280 Plot::Bar { .. } | Plot::Line { .. } => unreachable!("handled above"),
10281 };
10282 let tag = format!("c:{root}");
10283 let mut start = BytesStart::new(&tag);
10284 push_attributes(&mut start, &markup.raw_attributes);
10285 writer
10286 .write_event(Event::Start(start))
10287 .map_err(OxmlError::from)?;
10288 let base = usize::from(property.is_some());
10289 if let Some((name, value, scalar_markup)) = property {
10290 emit_raw(writer, markup.raw_children.at(0))?;
10291 write_scalar(writer, name, value, scalar_markup)?;
10292 }
10293 let original_to_current =
10294 series_original_to_current(&markup.original_series_keys, series);
10295 emit_repeated_raw(
10296 writer,
10297 &markup.raw_children,
10298 base,
10299 &original_to_current,
10300 series.len(),
10301 0,
10302 )?;
10303 for (index, item) in series.iter().enumerate() {
10304 writer
10305 .get_mut()
10306 .write_all(&item.to_xml_for_plot(scatter)?)
10307 .map_err(OxmlError::from)?;
10308 emit_repeated_raw(
10309 writer,
10310 &markup.raw_children,
10311 base,
10312 &original_to_current,
10313 series.len(),
10314 index + 1,
10315 )?;
10316 }
10317 if let Some(labels) = data_labels {
10318 writer
10319 .get_mut()
10320 .write_all(&labels.to_xml()?)
10321 .map_err(OxmlError::from)?;
10322 }
10323 let trailing = base + markup.original_series_keys.len();
10324 if let Some(axis_ids) = axis_ids {
10325 let axis_original_to_current =
10326 axis_original_to_current(&markup.original_axis_ids, axis_ids);
10327 let axis_current_to_original =
10328 invert_original_to_current(&axis_original_to_current, axis_ids.len());
10329 emit_repeated_raw(
10330 writer,
10331 &markup.raw_children,
10332 trailing + 1,
10333 &axis_original_to_current,
10334 axis_ids.len(),
10335 0,
10336 )?;
10337 for (index, id) in axis_ids.iter().enumerate() {
10338 let default_markup = default_axis_id_markup(*id);
10339 let original_markup = axis_current_to_original[index]
10340 .and_then(|original| markup.axis_ids.get(original));
10341 write_axis_id(
10342 writer,
10343 "c:axId",
10344 *id,
10345 original_markup.unwrap_or(&default_markup),
10346 )?;
10347 emit_repeated_raw(
10348 writer,
10349 &markup.raw_children,
10350 trailing + 1,
10351 &axis_original_to_current,
10352 axis_ids.len(),
10353 index + 1,
10354 )?;
10355 }
10356 } else {
10357 emit_raw(writer, markup.raw_children.at(trailing + 1))?;
10358 if let Some(angle) = first_slice_angle {
10359 if angle != 0 || markup.first_slice_angle.is_some() {
10360 write_scalar(
10361 writer,
10362 "c:firstSliceAng",
10363 &angle.to_string(),
10364 markup.first_slice_angle.as_ref(),
10365 )?;
10366 }
10367 emit_raw(writer, markup.raw_children.at(trailing + 2))?;
10368 }
10369 if let Some(size) = hole_size {
10370 if size != 50 || markup.hole_size.is_some() {
10371 write_scalar(
10372 writer,
10373 "c:holeSize",
10374 &size.to_string(),
10375 markup.hole_size.as_ref(),
10376 )?;
10377 }
10378 emit_raw(writer, markup.raw_children.at(trailing + 3))?;
10379 }
10380 }
10381 writer
10382 .write_event(Event::End(BytesEnd::new(&tag)))
10383 .map_err(OxmlError::from)?;
10384 }
10385 }
10386 Ok(())
10387}
10388
10389fn series_original_to_current(original: &[(u32, u32)], current: &[Series]) -> Vec<Option<usize>> {
10390 let mut used = vec![false; current.len()];
10391 let mut matches = vec![None; original.len()];
10392 for original_index in 0..original.len().min(current.len()) {
10393 if original[original_index]
10394 == (current[original_index].index, current[original_index].order)
10395 {
10396 matches[original_index] = Some(original_index);
10397 used[original_index] = true;
10398 }
10399 }
10400 for (original_index, key) in original.iter().enumerate() {
10401 if matches[original_index].is_some() {
10402 continue;
10403 }
10404 if let Some(current_index) = current
10405 .iter()
10406 .enumerate()
10407 .position(|(index, item)| !used[index] && (item.index, item.order) == *key)
10408 {
10409 matches[original_index] = Some(current_index);
10410 used[current_index] = true;
10411 }
10412 }
10413 match_unidentified_by_position(&mut matches, &mut used);
10414 matches
10415}
10416
10417fn axis_original_to_current(original: &[AxisId], current: &[AxisId]) -> Vec<Option<usize>> {
10418 let mut used = vec![false; current.len()];
10419 let mut matches = vec![None; original.len()];
10420 for (original_index, id) in original.iter().enumerate() {
10421 if let Some(current_index) = current
10422 .iter()
10423 .enumerate()
10424 .position(|(index, current_id)| !used[index] && current_id == id)
10425 {
10426 matches[original_index] = Some(current_index);
10427 used[current_index] = true;
10428 }
10429 }
10430 match_unidentified_by_position(&mut matches, &mut used);
10431 matches
10432}
10433
10434fn match_unidentified_by_position(matches: &mut [Option<usize>], used: &mut [bool]) {
10435 for original_index in 0..matches.len() {
10436 if matches[original_index].is_none() && original_index < used.len() && !used[original_index]
10437 {
10438 matches[original_index] = Some(original_index);
10439 used[original_index] = true;
10440 }
10441 }
10442 for matched in matches.iter_mut().filter(|matched| matched.is_none()) {
10443 if let Some(current_index) = used.iter().position(|used| !used) {
10444 *matched = Some(current_index);
10445 used[current_index] = true;
10446 }
10447 }
10448}
10449
10450fn invert_original_to_current(
10451 original_to_current: &[Option<usize>],
10452 current_len: usize,
10453) -> Vec<Option<usize>> {
10454 let mut current_to_original = vec![None; current_len];
10455 for (original, current) in original_to_current.iter().enumerate() {
10456 if let Some(current) = current {
10457 current_to_original[*current] = Some(original);
10458 }
10459 }
10460 current_to_original
10461}
10462
10463fn emit_repeated_raw<W: Write>(
10464 writer: &mut Writer<W>,
10465 raw_children: &OrderedRawChildren,
10466 offset: usize,
10467 original_to_current: &[Option<usize>],
10468 current_len: usize,
10469 current_boundary: usize,
10470) -> Result<()> {
10471 if current_boundary == 0 {
10472 emit_raw(writer, raw_children.at(offset))?;
10473 }
10474 for original_boundary in 1..=original_to_current.len() {
10475 let effective = original_to_current
10476 .iter()
10477 .skip(original_boundary)
10478 .flatten()
10479 .copied()
10480 .next()
10481 .unwrap_or(current_len);
10482 if effective == current_boundary {
10483 emit_raw(writer, raw_children.at(offset + original_boundary))?;
10484 }
10485 }
10486 Ok(())
10487}
10488
10489fn parse_plot_axis(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Axis>> {
10490 let mut reader = Reader::from_reader(xml);
10491 let mut buffer = Vec::new();
10492 loop {
10493 match reader
10494 .read_event_into(&mut buffer)
10495 .map_err(OxmlError::from)?
10496 {
10497 Event::Start(element) | Event::Empty(element) => {
10498 if !element_is_in_namespace(&element, C_NS, inherited)? {
10499 return Ok(None);
10500 }
10501 let name = element.name();
10502 if AxisKind::parse(local_name(name.as_ref())).is_none() {
10503 return Ok(None);
10504 }
10505 return Axis::from_xml_with_namespaces(xml, inherited).map(Some);
10506 }
10507 Event::Eof => return Ok(None),
10508 _ => {}
10509 }
10510 buffer.clear();
10511 }
10512}
10513
10514fn validate_axis_pairs(axes: &[Axis]) -> Result<()> {
10515 for (index, axis) in axes.iter().enumerate() {
10516 if axes[..index].iter().any(|other| other.id == axis.id) {
10517 return Err(ChartError::DuplicateElement(format!(
10518 "c:axId {}",
10519 axis.id.value()
10520 )));
10521 }
10522 }
10523 for axis in axes {
10524 if axis.id == axis.cross_axis {
10525 return Err(ChartError::InvalidValue {
10526 element: "c:crossAx".to_owned(),
10527 value: format!("axis {} crosses itself", axis.id.value()),
10528 });
10529 }
10530 let crossed = axes
10531 .iter()
10532 .find(|candidate| candidate.id == axis.cross_axis)
10533 .ok_or_else(|| ChartError::InvalidValue {
10534 element: "c:crossAx".to_owned(),
10535 value: format!(
10536 "axis {} references missing axis {}",
10537 axis.id.value(),
10538 axis.cross_axis.value()
10539 ),
10540 })?;
10541 if crossed.cross_axis != axis.id {
10542 return Err(ChartError::InvalidValue {
10543 element: "c:crossAx".to_owned(),
10544 value: format!(
10545 "axis {} references {}, which references {}",
10546 axis.id.value(),
10547 crossed.id.value(),
10548 crossed.cross_axis.value()
10549 ),
10550 });
10551 }
10552 }
10553 Ok(())
10554}
10555
10556fn parse_plot_series(
10557 xml: &[u8],
10558 inherited: &NamespaceBindings,
10559 series: &mut Vec<Series>,
10560) -> Result<()> {
10561 let mut reader = Reader::from_reader(xml);
10562 let mut buffer = Vec::new();
10563 loop {
10564 match reader
10565 .read_event_into(&mut buffer)
10566 .map_err(OxmlError::from)?
10567 {
10568 Event::Start(element) => {
10569 if !element_is_in_namespace(&element, C_NS, inherited)? {
10570 return Ok(());
10571 }
10572 let local = local_name(element.name().as_ref()).to_vec();
10573 if !is_supported_series_plot(&local) {
10574 return Ok(());
10575 }
10576 let namespaces = chart_bindings(inherited, &element)?;
10577 let mut inner = Vec::new();
10578 loop {
10579 match reader
10580 .read_event_into(&mut inner)
10581 .map_err(OxmlError::from)?
10582 {
10583 Event::Start(child) => {
10584 let name = chart_child_local(&child, &namespaces)?;
10585 let raw = capture_element(&mut reader, &child)?;
10586 if name.as_deref() == Some(b"ser") {
10587 series.push(Series::from_xml_with_namespaces(&raw, &namespaces)?);
10588 }
10589 }
10590 Event::Empty(child) => {
10591 let name = chart_child_local(&child, &namespaces)?;
10592 if name.as_deref() == Some(b"ser") {
10593 return Err(ChartError::MissingElement("c:ser/c:idx".to_owned()));
10594 }
10595 }
10596 Event::End(end) if matches_local_name(end.name().as_ref(), &local) => {
10597 return Ok(());
10598 }
10599 Event::Eof => {
10600 return Err(missing_end(&format!(
10601 "c:{}",
10602 String::from_utf8_lossy(&local)
10603 )));
10604 }
10605 _ => {}
10606 }
10607 inner.clear();
10608 }
10609 }
10610 Event::Empty(_) | Event::Eof => return Ok(()),
10611 _ => {}
10612 }
10613 buffer.clear();
10614 }
10615}
10616
10617fn is_supported_series_plot(local: &[u8]) -> bool {
10618 matches!(
10619 local,
10620 b"areaChart"
10621 | b"area3DChart"
10622 | b"barChart"
10623 | b"bar3DChart"
10624 | b"doughnutChart"
10625 | b"lineChart"
10626 | b"line3DChart"
10627 | b"ofPieChart"
10628 | b"pieChart"
10629 | b"pie3DChart"
10630 | b"radarChart"
10631 | b"stockChart"
10632 | b"surfaceChart"
10633 | b"surface3DChart"
10634 )
10635}
10636
10637impl CT_Legend {
10638 fn from_xml(xml: &[u8]) -> Result<Self> {
10639 let (raw_attributes, raw_children) = parse_raw_shell(xml, b"legend", "c:legend")?;
10640 Ok(Self {
10641 raw_attributes,
10642 raw_children,
10643 })
10644 }
10645
10646 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
10647 write_raw_shell(writer, "c:legend", &self.raw_attributes, &self.raw_children)
10648 }
10649
10650 pub fn raw_children(&self) -> &OrderedRawChildren {
10651 &self.raw_children
10652 }
10653}
10654
10655fn parse_raw_shell(
10656 xml: &[u8],
10657 local: &[u8],
10658 tag: &str,
10659) -> Result<(XmlAttributes, OrderedRawChildren)> {
10660 let mut reader = Reader::from_reader(xml);
10661 let mut buffer = Vec::new();
10662 loop {
10663 match reader
10664 .read_event_into(&mut buffer)
10665 .map_err(OxmlError::from)?
10666 {
10667 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
10668 reject_conflicting_prefix(&element, b"c", C_NS)?;
10669 let raw_attributes = capture_attributes(&element)?;
10670 let mut raw_children = OrderedRawChildren::default();
10671 let mut child_buffer = Vec::new();
10672 loop {
10673 match reader
10674 .read_event_into(&mut child_buffer)
10675 .map_err(OxmlError::from)?
10676 {
10677 Event::Start(child) => {
10678 raw_children.push(0, capture_element(&mut reader, &child)?)
10679 }
10680 Event::Empty(child) => raw_children.push(0, capture_empty_element(&child)?),
10681 event @ (Event::Text(_)
10682 | Event::CData(_)
10683 | Event::Comment(_)
10684 | Event::PI(_)
10685 | Event::GeneralRef(_)) => {
10686 raw_children.push(0, capture_event(event)?);
10687 }
10688 Event::End(end) if matches_local_name(end.name().as_ref(), local) => break,
10689 Event::Eof => return Err(missing_end(tag)),
10690 _ => {}
10691 }
10692 child_buffer.clear();
10693 }
10694 return Ok((raw_attributes, raw_children));
10695 }
10696 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
10697 reject_conflicting_prefix(&element, b"c", C_NS)?;
10698 return Ok((capture_attributes(&element)?, OrderedRawChildren::default()));
10699 }
10700 Event::Start(element) | Event::Empty(element) => {
10701 return Err(ChartError::UnexpectedElement(element_name(&element)));
10702 }
10703 Event::Eof => return Err(ChartError::MissingElement(tag.to_owned())),
10704 _ => {}
10705 }
10706 buffer.clear();
10707 }
10708}
10709
10710fn write_raw_shell<W: Write>(
10711 writer: &mut Writer<W>,
10712 tag: &str,
10713 raw_attributes: &XmlAttributes,
10714 raw_children: &OrderedRawChildren,
10715) -> Result<()> {
10716 let mut start = BytesStart::new(tag);
10717 push_attributes(&mut start, raw_attributes);
10718 if raw_children.is_empty() {
10719 writer
10720 .write_event(Event::Empty(start))
10721 .map_err(OxmlError::from)?;
10722 return Ok(());
10723 }
10724 writer
10725 .write_event(Event::Start(start))
10726 .map_err(OxmlError::from)?;
10727 emit_raw(writer, raw_children.at(0))?;
10728 writer
10729 .write_event(Event::End(BytesEnd::new(tag)))
10730 .map_err(OxmlError::from)?;
10731 Ok(())
10732}
10733
10734#[derive(Clone, Debug, PartialEq)]
10736pub struct CT_Chart {
10737 pub title: Option<CT_Title>,
10738 pub auto_title_deleted: bool,
10739 pub plot_area: CT_PlotArea,
10740 pub legend: Option<CT_Legend>,
10741 pub plot_vis_only: bool,
10742 pub disp_blanks_as: DispBlanksAs,
10743 auto_title_deleted_markup: Option<ScalarMarkup>,
10744 plot_vis_only_markup: Option<ScalarMarkup>,
10745 disp_blanks_as_markup: Option<ScalarMarkup>,
10746 raw_attributes: Vec<(String, String)>,
10747 raw_children: OrderedRawChildren,
10748}
10749
10750impl CT_Chart {
10751 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
10752 let mut reader = Reader::from_reader(xml);
10753 let mut buffer = Vec::new();
10754 loop {
10755 match reader
10756 .read_event_into(&mut buffer)
10757 .map_err(OxmlError::from)?
10758 {
10759 Event::Start(element) if matches_local_name(element.name().as_ref(), b"chart") => {
10760 reject_conflicting_prefix(&element, b"c", C_NS)?;
10761 return Self::from_element(&mut reader, &element, inherited);
10762 }
10763 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"chart") => {
10764 return Err(ChartError::MissingElement("c:plotArea".to_owned()));
10765 }
10766 Event::Start(element) | Event::Empty(element) => {
10767 return Err(ChartError::UnexpectedElement(element_name(&element)));
10768 }
10769 Event::Eof => return Err(ChartError::MissingElement("c:chart".to_owned())),
10770 _ => {}
10771 }
10772 buffer.clear();
10773 }
10774 }
10775
10776 fn from_element(
10777 reader: &mut Reader<&[u8]>,
10778 start: &BytesStart<'_>,
10779 inherited: &NamespaceBindings,
10780 ) -> Result<Self> {
10781 let namespaces = bindings_with_local(inherited, start)?;
10782 let raw_attributes = capture_attributes(start)?;
10783 let mut state = ChartParseState::default();
10784 let mut buffer = Vec::new();
10785
10786 loop {
10787 match reader
10788 .read_event_into(&mut buffer)
10789 .map_err(OxmlError::from)?
10790 {
10791 Event::Start(element) => {
10792 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10793 local_name(element.name().as_ref()).to_vec()
10794 } else {
10795 Vec::new()
10796 };
10797 let raw = capture_element(reader, &element)?;
10798 state.parse_child(&name, raw, &namespaces)?;
10799 }
10800 Event::Empty(element) => {
10801 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10802 local_name(element.name().as_ref()).to_vec()
10803 } else {
10804 Vec::new()
10805 };
10806 let raw = capture_empty_element(&element)?;
10807 state.parse_child(&name, raw, &namespaces)?;
10808 }
10809 event @ (Event::Text(_)
10810 | Event::CData(_)
10811 | Event::Comment(_)
10812 | Event::PI(_)
10813 | Event::GeneralRef(_)) => {
10814 state.capture_event(capture_event(event)?);
10815 }
10816 Event::End(element) if matches_local_name(element.name().as_ref(), b"chart") => {
10817 break;
10818 }
10819 Event::Eof => return Err(missing_end("c:chart")),
10820 _ => {}
10821 }
10822 buffer.clear();
10823 }
10824
10825 state.finish(raw_attributes)
10826 }
10827
10828 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
10829 let mut start = BytesStart::new("c:chart");
10830 push_attributes(&mut start, &self.raw_attributes);
10831 writer
10832 .write_event(Event::Start(start))
10833 .map_err(OxmlError::from)?;
10834 emit_raw(writer, self.raw_children.at(0))?;
10835 if let Some(title) = &self.title {
10836 title.write_xml(writer)?;
10837 }
10838 emit_raw(writer, self.raw_children.at(1))?;
10839 if self.auto_title_deleted_markup.is_some() || self.auto_title_deleted {
10840 write_scalar(
10841 writer,
10842 "c:autoTitleDeleted",
10843 if self.auto_title_deleted { "1" } else { "0" },
10844 self.auto_title_deleted_markup.as_ref(),
10845 )?;
10846 }
10847 for raw_boundary in 2..8 {
10848 emit_raw(writer, self.raw_children.at(raw_boundary))?;
10849 }
10850 self.plot_area.write_xml(writer)?;
10851 emit_raw(writer, self.raw_children.at(8))?;
10852 if let Some(legend) = &self.legend {
10853 legend.write_xml(writer)?;
10854 }
10855 emit_raw(writer, self.raw_children.at(9))?;
10856 if self.plot_vis_only_markup.is_some() || !self.plot_vis_only {
10857 write_scalar(
10858 writer,
10859 "c:plotVisOnly",
10860 if self.plot_vis_only { "1" } else { "0" },
10861 self.plot_vis_only_markup.as_ref(),
10862 )?;
10863 }
10864 emit_raw(writer, self.raw_children.at(10))?;
10865 if self.disp_blanks_as_markup.is_some() || self.disp_blanks_as != DispBlanksAs::Gap {
10866 write_scalar(
10867 writer,
10868 "c:dispBlanksAs",
10869 self.disp_blanks_as.as_str(),
10870 self.disp_blanks_as_markup.as_ref(),
10871 )?;
10872 }
10873 for raw_boundary in 11..=13 {
10874 emit_raw(writer, self.raw_children.at(raw_boundary))?;
10875 }
10876 writer
10877 .write_event(Event::End(BytesEnd::new("c:chart")))
10878 .map_err(OxmlError::from)?;
10879 Ok(())
10880 }
10881
10882 pub fn raw_children(&self) -> &OrderedRawChildren {
10883 &self.raw_children
10884 }
10885}
10886
10887#[derive(Default)]
10888struct ChartParseState {
10889 title: Option<CT_Title>,
10890 auto_title_deleted: Option<(bool, ScalarMarkup)>,
10891 plot_area: Option<CT_PlotArea>,
10892 legend: Option<CT_Legend>,
10893 plot_vis_only: Option<(bool, ScalarMarkup)>,
10894 disp_blanks_as: Option<(DispBlanksAs, ScalarMarkup)>,
10895 raw_children: OrderedRawChildren,
10896 boundary: usize,
10897}
10898
10899impl ChartParseState {
10900 fn capture_event(&mut self, raw: Vec<u8>) {
10901 self.raw_children.push(self.boundary, raw);
10902 }
10903
10904 fn parse_child(
10905 &mut self,
10906 name: &[u8],
10907 raw: Vec<u8>,
10908 namespaces: &NamespaceBindings,
10909 ) -> Result<()> {
10910 match name {
10911 b"title" => {
10912 set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
10913 self.boundary = self.boundary.max(1);
10914 }
10915 b"autoTitleDeleted" => {
10916 set_once(
10917 &mut self.auto_title_deleted,
10918 parse_bool_value(&raw, "autoTitleDeleted")?,
10919 "c:autoTitleDeleted",
10920 )?;
10921 self.boundary = self.boundary.max(2);
10922 }
10923 b"plotArea" => {
10924 set_once(
10925 &mut self.plot_area,
10926 CT_PlotArea::from_xml_with_namespaces(&raw, namespaces)?,
10927 "c:plotArea",
10928 )?;
10929 self.boundary = self.boundary.max(8);
10930 }
10931 b"legend" => {
10932 set_once(&mut self.legend, CT_Legend::from_xml(&raw)?, "c:legend")?;
10933 self.boundary = self.boundary.max(9);
10934 }
10935 b"plotVisOnly" => {
10936 set_once(
10937 &mut self.plot_vis_only,
10938 parse_bool_value(&raw, "plotVisOnly")?,
10939 "c:plotVisOnly",
10940 )?;
10941 self.boundary = self.boundary.max(10);
10942 }
10943 b"dispBlanksAs" => {
10944 set_once(
10945 &mut self.disp_blanks_as,
10946 parse_enum_value(&raw, "dispBlanksAs")?,
10947 "c:dispBlanksAs",
10948 )?;
10949 self.boundary = self.boundary.max(11);
10950 }
10951 _ => {
10952 self.raw_children
10953 .push(chart_raw_slot(name).unwrap_or(self.boundary), raw);
10954 self.boundary = self.boundary.max(chart_raw_boundary_after(name));
10955 }
10956 }
10957 Ok(())
10958 }
10959
10960 fn finish(self, raw_attributes: XmlAttributes) -> Result<CT_Chart> {
10961 let (auto_title_deleted, auto_title_deleted_markup) = self
10962 .auto_title_deleted
10963 .map(|(value, markup)| (value, Some(markup)))
10964 .unwrap_or((false, None));
10965 let (plot_vis_only, plot_vis_only_markup) = self
10966 .plot_vis_only
10967 .map(|(value, markup)| (value, Some(markup)))
10968 .unwrap_or((true, None));
10969 let (disp_blanks_as, disp_blanks_as_markup) = self
10970 .disp_blanks_as
10971 .map(|(value, markup)| (value, Some(markup)))
10972 .unwrap_or((DispBlanksAs::Gap, None));
10973 Ok(CT_Chart {
10974 title: self.title,
10975 auto_title_deleted,
10976 plot_area: self
10977 .plot_area
10978 .ok_or_else(|| ChartError::MissingElement("c:plotArea".to_owned()))?,
10979 legend: self.legend,
10980 plot_vis_only,
10981 disp_blanks_as,
10982 auto_title_deleted_markup,
10983 plot_vis_only_markup,
10984 disp_blanks_as_markup,
10985 raw_attributes,
10986 raw_children: raw_children_in_schema_order(&self.raw_children, 13),
10987 })
10988 }
10989}
10990
10991fn chart_raw_slot(name: &[u8]) -> Option<usize> {
10992 match name {
10993 b"pivotFmts" => Some(2),
10994 b"view3D" => Some(3),
10995 b"floor" => Some(4),
10996 b"sideWall" => Some(5),
10997 b"backWall" => Some(6),
10998 b"showDLblsOverMax" | b"extLst" => Some(11),
11001 _ => None,
11002 }
11003}
11004
11005fn chart_raw_boundary_after(name: &[u8]) -> usize {
11006 match name {
11007 b"title" => 1,
11008 b"autoTitleDeleted" => 2,
11009 b"pivotFmts" => 3,
11010 b"view3D" => 4,
11011 b"floor" => 5,
11012 b"sideWall" => 6,
11013 b"backWall" => 7,
11014 b"plotArea" => 8,
11015 b"legend" => 9,
11016 b"plotVisOnly" => 10,
11017 b"dispBlanksAs" => 11,
11018 b"showDLblsOverMax" | b"extLst" => 11,
11019 _ => 0,
11020 }
11021}
11022
11023#[derive(Clone, Debug, PartialEq)]
11025pub struct CT_ChartSpace {
11026 pub chart: CT_Chart,
11027 pub sp_pr: Option<CT_ShapeProperties>,
11028 pub tx_pr: Option<CT_TextBody>,
11029 raw_attributes: Vec<(String, String)>,
11030 namespace_declarations: Vec<(String, String)>,
11031 raw_children: OrderedRawChildren,
11032}
11033
11034impl CT_ChartSpace {
11035 pub fn from_xml(xml: &[u8]) -> Result<Self> {
11037 let mut reader = Reader::from_reader(xml);
11038 let mut buffer = Vec::new();
11039 loop {
11040 match reader
11041 .read_event_into(&mut buffer)
11042 .map_err(OxmlError::from)?
11043 {
11044 Event::Start(element)
11045 if matches_local_name(element.name().as_ref(), b"chartSpace") =>
11046 {
11047 validate_chart_space_namespace(&element)?;
11048 return Self::from_element(&mut reader, &element);
11049 }
11050 Event::Empty(element)
11051 if matches_local_name(element.name().as_ref(), b"chartSpace") =>
11052 {
11053 validate_chart_space_namespace(&element)?;
11054 return Err(ChartError::MissingElement("c:chart".to_owned()));
11055 }
11056 Event::Start(element) | Event::Empty(element) => {
11057 return Err(ChartError::UnexpectedElement(element_name(&element)));
11058 }
11059 Event::Eof => return Err(ChartError::MissingElement("c:chartSpace".to_owned())),
11060 _ => {}
11061 }
11062 buffer.clear();
11063 }
11064 }
11065
11066 fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
11067 let namespaces = namespace_bindings(start)?;
11068 let (raw_attributes, namespace_declarations) = capture_root_attributes(start)?;
11069 let mut state = ChartSpaceParseState::default();
11070 let mut buffer = Vec::new();
11071
11072 loop {
11073 match reader
11074 .read_event_into(&mut buffer)
11075 .map_err(OxmlError::from)?
11076 {
11077 Event::Start(element) => {
11078 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
11079 local_name(element.name().as_ref()).to_vec()
11080 } else {
11081 Vec::new()
11082 };
11083 let raw = capture_element(reader, &element)?;
11084 state.parse_child(&name, raw, &namespaces)?;
11085 }
11086 Event::Empty(element) => {
11087 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
11088 local_name(element.name().as_ref()).to_vec()
11089 } else {
11090 Vec::new()
11091 };
11092 let raw = capture_empty_element(&element)?;
11093 state.parse_child(&name, raw, &namespaces)?;
11094 }
11095 event @ (Event::Text(_)
11096 | Event::CData(_)
11097 | Event::Comment(_)
11098 | Event::PI(_)
11099 | Event::GeneralRef(_)) => {
11100 state.capture_event(capture_event(event)?);
11101 }
11102 Event::End(element)
11103 if matches_local_name(element.name().as_ref(), b"chartSpace") =>
11104 {
11105 break;
11106 }
11107 Event::Eof => return Err(missing_end("c:chartSpace")),
11108 _ => {}
11109 }
11110 buffer.clear();
11111 }
11112
11113 state.finish(raw_attributes, namespace_declarations)
11114 }
11115
11116 pub fn to_xml(&self) -> Result<Vec<u8>> {
11118 let mut writer = Writer::new(Vec::new());
11119 let mut start = BytesStart::new("c:chartSpace");
11120 start.push_attribute(("xmlns:c", C_NS));
11121 start.push_attribute(("xmlns:a", A_NS));
11122 start.push_attribute(("xmlns:r", R_NS));
11123 push_attributes(&mut start, &self.namespace_declarations);
11124 push_attributes(&mut start, &self.raw_attributes);
11125 writer
11126 .write_event(Event::Start(start))
11127 .map_err(OxmlError::from)?;
11128 for raw_boundary in 0..8 {
11129 emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
11130 }
11131 self.chart.write_xml(&mut writer)?;
11132 emit_raw(&mut writer, self.raw_children.at(8))?;
11133 if let Some(properties) = &self.sp_pr {
11134 properties.write_xml_as(&mut writer, "c:spPr")?;
11135 }
11136 emit_raw(&mut writer, self.raw_children.at(9))?;
11137 if let Some(text) = &self.tx_pr {
11138 text.write_xml_as(&mut writer, "c:txPr")?;
11139 }
11140 for raw_boundary in 10..=14 {
11141 emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
11142 }
11143 writer
11144 .write_event(Event::End(BytesEnd::new("c:chartSpace")))
11145 .map_err(OxmlError::from)?;
11146 Ok(writer.into_inner())
11147 }
11148
11149 pub fn raw_children(&self) -> &OrderedRawChildren {
11150 &self.raw_children
11151 }
11152}
11153
11154#[derive(Default)]
11155struct ChartSpaceParseState {
11156 chart: Option<CT_Chart>,
11157 sp_pr: Option<CT_ShapeProperties>,
11158 tx_pr: Option<CT_TextBody>,
11159 raw_children: OrderedRawChildren,
11160 boundary: usize,
11161}
11162
11163impl ChartSpaceParseState {
11164 fn capture_event(&mut self, raw: Vec<u8>) {
11165 self.raw_children.push(self.boundary, raw);
11166 }
11167
11168 fn parse_child(
11169 &mut self,
11170 name: &[u8],
11171 raw: Vec<u8>,
11172 namespaces: &NamespaceBindings,
11173 ) -> Result<()> {
11174 match name {
11175 b"chart" => {
11176 set_once(
11177 &mut self.chart,
11178 CT_Chart::from_xml_with_namespaces(&raw, namespaces)?,
11179 "c:chart",
11180 )?;
11181 self.boundary = self.boundary.max(8);
11182 }
11183 b"spPr" => {
11184 parse_chart_shape_properties(&raw, namespaces, &mut self.sp_pr)?;
11185 self.boundary = self.boundary.max(9);
11186 }
11187 b"txPr" => {
11188 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
11189 let inherited = namespaces
11190 .iter()
11191 .map(|(prefix, uri)| {
11192 Ok((
11193 std::str::from_utf8(prefix)
11194 .map_err(OxmlError::from)?
11195 .to_owned(),
11196 uri.clone(),
11197 ))
11198 })
11199 .collect::<Result<Vec<_>>>()?;
11200 let text = CT_TextBody::from_xml_as_with_namespaces(&raw, b"txPr", &inherited)?;
11201 let mut writer = Writer::new(Vec::new());
11202 text.write_xml_as(&mut writer, "c:txPr")?;
11203 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
11204 set_once(&mut self.tx_pr, text, "c:txPr")?;
11205 self.boundary = self.boundary.max(10);
11206 }
11207 _ => {
11208 self.raw_children
11209 .push(chart_space_raw_slot(name).unwrap_or(self.boundary), raw);
11210 self.boundary = self.boundary.max(chart_space_raw_boundary_after(name));
11211 }
11212 }
11213 Ok(())
11214 }
11215
11216 fn finish(
11217 self,
11218 raw_attributes: XmlAttributes,
11219 namespace_declarations: XmlAttributes,
11220 ) -> Result<CT_ChartSpace> {
11221 Ok(CT_ChartSpace {
11222 chart: self
11223 .chart
11224 .ok_or_else(|| ChartError::MissingElement("c:chart".to_owned()))?,
11225 sp_pr: self.sp_pr,
11226 tx_pr: self.tx_pr,
11227 raw_attributes,
11228 namespace_declarations,
11229 raw_children: raw_children_in_schema_order(&self.raw_children, 14),
11230 })
11231 }
11232}
11233
11234fn raw_children_in_schema_order(
11235 raw_children: &OrderedRawChildren,
11236 final_boundary: usize,
11237) -> OrderedRawChildren {
11238 let mut ordered = OrderedRawChildren::default();
11239 for boundary in 0..=final_boundary {
11240 for raw in raw_children.at(boundary) {
11241 ordered.push(boundary, raw.to_vec());
11242 }
11243 }
11244 ordered
11245}
11246
11247fn chart_space_raw_slot(name: &[u8]) -> Option<usize> {
11248 match name {
11249 b"date1904" => Some(0),
11250 b"lang" => Some(1),
11251 b"roundedCorners" => Some(2),
11252 b"style" => Some(3),
11253 b"clrMapOvr" => Some(4),
11254 b"pivotSource" => Some(5),
11255 b"protection" => Some(6),
11256 b"externalData" => Some(10),
11257 b"printSettings" => Some(11),
11258 b"userShapes" => Some(12),
11259 b"extLst" => Some(13),
11260 _ => None,
11261 }
11262}
11263
11264fn chart_space_raw_boundary_after(name: &[u8]) -> usize {
11265 match name {
11266 b"date1904" => 1,
11267 b"lang" => 2,
11268 b"roundedCorners" => 3,
11269 b"style" => 4,
11270 b"clrMapOvr" => 5,
11271 b"pivotSource" => 6,
11272 b"protection" => 7,
11273 b"chart" => 8,
11274 b"spPr" => 9,
11275 b"txPr" => 10,
11276 b"externalData" => 11,
11277 b"printSettings" => 12,
11278 b"userShapes" => 13,
11279 b"extLst" => 14,
11280 _ => 0,
11281 }
11282}
11283
11284fn parse_bool_value(xml: &[u8], local: &str) -> Result<(bool, ScalarMarkup)> {
11285 let (value, markup) = scalar_value(xml, local)?;
11286 let value = match value.as_deref().unwrap_or("1") {
11287 "1" | "true" => true,
11288 "0" | "false" => false,
11289 invalid => return Err(invalid_attribute(local, "val", invalid.to_owned())),
11290 };
11291 Ok((value, markup))
11292}
11293
11294fn parse_enum_value(xml: &[u8], local: &str) -> Result<(DispBlanksAs, ScalarMarkup)> {
11295 let (value, markup) = scalar_value(xml, local)?;
11296 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
11297 let parsed =
11298 DispBlanksAs::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
11299 Ok((parsed, markup))
11300}
11301
11302fn scalar_value(xml: &[u8], expected: &str) -> Result<(Option<String>, ScalarMarkup)> {
11303 let mut reader = Reader::from_reader(xml);
11304 let mut buffer = Vec::new();
11305 loop {
11306 match reader
11307 .read_event_into(&mut buffer)
11308 .map_err(OxmlError::from)?
11309 {
11310 Event::Empty(element)
11311 if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
11312 {
11313 reject_conflicting_prefix(&element, b"c", C_NS)?;
11314 return Ok((
11315 attribute_value(&element, b"val")?,
11316 ScalarMarkup {
11317 raw_attributes: capture_attributes_except(&element, b"val")?,
11318 raw_content: Vec::new(),
11319 },
11320 ));
11321 }
11322 Event::Start(element)
11323 if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
11324 {
11325 reject_conflicting_prefix(&element, b"c", C_NS)?;
11326 let value = attribute_value(&element, b"val")?;
11327 let raw_attributes = capture_attributes_except(&element, b"val")?;
11328 let mut raw_content = Vec::new();
11329 let mut inner = Vec::new();
11330 loop {
11331 match reader
11332 .read_event_into(&mut inner)
11333 .map_err(OxmlError::from)?
11334 {
11335 Event::End(end)
11336 if matches_local_name(end.name().as_ref(), expected.as_bytes()) =>
11337 {
11338 return Ok((
11339 value,
11340 ScalarMarkup {
11341 raw_attributes,
11342 raw_content,
11343 },
11344 ));
11345 }
11346 event @ (Event::Text(_)
11347 | Event::CData(_)
11348 | Event::Comment(_)
11349 | Event::PI(_)
11350 | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
11351 Event::Eof => return Err(missing_end(expected)),
11352 _ => return Err(ChartError::UnexpectedElement(expected.to_owned())),
11353 }
11354 inner.clear();
11355 }
11356 }
11357 Event::Start(element) | Event::Empty(element) => {
11358 return Err(ChartError::UnexpectedElement(element_name(&element)));
11359 }
11360 Event::Eof => return Err(ChartError::MissingElement(expected.to_owned())),
11361 _ => {}
11362 }
11363 buffer.clear();
11364 }
11365}
11366
11367fn set_once<T>(slot: &mut Option<T>, value: T, name: &str) -> Result<()> {
11368 if slot.is_some() {
11369 return Err(ChartError::DuplicateElement(name.to_owned()));
11370 }
11371 *slot = Some(value);
11372 Ok(())
11373}
11374
11375fn mark_once(seen: &mut bool, name: &str) -> Result<()> {
11376 if *seen {
11377 return Err(ChartError::DuplicateElement(name.to_owned()));
11378 }
11379 *seen = true;
11380 Ok(())
11381}
11382
11383fn validate_chart_space_namespace(element: &BytesStart<'_>) -> Result<()> {
11384 let name = element.name();
11385 let qualified = name.as_ref();
11386 let declaration = match qualified.iter().position(|byte| *byte == b':') {
11387 Some(position) => {
11388 let mut declaration = b"xmlns:".to_vec();
11389 declaration.extend_from_slice(&qualified[..position]);
11390 declaration
11391 }
11392 None => b"xmlns".to_vec(),
11393 };
11394 let namespace = attribute_value(element, &declaration)?
11395 .ok_or_else(|| invalid_attribute("chartSpace", "namespace", "<missing>".to_owned()))?;
11396 if namespace != C_NS {
11397 return Err(invalid_attribute("chartSpace", "namespace", namespace));
11398 }
11399 reject_conflicting_prefix(element, b"c", C_NS)?;
11400 reject_conflicting_prefix(element, b"a", A_NS)?;
11401 reject_conflicting_prefix(element, b"r", R_NS)
11402}
11403
11404fn reject_conflicting_prefix_in_xml(xml: &[u8], prefix: &[u8], namespace: &str) -> Result<()> {
11405 let mut reader = Reader::from_reader(xml);
11406 let mut buffer = Vec::new();
11407 loop {
11408 match reader
11409 .read_event_into(&mut buffer)
11410 .map_err(OxmlError::from)?
11411 {
11412 Event::Start(element) | Event::Empty(element) => {
11413 return reject_conflicting_prefix(&element, prefix, namespace);
11414 }
11415 Event::Eof => return Err(ChartError::MissingElement("XML root".to_owned())),
11416 _ => {}
11417 }
11418 buffer.clear();
11419 }
11420}
11421
11422fn reject_rewritten_foreign_elements(
11423 original: &[u8],
11424 written: &[u8],
11425 inherited: &NamespaceBindings,
11426 root_local: &[u8],
11427) -> Result<()> {
11428 let original = foreign_element_regions(original, inherited, root_local)?;
11429 let mut written_namespaces = inherited.clone();
11430 written_namespaces.retain(|(prefix, _)| prefix != b"a");
11431 written_namespaces.push((b"a".to_vec(), A_NS.to_owned()));
11432 let written = foreign_element_regions(written, &written_namespaces, root_local)?;
11433 if original != written {
11434 return Err(invalid_attribute(
11435 &String::from_utf8_lossy(root_local),
11436 "namespace",
11437 "foreign DrawingML content would be converted to typed content".to_owned(),
11438 ));
11439 }
11440 Ok(())
11441}
11442
11443fn foreign_element_regions(
11444 xml: &[u8],
11445 inherited: &NamespaceBindings,
11446 root_local: &[u8],
11447) -> Result<Vec<Vec<u8>>> {
11448 let mut reader = Reader::from_reader(xml);
11449 let mut buffer = Vec::new();
11450 let root_namespaces = loop {
11451 match reader
11452 .read_event_into(&mut buffer)
11453 .map_err(OxmlError::from)?
11454 {
11455 Event::Start(element) if matches_local_name(element.name().as_ref(), root_local) => {
11456 break bindings_with_local(inherited, &element)?;
11457 }
11458 Event::Empty(element) if matches_local_name(element.name().as_ref(), root_local) => {
11459 return Ok(Vec::new());
11460 }
11461 Event::Start(element) | Event::Empty(element) => {
11462 return Err(ChartError::UnexpectedElement(element_name(&element)));
11463 }
11464 Event::Eof => {
11465 return Err(ChartError::MissingElement(
11466 String::from_utf8_lossy(root_local).into_owned(),
11467 ));
11468 }
11469 _ => {}
11470 }
11471 buffer.clear();
11472 };
11473
11474 let mut namespace_stack = vec![root_namespaces];
11475 let mut foreign = Vec::new();
11476 let mut child_buffer = Vec::new();
11477 loop {
11478 match reader
11479 .read_event_into(&mut child_buffer)
11480 .map_err(OxmlError::from)?
11481 {
11482 Event::Start(element) => {
11483 let namespaces = namespace_stack.last().expect("root namespace binding");
11484 if element_is_in_namespace(&element, A_NS, namespaces)? {
11485 namespace_stack.push(bindings_with_local(namespaces, &element)?);
11486 } else {
11487 foreign.push(capture_element(&mut reader, &element)?);
11488 }
11489 }
11490 Event::Empty(element) => {
11491 let namespaces = namespace_stack.last().expect("root namespace binding");
11492 if !element_is_in_namespace(&element, A_NS, namespaces)? {
11493 foreign.push(capture_empty_element(&element)?);
11494 }
11495 }
11496 Event::End(element) if matches_local_name(element.name().as_ref(), root_local) => {
11497 return Ok(foreign);
11498 }
11499 Event::End(_) => {
11500 namespace_stack.pop();
11501 }
11502 Event::Eof => {
11503 return Err(missing_end(&String::from_utf8_lossy(root_local)));
11504 }
11505 _ => {}
11506 }
11507 child_buffer.clear();
11508 }
11509}
11510
11511fn reject_conflicting_prefix(
11512 element: &BytesStart<'_>,
11513 prefix: &[u8],
11514 namespace: &str,
11515) -> Result<()> {
11516 let mut key = b"xmlns:".to_vec();
11517 key.extend_from_slice(prefix);
11518 if let Some(value) = attribute_value(element, &key)?
11519 && value != namespace
11520 {
11521 return Err(invalid_attribute(
11522 &element_name(element),
11523 &String::from_utf8_lossy(&key),
11524 value,
11525 ));
11526 }
11527 Ok(())
11528}
11529
11530fn namespace_bindings(element: &BytesStart<'_>) -> Result<NamespaceBindings> {
11531 let mut bindings = Vec::new();
11532 for attribute in element.attributes() {
11533 let attribute = attribute.map_err(OxmlError::from)?;
11534 let key = attribute.key.as_ref();
11535 let prefix = if key == b"xmlns" {
11536 Vec::new()
11537 } else if let Some(prefix) = key.strip_prefix(b"xmlns:") {
11538 prefix.to_vec()
11539 } else {
11540 continue;
11541 };
11542 let namespace = attribute
11543 .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
11544 .map_err(OxmlError::from)?
11545 .into_owned();
11546 bindings.push((prefix, namespace));
11547 }
11548 Ok(bindings)
11549}
11550
11551fn bindings_with_local(
11552 inherited: &NamespaceBindings,
11553 element: &BytesStart<'_>,
11554) -> Result<NamespaceBindings> {
11555 let mut bindings = inherited.clone();
11556 for (prefix, namespace) in namespace_bindings(element)? {
11557 upsert_namespace_binding(&mut bindings, prefix, namespace);
11558 }
11559 Ok(bindings)
11560}
11561
11562fn upsert_namespace_binding(bindings: &mut NamespaceBindings, prefix: Vec<u8>, namespace: String) {
11563 if let Some((_, current)) = bindings.iter_mut().find(|(current, _)| current == &prefix) {
11564 *current = namespace;
11565 } else {
11566 bindings.push((prefix, namespace));
11567 }
11568}
11569
11570fn element_is_in_namespace(
11571 element: &BytesStart<'_>,
11572 expected: &str,
11573 inherited: &NamespaceBindings,
11574) -> Result<bool> {
11575 let name = element.name();
11576 let qualified = name.as_ref();
11577 let prefix = qualified
11578 .iter()
11579 .position(|byte| *byte == b':')
11580 .map(|position| &qualified[..position])
11581 .unwrap_or_default();
11582 let local = namespace_bindings(element)?;
11583 let namespace = local
11584 .iter()
11585 .rev()
11586 .chain(inherited.iter().rev())
11587 .find(|(candidate, _)| candidate.as_slice() == prefix)
11588 .map(|(_, namespace)| namespace.as_str());
11589 Ok(namespace == Some(expected))
11590}
11591
11592fn attribute_value(element: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
11593 for attribute in element.attributes() {
11594 let attribute = attribute.map_err(OxmlError::from)?;
11595 if attribute.key.as_ref() == name {
11596 return Ok(Some(
11597 attribute
11598 .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
11599 .map_err(OxmlError::from)?
11600 .into_owned(),
11601 ));
11602 }
11603 }
11604 Ok(None)
11605}
11606
11607fn capture_root_attributes(start: &BytesStart<'_>) -> Result<RootAttributes> {
11608 let mut attributes = Vec::new();
11609 let mut namespaces = Vec::new();
11610 for attribute in start.attributes() {
11611 let attribute = attribute.map_err(OxmlError::from)?;
11612 let name = std::str::from_utf8(attribute.key.as_ref())
11613 .map_err(OxmlError::from)?
11614 .to_owned();
11615 let value = attribute
11616 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
11617 .map_err(OxmlError::from)?
11618 .into_owned();
11619 if name == "xmlns:c" || name == "xmlns:a" || name == "xmlns:r" {
11620 continue;
11621 }
11622 if name == "xmlns" || name.starts_with("xmlns:") {
11623 namespaces.push((name, value));
11624 } else {
11625 attributes.push((name, value));
11626 }
11627 }
11628 Ok((attributes, namespaces))
11629}
11630
11631fn capture_attributes(start: &BytesStart<'_>) -> Result<XmlAttributes> {
11632 let mut attributes = Vec::new();
11633 for attribute in start.attributes() {
11634 let attribute = attribute.map_err(OxmlError::from)?;
11635 attributes.push((
11636 std::str::from_utf8(attribute.key.as_ref())
11637 .map_err(OxmlError::from)?
11638 .to_owned(),
11639 attribute
11640 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
11641 .map_err(OxmlError::from)?
11642 .into_owned(),
11643 ));
11644 }
11645 Ok(attributes)
11646}
11647
11648fn capture_attributes_except(start: &BytesStart<'_>, excluded: &[u8]) -> Result<XmlAttributes> {
11649 let mut attributes = Vec::new();
11650 for attribute in start.attributes() {
11651 let attribute = attribute.map_err(OxmlError::from)?;
11652 if attribute.key.as_ref() == excluded {
11653 continue;
11654 }
11655 attributes.push((
11656 std::str::from_utf8(attribute.key.as_ref())
11657 .map_err(OxmlError::from)?
11658 .to_owned(),
11659 attribute
11660 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
11661 .map_err(OxmlError::from)?
11662 .into_owned(),
11663 ));
11664 }
11665 Ok(attributes)
11666}
11667
11668fn capture_attributes_excluding(
11669 start: &BytesStart<'_>,
11670 excluded: &[&[u8]],
11671) -> Result<XmlAttributes> {
11672 let mut attributes = Vec::new();
11673 for attribute in start.attributes() {
11674 let attribute = attribute.map_err(OxmlError::from)?;
11675 if excluded.contains(&attribute.key.as_ref()) {
11676 continue;
11677 }
11678 attributes.push((
11679 std::str::from_utf8(attribute.key.as_ref())
11680 .map_err(OxmlError::from)?
11681 .to_owned(),
11682 attribute
11683 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
11684 .map_err(OxmlError::from)?
11685 .into_owned(),
11686 ));
11687 }
11688 Ok(attributes)
11689}
11690
11691fn push_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
11692 for (name, value) in attributes {
11693 start.push_attribute((name.as_str(), value.as_str()));
11694 }
11695}
11696
11697fn write_scalar<W: Write>(
11698 writer: &mut Writer<W>,
11699 name: &str,
11700 value: &str,
11701 markup: Option<&ScalarMarkup>,
11702) -> Result<()> {
11703 let mut start = BytesStart::new(name);
11704 start.push_attribute(("val", value));
11705 if let Some(markup) = markup {
11706 push_attributes(&mut start, &markup.raw_attributes);
11707 }
11708 let has_content = markup.is_some_and(|markup| !markup.raw_content.is_empty());
11709 if !has_content {
11710 writer
11711 .write_event(Event::Empty(start))
11712 .map_err(OxmlError::from)?;
11713 return Ok(());
11714 }
11715 writer
11716 .write_event(Event::Start(start))
11717 .map_err(OxmlError::from)?;
11718 if let Some(markup) = markup {
11719 for raw in &markup.raw_content {
11720 writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
11721 }
11722 }
11723 writer
11724 .write_event(Event::End(BytesEnd::new(name)))
11725 .map_err(OxmlError::from)?;
11726 Ok(())
11727}
11728
11729fn capture_event(event: Event<'_>) -> Result<Vec<u8>> {
11730 let mut writer = Writer::new(Vec::new());
11731 writer
11732 .write_event(event.into_owned())
11733 .map_err(OxmlError::from)?;
11734 Ok(writer.into_inner())
11735}
11736
11737fn emit_raw<'a, W: Write>(
11738 writer: &mut Writer<W>,
11739 children: impl Iterator<Item = &'a [u8]>,
11740) -> Result<()> {
11741 for child in children {
11742 writer.get_mut().write_all(child).map_err(OxmlError::from)?;
11743 }
11744 Ok(())
11745}
11746
11747fn invalid_attribute(element: &str, attribute: &str, value: String) -> ChartError {
11748 ChartError::InvalidAttribute {
11749 element: element.to_owned(),
11750 attribute: attribute.to_owned(),
11751 value,
11752 }
11753}
11754
11755fn element_name(element: &BytesStart<'_>) -> String {
11756 String::from_utf8_lossy(element.name().as_ref()).into_owned()
11757}
11758
11759fn missing_end(element: &str) -> ChartError {
11760 ChartError::Xml(OxmlError::MissingElement(format!("closing {element}")))
11761}
11762
11763#[cfg(test)]
11764mod tests {
11765 use std::collections::HashSet;
11766 use std::fs;
11767 use std::path::{Path, PathBuf};
11768 use std::process::Command;
11769
11770 use oxml_core::raw_xml::{capture_element, capture_empty_element};
11771 use oxml_drawing::color::{ColorMap, ColorMapSlot, RgbColor, ThemeColorSlot};
11772 use oxml_drawing::theme::CT_OfficeStyleSheet;
11773 use oxml_layout::{
11774 Color, FontManager, LayoutResult, PageFrame, Paint, PathCommand, Point, PositionedElement,
11775 Rect,
11776 };
11777 use oxml_opc::OpcPackage;
11778 use quick_xml::Reader;
11779 use quick_xml::events::Event;
11780
11781 use super::{
11782 A_NS, Axis, AxisData, AxisId, AxisKind, AxisPosition, BarDirection, BarGrouping, C_NS,
11783 CT_ChartSpace, CT_DLbls, CT_ShapeProperties, CT_TextBody, ChartData, ChartGeometry,
11784 ChartKind, DataLabelPosition, DispBlanksAs, Domain, Grouping, NumberFormat, NumericData,
11785 Orientation, Plot, R_NS, ScatterStyle, Series, StringRef, TickLabelPosition, TickMark,
11786 authored_chart_parts, capture_event, chart_workbook_relationship_id, local_name,
11787 matches_local_name, nice_number_scale, read_chart_data,
11788 render_chart as render_chart_with_theme, render_geometry as render_geometry_with_theme,
11789 replace_chart_data,
11790 };
11791
11792 const MANIFEST: &str = include_str!("../../../scripts/pptx-corpus-manifest.tsv");
11793 const EXPECTED_DECKS: usize = 50;
11794 const LIBREOFFICE_VERSION: &str =
11795 "LibreOffice 26.2.5.2 cd7284b4cbbfeb507e630c1aac019f4157393acb";
11796 const PDFTOTEXT_VERSION: &str = "pdftotext version 26.01.0";
11797 const PDFTOPPM_VERSION: &str = "pdftoppm version 26.01.0";
11798 const PLOT_RENDER_NORMALIZED_MAE_THRESHOLD: f64 = 0.0;
11799
11800 #[test]
11801 fn authored_charts_emit_portable_viewer_defaults() {
11802 let data = ChartData {
11803 categories: vec!["August".to_owned(), "September".to_owned()],
11804 series: vec![("Price".to_owned(), vec![12.5, 19.2])],
11805 number_format: Some(r"0.##\%".to_owned()),
11806 category_axis_title: Some("Month".to_owned()),
11807 value_axis_title: Some("Change".to_owned()),
11808 palette: vec![
11809 RgbColor::parse("2B6FE3").unwrap(),
11810 RgbColor::parse("F0761F").unwrap(),
11811 ],
11812 };
11813 let (line, line_workbook) = authored_chart_parts(ChartKind::Line, &data, "rId1").unwrap();
11814 let line = std::str::from_utf8(&line).unwrap();
11815 assert_eq!(line.matches(r#"<c:delete val="0"/>"#).count(), 2);
11816 assert!(line.contains(r#"<c:numFmt formatCode="0.##\%" sourceLinked="0"/>"#));
11817 assert!(line.contains("<a:t>Month</a:t>"));
11818 assert!(line.contains("<a:t>Change</a:t>"));
11819 assert!(line.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
11820 let assert_portable_line = |xml: &str| {
11821 let titles = xml
11822 .split("<c:title>")
11823 .skip(1)
11824 .map(|tail| tail.split_once("</c:title>").unwrap().0)
11825 .collect::<Vec<_>>();
11826 assert_eq!(titles.len(), 2);
11827 for title in titles {
11828 let text = title.find("<c:tx>").unwrap();
11829 let layout = title.find("<c:layout/>").unwrap();
11830 let overlay = title.find(r#"<c:overlay val="0"/>"#).unwrap();
11831 assert!(text < layout && layout < overlay);
11832 }
11833
11834 let line_plot = xml
11835 .split_once("<c:lineChart>")
11836 .unwrap()
11837 .1
11838 .split_once("</c:lineChart>")
11839 .unwrap()
11840 .0;
11841 assert_eq!(line_plot.matches(r#"<c:marker val="0"/>"#).count(), 1);
11842 assert_eq!(line_plot.matches(r#"<c:smooth val="0"/>"#).count(), 1);
11843 let marker = line_plot.find(r#"<c:marker val="0"/>"#).unwrap();
11844 let smooth = line_plot.find(r#"<c:smooth val="0"/>"#).unwrap();
11845 let axis_id = line_plot.find("<c:axId").unwrap();
11846 assert!(marker < smooth && smooth < axis_id);
11847 };
11848 assert_portable_line(line);
11849 for axis in line.split("<c:crossAx").take(2) {
11850 let scaling = axis.rfind("<c:scaling").unwrap();
11851 let deleted = axis.rfind("<c:delete").unwrap();
11852 let position = axis.rfind("<c:axPos").unwrap();
11853 assert!(scaling < deleted && deleted < position);
11854 }
11855
11856 let parsed = CT_ChartSpace::from_xml(line.as_bytes()).unwrap();
11857 let rewritten = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
11858 assert_portable_line(&rewritten);
11859 assert!(rewritten.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
11860 assert!(rewritten.contains(r#"<c:externalData r:id="rId1">"#));
11861
11862 for kind in [
11863 ChartKind::Bar,
11864 ChartKind::Pie,
11865 ChartKind::Doughnut,
11866 ChartKind::Area,
11867 ChartKind::Scatter,
11868 ChartKind::Radar,
11869 ] {
11870 let mut kind_data = data.clone();
11871 if kind == ChartKind::Scatter {
11872 kind_data.categories = vec!["1".to_owned(), "2".to_owned()];
11873 }
11874 let (chart, workbook) = authored_chart_parts(kind, &kind_data, "rId1").unwrap();
11875 let chart = std::str::from_utf8(&chart).unwrap();
11876 assert!(!chart.contains(r#"<c:marker val="0"/>"#));
11877 assert!(!chart.contains(r#"<c:smooth val="0"/>"#));
11878 if kind != ChartKind::Scatter {
11879 assert_eq!(workbook, line_workbook);
11880 }
11881 if matches!(kind, ChartKind::Pie | ChartKind::Doughnut) {
11882 assert!(chart.contains("<c:legend"));
11883 assert!(chart.contains(r#"<c:showPercent val="1"/>"#));
11884 }
11885 if kind == ChartKind::Doughnut {
11886 assert!(chart.contains(r#"<c:holeSize val="50"/>"#));
11887 }
11888 if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) {
11889 assert_eq!(chart.matches("<c:dPt>").count(), 2);
11890 assert!(chart.contains(
11891 r#"<c:dPt><c:idx val="0"/><c:spPr><a:solidFill><a:srgbClr val="2B6FE3"/>"#
11892 ));
11893 assert!(chart.contains(
11894 r#"<c:dPt><c:idx val="1"/><c:spPr><a:solidFill><a:srgbClr val="F0761F"/>"#
11895 ));
11896 }
11897
11898 let parsed = CT_ChartSpace::from_xml(chart.as_bytes()).unwrap();
11899 let rewritten = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
11900 assert_eq!(
11901 rewritten.matches("<c:dPt>").count(),
11902 usize::from(matches!(
11903 kind,
11904 ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut
11905 )) * 2
11906 );
11907 assert!(rewritten.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
11908 if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) {
11909 assert!(rewritten.contains(r#"<a:srgbClr val="F0761F"/>"#));
11910 }
11911 }
11912 }
11913
11914 #[test]
11915 fn optional_numeric_cache_format_remains_omitted() {
11916 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>"#;
11917 let mut parsed = NumericData::from_xml(xml).unwrap();
11918 assert_eq!(parsed.format_code, "General");
11919 let written = parsed.to_xml().unwrap();
11920 assert!(!String::from_utf8_lossy(&written).contains("formatCode"));
11921 assert_eq!(NumericData::from_xml(&written).unwrap(), parsed);
11922
11923 parsed.format_code = "0.00".to_owned();
11924 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
11925 assert!(written.contains("<c:formatCode>0.00</c:formatCode>"));
11926 }
11927
11928 fn render_geometry(chart: &super::CT_Chart, bounds: Rect) -> super::Result<ChartGeometry> {
11929 render_geometry_with_theme(
11930 chart,
11931 bounds,
11932 &CT_OfficeStyleSheet::office_default(),
11933 &ColorMap::default(),
11934 )
11935 }
11936
11937 fn render_chart(
11938 chart: &super::CT_Chart,
11939 bounds: Rect,
11940 fonts: &mut FontManager,
11941 ) -> super::Result<oxml_layout::GroupElement> {
11942 render_chart_with_theme(
11943 chart,
11944 bounds,
11945 &CT_OfficeStyleSheet::office_default(),
11946 &ColorMap::default(),
11947 fonts,
11948 )
11949 }
11950
11951 #[test]
11952 fn bar_chart_rasterises_at_computed_positions() {
11953 let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11954 let geometry = render_geometry(
11955 &chart.chart,
11956 Rect {
11957 x: 0.0,
11958 y: 0.0,
11959 width: 200.0,
11960 height: 140.0,
11961 },
11962 )
11963 .unwrap();
11964 assert_eq!(
11965 geometry.plot_bounds,
11966 Rect {
11967 x: 36.0,
11968 y: 12.0,
11969 width: 152.0,
11970 height: 100.0,
11971 }
11972 );
11973 let bounds = path_bounds(&geometry);
11974 assert_eq!(bounds.len(), 2);
11975 assert_rect_near(
11976 bounds[0],
11977 Rect {
11978 x: 58.8,
11979 y: 62.0,
11980 width: 30.4,
11981 height: 50.0,
11982 },
11983 );
11984 assert_rect_near(
11985 bounds[1],
11986 Rect {
11987 x: 134.8,
11988 y: 12.0,
11989 width: 30.4,
11990 height: 100.0,
11991 },
11992 );
11993
11994 let layout = LayoutResult::new(
11995 vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
11996 Vec::new(),
11997 None,
11998 Vec::new(),
11999 );
12000 let png = oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap();
12001 let pixmap = tiny_skia::Pixmap::decode_png(&png).unwrap();
12002 for (x, y) in [(74, 87), (150, 37)] {
12003 let pixel = pixmap.pixel(x, y).unwrap();
12004 assert_ne!((pixel.red(), pixel.green(), pixel.blue()), (255, 255, 255));
12005 }
12006 }
12007
12008 #[test]
12009 fn unstyled_four_series_use_accent_one_through_four() {
12010 let chart = four_series_bar_chart();
12011 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12012 let colours = path_colours(&geometry);
12013
12014 assert_eq!(
12015 &colours[..4],
12016 &[
12017 Color::from_hex("156082"),
12018 Color::from_hex("E97132"),
12019 Color::from_hex("196B24"),
12020 Color::from_hex("0F9ED5"),
12021 ]
12022 );
12023 }
12024
12025 #[test]
12026 fn direct_series_solid_colour_overrides_theme_accent() {
12027 for (paint, expected) in [
12028 (
12029 "<a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill>",
12030 Color::from_hex("123456"),
12031 ),
12032 (
12033 "<a:ln><a:solidFill><a:srgbClr val=\"654321\"/></a:solidFill></a:ln>",
12034 Color::from_hex("654321"),
12035 ),
12036 (
12037 "<a:noFill/>",
12038 Color {
12039 r: 0.0,
12040 g: 0.0,
12041 b: 0.0,
12042 a: 0.0,
12043 },
12044 ),
12045 ] {
12046 let styled =
12047 plot_series(0).replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
12048 let plot = bar_plot("").replace(&plot_series(0), &styled);
12049 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
12050 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12051
12052 assert_eq!(path_colours(&geometry)[0], expected);
12053 }
12054
12055 let styled = plot_series(0).replace(
12056 "<q:cat>",
12057 "<q:spPr><a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill></q:spPr><q:cat>",
12058 );
12059 let plot = bar_plot("").replace(&plot_series(0), &styled);
12060 let xml = chart_with_plot(&plot).replace(
12061 "</q:plotArea></q:chart>",
12062 "</q:plotArea><q:legend/></q:chart>",
12063 );
12064 let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
12065 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12066 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12067 assert!(group.children.iter().any(|element| matches!(
12068 element,
12069 PositionedElement::Path(path)
12070 if path.fill == Some(Paint::Solid(Color::from_hex("123456")))
12071 )));
12072
12073 let mut theme = CT_OfficeStyleSheet::office_default();
12074 let preset = oxml_drawing::fill::Fill::from_xml(
12075 br#"<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:prstClr val="red"/></a:solidFill>"#,
12076 )
12077 .unwrap();
12078 let oxml_drawing::fill::Fill::Solid(preset) = preset else {
12079 panic!("expected solid fill");
12080 };
12081 theme.theme_elements.color_scheme.hyperlink = preset.color.unwrap();
12082 let geometry =
12083 render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &ColorMap::default())
12084 .unwrap();
12085 assert_eq!(path_colours(&geometry)[0], Color::from_hex("123456"));
12086
12087 for (paint, expected_alpha) in [
12088 ("<a:noFill/>", 0.0),
12089 (
12090 "<a:solidFill><a:srgbClr val=\"123456\"><a:alpha val=\"50000\"/></a:srgbClr></a:solidFill>",
12091 (128.0 / 255.0) * 0.55,
12092 ),
12093 ] {
12094 let (_, area, _) = remaining_plot_fixtures()
12095 .into_iter()
12096 .find(|(kind, _, _)| *kind == "areaChart")
12097 .unwrap();
12098 let styled_area = area.replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
12099 let chart =
12100 CT_ChartSpace::from_xml(chart_with_optional_axes(&styled_area, true).as_bytes())
12101 .unwrap();
12102 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12103 let actual_alpha = path_colours(&geometry)[0].a;
12104
12105 assert!((actual_alpha - expected_alpha).abs() < 1.0e-12);
12106 }
12107 }
12108
12109 #[test]
12110 fn series_accent_cycle_repeats_after_six() {
12111 let chart = seven_series_bar_chart();
12112 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12113 let colours = path_colours(&geometry);
12114
12115 assert_eq!(colours[0], Color::from_hex("156082"));
12116 assert_eq!(colours[6], colours[0]);
12117 }
12118
12119 #[test]
12120 fn series_colours_honor_colour_map_and_transform_order() {
12121 let styled = plot_series(0).replace(
12122 "<q:cat>",
12123 "<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>",
12124 );
12125 let plot = bar_plot("").replace(&plot_series(0), &styled);
12126 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
12127 let color_map =
12128 ColorMap::default().with_overrides(&[(ColorMapSlot::Accent1, ThemeColorSlot::Accent4)]);
12129 let mut theme = CT_OfficeStyleSheet::office_default();
12130 let theme_fill = oxml_drawing::fill::Fill::from_xml(
12131 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>"#,
12132 )
12133 .unwrap();
12134 let oxml_drawing::fill::Fill::Solid(theme_fill) = theme_fill else {
12135 panic!("expected solid fill");
12136 };
12137 let theme_colour = theme_fill.color.unwrap();
12138 theme.theme_elements.color_scheme.accent4 = theme_colour.clone();
12139 let geometry =
12140 render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &color_map).unwrap();
12141 let series_fill = oxml_drawing::fill::Fill::from_xml(
12142 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>"#,
12143 )
12144 .unwrap();
12145 let oxml_drawing::fill::Fill::Solid(series_fill) = series_fill else {
12146 panic!("expected solid fill");
12147 };
12148 let mut transforms = theme_colour.transforms().to_vec();
12149 transforms.extend_from_slice(series_fill.color.as_ref().unwrap().transforms());
12150 let expected = oxml_drawing::color::apply_color_transforms(
12151 RgbColor::parse("336699").unwrap(),
12152 &transforms,
12153 );
12154
12155 assert_eq!(path_colours(&geometry)[0], resolved_layout_colour(expected));
12156 }
12157
12158 #[test]
12159 fn chart_colours_do_not_use_word_tint_shade() {
12160 let styled = plot_series(0).replace(
12161 "<q:cat>",
12162 "<q:spPr><a:solidFill><a:srgbClr val=\"808080\"><a:tint val=\"50000\"/></a:srgbClr></a:solidFill></q:spPr><q:cat>",
12163 );
12164 let plot = bar_plot("").replace(&plot_series(0), &styled);
12165 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
12166 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12167
12168 assert_eq!(
12169 path_colours(&geometry)[0],
12170 Color {
12171 r: 205.0 / 255.0,
12172 g: 205.0 / 255.0,
12173 b: 205.0 / 255.0,
12174 a: 1.0,
12175 }
12176 );
12177 }
12178
12179 #[test]
12180 fn unsupported_direct_series_paint_is_contextual() {
12181 let styled = plot_series(0).replace(
12182 "<q:cat>",
12183 "<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>",
12184 );
12185 let plot = bar_plot("").replace(&plot_series(0), &styled);
12186 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
12187 let error = render_geometry(&chart.chart, chart_bounds()).unwrap_err();
12188
12189 assert!(error.to_string().contains("c:ser[0] direct fill paint"));
12190 assert!(error.to_string().contains("gradient"));
12191 }
12192
12193 #[test]
12194 fn resolved_chart_palette_raster_is_deterministic() {
12195 let raster = || {
12196 let chart = four_series_bar_chart();
12197 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12198 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12199 let layout = LayoutResult::new(
12200 vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
12201 fonts.all_font_data(),
12202 None,
12203 Vec::new(),
12204 );
12205 oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
12206 };
12207 let chart = four_series_bar_chart();
12208 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12209
12210 assert_eq!(path_colours(&geometry)[0], Color::from_hex("156082"));
12211 assert_eq!(raster(), raster());
12212 }
12213
12214 #[test]
12215 fn bar_geometry_handles_direction_grouping_gap_and_overlap() {
12216 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12217 let Plot::Bar {
12218 overlap, series, ..
12219 } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
12220 else {
12221 panic!("expected bar plot");
12222 };
12223 let mut second = series[0].clone();
12224 second.index = 1;
12225 second.order = 1;
12226 second.values.values = vec![2.0, 1.0];
12227 series.push(second);
12228
12229 *overlap = 25;
12230 let clustered = render_geometry(&chart.chart, chart_bounds()).unwrap();
12231 let clustered_bounds = path_bounds(&clustered);
12232 let clustered_width = 30.4 / 1.75;
12233 let clustered_advance = clustered_width * 0.75;
12234 assert_rects_near(
12235 &clustered_bounds,
12236 &[
12237 Rect {
12238 x: 58.8,
12239 y: 62.0,
12240 width: clustered_width,
12241 height: 50.0,
12242 },
12243 Rect {
12244 x: 134.8,
12245 y: 12.0,
12246 width: clustered_width,
12247 height: 100.0,
12248 },
12249 Rect {
12250 x: 58.8 + clustered_advance,
12251 y: 12.0,
12252 width: clustered_width,
12253 height: 100.0,
12254 },
12255 Rect {
12256 x: 134.8 + clustered_advance,
12257 y: 62.0,
12258 width: clustered_width,
12259 height: 50.0,
12260 },
12261 ],
12262 );
12263
12264 let Plot::Bar { direction, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12265 unreachable!()
12266 };
12267 *direction = BarDirection::Bar;
12268 let horizontal = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
12269 let horizontal_width = 20.0 / 1.75;
12270 let horizontal_advance = horizontal_width * 0.75;
12271 assert_rects_near(
12272 &horizontal,
12273 &[
12274 Rect {
12275 x: 36.0,
12276 y: 27.0,
12277 width: 76.0,
12278 height: horizontal_width,
12279 },
12280 Rect {
12281 x: 36.0,
12282 y: 77.0,
12283 width: 152.0,
12284 height: horizontal_width,
12285 },
12286 Rect {
12287 x: 36.0,
12288 y: 27.0 + horizontal_advance,
12289 width: 152.0,
12290 height: horizontal_width,
12291 },
12292 Rect {
12293 x: 36.0,
12294 y: 77.0 + horizontal_advance,
12295 width: 76.0,
12296 height: horizontal_width,
12297 },
12298 ],
12299 );
12300
12301 let Plot::Bar {
12302 direction,
12303 grouping,
12304 ..
12305 } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
12306 else {
12307 unreachable!()
12308 };
12309 *direction = BarDirection::Column;
12310 *grouping = BarGrouping::Stacked;
12311 let stacked = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
12312 assert_rects_near(
12313 &stacked,
12314 &[
12315 Rect {
12316 x: 58.8,
12317 y: 112.0 - 100.0 / 3.0,
12318 width: 30.4,
12319 height: 100.0 / 3.0,
12320 },
12321 Rect {
12322 x: 134.8,
12323 y: 112.0 - 200.0 / 3.0,
12324 width: 30.4,
12325 height: 200.0 / 3.0,
12326 },
12327 Rect {
12328 x: 58.8,
12329 y: 12.0,
12330 width: 30.4,
12331 height: 200.0 / 3.0,
12332 },
12333 Rect {
12334 x: 134.8,
12335 y: 12.0,
12336 width: 30.4,
12337 height: 100.0 / 3.0,
12338 },
12339 ],
12340 );
12341
12342 let Plot::Bar {
12343 grouping, series, ..
12344 } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
12345 else {
12346 unreachable!()
12347 };
12348 *grouping = BarGrouping::PercentStacked;
12349 series[1].values.values = vec![2.0, 2.0];
12350 let percent = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
12351 assert_rects_near(
12352 &percent,
12353 &[
12354 Rect {
12355 x: 58.8,
12356 y: 112.0 - 100.0 / 3.0,
12357 width: 30.4,
12358 height: 100.0 / 3.0,
12359 },
12360 Rect {
12361 x: 134.8,
12362 y: 62.0,
12363 width: 30.4,
12364 height: 50.0,
12365 },
12366 Rect {
12367 x: 58.8,
12368 y: 12.0,
12369 width: 30.4,
12370 height: 200.0 / 3.0,
12371 },
12372 Rect {
12373 x: 134.8,
12374 y: 12.0,
12375 width: 30.4,
12376 height: 50.0,
12377 },
12378 ],
12379 );
12380 }
12381
12382 #[test]
12383 fn line_scatter_and_radar_emit_paths_and_markers() {
12384 let line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
12385 let line_geometry = render_geometry(&line.chart, chart_bounds()).unwrap();
12386 assert_path_points(
12387 path_commands(&line_geometry)[0],
12388 &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
12389 false,
12390 );
12391 assert_rects_near(
12392 &path_bounds(&line_geometry)[1..],
12393 &[
12394 Rect {
12395 x: 71.5,
12396 y: 109.5,
12397 width: 5.0,
12398 height: 5.0,
12399 },
12400 Rect {
12401 x: 147.5,
12402 y: 9.5,
12403 width: 5.0,
12404 height: 5.0,
12405 },
12406 ],
12407 );
12408
12409 let (scatter_plot, _) = remaining_plot_fixtures()
12410 .into_iter()
12411 .find(|(kind, _, _)| *kind == "scatterChart")
12412 .map(|(_, plot, axes)| (plot, axes))
12413 .unwrap();
12414 let mut scatter =
12415 CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
12416 .unwrap();
12417 let Plot::Scatter { style, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
12418 else {
12419 unreachable!()
12420 };
12421 *style = ScatterStyle::LineMarker;
12422 let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12423 assert_path_points(
12424 path_commands(&scatter_geometry)[0],
12425 &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
12426 false,
12427 );
12428 assert_rects_near(
12429 &path_bounds(&scatter_geometry)[1..],
12430 &[
12431 Rect {
12432 x: 33.5,
12433 y: 109.5,
12434 width: 5.0,
12435 height: 5.0,
12436 },
12437 Rect {
12438 x: 185.5,
12439 y: 9.5,
12440 width: 5.0,
12441 height: 5.0,
12442 },
12443 ],
12444 );
12445
12446 let (radar_plot, _) = remaining_plot_fixtures()
12447 .into_iter()
12448 .find(|(kind, _, _)| *kind == "radarChart")
12449 .map(|(_, plot, axes)| (plot, axes))
12450 .unwrap();
12451 let radar = CT_ChartSpace::from_xml(chart_with_optional_axes(&radar_plot, true).as_bytes())
12452 .unwrap();
12453 let radar_geometry = render_geometry(&radar.chart, chart_bounds()).unwrap();
12454 assert_path_points(
12455 path_commands(&radar_geometry)[0],
12456 &[Point { x: 112.0, y: 37.0 }, Point { x: 112.0, y: 112.0 }],
12457 true,
12458 );
12459 assert_rects_near(
12460 &path_bounds(&radar_geometry)[1..],
12461 &[
12462 Rect {
12463 x: 109.5,
12464 y: 34.5,
12465 width: 5.0,
12466 height: 5.0,
12467 },
12468 Rect {
12469 x: 109.5,
12470 y: 109.5,
12471 width: 5.0,
12472 height: 5.0,
12473 },
12474 ],
12475 );
12476 }
12477
12478 #[test]
12479 fn pie_doughnut_and_area_emit_closed_paths() {
12480 let fixtures = remaining_plot_fixtures();
12481 let pie_plot = fixtures
12482 .iter()
12483 .find(|(kind, _, _)| *kind == "pieChart")
12484 .unwrap();
12485 let pie =
12486 CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot.1, pie_plot.2).as_bytes())
12487 .unwrap();
12488 let pie_geometry = render_geometry(&pie.chart, chart_bounds()).unwrap();
12489 let pie_commands = path_commands(&pie_geometry);
12490 assert_eq!(pie_commands.len(), 2);
12491 let [
12492 PathCommand::MoveTo(center),
12493 PathCommand::LineTo(start),
12494 ..,
12495 PathCommand::CurveTo { to: end, .. },
12496 PathCommand::Close,
12497 ] = pie_commands[0]
12498 else {
12499 panic!("expected closed cubic pie wedge");
12500 };
12501 assert_point_near(*center, Point { x: 112.0, y: 62.0 });
12502 assert_point_near(
12503 *start,
12504 Point {
12505 x: 137.0,
12506 y: 62.0 - 25.0 * 3.0_f64.sqrt(),
12507 },
12508 );
12509 assert_point_near(
12510 *end,
12511 Point {
12512 x: 137.0,
12513 y: 62.0 + 25.0 * 3.0_f64.sqrt(),
12514 },
12515 );
12516
12517 let doughnut_plot = fixtures
12518 .iter()
12519 .find(|(kind, _, _)| *kind == "doughnutChart")
12520 .unwrap();
12521 let doughnut = CT_ChartSpace::from_xml(
12522 chart_with_optional_axes(&doughnut_plot.1, doughnut_plot.2).as_bytes(),
12523 )
12524 .unwrap();
12525 let doughnut_geometry = render_geometry(&doughnut.chart, chart_bounds()).unwrap();
12526 let doughnut_commands = path_commands(&doughnut_geometry);
12527 assert_eq!(doughnut_commands.len(), 2);
12528 let commands = doughnut_commands[0];
12529 assert_eq!(commands.len(), 7);
12530 let PathCommand::MoveTo(outer_start) = commands[0] else {
12531 panic!("expected doughnut outer start");
12532 };
12533 let PathCommand::LineTo(inner_end) = commands[3] else {
12534 panic!("expected doughnut inner-radius join");
12535 };
12536 let PathCommand::CurveTo {
12537 to: inner_start, ..
12538 } = commands[5]
12539 else {
12540 panic!("expected reverse inner arc");
12541 };
12542 let diagonal = 25.0 * 2.0_f64.sqrt();
12543 assert_point_near(
12544 outer_start,
12545 Point {
12546 x: 112.0 + diagonal,
12547 y: 62.0 - diagonal,
12548 },
12549 );
12550 assert_point_near(
12551 inner_end,
12552 Point {
12553 x: 112.0 + 30.0 * 75.0_f64.to_radians().cos(),
12554 y: 62.0 + 30.0 * 75.0_f64.to_radians().sin(),
12555 },
12556 );
12557 let inner_diagonal = 15.0 * 2.0_f64.sqrt();
12558 assert_point_near(
12559 inner_start,
12560 Point {
12561 x: 112.0 + inner_diagonal,
12562 y: 62.0 - inner_diagonal,
12563 },
12564 );
12565 assert_eq!(commands.last(), Some(&PathCommand::Close));
12566
12567 let area_plot = fixtures
12568 .iter()
12569 .find(|(kind, _, _)| *kind == "areaChart")
12570 .unwrap();
12571 let mut area =
12572 CT_ChartSpace::from_xml(chart_with_optional_axes(&area_plot.1, area_plot.2).as_bytes())
12573 .unwrap();
12574 let area_geometry = render_geometry(&area.chart, chart_bounds()).unwrap();
12575 assert_path_points(
12576 path_commands(&area_geometry)[0],
12577 &[
12578 Point { x: 74.0, y: 62.0 },
12579 Point { x: 150.0, y: 12.0 },
12580 Point { x: 150.0, y: 112.0 },
12581 Point { x: 74.0, y: 112.0 },
12582 ],
12583 true,
12584 );
12585
12586 let Plot::Area {
12587 grouping, series, ..
12588 } = &mut area.chart.plot_area.plots_mut().unwrap()[0]
12589 else {
12590 unreachable!()
12591 };
12592 *grouping = Grouping::Stacked;
12593 let mut second = series[0].clone();
12594 second.index = 1;
12595 second.order = 1;
12596 second.values.values = vec![2.0, 1.0];
12597 series.push(second);
12598 let stacked_area = render_geometry(&area.chart, chart_bounds()).unwrap();
12599 assert_path_points(
12600 path_commands(&stacked_area)[1],
12601 &[
12602 Point { x: 74.0, y: 12.0 },
12603 Point { x: 150.0, y: 12.0 },
12604 Point {
12605 x: 150.0,
12606 y: 112.0 - 200.0 / 3.0,
12607 },
12608 Point {
12609 x: 74.0,
12610 y: 112.0 - 100.0 / 3.0,
12611 },
12612 ],
12613 true,
12614 );
12615 }
12616
12617 #[test]
12618 fn sparse_cache_indexes_preserve_slots_and_scatter_pairing() {
12619 let series = sparse_category_series();
12620 let bar = format!(
12621 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>"#
12622 );
12623 let bar = CT_ChartSpace::from_xml(chart_with_plot(&bar).as_bytes()).unwrap();
12624 assert_rects_near(
12625 &path_bounds(&render_geometry(&bar.chart, chart_bounds()).unwrap()),
12626 &[
12627 Rect {
12628 x: 51.2,
12629 y: 62.0,
12630 width: 304.0 / 15.0,
12631 height: 50.0,
12632 },
12633 Rect {
12634 x: 36.0 + 304.0 / 3.0 + 15.2,
12635 y: 12.0,
12636 width: 304.0 / 15.0,
12637 height: 100.0,
12638 },
12639 ],
12640 );
12641
12642 let line = format!(
12643 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>"#
12644 );
12645 let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line).as_bytes()).unwrap();
12646 let first = Point {
12647 x: 36.0 + 76.0 / 3.0,
12648 y: 62.0,
12649 };
12650 let missing = Point { x: 112.0, y: 112.0 };
12651 let last = Point {
12652 x: 36.0 + 380.0 / 3.0,
12653 y: 12.0,
12654 };
12655 let gap = render_geometry(&line.chart, chart_bounds()).unwrap();
12656 let gap_commands = path_commands(&gap);
12657 assert_eq!(gap_commands.len(), 2);
12658 assert_path_points(
12659 gap_commands[0],
12660 &[Point {
12661 x: first.x,
12662 y: 112.0,
12663 }],
12664 false,
12665 );
12666 assert_path_points(gap_commands[1], &[last], false);
12667 line.chart.disp_blanks_as = DispBlanksAs::Zero;
12668 assert_path_points(
12669 path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
12670 &[first, missing, last],
12671 false,
12672 );
12673 line.chart.disp_blanks_as = DispBlanksAs::Span;
12674 assert_path_points(
12675 path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
12676 &[
12677 Point {
12678 x: first.x,
12679 y: 112.0,
12680 },
12681 last,
12682 ],
12683 false,
12684 );
12685
12686 let area = format!(
12687 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:areaChart>"#
12688 );
12689 let mut area = CT_ChartSpace::from_xml(chart_with_plot(&area).as_bytes()).unwrap();
12690 let area_gap = render_geometry(&area.chart, chart_bounds()).unwrap();
12691 let area_gap_commands = path_commands(&area_gap);
12692 assert_eq!(area_gap_commands.len(), 2);
12693 assert_path_points(
12694 area_gap_commands[0],
12695 &[
12696 first,
12697 Point {
12698 x: first.x,
12699 y: 112.0,
12700 },
12701 ],
12702 true,
12703 );
12704 assert_path_points(
12705 area_gap_commands[1],
12706 &[
12707 last,
12708 Point {
12709 x: last.x,
12710 y: 112.0,
12711 },
12712 ],
12713 true,
12714 );
12715 area.chart.disp_blanks_as = DispBlanksAs::Zero;
12716 assert_path_points(
12717 path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
12718 &[
12719 first,
12720 missing,
12721 last,
12722 Point {
12723 x: last.x,
12724 y: 112.0,
12725 },
12726 missing,
12727 Point {
12728 x: first.x,
12729 y: 112.0,
12730 },
12731 ],
12732 true,
12733 );
12734 area.chart.disp_blanks_as = DispBlanksAs::Span;
12735 assert_path_points(
12736 path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
12737 &[
12738 first,
12739 last,
12740 Point {
12741 x: last.x,
12742 y: 112.0,
12743 },
12744 Point {
12745 x: first.x,
12746 y: 112.0,
12747 },
12748 ],
12749 true,
12750 );
12751
12752 let radar = format!(
12753 r#"<q:radarChart><q:radarStyle val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#
12754 );
12755 let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
12756 assert_path_points(
12757 path_commands(&render_geometry(&radar.chart, chart_bounds()).unwrap())[0],
12758 &[
12759 Point { x: 112.0, y: 37.0 },
12760 Point {
12761 x: 112.0 - 25.0 * 3.0_f64.sqrt(),
12762 y: 87.0,
12763 },
12764 ],
12765 true,
12766 );
12767
12768 let scatter = format!(
12769 r#"<q:scatterChart><q:scatterStyle val="lineMarker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:scatterChart>"#,
12770 sparse_scatter_series()
12771 );
12772 let mut scatter = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
12773 let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12774 assert_eq!(geometry_children(&scatter_geometry).len(), 4);
12775 let gap_commands = path_commands(&scatter_geometry);
12776 assert_path_points(gap_commands[0], &[Point { x: 36.0, y: 112.0 }], false);
12777 assert_path_points(gap_commands[1], &[Point { x: 188.0, y: 12.0 }], false);
12778
12779 scatter.chart.disp_blanks_as = DispBlanksAs::Zero;
12780 let zero = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12781 assert_path_points(
12782 path_commands(&zero)[0],
12783 &[
12784 Point { x: 74.0, y: 87.0 },
12785 Point { x: 36.0, y: 62.0 },
12786 Point { x: 150.0, y: 112.0 },
12787 Point { x: 188.0, y: 12.0 },
12788 ],
12789 false,
12790 );
12791
12792 scatter.chart.disp_blanks_as = DispBlanksAs::Span;
12793 assert_path_points(
12794 path_commands(&render_geometry(&scatter.chart, chart_bounds()).unwrap())[0],
12795 &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
12796 false,
12797 );
12798 }
12799
12800 #[test]
12801 fn sparse_scatter_domains_and_labels_use_only_matched_points() {
12802 let scatter = format!(
12803 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>"#,
12804 sparse_scatter_series()
12805 );
12806 let chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
12807 let plot = &chart.chart.plot_area.plots().unwrap()[0];
12808 assert_eq!(
12809 super::scatter_domains(plot, DispBlanksAs::Gap).unwrap(),
12810 (
12811 Domain {
12812 min: 10.0,
12813 max: 40.0,
12814 },
12815 Domain {
12816 min: 100.0,
12817 max: 400.0,
12818 },
12819 )
12820 );
12821 let axes = chart.chart.plot_area.axes().unwrap();
12822 let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
12823 let anchors =
12824 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12825 assert_eq!(
12826 anchors.keys().copied().collect::<Vec<_>>(),
12827 vec![(0, 0), (0, 3)]
12828 );
12829 assert_eq!(anchors[&(0, 0)].center, Point { x: 0.0, y: 140.0 });
12830 assert_eq!(anchors[&(0, 3)].center, Point { x: 200.0, y: 0.0 });
12831 }
12832
12833 #[test]
12834 fn sparse_bubble_size_labels_resolve_logical_indexes() {
12835 let scatter = format!(
12836 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>"#,
12837 sparse_scatter_series()
12838 );
12839 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
12840 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12841 let Plot::Scatter { series, .. } = plot else {
12842 panic!("expected scatter plot");
12843 };
12844 series[0].bubble_size = Some(
12845 NumericData::from_xml(
12846 format!(
12847 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>"#
12848 )
12849 .as_bytes(),
12850 )
12851 .unwrap(),
12852 );
12853 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12854 let mut rendered = Vec::new();
12855 super::render_data_labels(
12856 plot,
12857 chart_bounds(),
12858 DispBlanksAs::Gap,
12859 super::GeometryOptions::default(),
12860 &mut fonts,
12861 &mut rendered,
12862 )
12863 .unwrap();
12864 let text: Vec<_> = rendered
12865 .iter()
12866 .filter_map(|element| match element {
12867 PositionedElement::Text(run) => Some(run.text.as_str()),
12868 _ => None,
12869 })
12870 .collect();
12871 assert_eq!(text, vec!["5", "9"]);
12872 }
12873
12874 #[test]
12875 fn geometry_rejects_invalid_bounds_and_opaque_plots() {
12876 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12877 for bounds in [
12878 Rect {
12879 x: 0.0,
12880 y: 0.0,
12881 width: f64::NAN,
12882 height: 100.0,
12883 },
12884 Rect {
12885 x: 0.0,
12886 y: 0.0,
12887 width: 0.0,
12888 height: 100.0,
12889 },
12890 Rect {
12891 x: 0.0,
12892 y: 0.0,
12893 width: 40.0,
12894 height: 30.0,
12895 },
12896 ] {
12897 assert!(render_geometry(&chart.chart, bounds).is_err());
12898 }
12899
12900 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12901 unreachable!()
12902 };
12903 series[0].values.values.clear();
12904 assert!(render_geometry(&chart.chart, chart_bounds()).is_err());
12905
12906 let opaque = CT_ChartSpace::from_xml(
12907 chart_with_plot(r#"<q:bar3DChart><q:barDir val="col"/></q:bar3DChart>"#).as_bytes(),
12908 )
12909 .unwrap();
12910 let error = render_geometry(&opaque.chart, chart_bounds()).unwrap_err();
12911 assert!(error.to_string().contains("unsupported or combination"));
12912
12913 let (scatter_plot, _) = remaining_plot_fixtures()
12914 .into_iter()
12915 .find(|(kind, _, _)| *kind == "scatterChart")
12916 .map(|(_, plot, axes)| (plot, axes))
12917 .unwrap();
12918 let mut scatter =
12919 CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
12920 .unwrap();
12921 for invalid in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
12922 let Plot::Scatter { series, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
12923 else {
12924 unreachable!()
12925 };
12926 let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
12927 unreachable!()
12928 };
12929 x_values.values[0] = invalid;
12930 let error = render_geometry(&scatter.chart, chart_bounds()).unwrap_err();
12931 assert!(error.to_string().contains("xVal"));
12932 }
12933 }
12934
12935 #[test]
12936 fn finite_extremes_never_produce_nonfinite_geometry() {
12937 let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
12938 let Plot::Line { series, .. } = &mut line.chart.plot_area.plots_mut().unwrap()[0] else {
12939 unreachable!()
12940 };
12941 series[0].values.values = vec![-f64::MAX, f64::MAX];
12942 let extreme_line = render_geometry(&line.chart, chart_bounds()).unwrap();
12943 assert_path_points(
12944 path_commands(&extreme_line)[0],
12945 &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
12946 false,
12947 );
12948
12949 let mut stacked =
12950 CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12951 let Plot::Bar {
12952 grouping, series, ..
12953 } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
12954 else {
12955 unreachable!()
12956 };
12957 *grouping = BarGrouping::Stacked;
12958 series[0].values.values = vec![f64::MAX, 1.0];
12959 let mut second = series[0].clone();
12960 second.index = 1;
12961 second.order = 1;
12962 series.push(second);
12963 let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
12964 assert!(error.to_string().contains("stacked value total"));
12965
12966 let Plot::Bar { grouping, .. } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
12967 else {
12968 unreachable!()
12969 };
12970 *grouping = BarGrouping::PercentStacked;
12971 let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
12972 assert!(error.to_string().contains("stacked percentage total"));
12973
12974 let (pie_plot, _) = remaining_plot_fixtures()
12975 .into_iter()
12976 .find(|(kind, _, _)| *kind == "pieChart")
12977 .map(|(_, plot, axes)| (plot, axes))
12978 .unwrap();
12979 let mut pie =
12980 CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot, false).as_bytes()).unwrap();
12981 let Plot::Pie { series, .. } = &mut pie.chart.plot_area.plots_mut().unwrap()[0] else {
12982 unreachable!()
12983 };
12984 series[0].values.values = vec![f64::MAX, f64::MAX];
12985 let error = render_geometry(&pie.chart, chart_bounds()).unwrap_err();
12986 assert!(error.to_string().contains("pie value total"));
12987
12988 let (scatter_plot, _) = remaining_plot_fixtures()
12989 .into_iter()
12990 .find(|(kind, _, _)| *kind == "scatterChart")
12991 .map(|(_, plot, axes)| (plot, axes))
12992 .unwrap();
12993 let mut scatter =
12994 CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
12995 .unwrap();
12996 let Plot::Scatter { style, series, .. } =
12997 &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
12998 else {
12999 unreachable!()
13000 };
13001 *style = ScatterStyle::Line;
13002 let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
13003 unreachable!()
13004 };
13005 x_values.values = vec![-f64::MAX, f64::MAX];
13006 series[0].values.values = vec![-f64::MAX, f64::MAX];
13007 let extreme_scatter = render_geometry(&scatter.chart, chart_bounds()).unwrap();
13008 assert_path_points(
13009 path_commands(&extreme_scatter)[0],
13010 &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
13011 false,
13012 );
13013 }
13014
13015 #[test]
13016 fn geometry_is_backend_neutral_and_deterministic() {
13017 let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13018 let first = render_geometry(&chart.chart, chart_bounds()).unwrap();
13019 let second = render_geometry(&chart.chart, chart_bounds()).unwrap();
13020 assert_eq!(
13021 format!("{:?}", first.elements),
13022 format!("{:?}", second.elements)
13023 );
13024
13025 let raster = |geometry: ChartGeometry| {
13026 let layout = LayoutResult::new(
13027 vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
13028 Vec::new(),
13029 None,
13030 Vec::new(),
13031 );
13032 oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
13033 };
13034 assert_eq!(raster(first), raster(second));
13035 }
13036
13037 #[test]
13038 fn zero_to_one_hundred_axis_uses_expected_ticks() {
13039 let scale = nice_number_scale(
13040 Domain {
13041 min: 0.0,
13042 max: 100.0,
13043 },
13044 None,
13045 None,
13046 6,
13047 )
13048 .unwrap();
13049 assert_eq!(scale.ticks, vec![0.0, 20.0, 40.0, 60.0, 80.0, 100.0]);
13050 }
13051
13052 #[test]
13053 fn nice_number_ticks_cover_unpinned_extents() {
13054 for domain in [
13055 Domain {
13056 min: 3.0,
13057 max: 97.0,
13058 },
13059 Domain {
13060 min: -97.0,
13061 max: -3.0,
13062 },
13063 Domain {
13064 min: -3.5,
13065 max: 8.25,
13066 },
13067 Domain {
13068 min: 0.125,
13069 max: 0.875,
13070 },
13071 Domain {
13072 min: 1.0e12,
13073 max: 7.0e12,
13074 },
13075 Domain {
13076 min: -f64::MAX,
13077 max: f64::MAX,
13078 },
13079 Domain {
13080 min: f64::from_bits(1),
13081 max: f64::from_bits(5),
13082 },
13083 ] {
13084 let scale = nice_number_scale(domain, None, None, 6).unwrap();
13085 assert!(scale.domain.min <= domain.min);
13086 assert!(scale.domain.max >= domain.max);
13087 assert!(scale.ticks.iter().all(|tick| tick.is_finite()));
13088 assert!(scale.ticks.windows(2).all(|pair| pair[0] < pair[1]));
13089 }
13090
13091 let constant = nice_number_scale(Domain { min: 4.0, max: 4.0 }, None, None, 6).unwrap();
13092 assert!(constant.domain.min < 4.0 && constant.domain.max > 4.0);
13093 }
13094
13095 #[test]
13096 fn one_sided_explicit_scale_beyond_data_derives_a_valid_opposite_bound() {
13097 let minimum = nice_number_scale(
13098 Domain {
13099 min: 0.0,
13100 max: 10.0,
13101 },
13102 Some(20.0),
13103 None,
13104 6,
13105 )
13106 .unwrap();
13107 assert_eq!(minimum.domain.min, 20.0);
13108 assert!(minimum.domain.max > 20.0);
13109 assert!(minimum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
13110
13111 let maximum = nice_number_scale(
13112 Domain {
13113 min: 0.0,
13114 max: 10.0,
13115 },
13116 None,
13117 Some(-20.0),
13118 6,
13119 )
13120 .unwrap();
13121 assert_eq!(maximum.domain.max, -20.0);
13122 assert!(maximum.domain.min < -20.0);
13123 assert!(maximum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
13124 }
13125
13126 #[test]
13127 fn standard_line_domain_does_not_force_zero() {
13128 let mut chart =
13129 CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
13130 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13131 let Plot::Line { series, .. } = plot else {
13132 panic!("expected line plot");
13133 };
13134 series[0].values.values = vec![1_000.0, 1_010.0];
13135 assert_eq!(
13136 super::plot_value_domain(plot, DispBlanksAs::Gap).unwrap(),
13137 Some(Domain {
13138 min: 1_000.0,
13139 max: 1_010.0,
13140 })
13141 );
13142 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
13143 assert_path_points(
13144 path_commands(&geometry)[0],
13145 &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
13146 false,
13147 );
13148 }
13149
13150 #[test]
13151 fn explicit_scaling_bounds_and_orientation_are_honored() {
13152 let scale = nice_number_scale(
13153 Domain {
13154 min: 25.0,
13155 max: 75.0,
13156 },
13157 Some(0.0),
13158 Some(100.0),
13159 6,
13160 )
13161 .unwrap();
13162 assert_eq!(
13163 scale.domain,
13164 Domain {
13165 min: 0.0,
13166 max: 100.0
13167 }
13168 );
13169 assert_eq!(
13170 super::map_y_oriented(0.0, scale.domain, chart_bounds(), Orientation::MaxMin).unwrap(),
13171 0.0
13172 );
13173 assert_eq!(
13174 super::map_y_oriented(100.0, scale.domain, chart_bounds(), Orientation::MaxMin)
13175 .unwrap(),
13176 140.0
13177 );
13178
13179 let mut chart = labelled_bar_chart();
13180 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
13181 panic!("expected bar plot");
13182 };
13183 series[0].values.values = vec![25.0, 75.0];
13184 let value_axis = chart
13185 .chart
13186 .plot_area
13187 .axes
13188 .iter_mut()
13189 .find(|axis| axis.kind == AxisKind::Value)
13190 .unwrap();
13191 value_axis.scaling.orientation = Orientation::MaxMin;
13192 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13193 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13194 let plot = group
13195 .children
13196 .iter()
13197 .find_map(|element| match element {
13198 PositionedElement::Group(group) => Some(group),
13199 _ => None,
13200 })
13201 .unwrap();
13202 let bars: Vec<_> = plot
13203 .children
13204 .iter()
13205 .filter_map(|element| match element {
13206 PositionedElement::Path(path) => path.path.bounds(),
13207 _ => None,
13208 })
13209 .collect();
13210 assert_eq!(bars.len(), 2);
13211 assert!((bars[0].y - 12.0).abs() < 1.0e-9);
13212 assert!((bars[0].height - 25.0).abs() < 1.0e-9);
13213 assert!((bars[1].height - 75.0).abs() < 1.0e-9);
13214 }
13215
13216 #[test]
13217 fn family_label_anchors_share_rendered_geometry() {
13218 let mut bar = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13219 let bar_plot = &mut bar.chart.plot_area.plots_mut().unwrap()[0];
13220 let Plot::Bar { series, .. } = bar_plot else {
13221 panic!("expected bar plot");
13222 };
13223 let mut second = series[0].clone();
13224 second.index = 1;
13225 second.order = 1;
13226 series.push(second);
13227 let anchors = super::plot_label_anchors(
13228 bar_plot,
13229 chart_bounds(),
13230 DispBlanksAs::Gap,
13231 super::GeometryOptions::default(),
13232 )
13233 .unwrap();
13234 assert_point_near(anchors[&(0, 0)].center, Point { x: 40.0, y: 105.0 });
13235 assert_point_near(anchors[&(1, 0)].center, Point { x: 60.0, y: 105.0 });
13236
13237 let pie = CT_ChartSpace::from_xml(
13238 chart_with_optional_axes(
13239 &remaining_plot_fixtures()
13240 .into_iter()
13241 .find(|(kind, _, _)| *kind == "pieChart")
13242 .unwrap()
13243 .1,
13244 false,
13245 )
13246 .as_bytes(),
13247 )
13248 .unwrap();
13249 let Plot::Pie {
13250 first_slice_angle,
13251 series,
13252 ..
13253 } = &pie.chart.plot_area.plots().unwrap()[0]
13254 else {
13255 panic!("expected pie plot");
13256 };
13257 let slices = super::pie_slices(*first_slice_angle, None, series, chart_bounds()).unwrap();
13258 assert_eq!(slices.len(), 2);
13259 let first_span = std::f64::consts::TAU / 3.0;
13260 let first_midpoint = f64::from(*first_slice_angle).to_radians()
13261 - std::f64::consts::FRAC_PI_2
13262 + first_span / 2.0;
13263 assert_point_near(
13264 slices[0].anchor,
13265 super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, first_midpoint),
13266 );
13267 let second_midpoint = first_midpoint + std::f64::consts::PI;
13268 assert_point_near(
13269 slices[1].anchor,
13270 super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, second_midpoint),
13271 );
13272
13273 let radar = format!(
13274 r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
13275 plot_series(0)
13276 );
13277 let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
13278 let Plot::Radar { series, .. } = &radar.chart.plot_area.plots().unwrap()[0] else {
13279 panic!("expected radar plot");
13280 };
13281 let radar_points =
13282 super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
13283 .unwrap();
13284 assert_eq!(radar_points[0][0], (0, Point { x: 100.0, y: 35.0 }));
13285 assert_eq!(radar_points[0][1], (1, Point { x: 100.0, y: 140.0 }));
13286 }
13287
13288 #[test]
13289 fn radar_annotations_use_spokes_perimeter_labels_and_radial_gridlines() {
13290 let radar_plot = format!(
13291 r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
13292 sparse_category_series()
13293 );
13294 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
13295 for axis in &mut chart.chart.plot_area.axes {
13296 axis.major_tick_mark = TickMark::Cross;
13297 if axis.kind == AxisKind::Value {
13298 axis.major_gridlines = Some(super::ChartLines::default());
13299 }
13300 }
13301 let plot = &chart.chart.plot_area.plots().unwrap()[0];
13302 let axes = chart.chart.plot_area.axes().unwrap();
13303 let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
13304 let center = Point { x: 112.0, y: 62.0 };
13305 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13306 let annotations =
13307 super::render_axes_and_labels(plot, &axes, bounds, DispBlanksAs::Gap, &mut fonts)
13308 .unwrap();
13309 assert_eq!(annotations.axis_lines.len(), 4);
13310 for spoke in annotations.axis_lines.iter().take(3) {
13311 let PositionedElement::Path(spoke) = spoke else {
13312 panic!("radar spoke must be a path");
13313 };
13314 assert_eq!(
13315 spoke.path.commands.first(),
13316 Some(&PathCommand::MoveTo(center))
13317 );
13318 }
13319 assert!(!annotations.gridlines.is_empty());
13320 for gridline in &annotations.gridlines {
13321 let PositionedElement::Path(gridline) = gridline else {
13322 panic!("radar gridline must be a path");
13323 };
13324 assert_eq!(gridline.path.commands.last(), Some(&PathCommand::Close));
13325 let radii: Vec<_> = gridline
13326 .path
13327 .commands
13328 .iter()
13329 .filter_map(|command| match command {
13330 PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
13331 Some((point.x - center.x).hypot(point.y - center.y))
13332 }
13333 _ => None,
13334 })
13335 .collect();
13336 assert_eq!(radii.len(), 3);
13337 assert!(
13338 radii
13339 .windows(2)
13340 .all(|pair| (pair[0] - pair[1]).abs() < 1.0e-9)
13341 );
13342 }
13343 let text: Vec<_> = annotations
13344 .labels
13345 .iter()
13346 .filter_map(|element| match element {
13347 PositionedElement::Text(run) => Some(run.text.as_str()),
13348 _ => None,
13349 })
13350 .collect();
13351 assert!(text.contains(&"North"));
13352 assert!(text.contains(&"West"));
13353 }
13354
13355 #[test]
13356 fn radar_tick_label_positions_are_distinct() {
13357 let radar_plot = format!(
13358 r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
13359 plot_series(0)
13360 );
13361 let chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
13362 let plot = &chart.chart.plot_area.plots().unwrap()[0];
13363 let axes = chart.chart.plot_area.axes().unwrap();
13364 let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
13365 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13366
13367 let category_axis = axes
13368 .iter()
13369 .find(|axis| axis.kind == AxisKind::Category)
13370 .unwrap();
13371 for (position, expected) in [
13372 (TickLabelPosition::High, Point { x: 108.0, y: 0.0 }),
13373 (TickLabelPosition::Low, Point { x: 108.0, y: 25.0 }),
13374 (TickLabelPosition::NextTo, Point { x: 108.0, y: 5.0 }),
13375 ] {
13376 let mut axis = category_axis.clone();
13377 axis.tick_label_position = position;
13378 let annotations =
13379 super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
13380 .unwrap();
13381 let north = annotations
13382 .labels
13383 .iter()
13384 .find_map(|element| match element {
13385 PositionedElement::Text(run) if run.text == "North" => Some(run),
13386 _ => None,
13387 })
13388 .unwrap();
13389 assert_point_near(north.origin, expected);
13390 }
13391 let mut hidden_category = category_axis.clone();
13392 hidden_category.tick_label_position = TickLabelPosition::None;
13393 let annotations = super::render_axes_and_labels(
13394 plot,
13395 &[hidden_category],
13396 bounds,
13397 DispBlanksAs::Gap,
13398 &mut fonts,
13399 )
13400 .unwrap();
13401 assert!(annotations.labels.is_empty());
13402
13403 let narrow_bounds = Rect {
13404 x: 36.0,
13405 y: 12.0,
13406 width: 24.0,
13407 height: 100.0,
13408 };
13409 let narrow_center = Point { x: 48.0, y: 62.0 };
13410 assert_eq!(
13411 super::radar_category_label_origin(
13412 narrow_bounds,
13413 narrow_center,
13414 12.0,
13415 1,
13416 4,
13417 TickLabelPosition::High,
13418 ),
13419 Some(Point { x: 72.0, y: 65.0 })
13420 );
13421 assert_eq!(
13422 super::radar_category_label_origin(
13423 narrow_bounds,
13424 narrow_center,
13425 12.0,
13426 1,
13427 4,
13428 TickLabelPosition::NextTo,
13429 ),
13430 Some(Point { x: 66.0, y: 65.0 })
13431 );
13432
13433 let value_axis = axes
13434 .iter()
13435 .find(|axis| axis.kind == AxisKind::Value)
13436 .unwrap();
13437 for (position, expected) in [
13438 (TickLabelPosition::High, Point { x: 166.0, y: 65.0 }),
13439 (TickLabelPosition::Low, Point { x: 32.0, y: 65.0 }),
13440 (TickLabelPosition::NextTo, Point { x: 116.0, y: 65.0 }),
13441 ] {
13442 let mut axis = value_axis.clone();
13443 axis.tick_label_position = position;
13444 let annotations =
13445 super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
13446 .unwrap();
13447 let zero = annotations
13448 .labels
13449 .iter()
13450 .find_map(|element| match element {
13451 PositionedElement::Text(run) if run.text == "0" => Some(run),
13452 _ => None,
13453 })
13454 .unwrap();
13455 assert_point_near(zero.origin, expected);
13456 }
13457 let mut hidden_value = value_axis.clone();
13458 hidden_value.tick_label_position = TickLabelPosition::None;
13459 let annotations = super::render_axes_and_labels(
13460 plot,
13461 &[hidden_value],
13462 bounds,
13463 DispBlanksAs::Gap,
13464 &mut fonts,
13465 )
13466 .unwrap();
13467 assert!(annotations.labels.is_empty());
13468 }
13469
13470 #[test]
13471 fn radar_negative_and_mixed_sign_domains_preserve_values() {
13472 let radar_plot = format!(
13473 r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
13474 plot_series(0)
13475 );
13476 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
13477 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13478 let Plot::Radar { series, .. } = plot else {
13479 panic!("expected radar plot");
13480 };
13481 series[0].values.values = vec![-10.0, -5.0];
13482 let points =
13483 super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
13484 .unwrap();
13485 assert_eq!(points[0][0], (0, Point { x: 100.0, y: 70.0 }));
13486 assert_eq!(points[0][1], (1, Point { x: 100.0, y: 105.0 }));
13487
13488 series[0].values.values = vec![-5.0, 10.0];
13489 let options = super::GeometryOptions {
13490 value: Some(super::AxisMapping {
13491 domain: Domain {
13492 min: -10.0,
13493 max: 10.0,
13494 },
13495 orientation: Orientation::MinMax,
13496 }),
13497 ..super::GeometryOptions::default()
13498 };
13499 let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
13500 assert_eq!(points[0][0], (0, Point { x: 100.0, y: 52.5 }));
13501 assert_eq!(points[0][1], (1, Point { x: 100.0, y: 140.0 }));
13502 }
13503
13504 #[test]
13505 fn radar_explicit_bounds_suppress_out_of_range_points_and_labels() {
13506 let radar_plot = format!(
13507 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>"#,
13508 plot_series(0)
13509 );
13510 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
13511 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13512 let Plot::Radar { series, .. } = plot else {
13513 panic!("expected radar plot");
13514 };
13515 series[0].values.values = vec![25.0, 75.0];
13516 let options = super::GeometryOptions {
13517 value: Some(super::AxisMapping {
13518 domain: Domain {
13519 min: 50.0,
13520 max: 100.0,
13521 },
13522 orientation: Orientation::MinMax,
13523 }),
13524 ..super::GeometryOptions::default()
13525 };
13526 let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
13527 assert_eq!(points[0], vec![(1, Point { x: 100.0, y: 105.0 })]);
13528 let anchors =
13529 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
13530 assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 1)]);
13531
13532 let Plot::Radar { series, .. } = plot else {
13533 panic!("expected radar plot");
13534 };
13535 series[0].values.values = vec![75.0, 125.0];
13536 let reversed = super::GeometryOptions {
13537 value: Some(super::AxisMapping {
13538 domain: Domain {
13539 min: 50.0,
13540 max: 100.0,
13541 },
13542 orientation: Orientation::MaxMin,
13543 }),
13544 ..super::GeometryOptions::default()
13545 };
13546 let points = super::radar_series_points(series, chart_bounds(), reversed).unwrap();
13547 assert_eq!(points[0], vec![(0, Point { x: 100.0, y: 35.0 })]);
13548 let anchors =
13549 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
13550 assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
13551
13552 let value_axis = chart
13553 .chart
13554 .plot_area
13555 .axes
13556 .iter_mut()
13557 .find(|axis| axis.kind == AxisKind::Value)
13558 .unwrap();
13559 value_axis.scaling.minimum = Some(50.0);
13560 value_axis.scaling.maximum = Some(100.0);
13561 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13562 let Plot::Radar { series, .. } = plot else {
13563 panic!("expected radar plot");
13564 };
13565 series[0].values.values = vec![25.0, 25.0];
13566 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13567 let rendered = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13568 assert!(rendered.children.iter().any(|element| matches!(
13569 element,
13570 PositionedElement::Group(group) if group.clip.is_some() && group.children.is_empty()
13571 )));
13572 assert!(
13573 rendered
13574 .children
13575 .iter()
13576 .any(|element| matches!(element, PositionedElement::Path(_)))
13577 );
13578 assert!(!rendered.children.iter().any(|element| matches!(
13579 element,
13580 PositionedElement::Text(run) if run.text == "25"
13581 )));
13582 }
13583
13584 #[test]
13585 fn inside_and_outside_label_positions_follow_family_geometry() {
13586 let mut horizontal =
13587 CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13588 let plot = &mut horizontal.chart.plot_area.plots_mut().unwrap()[0];
13589 let Plot::Bar { direction, .. } = plot else {
13590 panic!("expected bar plot");
13591 };
13592 *direction = BarDirection::Bar;
13593 let anchors = super::plot_label_anchors(
13594 plot,
13595 chart_bounds(),
13596 DispBlanksAs::Gap,
13597 super::GeometryOptions::default(),
13598 )
13599 .unwrap();
13600 let anchor = anchors[&(0, 0)];
13601 assert_point_near(
13602 anchor.origin(Some(DataLabelPosition::InsideBase)),
13603 Point { x: 5.0, y: 35.0 },
13604 );
13605 assert_point_near(
13606 anchor.origin(Some(DataLabelPosition::InsideEnd)),
13607 Point { x: 95.0, y: 35.0 },
13608 );
13609 assert_point_near(
13610 anchor.origin(Some(DataLabelPosition::OutsideEnd)),
13611 Point { x: 105.0, y: 35.0 },
13612 );
13613
13614 let mut negative =
13615 CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13616 let plot = &mut negative.chart.plot_area.plots_mut().unwrap()[0];
13617 let Plot::Bar { series, .. } = plot else {
13618 panic!("expected bar plot");
13619 };
13620 series[0].values.values = vec![-1.0, -2.0];
13621 let anchors = super::plot_label_anchors(
13622 plot,
13623 chart_bounds(),
13624 DispBlanksAs::Gap,
13625 super::GeometryOptions::default(),
13626 )
13627 .unwrap();
13628 assert_point_near(
13629 anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
13630 Point { x: 50.0, y: 75.0 },
13631 );
13632 let reversed = super::GeometryOptions {
13633 value: Some(super::AxisMapping {
13634 domain: Domain {
13635 min: -2.0,
13636 max: 0.0,
13637 },
13638 orientation: Orientation::MaxMin,
13639 }),
13640 ..super::GeometryOptions::default()
13641 };
13642 let anchors =
13643 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
13644 assert_point_near(
13645 anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
13646 Point { x: 50.0, y: 65.0 },
13647 );
13648
13649 let pie = CT_ChartSpace::from_xml(
13650 chart_with_optional_axes(
13651 &remaining_plot_fixtures()
13652 .into_iter()
13653 .find(|(kind, _, _)| *kind == "pieChart")
13654 .unwrap()
13655 .1,
13656 false,
13657 )
13658 .as_bytes(),
13659 )
13660 .unwrap();
13661 let Plot::Pie { series, .. } = &pie.chart.plot_area.plots().unwrap()[0] else {
13662 panic!("expected pie plot");
13663 };
13664 let pie_slice = &super::pie_slices(30, None, series, chart_bounds()).unwrap()[0];
13665 assert_point_near(
13666 super::LabelAnchor::segment(pie_slice.anchor, pie_slice.base, pie_slice.end)
13667 .origin(Some(DataLabelPosition::OutsideEnd)),
13668 Point { x: 175.0, y: 70.0 },
13669 );
13670 let doughnut_slice = &super::pie_slices(30, Some(60), series, chart_bounds()).unwrap()[0];
13671 assert_point_near(
13672 super::LabelAnchor::segment(
13673 doughnut_slice.anchor,
13674 doughnut_slice.base,
13675 doughnut_slice.end,
13676 )
13677 .origin(Some(DataLabelPosition::InsideBase)),
13678 Point { x: 147.0, y: 70.0 },
13679 );
13680
13681 let short = super::LabelAnchor::segment(
13682 Point { x: 2.0, y: 0.0 },
13683 Point { x: 0.0, y: 0.0 },
13684 Point { x: 4.0, y: 0.0 },
13685 );
13686 assert_eq!(
13687 short.origin(Some(DataLabelPosition::InsideBase)),
13688 Point { x: 2.0, y: 0.0 }
13689 );
13690 assert_eq!(
13691 short.origin(Some(DataLabelPosition::InsideEnd)),
13692 Point { x: 2.0, y: 0.0 }
13693 );
13694 let thin_bounds = Rect {
13695 x: 0.0,
13696 y: 0.0,
13697 width: 80.0,
13698 height: 80.0,
13699 };
13700 let thin = &super::pie_slices(30, Some(90), series, thin_bounds).unwrap()[0];
13701 let thin = super::LabelAnchor::segment(thin.anchor, thin.base, thin.end);
13702 assert_point_near(
13703 thin.origin(Some(DataLabelPosition::InsideBase)),
13704 Point { x: 78.0, y: 40.0 },
13705 );
13706 assert_point_near(
13707 thin.origin(Some(DataLabelPosition::InsideEnd)),
13708 Point { x: 78.0, y: 40.0 },
13709 );
13710 }
13711
13712 #[test]
13713 fn degenerate_bar_and_radar_anchors_preserve_family_direction() {
13714 let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13715 let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
13716 let Plot::Bar { series, .. } = plot else {
13717 panic!("expected bar plot");
13718 };
13719 series[0].values.values = vec![80.0, 125.0];
13720 for (direction, orientation, boundary, outside) in [
13721 (
13722 BarDirection::Column,
13723 Orientation::MinMax,
13724 Point { x: 50.0, y: 140.0 },
13725 Point { x: 50.0, y: 135.0 },
13726 ),
13727 (
13728 BarDirection::Column,
13729 Orientation::MaxMin,
13730 Point { x: 50.0, y: 0.0 },
13731 Point { x: 50.0, y: 5.0 },
13732 ),
13733 (
13734 BarDirection::Bar,
13735 Orientation::MinMax,
13736 Point { x: 0.0, y: 35.0 },
13737 Point { x: 5.0, y: 35.0 },
13738 ),
13739 (
13740 BarDirection::Bar,
13741 Orientation::MaxMin,
13742 Point { x: 200.0, y: 35.0 },
13743 Point { x: 195.0, y: 35.0 },
13744 ),
13745 ] {
13746 let Plot::Bar {
13747 direction: plot_direction,
13748 ..
13749 } = plot
13750 else {
13751 panic!("expected bar plot");
13752 };
13753 *plot_direction = direction;
13754 let options = super::GeometryOptions {
13755 value: Some(super::AxisMapping {
13756 domain: Domain {
13757 min: 80.0,
13758 max: 100.0,
13759 },
13760 orientation,
13761 }),
13762 ..super::GeometryOptions::default()
13763 };
13764 let anchors =
13765 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options)
13766 .unwrap();
13767 let anchor = anchors[&(0, 0)];
13768 assert_point_near(anchor.center, boundary);
13769 assert_point_near(anchor.base, boundary);
13770 assert_point_near(anchor.end, boundary);
13771 assert_point_near(anchor.origin(Some(DataLabelPosition::InsideBase)), boundary);
13772 assert_point_near(anchor.origin(Some(DataLabelPosition::InsideEnd)), boundary);
13773 assert_point_near(anchor.origin(Some(DataLabelPosition::OutsideEnd)), outside);
13774 }
13775
13776 let radar_plot = format!(
13777 r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
13778 plot_series(0)
13779 );
13780 let mut radar = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
13781 let plot = &mut radar.chart.plot_area.plots_mut().unwrap()[0];
13782 let Plot::Radar { series, .. } = plot else {
13783 panic!("expected radar plot");
13784 };
13785 series[0].values.values = vec![0.0, 0.0, 0.0, 0.0];
13786 let anchors = super::plot_label_anchors(
13787 plot,
13788 chart_bounds(),
13789 DispBlanksAs::Gap,
13790 super::GeometryOptions::default(),
13791 )
13792 .unwrap();
13793 let center = Point { x: 100.0, y: 70.0 };
13794 for logical_index in 0..4 {
13795 assert_point_near(
13796 anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideBase)),
13797 center,
13798 );
13799 assert_point_near(
13800 anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideEnd)),
13801 center,
13802 );
13803 }
13804 assert_point_near(
13805 anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
13806 Point { x: 100.0, y: 65.0 },
13807 );
13808 assert_point_near(
13809 anchors[&(0, 1)].origin(Some(DataLabelPosition::OutsideEnd)),
13810 Point { x: 105.0, y: 70.0 },
13811 );
13812 assert_point_near(
13813 anchors[&(0, 2)].origin(Some(DataLabelPosition::OutsideEnd)),
13814 Point { x: 100.0, y: 75.0 },
13815 );
13816 assert_point_near(
13817 anchors[&(0, 3)].origin(Some(DataLabelPosition::OutsideEnd)),
13818 Point { x: 95.0, y: 70.0 },
13819 );
13820 }
13821
13822 #[test]
13823 fn labels_outside_explicit_axis_bounds_are_suppressed() {
13824 let mut chart =
13825 CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
13826 {
13827 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13828 let Plot::Line { series, .. } = plot else {
13829 panic!("expected line plot");
13830 };
13831 series[0].values.values = vec![25.0, 75.0];
13832 }
13833 let value_axis = chart
13834 .chart
13835 .plot_area
13836 .axes
13837 .iter_mut()
13838 .find(|axis| axis.kind == AxisKind::Value)
13839 .unwrap();
13840 value_axis.scaling.minimum = Some(50.0);
13841 value_axis.scaling.maximum = Some(100.0);
13842 value_axis.scaling.orientation = Orientation::MaxMin;
13843 let plot = &chart.chart.plot_area.plots().unwrap()[0];
13844 let axes = chart.chart.plot_area.axes().unwrap();
13845 let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
13846 let anchors =
13847 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
13848 assert!(!anchors.contains_key(&(0, 0)));
13849 assert_point_near(anchors[&(0, 1)].center, Point { x: 150.0, y: 70.0 });
13850
13851 let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13852 let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
13853 let Plot::Bar { series, .. } = plot else {
13854 panic!("expected bar plot");
13855 };
13856 series[0].values.values = vec![90.0, 125.0];
13857 let options = super::GeometryOptions {
13858 value: Some(super::AxisMapping {
13859 domain: Domain {
13860 min: 80.0,
13861 max: 100.0,
13862 },
13863 orientation: Orientation::MinMax,
13864 }),
13865 ..super::GeometryOptions::default()
13866 };
13867 let anchors =
13868 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
13869 assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
13870 assert_point_near(anchors[&(0, 0)].base, Point { x: 50.0, y: 140.0 });
13871 assert_point_near(anchors[&(0, 0)].end, Point { x: 50.0, y: 70.0 });
13872 assert_point_near(anchors[&(0, 0)].center, Point { x: 50.0, y: 105.0 });
13873 assert_point_near(
13874 anchors[&(0, 0)].origin(Some(DataLabelPosition::InsideBase)),
13875 Point { x: 50.0, y: 135.0 },
13876 );
13877 }
13878
13879 #[test]
13880 fn zero_bars_render_and_emit_requested_value_labels() {
13881 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13882 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13883 let Plot::Bar { series, .. } = plot else {
13884 panic!("expected bar plot");
13885 };
13886 series[0].values.values = vec![0.0, 0.0];
13887 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
13888 assert_eq!(path_commands(&geometry).len(), 2);
13889 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13890 render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13891 let plot = &chart.chart.plot_area.plots().unwrap()[0];
13892 let mut labels = Vec::new();
13893 super::render_data_labels(
13894 plot,
13895 chart_bounds(),
13896 DispBlanksAs::Gap,
13897 super::GeometryOptions::default(),
13898 &mut fonts,
13899 &mut labels,
13900 )
13901 .unwrap();
13902 let text: Vec<_> = labels
13903 .iter()
13904 .filter_map(|element| match element {
13905 PositionedElement::Text(run) => Some(run.text.as_str()),
13906 _ => None,
13907 })
13908 .collect();
13909 assert_eq!(text, vec!["0", "0"]);
13910 }
13911
13912 #[test]
13913 fn axes_gridlines_and_tick_marks_follow_model_state() {
13914 let chart = labelled_bar_chart();
13915 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13916 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13917 let ranks: Vec<_> = group
13918 .children
13919 .iter()
13920 .map(|element| match element {
13921 PositionedElement::Group(group) => {
13922 assert!(group.clip.is_some());
13923 1
13924 }
13925 PositionedElement::Path(path)
13926 if path
13927 .stroke
13928 .as_ref()
13929 .is_some_and(|stroke| stroke.width == 0.5) =>
13930 {
13931 0
13932 }
13933 PositionedElement::Path(path) if path.fill.is_some() => 4,
13934 PositionedElement::Path(path) => {
13935 let bounds = path.path.bounds().expect("axis or tick bounds");
13936 assert_eq!(path.stroke.as_ref().map(|stroke| stroke.width), Some(1.0));
13937 if bounds.width > 4.0 || bounds.height > 4.0 {
13938 2
13939 } else {
13940 3
13941 }
13942 }
13943 PositionedElement::Text(_) => 5,
13944 _ => panic!("unexpected labelled chart element"),
13945 })
13946 .collect();
13947 assert!(ranks.windows(2).all(|pair| pair[0] <= pair[1]), "{ranks:?}");
13948 for required in 0..=5 {
13949 assert!(
13950 ranks.contains(&required),
13951 "missing z-order rank {required}: {ranks:?}"
13952 );
13953 }
13954 assert_eq!(ranks.iter().filter(|rank| **rank == 1).count(), 1);
13955 assert_eq!(ranks.iter().filter(|rank| **rank == 2).count(), 2);
13956
13957 let mut deleted = chart.clone();
13958 for axis in &mut deleted.chart.plot_area.axes {
13959 axis.deleted = true;
13960 }
13961 let deleted_group = render_chart(&deleted.chart, chart_bounds(), &mut fonts).unwrap();
13962 assert_eq!(
13963 deleted_group
13964 .children
13965 .iter()
13966 .filter(|element| {
13967 matches!(
13968 element,
13969 PositionedElement::Path(path) if path.stroke.is_some()
13970 )
13971 })
13972 .count(),
13973 0
13974 );
13975 }
13976
13977 #[test]
13978 fn category_ticks_cover_unlabelled_and_sparse_logical_slots() {
13979 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13980 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
13981 panic!("expected bar plot");
13982 };
13983 series[0].categories = None;
13984 let plot = &chart.chart.plot_area.plots().unwrap()[0];
13985 let mut category_axis = chart
13986 .chart
13987 .plot_area
13988 .axes()
13989 .unwrap()
13990 .into_iter()
13991 .find(|axis| axis.kind == AxisKind::Category)
13992 .unwrap();
13993 category_axis.major_gridlines = Some(super::ChartLines::default());
13994 category_axis.major_tick_mark = TickMark::Cross;
13995 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13996 let annotations = super::render_axes_and_labels(
13997 plot,
13998 &[category_axis],
13999 chart_bounds(),
14000 DispBlanksAs::Gap,
14001 &mut fonts,
14002 )
14003 .unwrap();
14004 assert_eq!(annotations.gridlines.len(), 2);
14005 assert_eq!(annotations.ticks.len(), 2);
14006 assert!(annotations.labels.is_empty());
14007
14008 let sparse_plot = format!(
14009 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>"#,
14010 sparse_category_series()
14011 );
14012 let sparse = CT_ChartSpace::from_xml(chart_with_plot(&sparse_plot).as_bytes()).unwrap();
14013 let plot = &sparse.chart.plot_area.plots().unwrap()[0];
14014 let mut category_axis = sparse
14015 .chart
14016 .plot_area
14017 .axes()
14018 .unwrap()
14019 .into_iter()
14020 .find(|axis| axis.kind == AxisKind::Category)
14021 .unwrap();
14022 category_axis.major_gridlines = Some(super::ChartLines::default());
14023 category_axis.major_tick_mark = TickMark::Cross;
14024 let annotations = super::render_axes_and_labels(
14025 plot,
14026 &[category_axis],
14027 chart_bounds(),
14028 DispBlanksAs::Gap,
14029 &mut fonts,
14030 )
14031 .unwrap();
14032 assert_eq!(annotations.gridlines.len(), 3);
14033 assert_eq!(annotations.ticks.len(), 3);
14034 assert_eq!(annotations.labels.len(), 2);
14035
14036 let oversized_plot = sparse_plot.replacen(
14037 r#"<q:ptCount val="3"/>"#,
14038 r#"<q:ptCount val="4294967295"/>"#,
14039 1,
14040 );
14041 let oversized =
14042 CT_ChartSpace::from_xml(chart_with_plot(&oversized_plot).as_bytes()).unwrap();
14043 let plot = &oversized.chart.plot_area.plots().unwrap()[0];
14044 let category_axis = oversized
14045 .chart
14046 .plot_area
14047 .axes()
14048 .unwrap()
14049 .into_iter()
14050 .find(|axis| axis.kind == AxisKind::Category)
14051 .unwrap();
14052 let result = super::render_axes_and_labels(
14053 plot,
14054 &[category_axis],
14055 chart_bounds(),
14056 DispBlanksAs::Gap,
14057 &mut fonts,
14058 );
14059 let error = match result {
14060 Err(error) => error,
14061 Ok(_) => panic!("oversized category annotations rendered"),
14062 };
14063 assert!(
14064 error
14065 .to_string()
14066 .contains("logical category count 4294967295")
14067 );
14068 assert!(error.to_string().contains("exceeds 16384"));
14069 }
14070
14071 #[test]
14072 fn unsupported_numeric_category_formats_return_projection_errors() {
14073 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14074 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
14075 panic!("expected bar plot");
14076 };
14077 series[0].categories = Some(AxisData::Numeric(
14078 NumericData::new(
14079 "Sheet1!$A$2:$A$3".to_owned(),
14080 "General".to_owned(),
14081 vec![1.0, 2.0],
14082 )
14083 .unwrap(),
14084 ));
14085 let plot = &chart.chart.plot_area.plots().unwrap()[0];
14086 let mut category_axis = chart
14087 .chart
14088 .plot_area
14089 .axes()
14090 .unwrap()
14091 .into_iter()
14092 .find(|axis| axis.kind == AxisKind::Category)
14093 .unwrap();
14094 category_axis.number_format = Some(NumberFormat::new("0.00".to_owned(), false).unwrap());
14095 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14096 let annotations = super::render_axes_and_labels(
14097 plot,
14098 &[category_axis.clone()],
14099 chart_bounds(),
14100 DispBlanksAs::Gap,
14101 &mut fonts,
14102 )
14103 .unwrap();
14104 let text: Vec<_> = annotations
14105 .labels
14106 .iter()
14107 .filter_map(|element| match element {
14108 PositionedElement::Text(run) => Some(run.text.as_str()),
14109 _ => None,
14110 })
14111 .collect();
14112 assert_eq!(text, vec!["1.00", "2.00"]);
14113
14114 category_axis.number_format = Some(NumberFormat::new("#,##0".to_owned(), false).unwrap());
14115 let result = super::render_axes_and_labels(
14116 plot,
14117 &[category_axis],
14118 chart_bounds(),
14119 DispBlanksAs::Gap,
14120 &mut fonts,
14121 );
14122 let error = match result {
14123 Err(error) => error,
14124 Ok(_) => panic!("unsupported category-axis format rendered"),
14125 };
14126 assert!(
14127 error
14128 .to_string()
14129 .contains("c:numFmt/@formatCode projection")
14130 );
14131 assert!(error.to_string().contains("#,##0"));
14132 }
14133
14134 #[test]
14135 fn labels_and_legend_shape_with_deterministic_fonts() {
14136 let chart = labelled_bar_chart();
14137 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14138 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
14139 let labels: Vec<_> = group
14140 .children
14141 .iter()
14142 .filter_map(|element| match element {
14143 PositionedElement::Text(run) => Some(run),
14144 _ => None,
14145 })
14146 .collect();
14147 for text in ["North", "South", "20", "Sales", "100"] {
14148 assert!(labels.iter().any(|run| run.text == text), "missing {text}");
14149 }
14150 let styled_tick = labels.iter().find(|run| run.text == "20").unwrap();
14151 assert_eq!(styled_tick.font_size, 12.0);
14152 assert!(styled_tick.bold);
14153 assert!(
14154 labels
14155 .iter()
14156 .all(|run| !run.glyph_ids.is_empty() && run.glyph_ids.len() == run.advances.len())
14157 );
14158 }
14159
14160 #[test]
14161 fn labelled_chart_raster_is_deterministic() {
14162 let raster = || {
14163 let chart = labelled_bar_chart();
14164 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14165 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
14166 let layout = LayoutResult::new(
14167 vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
14168 fonts.all_font_data(),
14169 None,
14170 Vec::new(),
14171 );
14172 oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
14173 };
14174 assert_eq!(raster(), raster());
14175 }
14176
14177 fn labelled_bar_chart() -> CT_ChartSpace {
14178 let series = plot_series(0)
14179 .replace(
14180 "<q:cat>",
14181 "<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>",
14182 )
14183 .replace("<q:v>1</q:v>", "<q:v>0</q:v>")
14184 .replace("<q:v>2</q:v>", "<q:v>100</q:v>");
14185 let plot = format!(
14186 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>"#
14187 );
14188 let xml = chart_with_plot(&plot)
14189 .replacen(
14190 "<q:scaling/>",
14191 "<q:scaling><q:orientation val=\"minMax\"/></q:scaling>",
14192 1,
14193 )
14194 .replacen(
14195 "<q:scaling/>",
14196 "<q:scaling><q:max val=\"100\"/><q:min val=\"0\"/></q:scaling>",
14197 1,
14198 )
14199 .replace(
14200 "<q:axPos val=\"b\"/>",
14201 "<q:axPos val=\"b\"/><q:majorTickMark val=\"cross\"/>",
14202 )
14203 .replace(
14204 "<q:axPos val=\"l\"/>",
14205 "<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>",
14206 )
14207 .replace("</q:plotArea></q:chart>", "</q:plotArea><q:legend/></q:chart>");
14208 CT_ChartSpace::from_xml(xml.as_bytes()).unwrap()
14209 }
14210
14211 fn chart_bounds() -> Rect {
14212 Rect {
14213 x: 0.0,
14214 y: 0.0,
14215 width: 200.0,
14216 height: 140.0,
14217 }
14218 }
14219
14220 fn four_series_bar_chart() -> CT_ChartSpace {
14221 let series = (0..4).map(plot_series).collect::<String>();
14222 let plot = bar_plot("").replace(&plot_series(0), &series);
14223 CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
14224 }
14225
14226 fn seven_series_bar_chart() -> CT_ChartSpace {
14227 let series = (0..7).map(plot_series).collect::<String>();
14228 let plot = bar_plot("").replace(&plot_series(0), &series);
14229 CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
14230 }
14231
14232 fn path_colours(geometry: &ChartGeometry) -> Vec<Color> {
14233 let mut colours = geometry_children(geometry)
14234 .iter()
14235 .filter_map(|element| match element {
14236 PositionedElement::Path(path) => match path
14237 .fill
14238 .as_ref()
14239 .or_else(|| path.stroke.as_ref().map(|stroke| &stroke.paint))
14240 {
14241 Some(Paint::Solid(colour)) => Some(*colour),
14242 _ => None,
14243 },
14244 _ => None,
14245 })
14246 .collect::<Vec<_>>();
14247 colours.dedup_by(|left, right| left == right);
14248 colours
14249 }
14250
14251 fn resolved_layout_colour(colour: oxml_drawing::color::ResolvedColor) -> Color {
14252 Color {
14253 r: f64::from(colour.red) / 255.0,
14254 g: f64::from(colour.green) / 255.0,
14255 b: f64::from(colour.blue) / 255.0,
14256 a: f64::from(colour.alpha) / 255.0,
14257 }
14258 }
14259
14260 fn geometry_children(geometry: &ChartGeometry) -> &[PositionedElement] {
14261 let [PositionedElement::Group(group)] = geometry.elements.as_slice() else {
14262 panic!("chart geometry must contain one group");
14263 };
14264 &group.children
14265 }
14266
14267 fn path_commands(geometry: &ChartGeometry) -> Vec<&[PathCommand]> {
14268 geometry_children(geometry)
14269 .iter()
14270 .map(|element| {
14271 let PositionedElement::Path(path) = element else {
14272 panic!("chart geometry child must be a path");
14273 };
14274 path.path.commands.as_slice()
14275 })
14276 .collect()
14277 }
14278
14279 fn path_bounds(geometry: &ChartGeometry) -> Vec<Rect> {
14280 geometry_children(geometry)
14281 .iter()
14282 .map(|element| {
14283 let PositionedElement::Path(path) = element else {
14284 panic!("chart geometry child must be a path");
14285 };
14286 path.path.bounds().expect("chart path has bounds")
14287 })
14288 .collect()
14289 }
14290
14291 fn assert_rect_near(actual: Rect, expected: Rect) {
14292 assert_near(actual.x, expected.x);
14293 assert_near(actual.y, expected.y);
14294 assert_near(actual.width, expected.width);
14295 assert_near(actual.height, expected.height);
14296 }
14297
14298 fn assert_rects_near(actual: &[Rect], expected: &[Rect]) {
14299 assert_eq!(actual.len(), expected.len());
14300 for (actual, expected) in actual.iter().copied().zip(expected.iter().copied()) {
14301 assert_rect_near(actual, expected);
14302 }
14303 }
14304
14305 fn assert_path_points(commands: &[PathCommand], expected: &[Point], closed: bool) {
14306 let points: Vec<_> = commands
14307 .iter()
14308 .filter_map(|command| match command {
14309 PathCommand::MoveTo(point) | PathCommand::LineTo(point) => Some(*point),
14310 PathCommand::Close => None,
14311 PathCommand::CurveTo { .. } => panic!("expected polygonal path"),
14312 })
14313 .collect();
14314 assert_eq!(points.len(), expected.len());
14315 for (actual, expected) in points.into_iter().zip(expected.iter().copied()) {
14316 assert_point_near(actual, expected);
14317 }
14318 assert_eq!(commands.last() == Some(&PathCommand::Close), closed);
14319 }
14320
14321 fn assert_point_near(actual: Point, expected: Point) {
14322 assert_near(actual.x, expected.x);
14323 assert_near(actual.y, expected.y);
14324 }
14325
14326 fn assert_near(actual: f64, expected: f64) {
14327 assert!(
14328 (actual - expected).abs() < 1e-9,
14329 "expected {expected}, got {actual}"
14330 );
14331 }
14332
14333 #[test]
14334 fn remaining_v1_plots_round_trip_and_render() {
14335 for (kind, plot, axes) in remaining_plot_fixtures() {
14336 let xml = chart_with_optional_axes(&plot, axes);
14337 let parsed = CT_ChartSpace::from_xml(xml.as_bytes())
14338 .unwrap_or_else(|error| panic!("{kind}: parse failed: {error}"));
14339 assert_eq!(parsed.chart.plot_area.plots().unwrap().len(), 1);
14340 assert_eq!(
14341 parsed.chart.plot_area.axes().unwrap().len(),
14342 usize::from(axes) * 2
14343 );
14344 let written = parsed
14345 .to_xml()
14346 .unwrap_or_else(|error| panic!("{kind}: write failed: {error}"));
14347 assert_eq!(
14348 parsed,
14349 CT_ChartSpace::from_xml(&written)
14350 .unwrap_or_else(|error| panic!("{kind}: reparse failed: {error}"))
14351 );
14352 }
14353 if let Some(corpus) = require_or_skip_corpus() {
14354 verify_remaining_plot_viewer_gate(&corpus);
14355 }
14356 }
14357
14358 #[test]
14359 fn remaining_plot_families_write_fixed_prefixes_in_schema_order() {
14360 for (kind, plot, axes) in remaining_plot_fixtures() {
14361 let parsed =
14362 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
14363 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14364 assert!(written.contains(&format!("<c:{kind}")));
14365 assert!(!written.contains(&format!("<q:{kind}")));
14366 let series = written.find("<c:ser").unwrap();
14367 let labels = written.find("<c:dLbls").unwrap();
14368 assert!(series < labels, "{kind}: series must precede labels");
14369 if matches!(kind, "areaChart" | "scatterChart" | "radarChart") {
14370 let property = match kind {
14371 "areaChart" => "<c:grouping",
14372 "scatterChart" => "<c:scatterStyle",
14373 "radarChart" => "<c:radarStyle",
14374 _ => unreachable!(),
14375 };
14376 assert!(written.find(property).unwrap() < series);
14377 assert!(labels < written.find("<c:axId").unwrap());
14378 } else {
14379 assert!(labels < written.find("<c:firstSliceAng").unwrap());
14380 }
14381 if kind == "doughnutChart" {
14382 assert!(
14383 written.find("<c:firstSliceAng").unwrap()
14384 < written.find("<c:holeSize").unwrap()
14385 );
14386 }
14387 }
14388 }
14389
14390 #[test]
14391 fn scatter_series_map_numeric_categories_and_values_to_x_and_y() {
14392 let plot = remaining_plot_fixtures()
14393 .into_iter()
14394 .find(|(kind, _, _)| *kind == "scatterChart")
14395 .unwrap()
14396 .1;
14397 let mut parsed =
14398 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, true).as_bytes()).unwrap();
14399 let standalone_series = {
14400 let Plot::Scatter { series, .. } = &mut parsed.chart.plot_area.plots_mut().unwrap()[0]
14401 else {
14402 panic!("expected scatter plot");
14403 };
14404 let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
14405 panic!("expected numeric x values");
14406 };
14407 x_values.values[0] = 3.5;
14408 series[0].values.values[0] = 7.5;
14409 series[0].clone()
14410 };
14411 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14412 assert!(written.contains("<c:xVal>"));
14413 assert!(written.contains("<c:yVal>"));
14414 assert!(!written.contains("<c:cat>"));
14415 assert!(!written.contains("<c:val>"));
14416 assert!(written.contains("<c:v>3.5</c:v>"));
14417 assert!(written.contains("<c:v>7.5</c:v>"));
14418 let standalone = String::from_utf8(standalone_series.to_xml().unwrap()).unwrap();
14419 assert!(standalone.contains("<c:xVal>"));
14420 assert!(standalone.contains("<c:yVal>"));
14421 assert!(!standalone.contains("<c:cat>"));
14422 assert!(!standalone.contains("<c:val>"));
14423 }
14424
14425 #[test]
14426 fn malformed_remaining_plots_return_errors_without_panicking() {
14427 let series = plot_series(0);
14428 let scatter = numeric_plot_series(0);
14429 let opaque_bubble = series.replace(
14430 "</q:val></q:ser>",
14431 "</q:val><q:bubbleSize><x:opaque/></q:bubbleSize></q:ser>",
14432 );
14433 let cases = [
14434 r#"<q:pieChart/>"#.to_owned(),
14435 format!(r#"<q:pieChart>{series}<q:firstSliceAng val="361"/></q:pieChart>"#),
14436 format!(
14437 r#"<q:pieChart>{series}<q:firstSliceAng val="1"/><q:firstSliceAng val="2"/></q:pieChart>"#
14438 ),
14439 format!(r#"<q:doughnutChart>{series}<q:holeSize val="9"/></q:doughnutChart>"#),
14440 format!(
14441 r#"<q:doughnutChart>{series}<q:holeSize val="50"/><q:holeSize val="60"/></q:doughnutChart>"#
14442 ),
14443 format!(
14444 r#"<q:areaChart><q:grouping val="clustered"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
14445 ),
14446 format!(
14447 r#"<q:areaChart><q:grouping val="standard"/><q:grouping val="stacked"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
14448 ),
14449 format!(
14450 r#"<q:scatterChart><q:scatterStyle val="dots"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
14451 ),
14452 format!(
14453 r#"<q:scatterChart><q:scatterStyle val="marker"/><q:scatterStyle val="line"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
14454 ),
14455 format!(
14456 r#"<q:radarChart><q:radarStyle val="smooth"/>{series}<q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
14457 ),
14458 format!(
14459 r#"<q:radarChart><q:radarStyle val="marker"/>{series}<q:dLbls/><q:dLbls/><q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
14460 ),
14461 format!(r#"<q:pieChart>{series}<q:axId val="1"/></q:pieChart>"#),
14462 format!(
14463 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/></q:areaChart>"#
14464 ),
14465 r#"<q:areaChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:areaChart>"#.to_owned(),
14466 format!(
14467 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/><q:axId val="2"/><q:axId val="3"/></q:areaChart>"#
14468 ),
14469 format!(
14470 r#"<q:scatterChart><q:scatterStyle val="marker"/>{series}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
14471 ),
14472 format!(
14473 r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
14474 scatter
14475 .replace("<q:yVal>", "<q:val>")
14476 .replace("</q:yVal>", "</q:val>")
14477 ),
14478 format!(
14479 r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
14480 scatter_without_x(0)
14481 ),
14482 format!(
14483 r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
14484 scatter_without_y(0)
14485 ),
14486 format!(r#"<q:pieChart>{opaque_bubble}</q:pieChart>"#),
14487 ];
14488 for plot in cases {
14489 let xml = chart_with_optional_axes(&plot, false);
14490 let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
14491 assert!(result.is_ok(), "parser panicked for {plot}");
14492 assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
14493 }
14494
14495 let missing_axis = format!(
14496 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="7"/></q:areaChart>"#
14497 );
14498 let missing_axis =
14499 CT_ChartSpace::from_xml(chart_with_optional_axes(&missing_axis, true).as_bytes())
14500 .unwrap_err();
14501 assert!(missing_axis.to_string().contains("missing axis 7"));
14502
14503 let values =
14504 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
14505 let mut bubble_series = Series::new(0, 0, values.clone());
14506 bubble_series.bubble_size = Some(values);
14507 assert!(Plot::pie(vec![bubble_series]).is_err());
14508 }
14509
14510 #[test]
14511 fn unsupported_plot_families_and_children_remain_byte_preserved() {
14512 for raw in [
14513 r#"<q:pie3DChart x:keep="3d"><q:ser><x:opaque/></q:ser></q:pie3DChart>"#,
14514 r#"<q:ofPieChart x:keep="of"><x:opaque/></q:ofPieChart>"#,
14515 r#"<q:stockChart x:keep="stock"><x:opaque/></q:stockChart>"#,
14516 r#"<q:surfaceChart x:keep="surface"><x:opaque/></q:surfaceChart>"#,
14517 r#"<q:bubbleChart x:keep="bubble"><x:opaque/></q:bubbleChart>"#,
14518 ] {
14519 let parsed =
14520 CT_ChartSpace::from_xml(chart_with_optional_axes(raw, false).as_bytes()).unwrap();
14521 assert!(parsed.chart.plot_area.plots().is_err());
14522 let written = parsed.to_xml().unwrap();
14523 assert!(
14524 written
14525 .windows(raw.len())
14526 .any(|window| window == raw.as_bytes())
14527 );
14528 }
14529
14530 let pie = remaining_plot_fixtures()[0].1.clone();
14531 let preserved = r#"<!--point--><q:dPt x:id="1"><q:explosion val="7"/><x:tail/></q:dPt>"#;
14532 let plot = pie.replace("<q:dLbls>", &format!("{preserved}<q:dLbls>"));
14533 let parsed =
14534 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, false).as_bytes()).unwrap();
14535 let written = parsed.to_xml().unwrap();
14536 assert!(
14537 written
14538 .windows(preserved.len())
14539 .any(|window| window == preserved.as_bytes())
14540 );
14541
14542 for (kind, mut plot, axes) in [
14543 ("pieChart", remaining_plot_fixtures()[0].1.clone(), false),
14544 ("areaChart", remaining_plot_fixtures()[2].1.clone(), true),
14545 ] {
14546 plot = plot.replace(r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#, "");
14547 if kind == "pieChart" {
14548 plot = plot.replace(
14549 "</q:extLst></q:pieChart>",
14550 "</q:extLst><!--after-ext--></q:pieChart>",
14551 );
14552 } else {
14553 plot = plot.replace(
14554 "</q:dropLines><q:axId",
14555 "</q:dropLines><!--after-drop-lines--><q:axId",
14556 );
14557 }
14558 let mut parsed =
14559 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
14560 let labels =
14561 CT_DLbls::from_xml(format!(r#"<c:dLbls xmlns:c="{C_NS}"/>"#).as_bytes()).unwrap();
14562 match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
14563 Plot::Pie { data_labels, .. } | Plot::Area { data_labels, .. } => {
14564 *data_labels = Some(labels)
14565 }
14566 _ => panic!("expected pie or area"),
14567 }
14568 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14569 assert!(
14570 written.find("<c:dLbls").unwrap() < written.find("<q:extLst").unwrap(),
14571 "{kind}: inserted labels must precede the preserved extension"
14572 );
14573 if kind == "areaChart" {
14574 assert!(written.find("<c:dLbls").unwrap() < written.find("<q:dropLines").unwrap());
14575 assert!(
14576 written.find("<q:dropLines").unwrap()
14577 < written.find("<!--after-drop-lines-->").unwrap()
14578 );
14579 } else {
14580 assert!(
14581 written.find("<q:extLst").unwrap() < written.find("<!--after-ext-->").unwrap()
14582 );
14583 }
14584 }
14585 }
14586
14587 #[test]
14588 fn every_supported_corpus_plot_round_trips_structurally() {
14589 let Some(corpus) = require_or_skip_corpus() else {
14590 return;
14591 };
14592 verify_fetched_corpus(&corpus);
14593 let mut pie_count = 0usize;
14594 let mut bar_count = 0usize;
14595 let mut line_count = 0usize;
14596 for path in manifest_paths() {
14597 let package = OpcPackage::open(corpus.join(path)).unwrap();
14598 for (part, xml) in &package.parts {
14599 if !is_chart_part(part) {
14600 continue;
14601 }
14602 let parsed = CT_ChartSpace::from_xml(xml).unwrap();
14603 if let Ok(plots) = parsed.chart.plot_area.plots() {
14604 for plot in plots {
14605 match plot {
14606 Plot::Pie { .. } => pie_count += 1,
14607 Plot::Bar { .. } => bar_count += 1,
14608 Plot::Line { .. } => line_count += 1,
14609 _ => {}
14610 }
14611 }
14612 }
14613 let written = parsed.to_xml().unwrap();
14614 assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
14615 }
14616 }
14617 assert_eq!(pie_count, 1, "typed corpus pie coverage changed");
14618 assert_eq!(bar_count, 11, "typed corpus bar coverage changed");
14619 assert_eq!(line_count, 2, "typed corpus line coverage changed");
14620 }
14621
14622 fn standalone_axis(local: &str, children: &str) -> String {
14623 format!(
14624 r#"<c:{local} xmlns:c="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}">{children}</c:{local}>"#
14625 )
14626 }
14627
14628 fn minimal_axis(local: &str, id: &str, position: &str, cross_axis: &str) -> String {
14629 format!(
14630 r#"<c:{local}><c:axId val="{id}"/><c:scaling/><c:axPos val="{position}"/><c:crossAx val="{cross_axis}"/></c:{local}>"#
14631 )
14632 }
14633
14634 fn chart_with_axes(axes: &[String]) -> String {
14635 format!(
14636 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>"#,
14637 axes.join("")
14638 )
14639 }
14640
14641 #[test]
14642 fn data_labels_write_fixed_prefixes_in_schema_order() {
14643 let xml = format!(
14644 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>"#
14645 );
14646 let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
14647 assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0%");
14648 assert_eq!(parsed.position, Some(DataLabelPosition::OutsideEnd));
14649 assert_eq!(parsed.separator.as_deref(), Some(" / "));
14650 assert!(parsed.show_legend_key);
14651 assert!(parsed.show_value);
14652 assert!(!parsed.show_category_name);
14653 assert!(parsed.show_series_name);
14654 assert!(!parsed.show_percent);
14655 assert!(parsed.show_bubble_size);
14656
14657 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14658 assert!(written.starts_with(&format!(
14659 "<c:dLbls xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
14660 )));
14661 let mut cursor = 0usize;
14662 for tag in [
14663 "<q:dLbl",
14664 "<c:numFmt",
14665 "<q:spPr",
14666 "<q:txPr",
14667 "<c:dLblPos",
14668 "<c:showLegendKey",
14669 "<c:showVal",
14670 "<c:showCatName",
14671 "<c:showSerName",
14672 "<c:showPercent",
14673 "<c:showBubbleSize",
14674 "<c:separator",
14675 "<q:leaderLines",
14676 "<q:extLst",
14677 ] {
14678 let position = written[cursor..]
14679 .find(tag)
14680 .unwrap_or_else(|| panic!("missing ordered data-label tag {tag}"));
14681 cursor += position + tag.len();
14682 }
14683 assert_eq!(parsed, CT_DLbls::from_xml(written.as_bytes()).unwrap());
14684 }
14685
14686 #[test]
14687 fn percentage_formatted_label_renders_with_correct_text() {
14688 let Some(corpus) = require_or_skip_corpus() else {
14689 return;
14690 };
14691 verify_fetched_corpus(&corpus);
14692 assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
14693 assert_command_version("pdftotext", &["-v"], PDFTOTEXT_VERSION);
14694
14695 let source = corpus.join("bar-chart.pptx");
14696 let mut package = OpcPackage::open(&source).expect("open percentage-label source deck");
14697 let chart_part = "/ppt/charts/chart1.xml";
14698 let original_parts = package.parts.clone();
14699 let chart_xml = package
14700 .get_part(chart_part)
14701 .expect("bar chart part")
14702 .to_vec();
14703 let original_series = first_series_xml(&chart_xml).expect("bar chart series");
14704 let mut series = Series::from_xml(&original_series).expect("parse bar chart series");
14705 series.values.values.fill(0.25);
14706 let labels = CT_DLbls {
14707 number_format: Some(NumberFormat::new("0%".to_owned(), false).unwrap()),
14708 position: Some(DataLabelPosition::OutsideEnd),
14709 show_value: true,
14710 ..CT_DLbls::default()
14711 };
14712 series.data_labels = Some(labels);
14713 let replacement = series.to_xml().expect("serialize typed percentage labels");
14714 let candidate_chart = replace_once(&chart_xml, &original_series, &replacement);
14715 package.set_part(chart_part, candidate_chart);
14716 for (part, bytes) in &original_parts {
14717 if part == chart_part {
14718 assert_ne!(package.parts[part], *bytes);
14719 } else {
14720 assert_eq!(
14721 package.parts[part], *bytes,
14722 "unexpected changed part {part}"
14723 );
14724 }
14725 }
14726
14727 let temp_root =
14728 std::env::temp_dir().join(format!("oxml-chart-f123-label-gate-{}", std::process::id()));
14729 let profile = std::env::temp_dir().join(format!(
14730 "oxml-chart-f123-label-profile-{}",
14731 std::process::id()
14732 ));
14733 if temp_root.exists() {
14734 fs::remove_dir_all(&temp_root).expect("remove stale F-123 evidence");
14735 }
14736 if profile.exists() {
14737 fs::remove_dir_all(&profile).expect("remove stale F-123 profile");
14738 }
14739 fs::create_dir_all(&temp_root).expect("create F-123 evidence root");
14740 let unbound_candidate = temp_root.join("f123-percentage-label.pptx");
14741 package
14742 .save(&unbound_candidate)
14743 .expect("save percentage-label candidate");
14744 let candidate_sha = sha256(&unbound_candidate);
14745 let evidence_dir = temp_root.join(&candidate_sha);
14746 fs::create_dir(&evidence_dir).expect("create SHA-bound evidence directory");
14747 let candidate = evidence_dir.join("f123-percentage-label.pptx");
14748 fs::rename(&unbound_candidate, &candidate).expect("bind candidate to evidence SHA");
14749 assert_eq!(sha256(&candidate), candidate_sha);
14750
14751 let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
14752 let conversion = Command::new("soffice")
14753 .args([
14754 "--headless",
14755 &profile_argument,
14756 "--convert-to",
14757 "pdf:impress_pdf_Export",
14758 "--outdir",
14759 ])
14760 .arg(&evidence_dir)
14761 .arg(&candidate)
14762 .output()
14763 .expect("run pinned LibreOffice percentage-label gate");
14764 assert!(
14765 conversion.status.success(),
14766 "LibreOffice conversion failed: {}",
14767 String::from_utf8_lossy(&conversion.stderr)
14768 );
14769 let pdf = evidence_dir.join("f123-percentage-label.pdf");
14770 assert!(
14771 pdf.is_file(),
14772 "LibreOffice did not create {}",
14773 pdf.display()
14774 );
14775 let text_path = evidence_dir.join("f123-percentage-label.txt");
14776 let extraction = Command::new("pdftotext")
14777 .arg(&pdf)
14778 .arg(&text_path)
14779 .output()
14780 .expect("run pinned Poppler percentage-label extraction");
14781 assert!(
14782 extraction.status.success(),
14783 "pdftotext failed: {}",
14784 String::from_utf8_lossy(&extraction.stderr)
14785 );
14786 let extracted = fs::read_to_string(&text_path).expect("read percentage-label text");
14787 assert!(
14788 extracted.contains("25%"),
14789 "SHA-bound viewer text lacks 25%: {extracted:?}"
14790 );
14791 for artifact in [&candidate, &pdf, &text_path] {
14792 assert_eq!(
14793 artifact.parent(),
14794 Some(evidence_dir.as_path()),
14795 "evidence escaped candidate SHA"
14796 );
14797 }
14798 eprintln!("F-123 candidate/render/text evidence SHA-256 {candidate_sha}, extracted 25%");
14799 fs::remove_dir_all(&temp_root).expect("remove F-123 evidence");
14800 if profile.exists() {
14801 fs::remove_dir_all(&profile).expect("remove F-123 profile");
14802 }
14803 }
14804
14805 #[test]
14806 fn common_number_formats_project_cached_values_deterministically() {
14807 for (code, value, expected) in [
14808 ("General", 0.25, "0.25"),
14809 ("0", 12.6, "13"),
14810 ("0.0", 12.64, "12.6"),
14811 ("0.00", -12.645, "-12.64"),
14812 ("0%", 0.25, "25%"),
14813 ("0.0%", 0.256, "25.6%"),
14814 ("0.00%", 0.256, "25.60%"),
14815 ] {
14816 let format = NumberFormat::new(code.to_owned(), false).unwrap();
14817 assert_eq!(format.format_value(value).unwrap(), expected, "{code}");
14818 }
14819 }
14820
14821 #[test]
14822 fn malformed_data_labels_and_number_formats_return_errors_without_panicking() {
14823 let cases = [
14824 format!(
14825 r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="1"/><c:showVal val="0"/></c:dLbls>"#
14826 ),
14827 format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="yes"/></c:dLbls>"#),
14828 format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:dLblPos val="middle"/></c:dLbls>"#),
14829 format!(
14830 r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="" sourceLinked="1"/></c:dLbls>"#
14831 ),
14832 format!(
14833 r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="0" sourceLinked="maybe"/></c:dLbls>"#
14834 ),
14835 ];
14836 for xml in cases {
14837 let result = std::panic::catch_unwind(|| CT_DLbls::from_xml(xml.as_bytes()));
14838 assert!(result.is_ok(), "data-label parser panicked for {xml}");
14839 assert!(result.unwrap().is_err(), "malformed labels parsed: {xml}");
14840 }
14841
14842 let invalid_code = NumberFormat::new("bad\u{0}code".to_owned(), false);
14843 assert!(invalid_code.is_err());
14844 let unsupported = NumberFormat::new("#,##0".to_owned(), false).unwrap();
14845 assert!(unsupported.format_value(12.0).is_err());
14846 let general = NumberFormat::new("General".to_owned(), false).unwrap();
14847 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
14848 assert!(general.format_value(value).is_err());
14849 }
14850
14851 let invalid_separator = CT_DLbls {
14852 separator: Some("bad\u{0}separator".to_owned()),
14853 ..CT_DLbls::default()
14854 };
14855 assert!(invalid_separator.to_xml().is_err());
14856 }
14857
14858 #[test]
14859 fn data_labels_preserve_point_overrides_and_extensions_byte_for_byte() {
14860 let xml = format!(
14861 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>"#
14862 );
14863 let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
14864 let written = parsed.to_xml().unwrap();
14865 for raw in [
14866 br#"<!--before-->"#.as_slice(),
14867 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(),
14868 br#"<?after-point?>"#.as_slice(),
14869 br#"<q:spPr><x:collection-shape/></q:spPr>"#.as_slice(),
14870 br#"<q:txPr><x:collection-text/></q:txPr>"#.as_slice(),
14871 br#"<q:showLeaderLines val="1"/>"#.as_slice(),
14872 br#"<q:leaderLines><q:spPr><x:leader-shape/></q:spPr></q:leaderLines>"#.as_slice(),
14873 br#"<q:extLst><q:ext uri="collection"><x:tail/></q:ext></q:extLst>"#.as_slice(),
14874 br#"<!--after-->"#.as_slice(),
14875 ] {
14876 assert!(
14877 written.windows(raw.len()).any(|window| window == raw),
14878 "preserved data-label bytes changed: {}",
14879 String::from_utf8_lossy(raw)
14880 );
14881 }
14882 assert_eq!(parsed, CT_DLbls::from_xml(&written).unwrap());
14883 }
14884
14885 #[test]
14886 fn every_corpus_data_label_collection_round_trips_structurally() {
14887 let Some(corpus) = require_or_skip_corpus() else {
14888 return;
14889 };
14890 verify_fetched_corpus(&corpus);
14891 let mut label_collections = 0usize;
14892 let mut axis_number_formats = 0usize;
14893 for path in manifest_paths() {
14894 let package = OpcPackage::open(corpus.join(path))
14895 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14896 for (part, xml) in &package.parts {
14897 if !is_chart_part(part) {
14898 continue;
14899 }
14900 let chart = CT_ChartSpace::from_xml(xml)
14901 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14902 for series in chart.chart.plot_area.series().unwrap_or_else(|error| {
14903 panic!("{path} {part}: series projection failed: {error}")
14904 }) {
14905 if let Some(labels) = series.data_labels {
14906 let written = labels.to_xml().unwrap_or_else(|error| {
14907 panic!("{path} {part}: label write failed: {error}")
14908 });
14909 let reparsed = CT_DLbls::from_xml(&written).unwrap_or_else(|error| {
14910 panic!("{path} {part}: written labels parse failed: {error}")
14911 });
14912 assert_eq!(labels, reparsed, "{path} {part}: labels changed");
14913 label_collections += 1;
14914 }
14915 }
14916 for axis in chart.chart.plot_area.axes().unwrap_or_else(|error| {
14917 panic!("{path} {part}: axis projection failed: {error}")
14918 }) {
14919 if axis.number_format.is_some() {
14920 let written = axis.to_xml().unwrap_or_else(|error| {
14921 panic!("{path} {part}: axis write failed: {error}")
14922 });
14923 let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
14924 panic!("{path} {part}: written axis parse failed: {error}")
14925 });
14926 assert_eq!(axis, reparsed, "{path} {part}: axis changed");
14927 axis_number_formats += 1;
14928 }
14929 }
14930 }
14931 }
14932 assert!(label_collections > 0, "the pinned corpus has no c:dLbls");
14933 assert!(
14934 axis_number_formats > 0,
14935 "the pinned corpus has no axis c:numFmt"
14936 );
14937 eprintln!(
14938 "ChartML data-label corpus gate checked {label_collections} collections and {axis_number_formats} axis number formats"
14939 );
14940 }
14941
14942 fn full_axis(local: &str, position: &str) -> String {
14943 format!(
14944 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}>"#
14945 )
14946 }
14947
14948 #[test]
14949 fn axis_id_pairs_are_reciprocal() {
14950 assert_eq!(
14951 AxisId::new(i64::from(i32::MIN)).unwrap().value(),
14952 i64::from(i32::MIN)
14953 );
14954 assert_eq!(
14955 AxisId::new(i64::from(u32::MAX)).unwrap().value(),
14956 i64::from(u32::MAX)
14957 );
14958 assert!(AxisId::new(i64::from(i32::MIN) - 1).is_err());
14959 assert!(AxisId::new(i64::from(u32::MAX) + 1).is_err());
14960
14961 let valid = chart_with_axes(&[
14962 minimal_axis("catAx", "-1884094432", "b", "-1884097184"),
14963 minimal_axis("valAx", "-1884097184", "l", "-1884094432"),
14964 ]);
14965 let axes = CT_ChartSpace::from_xml(valid.as_bytes())
14966 .unwrap()
14967 .chart
14968 .plot_area
14969 .axes()
14970 .unwrap();
14971 assert_eq!(axes.len(), 2);
14972 assert_eq!(axes[0].id.value(), -1_884_094_432);
14973 assert_eq!(axes[0].cross_axis, axes[1].id);
14974 assert_eq!(axes[1].cross_axis, axes[0].id);
14975
14976 let invalid_sets = [
14977 vec![
14978 minimal_axis("catAx", "1", "b", "2"),
14979 minimal_axis("valAx", "1", "l", "2"),
14980 ],
14981 vec![minimal_axis("catAx", "1", "b", "1")],
14982 vec![minimal_axis("catAx", "1", "b", "2")],
14983 vec![
14984 minimal_axis("catAx", "1", "b", "2"),
14985 minimal_axis("valAx", "2", "l", "3"),
14986 minimal_axis("serAx", "3", "r", "2"),
14987 ],
14988 ];
14989 for axes in invalid_sets {
14990 let chart = CT_ChartSpace::from_xml(chart_with_axes(&axes).as_bytes()).unwrap();
14991 assert!(chart.chart.plot_area.axes().is_err());
14992 }
14993
14994 let empty = CT_ChartSpace::from_xml(chart_with_axes(&[]).as_bytes()).unwrap();
14995 assert!(empty.chart.plot_area.axes().unwrap().is_empty());
14996 }
14997
14998 #[test]
14999 fn all_axis_forms_write_fixed_prefixes_in_schema_order() {
15000 for (local, kind, position) in [
15001 ("catAx", AxisKind::Category, AxisPosition::Bottom),
15002 ("valAx", AxisKind::Value, AxisPosition::Left),
15003 ("dateAx", AxisKind::Date, AxisPosition::Top),
15004 ("serAx", AxisKind::Series, AxisPosition::Right),
15005 ] {
15006 let xml = full_axis(local, position.as_str());
15007 let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
15008 assert_eq!(parsed.kind, kind);
15009 assert_eq!(parsed.id.value(), -1_125_255_920);
15010 assert_eq!(parsed.scaling.log_base, Some(10.0));
15011 assert_eq!(parsed.scaling.orientation, Orientation::MaxMin);
15012 assert_eq!(parsed.scaling.maximum, Some(100.0));
15013 assert_eq!(parsed.scaling.minimum, Some(-10.0));
15014 assert!(parsed.deleted);
15015 assert_eq!(parsed.position, position);
15016 assert!(parsed.major_gridlines.is_some());
15017 assert!(parsed.minor_gridlines.is_some());
15018 assert!(parsed.title.is_some());
15019 assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0");
15020 assert_eq!(parsed.major_tick_mark, TickMark::Cross);
15021 assert_eq!(parsed.minor_tick_mark, TickMark::Inside);
15022 assert_eq!(parsed.tick_label_position, TickLabelPosition::High);
15023 assert!(parsed.sp_pr.is_some());
15024 assert!(parsed.tx_pr.is_some());
15025 assert_eq!(parsed.cross_axis.value(), -1_004_303_696);
15026
15027 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15028 assert!(written.starts_with(&format!(
15029 "<c:{local} xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
15030 )));
15031 assert!(written.contains("<c:axId val=\"-1125255920\""));
15032 let mut cursor = 0usize;
15033 for tag in [
15034 "<c:axId",
15035 "<c:scaling",
15036 "<c:delete",
15037 "<c:axPos",
15038 "<c:majorGridlines",
15039 "<c:minorGridlines",
15040 "<c:title",
15041 "<c:numFmt",
15042 "<c:majorTickMark",
15043 "<c:minorTickMark",
15044 "<c:tickLblPos",
15045 "<c:spPr",
15046 "<c:txPr",
15047 "<c:crossAx",
15048 ] {
15049 let position = written[cursor..]
15050 .find(tag)
15051 .unwrap_or_else(|| panic!("missing ordered axis tag {tag}"));
15052 cursor += position + tag.len();
15053 }
15054 assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
15055 }
15056
15057 let mut constructed = Axis::new(
15058 AxisKind::Value,
15059 AxisId::new(1).unwrap(),
15060 AxisPosition::Left,
15061 AxisId::new(2).unwrap(),
15062 );
15063 assert_eq!(
15064 constructed,
15065 Axis::from_xml(&constructed.to_xml().unwrap()).unwrap()
15066 );
15067 constructed.kind = AxisKind::Category;
15068 constructed.id = AxisId::new(3).unwrap();
15069 constructed.cross_axis = AxisId::new(4).unwrap();
15070 constructed.scaling.minimum = Some(0.0);
15071 constructed.scaling.maximum = Some(10.0);
15072 let written = constructed.to_xml().unwrap();
15073 assert!(written.starts_with(b"<c:catAx"));
15074 assert_eq!(constructed, Axis::from_xml(&written).unwrap());
15075 }
15076
15077 #[test]
15078 fn axis_equality_normalizes_ids_without_losing_lexical_preservation() {
15079 let padded = Axis::from_xml(
15080 standalone_axis(
15081 "catAx",
15082 "<c:axId val=\"01\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"002\"/>",
15083 )
15084 .as_bytes(),
15085 )
15086 .unwrap();
15087 let normalized = Axis::from_xml(
15088 standalone_axis(
15089 "catAx",
15090 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15091 )
15092 .as_bytes(),
15093 )
15094 .unwrap();
15095 assert_eq!(padded, normalized);
15096
15097 let preserved = String::from_utf8(padded.to_xml().unwrap()).unwrap();
15098 assert!(preserved.contains("<c:axId val=\"01\""));
15099 assert!(preserved.contains("<c:crossAx val=\"002\""));
15100
15101 let mut mutated = padded;
15102 mutated.id = AxisId::new(3).unwrap();
15103 mutated.cross_axis = AxisId::new(4).unwrap();
15104 let rewritten = String::from_utf8(mutated.to_xml().unwrap()).unwrap();
15105 assert!(rewritten.contains("<c:axId val=\"3\""));
15106 assert!(rewritten.contains("<c:crossAx val=\"4\""));
15107 assert!(!rewritten.contains("val=\"01\""));
15108 assert!(!rewritten.contains("val=\"002\""));
15109 }
15110
15111 #[test]
15112 fn malformed_axis_values_return_errors_without_panicking() {
15113 let cases = [
15114 standalone_axis(
15115 "catAx",
15116 "<c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15117 ),
15118 standalone_axis(
15119 "catAx",
15120 "<c:axId val=\"1\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15121 ),
15122 standalone_axis(
15123 "catAx",
15124 "<c:axId val=\"1\"/><c:scaling/><c:crossAx val=\"2\"/>",
15125 ),
15126 standalone_axis(
15127 "catAx",
15128 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/>",
15129 ),
15130 standalone_axis(
15131 "catAx",
15132 "<c:axId val=\"-2147483649\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15133 ),
15134 standalone_axis(
15135 "catAx",
15136 "<c:axId val=\"4294967296\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15137 ),
15138 standalone_axis(
15139 "catAx",
15140 "<c:axId val=\"1\"/><c:scaling><c:logBase val=\"1\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15141 ),
15142 standalone_axis(
15143 "catAx",
15144 "<c:axId val=\"1\"/><c:scaling><c:max val=\"NaN\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15145 ),
15146 standalone_axis(
15147 "catAx",
15148 "<c:axId val=\"1\"/><c:scaling><c:max val=\"1\"/><c:min val=\"2\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15149 ),
15150 standalone_axis(
15151 "catAx",
15152 "<c:axId val=\"1\"/><c:scaling><c:orientation val=\"sideways\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15153 ),
15154 standalone_axis(
15155 "catAx",
15156 "<c:axId val=\"1\"/><c:scaling/><c:delete val=\"yes\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15157 ),
15158 standalone_axis(
15159 "catAx",
15160 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"middle\"/><c:crossAx val=\"2\"/>",
15161 ),
15162 standalone_axis(
15163 "catAx",
15164 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:majorTickMark val=\"near\"/><c:crossAx val=\"2\"/>",
15165 ),
15166 standalone_axis(
15167 "catAx",
15168 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:tickLblPos val=\"middle\"/><c:crossAx val=\"2\"/>",
15169 ),
15170 standalone_axis(
15171 "catAx",
15172 "<c:axId val=\"1\"/><c:axId val=\"2\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15173 ),
15174 standalone_axis(
15175 "catAx",
15176 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt sourceLinked=\"1\"/><c:crossAx val=\"2\"/>",
15177 ),
15178 standalone_axis(
15179 "catAx",
15180 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt formatCode=\"0\" sourceLinked=\"maybe\"/><c:crossAx val=\"2\"/>",
15181 ),
15182 ];
15183 for xml in cases {
15184 let result = std::panic::catch_unwind(|| Axis::from_xml(xml.as_bytes()));
15185 assert!(result.is_ok(), "axis parser panicked for {xml}");
15186 assert!(result.unwrap().is_err(), "malformed axis parsed: {xml}");
15187 }
15188
15189 let mut axis = Axis::new(
15190 AxisKind::Value,
15191 AxisId::new(1).unwrap(),
15192 AxisPosition::Left,
15193 AxisId::new(2).unwrap(),
15194 );
15195 axis.scaling.maximum = Some(f64::INFINITY);
15196 assert!(axis.to_xml().is_err());
15197 }
15198
15199 #[test]
15200 fn axes_preserve_unmodelled_children_byte_for_byte() {
15201 let xml = format!(
15202 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>"#
15203 );
15204 let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
15205 let mut relabelled = parsed.clone();
15206 relabelled.kind = AxisKind::Value;
15207 assert!(
15208 relabelled.to_xml().is_err(),
15209 "a parsed category axis must not retain its tail under a value-axis root"
15210 );
15211 let written = parsed.to_xml().unwrap();
15212 for raw in [
15213 br#"<!--before-->"#.as_slice(),
15214 br#"<?after-id?>"#.as_slice(),
15215 br#"<!--scale-->"#.as_slice(),
15216 br#"<q:extLst><q:ext uri="scale"><x:data/></q:ext></q:extLst>"#.as_slice(),
15217 br#"<x:grid/>"#.as_slice(),
15218 br#"<x:title/>"#.as_slice(),
15219 br#"<q:crosses val="autoZero"/>"#.as_slice(),
15220 br#"<q:auto val="1"><x:auto/></q:auto>"#.as_slice(),
15221 br#"<q:extLst><q:ext uri="axis"><x:tail/></q:ext></q:extLst>"#.as_slice(),
15222 br#"<!--after-->"#.as_slice(),
15223 ] {
15224 assert!(
15225 written.windows(raw.len()).any(|window| window == raw),
15226 "preserved axis bytes changed: {}",
15227 String::from_utf8_lossy(raw)
15228 );
15229 }
15230 let written = String::from_utf8(written).unwrap();
15231 assert!(written.starts_with("<c:catAx"));
15232 assert!(written.contains("x:keep=\"axis\""));
15233 assert!(written.contains("x:keep=\"id\""));
15234 assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
15235 }
15236
15237 #[test]
15238 fn every_corpus_axis_round_trips_structurally() {
15239 let Some(corpus) = require_or_skip_corpus() else {
15240 return;
15241 };
15242 verify_fetched_corpus(&corpus);
15243 let mut axis_count = 0usize;
15244 let mut chart_parts = 0usize;
15245 let mut kinds = HashSet::new();
15246 for path in manifest_paths() {
15247 let package = OpcPackage::open(corpus.join(path))
15248 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
15249 for (part, xml) in &package.parts {
15250 if !is_chart_part(part) {
15251 continue;
15252 }
15253 let chart = CT_ChartSpace::from_xml(xml)
15254 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
15255 let axes = chart.chart.plot_area.axes().unwrap_or_else(|error| {
15256 panic!("{path} {part}: axis projection failed: {error}")
15257 });
15258 for axis in axes {
15259 kinds.insert(axis.kind);
15260 let written = axis.to_xml().unwrap_or_else(|error| {
15261 panic!("{path} {part}: axis write failed: {error}")
15262 });
15263 let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
15264 panic!("{path} {part}: written axis parse failed: {error}")
15265 });
15266 assert_eq!(axis, reparsed, "{path} {part}: axis model changed");
15267 axis_count += 1;
15268 }
15269 chart_parts += 1;
15270 }
15271 }
15272 assert!(
15273 axis_count > 0,
15274 "the pinned corpus contained no ChartML axes"
15275 );
15276 assert!(kinds.contains(&AxisKind::Category));
15277 assert!(kinds.contains(&AxisKind::Value));
15278
15279 for kind in ["dateAx", "serAx"] {
15280 let parsed = Axis::from_xml(
15281 standalone_axis(
15282 kind,
15283 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15284 )
15285 .as_bytes(),
15286 )
15287 .unwrap();
15288 assert_eq!(parsed, Axis::from_xml(&parsed.to_xml().unwrap()).unwrap());
15289 }
15290 eprintln!(
15291 "ChartML axis corpus gate checked {axis_count} axes across {chart_parts} chart parts"
15292 );
15293 }
15294
15295 #[test]
15296 fn series_formula_and_cache_are_consistent_with_one_source() {
15297 let values = NumericData::new(
15298 "'Sales 24'!$B$2:$B$4".to_owned(),
15299 "0.0".to_owned(),
15300 vec![4.25, 8.5, 17.0],
15301 )
15302 .unwrap();
15303 let mut series = Series::new(3, 1, values);
15304 series.categories = Some(AxisData::String(
15305 StringRef::new(
15306 "'Sales 24'!$A$2:$A$4".to_owned(),
15307 vec!["North".to_owned(), "South".to_owned(), "West".to_owned()],
15308 )
15309 .unwrap(),
15310 ));
15311
15312 let written = String::from_utf8(series.to_xml().unwrap()).unwrap();
15313 assert!(written.contains("<c:f>'Sales 24'!$B$2:$B$4</c:f>"));
15314 assert!(written.contains("<c:formatCode>0.0</c:formatCode>"));
15315 assert_eq!(written.matches("<c:ptCount val=\"3\"").count(), 2);
15316 for (index, value) in ["4.25", "8.5", "17"].iter().enumerate() {
15317 assert!(written.contains(&format!("<c:pt idx=\"{index}\"><c:v>{value}</c:v></c:pt>")));
15318 }
15319 let reparsed = Series::from_xml(written.as_bytes()).unwrap();
15320 assert_eq!(reparsed.index, series.index);
15321 assert_eq!(reparsed.order, series.order);
15322 match (&reparsed.categories, &series.categories) {
15323 (Some(AxisData::String(left)), Some(AxisData::String(right))) => {
15324 assert_eq!(left.formula, right.formula);
15325 assert_eq!(left.values, right.values);
15326 }
15327 _ => panic!("expected string categories"),
15328 }
15329 assert_eq!(reparsed.values.formula, series.values.formula);
15330 assert_eq!(reparsed.values.format_code, series.values.format_code);
15331 assert_eq!(reparsed.values.values, series.values.values);
15332 }
15333
15334 #[test]
15335 fn string_and_numeric_references_write_fixed_prefixes_in_schema_order() {
15336 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>"#;
15337 let string_ref = StringRef::from_xml(string_xml).unwrap();
15338 let written = String::from_utf8(string_ref.to_xml().unwrap()).unwrap();
15339 assert!(written.starts_with("<c:strRef xmlns:c="));
15340 assert!(written.find("<c:f>").unwrap() < written.find("<c:strCache>").unwrap());
15341 assert!(written.find("<c:ptCount").unwrap() < written.find("<c:pt idx=").unwrap());
15342
15343 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>"#;
15344 let numeric = NumericData::from_xml(numeric_xml).unwrap();
15345 let written = String::from_utf8(numeric.to_xml().unwrap()).unwrap();
15346 let positions: Vec<_> = [
15347 "<c:f>",
15348 "<c:numCache>",
15349 "<c:formatCode>",
15350 "<c:ptCount",
15351 "<c:pt idx=",
15352 ]
15353 .iter()
15354 .map(|tag| written.find(tag).unwrap())
15355 .collect();
15356 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15357 assert_eq!(numeric, NumericData::from_xml(written.as_bytes()).unwrap());
15358 }
15359
15360 #[test]
15361 fn mixed_chartml_aliases_resolve_by_namespace_uri() {
15362 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>"#;
15363 let parsed = Series::from_xml(xml).unwrap();
15364 assert_eq!(parsed.index, 4);
15365 assert_eq!(parsed.order, 2);
15366 assert_eq!(parsed.values.values, vec![1.5, 2.5]);
15367 assert!(matches!(parsed.categories, Some(AxisData::String(_))));
15368 assert_eq!(parsed, Series::from_xml(&parsed.to_xml().unwrap()).unwrap());
15369
15370 let foreign = 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>"#;
15371 let parsed = Series::from_xml(foreign).unwrap();
15372 assert!(parsed.sp_pr.as_ref().unwrap().fill.is_none());
15373 let written = parsed.to_xml().unwrap();
15374 assert!(
15375 std::str::from_utf8(&written)
15376 .unwrap()
15377 .contains(r#"<x:solidFill><x:srgbClr val="112233"/></x:solidFill>"#)
15378 );
15379 assert_eq!(parsed, Series::from_xml(&written).unwrap());
15380 }
15381
15382 #[test]
15383 fn plot_area_series_ignores_inherited_foreign_plot_aliases() {
15384 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>"#;
15385 let chart = CT_ChartSpace::from_xml(xml).unwrap();
15386 let series = chart.chart.plot_area.series().unwrap();
15387 assert_eq!(series.len(), 1);
15388 assert_eq!(series[0].index, 1);
15389 let written = String::from_utf8(series[0].to_xml().unwrap()).unwrap();
15390 assert!(
15391 written.contains(r#"xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart""#)
15392 );
15393 assert!(written.contains(r#"xmlns:x="urn:producer""#));
15394 assert!(written.contains("<q:marker><x:data/></q:marker>"));
15395 assert_eq!(series[0], Series::from_xml(written.as_bytes()).unwrap());
15396
15397 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>"#;
15398 let chart = CT_ChartSpace::from_xml(conflicting).unwrap();
15399 assert!(chart.chart.plot_area.series().is_err());
15400 }
15401
15402 #[test]
15403 fn malformed_series_and_cache_values_return_errors_without_panicking() {
15404 let cases: &[&[u8]] = &[
15405 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>"#,
15406 br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/></c:ser>"#,
15407 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>"#,
15408 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>"#,
15409 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>"#,
15410 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>"#,
15411 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>"#,
15412 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>"#,
15413 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>"#,
15414 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>"#,
15415 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>"#,
15416 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>"#,
15417 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>"#,
15418 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>"#,
15419 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>"#,
15420 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>"#,
15421 ];
15422 for xml in cases {
15423 let result = std::panic::catch_unwind(|| Series::from_xml(xml));
15424 assert!(result.is_ok(), "series parser panicked");
15425 assert!(
15426 result.unwrap().is_err(),
15427 "malformed series parsed: {}",
15428 String::from_utf8_lossy(xml)
15429 );
15430 }
15431 assert!(
15432 NumericData::new(
15433 "S!$A$1".to_owned(),
15434 "General".to_owned(),
15435 vec![f64::INFINITY]
15436 )
15437 .is_err()
15438 );
15439 }
15440
15441 #[test]
15442 fn chart_text_rejects_xml_forbidden_characters_and_escapes_metacharacters() {
15443 assert!(StringRef::new("S!$A$1\u{1}".to_owned(), Vec::new()).is_err());
15444 assert!(StringRef::new("S!$A$1".to_owned(), vec!["invalid\u{1}".to_owned()]).is_err());
15445 assert!(
15446 NumericData::new("S!$A$1\u{1}".to_owned(), "General".to_owned(), Vec::new()).is_err()
15447 );
15448 assert!(
15449 NumericData::new("S!$A$1".to_owned(), "General\u{1}".to_owned(), Vec::new()).is_err()
15450 );
15451
15452 let mut strings = StringRef::new("S!$A$1".to_owned(), vec!["valid".to_owned()]).unwrap();
15453 strings.formula = "invalid\u{1}".to_owned();
15454 assert!(strings.to_xml().is_err());
15455 strings.formula = "S!$A$1".to_owned();
15456 strings.values[0] = "invalid\u{1}".to_owned();
15457 assert!(strings.to_xml().is_err());
15458
15459 let mut numbers =
15460 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap();
15461 numbers.formula = "invalid\u{1}".to_owned();
15462 assert!(numbers.to_xml().is_err());
15463 numbers.formula = "S!$A$1".to_owned();
15464 numbers.format_code = "invalid\u{1}".to_owned();
15465 assert!(numbers.to_xml().is_err());
15466
15467 let strings =
15468 StringRef::new("S!$A$1&".to_owned(), vec!["North < West & East".to_owned()]).unwrap();
15469 let written = String::from_utf8(strings.to_xml().unwrap()).unwrap();
15470 assert!(written.contains("S!$A$1&"));
15471 assert!(written.contains("North < West & East"));
15472 }
15473
15474 #[test]
15475 fn series_preserves_unmodelled_children_byte_for_byte() {
15476 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>"#;
15477 let parsed = Series::from_xml(xml).unwrap();
15478 let written = parsed.to_xml().unwrap();
15479 for raw in [
15480 br#"<!--before-->"#.as_slice(),
15481 br#"<x:between/>"#.as_slice(),
15482 br#"<q:dPt x:id="one"><q:idx val="0"/></q:dPt>"#.as_slice(),
15483 br#"<q:trendline x:id="trend"/>"#.as_slice(),
15484 br#"<q:extLst><q:ext uri="keep"><x:data/></q:ext></q:extLst>"#.as_slice(),
15485 br#"<!--point-->"#.as_slice(),
15486 br#"<x:cacheExt/>"#.as_slice(),
15487 ] {
15488 assert!(
15489 written.windows(raw.len()).any(|window| window == raw),
15490 "preserved series bytes changed: {}",
15491 String::from_utf8_lossy(raw)
15492 );
15493 }
15494 let written = String::from_utf8(written.clone()).unwrap();
15495 assert!(written.contains(r#"x:keep="series""#));
15496 assert!(written.contains(r#"x:keep="formula""#));
15497 assert!(written.contains("<c:dLbls"));
15498 assert!(written.contains(r#"x:id="labels""#));
15499 assert!(parsed.data_labels.is_some());
15500 assert_eq!(parsed, Series::from_xml(written.as_bytes()).unwrap());
15501 }
15502
15503 #[test]
15504 fn public_series_edits_do_not_duplicate_or_drop_preserved_payloads() {
15505 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>"#;
15506 let parsed = Series::from_xml(opaque_xml).unwrap();
15507
15508 let mut edited = parsed.clone();
15509 edited.name = Some(StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap());
15510 assert!(edited.to_xml().is_err());
15511
15512 let mut edited = parsed.clone();
15513 edited.categories = Some(AxisData::Numeric(
15514 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap(),
15515 ));
15516 assert!(edited.to_xml().is_err());
15517
15518 let mut edited = parsed;
15519 edited.bubble_size =
15520 Some(NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
15521 assert!(edited.to_xml().is_err());
15522
15523 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>"#;
15524 let parsed = Series::from_xml(cache_xml).unwrap();
15525 let tail = "<c:extLst><c:ext uri=\"keep\"><x:data/></c:ext></c:extLst>";
15526
15527 let mut shortened = parsed.clone();
15528 shortened.values.values.truncate(1);
15529 let written = String::from_utf8(shortened.to_xml().unwrap()).unwrap();
15530 assert!(!written.contains("<c:pt idx=\"1\""));
15531 assert!(written.find("<c:pt idx=\"0\"").unwrap() < written.find(tail).unwrap());
15532 assert!(Series::from_xml(written.as_bytes()).is_ok());
15533
15534 let mut grown = parsed;
15535 grown.values.values.push(3.0);
15536 let written = String::from_utf8(grown.to_xml().unwrap()).unwrap();
15537 assert!(written.find("<c:pt idx=\"2\"").unwrap() < written.find(tail).unwrap());
15538 assert!(Series::from_xml(written.as_bytes()).is_ok());
15539
15540 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>"#;
15541 let mut edited = Series::from_xml(ordered_xml).unwrap();
15542 edited.name = Some(StringRef::new("S!$B$1".to_owned(), Vec::new()).unwrap());
15543 edited.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
15544 edited.categories = Some(AxisData::String(
15545 StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap(),
15546 ));
15547 edited.bubble_size =
15548 Some(NumericData::new("S!$C$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
15549 let written = String::from_utf8(edited.to_xml().unwrap()).unwrap();
15550 let positions: Vec<_> = [
15551 "<c:tx>",
15552 "<c:spPr",
15553 "<c:marker>",
15554 "<c:cat>",
15555 "<c:val>",
15556 "<c:bubbleSize>",
15557 "<c:extLst>",
15558 ]
15559 .iter()
15560 .map(|tag| written.find(tag).unwrap())
15561 .collect();
15562 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15563 assert!(Series::from_xml(written.as_bytes()).is_ok());
15564 }
15565
15566 #[test]
15567 fn every_corpus_series_round_trips_structurally() {
15568 let Some(corpus) = require_or_skip_corpus() else {
15569 return;
15570 };
15571 verify_fetched_corpus(&corpus);
15572 let mut series_count = 0usize;
15573 let mut chart_parts = 0usize;
15574 for path in manifest_paths() {
15575 let package = OpcPackage::open(corpus.join(path))
15576 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
15577 for (part, xml) in &package.parts {
15578 if !is_chart_part(part) {
15579 continue;
15580 }
15581 let chart = CT_ChartSpace::from_xml(xml)
15582 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
15583 let parsed_series =
15584 chart.chart.plot_area.series().unwrap_or_else(|error| {
15585 panic!("{path} {part}: series parse failed: {error}")
15586 });
15587 for series in parsed_series {
15588 let written = series.to_xml().unwrap_or_else(|error| {
15589 panic!("{path} {part}: series write failed: {error}")
15590 });
15591 let reparsed = Series::from_xml(&written).unwrap_or_else(|error| {
15592 panic!("{path} {part}: written series parse failed: {error}")
15593 });
15594 assert_eq!(series, reparsed, "{path} {part}: series model changed");
15595 series_count += 1;
15596 }
15597 chart_parts += 1;
15598 }
15599 }
15600 assert!(
15601 series_count > 0,
15602 "the pinned corpus contained no supported series"
15603 );
15604 eprintln!(
15605 "ChartML series corpus gate checked {series_count} series across {chart_parts} chart parts"
15606 );
15607 }
15608
15609 #[test]
15610 fn chart_space_reads_aliases_and_writes_fixed_prefixes_in_schema_order() {
15611 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>"#;
15612 let parsed = CT_ChartSpace::from_xml(xml).unwrap();
15613 assert!(!parsed.chart.auto_title_deleted);
15614 assert!(!parsed.chart.plot_vis_only);
15615 assert_eq!(parsed.chart.disp_blanks_as, DispBlanksAs::Span);
15616 assert!(parsed.chart.title.is_some());
15617 assert!(parsed.chart.legend.is_some());
15618 assert!(parsed.sp_pr.as_ref().unwrap().fill.is_some());
15619 assert!(parsed.tx_pr.is_some());
15620
15621 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15622 assert!(written.starts_with(
15623 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""#
15624 ));
15625 for tag in [
15626 "<c:chart>",
15627 "<c:title>",
15628 "<c:plotArea>",
15629 "<c:legend>",
15630 "<c:plotVisOnly",
15631 "<c:dispBlanksAs",
15632 "<c:spPr>",
15633 "<c:txPr",
15634 ] {
15635 assert!(written.contains(tag), "missing fixed-prefix tag {tag}");
15636 }
15637 let positions: Vec<_> = [
15638 "<c:title>",
15639 "<c:plotArea>",
15640 "<c:legend>",
15641 "<c:plotVisOnly",
15642 "<c:dispBlanksAs",
15643 "<c:spPr>",
15644 "<c:txPr",
15645 ]
15646 .iter()
15647 .map(|tag| written.find(tag).unwrap())
15648 .collect();
15649 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15650 assert!(written.contains("<a:solidFill><a:srgbClr"));
15651 assert!(written.contains("<d:bodyPr/><d:p"));
15652 assert!(written.contains(&format!("xmlns:d=\"{A_NS}\"")));
15653 assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
15654 }
15655
15656 #[test]
15657 fn public_core_edits_keep_preserved_children_in_their_schema_slots() {
15658 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>"#;
15659 let mut parsed = CT_ChartSpace::from_xml(xml).unwrap();
15660 parsed.chart.auto_title_deleted = true;
15661 parsed.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
15662 parsed.tx_pr =
15663 Some(CT_TextBody::from_xml(br#"<a:txBody><a:bodyPr/><a:p/></a:txBody>"#).unwrap());
15664 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15665 let positions: Vec<_> = [
15666 "<c:autoTitleDeleted",
15667 "<c:pivotFmts>",
15668 "<c:plotArea",
15669 "<c:spPr",
15670 "<c:txPr",
15671 "<c:externalData",
15672 ]
15673 .iter()
15674 .map(|tag| written.find(tag).unwrap())
15675 .collect();
15676 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15677 assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
15678 }
15679
15680 #[test]
15681 fn malformed_core_chart_values_return_errors_without_panicking() {
15682 let cases: &[&[u8]] = &[
15683 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"/>"#,
15684 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart/></c:chartSpace>"#,
15685 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:plotArea/></c:chart></c:chartSpace>"#,
15686 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:autoTitleDeleted val="yes"/><c:plotArea/></c:chart></c:chartSpace>"#,
15687 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:dispBlanksAs val="empty"/></c:chart></c:chartSpace>"#,
15688 br#"<c:chartSpace xmlns:c="urn:wrong"><c:chart><c:plotArea/></c:chart></c:chartSpace>"#,
15689 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:foreign"><x:chart><x:plotArea/></x:chart></c:chartSpace>"#,
15690 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>"#,
15691 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:title/><c:title/><c:plotArea/></c:chart></c:chartSpace>"#,
15692 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>"#,
15693 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><x:bodyPr/><a:p/></c:txPr></c:chartSpace>"#,
15694 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>"#,
15695 ];
15696 for xml in cases {
15697 let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml));
15698 assert!(result.is_ok(), "ChartML parser panicked");
15699 assert!(
15700 result.unwrap().is_err(),
15701 "malformed ChartML parsed: {}",
15702 String::from_utf8_lossy(xml)
15703 );
15704 }
15705 }
15706
15707 #[test]
15708 fn fx215_chart_text_adapter_preserves_qualified_and_foreign_owners() {
15709 for (prefix, declarations) in [
15710 ("d:", format!("xmlns:d=\"{A_NS}\"")),
15711 ("", format!("xmlns=\"{A_NS}\"")),
15712 ] {
15713 let opaque = "<x:r><x:t>foreign</x:t></x:r>";
15714 let xml = format!(
15715 "<c:chartSpace xmlns:c=\"{C_NS}\" {declarations} xmlns:x=\"urn:foreign\"><c:chart><c:plotArea/></c:chart><c:txPr><{prefix}bodyPr/><{prefix}p>{opaque}<{prefix}r><{prefix}t>owned</{prefix}t></{prefix}r></{prefix}p></c:txPr></c:chartSpace>"
15716 );
15717 let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15718 let text = chart.tx_pr.as_ref().unwrap();
15719 assert_eq!(text.paragraphs().len(), 1);
15720 assert_eq!(text.paragraphs()[0].runs.len(), 1);
15721 let written = chart.to_xml().unwrap();
15722 assert!(std::str::from_utf8(&written).unwrap().contains(opaque));
15723 let reopened = CT_ChartSpace::from_xml(&written).unwrap();
15724 assert_eq!(
15725 reopened.tx_pr.as_ref().unwrap().paragraphs()[0].runs.len(),
15726 1
15727 );
15728 assert_eq!(
15729 text.body_properties,
15730 reopened.tx_pr.as_ref().unwrap().body_properties
15731 );
15732 for body in [text, reopened.tx_pr.as_ref().unwrap()] {
15733 let oxml_drawing::text::TextRun::Run(run) = &body.paragraphs()[0].runs[0] else {
15734 panic!("qualified text must remain a regular run");
15735 };
15736 assert_eq!(run.text.value, "owned");
15737 }
15738 }
15739 let xml = format!(
15740 "<c:chartSpace xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" 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>"
15741 );
15742 let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15743 assert!(
15744 chart.tx_pr.as_ref().unwrap().paragraphs()[0]
15745 .runs
15746 .is_empty()
15747 );
15748 let saved = chart.to_xml().unwrap();
15749 assert!(
15750 std::str::from_utf8(&saved)
15751 .unwrap()
15752 .contains("<x:r><x:t>foreign</x:t></x:r>")
15753 );
15754 assert_eq!(chart, CT_ChartSpace::from_xml(&saved).unwrap());
15755 }
15756
15757 #[test]
15758 fn point_label_overrides_render_without_changing_preserved_xml() {
15759 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>"#;
15760 let plot = bar_plot("").replace(
15761 r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
15762 &format!(
15763 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>"#
15764 ),
15765 );
15766 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
15767 let plot = &chart.chart.plot_area.plots().unwrap()[0];
15768 let Plot::Bar { data_labels, .. } = plot else {
15769 panic!("expected bar plot");
15770 };
15771 let labels = data_labels.as_ref().unwrap();
15772 let written = labels.to_xml().unwrap();
15773 assert!(
15774 written
15775 .windows(raw_overrides.len())
15776 .any(|window| window == raw_overrides.as_bytes())
15777 );
15778 let overrides = super::point_label_overrides(labels).unwrap();
15779 assert_eq!(overrides.keys().copied().collect::<Vec<_>>(), vec![1]);
15780
15781 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
15782 let mut rendered = Vec::new();
15783 super::render_data_labels(
15784 plot,
15785 chart_bounds(),
15786 DispBlanksAs::Gap,
15787 super::GeometryOptions::default(),
15788 &mut fonts,
15789 &mut rendered,
15790 )
15791 .unwrap();
15792 let labels: Vec<_> = rendered
15793 .iter()
15794 .filter_map(|element| match element {
15795 PositionedElement::Text(run) => Some(run),
15796 _ => None,
15797 })
15798 .collect();
15799 assert_eq!(labels.len(), 2);
15800 assert_eq!(labels[0].text, "North, 1");
15801 assert_eq!(labels[1].text, "South / 2.0");
15802 assert_point_near(labels[0].origin, Point { x: 50.0, y: 105.0 });
15803 assert_point_near(labels[1].origin, Point { x: 158.0, y: 70.0 });
15804 assert_eq!(
15805 CT_DLbls::from_xml(&written).unwrap().to_xml().unwrap(),
15806 written
15807 );
15808 }
15809
15810 #[test]
15811 fn malformed_point_label_overrides_return_contextual_errors() {
15812 let cases = [
15813 r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:showVal val="1"/></q:dLbl></q:dLbls>"#,
15814 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>"#,
15815 r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="point"/></q:dLbl></q:dLbls>"#,
15816 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>"#,
15817 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>"#,
15818 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>"#,
15819 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>"#,
15820 ];
15821 for xml in cases {
15822 let labels = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
15823 let result = std::panic::catch_unwind(|| super::point_label_overrides(&labels));
15824 assert!(result.is_ok(), "point-label parser panicked for {xml}");
15825 assert!(
15826 result.unwrap().is_err(),
15827 "malformed point labels parsed: {xml}"
15828 );
15829 }
15830
15831 let foreign = format!(
15832 r#"<q:dLbls xmlns:q="{C_NS}" xmlns:x="urn:foreign"><x:dLbl><x:idx val="0"/></x:dLbl></q:dLbls>"#
15833 );
15834 let labels = CT_DLbls::from_xml(foreign.as_bytes()).unwrap();
15835 assert!(super::point_label_overrides(&labels).unwrap().is_empty());
15836 }
15837
15838 #[test]
15839 fn percentage_label_aggregation_rejects_nonfinite_totals() {
15840 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
15841 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
15842 let Plot::Bar { series, .. } = plot else {
15843 panic!("expected bar plot");
15844 };
15845 series[0].values.values = vec![f64::MAX, f64::MAX];
15846 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
15847 super::render_data_labels(
15848 plot,
15849 chart_bounds(),
15850 DispBlanksAs::Gap,
15851 super::GeometryOptions::default(),
15852 &mut fonts,
15853 &mut Vec::new(),
15854 )
15855 .expect("value-only labels do not need a percentage total");
15856 let Plot::Bar { data_labels, .. } = plot else {
15857 panic!("expected bar plot");
15858 };
15859 data_labels.as_mut().unwrap().show_percent = true;
15860 let error = super::render_data_labels(
15861 plot,
15862 chart_bounds(),
15863 DispBlanksAs::Gap,
15864 super::GeometryOptions::default(),
15865 &mut fonts,
15866 &mut Vec::new(),
15867 )
15868 .unwrap_err();
15869 assert!(error.to_string().contains("data-label percentage total"));
15870
15871 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>"#;
15872 let plot_xml = bar_plot("").replace(
15873 r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
15874 &format!(
15875 r#"<q:dLbls>{overrides}<q:showVal val="1"/><q:showPercent val="1"/></q:dLbls>"#
15876 ),
15877 );
15878 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
15879 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
15880 let Plot::Bar { series, .. } = plot else {
15881 panic!("expected bar plot");
15882 };
15883 series[0].values.values = vec![f64::MAX, f64::MAX];
15884 super::render_data_labels(
15885 plot,
15886 chart_bounds(),
15887 DispBlanksAs::Gap,
15888 super::GeometryOptions::default(),
15889 &mut fonts,
15890 &mut Vec::new(),
15891 )
15892 .expect("effective point overrides disable percentage aggregation");
15893 }
15894
15895 #[test]
15896 fn percentage_labels_use_effective_number_format_precision() {
15897 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
15898 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
15899 let Plot::Bar {
15900 series,
15901 data_labels,
15902 ..
15903 } = plot
15904 else {
15905 panic!("expected bar plot");
15906 };
15907 series[0].values.values = vec![1.0, 2.0];
15908 let labels = data_labels.as_mut().unwrap();
15909 labels.show_value = false;
15910 labels.show_percent = true;
15911 labels.number_format = Some(NumberFormat::new("0.00%".to_owned(), false).unwrap());
15912 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
15913 let mut rendered = Vec::new();
15914 super::render_data_labels(
15915 plot,
15916 chart_bounds(),
15917 DispBlanksAs::Gap,
15918 super::GeometryOptions::default(),
15919 &mut fonts,
15920 &mut rendered,
15921 )
15922 .unwrap();
15923 let text: Vec<_> = rendered
15924 .iter()
15925 .filter_map(|element| match element {
15926 PositionedElement::Text(run) => Some(run.text.as_str()),
15927 _ => None,
15928 })
15929 .collect();
15930 assert_eq!(text, vec!["33.33%", "66.67%"]);
15931
15932 let point_override = r#"<q:dLbl><q:idx val="0"/><q:numFmt formatCode="0.00%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbl>"#;
15933 let plot_xml = bar_plot("").replace(
15934 r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
15935 &format!(
15936 r#"<q:dLbls>{point_override}<q:numFmt formatCode="0%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbls>"#
15937 ),
15938 );
15939 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
15940 let plot = &chart.chart.plot_area.plots().unwrap()[0];
15941 let mut rendered = Vec::new();
15942 super::render_data_labels(
15943 plot,
15944 chart_bounds(),
15945 DispBlanksAs::Gap,
15946 super::GeometryOptions::default(),
15947 &mut fonts,
15948 &mut rendered,
15949 )
15950 .unwrap();
15951 let text: Vec<_> = rendered
15952 .iter()
15953 .filter_map(|element| match element {
15954 PositionedElement::Text(run) => Some(run.text.as_str()),
15955 _ => None,
15956 })
15957 .collect();
15958 assert_eq!(text, vec!["33.33%", "67%"]);
15959 }
15960
15961 #[test]
15962 fn core_chart_shells_preserve_unmodelled_children_byte_for_byte() {
15963 let preserved = [
15964 br#"<x:root x:id="1"><x:nested>one & two</x:nested><!--root--></x:root>"#
15965 .as_slice(),
15966 br#"<x:title x:id="2"><x:nested><![CDATA[title]]></x:nested></x:title>"#.as_slice(),
15967 br#"<x:plot x:id="3"><x:nested>plot</x:nested></x:plot>"#.as_slice(),
15968 br#"<x:legend x:id="4"><x:nested>legend</x:nested></x:legend>"#.as_slice(),
15969 br#"<x:extension x:id="5"><x:nested>extension</x:nested></x:extension>"#.as_slice(),
15970 br#"<!--space-comment-->"#.as_slice(),
15971 br#"<?space processing?>"#.as_slice(),
15972 br#"<!--chart-comment-->"#.as_slice(),
15973 br#"<?title processing?>"#.as_slice(),
15974 br#"<!--flag-comment-->"#.as_slice(),
15975 br#"<?flag processing?>"#.as_slice(),
15976 br#"<x:producerShape/>"#.as_slice(),
15977 br#"<x:producerText/>"#.as_slice(),
15978 ];
15979 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>"#;
15980 let parsed = CT_ChartSpace::from_xml(xml).unwrap();
15981 let written = parsed.to_xml().unwrap();
15982 for raw in preserved {
15983 assert!(
15984 written.windows(raw.len()).any(|window| window == raw),
15985 "preserved subtree changed: {}",
15986 String::from_utf8_lossy(raw)
15987 );
15988 }
15989 let written_text = String::from_utf8(written.clone()).unwrap();
15990 assert!(written_text.contains(r#"x:keep="one & two""#));
15991 assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
15992 }
15993
15994 #[test]
15995 fn every_corpus_chart_part_round_trips_structurally() {
15996 let Some(corpus) = require_or_skip_corpus() else {
15997 return;
15998 };
15999 verify_fetched_corpus(&corpus);
16000 let mut chart_parts = 0usize;
16001 let mut decks_with_charts = HashSet::new();
16002 for path in manifest_paths() {
16003 let package = OpcPackage::open(corpus.join(path))
16004 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
16005 for (part, xml) in &package.parts {
16006 if !is_chart_part(part) {
16007 continue;
16008 }
16009 let parsed = CT_ChartSpace::from_xml(xml)
16010 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
16011 let written = parsed
16012 .to_xml()
16013 .unwrap_or_else(|error| panic!("{path} {part}: write failed: {error}"));
16014 let original_preserved = preserved_chartml_regions(xml)
16015 .unwrap_or_else(|error| panic!("{path} {part}: original scan failed: {error}"));
16016 let written_preserved = preserved_chartml_regions(&written)
16017 .unwrap_or_else(|error| panic!("{path} {part}: written scan failed: {error}"));
16018 assert_eq!(
16019 original_preserved.len(),
16020 written_preserved.len(),
16021 "{path} {part}: preserved ChartML region count changed"
16022 );
16023 for (index, (original, written)) in original_preserved
16024 .iter()
16025 .zip(&written_preserved)
16026 .enumerate()
16027 {
16028 assert_eq!(
16029 original.parent_path, written.parent_path,
16030 "{path} {part}: preserved region {index} parent changed"
16031 );
16032 assert_eq!(
16033 original.boundary, written.boundary,
16034 "{path} {part}: preserved region {index} boundary changed"
16035 );
16036 assert_eq!(
16037 original.sibling_order, written.sibling_order,
16038 "{path} {part}: preserved region {index} order changed"
16039 );
16040 assert_eq!(
16041 original.xml, written.xml,
16042 "{path} {part}: preserved region {index} bytes changed"
16043 );
16044 }
16045 let reparsed = CT_ChartSpace::from_xml(&written)
16046 .unwrap_or_else(|error| panic!("{path} {part}: written parse failed: {error}"));
16047 assert_eq!(parsed, reparsed, "{path} {part}: core model changed");
16048 chart_parts += 1;
16049 decks_with_charts.insert(path);
16050 }
16051 }
16052 assert!(
16053 chart_parts > 0,
16054 "the pinned corpus contained no chart parts"
16055 );
16056 eprintln!(
16057 "ChartML corpus gate checked {chart_parts} chart parts across {} decks",
16058 decks_with_charts.len()
16059 );
16060 }
16061
16062 #[derive(Debug, Eq, PartialEq)]
16063 struct PreservedChartRegion {
16064 parent_path: Vec<Vec<u8>>,
16065 boundary: usize,
16066 sibling_order: usize,
16067 xml: Vec<u8>,
16068 }
16069
16070 struct PreservationFrame {
16071 local_name: Vec<u8>,
16072 boundary: usize,
16073 next_sibling: usize,
16074 }
16075
16076 fn preserved_chartml_regions(xml: &[u8]) -> Result<Vec<PreservedChartRegion>, String> {
16077 let mut reader = Reader::from_reader(xml);
16078 let mut buffer = Vec::new();
16079 let mut parents: Vec<PreservationFrame> = Vec::new();
16080 let mut preserved = Vec::new();
16081 loop {
16082 match reader.read_event_into(&mut buffer) {
16083 Ok(Event::Start(element)) => {
16084 let child = local_name(element.name().as_ref()).to_vec();
16085 if parents.is_empty() && child == b"chartSpace" {
16086 parents.push(PreservationFrame {
16087 local_name: child,
16088 boundary: 0,
16089 next_sibling: 0,
16090 });
16091 } else if let Some(parent) = parents.last()
16092 && is_opaque_chartml_child(&parent.local_name, &child)
16093 {
16094 let boundary =
16095 preserved_child_boundary(&parent.local_name, &child, parent.boundary);
16096 let xml = capture_element(&mut reader, &element)
16097 .map_err(|error| error.to_string())?;
16098 record_preserved_region(&mut parents, &mut preserved, boundary, xml);
16099 advance_preservation_boundary(&mut parents, &child);
16100 } else {
16101 advance_preservation_boundary(&mut parents, &child);
16102 if is_chartml_preservation_parent(&child) {
16103 parents.push(PreservationFrame {
16104 local_name: child,
16105 boundary: 0,
16106 next_sibling: 0,
16107 });
16108 } else {
16109 capture_element(&mut reader, &element)
16110 .map_err(|error| error.to_string())?;
16111 }
16112 }
16113 }
16114 Ok(Event::Empty(element)) => {
16115 let name = element.name();
16116 let child = local_name(name.as_ref()).to_vec();
16117 if let Some(parent) = parents.last()
16118 && is_opaque_chartml_child(&parent.local_name, &child)
16119 {
16120 let boundary =
16121 preserved_child_boundary(&parent.local_name, &child, parent.boundary);
16122 let xml =
16123 capture_empty_element(&element).map_err(|error| error.to_string())?;
16124 record_preserved_region(&mut parents, &mut preserved, boundary, xml);
16125 }
16126 advance_preservation_boundary(&mut parents, &child);
16127 }
16128 Ok(
16129 event @ (Event::Comment(_)
16130 | Event::PI(_)
16131 | Event::CData(_)
16132 | Event::GeneralRef(_)),
16133 ) if !parents.is_empty() => {
16134 let boundary = parents.last().expect("preservation parent").boundary;
16135 let xml = capture_event(event).map_err(|error| error.to_string())?;
16136 record_preserved_region(&mut parents, &mut preserved, boundary, xml);
16137 }
16138 Ok(Event::End(_)) => {
16139 parents.pop();
16140 }
16141 Ok(Event::Eof) => break,
16142 Ok(_) => {}
16143 Err(error) => return Err(error.to_string()),
16144 }
16145 buffer.clear();
16146 }
16147 Ok(preserved)
16148 }
16149
16150 fn record_preserved_region(
16151 parents: &mut [PreservationFrame],
16152 preserved: &mut Vec<PreservedChartRegion>,
16153 boundary: usize,
16154 xml: Vec<u8>,
16155 ) {
16156 let sibling_order = parents.last().expect("preservation parent").next_sibling;
16157 parents
16158 .last_mut()
16159 .expect("preservation parent")
16160 .next_sibling += 1;
16161 preserved.push(PreservedChartRegion {
16162 parent_path: parents
16163 .iter()
16164 .map(|parent| parent.local_name.clone())
16165 .collect(),
16166 boundary,
16167 sibling_order,
16168 xml,
16169 });
16170 }
16171
16172 fn advance_preservation_boundary(parents: &mut [PreservationFrame], child: &[u8]) {
16173 let Some(parent) = parents.last_mut() else {
16174 return;
16175 };
16176 parent.boundary = parent
16177 .boundary
16178 .max(preserved_boundary_after(&parent.local_name, child));
16179 }
16180
16181 fn preserved_child_boundary(parent: &[u8], child: &[u8], current: usize) -> usize {
16182 match parent {
16183 b"chartSpace" => match child {
16184 b"date1904" => 0,
16185 b"lang" => 1,
16186 b"roundedCorners" => 2,
16187 b"style" => 3,
16188 b"clrMapOvr" => 4,
16189 b"pivotSource" => 5,
16190 b"protection" => 6,
16191 b"externalData" => 10,
16192 b"printSettings" => 11,
16193 b"userShapes" => 12,
16194 b"extLst" => 13,
16195 _ => current,
16196 },
16197 b"chart" => match child {
16198 b"pivotFmts" => 2,
16199 b"view3D" => 3,
16200 b"floor" => 4,
16201 b"sideWall" => 5,
16202 b"backWall" => 6,
16203 b"showDLblsOverMax" | b"extLst" => 11,
16204 _ => current,
16205 },
16206 _ => current,
16207 }
16208 }
16209
16210 fn preserved_boundary_after(parent: &[u8], child: &[u8]) -> usize {
16211 match parent {
16212 b"chartSpace" => match child {
16213 b"date1904" => 1,
16214 b"lang" => 2,
16215 b"roundedCorners" => 3,
16216 b"style" => 4,
16217 b"clrMapOvr" => 5,
16218 b"pivotSource" => 6,
16219 b"protection" => 7,
16220 b"chart" => 8,
16221 b"spPr" => 9,
16222 b"txPr" => 10,
16223 b"externalData" => 11,
16224 b"printSettings" => 12,
16225 b"userShapes" => 13,
16226 b"extLst" => 14,
16227 _ => 0,
16228 },
16229 b"chart" => match child {
16230 b"title" => 1,
16231 b"autoTitleDeleted" => 2,
16232 b"pivotFmts" => 3,
16233 b"view3D" => 4,
16234 b"floor" => 5,
16235 b"sideWall" => 6,
16236 b"backWall" => 7,
16237 b"plotArea" => 8,
16238 b"legend" => 9,
16239 b"plotVisOnly" => 10,
16240 b"dispBlanksAs" => 11,
16241 b"showDLblsOverMax" | b"extLst" => 11,
16242 _ => 0,
16243 },
16244 b"pieChart" => match child {
16245 b"ser" => 1,
16246 b"dLbls" => 2,
16247 b"firstSliceAng" => 3,
16248 _ => 0,
16249 },
16250 b"doughnutChart" => match child {
16251 b"ser" => 1,
16252 b"dLbls" => 2,
16253 b"firstSliceAng" => 3,
16254 b"holeSize" => 4,
16255 _ => 0,
16256 },
16257 b"areaChart" => match child {
16258 b"grouping" => 1,
16259 b"ser" => 2,
16260 b"dLbls" => 3,
16261 b"axId" => 4,
16262 _ => 0,
16263 },
16264 b"scatterChart" | b"radarChart" => match child {
16265 b"scatterStyle" | b"radarStyle" => 1,
16266 b"ser" => 2,
16267 b"dLbls" => 3,
16268 b"axId" => 4,
16269 _ => 0,
16270 },
16271 _ => 0,
16272 }
16273 }
16274
16275 fn is_opaque_chartml_child(parent: &[u8], child: &[u8]) -> bool {
16276 match parent {
16277 b"chartSpace" => !matches!(child, b"chart" | b"spPr" | b"txPr"),
16278 b"chart" => !matches!(
16279 child,
16280 b"title"
16281 | b"autoTitleDeleted"
16282 | b"plotArea"
16283 | b"legend"
16284 | b"plotVisOnly"
16285 | b"dispBlanksAs"
16286 ),
16287 b"plotArea" => !matches!(
16288 child,
16289 b"barChart"
16290 | b"lineChart"
16291 | b"pieChart"
16292 | b"doughnutChart"
16293 | b"areaChart"
16294 | b"scatterChart"
16295 | b"radarChart"
16296 | b"catAx"
16297 | b"valAx"
16298 | b"dateAx"
16299 | b"serAx"
16300 ),
16301 b"pieChart" => !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng"),
16302 b"doughnutChart" => {
16303 !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng" | b"holeSize")
16304 }
16305 b"areaChart" => !matches!(child, b"grouping" | b"ser" | b"dLbls" | b"axId"),
16306 b"scatterChart" => !matches!(child, b"scatterStyle" | b"ser" | b"dLbls" | b"axId"),
16307 b"radarChart" => !matches!(child, b"radarStyle" | b"ser" | b"dLbls" | b"axId"),
16308 b"title" | b"legend" => true,
16309 _ => false,
16310 }
16311 }
16312
16313 fn is_chartml_preservation_parent(parent: &[u8]) -> bool {
16314 matches!(
16315 parent,
16316 b"chartSpace"
16317 | b"chart"
16318 | b"title"
16319 | b"plotArea"
16320 | b"pieChart"
16321 | b"doughnutChart"
16322 | b"areaChart"
16323 | b"scatterChart"
16324 | b"radarChart"
16325 | b"legend"
16326 | b"autoTitleDeleted"
16327 | b"plotVisOnly"
16328 | b"dispBlanksAs"
16329 )
16330 }
16331
16332 #[test]
16333 fn oxml_chart_is_an_explicit_publication_candidate() {
16334 let manifest = include_str!("../Cargo.toml");
16335 assert!(manifest.contains("name = \"oxml-chart\""));
16336 assert!(manifest.contains("version = \"0.14.1\""));
16337 assert!(manifest.contains("publish = true"));
16338 for dependency in [
16339 "oxml-core.workspace",
16340 "oxml-drawing.workspace",
16341 "quick-xml.workspace",
16342 ] {
16343 assert!(manifest.contains(dependency));
16344 }
16345 assert!(!manifest.contains("rpptx.workspace"));
16346 assert!(!manifest.contains("rdocx.workspace"));
16347 }
16348
16349 fn first_series_xml(xml: &[u8]) -> Option<Vec<u8>> {
16350 let mut reader = Reader::from_reader(xml);
16351 let mut buffer = Vec::new();
16352 loop {
16353 match reader.read_event_into(&mut buffer).ok()? {
16354 Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
16355 return capture_element(&mut reader, &element).ok();
16356 }
16357 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
16358 return capture_empty_element(&element).ok();
16359 }
16360 Event::Eof => return None,
16361 _ => {}
16362 }
16363 buffer.clear();
16364 }
16365 }
16366
16367 fn replace_once(haystack: &[u8], needle: &[u8], replacement: &[u8]) -> Vec<u8> {
16368 let offset = haystack
16369 .windows(needle.len())
16370 .position(|window| window == needle)
16371 .expect("serialized source series occurs in chart part");
16372 assert!(
16373 !haystack[offset + needle.len()..]
16374 .windows(needle.len())
16375 .any(|window| window == needle),
16376 "source series unexpectedly occurs more than once"
16377 );
16378 let mut result = Vec::with_capacity(haystack.len() - needle.len() + replacement.len());
16379 result.extend_from_slice(&haystack[..offset]);
16380 result.extend_from_slice(replacement);
16381 result.extend_from_slice(&haystack[offset + needle.len()..]);
16382 result
16383 }
16384
16385 fn assert_command_version(program: &str, arguments: &[&str], expected: &str) {
16386 let output = Command::new(program)
16387 .args(arguments)
16388 .output()
16389 .unwrap_or_else(|error| panic!("run {program} version check: {error}"));
16390 assert!(
16391 output.status.success(),
16392 "{program} version check failed: {}",
16393 String::from_utf8_lossy(&output.stderr)
16394 );
16395 let stdout = String::from_utf8(output.stdout).expect("version stdout is UTF-8");
16396 let stderr = String::from_utf8(output.stderr).expect("version stderr is UTF-8");
16397 let actual = if stdout.trim().is_empty() {
16398 stderr.trim()
16399 } else {
16400 stdout.trim()
16401 };
16402 assert_eq!(
16403 actual.lines().next(),
16404 Some(expected),
16405 "{program} version drift"
16406 );
16407 }
16408
16409 fn sha256(path: &Path) -> String {
16410 let output = Command::new("shasum")
16411 .args(["-a", "256"])
16412 .arg(path)
16413 .output()
16414 .unwrap_or_else(|error| panic!("{}: run shasum: {error}", path.display()));
16415 assert!(
16416 output.status.success(),
16417 "{}: shasum failed: {}",
16418 path.display(),
16419 String::from_utf8_lossy(&output.stderr)
16420 );
16421 String::from_utf8(output.stdout)
16422 .expect("shasum output is UTF-8")
16423 .split_whitespace()
16424 .next()
16425 .expect("shasum digest")
16426 .to_owned()
16427 }
16428
16429 fn workspace_root() -> PathBuf {
16430 Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
16431 }
16432
16433 fn corpus_dir() -> PathBuf {
16434 std::env::var_os("RDOCX_PPTX_CORPUS_DIR")
16435 .map(PathBuf::from)
16436 .unwrap_or_else(|| workspace_root().join("corpus/pptx"))
16437 }
16438
16439 fn require_or_skip_corpus() -> Option<PathBuf> {
16440 let corpus = corpus_dir();
16441 if corpus.is_dir() {
16442 return Some(corpus);
16443 }
16444 assert_ne!(
16445 std::env::var_os("RDOCX_PPTX_CORPUS_REQUIRED").as_deref(),
16446 Some(std::ffi::OsStr::new("1")),
16447 "the pinned corpus is required but {} does not exist",
16448 corpus.display()
16449 );
16450 eprintln!(
16451 "ChartML corpus gate skipped because {} is absent",
16452 corpus.display()
16453 );
16454 None
16455 }
16456
16457 fn verify_fetched_corpus(corpus: &Path) {
16458 let status = Command::new("python3")
16459 .arg(workspace_root().join("scripts/fetch_pptx_corpus.py"))
16460 .arg("--check")
16461 .env("RDOCX_PPTX_CORPUS_DIR", corpus)
16462 .status()
16463 .expect("run corpus verifier");
16464 assert!(status.success(), "pinned corpus verification failed");
16465 }
16466
16467 fn manifest_paths() -> Vec<&'static str> {
16468 let mut lines = MANIFEST.lines();
16469 assert_eq!(lines.next(), Some("path\tproducer\tsha256\turl"));
16470 let paths: Vec<_> = lines
16471 .map(|line| line.split('\t').next().expect("manifest path"))
16472 .collect();
16473 assert_eq!(paths.len(), EXPECTED_DECKS);
16474 paths
16475 }
16476
16477 fn is_chart_part(part: &str) -> bool {
16478 let Some(stem) = part
16479 .strip_prefix("/ppt/charts/chart")
16480 .and_then(|name| name.strip_suffix(".xml"))
16481 else {
16482 return false;
16483 };
16484 !stem.is_empty() && stem.bytes().all(|byte| byte.is_ascii_digit())
16485 }
16486
16487 fn plot_series(index: u32) -> String {
16488 format!(
16489 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>"#
16490 )
16491 }
16492
16493 fn sparse_category_series() -> &'static str {
16494 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>"#
16495 }
16496
16497 fn sparse_scatter_series() -> &'static str {
16498 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>"#
16499 }
16500
16501 fn chart_with_plot(plot: &str) -> String {
16502 format!(
16503 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>"#
16504 )
16505 }
16506
16507 fn chart_with_optional_axes(plot: &str, axes: bool) -> String {
16508 let axis_xml = if axes {
16509 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>"#
16510 } else {
16511 ""
16512 };
16513 format!(
16514 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>"#
16515 )
16516 }
16517
16518 fn numeric_plot_series(index: u32) -> String {
16519 format!(
16520 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>"#
16521 )
16522 }
16523
16524 fn scatter_without_x(index: u32) -> String {
16525 format!(
16526 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>"#
16527 )
16528 }
16529
16530 fn scatter_without_y(index: u32) -> String {
16531 format!(
16532 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>"#
16533 )
16534 }
16535
16536 fn remaining_plot_fixtures() -> Vec<(&'static str, String, bool)> {
16537 let series = plot_series(0);
16538 let scatter = numeric_plot_series(0);
16539 vec![
16540 (
16541 "pieChart",
16542 format!(
16543 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>"#
16544 ),
16545 false,
16546 ),
16547 (
16548 "doughnutChart",
16549 format!(
16550 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>"#
16551 ),
16552 false,
16553 ),
16554 (
16555 "areaChart",
16556 format!(
16557 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>"#
16558 ),
16559 true,
16560 ),
16561 (
16562 "scatterChart",
16563 format!(
16564 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>"#
16565 ),
16566 true,
16567 ),
16568 (
16569 "radarChart",
16570 format!(
16571 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>"#
16572 ),
16573 true,
16574 ),
16575 ]
16576 }
16577
16578 fn bar_plot(extra: &str) -> String {
16579 format!(
16580 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>"#,
16581 plot_series(0)
16582 )
16583 }
16584
16585 fn line_plot(extra: &str) -> String {
16586 format!(
16587 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>"#,
16588 plot_series(0)
16589 )
16590 }
16591
16592 #[test]
16593 fn bar_and_line_plots_round_trip_and_render() {
16594 for (xml, is_bar) in [
16595 (chart_with_plot(&bar_plot("")), true),
16596 (chart_with_plot(&line_plot("")), false),
16597 ] {
16598 let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16599 let plots = parsed.chart.plot_area.plots().unwrap();
16600 assert_eq!(plots.len(), 1);
16601 assert_eq!(matches!(plots[0], Plot::Bar { .. }), is_bar);
16602 assert_eq!(matches!(plots[0], Plot::Line { .. }), !is_bar);
16603 assert_eq!(parsed.chart.plot_area.axes().unwrap().len(), 2);
16604 let written = parsed.to_xml().unwrap();
16605 let reparsed = CT_ChartSpace::from_xml(&written).unwrap();
16606 assert_eq!(parsed, reparsed);
16607 }
16608 if let Some(corpus) = require_or_skip_corpus() {
16609 verify_bar_and_line_viewer_gate(&corpus);
16610 }
16611 }
16612
16613 #[test]
16614 fn ppm_parser_preserves_whitespace_valued_first_pixels() {
16615 for first_pixel in *b" \n\r\t" {
16616 let mut ppm = b"P6\n1 1\n255\n".to_vec();
16617 ppm.extend_from_slice(&[first_pixel, 0x7f, 0xff]);
16618 let (width, height, pixels) = ppm_pixels(&ppm);
16619 assert_eq!((width, height), (1, 1));
16620 assert_eq!(pixels, [first_pixel, 0x7f, 0xff]);
16621 }
16622
16623 let ppm = b"P6\r\n1 1\r\n255\r\n\x7f\xff";
16624 assert_eq!(ppm_pixels(ppm).2, [b'\n', 0x7f, 0xff]);
16625 }
16626
16627 #[test]
16628 fn bar_and_line_plots_write_fixed_prefixes_in_schema_order() {
16629 for xml in [
16630 chart_with_plot(&bar_plot(r#"<q:extLst><x:bar-tail/></q:extLst>"#)),
16631 chart_with_plot(&line_plot(r#"<q:extLst><x:line-tail/></q:extLst>"#)),
16632 ] {
16633 let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16634 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
16635 assert!(written.contains("<c:barChart") || written.contains("<c:lineChart"));
16636 assert!(!written.contains("<q:barChart"));
16637 assert!(!written.contains("<q:lineChart"));
16638 if written.contains("<c:barChart") {
16639 let positions: Vec<_> = [
16640 "<c:barDir",
16641 "<c:grouping",
16642 "<q:varyColors",
16643 "<c:ser",
16644 "<c:dLbls",
16645 "<c:gapWidth",
16646 "<c:overlap",
16647 "<q:serLines",
16648 "<c:axId",
16649 "<q:extLst",
16650 ]
16651 .iter()
16652 .map(|tag| written.find(tag).unwrap())
16653 .collect();
16654 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
16655 } else {
16656 let positions: Vec<_> = [
16657 "<c:grouping",
16658 "<q:varyColors",
16659 "<c:ser",
16660 "<c:dLbls",
16661 "<q:dropLines",
16662 "<c:marker",
16663 "<c:smooth",
16664 "<c:axId",
16665 "<q:extLst",
16666 ]
16667 .iter()
16668 .map(|tag| written.find(tag).unwrap())
16669 .collect();
16670 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
16671 }
16672 assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
16673 }
16674
16675 for plot in [bar_plot(""), line_plot("")] {
16676 let first = plot_series(0);
16677 let with_series_boundary = plot.replacen(
16678 &first,
16679 &format!(
16680 "{first}<!--between-series--><?series-boundary?>\n {}",
16681 plot_series(1)
16682 ),
16683 1,
16684 );
16685 let with_axis_boundary = with_series_boundary.replacen(
16686 r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
16687 r#"<q:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
16688 <q:axId val="-1884097184"/>"#,
16689 1,
16690 );
16691 let parsed =
16692 CT_ChartSpace::from_xml(chart_with_plot(&with_axis_boundary).as_bytes()).unwrap();
16693 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
16694 assert!(written.contains("</c:ser><!--between-series--><?series-boundary?>\n <c:ser"));
16695 assert!(written.contains(
16696 r#"<c:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
16697 <c:axId val="-1884097184"/>"#
16698 ));
16699 assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
16700 }
16701 }
16702
16703 #[test]
16704 fn malformed_bar_and_line_plots_return_errors_without_panicking() {
16705 let valid_series = plot_series(0);
16706 let cases = [
16707 format!(r#"<q:barChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
16708 format!(r#"<q:barChart><q:barDir val="diagonal"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
16709 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>"#),
16710 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>"#),
16711 format!(r#"<q:lineChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
16712 format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:marker val="maybe"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
16713 r#"<q:lineChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#.to_owned(),
16714 format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:axId val="1"/><q:axId val="1"/></q:lineChart>"#),
16715 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>"#),
16716 format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
16717 ];
16718 for plot in cases {
16719 let xml = chart_with_plot(&plot);
16720 let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
16721 assert!(result.is_ok(), "plot parser panicked for {plot}");
16722 assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
16723 }
16724
16725 let missing_axis = format!(
16726 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>"#,
16727 bar_plot("")
16728 );
16729 assert!(CT_ChartSpace::from_xml(missing_axis.as_bytes()).is_err());
16730
16731 let values =
16732 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
16733 let series = vec![Series::new(0, 0, values)];
16734 let ids = [AxisId::new(1).unwrap(), AxisId::new(2).unwrap()];
16735 let mut invalid =
16736 Plot::bar(BarDirection::Column, BarGrouping::Clustered, series, ids).unwrap();
16737 if let Plot::Bar { gap_width, .. } = &mut invalid {
16738 *gap_width = 501;
16739 }
16740 assert!(invalid.validate().is_err());
16741 }
16742
16743 #[test]
16744 fn unsupported_and_combo_plots_remain_byte_preserved() {
16745 let bar = bar_plot("");
16746 let line = line_plot("");
16747 let combo = chart_with_plot(&format!("<!--before-->{bar}<?between?>{line}<!--after-->"));
16748 let parsed = CT_ChartSpace::from_xml(combo.as_bytes()).unwrap();
16749 assert!(parsed.chart.plot_area.plots().is_err());
16750 let written = parsed.to_xml().unwrap();
16751 for raw in [
16752 bar.as_bytes(),
16753 line.as_bytes(),
16754 br#"<!--before-->"#.as_slice(),
16755 br#"<?between?>"#.as_slice(),
16756 br#"<!--after-->"#.as_slice(),
16757 ] {
16758 assert!(written.windows(raw.len()).any(|window| window == raw));
16759 }
16760
16761 let unsupported = chart_with_plot(
16762 r#"<q:bar3DChart x:keep="three"><q:barDir val="col"/><q:ser><x:opaque/></q:ser></q:bar3DChart>"#,
16763 );
16764 let parsed = CT_ChartSpace::from_xml(unsupported.as_bytes()).unwrap();
16765 assert!(parsed.chart.plot_area.plots().is_err());
16766 assert!(
16767 String::from_utf8(parsed.to_xml().unwrap())
16768 .unwrap()
16769 .contains(r#"<q:bar3DChart x:keep="three">"#)
16770 );
16771 }
16772
16773 #[test]
16774 fn public_plot_edits_preserve_axes_and_unselected_payloads() {
16775 let xml = chart_with_plot(&bar_plot(
16776 r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#,
16777 ));
16778 let mut parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16779 let axes_before = parsed.chart.plot_area.axes().unwrap();
16780 match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
16781 Plot::Bar {
16782 gap_width,
16783 overlap,
16784 series,
16785 ..
16786 } => {
16787 *gap_width = 225;
16788 *overlap = -25;
16789 series[0].index = 7;
16790 series[0].values.values[0] = 9.0;
16791 }
16792 _ => panic!("expected bar plot"),
16793 }
16794 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
16795 assert!(written.contains(r#"<c:gapWidth val="225"/>"#));
16796 assert!(written.contains(r#"<c:overlap val="-25"/>"#));
16797 assert!(written.contains("<c:v>9</c:v>"));
16798 assert!(written.contains("<q:varyColors val=\"1\"/>"));
16799 assert!(written.find("<q:varyColors").unwrap() < written.find("<c:ser").unwrap());
16800 assert!(written.contains("<q:serLines><x:line/></q:serLines>"));
16801 assert!(written.contains(r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#));
16802 let reparsed = CT_ChartSpace::from_xml(written.as_bytes()).unwrap();
16803 assert_eq!(axes_before, reparsed.chart.plot_area.axes().unwrap());
16804
16805 let first = plot_series(0);
16806 let colliding_series =
16807 bar_plot("").replacen(&first, &format!("{first}{}", plot_series(1)), 1);
16808 let mut collision =
16809 CT_ChartSpace::from_xml(chart_with_plot(&colliding_series).as_bytes()).unwrap();
16810 if let Plot::Bar { series, .. } = &mut collision.chart.plot_area.plots_mut().unwrap()[0] {
16811 series[0].index = series[1].index;
16812 series[0].order = series[1].order;
16813 }
16814 let collision = String::from_utf8(collision.to_xml().unwrap()).unwrap();
16815 assert!(collision.find("<q:varyColors").unwrap() < collision.find("<c:ser").unwrap());
16816
16817 let mut inserted = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16818 if let Plot::Bar { series, .. } = &mut inserted.chart.plot_area.plots_mut().unwrap()[0] {
16819 let mut new_series = series[0].clone();
16820 new_series.index = 99;
16821 new_series.order = 99;
16822 series.insert(0, new_series);
16823 }
16824 let inserted = String::from_utf8(inserted.to_xml().unwrap()).unwrap();
16825 assert!(inserted.find("<q:varyColors").unwrap() < inserted.find("<c:ser").unwrap());
16826
16827 let axis_payload = bar_plot("").replacen(
16828 r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
16829 r#"<q:axId val="-1884094432" x:slot="category"/><!--axis-anchor--><q:axId val="-1884097184" x:slot="value"/>"#,
16830 1,
16831 );
16832 let mut swapped =
16833 CT_ChartSpace::from_xml(chart_with_plot(&axis_payload).as_bytes()).unwrap();
16834 if let Plot::Bar { axis_ids, .. } = &mut swapped.chart.plot_area.plots_mut().unwrap()[0] {
16835 axis_ids.swap(0, 1);
16836 }
16837 let swapped = String::from_utf8(swapped.to_xml().unwrap()).unwrap();
16838 let anchor = swapped.find("<!--axis-anchor-->").unwrap();
16839 let value_axis = swapped
16840 .find(r#"<c:axId val="-1884097184" x:slot="value"/>"#)
16841 .unwrap();
16842 let category_axis = swapped
16843 .find(r#"<c:axId val="-1884094432" x:slot="category"/>"#)
16844 .unwrap();
16845 let series_lines = swapped.find("<q:serLines").unwrap();
16846 assert!(series_lines < value_axis && series_lines < category_axis);
16847 assert!(anchor < value_axis && value_axis < category_axis);
16848
16849 let mut replaced = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16850 let Plot::Bar {
16851 series, axis_ids, ..
16852 } = &replaced.chart.plot_area.plots().unwrap()[0]
16853 else {
16854 panic!("expected bar plot");
16855 };
16856 let line = Plot::line(Grouping::Standard, series.clone(), *axis_ids).unwrap();
16857 replaced.chart.plot_area.plots_mut().unwrap()[0] = line;
16858 assert!(replaced.to_xml().is_err());
16859 }
16860
16861 #[test]
16862 fn every_corpus_bar_and_line_plot_round_trips_structurally() {
16863 let Some(corpus) = require_or_skip_corpus() else {
16864 return;
16865 };
16866 verify_fetched_corpus(&corpus);
16867 let mut typed_bar_count = 0usize;
16868 let mut typed_line_count = 0usize;
16869 let mut opaque_bar_count = 0usize;
16870 let mut opaque_line_count = 0usize;
16871 let mut opaque_combo_count = 0usize;
16872 for path in manifest_paths() {
16873 let package = OpcPackage::open(corpus.join(path))
16874 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
16875 for (part, xml) in &package.parts {
16876 if !is_chart_part(part) {
16877 continue;
16878 }
16879 let chart = CT_ChartSpace::from_xml(xml)
16880 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
16881 match chart.chart.plot_area.plots() {
16882 Ok(plots) => {
16883 for plot in plots {
16884 match plot {
16885 Plot::Bar { .. } => typed_bar_count += 1,
16886 Plot::Line { .. } => typed_line_count += 1,
16887 _ => {}
16888 }
16889 }
16890 }
16891 Err(_) => {
16892 let mut preserved_plot_count = 0usize;
16893 for raw in chart.chart.plot_area.raw_children.at(0) {
16894 if let Ok(Some(local)) = super::chart_root_local(
16895 raw,
16896 &chart.chart.plot_area.namespace_bindings,
16897 ) {
16898 match local.as_slice() {
16899 b"barChart" => {
16900 opaque_bar_count += 1;
16901 preserved_plot_count += 1;
16902 }
16903 b"lineChart" => {
16904 opaque_line_count += 1;
16905 preserved_plot_count += 1;
16906 }
16907 _ => {}
16908 }
16909 }
16910 }
16911 if preserved_plot_count > 1 {
16912 opaque_combo_count += 1;
16913 }
16914 }
16915 }
16916 let written = chart
16917 .to_xml()
16918 .unwrap_or_else(|error| panic!("{path} {part}: chart write failed: {error}"));
16919 let reparsed = CT_ChartSpace::from_xml(&written).unwrap_or_else(|error| {
16920 panic!("{path} {part}: written chart parse failed: {error}")
16921 });
16922 assert_eq!(chart, reparsed, "{path} {part}: chart model changed");
16923 }
16924 }
16925 assert_eq!(typed_bar_count, 11, "typed corpus bar coverage changed");
16926 assert_eq!(typed_line_count, 2, "typed corpus line coverage changed");
16927 assert_eq!(opaque_bar_count, 1, "preserved combo bar coverage changed");
16928 assert_eq!(
16929 opaque_line_count, 1,
16930 "preserved combo line coverage changed"
16931 );
16932 assert_eq!(opaque_combo_count, 1, "preserved combo coverage changed");
16933 assert_eq!(typed_bar_count + opaque_bar_count, 12);
16934 assert_eq!(typed_line_count + opaque_line_count, 3);
16935 eprintln!(
16936 "ChartML plot corpus gate checked {typed_bar_count} typed bar, {typed_line_count} typed line, and one preserved bar-line combination"
16937 );
16938 }
16939
16940 fn verify_remaining_plot_viewer_gate(corpus: &Path) {
16941 verify_fetched_corpus(corpus);
16942 assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
16943 assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
16944 let source = corpus.join("bar-chart.pptx");
16945 let source_sha = sha256(&source);
16946 let temp_root = std::env::temp_dir().join(format!(
16947 "oxml-chart-f122-viewer-gate-{}",
16948 std::process::id()
16949 ));
16950 if temp_root.exists() {
16951 fs::remove_dir_all(&temp_root).expect("remove stale F-122 evidence");
16952 }
16953 fs::create_dir_all(&temp_root).expect("create F-122 evidence root");
16954
16955 for (kind, plot, axes) in remaining_plot_fixtures() {
16956 let mut package = OpcPackage::open(&source).expect("open F-122 viewer source");
16957 package.set_part(
16958 "/ppt/charts/chart1.xml",
16959 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes())
16960 .expect("parse F-122 viewer chart")
16961 .to_xml()
16962 .expect("serialize F-122 viewer chart"),
16963 );
16964 let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
16965 package.save(&unbound).expect("save F-122 viewer candidate");
16966 let candidate_sha = sha256(&unbound);
16967 let evidence = temp_root.join(format!("{source_sha}-{candidate_sha}"));
16968 fs::create_dir(&evidence).expect("create SHA-bound F-122 evidence directory");
16969 let candidate = evidence.join(format!("{kind}-candidate.pptx"));
16970 fs::rename(&unbound, &candidate).expect("bind F-122 candidate to SHA");
16971 assert_eq!(sha256(&candidate), candidate_sha);
16972 let render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
16973 let bytes = fs::read(&render).expect("read F-122 viewer PPM");
16974 let (width, height, pixels) = ppm_pixels(&bytes);
16975 let left = width / 5;
16976 let right = width * 4 / 5;
16977 let top = height * 3 / 20;
16978 let bottom = height * 17 / 20;
16979 let mut nonblank = 0usize;
16980 for y in top..bottom {
16981 for x in left..right {
16982 let offset = (y * width + x) * 3;
16983 if pixels[offset..offset + 3]
16984 .iter()
16985 .any(|channel| *channel < 245)
16986 {
16987 nonblank += 1;
16988 }
16989 }
16990 }
16991 const CHART_RECTANGLE_NONBLANK_THRESHOLD: usize = 1_000;
16992 assert!(
16993 nonblank >= CHART_RECTANGLE_NONBLANK_THRESHOLD,
16994 "{kind}: chart rectangle [{left},{top}) to [{right},{bottom}) contains {nonblank} nonblank pixels, below {CHART_RECTANGLE_NONBLANK_THRESHOLD}"
16995 );
16996 eprintln!(
16997 "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}",
16998 sha256(&render)
16999 );
17000 }
17001 fs::remove_dir_all(&temp_root).expect("remove F-122 viewer evidence");
17002 }
17003
17004 fn verify_bar_and_line_viewer_gate(corpus: &Path) {
17005 verify_fetched_corpus(corpus);
17006 assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
17007 assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
17008 let temp_root = std::env::temp_dir().join(format!(
17009 "oxml-chart-f121-viewer-gate-{}",
17010 std::process::id()
17011 ));
17012 if temp_root.exists() {
17013 fs::remove_dir_all(&temp_root).expect("remove stale F-121 evidence");
17014 }
17015 fs::create_dir_all(&temp_root).expect("create F-121 evidence root");
17016
17017 for (kind, deck) in [("bar", "bar-chart.pptx"), ("line", "line-chart.pptx")] {
17018 let source = corpus.join(deck);
17019 let original_sha = sha256(&source);
17020 let mut package = OpcPackage::open(&source)
17021 .unwrap_or_else(|error| panic!("{deck}: open viewer source: {error}"));
17022 let chart_part = "/ppt/charts/chart1.xml";
17023 let original_parts = package.parts.clone();
17024 let parsed = CT_ChartSpace::from_xml(
17025 package
17026 .get_part(chart_part)
17027 .unwrap_or_else(|| panic!("{deck}: missing {chart_part}")),
17028 )
17029 .unwrap_or_else(|error| panic!("{deck}: parse viewer chart: {error}"));
17030 assert!(
17031 parsed.chart.plot_area.plots().is_ok(),
17032 "{deck}: plot is opaque"
17033 );
17034 package.set_part(
17035 chart_part,
17036 parsed
17037 .to_xml()
17038 .unwrap_or_else(|error| panic!("{deck}: serialize viewer chart: {error}")),
17039 );
17040 for (part, bytes) in &original_parts {
17041 if part == chart_part {
17042 assert_ne!(package.parts[part], *bytes, "{deck}: chart did not rewrite");
17043 } else {
17044 assert_eq!(package.parts[part], *bytes, "{deck}: changed part {part}");
17045 }
17046 }
17047
17048 let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
17049 package
17050 .save(&unbound)
17051 .unwrap_or_else(|error| panic!("{deck}: save viewer candidate: {error}"));
17052 let candidate_sha = sha256(&unbound);
17053 let evidence = temp_root.join(format!("{original_sha}-{candidate_sha}"));
17054 fs::create_dir(&evidence).expect("create SHA-bound F-121 evidence directory");
17055 let original = evidence.join(format!("{kind}-original.pptx"));
17056 let candidate = evidence.join(format!("{kind}-candidate.pptx"));
17057 fs::copy(&source, &original).expect("copy SHA-bound viewer original");
17058 fs::rename(&unbound, &candidate).expect("bind viewer candidate to SHA");
17059 assert_eq!(sha256(&original), original_sha);
17060 assert_eq!(sha256(&candidate), candidate_sha);
17061
17062 let original_render = render_deck_to_ppm(&original, &evidence, kind, "original");
17063 let candidate_render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
17064 let original_bytes = fs::read(&original_render).expect("read original PPM");
17065 let candidate_bytes = fs::read(&candidate_render).expect("read candidate PPM");
17066 let normalized_mae = normalized_ppm_mae(&original_bytes, &candidate_bytes);
17067 assert!(
17068 normalized_mae <= PLOT_RENDER_NORMALIZED_MAE_THRESHOLD,
17069 "{deck}: normalized RGB MAE {normalized_mae:.8} exceeds {:.8}",
17070 PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
17071 );
17072 eprintln!(
17073 "F-121 {kind} viewer gate original deck {original_sha}, candidate deck {candidate_sha}, original render {}, candidate render {}, normalized RGB MAE {normalized_mae:.8} <= {:.8}",
17074 sha256(&original_render),
17075 sha256(&candidate_render),
17076 PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
17077 );
17078 }
17079 fs::remove_dir_all(&temp_root).expect("remove F-121 viewer evidence");
17080 }
17081
17082 fn render_deck_to_ppm(deck: &Path, evidence: &Path, kind: &str, side: &str) -> PathBuf {
17083 let profile = std::env::temp_dir().join(format!(
17084 "oxml-chart-f121-{kind}-{side}-profile-{}",
17085 std::process::id()
17086 ));
17087 if profile.exists() {
17088 fs::remove_dir_all(&profile).expect("remove stale F-121 viewer profile");
17089 }
17090 let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
17091 let conversion = Command::new("soffice")
17092 .args([
17093 "--headless",
17094 &profile_argument,
17095 "--convert-to",
17096 "pdf:impress_pdf_Export",
17097 "--outdir",
17098 ])
17099 .arg(evidence)
17100 .arg(deck)
17101 .output()
17102 .expect("run pinned LibreOffice F-121 viewer gate");
17103 assert!(
17104 conversion.status.success(),
17105 "{}: LibreOffice conversion failed: {}",
17106 deck.display(),
17107 String::from_utf8_lossy(&conversion.stderr)
17108 );
17109 let pdf = deck.with_extension("pdf");
17110 assert!(
17111 pdf.is_file(),
17112 "LibreOffice did not create {}",
17113 pdf.display()
17114 );
17115 let prefix = evidence.join(format!("{kind}-{side}"));
17116 let raster = Command::new("pdftoppm")
17117 .args(["-f", "1", "-singlefile", "-r", "150"])
17118 .arg(&pdf)
17119 .arg(&prefix)
17120 .output()
17121 .expect("run pinned Poppler F-121 raster gate");
17122 assert!(
17123 raster.status.success(),
17124 "{}: pdftoppm failed: {}",
17125 pdf.display(),
17126 String::from_utf8_lossy(&raster.stderr)
17127 );
17128 if profile.exists() {
17129 fs::remove_dir_all(&profile).expect("remove F-121 viewer profile");
17130 }
17131 prefix.with_extension("ppm")
17132 }
17133
17134 fn normalized_ppm_mae(left: &[u8], right: &[u8]) -> f64 {
17135 let (left_width, left_height, left_pixels) = ppm_pixels(left);
17136 let (right_width, right_height, right_pixels) = ppm_pixels(right);
17137 assert_eq!((left_width, left_height), (right_width, right_height));
17138 assert_eq!(left_pixels.len(), right_pixels.len());
17139 let difference: u64 = left_pixels
17140 .iter()
17141 .zip(right_pixels)
17142 .map(|(left, right)| u64::from(left.abs_diff(*right)))
17143 .sum();
17144 difference as f64 / (left_pixels.len() as f64 * 255.0)
17145 }
17146
17147 fn ppm_pixels(bytes: &[u8]) -> (usize, usize, &[u8]) {
17148 let mut cursor = 0usize;
17149 let mut tokens = Vec::new();
17150 while tokens.len() < 4 {
17151 while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
17152 cursor += 1;
17153 }
17154 if cursor < bytes.len() && bytes[cursor] == b'#' {
17155 while cursor < bytes.len() && bytes[cursor] != b'\n' {
17156 cursor += 1;
17157 }
17158 continue;
17159 }
17160 let start = cursor;
17161 while cursor < bytes.len() && !bytes[cursor].is_ascii_whitespace() {
17162 cursor += 1;
17163 }
17164 tokens.push(std::str::from_utf8(&bytes[start..cursor]).expect("PPM header UTF-8"));
17165 }
17166 assert_eq!(tokens[0], "P6");
17167 let width = tokens[1].parse::<usize>().expect("PPM width");
17168 let height = tokens[2].parse::<usize>().expect("PPM height");
17169 assert_eq!(tokens[3], "255");
17170 assert!(
17171 cursor < bytes.len() && bytes[cursor].is_ascii_whitespace(),
17172 "PPM header must end with whitespace"
17173 );
17174 cursor += 1;
17175 let pixels = &bytes[cursor..];
17176 assert_eq!(pixels.len(), width * height * 3, "PPM pixel length");
17177 (width, height, pixels)
17178 }
17179 #[test]
17180 fn fx215_existing_chart_data_projection() {
17181 let data = ChartData {
17182 categories: vec!["1".into(), "2".into()],
17183 series: vec![("Revenue & margin".into(), vec![1.25, 2.5])],
17184 number_format: Some("0.00".into()),
17185 ..ChartData::default()
17186 };
17187 for kind in [
17188 ChartKind::Bar,
17189 ChartKind::Line,
17190 ChartKind::Pie,
17191 ChartKind::Doughnut,
17192 ChartKind::Area,
17193 ChartKind::Scatter,
17194 ChartKind::Radar,
17195 ] {
17196 let (xml, _) = authored_chart_parts(kind, &data, "rIdWorkbook").unwrap();
17197 let original = xml.clone();
17198 let (actual_kind, actual) = read_chart_data(&xml).unwrap();
17199 assert_eq!(actual_kind, kind);
17200 assert_eq!(actual.categories, data.categories);
17201 assert_eq!(actual.series, data.series);
17202 assert_eq!(actual.number_format, data.number_format);
17203 assert_eq!(xml, original);
17204 assert_eq!(
17205 chart_workbook_relationship_id(&xml).unwrap().as_deref(),
17206 Some("rIdWorkbook")
17207 );
17208 }
17209 }
17210
17211 #[test]
17212 fn fx215_workbook_relationship_ignores_foreign_id_and_accepts_alias() {
17213 let data = ChartData {
17214 categories: vec!["One".into()],
17215 series: vec![("Values".into(), vec![2.0])],
17216 ..ChartData::default()
17217 };
17218 let (xml, _) = authored_chart_parts(ChartKind::Bar, &data, "rIdWorkbook").unwrap();
17219 let xml = String::from_utf8(xml)
17220 .unwrap()
17221 .replace("r:id=", "rr:id=")
17222 .replace(
17223 "<c:externalData",
17224 &format!(
17225 "<c:externalData xmlns:rr=\"{R_NS}\" xmlns:f=\"urn:foreign\" f:id=\"wrong\""
17226 ),
17227 );
17228 assert_eq!(
17229 chart_workbook_relationship_id(xml.as_bytes())
17230 .unwrap()
17231 .as_deref(),
17232 Some("rIdWorkbook")
17233 );
17234 let foreign = xml.replace(&format!("xmlns:rr=\"{R_NS}\""), "xmlns:rr=\"urn:foreign\"");
17235 assert!(chart_workbook_relationship_id(foreign.as_bytes()).is_err());
17236 }
17237
17238 #[test]
17239 fn fx215_replace_data_updates_cache_formula_and_workbook() {
17240 let original = ChartData {
17241 categories: vec!["North".into(), "South".into()],
17242 series: vec![
17243 ("Revenue".into(), vec![1.0, 2.0]),
17244 ("Cost".into(), vec![0.5, 1.0]),
17245 ],
17246 ..ChartData::default()
17247 };
17248 for kind in [ChartKind::Bar, ChartKind::Line] {
17249 let (xml, workbook) = authored_chart_parts(kind, &original, "rIdWorkbook").unwrap();
17250 for series in [
17251 vec![("New revenue".into(), vec![8.0, 9.0, 10.0])],
17252 vec![
17253 ("New revenue".into(), vec![8.0, 9.0, 10.0]),
17254 ("New cost".into(), vec![1.0, 2.0, 3.0]),
17255 ("Profit".into(), vec![7.0, 7.0, 7.0]),
17256 ],
17257 ] {
17258 let data = ChartData {
17259 categories: vec!["East".into(), "West".into(), "Central".into()],
17260 series,
17261 ..ChartData::default()
17262 };
17263 let (chart, book) = replace_chart_data(&xml, &workbook, &data).unwrap();
17264 assert_eq!(read_chart_data(&chart).unwrap().1.series, data.series);
17265 assert_eq!(
17266 read_chart_data(&chart).unwrap().1.categories,
17267 data.categories
17268 );
17269 let package = OpcPackage::from_reader(std::io::Cursor::new(book)).unwrap();
17270 let sheet = String::from_utf8(
17271 package
17272 .get_part("/xl/worksheets/sheet1.xml")
17273 .unwrap()
17274 .to_vec(),
17275 )
17276 .unwrap();
17277 assert!(sheet.contains("<v>8</v>"));
17278 assert!(sheet.contains("<v>10</v>"));
17279 assert!(sheet.contains("East"));
17280 }
17281 }
17282 }
17283
17284 #[test]
17285 fn fx215_surviving_series_and_workbook_opaque_payload() {
17286 let original = ChartData {
17287 categories: vec!["North".into(), "South".into()],
17288 series: vec![("Revenue".into(), vec![1.0, 2.0])],
17289 ..ChartData::default()
17290 };
17291 let (xml, workbook) =
17292 authored_chart_parts(ChartKind::Bar, &original, "rIdWorkbook").unwrap();
17293 let source=String::from_utf8(xml).unwrap().replace("<c:chartSpace ","<c:chartSpace xmlns:f=\"urn:foreign\" f:keep='root' ").replace("<c:ser ","<c:ser f:keep='series' ").replace("<c:idx", "<f:opaque data='exact'/><c:idx").replace("</c:ser>","<c:extLst><c:ext uri='producer'><f:payload value='retain'/></c:ext></c:extLst></c:ser>");
17294 assert!(source.contains("f:keep='series'"));
17295 assert!(source.contains("<f:opaque data='exact'/>"));
17296 let data = ChartData {
17297 categories: vec!["West".into()],
17298 series: vec![("Changed".into(), vec![9.0])],
17299 ..ChartData::default()
17300 };
17301 let (changed, _) = replace_chart_data(source.as_bytes(), &workbook, &data).unwrap();
17302 let changed = String::from_utf8(changed).unwrap();
17303 for exact in [
17304 "f:keep='root'",
17305 "f:keep='series'",
17306 "<f:opaque data='exact'/>",
17307 "<c:extLst><c:ext uri='producer'><f:payload value='retain'/></c:ext></c:extLst>",
17308 ] {
17309 assert!(changed.contains(exact), "{exact}");
17310 }
17311 let space = CT_ChartSpace::from_xml(changed.as_bytes()).unwrap();
17312 assert_eq!(space.chart.plot_area.series().unwrap()[0].index, 0);
17313 let no_op = replace_chart_data(source.as_bytes(), &workbook, &original).unwrap();
17314 assert_eq!(no_op.0, source.as_bytes());
17315 assert_eq!(no_op.1, workbook);
17316 }
17317
17318 #[test]
17319 fn fx215_all_seven_families_replace_data_without_changing_kind() {
17320 let original = ChartData {
17321 categories: vec!["1".into(), "2".into()],
17322 series: vec![("Old".into(), vec![1.0, 2.0])],
17323 ..ChartData::default()
17324 };
17325 let data = ChartData {
17326 categories: vec!["3".into(), "4".into(), "5".into()],
17327 series: vec![("New".into(), vec![0.25, 0.5, 0.75])],
17328 number_format: Some("0.0%".into()),
17329 ..ChartData::default()
17330 };
17331 for kind in [
17332 ChartKind::Bar,
17333 ChartKind::Line,
17334 ChartKind::Pie,
17335 ChartKind::Doughnut,
17336 ChartKind::Area,
17337 ChartKind::Scatter,
17338 ChartKind::Radar,
17339 ] {
17340 let (chart, book) = authored_chart_parts(kind, &original, "rIdWorkbook").unwrap();
17341 let (updated, workbook) = replace_chart_data(&chart, &book, &data).unwrap();
17342 let (actual, projected) = read_chart_data(&updated).unwrap();
17343 assert_eq!(actual, kind);
17344 assert_eq!(projected.categories, data.categories);
17345 assert_eq!(projected.series, data.series);
17346 assert_eq!(projected.number_format, data.number_format);
17347 let package = OpcPackage::from_reader(std::io::Cursor::new(workbook)).unwrap();
17348 let styles =
17349 String::from_utf8(package.get_part("/xl/styles.xml").unwrap().to_vec()).unwrap();
17350 assert!(styles.contains("formatCode=\"0.0%\""));
17351 }
17352 }
17353 #[test]
17354 fn fx215_qualified_corpus_replacement_cardinalities() {
17355 let corpus = corpus_dir();
17356 assert!(corpus.is_dir(), "F-X215 required corpus is missing");
17357 for deck in ["bar-chart.pptx", "line-chart.pptx", "chart-picture-bg.pptx"] {
17358 let path = corpus.join(deck);
17359 let expected = MANIFEST
17360 .lines()
17361 .find(|line| line.starts_with(&format!("{deck}\t")))
17362 .unwrap()
17363 .split('\t')
17364 .nth(2)
17365 .unwrap();
17366 assert_eq!(sha256(&path), expected);
17367 let package = OpcPackage::open(&path).unwrap();
17368 let chart_part = "/ppt/charts/chart1.xml";
17369 let chart = package.get_part(chart_part).unwrap();
17370 let (kind, original) = read_chart_data(chart).unwrap();
17371 let workbook_id = chart_workbook_relationship_id(chart).unwrap().unwrap();
17372 let rel = package
17373 .get_part_rels(chart_part)
17374 .unwrap()
17375 .items
17376 .iter()
17377 .find(|rel| rel.id == workbook_id)
17378 .unwrap();
17379 assert_eq!(rel.rel_type, oxml_opc::relationship::rel_types::PACKAGE);
17380 assert_ne!(rel.target_mode.as_deref(), Some("External"));
17381 let workbook_part = OpcPackage::resolve_rel_target(chart_part, &rel.target);
17382 let workbook = package.get_part(&workbook_part).unwrap();
17383 let mut variants = Vec::new();
17384 if original.series.len() > 1 {
17385 let mut fewer = original.clone();
17386 fewer.series.pop();
17387 variants.push(("fewer", fewer));
17388 }
17389 let mut more = original.clone();
17390 more.series.push((
17391 "Added exact values".into(),
17392 (0..more.categories.len())
17393 .map(|index| 100.0 + index as f64)
17394 .collect(),
17395 ));
17396 variants.push(("more", more));
17397 let mut categories = original.clone();
17398 categories.categories.push("Added category".into());
17399 for (_, values) in &mut categories.series {
17400 values.push(777.0);
17401 }
17402 variants.push(("categories", categories));
17403 for (label, data) in variants {
17404 let (changed, book) = replace_chart_data(chart, workbook, &data)
17405 .unwrap_or_else(|error| panic!("{deck}/{label}: {error}"));
17406 let (actual, projection) = read_chart_data(&changed).unwrap();
17407 assert_eq!(actual, kind);
17408 assert_eq!(projection.categories, data.categories);
17409 assert_eq!(projection.series, data.series);
17410 let mut candidate = package.clone();
17411 candidate.set_part(chart_part, changed);
17412 candidate.set_part(&workbook_part, book);
17413 let mut bytes = std::io::Cursor::new(Vec::new());
17414 candidate.write_to(&mut bytes).unwrap();
17415 if let Some(directory) = std::env::var_os("RDOCX_FX215_CANDIDATES_DIR") {
17416 let directory = PathBuf::from(directory);
17417 fs::create_dir_all(&directory).unwrap();
17418 fs::write(
17419 directory.join(format!("{deck}-{label}.pptx")),
17420 bytes.get_ref(),
17421 )
17422 .unwrap();
17423 }
17424 let reopened =
17425 OpcPackage::from_reader(std::io::Cursor::new(bytes.into_inner())).unwrap();
17426 for (part, source) in &package.parts {
17427 if part != chart_part && part != &workbook_part {
17428 assert_eq!(
17429 reopened.get_part(part).unwrap(),
17430 source,
17431 "{deck}/{label}: {part}"
17432 );
17433 }
17434 }
17435 for (owner, relationships) in &package.part_rels {
17436 assert_eq!(
17437 reopened.get_part_rels(owner).unwrap().to_xml().unwrap(),
17438 relationships.to_xml().unwrap()
17439 );
17440 }
17441 let old_book = OpcPackage::from_reader(std::io::Cursor::new(workbook)).unwrap();
17442 let new_book = OpcPackage::from_reader(std::io::Cursor::new(
17443 reopened.get_part(&workbook_part).unwrap(),
17444 ))
17445 .unwrap();
17446 for (part, source) in &old_book.parts {
17447 if part != "/xl/worksheets/sheet1.xml" && part != "/xl/styles.xml" {
17448 assert_eq!(
17449 new_book.get_part(part).unwrap(),
17450 source,
17451 "{deck}/{label}: workbook {part}"
17452 );
17453 }
17454 }
17455 eprintln!("F-X215 corpus {deck}/{label} qualified cache and workbook candidate");
17456 }
17457 }
17458 }
17459 #[test]
17460 fn fx215_sparse_cache_projection_retains_missing_values() {
17461 let plot = format!(
17462 r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:barChart>"#,
17463 sparse_category_series()
17464 );
17465 let xml = chart_with_plot(&plot);
17466 let (_, data) = read_chart_data(xml.as_bytes()).unwrap();
17467 assert_eq!(data.categories, vec!["North", "", "West"]);
17468 assert_eq!(data.series[0].1[0], 1.0);
17469 assert!(data.series[0].1[1].is_nan());
17470 assert_eq!(data.series[0].1[2], 2.0);
17471 }
17472
17473 #[test]
17474 fn fx215_invalid_data_and_external_formula_preserve_inputs() {
17475 let original = ChartData {
17476 categories: vec!["North".into(), "South".into()],
17477 series: vec![("Revenue".into(), vec![1.0, 2.0])],
17478 ..ChartData::default()
17479 };
17480 let (xml, book) = authored_chart_parts(ChartKind::Bar, &original, "rIdWorkbook").unwrap();
17481 let chart_source = xml.clone();
17482 let book_source = book.clone();
17483 let invalid = ChartData {
17484 categories: original.categories.clone(),
17485 series: vec![("Bad".into(), vec![f64::NAN, 1.0])],
17486 ..ChartData::default()
17487 };
17488 assert!(replace_chart_data(&xml, &book, &invalid).is_err());
17489 assert_eq!(xml, chart_source);
17490 assert_eq!(book, book_source);
17491 let external = String::from_utf8(xml.clone())
17492 .unwrap()
17493 .replace("Sheet1!", "'[1]Sheet1'!");
17494 let changed = ChartData {
17495 categories: original.categories.clone(),
17496 series: vec![("New".into(), vec![8.0, 9.0])],
17497 ..ChartData::default()
17498 };
17499 let error = replace_chart_data(external.as_bytes(), &book, &changed).unwrap_err();
17500 assert!(error.to_string().contains("external workbook formula"));
17501 assert_eq!(book, book_source);
17502 }
17503}