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
340pub fn read_chart_type(chart_xml: &[u8]) -> Result<Option<i64>> {
344 oxml_core::xml::validate_strict_xml_1_0(chart_xml)
345 .map_err(|error| invalid_authoring_value(format!("invalid chart XML: {error:?}")))?;
346 let nodes = chart_source_nodes(chart_xml)?;
347 if !nodes
348 .first()
349 .is_some_and(|node| node.qualified && node.local == b"chartSpace")
350 || nodes.iter().filter(|node| node.parent.is_none()).count() != 1
351 {
352 return Err(invalid_authoring_value("owned chartSpace required"));
353 }
354 let chart = chart_child_node(&nodes, 0, b"chart")?;
355 let area = chart_child_node(&nodes, chart, b"plotArea")?;
356 let mut plots = nodes.iter().enumerate().filter(|(_, node)| {
357 node.parent == Some(area) && node.qualified && node.local.ends_with(b"Chart")
358 });
359 let Some((source_plot, plot)) = plots.next() else {
360 return Ok(None);
361 };
362 if plots.next().is_some() {
363 return Ok(None);
364 }
365 let mut children = vec![Vec::new(); nodes.len()];
368 for (index, node) in nodes.iter().enumerate() {
369 if let Some(parent) = node.parent {
370 children[parent].push(index);
371 }
372 }
373 let mut remaining_work = nodes.len().saturating_mul(4);
374 let mut marker_none = false;
375 let mut exploded = false;
376 for (index, _) in nodes.iter().enumerate().filter(|(_, node)| {
377 node.parent == Some(source_plot) && node.qualified && node.local == b"ser"
378 }) {
379 if let Some(marker) =
380 chart_optional_child_node(&nodes, &children, index, b"marker", &mut remaining_work)?
381 && let Some(symbol) = chart_optional_child_node(
382 &nodes,
383 &children,
384 marker,
385 b"symbol",
386 &mut remaining_work,
387 )?
388 {
389 let symbol = chart_type_scalar(chart_xml, &nodes[symbol])?;
390 if !matches!(
391 symbol.as_str(),
392 "auto"
393 | "circle"
394 | "dash"
395 | "diamond"
396 | "dot"
397 | "none"
398 | "picture"
399 | "plus"
400 | "square"
401 | "star"
402 | "triangle"
403 | "x"
404 ) {
405 return Err(invalid_authoring_value("invalid marker symbol"));
406 }
407 marker_none |= symbol == "none";
408 }
409 if let Some(explosion) =
410 chart_optional_child_node(&nodes, &children, index, b"explosion", &mut remaining_work)?
411 {
412 chart_type_scalar(chart_xml, &nodes[explosion])?
413 .parse::<u32>()
414 .map_err(|_| invalid_authoring_value("invalid series explosion"))?;
415 exploded = true;
416 }
417 }
418 let mut scalar = |local: &[u8]| -> Result<Option<String>> {
419 chart_optional_child_node(&nodes, &children, source_plot, local, &mut remaining_work)?
420 .map(|index| chart_type_scalar(chart_xml, &nodes[index]))
421 .transpose()
422 };
423 Ok(match plot.local.as_slice() {
424 b"barChart" => {
425 let direction =
426 scalar(b"barDir")?.ok_or_else(|| ChartError::MissingElement("c:barDir".into()))?;
427 let direction = BarDirection::parse(&direction)
428 .ok_or_else(|| invalid_authoring_value("invalid bar direction"))?;
429 let grouping = scalar(b"grouping")?.unwrap_or_else(|| "standard".into());
430 let grouping = BarGrouping::parse(&grouping)
431 .ok_or_else(|| invalid_authoring_value("invalid bar grouping"))?;
432 match (direction, grouping) {
433 (BarDirection::Bar, BarGrouping::Clustered) => Some(57),
434 (BarDirection::Bar, BarGrouping::Stacked) => Some(58),
435 (BarDirection::Bar, BarGrouping::PercentStacked) => Some(59),
436 (BarDirection::Column, BarGrouping::Clustered) => Some(51),
437 (BarDirection::Column, BarGrouping::Stacked) => Some(52),
438 (BarDirection::Column, BarGrouping::PercentStacked) => Some(53),
439 (_, BarGrouping::Standard) => None,
440 }
441 }
442 b"lineChart" | b"areaChart" => {
443 let grouping = scalar(b"grouping")?.unwrap_or_else(|| "standard".into());
444 let grouping = Grouping::parse(&grouping)
445 .ok_or_else(|| invalid_authoring_value("invalid chart grouping"))?;
446 Some(if plot.local == b"areaChart" {
447 match grouping {
448 Grouping::Standard => 1,
449 Grouping::Stacked => 76,
450 Grouping::PercentStacked => 77,
451 }
452 } else {
453 match (grouping, marker_none) {
454 (Grouping::Standard, true) => 4,
455 (Grouping::Stacked, true) => 63,
456 (Grouping::PercentStacked, true) => 64,
457 (Grouping::Standard, false) => 65,
458 (Grouping::Stacked, false) => 66,
459 (Grouping::PercentStacked, false) => 67,
460 }
461 })
462 }
463 b"pieChart" => Some(if exploded { 69 } else { 5 }),
464 b"doughnutChart" => Some(if exploded { 80 } else { -4120 }),
465 b"scatterChart" => {
466 let value = scalar(b"scatterStyle")?
467 .ok_or_else(|| ChartError::MissingElement("c:scatterStyle".into()))?;
468 let style = ScatterStyle::parse(&value)
469 .ok_or_else(|| invalid_authoring_value("invalid scatter style"))?;
470 (style == ScatterStyle::LineMarker && !marker_none).then_some(74)
471 }
472 b"radarChart" => {
473 let value = scalar(b"radarStyle")?
474 .ok_or_else(|| ChartError::MissingElement("c:radarStyle".into()))?;
475 let style = RadarStyle::parse(&value)
476 .ok_or_else(|| invalid_authoring_value("invalid radar style"))?;
477 (style == RadarStyle::Standard && !marker_none).then_some(81)
478 }
479 _ => None,
480 })
481}
482
483fn chart_optional_child_node(
484 nodes: &[ChartSourceNode],
485 children: &[Vec<usize>],
486 parent: usize,
487 local: &[u8],
488 remaining_work: &mut usize,
489) -> Result<Option<usize>> {
490 let mut found = None;
491 for &index in &children[parent] {
492 *remaining_work = remaining_work
493 .checked_sub(1)
494 .ok_or_else(|| invalid_authoring_value("chart metadata work limit"))?;
495 let node = &nodes[index];
496 if node.qualified && node.local == local && found.replace(index).is_some() {
497 return Err(ChartError::DuplicateElement(format!(
498 "c:{}",
499 String::from_utf8_lossy(local)
500 )));
501 }
502 }
503 Ok(found)
504}
505
506fn chart_type_scalar(xml: &[u8], node: &ChartSourceNode) -> Result<String> {
507 let mut reader = Reader::from_reader(&xml[node.start..node.end]);
508 let value = match reader.read_event().map_err(OxmlError::from)? {
509 Event::Start(element) | Event::Empty(element) => attribute_value(&element, b"val")?
510 .ok_or_else(|| invalid_authoring_value("chart type scalar requires val"))?,
511 _ => return Err(invalid_authoring_value("chart type scalar root")),
512 };
513 loop {
514 match reader.read_event().map_err(OxmlError::from)? {
515 Event::End(_) | Event::Comment(_) | Event::PI(_) => {}
516 Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
517 Event::Eof => return Ok(value),
518 _ => {
519 return Err(invalid_authoring_value(
520 "chart type scalar has non-scalar content",
521 ));
522 }
523 }
524 }
525}
526
527fn chart_dense_numbers(data: &NumericData) -> Result<Vec<f64>> {
528 let (count, indexes) = cache_layout(&data.markup, data.values.len())?;
529 let count = chart_projection_count(count)?;
530 let mut values = vec![f64::NAN; count];
531 for (index, value) in indexes.into_iter().zip(&data.values) {
532 values[index as usize] = *value;
533 }
534 Ok(values)
535}
536
537fn chart_projection_count(count: u32) -> Result<usize> {
538 if count > 1_048_576 {
539 return Err(invalid_attribute("ptCount", "val", count.to_string()));
540 }
541 Ok(count as usize)
542}
543
544fn chart_category_labels(series: &Series) -> Result<Vec<String>> {
545 let (markup, labels) = match &series.categories {
546 Some(AxisData::String(data)) => (&data.markup, data.values.clone()),
547 Some(AxisData::Numeric(data)) => (
548 &data.markup,
549 data.values.iter().map(|value| value.to_string()).collect(),
550 ),
551 None if series.opaque_categories => {
552 return Err(invalid_authoring_value(
553 "unsupported literal or multilevel category projection",
554 ));
555 }
556 None => return Ok(Vec::new()),
557 };
558 let (count, indexes) = cache_layout(markup, labels.len())?;
559 let mut values = vec![String::new(); chart_projection_count(count)?];
560 for (index, value) in indexes.into_iter().zip(labels) {
561 values[index as usize] = value;
562 }
563 Ok(values)
564}
565
566fn chart_literal_name(series: &Series) -> Result<String> {
567 for raw in series.raw_children.at(2) {
568 let inherited = series
569 .namespace_declarations
570 .iter()
571 .filter_map(|(name, value)| {
572 let prefix = if name == "xmlns" {
573 ""
574 } else {
575 name.strip_prefix("xmlns:")?
576 };
577 Some((prefix.as_bytes().to_vec(), value.clone()))
578 })
579 .collect::<NamespaceBindings>();
580 let mut reader = Reader::from_reader(raw);
581 let mut buffer = Vec::new();
582 let mut namespaces = inherited;
583 let mut depth = 0usize;
584 loop {
585 match reader
586 .read_event_into(&mut buffer)
587 .map_err(OxmlError::from)?
588 {
589 Event::Start(element) => {
590 if depth == 0 {
591 if chart_child_local(&element, &namespaces)?.as_deref() != Some(b"tx") {
592 break;
593 }
594 namespaces = chart_bindings(&namespaces, &element)?;
595 }
596 if depth == 1
597 && chart_child_local(&element, &namespaces)?.as_deref() == Some(b"v")
598 {
599 let value = capture_element(&mut reader, &element)?;
600 return Ok(parse_text_element(&value, b"v", &namespaces)?.0);
601 }
602 depth += 1;
603 }
604 Event::End(_) => {
605 depth = depth.saturating_sub(1);
606 }
607 Event::Eof => break,
608 _ => {}
609 }
610 buffer.clear();
611 }
612 }
613 Err(invalid_authoring_value(
614 "unsupported series name projection",
615 ))
616}
617
618pub fn chart_workbook_relationship_id(chart_xml: &[u8]) -> Result<Option<String>> {
620 use quick_xml::name::ResolveResult;
621 use quick_xml::reader::NsReader;
622 CT_ChartSpace::from_xml(chart_xml)?;
623 let mut reader = NsReader::from_reader(chart_xml);
624 let mut buffer = Vec::new();
625 let mut depth = 0usize;
626 let mut result = None;
627 loop {
628 let (namespace, event) = reader
629 .read_resolved_event_into(&mut buffer)
630 .map_err(OxmlError::from)?;
631 let empty = matches!(&event, Event::Empty(_));
632 match (namespace, event) {
633 (namespace, Event::Start(element) | Event::Empty(element)) => {
634 if depth == 1
635 && matches!(namespace, ResolveResult::Bound(value) if value.as_ref() == C_NS.as_bytes())
636 && element.local_name().as_ref() == b"externalData"
637 {
638 if result.is_some() {
639 return Err(ChartError::DuplicateElement("c:externalData".into()));
640 }
641 for attribute in element.attributes() {
642 let attribute = attribute.map_err(OxmlError::from)?;
643 let (namespace, local) = reader.resolver().resolve_attribute(attribute.key);
644 if matches!(namespace, ResolveResult::Bound(value) if value.as_ref() == R_NS.as_bytes())
645 && local.as_ref() == b"id"
646 {
647 result = Some(
648 attribute
649 .decoded_and_normalized_value(
650 XmlVersion::Implicit1_0,
651 reader.decoder(),
652 )
653 .map_err(OxmlError::from)?
654 .into_owned(),
655 );
656 }
657 }
658 if result.is_none() {
659 return Err(ChartError::MissingElement("c:externalData/@r:id".into()));
660 }
661 }
662 if !empty {
663 depth += 1;
664 }
665 }
666 (_, Event::End(_)) => {
667 depth = depth.saturating_sub(1);
668 }
669 (_, Event::DocType(_)) => {
670 return Err(invalid_authoring_value(
671 "DOCTYPE is unsupported in chart data",
672 ));
673 }
674 (_, Event::Eof) => break,
675 _ => {}
676 }
677 buffer.clear();
678 }
679 Ok(result)
680}
681
682#[derive(Debug)]
683struct ChartSourceNode {
684 local: Vec<u8>,
685 qualified: bool,
686 parent: Option<usize>,
687 start: usize,
688 end: usize,
689}
690
691fn chart_source_nodes(xml: &[u8]) -> Result<Vec<ChartSourceNode>> {
692 use quick_xml::name::ResolveResult;
693 use quick_xml::reader::NsReader;
694 let mut reader = NsReader::from_reader(xml);
695 let mut nodes: Vec<ChartSourceNode> = Vec::new();
696 let mut stack = Vec::new();
697 let mut buffer = Vec::new();
698 loop {
699 let start = reader.buffer_position() as usize;
700 let event = reader
701 .read_event_into(&mut buffer)
702 .map_err(OxmlError::from)?;
703 let end = reader.buffer_position() as usize;
704 let empty = matches!(&event, Event::Empty(_));
705 match event {
706 Event::Start(element) | Event::Empty(element) => {
707 let (namespace, local) = reader.resolver().resolve_element(element.name());
708 let qualified = matches!(namespace,ResolveResult::Bound(value) if value.as_ref()==C_NS.as_bytes());
709 let local = local.as_ref().to_vec();
710 let index = nodes.len();
711 if index >= 1_000_000 {
712 return Err(invalid_authoring_value("chart XML node limit"));
713 }
714 nodes.push(ChartSourceNode {
715 local,
716 qualified,
717 parent: stack.last().copied(),
718 start,
719 end,
720 });
721 if !empty {
722 stack.push(index);
723 }
724 }
725 Event::End(_) => {
726 let index = stack
727 .pop()
728 .ok_or_else(|| invalid_authoring_value("unbalanced chart source"))?;
729 nodes[index].end = end;
730 }
731 Event::Text(text) if stack.is_empty() && !text.iter().all(u8::is_ascii_whitespace) => {
732 return Err(invalid_authoring_value("text outside chart XML root"));
733 }
734 Event::CData(_) | Event::GeneralRef(_) if stack.is_empty() => {
735 return Err(invalid_authoring_value("content outside chart XML root"));
736 }
737 Event::Decl(_) if start != 0 && !(start == 3 && xml.starts_with(b"\xef\xbb\xbf")) => {
738 return Err(invalid_authoring_value("misplaced chart XML declaration"));
739 }
740 Event::DocType(_) => {
741 return Err(invalid_authoring_value(
742 "DOCTYPE is unsupported in chart source",
743 ));
744 }
745 Event::Eof => break,
746 _ => {}
747 }
748 buffer.clear();
749 }
750 if !stack.is_empty() {
751 return Err(invalid_authoring_value("truncated chart source"));
752 }
753 Ok(nodes)
754}
755
756fn chart_child_node(nodes: &[ChartSourceNode], parent: usize, local: &[u8]) -> Result<usize> {
757 let found = nodes
758 .iter()
759 .enumerate()
760 .filter(|(_, node)| node.parent == Some(parent) && node.qualified && node.local == local)
761 .map(|(index, _)| index)
762 .collect::<Vec<_>>();
763 let [index] = found.as_slice() else {
764 return Err(invalid_authoring_value(format!(
765 "chart data requires one owned {}",
766 String::from_utf8_lossy(local)
767 )));
768 };
769 Ok(*index)
770}
771
772fn chart_source_series(xml: &[u8]) -> Result<Vec<Series>> {
773 CT_ChartSpace::from_xml(xml)?.chart.plot_area.series()
774}
775
776fn resized_chart_range(original: &ChartRange, count: usize) -> Result<ChartRange> {
777 let mut range = original.clone();
778 range.last_row = range
779 .first_row
780 .checked_add(
781 count
782 .checked_sub(1)
783 .ok_or_else(|| invalid_authoring_value("empty chart data range"))?,
784 )
785 .ok_or_else(|| invalid_authoring_value("chart data row overflow"))?;
786 range.formula().map_err(invalid_authoring_value)?;
787 Ok(range)
788}
789
790fn replaced_string_reference(
791 original: &StringRef,
792 formula: String,
793 values: Vec<String>,
794) -> StringRef {
795 let mut result = original.clone();
796 result.formula = formula;
797 result.values = values;
798 result.markup.declared_point_count = None;
799 result.markup.point_indexes.clear();
800 result
801}
802
803fn replaced_numeric_reference(
804 original: &NumericData,
805 formula: String,
806 values: Vec<f64>,
807 number_format: Option<&str>,
808) -> NumericData {
809 let mut result = original.clone();
810 result.formula = formula;
811 result.values = values;
812 if let Some(format) = number_format {
813 result.format_code = format.to_owned();
814 }
815 result.markup.declared_point_count = None;
816 result.markup.point_indexes.clear();
817 result
818}
819
820pub fn replace_chart_data(
824 chart_xml: &[u8],
825 workbook: &[u8],
826 data: &ChartData,
827) -> Result<(Vec<u8>, Vec<u8>)> {
828 let (kind, current) = read_chart_data(chart_xml)?;
829 validate_chart_data(kind, data)?;
830 if data.category_axis_title.is_some()
831 || data.value_axis_title.is_some()
832 || !data.palette.is_empty()
833 {
834 return Err(invalid_authoring_value(
835 "replace_data changes chart data, not axis titles or palette",
836 ));
837 }
838 let workbook_id = chart_workbook_relationship_id(chart_xml)?
839 .ok_or_else(|| invalid_authoring_value("chart has no embedded workbook reference"))?;
840 let old_series = chart_source_series(chart_xml)?;
841 let first = old_series
842 .first()
843 .ok_or_else(|| invalid_authoring_value("chart has no editable series"))?;
844 let first_name = first
845 .name
846 .as_ref()
847 .ok_or_else(|| invalid_authoring_value("series title needs an owned workbook formula"))?;
848 let name_origin =
849 ChartRange::from_formula(&first_name.formula).map_err(invalid_authoring_value)?;
850 let values_origin =
851 ChartRange::from_formula(&first.values.formula).map_err(invalid_authoring_value)?;
852 if name_origin.first_row != name_origin.last_row || name_origin.sheet != values_origin.sheet {
853 return Err(invalid_authoring_value(
854 "series title and values must own one embedded worksheet",
855 ));
856 }
857 let category_origin = match first.categories.as_ref() {
858 Some(AxisData::String(reference)) => ChartRange::from_formula(&reference.formula),
859 Some(AxisData::Numeric(reference)) => ChartRange::from_formula(&reference.formula),
860 None => {
861 return Err(invalid_authoring_value(
862 "categories need an owned workbook formula",
863 ));
864 }
865 }
866 .map_err(invalid_authoring_value)?;
867 if category_origin.sheet != values_origin.sheet {
868 return Err(invalid_authoring_value(
869 "chart data must own one embedded worksheet",
870 ));
871 }
872 for series in &old_series {
874 for formula in [
875 series.name.as_ref().map(|r| r.formula.as_str()),
876 Some(series.values.formula.as_str()),
877 series.categories.as_ref().map(|r| match r {
878 AxisData::String(r) => r.formula.as_str(),
879 AxisData::Numeric(r) => r.formula.as_str(),
880 }),
881 ] {
882 let formula = formula.ok_or_else(|| {
883 invalid_authoring_value("series needs ordinary formula-backed data")
884 })?;
885 let range = ChartRange::from_formula(formula).map_err(invalid_authoring_value)?;
886 if range.sheet != values_origin.sheet {
887 return Err(invalid_authoring_value(
888 "series formula refers to a different worksheet",
889 ));
890 }
891 }
892 }
893 if data.categories == current.categories
894 && data.series == current.series
895 && data.number_format.as_ref().is_none_or(|format| {
896 Some(format) == current.number_format.as_ref()
897 || (format == "General" && current.number_format.is_none())
898 })
899 {
900 oxml_sml::chart_unused_columns(workbook, &values_origin.sheet, 0)
901 .map_err(invalid_authoring_value)?;
902 return Ok((chart_xml.to_vec(), workbook.to_vec()));
903 }
904 let nodes = chart_source_nodes(chart_xml)?;
905 let chart = chart_child_node(&nodes, 0, b"chart")?;
906 let plot_area = chart_child_node(&nodes, chart, b"plotArea")?;
907 let plot_local = match kind {
908 ChartKind::Bar => b"barChart".as_slice(),
909 ChartKind::Line => b"lineChart",
910 ChartKind::Pie => b"pieChart",
911 ChartKind::Doughnut => b"doughnutChart",
912 ChartKind::Area => b"areaChart",
913 ChartKind::Scatter => b"scatterChart",
914 ChartKind::Radar => b"radarChart",
915 };
916 let plot = chart_child_node(&nodes, plot_area, plot_local)?;
917 let series_nodes = nodes
918 .iter()
919 .enumerate()
920 .filter(|(_, node)| node.parent == Some(plot) && node.qualified && node.local == b"ser")
921 .map(|(index, _)| index)
922 .collect::<Vec<_>>();
923 if series_nodes.len() != old_series.len() {
924 return Err(invalid_authoring_value(
925 "editable series source inventory disagrees",
926 ));
927 }
928 let mut updates = Vec::new();
929 let mut patches = Vec::new();
930 for (ordinal, (old, &source_index)) in old_series.iter().zip(&series_nodes).enumerate() {
931 let old_name = old
932 .name
933 .as_ref()
934 .ok_or_else(|| invalid_authoring_value("series title needs formula-backed data"))?;
935 let old_name_range =
936 ChartRange::from_formula(&old_name.formula).map_err(invalid_authoring_value)?;
937 if old_name_range.first_row != old_name_range.last_row {
938 return Err(invalid_authoring_value(
939 "series title formula must own one cell",
940 ));
941 }
942 let old_value_range =
943 ChartRange::from_formula(&old.values.formula).map_err(invalid_authoring_value)?;
944 let categories = old
945 .categories
946 .as_ref()
947 .ok_or_else(|| invalid_authoring_value("series categories are missing"))?;
948 let old_category_range = ChartRange::from_formula(match categories {
949 AxisData::String(r) => &r.formula,
950 AxisData::Numeric(r) => &r.formula,
951 })
952 .map_err(invalid_authoring_value)?;
953 let Some((name, values)) = data.series.get(ordinal) else {
954 for original in [old_name_range, old_value_range, old_category_range] {
955 updates.push(ChartRangeUpdate {
956 number_format: None,
957 original: Some(original),
958 target: None,
959 values: Vec::new(),
960 });
961 }
962 patches.push((
963 nodes[source_index].start,
964 nodes[source_index].end,
965 Vec::new(),
966 ));
967 continue;
968 };
969 let new_value_range = resized_chart_range(&old_value_range, values.len())?;
970 let new_category_range = resized_chart_range(&old_category_range, data.categories.len())?;
971 updates.push(ChartRangeUpdate {
972 number_format: None,
973 original: Some(old_name_range.clone()),
974 target: Some(old_name_range.clone()),
975 values: vec![ChartCellValue::Text(name.clone())],
976 });
977 updates.push(ChartRangeUpdate {
978 number_format: Some(
979 data.number_format
980 .as_deref()
981 .unwrap_or(&old.values.format_code)
982 .to_owned(),
983 ),
984 original: Some(old_value_range),
985 target: Some(new_value_range.clone()),
986 values: values.iter().copied().map(ChartCellValue::Number).collect(),
987 });
988 let category_cells = if matches!(categories, AxisData::Numeric(_)) {
989 data.categories
990 .iter()
991 .map(|value| {
992 value
993 .parse::<f64>()
994 .map(ChartCellValue::Number)
995 .map_err(invalid_authoring_value)
996 })
997 .collect::<Result<Vec<_>>>()?
998 } else {
999 data.categories
1000 .iter()
1001 .cloned()
1002 .map(ChartCellValue::Text)
1003 .collect()
1004 };
1005 updates.push(ChartRangeUpdate {
1006 number_format: None,
1007 original: Some(old_category_range),
1008 target: Some(new_category_range.clone()),
1009 values: category_cells,
1010 });
1011 let tx = chart_child_node(&nodes, source_index, b"tx")?;
1012 let name_ref = chart_child_node(&nodes, tx, b"strRef")?;
1013 let name_reference = replaced_string_reference(
1014 old_name,
1015 old_name_range.formula().map_err(invalid_authoring_value)?,
1016 vec![name.clone()],
1017 );
1018 patches.push((
1019 nodes[name_ref].start,
1020 nodes[name_ref].end,
1021 name_reference.to_xml()?,
1022 ));
1023 let value_wrapper = chart_child_node(
1024 &nodes,
1025 source_index,
1026 if kind == ChartKind::Scatter {
1027 b"yVal"
1028 } else {
1029 b"val"
1030 },
1031 )?;
1032 let value_ref = chart_child_node(&nodes, value_wrapper, b"numRef")?;
1033 let value_reference = replaced_numeric_reference(
1034 &old.values,
1035 new_value_range.formula().map_err(invalid_authoring_value)?,
1036 values.clone(),
1037 data.number_format.as_deref(),
1038 );
1039 patches.push((
1040 nodes[value_ref].start,
1041 nodes[value_ref].end,
1042 value_reference.to_xml()?,
1043 ));
1044 let category_wrapper = chart_child_node(
1045 &nodes,
1046 source_index,
1047 if kind == ChartKind::Scatter {
1048 b"xVal"
1049 } else {
1050 b"cat"
1051 },
1052 )?;
1053 let (local, bytes) = match categories {
1054 AxisData::String(reference) => (
1055 b"strRef".as_slice(),
1056 replaced_string_reference(
1057 reference,
1058 new_category_range
1059 .formula()
1060 .map_err(invalid_authoring_value)?,
1061 data.categories.clone(),
1062 )
1063 .to_xml()?,
1064 ),
1065 AxisData::Numeric(reference) => (
1066 b"numRef".as_slice(),
1067 replaced_numeric_reference(
1068 reference,
1069 new_category_range
1070 .formula()
1071 .map_err(invalid_authoring_value)?,
1072 data.categories
1073 .iter()
1074 .map(|v| v.parse::<f64>().map_err(invalid_authoring_value))
1075 .collect::<Result<Vec<_>>>()?,
1076 None,
1077 )
1078 .to_xml()?,
1079 ),
1080 };
1081 let category_ref = chart_child_node(&nodes, category_wrapper, local)?;
1082 patches.push((nodes[category_ref].start, nodes[category_ref].end, bytes));
1083 }
1084 let added = data.series.len().saturating_sub(old_series.len());
1085 if added > 0 {
1086 let free = oxml_sml::chart_unused_columns(workbook, &values_origin.sheet, added)
1087 .map_err(invalid_authoring_value)?;
1088 let (default_xml, _) = authored_chart_parts(kind, data, &workbook_id)?;
1089 let defaults = chart_source_series(&default_xml)?;
1090 let max_index = old_series.iter().map(|s| s.index).max().unwrap();
1091 let max_order = old_series.iter().map(|s| s.order).max().unwrap();
1092 let mut bytes = Vec::new();
1093 for (offset, column) in free.into_iter().enumerate() {
1094 let ordinal = old_series.len() + offset;
1095 let (name, values) = &data.series[ordinal];
1096 let mut series = defaults[ordinal].clone();
1097 let step = u32::try_from(offset + 1).map_err(invalid_authoring_value)?;
1098 series.index = max_index
1099 .checked_add(step)
1100 .ok_or_else(|| invalid_authoring_value("chart series index overflow"))?;
1101 series.order = max_order
1102 .checked_add(step)
1103 .ok_or_else(|| invalid_authoring_value("chart series order overflow"))?;
1104 let mut title = name_origin.clone();
1105 title.column = column;
1106 let mut numbers = resized_chart_range(&values_origin, values.len())?;
1107 numbers.column = column;
1108 let category = resized_chart_range(&category_origin, data.categories.len())?;
1109 series.name = Some(StringRef::new(
1110 title.formula().map_err(invalid_authoring_value)?,
1111 vec![name.clone()],
1112 )?);
1113 series.values = NumericData::new(
1114 numbers.formula().map_err(invalid_authoring_value)?,
1115 data.number_format
1116 .as_deref()
1117 .unwrap_or(&first.values.format_code)
1118 .to_owned(),
1119 values.clone(),
1120 )?;
1121 series.categories = Some(if kind == ChartKind::Scatter {
1122 AxisData::Numeric(NumericData::new(
1123 category.formula().map_err(invalid_authoring_value)?,
1124 "General".into(),
1125 data.categories
1126 .iter()
1127 .map(|v| v.parse::<f64>().map_err(invalid_authoring_value))
1128 .collect::<Result<Vec<_>>>()?,
1129 )?)
1130 } else {
1131 AxisData::String(StringRef::new(
1132 category.formula().map_err(invalid_authoring_value)?,
1133 data.categories.clone(),
1134 )?)
1135 });
1136 updates.push(ChartRangeUpdate {
1137 number_format: None,
1138 original: None,
1139 target: Some(title),
1140 values: vec![ChartCellValue::Text(name.clone())],
1141 });
1142 updates.push(ChartRangeUpdate {
1143 number_format: Some(
1144 data.number_format
1145 .as_deref()
1146 .unwrap_or(&first.values.format_code)
1147 .to_owned(),
1148 ),
1149 original: None,
1150 target: Some(numbers),
1151 values: values.iter().copied().map(ChartCellValue::Number).collect(),
1152 });
1153 bytes.extend(series.to_xml_for_plot(kind == ChartKind::Scatter)?);
1154 }
1155 let position = nodes[*series_nodes.last().unwrap()].end;
1156 patches.push((position, position, bytes));
1157 }
1158 let updated_workbook =
1159 oxml_sml::merge_chart_ranges(workbook, &updates).map_err(invalid_authoring_value)?;
1160 patches.sort_by_key(|(start, end, _)| (*start, *end));
1161 for pair in patches.windows(2) {
1162 if pair[0].1 > pair[1].0 {
1163 return Err(invalid_authoring_value(
1164 "overlapping chart data source edits",
1165 ));
1166 }
1167 }
1168 let mut updated_chart = chart_xml.to_vec();
1169 for (start, end, bytes) in patches.into_iter().rev() {
1170 updated_chart.splice(start..end, bytes);
1171 }
1172 let (_, projected) = read_chart_data(&updated_chart)?;
1173 if projected.categories != data.categories || projected.series != data.series {
1174 return Err(invalid_authoring_value(
1175 "replacement chart cache projection disagrees with requested data",
1176 ));
1177 }
1178 Ok((updated_chart, updated_workbook))
1179}
1180
1181fn validate_chart_data(kind: ChartKind, data: &ChartData) -> Result<()> {
1182 if data.categories.is_empty() {
1183 return Err(invalid_authoring_value("at least one category is required"));
1184 }
1185 if data.series.is_empty() {
1186 return Err(invalid_authoring_value("at least one series is required"));
1187 }
1188 if matches!(kind, ChartKind::Pie | ChartKind::Doughnut) && data.series.len() != 1 {
1189 return Err(invalid_authoring_value(
1190 "pie and doughnut charts require exactly one series",
1191 ));
1192 }
1193 for (name, values) in &data.series {
1194 if values.len() != data.categories.len() {
1195 return Err(invalid_authoring_value(format!(
1196 "series {name:?} has {} values for {} categories",
1197 values.len(),
1198 data.categories.len()
1199 )));
1200 }
1201 if values.iter().any(|value| !value.is_finite()) {
1202 return Err(invalid_authoring_value(format!(
1203 "series {name:?} contains a nonfinite value"
1204 )));
1205 }
1206 }
1207 if kind == ChartKind::Scatter {
1208 for category in &data.categories {
1209 if category
1210 .parse::<f64>()
1211 .ok()
1212 .is_none_or(|value| !value.is_finite())
1213 {
1214 return Err(invalid_authoring_value(format!(
1215 "scatter category {category:?} is not a finite number"
1216 )));
1217 }
1218 }
1219 }
1220 if let Some(format) = &data.number_format {
1221 NumericData::new("Sheet1!$A$2".to_owned(), format.clone(), vec![0.0])?;
1222 }
1223 Ok(())
1224}
1225
1226fn invalid_authoring_value(error: impl fmt::Display) -> ChartError {
1227 ChartError::InvalidValue {
1228 element: "chart authoring".to_owned(),
1229 value: error.to_string(),
1230 }
1231}
1232
1233fn category_value_axes(category: AxisId, value: AxisId, data: &ChartData) -> Result<Vec<Axis>> {
1234 authored_axes(
1235 [
1236 Axis::new(AxisKind::Category, category, AxisPosition::Bottom, value),
1237 Axis::new(AxisKind::Value, value, AxisPosition::Left, category),
1238 ],
1239 data,
1240 )
1241}
1242
1243fn authored_axes(mut axes: [Axis; 2], data: &ChartData) -> Result<Vec<Axis>> {
1244 for axis in &mut axes {
1245 axis.set_deleted(false);
1246 }
1247 axes[0].title = data
1248 .category_axis_title
1249 .as_deref()
1250 .map(CT_Title::plain_text);
1251 axes[1].title = data.value_axis_title.as_deref().map(CT_Title::plain_text);
1252 axes[1].number_format = data
1253 .number_format
1254 .as_ref()
1255 .map(|format| NumberFormat::new(format.clone(), false))
1256 .transpose()?;
1257 Ok(axes.into())
1258}
1259
1260fn apply_palette(kind: ChartKind, data: &ChartData, series: &mut [Series]) {
1261 if data.palette.is_empty() {
1262 return;
1263 }
1264 for (index, item) in series.iter_mut().enumerate() {
1265 item.sp_pr = Some(series_shape(kind, data.palette[index % data.palette.len()]));
1266 }
1267 if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) && series.len() == 1 {
1268 series[0].authored_data_points = data
1269 .categories
1270 .iter()
1271 .enumerate()
1272 .map(|(index, _)| DataPoint {
1273 index: index as u32,
1274 sp_pr: series_shape(kind, data.palette[index % data.palette.len()]),
1275 })
1276 .collect();
1277 }
1278}
1279
1280fn series_shape(kind: ChartKind, color: RgbColor) -> CT_ShapeProperties {
1281 let mut solid = SolidFill::default();
1282 solid.color = Some(ColorChoice::srgb(color));
1283 let fill = Fill::Solid(solid);
1284 let mut properties = CT_ShapeProperties::default();
1285 if matches!(
1286 kind,
1287 ChartKind::Line | ChartKind::Scatter | ChartKind::Radar
1288 ) {
1289 let mut line = CT_LineProperties::default();
1290 line.fill = Some(fill);
1291 properties.line = Some(line);
1292 } else {
1293 properties.fill = Some(fill);
1294 }
1295 properties
1296}
1297
1298fn spreadsheet_column_name(mut index: usize) -> String {
1299 let mut name = Vec::new();
1300 index += 1;
1301 while index != 0 {
1302 let remainder = (index - 1) % 26;
1303 name.push((b'A' + remainder as u8) as char);
1304 index = (index - 1) / 26;
1305 }
1306 name.iter().rev().collect()
1307}
1308
1309#[derive(Clone, Debug)]
1311pub struct ChartGeometry {
1312 pub plot_bounds: Rect,
1313 pub elements: Vec<PositionedElement>,
1314}
1315
1316#[derive(Clone, Debug)]
1317struct AxisScale {
1318 domain: Domain,
1319 ticks: Vec<f64>,
1320}
1321
1322#[derive(Clone, Copy, Debug)]
1323struct AxisMapping {
1324 domain: Domain,
1325 orientation: Orientation,
1326}
1327
1328#[derive(Clone, Copy, Debug, Default)]
1329struct GeometryOptions {
1330 category_orientation: Orientation,
1331 value: Option<AxisMapping>,
1332 scatter_x: Option<AxisMapping>,
1333 scatter_y: Option<AxisMapping>,
1334}
1335
1336#[derive(Default)]
1337struct ChartAnnotations {
1338 gridlines: Vec<PositionedElement>,
1339 axis_lines: Vec<PositionedElement>,
1340 ticks: Vec<PositionedElement>,
1341 labels: Vec<PositionedElement>,
1342}
1343
1344pub fn render_chart(
1346 chart: &CT_Chart,
1347 bounds: Rect,
1348 theme: &CT_OfficeStyleSheet,
1349 color_map: &ColorMap,
1350 fonts: &mut FontManager,
1351) -> Result<GroupElement> {
1352 let plots = chart.plot_area.plots()?;
1353 let plot = plots
1354 .first()
1355 .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
1356 plot.validate()?;
1357 let legend_layout = layout_legend(chart, plot, bounds, fonts)?;
1358 let plot_bounds = legend_layout.plot;
1359 let series_colours = resolve_series_colours(plot, theme, color_map)?;
1360 let axes = chart.plot_area.axes()?;
1361 let options = geometry_options(plot, &axes, chart.disp_blanks_as)?;
1362 let plot_children = render_plot_geometry(
1363 plot,
1364 plot_bounds,
1365 chart.disp_blanks_as,
1366 options,
1367 &series_colours,
1368 )?;
1369 if plot_children.is_empty() && !plot_can_render_without_marks(plot) {
1370 return Err(ChartError::InvalidValue {
1371 element: "c:plotArea".to_owned(),
1372 value: "plot has no renderable cached data".to_owned(),
1373 });
1374 }
1375
1376 let mut chart_annotations =
1377 render_axes_and_labels(plot, &axes, plot_bounds, chart.disp_blanks_as, fonts)?;
1378 let mut annotations = Vec::new();
1379 render_data_labels(
1380 plot,
1381 plot_bounds,
1382 chart.disp_blanks_as,
1383 options,
1384 fonts,
1385 &mut chart_annotations.labels,
1386 )?;
1387 render_legend(
1388 legend_layout,
1389 &series_colours,
1390 &mut annotations,
1391 &mut chart_annotations.labels,
1392 );
1393
1394 validate_geometry_coordinates(&chart_annotations.gridlines)?;
1395 validate_geometry_coordinates(&plot_children)?;
1396 validate_geometry_coordinates(&chart_annotations.axis_lines)?;
1397 validate_geometry_coordinates(&chart_annotations.ticks)?;
1398 validate_geometry_coordinates(&annotations)?;
1399 let mut children = chart_annotations.gridlines;
1400 children.push(PositionedElement::Group(GroupElement {
1401 transform: Transform::IDENTITY,
1402 clip: Some(Path::rect(plot_bounds)),
1403 opacity: 1.0,
1404 effects: Vec::new(),
1405 children: plot_children,
1406 }));
1407 children.extend(chart_annotations.axis_lines);
1408 children.extend(chart_annotations.ticks);
1409 children.extend(annotations);
1410 children.extend(chart_annotations.labels);
1411 Ok(GroupElement {
1412 transform: Transform::IDENTITY,
1413 clip: None,
1414 opacity: 1.0,
1415 effects: Vec::new(),
1416 children,
1417 })
1418}
1419
1420pub fn render_chart_placeholder(bounds: Rect, fonts: &mut FontManager) -> Result<GroupElement> {
1422 if !bounds.width.is_finite()
1423 || !bounds.height.is_finite()
1424 || bounds.width <= 0.0
1425 || bounds.height <= 0.0
1426 {
1427 return Err(ChartError::InvalidValue {
1428 element: "chart fallback bounds".to_owned(),
1429 value: "width and height must be finite and positive".to_owned(),
1430 });
1431 }
1432 let label = shape_label(
1433 fonts,
1434 "Unsupported chart",
1435 Point {
1436 x: 4.0,
1437 y: (bounds.height / 2.0).max(9.0),
1438 },
1439 )?;
1440 Ok(GroupElement {
1441 transform: Transform::IDENTITY,
1442 clip: Some(Path::rect(Rect {
1443 x: 0.0,
1444 y: 0.0,
1445 width: bounds.width,
1446 height: bounds.height,
1447 })),
1448 opacity: 1.0,
1449 effects: Vec::new(),
1450 children: vec![PositionedElement::Text(label)],
1451 })
1452}
1453
1454pub fn render_geometry(
1456 chart: &CT_Chart,
1457 bounds: Rect,
1458 theme: &CT_OfficeStyleSheet,
1459 color_map: &ColorMap,
1460) -> Result<ChartGeometry> {
1461 let plot_bounds = geometry_plot_bounds(bounds)?;
1462 let plots = chart.plot_area.plots()?;
1463 let plot = plots
1464 .first()
1465 .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
1466 plot.validate()?;
1467 let series_colours = resolve_series_colours(plot, theme, color_map)?;
1468 let children = render_plot_geometry(
1469 plot,
1470 plot_bounds,
1471 chart.disp_blanks_as,
1472 geometry_options(plot, &[], chart.disp_blanks_as)?,
1473 &series_colours,
1474 )?;
1475 if children.is_empty() && !plot_can_render_without_marks(plot) {
1476 return Err(ChartError::InvalidValue {
1477 element: "c:plotArea".to_owned(),
1478 value: "plot has no renderable cached data".to_owned(),
1479 });
1480 }
1481 validate_geometry_coordinates(&children)?;
1482 Ok(ChartGeometry {
1483 plot_bounds,
1484 elements: vec![PositionedElement::Group(GroupElement {
1485 transform: Transform::IDENTITY,
1486 clip: None,
1487 opacity: 1.0,
1488 effects: Vec::new(),
1489 children,
1490 })],
1491 })
1492}
1493
1494fn plot_can_render_without_marks(plot: &Plot) -> bool {
1495 matches!(
1496 plot,
1497 Plot::Radar { series, .. }
1498 if series
1499 .iter()
1500 .any(|series| !series.values.values.is_empty())
1501 )
1502}
1503
1504fn geometry_plot_bounds(bounds: Rect) -> Result<Rect> {
1505 if ![bounds.x, bounds.y, bounds.width, bounds.height]
1506 .into_iter()
1507 .all(f64::is_finite)
1508 || bounds.width <= 0.0
1509 || bounds.height <= 0.0
1510 || !(bounds.x + bounds.width).is_finite()
1511 || !(bounds.y + bounds.height).is_finite()
1512 {
1513 return Err(ChartError::InvalidValue {
1514 element: "chart geometry bounds".to_owned(),
1515 value: format!(
1516 "x={}, y={}, width={}, height={}",
1517 bounds.x, bounds.y, bounds.width, bounds.height
1518 ),
1519 });
1520 }
1521 let plot_bounds = Rect {
1522 x: bounds.x + PLOT_MARGIN_LEFT,
1523 y: bounds.y + PLOT_MARGIN_TOP,
1524 width: bounds.width - PLOT_MARGIN_LEFT - PLOT_MARGIN_RIGHT,
1525 height: bounds.height - PLOT_MARGIN_TOP - PLOT_MARGIN_BOTTOM,
1526 };
1527 if ![
1528 plot_bounds.x,
1529 plot_bounds.y,
1530 plot_bounds.width,
1531 plot_bounds.height,
1532 plot_bounds.x + plot_bounds.width,
1533 plot_bounds.y + plot_bounds.height,
1534 ]
1535 .into_iter()
1536 .all(f64::is_finite)
1537 || plot_bounds.width <= 0.0
1538 || plot_bounds.height <= 0.0
1539 {
1540 return Err(ChartError::InvalidValue {
1541 element: "chart geometry bounds".to_owned(),
1542 value: "chart is too small for the fixed plot margins".to_owned(),
1543 });
1544 }
1545 Ok(plot_bounds)
1546}
1547
1548fn geometry_options(plot: &Plot, axes: &[Axis], blanks: DispBlanksAs) -> Result<GeometryOptions> {
1549 let mut options = GeometryOptions::default();
1550 if let Some(category_axis) = axes.iter().find(|axis| axis.kind != AxisKind::Value) {
1551 options.category_orientation = category_axis.scaling.orientation;
1552 }
1553
1554 if let Plot::Scatter { axis_ids, .. } = plot {
1555 let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
1556 if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[0]) {
1557 options.scatter_x = Some(axis_mapping(axis, x_domain)?);
1558 }
1559 if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[1]) {
1560 options.scatter_y = Some(axis_mapping(axis, y_domain)?);
1561 }
1562 } else if let Some(domain) = plot_value_domain(plot, blanks)? {
1563 options.value = Some(
1564 axes.iter()
1565 .find(|axis| axis.kind == AxisKind::Value)
1566 .map(|axis| axis_mapping(axis, domain))
1567 .transpose()?
1568 .unwrap_or(AxisMapping {
1569 domain,
1570 orientation: Orientation::MinMax,
1571 }),
1572 );
1573 }
1574 Ok(options)
1575}
1576
1577fn axis_mapping(axis: &Axis, extent: Domain) -> Result<AxisMapping> {
1578 if axis.scaling.log_base.is_some() {
1579 return Err(ChartError::InvalidValue {
1580 element: "c:scaling/c:logBase".to_owned(),
1581 value: "native chart rendering currently supports linear axes".to_owned(),
1582 });
1583 }
1584 let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
1585 Ok(AxisMapping {
1586 domain: scale.domain,
1587 orientation: axis.scaling.orientation,
1588 })
1589}
1590
1591fn plot_value_domain(plot: &Plot, blanks: DispBlanksAs) -> Result<Option<Domain>> {
1592 let domain = match plot {
1593 Plot::Bar {
1594 grouping, series, ..
1595 } => {
1596 let logical = logical_series_values(series)?;
1597 let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
1598 let layers =
1599 stacked_series_bounds(&logical, stacked, *grouping == BarGrouping::PercentStacked)?;
1600 domain_from_layers(&layers)?
1601 }
1602 Plot::Line {
1603 grouping, series, ..
1604 } => {
1605 let logical = logical_series_values(series)?;
1606 if *grouping == Grouping::Standard {
1607 let values = if blanks == DispBlanksAs::Zero {
1608 zero_filled_series(&logical, series_category_count(series)?)
1609 .into_iter()
1610 .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
1611 .collect::<Vec<_>>()
1612 } else {
1613 logical
1614 .into_iter()
1615 .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
1616 .collect::<Vec<_>>()
1617 };
1618 data_domain(&values, false)?
1619 } else {
1620 let layers =
1621 stacked_series_bounds(&logical, true, *grouping == Grouping::PercentStacked)?;
1622 domain_from_layers(&layers)?
1623 }
1624 }
1625 Plot::Area {
1626 grouping, series, ..
1627 } => {
1628 let logical = logical_series_values(series)?;
1629 let layers = stacked_series_bounds(
1630 &logical,
1631 *grouping != Grouping::Standard,
1632 *grouping == Grouping::PercentStacked,
1633 )?;
1634 domain_from_layers(&layers)?
1635 }
1636 Plot::Radar { series, .. } => {
1637 let values = series
1638 .iter()
1639 .flat_map(|series| series.values.values.iter().copied())
1640 .collect::<Vec<_>>();
1641 data_domain(&values, true)?
1642 }
1643 Plot::Pie { .. } | Plot::Doughnut { .. } | Plot::Scatter { .. } => return Ok(None),
1644 };
1645 Ok(Some(domain))
1646}
1647
1648fn scatter_domains(plot: &Plot, blanks: DispBlanksAs) -> Result<(Domain, Domain)> {
1649 let Plot::Scatter { series, .. } = plot else {
1650 return Err(ChartError::UnexpectedElement("non-scatter plot".to_owned()));
1651 };
1652 let mut x_values = Vec::new();
1653 let mut y_values = Vec::new();
1654 for series in series {
1655 for (_, x, y) in scatter_render_points(series, blanks)? {
1656 x_values.push(x);
1657 y_values.push(y);
1658 }
1659 }
1660 Ok((
1661 data_domain(&x_values, false)?,
1662 data_domain(&y_values, false)?,
1663 ))
1664}
1665
1666fn render_axes_and_labels(
1667 plot: &Plot,
1668 axes: &[Axis],
1669 bounds: Rect,
1670 blanks: DispBlanksAs,
1671 fonts: &mut FontManager,
1672) -> Result<ChartAnnotations> {
1673 if matches!(plot, Plot::Radar { .. }) {
1674 return render_radar_axes_and_labels(plot, axes, bounds, blanks, fonts);
1675 }
1676 let mut annotations = ChartAnnotations::default();
1677 let series = plot_series(plot);
1678 let category_count = if series.is_empty() {
1679 1
1680 } else {
1681 series_category_count(series)?
1682 };
1683 for axis in axes {
1684 if axis.deleted {
1685 continue;
1686 }
1687 axis_line(&mut annotations.axis_lines, bounds, axis.position);
1688 if axis.kind == AxisKind::Value {
1689 let extent = axis_extent(plot, axis, blanks)?;
1690 let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
1691 for tick in &scale.ticks {
1692 let coordinate = axis_tick_coordinate(
1693 *tick,
1694 scale.domain,
1695 bounds,
1696 axis.position,
1697 axis.scaling.orientation,
1698 )?;
1699 if axis.major_gridlines.is_some() {
1700 major_gridline(
1701 &mut annotations.gridlines,
1702 bounds,
1703 axis.position,
1704 coordinate,
1705 );
1706 }
1707 axis_tick(
1708 &mut annotations.ticks,
1709 bounds,
1710 axis.position,
1711 coordinate,
1712 axis.major_tick_mark,
1713 );
1714 if axis.tick_label_position != TickLabelPosition::None {
1715 let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
1716 let origin = tick_label_origin(
1717 bounds,
1718 axis.position,
1719 axis.tick_label_position,
1720 coordinate,
1721 );
1722 annotations
1723 .labels
1724 .push(PositionedElement::Text(shape_label_with_properties(
1725 fonts,
1726 &text,
1727 origin,
1728 axis.tx_pr.as_ref(),
1729 )?));
1730 }
1731 }
1732 } else {
1733 let count = category_count;
1734 if count > MAX_CATEGORY_ANNOTATIONS {
1735 return Err(ChartError::InvalidValue {
1736 element: "c:catAx annotations".to_owned(),
1737 value: format!(
1738 "logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"
1739 ),
1740 });
1741 }
1742 let category_labels: BTreeMap<_, _> =
1743 if axis.tick_label_position != TickLabelPosition::None {
1744 series
1745 .first()
1746 .map(|series| series_categories(series, axis.number_format.as_ref()))
1747 .transpose()?
1748 .unwrap_or_default()
1749 .into_iter()
1750 .collect()
1751 } else {
1752 BTreeMap::new()
1753 };
1754 for logical_index in 0..count {
1755 let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
1756 let fraction = (index as f64 + 0.5) / count as f64;
1757 let coordinate =
1758 if matches!(axis.position, AxisPosition::Bottom | AxisPosition::Top) {
1759 bounds.x + fraction * bounds.width
1760 } else {
1761 bounds.y + fraction * bounds.height
1762 };
1763 if axis.major_gridlines.is_some() {
1764 major_gridline(
1765 &mut annotations.gridlines,
1766 bounds,
1767 axis.position,
1768 coordinate,
1769 );
1770 }
1771 axis_tick(
1772 &mut annotations.ticks,
1773 bounds,
1774 axis.position,
1775 coordinate,
1776 axis.major_tick_mark,
1777 );
1778 if axis.tick_label_position != TickLabelPosition::None
1779 && let Some(text) = category_labels.get(&logical_index)
1780 {
1781 let origin = tick_label_origin(
1782 bounds,
1783 axis.position,
1784 axis.tick_label_position,
1785 coordinate,
1786 );
1787 annotations
1788 .labels
1789 .push(PositionedElement::Text(shape_label_with_properties(
1790 fonts,
1791 text,
1792 origin,
1793 axis.tx_pr.as_ref(),
1794 )?));
1795 }
1796 }
1797 }
1798 }
1799 Ok(annotations)
1800}
1801
1802fn render_radar_axes_and_labels(
1803 plot: &Plot,
1804 axes: &[Axis],
1805 bounds: Rect,
1806 blanks: DispBlanksAs,
1807 fonts: &mut FontManager,
1808) -> Result<ChartAnnotations> {
1809 let mut annotations = ChartAnnotations::default();
1810 let series = plot_series(plot);
1811 let count = series_category_count(series)?;
1812 if count > MAX_CATEGORY_ANNOTATIONS {
1813 return Err(ChartError::InvalidValue {
1814 element: "c:radarChart annotations".to_owned(),
1815 value: format!("logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"),
1816 });
1817 }
1818 let center = Point {
1819 x: bounds.x + bounds.width / 2.0,
1820 y: bounds.y + bounds.height / 2.0,
1821 };
1822 let radius = bounds.width.min(bounds.height) / 2.0;
1823 for axis in axes {
1824 if axis.deleted {
1825 continue;
1826 }
1827 if axis.kind == AxisKind::Value {
1828 let scale = nice_number_scale(
1829 axis_extent(plot, axis, blanks)?,
1830 axis.scaling.minimum,
1831 axis.scaling.maximum,
1832 6,
1833 )?;
1834 annotations.axis_lines.push(stroked_segment(
1835 center,
1836 radial_point(center, radius, 0, count),
1837 Color::BLACK,
1838 1.0,
1839 ));
1840 for tick in &scale.ticks {
1841 let distance =
1842 normalized_value_oriented(*tick, scale.domain, axis.scaling.orientation)?
1843 * radius;
1844 if axis.major_gridlines.is_some() && distance > 0.0 {
1845 let points: Vec<_> = (0..count)
1846 .map(|index| radial_point(center, distance, index, count))
1847 .collect();
1848 annotations
1849 .gridlines
1850 .push(PositionedElement::Path(PathElement {
1851 path: polyline_path(&points, true),
1852 fill: None,
1853 stroke: Some(Stroke::new(
1854 Paint::Solid(Color {
1855 r: 0.82,
1856 g: 0.82,
1857 b: 0.82,
1858 a: 1.0,
1859 }),
1860 0.5,
1861 )),
1862 }));
1863 }
1864 radar_value_tick(
1865 &mut annotations.ticks,
1866 center,
1867 distance,
1868 axis.major_tick_mark,
1869 );
1870 let origin = match axis.tick_label_position {
1871 TickLabelPosition::High => Some(Point {
1872 x: center.x + radius + 4.0,
1873 y: center.y - distance + 3.0,
1874 }),
1875 TickLabelPosition::Low => Some(Point {
1876 x: center.x - radius - 30.0,
1877 y: center.y - distance + 3.0,
1878 }),
1879 TickLabelPosition::NextTo => Some(Point {
1880 x: center.x + 4.0,
1881 y: center.y - distance + 3.0,
1882 }),
1883 TickLabelPosition::None => None,
1884 };
1885 if let Some(origin) = origin {
1886 let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
1887 annotations
1888 .labels
1889 .push(PositionedElement::Text(shape_label_with_properties(
1890 fonts,
1891 &text,
1892 origin,
1893 axis.tx_pr.as_ref(),
1894 )?));
1895 }
1896 }
1897 } else {
1898 let category_labels: BTreeMap<_, _> =
1899 if axis.tick_label_position != TickLabelPosition::None {
1900 series
1901 .first()
1902 .map(|series| series_categories(series, axis.number_format.as_ref()))
1903 .transpose()?
1904 .unwrap_or_default()
1905 .into_iter()
1906 .collect()
1907 } else {
1908 BTreeMap::new()
1909 };
1910 for logical_index in 0..count {
1911 let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
1912 let end = radial_point(center, radius, index, count);
1913 annotations
1914 .axis_lines
1915 .push(stroked_segment(center, end, Color::BLACK, 1.0));
1916 radar_category_tick(
1917 &mut annotations.ticks,
1918 center,
1919 radius,
1920 index,
1921 count,
1922 axis.major_tick_mark,
1923 );
1924 if let Some(text) = category_labels.get(&logical_index) {
1925 let Some(origin) = radar_category_label_origin(
1926 bounds,
1927 center,
1928 radius,
1929 index,
1930 count,
1931 axis.tick_label_position,
1932 ) else {
1933 continue;
1934 };
1935 annotations
1936 .labels
1937 .push(PositionedElement::Text(shape_label_with_properties(
1938 fonts,
1939 text,
1940 origin,
1941 axis.tx_pr.as_ref(),
1942 )?));
1943 }
1944 }
1945 }
1946 }
1947 Ok(annotations)
1948}
1949
1950fn radar_category_label_origin(
1951 bounds: Rect,
1952 center: Point,
1953 radius: f64,
1954 index: usize,
1955 count: usize,
1956 position: TickLabelPosition,
1957) -> Option<Point> {
1958 let label_radius = match position {
1959 TickLabelPosition::High => radius + 18.0,
1960 TickLabelPosition::Low => (radius - 10.0).max(0.0),
1961 TickLabelPosition::NextTo => radius + 10.0,
1962 TickLabelPosition::None => return None,
1963 };
1964 let label = radial_point(center, label_radius, index, count);
1965 let mut origin = Point {
1966 x: label.x - 4.0,
1967 y: label.y + 3.0,
1968 };
1969 if position == TickLabelPosition::High {
1970 origin.x = origin.x.clamp(
1971 bounds.x - PLOT_MARGIN_LEFT,
1972 bounds.x + bounds.width + PLOT_MARGIN_RIGHT,
1973 );
1974 origin.y = origin.y.clamp(
1975 bounds.y - PLOT_MARGIN_TOP,
1976 bounds.y + bounds.height + PLOT_MARGIN_BOTTOM,
1977 );
1978 }
1979 Some(origin)
1980}
1981
1982fn radar_value_tick(
1983 elements: &mut Vec<PositionedElement>,
1984 center: Point,
1985 distance: f64,
1986 mark: TickMark,
1987) {
1988 let (left, right) = match mark {
1989 TickMark::None => return,
1990 TickMark::Inside => (4.0, 0.0),
1991 TickMark::Outside => (0.0, 4.0),
1992 TickMark::Cross => (2.0, 2.0),
1993 };
1994 let y = center.y - distance;
1995 elements.push(stroked_segment(
1996 Point {
1997 x: center.x - left,
1998 y,
1999 },
2000 Point {
2001 x: center.x + right,
2002 y,
2003 },
2004 Color::BLACK,
2005 1.0,
2006 ));
2007}
2008
2009fn radar_category_tick(
2010 elements: &mut Vec<PositionedElement>,
2011 center: Point,
2012 radius: f64,
2013 index: usize,
2014 count: usize,
2015 mark: TickMark,
2016) {
2017 let (inside, outside) = match mark {
2018 TickMark::None => return,
2019 TickMark::Inside => (4.0, 0.0),
2020 TickMark::Outside => (0.0, 4.0),
2021 TickMark::Cross => (2.0, 2.0),
2022 };
2023 elements.push(stroked_segment(
2024 radial_point(center, radius - inside, index, count),
2025 radial_point(center, radius + outside, index, count),
2026 Color::BLACK,
2027 1.0,
2028 ));
2029}
2030
2031fn axis_extent(plot: &Plot, axis: &Axis, blanks: DispBlanksAs) -> Result<Domain> {
2032 if let Plot::Scatter { axis_ids, .. } = plot {
2033 let (x, y) = scatter_domains(plot, blanks)?;
2034 return Ok(if axis.id == axis_ids[0] { x } else { y });
2035 }
2036 plot_value_domain(plot, blanks)?.ok_or_else(|| ChartError::InvalidValue {
2037 element: "chart axis".to_owned(),
2038 value: "axis-free plot supplied an axis".to_owned(),
2039 })
2040}
2041
2042fn axis_tick_coordinate(
2043 value: f64,
2044 domain: Domain,
2045 bounds: Rect,
2046 position: AxisPosition,
2047 orientation: Orientation,
2048) -> Result<f64> {
2049 if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
2050 map_x_oriented(value, domain, bounds, orientation)
2051 } else {
2052 map_y_oriented(value, domain, bounds, orientation)
2053 }
2054}
2055
2056fn axis_line(elements: &mut Vec<PositionedElement>, bounds: Rect, position: AxisPosition) {
2057 let (start, end) = match position {
2058 AxisPosition::Bottom => (
2059 Point {
2060 x: bounds.x,
2061 y: bounds.y + bounds.height,
2062 },
2063 Point {
2064 x: bounds.x + bounds.width,
2065 y: bounds.y + bounds.height,
2066 },
2067 ),
2068 AxisPosition::Top => (
2069 Point {
2070 x: bounds.x,
2071 y: bounds.y,
2072 },
2073 Point {
2074 x: bounds.x + bounds.width,
2075 y: bounds.y,
2076 },
2077 ),
2078 AxisPosition::Left => (
2079 Point {
2080 x: bounds.x,
2081 y: bounds.y,
2082 },
2083 Point {
2084 x: bounds.x,
2085 y: bounds.y + bounds.height,
2086 },
2087 ),
2088 AxisPosition::Right => (
2089 Point {
2090 x: bounds.x + bounds.width,
2091 y: bounds.y,
2092 },
2093 Point {
2094 x: bounds.x + bounds.width,
2095 y: bounds.y + bounds.height,
2096 },
2097 ),
2098 };
2099 elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
2100}
2101
2102fn major_gridline(
2103 elements: &mut Vec<PositionedElement>,
2104 bounds: Rect,
2105 position: AxisPosition,
2106 coordinate: f64,
2107) {
2108 let (start, end) = if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
2109 (
2110 Point {
2111 x: coordinate,
2112 y: bounds.y,
2113 },
2114 Point {
2115 x: coordinate,
2116 y: bounds.y + bounds.height,
2117 },
2118 )
2119 } else {
2120 (
2121 Point {
2122 x: bounds.x,
2123 y: coordinate,
2124 },
2125 Point {
2126 x: bounds.x + bounds.width,
2127 y: coordinate,
2128 },
2129 )
2130 };
2131 elements.push(stroked_segment(
2132 start,
2133 end,
2134 Color {
2135 r: 0.82,
2136 g: 0.82,
2137 b: 0.82,
2138 a: 1.0,
2139 },
2140 0.5,
2141 ));
2142}
2143
2144fn axis_tick(
2145 elements: &mut Vec<PositionedElement>,
2146 bounds: Rect,
2147 position: AxisPosition,
2148 coordinate: f64,
2149 mark: TickMark,
2150) {
2151 let (inside, outside) = match mark {
2152 TickMark::None => return,
2153 TickMark::Inside => (4.0, 0.0),
2154 TickMark::Outside => (0.0, 4.0),
2155 TickMark::Cross => (2.0, 2.0),
2156 };
2157 let (start, end) = match position {
2158 AxisPosition::Bottom => (
2159 Point {
2160 x: coordinate,
2161 y: bounds.y + bounds.height - inside,
2162 },
2163 Point {
2164 x: coordinate,
2165 y: bounds.y + bounds.height + outside,
2166 },
2167 ),
2168 AxisPosition::Top => (
2169 Point {
2170 x: coordinate,
2171 y: bounds.y + inside,
2172 },
2173 Point {
2174 x: coordinate,
2175 y: bounds.y - outside,
2176 },
2177 ),
2178 AxisPosition::Left => (
2179 Point {
2180 x: bounds.x + inside,
2181 y: coordinate,
2182 },
2183 Point {
2184 x: bounds.x - outside,
2185 y: coordinate,
2186 },
2187 ),
2188 AxisPosition::Right => (
2189 Point {
2190 x: bounds.x + bounds.width - inside,
2191 y: coordinate,
2192 },
2193 Point {
2194 x: bounds.x + bounds.width + outside,
2195 y: coordinate,
2196 },
2197 ),
2198 };
2199 elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
2200}
2201
2202fn tick_label_origin(
2203 bounds: Rect,
2204 axis_position: AxisPosition,
2205 label_position: TickLabelPosition,
2206 coordinate: f64,
2207) -> Point {
2208 let side = match label_position {
2209 TickLabelPosition::NextTo => axis_position,
2210 TickLabelPosition::High => {
2211 if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
2212 AxisPosition::Top
2213 } else {
2214 AxisPosition::Right
2215 }
2216 }
2217 TickLabelPosition::Low => {
2218 if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
2219 AxisPosition::Bottom
2220 } else {
2221 AxisPosition::Left
2222 }
2223 }
2224 TickLabelPosition::None => axis_position,
2225 };
2226 match side {
2227 AxisPosition::Bottom => Point {
2228 x: coordinate - 4.0,
2229 y: bounds.y + bounds.height + 12.0,
2230 },
2231 AxisPosition::Top => Point {
2232 x: coordinate - 4.0,
2233 y: bounds.y - 4.0,
2234 },
2235 AxisPosition::Left => Point {
2236 x: bounds.x - 30.0,
2237 y: coordinate + 3.0,
2238 },
2239 AxisPosition::Right => Point {
2240 x: bounds.x + bounds.width + 4.0,
2241 y: coordinate + 3.0,
2242 },
2243 }
2244}
2245
2246fn format_axis_value(format: Option<&NumberFormat>, value: f64) -> Result<String> {
2247 if let Some(format) = format {
2248 format.format_value(value)
2249 } else {
2250 NumberFormat::new("General".to_owned(), false)?.format_value(value)
2251 }
2252}
2253
2254fn shape_label(fonts: &mut FontManager, text: &str, origin: Point) -> Result<GlyphRun> {
2255 shape_label_with_properties(fonts, text, origin, None)
2256}
2257
2258fn shape_label_with_properties(
2259 fonts: &mut FontManager,
2260 text: &str,
2261 origin: Point,
2262 properties: Option<&CT_TextBody>,
2263) -> Result<GlyphRun> {
2264 const FAMILY: &str = "Carlito";
2265 const SIZE: f64 = 9.0;
2266 let properties = properties.and_then(chart_text_properties);
2267 let family = properties
2268 .and_then(|properties| properties.latin.as_ref())
2269 .map(|font| font.typeface.as_str())
2270 .unwrap_or(FAMILY);
2271 let size = properties
2272 .and_then(|properties| properties.font_size)
2273 .map(|size| f64::from(size) / 100.0)
2274 .unwrap_or(SIZE);
2275 let bold = properties
2276 .and_then(|properties| properties.bold)
2277 .unwrap_or(false);
2278 let italic = properties
2279 .and_then(|properties| properties.italic)
2280 .unwrap_or(false);
2281 let font_id = fonts
2282 .resolve_font_for_text(Some(family), bold, italic, text)
2283 .map_err(|error| ChartError::InvalidValue {
2284 element: "chart label font".to_owned(),
2285 value: error.to_string(),
2286 })?;
2287 let shaped =
2288 fonts
2289 .shape_text(font_id, text, size)
2290 .map_err(|error| ChartError::InvalidValue {
2291 element: "chart label shaping".to_owned(),
2292 value: error.to_string(),
2293 })?;
2294 Ok(GlyphRun {
2295 origin,
2296 font_id,
2297 font_size: size,
2298 glyph_ids: shaped.glyph_ids,
2299 advances: shaped.advances,
2300 text: text.to_owned(),
2301 source: None,
2302 color: Color::BLACK,
2303 bold,
2304 italic,
2305 field_kind: None,
2306 field_source: None,
2307 note: None,
2308 note_reference_source: None,
2309 tab_aligned: None,
2310 })
2311}
2312
2313fn chart_text_properties(body: &CT_TextBody) -> Option<&CT_TextCharacterProperties> {
2314 body.paragraphs()
2315 .first()
2316 .and_then(|paragraph| paragraph.properties.as_ref())
2317 .and_then(|properties| properties.default_run_properties.as_ref())
2318}
2319
2320fn stroked_segment(start: Point, end: Point, color: Color, width: f64) -> PositionedElement {
2321 PositionedElement::Path(PathElement {
2322 path: Path {
2323 commands: vec![PathCommand::MoveTo(start), PathCommand::LineTo(end)],
2324 fill_rule: FillRule::NonZero,
2325 },
2326 fill: None,
2327 stroke: Some(Stroke::new(Paint::Solid(color), width)),
2328 })
2329}
2330
2331fn plot_series(plot: &Plot) -> &[Series] {
2332 match plot {
2333 Plot::Bar { series, .. }
2334 | Plot::Line { series, .. }
2335 | Plot::Pie { series, .. }
2336 | Plot::Doughnut { series, .. }
2337 | Plot::Area { series, .. }
2338 | Plot::Scatter { series, .. }
2339 | Plot::Radar { series, .. } => series,
2340 }
2341}
2342
2343fn plot_data_labels(plot: &Plot) -> Option<&CT_DLbls> {
2344 match plot {
2345 Plot::Bar { data_labels, .. }
2346 | Plot::Line { data_labels, .. }
2347 | Plot::Pie { data_labels, .. }
2348 | Plot::Doughnut { data_labels, .. }
2349 | Plot::Area { data_labels, .. }
2350 | Plot::Scatter { data_labels, .. }
2351 | Plot::Radar { data_labels, .. } => data_labels.as_ref(),
2352 }
2353}
2354
2355fn series_categories(
2356 series: &Series,
2357 number_format: Option<&NumberFormat>,
2358) -> Result<Vec<(usize, String)>> {
2359 let Some(categories) = &series.categories else {
2360 return Ok(Vec::new());
2361 };
2362 match categories {
2363 AxisData::String(data) => {
2364 let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
2365 indexes
2366 .into_iter()
2367 .zip(&data.values)
2368 .map(|(index, value)| {
2369 usize::try_from(index)
2370 .map(|index| (index, value.clone()))
2371 .map_err(|_| ChartError::InvalidValue {
2372 element: "c:cat/c:pt/@idx".to_owned(),
2373 value: index.to_string(),
2374 })
2375 })
2376 .collect()
2377 }
2378 AxisData::Numeric(data) => {
2379 let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
2380 let cache_format;
2381 let format = if let Some(format) = number_format {
2382 format
2383 } else {
2384 cache_format = NumberFormat::new(data.format_code.clone(), false)?;
2385 &cache_format
2386 };
2387 indexes
2388 .into_iter()
2389 .zip(&data.values)
2390 .map(|(index, value)| {
2391 let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
2392 element: "c:cat/c:pt/@idx".to_owned(),
2393 value: index.to_string(),
2394 })?;
2395 let text = format.format_value(*value)?;
2396 Ok((index, text))
2397 })
2398 .collect()
2399 }
2400 }
2401}
2402
2403fn series_name(series: &Series, index: usize) -> String {
2404 series
2405 .name
2406 .as_ref()
2407 .and_then(|name| name.values.first())
2408 .cloned()
2409 .unwrap_or_else(|| format!("Series {}", index + 1))
2410}
2411
2412fn render_data_labels(
2413 plot: &Plot,
2414 bounds: Rect,
2415 blanks: DispBlanksAs,
2416 options: GeometryOptions,
2417 fonts: &mut FontManager,
2418 elements: &mut Vec<PositionedElement>,
2419) -> Result<()> {
2420 let plot_labels = plot_data_labels(plot);
2421 let anchors = plot_label_anchors(plot, bounds, blanks, options)?;
2422 for (series_index, series) in plot_series(plot).iter().enumerate() {
2423 let Some(labels) = series.data_labels.as_ref().or(plot_labels) else {
2424 continue;
2425 };
2426 let overrides = point_label_overrides(labels)?;
2427 let mut categories = None;
2428 let mut bubble_sizes = None;
2429 let (_, values) = logical_numeric_values(&series.values)?;
2430 let mut percentage_total = None;
2431 for (logical_index, value) in values {
2432 let point_override = overrides.get(&logical_index);
2433 if point_override.is_some_and(|point| point.deleted == Some(true)) {
2434 continue;
2435 }
2436 let effective_labels = point_override
2437 .map(|point| point.apply_to(labels))
2438 .unwrap_or_else(|| labels.clone());
2439 let labels = &effective_labels;
2440 let Some(anchor) = anchors.get(&(series_index, logical_index)).copied() else {
2441 continue;
2442 };
2443 let mut fields = Vec::new();
2444 if labels.show_series_name {
2445 fields.push(series_name(series, series_index));
2446 }
2447 if labels.show_category_name {
2448 if categories.is_none() {
2449 categories = Some(
2450 series_categories(series, None)?
2451 .into_iter()
2452 .collect::<BTreeMap<_, _>>(),
2453 );
2454 }
2455 if let Some(category) = categories
2456 .as_ref()
2457 .and_then(|categories| categories.get(&logical_index))
2458 {
2459 fields.push(category.clone());
2460 }
2461 }
2462 if labels.show_value {
2463 fields.push(format_axis_value(labels.number_format.as_ref(), value)?);
2464 }
2465 if labels.show_percent {
2466 let total = if let Some(total) = percentage_total {
2467 total
2468 } else {
2469 let total = series
2470 .values
2471 .values
2472 .iter()
2473 .copied()
2474 .filter(|value| *value > 0.0)
2475 .try_fold(0.0, |total, value| {
2476 checked_geometry_sum(total, value, "data-label percentage total")
2477 })?;
2478 percentage_total = Some(total);
2479 total
2480 };
2481 if total > 0.0 {
2482 let text = if let Some(format) = &labels.number_format {
2483 format.format_value(value / total)?
2484 } else {
2485 NumberFormat::new("0%".to_owned(), false)?.format_value(value / total)?
2486 };
2487 fields.push(text);
2488 }
2489 }
2490 if labels.show_bubble_size {
2491 if bubble_sizes.is_none() {
2492 bubble_sizes = Some(
2493 series
2494 .bubble_size
2495 .as_ref()
2496 .map(logical_numeric_values)
2497 .transpose()?
2498 .map(|(_, values)| values.into_iter().collect::<BTreeMap<_, _>>())
2499 .unwrap_or_default(),
2500 );
2501 }
2502 if let Some(value) = bubble_sizes
2503 .as_ref()
2504 .and_then(|sizes| sizes.get(&logical_index))
2505 {
2506 fields.push(format_axis_value(labels.number_format.as_ref(), *value)?);
2507 }
2508 }
2509 if fields.is_empty() {
2510 continue;
2511 }
2512 let separator = labels.separator.as_deref().unwrap_or(", ");
2513 let text = fields.join(separator);
2514 let origin = anchor.origin(labels.position);
2515 elements.push(PositionedElement::Text(shape_label(fonts, &text, origin)?));
2516 }
2517 }
2518 Ok(())
2519}
2520
2521#[derive(Clone, Debug, Default)]
2522struct PointLabelOverride {
2523 index: Option<usize>,
2524 deleted: Option<bool>,
2525 number_format: Option<NumberFormat>,
2526 position: Option<DataLabelPosition>,
2527 separator: Option<String>,
2528 show_legend_key: Option<bool>,
2529 show_value: Option<bool>,
2530 show_category_name: Option<bool>,
2531 show_series_name: Option<bool>,
2532 show_percent: Option<bool>,
2533 show_bubble_size: Option<bool>,
2534}
2535
2536impl PointLabelOverride {
2537 fn apply_to(&self, labels: &CT_DLbls) -> CT_DLbls {
2538 let mut effective = labels.clone();
2539 if let Some(value) = &self.number_format {
2540 effective.number_format = Some(value.clone());
2541 }
2542 if let Some(value) = self.position {
2543 effective.position = Some(value);
2544 }
2545 if let Some(value) = &self.separator {
2546 effective.separator = Some(value.clone());
2547 }
2548 if let Some(value) = self.show_legend_key {
2549 effective.show_legend_key = value;
2550 }
2551 if let Some(value) = self.show_value {
2552 effective.show_value = value;
2553 }
2554 if let Some(value) = self.show_category_name {
2555 effective.show_category_name = value;
2556 }
2557 if let Some(value) = self.show_series_name {
2558 effective.show_series_name = value;
2559 }
2560 if let Some(value) = self.show_percent {
2561 effective.show_percent = value;
2562 }
2563 if let Some(value) = self.show_bubble_size {
2564 effective.show_bubble_size = value;
2565 }
2566 effective
2567 }
2568}
2569
2570fn point_label_overrides(labels: &CT_DLbls) -> Result<BTreeMap<usize, PointLabelOverride>> {
2571 let mut namespaces = chart_namespace_defaults();
2572 for (name, namespace) in &labels.namespace_declarations {
2573 let prefix = if name == "xmlns" {
2574 Vec::new()
2575 } else if let Some(prefix) = name.strip_prefix("xmlns:") {
2576 prefix.as_bytes().to_vec()
2577 } else {
2578 continue;
2579 };
2580 upsert_namespace_binding(&mut namespaces, prefix, namespace.clone());
2581 }
2582 let mut overrides = BTreeMap::new();
2583 for raw in labels.raw_children.at(0) {
2584 if chart_root_local(raw, &namespaces)?.as_deref() != Some(b"dLbl") {
2585 continue;
2586 }
2587 let point = parse_point_label_override(raw, &namespaces)?;
2588 let index = point
2589 .index
2590 .ok_or_else(|| ChartError::MissingElement("c:dLbl/c:idx".to_owned()))?;
2591 if overrides.insert(index, point).is_some() {
2592 return Err(ChartError::DuplicateElement(format!(
2593 "c:dLbl[c:idx={index}]"
2594 )));
2595 }
2596 }
2597 Ok(overrides)
2598}
2599
2600fn parse_point_label_override(
2601 xml: &[u8],
2602 inherited: &NamespaceBindings,
2603) -> Result<PointLabelOverride> {
2604 let mut reader = Reader::from_reader(xml);
2605 let mut buffer = Vec::new();
2606 let mut namespaces = None;
2607 let mut state = PointLabelOverride::default();
2608 let mut order = 0;
2609 loop {
2610 match reader
2611 .read_event_into(&mut buffer)
2612 .map_err(OxmlError::from)?
2613 {
2614 Event::Start(element) if namespaces.is_none() => {
2615 if !matches_local_name(element.name().as_ref(), b"dLbl")
2616 || !element_is_in_namespace(&element, C_NS, inherited)?
2617 {
2618 return Err(ChartError::UnexpectedElement(element_name(&element)));
2619 }
2620 namespaces = Some(chart_bindings(inherited, &element)?);
2621 }
2622 Event::Start(element) => {
2623 let bindings = namespaces
2624 .as_ref()
2625 .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
2626 let name = chart_child_local(&element, bindings)?;
2627 let raw = capture_element(&mut reader, &element)?;
2628 parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
2629 }
2630 Event::Empty(element) => {
2631 let bindings = namespaces
2632 .as_ref()
2633 .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
2634 let name = chart_child_local(&element, bindings)?;
2635 let raw = capture_empty_element(&element)?;
2636 parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
2637 }
2638 Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbl") => {
2639 if state.index.is_none() {
2640 return Err(ChartError::MissingElement("c:dLbl/c:idx".to_owned()));
2641 }
2642 return Ok(state);
2643 }
2644 Event::Eof => return Err(missing_end("c:dLbl")),
2645 _ => {}
2646 }
2647 buffer.clear();
2648 }
2649}
2650
2651fn parse_point_label_child(
2652 state: &mut PointLabelOverride,
2653 name: Option<&[u8]>,
2654 raw: &[u8],
2655 namespaces: &NamespaceBindings,
2656 order: &mut usize,
2657) -> Result<()> {
2658 let Some(name) = name else {
2659 return Ok(());
2660 };
2661 let rank = match name {
2662 b"idx" => 0,
2663 b"delete" => 1,
2664 b"layout" => 2,
2665 b"tx" => 3,
2666 b"numFmt" => 4,
2667 b"spPr" => 5,
2668 b"txPr" => 6,
2669 b"dLblPos" => 7,
2670 b"showLegendKey" => 8,
2671 b"showVal" => 9,
2672 b"showCatName" => 10,
2673 b"showSerName" => 11,
2674 b"showPercent" => 12,
2675 b"showBubbleSize" => 13,
2676 b"separator" => 14,
2677 b"showLeaderLines" => 15,
2678 b"leaderLines" => 16,
2679 b"extLst" => 17,
2680 _ => return Ok(()),
2681 };
2682 if rank < *order {
2683 return Err(ChartError::InvalidValue {
2684 element: "c:dLbl".to_owned(),
2685 value: format!("c:{} is out of schema order", String::from_utf8_lossy(name)),
2686 });
2687 }
2688 *order = rank;
2689 match name {
2690 b"idx" => {
2691 let (index, _) = parse_u32_scalar(raw, "idx")?;
2692 let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
2693 element: "c:dLbl/c:idx".to_owned(),
2694 value: index.to_string(),
2695 })?;
2696 set_once(&mut state.index, index, "c:dLbl/c:idx")?;
2697 }
2698 b"delete" => set_once(
2699 &mut state.deleted,
2700 parse_bool_value(raw, "delete")?.0,
2701 "c:dLbl/c:delete",
2702 )?,
2703 b"numFmt" => set_once(
2704 &mut state.number_format,
2705 NumberFormat::from_xml(raw)?,
2706 "c:dLbl/c:numFmt",
2707 )?,
2708 b"dLblPos" => {
2709 let (value, _) = scalar_value(raw, "dLblPos")?;
2710 let value =
2711 value.ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
2712 let position = DataLabelPosition::parse(&value)
2713 .ok_or_else(|| invalid_attribute("dLblPos", "val", value))?;
2714 set_once(&mut state.position, position, "c:dLbl/c:dLblPos")?;
2715 }
2716 b"showLegendKey" => set_once(
2717 &mut state.show_legend_key,
2718 parse_bool_value(raw, "showLegendKey")?.0,
2719 "c:dLbl/c:showLegendKey",
2720 )?,
2721 b"showVal" => set_once(
2722 &mut state.show_value,
2723 parse_bool_value(raw, "showVal")?.0,
2724 "c:dLbl/c:showVal",
2725 )?,
2726 b"showCatName" => set_once(
2727 &mut state.show_category_name,
2728 parse_bool_value(raw, "showCatName")?.0,
2729 "c:dLbl/c:showCatName",
2730 )?,
2731 b"showSerName" => set_once(
2732 &mut state.show_series_name,
2733 parse_bool_value(raw, "showSerName")?.0,
2734 "c:dLbl/c:showSerName",
2735 )?,
2736 b"showPercent" => set_once(
2737 &mut state.show_percent,
2738 parse_bool_value(raw, "showPercent")?.0,
2739 "c:dLbl/c:showPercent",
2740 )?,
2741 b"showBubbleSize" => set_once(
2742 &mut state.show_bubble_size,
2743 parse_bool_value(raw, "showBubbleSize")?.0,
2744 "c:dLbl/c:showBubbleSize",
2745 )?,
2746 b"separator" => set_once(
2747 &mut state.separator,
2748 parse_text_element(raw, b"separator", namespaces)?.0,
2749 "c:dLbl/c:separator",
2750 )?,
2751 _ => {}
2752 }
2753 Ok(())
2754}
2755
2756#[derive(Clone, Copy, Debug)]
2757struct LabelAnchor {
2758 center: Point,
2759 base: Point,
2760 end: Point,
2761 direction: Point,
2762}
2763
2764impl LabelAnchor {
2765 const fn point(center: Point) -> Self {
2766 Self {
2767 center,
2768 base: center,
2769 end: center,
2770 direction: Point { x: 0.0, y: 0.0 },
2771 }
2772 }
2773
2774 const fn segment(center: Point, base: Point, end: Point) -> Self {
2775 Self {
2776 center,
2777 base,
2778 end,
2779 direction: Point {
2780 x: end.x - base.x,
2781 y: end.y - base.y,
2782 },
2783 }
2784 }
2785
2786 const fn directed_segment(center: Point, base: Point, end: Point, direction: Point) -> Self {
2787 Self {
2788 center,
2789 base,
2790 end,
2791 direction,
2792 }
2793 }
2794
2795 fn origin(self, position: Option<DataLabelPosition>) -> Point {
2796 let mut origin = self.center;
2797 match position.unwrap_or(DataLabelPosition::BestFit) {
2798 DataLabelPosition::Bottom => origin.y += 8.0,
2799 DataLabelPosition::Top => origin.y -= 5.0,
2800 DataLabelPosition::Left => origin.x -= 8.0,
2801 DataLabelPosition::Right => origin.x += 8.0,
2802 DataLabelPosition::InsideBase => {
2803 origin = self.along_segment(self.base, 5.0, true).unwrap_or(Point {
2804 x: origin.x,
2805 y: origin.y + 8.0,
2806 });
2807 }
2808 DataLabelPosition::InsideEnd => {
2809 origin = self.along_segment(self.end, -5.0, true).unwrap_or(origin);
2810 }
2811 DataLabelPosition::OutsideEnd => {
2812 origin = self.along_segment(self.end, 5.0, false).unwrap_or(Point {
2813 x: origin.x,
2814 y: origin.y - 5.0,
2815 });
2816 }
2817 DataLabelPosition::BestFit | DataLabelPosition::Center => {}
2818 }
2819 origin
2820 }
2821
2822 fn along_segment(self, origin: Point, distance: f64, clamp_inside: bool) -> Option<Point> {
2823 let dx = self.end.x - self.base.x;
2824 let dy = self.end.y - self.base.y;
2825 let length = dx.hypot(dy);
2826 let (dx, dy, direction_length) = if length.is_finite() && length > 0.0 {
2827 (dx, dy, length)
2828 } else {
2829 let direction_length = self.direction.x.hypot(self.direction.y);
2830 if !direction_length.is_finite() || direction_length == 0.0 {
2831 return None;
2832 }
2833 (self.direction.x, self.direction.y, direction_length)
2834 };
2835 let distance = if clamp_inside {
2836 distance.clamp(-length / 2.0, length / 2.0)
2837 } else {
2838 distance
2839 };
2840 Some(Point {
2841 x: origin.x + dx / direction_length * distance,
2842 y: origin.y + dy / direction_length * distance,
2843 })
2844 }
2845}
2846
2847fn plot_label_anchors(
2848 plot: &Plot,
2849 bounds: Rect,
2850 blanks: DispBlanksAs,
2851 options: GeometryOptions,
2852) -> Result<BTreeMap<(usize, usize), LabelAnchor>> {
2853 let mut anchors = BTreeMap::new();
2854 match plot {
2855 Plot::Bar {
2856 direction,
2857 grouping,
2858 gap_width,
2859 overlap,
2860 series,
2861 ..
2862 } => {
2863 for bar in bar_rectangles(
2864 *direction, *grouping, *gap_width, *overlap, series, bounds, options,
2865 )? {
2866 if !point_in_rect(bar.end, bounds) {
2867 continue;
2868 }
2869 let visible_base = Point {
2870 x: bar.base.x.clamp(bounds.x, bounds.x + bounds.width),
2871 y: bar.base.y.clamp(bounds.y, bounds.y + bounds.height),
2872 };
2873 let visible_end = Point {
2874 x: bar.end.x.clamp(bounds.x, bounds.x + bounds.width),
2875 y: bar.end.y.clamp(bounds.y, bounds.y + bounds.height),
2876 };
2877 anchors.insert(
2878 (bar.series_index, bar.logical_index),
2879 LabelAnchor::directed_segment(
2880 Point {
2881 x: (visible_base.x + visible_end.x) / 2.0,
2882 y: (visible_base.y + visible_end.y) / 2.0,
2883 },
2884 visible_base,
2885 visible_end,
2886 Point {
2887 x: bar.end.x - bar.base.x,
2888 y: bar.end.y - bar.base.y,
2889 },
2890 ),
2891 );
2892 }
2893 }
2894 Plot::Line {
2895 grouping, series, ..
2896 }
2897 | Plot::Area {
2898 grouping, series, ..
2899 } => {
2900 for (series_index, points) in
2901 category_label_points(*grouping, series, bounds, blanks, options)?
2902 .into_iter()
2903 .enumerate()
2904 {
2905 for (logical_index, point) in points {
2906 anchors.insert((series_index, logical_index), LabelAnchor::point(point));
2907 }
2908 }
2909 }
2910 Plot::Scatter { series, .. } => {
2911 let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
2912 let x_mapping = options.scatter_x.unwrap_or(AxisMapping {
2913 domain: x_domain,
2914 orientation: Orientation::MinMax,
2915 });
2916 let y_mapping = options.scatter_y.unwrap_or(AxisMapping {
2917 domain: y_domain,
2918 orientation: Orientation::MinMax,
2919 });
2920 for (series_index, series) in series.iter().enumerate() {
2921 let indexed = matched_scatter_points(series)?;
2922 let points = scatter_points(
2923 &indexed,
2924 x_mapping.domain,
2925 y_mapping.domain,
2926 bounds,
2927 x_mapping.orientation,
2928 y_mapping.orientation,
2929 )?;
2930 for ((logical_index, _, _), point) in indexed.into_iter().zip(points) {
2931 anchors.insert((series_index, logical_index), LabelAnchor::point(point));
2932 }
2933 }
2934 }
2935 Plot::Radar { series, .. } => {
2936 let center = Point {
2937 x: bounds.x + bounds.width / 2.0,
2938 y: bounds.y + bounds.height / 2.0,
2939 };
2940 let category_count = series_category_count(series)?;
2941 for (series_index, points) in radar_series_points(series, bounds, options)?
2942 .into_iter()
2943 .enumerate()
2944 {
2945 for (logical_index, point) in points {
2946 let spoke = radial_point(
2947 center,
2948 1.0,
2949 oriented_category_index(
2950 logical_index,
2951 category_count,
2952 options.category_orientation,
2953 ),
2954 category_count,
2955 );
2956 anchors.insert(
2957 (series_index, logical_index),
2958 LabelAnchor::directed_segment(
2959 point,
2960 center,
2961 point,
2962 Point {
2963 x: spoke.x - center.x,
2964 y: spoke.y - center.y,
2965 },
2966 ),
2967 );
2968 }
2969 }
2970 }
2971 Plot::Pie {
2972 first_slice_angle,
2973 series,
2974 ..
2975 } => {
2976 for slice in pie_slices(*first_slice_angle, None, series, bounds)? {
2977 anchors.insert(
2978 (slice.series_index, slice.logical_index),
2979 LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2980 );
2981 }
2982 }
2983 Plot::Doughnut {
2984 first_slice_angle,
2985 hole_size,
2986 series,
2987 ..
2988 } => {
2989 for slice in pie_slices(*first_slice_angle, Some(*hole_size), series, bounds)? {
2990 anchors.insert(
2991 (slice.series_index, slice.logical_index),
2992 LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2993 );
2994 }
2995 }
2996 }
2997 anchors.retain(|_, anchor| point_in_rect(anchor.end, bounds));
2998 Ok(anchors)
2999}
3000
3001fn category_label_points(
3002 grouping: Grouping,
3003 series: &[Series],
3004 bounds: Rect,
3005 blanks: DispBlanksAs,
3006 options: GeometryOptions,
3007) -> Result<Vec<Vec<(usize, Point)>>> {
3008 validate_series_values(series)?;
3009 let logical_series = logical_series_values(series)?;
3010 let category_count = series_category_count(series)?;
3011 let calculated_series = if blanks == DispBlanksAs::Zero {
3012 zero_filled_series(&logical_series, category_count)
3013 } else {
3014 logical_series.clone()
3015 };
3016 let layers = stacked_series_bounds(
3017 &calculated_series,
3018 grouping != Grouping::Standard,
3019 grouping == Grouping::PercentStacked,
3020 )?;
3021 let domain = options
3022 .value
3023 .map(|mapping| mapping.domain)
3024 .unwrap_or(domain_from_layers(&layers)?);
3025 let value_orientation = options
3026 .value
3027 .map(|mapping| mapping.orientation)
3028 .unwrap_or(Orientation::MinMax);
3029 let mut point_series = Vec::with_capacity(layers.len());
3030 for (series_index, layer) in layers.iter().enumerate() {
3031 let present: BTreeSet<_> = logical_series[series_index]
3032 .1
3033 .iter()
3034 .map(|(index, _)| *index)
3035 .collect();
3036 let indexed: Vec<_> = layer
3037 .iter()
3038 .filter(|(index, _, _)| present.contains(index))
3039 .map(|(index, _, upper)| (*index, *upper))
3040 .collect();
3041 let points = category_points(
3042 &indexed,
3043 category_count,
3044 domain,
3045 bounds,
3046 options.category_orientation,
3047 value_orientation,
3048 )?;
3049 point_series.push(
3050 indexed
3051 .into_iter()
3052 .map(|(index, _)| index)
3053 .zip(points)
3054 .collect(),
3055 );
3056 }
3057 Ok(point_series)
3058}
3059
3060fn point_in_rect(point: Point, bounds: Rect) -> bool {
3061 const EPSILON: f64 = 1.0e-9;
3062 point.x >= bounds.x - EPSILON
3063 && point.x <= bounds.x + bounds.width + EPSILON
3064 && point.y >= bounds.y - EPSILON
3065 && point.y <= bounds.y + bounds.height + EPSILON
3066}
3067
3068fn resolve_series_colours(
3069 plot: &Plot,
3070 theme: &CT_OfficeStyleSheet,
3071 color_map: &ColorMap,
3072) -> Result<Vec<Color>> {
3073 let lookup = theme_colour_lookup(theme);
3074 plot_series(plot)
3075 .iter()
3076 .enumerate()
3077 .map(|(index, series)| {
3078 if let Some(properties) = series.sp_pr.as_ref() {
3079 if let Some(fill) = properties.fill.as_ref() {
3080 return resolve_series_paint(fill, index, "fill", theme, color_map, &lookup);
3081 }
3082 if let Some(fill) = properties.line.as_ref().and_then(|line| line.fill.as_ref()) {
3083 return resolve_series_paint(fill, index, "line", theme, color_map, &lookup);
3084 }
3085 }
3086 resolve_series_accent(index, theme, color_map, &lookup)
3087 })
3088 .collect()
3089}
3090
3091fn resolve_series_paint(
3092 fill: &Fill,
3093 series_index: usize,
3094 source: &str,
3095 theme: &CT_OfficeStyleSheet,
3096 color_map: &ColorMap,
3097 lookup: &[(&str, RgbColor)],
3098) -> Result<Color> {
3099 match fill {
3100 Fill::NoFill(_) => Ok(Color {
3101 r: 0.0,
3102 g: 0.0,
3103 b: 0.0,
3104 a: 0.0,
3105 }),
3106 Fill::Solid(solid) => {
3107 let choice = solid
3108 .color
3109 .as_ref()
3110 .ok_or_else(|| ChartError::InvalidValue {
3111 element: format!("c:ser[{series_index}] direct {source} paint"),
3112 value: "solid fill has no colour choice".to_owned(),
3113 })?;
3114 resolve_direct_chart_colour(choice, theme, color_map, lookup, || {
3115 format!("c:ser[{series_index}] direct {source} paint")
3116 })
3117 }
3118 Fill::Gradient(_) => unsupported_series_paint(series_index, source, "gradient"),
3119 Fill::Pattern(_) => unsupported_series_paint(series_index, source, "pattern"),
3120 Fill::Blip(_) => unsupported_series_paint(series_index, source, "picture"),
3121 }
3122}
3123
3124fn resolve_direct_chart_colour(
3125 choice: &ColorChoice,
3126 theme: &CT_OfficeStyleSheet,
3127 color_map: &ColorMap,
3128 lookup: &[(&str, RgbColor)],
3129 element: impl FnOnce() -> String,
3130) -> Result<Color> {
3131 let ColorChoice::Scheme {
3132 value, transforms, ..
3133 } = choice
3134 else {
3135 return resolve_chart_colour(choice, color_map, lookup, element);
3136 };
3137 let theme_slot =
3138 mapped_theme_slot(value, color_map).ok_or_else(|| ChartError::InvalidValue {
3139 element: element(),
3140 value: format!("no concrete theme slot is available for scheme colour {value}"),
3141 })?;
3142 let theme_choice = theme.theme_elements.color_scheme.color(theme_slot);
3143 let base = concrete_theme_colour(theme_choice).ok_or_else(|| ChartError::InvalidValue {
3144 element: format!("a:clrScheme/a:{}", theme_slot.as_str()),
3145 value: "selected theme slot has no concrete sRGB or system fallback colour".to_owned(),
3146 })?;
3147 let mut combined = theme_choice.transforms().to_vec();
3148 combined.extend_from_slice(transforms);
3149 Ok(resolved_layout_colour(apply_color_transforms(
3150 base, &combined,
3151 )))
3152}
3153
3154fn mapped_theme_slot(value: &str, color_map: &ColorMap) -> Option<ThemeColorSlot> {
3155 let mapped = match value {
3156 "bg1" => color_map.theme_slot(ColorMapSlot::Background1),
3157 "tx1" => color_map.theme_slot(ColorMapSlot::Text1),
3158 "bg2" => color_map.theme_slot(ColorMapSlot::Background2),
3159 "tx2" => color_map.theme_slot(ColorMapSlot::Text2),
3160 "accent1" => color_map.theme_slot(ColorMapSlot::Accent1),
3161 "accent2" => color_map.theme_slot(ColorMapSlot::Accent2),
3162 "accent3" => color_map.theme_slot(ColorMapSlot::Accent3),
3163 "accent4" => color_map.theme_slot(ColorMapSlot::Accent4),
3164 "accent5" => color_map.theme_slot(ColorMapSlot::Accent5),
3165 "accent6" => color_map.theme_slot(ColorMapSlot::Accent6),
3166 "hlink" => color_map.theme_slot(ColorMapSlot::Hyperlink),
3167 "folHlink" => color_map.theme_slot(ColorMapSlot::FollowedHyperlink),
3168 "dk1" => ThemeColorSlot::Dark1,
3169 "lt1" => ThemeColorSlot::Light1,
3170 "dk2" => ThemeColorSlot::Dark2,
3171 "lt2" => ThemeColorSlot::Light2,
3172 _ => return None,
3173 };
3174 Some(mapped)
3175}
3176
3177fn unsupported_series_paint(series_index: usize, source: &str, kind: &str) -> Result<Color> {
3178 Err(ChartError::InvalidValue {
3179 element: format!("c:ser[{series_index}] direct {source} paint"),
3180 value: format!("unsupported {kind} paint"),
3181 })
3182}
3183
3184fn resolve_series_accent(
3185 series_index: usize,
3186 theme: &CT_OfficeStyleSheet,
3187 color_map: &ColorMap,
3188 lookup: &[(&str, RgbColor)],
3189) -> Result<Color> {
3190 const ACCENTS: [ColorMapSlot; 6] = [
3191 ColorMapSlot::Accent1,
3192 ColorMapSlot::Accent2,
3193 ColorMapSlot::Accent3,
3194 ColorMapSlot::Accent4,
3195 ColorMapSlot::Accent5,
3196 ColorMapSlot::Accent6,
3197 ];
3198 let semantic_slot = ACCENTS[series_index % ACCENTS.len()];
3199 let theme_slot = color_map.theme_slot(semantic_slot);
3200 let choice = theme.theme_elements.color_scheme.color(theme_slot);
3201 resolve_chart_colour(choice, color_map, lookup, || {
3202 format!("c:ser[{series_index}] theme accent")
3203 })
3204}
3205
3206fn resolve_chart_colour(
3207 choice: &ColorChoice,
3208 color_map: &ColorMap,
3209 lookup: &[(&str, RgbColor)],
3210 element: impl FnOnce() -> String,
3211) -> Result<Color> {
3212 resolve_color(choice, color_map, lookup)
3213 .map(resolved_layout_colour)
3214 .map_err(|error| ChartError::InvalidValue {
3215 element: element(),
3216 value: error.to_string(),
3217 })
3218}
3219
3220fn theme_colour_lookup(theme: &CT_OfficeStyleSheet) -> Vec<(&'static str, RgbColor)> {
3221 theme
3222 .theme_elements
3223 .color_scheme
3224 .iter()
3225 .filter_map(|(slot, choice)| {
3226 concrete_theme_colour(choice).map(|colour| (slot.as_str(), colour))
3227 })
3228 .collect()
3229}
3230
3231fn concrete_theme_colour(choice: &ColorChoice) -> Option<RgbColor> {
3232 match choice {
3233 ColorChoice::Srgb { value, .. } => Some(*value),
3234 ColorChoice::System { last_color, .. } => *last_color,
3235 ColorChoice::Scheme { .. } | ColorChoice::Preset { .. } => None,
3236 }
3237}
3238
3239fn resolved_layout_colour(colour: ResolvedColor) -> Color {
3240 Color {
3241 r: f64::from(colour.red) / 255.0,
3242 g: f64::from(colour.green) / 255.0,
3243 b: f64::from(colour.blue) / 255.0,
3244 a: f64::from(colour.alpha) / 255.0,
3245 }
3246}
3247
3248struct LegendLayout {
3249 plot: Rect,
3250 labels: Vec<GlyphRun>,
3251 keys: Vec<Rect>,
3252}
3253
3254fn layout_legend(
3255 chart: &CT_Chart,
3256 plot: &Plot,
3257 bounds: Rect,
3258 fonts: &mut FontManager,
3259) -> Result<LegendLayout> {
3260 let full_plot = geometry_plot_bounds(bounds)?;
3261 let Some(legend) = &chart.legend else {
3262 return Ok(LegendLayout {
3263 plot: full_plot,
3264 labels: Vec::new(),
3265 keys: Vec::new(),
3266 });
3267 };
3268 let settings = legend.settings;
3269 let horizontal = matches!(
3270 settings.position,
3271 LegendPosition::Top | LegendPosition::Bottom
3272 );
3273 if plot_series(plot).len() > MAX_CATEGORY_ANNOTATIONS {
3274 return Err(invalid_authoring_value("legend entry limit"));
3275 }
3276 let mut labels = plot_series(plot)
3277 .iter()
3278 .enumerate()
3279 .map(|(index, series)| {
3280 shape_label(fonts, &series_name(series, index), Point { x: 0.0, y: 0.0 })
3281 })
3282 .collect::<Result<Vec<_>>>()?;
3283 let available = if settings.overlay { full_plot } else { bounds };
3284 let capacity = available.width - 12.0;
3285 let row_height = labels.first().map_or(0.0, |label| label.font_size * 1.875);
3288 let mut positions = Vec::with_capacity(labels.len());
3289 let mut width: f64 = 0.0;
3290 let mut x = 0.0;
3291 let mut y = 0.0;
3292 for label in &labels {
3293 let entry_width = label.advances.iter().sum::<f64>() + label.font_size * 7.0 / 9.0 + 4.0;
3294 if !entry_width.is_finite() || entry_width > capacity {
3295 return Err(invalid_authoring_value("legend label exceeds chart width"));
3296 }
3297 if horizontal && x > 0.0 && x + entry_width > capacity {
3298 x = 0.0;
3299 y += row_height;
3300 }
3301 positions.push(Point { x, y });
3302 width = width.max(x + entry_width);
3303 if horizontal {
3304 x += entry_width + 4.0;
3305 } else {
3306 y += row_height;
3307 }
3308 }
3309 let height = if labels.is_empty() {
3310 0.0
3311 } else if horizontal {
3312 y + row_height
3313 } else {
3314 y
3315 };
3316 if height > available.height - 12.0 {
3317 return Err(invalid_authoring_value(
3318 "legend entries exceed chart height",
3319 ));
3320 }
3321 let mut content = bounds;
3322 if !settings.overlay && !labels.is_empty() {
3323 match settings.position {
3324 LegendPosition::Right | LegendPosition::TopRight => content.width -= width + 10.0,
3325 LegendPosition::Left => {
3326 content.x += width + 10.0;
3327 content.width -= width + 10.0;
3328 }
3329 LegendPosition::Top => {
3330 content.y += height + 10.0;
3331 content.height -= height + 10.0;
3332 }
3333 LegendPosition::Bottom => content.height -= height + 10.0,
3334 }
3335 }
3336 let plot_bounds = geometry_plot_bounds(content)?;
3337 let area = if settings.overlay {
3338 plot_bounds
3339 } else {
3340 bounds
3341 };
3342 let origin = match settings.position {
3343 LegendPosition::Right => Point {
3344 x: area.x + area.width - width - 6.0,
3345 y: area.y + (area.height - height) / 2.0,
3346 },
3347 LegendPosition::TopRight => Point {
3348 x: area.x + area.width - width - 6.0,
3349 y: area.y + 6.0,
3350 },
3351 LegendPosition::Left => Point {
3352 x: area.x + 6.0,
3353 y: area.y + (area.height - height) / 2.0,
3354 },
3355 LegendPosition::Top => Point {
3356 x: area.x + (area.width - width) / 2.0,
3357 y: area.y + 6.0,
3358 },
3359 LegendPosition::Bottom => Point {
3360 x: area.x + (area.width - width) / 2.0,
3361 y: area.y + area.height - height - 6.0,
3362 },
3363 };
3364 let mut keys = Vec::with_capacity(labels.len());
3365 for (label, offset) in labels.iter_mut().zip(positions) {
3366 let x = origin.x + offset.x;
3367 let y = origin.y + offset.y;
3368 keys.push(Rect {
3369 x,
3370 y: y + (row_height - label.font_size * 5.0 / 9.0) / 2.0,
3371 width: label.font_size * 5.0 / 9.0,
3372 height: label.font_size * 5.0 / 9.0,
3373 });
3374 label.origin = Point {
3375 x: x + label.font_size * 7.0 / 9.0,
3376 y: y + label.font_size * 7.0 / 6.0,
3377 };
3378 }
3379 Ok(LegendLayout {
3380 plot: plot_bounds,
3381 labels,
3382 keys,
3383 })
3384}
3385
3386fn render_legend(
3387 layout: LegendLayout,
3388 series_colours: &[Color],
3389 paths: &mut Vec<PositionedElement>,
3390 labels: &mut Vec<PositionedElement>,
3391) {
3392 for (key, colour) in layout.keys.into_iter().zip(series_colours) {
3393 paths.push(filled_path(Path::rect(key), *colour));
3394 }
3395 labels.extend(layout.labels.into_iter().map(PositionedElement::Text));
3396}
3397
3398fn validate_geometry_coordinates(elements: &[PositionedElement]) -> Result<()> {
3399 fn finite_point(point: Point) -> bool {
3400 point.x.is_finite() && point.y.is_finite()
3401 }
3402
3403 for element in elements {
3404 match element {
3405 PositionedElement::Path(path) => {
3406 for command in &path.path.commands {
3407 let finite = match command {
3408 PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
3409 finite_point(*point)
3410 }
3411 PathCommand::CurveTo { c1, c2, to } => {
3412 finite_point(*c1) && finite_point(*c2) && finite_point(*to)
3413 }
3414 PathCommand::Close => true,
3415 };
3416 if !finite {
3417 return Err(ChartError::InvalidValue {
3418 element: "chart geometry coordinate".to_owned(),
3419 value: "generated path contains a nonfinite point".to_owned(),
3420 });
3421 }
3422 }
3423 }
3424 PositionedElement::Group(group) => validate_geometry_coordinates(&group.children)?,
3425 _ => {}
3426 }
3427 }
3428 Ok(())
3429}
3430
3431fn render_plot_geometry(
3432 plot: &Plot,
3433 bounds: Rect,
3434 blanks: DispBlanksAs,
3435 options: GeometryOptions,
3436 series_colours: &[Color],
3437) -> Result<Vec<PositionedElement>> {
3438 match plot {
3439 Plot::Bar {
3440 direction,
3441 grouping,
3442 gap_width,
3443 overlap,
3444 series,
3445 ..
3446 } => render_bar_geometry(
3447 *direction,
3448 *grouping,
3449 *gap_width,
3450 *overlap,
3451 series,
3452 bounds,
3453 options,
3454 series_colours,
3455 ),
3456 Plot::Line {
3457 grouping,
3458 marker,
3459 series,
3460 ..
3461 } => render_line_geometry(
3462 *grouping,
3463 *marker,
3464 series,
3465 bounds,
3466 blanks,
3467 options,
3468 series_colours,
3469 ),
3470 Plot::Pie {
3471 first_slice_angle,
3472 series,
3473 ..
3474 } => render_pie_geometry(*first_slice_angle, None, series, bounds, series_colours),
3475 Plot::Doughnut {
3476 first_slice_angle,
3477 hole_size,
3478 series,
3479 ..
3480 } => render_pie_geometry(
3481 *first_slice_angle,
3482 Some(*hole_size),
3483 series,
3484 bounds,
3485 series_colours,
3486 ),
3487 Plot::Area {
3488 grouping, series, ..
3489 } => render_area_geometry(*grouping, series, bounds, blanks, options, series_colours),
3490 Plot::Scatter { style, series, .. } => {
3491 render_scatter_geometry(*style, series, bounds, blanks, options, series_colours)
3492 }
3493 Plot::Radar { style, series, .. } => {
3494 render_radar_geometry(*style, series, bounds, options, series_colours)
3495 }
3496 }
3497}
3498
3499#[allow(clippy::too_many_arguments)]
3500fn render_bar_geometry(
3501 direction: BarDirection,
3502 grouping: BarGrouping,
3503 gap_width: u16,
3504 overlap: i8,
3505 series: &[Series],
3506 bounds: Rect,
3507 options: GeometryOptions,
3508 series_colours: &[Color],
3509) -> Result<Vec<PositionedElement>> {
3510 let bars = bar_rectangles(
3511 direction, grouping, gap_width, overlap, series, bounds, options,
3512 )?;
3513 bars.into_iter()
3514 .map(|bar| {
3515 Ok(filled_path(
3516 Path::rect(bar.rect),
3517 series_colour(series_colours, bar.series_index)?,
3518 ))
3519 })
3520 .collect()
3521}
3522
3523#[derive(Clone, Copy, Debug)]
3524struct BarRectangle {
3525 series_index: usize,
3526 logical_index: usize,
3527 rect: Rect,
3528 base: Point,
3529 end: Point,
3530}
3531
3532fn bar_rectangles(
3533 direction: BarDirection,
3534 grouping: BarGrouping,
3535 gap_width: u16,
3536 overlap: i8,
3537 series: &[Series],
3538 bounds: Rect,
3539 options: GeometryOptions,
3540) -> Result<Vec<BarRectangle>> {
3541 validate_series_values(series)?;
3542 let logical_series = logical_series_values(series)?;
3543 let category_count = series_category_count(series)?;
3544 let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
3545 let percent = grouping == BarGrouping::PercentStacked;
3546 let layers = stacked_series_bounds(&logical_series, stacked, percent)?;
3547 let domain = options
3548 .value
3549 .map(|mapping| mapping.domain)
3550 .unwrap_or(domain_from_layers(&layers)?);
3551 let value_orientation = options
3552 .value
3553 .map(|mapping| mapping.orientation)
3554 .unwrap_or(Orientation::MinMax);
3555 let category_extent = if direction == BarDirection::Column {
3556 bounds.width
3557 } else {
3558 bounds.height
3559 };
3560 let slot = category_extent / category_count as f64;
3561 let cluster_extent = slot * 100.0 / (100.0 + f64::from(gap_width));
3562 let bars_per_cluster = if stacked { 1 } else { series.len() };
3563 let overlap_fraction = if stacked {
3564 0.0
3565 } else {
3566 f64::from(overlap) / 100.0
3567 };
3568 let divisor =
3569 bars_per_cluster as f64 - (bars_per_cluster.saturating_sub(1)) as f64 * overlap_fraction;
3570 let bar_extent = cluster_extent / divisor;
3571 let advance = bar_extent * (1.0 - overlap_fraction);
3572 let mut bars = Vec::new();
3573 for (series_index, layer) in layers.iter().enumerate() {
3574 for &(logical_index, start, end) in layer {
3575 let category = oriented_category_index(
3576 logical_index,
3577 category_count,
3578 options.category_orientation,
3579 );
3580 let cluster_start = category as f64 / category_count as f64 * category_extent
3581 + (slot - cluster_extent) / 2.0;
3582 let category_start = if stacked {
3583 cluster_start
3584 } else {
3585 cluster_start + series_index as f64 * advance
3586 };
3587 let (rect, base, end) = if direction == BarDirection::Column {
3588 let y0 = map_y_oriented(start, domain, bounds, value_orientation)?;
3589 let y1 = map_y_oriented(end, domain, bounds, value_orientation)?;
3590 let center_x = bounds.x + category_start + bar_extent / 2.0;
3591 (
3592 Rect {
3593 x: bounds.x + category_start,
3594 y: y0.min(y1),
3595 width: bar_extent,
3596 height: (y1 - y0).abs(),
3597 },
3598 Point { x: center_x, y: y0 },
3599 Point { x: center_x, y: y1 },
3600 )
3601 } else {
3602 let x0 = map_x_oriented(start, domain, bounds, value_orientation)?;
3603 let x1 = map_x_oriented(end, domain, bounds, value_orientation)?;
3604 let center_y = bounds.y + category_start + bar_extent / 2.0;
3605 (
3606 Rect {
3607 x: x0.min(x1),
3608 y: bounds.y + category_start,
3609 width: (x1 - x0).abs(),
3610 height: bar_extent,
3611 },
3612 Point { x: x0, y: center_y },
3613 Point { x: x1, y: center_y },
3614 )
3615 };
3616 bars.push(BarRectangle {
3617 series_index,
3618 logical_index,
3619 rect,
3620 base,
3621 end,
3622 });
3623 }
3624 }
3625 Ok(bars)
3626}
3627
3628fn render_line_geometry(
3629 grouping: Grouping,
3630 marker: bool,
3631 series: &[Series],
3632 bounds: Rect,
3633 blanks: DispBlanksAs,
3634 options: GeometryOptions,
3635 series_colours: &[Color],
3636) -> Result<Vec<PositionedElement>> {
3637 validate_series_values(series)?;
3638 let logical_series = logical_series_values(series)?;
3639 let category_count = series_category_count(series)?;
3640 let calculated_series = if blanks == DispBlanksAs::Zero {
3641 zero_filled_series(&logical_series, category_count)
3642 } else {
3643 logical_series.clone()
3644 };
3645 let stacked = grouping != Grouping::Standard;
3646 let percent = grouping == Grouping::PercentStacked;
3647 let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
3648 let domain = options
3649 .value
3650 .map(|mapping| mapping.domain)
3651 .unwrap_or(domain_from_layers(&layers)?);
3652 let value_orientation = options
3653 .value
3654 .map(|mapping| mapping.orientation)
3655 .unwrap_or(Orientation::MinMax);
3656 let mut elements = Vec::new();
3657 for (series_index, layer) in layers.iter().enumerate() {
3658 let colour = series_colour(series_colours, series_index)?;
3659 let indexed: Vec<_> = layer
3660 .iter()
3661 .map(|(index, _, upper)| (*index, *upper))
3662 .collect();
3663 let indexes: Vec<_> = indexed.iter().map(|(index, _)| *index).collect();
3664 for (start, end) in contiguous_ranges(&indexes, blanks) {
3665 let points = category_points(
3666 &indexed[start..end],
3667 category_count,
3668 domain,
3669 bounds,
3670 options.category_orientation,
3671 value_orientation,
3672 )?;
3673 elements.push(stroked_path(polyline_path(&points, false), colour));
3674 }
3675 if marker {
3676 let present: BTreeSet<_> = logical_series[series_index]
3677 .1
3678 .iter()
3679 .map(|(index, _)| *index)
3680 .collect();
3681 let marker_values: Vec<_> = indexed
3682 .iter()
3683 .copied()
3684 .filter(|(index, _)| present.contains(index))
3685 .collect();
3686 let marker_points = category_points(
3687 &marker_values,
3688 category_count,
3689 domain,
3690 bounds,
3691 options.category_orientation,
3692 value_orientation,
3693 )?;
3694 push_markers(&mut elements, &marker_points, colour);
3695 }
3696 }
3697 Ok(elements)
3698}
3699
3700fn render_area_geometry(
3701 grouping: Grouping,
3702 series: &[Series],
3703 bounds: Rect,
3704 blanks: DispBlanksAs,
3705 options: GeometryOptions,
3706 series_colours: &[Color],
3707) -> Result<Vec<PositionedElement>> {
3708 validate_series_values(series)?;
3709 let logical_series = logical_series_values(series)?;
3710 let category_count = series_category_count(series)?;
3711 let calculated_series = if blanks == DispBlanksAs::Zero {
3712 zero_filled_series(&logical_series, category_count)
3713 } else {
3714 logical_series
3715 };
3716 let stacked = grouping != Grouping::Standard;
3717 let percent = grouping == Grouping::PercentStacked;
3718 let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
3719 let domain = options
3720 .value
3721 .map(|mapping| mapping.domain)
3722 .unwrap_or(domain_from_layers(&layers)?);
3723 let value_orientation = options
3724 .value
3725 .map(|mapping| mapping.orientation)
3726 .unwrap_or(Orientation::MinMax);
3727 let mut elements = Vec::new();
3728 for (series_index, layer) in layers.iter().enumerate() {
3729 let colour = series_colour(series_colours, series_index)?;
3730 let indexes: Vec<_> = layer.iter().map(|(index, _, _)| *index).collect();
3731 for (start, end) in contiguous_ranges(&indexes, blanks) {
3732 let segment = &layer[start..end];
3733 let top_values: Vec<_> = segment
3734 .iter()
3735 .map(|(index, _, upper)| (*index, *upper))
3736 .collect();
3737 let lower_values: Vec<_> = segment
3738 .iter()
3739 .map(|(index, lower, _)| (*index, *lower))
3740 .collect();
3741 let top = category_points(
3742 &top_values,
3743 category_count,
3744 domain,
3745 bounds,
3746 options.category_orientation,
3747 value_orientation,
3748 )?;
3749 let mut bottom = category_points(
3750 &lower_values,
3751 category_count,
3752 domain,
3753 bounds,
3754 options.category_orientation,
3755 value_orientation,
3756 )?;
3757 bottom.reverse();
3758 let mut commands = Vec::with_capacity(top.len() + bottom.len() + 2);
3759 commands.push(PathCommand::MoveTo(top[0]));
3760 commands.extend(top.iter().skip(1).copied().map(PathCommand::LineTo));
3761 commands.extend(bottom.into_iter().map(PathCommand::LineTo));
3762 commands.push(PathCommand::Close);
3763 elements.push(filled_and_stroked_path(
3764 Path {
3765 commands,
3766 fill_rule: FillRule::NonZero,
3767 },
3768 colour,
3769 ));
3770 }
3771 }
3772 Ok(elements)
3773}
3774
3775fn render_scatter_geometry(
3776 style: ScatterStyle,
3777 series: &[Series],
3778 bounds: Rect,
3779 blanks: DispBlanksAs,
3780 options: GeometryOptions,
3781 series_colours: &[Color],
3782) -> Result<Vec<PositionedElement>> {
3783 validate_series_values(series)?;
3784 let mut paired_series = Vec::with_capacity(series.len());
3785 let mut marker_series = Vec::with_capacity(series.len());
3786 let mut all_x = Vec::new();
3787 let mut all_y = Vec::new();
3788 for item in series {
3789 let markers = matched_scatter_points(item)?;
3790 let paired = scatter_render_points(item, blanks)?;
3791 all_x.extend(paired.iter().map(|(_, x, _)| *x));
3792 all_y.extend(paired.iter().map(|(_, _, y)| *y));
3793 paired_series.push(paired);
3794 marker_series.push(markers);
3795 }
3796 let x_domain = options
3797 .scatter_x
3798 .map(|mapping| mapping.domain)
3799 .unwrap_or(data_domain(&all_x, false)?);
3800 let y_domain = options
3801 .scatter_y
3802 .map(|mapping| mapping.domain)
3803 .unwrap_or(data_domain(&all_y, false)?);
3804 let x_orientation = options
3805 .scatter_x
3806 .map(|mapping| mapping.orientation)
3807 .unwrap_or(Orientation::MinMax);
3808 let y_orientation = options
3809 .scatter_y
3810 .map(|mapping| mapping.orientation)
3811 .unwrap_or(Orientation::MinMax);
3812 let draw_line = matches!(
3813 style,
3814 ScatterStyle::Line
3815 | ScatterStyle::LineMarker
3816 | ScatterStyle::Smooth
3817 | ScatterStyle::SmoothMarker
3818 );
3819 let draw_marker = matches!(
3820 style,
3821 ScatterStyle::Marker | ScatterStyle::LineMarker | ScatterStyle::SmoothMarker
3822 );
3823 let mut elements = Vec::new();
3824 for (series_index, paired) in paired_series.iter().enumerate() {
3825 let colour = series_colour(series_colours, series_index)?;
3826 if draw_line {
3827 let indexes: Vec<_> = paired.iter().map(|(index, _, _)| *index).collect();
3828 for (start, end) in contiguous_ranges(&indexes, blanks) {
3829 let points = scatter_points(
3830 &paired[start..end],
3831 x_domain,
3832 y_domain,
3833 bounds,
3834 x_orientation,
3835 y_orientation,
3836 )?;
3837 elements.push(stroked_path(polyline_path(&points, false), colour));
3838 }
3839 }
3840 if draw_marker {
3841 let marker_points = scatter_points(
3842 &marker_series[series_index],
3843 x_domain,
3844 y_domain,
3845 bounds,
3846 x_orientation,
3847 y_orientation,
3848 )?;
3849 push_markers(&mut elements, &marker_points, colour);
3850 }
3851 }
3852 Ok(elements)
3853}
3854
3855type ScatterPoint = (usize, f64, f64);
3856
3857struct ScatterPointMaps {
3858 x_count: usize,
3859 y_count: usize,
3860 x_by_index: BTreeMap<usize, f64>,
3861 y_by_index: BTreeMap<usize, f64>,
3862}
3863
3864fn scatter_point_maps(series: &Series) -> Result<ScatterPointMaps> {
3865 let Some(AxisData::Numeric(x_values)) = &series.categories else {
3866 return Err(ChartError::InvalidValue {
3867 element: "c:xVal".to_owned(),
3868 value: "scatter series requires numeric cached x values".to_owned(),
3869 });
3870 };
3871 validate_finite(&x_values.values, "c:xVal/c:numCache")?;
3872 let (x_count, x_points) = logical_numeric_values(x_values)?;
3873 let (y_count, y_points) = logical_numeric_values(&series.values)?;
3874 Ok(ScatterPointMaps {
3875 x_count,
3876 y_count,
3877 x_by_index: x_points.into_iter().collect(),
3878 y_by_index: y_points.into_iter().collect(),
3879 })
3880}
3881
3882fn matched_scatter_points(series: &Series) -> Result<Vec<ScatterPoint>> {
3883 let points = scatter_point_maps(series)?;
3884 Ok(points
3885 .y_by_index
3886 .iter()
3887 .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
3888 .collect())
3889}
3890
3891fn scatter_render_points(series: &Series, blanks: DispBlanksAs) -> Result<Vec<ScatterPoint>> {
3892 let points = scatter_point_maps(series)?;
3893 if blanks != DispBlanksAs::Zero {
3894 return Ok(points
3895 .y_by_index
3896 .iter()
3897 .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
3898 .collect());
3899 }
3900 let count = points.x_count.max(points.y_count);
3901 let present = points
3902 .x_by_index
3903 .keys()
3904 .chain(points.y_by_index.keys())
3905 .copied()
3906 .collect();
3907 Ok(zero_control_indexes(count, &present)
3908 .into_iter()
3909 .map(|index| {
3910 (
3911 index,
3912 points.x_by_index.get(&index).copied().unwrap_or(0.0),
3913 points.y_by_index.get(&index).copied().unwrap_or(0.0),
3914 )
3915 })
3916 .collect())
3917}
3918
3919fn scatter_points(
3920 values: &[(usize, f64, f64)],
3921 x_domain: Domain,
3922 y_domain: Domain,
3923 bounds: Rect,
3924 x_orientation: Orientation,
3925 y_orientation: Orientation,
3926) -> Result<Vec<Point>> {
3927 values
3928 .iter()
3929 .map(|(_, x, y)| {
3930 Ok(Point {
3931 x: map_x_oriented(*x, x_domain, bounds, x_orientation)?,
3932 y: map_y_oriented(*y, y_domain, bounds, y_orientation)?,
3933 })
3934 })
3935 .collect()
3936}
3937
3938fn render_radar_geometry(
3939 style: RadarStyle,
3940 series: &[Series],
3941 bounds: Rect,
3942 options: GeometryOptions,
3943 series_colours: &[Color],
3944) -> Result<Vec<PositionedElement>> {
3945 let point_series = radar_series_points(series, bounds, options)?;
3946 let mut elements = Vec::new();
3947 for (series_index, points) in point_series.iter().enumerate() {
3948 let colour = series_colour(series_colours, series_index)?;
3949 let plain_points: Vec<_> = points.iter().map(|(_, point)| *point).collect();
3950 if plain_points.is_empty() {
3951 continue;
3952 }
3953 let path = polyline_path(&plain_points, true);
3954 if style == RadarStyle::Filled {
3955 elements.push(filled_and_stroked_path(path, colour));
3956 } else {
3957 elements.push(stroked_path(path, colour));
3958 }
3959 if style == RadarStyle::Marker {
3960 push_markers(&mut elements, &plain_points, colour);
3961 }
3962 }
3963 Ok(elements)
3964}
3965
3966fn radar_series_points(
3967 series: &[Series],
3968 bounds: Rect,
3969 options: GeometryOptions,
3970) -> Result<Vec<Vec<(usize, Point)>>> {
3971 validate_series_values(series)?;
3972 let logical_series = logical_series_values(series)?;
3973 let category_count = series_category_count(series)?;
3974 let values = logical_series
3975 .iter()
3976 .flat_map(|(_, points)| points.iter().map(|(_, value)| *value))
3977 .collect::<Vec<_>>();
3978 let mapping = options.value.unwrap_or(AxisMapping {
3979 domain: data_domain(&values, true)?,
3980 orientation: Orientation::MinMax,
3981 });
3982 let center = Point {
3983 x: bounds.x + bounds.width / 2.0,
3984 y: bounds.y + bounds.height / 2.0,
3985 };
3986 let radius = bounds.width.min(bounds.height) / 2.0;
3987 let mut point_series = Vec::with_capacity(logical_series.len());
3988 for (_, logical) in &logical_series {
3989 let mut points = Vec::with_capacity(logical.len());
3990 for (category, value) in logical {
3991 if *value < mapping.domain.min || *value > mapping.domain.max {
3992 continue;
3993 }
3994 points.push((
3995 *category,
3996 radial_point(
3997 center,
3998 normalized_value_oriented(*value, mapping.domain, mapping.orientation)?
3999 * radius,
4000 oriented_category_index(
4001 *category,
4002 category_count,
4003 options.category_orientation,
4004 ),
4005 category_count,
4006 ),
4007 ));
4008 }
4009 point_series.push(points);
4010 }
4011 Ok(point_series)
4012}
4013
4014fn render_pie_geometry(
4015 first_slice_angle: u16,
4016 hole_size: Option<u8>,
4017 series: &[Series],
4018 bounds: Rect,
4019 series_colours: &[Color],
4020) -> Result<Vec<PositionedElement>> {
4021 pie_slices(first_slice_angle, hole_size, series, bounds)?
4022 .into_iter()
4023 .map(|slice| {
4024 Ok(filled_path(
4025 slice.path,
4026 series_colour(series_colours, slice.series_index)?,
4027 ))
4028 })
4029 .collect()
4030}
4031
4032struct PieSliceGeometry {
4033 series_index: usize,
4034 logical_index: usize,
4035 path: Path,
4036 anchor: Point,
4037 base: Point,
4038 end: Point,
4039}
4040
4041fn pie_slices(
4042 first_slice_angle: u16,
4043 hole_size: Option<u8>,
4044 series: &[Series],
4045 bounds: Rect,
4046) -> Result<Vec<PieSliceGeometry>> {
4047 validate_series_values(series)?;
4048 let center = Point {
4049 x: bounds.x + bounds.width / 2.0,
4050 y: bounds.y + bounds.height / 2.0,
4051 };
4052 let radius = bounds.width.min(bounds.height) / 2.0;
4053 let mut elements = Vec::new();
4054 for (series_index, item) in series.iter().enumerate() {
4055 let (_, logical) = logical_numeric_values(&item.values)?;
4056 let total = logical
4057 .iter()
4058 .map(|(_, value)| *value)
4059 .filter(|value| *value > 0.0)
4060 .try_fold(0.0, |total, value| {
4061 checked_geometry_sum(total, value, "pie value total")
4062 })?;
4063 if total <= 0.0 {
4064 continue;
4065 }
4066 let mut angle = f64::from(first_slice_angle).to_radians() - std::f64::consts::FRAC_PI_2;
4067 for (logical_index, value) in logical {
4068 if value <= 0.0 {
4069 continue;
4070 }
4071 let next = angle + value / total * std::f64::consts::TAU;
4072 let path = if let Some(hole_size) = hole_size {
4073 doughnut_wedge(
4074 center,
4075 radius,
4076 radius * f64::from(hole_size) / 100.0,
4077 angle,
4078 next,
4079 )
4080 } else {
4081 pie_wedge(center, radius, angle, next)
4082 };
4083 let inner_radius = hole_size
4084 .map(|hole| radius * f64::from(hole) / 100.0)
4085 .unwrap_or(0.0);
4086 let anchor_radius = hole_size
4087 .map(|_| (radius + inner_radius) / 2.0)
4088 .unwrap_or(radius * 2.0 / 3.0);
4089 let midpoint = (angle + next) / 2.0;
4090 elements.push(PieSliceGeometry {
4091 series_index,
4092 logical_index,
4093 path,
4094 anchor: circle_point(center, anchor_radius, midpoint),
4095 base: circle_point(center, inner_radius, midpoint),
4096 end: circle_point(center, radius, midpoint),
4097 });
4098 angle = next;
4099 }
4100 }
4101 Ok(elements)
4102}
4103
4104fn validate_series_values(series: &[Series]) -> Result<()> {
4105 if series.is_empty() {
4106 return Err(ChartError::MissingElement("c:ser".to_owned()));
4107 }
4108 for item in series {
4109 validate_finite(&item.values.values, "c:val/c:numCache")?;
4110 }
4111 Ok(())
4112}
4113
4114fn validate_finite(values: &[f64], element: &str) -> Result<()> {
4115 if let Some(value) = values.iter().find(|value| !value.is_finite()) {
4116 return Err(ChartError::InvalidValue {
4117 element: element.to_owned(),
4118 value: value.to_string(),
4119 });
4120 }
4121 Ok(())
4122}
4123
4124type IndexedValues = Vec<(usize, f64)>;
4125type LogicalSeries = (usize, IndexedValues);
4126type GeometryLayer = Vec<(usize, f64, f64)>;
4127
4128fn zero_control_indexes(count: usize, present: &BTreeSet<usize>) -> BTreeSet<usize> {
4129 let mut controls = present.clone();
4130 if count == 0 {
4131 return controls;
4132 }
4133 controls.insert(0);
4134 controls.insert(count - 1);
4135 for index in present {
4136 if *index > 0 {
4137 controls.insert(*index - 1);
4138 }
4139 if index + 1 < count {
4140 controls.insert(index + 1);
4141 }
4142 }
4143 controls
4144}
4145
4146fn zero_filled_series(series: &[LogicalSeries], count: usize) -> Vec<LogicalSeries> {
4147 let present = series
4148 .iter()
4149 .flat_map(|(_, points)| points.iter().map(|(index, _)| *index))
4150 .collect();
4151 let controls = zero_control_indexes(count, &present);
4152 series
4153 .iter()
4154 .map(|(declared, points)| {
4155 let values: BTreeMap<_, _> = points.iter().copied().collect();
4156 let filled = controls
4157 .iter()
4158 .map(|index| (*index, values.get(index).copied().unwrap_or(0.0)))
4159 .collect();
4160 (*declared, filled)
4161 })
4162 .collect()
4163}
4164
4165fn contiguous_ranges(indexes: &[usize], blanks: DispBlanksAs) -> Vec<(usize, usize)> {
4166 if indexes.is_empty() {
4167 return Vec::new();
4168 }
4169 if blanks != DispBlanksAs::Gap {
4170 return vec![(0, indexes.len())];
4171 }
4172 let mut ranges = Vec::new();
4173 let mut start = 0usize;
4174 for position in 1..indexes.len() {
4175 if indexes[position - 1].checked_add(1) != Some(indexes[position]) {
4176 ranges.push((start, position));
4177 start = position;
4178 }
4179 }
4180 ranges.push((start, indexes.len()));
4181 ranges
4182}
4183
4184fn logical_numeric_values(data: &NumericData) -> Result<LogicalSeries> {
4185 validate_finite(&data.values, "c:numCache")?;
4186 let (declared, indexes) = cache_layout(&data.markup, data.values.len())?;
4187 let declared = usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
4188 element: "c:ptCount".to_owned(),
4189 value: declared.to_string(),
4190 })?;
4191 let points = indexes
4192 .into_iter()
4193 .zip(data.values.iter().copied())
4194 .map(|(index, value)| {
4195 usize::try_from(index)
4196 .map(|index| (index, value))
4197 .map_err(|_| ChartError::InvalidValue {
4198 element: "c:pt/@idx".to_owned(),
4199 value: index.to_string(),
4200 })
4201 })
4202 .collect::<Result<Vec<_>>>()?;
4203 Ok((declared, points))
4204}
4205
4206fn logical_series_values(series: &[Series]) -> Result<Vec<LogicalSeries>> {
4207 series
4208 .iter()
4209 .map(|item| logical_numeric_values(&item.values))
4210 .collect()
4211}
4212
4213fn reference_logical_count(markup: &ReferenceMarkup, value_count: usize) -> Result<usize> {
4214 let (declared, _) = cache_layout(markup, value_count)?;
4215 usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
4216 element: "c:ptCount".to_owned(),
4217 value: declared.to_string(),
4218 })
4219}
4220
4221fn series_category_count(series: &[Series]) -> Result<usize> {
4222 let mut count = 0usize;
4223 for item in series {
4224 count = count.max(reference_logical_count(
4225 &item.values.markup,
4226 item.values.values.len(),
4227 )?);
4228 if let Some(categories) = &item.categories {
4229 let category_count = match categories {
4230 AxisData::String(data) => reference_logical_count(&data.markup, data.values.len())?,
4231 AxisData::Numeric(data) => {
4232 reference_logical_count(&data.markup, data.values.len())?
4233 }
4234 };
4235 count = count.max(category_count);
4236 }
4237 }
4238 if count == 0 {
4239 return Err(ChartError::InvalidValue {
4240 element: "c:ser".to_owned(),
4241 value: "cached series contain no points".to_owned(),
4242 });
4243 }
4244 Ok(count)
4245}
4246
4247fn stacked_series_bounds(
4248 series: &[LogicalSeries],
4249 stacked: bool,
4250 percent: bool,
4251) -> Result<Vec<GeometryLayer>> {
4252 let mut positive_totals = BTreeMap::<usize, f64>::new();
4253 let mut negative_totals = BTreeMap::<usize, f64>::new();
4254 if percent {
4255 for (_, points) in series {
4256 for &(index, value) in points {
4257 let totals = if value >= 0.0 {
4258 &mut positive_totals
4259 } else {
4260 &mut negative_totals
4261 };
4262 let total = totals.entry(index).or_default();
4263 *total = checked_geometry_sum(*total, value.abs(), "stacked percentage total")?;
4264 }
4265 }
4266 }
4267 let mut positive = BTreeMap::<usize, f64>::new();
4268 let mut negative = BTreeMap::<usize, f64>::new();
4269 let mut layers = Vec::with_capacity(series.len());
4270 for (_, points) in series {
4271 let mut layer = Vec::with_capacity(points.len());
4272 for &(index, original) in points {
4273 let total = if original >= 0.0 {
4274 positive_totals.get(&index).copied().unwrap_or(0.0)
4275 } else {
4276 negative_totals.get(&index).copied().unwrap_or(0.0)
4277 };
4278 let value = if percent && total != 0.0 {
4279 original / total
4280 } else {
4281 original
4282 };
4283 if stacked {
4284 let accumulators = if value >= 0.0 {
4285 &mut positive
4286 } else {
4287 &mut negative
4288 };
4289 let accumulator = accumulators.entry(index).or_default();
4290 let lower = *accumulator;
4291 *accumulator = checked_geometry_sum(*accumulator, value, "stacked value total")?;
4292 layer.push((index, lower, *accumulator));
4293 } else {
4294 layer.push((index, 0.0, value));
4295 }
4296 }
4297 layers.push(layer);
4298 }
4299 Ok(layers)
4300}
4301
4302fn checked_geometry_sum(current: f64, value: f64, context: &str) -> Result<f64> {
4303 let sum = current + value;
4304 if sum.is_finite() {
4305 Ok(sum)
4306 } else {
4307 Err(ChartError::InvalidValue {
4308 element: "chart geometry aggregate".to_owned(),
4309 value: format!("{context} is nonfinite"),
4310 })
4311 }
4312}
4313
4314#[derive(Clone, Copy, Debug, PartialEq)]
4315struct Domain {
4316 min: f64,
4317 max: f64,
4318}
4319
4320fn nice_number_scale(
4321 extent: Domain,
4322 pinned_minimum: Option<f64>,
4323 pinned_maximum: Option<f64>,
4324 target_tick_count: usize,
4325) -> Result<AxisScale> {
4326 if target_tick_count < 2
4327 || !extent.min.is_finite()
4328 || !extent.max.is_finite()
4329 || pinned_minimum.is_some_and(|value| !value.is_finite())
4330 || pinned_maximum.is_some_and(|value| !value.is_finite())
4331 {
4332 return Err(ChartError::InvalidValue {
4333 element: "chart axis scale".to_owned(),
4334 value: "scale bounds and target tick count must be finite and valid".to_owned(),
4335 });
4336 }
4337
4338 let mut data_min = extent.min.min(extent.max);
4339 let mut data_max = extent.min.max(extent.max);
4340 if data_min == data_max {
4341 let expanded = expand_constant_domain(data_min)?;
4342 data_min = expanded.min;
4343 data_max = expanded.max;
4344 }
4345 let requested_min = match (pinned_minimum, pinned_maximum) {
4346 (Some(minimum), _) => minimum,
4347 (None, Some(maximum)) if data_min >= maximum => {
4348 extend_bound(maximum, stable_positive_step(data_min, data_max, 3)?, false)?
4349 }
4350 (None, _) => data_min,
4351 };
4352 let requested_max = match (pinned_minimum, pinned_maximum) {
4353 (_, Some(maximum)) => maximum,
4354 (Some(minimum), None) if data_max <= minimum => {
4355 extend_bound(minimum, stable_positive_step(data_min, data_max, 3)?, true)?
4356 }
4357 (_, None) => data_max,
4358 };
4359 if requested_min >= requested_max {
4360 return Err(ChartError::InvalidValue {
4361 element: "c:scaling".to_owned(),
4362 value: format!("minimum {requested_min} is not less than maximum {requested_max}"),
4363 });
4364 }
4365
4366 let raw_step = stable_positive_step(requested_min, requested_max, target_tick_count)?;
4367 let step = nice_step(raw_step)?;
4368 let rounded_minimum = (requested_min / step).floor() * step;
4369 let rounded_maximum = (requested_max / step).ceil() * step;
4370 let lower_fallback = pinned_minimum.is_none() && !rounded_minimum.is_finite();
4371 let upper_fallback = pinned_maximum.is_none() && !rounded_maximum.is_finite();
4372 let minimum = pinned_minimum.unwrap_or(if rounded_minimum.is_finite() {
4373 rounded_minimum
4374 } else {
4375 requested_min
4376 });
4377 let maximum = pinned_maximum.unwrap_or(if rounded_maximum.is_finite() {
4378 rounded_maximum
4379 } else {
4380 requested_max
4381 });
4382 if !minimum.is_finite() || !maximum.is_finite() || minimum >= maximum {
4383 return Err(ChartError::InvalidValue {
4384 element: "chart axis scale".to_owned(),
4385 value: "nice-number bounds are nonfinite or empty".to_owned(),
4386 });
4387 }
4388
4389 let first = (minimum / step).ceil();
4390 let last = (maximum / step).floor();
4391 let tick_count = if last >= first {
4392 (last - first).min(1024.0) as usize + 1
4393 } else {
4394 0
4395 };
4396 let mut ticks = Vec::with_capacity(tick_count);
4397 for index in 0..tick_count {
4398 let tick = (first + index as f64) * step;
4399 if tick.is_finite() && tick >= minimum && tick <= maximum {
4400 ticks.push(clean_tick(tick, step));
4401 }
4402 }
4403 if lower_fallback && ticks.first().is_none_or(|tick| *tick > minimum) {
4404 ticks.insert(0, minimum);
4405 }
4406 if upper_fallback && ticks.last().is_none_or(|tick| *tick < maximum) {
4407 ticks.push(maximum);
4408 }
4409 if ticks.len() < 2 {
4410 ticks.extend([minimum, maximum]);
4411 ticks.sort_by(f64::total_cmp);
4412 }
4413 ticks.dedup_by(|left, right| *left == *right);
4414 if ticks.len() < 2 || !ticks.windows(2).all(|pair| pair[0] < pair[1]) {
4415 return Err(ChartError::InvalidValue {
4416 element: "chart axis scale".to_owned(),
4417 value: "scale cannot produce two increasing finite ticks".to_owned(),
4418 });
4419 }
4420 Ok(AxisScale {
4421 domain: Domain {
4422 min: minimum,
4423 max: maximum,
4424 },
4425 ticks,
4426 })
4427}
4428
4429const MIN_POSITIVE_SUBNORMAL: f64 = f64::from_bits(1);
4430
4431fn stable_positive_step(minimum: f64, maximum: f64, target_tick_count: usize) -> Result<f64> {
4432 let scale = minimum.abs().max(maximum.abs());
4433 let scale = if scale == 0.0 {
4434 MIN_POSITIVE_SUBNORMAL
4435 } else {
4436 scale
4437 };
4438 let normalized =
4439 (maximum / scale - minimum / scale) / target_tick_count.saturating_sub(1) as f64;
4440 let step = normalized * scale;
4441 if normalized.is_finite() && normalized > 0.0 && step.is_finite() {
4442 Ok(step.max(MIN_POSITIVE_SUBNORMAL))
4443 } else {
4444 Err(ChartError::InvalidValue {
4445 element: "chart axis scale".to_owned(),
4446 value: "scale step is nonfinite or empty".to_owned(),
4447 })
4448 }
4449}
4450
4451fn nice_step(raw_step: f64) -> Result<f64> {
4452 if !raw_step.is_finite() || raw_step <= 0.0 {
4453 return Err(ChartError::InvalidValue {
4454 element: "chart axis scale".to_owned(),
4455 value: format!("invalid raw tick step {raw_step}"),
4456 });
4457 }
4458 let magnitude = 10.0_f64
4459 .powf(raw_step.log10().floor())
4460 .max(MIN_POSITIVE_SUBNORMAL);
4461 let fraction = raw_step / magnitude;
4462 let multiplier = if fraction <= 1.0 {
4463 1.0
4464 } else if fraction <= 2.0 {
4465 2.0
4466 } else if fraction <= 5.0 {
4467 5.0
4468 } else {
4469 10.0
4470 };
4471 let step = (multiplier * magnitude).max(MIN_POSITIVE_SUBNORMAL);
4472 if step.is_finite() && step > 0.0 {
4473 Ok(step)
4474 } else {
4475 Err(ChartError::InvalidValue {
4476 element: "chart axis scale".to_owned(),
4477 value: "nice-number step is nonfinite".to_owned(),
4478 })
4479 }
4480}
4481
4482fn expand_constant_domain(value: f64) -> Result<Domain> {
4483 if value == 0.0 {
4484 return Ok(Domain {
4485 min: -1.0,
4486 max: 1.0,
4487 });
4488 }
4489 let padding = (value.abs() * 0.5).max(MIN_POSITIVE_SUBNORMAL);
4490 let minimum = value - padding;
4491 let maximum = value + padding;
4492 if minimum.is_finite() && maximum.is_finite() && minimum < maximum {
4493 return Ok(Domain {
4494 min: minimum,
4495 max: maximum,
4496 });
4497 }
4498 let domain = if value.is_sign_positive() {
4499 Domain {
4500 min: 0.0,
4501 max: value,
4502 }
4503 } else {
4504 Domain {
4505 min: value,
4506 max: 0.0,
4507 }
4508 };
4509 if domain.min < domain.max {
4510 Ok(domain)
4511 } else {
4512 Err(ChartError::InvalidValue {
4513 element: "chart axis scale".to_owned(),
4514 value: format!("constant value {value} has no finite renderable extent"),
4515 })
4516 }
4517}
4518
4519fn extend_bound(bound: f64, span: f64, positive: bool) -> Result<f64> {
4520 let candidate = if positive { bound + span } else { bound - span };
4521 let extended = if candidate.is_finite()
4522 && if positive {
4523 candidate > bound
4524 } else {
4525 candidate < bound
4526 } {
4527 candidate
4528 } else if positive {
4529 bound.next_up()
4530 } else {
4531 bound.next_down()
4532 };
4533 if extended.is_finite()
4534 && if positive {
4535 extended > bound
4536 } else {
4537 extended < bound
4538 }
4539 {
4540 Ok(extended)
4541 } else {
4542 Err(ChartError::InvalidValue {
4543 element: "c:scaling".to_owned(),
4544 value: format!("one-sided bound {bound} leaves no finite automatic extent"),
4545 })
4546 }
4547}
4548
4549fn clean_tick(value: f64, step: f64) -> f64 {
4550 if value.abs() <= step.abs() * 1.0e-12 {
4551 0.0
4552 } else {
4553 value
4554 }
4555}
4556
4557fn domain_from_layers(layers: &[GeometryLayer]) -> Result<Domain> {
4558 let values = layers
4559 .iter()
4560 .flatten()
4561 .flat_map(|(_, lower, upper)| [*lower, *upper]);
4562 let mut min = 0.0_f64;
4563 let mut max = 0.0_f64;
4564 for value in values {
4565 if !value.is_finite() {
4566 return Err(ChartError::InvalidValue {
4567 element: "chart geometry domain".to_owned(),
4568 value: "aggregate produced a nonfinite value".to_owned(),
4569 });
4570 }
4571 min = min.min(value);
4572 max = max.max(value);
4573 }
4574 if min == max {
4575 max = 1.0;
4576 }
4577 Ok(Domain { min, max })
4578}
4579
4580fn data_domain(values: &[f64], include_zero: bool) -> Result<Domain> {
4581 validate_finite(values, "chart geometry cache")?;
4582 let Some(first) = values.first().copied() else {
4583 return Err(ChartError::InvalidValue {
4584 element: "c:numCache".to_owned(),
4585 value: "cached series contain no points".to_owned(),
4586 });
4587 };
4588 let mut min = first;
4589 let mut max = first;
4590 for value in values.iter().copied() {
4591 min = min.min(value);
4592 max = max.max(value);
4593 }
4594 if include_zero {
4595 min = min.min(0.0);
4596 max = max.max(0.0);
4597 }
4598 if min == max {
4599 if !include_zero {
4600 return expand_constant_domain(min);
4601 }
4602 if min > 0.0 {
4603 min = 0.0;
4604 } else if max < 0.0 {
4605 max = 0.0;
4606 } else {
4607 max = 1.0;
4608 }
4609 }
4610 Ok(Domain { min, max })
4611}
4612
4613fn normalized_value(value: f64, domain: Domain) -> Result<f64> {
4614 let scale = value
4615 .abs()
4616 .max(domain.min.abs())
4617 .max(domain.max.abs())
4618 .max(1.0);
4619 let value = value / scale;
4620 let min = domain.min / scale;
4621 let max = domain.max / scale;
4622 let range = max - min;
4623 let normalized = (value - min) / range;
4624 if !range.is_finite() || range <= 0.0 || !normalized.is_finite() {
4625 return Err(ChartError::InvalidValue {
4626 element: "chart geometry domain".to_owned(),
4627 value: format!("cannot map finite domain {} to {}", domain.min, domain.max),
4628 });
4629 }
4630 Ok(normalized)
4631}
4632
4633fn normalized_value_oriented(value: f64, domain: Domain, orientation: Orientation) -> Result<f64> {
4634 let normalized = normalized_value(value, domain)?;
4635 Ok(if orientation == Orientation::MaxMin {
4636 1.0 - normalized
4637 } else {
4638 normalized
4639 })
4640}
4641
4642fn map_x_oriented(
4643 value: f64,
4644 domain: Domain,
4645 bounds: Rect,
4646 orientation: Orientation,
4647) -> Result<f64> {
4648 let coordinate =
4649 bounds.x + normalized_value_oriented(value, domain, orientation)? * bounds.width;
4650 if coordinate.is_finite() {
4651 Ok(coordinate)
4652 } else {
4653 Err(ChartError::InvalidValue {
4654 element: "chart geometry coordinate".to_owned(),
4655 value: "x coordinate is nonfinite".to_owned(),
4656 })
4657 }
4658}
4659
4660fn map_y_oriented(
4661 value: f64,
4662 domain: Domain,
4663 bounds: Rect,
4664 orientation: Orientation,
4665) -> Result<f64> {
4666 let coordinate = bounds.y + bounds.height
4667 - normalized_value_oriented(value, domain, orientation)? * bounds.height;
4668 if coordinate.is_finite() {
4669 Ok(coordinate)
4670 } else {
4671 Err(ChartError::InvalidValue {
4672 element: "chart geometry coordinate".to_owned(),
4673 value: "y coordinate is nonfinite".to_owned(),
4674 })
4675 }
4676}
4677
4678fn oriented_category_index(index: usize, count: usize, orientation: Orientation) -> usize {
4679 if orientation == Orientation::MaxMin && index < count {
4680 count - index - 1
4681 } else {
4682 index
4683 }
4684}
4685
4686fn category_points(
4687 values: &[(usize, f64)],
4688 count: usize,
4689 domain: Domain,
4690 bounds: Rect,
4691 category_orientation: Orientation,
4692 value_orientation: Orientation,
4693) -> Result<Vec<Point>> {
4694 values
4695 .iter()
4696 .map(|(index, value)| {
4697 let index = oriented_category_index(*index, count, category_orientation);
4698 let x = bounds.x + (index as f64 + 0.5) / count as f64 * bounds.width;
4699 let y = map_y_oriented(*value, domain, bounds, value_orientation)?;
4700 if !x.is_finite() {
4701 return Err(ChartError::InvalidValue {
4702 element: "chart geometry coordinate".to_owned(),
4703 value: "category x coordinate is nonfinite".to_owned(),
4704 });
4705 }
4706 Ok(Point { x, y })
4707 })
4708 .collect()
4709}
4710
4711fn polyline_path(points: &[Point], close: bool) -> Path {
4712 let mut commands = Vec::with_capacity(points.len() + usize::from(close));
4713 if let Some(first) = points.first() {
4714 commands.push(PathCommand::MoveTo(*first));
4715 commands.extend(points.iter().skip(1).copied().map(PathCommand::LineTo));
4716 if close {
4717 commands.push(PathCommand::Close);
4718 }
4719 }
4720 Path {
4721 commands,
4722 fill_rule: FillRule::NonZero,
4723 }
4724}
4725
4726fn pie_wedge(center: Point, radius: f64, start: f64, end: f64) -> Path {
4727 let first = circle_point(center, radius, start);
4728 let mut commands = vec![PathCommand::MoveTo(center), PathCommand::LineTo(first)];
4729 append_arc(&mut commands, center, radius, start, end);
4730 commands.push(PathCommand::Close);
4731 Path {
4732 commands,
4733 fill_rule: FillRule::NonZero,
4734 }
4735}
4736
4737fn doughnut_wedge(
4738 center: Point,
4739 outer_radius: f64,
4740 inner_radius: f64,
4741 start: f64,
4742 end: f64,
4743) -> Path {
4744 let outer_start = circle_point(center, outer_radius, start);
4745 let inner_end = circle_point(center, inner_radius, end);
4746 let mut commands = vec![PathCommand::MoveTo(outer_start)];
4747 append_arc(&mut commands, center, outer_radius, start, end);
4748 commands.push(PathCommand::LineTo(inner_end));
4749 append_arc(&mut commands, center, inner_radius, end, start);
4750 commands.push(PathCommand::Close);
4751 Path {
4752 commands,
4753 fill_rule: FillRule::NonZero,
4754 }
4755}
4756
4757fn append_arc(commands: &mut Vec<PathCommand>, center: Point, radius: f64, start: f64, end: f64) {
4758 let segments = ((end - start).abs() / std::f64::consts::FRAC_PI_2)
4759 .ceil()
4760 .max(1.0) as usize;
4761 let sweep = (end - start) / segments as f64;
4762 for segment in 0..segments {
4763 let angle = start + segment as f64 * sweep;
4764 let next = angle + sweep;
4765 let factor = 4.0 / 3.0 * (sweep / 4.0).tan();
4766 let from = circle_point(center, radius, angle);
4767 let to = circle_point(center, radius, next);
4768 commands.push(PathCommand::CurveTo {
4769 c1: Point {
4770 x: from.x - factor * radius * angle.sin(),
4771 y: from.y + factor * radius * angle.cos(),
4772 },
4773 c2: Point {
4774 x: to.x + factor * radius * next.sin(),
4775 y: to.y - factor * radius * next.cos(),
4776 },
4777 to,
4778 });
4779 }
4780}
4781
4782fn circle_point(center: Point, radius: f64, angle: f64) -> Point {
4783 Point {
4784 x: center.x + radius * angle.cos(),
4785 y: center.y + radius * angle.sin(),
4786 }
4787}
4788
4789fn radial_point(center: Point, radius: f64, index: usize, count: usize) -> Point {
4790 circle_point(
4791 center,
4792 radius,
4793 index as f64 * std::f64::consts::TAU / count as f64 - std::f64::consts::FRAC_PI_2,
4794 )
4795}
4796
4797fn marker_path(center: Point) -> Path {
4798 const RADIUS: f64 = 2.5;
4799 const KAPPA: f64 = 0.552_284_749_830_793_6;
4800 let control = RADIUS * KAPPA;
4801 Path {
4802 commands: vec![
4803 PathCommand::MoveTo(Point {
4804 x: center.x + RADIUS,
4805 y: center.y,
4806 }),
4807 PathCommand::CurveTo {
4808 c1: Point {
4809 x: center.x + RADIUS,
4810 y: center.y + control,
4811 },
4812 c2: Point {
4813 x: center.x + control,
4814 y: center.y + RADIUS,
4815 },
4816 to: Point {
4817 x: center.x,
4818 y: center.y + RADIUS,
4819 },
4820 },
4821 PathCommand::CurveTo {
4822 c1: Point {
4823 x: center.x - control,
4824 y: center.y + RADIUS,
4825 },
4826 c2: Point {
4827 x: center.x - RADIUS,
4828 y: center.y + control,
4829 },
4830 to: Point {
4831 x: center.x - RADIUS,
4832 y: center.y,
4833 },
4834 },
4835 PathCommand::CurveTo {
4836 c1: Point {
4837 x: center.x - RADIUS,
4838 y: center.y - control,
4839 },
4840 c2: Point {
4841 x: center.x - control,
4842 y: center.y - RADIUS,
4843 },
4844 to: Point {
4845 x: center.x,
4846 y: center.y - RADIUS,
4847 },
4848 },
4849 PathCommand::CurveTo {
4850 c1: Point {
4851 x: center.x + control,
4852 y: center.y - RADIUS,
4853 },
4854 c2: Point {
4855 x: center.x + RADIUS,
4856 y: center.y - control,
4857 },
4858 to: Point {
4859 x: center.x + RADIUS,
4860 y: center.y,
4861 },
4862 },
4863 PathCommand::Close,
4864 ],
4865 fill_rule: FillRule::NonZero,
4866 }
4867}
4868
4869fn push_markers(elements: &mut Vec<PositionedElement>, points: &[Point], colour: Color) {
4870 for point in points {
4871 elements.push(filled_path(marker_path(*point), colour));
4872 }
4873}
4874
4875fn series_colour(series_colours: &[Color], index: usize) -> Result<Color> {
4876 series_colours
4877 .get(index)
4878 .copied()
4879 .ok_or_else(|| ChartError::InvalidValue {
4880 element: "chart series palette".to_owned(),
4881 value: format!("missing resolved colour for series {index}"),
4882 })
4883}
4884
4885fn filled_path(path: Path, color: Color) -> PositionedElement {
4886 PositionedElement::Path(PathElement {
4887 path,
4888 fill: Some(Paint::Solid(color)),
4889 stroke: None,
4890 })
4891}
4892
4893fn stroked_path(path: Path, color: Color) -> PositionedElement {
4894 PositionedElement::Path(PathElement {
4895 path,
4896 fill: None,
4897 stroke: Some(Stroke::new(Paint::Solid(color), 1.5)),
4898 })
4899}
4900
4901fn filled_and_stroked_path(path: Path, color: Color) -> PositionedElement {
4902 PositionedElement::Path(PathElement {
4903 path,
4904 fill: Some(Paint::Solid(Color {
4905 a: color.a * 0.55,
4906 ..color
4907 })),
4908 stroke: Some(Stroke::new(Paint::Solid(color), 1.0)),
4909 })
4910}
4911
4912type NamespaceBindings = Vec<(Vec<u8>, String)>;
4913type XmlAttributes = Vec<(String, String)>;
4914type RootAttributes = (XmlAttributes, XmlAttributes);
4915
4916#[derive(Clone, Debug, Default, Eq, PartialEq)]
4917struct ScalarMarkup {
4918 raw_attributes: XmlAttributes,
4919 raw_content: Vec<Vec<u8>>,
4920}
4921
4922#[derive(Clone, Debug, Default, Eq, PartialEq)]
4923struct TextMarkup {
4924 raw_attributes: XmlAttributes,
4925 raw_children: OrderedRawChildren,
4926}
4927
4928#[derive(Clone, Debug, Default, Eq, PartialEq)]
4929struct PointMarkup {
4930 raw_attributes: XmlAttributes,
4931 raw_children: OrderedRawChildren,
4932 value: TextMarkup,
4933}
4934
4935#[derive(Clone, Debug, Default, Eq, PartialEq)]
4936struct ReferenceMarkup {
4937 raw_attributes: XmlAttributes,
4938 raw_children: OrderedRawChildren,
4939 formula: TextMarkup,
4940 cache_attributes: XmlAttributes,
4941 cache_children: OrderedRawChildren,
4942 format_code: Option<TextMarkup>,
4943 format_code_omitted: bool,
4944 point_count: ScalarMarkup,
4945 declared_point_count: Option<u32>,
4946 point_indexes: Vec<u32>,
4947 points: Vec<PointMarkup>,
4948}
4949
4950#[derive(Clone, Debug, Default, Eq, PartialEq)]
4951struct WrapperMarkup {
4952 raw_attributes: XmlAttributes,
4953 raw_children: OrderedRawChildren,
4954}
4955
4956#[derive(Clone, Debug, PartialEq)]
4958pub struct StringRef {
4959 pub formula: String,
4960 pub values: Vec<String>,
4961 markup: ReferenceMarkup,
4962}
4963
4964impl StringRef {
4965 pub fn new(formula: String, values: Vec<String>) -> Result<Self> {
4966 let reference = Self {
4967 formula,
4968 values,
4969 markup: ReferenceMarkup::default(),
4970 };
4971 reference.validate()?;
4972 Ok(reference)
4973 }
4974
4975 pub fn from_xml(xml: &[u8]) -> Result<Self> {
4976 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
4977 }
4978
4979 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
4980 let parsed = parse_reference(xml, b"strRef", b"strCache", false, inherited)?;
4981 let values = parsed.values;
4982 let reference = Self {
4983 formula: parsed.formula,
4984 values,
4985 markup: parsed.markup,
4986 };
4987 reference.validate()?;
4988 Ok(reference)
4989 }
4990
4991 pub fn to_xml(&self) -> Result<Vec<u8>> {
4992 self.validate()?;
4993 let mut writer = Writer::new(Vec::new());
4994 self.write_xml(&mut writer, true)?;
4995 Ok(writer.into_inner())
4996 }
4997
4998 fn validate(&self) -> Result<()> {
4999 validate_formula(&self.formula, "c:strRef/c:f")?;
5000 for value in &self.values {
5001 validate_xml_text(value, "c:strCache/c:pt/c:v")?;
5002 }
5003 validate_point_count(self.values.len(), "c:strCache")
5004 }
5005
5006 fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
5007 self.validate()?;
5008 let mut start = BytesStart::new("c:strRef");
5009 if standalone {
5010 start.push_attribute(("xmlns:c", C_NS));
5011 }
5012 push_attributes(&mut start, &self.markup.raw_attributes);
5013 writer
5014 .write_event(Event::Start(start))
5015 .map_err(OxmlError::from)?;
5016 emit_raw(writer, self.markup.raw_children.at(0))?;
5017 write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
5018 emit_raw(writer, self.markup.raw_children.at(1))?;
5019 write_string_cache(writer, &self.values, &self.markup)?;
5020 emit_raw(writer, self.markup.raw_children.at(2))?;
5021 writer
5022 .write_event(Event::End(BytesEnd::new("c:strRef")))
5023 .map_err(OxmlError::from)?;
5024 Ok(())
5025 }
5026}
5027
5028#[derive(Clone, Debug, PartialEq)]
5030pub struct NumericData {
5031 pub formula: String,
5032 pub format_code: String,
5033 pub values: Vec<f64>,
5034 markup: ReferenceMarkup,
5035}
5036
5037impl NumericData {
5038 pub fn new(formula: String, format_code: String, values: Vec<f64>) -> Result<Self> {
5039 validate_formula(&formula, "c:numRef/c:f")?;
5040 validate_format_code(&format_code)?;
5041 validate_numeric_values(&values)?;
5042 validate_point_count(values.len(), "c:numCache")?;
5043 Ok(Self {
5044 formula,
5045 format_code,
5046 values,
5047 markup: ReferenceMarkup::default(),
5048 })
5049 }
5050
5051 pub fn from_xml(xml: &[u8]) -> Result<Self> {
5052 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
5053 }
5054
5055 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
5056 let parsed = parse_reference(xml, b"numRef", b"numCache", true, inherited)?;
5057 let format_code = parsed.format_code.unwrap_or_else(|| "General".to_owned());
5058 let mut values = Vec::with_capacity(parsed.values.len());
5059 for value in parsed.values {
5060 let number = value.parse::<f64>().map_err(|_| ChartError::InvalidValue {
5061 element: "c:v".to_owned(),
5062 value: value.clone(),
5063 })?;
5064 if !number.is_finite() {
5065 return Err(ChartError::InvalidValue {
5066 element: "c:v".to_owned(),
5067 value,
5068 });
5069 }
5070 values.push(number);
5071 }
5072 let data = Self {
5073 formula: parsed.formula,
5074 format_code,
5075 values,
5076 markup: parsed.markup,
5077 };
5078 data.validate()?;
5079 Ok(data)
5080 }
5081
5082 pub fn to_xml(&self) -> Result<Vec<u8>> {
5083 self.validate()?;
5084 let mut writer = Writer::new(Vec::new());
5085 self.write_xml(&mut writer, true)?;
5086 Ok(writer.into_inner())
5087 }
5088
5089 fn validate(&self) -> Result<()> {
5090 validate_formula(&self.formula, "c:numRef/c:f")?;
5091 validate_format_code(&self.format_code)?;
5092 validate_numeric_values(&self.values)?;
5093 validate_point_count(self.values.len(), "c:numCache")
5094 }
5095
5096 fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
5097 self.validate()?;
5098 let mut start = BytesStart::new("c:numRef");
5099 if standalone {
5100 start.push_attribute(("xmlns:c", C_NS));
5101 }
5102 push_attributes(&mut start, &self.markup.raw_attributes);
5103 writer
5104 .write_event(Event::Start(start))
5105 .map_err(OxmlError::from)?;
5106 emit_raw(writer, self.markup.raw_children.at(0))?;
5107 write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
5108 emit_raw(writer, self.markup.raw_children.at(1))?;
5109 write_numeric_cache(writer, self, &self.markup)?;
5110 emit_raw(writer, self.markup.raw_children.at(2))?;
5111 writer
5112 .write_event(Event::End(BytesEnd::new("c:numRef")))
5113 .map_err(OxmlError::from)?;
5114 Ok(())
5115 }
5116}
5117
5118#[derive(Clone, Debug, PartialEq)]
5119pub enum AxisData {
5120 String(StringRef),
5121 Numeric(NumericData),
5122}
5123
5124#[derive(Clone, Debug, PartialEq)]
5125struct DataPoint {
5126 index: u32,
5127 sp_pr: CT_ShapeProperties,
5128}
5129
5130impl DataPoint {
5131 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
5132 writer
5133 .write_event(Event::Start(BytesStart::new("c:dPt")))
5134 .map_err(OxmlError::from)?;
5135 write_scalar(writer, "c:idx", &self.index.to_string(), None)?;
5136 self.sp_pr.write_xml_as(writer, "c:spPr")?;
5137 writer
5138 .write_event(Event::End(BytesEnd::new("c:dPt")))
5139 .map_err(OxmlError::from)?;
5140 Ok(())
5141 }
5142}
5143
5144#[derive(Clone, Debug, PartialEq)]
5146pub struct Series {
5147 pub index: u32,
5148 pub order: u32,
5149 pub name: Option<StringRef>,
5150 pub categories: Option<AxisData>,
5151 pub values: NumericData,
5152 pub bubble_size: Option<NumericData>,
5153 pub sp_pr: Option<CT_ShapeProperties>,
5154 pub data_labels: Option<CT_DLbls>,
5155 authored_data_points: Vec<DataPoint>,
5156 index_markup: ScalarMarkup,
5157 order_markup: ScalarMarkup,
5158 name_markup: Option<WrapperMarkup>,
5159 categories_markup: Option<WrapperMarkup>,
5160 values_markup: WrapperMarkup,
5161 bubble_size_markup: Option<WrapperMarkup>,
5162 opaque_name: bool,
5163 opaque_categories: bool,
5164 opaque_bubble_size: bool,
5165 uses_scatter_wrappers: bool,
5166 parsed_from_xml: bool,
5167 raw_attributes: XmlAttributes,
5168 namespace_declarations: XmlAttributes,
5169 raw_children: OrderedRawChildren,
5170}
5171
5172impl Series {
5173 pub fn new(index: u32, order: u32, values: NumericData) -> Self {
5174 Self {
5175 index,
5176 order,
5177 name: None,
5178 categories: None,
5179 values,
5180 bubble_size: None,
5181 sp_pr: None,
5182 data_labels: None,
5183 authored_data_points: Vec::new(),
5184 index_markup: ScalarMarkup::default(),
5185 order_markup: ScalarMarkup::default(),
5186 name_markup: None,
5187 categories_markup: None,
5188 values_markup: WrapperMarkup::default(),
5189 bubble_size_markup: None,
5190 opaque_name: false,
5191 opaque_categories: false,
5192 opaque_bubble_size: false,
5193 uses_scatter_wrappers: false,
5194 parsed_from_xml: false,
5195 raw_attributes: Vec::new(),
5196 namespace_declarations: Vec::new(),
5197 raw_children: OrderedRawChildren::default(),
5198 }
5199 }
5200
5201 pub fn from_xml(xml: &[u8]) -> Result<Self> {
5202 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
5203 }
5204
5205 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
5206 let mut reader = Reader::from_reader(xml);
5207 let mut buffer = Vec::new();
5208 loop {
5209 match reader
5210 .read_event_into(&mut buffer)
5211 .map_err(OxmlError::from)?
5212 {
5213 Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
5214 chart_root_prefix(&element)?;
5215 if !element_is_in_namespace(&element, C_NS, inherited)? {
5216 return Err(ChartError::UnexpectedElement(element_name(&element)));
5217 }
5218 return Self::from_element(&mut reader, &element, inherited);
5219 }
5220 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
5221 return Err(ChartError::MissingElement("c:idx".to_owned()));
5222 }
5223 Event::Start(element) | Event::Empty(element) => {
5224 return Err(ChartError::UnexpectedElement(element_name(&element)));
5225 }
5226 Event::Eof => return Err(ChartError::MissingElement("c:ser".to_owned())),
5227 _ => {}
5228 }
5229 buffer.clear();
5230 }
5231 }
5232
5233 fn from_element(
5234 reader: &mut Reader<&[u8]>,
5235 start: &BytesStart<'_>,
5236 inherited: &NamespaceBindings,
5237 ) -> Result<Self> {
5238 reject_conflicting_prefix(start, b"a", A_NS)?;
5239 let namespaces = chart_bindings(inherited, start)?;
5240 require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
5241 require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
5242 require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
5243 let (raw_attributes, _) =
5244 capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
5245 let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
5246 let mut state = SeriesParseState::default();
5247 let mut buffer = Vec::new();
5248 loop {
5249 match reader
5250 .read_event_into(&mut buffer)
5251 .map_err(OxmlError::from)?
5252 {
5253 Event::Start(element) => {
5254 let name = chart_child_local(&element, &namespaces)?;
5255 let raw = capture_element(reader, &element)?;
5256 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
5257 }
5258 Event::Empty(element) => {
5259 let name = chart_child_local(&element, &namespaces)?;
5260 let raw = capture_empty_element(&element)?;
5261 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
5262 }
5263 event @ (Event::Text(_)
5264 | Event::CData(_)
5265 | Event::Comment(_)
5266 | Event::PI(_)
5267 | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
5268 Event::End(element) if matches_local_name(element.name().as_ref(), b"ser") => {
5269 break;
5270 }
5271 Event::Eof => return Err(missing_end("c:ser")),
5272 _ => {}
5273 }
5274 buffer.clear();
5275 }
5276 state.finish(raw_attributes, namespace_declarations)
5277 }
5278
5279 pub fn to_xml(&self) -> Result<Vec<u8>> {
5280 self.to_xml_for_plot(self.uses_scatter_wrappers)
5281 }
5282
5283 fn to_xml_for_plot(&self, scatter: bool) -> Result<Vec<u8>> {
5284 if self.opaque_name && self.name.is_some() {
5285 return Err(ChartError::DuplicateElement("c:tx".to_owned()));
5286 }
5287 if self.opaque_categories && self.categories.is_some() {
5288 return Err(ChartError::DuplicateElement("c:cat".to_owned()));
5289 }
5290 if self.opaque_bubble_size && self.bubble_size.is_some() {
5291 return Err(ChartError::DuplicateElement("c:bubbleSize".to_owned()));
5292 }
5293 self.values.validate()?;
5294 if let Some(name) = &self.name {
5295 name.validate()?;
5296 }
5297 if let Some(categories) = &self.categories {
5298 if scatter && !matches!(categories, AxisData::Numeric(_)) {
5299 return Err(ChartError::InvalidValue {
5300 element: "c:xVal".to_owned(),
5301 value: "scatter x values must use a numeric cache".to_owned(),
5302 });
5303 }
5304 match categories {
5305 AxisData::String(reference) => reference.validate()?,
5306 AxisData::Numeric(reference) => reference.validate()?,
5307 }
5308 }
5309 if scatter && self.categories.is_none() {
5310 return Err(ChartError::MissingElement("c:xVal".to_owned()));
5311 }
5312 if let Some(size) = &self.bubble_size {
5313 size.validate()?;
5314 }
5315 if let Some(labels) = &self.data_labels {
5316 labels.validate()?;
5317 }
5318 let mut writer = Writer::new(Vec::new());
5319 let mut start = BytesStart::new("c:ser");
5320 start.push_attribute(("xmlns:c", C_NS));
5321 start.push_attribute(("xmlns:a", A_NS));
5322 start.push_attribute(("xmlns:r", R_NS));
5323 push_attributes(&mut start, &self.namespace_declarations);
5324 push_attributes(&mut start, &self.raw_attributes);
5325 writer
5326 .write_event(Event::Start(start))
5327 .map_err(OxmlError::from)?;
5328 emit_raw(&mut writer, self.raw_children.at(0))?;
5329 write_scalar(
5330 &mut writer,
5331 "c:idx",
5332 &self.index.to_string(),
5333 Some(&self.index_markup),
5334 )?;
5335 emit_raw(&mut writer, self.raw_children.at(1))?;
5336 write_scalar(
5337 &mut writer,
5338 "c:order",
5339 &self.order.to_string(),
5340 Some(&self.order_markup),
5341 )?;
5342 emit_raw(&mut writer, self.raw_children.at(2))?;
5343 if let Some(name) = &self.name {
5344 write_wrapper_start(
5345 &mut writer,
5346 "c:tx",
5347 self.name_markup
5348 .as_ref()
5349 .unwrap_or(&WrapperMarkup::default()),
5350 )?;
5351 name.write_xml(&mut writer, false)?;
5352 write_wrapper_end(
5353 &mut writer,
5354 "c:tx",
5355 self.name_markup
5356 .as_ref()
5357 .unwrap_or(&WrapperMarkup::default()),
5358 )?;
5359 }
5360 emit_raw(&mut writer, self.raw_children.at(3))?;
5361 if let Some(properties) = &self.sp_pr {
5362 properties.write_xml_as(&mut writer, "c:spPr")?;
5363 }
5364 emit_raw(&mut writer, self.raw_children.at(4))?;
5365 for point in &self.authored_data_points {
5366 point.write_xml(&mut writer)?;
5367 }
5368 if let Some(labels) = &self.data_labels {
5369 labels.write_xml(&mut writer, false)?;
5370 }
5371 emit_raw(&mut writer, self.raw_children.at(5))?;
5372 if let Some(categories) = &self.categories {
5373 let default_markup = WrapperMarkup::default();
5374 let markup = self.categories_markup.as_ref().unwrap_or(&default_markup);
5375 write_wrapper_start(
5376 &mut writer,
5377 if scatter { "c:xVal" } else { "c:cat" },
5378 markup,
5379 )?;
5380 match categories {
5381 AxisData::String(reference) => reference.write_xml(&mut writer, false)?,
5382 AxisData::Numeric(reference) => reference.write_xml(&mut writer, false)?,
5383 }
5384 write_wrapper_end(
5385 &mut writer,
5386 if scatter { "c:xVal" } else { "c:cat" },
5387 markup,
5388 )?;
5389 }
5390 emit_raw(&mut writer, self.raw_children.at(6))?;
5391 write_wrapper_start(
5392 &mut writer,
5393 if scatter { "c:yVal" } else { "c:val" },
5394 &self.values_markup,
5395 )?;
5396 self.values.write_xml(&mut writer, false)?;
5397 write_wrapper_end(
5398 &mut writer,
5399 if scatter { "c:yVal" } else { "c:val" },
5400 &self.values_markup,
5401 )?;
5402 emit_raw(&mut writer, self.raw_children.at(7))?;
5403 if let Some(size) = &self.bubble_size {
5404 let default_markup = WrapperMarkup::default();
5405 let markup = self.bubble_size_markup.as_ref().unwrap_or(&default_markup);
5406 write_wrapper_start(&mut writer, "c:bubbleSize", markup)?;
5407 size.write_xml(&mut writer, false)?;
5408 write_wrapper_end(&mut writer, "c:bubbleSize", markup)?;
5409 }
5410 emit_raw(&mut writer, self.raw_children.at(8))?;
5411 writer
5412 .write_event(Event::End(BytesEnd::new("c:ser")))
5413 .map_err(OxmlError::from)?;
5414 Ok(writer.into_inner())
5415 }
5416
5417 pub fn raw_children(&self) -> &OrderedRawChildren {
5418 &self.raw_children
5419 }
5420}
5421
5422#[derive(Default)]
5423struct SeriesParseState {
5424 index: Option<(u32, ScalarMarkup)>,
5425 order: Option<(u32, ScalarMarkup)>,
5426 name: Option<(StringRef, WrapperMarkup)>,
5427 categories: Option<(AxisData, WrapperMarkup)>,
5428 values: Option<(NumericData, WrapperMarkup)>,
5429 bubble_size: Option<(NumericData, WrapperMarkup)>,
5430 data_labels: Option<CT_DLbls>,
5431 name_seen: bool,
5432 categories_seen: bool,
5433 bubble_size_seen: bool,
5434 opaque_name: bool,
5435 opaque_categories: bool,
5436 opaque_bubble_size: bool,
5437 saw_standard_wrapper: bool,
5438 saw_scatter_wrapper: bool,
5439 sp_pr: Option<CT_ShapeProperties>,
5440 raw_children: OrderedRawChildren,
5441 boundary: usize,
5442}
5443
5444impl SeriesParseState {
5445 fn capture_event(&mut self, raw: Vec<u8>) {
5446 self.raw_children.push(self.boundary, raw);
5447 }
5448
5449 fn parse_child(
5450 &mut self,
5451 name: &[u8],
5452 raw: Vec<u8>,
5453 namespaces: &NamespaceBindings,
5454 ) -> Result<()> {
5455 match name {
5456 b"idx" => {
5457 set_once(&mut self.index, parse_u32_scalar(&raw, "idx")?, "c:idx")?;
5458 self.boundary = self.boundary.max(1);
5459 }
5460 b"order" => {
5461 set_once(&mut self.order, parse_u32_scalar(&raw, "order")?, "c:order")?;
5462 self.boundary = self.boundary.max(2);
5463 }
5464 b"tx" => {
5465 mark_once(&mut self.name_seen, "c:tx")?;
5466 let parsed = parse_wrapper(&raw, b"tx", &[b"strRef"], namespaces)?;
5467 if let Some((_, reference)) = parsed.choice {
5468 self.name = Some((
5469 StringRef::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
5470 parsed.markup,
5471 ));
5472 } else {
5473 self.opaque_name = true;
5474 self.raw_children.push(2, raw);
5475 }
5476 self.boundary = self.boundary.max(3);
5477 }
5478 b"spPr" => {
5479 parse_chart_shape_properties(&raw, namespaces, &mut self.sp_pr)?;
5480 self.boundary = self.boundary.max(4);
5481 }
5482 b"dLbls" => {
5483 set_once(
5484 &mut self.data_labels,
5485 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
5486 "c:dLbls",
5487 )?;
5488 self.boundary = self.boundary.max(5);
5489 }
5490 b"cat" => {
5491 self.saw_standard_wrapper = true;
5492 mark_once(&mut self.categories_seen, "c:cat")?;
5493 let parsed = parse_wrapper(&raw, b"cat", &[b"strRef", b"numRef"], namespaces)?;
5494 if let Some((choice, reference)) = parsed.choice {
5495 let categories = if choice == b"strRef" {
5496 AxisData::String(StringRef::from_xml_with_namespaces(
5497 &reference,
5498 &parsed.namespaces,
5499 )?)
5500 } else {
5501 AxisData::Numeric(NumericData::from_xml_with_namespaces(
5502 &reference,
5503 &parsed.namespaces,
5504 )?)
5505 };
5506 self.categories = Some((categories, parsed.markup));
5507 } else {
5508 self.opaque_categories = true;
5509 self.raw_children.push(5, raw);
5510 }
5511 self.boundary = self.boundary.max(6);
5512 }
5513 b"xVal" => {
5514 self.saw_scatter_wrapper = true;
5515 mark_once(&mut self.categories_seen, "c:xVal")?;
5516 let parsed = parse_wrapper(&raw, b"xVal", &[b"numRef"], namespaces)?;
5517 let (_, reference) = parsed
5518 .choice
5519 .ok_or_else(|| ChartError::MissingElement("c:xVal/c:numRef".to_owned()))?;
5520 self.categories = Some((
5521 AxisData::Numeric(NumericData::from_xml_with_namespaces(
5522 &reference,
5523 &parsed.namespaces,
5524 )?),
5525 parsed.markup,
5526 ));
5527 self.boundary = self.boundary.max(6);
5528 }
5529 b"val" | b"yVal" => {
5530 if name == b"yVal" {
5531 self.saw_scatter_wrapper = true;
5532 } else {
5533 self.saw_standard_wrapper = true;
5534 }
5535 let wrapper = if name == b"yVal" {
5536 b"yVal".as_slice()
5537 } else {
5538 b"val".as_slice()
5539 };
5540 let parsed = parse_wrapper(&raw, wrapper, &[b"numRef"], namespaces)?;
5541 let (_, reference) = parsed.choice.ok_or_else(|| {
5542 ChartError::MissingElement(format!(
5543 "c:{}/c:numRef",
5544 String::from_utf8_lossy(wrapper)
5545 ))
5546 })?;
5547 set_once(
5548 &mut self.values,
5549 (
5550 NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
5551 parsed.markup,
5552 ),
5553 "c:val or c:yVal",
5554 )?;
5555 self.boundary = self.boundary.max(7);
5556 }
5557 b"bubbleSize" => {
5558 mark_once(&mut self.bubble_size_seen, "c:bubbleSize")?;
5559 let parsed = parse_wrapper(&raw, b"bubbleSize", &[b"numRef"], namespaces)?;
5560 if let Some((_, reference)) = parsed.choice {
5561 self.bubble_size = Some((
5562 NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
5563 parsed.markup,
5564 ));
5565 } else {
5566 self.opaque_bubble_size = true;
5567 self.raw_children.push(7, raw);
5568 }
5569 self.boundary = self.boundary.max(8);
5570 }
5571 _ => {
5572 let boundary = series_raw_boundary(name, self.boundary);
5573 self.raw_children.push(boundary, raw);
5574 self.boundary = self.boundary.max(boundary);
5575 }
5576 }
5577 Ok(())
5578 }
5579
5580 fn finish(
5581 self,
5582 raw_attributes: XmlAttributes,
5583 namespace_declarations: XmlAttributes,
5584 ) -> Result<Series> {
5585 if self.saw_standard_wrapper && self.saw_scatter_wrapper {
5586 return Err(ChartError::InvalidValue {
5587 element: "c:ser".to_owned(),
5588 value: "category/value and x/y wrappers cannot be combined".to_owned(),
5589 });
5590 }
5591 let (index, index_markup) = self
5592 .index
5593 .ok_or_else(|| ChartError::MissingElement("c:idx".to_owned()))?;
5594 let (order, order_markup) = self
5595 .order
5596 .ok_or_else(|| ChartError::MissingElement("c:order".to_owned()))?;
5597 let (values, values_markup) = self
5598 .values
5599 .ok_or_else(|| ChartError::MissingElement("c:val".to_owned()))?;
5600 let (name, name_markup) = self
5601 .name
5602 .map(|(value, markup)| (Some(value), Some(markup)))
5603 .unwrap_or((None, None));
5604 let (categories, categories_markup) = self
5605 .categories
5606 .map(|(value, markup)| (Some(value), Some(markup)))
5607 .unwrap_or((None, None));
5608 let (bubble_size, bubble_size_markup) = self
5609 .bubble_size
5610 .map(|(value, markup)| (Some(value), Some(markup)))
5611 .unwrap_or((None, None));
5612 Ok(Series {
5613 index,
5614 order,
5615 name,
5616 categories,
5617 values,
5618 bubble_size,
5619 sp_pr: self.sp_pr,
5620 data_labels: self.data_labels,
5621 authored_data_points: Vec::new(),
5622 index_markup,
5623 order_markup,
5624 name_markup,
5625 categories_markup,
5626 values_markup,
5627 bubble_size_markup,
5628 opaque_name: self.opaque_name,
5629 opaque_categories: self.opaque_categories,
5630 opaque_bubble_size: self.opaque_bubble_size,
5631 uses_scatter_wrappers: self.saw_scatter_wrapper,
5632 parsed_from_xml: true,
5633 raw_attributes,
5634 namespace_declarations,
5635 raw_children: raw_children_in_schema_order(&self.raw_children, 8),
5636 })
5637 }
5638}
5639
5640fn series_raw_boundary(name: &[u8], current: usize) -> usize {
5641 match name {
5642 b"marker" | b"invertIfNegative" | b"pictureOptions" | b"explosion" | b"dPt" => 4,
5643 b"trendline" | b"errBars" => 5,
5644 b"shape" | b"smooth" => 7,
5645 b"extLst" => 8,
5646 _ => current,
5647 }
5648}
5649
5650struct ParsedReference {
5651 formula: String,
5652 format_code: Option<String>,
5653 values: Vec<String>,
5654 markup: ReferenceMarkup,
5655}
5656
5657fn parse_reference(
5658 xml: &[u8],
5659 reference_local: &[u8],
5660 cache_local: &[u8],
5661 numeric: bool,
5662 inherited: &NamespaceBindings,
5663) -> Result<ParsedReference> {
5664 let mut reader = Reader::from_reader(xml);
5665 let mut buffer = Vec::new();
5666 loop {
5667 match reader
5668 .read_event_into(&mut buffer)
5669 .map_err(OxmlError::from)?
5670 {
5671 Event::Start(element)
5672 if matches_local_name(element.name().as_ref(), reference_local) =>
5673 {
5674 let namespaces = typed_rewrite_bindings(&element, inherited)?;
5675 let raw_attributes = capture_fixed_attributes(&element, &["xmlns:c"])?;
5676 let mut formula: Option<(String, TextMarkup)> = None;
5677 let mut cache: Option<(Option<String>, Vec<String>, CacheMarkup)> = None;
5678 let mut raw_children = OrderedRawChildren::default();
5679 let mut boundary = 0usize;
5680 let mut inner = Vec::new();
5681 loop {
5682 match reader
5683 .read_event_into(&mut inner)
5684 .map_err(OxmlError::from)?
5685 {
5686 Event::Start(child) => {
5687 let name = chart_child_local(&child, &namespaces)?;
5688 let raw = capture_element(&mut reader, &child)?;
5689 match name.as_deref().unwrap_or_default() {
5690 b"f" => {
5691 set_once(
5692 &mut formula,
5693 parse_text_element(&raw, b"f", &namespaces)?,
5694 "c:f",
5695 )?;
5696 boundary = boundary.max(1);
5697 }
5698 name if name == cache_local => {
5699 set_once(
5700 &mut cache,
5701 parse_cache(&raw, cache_local, numeric, &namespaces)?,
5702 &format!("c:{}", String::from_utf8_lossy(cache_local)),
5703 )?;
5704 boundary = boundary.max(2);
5705 }
5706 _ => raw_children.push(boundary, raw),
5707 }
5708 }
5709 Event::Empty(child) => {
5710 let name = chart_child_local(&child, &namespaces)?;
5711 let raw = capture_empty_element(&child)?;
5712 match name.as_deref().unwrap_or_default() {
5713 b"f" => {
5714 set_once(
5715 &mut formula,
5716 parse_text_element(&raw, b"f", &namespaces)?,
5717 "c:f",
5718 )?;
5719 boundary = boundary.max(1);
5720 }
5721 name if name == cache_local => {
5722 set_once(
5723 &mut cache,
5724 parse_cache(&raw, cache_local, numeric, &namespaces)?,
5725 &format!("c:{}", String::from_utf8_lossy(cache_local)),
5726 )?;
5727 boundary = boundary.max(2);
5728 }
5729 _ => raw_children.push(boundary, raw),
5730 }
5731 }
5732 event @ (Event::Text(_)
5733 | Event::CData(_)
5734 | Event::Comment(_)
5735 | Event::PI(_)
5736 | Event::GeneralRef(_)) => {
5737 raw_children.push(boundary, capture_event(event)?);
5738 }
5739 Event::End(end)
5740 if matches_local_name(end.name().as_ref(), reference_local) =>
5741 {
5742 let (formula, formula_markup) = formula
5743 .ok_or_else(|| ChartError::MissingElement("c:f".to_owned()))?;
5744 validate_formula(&formula, "c:f")?;
5745 let (format_code, values, cache_markup) = cache.ok_or_else(|| {
5746 ChartError::MissingElement(format!(
5747 "c:{}",
5748 String::from_utf8_lossy(cache_local)
5749 ))
5750 })?;
5751 let markup = ReferenceMarkup {
5752 raw_attributes,
5753 raw_children: raw_children_in_schema_order(&raw_children, 2),
5754 formula: formula_markup,
5755 cache_attributes: cache_markup.raw_attributes,
5756 cache_children: cache_markup.raw_children,
5757 format_code: cache_markup.format_code,
5758 format_code_omitted: cache_markup.format_code_omitted,
5759 point_count: cache_markup.point_count,
5760 declared_point_count: cache_markup.declared_point_count,
5761 point_indexes: cache_markup.point_indexes,
5762 points: cache_markup.points,
5763 };
5764 return Ok(ParsedReference {
5765 formula,
5766 format_code,
5767 values,
5768 markup,
5769 });
5770 }
5771 Event::Eof => {
5772 return Err(missing_end(&format!(
5773 "c:{}",
5774 String::from_utf8_lossy(reference_local)
5775 )));
5776 }
5777 _ => {}
5778 }
5779 inner.clear();
5780 }
5781 }
5782 Event::Empty(element)
5783 if matches_local_name(element.name().as_ref(), reference_local) =>
5784 {
5785 return Err(ChartError::MissingElement("c:f".to_owned()));
5786 }
5787 Event::Start(element) | Event::Empty(element) => {
5788 return Err(ChartError::UnexpectedElement(element_name(&element)));
5789 }
5790 Event::Eof => {
5791 return Err(ChartError::MissingElement(format!(
5792 "c:{}",
5793 String::from_utf8_lossy(reference_local)
5794 )));
5795 }
5796 _ => {}
5797 }
5798 buffer.clear();
5799 }
5800}
5801
5802struct CacheMarkup {
5803 raw_attributes: XmlAttributes,
5804 raw_children: OrderedRawChildren,
5805 format_code: Option<TextMarkup>,
5806 format_code_omitted: bool,
5807 point_count: ScalarMarkup,
5808 declared_point_count: Option<u32>,
5809 point_indexes: Vec<u32>,
5810 points: Vec<PointMarkup>,
5811}
5812
5813fn parse_cache(
5814 xml: &[u8],
5815 cache_local: &[u8],
5816 numeric: bool,
5817 inherited: &NamespaceBindings,
5818) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
5819 let mut reader = Reader::from_reader(xml);
5820 let mut buffer = Vec::new();
5821 loop {
5822 match reader
5823 .read_event_into(&mut buffer)
5824 .map_err(OxmlError::from)?
5825 {
5826 Event::Start(element) if matches_local_name(element.name().as_ref(), cache_local) => {
5827 let namespaces = typed_rewrite_bindings(&element, inherited)?;
5828 let raw_attributes = capture_attributes(&element)?;
5829 let mut state = CacheParseState::new(numeric, namespaces);
5830 let mut inner = Vec::new();
5831 loop {
5832 match reader
5833 .read_event_into(&mut inner)
5834 .map_err(OxmlError::from)?
5835 {
5836 Event::Start(child) => {
5837 let name = chart_child_local(&child, &state.namespaces)?;
5838 let raw = capture_element(&mut reader, &child)?;
5839 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
5840 }
5841 Event::Empty(child) => {
5842 let name = chart_child_local(&child, &state.namespaces)?;
5843 let raw = capture_empty_element(&child)?;
5844 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
5845 }
5846 event @ (Event::Text(_)
5847 | Event::CData(_)
5848 | Event::Comment(_)
5849 | Event::PI(_)
5850 | Event::GeneralRef(_)) => {
5851 state.capture_event(capture_event(event)?);
5852 }
5853 Event::End(end) if matches_local_name(end.name().as_ref(), cache_local) => {
5854 return state.finish(raw_attributes);
5855 }
5856 Event::Eof => {
5857 return Err(missing_end(&format!(
5858 "c:{}",
5859 String::from_utf8_lossy(cache_local)
5860 )));
5861 }
5862 _ => {}
5863 }
5864 inner.clear();
5865 }
5866 }
5867 Event::Empty(element) if matches_local_name(element.name().as_ref(), cache_local) => {
5868 return Err(ChartError::MissingElement("c:ptCount".to_owned()));
5869 }
5870 Event::Start(element) | Event::Empty(element) => {
5871 return Err(ChartError::UnexpectedElement(element_name(&element)));
5872 }
5873 Event::Eof => {
5874 return Err(ChartError::MissingElement(format!(
5875 "c:{}",
5876 String::from_utf8_lossy(cache_local)
5877 )));
5878 }
5879 _ => {}
5880 }
5881 buffer.clear();
5882 }
5883}
5884
5885struct CacheParseState {
5886 numeric: bool,
5887 namespaces: NamespaceBindings,
5888 point_base: usize,
5889 boundary: usize,
5890 format_code: Option<(String, TextMarkup)>,
5891 point_count: Option<(u32, ScalarMarkup)>,
5892 points: Vec<(u32, String, PointMarkup)>,
5893 raw_children: OrderedRawChildren,
5894}
5895
5896impl CacheParseState {
5897 fn new(numeric: bool, namespaces: NamespaceBindings) -> Self {
5898 Self {
5899 numeric,
5900 namespaces,
5901 point_base: usize::from(numeric) + 1,
5902 boundary: 0,
5903 format_code: None,
5904 point_count: None,
5905 points: Vec::new(),
5906 raw_children: OrderedRawChildren::default(),
5907 }
5908 }
5909
5910 fn capture_event(&mut self, raw: Vec<u8>) {
5911 self.raw_children.push(self.boundary, raw);
5912 }
5913
5914 fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
5915 match name {
5916 b"formatCode" if self.numeric => {
5917 set_once(
5918 &mut self.format_code,
5919 parse_text_element(&raw, b"formatCode", &self.namespaces)?,
5920 "c:formatCode",
5921 )?;
5922 self.boundary = self.boundary.max(1);
5923 }
5924 b"ptCount" => {
5925 set_once(
5926 &mut self.point_count,
5927 parse_u32_scalar(&raw, "ptCount")?,
5928 "c:ptCount",
5929 )?;
5930 self.boundary = self.boundary.max(self.point_base);
5931 }
5932 b"pt" => {
5933 self.points.push(parse_point(&raw, &self.namespaces)?);
5934 self.boundary = self.boundary.max(self.point_base + self.points.len());
5935 }
5936 _ => self.raw_children.push(self.boundary, raw),
5937 }
5938 Ok(())
5939 }
5940
5941 fn finish(
5942 self,
5943 raw_attributes: XmlAttributes,
5944 ) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
5945 let (declared, point_count_markup) = self
5946 .point_count
5947 .ok_or_else(|| ChartError::MissingElement("c:ptCount".to_owned()))?;
5948 let actual = u32::try_from(self.points.len()).map_err(|_| ChartError::InvalidValue {
5949 element: "c:ptCount".to_owned(),
5950 value: self.points.len().to_string(),
5951 })?;
5952 if declared < actual {
5953 return Err(invalid_attribute("ptCount", "val", declared.to_string()));
5954 }
5955 let mut values = Vec::with_capacity(self.points.len());
5956 let mut point_indexes = Vec::with_capacity(self.points.len());
5957 let mut point_markup = Vec::with_capacity(self.points.len());
5958 let mut previous = None;
5959 for (index, value, markup) in self.points {
5960 if index >= declared || previous.is_some_and(|last| index <= last) {
5961 return Err(invalid_attribute("pt", "idx", index.to_string()));
5962 }
5963 previous = Some(index);
5964 values.push(value);
5965 point_indexes.push(index);
5966 point_markup.push(markup);
5967 }
5968 let (format_code, format_markup) = self
5969 .format_code
5970 .map(|(value, markup)| (Some(value), Some(markup)))
5971 .unwrap_or((None, None));
5972 let format_code_omitted = self.numeric && format_code.is_none();
5973 Ok((
5974 format_code,
5975 values,
5976 CacheMarkup {
5977 raw_attributes,
5978 raw_children: raw_children_in_schema_order(
5979 &self.raw_children,
5980 self.point_base + point_markup.len(),
5981 ),
5982 format_code: format_markup,
5983 format_code_omitted,
5984 point_count: point_count_markup,
5985 declared_point_count: Some(declared),
5986 point_indexes,
5987 points: point_markup,
5988 },
5989 ))
5990 }
5991}
5992
5993fn parse_point(xml: &[u8], inherited: &NamespaceBindings) -> Result<(u32, String, PointMarkup)> {
5994 let mut reader = Reader::from_reader(xml);
5995 let mut buffer = Vec::new();
5996 loop {
5997 match reader
5998 .read_event_into(&mut buffer)
5999 .map_err(OxmlError::from)?
6000 {
6001 Event::Start(element) if matches_local_name(element.name().as_ref(), b"pt") => {
6002 let namespaces = typed_rewrite_bindings(&element, inherited)?;
6003 let index = required_u32_attribute(&element, "pt", b"idx")?;
6004 let raw_attributes = capture_attributes_except(&element, b"idx")?;
6005 let mut value: Option<(String, TextMarkup)> = None;
6006 let mut raw_children = OrderedRawChildren::default();
6007 let mut boundary = 0usize;
6008 let mut inner = Vec::new();
6009 loop {
6010 match reader
6011 .read_event_into(&mut inner)
6012 .map_err(OxmlError::from)?
6013 {
6014 Event::Start(child) => {
6015 let name = chart_child_local(&child, &namespaces)?;
6016 let raw = capture_element(&mut reader, &child)?;
6017 if name.as_deref() == Some(b"v") {
6018 set_once(
6019 &mut value,
6020 parse_text_element(&raw, b"v", &namespaces)?,
6021 "c:v",
6022 )?;
6023 boundary = 1;
6024 } else {
6025 raw_children.push(boundary, raw);
6026 }
6027 }
6028 Event::Empty(child) => {
6029 let name = chart_child_local(&child, &namespaces)?;
6030 let raw = capture_empty_element(&child)?;
6031 if name.as_deref() == Some(b"v") {
6032 set_once(
6033 &mut value,
6034 parse_text_element(&raw, b"v", &namespaces)?,
6035 "c:v",
6036 )?;
6037 boundary = 1;
6038 } else {
6039 raw_children.push(boundary, raw);
6040 }
6041 }
6042 event @ (Event::Text(_)
6043 | Event::CData(_)
6044 | Event::Comment(_)
6045 | Event::PI(_)
6046 | Event::GeneralRef(_)) => {
6047 raw_children.push(boundary, capture_event(event)?);
6048 }
6049 Event::End(end) if matches_local_name(end.name().as_ref(), b"pt") => {
6050 let (value, value_markup) = value
6051 .ok_or_else(|| ChartError::MissingElement("c:v".to_owned()))?;
6052 return Ok((
6053 index,
6054 value,
6055 PointMarkup {
6056 raw_attributes,
6057 raw_children: raw_children_in_schema_order(&raw_children, 1),
6058 value: value_markup,
6059 },
6060 ));
6061 }
6062 Event::Eof => return Err(missing_end("c:pt")),
6063 _ => {}
6064 }
6065 inner.clear();
6066 }
6067 }
6068 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pt") => {
6069 return Err(ChartError::MissingElement("c:v".to_owned()));
6070 }
6071 Event::Start(element) | Event::Empty(element) => {
6072 return Err(ChartError::UnexpectedElement(element_name(&element)));
6073 }
6074 Event::Eof => return Err(ChartError::MissingElement("c:pt".to_owned())),
6075 _ => {}
6076 }
6077 buffer.clear();
6078 }
6079}
6080
6081fn parse_text_element(
6082 xml: &[u8],
6083 local: &[u8],
6084 inherited: &NamespaceBindings,
6085) -> Result<(String, TextMarkup)> {
6086 let mut reader = Reader::from_reader(xml);
6087 let mut buffer = Vec::new();
6088 loop {
6089 match reader
6090 .read_event_into(&mut buffer)
6091 .map_err(OxmlError::from)?
6092 {
6093 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
6094 typed_rewrite_bindings(&element, inherited)?;
6095 let raw_attributes = capture_attributes(&element)?;
6096 let mut value = String::new();
6097 let mut raw_children = OrderedRawChildren::default();
6098 let mut seen_text = false;
6099 let mut inner = Vec::new();
6100 loop {
6101 match reader
6102 .read_event_into(&mut inner)
6103 .map_err(OxmlError::from)?
6104 {
6105 Event::Text(text) => {
6106 value.push_str(&decode_plain(&text));
6107 seen_text = true;
6108 }
6109 Event::CData(text) => {
6110 let decoded =
6111 text.decode().map_err(|error| ChartError::InvalidValue {
6112 element: format!("c:{}", String::from_utf8_lossy(local)),
6113 value: error.to_string(),
6114 })?;
6115 value.push_str(&decoded);
6116 seen_text = true;
6117 }
6118 Event::GeneralRef(reference) => {
6119 value.push_str(&resolve_entity(&reference));
6120 seen_text = true;
6121 }
6122 event @ (Event::Comment(_) | Event::PI(_)) => {
6123 raw_children.push(usize::from(seen_text), capture_event(event)?);
6124 }
6125 Event::Start(child) | Event::Empty(child) => {
6126 return Err(ChartError::UnexpectedElement(element_name(&child)));
6127 }
6128 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
6129 return Ok((
6130 value,
6131 TextMarkup {
6132 raw_attributes,
6133 raw_children: raw_children_in_schema_order(&raw_children, 1),
6134 },
6135 ));
6136 }
6137 Event::Eof => {
6138 return Err(missing_end(&format!(
6139 "c:{}",
6140 String::from_utf8_lossy(local)
6141 )));
6142 }
6143 _ => {}
6144 }
6145 inner.clear();
6146 }
6147 }
6148 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
6149 typed_rewrite_bindings(&element, inherited)?;
6150 return Ok((
6151 String::new(),
6152 TextMarkup {
6153 raw_attributes: capture_attributes(&element)?,
6154 raw_children: OrderedRawChildren::default(),
6155 },
6156 ));
6157 }
6158 Event::Start(element) | Event::Empty(element) => {
6159 return Err(ChartError::UnexpectedElement(element_name(&element)));
6160 }
6161 Event::Eof => {
6162 return Err(ChartError::MissingElement(format!(
6163 "c:{}",
6164 String::from_utf8_lossy(local)
6165 )));
6166 }
6167 _ => {}
6168 }
6169 buffer.clear();
6170 }
6171}
6172
6173struct ParsedWrapper {
6174 choice: Option<(Vec<u8>, Vec<u8>)>,
6175 markup: WrapperMarkup,
6176 namespaces: NamespaceBindings,
6177}
6178
6179fn parse_wrapper(
6180 xml: &[u8],
6181 local: &[u8],
6182 choices: &[&[u8]],
6183 inherited: &NamespaceBindings,
6184) -> Result<ParsedWrapper> {
6185 let mut reader = Reader::from_reader(xml);
6186 let mut buffer = Vec::new();
6187 loop {
6188 match reader
6189 .read_event_into(&mut buffer)
6190 .map_err(OxmlError::from)?
6191 {
6192 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
6193 let namespaces = typed_rewrite_bindings(&element, inherited)?;
6194 let raw_attributes = capture_attributes(&element)?;
6195 let mut choice = None;
6196 let mut raw_children = OrderedRawChildren::default();
6197 let mut boundary = 0usize;
6198 let mut inner = Vec::new();
6199 loop {
6200 match reader
6201 .read_event_into(&mut inner)
6202 .map_err(OxmlError::from)?
6203 {
6204 Event::Start(child) => {
6205 let name = chart_child_local(&child, &namespaces)?;
6206 let raw = capture_element(&mut reader, &child)?;
6207 if let Some(name) =
6208 name.filter(|name| choices.contains(&name.as_slice()))
6209 {
6210 if choice.is_some() {
6211 return Err(ChartError::DuplicateElement(format!(
6212 "c:{} reference",
6213 String::from_utf8_lossy(local)
6214 )));
6215 }
6216 choice = Some((name, raw));
6217 boundary = 1;
6218 } else {
6219 raw_children.push(boundary, raw);
6220 }
6221 }
6222 Event::Empty(child) => {
6223 let name = chart_child_local(&child, &namespaces)?;
6224 let raw = capture_empty_element(&child)?;
6225 if let Some(name) =
6226 name.filter(|name| choices.contains(&name.as_slice()))
6227 {
6228 if choice.is_some() {
6229 return Err(ChartError::DuplicateElement(format!(
6230 "c:{} reference",
6231 String::from_utf8_lossy(local)
6232 )));
6233 }
6234 choice = Some((name, raw));
6235 boundary = 1;
6236 } else {
6237 raw_children.push(boundary, raw);
6238 }
6239 }
6240 event @ (Event::Text(_)
6241 | Event::CData(_)
6242 | Event::Comment(_)
6243 | Event::PI(_)
6244 | Event::GeneralRef(_)) => {
6245 raw_children.push(boundary, capture_event(event)?);
6246 }
6247 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
6248 return Ok(ParsedWrapper {
6249 choice,
6250 markup: WrapperMarkup {
6251 raw_attributes,
6252 raw_children: raw_children_in_schema_order(&raw_children, 1),
6253 },
6254 namespaces,
6255 });
6256 }
6257 Event::Eof => {
6258 return Err(missing_end(&format!(
6259 "c:{}",
6260 String::from_utf8_lossy(local)
6261 )));
6262 }
6263 _ => {}
6264 }
6265 inner.clear();
6266 }
6267 }
6268 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
6269 let namespaces = chart_bindings(inherited, &element)?;
6270 return Ok(ParsedWrapper {
6271 choice: None,
6272 markup: WrapperMarkup {
6273 raw_attributes: capture_attributes(&element)?,
6274 raw_children: OrderedRawChildren::default(),
6275 },
6276 namespaces,
6277 });
6278 }
6279 Event::Start(element) | Event::Empty(element) => {
6280 return Err(ChartError::UnexpectedElement(element_name(&element)));
6281 }
6282 Event::Eof => {
6283 return Err(ChartError::MissingElement(format!(
6284 "c:{}",
6285 String::from_utf8_lossy(local)
6286 )));
6287 }
6288 _ => {}
6289 }
6290 buffer.clear();
6291 }
6292}
6293
6294fn write_string_cache(
6295 writer: &mut Writer<Vec<u8>>,
6296 values: &[String],
6297 markup: &ReferenceMarkup,
6298) -> Result<()> {
6299 let (declared_count, indexes) = cache_layout(markup, values.len())?;
6300 let mut start = BytesStart::new("c:strCache");
6301 push_attributes(&mut start, &markup.cache_attributes);
6302 writer
6303 .write_event(Event::Start(start))
6304 .map_err(OxmlError::from)?;
6305 emit_raw(writer, markup.cache_children.at(0))?;
6306 write_scalar(
6307 writer,
6308 "c:ptCount",
6309 &declared_count.to_string(),
6310 Some(&markup.point_count),
6311 )?;
6312 emit_cache_point_raw(writer, markup, 1, values.len(), 0)?;
6313 for (position, value) in values.iter().enumerate() {
6314 let default_markup = PointMarkup::default();
6315 write_point(
6316 writer,
6317 indexes[position],
6318 value,
6319 markup.points.get(position).unwrap_or(&default_markup),
6320 )?;
6321 emit_cache_point_raw(writer, markup, 1, values.len(), position + 1)?;
6322 }
6323 writer
6324 .write_event(Event::End(BytesEnd::new("c:strCache")))
6325 .map_err(OxmlError::from)?;
6326 Ok(())
6327}
6328
6329fn write_numeric_cache(
6330 writer: &mut Writer<Vec<u8>>,
6331 data: &NumericData,
6332 markup: &ReferenceMarkup,
6333) -> Result<()> {
6334 let (declared_count, indexes) = cache_layout(markup, data.values.len())?;
6335 let mut start = BytesStart::new("c:numCache");
6336 push_attributes(&mut start, &markup.cache_attributes);
6337 writer
6338 .write_event(Event::Start(start))
6339 .map_err(OxmlError::from)?;
6340 emit_raw(writer, markup.cache_children.at(0))?;
6341 if !markup.format_code_omitted || data.format_code != "General" {
6342 let default_format_markup = TextMarkup::default();
6343 write_text(
6344 writer,
6345 "c:formatCode",
6346 &data.format_code,
6347 markup
6348 .format_code
6349 .as_ref()
6350 .unwrap_or(&default_format_markup),
6351 )?;
6352 }
6353 emit_raw(writer, markup.cache_children.at(1))?;
6354 write_scalar(
6355 writer,
6356 "c:ptCount",
6357 &declared_count.to_string(),
6358 Some(&markup.point_count),
6359 )?;
6360 emit_cache_point_raw(writer, markup, 2, data.values.len(), 0)?;
6361 for (position, value) in data.values.iter().enumerate() {
6362 let default_markup = PointMarkup::default();
6363 write_point(
6364 writer,
6365 indexes[position],
6366 &value.to_string(),
6367 markup.points.get(position).unwrap_or(&default_markup),
6368 )?;
6369 emit_cache_point_raw(writer, markup, 2, data.values.len(), position + 1)?;
6370 }
6371 writer
6372 .write_event(Event::End(BytesEnd::new("c:numCache")))
6373 .map_err(OxmlError::from)?;
6374 Ok(())
6375}
6376
6377fn emit_cache_point_raw(
6378 writer: &mut Writer<Vec<u8>>,
6379 markup: &ReferenceMarkup,
6380 point_base: usize,
6381 current_count: usize,
6382 completed_points: usize,
6383) -> Result<()> {
6384 let original_count = markup.points.len();
6385 if current_count == 0 {
6386 for boundary in point_base..=point_base + original_count {
6387 emit_raw(writer, markup.cache_children.at(boundary))?;
6388 }
6389 } else if completed_points == 0 {
6390 if original_count > 0 {
6391 emit_raw(writer, markup.cache_children.at(point_base))?;
6392 }
6393 } else if completed_points < current_count {
6394 if completed_points < original_count {
6395 emit_raw(
6396 writer,
6397 markup.cache_children.at(point_base + completed_points),
6398 )?;
6399 }
6400 } else {
6401 let first_tail = if original_count == 0 {
6402 point_base
6403 } else {
6404 point_base + current_count.min(original_count)
6405 };
6406 for boundary in first_tail..=point_base + original_count {
6407 emit_raw(writer, markup.cache_children.at(boundary))?;
6408 }
6409 }
6410 Ok(())
6411}
6412
6413fn write_point(
6414 writer: &mut Writer<Vec<u8>>,
6415 index: u32,
6416 value: &str,
6417 markup: &PointMarkup,
6418) -> Result<()> {
6419 let index = index.to_string();
6420 let mut start = BytesStart::new("c:pt");
6421 start.push_attribute(("idx", index.as_str()));
6422 push_attributes(&mut start, &markup.raw_attributes);
6423 writer
6424 .write_event(Event::Start(start))
6425 .map_err(OxmlError::from)?;
6426 emit_raw(writer, markup.raw_children.at(0))?;
6427 write_text(writer, "c:v", value, &markup.value)?;
6428 emit_raw(writer, markup.raw_children.at(1))?;
6429 writer
6430 .write_event(Event::End(BytesEnd::new("c:pt")))
6431 .map_err(OxmlError::from)?;
6432 Ok(())
6433}
6434
6435fn cache_layout(markup: &ReferenceMarkup, value_count: usize) -> Result<(u32, Vec<u32>)> {
6436 if markup.point_indexes.len() == value_count
6437 && let Some(declared) = markup.declared_point_count
6438 {
6439 return Ok((declared, markup.point_indexes.clone()));
6440 }
6441 let declared = u32::try_from(value_count).map_err(|_| ChartError::InvalidValue {
6442 element: "c:ptCount".to_owned(),
6443 value: value_count.to_string(),
6444 })?;
6445 Ok((declared, (0..declared).collect()))
6446}
6447
6448fn write_text(
6449 writer: &mut Writer<Vec<u8>>,
6450 tag: &str,
6451 value: &str,
6452 markup: &TextMarkup,
6453) -> Result<()> {
6454 let mut start = BytesStart::new(tag);
6455 push_attributes(&mut start, &markup.raw_attributes);
6456 writer
6457 .write_event(Event::Start(start))
6458 .map_err(OxmlError::from)?;
6459 emit_raw(writer, markup.raw_children.at(0))?;
6460 writer
6461 .write_event(Event::Text(BytesText::new(value)))
6462 .map_err(OxmlError::from)?;
6463 emit_raw(writer, markup.raw_children.at(1))?;
6464 writer
6465 .write_event(Event::End(BytesEnd::new(tag)))
6466 .map_err(OxmlError::from)?;
6467 Ok(())
6468}
6469
6470fn write_wrapper_start(
6471 writer: &mut Writer<Vec<u8>>,
6472 tag: &str,
6473 markup: &WrapperMarkup,
6474) -> Result<()> {
6475 let mut start = BytesStart::new(tag);
6476 push_attributes(&mut start, &markup.raw_attributes);
6477 writer
6478 .write_event(Event::Start(start))
6479 .map_err(OxmlError::from)?;
6480 emit_raw(writer, markup.raw_children.at(0))
6481}
6482
6483fn write_wrapper_end(
6484 writer: &mut Writer<Vec<u8>>,
6485 tag: &str,
6486 markup: &WrapperMarkup,
6487) -> Result<()> {
6488 emit_raw(writer, markup.raw_children.at(1))?;
6489 writer
6490 .write_event(Event::End(BytesEnd::new(tag)))
6491 .map_err(OxmlError::from)?;
6492 Ok(())
6493}
6494
6495fn validate_formula(formula: &str, element: &str) -> Result<()> {
6496 if formula.trim().is_empty() {
6497 return Err(ChartError::InvalidValue {
6498 element: element.to_owned(),
6499 value: formula.to_owned(),
6500 });
6501 }
6502 validate_xml_text(formula, element)
6503}
6504
6505fn validate_format_code(format_code: &str) -> Result<()> {
6506 if format_code.is_empty() {
6507 return Err(ChartError::InvalidValue {
6508 element: "c:formatCode".to_owned(),
6509 value: format_code.to_owned(),
6510 });
6511 }
6512 validate_xml_text(format_code, "c:formatCode")
6513}
6514
6515fn validate_xml_text(value: &str, element: &str) -> Result<()> {
6516 if value.chars().all(|character| {
6517 matches!(
6518 character,
6519 '\u{9}' | '\u{A}' | '\u{D}' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}'
6520 )
6521 }) {
6522 return Ok(());
6523 }
6524 Err(ChartError::InvalidValue {
6525 element: element.to_owned(),
6526 value: value.to_owned(),
6527 })
6528}
6529
6530fn validate_numeric_values(values: &[f64]) -> Result<()> {
6531 if let Some(value) = values.iter().find(|value| !value.is_finite()) {
6532 return Err(ChartError::InvalidValue {
6533 element: "c:v".to_owned(),
6534 value: value.to_string(),
6535 });
6536 }
6537 Ok(())
6538}
6539
6540fn validate_point_count(count: usize, element: &str) -> Result<()> {
6541 u32::try_from(count)
6542 .map(|_| ())
6543 .map_err(|_| ChartError::InvalidValue {
6544 element: element.to_owned(),
6545 value: count.to_string(),
6546 })
6547}
6548
6549fn parse_u32_scalar(xml: &[u8], local: &str) -> Result<(u32, ScalarMarkup)> {
6550 let (value, markup) = scalar_value(xml, local)?;
6551 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
6552 let parsed = value
6553 .parse::<u32>()
6554 .map_err(|_| invalid_attribute(local, "val", value))?;
6555 Ok((parsed, markup))
6556}
6557
6558fn required_u32_attribute(element: &BytesStart<'_>, local: &str, attribute: &[u8]) -> Result<u32> {
6559 let value = attribute_value(element, attribute)?.ok_or_else(|| {
6560 invalid_attribute(
6561 local,
6562 &String::from_utf8_lossy(attribute),
6563 "<missing>".to_owned(),
6564 )
6565 })?;
6566 value
6567 .parse::<u32>()
6568 .map_err(|_| invalid_attribute(local, &String::from_utf8_lossy(attribute), value))
6569}
6570
6571fn chart_root_prefix(element: &BytesStart<'_>) -> Result<Vec<u8>> {
6572 let name = element.name();
6573 let qualified = name.as_ref();
6574 let prefix = qualified
6575 .iter()
6576 .position(|byte| *byte == b':')
6577 .map(|position| qualified[..position].to_vec())
6578 .unwrap_or_default();
6579 let declaration = if prefix.is_empty() {
6580 b"xmlns".to_vec()
6581 } else {
6582 let mut declaration = b"xmlns:".to_vec();
6583 declaration.extend_from_slice(&prefix);
6584 declaration
6585 };
6586 if let Some(namespace) = attribute_value(element, &declaration)?
6587 && namespace != C_NS
6588 {
6589 return Err(invalid_attribute(
6590 &element_name(element),
6591 "namespace",
6592 namespace,
6593 ));
6594 }
6595 reject_conflicting_prefix(element, b"c", C_NS)?;
6596 Ok(prefix)
6597}
6598
6599fn typed_rewrite_bindings(
6600 element: &BytesStart<'_>,
6601 inherited: &NamespaceBindings,
6602) -> Result<NamespaceBindings> {
6603 chart_root_prefix(element)?;
6604 if !element_is_in_namespace(element, C_NS, inherited)? {
6605 return Err(ChartError::UnexpectedElement(element_name(element)));
6606 }
6607 let bindings = chart_bindings(inherited, element)?;
6608 require_fixed_namespace(&bindings, b"c", C_NS, element)?;
6609 Ok(bindings)
6610}
6611
6612fn chart_namespace_defaults() -> NamespaceBindings {
6613 vec![
6614 (b"c".to_vec(), C_NS.to_owned()),
6615 (b"a".to_vec(), A_NS.to_owned()),
6616 (b"r".to_vec(), R_NS.to_owned()),
6617 ]
6618}
6619
6620fn chart_bindings(
6621 inherited: &NamespaceBindings,
6622 element: &BytesStart<'_>,
6623) -> Result<NamespaceBindings> {
6624 let mut bindings = chart_namespace_defaults();
6625 for (prefix, namespace) in inherited
6626 .iter()
6627 .cloned()
6628 .chain(namespace_bindings(element)?)
6629 {
6630 upsert_namespace_binding(&mut bindings, prefix, namespace);
6631 }
6632 Ok(bindings)
6633}
6634
6635fn root_chart_bindings(
6636 xml: &[u8],
6637 local: &[u8],
6638 inherited: &NamespaceBindings,
6639) -> Result<NamespaceBindings> {
6640 let mut reader = Reader::from_reader(xml);
6641 let mut buffer = Vec::new();
6642 loop {
6643 match reader
6644 .read_event_into(&mut buffer)
6645 .map_err(OxmlError::from)?
6646 {
6647 Event::Start(element) | Event::Empty(element)
6648 if matches_local_name(element.name().as_ref(), local) =>
6649 {
6650 if !element_is_in_namespace(&element, C_NS, inherited)? {
6651 return Err(ChartError::UnexpectedElement(element_name(&element)));
6652 }
6653 return chart_bindings(inherited, &element);
6654 }
6655 Event::Start(element) | Event::Empty(element) => {
6656 return Err(ChartError::UnexpectedElement(element_name(&element)));
6657 }
6658 Event::Eof => {
6659 return Err(ChartError::MissingElement(format!(
6660 "c:{}",
6661 String::from_utf8_lossy(local)
6662 )));
6663 }
6664 _ => {}
6665 }
6666 buffer.clear();
6667 }
6668}
6669
6670fn chart_child_local(
6671 element: &BytesStart<'_>,
6672 inherited: &NamespaceBindings,
6673) -> Result<Option<Vec<u8>>> {
6674 Ok(element_is_in_namespace(element, C_NS, inherited)?
6675 .then(|| local_name(element.name().as_ref()).to_vec()))
6676}
6677
6678fn capture_fixed_root_attributes(
6679 start: &BytesStart<'_>,
6680 fixed: &[&str],
6681) -> Result<(XmlAttributes, XmlAttributes)> {
6682 let mut attributes = Vec::new();
6683 let mut namespaces = Vec::new();
6684 for attribute in start.attributes() {
6685 let attribute = attribute.map_err(OxmlError::from)?;
6686 let name = std::str::from_utf8(attribute.key.as_ref())
6687 .map_err(OxmlError::from)?
6688 .to_owned();
6689 let value = attribute
6690 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
6691 .map_err(OxmlError::from)?
6692 .into_owned();
6693 if fixed.contains(&name.as_str()) {
6694 continue;
6695 }
6696 if name == "xmlns" || name.starts_with("xmlns:") {
6697 namespaces.push((name, value));
6698 } else {
6699 attributes.push((name, value));
6700 }
6701 }
6702 Ok((attributes, namespaces))
6703}
6704
6705fn require_fixed_namespace(
6706 bindings: &NamespaceBindings,
6707 prefix: &[u8],
6708 expected: &str,
6709 element: &BytesStart<'_>,
6710) -> Result<()> {
6711 let actual = bindings
6712 .iter()
6713 .find(|(candidate, _)| candidate.as_slice() == prefix)
6714 .map(|(_, namespace)| namespace.as_str());
6715 if actual == Some(expected) {
6716 return Ok(());
6717 }
6718 Err(invalid_attribute(
6719 &element_name(element),
6720 &format!("xmlns:{}", String::from_utf8_lossy(prefix)),
6721 actual.unwrap_or("<missing>").to_owned(),
6722 ))
6723}
6724
6725fn standalone_namespace_declarations(bindings: &NamespaceBindings) -> Result<XmlAttributes> {
6726 bindings
6727 .iter()
6728 .filter(|(prefix, _)| prefix != b"c" && prefix != b"a" && prefix != b"r")
6729 .map(|(prefix, namespace)| {
6730 let prefix = std::str::from_utf8(prefix).map_err(OxmlError::from)?;
6731 let name = if prefix.is_empty() {
6732 "xmlns".to_owned()
6733 } else {
6734 format!("xmlns:{prefix}")
6735 };
6736 Ok((name, namespace.clone()))
6737 })
6738 .collect()
6739}
6740
6741fn capture_fixed_attributes(start: &BytesStart<'_>, fixed: &[&str]) -> Result<XmlAttributes> {
6742 let mut attributes = Vec::new();
6743 for attribute in start.attributes() {
6744 let attribute = attribute.map_err(OxmlError::from)?;
6745 let name = std::str::from_utf8(attribute.key.as_ref())
6746 .map_err(OxmlError::from)?
6747 .to_owned();
6748 if fixed.contains(&name.as_str()) {
6749 continue;
6750 }
6751 let value = attribute
6752 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
6753 .map_err(OxmlError::from)?
6754 .into_owned();
6755 attributes.push((name, value));
6756 }
6757 Ok(attributes)
6758}
6759
6760#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
6765pub struct AxisId(i64);
6766
6767impl AxisId {
6768 pub fn new(value: i64) -> Result<Self> {
6769 if (i64::from(i32::MIN)..=i64::from(u32::MAX)).contains(&value) {
6770 Ok(Self(value))
6771 } else {
6772 Err(ChartError::InvalidValue {
6773 element: "c:axId".to_owned(),
6774 value: value.to_string(),
6775 })
6776 }
6777 }
6778
6779 pub const fn value(self) -> i64 {
6780 self.0
6781 }
6782}
6783
6784#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6786pub enum BarDirection {
6787 Bar,
6788 Column,
6789}
6790
6791impl BarDirection {
6792 fn parse(value: &str) -> Option<Self> {
6793 match value {
6794 "bar" => Some(Self::Bar),
6795 "col" => Some(Self::Column),
6796 _ => None,
6797 }
6798 }
6799
6800 const fn as_str(self) -> &'static str {
6801 match self {
6802 Self::Bar => "bar",
6803 Self::Column => "col",
6804 }
6805 }
6806}
6807
6808#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6810pub enum BarGrouping {
6811 Clustered,
6812 PercentStacked,
6813 Stacked,
6814 Standard,
6815}
6816
6817impl BarGrouping {
6818 fn parse(value: &str) -> Option<Self> {
6819 match value {
6820 "clustered" => Some(Self::Clustered),
6821 "percentStacked" => Some(Self::PercentStacked),
6822 "stacked" => Some(Self::Stacked),
6823 "standard" => Some(Self::Standard),
6824 _ => None,
6825 }
6826 }
6827
6828 const fn as_str(self) -> &'static str {
6829 match self {
6830 Self::Clustered => "clustered",
6831 Self::PercentStacked => "percentStacked",
6832 Self::Stacked => "stacked",
6833 Self::Standard => "standard",
6834 }
6835 }
6836}
6837
6838#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6840pub enum Grouping {
6841 PercentStacked,
6842 Stacked,
6843 Standard,
6844}
6845
6846impl Grouping {
6847 fn parse(value: &str) -> Option<Self> {
6848 match value {
6849 "percentStacked" => Some(Self::PercentStacked),
6850 "stacked" => Some(Self::Stacked),
6851 "standard" => Some(Self::Standard),
6852 _ => None,
6853 }
6854 }
6855
6856 const fn as_str(self) -> &'static str {
6857 match self {
6858 Self::PercentStacked => "percentStacked",
6859 Self::Stacked => "stacked",
6860 Self::Standard => "standard",
6861 }
6862 }
6863}
6864
6865#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6867pub enum ScatterStyle {
6868 Line,
6869 LineMarker,
6870 Marker,
6871 None,
6872 Smooth,
6873 SmoothMarker,
6874}
6875
6876impl ScatterStyle {
6877 fn parse(value: &str) -> Option<Self> {
6878 match value {
6879 "line" => Some(Self::Line),
6880 "lineMarker" => Some(Self::LineMarker),
6881 "marker" => Some(Self::Marker),
6882 "none" => Some(Self::None),
6883 "smooth" => Some(Self::Smooth),
6884 "smoothMarker" => Some(Self::SmoothMarker),
6885 _ => None,
6886 }
6887 }
6888
6889 const fn as_str(self) -> &'static str {
6890 match self {
6891 Self::Line => "line",
6892 Self::LineMarker => "lineMarker",
6893 Self::Marker => "marker",
6894 Self::None => "none",
6895 Self::Smooth => "smooth",
6896 Self::SmoothMarker => "smoothMarker",
6897 }
6898 }
6899}
6900
6901#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6903pub enum RadarStyle {
6904 Filled,
6905 Marker,
6906 Standard,
6907}
6908
6909impl RadarStyle {
6910 fn parse(value: &str) -> Option<Self> {
6911 match value {
6912 "filled" => Some(Self::Filled),
6913 "marker" => Some(Self::Marker),
6914 "standard" => Some(Self::Standard),
6915 _ => None,
6916 }
6917 }
6918
6919 const fn as_str(self) -> &'static str {
6920 match self {
6921 Self::Filled => "filled",
6922 Self::Marker => "marker",
6923 Self::Standard => "standard",
6924 }
6925 }
6926}
6927
6928#[derive(Clone, Debug, PartialEq)]
6930pub enum Plot {
6931 Bar {
6932 direction: BarDirection,
6933 grouping: BarGrouping,
6934 gap_width: u16,
6935 overlap: i8,
6936 series: Vec<Series>,
6937 data_labels: Option<CT_DLbls>,
6938 axis_ids: [AxisId; 2],
6939 },
6940 Line {
6941 grouping: Grouping,
6942 marker: bool,
6943 smooth: bool,
6944 series: Vec<Series>,
6945 data_labels: Option<CT_DLbls>,
6946 axis_ids: [AxisId; 2],
6947 },
6948 Pie {
6949 first_slice_angle: u16,
6950 series: Vec<Series>,
6951 data_labels: Option<CT_DLbls>,
6952 },
6953 Doughnut {
6954 first_slice_angle: u16,
6955 hole_size: u8,
6956 series: Vec<Series>,
6957 data_labels: Option<CT_DLbls>,
6958 },
6959 Area {
6960 grouping: Grouping,
6961 series: Vec<Series>,
6962 data_labels: Option<CT_DLbls>,
6963 axis_ids: [AxisId; 2],
6964 },
6965 Scatter {
6966 style: ScatterStyle,
6967 series: Vec<Series>,
6968 data_labels: Option<CT_DLbls>,
6969 axis_ids: [AxisId; 2],
6970 },
6971 Radar {
6972 style: RadarStyle,
6973 series: Vec<Series>,
6974 data_labels: Option<CT_DLbls>,
6975 axis_ids: [AxisId; 2],
6976 },
6977}
6978
6979impl Plot {
6980 pub fn bar(
6981 direction: BarDirection,
6982 grouping: BarGrouping,
6983 series: Vec<Series>,
6984 axis_ids: [AxisId; 2],
6985 ) -> Result<Self> {
6986 let plot = Self::Bar {
6987 direction,
6988 grouping,
6989 gap_width: 150,
6990 overlap: 0,
6991 series,
6992 data_labels: None,
6993 axis_ids,
6994 };
6995 plot.validate()?;
6996 Ok(plot)
6997 }
6998
6999 pub fn line(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
7000 let plot = Self::Line {
7001 grouping,
7002 marker: false,
7003 smooth: false,
7004 series,
7005 data_labels: None,
7006 axis_ids,
7007 };
7008 plot.validate()?;
7009 Ok(plot)
7010 }
7011
7012 pub fn pie(series: Vec<Series>) -> Result<Self> {
7013 let plot = Self::Pie {
7014 first_slice_angle: 0,
7015 series,
7016 data_labels: None,
7017 };
7018 plot.validate()?;
7019 Ok(plot)
7020 }
7021
7022 pub fn doughnut(series: Vec<Series>) -> Result<Self> {
7023 let plot = Self::Doughnut {
7024 first_slice_angle: 0,
7025 hole_size: 50,
7026 series,
7027 data_labels: None,
7028 };
7029 plot.validate()?;
7030 Ok(plot)
7031 }
7032
7033 pub fn area(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
7034 let plot = Self::Area {
7035 grouping,
7036 series,
7037 data_labels: None,
7038 axis_ids,
7039 };
7040 plot.validate()?;
7041 Ok(plot)
7042 }
7043
7044 pub fn scatter(
7045 style: ScatterStyle,
7046 series: Vec<Series>,
7047 axis_ids: [AxisId; 2],
7048 ) -> Result<Self> {
7049 let plot = Self::Scatter {
7050 style,
7051 series,
7052 data_labels: None,
7053 axis_ids,
7054 };
7055 plot.validate()?;
7056 Ok(plot)
7057 }
7058
7059 pub fn radar(style: RadarStyle, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
7060 let plot = Self::Radar {
7061 style,
7062 series,
7063 data_labels: None,
7064 axis_ids,
7065 };
7066 plot.validate()?;
7067 Ok(plot)
7068 }
7069
7070 fn axis_ids(&self) -> Option<[AxisId; 2]> {
7071 match self {
7072 Self::Bar { axis_ids, .. }
7073 | Self::Line { axis_ids, .. }
7074 | Self::Area { axis_ids, .. }
7075 | Self::Scatter { axis_ids, .. }
7076 | Self::Radar { axis_ids, .. } => Some(*axis_ids),
7077 Self::Pie { .. } | Self::Doughnut { .. } => None,
7078 }
7079 }
7080
7081 fn validate(&self) -> Result<()> {
7082 let (series, labels, axis_ids) = match self {
7083 Self::Bar {
7084 gap_width,
7085 overlap,
7086 series,
7087 data_labels,
7088 axis_ids,
7089 ..
7090 } => {
7091 if *gap_width > 500 {
7092 return Err(ChartError::InvalidValue {
7093 element: "c:gapWidth".to_owned(),
7094 value: gap_width.to_string(),
7095 });
7096 }
7097 if !(-100..=100).contains(overlap) {
7098 return Err(ChartError::InvalidValue {
7099 element: "c:overlap".to_owned(),
7100 value: overlap.to_string(),
7101 });
7102 }
7103 (series, data_labels, Some(axis_ids))
7104 }
7105 Self::Line {
7106 series,
7107 data_labels,
7108 axis_ids,
7109 ..
7110 } => (series, data_labels, Some(axis_ids)),
7111 Self::Pie {
7112 first_slice_angle,
7113 series,
7114 data_labels,
7115 } => {
7116 if *first_slice_angle > 360 {
7117 return Err(ChartError::InvalidValue {
7118 element: "c:firstSliceAng".to_owned(),
7119 value: first_slice_angle.to_string(),
7120 });
7121 }
7122 (series, data_labels, None)
7123 }
7124 Self::Doughnut {
7125 first_slice_angle,
7126 hole_size,
7127 series,
7128 data_labels,
7129 } => {
7130 if *first_slice_angle > 360 {
7131 return Err(ChartError::InvalidValue {
7132 element: "c:firstSliceAng".to_owned(),
7133 value: first_slice_angle.to_string(),
7134 });
7135 }
7136 if !(10..=90).contains(hole_size) {
7137 return Err(ChartError::InvalidValue {
7138 element: "c:holeSize".to_owned(),
7139 value: hole_size.to_string(),
7140 });
7141 }
7142 (series, data_labels, None)
7143 }
7144 Self::Area {
7145 series,
7146 data_labels,
7147 axis_ids,
7148 ..
7149 }
7150 | Self::Radar {
7151 series,
7152 data_labels,
7153 axis_ids,
7154 ..
7155 } => (series, data_labels, Some(axis_ids)),
7156 Self::Scatter {
7157 series,
7158 data_labels,
7159 axis_ids,
7160 ..
7161 } => {
7162 for item in series {
7163 if !matches!(item.categories, Some(AxisData::Numeric(_))) {
7164 return Err(ChartError::InvalidValue {
7165 element: "c:xVal".to_owned(),
7166 value: "scatter series requires numeric x values".to_owned(),
7167 });
7168 }
7169 }
7170 (series, data_labels, Some(axis_ids))
7171 }
7172 };
7173 if series.is_empty() {
7174 return Err(ChartError::MissingElement("c:ser".to_owned()));
7175 }
7176 let scatter_plot = matches!(self, Self::Scatter { .. });
7177 for item in series {
7178 if item.bubble_size.is_some() || item.opaque_bubble_size {
7179 return Err(ChartError::InvalidValue {
7180 element: "c:bubbleSize".to_owned(),
7181 value: "bubble size is only valid in an opaque bubble plot".to_owned(),
7182 });
7183 }
7184 if item.uses_scatter_wrappers && !scatter_plot {
7185 return Err(ChartError::InvalidValue {
7186 element: "c:ser".to_owned(),
7187 value: "x/y wrappers are only valid in scatter plots".to_owned(),
7188 });
7189 }
7190 if scatter_plot && item.parsed_from_xml && !item.uses_scatter_wrappers {
7191 return Err(ChartError::InvalidValue {
7192 element: "c:ser".to_owned(),
7193 value: "scatter plots require xVal/yVal wrappers".to_owned(),
7194 });
7195 }
7196 }
7197 if let Some(axis_ids) = axis_ids {
7198 if axis_ids[0] == axis_ids[1] {
7199 return Err(ChartError::DuplicateElement(format!(
7200 "c:axId {}",
7201 axis_ids[0].value()
7202 )));
7203 }
7204 for id in axis_ids {
7205 AxisId::new(id.value())?;
7206 }
7207 }
7208 if let Some(labels) = labels {
7209 labels.validate()?;
7210 }
7211 for item in series {
7212 item.values.validate()?;
7213 }
7214 Ok(())
7215 }
7216}
7217
7218#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
7220pub enum AxisKind {
7221 Category,
7222 Value,
7223 Date,
7224 Series,
7225}
7226
7227impl AxisKind {
7228 fn parse(local: &[u8]) -> Option<Self> {
7229 match local {
7230 b"catAx" => Some(Self::Category),
7231 b"valAx" => Some(Self::Value),
7232 b"dateAx" => Some(Self::Date),
7233 b"serAx" => Some(Self::Series),
7234 _ => None,
7235 }
7236 }
7237
7238 const fn local(self) -> &'static str {
7239 match self {
7240 Self::Category => "catAx",
7241 Self::Value => "valAx",
7242 Self::Date => "dateAx",
7243 Self::Series => "serAx",
7244 }
7245 }
7246}
7247
7248#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
7250pub enum Orientation {
7251 #[default]
7252 MinMax,
7253 MaxMin,
7254}
7255
7256impl Orientation {
7257 fn parse(value: &str) -> Option<Self> {
7258 match value {
7259 "minMax" => Some(Self::MinMax),
7260 "maxMin" => Some(Self::MaxMin),
7261 _ => None,
7262 }
7263 }
7264
7265 const fn as_str(self) -> &'static str {
7266 match self {
7267 Self::MinMax => "minMax",
7268 Self::MaxMin => "maxMin",
7269 }
7270 }
7271}
7272
7273#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7275pub enum AxisPosition {
7276 Bottom,
7277 Left,
7278 Right,
7279 Top,
7280}
7281
7282impl AxisPosition {
7283 fn parse(value: &str) -> Option<Self> {
7284 match value {
7285 "b" => Some(Self::Bottom),
7286 "l" => Some(Self::Left),
7287 "r" => Some(Self::Right),
7288 "t" => Some(Self::Top),
7289 _ => None,
7290 }
7291 }
7292
7293 const fn as_str(self) -> &'static str {
7294 match self {
7295 Self::Bottom => "b",
7296 Self::Left => "l",
7297 Self::Right => "r",
7298 Self::Top => "t",
7299 }
7300 }
7301}
7302
7303#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
7305pub enum TickMark {
7306 Cross,
7307 Inside,
7308 #[default]
7309 None,
7310 Outside,
7311}
7312
7313impl TickMark {
7314 fn parse(value: &str) -> Option<Self> {
7315 match value {
7316 "cross" => Some(Self::Cross),
7317 "in" => Some(Self::Inside),
7318 "none" => Some(Self::None),
7319 "out" => Some(Self::Outside),
7320 _ => None,
7321 }
7322 }
7323
7324 const fn as_str(self) -> &'static str {
7325 match self {
7326 Self::Cross => "cross",
7327 Self::Inside => "in",
7328 Self::None => "none",
7329 Self::Outside => "out",
7330 }
7331 }
7332}
7333
7334#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
7336pub enum TickLabelPosition {
7337 High,
7338 Low,
7339 #[default]
7340 NextTo,
7341 None,
7342}
7343
7344impl TickLabelPosition {
7345 fn parse(value: &str) -> Option<Self> {
7346 match value {
7347 "high" => Some(Self::High),
7348 "low" => Some(Self::Low),
7349 "nextTo" => Some(Self::NextTo),
7350 "none" => Some(Self::None),
7351 _ => None,
7352 }
7353 }
7354
7355 const fn as_str(self) -> &'static str {
7356 match self {
7357 Self::High => "high",
7358 Self::Low => "low",
7359 Self::NextTo => "nextTo",
7360 Self::None => "none",
7361 }
7362 }
7363}
7364
7365#[derive(Clone, Debug)]
7367pub struct Scaling {
7368 pub log_base: Option<f64>,
7369 pub orientation: Orientation,
7370 pub maximum: Option<f64>,
7371 pub minimum: Option<f64>,
7372 log_base_markup: Option<ScalarMarkup>,
7373 orientation_markup: Option<ScalarMarkup>,
7374 maximum_markup: Option<ScalarMarkup>,
7375 minimum_markup: Option<ScalarMarkup>,
7376 raw_attributes: XmlAttributes,
7377 raw_children: OrderedRawChildren,
7378}
7379
7380impl PartialEq for Scaling {
7381 fn eq(&self, other: &Self) -> bool {
7382 self.log_base == other.log_base
7383 && self.orientation == other.orientation
7384 && self.maximum == other.maximum
7385 && self.minimum == other.minimum
7386 && optional_scalar_markup_eq(&self.log_base_markup, &other.log_base_markup)
7387 && optional_scalar_markup_eq(&self.orientation_markup, &other.orientation_markup)
7388 && optional_scalar_markup_eq(&self.maximum_markup, &other.maximum_markup)
7389 && optional_scalar_markup_eq(&self.minimum_markup, &other.minimum_markup)
7390 && self.raw_attributes == other.raw_attributes
7391 && self.raw_children == other.raw_children
7392 }
7393}
7394
7395impl Default for Scaling {
7396 fn default() -> Self {
7397 Self {
7398 log_base: None,
7399 orientation: Orientation::MinMax,
7400 maximum: None,
7401 minimum: None,
7402 log_base_markup: None,
7403 orientation_markup: None,
7404 maximum_markup: None,
7405 minimum_markup: None,
7406 raw_attributes: Vec::new(),
7407 raw_children: OrderedRawChildren::default(),
7408 }
7409 }
7410}
7411
7412#[derive(Clone, Debug, Default, PartialEq)]
7414pub struct ChartLines {
7415 pub sp_pr: Option<CT_ShapeProperties>,
7416 raw_attributes: XmlAttributes,
7417 raw_children: OrderedRawChildren,
7418}
7419
7420#[derive(Clone, Debug, Eq, PartialEq)]
7422pub struct NumberFormat {
7423 pub format_code: String,
7424 pub source_linked: bool,
7425 raw_attributes: XmlAttributes,
7426 raw_content: Vec<Vec<u8>>,
7427}
7428
7429#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7431pub enum DataLabelPosition {
7432 BestFit,
7433 Bottom,
7434 Center,
7435 InsideBase,
7436 InsideEnd,
7437 Left,
7438 OutsideEnd,
7439 Right,
7440 Top,
7441}
7442
7443impl DataLabelPosition {
7444 fn parse(value: &str) -> Option<Self> {
7445 match value {
7446 "bestFit" => Some(Self::BestFit),
7447 "b" => Some(Self::Bottom),
7448 "ctr" => Some(Self::Center),
7449 "inBase" => Some(Self::InsideBase),
7450 "inEnd" => Some(Self::InsideEnd),
7451 "l" => Some(Self::Left),
7452 "outEnd" => Some(Self::OutsideEnd),
7453 "r" => Some(Self::Right),
7454 "t" => Some(Self::Top),
7455 _ => None,
7456 }
7457 }
7458
7459 const fn as_str(self) -> &'static str {
7460 match self {
7461 Self::BestFit => "bestFit",
7462 Self::Bottom => "b",
7463 Self::Center => "ctr",
7464 Self::InsideBase => "inBase",
7465 Self::InsideEnd => "inEnd",
7466 Self::Left => "l",
7467 Self::OutsideEnd => "outEnd",
7468 Self::Right => "r",
7469 Self::Top => "t",
7470 }
7471 }
7472}
7473
7474#[derive(Clone, Debug, Default, PartialEq)]
7476pub struct CT_DLbls {
7477 pub number_format: Option<NumberFormat>,
7478 pub position: Option<DataLabelPosition>,
7479 pub separator: Option<String>,
7480 pub show_legend_key: bool,
7481 pub show_value: bool,
7482 pub show_category_name: bool,
7483 pub show_series_name: bool,
7484 pub show_percent: bool,
7485 pub show_bubble_size: bool,
7486 position_markup: Option<ScalarMarkup>,
7487 show_legend_key_markup: Option<ScalarMarkup>,
7488 show_value_markup: Option<ScalarMarkup>,
7489 show_category_name_markup: Option<ScalarMarkup>,
7490 show_series_name_markup: Option<ScalarMarkup>,
7491 show_percent_markup: Option<ScalarMarkup>,
7492 show_bubble_size_markup: Option<ScalarMarkup>,
7493 separator_markup: Option<TextMarkup>,
7494 raw_attributes: XmlAttributes,
7495 namespace_declarations: XmlAttributes,
7496 raw_children: OrderedRawChildren,
7497}
7498
7499#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
7501pub enum DispBlanksAs {
7502 #[default]
7503 Gap,
7504 Zero,
7505 Span,
7506}
7507
7508impl DispBlanksAs {
7509 fn parse(value: &str) -> Option<Self> {
7510 match value {
7511 "gap" => Some(Self::Gap),
7512 "zero" => Some(Self::Zero),
7513 "span" => Some(Self::Span),
7514 _ => None,
7515 }
7516 }
7517
7518 const fn as_str(self) -> &'static str {
7519 match self {
7520 Self::Gap => "gap",
7521 Self::Zero => "zero",
7522 Self::Span => "span",
7523 }
7524 }
7525}
7526
7527#[derive(Clone, Debug, Default, Eq, PartialEq)]
7529pub struct CT_Title {
7530 text: Option<CT_TextBody>,
7531 raw_attributes: Vec<(String, String)>,
7532 raw_children: OrderedRawChildren,
7533}
7534
7535#[derive(Clone, Debug, Default, PartialEq)]
7537struct PlotMarkup {
7538 raw_attributes: XmlAttributes,
7539 raw_children: OrderedRawChildren,
7540 direction: Option<ScalarMarkup>,
7541 grouping: Option<ScalarMarkup>,
7542 gap_width: Option<ScalarMarkup>,
7543 overlap: Option<ScalarMarkup>,
7544 marker: Option<ScalarMarkup>,
7545 smooth: Option<ScalarMarkup>,
7546 first_slice_angle: Option<ScalarMarkup>,
7547 hole_size: Option<ScalarMarkup>,
7548 style: Option<ScalarMarkup>,
7549 axis_ids: Vec<AxisIdMarkup>,
7550 original_series_keys: Vec<(u32, u32)>,
7551 original_axis_ids: Vec<AxisId>,
7552 parsed_bar: Option<bool>,
7553 parsed_remaining: Option<&'static str>,
7554}
7555
7556#[derive(Clone, Debug, PartialEq)]
7558pub struct CT_PlotArea {
7559 raw_attributes: Vec<(String, String)>,
7560 raw_children: OrderedRawChildren,
7561 namespace_bindings: NamespaceBindings,
7562 plots: Option<Vec<Plot>>,
7563 plot_markup: Vec<PlotMarkup>,
7564 axes: Vec<Axis>,
7565}
7566
7567#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
7569pub enum LegendPosition {
7570 #[default]
7571 Right,
7572 Left,
7573 Top,
7574 Bottom,
7575 TopRight,
7576}
7577
7578impl LegendPosition {
7579 pub fn as_str(self) -> &'static str {
7581 match self {
7582 Self::Right => "r",
7583 Self::Left => "l",
7584 Self::Top => "t",
7585 Self::Bottom => "b",
7586 Self::TopRight => "tr",
7587 }
7588 }
7589 pub fn parse(value: &str) -> Result<Self> {
7591 match value {
7592 "r" => Ok(Self::Right),
7593 "l" => Ok(Self::Left),
7594 "t" => Ok(Self::Top),
7595 "b" => Ok(Self::Bottom),
7596 "tr" => Ok(Self::TopRight),
7597 _ => Err(invalid_authoring_value(format!(
7598 "unknown legend position {value}"
7599 ))),
7600 }
7601 }
7602}
7603
7604#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
7606pub struct LegendSettings {
7607 pub visible: bool,
7608 pub position: LegendPosition,
7609 pub overlay: bool,
7610}
7611
7612fn legend_scalar_value(xml: &[u8], node: &ChartSourceNode) -> Result<String> {
7613 let mut reader = Reader::from_reader(&xml[node.start..node.end]);
7614 let value = match reader.read_event().map_err(OxmlError::from)? {
7615 Event::Start(element) | Event::Empty(element) => attribute_value(&element, b"val")?
7616 .unwrap_or_else(|| {
7617 if node.local == b"legendPos" {
7618 "r".into()
7619 } else {
7620 "1".into()
7621 }
7622 }),
7623 _ => return Err(invalid_authoring_value("legend scalar root")),
7624 };
7625 loop {
7626 match reader.read_event().map_err(OxmlError::from)? {
7627 Event::End(_) | Event::Comment(_) | Event::PI(_) => {}
7628 Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
7629 Event::Eof => return Ok(value),
7630 _ => {
7631 return Err(invalid_authoring_value(
7632 "legend scalar has non-scalar content",
7633 ));
7634 }
7635 }
7636 }
7637}
7638
7639fn legend_settings_at(
7640 xml: &[u8],
7641 nodes: &[ChartSourceNode],
7642 legend: usize,
7643) -> Result<LegendSettings> {
7644 let mut settings = LegendSettings {
7645 visible: true,
7646 ..LegendSettings::default()
7647 };
7648 let mut position = None;
7649 let mut overlay = None;
7650 for (index, node) in nodes
7651 .iter()
7652 .enumerate()
7653 .filter(|(_, n)| n.parent == Some(legend) && n.qualified)
7654 {
7655 if node.local != b"legendPos" && node.local != b"overlay" {
7656 continue;
7657 }
7658 if nodes.iter().any(|child| child.parent == Some(index)) {
7659 return Err(invalid_authoring_value("legend scalar has child elements"));
7660 }
7661 let value = legend_scalar_value(xml, node)?;
7662 if node.local == b"legendPos" {
7663 set_once(&mut position, LegendPosition::parse(&value)?, "c:legendPos")?;
7664 } else {
7665 set_once(
7666 &mut overlay,
7667 parse_bool_lexical("c:overlay", "val", &value)?,
7668 "c:overlay",
7669 )?;
7670 }
7671 }
7672 settings.position = position.unwrap_or_default();
7673 settings.overlay = overlay.unwrap_or(false);
7674 Ok(settings)
7675}
7676
7677pub fn chart_legend_settings(xml: &[u8]) -> Result<LegendSettings> {
7679 oxml_core::xml::validate_strict_xml_1_0(xml)
7680 .map_err(|error| invalid_authoring_value(format!("invalid chart XML: {error:?}")))?;
7681 let nodes = chart_source_nodes(xml)?;
7682 if !nodes
7683 .first()
7684 .is_some_and(|n| n.qualified && n.local == b"chartSpace")
7685 || nodes.iter().filter(|n| n.parent.is_none()).count() != 1
7686 {
7687 return Err(invalid_authoring_value("owned chartSpace required"));
7688 }
7689 let chart = chart_child_node(&nodes, 0, b"chart")?;
7690 let legends = nodes
7691 .iter()
7692 .enumerate()
7693 .filter(|(_, n)| n.parent == Some(chart) && n.qualified && n.local == b"legend")
7694 .map(|(i, _)| i)
7695 .collect::<Vec<_>>();
7696 match legends.as_slice() {
7697 [] => Ok(LegendSettings::default()),
7698 [index] => legend_settings_at(xml, &nodes, *index),
7699 _ => Err(ChartError::DuplicateElement("c:legend".into())),
7700 }
7701}
7702
7703fn legend_child_rank(local: &[u8]) -> Option<u8> {
7704 match local {
7705 b"legendPos" => Some(0),
7706 b"legendEntry" => Some(1),
7707 b"layout" => Some(2),
7708 b"overlay" => Some(3),
7709 b"spPr" => Some(4),
7710 b"txPr" => Some(5),
7711 b"extLst" => Some(6),
7712 _ => None,
7713 }
7714}
7715
7716fn insert_chart_child(
7717 xml: &[u8],
7718 node: &ChartSourceNode,
7719 position: usize,
7720 bytes: &[u8],
7721) -> Result<Vec<u8>> {
7722 let mut reader = Reader::from_reader(&xml[node.start..node.end]);
7723 let event = reader.read_event().map_err(OxmlError::from)?;
7724 let mut result = xml.to_vec();
7725 if let Event::Empty(element) = event {
7726 let end = node.start + reader.buffer_position() as usize;
7727 let name = element_name(&element);
7728 let mut replacement = xml[node.start..end - 2].to_vec();
7729 replacement.push(b'>');
7730 replacement.extend_from_slice(bytes);
7731 replacement.extend_from_slice(format!("</{name}>").as_bytes());
7732 result.splice(node.start..node.end, replacement);
7733 } else {
7734 result.splice(position..position, bytes.iter().copied());
7735 }
7736 Ok(result)
7737}
7738
7739pub fn set_chart_legend(
7742 xml: &[u8],
7743 visible: Option<bool>,
7744 position: Option<LegendPosition>,
7745 overlay: Option<bool>,
7746) -> Result<Vec<u8>> {
7747 let current = chart_legend_settings(xml)?;
7748 if visible == Some(false) {
7749 if !current.visible {
7750 return Ok(xml.to_vec());
7751 }
7752 let nodes = chart_source_nodes(xml)?;
7753 let chart = chart_child_node(&nodes, 0, b"chart")?;
7754 let legend = chart_child_node(&nodes, chart, b"legend")?;
7755 let mut result = xml.to_vec();
7756 result.drain(nodes[legend].start..nodes[legend].end);
7757 return Ok(result);
7758 }
7759 if !current.visible && visible != Some(true) {
7760 return Ok(xml.to_vec());
7761 }
7762 if !current.visible {
7763 let nodes = chart_source_nodes(xml)?;
7764 let chart = chart_child_node(&nodes, 0, b"chart")?;
7765 let after = chart_child_node(&nodes, chart, b"plotArea")?;
7766 let bytes = format!(
7767 "<c:legend xmlns:c=\"{C_NS}\"><c:legendPos val=\"{}\"/><c:overlay val=\"{}\"/></c:legend>",
7768 position.unwrap_or_default().as_str(),
7769 u8::from(overlay.unwrap_or(false))
7770 );
7771 return insert_chart_child(xml, &nodes[chart], nodes[after].end, bytes.as_bytes());
7772 }
7773 let mut result = xml.to_vec();
7774 for (local, value, changed) in [
7775 (
7776 "legendPos",
7777 position.unwrap_or(current.position).as_str().to_owned(),
7778 position.is_some_and(|v| v != current.position),
7779 ),
7780 (
7781 "overlay",
7782 u8::from(overlay.unwrap_or(current.overlay)).to_string(),
7783 overlay.is_some_and(|v| v != current.overlay),
7784 ),
7785 ] {
7786 if !changed {
7787 continue;
7788 }
7789 let nodes = chart_source_nodes(&result)?;
7790 let chart = chart_child_node(&nodes, 0, b"chart")?;
7791 let legend = chart_child_node(&nodes, chart, b"legend")?;
7792 if let Some(node) = nodes
7793 .iter()
7794 .find(|n| n.parent == Some(legend) && n.qualified && n.local == local.as_bytes())
7795 {
7796 let mut reader = Reader::from_reader(&result[node.start..node.end]);
7797 let event = reader.read_event().map_err(OxmlError::from)?;
7798 let end = node.start + reader.buffer_position() as usize;
7799 let mut writer = Writer::new(Vec::new());
7800 match event {
7801 Event::Start(element) | Event::Empty(element) => {
7802 let mut start = BytesStart::new(element_name(&element));
7803 push_attributes(&mut start, &capture_attributes_except(&element, b"val")?);
7804 start.push_attribute(("val", value.as_str()));
7805 writer
7806 .write_event(if result[end - 2..end] == *b"/>" {
7807 Event::Empty(start)
7808 } else {
7809 Event::Start(start)
7810 })
7811 .map_err(OxmlError::from)?;
7812 }
7813 _ => return Err(invalid_authoring_value("legend scalar root")),
7814 }
7815 result.splice(node.start..end, writer.into_inner());
7816 } else {
7817 let rank = legend_child_rank(local.as_bytes()).expect("owned legend scalar");
7818 let insert = nodes
7819 .iter()
7820 .find(|n| {
7821 n.parent == Some(legend)
7822 && n.qualified
7823 && legend_child_rank(&n.local).is_some_and(|r| r > rank)
7824 })
7825 .map(|n| n.start)
7826 .unwrap_or_else(|| {
7827 result[..nodes[legend].end]
7828 .iter()
7829 .rposition(|v| *v == b'<')
7830 .unwrap_or(nodes[legend].start)
7831 });
7832 let bytes = format!("<c:{local} xmlns:c=\"{C_NS}\" val=\"{value}\"/>");
7833 result = insert_chart_child(&result, &nodes[legend], insert, bytes.as_bytes())?;
7834 }
7835 }
7836 let expected = LegendSettings {
7837 visible: true,
7838 position: position.unwrap_or(current.position),
7839 overlay: overlay.unwrap_or(current.overlay),
7840 };
7841 if chart_legend_settings(&result)? != expected {
7842 return Err(invalid_authoring_value("legend edit projection mismatch"));
7843 }
7844 Ok(result)
7845}
7846
7847#[derive(Clone, Debug, Eq, PartialEq)]
7849pub struct CT_Legend {
7850 root_name: String,
7851 settings: LegendSettings,
7852 raw_attributes: Vec<(String, String)>,
7853 raw_children: OrderedRawChildren,
7854}
7855
7856#[derive(Clone, Debug, PartialEq)]
7857struct AxisIdMarkup {
7858 scalar: ScalarMarkup,
7859 parsed: AxisId,
7860 lexical: String,
7861}
7862
7863#[derive(Clone, Debug)]
7865pub struct Axis {
7866 pub kind: AxisKind,
7867 pub id: AxisId,
7868 pub scaling: Scaling,
7869 pub deleted: bool,
7870 pub position: AxisPosition,
7871 pub major_gridlines: Option<ChartLines>,
7872 pub minor_gridlines: Option<ChartLines>,
7873 pub title: Option<CT_Title>,
7874 pub number_format: Option<NumberFormat>,
7875 pub major_tick_mark: TickMark,
7876 pub minor_tick_mark: TickMark,
7877 pub tick_label_position: TickLabelPosition,
7878 pub sp_pr: Option<CT_ShapeProperties>,
7879 pub tx_pr: Option<CT_TextBody>,
7880 pub cross_axis: AxisId,
7881 parsed_kind: Option<AxisKind>,
7882 id_markup: AxisIdMarkup,
7883 delete_markup: Option<ScalarMarkup>,
7884 position_markup: ScalarMarkup,
7885 major_tick_mark_markup: Option<ScalarMarkup>,
7886 minor_tick_mark_markup: Option<ScalarMarkup>,
7887 tick_label_position_markup: Option<ScalarMarkup>,
7888 cross_axis_markup: AxisIdMarkup,
7889 raw_attributes: XmlAttributes,
7890 namespace_declarations: XmlAttributes,
7891 raw_children: OrderedRawChildren,
7892}
7893
7894impl PartialEq for Axis {
7895 fn eq(&self, other: &Self) -> bool {
7896 self.kind == other.kind
7897 && self.id == other.id
7898 && axis_id_markup_eq(&self.id_markup, &other.id_markup)
7899 && self.scaling == other.scaling
7900 && self.deleted == other.deleted
7901 && optional_scalar_markup_eq(&self.delete_markup, &other.delete_markup)
7902 && self.position == other.position
7903 && self.position_markup == other.position_markup
7904 && self.major_gridlines == other.major_gridlines
7905 && self.minor_gridlines == other.minor_gridlines
7906 && self.title == other.title
7907 && self.number_format == other.number_format
7908 && self.major_tick_mark == other.major_tick_mark
7909 && optional_scalar_markup_eq(
7910 &self.major_tick_mark_markup,
7911 &other.major_tick_mark_markup,
7912 )
7913 && self.minor_tick_mark == other.minor_tick_mark
7914 && optional_scalar_markup_eq(
7915 &self.minor_tick_mark_markup,
7916 &other.minor_tick_mark_markup,
7917 )
7918 && self.tick_label_position == other.tick_label_position
7919 && optional_scalar_markup_eq(
7920 &self.tick_label_position_markup,
7921 &other.tick_label_position_markup,
7922 )
7923 && self.sp_pr == other.sp_pr
7924 && self.tx_pr == other.tx_pr
7925 && self.cross_axis == other.cross_axis
7926 && axis_id_markup_eq(&self.cross_axis_markup, &other.cross_axis_markup)
7927 && self.raw_attributes == other.raw_attributes
7928 && self.namespace_declarations == other.namespace_declarations
7929 && self.raw_children == other.raw_children
7930 }
7931}
7932
7933impl Axis {
7934 pub fn new(kind: AxisKind, id: AxisId, position: AxisPosition, cross_axis: AxisId) -> Self {
7935 Self {
7936 kind,
7937 id,
7938 scaling: Scaling::default(),
7939 deleted: false,
7940 position,
7941 major_gridlines: None,
7942 minor_gridlines: None,
7943 title: None,
7944 number_format: None,
7945 major_tick_mark: TickMark::None,
7946 minor_tick_mark: TickMark::None,
7947 tick_label_position: TickLabelPosition::NextTo,
7948 sp_pr: None,
7949 tx_pr: None,
7950 cross_axis,
7951 parsed_kind: None,
7952 id_markup: default_axis_id_markup(id),
7953 delete_markup: None,
7954 position_markup: ScalarMarkup::default(),
7955 major_tick_mark_markup: None,
7956 minor_tick_mark_markup: None,
7957 tick_label_position_markup: None,
7958 cross_axis_markup: default_axis_id_markup(cross_axis),
7959 raw_attributes: Vec::new(),
7960 namespace_declarations: Vec::new(),
7961 raw_children: OrderedRawChildren::default(),
7962 }
7963 }
7964
7965 pub fn set_deleted(&mut self, deleted: bool) {
7967 self.deleted = deleted;
7968 self.delete_markup.get_or_insert_with(ScalarMarkup::default);
7969 }
7970
7971 pub fn from_xml(xml: &[u8]) -> Result<Self> {
7972 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
7973 }
7974
7975 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7976 let mut reader = Reader::from_reader(xml);
7977 let mut buffer = Vec::new();
7978 loop {
7979 match reader
7980 .read_event_into(&mut buffer)
7981 .map_err(OxmlError::from)?
7982 {
7983 Event::Start(element) => {
7984 let name = element.name();
7985 let local = local_name(name.as_ref());
7986 let Some(kind) = AxisKind::parse(local) else {
7987 return Err(ChartError::UnexpectedElement(element_name(&element)));
7988 };
7989 chart_root_prefix(&element)?;
7990 if !element_is_in_namespace(&element, C_NS, inherited)? {
7991 return Err(ChartError::UnexpectedElement(element_name(&element)));
7992 }
7993 return Self::from_element(&mut reader, &element, inherited, kind);
7994 }
7995 Event::Empty(element) => {
7996 let name = element.name();
7997 let local = local_name(name.as_ref());
7998 if AxisKind::parse(local).is_some() {
7999 return Err(ChartError::MissingElement("c:axId".to_owned()));
8000 }
8001 return Err(ChartError::UnexpectedElement(element_name(&element)));
8002 }
8003 Event::Eof => return Err(ChartError::MissingElement("ChartML axis".to_owned())),
8004 _ => {}
8005 }
8006 buffer.clear();
8007 }
8008 }
8009
8010 fn from_element(
8011 reader: &mut Reader<&[u8]>,
8012 start: &BytesStart<'_>,
8013 inherited: &NamespaceBindings,
8014 kind: AxisKind,
8015 ) -> Result<Self> {
8016 reject_conflicting_prefix(start, b"a", A_NS)?;
8017 reject_conflicting_prefix(start, b"r", R_NS)?;
8018 let namespaces = chart_bindings(inherited, start)?;
8019 require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
8020 require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
8021 require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
8022 let (raw_attributes, _) =
8023 capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8024 let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
8025 let mut state = AxisParseState::default();
8026 let mut buffer = Vec::new();
8027 loop {
8028 match reader
8029 .read_event_into(&mut buffer)
8030 .map_err(OxmlError::from)?
8031 {
8032 Event::Start(element) => {
8033 let name = chart_child_local(&element, &namespaces)?;
8034 let raw = capture_element(reader, &element)?;
8035 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
8036 }
8037 Event::Empty(element) => {
8038 let name = chart_child_local(&element, &namespaces)?;
8039 let raw = capture_empty_element(&element)?;
8040 state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
8041 }
8042 event @ (Event::Text(_)
8043 | Event::CData(_)
8044 | Event::Comment(_)
8045 | Event::PI(_)
8046 | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
8047 Event::End(element)
8048 if matches_local_name(element.name().as_ref(), kind.local().as_bytes()) =>
8049 {
8050 break;
8051 }
8052 Event::Eof => return Err(missing_end(&format!("c:{}", kind.local()))),
8053 _ => {}
8054 }
8055 buffer.clear();
8056 }
8057 state.finish(kind, raw_attributes, namespace_declarations)
8058 }
8059
8060 pub fn to_xml(&self) -> Result<Vec<u8>> {
8061 self.validate()?;
8062 let mut writer = Writer::new(Vec::new());
8063 let root = format!("c:{}", self.kind.local());
8064 let mut start = BytesStart::new(&root);
8065 start.push_attribute(("xmlns:c", C_NS));
8066 start.push_attribute(("xmlns:a", A_NS));
8067 start.push_attribute(("xmlns:r", R_NS));
8068 push_attributes(&mut start, &self.namespace_declarations);
8069 push_attributes(&mut start, &self.raw_attributes);
8070 writer
8071 .write_event(Event::Start(start))
8072 .map_err(OxmlError::from)?;
8073 emit_raw(&mut writer, self.raw_children.at(0))?;
8074 write_axis_id(&mut writer, "c:axId", self.id, &self.id_markup)?;
8075 emit_raw(&mut writer, self.raw_children.at(1))?;
8076 self.scaling.write_xml(&mut writer)?;
8077 emit_raw(&mut writer, self.raw_children.at(2))?;
8078 if self.deleted || self.delete_markup.is_some() {
8079 write_scalar(
8080 &mut writer,
8081 "c:delete",
8082 bool_lexical(self.deleted),
8083 self.delete_markup.as_ref(),
8084 )?;
8085 }
8086 emit_raw(&mut writer, self.raw_children.at(3))?;
8087 write_scalar(
8088 &mut writer,
8089 "c:axPos",
8090 self.position.as_str(),
8091 Some(&self.position_markup),
8092 )?;
8093 emit_raw(&mut writer, self.raw_children.at(4))?;
8094 if let Some(lines) = &self.major_gridlines {
8095 lines.write_xml(&mut writer, "c:majorGridlines")?;
8096 }
8097 emit_raw(&mut writer, self.raw_children.at(5))?;
8098 if let Some(lines) = &self.minor_gridlines {
8099 lines.write_xml(&mut writer, "c:minorGridlines")?;
8100 }
8101 emit_raw(&mut writer, self.raw_children.at(6))?;
8102 if let Some(title) = &self.title {
8103 title.write_xml(&mut writer)?;
8104 }
8105 emit_raw(&mut writer, self.raw_children.at(7))?;
8106 if let Some(number_format) = &self.number_format {
8107 number_format.write_xml(&mut writer)?;
8108 }
8109 emit_raw(&mut writer, self.raw_children.at(8))?;
8110 if self.major_tick_mark != TickMark::None || self.major_tick_mark_markup.is_some() {
8111 write_scalar(
8112 &mut writer,
8113 "c:majorTickMark",
8114 self.major_tick_mark.as_str(),
8115 self.major_tick_mark_markup.as_ref(),
8116 )?;
8117 }
8118 emit_raw(&mut writer, self.raw_children.at(9))?;
8119 if self.minor_tick_mark != TickMark::None || self.minor_tick_mark_markup.is_some() {
8120 write_scalar(
8121 &mut writer,
8122 "c:minorTickMark",
8123 self.minor_tick_mark.as_str(),
8124 self.minor_tick_mark_markup.as_ref(),
8125 )?;
8126 }
8127 emit_raw(&mut writer, self.raw_children.at(10))?;
8128 if self.tick_label_position != TickLabelPosition::NextTo
8129 || self.tick_label_position_markup.is_some()
8130 {
8131 write_scalar(
8132 &mut writer,
8133 "c:tickLblPos",
8134 self.tick_label_position.as_str(),
8135 self.tick_label_position_markup.as_ref(),
8136 )?;
8137 }
8138 emit_raw(&mut writer, self.raw_children.at(11))?;
8139 if let Some(properties) = &self.sp_pr {
8140 properties.write_xml_as(&mut writer, "c:spPr")?;
8141 }
8142 emit_raw(&mut writer, self.raw_children.at(12))?;
8143 if let Some(text) = &self.tx_pr {
8144 text.write_xml_as(&mut writer, "c:txPr")?;
8145 }
8146 emit_raw(&mut writer, self.raw_children.at(13))?;
8147 write_axis_id(
8148 &mut writer,
8149 "c:crossAx",
8150 self.cross_axis,
8151 &self.cross_axis_markup,
8152 )?;
8153 emit_raw(&mut writer, self.raw_children.at(14))?;
8154 writer
8155 .write_event(Event::End(BytesEnd::new(&root)))
8156 .map_err(OxmlError::from)?;
8157 Ok(writer.into_inner())
8158 }
8159
8160 pub fn raw_children(&self) -> &OrderedRawChildren {
8161 &self.raw_children
8162 }
8163
8164 fn validate(&self) -> Result<()> {
8165 if let Some(parsed_kind) = self.parsed_kind
8166 && self.kind != parsed_kind
8167 {
8168 return Err(ChartError::InvalidValue {
8169 element: "ChartML axis root".to_owned(),
8170 value: format!(
8171 "parsed c:{} cannot be relabelled c:{}",
8172 parsed_kind.local(),
8173 self.kind.local()
8174 ),
8175 });
8176 }
8177 AxisId::new(self.id.value())?;
8178 AxisId::new(self.cross_axis.value())?;
8179 self.scaling.validate()?;
8180 if let Some(number_format) = &self.number_format {
8181 number_format.validate()?;
8182 }
8183 Ok(())
8184 }
8185}
8186
8187#[derive(Default)]
8188struct AxisParseState {
8189 id: Option<(AxisId, AxisIdMarkup)>,
8190 scaling: Option<Scaling>,
8191 deleted: Option<(bool, ScalarMarkup)>,
8192 position: Option<(AxisPosition, ScalarMarkup)>,
8193 major_gridlines: Option<ChartLines>,
8194 minor_gridlines: Option<ChartLines>,
8195 title: Option<CT_Title>,
8196 number_format: Option<NumberFormat>,
8197 major_tick_mark: Option<(TickMark, ScalarMarkup)>,
8198 minor_tick_mark: Option<(TickMark, ScalarMarkup)>,
8199 tick_label_position: Option<(TickLabelPosition, ScalarMarkup)>,
8200 sp_pr: Option<CT_ShapeProperties>,
8201 tx_pr: Option<CT_TextBody>,
8202 cross_axis: Option<(AxisId, AxisIdMarkup)>,
8203 raw_children: OrderedRawChildren,
8204 boundary: usize,
8205}
8206
8207impl AxisParseState {
8208 fn capture_event(&mut self, raw: Vec<u8>) {
8209 self.raw_children.push(self.boundary, raw);
8210 }
8211
8212 fn parse_child(
8213 &mut self,
8214 name: &[u8],
8215 raw: Vec<u8>,
8216 namespaces: &NamespaceBindings,
8217 ) -> Result<()> {
8218 match name {
8219 b"axId" => {
8220 set_once(&mut self.id, parse_axis_id_scalar(&raw, "axId")?, "c:axId")?;
8221 self.boundary = self.boundary.max(1);
8222 }
8223 b"scaling" => {
8224 set_once(
8225 &mut self.scaling,
8226 Scaling::from_xml(&raw, namespaces)?,
8227 "c:scaling",
8228 )?;
8229 self.boundary = self.boundary.max(2);
8230 }
8231 b"delete" => {
8232 set_once(
8233 &mut self.deleted,
8234 parse_bool_value(&raw, "delete")?,
8235 "c:delete",
8236 )?;
8237 self.boundary = self.boundary.max(3);
8238 }
8239 b"axPos" => {
8240 set_once(&mut self.position, parse_axis_position(&raw)?, "c:axPos")?;
8241 self.boundary = self.boundary.max(4);
8242 }
8243 b"majorGridlines" => {
8244 set_once(
8245 &mut self.major_gridlines,
8246 ChartLines::from_xml(&raw, b"majorGridlines", namespaces)?,
8247 "c:majorGridlines",
8248 )?;
8249 self.boundary = self.boundary.max(5);
8250 }
8251 b"minorGridlines" => {
8252 set_once(
8253 &mut self.minor_gridlines,
8254 ChartLines::from_xml(&raw, b"minorGridlines", namespaces)?,
8255 "c:minorGridlines",
8256 )?;
8257 self.boundary = self.boundary.max(6);
8258 }
8259 b"title" => {
8260 set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
8261 self.boundary = self.boundary.max(7);
8262 }
8263 b"numFmt" => {
8264 set_once(
8265 &mut self.number_format,
8266 NumberFormat::from_xml(&raw)?,
8267 "c:numFmt",
8268 )?;
8269 self.boundary = self.boundary.max(8);
8270 }
8271 b"majorTickMark" => {
8272 set_once(
8273 &mut self.major_tick_mark,
8274 parse_tick_mark(&raw, "majorTickMark")?,
8275 "c:majorTickMark",
8276 )?;
8277 self.boundary = self.boundary.max(9);
8278 }
8279 b"minorTickMark" => {
8280 set_once(
8281 &mut self.minor_tick_mark,
8282 parse_tick_mark(&raw, "minorTickMark")?,
8283 "c:minorTickMark",
8284 )?;
8285 self.boundary = self.boundary.max(10);
8286 }
8287 b"tickLblPos" => {
8288 set_once(
8289 &mut self.tick_label_position,
8290 parse_tick_label_position(&raw)?,
8291 "c:tickLblPos",
8292 )?;
8293 self.boundary = self.boundary.max(11);
8294 }
8295 b"spPr" => {
8296 parse_chart_shape_properties(&raw, namespaces, &mut self.sp_pr)?;
8297 self.boundary = self.boundary.max(12);
8298 }
8299 b"txPr" => {
8300 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
8301 let inherited = namespaces
8302 .iter()
8303 .map(|(prefix, uri)| {
8304 Ok((
8305 std::str::from_utf8(prefix)
8306 .map_err(OxmlError::from)?
8307 .to_owned(),
8308 uri.clone(),
8309 ))
8310 })
8311 .collect::<Result<Vec<_>>>()?;
8312 let text = CT_TextBody::from_xml_as_with_namespaces(&raw, b"txPr", &inherited)?;
8313 let mut writer = Writer::new(Vec::new());
8314 text.write_xml_as(&mut writer, "c:txPr")?;
8315 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
8316 set_once(&mut self.tx_pr, text, "c:txPr")?;
8317 self.boundary = self.boundary.max(13);
8318 }
8319 b"crossAx" => {
8320 set_once(
8321 &mut self.cross_axis,
8322 parse_axis_id_scalar(&raw, "crossAx")?,
8323 "c:crossAx",
8324 )?;
8325 self.boundary = self.boundary.max(14);
8326 }
8327 _ => self.raw_children.push(self.boundary, raw),
8328 }
8329 Ok(())
8330 }
8331
8332 fn finish(
8333 self,
8334 kind: AxisKind,
8335 raw_attributes: XmlAttributes,
8336 namespace_declarations: XmlAttributes,
8337 ) -> Result<Axis> {
8338 let (id, id_markup) = self
8339 .id
8340 .ok_or_else(|| ChartError::MissingElement("c:axId".to_owned()))?;
8341 let scaling = self
8342 .scaling
8343 .ok_or_else(|| ChartError::MissingElement("c:scaling".to_owned()))?;
8344 let (position, position_markup) = self
8345 .position
8346 .ok_or_else(|| ChartError::MissingElement("c:axPos".to_owned()))?;
8347 let (cross_axis, cross_axis_markup) = self
8348 .cross_axis
8349 .ok_or_else(|| ChartError::MissingElement("c:crossAx".to_owned()))?;
8350 let (deleted, delete_markup) = self
8351 .deleted
8352 .map(|(value, markup)| (value, Some(markup)))
8353 .unwrap_or((false, None));
8354 let (major_tick_mark, major_tick_mark_markup) = self
8355 .major_tick_mark
8356 .map(|(value, markup)| (value, Some(markup)))
8357 .unwrap_or((TickMark::None, None));
8358 let (minor_tick_mark, minor_tick_mark_markup) = self
8359 .minor_tick_mark
8360 .map(|(value, markup)| (value, Some(markup)))
8361 .unwrap_or((TickMark::None, None));
8362 let (tick_label_position, tick_label_position_markup) = self
8363 .tick_label_position
8364 .map(|(value, markup)| (value, Some(markup)))
8365 .unwrap_or((TickLabelPosition::NextTo, None));
8366 let axis = Axis {
8367 kind,
8368 id,
8369 scaling,
8370 deleted,
8371 position,
8372 major_gridlines: self.major_gridlines,
8373 minor_gridlines: self.minor_gridlines,
8374 title: self.title,
8375 number_format: self.number_format,
8376 major_tick_mark,
8377 minor_tick_mark,
8378 tick_label_position,
8379 sp_pr: self.sp_pr,
8380 tx_pr: self.tx_pr,
8381 cross_axis,
8382 parsed_kind: Some(kind),
8383 id_markup,
8384 delete_markup,
8385 position_markup,
8386 major_tick_mark_markup,
8387 minor_tick_mark_markup,
8388 tick_label_position_markup,
8389 cross_axis_markup,
8390 raw_attributes,
8391 namespace_declarations,
8392 raw_children: raw_children_in_schema_order(&self.raw_children, 14),
8393 };
8394 axis.validate()?;
8395 Ok(axis)
8396 }
8397}
8398
8399impl Scaling {
8400 fn from_xml(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
8401 let mut reader = Reader::from_reader(xml);
8402 let mut buffer = Vec::new();
8403 loop {
8404 match reader
8405 .read_event_into(&mut buffer)
8406 .map_err(OxmlError::from)?
8407 {
8408 Event::Start(element)
8409 if matches_local_name(element.name().as_ref(), b"scaling") =>
8410 {
8411 let namespaces = typed_rewrite_bindings(&element, inherited)?;
8412 let raw_attributes = capture_attributes(&element)?;
8413 let mut log_base = None;
8414 let mut orientation = None;
8415 let mut maximum = None;
8416 let mut minimum = None;
8417 let mut raw_children = OrderedRawChildren::default();
8418 let mut boundary = 0usize;
8419 let mut inner = Vec::new();
8420 loop {
8421 match reader
8422 .read_event_into(&mut inner)
8423 .map_err(OxmlError::from)?
8424 {
8425 Event::Start(child) => {
8426 let name = chart_child_local(&child, &namespaces)?;
8427 let raw = capture_element(&mut reader, &child)?;
8428 parse_scaling_child(
8429 name.as_deref().unwrap_or_default(),
8430 raw,
8431 &mut log_base,
8432 &mut orientation,
8433 &mut maximum,
8434 &mut minimum,
8435 &mut raw_children,
8436 &mut boundary,
8437 )?;
8438 }
8439 Event::Empty(child) => {
8440 let name = chart_child_local(&child, &namespaces)?;
8441 let raw = capture_empty_element(&child)?;
8442 parse_scaling_child(
8443 name.as_deref().unwrap_or_default(),
8444 raw,
8445 &mut log_base,
8446 &mut orientation,
8447 &mut maximum,
8448 &mut minimum,
8449 &mut raw_children,
8450 &mut boundary,
8451 )?;
8452 }
8453 event @ (Event::Text(_)
8454 | Event::CData(_)
8455 | Event::Comment(_)
8456 | Event::PI(_)
8457 | Event::GeneralRef(_)) => {
8458 raw_children.push(boundary, capture_event(event)?);
8459 }
8460 Event::End(end)
8461 if matches_local_name(end.name().as_ref(), b"scaling") =>
8462 {
8463 let (log_base, log_base_markup) = optional_markup(log_base);
8464 let (orientation, orientation_markup) = orientation
8465 .map(|(value, markup)| (value, Some(markup)))
8466 .unwrap_or((Orientation::MinMax, None));
8467 let (maximum, maximum_markup) = optional_markup(maximum);
8468 let (minimum, minimum_markup) = optional_markup(minimum);
8469 let scaling = Self {
8470 log_base,
8471 orientation,
8472 maximum,
8473 minimum,
8474 log_base_markup,
8475 orientation_markup,
8476 maximum_markup,
8477 minimum_markup,
8478 raw_attributes,
8479 raw_children: raw_children_in_schema_order(&raw_children, 4),
8480 };
8481 scaling.validate()?;
8482 return Ok(scaling);
8483 }
8484 Event::Eof => return Err(missing_end("c:scaling")),
8485 _ => {}
8486 }
8487 inner.clear();
8488 }
8489 }
8490 Event::Empty(element)
8491 if matches_local_name(element.name().as_ref(), b"scaling") =>
8492 {
8493 typed_rewrite_bindings(&element, inherited)?;
8494 return Ok(Self {
8495 raw_attributes: capture_attributes(&element)?,
8496 ..Self::default()
8497 });
8498 }
8499 Event::Start(element) | Event::Empty(element) => {
8500 return Err(ChartError::UnexpectedElement(element_name(&element)));
8501 }
8502 Event::Eof => return Err(ChartError::MissingElement("c:scaling".to_owned())),
8503 _ => {}
8504 }
8505 buffer.clear();
8506 }
8507 }
8508
8509 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8510 self.validate()?;
8511 let mut start = BytesStart::new("c:scaling");
8512 push_attributes(&mut start, &self.raw_attributes);
8513 writer
8514 .write_event(Event::Start(start))
8515 .map_err(OxmlError::from)?;
8516 emit_raw(writer, self.raw_children.at(0))?;
8517 if let Some(value) = self.log_base {
8518 write_scalar(
8519 writer,
8520 "c:logBase",
8521 &value.to_string(),
8522 self.log_base_markup.as_ref(),
8523 )?;
8524 }
8525 emit_raw(writer, self.raw_children.at(1))?;
8526 if self.orientation != Orientation::MinMax || self.orientation_markup.is_some() {
8527 write_scalar(
8528 writer,
8529 "c:orientation",
8530 self.orientation.as_str(),
8531 self.orientation_markup.as_ref(),
8532 )?;
8533 }
8534 emit_raw(writer, self.raw_children.at(2))?;
8535 if let Some(value) = self.maximum {
8536 write_scalar(
8537 writer,
8538 "c:max",
8539 &value.to_string(),
8540 self.maximum_markup.as_ref(),
8541 )?;
8542 }
8543 emit_raw(writer, self.raw_children.at(3))?;
8544 if let Some(value) = self.minimum {
8545 write_scalar(
8546 writer,
8547 "c:min",
8548 &value.to_string(),
8549 self.minimum_markup.as_ref(),
8550 )?;
8551 }
8552 emit_raw(writer, self.raw_children.at(4))?;
8553 writer
8554 .write_event(Event::End(BytesEnd::new("c:scaling")))
8555 .map_err(OxmlError::from)?;
8556 Ok(())
8557 }
8558
8559 fn validate(&self) -> Result<()> {
8560 if let Some(value) = self.log_base
8561 && (!value.is_finite() || !(2.0..=1000.0).contains(&value))
8562 {
8563 return Err(ChartError::InvalidValue {
8564 element: "c:logBase".to_owned(),
8565 value: value.to_string(),
8566 });
8567 }
8568 for (element, value) in [("c:max", self.maximum), ("c:min", self.minimum)] {
8569 if let Some(value) = value
8570 && !value.is_finite()
8571 {
8572 return Err(ChartError::InvalidValue {
8573 element: element.to_owned(),
8574 value: value.to_string(),
8575 });
8576 }
8577 }
8578 if let (Some(minimum), Some(maximum)) = (self.minimum, self.maximum)
8579 && minimum > maximum
8580 {
8581 return Err(ChartError::InvalidValue {
8582 element: "c:scaling".to_owned(),
8583 value: format!("minimum {minimum} exceeds maximum {maximum}"),
8584 });
8585 }
8586 Ok(())
8587 }
8588}
8589
8590#[allow(clippy::too_many_arguments)]
8591fn parse_scaling_child(
8592 name: &[u8],
8593 raw: Vec<u8>,
8594 log_base: &mut Option<(f64, ScalarMarkup)>,
8595 orientation: &mut Option<(Orientation, ScalarMarkup)>,
8596 maximum: &mut Option<(f64, ScalarMarkup)>,
8597 minimum: &mut Option<(f64, ScalarMarkup)>,
8598 raw_children: &mut OrderedRawChildren,
8599 boundary: &mut usize,
8600) -> Result<()> {
8601 match name {
8602 b"logBase" => {
8603 set_once(log_base, parse_f64_scalar(&raw, "logBase")?, "c:logBase")?;
8604 *boundary = (*boundary).max(1);
8605 }
8606 b"orientation" => {
8607 let (value, markup) = scalar_value(&raw, "orientation")?;
8608 let value = value
8609 .ok_or_else(|| invalid_attribute("orientation", "val", "<missing>".to_owned()))?;
8610 let parsed = Orientation::parse(&value)
8611 .ok_or_else(|| invalid_attribute("orientation", "val", value))?;
8612 set_once(orientation, (parsed, markup), "c:orientation")?;
8613 *boundary = (*boundary).max(2);
8614 }
8615 b"max" => {
8616 set_once(maximum, parse_f64_scalar(&raw, "max")?, "c:max")?;
8617 *boundary = (*boundary).max(3);
8618 }
8619 b"min" => {
8620 set_once(minimum, parse_f64_scalar(&raw, "min")?, "c:min")?;
8621 *boundary = (*boundary).max(4);
8622 }
8623 _ => raw_children.push(*boundary, raw),
8624 }
8625 Ok(())
8626}
8627
8628impl ChartLines {
8629 fn from_xml(xml: &[u8], local: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
8630 let mut reader = Reader::from_reader(xml);
8631 let mut buffer = Vec::new();
8632 loop {
8633 match reader
8634 .read_event_into(&mut buffer)
8635 .map_err(OxmlError::from)?
8636 {
8637 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
8638 let namespaces = typed_rewrite_bindings(&element, inherited)?;
8639 let raw_attributes = capture_attributes(&element)?;
8640 let mut sp_pr = None;
8641 let mut raw_children = OrderedRawChildren::default();
8642 let mut boundary = 0usize;
8643 let mut inner = Vec::new();
8644 loop {
8645 match reader
8646 .read_event_into(&mut inner)
8647 .map_err(OxmlError::from)?
8648 {
8649 Event::Start(child) => {
8650 let name = chart_child_local(&child, &namespaces)?;
8651 let raw = capture_element(&mut reader, &child)?;
8652 if name.as_deref() == Some(b"spPr") {
8653 parse_chart_shape_properties(&raw, &namespaces, &mut sp_pr)?;
8654 boundary = 1;
8655 } else {
8656 raw_children.push(boundary, raw);
8657 }
8658 }
8659 Event::Empty(child) => {
8660 let name = chart_child_local(&child, &namespaces)?;
8661 let raw = capture_empty_element(&child)?;
8662 if name.as_deref() == Some(b"spPr") {
8663 parse_chart_shape_properties(&raw, &namespaces, &mut sp_pr)?;
8664 boundary = 1;
8665 } else {
8666 raw_children.push(boundary, raw);
8667 }
8668 }
8669 event @ (Event::Text(_)
8670 | Event::CData(_)
8671 | Event::Comment(_)
8672 | Event::PI(_)
8673 | Event::GeneralRef(_)) => {
8674 raw_children.push(boundary, capture_event(event)?);
8675 }
8676 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
8677 return Ok(Self {
8678 sp_pr,
8679 raw_attributes,
8680 raw_children: raw_children_in_schema_order(&raw_children, 1),
8681 });
8682 }
8683 Event::Eof => {
8684 return Err(missing_end(&format!(
8685 "c:{}",
8686 String::from_utf8_lossy(local)
8687 )));
8688 }
8689 _ => {}
8690 }
8691 inner.clear();
8692 }
8693 }
8694 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
8695 typed_rewrite_bindings(&element, inherited)?;
8696 return Ok(Self {
8697 sp_pr: None,
8698 raw_attributes: capture_attributes(&element)?,
8699 raw_children: OrderedRawChildren::default(),
8700 });
8701 }
8702 Event::Start(element) | Event::Empty(element) => {
8703 return Err(ChartError::UnexpectedElement(element_name(&element)));
8704 }
8705 Event::Eof => {
8706 return Err(ChartError::MissingElement(format!(
8707 "c:{}",
8708 String::from_utf8_lossy(local)
8709 )));
8710 }
8711 _ => {}
8712 }
8713 buffer.clear();
8714 }
8715 }
8716
8717 fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
8718 let mut start = BytesStart::new(tag);
8719 push_attributes(&mut start, &self.raw_attributes);
8720 if self.sp_pr.is_none() && self.raw_children.is_empty() {
8721 writer
8722 .write_event(Event::Empty(start))
8723 .map_err(OxmlError::from)?;
8724 return Ok(());
8725 }
8726 writer
8727 .write_event(Event::Start(start))
8728 .map_err(OxmlError::from)?;
8729 emit_raw(writer, self.raw_children.at(0))?;
8730 if let Some(properties) = &self.sp_pr {
8731 properties.write_xml_as(writer, "c:spPr")?;
8732 }
8733 emit_raw(writer, self.raw_children.at(1))?;
8734 writer
8735 .write_event(Event::End(BytesEnd::new(tag)))
8736 .map_err(OxmlError::from)?;
8737 Ok(())
8738 }
8739}
8740
8741fn parse_chart_shape_properties(
8742 raw: &[u8],
8743 namespaces: &NamespaceBindings,
8744 slot: &mut Option<CT_ShapeProperties>,
8745) -> Result<()> {
8746 reject_conflicting_prefix_in_xml(raw, b"a", A_NS)?;
8747 let inherited = namespaces
8748 .iter()
8749 .map(|(prefix, uri)| {
8750 Ok((
8751 std::str::from_utf8(prefix)
8752 .map_err(OxmlError::from)?
8753 .to_owned(),
8754 uri.clone(),
8755 ))
8756 })
8757 .collect::<Result<Vec<_>>>()?;
8758 let properties = CT_ShapeProperties::from_xml_with_inherited_namespaces(raw, &inherited)?;
8759 let mut writer = Writer::new(Vec::new());
8760 properties.write_xml_as(&mut writer, "c:spPr")?;
8761 reject_rewritten_foreign_elements(raw, &writer.into_inner(), namespaces, b"spPr")?;
8762 set_once(slot, properties, "c:spPr")
8763}
8764
8765impl NumberFormat {
8766 pub fn new(format_code: String, source_linked: bool) -> Result<Self> {
8767 let number_format = Self {
8768 format_code,
8769 source_linked,
8770 raw_attributes: Vec::new(),
8771 raw_content: Vec::new(),
8772 };
8773 number_format.validate()?;
8774 Ok(number_format)
8775 }
8776
8777 fn from_xml(xml: &[u8]) -> Result<Self> {
8778 let mut reader = Reader::from_reader(xml);
8779 let mut buffer = Vec::new();
8780 loop {
8781 match reader
8782 .read_event_into(&mut buffer)
8783 .map_err(OxmlError::from)?
8784 {
8785 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
8786 return Self::from_element(&element, Vec::new());
8787 }
8788 Event::Start(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
8789 let mut raw_content = Vec::new();
8790 let mut inner = Vec::new();
8791 loop {
8792 match reader
8793 .read_event_into(&mut inner)
8794 .map_err(OxmlError::from)?
8795 {
8796 Event::End(end)
8797 if matches_local_name(end.name().as_ref(), b"numFmt") =>
8798 {
8799 return Self::from_element(&element, raw_content);
8800 }
8801 event @ (Event::Text(_)
8802 | Event::CData(_)
8803 | Event::Comment(_)
8804 | Event::PI(_)
8805 | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
8806 Event::Eof => return Err(missing_end("c:numFmt")),
8807 _ => return Err(ChartError::UnexpectedElement("c:numFmt".to_owned())),
8808 }
8809 inner.clear();
8810 }
8811 }
8812 Event::Start(element) | Event::Empty(element) => {
8813 return Err(ChartError::UnexpectedElement(element_name(&element)));
8814 }
8815 Event::Eof => return Err(ChartError::MissingElement("c:numFmt".to_owned())),
8816 _ => {}
8817 }
8818 buffer.clear();
8819 }
8820 }
8821
8822 fn from_element(element: &BytesStart<'_>, raw_content: Vec<Vec<u8>>) -> Result<Self> {
8823 reject_conflicting_prefix(element, b"c", C_NS)?;
8824 let format_code = attribute_value(element, b"formatCode")?
8825 .ok_or_else(|| invalid_attribute("numFmt", "formatCode", "<missing>".to_owned()))?;
8826 let source_linked = attribute_value(element, b"sourceLinked")?
8827 .ok_or_else(|| invalid_attribute("numFmt", "sourceLinked", "<missing>".to_owned()))?;
8828 let source_linked = parse_bool_lexical("numFmt", "sourceLinked", &source_linked)?;
8829 let number_format = Self {
8830 format_code,
8831 source_linked,
8832 raw_attributes: capture_attributes_excluding(
8833 element,
8834 &[b"formatCode", b"sourceLinked"],
8835 )?,
8836 raw_content,
8837 };
8838 number_format.validate()?;
8839 Ok(number_format)
8840 }
8841
8842 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8843 self.validate()?;
8844 let mut start = BytesStart::new("c:numFmt");
8845 start.push_attribute(("formatCode", self.format_code.as_str()));
8846 start.push_attribute(("sourceLinked", bool_lexical(self.source_linked)));
8847 push_attributes(&mut start, &self.raw_attributes);
8848 if self.raw_content.is_empty() {
8849 writer
8850 .write_event(Event::Empty(start))
8851 .map_err(OxmlError::from)?;
8852 return Ok(());
8853 }
8854 writer
8855 .write_event(Event::Start(start))
8856 .map_err(OxmlError::from)?;
8857 for raw in &self.raw_content {
8858 writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
8859 }
8860 writer
8861 .write_event(Event::End(BytesEnd::new("c:numFmt")))
8862 .map_err(OxmlError::from)?;
8863 Ok(())
8864 }
8865
8866 pub fn format_value(&self, value: f64) -> Result<String> {
8868 if !value.is_finite() {
8869 return Err(ChartError::InvalidValue {
8870 element: "c:numFmt value".to_owned(),
8871 value: value.to_string(),
8872 });
8873 }
8874 if self.format_code == "General" {
8875 return Ok(value.to_string());
8876 }
8877
8878 let (numeric_code, percentage) = self
8879 .format_code
8880 .strip_suffix('%')
8881 .map(|code| (code, true))
8882 .unwrap_or((&self.format_code, false));
8883 let precision = if numeric_code == "0" {
8884 Some(0)
8885 } else if let Some(decimals) = numeric_code.strip_prefix("0.")
8886 && !decimals.is_empty()
8887 && decimals.bytes().all(|byte| byte == b'0')
8888 {
8889 Some(decimals.len())
8890 } else {
8891 None
8892 };
8893 let Some(precision) = precision else {
8894 return Err(ChartError::InvalidValue {
8895 element: "c:numFmt/@formatCode projection".to_owned(),
8896 value: self.format_code.clone(),
8897 });
8898 };
8899 let projected = if percentage { value * 100.0 } else { value };
8900 if !projected.is_finite() {
8901 return Err(ChartError::InvalidValue {
8902 element: "c:numFmt value".to_owned(),
8903 value: projected.to_string(),
8904 });
8905 }
8906 let mut formatted = format!("{projected:.precision$}");
8907 if percentage {
8908 formatted.push('%');
8909 }
8910 Ok(formatted)
8911 }
8912
8913 fn validate(&self) -> Result<()> {
8914 if self.format_code.is_empty() {
8915 return Err(ChartError::InvalidValue {
8916 element: "c:numFmt/@formatCode".to_owned(),
8917 value: self.format_code.clone(),
8918 });
8919 }
8920 validate_xml_text(&self.format_code, "c:numFmt/@formatCode")
8921 }
8922}
8923
8924impl CT_DLbls {
8925 pub fn from_xml(xml: &[u8]) -> Result<Self> {
8926 Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
8927 }
8928
8929 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
8930 let mut reader = Reader::from_reader(xml);
8931 let mut buffer = Vec::new();
8932 loop {
8933 match reader
8934 .read_event_into(&mut buffer)
8935 .map_err(OxmlError::from)?
8936 {
8937 Event::Start(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
8938 chart_root_prefix(&element)?;
8939 if !element_is_in_namespace(&element, C_NS, inherited)? {
8940 return Err(ChartError::UnexpectedElement(element_name(&element)));
8941 }
8942 return Self::from_element(&mut reader, &element, inherited);
8943 }
8944 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
8945 chart_root_prefix(&element)?;
8946 let namespaces = chart_bindings(inherited, &element)?;
8947 if !element_is_in_namespace(&element, C_NS, inherited)? {
8948 return Err(ChartError::UnexpectedElement(element_name(&element)));
8949 }
8950 let (raw_attributes, _) = capture_fixed_root_attributes(
8951 &element,
8952 &["xmlns:c", "xmlns:a", "xmlns:r"],
8953 )?;
8954 return Ok(Self {
8955 raw_attributes,
8956 namespace_declarations: standalone_namespace_declarations(&namespaces)?,
8957 ..Self::default()
8958 });
8959 }
8960 Event::Start(element) | Event::Empty(element) => {
8961 return Err(ChartError::UnexpectedElement(element_name(&element)));
8962 }
8963 Event::Eof => return Err(ChartError::MissingElement("c:dLbls".to_owned())),
8964 _ => {}
8965 }
8966 buffer.clear();
8967 }
8968 }
8969
8970 fn from_element(
8971 reader: &mut Reader<&[u8]>,
8972 start: &BytesStart<'_>,
8973 inherited: &NamespaceBindings,
8974 ) -> Result<Self> {
8975 reject_conflicting_prefix(start, b"a", A_NS)?;
8976 let namespaces = chart_bindings(inherited, start)?;
8977 require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
8978 let (raw_attributes, _) =
8979 capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8980 let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
8981 let mut state = DataLabelsParseState::new(namespaces);
8982 let mut buffer = Vec::new();
8983 loop {
8984 match reader
8985 .read_event_into(&mut buffer)
8986 .map_err(OxmlError::from)?
8987 {
8988 Event::Start(element) => {
8989 let name = chart_child_local(&element, &state.namespaces)?;
8990 let raw = capture_element(reader, &element)?;
8991 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
8992 }
8993 Event::Empty(element) => {
8994 let name = chart_child_local(&element, &state.namespaces)?;
8995 let raw = capture_empty_element(&element)?;
8996 state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
8997 }
8998 event @ (Event::Text(_)
8999 | Event::CData(_)
9000 | Event::Comment(_)
9001 | Event::PI(_)
9002 | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
9003 Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
9004 return state.finish(raw_attributes, namespace_declarations);
9005 }
9006 Event::Eof => return Err(missing_end("c:dLbls")),
9007 _ => {}
9008 }
9009 buffer.clear();
9010 }
9011 }
9012
9013 pub fn to_xml(&self) -> Result<Vec<u8>> {
9014 let mut writer = Writer::new(Vec::new());
9015 self.write_xml(&mut writer, true)?;
9016 Ok(writer.into_inner())
9017 }
9018
9019 fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
9020 self.validate()?;
9021 let mut start = BytesStart::new("c:dLbls");
9022 if standalone {
9023 start.push_attribute(("xmlns:c", C_NS));
9024 start.push_attribute(("xmlns:a", A_NS));
9025 start.push_attribute(("xmlns:r", R_NS));
9026 }
9027 push_attributes(&mut start, &self.namespace_declarations);
9028 push_attributes(&mut start, &self.raw_attributes);
9029 writer
9030 .write_event(Event::Start(start))
9031 .map_err(OxmlError::from)?;
9032 emit_raw(writer, self.raw_children.at(0))?;
9033 if let Some(number_format) = &self.number_format {
9034 number_format.write_xml(writer)?;
9035 }
9036 emit_raw(writer, self.raw_children.at(1))?;
9037 if let Some(position) = self.position {
9038 write_scalar(
9039 writer,
9040 "c:dLblPos",
9041 position.as_str(),
9042 self.position_markup.as_ref(),
9043 )?;
9044 }
9045 emit_raw(writer, self.raw_children.at(2))?;
9046 write_optional_bool(
9047 writer,
9048 "c:showLegendKey",
9049 self.show_legend_key,
9050 self.show_legend_key_markup.as_ref(),
9051 )?;
9052 emit_raw(writer, self.raw_children.at(3))?;
9053 write_optional_bool(
9054 writer,
9055 "c:showVal",
9056 self.show_value,
9057 self.show_value_markup.as_ref(),
9058 )?;
9059 emit_raw(writer, self.raw_children.at(4))?;
9060 write_optional_bool(
9061 writer,
9062 "c:showCatName",
9063 self.show_category_name,
9064 self.show_category_name_markup.as_ref(),
9065 )?;
9066 emit_raw(writer, self.raw_children.at(5))?;
9067 write_optional_bool(
9068 writer,
9069 "c:showSerName",
9070 self.show_series_name,
9071 self.show_series_name_markup.as_ref(),
9072 )?;
9073 emit_raw(writer, self.raw_children.at(6))?;
9074 write_optional_bool(
9075 writer,
9076 "c:showPercent",
9077 self.show_percent,
9078 self.show_percent_markup.as_ref(),
9079 )?;
9080 emit_raw(writer, self.raw_children.at(7))?;
9081 write_optional_bool(
9082 writer,
9083 "c:showBubbleSize",
9084 self.show_bubble_size,
9085 self.show_bubble_size_markup.as_ref(),
9086 )?;
9087 emit_raw(writer, self.raw_children.at(8))?;
9088 if let Some(separator) = &self.separator {
9089 write_text(
9090 writer,
9091 "c:separator",
9092 separator,
9093 self.separator_markup
9094 .as_ref()
9095 .unwrap_or(&TextMarkup::default()),
9096 )?;
9097 }
9098 emit_raw(writer, self.raw_children.at(9))?;
9099 writer
9100 .write_event(Event::End(BytesEnd::new("c:dLbls")))
9101 .map_err(OxmlError::from)?;
9102 Ok(())
9103 }
9104
9105 fn validate(&self) -> Result<()> {
9106 if let Some(number_format) = &self.number_format {
9107 number_format.validate()?;
9108 }
9109 if let Some(separator) = &self.separator {
9110 validate_xml_text(separator, "c:separator")?;
9111 }
9112 Ok(())
9113 }
9114
9115 pub fn raw_children(&self) -> &OrderedRawChildren {
9116 &self.raw_children
9117 }
9118}
9119
9120struct DataLabelsParseState {
9121 namespaces: NamespaceBindings,
9122 number_format: Option<NumberFormat>,
9123 position: Option<(DataLabelPosition, ScalarMarkup)>,
9124 show_legend_key: Option<(bool, ScalarMarkup)>,
9125 show_value: Option<(bool, ScalarMarkup)>,
9126 show_category_name: Option<(bool, ScalarMarkup)>,
9127 show_series_name: Option<(bool, ScalarMarkup)>,
9128 show_percent: Option<(bool, ScalarMarkup)>,
9129 show_bubble_size: Option<(bool, ScalarMarkup)>,
9130 separator: Option<(String, TextMarkup)>,
9131 raw_children: OrderedRawChildren,
9132 boundary: usize,
9133}
9134
9135impl DataLabelsParseState {
9136 fn new(namespaces: NamespaceBindings) -> Self {
9137 Self {
9138 namespaces,
9139 number_format: None,
9140 position: None,
9141 show_legend_key: None,
9142 show_value: None,
9143 show_category_name: None,
9144 show_series_name: None,
9145 show_percent: None,
9146 show_bubble_size: None,
9147 separator: None,
9148 raw_children: OrderedRawChildren::default(),
9149 boundary: 0,
9150 }
9151 }
9152
9153 fn capture_event(&mut self, raw: Vec<u8>) {
9154 self.raw_children.push(self.boundary, raw);
9155 }
9156
9157 fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
9158 match name {
9159 b"numFmt" => {
9160 set_once(
9161 &mut self.number_format,
9162 NumberFormat::from_xml(&raw)?,
9163 "c:numFmt",
9164 )?;
9165 self.boundary = self.boundary.max(1);
9166 }
9167 b"dLblPos" => {
9168 let (value, markup) = scalar_value(&raw, "dLblPos")?;
9169 let lexical = value
9170 .ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
9171 let position = DataLabelPosition::parse(&lexical)
9172 .ok_or_else(|| invalid_attribute("dLblPos", "val", lexical))?;
9173 set_once(&mut self.position, (position, markup), "c:dLblPos")?;
9174 self.boundary = self.boundary.max(2);
9175 }
9176 b"showLegendKey" => {
9177 set_once(
9178 &mut self.show_legend_key,
9179 parse_bool_value(&raw, "showLegendKey")?,
9180 "c:showLegendKey",
9181 )?;
9182 self.boundary = self.boundary.max(3);
9183 }
9184 b"showVal" => {
9185 set_once(
9186 &mut self.show_value,
9187 parse_bool_value(&raw, "showVal")?,
9188 "c:showVal",
9189 )?;
9190 self.boundary = self.boundary.max(4);
9191 }
9192 b"showCatName" => {
9193 set_once(
9194 &mut self.show_category_name,
9195 parse_bool_value(&raw, "showCatName")?,
9196 "c:showCatName",
9197 )?;
9198 self.boundary = self.boundary.max(5);
9199 }
9200 b"showSerName" => {
9201 set_once(
9202 &mut self.show_series_name,
9203 parse_bool_value(&raw, "showSerName")?,
9204 "c:showSerName",
9205 )?;
9206 self.boundary = self.boundary.max(6);
9207 }
9208 b"showPercent" => {
9209 set_once(
9210 &mut self.show_percent,
9211 parse_bool_value(&raw, "showPercent")?,
9212 "c:showPercent",
9213 )?;
9214 self.boundary = self.boundary.max(7);
9215 }
9216 b"showBubbleSize" => {
9217 set_once(
9218 &mut self.show_bubble_size,
9219 parse_bool_value(&raw, "showBubbleSize")?,
9220 "c:showBubbleSize",
9221 )?;
9222 self.boundary = self.boundary.max(8);
9223 }
9224 b"separator" => {
9225 set_once(
9226 &mut self.separator,
9227 parse_text_element(&raw, b"separator", &self.namespaces)?,
9228 "c:separator",
9229 )?;
9230 self.boundary = self.boundary.max(9);
9231 }
9232 _ => {
9233 let boundary = data_labels_raw_boundary(name, self.boundary);
9234 self.raw_children.push(boundary, raw);
9235 self.boundary = self.boundary.max(boundary);
9236 }
9237 }
9238 Ok(())
9239 }
9240
9241 fn finish(
9242 self,
9243 raw_attributes: XmlAttributes,
9244 namespace_declarations: XmlAttributes,
9245 ) -> Result<CT_DLbls> {
9246 let (position, position_markup) = optional_markup(self.position);
9247 let (show_legend_key, show_legend_key_markup) = optional_bool_markup(self.show_legend_key);
9248 let (show_value, show_value_markup) = optional_bool_markup(self.show_value);
9249 let (show_category_name, show_category_name_markup) =
9250 optional_bool_markup(self.show_category_name);
9251 let (show_series_name, show_series_name_markup) =
9252 optional_bool_markup(self.show_series_name);
9253 let (show_percent, show_percent_markup) = optional_bool_markup(self.show_percent);
9254 let (show_bubble_size, show_bubble_size_markup) =
9255 optional_bool_markup(self.show_bubble_size);
9256 let (separator, separator_markup) = self
9257 .separator
9258 .map(|(value, markup)| (Some(value), Some(markup)))
9259 .unwrap_or((None, None));
9260 let labels = CT_DLbls {
9261 number_format: self.number_format,
9262 position,
9263 separator,
9264 show_legend_key,
9265 show_value,
9266 show_category_name,
9267 show_series_name,
9268 show_percent,
9269 show_bubble_size,
9270 position_markup,
9271 show_legend_key_markup,
9272 show_value_markup,
9273 show_category_name_markup,
9274 show_series_name_markup,
9275 show_percent_markup,
9276 show_bubble_size_markup,
9277 separator_markup,
9278 raw_attributes,
9279 namespace_declarations,
9280 raw_children: raw_children_in_schema_order(&self.raw_children, 9),
9281 };
9282 labels.validate()?;
9283 Ok(labels)
9284 }
9285}
9286
9287fn data_labels_raw_boundary(name: &[u8], current: usize) -> usize {
9288 match name {
9289 b"dLbl" | b"delete" => 0,
9290 b"spPr" | b"txPr" => 1,
9291 b"showLeaderLines" | b"leaderLines" | b"extLst" => 9,
9292 _ => current,
9293 }
9294}
9295
9296fn optional_bool_markup(value: Option<(bool, ScalarMarkup)>) -> (bool, Option<ScalarMarkup>) {
9297 value
9298 .map(|(value, markup)| (value, Some(markup)))
9299 .unwrap_or((false, None))
9300}
9301
9302fn write_optional_bool(
9303 writer: &mut Writer<Vec<u8>>,
9304 tag: &str,
9305 value: bool,
9306 markup: Option<&ScalarMarkup>,
9307) -> Result<()> {
9308 if value || markup.is_some() {
9309 write_scalar(writer, tag, bool_lexical(value), markup)?;
9310 }
9311 Ok(())
9312}
9313
9314fn default_axis_id_markup(id: AxisId) -> AxisIdMarkup {
9315 AxisIdMarkup {
9316 scalar: ScalarMarkup::default(),
9317 parsed: id,
9318 lexical: id.value().to_string(),
9319 }
9320}
9321
9322fn parse_axis_id_scalar(xml: &[u8], local: &str) -> Result<(AxisId, AxisIdMarkup)> {
9323 let (value, scalar) = scalar_value(xml, local)?;
9324 let lexical = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
9325 let parsed = lexical
9326 .parse::<i64>()
9327 .map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
9328 let id = AxisId::new(parsed).map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
9329 Ok((
9330 id,
9331 AxisIdMarkup {
9332 scalar,
9333 parsed: id,
9334 lexical,
9335 },
9336 ))
9337}
9338
9339fn write_axis_id<W: Write>(
9340 writer: &mut Writer<W>,
9341 tag: &str,
9342 id: AxisId,
9343 markup: &AxisIdMarkup,
9344) -> Result<()> {
9345 AxisId::new(id.value())?;
9346 let normalized;
9347 let value = if id == markup.parsed {
9348 markup.lexical.as_str()
9349 } else {
9350 normalized = id.value().to_string();
9351 normalized.as_str()
9352 };
9353 write_scalar(writer, tag, value, Some(&markup.scalar))
9354}
9355
9356fn parse_f64_scalar(xml: &[u8], local: &str) -> Result<(f64, ScalarMarkup)> {
9357 let (value, markup) = scalar_value(xml, local)?;
9358 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
9359 let parsed = value
9360 .parse::<f64>()
9361 .map_err(|_| invalid_attribute(local, "val", value.clone()))?;
9362 if !parsed.is_finite() {
9363 return Err(invalid_attribute(local, "val", value));
9364 }
9365 Ok((parsed, markup))
9366}
9367
9368fn parse_axis_position(xml: &[u8]) -> Result<(AxisPosition, ScalarMarkup)> {
9369 let (value, markup) = scalar_value(xml, "axPos")?;
9370 let value = value.ok_or_else(|| invalid_attribute("axPos", "val", "<missing>".to_owned()))?;
9371 let parsed =
9372 AxisPosition::parse(&value).ok_or_else(|| invalid_attribute("axPos", "val", value))?;
9373 Ok((parsed, markup))
9374}
9375
9376fn parse_tick_mark(xml: &[u8], local: &str) -> Result<(TickMark, ScalarMarkup)> {
9377 let (value, markup) = scalar_value(xml, local)?;
9378 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
9379 let parsed = TickMark::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
9380 Ok((parsed, markup))
9381}
9382
9383fn parse_tick_label_position(xml: &[u8]) -> Result<(TickLabelPosition, ScalarMarkup)> {
9384 let (value, markup) = scalar_value(xml, "tickLblPos")?;
9385 let value =
9386 value.ok_or_else(|| invalid_attribute("tickLblPos", "val", "<missing>".to_owned()))?;
9387 let parsed = TickLabelPosition::parse(&value)
9388 .ok_or_else(|| invalid_attribute("tickLblPos", "val", value))?;
9389 Ok((parsed, markup))
9390}
9391
9392fn optional_markup<T>(value: Option<(T, ScalarMarkup)>) -> (Option<T>, Option<ScalarMarkup>) {
9393 value
9394 .map(|(value, markup)| (Some(value), Some(markup)))
9395 .unwrap_or((None, None))
9396}
9397
9398fn optional_scalar_markup_eq(left: &Option<ScalarMarkup>, right: &Option<ScalarMarkup>) -> bool {
9399 let empty = ScalarMarkup::default();
9400 left.as_ref().unwrap_or(&empty) == right.as_ref().unwrap_or(&empty)
9401}
9402
9403fn axis_id_markup_eq(left: &AxisIdMarkup, right: &AxisIdMarkup) -> bool {
9404 left.scalar == right.scalar
9405}
9406
9407const fn bool_lexical(value: bool) -> &'static str {
9408 if value { "1" } else { "0" }
9409}
9410
9411fn parse_bool_lexical(element: &str, attribute: &str, value: &str) -> Result<bool> {
9412 match value {
9413 "1" | "true" => Ok(true),
9414 "0" | "false" => Ok(false),
9415 _ => Err(invalid_attribute(element, attribute, value.to_owned())),
9416 }
9417}
9418
9419impl CT_Title {
9420 fn plain_text(text: &str) -> Self {
9421 let mut body = CT_TextBody::new();
9422 body.set_text(text);
9423 let mut raw_children = OrderedRawChildren::default();
9424 raw_children.push(1, br#"<c:layout/><c:overlay val="0"/>"#.to_vec());
9425 Self {
9426 text: Some(body),
9427 raw_children,
9428 ..Self::default()
9429 }
9430 }
9431
9432 fn from_xml(xml: &[u8]) -> Result<Self> {
9433 let (raw_attributes, raw_children) = parse_raw_shell(xml, b"title", "c:title")?;
9434 Ok(Self {
9435 text: None,
9436 raw_attributes,
9437 raw_children,
9438 })
9439 }
9440
9441 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
9442 if self.text.is_none() {
9443 return write_raw_shell(writer, "c:title", &self.raw_attributes, &self.raw_children);
9444 }
9445 let mut start = BytesStart::new("c:title");
9446 push_attributes(&mut start, &self.raw_attributes);
9447 writer
9448 .write_event(Event::Start(start))
9449 .map_err(OxmlError::from)?;
9450 emit_raw(writer, self.raw_children.at(0))?;
9451 writer
9452 .write_event(Event::Start(BytesStart::new("c:tx")))
9453 .map_err(OxmlError::from)?;
9454 if let Some(text) = &self.text {
9455 text.write_xml_as(writer, "c:rich")?;
9456 }
9457 writer
9458 .write_event(Event::End(BytesEnd::new("c:tx")))
9459 .map_err(OxmlError::from)?;
9460 emit_raw(writer, self.raw_children.at(1))?;
9461 writer
9462 .write_event(Event::End(BytesEnd::new("c:title")))
9463 .map_err(OxmlError::from)?;
9464 Ok(())
9465 }
9466
9467 pub fn raw_children(&self) -> &OrderedRawChildren {
9468 &self.raw_children
9469 }
9470}
9471
9472impl CT_PlotArea {
9473 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
9474 let (raw_attributes, captured_children) = parse_raw_shell(xml, b"plotArea", "c:plotArea")?;
9475 let namespace_bindings = root_chart_bindings(xml, b"plotArea", inherited)?;
9476 let direct_roots: Vec<_> = captured_children
9477 .at(0)
9478 .filter_map(|raw| chart_root_local(raw, &namespace_bindings).transpose())
9479 .collect::<Result<_>>()?;
9480 let plot_roots: Vec<_> = direct_roots
9481 .iter()
9482 .filter(|local| is_plot_container(local))
9483 .cloned()
9484 .collect();
9485 let typed_local = (plot_roots.len() == 1
9486 && matches!(
9487 plot_roots[0].as_slice(),
9488 b"barChart"
9489 | b"lineChart"
9490 | b"pieChart"
9491 | b"doughnutChart"
9492 | b"areaChart"
9493 | b"scatterChart"
9494 | b"radarChart"
9495 ))
9496 .then(|| plot_roots[0].clone());
9497
9498 let Some(typed_local) = typed_local else {
9499 return Ok(Self {
9500 raw_attributes,
9501 raw_children: captured_children,
9502 namespace_bindings,
9503 plots: None,
9504 plot_markup: Vec::new(),
9505 axes: Vec::new(),
9506 });
9507 };
9508
9509 let mut raw_children = OrderedRawChildren::default();
9510 let mut plots = Vec::new();
9511 let mut plot_markup = Vec::new();
9512 let mut axes = Vec::new();
9513 let mut boundary = 0usize;
9514 for raw in captured_children.at(0) {
9515 let local = chart_root_local(raw, &namespace_bindings)?;
9516 if local.as_deref() == Some(typed_local.as_slice()) {
9517 let (plot, markup) = parse_plot(raw, &namespace_bindings)?;
9518 plots.push(plot);
9519 plot_markup.push(markup);
9520 boundary = 1;
9521 } else if local.as_deref().and_then(AxisKind::parse).is_some() {
9522 axes.push(Axis::from_xml_with_namespaces(raw, &namespace_bindings)?);
9523 boundary = axes.len() + 1;
9524 } else {
9525 raw_children.push(boundary, raw.to_vec());
9526 }
9527 }
9528 let plot_area = Self {
9529 raw_attributes,
9530 raw_children,
9531 namespace_bindings,
9532 plots: Some(plots),
9533 plot_markup,
9534 axes,
9535 };
9536 plot_area.validate_typed()?;
9537 Ok(plot_area)
9538 }
9539
9540 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
9541 let Some(plots) = &self.plots else {
9542 return write_raw_shell(
9543 writer,
9544 "c:plotArea",
9545 &self.raw_attributes,
9546 &self.raw_children,
9547 );
9548 };
9549 self.validate_typed()?;
9550 let mut start = BytesStart::new("c:plotArea");
9551 push_attributes(&mut start, &self.raw_attributes);
9552 writer
9553 .write_event(Event::Start(start))
9554 .map_err(OxmlError::from)?;
9555 emit_raw(writer, self.raw_children.at(0))?;
9556 for (index, plot) in plots.iter().enumerate() {
9557 write_plot(
9558 writer,
9559 plot,
9560 self.plot_markup
9561 .get(index)
9562 .unwrap_or(&PlotMarkup::default()),
9563 )?;
9564 }
9565 emit_raw(writer, self.raw_children.at(1))?;
9566 for (index, axis) in self.axes.iter().enumerate() {
9567 writer
9568 .get_mut()
9569 .write_all(&axis.to_xml()?)
9570 .map_err(OxmlError::from)?;
9571 emit_raw(writer, self.raw_children.at(index + 2))?;
9572 }
9573 writer
9574 .write_event(Event::End(BytesEnd::new("c:plotArea")))
9575 .map_err(OxmlError::from)?;
9576 Ok(())
9577 }
9578
9579 pub fn raw_children(&self) -> &OrderedRawChildren {
9580 &self.raw_children
9581 }
9582
9583 pub fn new(plots: Vec<Plot>, axes: Vec<Axis>) -> Result<Self> {
9585 let plot_markup = plots
9586 .iter()
9587 .map(|plot| {
9588 let mut markup = PlotMarkup::default();
9589 if matches!(plot, Plot::Line { .. }) {
9590 markup.marker = Some(ScalarMarkup::default());
9591 markup.smooth = Some(ScalarMarkup::default());
9592 }
9593 if matches!(plot, Plot::Doughnut { .. }) {
9594 markup.hole_size = Some(ScalarMarkup::default());
9595 }
9596 markup
9597 })
9598 .collect();
9599 let plot_area = Self {
9600 raw_attributes: Vec::new(),
9601 raw_children: OrderedRawChildren::default(),
9602 namespace_bindings: chart_namespace_defaults(),
9603 plot_markup,
9604 plots: Some(plots),
9605 axes,
9606 };
9607 plot_area.validate_typed()?;
9608 Ok(plot_area)
9609 }
9610
9611 pub fn plots(&self) -> Result<&[Plot]> {
9613 self.plots
9614 .as_deref()
9615 .ok_or_else(|| ChartError::InvalidValue {
9616 element: "c:plotArea".to_owned(),
9617 value: "unsupported or combination plot area is opaque".to_owned(),
9618 })
9619 }
9620
9621 pub fn plots_mut(&mut self) -> Result<&mut [Plot]> {
9623 self.plots
9624 .as_deref_mut()
9625 .ok_or_else(|| ChartError::InvalidValue {
9626 element: "c:plotArea".to_owned(),
9627 value: "cannot combine typed plots with an opaque plot choice".to_owned(),
9628 })
9629 }
9630
9631 pub fn series(&self) -> Result<Vec<Series>> {
9633 if let Some(plots) = &self.plots {
9634 return Ok(plots
9635 .iter()
9636 .flat_map(|plot| match plot {
9637 Plot::Bar { series, .. }
9638 | Plot::Line { series, .. }
9639 | Plot::Pie { series, .. }
9640 | Plot::Doughnut { series, .. }
9641 | Plot::Area { series, .. }
9642 | Plot::Scatter { series, .. }
9643 | Plot::Radar { series, .. } => series.clone(),
9644 })
9645 .collect());
9646 }
9647 let mut series = Vec::new();
9648 for raw in self.raw_children.at(0) {
9649 parse_plot_series(raw, &self.namespace_bindings, &mut series)?;
9650 }
9651 Ok(series)
9652 }
9653
9654 pub fn axes(&self) -> Result<Vec<Axis>> {
9656 if self.plots.is_some() {
9657 self.validate_typed()?;
9658 return Ok(self.axes.clone());
9659 }
9660 let mut axes = Vec::new();
9661 for raw in self.raw_children.at(0) {
9662 if let Some(axis) = parse_plot_axis(raw, &self.namespace_bindings)? {
9663 axes.push(axis);
9664 }
9665 }
9666 validate_axis_pairs(&axes)?;
9667 Ok(axes)
9668 }
9669
9670 fn validate_typed(&self) -> Result<()> {
9671 let plots = self
9672 .plots
9673 .as_ref()
9674 .ok_or_else(|| ChartError::InvalidValue {
9675 element: "c:plotArea".to_owned(),
9676 value: "opaque plot area has no typed validation view".to_owned(),
9677 })?;
9678 if plots.len() != 1 {
9679 return Err(ChartError::InvalidValue {
9680 element: "c:plotArea".to_owned(),
9681 value: "typed plot area requires exactly one plot family".to_owned(),
9682 });
9683 }
9684 validate_axis_pairs(&self.axes)?;
9685 for plot in plots {
9686 plot.validate()?;
9687 if let Some(axis_ids) = plot.axis_ids() {
9688 for id in axis_ids {
9689 if !self.axes.iter().any(|axis| axis.id == id) {
9690 return Err(ChartError::InvalidValue {
9691 element: "c:axId".to_owned(),
9692 value: format!("plot references missing axis {}", id.value()),
9693 });
9694 }
9695 }
9696 } else if !self.axes.is_empty() {
9697 return Err(ChartError::InvalidValue {
9698 element: "c:plotArea".to_owned(),
9699 value: "pie and doughnut plot areas must not own axes".to_owned(),
9700 });
9701 }
9702 if plot.axis_ids().is_some() && self.axes.len() != 2 {
9703 return Err(ChartError::InvalidValue {
9704 element: "c:plotArea".to_owned(),
9705 value: format!(
9706 "axis-owned plot requires exactly 2 axes, found {}",
9707 self.axes.len()
9708 ),
9709 });
9710 }
9711 }
9712 Ok(())
9713 }
9714}
9715
9716impl Default for CT_PlotArea {
9717 fn default() -> Self {
9718 Self {
9719 raw_attributes: Vec::new(),
9720 raw_children: OrderedRawChildren::default(),
9721 namespace_bindings: chart_namespace_defaults(),
9722 plots: Some(Vec::new()),
9723 plot_markup: Vec::new(),
9724 axes: Vec::new(),
9725 }
9726 }
9727}
9728
9729fn chart_root_local(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Vec<u8>>> {
9730 let mut reader = Reader::from_reader(xml);
9731 let mut buffer = Vec::new();
9732 loop {
9733 match reader
9734 .read_event_into(&mut buffer)
9735 .map_err(OxmlError::from)?
9736 {
9737 Event::Start(element) | Event::Empty(element) => {
9738 return Ok(element_is_in_namespace(&element, C_NS, inherited)?
9739 .then(|| local_name(element.name().as_ref()).to_vec()));
9740 }
9741 Event::Eof => return Ok(None),
9742 _ => {}
9743 }
9744 buffer.clear();
9745 }
9746}
9747
9748fn is_plot_container(local: &[u8]) -> bool {
9749 matches!(
9750 local,
9751 b"areaChart"
9752 | b"area3DChart"
9753 | b"barChart"
9754 | b"bar3DChart"
9755 | b"bubbleChart"
9756 | b"doughnutChart"
9757 | b"lineChart"
9758 | b"line3DChart"
9759 | b"ofPieChart"
9760 | b"pieChart"
9761 | b"pie3DChart"
9762 | b"radarChart"
9763 | b"scatterChart"
9764 | b"stockChart"
9765 | b"surfaceChart"
9766 | b"surface3DChart"
9767 )
9768}
9769
9770fn parse_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
9771 let local = chart_root_local(xml, inherited)?
9772 .ok_or_else(|| ChartError::MissingElement("c:barChart or c:lineChart".to_owned()))?;
9773 match local.as_slice() {
9774 b"barChart" => parse_bar_plot(xml, inherited),
9775 b"lineChart" => parse_line_plot(xml, inherited),
9776 b"pieChart" | b"doughnutChart" | b"areaChart" | b"scatterChart" | b"radarChart" => {
9777 parse_remaining_plot(xml, inherited, &local)
9778 }
9779 _ => Err(ChartError::UnexpectedElement(format!(
9780 "c:{}",
9781 String::from_utf8_lossy(&local)
9782 ))),
9783 }
9784}
9785
9786fn parse_remaining_plot(
9787 xml: &[u8],
9788 inherited: &NamespaceBindings,
9789 local: &[u8],
9790) -> Result<(Plot, PlotMarkup)> {
9791 let root = String::from_utf8_lossy(local).into_owned();
9792 let mut reader = Reader::from_reader(xml);
9793 let mut buffer = Vec::new();
9794 loop {
9795 match reader
9796 .read_event_into(&mut buffer)
9797 .map_err(OxmlError::from)?
9798 {
9799 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
9800 let namespaces = typed_rewrite_bindings(&element, inherited)?;
9801 let (mut raw_attributes, namespace_declarations) =
9802 capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
9803 raw_attributes.splice(0..0, namespace_declarations);
9804 let mut grouping = None;
9805 let mut style = None;
9806 let mut first_slice_angle = None;
9807 let mut hole_size = None;
9808 let mut series = Vec::new();
9809 let mut data_labels = None;
9810 let mut axis_ids = Vec::new();
9811 let mut raw_children = OrderedRawChildren::default();
9812 let mut boundary = 0usize;
9813 let mut inner = Vec::new();
9814 loop {
9815 match reader
9816 .read_event_into(&mut inner)
9817 .map_err(OxmlError::from)?
9818 {
9819 Event::Start(child) => {
9820 let name = chart_child_local(&child, &namespaces)?;
9821 let raw = capture_element(&mut reader, &child)?;
9822 parse_remaining_plot_child(
9823 local,
9824 name.as_deref().unwrap_or_default(),
9825 raw,
9826 &namespaces,
9827 &mut grouping,
9828 &mut style,
9829 &mut first_slice_angle,
9830 &mut hole_size,
9831 &mut series,
9832 &mut data_labels,
9833 &mut axis_ids,
9834 &mut raw_children,
9835 &mut boundary,
9836 )?;
9837 }
9838 Event::Empty(child) => {
9839 let name = chart_child_local(&child, &namespaces)?;
9840 let raw = capture_empty_element(&child)?;
9841 parse_remaining_plot_child(
9842 local,
9843 name.as_deref().unwrap_or_default(),
9844 raw,
9845 &namespaces,
9846 &mut grouping,
9847 &mut style,
9848 &mut first_slice_angle,
9849 &mut hole_size,
9850 &mut series,
9851 &mut data_labels,
9852 &mut axis_ids,
9853 &mut raw_children,
9854 &mut boundary,
9855 )?;
9856 }
9857 event @ (Event::Text(_)
9858 | Event::CData(_)
9859 | Event::Comment(_)
9860 | Event::PI(_)
9861 | Event::GeneralRef(_)) => {
9862 raw_children.push(boundary, capture_event(event)?);
9863 }
9864 Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
9865 let needs_axes =
9866 matches!(local, b"areaChart" | b"scatterChart" | b"radarChart");
9867 if needs_axes && axis_ids.len() != 2 {
9868 return Err(ChartError::InvalidValue {
9869 element: format!("c:{root}/c:axId"),
9870 value: format!("expected 2, found {}", axis_ids.len()),
9871 });
9872 }
9873 if !needs_axes && !axis_ids.is_empty() {
9874 return Err(ChartError::InvalidValue {
9875 element: format!("c:{root}/c:axId"),
9876 value: "axis-free plot contains axis references".to_owned(),
9877 });
9878 }
9879 let parsed_ids = needs_axes.then(|| [axis_ids[0].0, axis_ids[1].0]);
9880 let original_series_keys = series
9881 .iter()
9882 .map(|item| (item.index, item.order))
9883 .collect::<Vec<_>>();
9884 let plot = match local {
9885 b"pieChart" => Plot::Pie {
9886 first_slice_angle: first_slice_angle
9887 .as_ref()
9888 .map_or(0, |item| item.0),
9889 series,
9890 data_labels,
9891 },
9892 b"doughnutChart" => Plot::Doughnut {
9893 first_slice_angle: first_slice_angle
9894 .as_ref()
9895 .map_or(0, |item| item.0),
9896 hole_size: hole_size.as_ref().map_or(50, |item| item.0),
9897 series,
9898 data_labels,
9899 },
9900 b"areaChart" => Plot::Area {
9901 grouping: grouping
9902 .as_ref()
9903 .ok_or_else(|| {
9904 ChartError::MissingElement("c:grouping".to_owned())
9905 })?
9906 .0,
9907 series,
9908 data_labels,
9909 axis_ids: parsed_ids.ok_or_else(|| {
9910 ChartError::MissingElement("c:areaChart/c:axId".to_owned())
9911 })?,
9912 },
9913 b"scatterChart" => {
9914 let lexical = style
9915 .as_ref()
9916 .ok_or_else(|| {
9917 ChartError::MissingElement("c:scatterStyle".to_owned())
9918 })?
9919 .0
9920 .as_str();
9921 Plot::Scatter {
9922 style: ScatterStyle::parse(lexical).ok_or_else(|| {
9923 invalid_attribute(
9924 "scatterStyle",
9925 "val",
9926 lexical.to_owned(),
9927 )
9928 })?,
9929 series,
9930 data_labels,
9931 axis_ids: parsed_ids.ok_or_else(|| {
9932 ChartError::MissingElement(
9933 "c:scatterChart/c:axId".to_owned(),
9934 )
9935 })?,
9936 }
9937 }
9938 b"radarChart" => {
9939 let lexical = style
9940 .as_ref()
9941 .ok_or_else(|| {
9942 ChartError::MissingElement("c:radarStyle".to_owned())
9943 })?
9944 .0
9945 .as_str();
9946 Plot::Radar {
9947 style: RadarStyle::parse(lexical).ok_or_else(|| {
9948 invalid_attribute(
9949 "radarStyle",
9950 "val",
9951 lexical.to_owned(),
9952 )
9953 })?,
9954 series,
9955 data_labels,
9956 axis_ids: parsed_ids.ok_or_else(|| {
9957 ChartError::MissingElement(
9958 "c:radarChart/c:axId".to_owned(),
9959 )
9960 })?,
9961 }
9962 }
9963 _ => {
9964 return Err(ChartError::UnexpectedElement(format!(
9965 "c:{}",
9966 String::from_utf8_lossy(local)
9967 )));
9968 }
9969 };
9970 plot.validate()?;
9971 let final_boundary = boundary.max(original_series_keys.len() + 6);
9972 return Ok((
9973 plot,
9974 PlotMarkup {
9975 raw_attributes,
9976 raw_children: raw_children_in_schema_order(
9977 &raw_children,
9978 final_boundary,
9979 ),
9980 grouping: grouping.map(|item| item.1),
9981 first_slice_angle: first_slice_angle.map(|item| item.1),
9982 hole_size: hole_size.map(|item| item.1),
9983 style: style.map(|item| item.1),
9984 axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
9985 original_series_keys,
9986 original_axis_ids: parsed_ids
9987 .map_or_else(Vec::new, |ids| ids.to_vec()),
9988 parsed_remaining: match local {
9989 b"pieChart" => Some("pieChart"),
9990 b"doughnutChart" => Some("doughnutChart"),
9991 b"areaChart" => Some("areaChart"),
9992 b"scatterChart" => Some("scatterChart"),
9993 b"radarChart" => Some("radarChart"),
9994 _ => None,
9995 },
9996 ..PlotMarkup::default()
9997 },
9998 ));
9999 }
10000 Event::Eof => return Err(missing_end(&format!("c:{root}"))),
10001 _ => {}
10002 }
10003 inner.clear();
10004 }
10005 }
10006 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
10007 return Err(ChartError::MissingElement("c:ser".to_owned()));
10008 }
10009 Event::Start(element) | Event::Empty(element) => {
10010 return Err(ChartError::UnexpectedElement(element_name(&element)));
10011 }
10012 Event::Eof => return Err(ChartError::MissingElement(format!("c:{root}"))),
10013 _ => {}
10014 }
10015 buffer.clear();
10016 }
10017}
10018
10019#[allow(clippy::too_many_arguments)]
10020fn parse_remaining_plot_child(
10021 root: &[u8],
10022 name: &[u8],
10023 raw: Vec<u8>,
10024 namespaces: &NamespaceBindings,
10025 grouping: &mut Option<(Grouping, ScalarMarkup)>,
10026 style: &mut Option<(String, ScalarMarkup)>,
10027 first_slice_angle: &mut Option<(u16, ScalarMarkup)>,
10028 hole_size: &mut Option<(u8, ScalarMarkup)>,
10029 series: &mut Vec<Series>,
10030 data_labels: &mut Option<CT_DLbls>,
10031 axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
10032 raw_children: &mut OrderedRawChildren,
10033 boundary: &mut usize,
10034) -> Result<()> {
10035 let property_first = matches!(root, b"areaChart" | b"scatterChart" | b"radarChart");
10036 let base = usize::from(property_first);
10037 match name {
10038 b"grouping" if root == b"areaChart" => {
10039 let (value, markup) = required_scalar(&raw, "grouping")?;
10040 let parsed = Grouping::parse(&value)
10041 .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
10042 set_once(grouping, (parsed, markup), "c:grouping")?;
10043 *boundary = (*boundary).max(1);
10044 }
10045 b"scatterStyle" if root == b"scatterChart" => {
10046 let (value, markup) = required_scalar(&raw, "scatterStyle")?;
10047 if ScatterStyle::parse(&value).is_none() {
10048 return Err(invalid_attribute("scatterStyle", "val", value));
10049 }
10050 set_once(style, (value, markup), "c:scatterStyle")?;
10051 *boundary = (*boundary).max(1);
10052 }
10053 b"radarStyle" if root == b"radarChart" => {
10054 let (value, markup) = required_scalar(&raw, "radarStyle")?;
10055 if RadarStyle::parse(&value).is_none() {
10056 return Err(invalid_attribute("radarStyle", "val", value));
10057 }
10058 set_once(style, (value, markup), "c:radarStyle")?;
10059 *boundary = (*boundary).max(1);
10060 }
10061 b"ser" => {
10062 series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
10063 *boundary = (*boundary).max(base + series.len());
10064 }
10065 b"dLbls" => {
10066 set_once(
10067 data_labels,
10068 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
10069 "c:dLbls",
10070 )?;
10071 *boundary = (*boundary).max(base + series.len() + 1);
10072 }
10073 b"firstSliceAng" if matches!(root, b"pieChart" | b"doughnutChart") => {
10074 let (value, markup) = required_scalar(&raw, "firstSliceAng")?;
10075 let parsed = value
10076 .parse::<u16>()
10077 .map_err(|_| invalid_attribute("firstSliceAng", "val", value.clone()))?;
10078 if parsed > 360 {
10079 return Err(invalid_attribute("firstSliceAng", "val", value));
10080 }
10081 set_once(first_slice_angle, (parsed, markup), "c:firstSliceAng")?;
10082 *boundary = (*boundary).max(series.len() + 2);
10083 }
10084 b"holeSize" if root == b"doughnutChart" => {
10085 let (value, markup) = required_scalar(&raw, "holeSize")?;
10086 let parsed = value
10087 .parse::<u8>()
10088 .map_err(|_| invalid_attribute("holeSize", "val", value.clone()))?;
10089 if !(10..=90).contains(&parsed) {
10090 return Err(invalid_attribute("holeSize", "val", value));
10091 }
10092 set_once(hole_size, (parsed, markup), "c:holeSize")?;
10093 *boundary = (*boundary).max(series.len() + 3);
10094 }
10095 b"axId" if property_first => {
10096 if axis_ids.len() == 2 {
10097 return Err(ChartError::DuplicateElement("c:axId".to_owned()));
10098 }
10099 axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
10100 *boundary = (*boundary).max(base + series.len() + 1 + axis_ids.len());
10101 }
10102 b"axId" => {
10103 return Err(ChartError::InvalidValue {
10104 element: "c:axId".to_owned(),
10105 value: "axis-free plot contains axis reference".to_owned(),
10106 });
10107 }
10108 _ => {
10109 let raw_boundary = match (root, name) {
10110 (_, b"varyColors") => base,
10111 (b"pieChart", b"extLst") => series.len() + 2,
10112 (b"doughnutChart", b"extLst") => series.len() + 3,
10113 (b"areaChart" | b"scatterChart" | b"radarChart", b"dropLines") => {
10114 base + series.len() + 1
10115 }
10116 (b"areaChart" | b"scatterChart" | b"radarChart", b"extLst") => {
10117 base + series.len() + 3
10118 }
10119 _ => *boundary,
10120 };
10121 raw_children.push(raw_boundary, raw);
10122 *boundary = (*boundary).max(raw_boundary);
10123 }
10124 }
10125 Ok(())
10126}
10127
10128fn parse_bar_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
10129 let mut reader = Reader::from_reader(xml);
10130 let mut buffer = Vec::new();
10131 loop {
10132 match reader
10133 .read_event_into(&mut buffer)
10134 .map_err(OxmlError::from)?
10135 {
10136 Event::Start(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
10137 let namespaces = typed_rewrite_bindings(&element, inherited)?;
10138 let (mut raw_attributes, namespace_declarations) =
10139 capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
10140 raw_attributes.splice(0..0, namespace_declarations);
10141 let mut direction = None;
10142 let mut grouping = None;
10143 let mut gap_width = None;
10144 let mut overlap = None;
10145 let mut series = Vec::new();
10146 let mut data_labels = None;
10147 let mut axis_ids = Vec::new();
10148 let mut raw_children = OrderedRawChildren::default();
10149 let mut boundary = 0usize;
10150 let mut inner = Vec::new();
10151 loop {
10152 match reader
10153 .read_event_into(&mut inner)
10154 .map_err(OxmlError::from)?
10155 {
10156 Event::Start(child) => {
10157 let name = chart_child_local(&child, &namespaces)?;
10158 let raw = capture_element(&mut reader, &child)?;
10159 parse_bar_child(
10160 name.as_deref().unwrap_or_default(),
10161 raw,
10162 &namespaces,
10163 &mut direction,
10164 &mut grouping,
10165 &mut gap_width,
10166 &mut overlap,
10167 &mut series,
10168 &mut data_labels,
10169 &mut axis_ids,
10170 &mut raw_children,
10171 &mut boundary,
10172 )?;
10173 }
10174 Event::Empty(child) => {
10175 let name = chart_child_local(&child, &namespaces)?;
10176 let raw = capture_empty_element(&child)?;
10177 parse_bar_child(
10178 name.as_deref().unwrap_or_default(),
10179 raw,
10180 &namespaces,
10181 &mut direction,
10182 &mut grouping,
10183 &mut gap_width,
10184 &mut overlap,
10185 &mut series,
10186 &mut data_labels,
10187 &mut axis_ids,
10188 &mut raw_children,
10189 &mut boundary,
10190 )?;
10191 }
10192 event @ (Event::Text(_)
10193 | Event::CData(_)
10194 | Event::Comment(_)
10195 | Event::PI(_)
10196 | Event::GeneralRef(_)) => {
10197 raw_children.push(boundary, capture_event(event)?);
10198 }
10199 Event::End(end) if matches_local_name(end.name().as_ref(), b"barChart") => {
10200 let (direction, direction_markup) = direction
10201 .ok_or_else(|| ChartError::MissingElement("c:barDir".to_owned()))?;
10202 let (grouping, grouping_markup) = grouping.ok_or_else(|| {
10203 ChartError::MissingElement("c:grouping".to_owned())
10204 })?;
10205 if axis_ids.len() != 2 {
10206 return Err(ChartError::InvalidValue {
10207 element: "c:barChart/c:axId".to_owned(),
10208 value: format!("expected 2, found {}", axis_ids.len()),
10209 });
10210 }
10211 let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
10212 let original_series_keys: Vec<(u32, u32)> =
10213 series.iter().map(|item| (item.index, item.order)).collect();
10214 let plot = Plot::Bar {
10215 direction,
10216 grouping,
10217 gap_width: gap_width.as_ref().map_or(150, |item| item.0),
10218 overlap: overlap.as_ref().map_or(0, |item| item.0),
10219 series,
10220 data_labels,
10221 axis_ids: parsed_ids,
10222 };
10223 plot.validate()?;
10224 return Ok((
10225 plot,
10226 PlotMarkup {
10227 raw_attributes,
10228 raw_children: raw_children_in_schema_order(
10229 &raw_children,
10230 original_series_keys.len() + 7,
10231 ),
10232 direction: Some(direction_markup),
10233 grouping: Some(grouping_markup),
10234 gap_width: gap_width.map(|item| item.1),
10235 overlap: overlap.map(|item| item.1),
10236 marker: None,
10237 smooth: None,
10238 first_slice_angle: None,
10239 hole_size: None,
10240 style: None,
10241 axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
10242 original_series_keys,
10243 original_axis_ids: parsed_ids.to_vec(),
10244 parsed_bar: Some(true),
10245 parsed_remaining: None,
10246 },
10247 ));
10248 }
10249 Event::Eof => return Err(missing_end("c:barChart")),
10250 _ => {}
10251 }
10252 inner.clear();
10253 }
10254 }
10255 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
10256 return Err(ChartError::MissingElement("c:barDir".to_owned()));
10257 }
10258 Event::Start(element) | Event::Empty(element) => {
10259 return Err(ChartError::UnexpectedElement(element_name(&element)));
10260 }
10261 Event::Eof => return Err(ChartError::MissingElement("c:barChart".to_owned())),
10262 _ => {}
10263 }
10264 buffer.clear();
10265 }
10266}
10267
10268#[allow(clippy::too_many_arguments)]
10269fn parse_bar_child(
10270 name: &[u8],
10271 raw: Vec<u8>,
10272 namespaces: &NamespaceBindings,
10273 direction: &mut Option<(BarDirection, ScalarMarkup)>,
10274 grouping: &mut Option<(BarGrouping, ScalarMarkup)>,
10275 gap_width: &mut Option<(u16, ScalarMarkup)>,
10276 overlap: &mut Option<(i8, ScalarMarkup)>,
10277 series: &mut Vec<Series>,
10278 data_labels: &mut Option<CT_DLbls>,
10279 axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
10280 raw_children: &mut OrderedRawChildren,
10281 boundary: &mut usize,
10282) -> Result<()> {
10283 match name {
10284 b"barDir" => {
10285 let (value, markup) = required_scalar(&raw, "barDir")?;
10286 let value = BarDirection::parse(&value)
10287 .ok_or_else(|| invalid_attribute("barDir", "val", value))?;
10288 set_once(direction, (value, markup), "c:barDir")?;
10289 *boundary = (*boundary).max(1);
10290 }
10291 b"grouping" => {
10292 let (value, markup) = required_scalar(&raw, "grouping")?;
10293 let value = BarGrouping::parse(&value)
10294 .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
10295 set_once(grouping, (value, markup), "c:grouping")?;
10296 *boundary = (*boundary).max(2);
10297 }
10298 b"ser" => {
10299 series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
10300 *boundary = (*boundary).max(series.len() + 2);
10301 }
10302 b"dLbls" => {
10303 set_once(
10304 data_labels,
10305 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
10306 "c:dLbls",
10307 )?;
10308 *boundary = (*boundary).max(series.len() + 3);
10309 }
10310 b"gapWidth" => {
10311 let (value, markup) = required_scalar(&raw, "gapWidth")?;
10312 let parsed = value
10313 .parse::<u16>()
10314 .map_err(|_| invalid_attribute("gapWidth", "val", value.clone()))?;
10315 if parsed > 500 {
10316 return Err(invalid_attribute("gapWidth", "val", value));
10317 }
10318 set_once(gap_width, (parsed, markup), "c:gapWidth")?;
10319 *boundary = (*boundary).max(series.len() + 4);
10320 }
10321 b"overlap" => {
10322 let (value, markup) = required_scalar(&raw, "overlap")?;
10323 let parsed = value
10324 .parse::<i8>()
10325 .map_err(|_| invalid_attribute("overlap", "val", value.clone()))?;
10326 if !(-100..=100).contains(&parsed) {
10327 return Err(invalid_attribute("overlap", "val", value));
10328 }
10329 set_once(overlap, (parsed, markup), "c:overlap")?;
10330 *boundary = (*boundary).max(series.len() + 5);
10331 }
10332 b"axId" => {
10333 if axis_ids.len() == 2 {
10334 return Err(ChartError::DuplicateElement("c:axId".to_owned()));
10335 }
10336 axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
10337 *boundary = (*boundary).max(series.len() + 5 + axis_ids.len());
10338 }
10339 _ => raw_children.push(*boundary, raw),
10340 }
10341 Ok(())
10342}
10343
10344fn parse_line_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
10345 let mut reader = Reader::from_reader(xml);
10346 let mut buffer = Vec::new();
10347 loop {
10348 match reader
10349 .read_event_into(&mut buffer)
10350 .map_err(OxmlError::from)?
10351 {
10352 Event::Start(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
10353 let namespaces = typed_rewrite_bindings(&element, inherited)?;
10354 let (mut raw_attributes, namespace_declarations) =
10355 capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
10356 raw_attributes.splice(0..0, namespace_declarations);
10357 let mut grouping = None;
10358 let mut marker = None;
10359 let mut smooth = None;
10360 let mut series = Vec::new();
10361 let mut data_labels = None;
10362 let mut axis_ids = Vec::new();
10363 let mut raw_children = OrderedRawChildren::default();
10364 let mut boundary = 0usize;
10365 let mut inner = Vec::new();
10366 loop {
10367 match reader
10368 .read_event_into(&mut inner)
10369 .map_err(OxmlError::from)?
10370 {
10371 Event::Start(child) => {
10372 let name = chart_child_local(&child, &namespaces)?;
10373 let raw = capture_element(&mut reader, &child)?;
10374 parse_line_child(
10375 name.as_deref().unwrap_or_default(),
10376 raw,
10377 &namespaces,
10378 &mut grouping,
10379 &mut marker,
10380 &mut smooth,
10381 &mut series,
10382 &mut data_labels,
10383 &mut axis_ids,
10384 &mut raw_children,
10385 &mut boundary,
10386 )?;
10387 }
10388 Event::Empty(child) => {
10389 let name = chart_child_local(&child, &namespaces)?;
10390 let raw = capture_empty_element(&child)?;
10391 parse_line_child(
10392 name.as_deref().unwrap_or_default(),
10393 raw,
10394 &namespaces,
10395 &mut grouping,
10396 &mut marker,
10397 &mut smooth,
10398 &mut series,
10399 &mut data_labels,
10400 &mut axis_ids,
10401 &mut raw_children,
10402 &mut boundary,
10403 )?;
10404 }
10405 event @ (Event::Text(_)
10406 | Event::CData(_)
10407 | Event::Comment(_)
10408 | Event::PI(_)
10409 | Event::GeneralRef(_)) => {
10410 raw_children.push(boundary, capture_event(event)?);
10411 }
10412 Event::End(end)
10413 if matches_local_name(end.name().as_ref(), b"lineChart") =>
10414 {
10415 let (grouping, grouping_markup) = grouping.ok_or_else(|| {
10416 ChartError::MissingElement("c:grouping".to_owned())
10417 })?;
10418 if axis_ids.len() != 2 {
10419 return Err(ChartError::InvalidValue {
10420 element: "c:lineChart/c:axId".to_owned(),
10421 value: format!("expected 2, found {}", axis_ids.len()),
10422 });
10423 }
10424 let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
10425 let original_series_keys: Vec<(u32, u32)> =
10426 series.iter().map(|item| (item.index, item.order)).collect();
10427 let plot = Plot::Line {
10428 grouping,
10429 marker: marker.as_ref().is_some_and(|item| item.0),
10430 smooth: smooth.as_ref().is_some_and(|item| item.0),
10431 series,
10432 data_labels,
10433 axis_ids: parsed_ids,
10434 };
10435 plot.validate()?;
10436 return Ok((
10437 plot,
10438 PlotMarkup {
10439 raw_attributes,
10440 raw_children: raw_children_in_schema_order(
10441 &raw_children,
10442 original_series_keys.len() + 6,
10443 ),
10444 direction: None,
10445 grouping: Some(grouping_markup),
10446 gap_width: None,
10447 overlap: None,
10448 marker: marker.map(|item| item.1),
10449 smooth: smooth.map(|item| item.1),
10450 first_slice_angle: None,
10451 hole_size: None,
10452 style: None,
10453 axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
10454 original_series_keys,
10455 original_axis_ids: parsed_ids.to_vec(),
10456 parsed_bar: Some(false),
10457 parsed_remaining: None,
10458 },
10459 ));
10460 }
10461 Event::Eof => return Err(missing_end("c:lineChart")),
10462 _ => {}
10463 }
10464 inner.clear();
10465 }
10466 }
10467 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
10468 return Err(ChartError::MissingElement("c:grouping".to_owned()));
10469 }
10470 Event::Start(element) | Event::Empty(element) => {
10471 return Err(ChartError::UnexpectedElement(element_name(&element)));
10472 }
10473 Event::Eof => return Err(ChartError::MissingElement("c:lineChart".to_owned())),
10474 _ => {}
10475 }
10476 buffer.clear();
10477 }
10478}
10479
10480#[allow(clippy::too_many_arguments)]
10481fn parse_line_child(
10482 name: &[u8],
10483 raw: Vec<u8>,
10484 namespaces: &NamespaceBindings,
10485 grouping: &mut Option<(Grouping, ScalarMarkup)>,
10486 marker: &mut Option<(bool, ScalarMarkup)>,
10487 smooth: &mut Option<(bool, ScalarMarkup)>,
10488 series: &mut Vec<Series>,
10489 data_labels: &mut Option<CT_DLbls>,
10490 axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
10491 raw_children: &mut OrderedRawChildren,
10492 boundary: &mut usize,
10493) -> Result<()> {
10494 match name {
10495 b"grouping" => {
10496 let (value, markup) = required_scalar(&raw, "grouping")?;
10497 let value = Grouping::parse(&value)
10498 .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
10499 set_once(grouping, (value, markup), "c:grouping")?;
10500 *boundary = (*boundary).max(1);
10501 }
10502 b"ser" => {
10503 series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
10504 *boundary = (*boundary).max(series.len() + 1);
10505 }
10506 b"dLbls" => {
10507 set_once(
10508 data_labels,
10509 CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
10510 "c:dLbls",
10511 )?;
10512 *boundary = (*boundary).max(series.len() + 2);
10513 }
10514 b"marker" => {
10515 set_once(marker, parse_bool_value(&raw, "marker")?, "c:marker")?;
10516 *boundary = (*boundary).max(series.len() + 3);
10517 }
10518 b"smooth" => {
10519 set_once(smooth, parse_bool_value(&raw, "smooth")?, "c:smooth")?;
10520 *boundary = (*boundary).max(series.len() + 4);
10521 }
10522 b"axId" => {
10523 if axis_ids.len() == 2 {
10524 return Err(ChartError::DuplicateElement("c:axId".to_owned()));
10525 }
10526 axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
10527 *boundary = (*boundary).max(series.len() + 4 + axis_ids.len());
10528 }
10529 _ => raw_children.push(*boundary, raw),
10530 }
10531 Ok(())
10532}
10533
10534fn required_scalar(xml: &[u8], local: &str) -> Result<(String, ScalarMarkup)> {
10535 let (value, markup) = scalar_value(xml, local)?;
10536 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
10537 Ok((value, markup))
10538}
10539
10540fn write_plot<W: Write>(writer: &mut Writer<W>, plot: &Plot, markup: &PlotMarkup) -> Result<()> {
10541 plot.validate()?;
10542 if let Some(parsed_bar) = markup.parsed_bar
10543 && parsed_bar != matches!(plot, Plot::Bar { .. })
10544 {
10545 return Err(ChartError::InvalidValue {
10546 element: "c:plotArea".to_owned(),
10547 value: "a parsed plot family cannot be replaced while preserved payload remains"
10548 .to_owned(),
10549 });
10550 }
10551 let current_remaining = match plot {
10552 Plot::Pie { .. } => Some("pieChart"),
10553 Plot::Doughnut { .. } => Some("doughnutChart"),
10554 Plot::Area { .. } => Some("areaChart"),
10555 Plot::Scatter { .. } => Some("scatterChart"),
10556 Plot::Radar { .. } => Some("radarChart"),
10557 Plot::Bar { .. } | Plot::Line { .. } => None,
10558 };
10559 if let Some(parsed) = markup.parsed_remaining
10560 && current_remaining != Some(parsed)
10561 {
10562 return Err(ChartError::InvalidValue {
10563 element: "c:plotArea".to_owned(),
10564 value: "a parsed plot family cannot be replaced while preserved payload remains"
10565 .to_owned(),
10566 });
10567 }
10568 match plot {
10569 Plot::Bar {
10570 direction,
10571 grouping,
10572 gap_width,
10573 overlap,
10574 series,
10575 data_labels,
10576 axis_ids,
10577 } => {
10578 let mut start = BytesStart::new("c:barChart");
10579 push_attributes(&mut start, &markup.raw_attributes);
10580 writer
10581 .write_event(Event::Start(start))
10582 .map_err(OxmlError::from)?;
10583 emit_raw(writer, markup.raw_children.at(0))?;
10584 write_scalar(
10585 writer,
10586 "c:barDir",
10587 direction.as_str(),
10588 markup.direction.as_ref(),
10589 )?;
10590 emit_raw(writer, markup.raw_children.at(1))?;
10591 write_scalar(
10592 writer,
10593 "c:grouping",
10594 grouping.as_str(),
10595 markup.grouping.as_ref(),
10596 )?;
10597 let original_to_current =
10598 series_original_to_current(&markup.original_series_keys, series);
10599 emit_repeated_raw(
10600 writer,
10601 &markup.raw_children,
10602 2,
10603 &original_to_current,
10604 series.len(),
10605 0,
10606 )?;
10607 for (index, item) in series.iter().enumerate() {
10608 writer
10609 .get_mut()
10610 .write_all(&item.to_xml()?)
10611 .map_err(OxmlError::from)?;
10612 emit_repeated_raw(
10613 writer,
10614 &markup.raw_children,
10615 2,
10616 &original_to_current,
10617 series.len(),
10618 index + 1,
10619 )?;
10620 }
10621 if let Some(labels) = data_labels {
10622 writer
10623 .get_mut()
10624 .write_all(&labels.to_xml()?)
10625 .map_err(OxmlError::from)?;
10626 }
10627 let trailing = markup.original_series_keys.len();
10628 emit_raw(writer, markup.raw_children.at(trailing + 3))?;
10629 if *gap_width != 150 || markup.gap_width.is_some() {
10630 write_scalar(
10631 writer,
10632 "c:gapWidth",
10633 &gap_width.to_string(),
10634 markup.gap_width.as_ref(),
10635 )?;
10636 }
10637 emit_raw(writer, markup.raw_children.at(trailing + 4))?;
10638 if *overlap != 0 || markup.overlap.is_some() {
10639 write_scalar(
10640 writer,
10641 "c:overlap",
10642 &overlap.to_string(),
10643 markup.overlap.as_ref(),
10644 )?;
10645 }
10646 let axis_original_to_current =
10647 axis_original_to_current(&markup.original_axis_ids, axis_ids);
10648 let axis_current_to_original =
10649 invert_original_to_current(&axis_original_to_current, axis_ids.len());
10650 emit_repeated_raw(
10651 writer,
10652 &markup.raw_children,
10653 trailing + 5,
10654 &axis_original_to_current,
10655 axis_ids.len(),
10656 0,
10657 )?;
10658 for (index, id) in axis_ids.iter().enumerate() {
10659 let default_markup = default_axis_id_markup(*id);
10660 let original_markup = axis_current_to_original[index]
10661 .and_then(|original| markup.axis_ids.get(original));
10662 write_axis_id(
10663 writer,
10664 "c:axId",
10665 *id,
10666 original_markup.unwrap_or(&default_markup),
10667 )?;
10668 emit_repeated_raw(
10669 writer,
10670 &markup.raw_children,
10671 trailing + 5,
10672 &axis_original_to_current,
10673 axis_ids.len(),
10674 index + 1,
10675 )?;
10676 }
10677 writer
10678 .write_event(Event::End(BytesEnd::new("c:barChart")))
10679 .map_err(OxmlError::from)?;
10680 }
10681 Plot::Line {
10682 grouping,
10683 marker,
10684 smooth,
10685 series,
10686 data_labels,
10687 axis_ids,
10688 } => {
10689 let mut start = BytesStart::new("c:lineChart");
10690 push_attributes(&mut start, &markup.raw_attributes);
10691 writer
10692 .write_event(Event::Start(start))
10693 .map_err(OxmlError::from)?;
10694 emit_raw(writer, markup.raw_children.at(0))?;
10695 write_scalar(
10696 writer,
10697 "c:grouping",
10698 grouping.as_str(),
10699 markup.grouping.as_ref(),
10700 )?;
10701 let original_to_current =
10702 series_original_to_current(&markup.original_series_keys, series);
10703 emit_repeated_raw(
10704 writer,
10705 &markup.raw_children,
10706 1,
10707 &original_to_current,
10708 series.len(),
10709 0,
10710 )?;
10711 for (index, item) in series.iter().enumerate() {
10712 writer
10713 .get_mut()
10714 .write_all(&item.to_xml()?)
10715 .map_err(OxmlError::from)?;
10716 emit_repeated_raw(
10717 writer,
10718 &markup.raw_children,
10719 1,
10720 &original_to_current,
10721 series.len(),
10722 index + 1,
10723 )?;
10724 }
10725 if let Some(labels) = data_labels {
10726 writer
10727 .get_mut()
10728 .write_all(&labels.to_xml()?)
10729 .map_err(OxmlError::from)?;
10730 }
10731 let trailing = markup.original_series_keys.len();
10732 emit_raw(writer, markup.raw_children.at(trailing + 2))?;
10733 if *marker || markup.marker.is_some() {
10734 write_scalar(
10735 writer,
10736 "c:marker",
10737 bool_lexical(*marker),
10738 markup.marker.as_ref(),
10739 )?;
10740 }
10741 emit_raw(writer, markup.raw_children.at(trailing + 3))?;
10742 if *smooth || markup.smooth.is_some() {
10743 write_scalar(
10744 writer,
10745 "c:smooth",
10746 bool_lexical(*smooth),
10747 markup.smooth.as_ref(),
10748 )?;
10749 }
10750 let axis_original_to_current =
10751 axis_original_to_current(&markup.original_axis_ids, axis_ids);
10752 let axis_current_to_original =
10753 invert_original_to_current(&axis_original_to_current, axis_ids.len());
10754 emit_repeated_raw(
10755 writer,
10756 &markup.raw_children,
10757 trailing + 4,
10758 &axis_original_to_current,
10759 axis_ids.len(),
10760 0,
10761 )?;
10762 for (index, id) in axis_ids.iter().enumerate() {
10763 let default_markup = default_axis_id_markup(*id);
10764 let original_markup = axis_current_to_original[index]
10765 .and_then(|original| markup.axis_ids.get(original));
10766 write_axis_id(
10767 writer,
10768 "c:axId",
10769 *id,
10770 original_markup.unwrap_or(&default_markup),
10771 )?;
10772 emit_repeated_raw(
10773 writer,
10774 &markup.raw_children,
10775 trailing + 4,
10776 &axis_original_to_current,
10777 axis_ids.len(),
10778 index + 1,
10779 )?;
10780 }
10781 writer
10782 .write_event(Event::End(BytesEnd::new("c:lineChart")))
10783 .map_err(OxmlError::from)?;
10784 }
10785 remaining => {
10786 let (
10787 root,
10788 property,
10789 series,
10790 data_labels,
10791 axis_ids,
10792 first_slice_angle,
10793 hole_size,
10794 scatter,
10795 ) = match remaining {
10796 Plot::Pie {
10797 first_slice_angle,
10798 series,
10799 data_labels,
10800 } => (
10801 "pieChart",
10802 None,
10803 series,
10804 data_labels,
10805 None,
10806 Some(*first_slice_angle),
10807 None,
10808 false,
10809 ),
10810 Plot::Doughnut {
10811 first_slice_angle,
10812 hole_size,
10813 series,
10814 data_labels,
10815 } => (
10816 "doughnutChart",
10817 None,
10818 series,
10819 data_labels,
10820 None,
10821 Some(*first_slice_angle),
10822 Some(*hole_size),
10823 false,
10824 ),
10825 Plot::Area {
10826 grouping,
10827 series,
10828 data_labels,
10829 axis_ids,
10830 } => (
10831 "areaChart",
10832 Some(("c:grouping", grouping.as_str(), markup.grouping.as_ref())),
10833 series,
10834 data_labels,
10835 Some(axis_ids),
10836 None,
10837 None,
10838 false,
10839 ),
10840 Plot::Scatter {
10841 style,
10842 series,
10843 data_labels,
10844 axis_ids,
10845 } => (
10846 "scatterChart",
10847 Some(("c:scatterStyle", style.as_str(), markup.style.as_ref())),
10848 series,
10849 data_labels,
10850 Some(axis_ids),
10851 None,
10852 None,
10853 true,
10854 ),
10855 Plot::Radar {
10856 style,
10857 series,
10858 data_labels,
10859 axis_ids,
10860 } => (
10861 "radarChart",
10862 Some(("c:radarStyle", style.as_str(), markup.style.as_ref())),
10863 series,
10864 data_labels,
10865 Some(axis_ids),
10866 None,
10867 None,
10868 false,
10869 ),
10870 Plot::Bar { .. } | Plot::Line { .. } => unreachable!("handled above"),
10871 };
10872 let tag = format!("c:{root}");
10873 let mut start = BytesStart::new(&tag);
10874 push_attributes(&mut start, &markup.raw_attributes);
10875 writer
10876 .write_event(Event::Start(start))
10877 .map_err(OxmlError::from)?;
10878 let base = usize::from(property.is_some());
10879 if let Some((name, value, scalar_markup)) = property {
10880 emit_raw(writer, markup.raw_children.at(0))?;
10881 write_scalar(writer, name, value, scalar_markup)?;
10882 }
10883 let original_to_current =
10884 series_original_to_current(&markup.original_series_keys, series);
10885 emit_repeated_raw(
10886 writer,
10887 &markup.raw_children,
10888 base,
10889 &original_to_current,
10890 series.len(),
10891 0,
10892 )?;
10893 for (index, item) in series.iter().enumerate() {
10894 writer
10895 .get_mut()
10896 .write_all(&item.to_xml_for_plot(scatter)?)
10897 .map_err(OxmlError::from)?;
10898 emit_repeated_raw(
10899 writer,
10900 &markup.raw_children,
10901 base,
10902 &original_to_current,
10903 series.len(),
10904 index + 1,
10905 )?;
10906 }
10907 if let Some(labels) = data_labels {
10908 writer
10909 .get_mut()
10910 .write_all(&labels.to_xml()?)
10911 .map_err(OxmlError::from)?;
10912 }
10913 let trailing = base + markup.original_series_keys.len();
10914 if let Some(axis_ids) = axis_ids {
10915 let axis_original_to_current =
10916 axis_original_to_current(&markup.original_axis_ids, axis_ids);
10917 let axis_current_to_original =
10918 invert_original_to_current(&axis_original_to_current, axis_ids.len());
10919 emit_repeated_raw(
10920 writer,
10921 &markup.raw_children,
10922 trailing + 1,
10923 &axis_original_to_current,
10924 axis_ids.len(),
10925 0,
10926 )?;
10927 for (index, id) in axis_ids.iter().enumerate() {
10928 let default_markup = default_axis_id_markup(*id);
10929 let original_markup = axis_current_to_original[index]
10930 .and_then(|original| markup.axis_ids.get(original));
10931 write_axis_id(
10932 writer,
10933 "c:axId",
10934 *id,
10935 original_markup.unwrap_or(&default_markup),
10936 )?;
10937 emit_repeated_raw(
10938 writer,
10939 &markup.raw_children,
10940 trailing + 1,
10941 &axis_original_to_current,
10942 axis_ids.len(),
10943 index + 1,
10944 )?;
10945 }
10946 } else {
10947 emit_raw(writer, markup.raw_children.at(trailing + 1))?;
10948 if let Some(angle) = first_slice_angle {
10949 if angle != 0 || markup.first_slice_angle.is_some() {
10950 write_scalar(
10951 writer,
10952 "c:firstSliceAng",
10953 &angle.to_string(),
10954 markup.first_slice_angle.as_ref(),
10955 )?;
10956 }
10957 emit_raw(writer, markup.raw_children.at(trailing + 2))?;
10958 }
10959 if let Some(size) = hole_size {
10960 if size != 50 || markup.hole_size.is_some() {
10961 write_scalar(
10962 writer,
10963 "c:holeSize",
10964 &size.to_string(),
10965 markup.hole_size.as_ref(),
10966 )?;
10967 }
10968 emit_raw(writer, markup.raw_children.at(trailing + 3))?;
10969 }
10970 }
10971 writer
10972 .write_event(Event::End(BytesEnd::new(&tag)))
10973 .map_err(OxmlError::from)?;
10974 }
10975 }
10976 Ok(())
10977}
10978
10979fn series_original_to_current(original: &[(u32, u32)], current: &[Series]) -> Vec<Option<usize>> {
10980 let mut used = vec![false; current.len()];
10981 let mut matches = vec![None; original.len()];
10982 for original_index in 0..original.len().min(current.len()) {
10983 if original[original_index]
10984 == (current[original_index].index, current[original_index].order)
10985 {
10986 matches[original_index] = Some(original_index);
10987 used[original_index] = true;
10988 }
10989 }
10990 for (original_index, key) in original.iter().enumerate() {
10991 if matches[original_index].is_some() {
10992 continue;
10993 }
10994 if let Some(current_index) = current
10995 .iter()
10996 .enumerate()
10997 .position(|(index, item)| !used[index] && (item.index, item.order) == *key)
10998 {
10999 matches[original_index] = Some(current_index);
11000 used[current_index] = true;
11001 }
11002 }
11003 match_unidentified_by_position(&mut matches, &mut used);
11004 matches
11005}
11006
11007fn axis_original_to_current(original: &[AxisId], current: &[AxisId]) -> Vec<Option<usize>> {
11008 let mut used = vec![false; current.len()];
11009 let mut matches = vec![None; original.len()];
11010 for (original_index, id) in original.iter().enumerate() {
11011 if let Some(current_index) = current
11012 .iter()
11013 .enumerate()
11014 .position(|(index, current_id)| !used[index] && current_id == id)
11015 {
11016 matches[original_index] = Some(current_index);
11017 used[current_index] = true;
11018 }
11019 }
11020 match_unidentified_by_position(&mut matches, &mut used);
11021 matches
11022}
11023
11024fn match_unidentified_by_position(matches: &mut [Option<usize>], used: &mut [bool]) {
11025 for original_index in 0..matches.len() {
11026 if matches[original_index].is_none() && original_index < used.len() && !used[original_index]
11027 {
11028 matches[original_index] = Some(original_index);
11029 used[original_index] = true;
11030 }
11031 }
11032 for matched in matches.iter_mut().filter(|matched| matched.is_none()) {
11033 if let Some(current_index) = used.iter().position(|used| !used) {
11034 *matched = Some(current_index);
11035 used[current_index] = true;
11036 }
11037 }
11038}
11039
11040fn invert_original_to_current(
11041 original_to_current: &[Option<usize>],
11042 current_len: usize,
11043) -> Vec<Option<usize>> {
11044 let mut current_to_original = vec![None; current_len];
11045 for (original, current) in original_to_current.iter().enumerate() {
11046 if let Some(current) = current {
11047 current_to_original[*current] = Some(original);
11048 }
11049 }
11050 current_to_original
11051}
11052
11053fn emit_repeated_raw<W: Write>(
11054 writer: &mut Writer<W>,
11055 raw_children: &OrderedRawChildren,
11056 offset: usize,
11057 original_to_current: &[Option<usize>],
11058 current_len: usize,
11059 current_boundary: usize,
11060) -> Result<()> {
11061 if current_boundary == 0 {
11062 emit_raw(writer, raw_children.at(offset))?;
11063 }
11064 for original_boundary in 1..=original_to_current.len() {
11065 let effective = original_to_current
11066 .iter()
11067 .skip(original_boundary)
11068 .flatten()
11069 .copied()
11070 .next()
11071 .unwrap_or(current_len);
11072 if effective == current_boundary {
11073 emit_raw(writer, raw_children.at(offset + original_boundary))?;
11074 }
11075 }
11076 Ok(())
11077}
11078
11079fn parse_plot_axis(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Axis>> {
11080 let mut reader = Reader::from_reader(xml);
11081 let mut buffer = Vec::new();
11082 loop {
11083 match reader
11084 .read_event_into(&mut buffer)
11085 .map_err(OxmlError::from)?
11086 {
11087 Event::Start(element) | Event::Empty(element) => {
11088 if !element_is_in_namespace(&element, C_NS, inherited)? {
11089 return Ok(None);
11090 }
11091 let name = element.name();
11092 if AxisKind::parse(local_name(name.as_ref())).is_none() {
11093 return Ok(None);
11094 }
11095 return Axis::from_xml_with_namespaces(xml, inherited).map(Some);
11096 }
11097 Event::Eof => return Ok(None),
11098 _ => {}
11099 }
11100 buffer.clear();
11101 }
11102}
11103
11104fn validate_axis_pairs(axes: &[Axis]) -> Result<()> {
11105 for (index, axis) in axes.iter().enumerate() {
11106 if axes[..index].iter().any(|other| other.id == axis.id) {
11107 return Err(ChartError::DuplicateElement(format!(
11108 "c:axId {}",
11109 axis.id.value()
11110 )));
11111 }
11112 }
11113 for axis in axes {
11114 if axis.id == axis.cross_axis {
11115 return Err(ChartError::InvalidValue {
11116 element: "c:crossAx".to_owned(),
11117 value: format!("axis {} crosses itself", axis.id.value()),
11118 });
11119 }
11120 let crossed = axes
11121 .iter()
11122 .find(|candidate| candidate.id == axis.cross_axis)
11123 .ok_or_else(|| ChartError::InvalidValue {
11124 element: "c:crossAx".to_owned(),
11125 value: format!(
11126 "axis {} references missing axis {}",
11127 axis.id.value(),
11128 axis.cross_axis.value()
11129 ),
11130 })?;
11131 if crossed.cross_axis != axis.id {
11132 return Err(ChartError::InvalidValue {
11133 element: "c:crossAx".to_owned(),
11134 value: format!(
11135 "axis {} references {}, which references {}",
11136 axis.id.value(),
11137 crossed.id.value(),
11138 crossed.cross_axis.value()
11139 ),
11140 });
11141 }
11142 }
11143 Ok(())
11144}
11145
11146fn parse_plot_series(
11147 xml: &[u8],
11148 inherited: &NamespaceBindings,
11149 series: &mut Vec<Series>,
11150) -> Result<()> {
11151 let mut reader = Reader::from_reader(xml);
11152 let mut buffer = Vec::new();
11153 loop {
11154 match reader
11155 .read_event_into(&mut buffer)
11156 .map_err(OxmlError::from)?
11157 {
11158 Event::Start(element) => {
11159 if !element_is_in_namespace(&element, C_NS, inherited)? {
11160 return Ok(());
11161 }
11162 let local = local_name(element.name().as_ref()).to_vec();
11163 if !is_supported_series_plot(&local) {
11164 return Ok(());
11165 }
11166 let namespaces = chart_bindings(inherited, &element)?;
11167 let mut inner = Vec::new();
11168 loop {
11169 match reader
11170 .read_event_into(&mut inner)
11171 .map_err(OxmlError::from)?
11172 {
11173 Event::Start(child) => {
11174 let name = chart_child_local(&child, &namespaces)?;
11175 let raw = capture_element(&mut reader, &child)?;
11176 if name.as_deref() == Some(b"ser") {
11177 series.push(Series::from_xml_with_namespaces(&raw, &namespaces)?);
11178 }
11179 }
11180 Event::Empty(child) => {
11181 let name = chart_child_local(&child, &namespaces)?;
11182 if name.as_deref() == Some(b"ser") {
11183 return Err(ChartError::MissingElement("c:ser/c:idx".to_owned()));
11184 }
11185 }
11186 Event::End(end) if matches_local_name(end.name().as_ref(), &local) => {
11187 return Ok(());
11188 }
11189 Event::Eof => {
11190 return Err(missing_end(&format!(
11191 "c:{}",
11192 String::from_utf8_lossy(&local)
11193 )));
11194 }
11195 _ => {}
11196 }
11197 inner.clear();
11198 }
11199 }
11200 Event::Empty(_) | Event::Eof => return Ok(()),
11201 _ => {}
11202 }
11203 buffer.clear();
11204 }
11205}
11206
11207fn is_supported_series_plot(local: &[u8]) -> bool {
11208 matches!(
11209 local,
11210 b"areaChart"
11211 | b"area3DChart"
11212 | b"barChart"
11213 | b"bar3DChart"
11214 | b"doughnutChart"
11215 | b"lineChart"
11216 | b"line3DChart"
11217 | b"ofPieChart"
11218 | b"pieChart"
11219 | b"pie3DChart"
11220 | b"radarChart"
11221 | b"stockChart"
11222 | b"surfaceChart"
11223 | b"surface3DChart"
11224 )
11225}
11226
11227impl Default for CT_Legend {
11228 fn default() -> Self {
11229 Self {
11230 root_name: "c:legend".into(),
11231 settings: LegendSettings {
11232 visible: true,
11233 ..LegendSettings::default()
11234 },
11235 raw_attributes: Vec::new(),
11236 raw_children: OrderedRawChildren::default(),
11237 }
11238 }
11239}
11240
11241impl CT_Legend {
11242 fn from_xml_with_namespaces(xml: &[u8], namespaces: &NamespaceBindings) -> Result<Self> {
11243 let mut wrapper = BytesStart::new("scope");
11244 for (prefix, uri) in namespaces {
11245 let name = if prefix.is_empty() {
11246 "xmlns".to_owned()
11247 } else {
11248 format!("xmlns:{}", String::from_utf8_lossy(prefix))
11249 };
11250 wrapper.push_attribute((name.as_str(), uri.as_str()));
11251 }
11252 let mut writer = Writer::new(Vec::new());
11253 writer
11254 .write_event(Event::Start(wrapper))
11255 .map_err(OxmlError::from)?;
11256 writer.get_mut().extend_from_slice(xml);
11257 writer
11258 .write_event(Event::End(BytesEnd::new("scope")))
11259 .map_err(OxmlError::from)?;
11260 let wrapped = writer.into_inner();
11261 let nodes = chart_source_nodes(&wrapped)?;
11262 let legend = chart_child_node(&nodes, 0, b"legend")?;
11263 let settings = legend_settings_at(&wrapped, &nodes, legend)?;
11264 let (raw_attributes, raw_children) = parse_raw_shell(xml, b"legend", "c:legend")?;
11265 let mut reader = Reader::from_reader(xml);
11266 let root_name = loop {
11267 match reader.read_event().map_err(OxmlError::from)? {
11268 Event::Start(e) | Event::Empty(e) => break element_name(&e),
11269 Event::Eof => return Err(missing_end("legend")),
11270 _ => {}
11271 }
11272 };
11273 let root_name = if raw_attributes
11276 .iter()
11277 .any(|(name, uri)| name == "xmlns:c" && uri != C_NS)
11278 {
11279 root_name
11280 } else {
11281 "c:legend".to_owned()
11282 };
11283 Ok(Self {
11284 root_name,
11285 settings,
11286 raw_attributes,
11287 raw_children,
11288 })
11289 }
11290
11291 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
11292 write_raw_shell(
11293 writer,
11294 &self.root_name,
11295 &self.raw_attributes,
11296 &self.raw_children,
11297 )
11298 }
11299
11300 pub fn raw_children(&self) -> &OrderedRawChildren {
11301 &self.raw_children
11302 }
11303}
11304
11305fn parse_raw_shell(
11306 xml: &[u8],
11307 local: &[u8],
11308 tag: &str,
11309) -> Result<(XmlAttributes, OrderedRawChildren)> {
11310 let mut reader = Reader::from_reader(xml);
11311 let mut buffer = Vec::new();
11312 loop {
11313 match reader
11314 .read_event_into(&mut buffer)
11315 .map_err(OxmlError::from)?
11316 {
11317 Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
11318 if local != b"legend" {
11319 reject_conflicting_prefix(&element, b"c", C_NS)?;
11320 }
11321 let raw_attributes = capture_attributes(&element)?;
11322 let mut raw_children = OrderedRawChildren::default();
11323 let mut child_buffer = Vec::new();
11324 loop {
11325 match reader
11326 .read_event_into(&mut child_buffer)
11327 .map_err(OxmlError::from)?
11328 {
11329 Event::Start(child) => {
11330 raw_children.push(0, capture_element(&mut reader, &child)?)
11331 }
11332 Event::Empty(child) => raw_children.push(0, capture_empty_element(&child)?),
11333 event @ (Event::Text(_)
11334 | Event::CData(_)
11335 | Event::Comment(_)
11336 | Event::PI(_)
11337 | Event::GeneralRef(_)) => {
11338 raw_children.push(0, capture_event(event)?);
11339 }
11340 Event::End(end) if matches_local_name(end.name().as_ref(), local) => break,
11341 Event::Eof => return Err(missing_end(tag)),
11342 _ => {}
11343 }
11344 child_buffer.clear();
11345 }
11346 return Ok((raw_attributes, raw_children));
11347 }
11348 Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
11349 if local != b"legend" {
11350 reject_conflicting_prefix(&element, b"c", C_NS)?;
11351 }
11352 return Ok((capture_attributes(&element)?, OrderedRawChildren::default()));
11353 }
11354 Event::Start(element) | Event::Empty(element) => {
11355 return Err(ChartError::UnexpectedElement(element_name(&element)));
11356 }
11357 Event::Eof => return Err(ChartError::MissingElement(tag.to_owned())),
11358 _ => {}
11359 }
11360 buffer.clear();
11361 }
11362}
11363
11364fn write_raw_shell<W: Write>(
11365 writer: &mut Writer<W>,
11366 tag: &str,
11367 raw_attributes: &XmlAttributes,
11368 raw_children: &OrderedRawChildren,
11369) -> Result<()> {
11370 let mut start = BytesStart::new(tag);
11371 push_attributes(&mut start, raw_attributes);
11372 if raw_children.is_empty() {
11373 writer
11374 .write_event(Event::Empty(start))
11375 .map_err(OxmlError::from)?;
11376 return Ok(());
11377 }
11378 writer
11379 .write_event(Event::Start(start))
11380 .map_err(OxmlError::from)?;
11381 emit_raw(writer, raw_children.at(0))?;
11382 writer
11383 .write_event(Event::End(BytesEnd::new(tag)))
11384 .map_err(OxmlError::from)?;
11385 Ok(())
11386}
11387
11388#[derive(Clone, Debug, PartialEq)]
11390pub struct CT_Chart {
11391 pub title: Option<CT_Title>,
11392 pub auto_title_deleted: bool,
11393 pub plot_area: CT_PlotArea,
11394 pub legend: Option<CT_Legend>,
11395 pub plot_vis_only: bool,
11396 pub disp_blanks_as: DispBlanksAs,
11397 auto_title_deleted_markup: Option<ScalarMarkup>,
11398 plot_vis_only_markup: Option<ScalarMarkup>,
11399 disp_blanks_as_markup: Option<ScalarMarkup>,
11400 raw_attributes: Vec<(String, String)>,
11401 raw_children: OrderedRawChildren,
11402}
11403
11404impl CT_Chart {
11405 fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
11406 let mut reader = Reader::from_reader(xml);
11407 let mut buffer = Vec::new();
11408 loop {
11409 match reader
11410 .read_event_into(&mut buffer)
11411 .map_err(OxmlError::from)?
11412 {
11413 Event::Start(element) if matches_local_name(element.name().as_ref(), b"chart") => {
11414 reject_conflicting_prefix(&element, b"c", C_NS)?;
11415 return Self::from_element(&mut reader, &element, inherited);
11416 }
11417 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"chart") => {
11418 return Err(ChartError::MissingElement("c:plotArea".to_owned()));
11419 }
11420 Event::Start(element) | Event::Empty(element) => {
11421 return Err(ChartError::UnexpectedElement(element_name(&element)));
11422 }
11423 Event::Eof => return Err(ChartError::MissingElement("c:chart".to_owned())),
11424 _ => {}
11425 }
11426 buffer.clear();
11427 }
11428 }
11429
11430 fn from_element(
11431 reader: &mut Reader<&[u8]>,
11432 start: &BytesStart<'_>,
11433 inherited: &NamespaceBindings,
11434 ) -> Result<Self> {
11435 let namespaces = bindings_with_local(inherited, start)?;
11436 let raw_attributes = capture_attributes(start)?;
11437 let mut state = ChartParseState::default();
11438 let mut buffer = Vec::new();
11439
11440 loop {
11441 match reader
11442 .read_event_into(&mut buffer)
11443 .map_err(OxmlError::from)?
11444 {
11445 Event::Start(element) => {
11446 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
11447 local_name(element.name().as_ref()).to_vec()
11448 } else {
11449 Vec::new()
11450 };
11451 let raw = capture_element(reader, &element)?;
11452 state.parse_child(&name, raw, &namespaces)?;
11453 }
11454 Event::Empty(element) => {
11455 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
11456 local_name(element.name().as_ref()).to_vec()
11457 } else {
11458 Vec::new()
11459 };
11460 let raw = capture_empty_element(&element)?;
11461 state.parse_child(&name, raw, &namespaces)?;
11462 }
11463 event @ (Event::Text(_)
11464 | Event::CData(_)
11465 | Event::Comment(_)
11466 | Event::PI(_)
11467 | Event::GeneralRef(_)) => {
11468 state.capture_event(capture_event(event)?);
11469 }
11470 Event::End(element) if matches_local_name(element.name().as_ref(), b"chart") => {
11471 break;
11472 }
11473 Event::Eof => return Err(missing_end("c:chart")),
11474 _ => {}
11475 }
11476 buffer.clear();
11477 }
11478
11479 state.finish(raw_attributes)
11480 }
11481
11482 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
11483 let mut start = BytesStart::new("c:chart");
11484 push_attributes(&mut start, &self.raw_attributes);
11485 writer
11486 .write_event(Event::Start(start))
11487 .map_err(OxmlError::from)?;
11488 emit_raw(writer, self.raw_children.at(0))?;
11489 if let Some(title) = &self.title {
11490 title.write_xml(writer)?;
11491 }
11492 emit_raw(writer, self.raw_children.at(1))?;
11493 if self.auto_title_deleted_markup.is_some() || self.auto_title_deleted {
11494 write_scalar(
11495 writer,
11496 "c:autoTitleDeleted",
11497 if self.auto_title_deleted { "1" } else { "0" },
11498 self.auto_title_deleted_markup.as_ref(),
11499 )?;
11500 }
11501 for raw_boundary in 2..8 {
11502 emit_raw(writer, self.raw_children.at(raw_boundary))?;
11503 }
11504 self.plot_area.write_xml(writer)?;
11505 emit_raw(writer, self.raw_children.at(8))?;
11506 if let Some(legend) = &self.legend {
11507 legend.write_xml(writer)?;
11508 }
11509 emit_raw(writer, self.raw_children.at(9))?;
11510 if self.plot_vis_only_markup.is_some() || !self.plot_vis_only {
11511 write_scalar(
11512 writer,
11513 "c:plotVisOnly",
11514 if self.plot_vis_only { "1" } else { "0" },
11515 self.plot_vis_only_markup.as_ref(),
11516 )?;
11517 }
11518 emit_raw(writer, self.raw_children.at(10))?;
11519 if self.disp_blanks_as_markup.is_some() || self.disp_blanks_as != DispBlanksAs::Gap {
11520 write_scalar(
11521 writer,
11522 "c:dispBlanksAs",
11523 self.disp_blanks_as.as_str(),
11524 self.disp_blanks_as_markup.as_ref(),
11525 )?;
11526 }
11527 for raw_boundary in 11..=13 {
11528 emit_raw(writer, self.raw_children.at(raw_boundary))?;
11529 }
11530 writer
11531 .write_event(Event::End(BytesEnd::new("c:chart")))
11532 .map_err(OxmlError::from)?;
11533 Ok(())
11534 }
11535
11536 pub fn raw_children(&self) -> &OrderedRawChildren {
11537 &self.raw_children
11538 }
11539}
11540
11541#[derive(Default)]
11542struct ChartParseState {
11543 title: Option<CT_Title>,
11544 auto_title_deleted: Option<(bool, ScalarMarkup)>,
11545 plot_area: Option<CT_PlotArea>,
11546 legend: Option<CT_Legend>,
11547 plot_vis_only: Option<(bool, ScalarMarkup)>,
11548 disp_blanks_as: Option<(DispBlanksAs, ScalarMarkup)>,
11549 raw_children: OrderedRawChildren,
11550 boundary: usize,
11551}
11552
11553impl ChartParseState {
11554 fn capture_event(&mut self, raw: Vec<u8>) {
11555 self.raw_children.push(self.boundary, raw);
11556 }
11557
11558 fn parse_child(
11559 &mut self,
11560 name: &[u8],
11561 raw: Vec<u8>,
11562 namespaces: &NamespaceBindings,
11563 ) -> Result<()> {
11564 match name {
11565 b"title" => {
11566 set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
11567 self.boundary = self.boundary.max(1);
11568 }
11569 b"autoTitleDeleted" => {
11570 set_once(
11571 &mut self.auto_title_deleted,
11572 parse_bool_value(&raw, "autoTitleDeleted")?,
11573 "c:autoTitleDeleted",
11574 )?;
11575 self.boundary = self.boundary.max(2);
11576 }
11577 b"plotArea" => {
11578 set_once(
11579 &mut self.plot_area,
11580 CT_PlotArea::from_xml_with_namespaces(&raw, namespaces)?,
11581 "c:plotArea",
11582 )?;
11583 self.boundary = self.boundary.max(8);
11584 }
11585 b"legend" => {
11586 set_once(
11587 &mut self.legend,
11588 CT_Legend::from_xml_with_namespaces(&raw, namespaces)?,
11589 "c:legend",
11590 )?;
11591 self.boundary = self.boundary.max(9);
11592 }
11593 b"plotVisOnly" => {
11594 set_once(
11595 &mut self.plot_vis_only,
11596 parse_bool_value(&raw, "plotVisOnly")?,
11597 "c:plotVisOnly",
11598 )?;
11599 self.boundary = self.boundary.max(10);
11600 }
11601 b"dispBlanksAs" => {
11602 set_once(
11603 &mut self.disp_blanks_as,
11604 parse_enum_value(&raw, "dispBlanksAs")?,
11605 "c:dispBlanksAs",
11606 )?;
11607 self.boundary = self.boundary.max(11);
11608 }
11609 _ => {
11610 self.raw_children
11611 .push(chart_raw_slot(name).unwrap_or(self.boundary), raw);
11612 self.boundary = self.boundary.max(chart_raw_boundary_after(name));
11613 }
11614 }
11615 Ok(())
11616 }
11617
11618 fn finish(self, raw_attributes: XmlAttributes) -> Result<CT_Chart> {
11619 let (auto_title_deleted, auto_title_deleted_markup) = self
11620 .auto_title_deleted
11621 .map(|(value, markup)| (value, Some(markup)))
11622 .unwrap_or((false, None));
11623 let (plot_vis_only, plot_vis_only_markup) = self
11624 .plot_vis_only
11625 .map(|(value, markup)| (value, Some(markup)))
11626 .unwrap_or((true, None));
11627 let (disp_blanks_as, disp_blanks_as_markup) = self
11628 .disp_blanks_as
11629 .map(|(value, markup)| (value, Some(markup)))
11630 .unwrap_or((DispBlanksAs::Gap, None));
11631 Ok(CT_Chart {
11632 title: self.title,
11633 auto_title_deleted,
11634 plot_area: self
11635 .plot_area
11636 .ok_or_else(|| ChartError::MissingElement("c:plotArea".to_owned()))?,
11637 legend: self.legend,
11638 plot_vis_only,
11639 disp_blanks_as,
11640 auto_title_deleted_markup,
11641 plot_vis_only_markup,
11642 disp_blanks_as_markup,
11643 raw_attributes,
11644 raw_children: raw_children_in_schema_order(&self.raw_children, 13),
11645 })
11646 }
11647}
11648
11649fn chart_raw_slot(name: &[u8]) -> Option<usize> {
11650 match name {
11651 b"pivotFmts" => Some(2),
11652 b"view3D" => Some(3),
11653 b"floor" => Some(4),
11654 b"sideWall" => Some(5),
11655 b"backWall" => Some(6),
11656 b"showDLblsOverMax" | b"extLst" => Some(11),
11659 _ => None,
11660 }
11661}
11662
11663fn chart_raw_boundary_after(name: &[u8]) -> usize {
11664 match name {
11665 b"title" => 1,
11666 b"autoTitleDeleted" => 2,
11667 b"pivotFmts" => 3,
11668 b"view3D" => 4,
11669 b"floor" => 5,
11670 b"sideWall" => 6,
11671 b"backWall" => 7,
11672 b"plotArea" => 8,
11673 b"legend" => 9,
11674 b"plotVisOnly" => 10,
11675 b"dispBlanksAs" => 11,
11676 b"showDLblsOverMax" | b"extLst" => 11,
11677 _ => 0,
11678 }
11679}
11680
11681#[derive(Clone, Debug, PartialEq)]
11683pub struct CT_ChartSpace {
11684 pub chart: CT_Chart,
11685 pub sp_pr: Option<CT_ShapeProperties>,
11686 pub tx_pr: Option<CT_TextBody>,
11687 raw_attributes: Vec<(String, String)>,
11688 namespace_declarations: Vec<(String, String)>,
11689 raw_children: OrderedRawChildren,
11690}
11691
11692impl CT_ChartSpace {
11693 pub fn from_xml(xml: &[u8]) -> Result<Self> {
11695 let mut reader = Reader::from_reader(xml);
11696 let mut buffer = Vec::new();
11697 loop {
11698 match reader
11699 .read_event_into(&mut buffer)
11700 .map_err(OxmlError::from)?
11701 {
11702 Event::Start(element)
11703 if matches_local_name(element.name().as_ref(), b"chartSpace") =>
11704 {
11705 validate_chart_space_namespace(&element)?;
11706 return Self::from_element(&mut reader, &element);
11707 }
11708 Event::Empty(element)
11709 if matches_local_name(element.name().as_ref(), b"chartSpace") =>
11710 {
11711 validate_chart_space_namespace(&element)?;
11712 return Err(ChartError::MissingElement("c:chart".to_owned()));
11713 }
11714 Event::Start(element) | Event::Empty(element) => {
11715 return Err(ChartError::UnexpectedElement(element_name(&element)));
11716 }
11717 Event::Eof => return Err(ChartError::MissingElement("c:chartSpace".to_owned())),
11718 _ => {}
11719 }
11720 buffer.clear();
11721 }
11722 }
11723
11724 fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
11725 let namespaces = namespace_bindings(start)?;
11726 let (raw_attributes, namespace_declarations) = capture_root_attributes(start)?;
11727 let mut state = ChartSpaceParseState::default();
11728 let mut buffer = Vec::new();
11729
11730 loop {
11731 match reader
11732 .read_event_into(&mut buffer)
11733 .map_err(OxmlError::from)?
11734 {
11735 Event::Start(element) => {
11736 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
11737 local_name(element.name().as_ref()).to_vec()
11738 } else {
11739 Vec::new()
11740 };
11741 let raw = capture_element(reader, &element)?;
11742 state.parse_child(&name, raw, &namespaces)?;
11743 }
11744 Event::Empty(element) => {
11745 let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
11746 local_name(element.name().as_ref()).to_vec()
11747 } else {
11748 Vec::new()
11749 };
11750 let raw = capture_empty_element(&element)?;
11751 state.parse_child(&name, raw, &namespaces)?;
11752 }
11753 event @ (Event::Text(_)
11754 | Event::CData(_)
11755 | Event::Comment(_)
11756 | Event::PI(_)
11757 | Event::GeneralRef(_)) => {
11758 state.capture_event(capture_event(event)?);
11759 }
11760 Event::End(element)
11761 if matches_local_name(element.name().as_ref(), b"chartSpace") =>
11762 {
11763 break;
11764 }
11765 Event::Eof => return Err(missing_end("c:chartSpace")),
11766 _ => {}
11767 }
11768 buffer.clear();
11769 }
11770
11771 state.finish(raw_attributes, namespace_declarations)
11772 }
11773
11774 pub fn to_xml(&self) -> Result<Vec<u8>> {
11776 let mut writer = Writer::new(Vec::new());
11777 let mut start = BytesStart::new("c:chartSpace");
11778 start.push_attribute(("xmlns:c", C_NS));
11779 start.push_attribute(("xmlns:a", A_NS));
11780 start.push_attribute(("xmlns:r", R_NS));
11781 push_attributes(&mut start, &self.namespace_declarations);
11782 push_attributes(&mut start, &self.raw_attributes);
11783 writer
11784 .write_event(Event::Start(start))
11785 .map_err(OxmlError::from)?;
11786 for raw_boundary in 0..8 {
11787 emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
11788 }
11789 self.chart.write_xml(&mut writer)?;
11790 emit_raw(&mut writer, self.raw_children.at(8))?;
11791 if let Some(properties) = &self.sp_pr {
11792 properties.write_xml_as(&mut writer, "c:spPr")?;
11793 }
11794 emit_raw(&mut writer, self.raw_children.at(9))?;
11795 if let Some(text) = &self.tx_pr {
11796 text.write_xml_as(&mut writer, "c:txPr")?;
11797 }
11798 for raw_boundary in 10..=14 {
11799 emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
11800 }
11801 writer
11802 .write_event(Event::End(BytesEnd::new("c:chartSpace")))
11803 .map_err(OxmlError::from)?;
11804 Ok(writer.into_inner())
11805 }
11806
11807 pub fn raw_children(&self) -> &OrderedRawChildren {
11808 &self.raw_children
11809 }
11810}
11811
11812#[derive(Default)]
11813struct ChartSpaceParseState {
11814 chart: Option<CT_Chart>,
11815 sp_pr: Option<CT_ShapeProperties>,
11816 tx_pr: Option<CT_TextBody>,
11817 raw_children: OrderedRawChildren,
11818 boundary: usize,
11819}
11820
11821impl ChartSpaceParseState {
11822 fn capture_event(&mut self, raw: Vec<u8>) {
11823 self.raw_children.push(self.boundary, raw);
11824 }
11825
11826 fn parse_child(
11827 &mut self,
11828 name: &[u8],
11829 raw: Vec<u8>,
11830 namespaces: &NamespaceBindings,
11831 ) -> Result<()> {
11832 match name {
11833 b"chart" => {
11834 set_once(
11835 &mut self.chart,
11836 CT_Chart::from_xml_with_namespaces(&raw, namespaces)?,
11837 "c:chart",
11838 )?;
11839 self.boundary = self.boundary.max(8);
11840 }
11841 b"spPr" => {
11842 parse_chart_shape_properties(&raw, namespaces, &mut self.sp_pr)?;
11843 self.boundary = self.boundary.max(9);
11844 }
11845 b"txPr" => {
11846 reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
11847 let inherited = namespaces
11848 .iter()
11849 .map(|(prefix, uri)| {
11850 Ok((
11851 std::str::from_utf8(prefix)
11852 .map_err(OxmlError::from)?
11853 .to_owned(),
11854 uri.clone(),
11855 ))
11856 })
11857 .collect::<Result<Vec<_>>>()?;
11858 let text = CT_TextBody::from_xml_as_with_namespaces(&raw, b"txPr", &inherited)?;
11859 let mut writer = Writer::new(Vec::new());
11860 text.write_xml_as(&mut writer, "c:txPr")?;
11861 reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
11862 set_once(&mut self.tx_pr, text, "c:txPr")?;
11863 self.boundary = self.boundary.max(10);
11864 }
11865 _ => {
11866 self.raw_children
11867 .push(chart_space_raw_slot(name).unwrap_or(self.boundary), raw);
11868 self.boundary = self.boundary.max(chart_space_raw_boundary_after(name));
11869 }
11870 }
11871 Ok(())
11872 }
11873
11874 fn finish(
11875 self,
11876 raw_attributes: XmlAttributes,
11877 namespace_declarations: XmlAttributes,
11878 ) -> Result<CT_ChartSpace> {
11879 Ok(CT_ChartSpace {
11880 chart: self
11881 .chart
11882 .ok_or_else(|| ChartError::MissingElement("c:chart".to_owned()))?,
11883 sp_pr: self.sp_pr,
11884 tx_pr: self.tx_pr,
11885 raw_attributes,
11886 namespace_declarations,
11887 raw_children: raw_children_in_schema_order(&self.raw_children, 14),
11888 })
11889 }
11890}
11891
11892fn raw_children_in_schema_order(
11893 raw_children: &OrderedRawChildren,
11894 final_boundary: usize,
11895) -> OrderedRawChildren {
11896 let mut ordered = OrderedRawChildren::default();
11897 for boundary in 0..=final_boundary {
11898 for raw in raw_children.at(boundary) {
11899 ordered.push(boundary, raw.to_vec());
11900 }
11901 }
11902 ordered
11903}
11904
11905fn chart_space_raw_slot(name: &[u8]) -> Option<usize> {
11906 match name {
11907 b"date1904" => Some(0),
11908 b"lang" => Some(1),
11909 b"roundedCorners" => Some(2),
11910 b"style" => Some(3),
11911 b"clrMapOvr" => Some(4),
11912 b"pivotSource" => Some(5),
11913 b"protection" => Some(6),
11914 b"externalData" => Some(10),
11915 b"printSettings" => Some(11),
11916 b"userShapes" => Some(12),
11917 b"extLst" => Some(13),
11918 _ => None,
11919 }
11920}
11921
11922fn chart_space_raw_boundary_after(name: &[u8]) -> usize {
11923 match name {
11924 b"date1904" => 1,
11925 b"lang" => 2,
11926 b"roundedCorners" => 3,
11927 b"style" => 4,
11928 b"clrMapOvr" => 5,
11929 b"pivotSource" => 6,
11930 b"protection" => 7,
11931 b"chart" => 8,
11932 b"spPr" => 9,
11933 b"txPr" => 10,
11934 b"externalData" => 11,
11935 b"printSettings" => 12,
11936 b"userShapes" => 13,
11937 b"extLst" => 14,
11938 _ => 0,
11939 }
11940}
11941
11942fn parse_bool_value(xml: &[u8], local: &str) -> Result<(bool, ScalarMarkup)> {
11943 let (value, markup) = scalar_value(xml, local)?;
11944 let value = match value.as_deref().unwrap_or("1") {
11945 "1" | "true" => true,
11946 "0" | "false" => false,
11947 invalid => return Err(invalid_attribute(local, "val", invalid.to_owned())),
11948 };
11949 Ok((value, markup))
11950}
11951
11952fn parse_enum_value(xml: &[u8], local: &str) -> Result<(DispBlanksAs, ScalarMarkup)> {
11953 let (value, markup) = scalar_value(xml, local)?;
11954 let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
11955 let parsed =
11956 DispBlanksAs::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
11957 Ok((parsed, markup))
11958}
11959
11960fn scalar_value(xml: &[u8], expected: &str) -> Result<(Option<String>, ScalarMarkup)> {
11961 let mut reader = Reader::from_reader(xml);
11962 let mut buffer = Vec::new();
11963 loop {
11964 match reader
11965 .read_event_into(&mut buffer)
11966 .map_err(OxmlError::from)?
11967 {
11968 Event::Empty(element)
11969 if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
11970 {
11971 reject_conflicting_prefix(&element, b"c", C_NS)?;
11972 return Ok((
11973 attribute_value(&element, b"val")?,
11974 ScalarMarkup {
11975 raw_attributes: capture_attributes_except(&element, b"val")?,
11976 raw_content: Vec::new(),
11977 },
11978 ));
11979 }
11980 Event::Start(element)
11981 if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
11982 {
11983 reject_conflicting_prefix(&element, b"c", C_NS)?;
11984 let value = attribute_value(&element, b"val")?;
11985 let raw_attributes = capture_attributes_except(&element, b"val")?;
11986 let mut raw_content = Vec::new();
11987 let mut inner = Vec::new();
11988 loop {
11989 match reader
11990 .read_event_into(&mut inner)
11991 .map_err(OxmlError::from)?
11992 {
11993 Event::End(end)
11994 if matches_local_name(end.name().as_ref(), expected.as_bytes()) =>
11995 {
11996 return Ok((
11997 value,
11998 ScalarMarkup {
11999 raw_attributes,
12000 raw_content,
12001 },
12002 ));
12003 }
12004 event @ (Event::Text(_)
12005 | Event::CData(_)
12006 | Event::Comment(_)
12007 | Event::PI(_)
12008 | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
12009 Event::Eof => return Err(missing_end(expected)),
12010 _ => return Err(ChartError::UnexpectedElement(expected.to_owned())),
12011 }
12012 inner.clear();
12013 }
12014 }
12015 Event::Start(element) | Event::Empty(element) => {
12016 return Err(ChartError::UnexpectedElement(element_name(&element)));
12017 }
12018 Event::Eof => return Err(ChartError::MissingElement(expected.to_owned())),
12019 _ => {}
12020 }
12021 buffer.clear();
12022 }
12023}
12024
12025fn set_once<T>(slot: &mut Option<T>, value: T, name: &str) -> Result<()> {
12026 if slot.is_some() {
12027 return Err(ChartError::DuplicateElement(name.to_owned()));
12028 }
12029 *slot = Some(value);
12030 Ok(())
12031}
12032
12033fn mark_once(seen: &mut bool, name: &str) -> Result<()> {
12034 if *seen {
12035 return Err(ChartError::DuplicateElement(name.to_owned()));
12036 }
12037 *seen = true;
12038 Ok(())
12039}
12040
12041fn validate_chart_space_namespace(element: &BytesStart<'_>) -> Result<()> {
12042 let name = element.name();
12043 let qualified = name.as_ref();
12044 let declaration = match qualified.iter().position(|byte| *byte == b':') {
12045 Some(position) => {
12046 let mut declaration = b"xmlns:".to_vec();
12047 declaration.extend_from_slice(&qualified[..position]);
12048 declaration
12049 }
12050 None => b"xmlns".to_vec(),
12051 };
12052 let namespace = attribute_value(element, &declaration)?
12053 .ok_or_else(|| invalid_attribute("chartSpace", "namespace", "<missing>".to_owned()))?;
12054 if namespace != C_NS {
12055 return Err(invalid_attribute("chartSpace", "namespace", namespace));
12056 }
12057 reject_conflicting_prefix(element, b"c", C_NS)?;
12058 reject_conflicting_prefix(element, b"a", A_NS)?;
12059 reject_conflicting_prefix(element, b"r", R_NS)
12060}
12061
12062fn reject_conflicting_prefix_in_xml(xml: &[u8], prefix: &[u8], namespace: &str) -> Result<()> {
12063 let mut reader = Reader::from_reader(xml);
12064 let mut buffer = Vec::new();
12065 loop {
12066 match reader
12067 .read_event_into(&mut buffer)
12068 .map_err(OxmlError::from)?
12069 {
12070 Event::Start(element) | Event::Empty(element) => {
12071 return reject_conflicting_prefix(&element, prefix, namespace);
12072 }
12073 Event::Eof => return Err(ChartError::MissingElement("XML root".to_owned())),
12074 _ => {}
12075 }
12076 buffer.clear();
12077 }
12078}
12079
12080fn reject_rewritten_foreign_elements(
12081 original: &[u8],
12082 written: &[u8],
12083 inherited: &NamespaceBindings,
12084 root_local: &[u8],
12085) -> Result<()> {
12086 let original = foreign_element_regions(original, inherited, root_local)?;
12087 let mut written_namespaces = inherited.clone();
12088 written_namespaces.retain(|(prefix, _)| prefix != b"a");
12089 written_namespaces.push((b"a".to_vec(), A_NS.to_owned()));
12090 let written = foreign_element_regions(written, &written_namespaces, root_local)?;
12091 if original != written {
12092 return Err(invalid_attribute(
12093 &String::from_utf8_lossy(root_local),
12094 "namespace",
12095 "foreign DrawingML content would be converted to typed content".to_owned(),
12096 ));
12097 }
12098 Ok(())
12099}
12100
12101fn foreign_element_regions(
12102 xml: &[u8],
12103 inherited: &NamespaceBindings,
12104 root_local: &[u8],
12105) -> Result<Vec<Vec<u8>>> {
12106 let mut reader = Reader::from_reader(xml);
12107 let mut buffer = Vec::new();
12108 let root_namespaces = loop {
12109 match reader
12110 .read_event_into(&mut buffer)
12111 .map_err(OxmlError::from)?
12112 {
12113 Event::Start(element) if matches_local_name(element.name().as_ref(), root_local) => {
12114 break bindings_with_local(inherited, &element)?;
12115 }
12116 Event::Empty(element) if matches_local_name(element.name().as_ref(), root_local) => {
12117 return Ok(Vec::new());
12118 }
12119 Event::Start(element) | Event::Empty(element) => {
12120 return Err(ChartError::UnexpectedElement(element_name(&element)));
12121 }
12122 Event::Eof => {
12123 return Err(ChartError::MissingElement(
12124 String::from_utf8_lossy(root_local).into_owned(),
12125 ));
12126 }
12127 _ => {}
12128 }
12129 buffer.clear();
12130 };
12131
12132 let mut namespace_stack = vec![root_namespaces];
12133 let mut foreign = Vec::new();
12134 let mut child_buffer = Vec::new();
12135 loop {
12136 match reader
12137 .read_event_into(&mut child_buffer)
12138 .map_err(OxmlError::from)?
12139 {
12140 Event::Start(element) => {
12141 let namespaces = namespace_stack.last().expect("root namespace binding");
12142 if element_is_in_namespace(&element, A_NS, namespaces)? {
12143 namespace_stack.push(bindings_with_local(namespaces, &element)?);
12144 } else {
12145 foreign.push(capture_element(&mut reader, &element)?);
12146 }
12147 }
12148 Event::Empty(element) => {
12149 let namespaces = namespace_stack.last().expect("root namespace binding");
12150 if !element_is_in_namespace(&element, A_NS, namespaces)? {
12151 foreign.push(capture_empty_element(&element)?);
12152 }
12153 }
12154 Event::End(element) if matches_local_name(element.name().as_ref(), root_local) => {
12155 return Ok(foreign);
12156 }
12157 Event::End(_) => {
12158 namespace_stack.pop();
12159 }
12160 Event::Eof => {
12161 return Err(missing_end(&String::from_utf8_lossy(root_local)));
12162 }
12163 _ => {}
12164 }
12165 child_buffer.clear();
12166 }
12167}
12168
12169fn reject_conflicting_prefix(
12170 element: &BytesStart<'_>,
12171 prefix: &[u8],
12172 namespace: &str,
12173) -> Result<()> {
12174 let mut key = b"xmlns:".to_vec();
12175 key.extend_from_slice(prefix);
12176 if let Some(value) = attribute_value(element, &key)?
12177 && value != namespace
12178 {
12179 return Err(invalid_attribute(
12180 &element_name(element),
12181 &String::from_utf8_lossy(&key),
12182 value,
12183 ));
12184 }
12185 Ok(())
12186}
12187
12188fn namespace_bindings(element: &BytesStart<'_>) -> Result<NamespaceBindings> {
12189 let mut bindings = Vec::new();
12190 for attribute in element.attributes() {
12191 let attribute = attribute.map_err(OxmlError::from)?;
12192 let key = attribute.key.as_ref();
12193 let prefix = if key == b"xmlns" {
12194 Vec::new()
12195 } else if let Some(prefix) = key.strip_prefix(b"xmlns:") {
12196 prefix.to_vec()
12197 } else {
12198 continue;
12199 };
12200 let namespace = attribute
12201 .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
12202 .map_err(OxmlError::from)?
12203 .into_owned();
12204 bindings.push((prefix, namespace));
12205 }
12206 Ok(bindings)
12207}
12208
12209fn bindings_with_local(
12210 inherited: &NamespaceBindings,
12211 element: &BytesStart<'_>,
12212) -> Result<NamespaceBindings> {
12213 let mut bindings = inherited.clone();
12214 for (prefix, namespace) in namespace_bindings(element)? {
12215 upsert_namespace_binding(&mut bindings, prefix, namespace);
12216 }
12217 Ok(bindings)
12218}
12219
12220fn upsert_namespace_binding(bindings: &mut NamespaceBindings, prefix: Vec<u8>, namespace: String) {
12221 if let Some((_, current)) = bindings.iter_mut().find(|(current, _)| current == &prefix) {
12222 *current = namespace;
12223 } else {
12224 bindings.push((prefix, namespace));
12225 }
12226}
12227
12228fn element_is_in_namespace(
12229 element: &BytesStart<'_>,
12230 expected: &str,
12231 inherited: &NamespaceBindings,
12232) -> Result<bool> {
12233 let name = element.name();
12234 let qualified = name.as_ref();
12235 let prefix = qualified
12236 .iter()
12237 .position(|byte| *byte == b':')
12238 .map(|position| &qualified[..position])
12239 .unwrap_or_default();
12240 let local = namespace_bindings(element)?;
12241 let namespace = local
12242 .iter()
12243 .rev()
12244 .chain(inherited.iter().rev())
12245 .find(|(candidate, _)| candidate.as_slice() == prefix)
12246 .map(|(_, namespace)| namespace.as_str());
12247 Ok(namespace == Some(expected))
12248}
12249
12250fn attribute_value(element: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
12251 for attribute in element.attributes() {
12252 let attribute = attribute.map_err(OxmlError::from)?;
12253 if attribute.key.as_ref() == name {
12254 return Ok(Some(
12255 attribute
12256 .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
12257 .map_err(OxmlError::from)?
12258 .into_owned(),
12259 ));
12260 }
12261 }
12262 Ok(None)
12263}
12264
12265fn capture_root_attributes(start: &BytesStart<'_>) -> Result<RootAttributes> {
12266 let mut attributes = Vec::new();
12267 let mut namespaces = Vec::new();
12268 for attribute in start.attributes() {
12269 let attribute = attribute.map_err(OxmlError::from)?;
12270 let name = std::str::from_utf8(attribute.key.as_ref())
12271 .map_err(OxmlError::from)?
12272 .to_owned();
12273 let value = attribute
12274 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
12275 .map_err(OxmlError::from)?
12276 .into_owned();
12277 if name == "xmlns:c" || name == "xmlns:a" || name == "xmlns:r" {
12278 continue;
12279 }
12280 if name == "xmlns" || name.starts_with("xmlns:") {
12281 namespaces.push((name, value));
12282 } else {
12283 attributes.push((name, value));
12284 }
12285 }
12286 Ok((attributes, namespaces))
12287}
12288
12289fn capture_attributes(start: &BytesStart<'_>) -> Result<XmlAttributes> {
12290 let mut attributes = Vec::new();
12291 for attribute in start.attributes() {
12292 let attribute = attribute.map_err(OxmlError::from)?;
12293 attributes.push((
12294 std::str::from_utf8(attribute.key.as_ref())
12295 .map_err(OxmlError::from)?
12296 .to_owned(),
12297 attribute
12298 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
12299 .map_err(OxmlError::from)?
12300 .into_owned(),
12301 ));
12302 }
12303 Ok(attributes)
12304}
12305
12306fn capture_attributes_except(start: &BytesStart<'_>, excluded: &[u8]) -> Result<XmlAttributes> {
12307 let mut attributes = Vec::new();
12308 for attribute in start.attributes() {
12309 let attribute = attribute.map_err(OxmlError::from)?;
12310 if attribute.key.as_ref() == excluded {
12311 continue;
12312 }
12313 attributes.push((
12314 std::str::from_utf8(attribute.key.as_ref())
12315 .map_err(OxmlError::from)?
12316 .to_owned(),
12317 attribute
12318 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
12319 .map_err(OxmlError::from)?
12320 .into_owned(),
12321 ));
12322 }
12323 Ok(attributes)
12324}
12325
12326fn capture_attributes_excluding(
12327 start: &BytesStart<'_>,
12328 excluded: &[&[u8]],
12329) -> Result<XmlAttributes> {
12330 let mut attributes = Vec::new();
12331 for attribute in start.attributes() {
12332 let attribute = attribute.map_err(OxmlError::from)?;
12333 if excluded.contains(&attribute.key.as_ref()) {
12334 continue;
12335 }
12336 attributes.push((
12337 std::str::from_utf8(attribute.key.as_ref())
12338 .map_err(OxmlError::from)?
12339 .to_owned(),
12340 attribute
12341 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
12342 .map_err(OxmlError::from)?
12343 .into_owned(),
12344 ));
12345 }
12346 Ok(attributes)
12347}
12348
12349fn push_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
12350 for (name, value) in attributes {
12351 start.push_attribute((name.as_str(), value.as_str()));
12352 }
12353}
12354
12355fn write_scalar<W: Write>(
12356 writer: &mut Writer<W>,
12357 name: &str,
12358 value: &str,
12359 markup: Option<&ScalarMarkup>,
12360) -> Result<()> {
12361 let mut start = BytesStart::new(name);
12362 start.push_attribute(("val", value));
12363 if let Some(markup) = markup {
12364 push_attributes(&mut start, &markup.raw_attributes);
12365 }
12366 let has_content = markup.is_some_and(|markup| !markup.raw_content.is_empty());
12367 if !has_content {
12368 writer
12369 .write_event(Event::Empty(start))
12370 .map_err(OxmlError::from)?;
12371 return Ok(());
12372 }
12373 writer
12374 .write_event(Event::Start(start))
12375 .map_err(OxmlError::from)?;
12376 if let Some(markup) = markup {
12377 for raw in &markup.raw_content {
12378 writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
12379 }
12380 }
12381 writer
12382 .write_event(Event::End(BytesEnd::new(name)))
12383 .map_err(OxmlError::from)?;
12384 Ok(())
12385}
12386
12387fn capture_event(event: Event<'_>) -> Result<Vec<u8>> {
12388 let mut writer = Writer::new(Vec::new());
12389 writer
12390 .write_event(event.into_owned())
12391 .map_err(OxmlError::from)?;
12392 Ok(writer.into_inner())
12393}
12394
12395fn emit_raw<'a, W: Write>(
12396 writer: &mut Writer<W>,
12397 children: impl Iterator<Item = &'a [u8]>,
12398) -> Result<()> {
12399 for child in children {
12400 writer.get_mut().write_all(child).map_err(OxmlError::from)?;
12401 }
12402 Ok(())
12403}
12404
12405fn invalid_attribute(element: &str, attribute: &str, value: String) -> ChartError {
12406 ChartError::InvalidAttribute {
12407 element: element.to_owned(),
12408 attribute: attribute.to_owned(),
12409 value,
12410 }
12411}
12412
12413fn element_name(element: &BytesStart<'_>) -> String {
12414 String::from_utf8_lossy(element.name().as_ref()).into_owned()
12415}
12416
12417fn missing_end(element: &str) -> ChartError {
12418 ChartError::Xml(OxmlError::MissingElement(format!("closing {element}")))
12419}
12420
12421#[cfg(test)]
12422mod tests {
12423 use std::collections::HashSet;
12424 use std::fs;
12425 use std::path::{Path, PathBuf};
12426 use std::process::Command;
12427
12428 use oxml_core::raw_xml::{capture_element, capture_empty_element};
12429 use oxml_drawing::color::{ColorMap, ColorMapSlot, RgbColor, ThemeColorSlot};
12430 use oxml_drawing::theme::CT_OfficeStyleSheet;
12431 use oxml_layout::{
12432 Color, FontManager, LayoutResult, PageFrame, Paint, PathCommand, Point, PositionedElement,
12433 Rect,
12434 };
12435 use oxml_opc::OpcPackage;
12436 use quick_xml::Reader;
12437 use quick_xml::events::Event;
12438
12439 use super::{
12440 A_NS, Axis, AxisData, AxisId, AxisKind, AxisPosition, BarDirection, BarGrouping, C_NS,
12441 CT_ChartSpace, CT_DLbls, CT_ShapeProperties, CT_TextBody, ChartData, ChartGeometry,
12442 ChartKind, DataLabelPosition, DispBlanksAs, Domain, Grouping, NumberFormat, NumericData,
12443 Orientation, Plot, R_NS, ScatterStyle, Series, StringRef, TickLabelPosition, TickMark,
12444 authored_chart_parts, capture_event, chart_workbook_relationship_id, local_name,
12445 matches_local_name, nice_number_scale, read_chart_data,
12446 render_chart as render_chart_with_theme, render_geometry as render_geometry_with_theme,
12447 replace_chart_data,
12448 };
12449
12450 const MANIFEST: &str = include_str!("../../../scripts/pptx-corpus-manifest.tsv");
12451 const EXPECTED_DECKS: usize = 50;
12452 const LIBREOFFICE_VERSION: &str =
12453 "LibreOffice 26.2.5.2 cd7284b4cbbfeb507e630c1aac019f4157393acb";
12454 const PDFTOTEXT_VERSION: &str = "pdftotext version 26.01.0";
12455 const PDFTOPPM_VERSION: &str = "pdftoppm version 26.01.0";
12456 const PLOT_RENDER_NORMALIZED_MAE_THRESHOLD: f64 = 0.0;
12457
12458 #[test]
12459 fn authored_charts_emit_portable_viewer_defaults() {
12460 let data = ChartData {
12461 categories: vec!["August".to_owned(), "September".to_owned()],
12462 series: vec![("Price".to_owned(), vec![12.5, 19.2])],
12463 number_format: Some(r"0.##\%".to_owned()),
12464 category_axis_title: Some("Month".to_owned()),
12465 value_axis_title: Some("Change".to_owned()),
12466 palette: vec![
12467 RgbColor::parse("2B6FE3").unwrap(),
12468 RgbColor::parse("F0761F").unwrap(),
12469 ],
12470 };
12471 let (line, line_workbook) = authored_chart_parts(ChartKind::Line, &data, "rId1").unwrap();
12472 let line = std::str::from_utf8(&line).unwrap();
12473 assert_eq!(line.matches(r#"<c:delete val="0"/>"#).count(), 2);
12474 assert!(line.contains(r#"<c:numFmt formatCode="0.##\%" sourceLinked="0"/>"#));
12475 assert!(line.contains("<a:t>Month</a:t>"));
12476 assert!(line.contains("<a:t>Change</a:t>"));
12477 assert!(line.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
12478 let assert_portable_line = |xml: &str| {
12479 let titles = xml
12480 .split("<c:title>")
12481 .skip(1)
12482 .map(|tail| tail.split_once("</c:title>").unwrap().0)
12483 .collect::<Vec<_>>();
12484 assert_eq!(titles.len(), 2);
12485 for title in titles {
12486 let text = title.find("<c:tx>").unwrap();
12487 let layout = title.find("<c:layout/>").unwrap();
12488 let overlay = title.find(r#"<c:overlay val="0"/>"#).unwrap();
12489 assert!(text < layout && layout < overlay);
12490 }
12491
12492 let line_plot = xml
12493 .split_once("<c:lineChart>")
12494 .unwrap()
12495 .1
12496 .split_once("</c:lineChart>")
12497 .unwrap()
12498 .0;
12499 assert_eq!(line_plot.matches(r#"<c:marker val="0"/>"#).count(), 1);
12500 assert_eq!(line_plot.matches(r#"<c:smooth val="0"/>"#).count(), 1);
12501 let marker = line_plot.find(r#"<c:marker val="0"/>"#).unwrap();
12502 let smooth = line_plot.find(r#"<c:smooth val="0"/>"#).unwrap();
12503 let axis_id = line_plot.find("<c:axId").unwrap();
12504 assert!(marker < smooth && smooth < axis_id);
12505 };
12506 assert_portable_line(line);
12507 for axis in line.split("<c:crossAx").take(2) {
12508 let scaling = axis.rfind("<c:scaling").unwrap();
12509 let deleted = axis.rfind("<c:delete").unwrap();
12510 let position = axis.rfind("<c:axPos").unwrap();
12511 assert!(scaling < deleted && deleted < position);
12512 }
12513
12514 let parsed = CT_ChartSpace::from_xml(line.as_bytes()).unwrap();
12515 let rewritten = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
12516 assert_portable_line(&rewritten);
12517 assert!(rewritten.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
12518 assert!(rewritten.contains(r#"<c:externalData r:id="rId1">"#));
12519
12520 for kind in [
12521 ChartKind::Bar,
12522 ChartKind::Pie,
12523 ChartKind::Doughnut,
12524 ChartKind::Area,
12525 ChartKind::Scatter,
12526 ChartKind::Radar,
12527 ] {
12528 let mut kind_data = data.clone();
12529 if kind == ChartKind::Scatter {
12530 kind_data.categories = vec!["1".to_owned(), "2".to_owned()];
12531 }
12532 let (chart, workbook) = authored_chart_parts(kind, &kind_data, "rId1").unwrap();
12533 let chart = std::str::from_utf8(&chart).unwrap();
12534 assert!(!chart.contains(r#"<c:marker val="0"/>"#));
12535 assert!(!chart.contains(r#"<c:smooth val="0"/>"#));
12536 if kind != ChartKind::Scatter {
12537 assert_eq!(workbook, line_workbook);
12538 }
12539 if matches!(kind, ChartKind::Pie | ChartKind::Doughnut) {
12540 assert!(chart.contains("<c:legend"));
12541 assert!(chart.contains(r#"<c:showPercent val="1"/>"#));
12542 }
12543 if kind == ChartKind::Doughnut {
12544 assert!(chart.contains(r#"<c:holeSize val="50"/>"#));
12545 }
12546 if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) {
12547 assert_eq!(chart.matches("<c:dPt>").count(), 2);
12548 assert!(chart.contains(
12549 r#"<c:dPt><c:idx val="0"/><c:spPr><a:solidFill><a:srgbClr val="2B6FE3"/>"#
12550 ));
12551 assert!(chart.contains(
12552 r#"<c:dPt><c:idx val="1"/><c:spPr><a:solidFill><a:srgbClr val="F0761F"/>"#
12553 ));
12554 }
12555
12556 let parsed = CT_ChartSpace::from_xml(chart.as_bytes()).unwrap();
12557 let rewritten = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
12558 assert_eq!(
12559 rewritten.matches("<c:dPt>").count(),
12560 usize::from(matches!(
12561 kind,
12562 ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut
12563 )) * 2
12564 );
12565 assert!(rewritten.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
12566 if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) {
12567 assert!(rewritten.contains(r#"<a:srgbClr val="F0761F"/>"#));
12568 }
12569 }
12570 }
12571
12572 #[test]
12573 fn optional_numeric_cache_format_remains_omitted() {
12574 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>"#;
12575 let mut parsed = NumericData::from_xml(xml).unwrap();
12576 assert_eq!(parsed.format_code, "General");
12577 let written = parsed.to_xml().unwrap();
12578 assert!(!String::from_utf8_lossy(&written).contains("formatCode"));
12579 assert_eq!(NumericData::from_xml(&written).unwrap(), parsed);
12580
12581 parsed.format_code = "0.00".to_owned();
12582 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
12583 assert!(written.contains("<c:formatCode>0.00</c:formatCode>"));
12584 }
12585
12586 fn render_geometry(chart: &super::CT_Chart, bounds: Rect) -> super::Result<ChartGeometry> {
12587 render_geometry_with_theme(
12588 chart,
12589 bounds,
12590 &CT_OfficeStyleSheet::office_default(),
12591 &ColorMap::default(),
12592 )
12593 }
12594
12595 fn render_chart(
12596 chart: &super::CT_Chart,
12597 bounds: Rect,
12598 fonts: &mut FontManager,
12599 ) -> super::Result<oxml_layout::GroupElement> {
12600 render_chart_with_theme(
12601 chart,
12602 bounds,
12603 &CT_OfficeStyleSheet::office_default(),
12604 &ColorMap::default(),
12605 fonts,
12606 )
12607 }
12608
12609 #[test]
12610 fn bar_chart_rasterises_at_computed_positions() {
12611 let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12612 let geometry = render_geometry(
12613 &chart.chart,
12614 Rect {
12615 x: 0.0,
12616 y: 0.0,
12617 width: 200.0,
12618 height: 140.0,
12619 },
12620 )
12621 .unwrap();
12622 assert_eq!(
12623 geometry.plot_bounds,
12624 Rect {
12625 x: 36.0,
12626 y: 12.0,
12627 width: 152.0,
12628 height: 100.0,
12629 }
12630 );
12631 let bounds = path_bounds(&geometry);
12632 assert_eq!(bounds.len(), 2);
12633 assert_rect_near(
12634 bounds[0],
12635 Rect {
12636 x: 58.8,
12637 y: 62.0,
12638 width: 30.4,
12639 height: 50.0,
12640 },
12641 );
12642 assert_rect_near(
12643 bounds[1],
12644 Rect {
12645 x: 134.8,
12646 y: 12.0,
12647 width: 30.4,
12648 height: 100.0,
12649 },
12650 );
12651
12652 let layout = LayoutResult::new(
12653 vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
12654 Vec::new(),
12655 None,
12656 Vec::new(),
12657 );
12658 let png = oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap();
12659 let pixmap = tiny_skia::Pixmap::decode_png(&png).unwrap();
12660 for (x, y) in [(74, 87), (150, 37)] {
12661 let pixel = pixmap.pixel(x, y).unwrap();
12662 assert_ne!((pixel.red(), pixel.green(), pixel.blue()), (255, 255, 255));
12663 }
12664 }
12665
12666 #[test]
12667 fn unstyled_four_series_use_accent_one_through_four() {
12668 let chart = four_series_bar_chart();
12669 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12670 let colours = path_colours(&geometry);
12671
12672 assert_eq!(
12673 &colours[..4],
12674 &[
12675 Color::from_hex("156082"),
12676 Color::from_hex("E97132"),
12677 Color::from_hex("196B24"),
12678 Color::from_hex("0F9ED5"),
12679 ]
12680 );
12681 }
12682
12683 #[test]
12684 fn direct_series_solid_colour_overrides_theme_accent() {
12685 for (paint, expected) in [
12686 (
12687 "<a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill>",
12688 Color::from_hex("123456"),
12689 ),
12690 (
12691 "<a:ln><a:solidFill><a:srgbClr val=\"654321\"/></a:solidFill></a:ln>",
12692 Color::from_hex("654321"),
12693 ),
12694 (
12695 "<a:noFill/>",
12696 Color {
12697 r: 0.0,
12698 g: 0.0,
12699 b: 0.0,
12700 a: 0.0,
12701 },
12702 ),
12703 ] {
12704 let styled =
12705 plot_series(0).replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
12706 let plot = bar_plot("").replace(&plot_series(0), &styled);
12707 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
12708 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12709
12710 assert_eq!(path_colours(&geometry)[0], expected);
12711 }
12712
12713 let styled = plot_series(0).replace(
12714 "<q:cat>",
12715 "<q:spPr><a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill></q:spPr><q:cat>",
12716 );
12717 let plot = bar_plot("").replace(&plot_series(0), &styled);
12718 let xml = chart_with_plot(&plot).replace(
12719 "</q:plotArea></q:chart>",
12720 "</q:plotArea><q:legend/></q:chart>",
12721 );
12722 let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
12723 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12724 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12725 assert!(group.children.iter().any(|element| matches!(
12726 element,
12727 PositionedElement::Path(path)
12728 if path.fill == Some(Paint::Solid(Color::from_hex("123456")))
12729 )));
12730
12731 let mut theme = CT_OfficeStyleSheet::office_default();
12732 let preset = oxml_drawing::fill::Fill::from_xml(
12733 br#"<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:prstClr val="red"/></a:solidFill>"#,
12734 )
12735 .unwrap();
12736 let oxml_drawing::fill::Fill::Solid(preset) = preset else {
12737 panic!("expected solid fill");
12738 };
12739 theme.theme_elements.color_scheme.hyperlink = preset.color.unwrap();
12740 let geometry =
12741 render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &ColorMap::default())
12742 .unwrap();
12743 assert_eq!(path_colours(&geometry)[0], Color::from_hex("123456"));
12744
12745 for (paint, expected_alpha) in [
12746 ("<a:noFill/>", 0.0),
12747 (
12748 "<a:solidFill><a:srgbClr val=\"123456\"><a:alpha val=\"50000\"/></a:srgbClr></a:solidFill>",
12749 (128.0 / 255.0) * 0.55,
12750 ),
12751 ] {
12752 let (_, area, _) = remaining_plot_fixtures()
12753 .into_iter()
12754 .find(|(kind, _, _)| *kind == "areaChart")
12755 .unwrap();
12756 let styled_area = area.replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
12757 let chart =
12758 CT_ChartSpace::from_xml(chart_with_optional_axes(&styled_area, true).as_bytes())
12759 .unwrap();
12760 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12761 let actual_alpha = path_colours(&geometry)[0].a;
12762
12763 assert!((actual_alpha - expected_alpha).abs() < 1.0e-12);
12764 }
12765 }
12766
12767 #[test]
12768 fn series_accent_cycle_repeats_after_six() {
12769 let chart = seven_series_bar_chart();
12770 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12771 let colours = path_colours(&geometry);
12772
12773 assert_eq!(colours[0], Color::from_hex("156082"));
12774 assert_eq!(colours[6], colours[0]);
12775 }
12776
12777 #[test]
12778 fn series_colours_honor_colour_map_and_transform_order() {
12779 let styled = plot_series(0).replace(
12780 "<q:cat>",
12781 "<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>",
12782 );
12783 let plot = bar_plot("").replace(&plot_series(0), &styled);
12784 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
12785 let color_map =
12786 ColorMap::default().with_overrides(&[(ColorMapSlot::Accent1, ThemeColorSlot::Accent4)]);
12787 let mut theme = CT_OfficeStyleSheet::office_default();
12788 let theme_fill = oxml_drawing::fill::Fill::from_xml(
12789 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>"#,
12790 )
12791 .unwrap();
12792 let oxml_drawing::fill::Fill::Solid(theme_fill) = theme_fill else {
12793 panic!("expected solid fill");
12794 };
12795 let theme_colour = theme_fill.color.unwrap();
12796 theme.theme_elements.color_scheme.accent4 = theme_colour.clone();
12797 let geometry =
12798 render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &color_map).unwrap();
12799 let series_fill = oxml_drawing::fill::Fill::from_xml(
12800 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>"#,
12801 )
12802 .unwrap();
12803 let oxml_drawing::fill::Fill::Solid(series_fill) = series_fill else {
12804 panic!("expected solid fill");
12805 };
12806 let mut transforms = theme_colour.transforms().to_vec();
12807 transforms.extend_from_slice(series_fill.color.as_ref().unwrap().transforms());
12808 let expected = oxml_drawing::color::apply_color_transforms(
12809 RgbColor::parse("336699").unwrap(),
12810 &transforms,
12811 );
12812
12813 assert_eq!(path_colours(&geometry)[0], resolved_layout_colour(expected));
12814 }
12815
12816 #[test]
12817 fn chart_colours_do_not_use_word_tint_shade() {
12818 let styled = plot_series(0).replace(
12819 "<q:cat>",
12820 "<q:spPr><a:solidFill><a:srgbClr val=\"808080\"><a:tint val=\"50000\"/></a:srgbClr></a:solidFill></q:spPr><q:cat>",
12821 );
12822 let plot = bar_plot("").replace(&plot_series(0), &styled);
12823 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
12824 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12825
12826 assert_eq!(
12827 path_colours(&geometry)[0],
12828 Color {
12829 r: 205.0 / 255.0,
12830 g: 205.0 / 255.0,
12831 b: 205.0 / 255.0,
12832 a: 1.0,
12833 }
12834 );
12835 }
12836
12837 #[test]
12838 fn unsupported_direct_series_paint_is_contextual() {
12839 let styled = plot_series(0).replace(
12840 "<q:cat>",
12841 "<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>",
12842 );
12843 let plot = bar_plot("").replace(&plot_series(0), &styled);
12844 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
12845 let error = render_geometry(&chart.chart, chart_bounds()).unwrap_err();
12846
12847 assert!(error.to_string().contains("c:ser[0] direct fill paint"));
12848 assert!(error.to_string().contains("gradient"));
12849 }
12850
12851 #[test]
12852 fn resolved_chart_palette_raster_is_deterministic() {
12853 let raster = || {
12854 let chart = four_series_bar_chart();
12855 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12856 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12857 let layout = LayoutResult::new(
12858 vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
12859 fonts.all_font_data(),
12860 None,
12861 Vec::new(),
12862 );
12863 oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
12864 };
12865 let chart = four_series_bar_chart();
12866 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12867
12868 assert_eq!(path_colours(&geometry)[0], Color::from_hex("156082"));
12869 assert_eq!(raster(), raster());
12870 }
12871
12872 #[test]
12873 fn bar_geometry_handles_direction_grouping_gap_and_overlap() {
12874 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12875 let Plot::Bar {
12876 overlap, series, ..
12877 } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
12878 else {
12879 panic!("expected bar plot");
12880 };
12881 let mut second = series[0].clone();
12882 second.index = 1;
12883 second.order = 1;
12884 second.values.values = vec![2.0, 1.0];
12885 series.push(second);
12886
12887 *overlap = 25;
12888 let clustered = render_geometry(&chart.chart, chart_bounds()).unwrap();
12889 let clustered_bounds = path_bounds(&clustered);
12890 let clustered_width = 30.4 / 1.75;
12891 let clustered_advance = clustered_width * 0.75;
12892 assert_rects_near(
12893 &clustered_bounds,
12894 &[
12895 Rect {
12896 x: 58.8,
12897 y: 62.0,
12898 width: clustered_width,
12899 height: 50.0,
12900 },
12901 Rect {
12902 x: 134.8,
12903 y: 12.0,
12904 width: clustered_width,
12905 height: 100.0,
12906 },
12907 Rect {
12908 x: 58.8 + clustered_advance,
12909 y: 12.0,
12910 width: clustered_width,
12911 height: 100.0,
12912 },
12913 Rect {
12914 x: 134.8 + clustered_advance,
12915 y: 62.0,
12916 width: clustered_width,
12917 height: 50.0,
12918 },
12919 ],
12920 );
12921
12922 let Plot::Bar { direction, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12923 unreachable!()
12924 };
12925 *direction = BarDirection::Bar;
12926 let horizontal = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
12927 let horizontal_width = 20.0 / 1.75;
12928 let horizontal_advance = horizontal_width * 0.75;
12929 assert_rects_near(
12930 &horizontal,
12931 &[
12932 Rect {
12933 x: 36.0,
12934 y: 27.0,
12935 width: 76.0,
12936 height: horizontal_width,
12937 },
12938 Rect {
12939 x: 36.0,
12940 y: 77.0,
12941 width: 152.0,
12942 height: horizontal_width,
12943 },
12944 Rect {
12945 x: 36.0,
12946 y: 27.0 + horizontal_advance,
12947 width: 152.0,
12948 height: horizontal_width,
12949 },
12950 Rect {
12951 x: 36.0,
12952 y: 77.0 + horizontal_advance,
12953 width: 76.0,
12954 height: horizontal_width,
12955 },
12956 ],
12957 );
12958
12959 let Plot::Bar {
12960 direction,
12961 grouping,
12962 ..
12963 } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
12964 else {
12965 unreachable!()
12966 };
12967 *direction = BarDirection::Column;
12968 *grouping = BarGrouping::Stacked;
12969 let stacked = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
12970 assert_rects_near(
12971 &stacked,
12972 &[
12973 Rect {
12974 x: 58.8,
12975 y: 112.0 - 100.0 / 3.0,
12976 width: 30.4,
12977 height: 100.0 / 3.0,
12978 },
12979 Rect {
12980 x: 134.8,
12981 y: 112.0 - 200.0 / 3.0,
12982 width: 30.4,
12983 height: 200.0 / 3.0,
12984 },
12985 Rect {
12986 x: 58.8,
12987 y: 12.0,
12988 width: 30.4,
12989 height: 200.0 / 3.0,
12990 },
12991 Rect {
12992 x: 134.8,
12993 y: 12.0,
12994 width: 30.4,
12995 height: 100.0 / 3.0,
12996 },
12997 ],
12998 );
12999
13000 let Plot::Bar {
13001 grouping, series, ..
13002 } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
13003 else {
13004 unreachable!()
13005 };
13006 *grouping = BarGrouping::PercentStacked;
13007 series[1].values.values = vec![2.0, 2.0];
13008 let percent = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
13009 assert_rects_near(
13010 &percent,
13011 &[
13012 Rect {
13013 x: 58.8,
13014 y: 112.0 - 100.0 / 3.0,
13015 width: 30.4,
13016 height: 100.0 / 3.0,
13017 },
13018 Rect {
13019 x: 134.8,
13020 y: 62.0,
13021 width: 30.4,
13022 height: 50.0,
13023 },
13024 Rect {
13025 x: 58.8,
13026 y: 12.0,
13027 width: 30.4,
13028 height: 200.0 / 3.0,
13029 },
13030 Rect {
13031 x: 134.8,
13032 y: 12.0,
13033 width: 30.4,
13034 height: 50.0,
13035 },
13036 ],
13037 );
13038 }
13039
13040 #[test]
13041 fn line_scatter_and_radar_emit_paths_and_markers() {
13042 let line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
13043 let line_geometry = render_geometry(&line.chart, chart_bounds()).unwrap();
13044 assert_path_points(
13045 path_commands(&line_geometry)[0],
13046 &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
13047 false,
13048 );
13049 assert_rects_near(
13050 &path_bounds(&line_geometry)[1..],
13051 &[
13052 Rect {
13053 x: 71.5,
13054 y: 109.5,
13055 width: 5.0,
13056 height: 5.0,
13057 },
13058 Rect {
13059 x: 147.5,
13060 y: 9.5,
13061 width: 5.0,
13062 height: 5.0,
13063 },
13064 ],
13065 );
13066
13067 let (scatter_plot, _) = remaining_plot_fixtures()
13068 .into_iter()
13069 .find(|(kind, _, _)| *kind == "scatterChart")
13070 .map(|(_, plot, axes)| (plot, axes))
13071 .unwrap();
13072 let mut scatter =
13073 CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
13074 .unwrap();
13075 let Plot::Scatter { style, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
13076 else {
13077 unreachable!()
13078 };
13079 *style = ScatterStyle::LineMarker;
13080 let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
13081 assert_path_points(
13082 path_commands(&scatter_geometry)[0],
13083 &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
13084 false,
13085 );
13086 assert_rects_near(
13087 &path_bounds(&scatter_geometry)[1..],
13088 &[
13089 Rect {
13090 x: 33.5,
13091 y: 109.5,
13092 width: 5.0,
13093 height: 5.0,
13094 },
13095 Rect {
13096 x: 185.5,
13097 y: 9.5,
13098 width: 5.0,
13099 height: 5.0,
13100 },
13101 ],
13102 );
13103
13104 let (radar_plot, _) = remaining_plot_fixtures()
13105 .into_iter()
13106 .find(|(kind, _, _)| *kind == "radarChart")
13107 .map(|(_, plot, axes)| (plot, axes))
13108 .unwrap();
13109 let radar = CT_ChartSpace::from_xml(chart_with_optional_axes(&radar_plot, true).as_bytes())
13110 .unwrap();
13111 let radar_geometry = render_geometry(&radar.chart, chart_bounds()).unwrap();
13112 assert_path_points(
13113 path_commands(&radar_geometry)[0],
13114 &[Point { x: 112.0, y: 37.0 }, Point { x: 112.0, y: 112.0 }],
13115 true,
13116 );
13117 assert_rects_near(
13118 &path_bounds(&radar_geometry)[1..],
13119 &[
13120 Rect {
13121 x: 109.5,
13122 y: 34.5,
13123 width: 5.0,
13124 height: 5.0,
13125 },
13126 Rect {
13127 x: 109.5,
13128 y: 109.5,
13129 width: 5.0,
13130 height: 5.0,
13131 },
13132 ],
13133 );
13134 }
13135
13136 #[test]
13137 fn pie_doughnut_and_area_emit_closed_paths() {
13138 let fixtures = remaining_plot_fixtures();
13139 let pie_plot = fixtures
13140 .iter()
13141 .find(|(kind, _, _)| *kind == "pieChart")
13142 .unwrap();
13143 let pie =
13144 CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot.1, pie_plot.2).as_bytes())
13145 .unwrap();
13146 let pie_geometry = render_geometry(&pie.chart, chart_bounds()).unwrap();
13147 let pie_commands = path_commands(&pie_geometry);
13148 assert_eq!(pie_commands.len(), 2);
13149 let [
13150 PathCommand::MoveTo(center),
13151 PathCommand::LineTo(start),
13152 ..,
13153 PathCommand::CurveTo { to: end, .. },
13154 PathCommand::Close,
13155 ] = pie_commands[0]
13156 else {
13157 panic!("expected closed cubic pie wedge");
13158 };
13159 assert_point_near(*center, Point { x: 112.0, y: 62.0 });
13160 assert_point_near(
13161 *start,
13162 Point {
13163 x: 137.0,
13164 y: 62.0 - 25.0 * 3.0_f64.sqrt(),
13165 },
13166 );
13167 assert_point_near(
13168 *end,
13169 Point {
13170 x: 137.0,
13171 y: 62.0 + 25.0 * 3.0_f64.sqrt(),
13172 },
13173 );
13174
13175 let doughnut_plot = fixtures
13176 .iter()
13177 .find(|(kind, _, _)| *kind == "doughnutChart")
13178 .unwrap();
13179 let doughnut = CT_ChartSpace::from_xml(
13180 chart_with_optional_axes(&doughnut_plot.1, doughnut_plot.2).as_bytes(),
13181 )
13182 .unwrap();
13183 let doughnut_geometry = render_geometry(&doughnut.chart, chart_bounds()).unwrap();
13184 let doughnut_commands = path_commands(&doughnut_geometry);
13185 assert_eq!(doughnut_commands.len(), 2);
13186 let commands = doughnut_commands[0];
13187 assert_eq!(commands.len(), 7);
13188 let PathCommand::MoveTo(outer_start) = commands[0] else {
13189 panic!("expected doughnut outer start");
13190 };
13191 let PathCommand::LineTo(inner_end) = commands[3] else {
13192 panic!("expected doughnut inner-radius join");
13193 };
13194 let PathCommand::CurveTo {
13195 to: inner_start, ..
13196 } = commands[5]
13197 else {
13198 panic!("expected reverse inner arc");
13199 };
13200 let diagonal = 25.0 * 2.0_f64.sqrt();
13201 assert_point_near(
13202 outer_start,
13203 Point {
13204 x: 112.0 + diagonal,
13205 y: 62.0 - diagonal,
13206 },
13207 );
13208 assert_point_near(
13209 inner_end,
13210 Point {
13211 x: 112.0 + 30.0 * 75.0_f64.to_radians().cos(),
13212 y: 62.0 + 30.0 * 75.0_f64.to_radians().sin(),
13213 },
13214 );
13215 let inner_diagonal = 15.0 * 2.0_f64.sqrt();
13216 assert_point_near(
13217 inner_start,
13218 Point {
13219 x: 112.0 + inner_diagonal,
13220 y: 62.0 - inner_diagonal,
13221 },
13222 );
13223 assert_eq!(commands.last(), Some(&PathCommand::Close));
13224
13225 let area_plot = fixtures
13226 .iter()
13227 .find(|(kind, _, _)| *kind == "areaChart")
13228 .unwrap();
13229 let mut area =
13230 CT_ChartSpace::from_xml(chart_with_optional_axes(&area_plot.1, area_plot.2).as_bytes())
13231 .unwrap();
13232 let area_geometry = render_geometry(&area.chart, chart_bounds()).unwrap();
13233 assert_path_points(
13234 path_commands(&area_geometry)[0],
13235 &[
13236 Point { x: 74.0, y: 62.0 },
13237 Point { x: 150.0, y: 12.0 },
13238 Point { x: 150.0, y: 112.0 },
13239 Point { x: 74.0, y: 112.0 },
13240 ],
13241 true,
13242 );
13243
13244 let Plot::Area {
13245 grouping, series, ..
13246 } = &mut area.chart.plot_area.plots_mut().unwrap()[0]
13247 else {
13248 unreachable!()
13249 };
13250 *grouping = Grouping::Stacked;
13251 let mut second = series[0].clone();
13252 second.index = 1;
13253 second.order = 1;
13254 second.values.values = vec![2.0, 1.0];
13255 series.push(second);
13256 let stacked_area = render_geometry(&area.chart, chart_bounds()).unwrap();
13257 assert_path_points(
13258 path_commands(&stacked_area)[1],
13259 &[
13260 Point { x: 74.0, y: 12.0 },
13261 Point { x: 150.0, y: 12.0 },
13262 Point {
13263 x: 150.0,
13264 y: 112.0 - 200.0 / 3.0,
13265 },
13266 Point {
13267 x: 74.0,
13268 y: 112.0 - 100.0 / 3.0,
13269 },
13270 ],
13271 true,
13272 );
13273 }
13274
13275 #[test]
13276 fn sparse_cache_indexes_preserve_slots_and_scatter_pairing() {
13277 let series = sparse_category_series();
13278 let bar = format!(
13279 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>"#
13280 );
13281 let bar = CT_ChartSpace::from_xml(chart_with_plot(&bar).as_bytes()).unwrap();
13282 assert_rects_near(
13283 &path_bounds(&render_geometry(&bar.chart, chart_bounds()).unwrap()),
13284 &[
13285 Rect {
13286 x: 51.2,
13287 y: 62.0,
13288 width: 304.0 / 15.0,
13289 height: 50.0,
13290 },
13291 Rect {
13292 x: 36.0 + 304.0 / 3.0 + 15.2,
13293 y: 12.0,
13294 width: 304.0 / 15.0,
13295 height: 100.0,
13296 },
13297 ],
13298 );
13299
13300 let line = format!(
13301 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>"#
13302 );
13303 let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line).as_bytes()).unwrap();
13304 let first = Point {
13305 x: 36.0 + 76.0 / 3.0,
13306 y: 62.0,
13307 };
13308 let missing = Point { x: 112.0, y: 112.0 };
13309 let last = Point {
13310 x: 36.0 + 380.0 / 3.0,
13311 y: 12.0,
13312 };
13313 let gap = render_geometry(&line.chart, chart_bounds()).unwrap();
13314 let gap_commands = path_commands(&gap);
13315 assert_eq!(gap_commands.len(), 2);
13316 assert_path_points(
13317 gap_commands[0],
13318 &[Point {
13319 x: first.x,
13320 y: 112.0,
13321 }],
13322 false,
13323 );
13324 assert_path_points(gap_commands[1], &[last], false);
13325 line.chart.disp_blanks_as = DispBlanksAs::Zero;
13326 assert_path_points(
13327 path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
13328 &[first, missing, last],
13329 false,
13330 );
13331 line.chart.disp_blanks_as = DispBlanksAs::Span;
13332 assert_path_points(
13333 path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
13334 &[
13335 Point {
13336 x: first.x,
13337 y: 112.0,
13338 },
13339 last,
13340 ],
13341 false,
13342 );
13343
13344 let area = format!(
13345 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:areaChart>"#
13346 );
13347 let mut area = CT_ChartSpace::from_xml(chart_with_plot(&area).as_bytes()).unwrap();
13348 let area_gap = render_geometry(&area.chart, chart_bounds()).unwrap();
13349 let area_gap_commands = path_commands(&area_gap);
13350 assert_eq!(area_gap_commands.len(), 2);
13351 assert_path_points(
13352 area_gap_commands[0],
13353 &[
13354 first,
13355 Point {
13356 x: first.x,
13357 y: 112.0,
13358 },
13359 ],
13360 true,
13361 );
13362 assert_path_points(
13363 area_gap_commands[1],
13364 &[
13365 last,
13366 Point {
13367 x: last.x,
13368 y: 112.0,
13369 },
13370 ],
13371 true,
13372 );
13373 area.chart.disp_blanks_as = DispBlanksAs::Zero;
13374 assert_path_points(
13375 path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
13376 &[
13377 first,
13378 missing,
13379 last,
13380 Point {
13381 x: last.x,
13382 y: 112.0,
13383 },
13384 missing,
13385 Point {
13386 x: first.x,
13387 y: 112.0,
13388 },
13389 ],
13390 true,
13391 );
13392 area.chart.disp_blanks_as = DispBlanksAs::Span;
13393 assert_path_points(
13394 path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
13395 &[
13396 first,
13397 last,
13398 Point {
13399 x: last.x,
13400 y: 112.0,
13401 },
13402 Point {
13403 x: first.x,
13404 y: 112.0,
13405 },
13406 ],
13407 true,
13408 );
13409
13410 let radar = format!(
13411 r#"<q:radarChart><q:radarStyle val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#
13412 );
13413 let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
13414 assert_path_points(
13415 path_commands(&render_geometry(&radar.chart, chart_bounds()).unwrap())[0],
13416 &[
13417 Point { x: 112.0, y: 37.0 },
13418 Point {
13419 x: 112.0 - 25.0 * 3.0_f64.sqrt(),
13420 y: 87.0,
13421 },
13422 ],
13423 true,
13424 );
13425
13426 let scatter = format!(
13427 r#"<q:scatterChart><q:scatterStyle val="lineMarker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:scatterChart>"#,
13428 sparse_scatter_series()
13429 );
13430 let mut scatter = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
13431 let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
13432 assert_eq!(geometry_children(&scatter_geometry).len(), 4);
13433 let gap_commands = path_commands(&scatter_geometry);
13434 assert_path_points(gap_commands[0], &[Point { x: 36.0, y: 112.0 }], false);
13435 assert_path_points(gap_commands[1], &[Point { x: 188.0, y: 12.0 }], false);
13436
13437 scatter.chart.disp_blanks_as = DispBlanksAs::Zero;
13438 let zero = render_geometry(&scatter.chart, chart_bounds()).unwrap();
13439 assert_path_points(
13440 path_commands(&zero)[0],
13441 &[
13442 Point { x: 74.0, y: 87.0 },
13443 Point { x: 36.0, y: 62.0 },
13444 Point { x: 150.0, y: 112.0 },
13445 Point { x: 188.0, y: 12.0 },
13446 ],
13447 false,
13448 );
13449
13450 scatter.chart.disp_blanks_as = DispBlanksAs::Span;
13451 assert_path_points(
13452 path_commands(&render_geometry(&scatter.chart, chart_bounds()).unwrap())[0],
13453 &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
13454 false,
13455 );
13456 }
13457
13458 #[test]
13459 fn sparse_scatter_domains_and_labels_use_only_matched_points() {
13460 let scatter = format!(
13461 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>"#,
13462 sparse_scatter_series()
13463 );
13464 let chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
13465 let plot = &chart.chart.plot_area.plots().unwrap()[0];
13466 assert_eq!(
13467 super::scatter_domains(plot, DispBlanksAs::Gap).unwrap(),
13468 (
13469 Domain {
13470 min: 10.0,
13471 max: 40.0,
13472 },
13473 Domain {
13474 min: 100.0,
13475 max: 400.0,
13476 },
13477 )
13478 );
13479 let axes = chart.chart.plot_area.axes().unwrap();
13480 let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
13481 let anchors =
13482 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
13483 assert_eq!(
13484 anchors.keys().copied().collect::<Vec<_>>(),
13485 vec![(0, 0), (0, 3)]
13486 );
13487 assert_eq!(anchors[&(0, 0)].center, Point { x: 0.0, y: 140.0 });
13488 assert_eq!(anchors[&(0, 3)].center, Point { x: 200.0, y: 0.0 });
13489 }
13490
13491 #[test]
13492 fn sparse_bubble_size_labels_resolve_logical_indexes() {
13493 let scatter = format!(
13494 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>"#,
13495 sparse_scatter_series()
13496 );
13497 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
13498 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13499 let Plot::Scatter { series, .. } = plot else {
13500 panic!("expected scatter plot");
13501 };
13502 series[0].bubble_size = Some(
13503 NumericData::from_xml(
13504 format!(
13505 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>"#
13506 )
13507 .as_bytes(),
13508 )
13509 .unwrap(),
13510 );
13511 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13512 let mut rendered = Vec::new();
13513 super::render_data_labels(
13514 plot,
13515 chart_bounds(),
13516 DispBlanksAs::Gap,
13517 super::GeometryOptions::default(),
13518 &mut fonts,
13519 &mut rendered,
13520 )
13521 .unwrap();
13522 let text: Vec<_> = rendered
13523 .iter()
13524 .filter_map(|element| match element {
13525 PositionedElement::Text(run) => Some(run.text.as_str()),
13526 _ => None,
13527 })
13528 .collect();
13529 assert_eq!(text, vec!["5", "9"]);
13530 }
13531
13532 #[test]
13533 fn geometry_rejects_invalid_bounds_and_opaque_plots() {
13534 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13535 for bounds in [
13536 Rect {
13537 x: 0.0,
13538 y: 0.0,
13539 width: f64::NAN,
13540 height: 100.0,
13541 },
13542 Rect {
13543 x: 0.0,
13544 y: 0.0,
13545 width: 0.0,
13546 height: 100.0,
13547 },
13548 Rect {
13549 x: 0.0,
13550 y: 0.0,
13551 width: 40.0,
13552 height: 30.0,
13553 },
13554 ] {
13555 assert!(render_geometry(&chart.chart, bounds).is_err());
13556 }
13557
13558 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
13559 unreachable!()
13560 };
13561 series[0].values.values.clear();
13562 assert!(render_geometry(&chart.chart, chart_bounds()).is_err());
13563
13564 let opaque = CT_ChartSpace::from_xml(
13565 chart_with_plot(r#"<q:bar3DChart><q:barDir val="col"/></q:bar3DChart>"#).as_bytes(),
13566 )
13567 .unwrap();
13568 let error = render_geometry(&opaque.chart, chart_bounds()).unwrap_err();
13569 assert!(error.to_string().contains("unsupported or combination"));
13570
13571 let (scatter_plot, _) = remaining_plot_fixtures()
13572 .into_iter()
13573 .find(|(kind, _, _)| *kind == "scatterChart")
13574 .map(|(_, plot, axes)| (plot, axes))
13575 .unwrap();
13576 let mut scatter =
13577 CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
13578 .unwrap();
13579 for invalid in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
13580 let Plot::Scatter { series, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
13581 else {
13582 unreachable!()
13583 };
13584 let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
13585 unreachable!()
13586 };
13587 x_values.values[0] = invalid;
13588 let error = render_geometry(&scatter.chart, chart_bounds()).unwrap_err();
13589 assert!(error.to_string().contains("xVal"));
13590 }
13591 }
13592
13593 #[test]
13594 fn finite_extremes_never_produce_nonfinite_geometry() {
13595 let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
13596 let Plot::Line { series, .. } = &mut line.chart.plot_area.plots_mut().unwrap()[0] else {
13597 unreachable!()
13598 };
13599 series[0].values.values = vec![-f64::MAX, f64::MAX];
13600 let extreme_line = render_geometry(&line.chart, chart_bounds()).unwrap();
13601 assert_path_points(
13602 path_commands(&extreme_line)[0],
13603 &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
13604 false,
13605 );
13606
13607 let mut stacked =
13608 CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13609 let Plot::Bar {
13610 grouping, series, ..
13611 } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
13612 else {
13613 unreachable!()
13614 };
13615 *grouping = BarGrouping::Stacked;
13616 series[0].values.values = vec![f64::MAX, 1.0];
13617 let mut second = series[0].clone();
13618 second.index = 1;
13619 second.order = 1;
13620 series.push(second);
13621 let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
13622 assert!(error.to_string().contains("stacked value total"));
13623
13624 let Plot::Bar { grouping, .. } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
13625 else {
13626 unreachable!()
13627 };
13628 *grouping = BarGrouping::PercentStacked;
13629 let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
13630 assert!(error.to_string().contains("stacked percentage total"));
13631
13632 let (pie_plot, _) = remaining_plot_fixtures()
13633 .into_iter()
13634 .find(|(kind, _, _)| *kind == "pieChart")
13635 .map(|(_, plot, axes)| (plot, axes))
13636 .unwrap();
13637 let mut pie =
13638 CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot, false).as_bytes()).unwrap();
13639 let Plot::Pie { series, .. } = &mut pie.chart.plot_area.plots_mut().unwrap()[0] else {
13640 unreachable!()
13641 };
13642 series[0].values.values = vec![f64::MAX, f64::MAX];
13643 let error = render_geometry(&pie.chart, chart_bounds()).unwrap_err();
13644 assert!(error.to_string().contains("pie value total"));
13645
13646 let (scatter_plot, _) = remaining_plot_fixtures()
13647 .into_iter()
13648 .find(|(kind, _, _)| *kind == "scatterChart")
13649 .map(|(_, plot, axes)| (plot, axes))
13650 .unwrap();
13651 let mut scatter =
13652 CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
13653 .unwrap();
13654 let Plot::Scatter { style, series, .. } =
13655 &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
13656 else {
13657 unreachable!()
13658 };
13659 *style = ScatterStyle::Line;
13660 let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
13661 unreachable!()
13662 };
13663 x_values.values = vec![-f64::MAX, f64::MAX];
13664 series[0].values.values = vec![-f64::MAX, f64::MAX];
13665 let extreme_scatter = render_geometry(&scatter.chart, chart_bounds()).unwrap();
13666 assert_path_points(
13667 path_commands(&extreme_scatter)[0],
13668 &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
13669 false,
13670 );
13671 }
13672
13673 #[test]
13674 fn geometry_is_backend_neutral_and_deterministic() {
13675 let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13676 let first = render_geometry(&chart.chart, chart_bounds()).unwrap();
13677 let second = render_geometry(&chart.chart, chart_bounds()).unwrap();
13678 assert_eq!(
13679 format!("{:?}", first.elements),
13680 format!("{:?}", second.elements)
13681 );
13682
13683 let raster = |geometry: ChartGeometry| {
13684 let layout = LayoutResult::new(
13685 vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
13686 Vec::new(),
13687 None,
13688 Vec::new(),
13689 );
13690 oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
13691 };
13692 assert_eq!(raster(first), raster(second));
13693 }
13694
13695 #[test]
13696 fn zero_to_one_hundred_axis_uses_expected_ticks() {
13697 let scale = nice_number_scale(
13698 Domain {
13699 min: 0.0,
13700 max: 100.0,
13701 },
13702 None,
13703 None,
13704 6,
13705 )
13706 .unwrap();
13707 assert_eq!(scale.ticks, vec![0.0, 20.0, 40.0, 60.0, 80.0, 100.0]);
13708 }
13709
13710 #[test]
13711 fn nice_number_ticks_cover_unpinned_extents() {
13712 for domain in [
13713 Domain {
13714 min: 3.0,
13715 max: 97.0,
13716 },
13717 Domain {
13718 min: -97.0,
13719 max: -3.0,
13720 },
13721 Domain {
13722 min: -3.5,
13723 max: 8.25,
13724 },
13725 Domain {
13726 min: 0.125,
13727 max: 0.875,
13728 },
13729 Domain {
13730 min: 1.0e12,
13731 max: 7.0e12,
13732 },
13733 Domain {
13734 min: -f64::MAX,
13735 max: f64::MAX,
13736 },
13737 Domain {
13738 min: f64::from_bits(1),
13739 max: f64::from_bits(5),
13740 },
13741 ] {
13742 let scale = nice_number_scale(domain, None, None, 6).unwrap();
13743 assert!(scale.domain.min <= domain.min);
13744 assert!(scale.domain.max >= domain.max);
13745 assert!(scale.ticks.iter().all(|tick| tick.is_finite()));
13746 assert!(scale.ticks.windows(2).all(|pair| pair[0] < pair[1]));
13747 }
13748
13749 let constant = nice_number_scale(Domain { min: 4.0, max: 4.0 }, None, None, 6).unwrap();
13750 assert!(constant.domain.min < 4.0 && constant.domain.max > 4.0);
13751 }
13752
13753 #[test]
13754 fn one_sided_explicit_scale_beyond_data_derives_a_valid_opposite_bound() {
13755 let minimum = nice_number_scale(
13756 Domain {
13757 min: 0.0,
13758 max: 10.0,
13759 },
13760 Some(20.0),
13761 None,
13762 6,
13763 )
13764 .unwrap();
13765 assert_eq!(minimum.domain.min, 20.0);
13766 assert!(minimum.domain.max > 20.0);
13767 assert!(minimum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
13768
13769 let maximum = nice_number_scale(
13770 Domain {
13771 min: 0.0,
13772 max: 10.0,
13773 },
13774 None,
13775 Some(-20.0),
13776 6,
13777 )
13778 .unwrap();
13779 assert_eq!(maximum.domain.max, -20.0);
13780 assert!(maximum.domain.min < -20.0);
13781 assert!(maximum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
13782 }
13783
13784 #[test]
13785 fn standard_line_domain_does_not_force_zero() {
13786 let mut chart =
13787 CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
13788 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13789 let Plot::Line { series, .. } = plot else {
13790 panic!("expected line plot");
13791 };
13792 series[0].values.values = vec![1_000.0, 1_010.0];
13793 assert_eq!(
13794 super::plot_value_domain(plot, DispBlanksAs::Gap).unwrap(),
13795 Some(Domain {
13796 min: 1_000.0,
13797 max: 1_010.0,
13798 })
13799 );
13800 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
13801 assert_path_points(
13802 path_commands(&geometry)[0],
13803 &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
13804 false,
13805 );
13806 }
13807
13808 #[test]
13809 fn explicit_scaling_bounds_and_orientation_are_honored() {
13810 let scale = nice_number_scale(
13811 Domain {
13812 min: 25.0,
13813 max: 75.0,
13814 },
13815 Some(0.0),
13816 Some(100.0),
13817 6,
13818 )
13819 .unwrap();
13820 assert_eq!(
13821 scale.domain,
13822 Domain {
13823 min: 0.0,
13824 max: 100.0
13825 }
13826 );
13827 assert_eq!(
13828 super::map_y_oriented(0.0, scale.domain, chart_bounds(), Orientation::MaxMin).unwrap(),
13829 0.0
13830 );
13831 assert_eq!(
13832 super::map_y_oriented(100.0, scale.domain, chart_bounds(), Orientation::MaxMin)
13833 .unwrap(),
13834 140.0
13835 );
13836
13837 let mut chart = labelled_bar_chart();
13838 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
13839 panic!("expected bar plot");
13840 };
13841 series[0].values.values = vec![25.0, 75.0];
13842 let value_axis = chart
13843 .chart
13844 .plot_area
13845 .axes
13846 .iter_mut()
13847 .find(|axis| axis.kind == AxisKind::Value)
13848 .unwrap();
13849 value_axis.scaling.orientation = Orientation::MaxMin;
13850 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13851 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13852 let plot = group
13853 .children
13854 .iter()
13855 .find_map(|element| match element {
13856 PositionedElement::Group(group) => Some(group),
13857 _ => None,
13858 })
13859 .unwrap();
13860 let bars: Vec<_> = plot
13861 .children
13862 .iter()
13863 .filter_map(|element| match element {
13864 PositionedElement::Path(path) => path.path.bounds(),
13865 _ => None,
13866 })
13867 .collect();
13868 assert_eq!(bars.len(), 2);
13869 assert!((bars[0].y - 12.0).abs() < 1.0e-9);
13870 assert!((bars[0].height - 25.0).abs() < 1.0e-9);
13871 assert!((bars[1].height - 75.0).abs() < 1.0e-9);
13872 }
13873
13874 #[test]
13875 fn family_label_anchors_share_rendered_geometry() {
13876 let mut bar = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13877 let bar_plot = &mut bar.chart.plot_area.plots_mut().unwrap()[0];
13878 let Plot::Bar { series, .. } = bar_plot else {
13879 panic!("expected bar plot");
13880 };
13881 let mut second = series[0].clone();
13882 second.index = 1;
13883 second.order = 1;
13884 series.push(second);
13885 let anchors = super::plot_label_anchors(
13886 bar_plot,
13887 chart_bounds(),
13888 DispBlanksAs::Gap,
13889 super::GeometryOptions::default(),
13890 )
13891 .unwrap();
13892 assert_point_near(anchors[&(0, 0)].center, Point { x: 40.0, y: 105.0 });
13893 assert_point_near(anchors[&(1, 0)].center, Point { x: 60.0, y: 105.0 });
13894
13895 let pie = CT_ChartSpace::from_xml(
13896 chart_with_optional_axes(
13897 &remaining_plot_fixtures()
13898 .into_iter()
13899 .find(|(kind, _, _)| *kind == "pieChart")
13900 .unwrap()
13901 .1,
13902 false,
13903 )
13904 .as_bytes(),
13905 )
13906 .unwrap();
13907 let Plot::Pie {
13908 first_slice_angle,
13909 series,
13910 ..
13911 } = &pie.chart.plot_area.plots().unwrap()[0]
13912 else {
13913 panic!("expected pie plot");
13914 };
13915 let slices = super::pie_slices(*first_slice_angle, None, series, chart_bounds()).unwrap();
13916 assert_eq!(slices.len(), 2);
13917 let first_span = std::f64::consts::TAU / 3.0;
13918 let first_midpoint = f64::from(*first_slice_angle).to_radians()
13919 - std::f64::consts::FRAC_PI_2
13920 + first_span / 2.0;
13921 assert_point_near(
13922 slices[0].anchor,
13923 super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, first_midpoint),
13924 );
13925 let second_midpoint = first_midpoint + std::f64::consts::PI;
13926 assert_point_near(
13927 slices[1].anchor,
13928 super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, second_midpoint),
13929 );
13930
13931 let radar = format!(
13932 r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
13933 plot_series(0)
13934 );
13935 let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
13936 let Plot::Radar { series, .. } = &radar.chart.plot_area.plots().unwrap()[0] else {
13937 panic!("expected radar plot");
13938 };
13939 let radar_points =
13940 super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
13941 .unwrap();
13942 assert_eq!(radar_points[0][0], (0, Point { x: 100.0, y: 35.0 }));
13943 assert_eq!(radar_points[0][1], (1, Point { x: 100.0, y: 140.0 }));
13944 }
13945
13946 #[test]
13947 fn radar_annotations_use_spokes_perimeter_labels_and_radial_gridlines() {
13948 let radar_plot = format!(
13949 r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
13950 sparse_category_series()
13951 );
13952 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
13953 for axis in &mut chart.chart.plot_area.axes {
13954 axis.major_tick_mark = TickMark::Cross;
13955 if axis.kind == AxisKind::Value {
13956 axis.major_gridlines = Some(super::ChartLines::default());
13957 }
13958 }
13959 let plot = &chart.chart.plot_area.plots().unwrap()[0];
13960 let axes = chart.chart.plot_area.axes().unwrap();
13961 let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
13962 let center = Point { x: 112.0, y: 62.0 };
13963 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13964 let annotations =
13965 super::render_axes_and_labels(plot, &axes, bounds, DispBlanksAs::Gap, &mut fonts)
13966 .unwrap();
13967 assert_eq!(annotations.axis_lines.len(), 4);
13968 for spoke in annotations.axis_lines.iter().take(3) {
13969 let PositionedElement::Path(spoke) = spoke else {
13970 panic!("radar spoke must be a path");
13971 };
13972 assert_eq!(
13973 spoke.path.commands.first(),
13974 Some(&PathCommand::MoveTo(center))
13975 );
13976 }
13977 assert!(!annotations.gridlines.is_empty());
13978 for gridline in &annotations.gridlines {
13979 let PositionedElement::Path(gridline) = gridline else {
13980 panic!("radar gridline must be a path");
13981 };
13982 assert_eq!(gridline.path.commands.last(), Some(&PathCommand::Close));
13983 let radii: Vec<_> = gridline
13984 .path
13985 .commands
13986 .iter()
13987 .filter_map(|command| match command {
13988 PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
13989 Some((point.x - center.x).hypot(point.y - center.y))
13990 }
13991 _ => None,
13992 })
13993 .collect();
13994 assert_eq!(radii.len(), 3);
13995 assert!(
13996 radii
13997 .windows(2)
13998 .all(|pair| (pair[0] - pair[1]).abs() < 1.0e-9)
13999 );
14000 }
14001 let text: Vec<_> = annotations
14002 .labels
14003 .iter()
14004 .filter_map(|element| match element {
14005 PositionedElement::Text(run) => Some(run.text.as_str()),
14006 _ => None,
14007 })
14008 .collect();
14009 assert!(text.contains(&"North"));
14010 assert!(text.contains(&"West"));
14011 }
14012
14013 #[test]
14014 fn radar_tick_label_positions_are_distinct() {
14015 let radar_plot = format!(
14016 r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
14017 plot_series(0)
14018 );
14019 let chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
14020 let plot = &chart.chart.plot_area.plots().unwrap()[0];
14021 let axes = chart.chart.plot_area.axes().unwrap();
14022 let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
14023 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14024
14025 let category_axis = axes
14026 .iter()
14027 .find(|axis| axis.kind == AxisKind::Category)
14028 .unwrap();
14029 for (position, expected) in [
14030 (TickLabelPosition::High, Point { x: 108.0, y: 0.0 }),
14031 (TickLabelPosition::Low, Point { x: 108.0, y: 25.0 }),
14032 (TickLabelPosition::NextTo, Point { x: 108.0, y: 5.0 }),
14033 ] {
14034 let mut axis = category_axis.clone();
14035 axis.tick_label_position = position;
14036 let annotations =
14037 super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
14038 .unwrap();
14039 let north = annotations
14040 .labels
14041 .iter()
14042 .find_map(|element| match element {
14043 PositionedElement::Text(run) if run.text == "North" => Some(run),
14044 _ => None,
14045 })
14046 .unwrap();
14047 assert_point_near(north.origin, expected);
14048 }
14049 let mut hidden_category = category_axis.clone();
14050 hidden_category.tick_label_position = TickLabelPosition::None;
14051 let annotations = super::render_axes_and_labels(
14052 plot,
14053 &[hidden_category],
14054 bounds,
14055 DispBlanksAs::Gap,
14056 &mut fonts,
14057 )
14058 .unwrap();
14059 assert!(annotations.labels.is_empty());
14060
14061 let narrow_bounds = Rect {
14062 x: 36.0,
14063 y: 12.0,
14064 width: 24.0,
14065 height: 100.0,
14066 };
14067 let narrow_center = Point { x: 48.0, y: 62.0 };
14068 assert_eq!(
14069 super::radar_category_label_origin(
14070 narrow_bounds,
14071 narrow_center,
14072 12.0,
14073 1,
14074 4,
14075 TickLabelPosition::High,
14076 ),
14077 Some(Point { x: 72.0, y: 65.0 })
14078 );
14079 assert_eq!(
14080 super::radar_category_label_origin(
14081 narrow_bounds,
14082 narrow_center,
14083 12.0,
14084 1,
14085 4,
14086 TickLabelPosition::NextTo,
14087 ),
14088 Some(Point { x: 66.0, y: 65.0 })
14089 );
14090
14091 let value_axis = axes
14092 .iter()
14093 .find(|axis| axis.kind == AxisKind::Value)
14094 .unwrap();
14095 for (position, expected) in [
14096 (TickLabelPosition::High, Point { x: 166.0, y: 65.0 }),
14097 (TickLabelPosition::Low, Point { x: 32.0, y: 65.0 }),
14098 (TickLabelPosition::NextTo, Point { x: 116.0, y: 65.0 }),
14099 ] {
14100 let mut axis = value_axis.clone();
14101 axis.tick_label_position = position;
14102 let annotations =
14103 super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
14104 .unwrap();
14105 let zero = annotations
14106 .labels
14107 .iter()
14108 .find_map(|element| match element {
14109 PositionedElement::Text(run) if run.text == "0" => Some(run),
14110 _ => None,
14111 })
14112 .unwrap();
14113 assert_point_near(zero.origin, expected);
14114 }
14115 let mut hidden_value = value_axis.clone();
14116 hidden_value.tick_label_position = TickLabelPosition::None;
14117 let annotations = super::render_axes_and_labels(
14118 plot,
14119 &[hidden_value],
14120 bounds,
14121 DispBlanksAs::Gap,
14122 &mut fonts,
14123 )
14124 .unwrap();
14125 assert!(annotations.labels.is_empty());
14126 }
14127
14128 #[test]
14129 fn radar_negative_and_mixed_sign_domains_preserve_values() {
14130 let radar_plot = format!(
14131 r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
14132 plot_series(0)
14133 );
14134 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
14135 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14136 let Plot::Radar { series, .. } = plot else {
14137 panic!("expected radar plot");
14138 };
14139 series[0].values.values = vec![-10.0, -5.0];
14140 let points =
14141 super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
14142 .unwrap();
14143 assert_eq!(points[0][0], (0, Point { x: 100.0, y: 70.0 }));
14144 assert_eq!(points[0][1], (1, Point { x: 100.0, y: 105.0 }));
14145
14146 series[0].values.values = vec![-5.0, 10.0];
14147 let options = super::GeometryOptions {
14148 value: Some(super::AxisMapping {
14149 domain: Domain {
14150 min: -10.0,
14151 max: 10.0,
14152 },
14153 orientation: Orientation::MinMax,
14154 }),
14155 ..super::GeometryOptions::default()
14156 };
14157 let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
14158 assert_eq!(points[0][0], (0, Point { x: 100.0, y: 52.5 }));
14159 assert_eq!(points[0][1], (1, Point { x: 100.0, y: 140.0 }));
14160 }
14161
14162 #[test]
14163 fn radar_explicit_bounds_suppress_out_of_range_points_and_labels() {
14164 let radar_plot = format!(
14165 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>"#,
14166 plot_series(0)
14167 );
14168 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
14169 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14170 let Plot::Radar { series, .. } = plot else {
14171 panic!("expected radar plot");
14172 };
14173 series[0].values.values = vec![25.0, 75.0];
14174 let options = super::GeometryOptions {
14175 value: Some(super::AxisMapping {
14176 domain: Domain {
14177 min: 50.0,
14178 max: 100.0,
14179 },
14180 orientation: Orientation::MinMax,
14181 }),
14182 ..super::GeometryOptions::default()
14183 };
14184 let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
14185 assert_eq!(points[0], vec![(1, Point { x: 100.0, y: 105.0 })]);
14186 let anchors =
14187 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
14188 assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 1)]);
14189
14190 let Plot::Radar { series, .. } = plot else {
14191 panic!("expected radar plot");
14192 };
14193 series[0].values.values = vec![75.0, 125.0];
14194 let reversed = super::GeometryOptions {
14195 value: Some(super::AxisMapping {
14196 domain: Domain {
14197 min: 50.0,
14198 max: 100.0,
14199 },
14200 orientation: Orientation::MaxMin,
14201 }),
14202 ..super::GeometryOptions::default()
14203 };
14204 let points = super::radar_series_points(series, chart_bounds(), reversed).unwrap();
14205 assert_eq!(points[0], vec![(0, Point { x: 100.0, y: 35.0 })]);
14206 let anchors =
14207 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
14208 assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
14209
14210 let value_axis = chart
14211 .chart
14212 .plot_area
14213 .axes
14214 .iter_mut()
14215 .find(|axis| axis.kind == AxisKind::Value)
14216 .unwrap();
14217 value_axis.scaling.minimum = Some(50.0);
14218 value_axis.scaling.maximum = Some(100.0);
14219 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14220 let Plot::Radar { series, .. } = plot else {
14221 panic!("expected radar plot");
14222 };
14223 series[0].values.values = vec![25.0, 25.0];
14224 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14225 let rendered = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
14226 assert!(rendered.children.iter().any(|element| matches!(
14227 element,
14228 PositionedElement::Group(group) if group.clip.is_some() && group.children.is_empty()
14229 )));
14230 assert!(
14231 rendered
14232 .children
14233 .iter()
14234 .any(|element| matches!(element, PositionedElement::Path(_)))
14235 );
14236 assert!(!rendered.children.iter().any(|element| matches!(
14237 element,
14238 PositionedElement::Text(run) if run.text == "25"
14239 )));
14240 }
14241
14242 #[test]
14243 fn inside_and_outside_label_positions_follow_family_geometry() {
14244 let mut horizontal =
14245 CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14246 let plot = &mut horizontal.chart.plot_area.plots_mut().unwrap()[0];
14247 let Plot::Bar { direction, .. } = plot else {
14248 panic!("expected bar plot");
14249 };
14250 *direction = BarDirection::Bar;
14251 let anchors = super::plot_label_anchors(
14252 plot,
14253 chart_bounds(),
14254 DispBlanksAs::Gap,
14255 super::GeometryOptions::default(),
14256 )
14257 .unwrap();
14258 let anchor = anchors[&(0, 0)];
14259 assert_point_near(
14260 anchor.origin(Some(DataLabelPosition::InsideBase)),
14261 Point { x: 5.0, y: 35.0 },
14262 );
14263 assert_point_near(
14264 anchor.origin(Some(DataLabelPosition::InsideEnd)),
14265 Point { x: 95.0, y: 35.0 },
14266 );
14267 assert_point_near(
14268 anchor.origin(Some(DataLabelPosition::OutsideEnd)),
14269 Point { x: 105.0, y: 35.0 },
14270 );
14271
14272 let mut negative =
14273 CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14274 let plot = &mut negative.chart.plot_area.plots_mut().unwrap()[0];
14275 let Plot::Bar { series, .. } = plot else {
14276 panic!("expected bar plot");
14277 };
14278 series[0].values.values = vec![-1.0, -2.0];
14279 let anchors = super::plot_label_anchors(
14280 plot,
14281 chart_bounds(),
14282 DispBlanksAs::Gap,
14283 super::GeometryOptions::default(),
14284 )
14285 .unwrap();
14286 assert_point_near(
14287 anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
14288 Point { x: 50.0, y: 75.0 },
14289 );
14290 let reversed = super::GeometryOptions {
14291 value: Some(super::AxisMapping {
14292 domain: Domain {
14293 min: -2.0,
14294 max: 0.0,
14295 },
14296 orientation: Orientation::MaxMin,
14297 }),
14298 ..super::GeometryOptions::default()
14299 };
14300 let anchors =
14301 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
14302 assert_point_near(
14303 anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
14304 Point { x: 50.0, y: 65.0 },
14305 );
14306
14307 let pie = CT_ChartSpace::from_xml(
14308 chart_with_optional_axes(
14309 &remaining_plot_fixtures()
14310 .into_iter()
14311 .find(|(kind, _, _)| *kind == "pieChart")
14312 .unwrap()
14313 .1,
14314 false,
14315 )
14316 .as_bytes(),
14317 )
14318 .unwrap();
14319 let Plot::Pie { series, .. } = &pie.chart.plot_area.plots().unwrap()[0] else {
14320 panic!("expected pie plot");
14321 };
14322 let pie_slice = &super::pie_slices(30, None, series, chart_bounds()).unwrap()[0];
14323 assert_point_near(
14324 super::LabelAnchor::segment(pie_slice.anchor, pie_slice.base, pie_slice.end)
14325 .origin(Some(DataLabelPosition::OutsideEnd)),
14326 Point { x: 175.0, y: 70.0 },
14327 );
14328 let doughnut_slice = &super::pie_slices(30, Some(60), series, chart_bounds()).unwrap()[0];
14329 assert_point_near(
14330 super::LabelAnchor::segment(
14331 doughnut_slice.anchor,
14332 doughnut_slice.base,
14333 doughnut_slice.end,
14334 )
14335 .origin(Some(DataLabelPosition::InsideBase)),
14336 Point { x: 147.0, y: 70.0 },
14337 );
14338
14339 let short = super::LabelAnchor::segment(
14340 Point { x: 2.0, y: 0.0 },
14341 Point { x: 0.0, y: 0.0 },
14342 Point { x: 4.0, y: 0.0 },
14343 );
14344 assert_eq!(
14345 short.origin(Some(DataLabelPosition::InsideBase)),
14346 Point { x: 2.0, y: 0.0 }
14347 );
14348 assert_eq!(
14349 short.origin(Some(DataLabelPosition::InsideEnd)),
14350 Point { x: 2.0, y: 0.0 }
14351 );
14352 let thin_bounds = Rect {
14353 x: 0.0,
14354 y: 0.0,
14355 width: 80.0,
14356 height: 80.0,
14357 };
14358 let thin = &super::pie_slices(30, Some(90), series, thin_bounds).unwrap()[0];
14359 let thin = super::LabelAnchor::segment(thin.anchor, thin.base, thin.end);
14360 assert_point_near(
14361 thin.origin(Some(DataLabelPosition::InsideBase)),
14362 Point { x: 78.0, y: 40.0 },
14363 );
14364 assert_point_near(
14365 thin.origin(Some(DataLabelPosition::InsideEnd)),
14366 Point { x: 78.0, y: 40.0 },
14367 );
14368 }
14369
14370 #[test]
14371 fn degenerate_bar_and_radar_anchors_preserve_family_direction() {
14372 let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14373 let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
14374 let Plot::Bar { series, .. } = plot else {
14375 panic!("expected bar plot");
14376 };
14377 series[0].values.values = vec![80.0, 125.0];
14378 for (direction, orientation, boundary, outside) in [
14379 (
14380 BarDirection::Column,
14381 Orientation::MinMax,
14382 Point { x: 50.0, y: 140.0 },
14383 Point { x: 50.0, y: 135.0 },
14384 ),
14385 (
14386 BarDirection::Column,
14387 Orientation::MaxMin,
14388 Point { x: 50.0, y: 0.0 },
14389 Point { x: 50.0, y: 5.0 },
14390 ),
14391 (
14392 BarDirection::Bar,
14393 Orientation::MinMax,
14394 Point { x: 0.0, y: 35.0 },
14395 Point { x: 5.0, y: 35.0 },
14396 ),
14397 (
14398 BarDirection::Bar,
14399 Orientation::MaxMin,
14400 Point { x: 200.0, y: 35.0 },
14401 Point { x: 195.0, y: 35.0 },
14402 ),
14403 ] {
14404 let Plot::Bar {
14405 direction: plot_direction,
14406 ..
14407 } = plot
14408 else {
14409 panic!("expected bar plot");
14410 };
14411 *plot_direction = direction;
14412 let options = super::GeometryOptions {
14413 value: Some(super::AxisMapping {
14414 domain: Domain {
14415 min: 80.0,
14416 max: 100.0,
14417 },
14418 orientation,
14419 }),
14420 ..super::GeometryOptions::default()
14421 };
14422 let anchors =
14423 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options)
14424 .unwrap();
14425 let anchor = anchors[&(0, 0)];
14426 assert_point_near(anchor.center, boundary);
14427 assert_point_near(anchor.base, boundary);
14428 assert_point_near(anchor.end, boundary);
14429 assert_point_near(anchor.origin(Some(DataLabelPosition::InsideBase)), boundary);
14430 assert_point_near(anchor.origin(Some(DataLabelPosition::InsideEnd)), boundary);
14431 assert_point_near(anchor.origin(Some(DataLabelPosition::OutsideEnd)), outside);
14432 }
14433
14434 let radar_plot = format!(
14435 r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
14436 plot_series(0)
14437 );
14438 let mut radar = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
14439 let plot = &mut radar.chart.plot_area.plots_mut().unwrap()[0];
14440 let Plot::Radar { series, .. } = plot else {
14441 panic!("expected radar plot");
14442 };
14443 series[0].values.values = vec![0.0, 0.0, 0.0, 0.0];
14444 let anchors = super::plot_label_anchors(
14445 plot,
14446 chart_bounds(),
14447 DispBlanksAs::Gap,
14448 super::GeometryOptions::default(),
14449 )
14450 .unwrap();
14451 let center = Point { x: 100.0, y: 70.0 };
14452 for logical_index in 0..4 {
14453 assert_point_near(
14454 anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideBase)),
14455 center,
14456 );
14457 assert_point_near(
14458 anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideEnd)),
14459 center,
14460 );
14461 }
14462 assert_point_near(
14463 anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
14464 Point { x: 100.0, y: 65.0 },
14465 );
14466 assert_point_near(
14467 anchors[&(0, 1)].origin(Some(DataLabelPosition::OutsideEnd)),
14468 Point { x: 105.0, y: 70.0 },
14469 );
14470 assert_point_near(
14471 anchors[&(0, 2)].origin(Some(DataLabelPosition::OutsideEnd)),
14472 Point { x: 100.0, y: 75.0 },
14473 );
14474 assert_point_near(
14475 anchors[&(0, 3)].origin(Some(DataLabelPosition::OutsideEnd)),
14476 Point { x: 95.0, y: 70.0 },
14477 );
14478 }
14479
14480 #[test]
14481 fn labels_outside_explicit_axis_bounds_are_suppressed() {
14482 let mut chart =
14483 CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
14484 {
14485 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14486 let Plot::Line { series, .. } = plot else {
14487 panic!("expected line plot");
14488 };
14489 series[0].values.values = vec![25.0, 75.0];
14490 }
14491 let value_axis = chart
14492 .chart
14493 .plot_area
14494 .axes
14495 .iter_mut()
14496 .find(|axis| axis.kind == AxisKind::Value)
14497 .unwrap();
14498 value_axis.scaling.minimum = Some(50.0);
14499 value_axis.scaling.maximum = Some(100.0);
14500 value_axis.scaling.orientation = Orientation::MaxMin;
14501 let plot = &chart.chart.plot_area.plots().unwrap()[0];
14502 let axes = chart.chart.plot_area.axes().unwrap();
14503 let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
14504 let anchors =
14505 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
14506 assert!(!anchors.contains_key(&(0, 0)));
14507 assert_point_near(anchors[&(0, 1)].center, Point { x: 150.0, y: 70.0 });
14508
14509 let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14510 let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
14511 let Plot::Bar { series, .. } = plot else {
14512 panic!("expected bar plot");
14513 };
14514 series[0].values.values = vec![90.0, 125.0];
14515 let options = super::GeometryOptions {
14516 value: Some(super::AxisMapping {
14517 domain: Domain {
14518 min: 80.0,
14519 max: 100.0,
14520 },
14521 orientation: Orientation::MinMax,
14522 }),
14523 ..super::GeometryOptions::default()
14524 };
14525 let anchors =
14526 super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
14527 assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
14528 assert_point_near(anchors[&(0, 0)].base, Point { x: 50.0, y: 140.0 });
14529 assert_point_near(anchors[&(0, 0)].end, Point { x: 50.0, y: 70.0 });
14530 assert_point_near(anchors[&(0, 0)].center, Point { x: 50.0, y: 105.0 });
14531 assert_point_near(
14532 anchors[&(0, 0)].origin(Some(DataLabelPosition::InsideBase)),
14533 Point { x: 50.0, y: 135.0 },
14534 );
14535 }
14536
14537 #[test]
14538 fn zero_bars_render_and_emit_requested_value_labels() {
14539 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14540 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14541 let Plot::Bar { series, .. } = plot else {
14542 panic!("expected bar plot");
14543 };
14544 series[0].values.values = vec![0.0, 0.0];
14545 let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
14546 assert_eq!(path_commands(&geometry).len(), 2);
14547 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14548 render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
14549 let plot = &chart.chart.plot_area.plots().unwrap()[0];
14550 let mut labels = Vec::new();
14551 super::render_data_labels(
14552 plot,
14553 chart_bounds(),
14554 DispBlanksAs::Gap,
14555 super::GeometryOptions::default(),
14556 &mut fonts,
14557 &mut labels,
14558 )
14559 .unwrap();
14560 let text: Vec<_> = labels
14561 .iter()
14562 .filter_map(|element| match element {
14563 PositionedElement::Text(run) => Some(run.text.as_str()),
14564 _ => None,
14565 })
14566 .collect();
14567 assert_eq!(text, vec!["0", "0"]);
14568 }
14569
14570 #[test]
14571 fn axes_gridlines_and_tick_marks_follow_model_state() {
14572 let chart = labelled_bar_chart();
14573 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14574 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
14575 let ranks: Vec<_> = group
14576 .children
14577 .iter()
14578 .map(|element| match element {
14579 PositionedElement::Group(group) => {
14580 assert!(group.clip.is_some());
14581 1
14582 }
14583 PositionedElement::Path(path)
14584 if path
14585 .stroke
14586 .as_ref()
14587 .is_some_and(|stroke| stroke.width == 0.5) =>
14588 {
14589 0
14590 }
14591 PositionedElement::Path(path) if path.fill.is_some() => 4,
14592 PositionedElement::Path(path) => {
14593 let bounds = path.path.bounds().expect("axis or tick bounds");
14594 assert_eq!(path.stroke.as_ref().map(|stroke| stroke.width), Some(1.0));
14595 if bounds.width > 4.0 || bounds.height > 4.0 {
14596 2
14597 } else {
14598 3
14599 }
14600 }
14601 PositionedElement::Text(_) => 5,
14602 _ => panic!("unexpected labelled chart element"),
14603 })
14604 .collect();
14605 assert!(ranks.windows(2).all(|pair| pair[0] <= pair[1]), "{ranks:?}");
14606 for required in 0..=5 {
14607 assert!(
14608 ranks.contains(&required),
14609 "missing z-order rank {required}: {ranks:?}"
14610 );
14611 }
14612 assert_eq!(ranks.iter().filter(|rank| **rank == 1).count(), 1);
14613 assert_eq!(ranks.iter().filter(|rank| **rank == 2).count(), 2);
14614
14615 let mut deleted = chart.clone();
14616 for axis in &mut deleted.chart.plot_area.axes {
14617 axis.deleted = true;
14618 }
14619 let deleted_group = render_chart(&deleted.chart, chart_bounds(), &mut fonts).unwrap();
14620 assert_eq!(
14621 deleted_group
14622 .children
14623 .iter()
14624 .filter(|element| {
14625 matches!(
14626 element,
14627 PositionedElement::Path(path) if path.stroke.is_some()
14628 )
14629 })
14630 .count(),
14631 0
14632 );
14633 }
14634
14635 #[test]
14636 fn category_ticks_cover_unlabelled_and_sparse_logical_slots() {
14637 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14638 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
14639 panic!("expected bar plot");
14640 };
14641 series[0].categories = None;
14642 let plot = &chart.chart.plot_area.plots().unwrap()[0];
14643 let mut category_axis = chart
14644 .chart
14645 .plot_area
14646 .axes()
14647 .unwrap()
14648 .into_iter()
14649 .find(|axis| axis.kind == AxisKind::Category)
14650 .unwrap();
14651 category_axis.major_gridlines = Some(super::ChartLines::default());
14652 category_axis.major_tick_mark = TickMark::Cross;
14653 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14654 let annotations = super::render_axes_and_labels(
14655 plot,
14656 &[category_axis],
14657 chart_bounds(),
14658 DispBlanksAs::Gap,
14659 &mut fonts,
14660 )
14661 .unwrap();
14662 assert_eq!(annotations.gridlines.len(), 2);
14663 assert_eq!(annotations.ticks.len(), 2);
14664 assert!(annotations.labels.is_empty());
14665
14666 let sparse_plot = format!(
14667 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>"#,
14668 sparse_category_series()
14669 );
14670 let sparse = CT_ChartSpace::from_xml(chart_with_plot(&sparse_plot).as_bytes()).unwrap();
14671 let plot = &sparse.chart.plot_area.plots().unwrap()[0];
14672 let mut category_axis = sparse
14673 .chart
14674 .plot_area
14675 .axes()
14676 .unwrap()
14677 .into_iter()
14678 .find(|axis| axis.kind == AxisKind::Category)
14679 .unwrap();
14680 category_axis.major_gridlines = Some(super::ChartLines::default());
14681 category_axis.major_tick_mark = TickMark::Cross;
14682 let annotations = super::render_axes_and_labels(
14683 plot,
14684 &[category_axis],
14685 chart_bounds(),
14686 DispBlanksAs::Gap,
14687 &mut fonts,
14688 )
14689 .unwrap();
14690 assert_eq!(annotations.gridlines.len(), 3);
14691 assert_eq!(annotations.ticks.len(), 3);
14692 assert_eq!(annotations.labels.len(), 2);
14693
14694 let oversized_plot = sparse_plot.replacen(
14695 r#"<q:ptCount val="3"/>"#,
14696 r#"<q:ptCount val="4294967295"/>"#,
14697 1,
14698 );
14699 let oversized =
14700 CT_ChartSpace::from_xml(chart_with_plot(&oversized_plot).as_bytes()).unwrap();
14701 let plot = &oversized.chart.plot_area.plots().unwrap()[0];
14702 let category_axis = oversized
14703 .chart
14704 .plot_area
14705 .axes()
14706 .unwrap()
14707 .into_iter()
14708 .find(|axis| axis.kind == AxisKind::Category)
14709 .unwrap();
14710 let result = super::render_axes_and_labels(
14711 plot,
14712 &[category_axis],
14713 chart_bounds(),
14714 DispBlanksAs::Gap,
14715 &mut fonts,
14716 );
14717 let error = match result {
14718 Err(error) => error,
14719 Ok(_) => panic!("oversized category annotations rendered"),
14720 };
14721 assert!(
14722 error
14723 .to_string()
14724 .contains("logical category count 4294967295")
14725 );
14726 assert!(error.to_string().contains("exceeds 16384"));
14727 }
14728
14729 #[test]
14730 fn unsupported_numeric_category_formats_return_projection_errors() {
14731 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14732 let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
14733 panic!("expected bar plot");
14734 };
14735 series[0].categories = Some(AxisData::Numeric(
14736 NumericData::new(
14737 "Sheet1!$A$2:$A$3".to_owned(),
14738 "General".to_owned(),
14739 vec![1.0, 2.0],
14740 )
14741 .unwrap(),
14742 ));
14743 let plot = &chart.chart.plot_area.plots().unwrap()[0];
14744 let mut category_axis = chart
14745 .chart
14746 .plot_area
14747 .axes()
14748 .unwrap()
14749 .into_iter()
14750 .find(|axis| axis.kind == AxisKind::Category)
14751 .unwrap();
14752 category_axis.number_format = Some(NumberFormat::new("0.00".to_owned(), false).unwrap());
14753 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14754 let annotations = super::render_axes_and_labels(
14755 plot,
14756 &[category_axis.clone()],
14757 chart_bounds(),
14758 DispBlanksAs::Gap,
14759 &mut fonts,
14760 )
14761 .unwrap();
14762 let text: Vec<_> = annotations
14763 .labels
14764 .iter()
14765 .filter_map(|element| match element {
14766 PositionedElement::Text(run) => Some(run.text.as_str()),
14767 _ => None,
14768 })
14769 .collect();
14770 assert_eq!(text, vec!["1.00", "2.00"]);
14771
14772 category_axis.number_format = Some(NumberFormat::new("#,##0".to_owned(), false).unwrap());
14773 let result = super::render_axes_and_labels(
14774 plot,
14775 &[category_axis],
14776 chart_bounds(),
14777 DispBlanksAs::Gap,
14778 &mut fonts,
14779 );
14780 let error = match result {
14781 Err(error) => error,
14782 Ok(_) => panic!("unsupported category-axis format rendered"),
14783 };
14784 assert!(
14785 error
14786 .to_string()
14787 .contains("c:numFmt/@formatCode projection")
14788 );
14789 assert!(error.to_string().contains("#,##0"));
14790 }
14791
14792 #[test]
14793 fn labels_and_legend_shape_with_deterministic_fonts() {
14794 let chart = labelled_bar_chart();
14795 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14796 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
14797 let labels: Vec<_> = group
14798 .children
14799 .iter()
14800 .filter_map(|element| match element {
14801 PositionedElement::Text(run) => Some(run),
14802 _ => None,
14803 })
14804 .collect();
14805 for text in ["North", "South", "20", "Sales", "100"] {
14806 assert!(labels.iter().any(|run| run.text == text), "missing {text}");
14807 }
14808 let styled_tick = labels.iter().find(|run| run.text == "20").unwrap();
14809 assert_eq!(styled_tick.font_size, 12.0);
14810 assert!(styled_tick.bold);
14811 assert!(
14812 labels
14813 .iter()
14814 .all(|run| !run.glyph_ids.is_empty() && run.glyph_ids.len() == run.advances.len())
14815 );
14816 }
14817
14818 #[test]
14819 fn labelled_chart_raster_is_deterministic() {
14820 let raster = || {
14821 let chart = labelled_bar_chart();
14822 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14823 let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
14824 let layout = LayoutResult::new(
14825 vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
14826 fonts.all_font_data(),
14827 None,
14828 Vec::new(),
14829 );
14830 oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
14831 };
14832 assert_eq!(raster(), raster());
14833 }
14834
14835 fn labelled_bar_chart() -> CT_ChartSpace {
14836 let series = plot_series(0)
14837 .replace(
14838 "<q:cat>",
14839 "<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>",
14840 )
14841 .replace("<q:v>1</q:v>", "<q:v>0</q:v>")
14842 .replace("<q:v>2</q:v>", "<q:v>100</q:v>");
14843 let plot = format!(
14844 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>"#
14845 );
14846 let xml = chart_with_plot(&plot)
14847 .replacen(
14848 "<q:scaling/>",
14849 "<q:scaling><q:orientation val=\"minMax\"/></q:scaling>",
14850 1,
14851 )
14852 .replacen(
14853 "<q:scaling/>",
14854 "<q:scaling><q:max val=\"100\"/><q:min val=\"0\"/></q:scaling>",
14855 1,
14856 )
14857 .replace(
14858 "<q:axPos val=\"b\"/>",
14859 "<q:axPos val=\"b\"/><q:majorTickMark val=\"cross\"/>",
14860 )
14861 .replace(
14862 "<q:axPos val=\"l\"/>",
14863 "<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>",
14864 )
14865 .replace("</q:plotArea></q:chart>", "</q:plotArea><q:legend/></q:chart>");
14866 CT_ChartSpace::from_xml(xml.as_bytes()).unwrap()
14867 }
14868
14869 fn chart_bounds() -> Rect {
14870 Rect {
14871 x: 0.0,
14872 y: 0.0,
14873 width: 200.0,
14874 height: 140.0,
14875 }
14876 }
14877
14878 fn four_series_bar_chart() -> CT_ChartSpace {
14879 let series = (0..4).map(plot_series).collect::<String>();
14880 let plot = bar_plot("").replace(&plot_series(0), &series);
14881 CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
14882 }
14883
14884 fn seven_series_bar_chart() -> CT_ChartSpace {
14885 let series = (0..7).map(plot_series).collect::<String>();
14886 let plot = bar_plot("").replace(&plot_series(0), &series);
14887 CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
14888 }
14889
14890 fn path_colours(geometry: &ChartGeometry) -> Vec<Color> {
14891 let mut colours = geometry_children(geometry)
14892 .iter()
14893 .filter_map(|element| match element {
14894 PositionedElement::Path(path) => match path
14895 .fill
14896 .as_ref()
14897 .or_else(|| path.stroke.as_ref().map(|stroke| &stroke.paint))
14898 {
14899 Some(Paint::Solid(colour)) => Some(*colour),
14900 _ => None,
14901 },
14902 _ => None,
14903 })
14904 .collect::<Vec<_>>();
14905 colours.dedup_by(|left, right| left == right);
14906 colours
14907 }
14908
14909 fn resolved_layout_colour(colour: oxml_drawing::color::ResolvedColor) -> Color {
14910 Color {
14911 r: f64::from(colour.red) / 255.0,
14912 g: f64::from(colour.green) / 255.0,
14913 b: f64::from(colour.blue) / 255.0,
14914 a: f64::from(colour.alpha) / 255.0,
14915 }
14916 }
14917
14918 fn geometry_children(geometry: &ChartGeometry) -> &[PositionedElement] {
14919 let [PositionedElement::Group(group)] = geometry.elements.as_slice() else {
14920 panic!("chart geometry must contain one group");
14921 };
14922 &group.children
14923 }
14924
14925 fn path_commands(geometry: &ChartGeometry) -> Vec<&[PathCommand]> {
14926 geometry_children(geometry)
14927 .iter()
14928 .map(|element| {
14929 let PositionedElement::Path(path) = element else {
14930 panic!("chart geometry child must be a path");
14931 };
14932 path.path.commands.as_slice()
14933 })
14934 .collect()
14935 }
14936
14937 fn path_bounds(geometry: &ChartGeometry) -> Vec<Rect> {
14938 geometry_children(geometry)
14939 .iter()
14940 .map(|element| {
14941 let PositionedElement::Path(path) = element else {
14942 panic!("chart geometry child must be a path");
14943 };
14944 path.path.bounds().expect("chart path has bounds")
14945 })
14946 .collect()
14947 }
14948
14949 fn assert_rect_near(actual: Rect, expected: Rect) {
14950 assert_near(actual.x, expected.x);
14951 assert_near(actual.y, expected.y);
14952 assert_near(actual.width, expected.width);
14953 assert_near(actual.height, expected.height);
14954 }
14955
14956 fn assert_rects_near(actual: &[Rect], expected: &[Rect]) {
14957 assert_eq!(actual.len(), expected.len());
14958 for (actual, expected) in actual.iter().copied().zip(expected.iter().copied()) {
14959 assert_rect_near(actual, expected);
14960 }
14961 }
14962
14963 fn assert_path_points(commands: &[PathCommand], expected: &[Point], closed: bool) {
14964 let points: Vec<_> = commands
14965 .iter()
14966 .filter_map(|command| match command {
14967 PathCommand::MoveTo(point) | PathCommand::LineTo(point) => Some(*point),
14968 PathCommand::Close => None,
14969 PathCommand::CurveTo { .. } => panic!("expected polygonal path"),
14970 })
14971 .collect();
14972 assert_eq!(points.len(), expected.len());
14973 for (actual, expected) in points.into_iter().zip(expected.iter().copied()) {
14974 assert_point_near(actual, expected);
14975 }
14976 assert_eq!(commands.last() == Some(&PathCommand::Close), closed);
14977 }
14978
14979 fn assert_point_near(actual: Point, expected: Point) {
14980 assert_near(actual.x, expected.x);
14981 assert_near(actual.y, expected.y);
14982 }
14983
14984 fn assert_near(actual: f64, expected: f64) {
14985 assert!(
14986 (actual - expected).abs() < 1e-9,
14987 "expected {expected}, got {actual}"
14988 );
14989 }
14990
14991 #[test]
14992 fn remaining_v1_plots_round_trip_and_render() {
14993 for (kind, plot, axes) in remaining_plot_fixtures() {
14994 let xml = chart_with_optional_axes(&plot, axes);
14995 let parsed = CT_ChartSpace::from_xml(xml.as_bytes())
14996 .unwrap_or_else(|error| panic!("{kind}: parse failed: {error}"));
14997 assert_eq!(parsed.chart.plot_area.plots().unwrap().len(), 1);
14998 assert_eq!(
14999 parsed.chart.plot_area.axes().unwrap().len(),
15000 usize::from(axes) * 2
15001 );
15002 let written = parsed
15003 .to_xml()
15004 .unwrap_or_else(|error| panic!("{kind}: write failed: {error}"));
15005 assert_eq!(
15006 parsed,
15007 CT_ChartSpace::from_xml(&written)
15008 .unwrap_or_else(|error| panic!("{kind}: reparse failed: {error}"))
15009 );
15010 }
15011 if let Some(corpus) = require_or_skip_corpus() {
15012 verify_remaining_plot_viewer_gate(&corpus);
15013 }
15014 }
15015
15016 #[test]
15017 fn remaining_plot_families_write_fixed_prefixes_in_schema_order() {
15018 for (kind, plot, axes) in remaining_plot_fixtures() {
15019 let parsed =
15020 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
15021 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15022 assert!(written.contains(&format!("<c:{kind}")));
15023 assert!(!written.contains(&format!("<q:{kind}")));
15024 let series = written.find("<c:ser").unwrap();
15025 let labels = written.find("<c:dLbls").unwrap();
15026 assert!(series < labels, "{kind}: series must precede labels");
15027 if matches!(kind, "areaChart" | "scatterChart" | "radarChart") {
15028 let property = match kind {
15029 "areaChart" => "<c:grouping",
15030 "scatterChart" => "<c:scatterStyle",
15031 "radarChart" => "<c:radarStyle",
15032 _ => unreachable!(),
15033 };
15034 assert!(written.find(property).unwrap() < series);
15035 assert!(labels < written.find("<c:axId").unwrap());
15036 } else {
15037 assert!(labels < written.find("<c:firstSliceAng").unwrap());
15038 }
15039 if kind == "doughnutChart" {
15040 assert!(
15041 written.find("<c:firstSliceAng").unwrap()
15042 < written.find("<c:holeSize").unwrap()
15043 );
15044 }
15045 }
15046 }
15047
15048 #[test]
15049 fn scatter_series_map_numeric_categories_and_values_to_x_and_y() {
15050 let plot = remaining_plot_fixtures()
15051 .into_iter()
15052 .find(|(kind, _, _)| *kind == "scatterChart")
15053 .unwrap()
15054 .1;
15055 let mut parsed =
15056 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, true).as_bytes()).unwrap();
15057 let standalone_series = {
15058 let Plot::Scatter { series, .. } = &mut parsed.chart.plot_area.plots_mut().unwrap()[0]
15059 else {
15060 panic!("expected scatter plot");
15061 };
15062 let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
15063 panic!("expected numeric x values");
15064 };
15065 x_values.values[0] = 3.5;
15066 series[0].values.values[0] = 7.5;
15067 series[0].clone()
15068 };
15069 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15070 assert!(written.contains("<c:xVal>"));
15071 assert!(written.contains("<c:yVal>"));
15072 assert!(!written.contains("<c:cat>"));
15073 assert!(!written.contains("<c:val>"));
15074 assert!(written.contains("<c:v>3.5</c:v>"));
15075 assert!(written.contains("<c:v>7.5</c:v>"));
15076 let standalone = String::from_utf8(standalone_series.to_xml().unwrap()).unwrap();
15077 assert!(standalone.contains("<c:xVal>"));
15078 assert!(standalone.contains("<c:yVal>"));
15079 assert!(!standalone.contains("<c:cat>"));
15080 assert!(!standalone.contains("<c:val>"));
15081 }
15082
15083 #[test]
15084 fn malformed_remaining_plots_return_errors_without_panicking() {
15085 let series = plot_series(0);
15086 let scatter = numeric_plot_series(0);
15087 let opaque_bubble = series.replace(
15088 "</q:val></q:ser>",
15089 "</q:val><q:bubbleSize><x:opaque/></q:bubbleSize></q:ser>",
15090 );
15091 let cases = [
15092 r#"<q:pieChart/>"#.to_owned(),
15093 format!(r#"<q:pieChart>{series}<q:firstSliceAng val="361"/></q:pieChart>"#),
15094 format!(
15095 r#"<q:pieChart>{series}<q:firstSliceAng val="1"/><q:firstSliceAng val="2"/></q:pieChart>"#
15096 ),
15097 format!(r#"<q:doughnutChart>{series}<q:holeSize val="9"/></q:doughnutChart>"#),
15098 format!(
15099 r#"<q:doughnutChart>{series}<q:holeSize val="50"/><q:holeSize val="60"/></q:doughnutChart>"#
15100 ),
15101 format!(
15102 r#"<q:areaChart><q:grouping val="clustered"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
15103 ),
15104 format!(
15105 r#"<q:areaChart><q:grouping val="standard"/><q:grouping val="stacked"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
15106 ),
15107 format!(
15108 r#"<q:scatterChart><q:scatterStyle val="dots"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
15109 ),
15110 format!(
15111 r#"<q:scatterChart><q:scatterStyle val="marker"/><q:scatterStyle val="line"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
15112 ),
15113 format!(
15114 r#"<q:radarChart><q:radarStyle val="smooth"/>{series}<q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
15115 ),
15116 format!(
15117 r#"<q:radarChart><q:radarStyle val="marker"/>{series}<q:dLbls/><q:dLbls/><q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
15118 ),
15119 format!(r#"<q:pieChart>{series}<q:axId val="1"/></q:pieChart>"#),
15120 format!(
15121 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/></q:areaChart>"#
15122 ),
15123 r#"<q:areaChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:areaChart>"#.to_owned(),
15124 format!(
15125 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/><q:axId val="2"/><q:axId val="3"/></q:areaChart>"#
15126 ),
15127 format!(
15128 r#"<q:scatterChart><q:scatterStyle val="marker"/>{series}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
15129 ),
15130 format!(
15131 r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
15132 scatter
15133 .replace("<q:yVal>", "<q:val>")
15134 .replace("</q:yVal>", "</q:val>")
15135 ),
15136 format!(
15137 r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
15138 scatter_without_x(0)
15139 ),
15140 format!(
15141 r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
15142 scatter_without_y(0)
15143 ),
15144 format!(r#"<q:pieChart>{opaque_bubble}</q:pieChart>"#),
15145 ];
15146 for plot in cases {
15147 let xml = chart_with_optional_axes(&plot, false);
15148 let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
15149 assert!(result.is_ok(), "parser panicked for {plot}");
15150 assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
15151 }
15152
15153 let missing_axis = format!(
15154 r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="7"/></q:areaChart>"#
15155 );
15156 let missing_axis =
15157 CT_ChartSpace::from_xml(chart_with_optional_axes(&missing_axis, true).as_bytes())
15158 .unwrap_err();
15159 assert!(missing_axis.to_string().contains("missing axis 7"));
15160
15161 let values =
15162 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
15163 let mut bubble_series = Series::new(0, 0, values.clone());
15164 bubble_series.bubble_size = Some(values);
15165 assert!(Plot::pie(vec![bubble_series]).is_err());
15166 }
15167
15168 #[test]
15169 fn unsupported_plot_families_and_children_remain_byte_preserved() {
15170 for raw in [
15171 r#"<q:pie3DChart x:keep="3d"><q:ser><x:opaque/></q:ser></q:pie3DChart>"#,
15172 r#"<q:ofPieChart x:keep="of"><x:opaque/></q:ofPieChart>"#,
15173 r#"<q:stockChart x:keep="stock"><x:opaque/></q:stockChart>"#,
15174 r#"<q:surfaceChart x:keep="surface"><x:opaque/></q:surfaceChart>"#,
15175 r#"<q:bubbleChart x:keep="bubble"><x:opaque/></q:bubbleChart>"#,
15176 ] {
15177 let parsed =
15178 CT_ChartSpace::from_xml(chart_with_optional_axes(raw, false).as_bytes()).unwrap();
15179 assert!(parsed.chart.plot_area.plots().is_err());
15180 let written = parsed.to_xml().unwrap();
15181 assert!(
15182 written
15183 .windows(raw.len())
15184 .any(|window| window == raw.as_bytes())
15185 );
15186 }
15187
15188 let pie = remaining_plot_fixtures()[0].1.clone();
15189 let preserved = r#"<!--point--><q:dPt x:id="1"><q:explosion val="7"/><x:tail/></q:dPt>"#;
15190 let plot = pie.replace("<q:dLbls>", &format!("{preserved}<q:dLbls>"));
15191 let parsed =
15192 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, false).as_bytes()).unwrap();
15193 let written = parsed.to_xml().unwrap();
15194 assert!(
15195 written
15196 .windows(preserved.len())
15197 .any(|window| window == preserved.as_bytes())
15198 );
15199
15200 for (kind, mut plot, axes) in [
15201 ("pieChart", remaining_plot_fixtures()[0].1.clone(), false),
15202 ("areaChart", remaining_plot_fixtures()[2].1.clone(), true),
15203 ] {
15204 plot = plot.replace(r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#, "");
15205 if kind == "pieChart" {
15206 plot = plot.replace(
15207 "</q:extLst></q:pieChart>",
15208 "</q:extLst><!--after-ext--></q:pieChart>",
15209 );
15210 } else {
15211 plot = plot.replace(
15212 "</q:dropLines><q:axId",
15213 "</q:dropLines><!--after-drop-lines--><q:axId",
15214 );
15215 }
15216 let mut parsed =
15217 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
15218 let labels =
15219 CT_DLbls::from_xml(format!(r#"<c:dLbls xmlns:c="{C_NS}"/>"#).as_bytes()).unwrap();
15220 match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
15221 Plot::Pie { data_labels, .. } | Plot::Area { data_labels, .. } => {
15222 *data_labels = Some(labels)
15223 }
15224 _ => panic!("expected pie or area"),
15225 }
15226 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15227 assert!(
15228 written.find("<c:dLbls").unwrap() < written.find("<q:extLst").unwrap(),
15229 "{kind}: inserted labels must precede the preserved extension"
15230 );
15231 if kind == "areaChart" {
15232 assert!(written.find("<c:dLbls").unwrap() < written.find("<q:dropLines").unwrap());
15233 assert!(
15234 written.find("<q:dropLines").unwrap()
15235 < written.find("<!--after-drop-lines-->").unwrap()
15236 );
15237 } else {
15238 assert!(
15239 written.find("<q:extLst").unwrap() < written.find("<!--after-ext-->").unwrap()
15240 );
15241 }
15242 }
15243 }
15244
15245 #[test]
15246 fn every_supported_corpus_plot_round_trips_structurally() {
15247 let Some(corpus) = require_or_skip_corpus() else {
15248 return;
15249 };
15250 verify_fetched_corpus(&corpus);
15251 let mut pie_count = 0usize;
15252 let mut bar_count = 0usize;
15253 let mut line_count = 0usize;
15254 for path in manifest_paths() {
15255 let package = OpcPackage::open(corpus.join(path)).unwrap();
15256 for (part, xml) in &package.parts {
15257 if !is_chart_part(part) {
15258 continue;
15259 }
15260 let parsed = CT_ChartSpace::from_xml(xml).unwrap();
15261 if let Ok(plots) = parsed.chart.plot_area.plots() {
15262 for plot in plots {
15263 match plot {
15264 Plot::Pie { .. } => pie_count += 1,
15265 Plot::Bar { .. } => bar_count += 1,
15266 Plot::Line { .. } => line_count += 1,
15267 _ => {}
15268 }
15269 }
15270 }
15271 let written = parsed.to_xml().unwrap();
15272 assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
15273 }
15274 }
15275 assert_eq!(pie_count, 1, "typed corpus pie coverage changed");
15276 assert_eq!(bar_count, 11, "typed corpus bar coverage changed");
15277 assert_eq!(line_count, 2, "typed corpus line coverage changed");
15278 }
15279
15280 fn standalone_axis(local: &str, children: &str) -> String {
15281 format!(
15282 r#"<c:{local} xmlns:c="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}">{children}</c:{local}>"#
15283 )
15284 }
15285
15286 fn minimal_axis(local: &str, id: &str, position: &str, cross_axis: &str) -> String {
15287 format!(
15288 r#"<c:{local}><c:axId val="{id}"/><c:scaling/><c:axPos val="{position}"/><c:crossAx val="{cross_axis}"/></c:{local}>"#
15289 )
15290 }
15291
15292 fn chart_with_axes(axes: &[String]) -> String {
15293 format!(
15294 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>"#,
15295 axes.join("")
15296 )
15297 }
15298
15299 #[test]
15300 fn data_labels_write_fixed_prefixes_in_schema_order() {
15301 let xml = format!(
15302 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>"#
15303 );
15304 let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
15305 assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0%");
15306 assert_eq!(parsed.position, Some(DataLabelPosition::OutsideEnd));
15307 assert_eq!(parsed.separator.as_deref(), Some(" / "));
15308 assert!(parsed.show_legend_key);
15309 assert!(parsed.show_value);
15310 assert!(!parsed.show_category_name);
15311 assert!(parsed.show_series_name);
15312 assert!(!parsed.show_percent);
15313 assert!(parsed.show_bubble_size);
15314
15315 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15316 assert!(written.starts_with(&format!(
15317 "<c:dLbls xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
15318 )));
15319 let mut cursor = 0usize;
15320 for tag in [
15321 "<q:dLbl",
15322 "<c:numFmt",
15323 "<q:spPr",
15324 "<q:txPr",
15325 "<c:dLblPos",
15326 "<c:showLegendKey",
15327 "<c:showVal",
15328 "<c:showCatName",
15329 "<c:showSerName",
15330 "<c:showPercent",
15331 "<c:showBubbleSize",
15332 "<c:separator",
15333 "<q:leaderLines",
15334 "<q:extLst",
15335 ] {
15336 let position = written[cursor..]
15337 .find(tag)
15338 .unwrap_or_else(|| panic!("missing ordered data-label tag {tag}"));
15339 cursor += position + tag.len();
15340 }
15341 assert_eq!(parsed, CT_DLbls::from_xml(written.as_bytes()).unwrap());
15342 }
15343
15344 #[test]
15345 fn percentage_formatted_label_renders_with_correct_text() {
15346 let Some(corpus) = require_or_skip_corpus() else {
15347 return;
15348 };
15349 verify_fetched_corpus(&corpus);
15350 assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
15351 assert_command_version("pdftotext", &["-v"], PDFTOTEXT_VERSION);
15352
15353 let source = corpus.join("bar-chart.pptx");
15354 let mut package = OpcPackage::open(&source).expect("open percentage-label source deck");
15355 let chart_part = "/ppt/charts/chart1.xml";
15356 let original_parts = package.parts.clone();
15357 let chart_xml = package
15358 .get_part(chart_part)
15359 .expect("bar chart part")
15360 .to_vec();
15361 let original_series = first_series_xml(&chart_xml).expect("bar chart series");
15362 let mut series = Series::from_xml(&original_series).expect("parse bar chart series");
15363 series.values.values.fill(0.25);
15364 let labels = CT_DLbls {
15365 number_format: Some(NumberFormat::new("0%".to_owned(), false).unwrap()),
15366 position: Some(DataLabelPosition::OutsideEnd),
15367 show_value: true,
15368 ..CT_DLbls::default()
15369 };
15370 series.data_labels = Some(labels);
15371 let replacement = series.to_xml().expect("serialize typed percentage labels");
15372 let candidate_chart = replace_once(&chart_xml, &original_series, &replacement);
15373 package.set_part(chart_part, candidate_chart);
15374 for (part, bytes) in &original_parts {
15375 if part == chart_part {
15376 assert_ne!(package.parts[part], *bytes);
15377 } else {
15378 assert_eq!(
15379 package.parts[part], *bytes,
15380 "unexpected changed part {part}"
15381 );
15382 }
15383 }
15384
15385 let temp_root =
15386 std::env::temp_dir().join(format!("oxml-chart-f123-label-gate-{}", std::process::id()));
15387 let profile = std::env::temp_dir().join(format!(
15388 "oxml-chart-f123-label-profile-{}",
15389 std::process::id()
15390 ));
15391 if temp_root.exists() {
15392 fs::remove_dir_all(&temp_root).expect("remove stale F-123 evidence");
15393 }
15394 if profile.exists() {
15395 fs::remove_dir_all(&profile).expect("remove stale F-123 profile");
15396 }
15397 fs::create_dir_all(&temp_root).expect("create F-123 evidence root");
15398 let unbound_candidate = temp_root.join("f123-percentage-label.pptx");
15399 package
15400 .save(&unbound_candidate)
15401 .expect("save percentage-label candidate");
15402 let candidate_sha = sha256(&unbound_candidate);
15403 let evidence_dir = temp_root.join(&candidate_sha);
15404 fs::create_dir(&evidence_dir).expect("create SHA-bound evidence directory");
15405 let candidate = evidence_dir.join("f123-percentage-label.pptx");
15406 fs::rename(&unbound_candidate, &candidate).expect("bind candidate to evidence SHA");
15407 assert_eq!(sha256(&candidate), candidate_sha);
15408
15409 let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
15410 let conversion = Command::new("soffice")
15411 .args([
15412 "--headless",
15413 &profile_argument,
15414 "--convert-to",
15415 "pdf:impress_pdf_Export",
15416 "--outdir",
15417 ])
15418 .arg(&evidence_dir)
15419 .arg(&candidate)
15420 .output()
15421 .expect("run pinned LibreOffice percentage-label gate");
15422 assert!(
15423 conversion.status.success(),
15424 "LibreOffice conversion failed: {}",
15425 String::from_utf8_lossy(&conversion.stderr)
15426 );
15427 let pdf = evidence_dir.join("f123-percentage-label.pdf");
15428 assert!(
15429 pdf.is_file(),
15430 "LibreOffice did not create {}",
15431 pdf.display()
15432 );
15433 let text_path = evidence_dir.join("f123-percentage-label.txt");
15434 let extraction = Command::new("pdftotext")
15435 .arg(&pdf)
15436 .arg(&text_path)
15437 .output()
15438 .expect("run pinned Poppler percentage-label extraction");
15439 assert!(
15440 extraction.status.success(),
15441 "pdftotext failed: {}",
15442 String::from_utf8_lossy(&extraction.stderr)
15443 );
15444 let extracted = fs::read_to_string(&text_path).expect("read percentage-label text");
15445 assert!(
15446 extracted.contains("25%"),
15447 "SHA-bound viewer text lacks 25%: {extracted:?}"
15448 );
15449 for artifact in [&candidate, &pdf, &text_path] {
15450 assert_eq!(
15451 artifact.parent(),
15452 Some(evidence_dir.as_path()),
15453 "evidence escaped candidate SHA"
15454 );
15455 }
15456 eprintln!("F-123 candidate/render/text evidence SHA-256 {candidate_sha}, extracted 25%");
15457 fs::remove_dir_all(&temp_root).expect("remove F-123 evidence");
15458 if profile.exists() {
15459 fs::remove_dir_all(&profile).expect("remove F-123 profile");
15460 }
15461 }
15462
15463 #[test]
15464 fn common_number_formats_project_cached_values_deterministically() {
15465 for (code, value, expected) in [
15466 ("General", 0.25, "0.25"),
15467 ("0", 12.6, "13"),
15468 ("0.0", 12.64, "12.6"),
15469 ("0.00", -12.645, "-12.64"),
15470 ("0%", 0.25, "25%"),
15471 ("0.0%", 0.256, "25.6%"),
15472 ("0.00%", 0.256, "25.60%"),
15473 ] {
15474 let format = NumberFormat::new(code.to_owned(), false).unwrap();
15475 assert_eq!(format.format_value(value).unwrap(), expected, "{code}");
15476 }
15477 }
15478
15479 #[test]
15480 fn malformed_data_labels_and_number_formats_return_errors_without_panicking() {
15481 let cases = [
15482 format!(
15483 r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="1"/><c:showVal val="0"/></c:dLbls>"#
15484 ),
15485 format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="yes"/></c:dLbls>"#),
15486 format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:dLblPos val="middle"/></c:dLbls>"#),
15487 format!(
15488 r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="" sourceLinked="1"/></c:dLbls>"#
15489 ),
15490 format!(
15491 r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="0" sourceLinked="maybe"/></c:dLbls>"#
15492 ),
15493 ];
15494 for xml in cases {
15495 let result = std::panic::catch_unwind(|| CT_DLbls::from_xml(xml.as_bytes()));
15496 assert!(result.is_ok(), "data-label parser panicked for {xml}");
15497 assert!(result.unwrap().is_err(), "malformed labels parsed: {xml}");
15498 }
15499
15500 let invalid_code = NumberFormat::new("bad\u{0}code".to_owned(), false);
15501 assert!(invalid_code.is_err());
15502 let unsupported = NumberFormat::new("#,##0".to_owned(), false).unwrap();
15503 assert!(unsupported.format_value(12.0).is_err());
15504 let general = NumberFormat::new("General".to_owned(), false).unwrap();
15505 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
15506 assert!(general.format_value(value).is_err());
15507 }
15508
15509 let invalid_separator = CT_DLbls {
15510 separator: Some("bad\u{0}separator".to_owned()),
15511 ..CT_DLbls::default()
15512 };
15513 assert!(invalid_separator.to_xml().is_err());
15514 }
15515
15516 #[test]
15517 fn data_labels_preserve_point_overrides_and_extensions_byte_for_byte() {
15518 let xml = format!(
15519 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>"#
15520 );
15521 let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
15522 let written = parsed.to_xml().unwrap();
15523 for raw in [
15524 br#"<!--before-->"#.as_slice(),
15525 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(),
15526 br#"<?after-point?>"#.as_slice(),
15527 br#"<q:spPr><x:collection-shape/></q:spPr>"#.as_slice(),
15528 br#"<q:txPr><x:collection-text/></q:txPr>"#.as_slice(),
15529 br#"<q:showLeaderLines val="1"/>"#.as_slice(),
15530 br#"<q:leaderLines><q:spPr><x:leader-shape/></q:spPr></q:leaderLines>"#.as_slice(),
15531 br#"<q:extLst><q:ext uri="collection"><x:tail/></q:ext></q:extLst>"#.as_slice(),
15532 br#"<!--after-->"#.as_slice(),
15533 ] {
15534 assert!(
15535 written.windows(raw.len()).any(|window| window == raw),
15536 "preserved data-label bytes changed: {}",
15537 String::from_utf8_lossy(raw)
15538 );
15539 }
15540 assert_eq!(parsed, CT_DLbls::from_xml(&written).unwrap());
15541 }
15542
15543 #[test]
15544 fn every_corpus_data_label_collection_round_trips_structurally() {
15545 let Some(corpus) = require_or_skip_corpus() else {
15546 return;
15547 };
15548 verify_fetched_corpus(&corpus);
15549 let mut label_collections = 0usize;
15550 let mut axis_number_formats = 0usize;
15551 for path in manifest_paths() {
15552 let package = OpcPackage::open(corpus.join(path))
15553 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
15554 for (part, xml) in &package.parts {
15555 if !is_chart_part(part) {
15556 continue;
15557 }
15558 let chart = CT_ChartSpace::from_xml(xml)
15559 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
15560 for series in chart.chart.plot_area.series().unwrap_or_else(|error| {
15561 panic!("{path} {part}: series projection failed: {error}")
15562 }) {
15563 if let Some(labels) = series.data_labels {
15564 let written = labels.to_xml().unwrap_or_else(|error| {
15565 panic!("{path} {part}: label write failed: {error}")
15566 });
15567 let reparsed = CT_DLbls::from_xml(&written).unwrap_or_else(|error| {
15568 panic!("{path} {part}: written labels parse failed: {error}")
15569 });
15570 assert_eq!(labels, reparsed, "{path} {part}: labels changed");
15571 label_collections += 1;
15572 }
15573 }
15574 for axis in chart.chart.plot_area.axes().unwrap_or_else(|error| {
15575 panic!("{path} {part}: axis projection failed: {error}")
15576 }) {
15577 if axis.number_format.is_some() {
15578 let written = axis.to_xml().unwrap_or_else(|error| {
15579 panic!("{path} {part}: axis write failed: {error}")
15580 });
15581 let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
15582 panic!("{path} {part}: written axis parse failed: {error}")
15583 });
15584 assert_eq!(axis, reparsed, "{path} {part}: axis changed");
15585 axis_number_formats += 1;
15586 }
15587 }
15588 }
15589 }
15590 assert!(label_collections > 0, "the pinned corpus has no c:dLbls");
15591 assert!(
15592 axis_number_formats > 0,
15593 "the pinned corpus has no axis c:numFmt"
15594 );
15595 eprintln!(
15596 "ChartML data-label corpus gate checked {label_collections} collections and {axis_number_formats} axis number formats"
15597 );
15598 }
15599
15600 fn full_axis(local: &str, position: &str) -> String {
15601 format!(
15602 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}>"#
15603 )
15604 }
15605
15606 #[test]
15607 fn axis_id_pairs_are_reciprocal() {
15608 assert_eq!(
15609 AxisId::new(i64::from(i32::MIN)).unwrap().value(),
15610 i64::from(i32::MIN)
15611 );
15612 assert_eq!(
15613 AxisId::new(i64::from(u32::MAX)).unwrap().value(),
15614 i64::from(u32::MAX)
15615 );
15616 assert!(AxisId::new(i64::from(i32::MIN) - 1).is_err());
15617 assert!(AxisId::new(i64::from(u32::MAX) + 1).is_err());
15618
15619 let valid = chart_with_axes(&[
15620 minimal_axis("catAx", "-1884094432", "b", "-1884097184"),
15621 minimal_axis("valAx", "-1884097184", "l", "-1884094432"),
15622 ]);
15623 let axes = CT_ChartSpace::from_xml(valid.as_bytes())
15624 .unwrap()
15625 .chart
15626 .plot_area
15627 .axes()
15628 .unwrap();
15629 assert_eq!(axes.len(), 2);
15630 assert_eq!(axes[0].id.value(), -1_884_094_432);
15631 assert_eq!(axes[0].cross_axis, axes[1].id);
15632 assert_eq!(axes[1].cross_axis, axes[0].id);
15633
15634 let invalid_sets = [
15635 vec![
15636 minimal_axis("catAx", "1", "b", "2"),
15637 minimal_axis("valAx", "1", "l", "2"),
15638 ],
15639 vec![minimal_axis("catAx", "1", "b", "1")],
15640 vec![minimal_axis("catAx", "1", "b", "2")],
15641 vec![
15642 minimal_axis("catAx", "1", "b", "2"),
15643 minimal_axis("valAx", "2", "l", "3"),
15644 minimal_axis("serAx", "3", "r", "2"),
15645 ],
15646 ];
15647 for axes in invalid_sets {
15648 let chart = CT_ChartSpace::from_xml(chart_with_axes(&axes).as_bytes()).unwrap();
15649 assert!(chart.chart.plot_area.axes().is_err());
15650 }
15651
15652 let empty = CT_ChartSpace::from_xml(chart_with_axes(&[]).as_bytes()).unwrap();
15653 assert!(empty.chart.plot_area.axes().unwrap().is_empty());
15654 }
15655
15656 #[test]
15657 fn all_axis_forms_write_fixed_prefixes_in_schema_order() {
15658 for (local, kind, position) in [
15659 ("catAx", AxisKind::Category, AxisPosition::Bottom),
15660 ("valAx", AxisKind::Value, AxisPosition::Left),
15661 ("dateAx", AxisKind::Date, AxisPosition::Top),
15662 ("serAx", AxisKind::Series, AxisPosition::Right),
15663 ] {
15664 let xml = full_axis(local, position.as_str());
15665 let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
15666 assert_eq!(parsed.kind, kind);
15667 assert_eq!(parsed.id.value(), -1_125_255_920);
15668 assert_eq!(parsed.scaling.log_base, Some(10.0));
15669 assert_eq!(parsed.scaling.orientation, Orientation::MaxMin);
15670 assert_eq!(parsed.scaling.maximum, Some(100.0));
15671 assert_eq!(parsed.scaling.minimum, Some(-10.0));
15672 assert!(parsed.deleted);
15673 assert_eq!(parsed.position, position);
15674 assert!(parsed.major_gridlines.is_some());
15675 assert!(parsed.minor_gridlines.is_some());
15676 assert!(parsed.title.is_some());
15677 assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0");
15678 assert_eq!(parsed.major_tick_mark, TickMark::Cross);
15679 assert_eq!(parsed.minor_tick_mark, TickMark::Inside);
15680 assert_eq!(parsed.tick_label_position, TickLabelPosition::High);
15681 assert!(parsed.sp_pr.is_some());
15682 assert!(parsed.tx_pr.is_some());
15683 assert_eq!(parsed.cross_axis.value(), -1_004_303_696);
15684
15685 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15686 assert!(written.starts_with(&format!(
15687 "<c:{local} xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
15688 )));
15689 assert!(written.contains("<c:axId val=\"-1125255920\""));
15690 let mut cursor = 0usize;
15691 for tag in [
15692 "<c:axId",
15693 "<c:scaling",
15694 "<c:delete",
15695 "<c:axPos",
15696 "<c:majorGridlines",
15697 "<c:minorGridlines",
15698 "<c:title",
15699 "<c:numFmt",
15700 "<c:majorTickMark",
15701 "<c:minorTickMark",
15702 "<c:tickLblPos",
15703 "<c:spPr",
15704 "<c:txPr",
15705 "<c:crossAx",
15706 ] {
15707 let position = written[cursor..]
15708 .find(tag)
15709 .unwrap_or_else(|| panic!("missing ordered axis tag {tag}"));
15710 cursor += position + tag.len();
15711 }
15712 assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
15713 }
15714
15715 let mut constructed = Axis::new(
15716 AxisKind::Value,
15717 AxisId::new(1).unwrap(),
15718 AxisPosition::Left,
15719 AxisId::new(2).unwrap(),
15720 );
15721 assert_eq!(
15722 constructed,
15723 Axis::from_xml(&constructed.to_xml().unwrap()).unwrap()
15724 );
15725 constructed.kind = AxisKind::Category;
15726 constructed.id = AxisId::new(3).unwrap();
15727 constructed.cross_axis = AxisId::new(4).unwrap();
15728 constructed.scaling.minimum = Some(0.0);
15729 constructed.scaling.maximum = Some(10.0);
15730 let written = constructed.to_xml().unwrap();
15731 assert!(written.starts_with(b"<c:catAx"));
15732 assert_eq!(constructed, Axis::from_xml(&written).unwrap());
15733 }
15734
15735 #[test]
15736 fn axis_equality_normalizes_ids_without_losing_lexical_preservation() {
15737 let padded = Axis::from_xml(
15738 standalone_axis(
15739 "catAx",
15740 "<c:axId val=\"01\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"002\"/>",
15741 )
15742 .as_bytes(),
15743 )
15744 .unwrap();
15745 let normalized = Axis::from_xml(
15746 standalone_axis(
15747 "catAx",
15748 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15749 )
15750 .as_bytes(),
15751 )
15752 .unwrap();
15753 assert_eq!(padded, normalized);
15754
15755 let preserved = String::from_utf8(padded.to_xml().unwrap()).unwrap();
15756 assert!(preserved.contains("<c:axId val=\"01\""));
15757 assert!(preserved.contains("<c:crossAx val=\"002\""));
15758
15759 let mut mutated = padded;
15760 mutated.id = AxisId::new(3).unwrap();
15761 mutated.cross_axis = AxisId::new(4).unwrap();
15762 let rewritten = String::from_utf8(mutated.to_xml().unwrap()).unwrap();
15763 assert!(rewritten.contains("<c:axId val=\"3\""));
15764 assert!(rewritten.contains("<c:crossAx val=\"4\""));
15765 assert!(!rewritten.contains("val=\"01\""));
15766 assert!(!rewritten.contains("val=\"002\""));
15767 }
15768
15769 #[test]
15770 fn malformed_axis_values_return_errors_without_panicking() {
15771 let cases = [
15772 standalone_axis(
15773 "catAx",
15774 "<c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15775 ),
15776 standalone_axis(
15777 "catAx",
15778 "<c:axId val=\"1\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15779 ),
15780 standalone_axis(
15781 "catAx",
15782 "<c:axId val=\"1\"/><c:scaling/><c:crossAx val=\"2\"/>",
15783 ),
15784 standalone_axis(
15785 "catAx",
15786 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/>",
15787 ),
15788 standalone_axis(
15789 "catAx",
15790 "<c:axId val=\"-2147483649\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15791 ),
15792 standalone_axis(
15793 "catAx",
15794 "<c:axId val=\"4294967296\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15795 ),
15796 standalone_axis(
15797 "catAx",
15798 "<c:axId val=\"1\"/><c:scaling><c:logBase val=\"1\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15799 ),
15800 standalone_axis(
15801 "catAx",
15802 "<c:axId val=\"1\"/><c:scaling><c:max val=\"NaN\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15803 ),
15804 standalone_axis(
15805 "catAx",
15806 "<c:axId val=\"1\"/><c:scaling><c:max val=\"1\"/><c:min val=\"2\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15807 ),
15808 standalone_axis(
15809 "catAx",
15810 "<c:axId val=\"1\"/><c:scaling><c:orientation val=\"sideways\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15811 ),
15812 standalone_axis(
15813 "catAx",
15814 "<c:axId val=\"1\"/><c:scaling/><c:delete val=\"yes\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15815 ),
15816 standalone_axis(
15817 "catAx",
15818 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"middle\"/><c:crossAx val=\"2\"/>",
15819 ),
15820 standalone_axis(
15821 "catAx",
15822 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:majorTickMark val=\"near\"/><c:crossAx val=\"2\"/>",
15823 ),
15824 standalone_axis(
15825 "catAx",
15826 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:tickLblPos val=\"middle\"/><c:crossAx val=\"2\"/>",
15827 ),
15828 standalone_axis(
15829 "catAx",
15830 "<c:axId val=\"1\"/><c:axId val=\"2\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15831 ),
15832 standalone_axis(
15833 "catAx",
15834 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt sourceLinked=\"1\"/><c:crossAx val=\"2\"/>",
15835 ),
15836 standalone_axis(
15837 "catAx",
15838 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt formatCode=\"0\" sourceLinked=\"maybe\"/><c:crossAx val=\"2\"/>",
15839 ),
15840 ];
15841 for xml in cases {
15842 let result = std::panic::catch_unwind(|| Axis::from_xml(xml.as_bytes()));
15843 assert!(result.is_ok(), "axis parser panicked for {xml}");
15844 assert!(result.unwrap().is_err(), "malformed axis parsed: {xml}");
15845 }
15846
15847 let mut axis = Axis::new(
15848 AxisKind::Value,
15849 AxisId::new(1).unwrap(),
15850 AxisPosition::Left,
15851 AxisId::new(2).unwrap(),
15852 );
15853 axis.scaling.maximum = Some(f64::INFINITY);
15854 assert!(axis.to_xml().is_err());
15855 }
15856
15857 #[test]
15858 fn axes_preserve_unmodelled_children_byte_for_byte() {
15859 let xml = format!(
15860 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>"#
15861 );
15862 let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
15863 let mut relabelled = parsed.clone();
15864 relabelled.kind = AxisKind::Value;
15865 assert!(
15866 relabelled.to_xml().is_err(),
15867 "a parsed category axis must not retain its tail under a value-axis root"
15868 );
15869 let written = parsed.to_xml().unwrap();
15870 for raw in [
15871 br#"<!--before-->"#.as_slice(),
15872 br#"<?after-id?>"#.as_slice(),
15873 br#"<!--scale-->"#.as_slice(),
15874 br#"<q:extLst><q:ext uri="scale"><x:data/></q:ext></q:extLst>"#.as_slice(),
15875 br#"<x:grid/>"#.as_slice(),
15876 br#"<x:title/>"#.as_slice(),
15877 br#"<q:crosses val="autoZero"/>"#.as_slice(),
15878 br#"<q:auto val="1"><x:auto/></q:auto>"#.as_slice(),
15879 br#"<q:extLst><q:ext uri="axis"><x:tail/></q:ext></q:extLst>"#.as_slice(),
15880 br#"<!--after-->"#.as_slice(),
15881 ] {
15882 assert!(
15883 written.windows(raw.len()).any(|window| window == raw),
15884 "preserved axis bytes changed: {}",
15885 String::from_utf8_lossy(raw)
15886 );
15887 }
15888 let written = String::from_utf8(written).unwrap();
15889 assert!(written.starts_with("<c:catAx"));
15890 assert!(written.contains("x:keep=\"axis\""));
15891 assert!(written.contains("x:keep=\"id\""));
15892 assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
15893 }
15894
15895 #[test]
15896 fn every_corpus_axis_round_trips_structurally() {
15897 let Some(corpus) = require_or_skip_corpus() else {
15898 return;
15899 };
15900 verify_fetched_corpus(&corpus);
15901 let mut axis_count = 0usize;
15902 let mut chart_parts = 0usize;
15903 let mut kinds = HashSet::new();
15904 for path in manifest_paths() {
15905 let package = OpcPackage::open(corpus.join(path))
15906 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
15907 for (part, xml) in &package.parts {
15908 if !is_chart_part(part) {
15909 continue;
15910 }
15911 let chart = CT_ChartSpace::from_xml(xml)
15912 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
15913 let axes = chart.chart.plot_area.axes().unwrap_or_else(|error| {
15914 panic!("{path} {part}: axis projection failed: {error}")
15915 });
15916 for axis in axes {
15917 kinds.insert(axis.kind);
15918 let written = axis.to_xml().unwrap_or_else(|error| {
15919 panic!("{path} {part}: axis write failed: {error}")
15920 });
15921 let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
15922 panic!("{path} {part}: written axis parse failed: {error}")
15923 });
15924 assert_eq!(axis, reparsed, "{path} {part}: axis model changed");
15925 axis_count += 1;
15926 }
15927 chart_parts += 1;
15928 }
15929 }
15930 assert!(
15931 axis_count > 0,
15932 "the pinned corpus contained no ChartML axes"
15933 );
15934 assert!(kinds.contains(&AxisKind::Category));
15935 assert!(kinds.contains(&AxisKind::Value));
15936
15937 for kind in ["dateAx", "serAx"] {
15938 let parsed = Axis::from_xml(
15939 standalone_axis(
15940 kind,
15941 "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15942 )
15943 .as_bytes(),
15944 )
15945 .unwrap();
15946 assert_eq!(parsed, Axis::from_xml(&parsed.to_xml().unwrap()).unwrap());
15947 }
15948 eprintln!(
15949 "ChartML axis corpus gate checked {axis_count} axes across {chart_parts} chart parts"
15950 );
15951 }
15952
15953 #[test]
15954 fn series_formula_and_cache_are_consistent_with_one_source() {
15955 let values = NumericData::new(
15956 "'Sales 24'!$B$2:$B$4".to_owned(),
15957 "0.0".to_owned(),
15958 vec![4.25, 8.5, 17.0],
15959 )
15960 .unwrap();
15961 let mut series = Series::new(3, 1, values);
15962 series.categories = Some(AxisData::String(
15963 StringRef::new(
15964 "'Sales 24'!$A$2:$A$4".to_owned(),
15965 vec!["North".to_owned(), "South".to_owned(), "West".to_owned()],
15966 )
15967 .unwrap(),
15968 ));
15969
15970 let written = String::from_utf8(series.to_xml().unwrap()).unwrap();
15971 assert!(written.contains("<c:f>'Sales 24'!$B$2:$B$4</c:f>"));
15972 assert!(written.contains("<c:formatCode>0.0</c:formatCode>"));
15973 assert_eq!(written.matches("<c:ptCount val=\"3\"").count(), 2);
15974 for (index, value) in ["4.25", "8.5", "17"].iter().enumerate() {
15975 assert!(written.contains(&format!("<c:pt idx=\"{index}\"><c:v>{value}</c:v></c:pt>")));
15976 }
15977 let reparsed = Series::from_xml(written.as_bytes()).unwrap();
15978 assert_eq!(reparsed.index, series.index);
15979 assert_eq!(reparsed.order, series.order);
15980 match (&reparsed.categories, &series.categories) {
15981 (Some(AxisData::String(left)), Some(AxisData::String(right))) => {
15982 assert_eq!(left.formula, right.formula);
15983 assert_eq!(left.values, right.values);
15984 }
15985 _ => panic!("expected string categories"),
15986 }
15987 assert_eq!(reparsed.values.formula, series.values.formula);
15988 assert_eq!(reparsed.values.format_code, series.values.format_code);
15989 assert_eq!(reparsed.values.values, series.values.values);
15990 }
15991
15992 #[test]
15993 fn string_and_numeric_references_write_fixed_prefixes_in_schema_order() {
15994 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>"#;
15995 let string_ref = StringRef::from_xml(string_xml).unwrap();
15996 let written = String::from_utf8(string_ref.to_xml().unwrap()).unwrap();
15997 assert!(written.starts_with("<c:strRef xmlns:c="));
15998 assert!(written.find("<c:f>").unwrap() < written.find("<c:strCache>").unwrap());
15999 assert!(written.find("<c:ptCount").unwrap() < written.find("<c:pt idx=").unwrap());
16000
16001 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>"#;
16002 let numeric = NumericData::from_xml(numeric_xml).unwrap();
16003 let written = String::from_utf8(numeric.to_xml().unwrap()).unwrap();
16004 let positions: Vec<_> = [
16005 "<c:f>",
16006 "<c:numCache>",
16007 "<c:formatCode>",
16008 "<c:ptCount",
16009 "<c:pt idx=",
16010 ]
16011 .iter()
16012 .map(|tag| written.find(tag).unwrap())
16013 .collect();
16014 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
16015 assert_eq!(numeric, NumericData::from_xml(written.as_bytes()).unwrap());
16016 }
16017
16018 #[test]
16019 fn mixed_chartml_aliases_resolve_by_namespace_uri() {
16020 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>"#;
16021 let parsed = Series::from_xml(xml).unwrap();
16022 assert_eq!(parsed.index, 4);
16023 assert_eq!(parsed.order, 2);
16024 assert_eq!(parsed.values.values, vec![1.5, 2.5]);
16025 assert!(matches!(parsed.categories, Some(AxisData::String(_))));
16026 assert_eq!(parsed, Series::from_xml(&parsed.to_xml().unwrap()).unwrap());
16027
16028 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>"#;
16029 let parsed = Series::from_xml(foreign).unwrap();
16030 assert!(parsed.sp_pr.as_ref().unwrap().fill.is_none());
16031 let written = parsed.to_xml().unwrap();
16032 assert!(
16033 std::str::from_utf8(&written)
16034 .unwrap()
16035 .contains(r#"<x:solidFill><x:srgbClr val="112233"/></x:solidFill>"#)
16036 );
16037 assert_eq!(parsed, Series::from_xml(&written).unwrap());
16038 }
16039
16040 #[test]
16041 fn plot_area_series_ignores_inherited_foreign_plot_aliases() {
16042 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>"#;
16043 let chart = CT_ChartSpace::from_xml(xml).unwrap();
16044 let series = chart.chart.plot_area.series().unwrap();
16045 assert_eq!(series.len(), 1);
16046 assert_eq!(series[0].index, 1);
16047 let written = String::from_utf8(series[0].to_xml().unwrap()).unwrap();
16048 assert!(
16049 written.contains(r#"xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart""#)
16050 );
16051 assert!(written.contains(r#"xmlns:x="urn:producer""#));
16052 assert!(written.contains("<q:marker><x:data/></q:marker>"));
16053 assert_eq!(series[0], Series::from_xml(written.as_bytes()).unwrap());
16054
16055 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>"#;
16056 let chart = CT_ChartSpace::from_xml(conflicting).unwrap();
16057 assert!(chart.chart.plot_area.series().is_err());
16058 }
16059
16060 #[test]
16061 fn malformed_series_and_cache_values_return_errors_without_panicking() {
16062 let cases: &[&[u8]] = &[
16063 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>"#,
16064 br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/></c:ser>"#,
16065 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>"#,
16066 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>"#,
16067 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>"#,
16068 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>"#,
16069 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>"#,
16070 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>"#,
16071 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>"#,
16072 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>"#,
16073 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>"#,
16074 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>"#,
16075 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>"#,
16076 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>"#,
16077 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>"#,
16078 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>"#,
16079 ];
16080 for xml in cases {
16081 let result = std::panic::catch_unwind(|| Series::from_xml(xml));
16082 assert!(result.is_ok(), "series parser panicked");
16083 assert!(
16084 result.unwrap().is_err(),
16085 "malformed series parsed: {}",
16086 String::from_utf8_lossy(xml)
16087 );
16088 }
16089 assert!(
16090 NumericData::new(
16091 "S!$A$1".to_owned(),
16092 "General".to_owned(),
16093 vec![f64::INFINITY]
16094 )
16095 .is_err()
16096 );
16097 }
16098
16099 #[test]
16100 fn chart_text_rejects_xml_forbidden_characters_and_escapes_metacharacters() {
16101 assert!(StringRef::new("S!$A$1\u{1}".to_owned(), Vec::new()).is_err());
16102 assert!(StringRef::new("S!$A$1".to_owned(), vec!["invalid\u{1}".to_owned()]).is_err());
16103 assert!(
16104 NumericData::new("S!$A$1\u{1}".to_owned(), "General".to_owned(), Vec::new()).is_err()
16105 );
16106 assert!(
16107 NumericData::new("S!$A$1".to_owned(), "General\u{1}".to_owned(), Vec::new()).is_err()
16108 );
16109
16110 let mut strings = StringRef::new("S!$A$1".to_owned(), vec!["valid".to_owned()]).unwrap();
16111 strings.formula = "invalid\u{1}".to_owned();
16112 assert!(strings.to_xml().is_err());
16113 strings.formula = "S!$A$1".to_owned();
16114 strings.values[0] = "invalid\u{1}".to_owned();
16115 assert!(strings.to_xml().is_err());
16116
16117 let mut numbers =
16118 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap();
16119 numbers.formula = "invalid\u{1}".to_owned();
16120 assert!(numbers.to_xml().is_err());
16121 numbers.formula = "S!$A$1".to_owned();
16122 numbers.format_code = "invalid\u{1}".to_owned();
16123 assert!(numbers.to_xml().is_err());
16124
16125 let strings =
16126 StringRef::new("S!$A$1&".to_owned(), vec!["North < West & East".to_owned()]).unwrap();
16127 let written = String::from_utf8(strings.to_xml().unwrap()).unwrap();
16128 assert!(written.contains("S!$A$1&"));
16129 assert!(written.contains("North < West & East"));
16130 }
16131
16132 #[test]
16133 fn series_preserves_unmodelled_children_byte_for_byte() {
16134 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>"#;
16135 let parsed = Series::from_xml(xml).unwrap();
16136 let written = parsed.to_xml().unwrap();
16137 for raw in [
16138 br#"<!--before-->"#.as_slice(),
16139 br#"<x:between/>"#.as_slice(),
16140 br#"<q:dPt x:id="one"><q:idx val="0"/></q:dPt>"#.as_slice(),
16141 br#"<q:trendline x:id="trend"/>"#.as_slice(),
16142 br#"<q:extLst><q:ext uri="keep"><x:data/></q:ext></q:extLst>"#.as_slice(),
16143 br#"<!--point-->"#.as_slice(),
16144 br#"<x:cacheExt/>"#.as_slice(),
16145 ] {
16146 assert!(
16147 written.windows(raw.len()).any(|window| window == raw),
16148 "preserved series bytes changed: {}",
16149 String::from_utf8_lossy(raw)
16150 );
16151 }
16152 let written = String::from_utf8(written.clone()).unwrap();
16153 assert!(written.contains(r#"x:keep="series""#));
16154 assert!(written.contains(r#"x:keep="formula""#));
16155 assert!(written.contains("<c:dLbls"));
16156 assert!(written.contains(r#"x:id="labels""#));
16157 assert!(parsed.data_labels.is_some());
16158 assert_eq!(parsed, Series::from_xml(written.as_bytes()).unwrap());
16159 }
16160
16161 #[test]
16162 fn public_series_edits_do_not_duplicate_or_drop_preserved_payloads() {
16163 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>"#;
16164 let parsed = Series::from_xml(opaque_xml).unwrap();
16165
16166 let mut edited = parsed.clone();
16167 edited.name = Some(StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap());
16168 assert!(edited.to_xml().is_err());
16169
16170 let mut edited = parsed.clone();
16171 edited.categories = Some(AxisData::Numeric(
16172 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap(),
16173 ));
16174 assert!(edited.to_xml().is_err());
16175
16176 let mut edited = parsed;
16177 edited.bubble_size =
16178 Some(NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
16179 assert!(edited.to_xml().is_err());
16180
16181 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>"#;
16182 let parsed = Series::from_xml(cache_xml).unwrap();
16183 let tail = "<c:extLst><c:ext uri=\"keep\"><x:data/></c:ext></c:extLst>";
16184
16185 let mut shortened = parsed.clone();
16186 shortened.values.values.truncate(1);
16187 let written = String::from_utf8(shortened.to_xml().unwrap()).unwrap();
16188 assert!(!written.contains("<c:pt idx=\"1\""));
16189 assert!(written.find("<c:pt idx=\"0\"").unwrap() < written.find(tail).unwrap());
16190 assert!(Series::from_xml(written.as_bytes()).is_ok());
16191
16192 let mut grown = parsed;
16193 grown.values.values.push(3.0);
16194 let written = String::from_utf8(grown.to_xml().unwrap()).unwrap();
16195 assert!(written.find("<c:pt idx=\"2\"").unwrap() < written.find(tail).unwrap());
16196 assert!(Series::from_xml(written.as_bytes()).is_ok());
16197
16198 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>"#;
16199 let mut edited = Series::from_xml(ordered_xml).unwrap();
16200 edited.name = Some(StringRef::new("S!$B$1".to_owned(), Vec::new()).unwrap());
16201 edited.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
16202 edited.categories = Some(AxisData::String(
16203 StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap(),
16204 ));
16205 edited.bubble_size =
16206 Some(NumericData::new("S!$C$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
16207 let written = String::from_utf8(edited.to_xml().unwrap()).unwrap();
16208 let positions: Vec<_> = [
16209 "<c:tx>",
16210 "<c:spPr",
16211 "<c:marker>",
16212 "<c:cat>",
16213 "<c:val>",
16214 "<c:bubbleSize>",
16215 "<c:extLst>",
16216 ]
16217 .iter()
16218 .map(|tag| written.find(tag).unwrap())
16219 .collect();
16220 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
16221 assert!(Series::from_xml(written.as_bytes()).is_ok());
16222 }
16223
16224 #[test]
16225 fn every_corpus_series_round_trips_structurally() {
16226 let Some(corpus) = require_or_skip_corpus() else {
16227 return;
16228 };
16229 verify_fetched_corpus(&corpus);
16230 let mut series_count = 0usize;
16231 let mut chart_parts = 0usize;
16232 for path in manifest_paths() {
16233 let package = OpcPackage::open(corpus.join(path))
16234 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
16235 for (part, xml) in &package.parts {
16236 if !is_chart_part(part) {
16237 continue;
16238 }
16239 let chart = CT_ChartSpace::from_xml(xml)
16240 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
16241 let parsed_series =
16242 chart.chart.plot_area.series().unwrap_or_else(|error| {
16243 panic!("{path} {part}: series parse failed: {error}")
16244 });
16245 for series in parsed_series {
16246 let written = series.to_xml().unwrap_or_else(|error| {
16247 panic!("{path} {part}: series write failed: {error}")
16248 });
16249 let reparsed = Series::from_xml(&written).unwrap_or_else(|error| {
16250 panic!("{path} {part}: written series parse failed: {error}")
16251 });
16252 assert_eq!(series, reparsed, "{path} {part}: series model changed");
16253 series_count += 1;
16254 }
16255 chart_parts += 1;
16256 }
16257 }
16258 assert!(
16259 series_count > 0,
16260 "the pinned corpus contained no supported series"
16261 );
16262 eprintln!(
16263 "ChartML series corpus gate checked {series_count} series across {chart_parts} chart parts"
16264 );
16265 }
16266
16267 #[test]
16268 fn chart_space_reads_aliases_and_writes_fixed_prefixes_in_schema_order() {
16269 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>"#;
16270 let parsed = CT_ChartSpace::from_xml(xml).unwrap();
16271 assert!(!parsed.chart.auto_title_deleted);
16272 assert!(!parsed.chart.plot_vis_only);
16273 assert_eq!(parsed.chart.disp_blanks_as, DispBlanksAs::Span);
16274 assert!(parsed.chart.title.is_some());
16275 assert!(parsed.chart.legend.is_some());
16276 assert!(parsed.sp_pr.as_ref().unwrap().fill.is_some());
16277 assert!(parsed.tx_pr.is_some());
16278
16279 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
16280 assert!(written.starts_with(
16281 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""#
16282 ));
16283 for tag in [
16284 "<c:chart>",
16285 "<c:title>",
16286 "<c:plotArea>",
16287 "<c:legend>",
16288 "<c:plotVisOnly",
16289 "<c:dispBlanksAs",
16290 "<c:spPr>",
16291 "<c:txPr",
16292 ] {
16293 assert!(written.contains(tag), "missing fixed-prefix tag {tag}");
16294 }
16295 let positions: Vec<_> = [
16296 "<c:title>",
16297 "<c:plotArea>",
16298 "<c:legend>",
16299 "<c:plotVisOnly",
16300 "<c:dispBlanksAs",
16301 "<c:spPr>",
16302 "<c:txPr",
16303 ]
16304 .iter()
16305 .map(|tag| written.find(tag).unwrap())
16306 .collect();
16307 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
16308 assert!(written.contains("<a:solidFill><a:srgbClr"));
16309 assert!(written.contains("<d:bodyPr/><d:p"));
16310 assert!(written.contains(&format!("xmlns:d=\"{A_NS}\"")));
16311 assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
16312 }
16313
16314 #[test]
16315 fn public_core_edits_keep_preserved_children_in_their_schema_slots() {
16316 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>"#;
16317 let mut parsed = CT_ChartSpace::from_xml(xml).unwrap();
16318 parsed.chart.auto_title_deleted = true;
16319 parsed.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
16320 parsed.tx_pr =
16321 Some(CT_TextBody::from_xml(br#"<a:txBody><a:bodyPr/><a:p/></a:txBody>"#).unwrap());
16322 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
16323 let positions: Vec<_> = [
16324 "<c:autoTitleDeleted",
16325 "<c:pivotFmts>",
16326 "<c:plotArea",
16327 "<c:spPr",
16328 "<c:txPr",
16329 "<c:externalData",
16330 ]
16331 .iter()
16332 .map(|tag| written.find(tag).unwrap())
16333 .collect();
16334 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
16335 assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
16336 }
16337
16338 #[test]
16339 fn malformed_core_chart_values_return_errors_without_panicking() {
16340 let cases: &[&[u8]] = &[
16341 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"/>"#,
16342 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart/></c:chartSpace>"#,
16343 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:plotArea/></c:chart></c:chartSpace>"#,
16344 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:autoTitleDeleted val="yes"/><c:plotArea/></c:chart></c:chartSpace>"#,
16345 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:dispBlanksAs val="empty"/></c:chart></c:chartSpace>"#,
16346 br#"<c:chartSpace xmlns:c="urn:wrong"><c:chart><c:plotArea/></c:chart></c:chartSpace>"#,
16347 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:foreign"><x:chart><x:plotArea/></x:chart></c:chartSpace>"#,
16348 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>"#,
16349 br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:title/><c:title/><c:plotArea/></c:chart></c:chartSpace>"#,
16350 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>"#,
16351 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>"#,
16352 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>"#,
16353 ];
16354 for xml in cases {
16355 let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml));
16356 assert!(result.is_ok(), "ChartML parser panicked");
16357 assert!(
16358 result.unwrap().is_err(),
16359 "malformed ChartML parsed: {}",
16360 String::from_utf8_lossy(xml)
16361 );
16362 }
16363 }
16364
16365 #[test]
16366 fn fx215_chart_text_adapter_preserves_qualified_and_foreign_owners() {
16367 for (prefix, declarations) in [
16368 ("d:", format!("xmlns:d=\"{A_NS}\"")),
16369 ("", format!("xmlns=\"{A_NS}\"")),
16370 ] {
16371 let opaque = "<x:r><x:t>foreign</x:t></x:r>";
16372 let xml = format!(
16373 "<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>"
16374 );
16375 let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16376 let text = chart.tx_pr.as_ref().unwrap();
16377 assert_eq!(text.paragraphs().len(), 1);
16378 assert_eq!(text.paragraphs()[0].runs.len(), 1);
16379 let written = chart.to_xml().unwrap();
16380 assert!(std::str::from_utf8(&written).unwrap().contains(opaque));
16381 let reopened = CT_ChartSpace::from_xml(&written).unwrap();
16382 assert_eq!(
16383 reopened.tx_pr.as_ref().unwrap().paragraphs()[0].runs.len(),
16384 1
16385 );
16386 assert_eq!(
16387 text.body_properties,
16388 reopened.tx_pr.as_ref().unwrap().body_properties
16389 );
16390 for body in [text, reopened.tx_pr.as_ref().unwrap()] {
16391 let oxml_drawing::text::TextRun::Run(run) = &body.paragraphs()[0].runs[0] else {
16392 panic!("qualified text must remain a regular run");
16393 };
16394 assert_eq!(run.text.value, "owned");
16395 }
16396 }
16397 let xml = format!(
16398 "<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>"
16399 );
16400 let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16401 assert!(
16402 chart.tx_pr.as_ref().unwrap().paragraphs()[0]
16403 .runs
16404 .is_empty()
16405 );
16406 let saved = chart.to_xml().unwrap();
16407 assert!(
16408 std::str::from_utf8(&saved)
16409 .unwrap()
16410 .contains("<x:r><x:t>foreign</x:t></x:r>")
16411 );
16412 assert_eq!(chart, CT_ChartSpace::from_xml(&saved).unwrap());
16413 }
16414
16415 #[test]
16416 fn point_label_overrides_render_without_changing_preserved_xml() {
16417 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>"#;
16418 let plot = bar_plot("").replace(
16419 r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
16420 &format!(
16421 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>"#
16422 ),
16423 );
16424 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
16425 let plot = &chart.chart.plot_area.plots().unwrap()[0];
16426 let Plot::Bar { data_labels, .. } = plot else {
16427 panic!("expected bar plot");
16428 };
16429 let labels = data_labels.as_ref().unwrap();
16430 let written = labels.to_xml().unwrap();
16431 assert!(
16432 written
16433 .windows(raw_overrides.len())
16434 .any(|window| window == raw_overrides.as_bytes())
16435 );
16436 let overrides = super::point_label_overrides(labels).unwrap();
16437 assert_eq!(overrides.keys().copied().collect::<Vec<_>>(), vec![1]);
16438
16439 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
16440 let mut rendered = Vec::new();
16441 super::render_data_labels(
16442 plot,
16443 chart_bounds(),
16444 DispBlanksAs::Gap,
16445 super::GeometryOptions::default(),
16446 &mut fonts,
16447 &mut rendered,
16448 )
16449 .unwrap();
16450 let labels: Vec<_> = rendered
16451 .iter()
16452 .filter_map(|element| match element {
16453 PositionedElement::Text(run) => Some(run),
16454 _ => None,
16455 })
16456 .collect();
16457 assert_eq!(labels.len(), 2);
16458 assert_eq!(labels[0].text, "North, 1");
16459 assert_eq!(labels[1].text, "South / 2.0");
16460 assert_point_near(labels[0].origin, Point { x: 50.0, y: 105.0 });
16461 assert_point_near(labels[1].origin, Point { x: 158.0, y: 70.0 });
16462 assert_eq!(
16463 CT_DLbls::from_xml(&written).unwrap().to_xml().unwrap(),
16464 written
16465 );
16466 }
16467
16468 #[test]
16469 fn malformed_point_label_overrides_return_contextual_errors() {
16470 let cases = [
16471 r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:showVal val="1"/></q:dLbl></q:dLbls>"#,
16472 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>"#,
16473 r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="point"/></q:dLbl></q:dLbls>"#,
16474 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>"#,
16475 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>"#,
16476 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>"#,
16477 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>"#,
16478 ];
16479 for xml in cases {
16480 let labels = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
16481 let result = std::panic::catch_unwind(|| super::point_label_overrides(&labels));
16482 assert!(result.is_ok(), "point-label parser panicked for {xml}");
16483 assert!(
16484 result.unwrap().is_err(),
16485 "malformed point labels parsed: {xml}"
16486 );
16487 }
16488
16489 let foreign = format!(
16490 r#"<q:dLbls xmlns:q="{C_NS}" xmlns:x="urn:foreign"><x:dLbl><x:idx val="0"/></x:dLbl></q:dLbls>"#
16491 );
16492 let labels = CT_DLbls::from_xml(foreign.as_bytes()).unwrap();
16493 assert!(super::point_label_overrides(&labels).unwrap().is_empty());
16494 }
16495
16496 #[test]
16497 fn percentage_label_aggregation_rejects_nonfinite_totals() {
16498 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
16499 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
16500 let Plot::Bar { series, .. } = plot else {
16501 panic!("expected bar plot");
16502 };
16503 series[0].values.values = vec![f64::MAX, f64::MAX];
16504 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
16505 super::render_data_labels(
16506 plot,
16507 chart_bounds(),
16508 DispBlanksAs::Gap,
16509 super::GeometryOptions::default(),
16510 &mut fonts,
16511 &mut Vec::new(),
16512 )
16513 .expect("value-only labels do not need a percentage total");
16514 let Plot::Bar { data_labels, .. } = plot else {
16515 panic!("expected bar plot");
16516 };
16517 data_labels.as_mut().unwrap().show_percent = true;
16518 let error = super::render_data_labels(
16519 plot,
16520 chart_bounds(),
16521 DispBlanksAs::Gap,
16522 super::GeometryOptions::default(),
16523 &mut fonts,
16524 &mut Vec::new(),
16525 )
16526 .unwrap_err();
16527 assert!(error.to_string().contains("data-label percentage total"));
16528
16529 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>"#;
16530 let plot_xml = bar_plot("").replace(
16531 r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
16532 &format!(
16533 r#"<q:dLbls>{overrides}<q:showVal val="1"/><q:showPercent val="1"/></q:dLbls>"#
16534 ),
16535 );
16536 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
16537 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
16538 let Plot::Bar { series, .. } = plot else {
16539 panic!("expected bar plot");
16540 };
16541 series[0].values.values = vec![f64::MAX, f64::MAX];
16542 super::render_data_labels(
16543 plot,
16544 chart_bounds(),
16545 DispBlanksAs::Gap,
16546 super::GeometryOptions::default(),
16547 &mut fonts,
16548 &mut Vec::new(),
16549 )
16550 .expect("effective point overrides disable percentage aggregation");
16551 }
16552
16553 #[test]
16554 fn percentage_labels_use_effective_number_format_precision() {
16555 let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
16556 let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
16557 let Plot::Bar {
16558 series,
16559 data_labels,
16560 ..
16561 } = plot
16562 else {
16563 panic!("expected bar plot");
16564 };
16565 series[0].values.values = vec![1.0, 2.0];
16566 let labels = data_labels.as_mut().unwrap();
16567 labels.show_value = false;
16568 labels.show_percent = true;
16569 labels.number_format = Some(NumberFormat::new("0.00%".to_owned(), false).unwrap());
16570 let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
16571 let mut rendered = Vec::new();
16572 super::render_data_labels(
16573 plot,
16574 chart_bounds(),
16575 DispBlanksAs::Gap,
16576 super::GeometryOptions::default(),
16577 &mut fonts,
16578 &mut rendered,
16579 )
16580 .unwrap();
16581 let text: Vec<_> = rendered
16582 .iter()
16583 .filter_map(|element| match element {
16584 PositionedElement::Text(run) => Some(run.text.as_str()),
16585 _ => None,
16586 })
16587 .collect();
16588 assert_eq!(text, vec!["33.33%", "66.67%"]);
16589
16590 let point_override = r#"<q:dLbl><q:idx val="0"/><q:numFmt formatCode="0.00%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbl>"#;
16591 let plot_xml = bar_plot("").replace(
16592 r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
16593 &format!(
16594 r#"<q:dLbls>{point_override}<q:numFmt formatCode="0%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbls>"#
16595 ),
16596 );
16597 let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
16598 let plot = &chart.chart.plot_area.plots().unwrap()[0];
16599 let mut rendered = Vec::new();
16600 super::render_data_labels(
16601 plot,
16602 chart_bounds(),
16603 DispBlanksAs::Gap,
16604 super::GeometryOptions::default(),
16605 &mut fonts,
16606 &mut rendered,
16607 )
16608 .unwrap();
16609 let text: Vec<_> = rendered
16610 .iter()
16611 .filter_map(|element| match element {
16612 PositionedElement::Text(run) => Some(run.text.as_str()),
16613 _ => None,
16614 })
16615 .collect();
16616 assert_eq!(text, vec!["33.33%", "67%"]);
16617 }
16618
16619 #[test]
16620 fn core_chart_shells_preserve_unmodelled_children_byte_for_byte() {
16621 let preserved = [
16622 br#"<x:root x:id="1"><x:nested>one & two</x:nested><!--root--></x:root>"#
16623 .as_slice(),
16624 br#"<x:title x:id="2"><x:nested><![CDATA[title]]></x:nested></x:title>"#.as_slice(),
16625 br#"<x:plot x:id="3"><x:nested>plot</x:nested></x:plot>"#.as_slice(),
16626 br#"<x:legend x:id="4"><x:nested>legend</x:nested></x:legend>"#.as_slice(),
16627 br#"<x:extension x:id="5"><x:nested>extension</x:nested></x:extension>"#.as_slice(),
16628 br#"<!--space-comment-->"#.as_slice(),
16629 br#"<?space processing?>"#.as_slice(),
16630 br#"<!--chart-comment-->"#.as_slice(),
16631 br#"<?title processing?>"#.as_slice(),
16632 br#"<!--flag-comment-->"#.as_slice(),
16633 br#"<?flag processing?>"#.as_slice(),
16634 br#"<x:producerShape/>"#.as_slice(),
16635 br#"<x:producerText/>"#.as_slice(),
16636 ];
16637 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>"#;
16638 let parsed = CT_ChartSpace::from_xml(xml).unwrap();
16639 let written = parsed.to_xml().unwrap();
16640 for raw in preserved {
16641 assert!(
16642 written.windows(raw.len()).any(|window| window == raw),
16643 "preserved subtree changed: {}",
16644 String::from_utf8_lossy(raw)
16645 );
16646 }
16647 let written_text = String::from_utf8(written.clone()).unwrap();
16648 assert!(written_text.contains(r#"x:keep="one & two""#));
16649 assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
16650 }
16651
16652 #[test]
16653 fn every_corpus_chart_part_round_trips_structurally() {
16654 let Some(corpus) = require_or_skip_corpus() else {
16655 return;
16656 };
16657 verify_fetched_corpus(&corpus);
16658 let mut chart_parts = 0usize;
16659 let mut decks_with_charts = HashSet::new();
16660 for path in manifest_paths() {
16661 let package = OpcPackage::open(corpus.join(path))
16662 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
16663 for (part, xml) in &package.parts {
16664 if !is_chart_part(part) {
16665 continue;
16666 }
16667 let parsed = CT_ChartSpace::from_xml(xml)
16668 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
16669 let written = parsed
16670 .to_xml()
16671 .unwrap_or_else(|error| panic!("{path} {part}: write failed: {error}"));
16672 let original_preserved = preserved_chartml_regions(xml)
16673 .unwrap_or_else(|error| panic!("{path} {part}: original scan failed: {error}"));
16674 let written_preserved = preserved_chartml_regions(&written)
16675 .unwrap_or_else(|error| panic!("{path} {part}: written scan failed: {error}"));
16676 assert_eq!(
16677 original_preserved.len(),
16678 written_preserved.len(),
16679 "{path} {part}: preserved ChartML region count changed"
16680 );
16681 for (index, (original, written)) in original_preserved
16682 .iter()
16683 .zip(&written_preserved)
16684 .enumerate()
16685 {
16686 assert_eq!(
16687 original.parent_path, written.parent_path,
16688 "{path} {part}: preserved region {index} parent changed"
16689 );
16690 assert_eq!(
16691 original.boundary, written.boundary,
16692 "{path} {part}: preserved region {index} boundary changed"
16693 );
16694 assert_eq!(
16695 original.sibling_order, written.sibling_order,
16696 "{path} {part}: preserved region {index} order changed"
16697 );
16698 assert_eq!(
16699 original.xml, written.xml,
16700 "{path} {part}: preserved region {index} bytes changed"
16701 );
16702 }
16703 let reparsed = CT_ChartSpace::from_xml(&written)
16704 .unwrap_or_else(|error| panic!("{path} {part}: written parse failed: {error}"));
16705 assert_eq!(parsed, reparsed, "{path} {part}: core model changed");
16706 chart_parts += 1;
16707 decks_with_charts.insert(path);
16708 }
16709 }
16710 assert!(
16711 chart_parts > 0,
16712 "the pinned corpus contained no chart parts"
16713 );
16714 eprintln!(
16715 "ChartML corpus gate checked {chart_parts} chart parts across {} decks",
16716 decks_with_charts.len()
16717 );
16718 }
16719
16720 #[derive(Debug, Eq, PartialEq)]
16721 struct PreservedChartRegion {
16722 parent_path: Vec<Vec<u8>>,
16723 boundary: usize,
16724 sibling_order: usize,
16725 xml: Vec<u8>,
16726 }
16727
16728 struct PreservationFrame {
16729 local_name: Vec<u8>,
16730 boundary: usize,
16731 next_sibling: usize,
16732 }
16733
16734 fn preserved_chartml_regions(xml: &[u8]) -> Result<Vec<PreservedChartRegion>, String> {
16735 let mut reader = Reader::from_reader(xml);
16736 let mut buffer = Vec::new();
16737 let mut parents: Vec<PreservationFrame> = Vec::new();
16738 let mut preserved = Vec::new();
16739 loop {
16740 match reader.read_event_into(&mut buffer) {
16741 Ok(Event::Start(element)) => {
16742 let child = local_name(element.name().as_ref()).to_vec();
16743 if parents.is_empty() && child == b"chartSpace" {
16744 parents.push(PreservationFrame {
16745 local_name: child,
16746 boundary: 0,
16747 next_sibling: 0,
16748 });
16749 } else if let Some(parent) = parents.last()
16750 && is_opaque_chartml_child(&parent.local_name, &child)
16751 {
16752 let boundary =
16753 preserved_child_boundary(&parent.local_name, &child, parent.boundary);
16754 let xml = capture_element(&mut reader, &element)
16755 .map_err(|error| error.to_string())?;
16756 record_preserved_region(&mut parents, &mut preserved, boundary, xml);
16757 advance_preservation_boundary(&mut parents, &child);
16758 } else {
16759 advance_preservation_boundary(&mut parents, &child);
16760 if is_chartml_preservation_parent(&child) {
16761 parents.push(PreservationFrame {
16762 local_name: child,
16763 boundary: 0,
16764 next_sibling: 0,
16765 });
16766 } else {
16767 capture_element(&mut reader, &element)
16768 .map_err(|error| error.to_string())?;
16769 }
16770 }
16771 }
16772 Ok(Event::Empty(element)) => {
16773 let name = element.name();
16774 let child = local_name(name.as_ref()).to_vec();
16775 if let Some(parent) = parents.last()
16776 && is_opaque_chartml_child(&parent.local_name, &child)
16777 {
16778 let boundary =
16779 preserved_child_boundary(&parent.local_name, &child, parent.boundary);
16780 let xml =
16781 capture_empty_element(&element).map_err(|error| error.to_string())?;
16782 record_preserved_region(&mut parents, &mut preserved, boundary, xml);
16783 }
16784 advance_preservation_boundary(&mut parents, &child);
16785 }
16786 Ok(
16787 event @ (Event::Comment(_)
16788 | Event::PI(_)
16789 | Event::CData(_)
16790 | Event::GeneralRef(_)),
16791 ) if !parents.is_empty() => {
16792 let boundary = parents.last().expect("preservation parent").boundary;
16793 let xml = capture_event(event).map_err(|error| error.to_string())?;
16794 record_preserved_region(&mut parents, &mut preserved, boundary, xml);
16795 }
16796 Ok(Event::End(_)) => {
16797 parents.pop();
16798 }
16799 Ok(Event::Eof) => break,
16800 Ok(_) => {}
16801 Err(error) => return Err(error.to_string()),
16802 }
16803 buffer.clear();
16804 }
16805 Ok(preserved)
16806 }
16807
16808 fn record_preserved_region(
16809 parents: &mut [PreservationFrame],
16810 preserved: &mut Vec<PreservedChartRegion>,
16811 boundary: usize,
16812 xml: Vec<u8>,
16813 ) {
16814 let sibling_order = parents.last().expect("preservation parent").next_sibling;
16815 parents
16816 .last_mut()
16817 .expect("preservation parent")
16818 .next_sibling += 1;
16819 preserved.push(PreservedChartRegion {
16820 parent_path: parents
16821 .iter()
16822 .map(|parent| parent.local_name.clone())
16823 .collect(),
16824 boundary,
16825 sibling_order,
16826 xml,
16827 });
16828 }
16829
16830 fn advance_preservation_boundary(parents: &mut [PreservationFrame], child: &[u8]) {
16831 let Some(parent) = parents.last_mut() else {
16832 return;
16833 };
16834 parent.boundary = parent
16835 .boundary
16836 .max(preserved_boundary_after(&parent.local_name, child));
16837 }
16838
16839 fn preserved_child_boundary(parent: &[u8], child: &[u8], current: usize) -> usize {
16840 match parent {
16841 b"chartSpace" => match child {
16842 b"date1904" => 0,
16843 b"lang" => 1,
16844 b"roundedCorners" => 2,
16845 b"style" => 3,
16846 b"clrMapOvr" => 4,
16847 b"pivotSource" => 5,
16848 b"protection" => 6,
16849 b"externalData" => 10,
16850 b"printSettings" => 11,
16851 b"userShapes" => 12,
16852 b"extLst" => 13,
16853 _ => current,
16854 },
16855 b"chart" => match child {
16856 b"pivotFmts" => 2,
16857 b"view3D" => 3,
16858 b"floor" => 4,
16859 b"sideWall" => 5,
16860 b"backWall" => 6,
16861 b"showDLblsOverMax" | b"extLst" => 11,
16862 _ => current,
16863 },
16864 _ => current,
16865 }
16866 }
16867
16868 fn preserved_boundary_after(parent: &[u8], child: &[u8]) -> usize {
16869 match parent {
16870 b"chartSpace" => match child {
16871 b"date1904" => 1,
16872 b"lang" => 2,
16873 b"roundedCorners" => 3,
16874 b"style" => 4,
16875 b"clrMapOvr" => 5,
16876 b"pivotSource" => 6,
16877 b"protection" => 7,
16878 b"chart" => 8,
16879 b"spPr" => 9,
16880 b"txPr" => 10,
16881 b"externalData" => 11,
16882 b"printSettings" => 12,
16883 b"userShapes" => 13,
16884 b"extLst" => 14,
16885 _ => 0,
16886 },
16887 b"chart" => match child {
16888 b"title" => 1,
16889 b"autoTitleDeleted" => 2,
16890 b"pivotFmts" => 3,
16891 b"view3D" => 4,
16892 b"floor" => 5,
16893 b"sideWall" => 6,
16894 b"backWall" => 7,
16895 b"plotArea" => 8,
16896 b"legend" => 9,
16897 b"plotVisOnly" => 10,
16898 b"dispBlanksAs" => 11,
16899 b"showDLblsOverMax" | b"extLst" => 11,
16900 _ => 0,
16901 },
16902 b"pieChart" => match child {
16903 b"ser" => 1,
16904 b"dLbls" => 2,
16905 b"firstSliceAng" => 3,
16906 _ => 0,
16907 },
16908 b"doughnutChart" => match child {
16909 b"ser" => 1,
16910 b"dLbls" => 2,
16911 b"firstSliceAng" => 3,
16912 b"holeSize" => 4,
16913 _ => 0,
16914 },
16915 b"areaChart" => match child {
16916 b"grouping" => 1,
16917 b"ser" => 2,
16918 b"dLbls" => 3,
16919 b"axId" => 4,
16920 _ => 0,
16921 },
16922 b"scatterChart" | b"radarChart" => match child {
16923 b"scatterStyle" | b"radarStyle" => 1,
16924 b"ser" => 2,
16925 b"dLbls" => 3,
16926 b"axId" => 4,
16927 _ => 0,
16928 },
16929 _ => 0,
16930 }
16931 }
16932
16933 fn is_opaque_chartml_child(parent: &[u8], child: &[u8]) -> bool {
16934 match parent {
16935 b"chartSpace" => !matches!(child, b"chart" | b"spPr" | b"txPr"),
16936 b"chart" => !matches!(
16937 child,
16938 b"title"
16939 | b"autoTitleDeleted"
16940 | b"plotArea"
16941 | b"legend"
16942 | b"plotVisOnly"
16943 | b"dispBlanksAs"
16944 ),
16945 b"plotArea" => !matches!(
16946 child,
16947 b"barChart"
16948 | b"lineChart"
16949 | b"pieChart"
16950 | b"doughnutChart"
16951 | b"areaChart"
16952 | b"scatterChart"
16953 | b"radarChart"
16954 | b"catAx"
16955 | b"valAx"
16956 | b"dateAx"
16957 | b"serAx"
16958 ),
16959 b"pieChart" => !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng"),
16960 b"doughnutChart" => {
16961 !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng" | b"holeSize")
16962 }
16963 b"areaChart" => !matches!(child, b"grouping" | b"ser" | b"dLbls" | b"axId"),
16964 b"scatterChart" => !matches!(child, b"scatterStyle" | b"ser" | b"dLbls" | b"axId"),
16965 b"radarChart" => !matches!(child, b"radarStyle" | b"ser" | b"dLbls" | b"axId"),
16966 b"title" | b"legend" => true,
16967 _ => false,
16968 }
16969 }
16970
16971 fn is_chartml_preservation_parent(parent: &[u8]) -> bool {
16972 matches!(
16973 parent,
16974 b"chartSpace"
16975 | b"chart"
16976 | b"title"
16977 | b"plotArea"
16978 | b"pieChart"
16979 | b"doughnutChart"
16980 | b"areaChart"
16981 | b"scatterChart"
16982 | b"radarChart"
16983 | b"legend"
16984 | b"autoTitleDeleted"
16985 | b"plotVisOnly"
16986 | b"dispBlanksAs"
16987 )
16988 }
16989
16990 #[test]
16991 fn oxml_chart_is_an_explicit_publication_candidate() {
16992 let manifest = include_str!("../Cargo.toml");
16993 assert!(manifest.contains("name = \"oxml-chart\""));
16994 assert!(manifest.contains("version = \"0.14.2\""));
16995 assert!(manifest.contains("publish = true"));
16996 for dependency in [
16997 "oxml-core.workspace",
16998 "oxml-drawing.workspace",
16999 "quick-xml.workspace",
17000 ] {
17001 assert!(manifest.contains(dependency));
17002 }
17003 assert!(!manifest.contains("rpptx.workspace"));
17004 assert!(!manifest.contains("rdocx.workspace"));
17005 }
17006
17007 fn first_series_xml(xml: &[u8]) -> Option<Vec<u8>> {
17008 let mut reader = Reader::from_reader(xml);
17009 let mut buffer = Vec::new();
17010 loop {
17011 match reader.read_event_into(&mut buffer).ok()? {
17012 Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
17013 return capture_element(&mut reader, &element).ok();
17014 }
17015 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
17016 return capture_empty_element(&element).ok();
17017 }
17018 Event::Eof => return None,
17019 _ => {}
17020 }
17021 buffer.clear();
17022 }
17023 }
17024
17025 fn replace_once(haystack: &[u8], needle: &[u8], replacement: &[u8]) -> Vec<u8> {
17026 let offset = haystack
17027 .windows(needle.len())
17028 .position(|window| window == needle)
17029 .expect("serialized source series occurs in chart part");
17030 assert!(
17031 !haystack[offset + needle.len()..]
17032 .windows(needle.len())
17033 .any(|window| window == needle),
17034 "source series unexpectedly occurs more than once"
17035 );
17036 let mut result = Vec::with_capacity(haystack.len() - needle.len() + replacement.len());
17037 result.extend_from_slice(&haystack[..offset]);
17038 result.extend_from_slice(replacement);
17039 result.extend_from_slice(&haystack[offset + needle.len()..]);
17040 result
17041 }
17042
17043 fn assert_command_version(program: &str, arguments: &[&str], expected: &str) {
17044 let output = Command::new(program)
17045 .args(arguments)
17046 .output()
17047 .unwrap_or_else(|error| panic!("run {program} version check: {error}"));
17048 assert!(
17049 output.status.success(),
17050 "{program} version check failed: {}",
17051 String::from_utf8_lossy(&output.stderr)
17052 );
17053 let stdout = String::from_utf8(output.stdout).expect("version stdout is UTF-8");
17054 let stderr = String::from_utf8(output.stderr).expect("version stderr is UTF-8");
17055 let actual = if stdout.trim().is_empty() {
17056 stderr.trim()
17057 } else {
17058 stdout.trim()
17059 };
17060 assert_eq!(
17061 actual.lines().next(),
17062 Some(expected),
17063 "{program} version drift"
17064 );
17065 }
17066
17067 fn sha256(path: &Path) -> String {
17068 let output = Command::new("shasum")
17069 .args(["-a", "256"])
17070 .arg(path)
17071 .output()
17072 .unwrap_or_else(|error| panic!("{}: run shasum: {error}", path.display()));
17073 assert!(
17074 output.status.success(),
17075 "{}: shasum failed: {}",
17076 path.display(),
17077 String::from_utf8_lossy(&output.stderr)
17078 );
17079 String::from_utf8(output.stdout)
17080 .expect("shasum output is UTF-8")
17081 .split_whitespace()
17082 .next()
17083 .expect("shasum digest")
17084 .to_owned()
17085 }
17086
17087 fn workspace_root() -> PathBuf {
17088 Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
17089 }
17090
17091 fn corpus_dir() -> PathBuf {
17092 std::env::var_os("RDOCX_PPTX_CORPUS_DIR")
17093 .map(PathBuf::from)
17094 .unwrap_or_else(|| workspace_root().join("corpus/pptx"))
17095 }
17096
17097 fn require_or_skip_corpus() -> Option<PathBuf> {
17098 let corpus = corpus_dir();
17099 if corpus.is_dir() {
17100 return Some(corpus);
17101 }
17102 assert_ne!(
17103 std::env::var_os("RDOCX_PPTX_CORPUS_REQUIRED").as_deref(),
17104 Some(std::ffi::OsStr::new("1")),
17105 "the pinned corpus is required but {} does not exist",
17106 corpus.display()
17107 );
17108 eprintln!(
17109 "ChartML corpus gate skipped because {} is absent",
17110 corpus.display()
17111 );
17112 None
17113 }
17114
17115 fn verify_fetched_corpus(corpus: &Path) {
17116 let status = Command::new("python3")
17117 .arg(workspace_root().join("scripts/fetch_pptx_corpus.py"))
17118 .arg("--check")
17119 .env("RDOCX_PPTX_CORPUS_DIR", corpus)
17120 .status()
17121 .expect("run corpus verifier");
17122 assert!(status.success(), "pinned corpus verification failed");
17123 }
17124
17125 fn manifest_paths() -> Vec<&'static str> {
17126 let mut lines = MANIFEST.lines();
17127 assert_eq!(lines.next(), Some("path\tproducer\tsha256\turl"));
17128 let paths: Vec<_> = lines
17129 .map(|line| line.split('\t').next().expect("manifest path"))
17130 .collect();
17131 assert_eq!(paths.len(), EXPECTED_DECKS);
17132 paths
17133 }
17134
17135 fn is_chart_part(part: &str) -> bool {
17136 let Some(stem) = part
17137 .strip_prefix("/ppt/charts/chart")
17138 .and_then(|name| name.strip_suffix(".xml"))
17139 else {
17140 return false;
17141 };
17142 !stem.is_empty() && stem.bytes().all(|byte| byte.is_ascii_digit())
17143 }
17144
17145 fn plot_series(index: u32) -> String {
17146 format!(
17147 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>"#
17148 )
17149 }
17150
17151 fn sparse_category_series() -> &'static str {
17152 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>"#
17153 }
17154
17155 fn sparse_scatter_series() -> &'static str {
17156 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>"#
17157 }
17158
17159 fn chart_with_plot(plot: &str) -> String {
17160 format!(
17161 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>"#
17162 )
17163 }
17164
17165 fn chart_with_optional_axes(plot: &str, axes: bool) -> String {
17166 let axis_xml = if axes {
17167 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>"#
17168 } else {
17169 ""
17170 };
17171 format!(
17172 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>"#
17173 )
17174 }
17175
17176 fn numeric_plot_series(index: u32) -> String {
17177 format!(
17178 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>"#
17179 )
17180 }
17181
17182 fn scatter_without_x(index: u32) -> String {
17183 format!(
17184 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>"#
17185 )
17186 }
17187
17188 fn scatter_without_y(index: u32) -> String {
17189 format!(
17190 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>"#
17191 )
17192 }
17193
17194 fn remaining_plot_fixtures() -> Vec<(&'static str, String, bool)> {
17195 let series = plot_series(0);
17196 let scatter = numeric_plot_series(0);
17197 vec![
17198 (
17199 "pieChart",
17200 format!(
17201 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>"#
17202 ),
17203 false,
17204 ),
17205 (
17206 "doughnutChart",
17207 format!(
17208 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>"#
17209 ),
17210 false,
17211 ),
17212 (
17213 "areaChart",
17214 format!(
17215 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>"#
17216 ),
17217 true,
17218 ),
17219 (
17220 "scatterChart",
17221 format!(
17222 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>"#
17223 ),
17224 true,
17225 ),
17226 (
17227 "radarChart",
17228 format!(
17229 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>"#
17230 ),
17231 true,
17232 ),
17233 ]
17234 }
17235
17236 fn bar_plot(extra: &str) -> String {
17237 format!(
17238 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>"#,
17239 plot_series(0)
17240 )
17241 }
17242
17243 fn line_plot(extra: &str) -> String {
17244 format!(
17245 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>"#,
17246 plot_series(0)
17247 )
17248 }
17249
17250 #[test]
17251 fn bar_and_line_plots_round_trip_and_render() {
17252 for (xml, is_bar) in [
17253 (chart_with_plot(&bar_plot("")), true),
17254 (chart_with_plot(&line_plot("")), false),
17255 ] {
17256 let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
17257 let plots = parsed.chart.plot_area.plots().unwrap();
17258 assert_eq!(plots.len(), 1);
17259 assert_eq!(matches!(plots[0], Plot::Bar { .. }), is_bar);
17260 assert_eq!(matches!(plots[0], Plot::Line { .. }), !is_bar);
17261 assert_eq!(parsed.chart.plot_area.axes().unwrap().len(), 2);
17262 let written = parsed.to_xml().unwrap();
17263 let reparsed = CT_ChartSpace::from_xml(&written).unwrap();
17264 assert_eq!(parsed, reparsed);
17265 }
17266 if let Some(corpus) = require_or_skip_corpus() {
17267 verify_bar_and_line_viewer_gate(&corpus);
17268 }
17269 }
17270
17271 #[test]
17272 fn ppm_parser_preserves_whitespace_valued_first_pixels() {
17273 for first_pixel in *b" \n\r\t" {
17274 let mut ppm = b"P6\n1 1\n255\n".to_vec();
17275 ppm.extend_from_slice(&[first_pixel, 0x7f, 0xff]);
17276 let (width, height, pixels) = ppm_pixels(&ppm);
17277 assert_eq!((width, height), (1, 1));
17278 assert_eq!(pixels, [first_pixel, 0x7f, 0xff]);
17279 }
17280
17281 let ppm = b"P6\r\n1 1\r\n255\r\n\x7f\xff";
17282 assert_eq!(ppm_pixels(ppm).2, [b'\n', 0x7f, 0xff]);
17283 }
17284
17285 #[test]
17286 fn bar_and_line_plots_write_fixed_prefixes_in_schema_order() {
17287 for xml in [
17288 chart_with_plot(&bar_plot(r#"<q:extLst><x:bar-tail/></q:extLst>"#)),
17289 chart_with_plot(&line_plot(r#"<q:extLst><x:line-tail/></q:extLst>"#)),
17290 ] {
17291 let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
17292 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
17293 assert!(written.contains("<c:barChart") || written.contains("<c:lineChart"));
17294 assert!(!written.contains("<q:barChart"));
17295 assert!(!written.contains("<q:lineChart"));
17296 if written.contains("<c:barChart") {
17297 let positions: Vec<_> = [
17298 "<c:barDir",
17299 "<c:grouping",
17300 "<q:varyColors",
17301 "<c:ser",
17302 "<c:dLbls",
17303 "<c:gapWidth",
17304 "<c:overlap",
17305 "<q:serLines",
17306 "<c:axId",
17307 "<q:extLst",
17308 ]
17309 .iter()
17310 .map(|tag| written.find(tag).unwrap())
17311 .collect();
17312 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
17313 } else {
17314 let positions: Vec<_> = [
17315 "<c:grouping",
17316 "<q:varyColors",
17317 "<c:ser",
17318 "<c:dLbls",
17319 "<q:dropLines",
17320 "<c:marker",
17321 "<c:smooth",
17322 "<c:axId",
17323 "<q:extLst",
17324 ]
17325 .iter()
17326 .map(|tag| written.find(tag).unwrap())
17327 .collect();
17328 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
17329 }
17330 assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
17331 }
17332
17333 for plot in [bar_plot(""), line_plot("")] {
17334 let first = plot_series(0);
17335 let with_series_boundary = plot.replacen(
17336 &first,
17337 &format!(
17338 "{first}<!--between-series--><?series-boundary?>\n {}",
17339 plot_series(1)
17340 ),
17341 1,
17342 );
17343 let with_axis_boundary = with_series_boundary.replacen(
17344 r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
17345 r#"<q:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
17346 <q:axId val="-1884097184"/>"#,
17347 1,
17348 );
17349 let parsed =
17350 CT_ChartSpace::from_xml(chart_with_plot(&with_axis_boundary).as_bytes()).unwrap();
17351 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
17352 assert!(written.contains("</c:ser><!--between-series--><?series-boundary?>\n <c:ser"));
17353 assert!(written.contains(
17354 r#"<c:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
17355 <c:axId val="-1884097184"/>"#
17356 ));
17357 assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
17358 }
17359 }
17360
17361 #[test]
17362 fn malformed_bar_and_line_plots_return_errors_without_panicking() {
17363 let valid_series = plot_series(0);
17364 let cases = [
17365 format!(r#"<q:barChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
17366 format!(r#"<q:barChart><q:barDir val="diagonal"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
17367 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>"#),
17368 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>"#),
17369 format!(r#"<q:lineChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
17370 format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:marker val="maybe"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
17371 r#"<q:lineChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#.to_owned(),
17372 format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:axId val="1"/><q:axId val="1"/></q:lineChart>"#),
17373 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>"#),
17374 format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
17375 ];
17376 for plot in cases {
17377 let xml = chart_with_plot(&plot);
17378 let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
17379 assert!(result.is_ok(), "plot parser panicked for {plot}");
17380 assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
17381 }
17382
17383 let missing_axis = format!(
17384 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>"#,
17385 bar_plot("")
17386 );
17387 assert!(CT_ChartSpace::from_xml(missing_axis.as_bytes()).is_err());
17388
17389 let values =
17390 NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
17391 let series = vec![Series::new(0, 0, values)];
17392 let ids = [AxisId::new(1).unwrap(), AxisId::new(2).unwrap()];
17393 let mut invalid =
17394 Plot::bar(BarDirection::Column, BarGrouping::Clustered, series, ids).unwrap();
17395 if let Plot::Bar { gap_width, .. } = &mut invalid {
17396 *gap_width = 501;
17397 }
17398 assert!(invalid.validate().is_err());
17399 }
17400
17401 #[test]
17402 fn unsupported_and_combo_plots_remain_byte_preserved() {
17403 let bar = bar_plot("");
17404 let line = line_plot("");
17405 let combo = chart_with_plot(&format!("<!--before-->{bar}<?between?>{line}<!--after-->"));
17406 let parsed = CT_ChartSpace::from_xml(combo.as_bytes()).unwrap();
17407 assert!(parsed.chart.plot_area.plots().is_err());
17408 let written = parsed.to_xml().unwrap();
17409 for raw in [
17410 bar.as_bytes(),
17411 line.as_bytes(),
17412 br#"<!--before-->"#.as_slice(),
17413 br#"<?between?>"#.as_slice(),
17414 br#"<!--after-->"#.as_slice(),
17415 ] {
17416 assert!(written.windows(raw.len()).any(|window| window == raw));
17417 }
17418
17419 let unsupported = chart_with_plot(
17420 r#"<q:bar3DChart x:keep="three"><q:barDir val="col"/><q:ser><x:opaque/></q:ser></q:bar3DChart>"#,
17421 );
17422 let parsed = CT_ChartSpace::from_xml(unsupported.as_bytes()).unwrap();
17423 assert!(parsed.chart.plot_area.plots().is_err());
17424 assert!(
17425 String::from_utf8(parsed.to_xml().unwrap())
17426 .unwrap()
17427 .contains(r#"<q:bar3DChart x:keep="three">"#)
17428 );
17429 }
17430
17431 #[test]
17432 fn public_plot_edits_preserve_axes_and_unselected_payloads() {
17433 let xml = chart_with_plot(&bar_plot(
17434 r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#,
17435 ));
17436 let mut parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
17437 let axes_before = parsed.chart.plot_area.axes().unwrap();
17438 match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
17439 Plot::Bar {
17440 gap_width,
17441 overlap,
17442 series,
17443 ..
17444 } => {
17445 *gap_width = 225;
17446 *overlap = -25;
17447 series[0].index = 7;
17448 series[0].values.values[0] = 9.0;
17449 }
17450 _ => panic!("expected bar plot"),
17451 }
17452 let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
17453 assert!(written.contains(r#"<c:gapWidth val="225"/>"#));
17454 assert!(written.contains(r#"<c:overlap val="-25"/>"#));
17455 assert!(written.contains("<c:v>9</c:v>"));
17456 assert!(written.contains("<q:varyColors val=\"1\"/>"));
17457 assert!(written.find("<q:varyColors").unwrap() < written.find("<c:ser").unwrap());
17458 assert!(written.contains("<q:serLines><x:line/></q:serLines>"));
17459 assert!(written.contains(r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#));
17460 let reparsed = CT_ChartSpace::from_xml(written.as_bytes()).unwrap();
17461 assert_eq!(axes_before, reparsed.chart.plot_area.axes().unwrap());
17462
17463 let first = plot_series(0);
17464 let colliding_series =
17465 bar_plot("").replacen(&first, &format!("{first}{}", plot_series(1)), 1);
17466 let mut collision =
17467 CT_ChartSpace::from_xml(chart_with_plot(&colliding_series).as_bytes()).unwrap();
17468 if let Plot::Bar { series, .. } = &mut collision.chart.plot_area.plots_mut().unwrap()[0] {
17469 series[0].index = series[1].index;
17470 series[0].order = series[1].order;
17471 }
17472 let collision = String::from_utf8(collision.to_xml().unwrap()).unwrap();
17473 assert!(collision.find("<q:varyColors").unwrap() < collision.find("<c:ser").unwrap());
17474
17475 let mut inserted = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
17476 if let Plot::Bar { series, .. } = &mut inserted.chart.plot_area.plots_mut().unwrap()[0] {
17477 let mut new_series = series[0].clone();
17478 new_series.index = 99;
17479 new_series.order = 99;
17480 series.insert(0, new_series);
17481 }
17482 let inserted = String::from_utf8(inserted.to_xml().unwrap()).unwrap();
17483 assert!(inserted.find("<q:varyColors").unwrap() < inserted.find("<c:ser").unwrap());
17484
17485 let axis_payload = bar_plot("").replacen(
17486 r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
17487 r#"<q:axId val="-1884094432" x:slot="category"/><!--axis-anchor--><q:axId val="-1884097184" x:slot="value"/>"#,
17488 1,
17489 );
17490 let mut swapped =
17491 CT_ChartSpace::from_xml(chart_with_plot(&axis_payload).as_bytes()).unwrap();
17492 if let Plot::Bar { axis_ids, .. } = &mut swapped.chart.plot_area.plots_mut().unwrap()[0] {
17493 axis_ids.swap(0, 1);
17494 }
17495 let swapped = String::from_utf8(swapped.to_xml().unwrap()).unwrap();
17496 let anchor = swapped.find("<!--axis-anchor-->").unwrap();
17497 let value_axis = swapped
17498 .find(r#"<c:axId val="-1884097184" x:slot="value"/>"#)
17499 .unwrap();
17500 let category_axis = swapped
17501 .find(r#"<c:axId val="-1884094432" x:slot="category"/>"#)
17502 .unwrap();
17503 let series_lines = swapped.find("<q:serLines").unwrap();
17504 assert!(series_lines < value_axis && series_lines < category_axis);
17505 assert!(anchor < value_axis && value_axis < category_axis);
17506
17507 let mut replaced = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
17508 let Plot::Bar {
17509 series, axis_ids, ..
17510 } = &replaced.chart.plot_area.plots().unwrap()[0]
17511 else {
17512 panic!("expected bar plot");
17513 };
17514 let line = Plot::line(Grouping::Standard, series.clone(), *axis_ids).unwrap();
17515 replaced.chart.plot_area.plots_mut().unwrap()[0] = line;
17516 assert!(replaced.to_xml().is_err());
17517 }
17518
17519 #[test]
17520 fn every_corpus_bar_and_line_plot_round_trips_structurally() {
17521 let Some(corpus) = require_or_skip_corpus() else {
17522 return;
17523 };
17524 verify_fetched_corpus(&corpus);
17525 let mut typed_bar_count = 0usize;
17526 let mut typed_line_count = 0usize;
17527 let mut opaque_bar_count = 0usize;
17528 let mut opaque_line_count = 0usize;
17529 let mut opaque_combo_count = 0usize;
17530 for path in manifest_paths() {
17531 let package = OpcPackage::open(corpus.join(path))
17532 .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
17533 for (part, xml) in &package.parts {
17534 if !is_chart_part(part) {
17535 continue;
17536 }
17537 let chart = CT_ChartSpace::from_xml(xml)
17538 .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
17539 match chart.chart.plot_area.plots() {
17540 Ok(plots) => {
17541 for plot in plots {
17542 match plot {
17543 Plot::Bar { .. } => typed_bar_count += 1,
17544 Plot::Line { .. } => typed_line_count += 1,
17545 _ => {}
17546 }
17547 }
17548 }
17549 Err(_) => {
17550 let mut preserved_plot_count = 0usize;
17551 for raw in chart.chart.plot_area.raw_children.at(0) {
17552 if let Ok(Some(local)) = super::chart_root_local(
17553 raw,
17554 &chart.chart.plot_area.namespace_bindings,
17555 ) {
17556 match local.as_slice() {
17557 b"barChart" => {
17558 opaque_bar_count += 1;
17559 preserved_plot_count += 1;
17560 }
17561 b"lineChart" => {
17562 opaque_line_count += 1;
17563 preserved_plot_count += 1;
17564 }
17565 _ => {}
17566 }
17567 }
17568 }
17569 if preserved_plot_count > 1 {
17570 opaque_combo_count += 1;
17571 }
17572 }
17573 }
17574 let written = chart
17575 .to_xml()
17576 .unwrap_or_else(|error| panic!("{path} {part}: chart write failed: {error}"));
17577 let reparsed = CT_ChartSpace::from_xml(&written).unwrap_or_else(|error| {
17578 panic!("{path} {part}: written chart parse failed: {error}")
17579 });
17580 assert_eq!(chart, reparsed, "{path} {part}: chart model changed");
17581 }
17582 }
17583 assert_eq!(typed_bar_count, 11, "typed corpus bar coverage changed");
17584 assert_eq!(typed_line_count, 2, "typed corpus line coverage changed");
17585 assert_eq!(opaque_bar_count, 1, "preserved combo bar coverage changed");
17586 assert_eq!(
17587 opaque_line_count, 1,
17588 "preserved combo line coverage changed"
17589 );
17590 assert_eq!(opaque_combo_count, 1, "preserved combo coverage changed");
17591 assert_eq!(typed_bar_count + opaque_bar_count, 12);
17592 assert_eq!(typed_line_count + opaque_line_count, 3);
17593 eprintln!(
17594 "ChartML plot corpus gate checked {typed_bar_count} typed bar, {typed_line_count} typed line, and one preserved bar-line combination"
17595 );
17596 }
17597
17598 fn verify_remaining_plot_viewer_gate(corpus: &Path) {
17599 verify_fetched_corpus(corpus);
17600 assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
17601 assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
17602 let source = corpus.join("bar-chart.pptx");
17603 let source_sha = sha256(&source);
17604 let temp_root = std::env::temp_dir().join(format!(
17605 "oxml-chart-f122-viewer-gate-{}",
17606 std::process::id()
17607 ));
17608 if temp_root.exists() {
17609 fs::remove_dir_all(&temp_root).expect("remove stale F-122 evidence");
17610 }
17611 fs::create_dir_all(&temp_root).expect("create F-122 evidence root");
17612
17613 for (kind, plot, axes) in remaining_plot_fixtures() {
17614 let mut package = OpcPackage::open(&source).expect("open F-122 viewer source");
17615 package.set_part(
17616 "/ppt/charts/chart1.xml",
17617 CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes())
17618 .expect("parse F-122 viewer chart")
17619 .to_xml()
17620 .expect("serialize F-122 viewer chart"),
17621 );
17622 let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
17623 package.save(&unbound).expect("save F-122 viewer candidate");
17624 let candidate_sha = sha256(&unbound);
17625 let evidence = temp_root.join(format!("{source_sha}-{candidate_sha}"));
17626 fs::create_dir(&evidence).expect("create SHA-bound F-122 evidence directory");
17627 let candidate = evidence.join(format!("{kind}-candidate.pptx"));
17628 fs::rename(&unbound, &candidate).expect("bind F-122 candidate to SHA");
17629 assert_eq!(sha256(&candidate), candidate_sha);
17630 let render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
17631 let bytes = fs::read(&render).expect("read F-122 viewer PPM");
17632 let (width, height, pixels) = ppm_pixels(&bytes);
17633 let left = width / 5;
17634 let right = width * 4 / 5;
17635 let top = height * 3 / 20;
17636 let bottom = height * 17 / 20;
17637 let mut nonblank = 0usize;
17638 for y in top..bottom {
17639 for x in left..right {
17640 let offset = (y * width + x) * 3;
17641 if pixels[offset..offset + 3]
17642 .iter()
17643 .any(|channel| *channel < 245)
17644 {
17645 nonblank += 1;
17646 }
17647 }
17648 }
17649 const CHART_RECTANGLE_NONBLANK_THRESHOLD: usize = 1_000;
17650 assert!(
17651 nonblank >= CHART_RECTANGLE_NONBLANK_THRESHOLD,
17652 "{kind}: chart rectangle [{left},{top}) to [{right},{bottom}) contains {nonblank} nonblank pixels, below {CHART_RECTANGLE_NONBLANK_THRESHOLD}"
17653 );
17654 eprintln!(
17655 "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}",
17656 sha256(&render)
17657 );
17658 }
17659 fs::remove_dir_all(&temp_root).expect("remove F-122 viewer evidence");
17660 }
17661
17662 fn verify_bar_and_line_viewer_gate(corpus: &Path) {
17663 verify_fetched_corpus(corpus);
17664 assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
17665 assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
17666 let temp_root = std::env::temp_dir().join(format!(
17667 "oxml-chart-f121-viewer-gate-{}",
17668 std::process::id()
17669 ));
17670 if temp_root.exists() {
17671 fs::remove_dir_all(&temp_root).expect("remove stale F-121 evidence");
17672 }
17673 fs::create_dir_all(&temp_root).expect("create F-121 evidence root");
17674
17675 for (kind, deck) in [("bar", "bar-chart.pptx"), ("line", "line-chart.pptx")] {
17676 let source = corpus.join(deck);
17677 let original_sha = sha256(&source);
17678 let mut package = OpcPackage::open(&source)
17679 .unwrap_or_else(|error| panic!("{deck}: open viewer source: {error}"));
17680 let chart_part = "/ppt/charts/chart1.xml";
17681 let original_parts = package.parts.clone();
17682 let parsed = CT_ChartSpace::from_xml(
17683 package
17684 .get_part(chart_part)
17685 .unwrap_or_else(|| panic!("{deck}: missing {chart_part}")),
17686 )
17687 .unwrap_or_else(|error| panic!("{deck}: parse viewer chart: {error}"));
17688 assert!(
17689 parsed.chart.plot_area.plots().is_ok(),
17690 "{deck}: plot is opaque"
17691 );
17692 package.set_part(
17693 chart_part,
17694 parsed
17695 .to_xml()
17696 .unwrap_or_else(|error| panic!("{deck}: serialize viewer chart: {error}")),
17697 );
17698 for (part, bytes) in &original_parts {
17699 if part == chart_part {
17700 assert_ne!(package.parts[part], *bytes, "{deck}: chart did not rewrite");
17701 } else {
17702 assert_eq!(package.parts[part], *bytes, "{deck}: changed part {part}");
17703 }
17704 }
17705
17706 let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
17707 package
17708 .save(&unbound)
17709 .unwrap_or_else(|error| panic!("{deck}: save viewer candidate: {error}"));
17710 let candidate_sha = sha256(&unbound);
17711 let evidence = temp_root.join(format!("{original_sha}-{candidate_sha}"));
17712 fs::create_dir(&evidence).expect("create SHA-bound F-121 evidence directory");
17713 let original = evidence.join(format!("{kind}-original.pptx"));
17714 let candidate = evidence.join(format!("{kind}-candidate.pptx"));
17715 fs::copy(&source, &original).expect("copy SHA-bound viewer original");
17716 fs::rename(&unbound, &candidate).expect("bind viewer candidate to SHA");
17717 assert_eq!(sha256(&original), original_sha);
17718 assert_eq!(sha256(&candidate), candidate_sha);
17719
17720 let original_render = render_deck_to_ppm(&original, &evidence, kind, "original");
17721 let candidate_render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
17722 let original_bytes = fs::read(&original_render).expect("read original PPM");
17723 let candidate_bytes = fs::read(&candidate_render).expect("read candidate PPM");
17724 let normalized_mae = normalized_ppm_mae(&original_bytes, &candidate_bytes);
17725 assert!(
17726 normalized_mae <= PLOT_RENDER_NORMALIZED_MAE_THRESHOLD,
17727 "{deck}: normalized RGB MAE {normalized_mae:.8} exceeds {:.8}",
17728 PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
17729 );
17730 eprintln!(
17731 "F-121 {kind} viewer gate original deck {original_sha}, candidate deck {candidate_sha}, original render {}, candidate render {}, normalized RGB MAE {normalized_mae:.8} <= {:.8}",
17732 sha256(&original_render),
17733 sha256(&candidate_render),
17734 PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
17735 );
17736 }
17737 fs::remove_dir_all(&temp_root).expect("remove F-121 viewer evidence");
17738 }
17739
17740 fn render_deck_to_ppm(deck: &Path, evidence: &Path, kind: &str, side: &str) -> PathBuf {
17741 let profile = std::env::temp_dir().join(format!(
17742 "oxml-chart-f121-{kind}-{side}-profile-{}",
17743 std::process::id()
17744 ));
17745 if profile.exists() {
17746 fs::remove_dir_all(&profile).expect("remove stale F-121 viewer profile");
17747 }
17748 let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
17749 let conversion = Command::new("soffice")
17750 .args([
17751 "--headless",
17752 &profile_argument,
17753 "--convert-to",
17754 "pdf:impress_pdf_Export",
17755 "--outdir",
17756 ])
17757 .arg(evidence)
17758 .arg(deck)
17759 .output()
17760 .expect("run pinned LibreOffice F-121 viewer gate");
17761 assert!(
17762 conversion.status.success(),
17763 "{}: LibreOffice conversion failed: {}",
17764 deck.display(),
17765 String::from_utf8_lossy(&conversion.stderr)
17766 );
17767 let pdf = deck.with_extension("pdf");
17768 assert!(
17769 pdf.is_file(),
17770 "LibreOffice did not create {}",
17771 pdf.display()
17772 );
17773 let prefix = evidence.join(format!("{kind}-{side}"));
17774 let raster = Command::new("pdftoppm")
17775 .args(["-f", "1", "-singlefile", "-r", "150"])
17776 .arg(&pdf)
17777 .arg(&prefix)
17778 .output()
17779 .expect("run pinned Poppler F-121 raster gate");
17780 assert!(
17781 raster.status.success(),
17782 "{}: pdftoppm failed: {}",
17783 pdf.display(),
17784 String::from_utf8_lossy(&raster.stderr)
17785 );
17786 if profile.exists() {
17787 fs::remove_dir_all(&profile).expect("remove F-121 viewer profile");
17788 }
17789 prefix.with_extension("ppm")
17790 }
17791
17792 fn normalized_ppm_mae(left: &[u8], right: &[u8]) -> f64 {
17793 let (left_width, left_height, left_pixels) = ppm_pixels(left);
17794 let (right_width, right_height, right_pixels) = ppm_pixels(right);
17795 assert_eq!((left_width, left_height), (right_width, right_height));
17796 assert_eq!(left_pixels.len(), right_pixels.len());
17797 let difference: u64 = left_pixels
17798 .iter()
17799 .zip(right_pixels)
17800 .map(|(left, right)| u64::from(left.abs_diff(*right)))
17801 .sum();
17802 difference as f64 / (left_pixels.len() as f64 * 255.0)
17803 }
17804
17805 fn ppm_pixels(bytes: &[u8]) -> (usize, usize, &[u8]) {
17806 let mut cursor = 0usize;
17807 let mut tokens = Vec::new();
17808 while tokens.len() < 4 {
17809 while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
17810 cursor += 1;
17811 }
17812 if cursor < bytes.len() && bytes[cursor] == b'#' {
17813 while cursor < bytes.len() && bytes[cursor] != b'\n' {
17814 cursor += 1;
17815 }
17816 continue;
17817 }
17818 let start = cursor;
17819 while cursor < bytes.len() && !bytes[cursor].is_ascii_whitespace() {
17820 cursor += 1;
17821 }
17822 tokens.push(std::str::from_utf8(&bytes[start..cursor]).expect("PPM header UTF-8"));
17823 }
17824 assert_eq!(tokens[0], "P6");
17825 let width = tokens[1].parse::<usize>().expect("PPM width");
17826 let height = tokens[2].parse::<usize>().expect("PPM height");
17827 assert_eq!(tokens[3], "255");
17828 assert!(
17829 cursor < bytes.len() && bytes[cursor].is_ascii_whitespace(),
17830 "PPM header must end with whitespace"
17831 );
17832 cursor += 1;
17833 let pixels = &bytes[cursor..];
17834 assert_eq!(pixels.len(), width * height * 3, "PPM pixel length");
17835 (width, height, pixels)
17836 }
17837 #[test]
17838 fn fx253_invalid_selected_metadata_is_distinct_from_valid_unsupported_styles() {
17839 for (plot, scalar, valid, expected) in [
17840 ("scatterChart", "scatterStyle", "smoothMarker", None),
17841 ("radarChart", "radarStyle", "filled", None),
17842 ("lineChart", "grouping", "standard", Some(65)),
17843 ] {
17844 let shell = format!(
17845 r#"<c:chartSpace xmlns:c="{C_NS}"><c:chart><c:plotArea><c:{plot}><c:{scalar} val="{valid}"/></c:{plot}></c:plotArea></c:chart></c:chartSpace>"#
17846 );
17847 assert_eq!(super::read_chart_type(shell.as_bytes()).unwrap(), expected);
17848 let invalid = shell.replace(&format!("val=\"{valid}\""), "val=\"garbage\"");
17849 assert!(super::read_chart_type(invalid.as_bytes()).is_err());
17850 }
17851 for symbol in ["none", "circle", "auto", "garbage"] {
17852 let shell = format!(
17853 r#"<c:chartSpace xmlns:c="{C_NS}"><c:chart><c:plotArea><c:lineChart><c:ser><c:marker><c:symbol val="{symbol}"/></c:marker></c:ser></c:lineChart></c:plotArea></c:chart></c:chartSpace>"#
17854 );
17855 let actual = super::read_chart_type(shell.as_bytes());
17856 if symbol == "garbage" {
17857 assert!(actual.is_err());
17858 } else {
17859 assert_eq!(actual.unwrap(), Some(if symbol == "none" { 4 } else { 65 }));
17860 }
17861 }
17862 }
17863
17864 #[test]
17865 fn fx253_many_sparse_series_fit_the_linear_metadata_work_budget() {
17866 let series = "<c:ser><c:idx val=\"0\"/><c:order val=\"0\"/><c:marker><c:symbol val=\"none\"/></c:marker></c:ser>".repeat(10_000);
17867 let xml = format!(
17868 r#"<c:chartSpace xmlns:c="{C_NS}"><c:chart><c:plotArea><c:lineChart><c:grouping val="standard"/>{series}</c:lineChart></c:plotArea></c:chart></c:chartSpace>"#
17869 );
17870 assert_eq!(super::read_chart_type(xml.as_bytes()).unwrap(), Some(4));
17873 let nodes = super::chart_source_nodes(xml.as_bytes()).unwrap();
17874 let mut children = vec![Vec::new(); nodes.len()];
17875 for (index, node) in nodes.iter().enumerate() {
17876 if let Some(parent) = node.parent {
17877 children[parent].push(index);
17878 }
17879 }
17880 assert!(super::chart_optional_child_node(&nodes, &children, 0, b"chart", &mut 0).is_err());
17881 }
17882
17883 #[test]
17884 fn fx253_imported_chart_types_use_direct_qualified_metadata() {
17885 let data = ChartData {
17886 categories: vec!["1".into(), "2".into()],
17887 series: vec![("Series".into(), vec![1.0, 2.0])],
17888 ..ChartData::default()
17889 };
17890 for (kind, expected) in [
17891 (ChartKind::Bar, 51),
17892 (ChartKind::Line, 65),
17893 (ChartKind::Pie, 5),
17894 (ChartKind::Doughnut, -4120),
17895 (ChartKind::Area, 1),
17896 (ChartKind::Scatter, 74),
17897 (ChartKind::Radar, 81),
17898 ] {
17899 let (xml, _) = authored_chart_parts(kind, &data, "rIdWorkbook").unwrap();
17900 let source = String::from_utf8(xml).unwrap();
17901 assert_eq!(
17902 super::read_chart_type(source.as_bytes()).unwrap(),
17903 Some(expected)
17904 );
17905 let changed = match kind {
17906 ChartKind::Bar => source
17907 .replace("val=\"col\"", "val=\"bar\"")
17908 .replace("val=\"clustered\"", "val=\"stacked\""),
17909 ChartKind::Line => source.replace(
17910 "</c:ser>",
17911 "<c:marker><c:symbol val=\"none\"/></c:marker></c:ser>",
17912 ),
17913 ChartKind::Pie => source.replace("</c:ser>", "<c:explosion val=\"0\"/></c:ser>"),
17914 _ => continue,
17915 };
17916 let expected = match kind {
17917 ChartKind::Bar => 58,
17918 ChartKind::Line => 4,
17919 _ => 69,
17920 };
17921 assert_eq!(
17922 super::read_chart_type(changed.as_bytes()).unwrap(),
17923 Some(expected)
17924 );
17925 if kind == ChartKind::Bar {
17926 let column = changed.replace("val=\"bar\"", "val=\"col\"");
17927 assert_eq!(super::read_chart_type(column.as_bytes()).unwrap(), Some(52));
17928 }
17929 let alias = changed.replace("xmlns:c=", "xmlns:q=").replace("c:", "q:");
17930 assert_eq!(
17931 super::read_chart_type(alias.as_bytes()).unwrap(),
17932 Some(expected)
17933 );
17934 let foreign = changed
17935 .replace("<c:marker>", "<c:marker xmlns:c=\"urn:foreign\">")
17936 .replace("<c:explosion ", "<c:explosion xmlns:c=\"urn:foreign\" ");
17937 if kind != ChartKind::Bar {
17938 assert_eq!(
17939 super::read_chart_type(foreign.as_bytes()).unwrap(),
17940 Some(if kind == ChartKind::Line { 65 } else { 5 })
17941 );
17942 let nested = source.replace("</c:ser>", "<c:extLst><c:ext uri=\"control\"><c:marker><c:symbol val=\"none\"/></c:marker><c:explosion val=\"0\"/></c:ext></c:extLst></c:ser>");
17943 assert_eq!(
17944 super::read_chart_type(nested.as_bytes()).unwrap(),
17945 Some(if kind == ChartKind::Line { 65 } else { 5 })
17946 );
17947 }
17948 }
17949 let shell = format!(
17950 "<c:chartSpace xmlns:c=\"{C_NS}\"><c:chart><c:plotArea><c:bubbleChart/></c:plotArea></c:chart></c:chartSpace>"
17951 );
17952 assert_eq!(super::read_chart_type(shell.as_bytes()).unwrap(), None);
17953 assert!(super::read_chart_type(b"<c:chartSpace").is_err());
17954 let mixed = shell.replace("<c:bubbleChart/>", "<c:pieChart/><c:barChart/>");
17955 assert_eq!(super::read_chart_type(mixed.as_bytes()).unwrap(), None);
17956 let foreign = shell
17957 .replace("xmlns:c=", "xmlns:foreign=")
17958 .replace("c:", "foreign:")
17959 .replace(C_NS, "urn:foreign");
17960 assert!(super::read_chart_type(foreign.as_bytes()).is_err());
17961 let invalid = shell.replace(
17962 "<c:bubbleChart/>",
17963 "<c:barChart><c:barDir val=\"invalid\"/></c:barChart>",
17964 );
17965 assert!(super::read_chart_type(invalid.as_bytes()).is_err());
17966 let duplicate = invalid
17967 .replace("val=\"invalid\"", "val=\"col\"")
17968 .replace("</c:barChart>", "<c:barDir val=\"bar\"/></c:barChart>");
17969 assert!(super::read_chart_type(duplicate.as_bytes()).is_err());
17970 }
17971
17972 #[test]
17973 fn fx215_existing_chart_data_projection() {
17974 let data = ChartData {
17975 categories: vec!["1".into(), "2".into()],
17976 series: vec![("Revenue & margin".into(), vec![1.25, 2.5])],
17977 number_format: Some("0.00".into()),
17978 ..ChartData::default()
17979 };
17980 for kind in [
17981 ChartKind::Bar,
17982 ChartKind::Line,
17983 ChartKind::Pie,
17984 ChartKind::Doughnut,
17985 ChartKind::Area,
17986 ChartKind::Scatter,
17987 ChartKind::Radar,
17988 ] {
17989 let (xml, _) = authored_chart_parts(kind, &data, "rIdWorkbook").unwrap();
17990 let original = xml.clone();
17991 let (actual_kind, actual) = read_chart_data(&xml).unwrap();
17992 assert_eq!(actual_kind, kind);
17993 assert_eq!(actual.categories, data.categories);
17994 assert_eq!(actual.series, data.series);
17995 assert_eq!(actual.number_format, data.number_format);
17996 assert_eq!(xml, original);
17997 assert_eq!(
17998 chart_workbook_relationship_id(&xml).unwrap().as_deref(),
17999 Some("rIdWorkbook")
18000 );
18001 }
18002 }
18003
18004 #[test]
18005 fn fx215_workbook_relationship_ignores_foreign_id_and_accepts_alias() {
18006 let data = ChartData {
18007 categories: vec!["One".into()],
18008 series: vec![("Values".into(), vec![2.0])],
18009 ..ChartData::default()
18010 };
18011 let (xml, _) = authored_chart_parts(ChartKind::Bar, &data, "rIdWorkbook").unwrap();
18012 let xml = String::from_utf8(xml)
18013 .unwrap()
18014 .replace("r:id=", "rr:id=")
18015 .replace(
18016 "<c:externalData",
18017 &format!(
18018 "<c:externalData xmlns:rr=\"{R_NS}\" xmlns:f=\"urn:foreign\" f:id=\"wrong\""
18019 ),
18020 );
18021 assert_eq!(
18022 chart_workbook_relationship_id(xml.as_bytes())
18023 .unwrap()
18024 .as_deref(),
18025 Some("rIdWorkbook")
18026 );
18027 let foreign = xml.replace(&format!("xmlns:rr=\"{R_NS}\""), "xmlns:rr=\"urn:foreign\"");
18028 assert!(chart_workbook_relationship_id(foreign.as_bytes()).is_err());
18029 }
18030
18031 #[test]
18032 fn fx215_replace_data_updates_cache_formula_and_workbook() {
18033 let original = ChartData {
18034 categories: vec!["North".into(), "South".into()],
18035 series: vec![
18036 ("Revenue".into(), vec![1.0, 2.0]),
18037 ("Cost".into(), vec![0.5, 1.0]),
18038 ],
18039 ..ChartData::default()
18040 };
18041 for kind in [ChartKind::Bar, ChartKind::Line] {
18042 let (xml, workbook) = authored_chart_parts(kind, &original, "rIdWorkbook").unwrap();
18043 for series in [
18044 vec![("New revenue".into(), vec![8.0, 9.0, 10.0])],
18045 vec![
18046 ("New revenue".into(), vec![8.0, 9.0, 10.0]),
18047 ("New cost".into(), vec![1.0, 2.0, 3.0]),
18048 ("Profit".into(), vec![7.0, 7.0, 7.0]),
18049 ],
18050 ] {
18051 let data = ChartData {
18052 categories: vec!["East".into(), "West".into(), "Central".into()],
18053 series,
18054 ..ChartData::default()
18055 };
18056 let (chart, book) = replace_chart_data(&xml, &workbook, &data).unwrap();
18057 assert_eq!(read_chart_data(&chart).unwrap().1.series, data.series);
18058 assert_eq!(
18059 read_chart_data(&chart).unwrap().1.categories,
18060 data.categories
18061 );
18062 let package = OpcPackage::from_reader(std::io::Cursor::new(book)).unwrap();
18063 let sheet = String::from_utf8(
18064 package
18065 .get_part("/xl/worksheets/sheet1.xml")
18066 .unwrap()
18067 .to_vec(),
18068 )
18069 .unwrap();
18070 assert!(sheet.contains("<v>8</v>"));
18071 assert!(sheet.contains("<v>10</v>"));
18072 assert!(sheet.contains("East"));
18073 }
18074 }
18075 }
18076
18077 #[test]
18078 fn fx215_surviving_series_and_workbook_opaque_payload() {
18079 let original = ChartData {
18080 categories: vec!["North".into(), "South".into()],
18081 series: vec![("Revenue".into(), vec![1.0, 2.0])],
18082 ..ChartData::default()
18083 };
18084 let (xml, workbook) =
18085 authored_chart_parts(ChartKind::Bar, &original, "rIdWorkbook").unwrap();
18086 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>");
18087 assert!(source.contains("f:keep='series'"));
18088 assert!(source.contains("<f:opaque data='exact'/>"));
18089 let data = ChartData {
18090 categories: vec!["West".into()],
18091 series: vec![("Changed".into(), vec![9.0])],
18092 ..ChartData::default()
18093 };
18094 let (changed, _) = replace_chart_data(source.as_bytes(), &workbook, &data).unwrap();
18095 let changed = String::from_utf8(changed).unwrap();
18096 for exact in [
18097 "f:keep='root'",
18098 "f:keep='series'",
18099 "<f:opaque data='exact'/>",
18100 "<c:extLst><c:ext uri='producer'><f:payload value='retain'/></c:ext></c:extLst>",
18101 ] {
18102 assert!(changed.contains(exact), "{exact}");
18103 }
18104 let space = CT_ChartSpace::from_xml(changed.as_bytes()).unwrap();
18105 assert_eq!(space.chart.plot_area.series().unwrap()[0].index, 0);
18106 let no_op = replace_chart_data(source.as_bytes(), &workbook, &original).unwrap();
18107 assert_eq!(no_op.0, source.as_bytes());
18108 assert_eq!(no_op.1, workbook);
18109 }
18110
18111 #[test]
18112 fn fx215_all_seven_families_replace_data_without_changing_kind() {
18113 let original = ChartData {
18114 categories: vec!["1".into(), "2".into()],
18115 series: vec![("Old".into(), vec![1.0, 2.0])],
18116 ..ChartData::default()
18117 };
18118 let data = ChartData {
18119 categories: vec!["3".into(), "4".into(), "5".into()],
18120 series: vec![("New".into(), vec![0.25, 0.5, 0.75])],
18121 number_format: Some("0.0%".into()),
18122 ..ChartData::default()
18123 };
18124 for kind in [
18125 ChartKind::Bar,
18126 ChartKind::Line,
18127 ChartKind::Pie,
18128 ChartKind::Doughnut,
18129 ChartKind::Area,
18130 ChartKind::Scatter,
18131 ChartKind::Radar,
18132 ] {
18133 let (chart, book) = authored_chart_parts(kind, &original, "rIdWorkbook").unwrap();
18134 let (updated, workbook) = replace_chart_data(&chart, &book, &data).unwrap();
18135 let (actual, projected) = read_chart_data(&updated).unwrap();
18136 assert_eq!(actual, kind);
18137 assert_eq!(projected.categories, data.categories);
18138 assert_eq!(projected.series, data.series);
18139 assert_eq!(projected.number_format, data.number_format);
18140 let package = OpcPackage::from_reader(std::io::Cursor::new(workbook)).unwrap();
18141 let styles =
18142 String::from_utf8(package.get_part("/xl/styles.xml").unwrap().to_vec()).unwrap();
18143 assert!(styles.contains("formatCode=\"0.0%\""));
18144 }
18145 }
18146 #[test]
18147 fn fx215_qualified_corpus_replacement_cardinalities() {
18148 let corpus = corpus_dir();
18149 assert!(corpus.is_dir(), "F-X215 required corpus is missing");
18150 for deck in ["bar-chart.pptx", "line-chart.pptx", "chart-picture-bg.pptx"] {
18151 let path = corpus.join(deck);
18152 let expected = MANIFEST
18153 .lines()
18154 .find(|line| line.starts_with(&format!("{deck}\t")))
18155 .unwrap()
18156 .split('\t')
18157 .nth(2)
18158 .unwrap();
18159 assert_eq!(sha256(&path), expected);
18160 let package = OpcPackage::open(&path).unwrap();
18161 let chart_part = "/ppt/charts/chart1.xml";
18162 let chart = package.get_part(chart_part).unwrap();
18163 let (kind, original) = read_chart_data(chart).unwrap();
18164 let workbook_id = chart_workbook_relationship_id(chart).unwrap().unwrap();
18165 let rel = package
18166 .get_part_rels(chart_part)
18167 .unwrap()
18168 .items
18169 .iter()
18170 .find(|rel| rel.id == workbook_id)
18171 .unwrap();
18172 assert_eq!(rel.rel_type, oxml_opc::relationship::rel_types::PACKAGE);
18173 assert_ne!(rel.target_mode.as_deref(), Some("External"));
18174 let workbook_part = OpcPackage::resolve_rel_target(chart_part, &rel.target);
18175 let workbook = package.get_part(&workbook_part).unwrap();
18176 let mut variants = Vec::new();
18177 if original.series.len() > 1 {
18178 let mut fewer = original.clone();
18179 fewer.series.pop();
18180 variants.push(("fewer", fewer));
18181 }
18182 let mut more = original.clone();
18183 more.series.push((
18184 "Added exact values".into(),
18185 (0..more.categories.len())
18186 .map(|index| 100.0 + index as f64)
18187 .collect(),
18188 ));
18189 variants.push(("more", more));
18190 let mut categories = original.clone();
18191 categories.categories.push("Added category".into());
18192 for (_, values) in &mut categories.series {
18193 values.push(777.0);
18194 }
18195 variants.push(("categories", categories));
18196 for (label, data) in variants {
18197 let (changed, book) = replace_chart_data(chart, workbook, &data)
18198 .unwrap_or_else(|error| panic!("{deck}/{label}: {error}"));
18199 let (actual, projection) = read_chart_data(&changed).unwrap();
18200 assert_eq!(actual, kind);
18201 assert_eq!(projection.categories, data.categories);
18202 assert_eq!(projection.series, data.series);
18203 let mut candidate = package.clone();
18204 candidate.set_part(chart_part, changed);
18205 candidate.set_part(&workbook_part, book);
18206 let mut bytes = std::io::Cursor::new(Vec::new());
18207 candidate.write_to(&mut bytes).unwrap();
18208 if let Some(directory) = std::env::var_os("RDOCX_FX215_CANDIDATES_DIR") {
18209 let directory = PathBuf::from(directory);
18210 fs::create_dir_all(&directory).unwrap();
18211 fs::write(
18212 directory.join(format!("{deck}-{label}.pptx")),
18213 bytes.get_ref(),
18214 )
18215 .unwrap();
18216 }
18217 let reopened =
18218 OpcPackage::from_reader(std::io::Cursor::new(bytes.into_inner())).unwrap();
18219 for (part, source) in &package.parts {
18220 if part != chart_part && part != &workbook_part {
18221 assert_eq!(
18222 reopened.get_part(part).unwrap(),
18223 source,
18224 "{deck}/{label}: {part}"
18225 );
18226 }
18227 }
18228 for (owner, relationships) in &package.part_rels {
18229 assert_eq!(
18230 reopened.get_part_rels(owner).unwrap().to_xml().unwrap(),
18231 relationships.to_xml().unwrap()
18232 );
18233 }
18234 let old_book = OpcPackage::from_reader(std::io::Cursor::new(workbook)).unwrap();
18235 let new_book = OpcPackage::from_reader(std::io::Cursor::new(
18236 reopened.get_part(&workbook_part).unwrap(),
18237 ))
18238 .unwrap();
18239 for (part, source) in &old_book.parts {
18240 if part != "/xl/worksheets/sheet1.xml" && part != "/xl/styles.xml" {
18241 assert_eq!(
18242 new_book.get_part(part).unwrap(),
18243 source,
18244 "{deck}/{label}: workbook {part}"
18245 );
18246 }
18247 }
18248 eprintln!("F-X215 corpus {deck}/{label} qualified cache and workbook candidate");
18249 }
18250 }
18251 }
18252 #[test]
18253 fn fx215_sparse_cache_projection_retains_missing_values() {
18254 let plot = format!(
18255 r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:barChart>"#,
18256 sparse_category_series()
18257 );
18258 let xml = chart_with_plot(&plot);
18259 let (_, data) = read_chart_data(xml.as_bytes()).unwrap();
18260 assert_eq!(data.categories, vec!["North", "", "West"]);
18261 assert_eq!(data.series[0].1[0], 1.0);
18262 assert!(data.series[0].1[1].is_nan());
18263 assert_eq!(data.series[0].1[2], 2.0);
18264 }
18265
18266 #[test]
18267 fn fx215_invalid_data_and_external_formula_preserve_inputs() {
18268 let original = ChartData {
18269 categories: vec!["North".into(), "South".into()],
18270 series: vec![("Revenue".into(), vec![1.0, 2.0])],
18271 ..ChartData::default()
18272 };
18273 let (xml, book) = authored_chart_parts(ChartKind::Bar, &original, "rIdWorkbook").unwrap();
18274 let chart_source = xml.clone();
18275 let book_source = book.clone();
18276 let invalid = ChartData {
18277 categories: original.categories.clone(),
18278 series: vec![("Bad".into(), vec![f64::NAN, 1.0])],
18279 ..ChartData::default()
18280 };
18281 assert!(replace_chart_data(&xml, &book, &invalid).is_err());
18282 assert_eq!(xml, chart_source);
18283 assert_eq!(book, book_source);
18284 let external = String::from_utf8(xml.clone())
18285 .unwrap()
18286 .replace("Sheet1!", "'[1]Sheet1'!");
18287 let changed = ChartData {
18288 categories: original.categories.clone(),
18289 series: vec![("New".into(), vec![8.0, 9.0])],
18290 ..ChartData::default()
18291 };
18292 let error = replace_chart_data(external.as_bytes(), &book, &changed).unwrap_err();
18293 assert!(error.to_string().contains("external workbook formula"));
18294 assert_eq!(book, book_source);
18295 }
18296 #[test]
18297 fn fx246_legend_position_reserves_measured_plot_space() {
18298 let mut outputs = Vec::new();
18299 for position in ["r", "b", "l"] {
18300 let xml = chart_with_plot(&bar_plot("")).replace(
18301 "</q:plotArea></q:chart>",
18302 &format!("</q:plotArea><q:legend><q:legendPos val=\"{position}\"/><q:overlay val=\"0\"/></q:legend></q:chart>"),
18303 );
18304 let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
18305 let mut fonts = FontManager::new_deterministic().unwrap();
18306 outputs.push(format!(
18307 "{:?}",
18308 render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap()
18309 ));
18310 }
18311 assert_ne!(
18312 outputs[0], outputs[1],
18313 "right and bottom legends must differ"
18314 );
18315 assert_ne!(outputs[0], outputs[2], "right and left legends must differ");
18316 }
18317
18318 #[test]
18319 fn fx246_legend_mutation_preserves_opaque_source_and_order() {
18320 let base = chart_with_plot(&bar_plot(""));
18321 let payload = "<x:legendPos xmlns:x='urn:foreign' val='unknown'/><!--keep exact--><x:opaque xmlns:x='urn:foreign' literal='a & b'/>";
18322 let xml=base.replace("</q:plotArea></q:chart>",&format!("</q:plotArea><q:legend xmlns:c='urn:foreign' producer='keep'>{payload}<q:layout/><q:overlay val='false' untouched='yes'/><q:extLst/></q:legend></q:chart>"));
18323 let settings = super::chart_legend_settings(xml.as_bytes()).unwrap();
18324 assert_eq!(
18325 settings,
18326 super::LegendSettings {
18327 visible: true,
18328 ..Default::default()
18329 }
18330 );
18331 let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
18332 assert_eq!(parsed.chart.legend.as_ref().unwrap().settings, settings);
18333 assert_eq!(
18334 super::set_chart_legend(
18335 xml.as_bytes(),
18336 None,
18337 Some(super::LegendPosition::Right),
18338 Some(false)
18339 )
18340 .unwrap(),
18341 xml.as_bytes()
18342 );
18343 let changed = super::set_chart_legend(
18344 xml.as_bytes(),
18345 None,
18346 Some(super::LegendPosition::Bottom),
18347 Some(true),
18348 )
18349 .unwrap();
18350 let text = String::from_utf8(changed.clone()).unwrap();
18351 assert!(text.contains(payload));
18352 assert!(text.contains("untouched=\"yes\""));
18353 assert!(text.find("<c:legendPos").unwrap() < text.find("<q:layout").unwrap());
18354 assert!(text.find("<q:overlay").unwrap() < text.find("<q:extLst").unwrap());
18355 assert_eq!(
18356 super::chart_legend_settings(&changed).unwrap(),
18357 super::LegendSettings {
18358 visible: true,
18359 position: super::LegendPosition::Bottom,
18360 overlay: true
18361 }
18362 );
18363 assert!(
18364 super::chart_legend_settings(
18365 xml.replace(
18366 "<q:layout/>",
18367 "<q:legendPos val='l'/><q:legendPos val='r'/>"
18368 )
18369 .as_bytes()
18370 )
18371 .is_err()
18372 );
18373 assert!(
18374 super::chart_legend_settings(xml.replace("val='false'", "val='maybe'").as_bytes())
18375 .is_err()
18376 );
18377 let hidden = super::set_chart_legend(&changed, Some(false), None, None).unwrap();
18378 assert!(!super::chart_legend_settings(&hidden).unwrap().visible);
18379 assert_eq!(
18380 super::set_chart_legend(&hidden, None, Some(super::LegendPosition::Left), Some(true))
18381 .unwrap(),
18382 hidden
18383 );
18384 let shown = super::set_chart_legend(
18385 &hidden,
18386 Some(true),
18387 Some(super::LegendPosition::Top),
18388 Some(true),
18389 )
18390 .unwrap();
18391 assert_eq!(
18392 super::chart_legend_settings(&shown).unwrap(),
18393 super::LegendSettings {
18394 visible: true,
18395 position: super::LegendPosition::Top,
18396 overlay: true
18397 }
18398 );
18399 }
18400
18401 #[test]
18402 fn fx246_overlay_and_absent_legends_keep_the_correct_plot() {
18403 use super::{CT_Legend, LegendPosition as P, LegendSettings};
18404 let mut chart = labelled_bar_chart();
18405 let bounds = chart_bounds();
18406 let full = super::geometry_plot_bounds(bounds).unwrap();
18407 let plot = chart.chart.plot_area.plots().unwrap()[0].clone();
18408 let mut fonts = FontManager::new_deterministic().unwrap();
18409 for position in [P::Right, P::Left, P::Top, P::Bottom, P::TopRight] {
18410 chart.chart.legend = Some(CT_Legend {
18411 settings: LegendSettings {
18412 visible: true,
18413 position,
18414 overlay: false,
18415 },
18416 ..Default::default()
18417 });
18418 let layout = super::layout_legend(&chart.chart, &plot, bounds, &mut fonts).unwrap();
18419 for (key, label) in layout.keys.iter().zip(&layout.labels) {
18420 let right = label.origin.x + label.advances.iter().sum::<f64>();
18421 assert!(key.x >= bounds.x && right <= bounds.x + bounds.width);
18422 let outside = key.x + key.width <= layout.plot.x
18423 || key.x >= layout.plot.x + layout.plot.width
18424 || key.y + key.height <= layout.plot.y
18425 || key.y >= layout.plot.y + layout.plot.height;
18426 assert!(outside, "non-overlay {position:?}");
18427 assert!(label.origin.y >= bounds.y && label.origin.y <= bounds.y + bounds.height);
18428 }
18429 chart.chart.legend.as_mut().unwrap().settings.overlay = true;
18430 let overlay = super::layout_legend(&chart.chart, &plot, bounds, &mut fonts).unwrap();
18431 assert_eq!(overlay.plot, full);
18432 assert!(
18433 overlay.keys.iter().any(|key| key.x < full.x + full.width
18434 && key.x + key.width > full.x
18435 && key.y < full.y + full.height
18436 && key.y + key.height > full.y),
18437 "negative control moved inside the plot must be rejected by the non-overlap observer"
18438 );
18439 }
18440 chart.chart.legend = None;
18441 let absent = super::layout_legend(&chart.chart, &plot, bounds, &mut fonts).unwrap();
18442 assert_eq!(absent.plot, full);
18443 assert!(absent.keys.is_empty());
18444 }
18445
18446 #[test]
18447 fn fx246_legend_labels_fit_bounded_chart_geometry() {
18448 let mut chart = labelled_bar_chart();
18449 if let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] {
18450 series[0].name.as_mut().unwrap().values[0] =
18451 "A legend considerably wider than fifty eight points".into();
18452 }
18453 let mut fonts = FontManager::new_deterministic().unwrap();
18454 assert!(
18455 render_chart(&chart.chart, chart_bounds(), &mut fonts).is_err(),
18456 "a chart too narrow for its label refuses explicitly"
18457 );
18458 let wide = Rect {
18459 width: 600.0,
18460 ..chart_bounds()
18461 };
18462 let group = render_chart(&chart.chart, wide, &mut fonts).unwrap();
18463 assert!(format!("{group:?}").contains("considerably wider"));
18464 }
18465 #[test]
18466 fn fx246_legend_scalar_defaults_and_local_shadows_are_qualified() {
18467 let base = chart_with_plot(&bar_plot(""));
18468 for (children, overlay) in [
18469 ("<q:legendPos/><q:overlay/>", true),
18470 (
18471 "<q:legendPos xmlns:q='urn:foreign' val='not owned'/><q:overlay val='0'/>",
18472 false,
18473 ),
18474 (
18475 "<c:legendPos xmlns:c='urn:foreign' val='not owned'/>",
18476 false,
18477 ),
18478 ] {
18479 let xml = base.replace(
18480 "</q:plotArea></q:chart>",
18481 &format!("</q:plotArea><q:legend>{children}</q:legend></q:chart>"),
18482 );
18483 let settings = super::chart_legend_settings(xml.as_bytes()).unwrap();
18484 assert_eq!(settings.position, super::LegendPosition::Right);
18485 assert_eq!(settings.overlay, overlay);
18486 assert_eq!(
18487 CT_ChartSpace::from_xml(xml.as_bytes())
18488 .unwrap()
18489 .chart
18490 .legend
18491 .unwrap()
18492 .settings,
18493 settings
18494 );
18495 }
18496 }
18497 #[test]
18498 fn fx246_legend_entrypoints_require_one_well_formed_xml_document() {
18499 let source = chart_with_plot(&bar_plot(""));
18500 for invalid in [
18501 format!("{source}NOT_XML_DOCUMENT_CONTENT"),
18502 format!("NOT_XML_DOCUMENT_CONTENT{source}"),
18503 format!("{source}<![CDATA[tail]]>"),
18504 format!("{source}&"),
18505 format!("{source}{source}"),
18506 format!("<!--prefix--><?xml version='1.0'?>{source}"),
18507 source.replace("</q:chartSpace>", "<u:opaque/></q:chartSpace>"),
18508 source.replace(
18509 "</q:chartSpace>",
18510 "<x:opaque xmlns:z='urn:producer' x:a='1' z:a='2'/></q:chartSpace>",
18511 ),
18512 ] {
18513 assert!(
18514 super::chart_legend_settings(invalid.as_bytes()).is_err(),
18515 "getter admitted {invalid}"
18516 );
18517 assert!(
18518 super::set_chart_legend(
18519 invalid.as_bytes(),
18520 Some(true),
18521 Some(super::LegendPosition::Left),
18522 None
18523 )
18524 .is_err(),
18525 "setter admitted {invalid}"
18526 );
18527 }
18528 let valid = format!(
18529 "<?xml version='1.0'?><!--prolog-->{source}\n<!--tail--><?producer preserved?>"
18530 );
18531 assert_eq!(
18532 super::set_chart_legend(valid.as_bytes(), None, None, None).unwrap(),
18533 valid.as_bytes()
18534 );
18535 }
18536
18537 #[test]
18538 fn fx246_nominal_legend_metrics_match_independent_native_components() {
18539 let chart = labelled_bar_chart();
18540 let plot = &chart.chart.plot_area.plots().unwrap()[0];
18541 let mut fonts = FontManager::new_deterministic().unwrap();
18542 let layout = super::layout_legend(&chart.chart, plot, chart_bounds(), &mut fonts).unwrap();
18543 assert!((layout.keys[0].width - 4.9433).abs() <= 1.0);
18547 assert!((layout.labels[0].origin.x - layout.keys[0].x - 7.0399).abs() <= 1.0);
18548 assert!((layout.keys[0].height - 4.9433).abs() <= 1.0);
18549 let mut multiple = four_series_bar_chart();
18550 multiple.chart.legend = Some(super::CT_Legend::default());
18551 let plot = &multiple.chart.plot_area.plots().unwrap()[0];
18552 let layout =
18553 super::layout_legend(&multiple.chart, plot, chart_bounds(), &mut fonts).unwrap();
18554 for pair in layout.keys.windows(2) {
18555 assert!((pair[1].y - pair[0].y - 16.875).abs() <= 1.0);
18556 }
18557 }
18558}