Skip to main content

oxml_chart/
lib.rs

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
31/// The transitional ChartML namespace used by OOXML packages.
32pub const C_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/chart";
33/// The fixed ChartML prefix used by writers in this crate.
34pub const C_PREFIX: &str = "c";
35/// The DrawingML namespace written on a chart-space root.
36pub const A_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
37/// The office-document relationships namespace written on a chart-space root.
38pub 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/// Errors produced while reading or writing the implemented ChartML core.
46#[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/// One supported two-dimensional chart family.
127#[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/// Data used to author one chart and its editable workbook.
139#[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
149/// Authors one ChartML part and its editable workbook from one validated source.
150pub 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
291/// Project one supported chart family and its cached data without changing source.
292/// Sparse numeric cache entries remain NaN, and sparse labels remain empty.
293/// Unsupported plots and incompatible category vectors return contextual errors.
294pub fn read_chart_data(chart_xml: &[u8]) -> Result<(ChartKind, ChartData)> {
295    let space = CT_ChartSpace::from_xml(chart_xml)?;
296    let [plot] = space.chart.plot_area.plots()? else {
297        return Err(invalid_authoring_value(
298            "chart data projection requires one supported plot",
299        ));
300    };
301    let (kind, series) = match plot {
302        Plot::Bar { series, .. } => (ChartKind::Bar, series),
303        Plot::Line { series, .. } => (ChartKind::Line, series),
304        Plot::Pie { series, .. } => (ChartKind::Pie, series),
305        Plot::Doughnut { series, .. } => (ChartKind::Doughnut, series),
306        Plot::Area { series, .. } => (ChartKind::Area, series),
307        Plot::Scatter { series, .. } => (ChartKind::Scatter, series),
308        Plot::Radar { series, .. } => (ChartKind::Radar, series),
309    };
310    let first = series
311        .first()
312        .ok_or_else(|| invalid_authoring_value("chart has no series"))?;
313    let categories = chart_category_labels(first)?;
314    let mut projected = Vec::with_capacity(series.len());
315    for item in series {
316        if chart_category_labels(item)? != categories {
317            return Err(invalid_authoring_value(
318                "chart series use different category or X vectors",
319            ));
320        }
321        let name = match &item.name {
322            Some(name) => name.values.first().cloned().unwrap_or_default(),
323            None if item.opaque_name => chart_literal_name(item)?,
324            None => String::new(),
325        };
326        projected.push((name, chart_dense_numbers(&item.values)?));
327    }
328    Ok((
329        kind,
330        ChartData {
331            categories,
332            series: projected,
333            number_format: (first.values.format_code != "General")
334                .then(|| first.values.format_code.clone()),
335            ..ChartData::default()
336        },
337    ))
338}
339
340fn chart_dense_numbers(data: &NumericData) -> Result<Vec<f64>> {
341    let (count, indexes) = cache_layout(&data.markup, data.values.len())?;
342    let count = chart_projection_count(count)?;
343    let mut values = vec![f64::NAN; count];
344    for (index, value) in indexes.into_iter().zip(&data.values) {
345        values[index as usize] = *value;
346    }
347    Ok(values)
348}
349
350fn chart_projection_count(count: u32) -> Result<usize> {
351    if count > 1_048_576 {
352        return Err(invalid_attribute("ptCount", "val", count.to_string()));
353    }
354    Ok(count as usize)
355}
356
357fn chart_category_labels(series: &Series) -> Result<Vec<String>> {
358    let (markup, labels) = match &series.categories {
359        Some(AxisData::String(data)) => (&data.markup, data.values.clone()),
360        Some(AxisData::Numeric(data)) => (
361            &data.markup,
362            data.values.iter().map(|value| value.to_string()).collect(),
363        ),
364        None if series.opaque_categories => {
365            return Err(invalid_authoring_value(
366                "unsupported literal or multilevel category projection",
367            ));
368        }
369        None => return Ok(Vec::new()),
370    };
371    let (count, indexes) = cache_layout(markup, labels.len())?;
372    let mut values = vec![String::new(); chart_projection_count(count)?];
373    for (index, value) in indexes.into_iter().zip(labels) {
374        values[index as usize] = value;
375    }
376    Ok(values)
377}
378
379fn chart_literal_name(series: &Series) -> Result<String> {
380    for raw in series.raw_children.at(2) {
381        let inherited = series
382            .namespace_declarations
383            .iter()
384            .filter_map(|(name, value)| {
385                let prefix = if name == "xmlns" {
386                    ""
387                } else {
388                    name.strip_prefix("xmlns:")?
389                };
390                Some((prefix.as_bytes().to_vec(), value.clone()))
391            })
392            .collect::<NamespaceBindings>();
393        let mut reader = Reader::from_reader(raw);
394        let mut buffer = Vec::new();
395        let mut namespaces = inherited;
396        let mut depth = 0usize;
397        loop {
398            match reader
399                .read_event_into(&mut buffer)
400                .map_err(OxmlError::from)?
401            {
402                Event::Start(element) => {
403                    if depth == 0 {
404                        if chart_child_local(&element, &namespaces)?.as_deref() != Some(b"tx") {
405                            break;
406                        }
407                        namespaces = chart_bindings(&namespaces, &element)?;
408                    }
409                    if depth == 1
410                        && chart_child_local(&element, &namespaces)?.as_deref() == Some(b"v")
411                    {
412                        let value = capture_element(&mut reader, &element)?;
413                        return Ok(parse_text_element(&value, b"v", &namespaces)?.0);
414                    }
415                    depth += 1;
416                }
417                Event::End(_) => {
418                    depth = depth.saturating_sub(1);
419                }
420                Event::Eof => break,
421                _ => {}
422            }
423            buffer.clear();
424        }
425    }
426    Err(invalid_authoring_value(
427        "unsupported series name projection",
428    ))
429}
430
431/// Resolve only the root-owned ChartML externalData relationships attribute.
432pub fn chart_workbook_relationship_id(chart_xml: &[u8]) -> Result<Option<String>> {
433    use quick_xml::name::ResolveResult;
434    use quick_xml::reader::NsReader;
435    CT_ChartSpace::from_xml(chart_xml)?;
436    let mut reader = NsReader::from_reader(chart_xml);
437    let mut buffer = Vec::new();
438    let mut depth = 0usize;
439    let mut result = None;
440    loop {
441        let (namespace, event) = reader
442            .read_resolved_event_into(&mut buffer)
443            .map_err(OxmlError::from)?;
444        let empty = matches!(&event, Event::Empty(_));
445        match (namespace, event) {
446            (namespace, Event::Start(element) | Event::Empty(element)) => {
447                if depth == 1
448                    && matches!(namespace, ResolveResult::Bound(value) if value.as_ref() == C_NS.as_bytes())
449                    && element.local_name().as_ref() == b"externalData"
450                {
451                    if result.is_some() {
452                        return Err(ChartError::DuplicateElement("c:externalData".into()));
453                    }
454                    for attribute in element.attributes() {
455                        let attribute = attribute.map_err(OxmlError::from)?;
456                        let (namespace, local) = reader.resolver().resolve_attribute(attribute.key);
457                        if matches!(namespace, ResolveResult::Bound(value) if value.as_ref() == R_NS.as_bytes())
458                            && local.as_ref() == b"id"
459                        {
460                            result = Some(
461                                attribute
462                                    .decoded_and_normalized_value(
463                                        XmlVersion::Implicit1_0,
464                                        reader.decoder(),
465                                    )
466                                    .map_err(OxmlError::from)?
467                                    .into_owned(),
468                            );
469                        }
470                    }
471                    if result.is_none() {
472                        return Err(ChartError::MissingElement("c:externalData/@r:id".into()));
473                    }
474                }
475                if !empty {
476                    depth += 1;
477                }
478            }
479            (_, Event::End(_)) => {
480                depth = depth.saturating_sub(1);
481            }
482            (_, Event::DocType(_)) => {
483                return Err(invalid_authoring_value(
484                    "DOCTYPE is unsupported in chart data",
485                ));
486            }
487            (_, Event::Eof) => break,
488            _ => {}
489        }
490        buffer.clear();
491    }
492    Ok(result)
493}
494
495#[derive(Debug)]
496struct ChartSourceNode {
497    local: Vec<u8>,
498    qualified: bool,
499    parent: Option<usize>,
500    start: usize,
501    end: usize,
502}
503
504fn chart_source_nodes(xml: &[u8]) -> Result<Vec<ChartSourceNode>> {
505    use quick_xml::name::ResolveResult;
506    use quick_xml::reader::NsReader;
507    let mut reader = NsReader::from_reader(xml);
508    let mut nodes: Vec<ChartSourceNode> = Vec::new();
509    let mut stack = Vec::new();
510    let mut buffer = Vec::new();
511    loop {
512        let start = reader.buffer_position() as usize;
513        let event = reader
514            .read_event_into(&mut buffer)
515            .map_err(OxmlError::from)?;
516        let end = reader.buffer_position() as usize;
517        let empty = matches!(&event, Event::Empty(_));
518        match event {
519            Event::Start(element) | Event::Empty(element) => {
520                let (namespace, local) = reader.resolver().resolve_element(element.name());
521                let qualified = matches!(namespace,ResolveResult::Bound(value) if value.as_ref()==C_NS.as_bytes());
522                let local = local.as_ref().to_vec();
523                let index = nodes.len();
524                if index >= 1_000_000 {
525                    return Err(invalid_authoring_value("chart XML node limit"));
526                }
527                nodes.push(ChartSourceNode {
528                    local,
529                    qualified,
530                    parent: stack.last().copied(),
531                    start,
532                    end,
533                });
534                if !empty {
535                    stack.push(index);
536                }
537            }
538            Event::End(_) => {
539                let index = stack
540                    .pop()
541                    .ok_or_else(|| invalid_authoring_value("unbalanced chart source"))?;
542                nodes[index].end = end;
543            }
544            Event::DocType(_) => {
545                return Err(invalid_authoring_value(
546                    "DOCTYPE is unsupported in chart source",
547                ));
548            }
549            Event::Eof => break,
550            _ => {}
551        }
552        buffer.clear();
553    }
554    if !stack.is_empty() {
555        return Err(invalid_authoring_value("truncated chart source"));
556    }
557    Ok(nodes)
558}
559
560fn chart_child_node(nodes: &[ChartSourceNode], parent: usize, local: &[u8]) -> Result<usize> {
561    let found = nodes
562        .iter()
563        .enumerate()
564        .filter(|(_, node)| node.parent == Some(parent) && node.qualified && node.local == local)
565        .map(|(index, _)| index)
566        .collect::<Vec<_>>();
567    let [index] = found.as_slice() else {
568        return Err(invalid_authoring_value(format!(
569            "chart data requires one owned {}",
570            String::from_utf8_lossy(local)
571        )));
572    };
573    Ok(*index)
574}
575
576fn chart_source_series(xml: &[u8]) -> Result<Vec<Series>> {
577    CT_ChartSpace::from_xml(xml)?.chart.plot_area.series()
578}
579
580fn resized_chart_range(original: &ChartRange, count: usize) -> Result<ChartRange> {
581    let mut range = original.clone();
582    range.last_row = range
583        .first_row
584        .checked_add(
585            count
586                .checked_sub(1)
587                .ok_or_else(|| invalid_authoring_value("empty chart data range"))?,
588        )
589        .ok_or_else(|| invalid_authoring_value("chart data row overflow"))?;
590    range.formula().map_err(invalid_authoring_value)?;
591    Ok(range)
592}
593
594fn replaced_string_reference(
595    original: &StringRef,
596    formula: String,
597    values: Vec<String>,
598) -> StringRef {
599    let mut result = original.clone();
600    result.formula = formula;
601    result.values = values;
602    result.markup.declared_point_count = None;
603    result.markup.point_indexes.clear();
604    result
605}
606
607fn replaced_numeric_reference(
608    original: &NumericData,
609    formula: String,
610    values: Vec<f64>,
611    number_format: Option<&str>,
612) -> NumericData {
613    let mut result = original.clone();
614    result.formula = formula;
615    result.values = values;
616    if let Some(format) = number_format {
617        result.format_code = format.to_owned();
618    }
619    result.markup.declared_point_count = None;
620    result.markup.point_indexes.clear();
621    result
622}
623
624/// Stage replacement chart caches and only their formula-owned workbook cells.
625/// The caller publishes both returned parts together after proving package owners.
626/// Original root, plot, axes, legend and surviving series formatting stay exact.
627pub fn replace_chart_data(
628    chart_xml: &[u8],
629    workbook: &[u8],
630    data: &ChartData,
631) -> Result<(Vec<u8>, Vec<u8>)> {
632    let (kind, current) = read_chart_data(chart_xml)?;
633    validate_chart_data(kind, data)?;
634    if data.category_axis_title.is_some()
635        || data.value_axis_title.is_some()
636        || !data.palette.is_empty()
637    {
638        return Err(invalid_authoring_value(
639            "replace_data changes chart data, not axis titles or palette",
640        ));
641    }
642    let workbook_id = chart_workbook_relationship_id(chart_xml)?
643        .ok_or_else(|| invalid_authoring_value("chart has no embedded workbook reference"))?;
644    let old_series = chart_source_series(chart_xml)?;
645    let first = old_series
646        .first()
647        .ok_or_else(|| invalid_authoring_value("chart has no editable series"))?;
648    let first_name = first
649        .name
650        .as_ref()
651        .ok_or_else(|| invalid_authoring_value("series title needs an owned workbook formula"))?;
652    let name_origin =
653        ChartRange::from_formula(&first_name.formula).map_err(invalid_authoring_value)?;
654    let values_origin =
655        ChartRange::from_formula(&first.values.formula).map_err(invalid_authoring_value)?;
656    if name_origin.first_row != name_origin.last_row || name_origin.sheet != values_origin.sheet {
657        return Err(invalid_authoring_value(
658            "series title and values must own one embedded worksheet",
659        ));
660    }
661    let category_origin = match first.categories.as_ref() {
662        Some(AxisData::String(reference)) => ChartRange::from_formula(&reference.formula),
663        Some(AxisData::Numeric(reference)) => ChartRange::from_formula(&reference.formula),
664        None => {
665            return Err(invalid_authoring_value(
666                "categories need an owned workbook formula",
667            ));
668        }
669    }
670    .map_err(invalid_authoring_value)?;
671    if category_origin.sheet != values_origin.sheet {
672        return Err(invalid_authoring_value(
673            "chart data must own one embedded worksheet",
674        ));
675    }
676    // Validate every formula boundary before an unchanged-data fast path.
677    for series in &old_series {
678        for formula in [
679            series.name.as_ref().map(|r| r.formula.as_str()),
680            Some(series.values.formula.as_str()),
681            series.categories.as_ref().map(|r| match r {
682                AxisData::String(r) => r.formula.as_str(),
683                AxisData::Numeric(r) => r.formula.as_str(),
684            }),
685        ] {
686            let formula = formula.ok_or_else(|| {
687                invalid_authoring_value("series needs ordinary formula-backed data")
688            })?;
689            let range = ChartRange::from_formula(formula).map_err(invalid_authoring_value)?;
690            if range.sheet != values_origin.sheet {
691                return Err(invalid_authoring_value(
692                    "series formula refers to a different worksheet",
693                ));
694            }
695        }
696    }
697    if data.categories == current.categories
698        && data.series == current.series
699        && data.number_format.as_ref().is_none_or(|format| {
700            Some(format) == current.number_format.as_ref()
701                || (format == "General" && current.number_format.is_none())
702        })
703    {
704        oxml_sml::chart_unused_columns(workbook, &values_origin.sheet, 0)
705            .map_err(invalid_authoring_value)?;
706        return Ok((chart_xml.to_vec(), workbook.to_vec()));
707    }
708    let nodes = chart_source_nodes(chart_xml)?;
709    let chart = chart_child_node(&nodes, 0, b"chart")?;
710    let plot_area = chart_child_node(&nodes, chart, b"plotArea")?;
711    let plot_local = match kind {
712        ChartKind::Bar => b"barChart".as_slice(),
713        ChartKind::Line => b"lineChart",
714        ChartKind::Pie => b"pieChart",
715        ChartKind::Doughnut => b"doughnutChart",
716        ChartKind::Area => b"areaChart",
717        ChartKind::Scatter => b"scatterChart",
718        ChartKind::Radar => b"radarChart",
719    };
720    let plot = chart_child_node(&nodes, plot_area, plot_local)?;
721    let series_nodes = nodes
722        .iter()
723        .enumerate()
724        .filter(|(_, node)| node.parent == Some(plot) && node.qualified && node.local == b"ser")
725        .map(|(index, _)| index)
726        .collect::<Vec<_>>();
727    if series_nodes.len() != old_series.len() {
728        return Err(invalid_authoring_value(
729            "editable series source inventory disagrees",
730        ));
731    }
732    let mut updates = Vec::new();
733    let mut patches = Vec::new();
734    for (ordinal, (old, &source_index)) in old_series.iter().zip(&series_nodes).enumerate() {
735        let old_name = old
736            .name
737            .as_ref()
738            .ok_or_else(|| invalid_authoring_value("series title needs formula-backed data"))?;
739        let old_name_range =
740            ChartRange::from_formula(&old_name.formula).map_err(invalid_authoring_value)?;
741        if old_name_range.first_row != old_name_range.last_row {
742            return Err(invalid_authoring_value(
743                "series title formula must own one cell",
744            ));
745        }
746        let old_value_range =
747            ChartRange::from_formula(&old.values.formula).map_err(invalid_authoring_value)?;
748        let categories = old
749            .categories
750            .as_ref()
751            .ok_or_else(|| invalid_authoring_value("series categories are missing"))?;
752        let old_category_range = ChartRange::from_formula(match categories {
753            AxisData::String(r) => &r.formula,
754            AxisData::Numeric(r) => &r.formula,
755        })
756        .map_err(invalid_authoring_value)?;
757        let Some((name, values)) = data.series.get(ordinal) else {
758            for original in [old_name_range, old_value_range, old_category_range] {
759                updates.push(ChartRangeUpdate {
760                    number_format: None,
761                    original: Some(original),
762                    target: None,
763                    values: Vec::new(),
764                });
765            }
766            patches.push((
767                nodes[source_index].start,
768                nodes[source_index].end,
769                Vec::new(),
770            ));
771            continue;
772        };
773        let new_value_range = resized_chart_range(&old_value_range, values.len())?;
774        let new_category_range = resized_chart_range(&old_category_range, data.categories.len())?;
775        updates.push(ChartRangeUpdate {
776            number_format: None,
777            original: Some(old_name_range.clone()),
778            target: Some(old_name_range.clone()),
779            values: vec![ChartCellValue::Text(name.clone())],
780        });
781        updates.push(ChartRangeUpdate {
782            number_format: Some(
783                data.number_format
784                    .as_deref()
785                    .unwrap_or(&old.values.format_code)
786                    .to_owned(),
787            ),
788            original: Some(old_value_range),
789            target: Some(new_value_range.clone()),
790            values: values.iter().copied().map(ChartCellValue::Number).collect(),
791        });
792        let category_cells = if matches!(categories, AxisData::Numeric(_)) {
793            data.categories
794                .iter()
795                .map(|value| {
796                    value
797                        .parse::<f64>()
798                        .map(ChartCellValue::Number)
799                        .map_err(invalid_authoring_value)
800                })
801                .collect::<Result<Vec<_>>>()?
802        } else {
803            data.categories
804                .iter()
805                .cloned()
806                .map(ChartCellValue::Text)
807                .collect()
808        };
809        updates.push(ChartRangeUpdate {
810            number_format: None,
811            original: Some(old_category_range),
812            target: Some(new_category_range.clone()),
813            values: category_cells,
814        });
815        let tx = chart_child_node(&nodes, source_index, b"tx")?;
816        let name_ref = chart_child_node(&nodes, tx, b"strRef")?;
817        let name_reference = replaced_string_reference(
818            old_name,
819            old_name_range.formula().map_err(invalid_authoring_value)?,
820            vec![name.clone()],
821        );
822        patches.push((
823            nodes[name_ref].start,
824            nodes[name_ref].end,
825            name_reference.to_xml()?,
826        ));
827        let value_wrapper = chart_child_node(
828            &nodes,
829            source_index,
830            if kind == ChartKind::Scatter {
831                b"yVal"
832            } else {
833                b"val"
834            },
835        )?;
836        let value_ref = chart_child_node(&nodes, value_wrapper, b"numRef")?;
837        let value_reference = replaced_numeric_reference(
838            &old.values,
839            new_value_range.formula().map_err(invalid_authoring_value)?,
840            values.clone(),
841            data.number_format.as_deref(),
842        );
843        patches.push((
844            nodes[value_ref].start,
845            nodes[value_ref].end,
846            value_reference.to_xml()?,
847        ));
848        let category_wrapper = chart_child_node(
849            &nodes,
850            source_index,
851            if kind == ChartKind::Scatter {
852                b"xVal"
853            } else {
854                b"cat"
855            },
856        )?;
857        let (local, bytes) = match categories {
858            AxisData::String(reference) => (
859                b"strRef".as_slice(),
860                replaced_string_reference(
861                    reference,
862                    new_category_range
863                        .formula()
864                        .map_err(invalid_authoring_value)?,
865                    data.categories.clone(),
866                )
867                .to_xml()?,
868            ),
869            AxisData::Numeric(reference) => (
870                b"numRef".as_slice(),
871                replaced_numeric_reference(
872                    reference,
873                    new_category_range
874                        .formula()
875                        .map_err(invalid_authoring_value)?,
876                    data.categories
877                        .iter()
878                        .map(|v| v.parse::<f64>().map_err(invalid_authoring_value))
879                        .collect::<Result<Vec<_>>>()?,
880                    None,
881                )
882                .to_xml()?,
883            ),
884        };
885        let category_ref = chart_child_node(&nodes, category_wrapper, local)?;
886        patches.push((nodes[category_ref].start, nodes[category_ref].end, bytes));
887    }
888    let added = data.series.len().saturating_sub(old_series.len());
889    if added > 0 {
890        let free = oxml_sml::chart_unused_columns(workbook, &values_origin.sheet, added)
891            .map_err(invalid_authoring_value)?;
892        let (default_xml, _) = authored_chart_parts(kind, data, &workbook_id)?;
893        let defaults = chart_source_series(&default_xml)?;
894        let max_index = old_series.iter().map(|s| s.index).max().unwrap();
895        let max_order = old_series.iter().map(|s| s.order).max().unwrap();
896        let mut bytes = Vec::new();
897        for (offset, column) in free.into_iter().enumerate() {
898            let ordinal = old_series.len() + offset;
899            let (name, values) = &data.series[ordinal];
900            let mut series = defaults[ordinal].clone();
901            let step = u32::try_from(offset + 1).map_err(invalid_authoring_value)?;
902            series.index = max_index
903                .checked_add(step)
904                .ok_or_else(|| invalid_authoring_value("chart series index overflow"))?;
905            series.order = max_order
906                .checked_add(step)
907                .ok_or_else(|| invalid_authoring_value("chart series order overflow"))?;
908            let mut title = name_origin.clone();
909            title.column = column;
910            let mut numbers = resized_chart_range(&values_origin, values.len())?;
911            numbers.column = column;
912            let category = resized_chart_range(&category_origin, data.categories.len())?;
913            series.name = Some(StringRef::new(
914                title.formula().map_err(invalid_authoring_value)?,
915                vec![name.clone()],
916            )?);
917            series.values = NumericData::new(
918                numbers.formula().map_err(invalid_authoring_value)?,
919                data.number_format
920                    .as_deref()
921                    .unwrap_or(&first.values.format_code)
922                    .to_owned(),
923                values.clone(),
924            )?;
925            series.categories = Some(if kind == ChartKind::Scatter {
926                AxisData::Numeric(NumericData::new(
927                    category.formula().map_err(invalid_authoring_value)?,
928                    "General".into(),
929                    data.categories
930                        .iter()
931                        .map(|v| v.parse::<f64>().map_err(invalid_authoring_value))
932                        .collect::<Result<Vec<_>>>()?,
933                )?)
934            } else {
935                AxisData::String(StringRef::new(
936                    category.formula().map_err(invalid_authoring_value)?,
937                    data.categories.clone(),
938                )?)
939            });
940            updates.push(ChartRangeUpdate {
941                number_format: None,
942                original: None,
943                target: Some(title),
944                values: vec![ChartCellValue::Text(name.clone())],
945            });
946            updates.push(ChartRangeUpdate {
947                number_format: Some(
948                    data.number_format
949                        .as_deref()
950                        .unwrap_or(&first.values.format_code)
951                        .to_owned(),
952                ),
953                original: None,
954                target: Some(numbers),
955                values: values.iter().copied().map(ChartCellValue::Number).collect(),
956            });
957            bytes.extend(series.to_xml_for_plot(kind == ChartKind::Scatter)?);
958        }
959        let position = nodes[*series_nodes.last().unwrap()].end;
960        patches.push((position, position, bytes));
961    }
962    let updated_workbook =
963        oxml_sml::merge_chart_ranges(workbook, &updates).map_err(invalid_authoring_value)?;
964    patches.sort_by_key(|(start, end, _)| (*start, *end));
965    for pair in patches.windows(2) {
966        if pair[0].1 > pair[1].0 {
967            return Err(invalid_authoring_value(
968                "overlapping chart data source edits",
969            ));
970        }
971    }
972    let mut updated_chart = chart_xml.to_vec();
973    for (start, end, bytes) in patches.into_iter().rev() {
974        updated_chart.splice(start..end, bytes);
975    }
976    let (_, projected) = read_chart_data(&updated_chart)?;
977    if projected.categories != data.categories || projected.series != data.series {
978        return Err(invalid_authoring_value(
979            "replacement chart cache projection disagrees with requested data",
980        ));
981    }
982    Ok((updated_chart, updated_workbook))
983}
984
985fn validate_chart_data(kind: ChartKind, data: &ChartData) -> Result<()> {
986    if data.categories.is_empty() {
987        return Err(invalid_authoring_value("at least one category is required"));
988    }
989    if data.series.is_empty() {
990        return Err(invalid_authoring_value("at least one series is required"));
991    }
992    if matches!(kind, ChartKind::Pie | ChartKind::Doughnut) && data.series.len() != 1 {
993        return Err(invalid_authoring_value(
994            "pie and doughnut charts require exactly one series",
995        ));
996    }
997    for (name, values) in &data.series {
998        if values.len() != data.categories.len() {
999            return Err(invalid_authoring_value(format!(
1000                "series {name:?} has {} values for {} categories",
1001                values.len(),
1002                data.categories.len()
1003            )));
1004        }
1005        if values.iter().any(|value| !value.is_finite()) {
1006            return Err(invalid_authoring_value(format!(
1007                "series {name:?} contains a nonfinite value"
1008            )));
1009        }
1010    }
1011    if kind == ChartKind::Scatter {
1012        for category in &data.categories {
1013            if category
1014                .parse::<f64>()
1015                .ok()
1016                .is_none_or(|value| !value.is_finite())
1017            {
1018                return Err(invalid_authoring_value(format!(
1019                    "scatter category {category:?} is not a finite number"
1020                )));
1021            }
1022        }
1023    }
1024    if let Some(format) = &data.number_format {
1025        NumericData::new("Sheet1!$A$2".to_owned(), format.clone(), vec![0.0])?;
1026    }
1027    Ok(())
1028}
1029
1030fn invalid_authoring_value(error: impl fmt::Display) -> ChartError {
1031    ChartError::InvalidValue {
1032        element: "chart authoring".to_owned(),
1033        value: error.to_string(),
1034    }
1035}
1036
1037fn category_value_axes(category: AxisId, value: AxisId, data: &ChartData) -> Result<Vec<Axis>> {
1038    authored_axes(
1039        [
1040            Axis::new(AxisKind::Category, category, AxisPosition::Bottom, value),
1041            Axis::new(AxisKind::Value, value, AxisPosition::Left, category),
1042        ],
1043        data,
1044    )
1045}
1046
1047fn authored_axes(mut axes: [Axis; 2], data: &ChartData) -> Result<Vec<Axis>> {
1048    for axis in &mut axes {
1049        axis.set_deleted(false);
1050    }
1051    axes[0].title = data
1052        .category_axis_title
1053        .as_deref()
1054        .map(CT_Title::plain_text);
1055    axes[1].title = data.value_axis_title.as_deref().map(CT_Title::plain_text);
1056    axes[1].number_format = data
1057        .number_format
1058        .as_ref()
1059        .map(|format| NumberFormat::new(format.clone(), false))
1060        .transpose()?;
1061    Ok(axes.into())
1062}
1063
1064fn apply_palette(kind: ChartKind, data: &ChartData, series: &mut [Series]) {
1065    if data.palette.is_empty() {
1066        return;
1067    }
1068    for (index, item) in series.iter_mut().enumerate() {
1069        item.sp_pr = Some(series_shape(kind, data.palette[index % data.palette.len()]));
1070    }
1071    if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) && series.len() == 1 {
1072        series[0].authored_data_points = data
1073            .categories
1074            .iter()
1075            .enumerate()
1076            .map(|(index, _)| DataPoint {
1077                index: index as u32,
1078                sp_pr: series_shape(kind, data.palette[index % data.palette.len()]),
1079            })
1080            .collect();
1081    }
1082}
1083
1084fn series_shape(kind: ChartKind, color: RgbColor) -> CT_ShapeProperties {
1085    let mut solid = SolidFill::default();
1086    solid.color = Some(ColorChoice::srgb(color));
1087    let fill = Fill::Solid(solid);
1088    let mut properties = CT_ShapeProperties::default();
1089    if matches!(
1090        kind,
1091        ChartKind::Line | ChartKind::Scatter | ChartKind::Radar
1092    ) {
1093        let mut line = CT_LineProperties::default();
1094        line.fill = Some(fill);
1095        properties.line = Some(line);
1096    } else {
1097        properties.fill = Some(fill);
1098    }
1099    properties
1100}
1101
1102fn spreadsheet_column_name(mut index: usize) -> String {
1103    let mut name = Vec::new();
1104    index += 1;
1105    while index != 0 {
1106        let remainder = (index - 1) % 26;
1107        name.push((b'A' + remainder as u8) as char);
1108        index = (index - 1) / 26;
1109    }
1110    name.iter().rev().collect()
1111}
1112
1113/// Backend-neutral plot geometry and the plot rectangle reserved inside a chart.
1114#[derive(Clone, Debug)]
1115pub struct ChartGeometry {
1116    pub plot_bounds: Rect,
1117    pub elements: Vec<PositionedElement>,
1118}
1119
1120#[derive(Clone, Debug)]
1121struct AxisScale {
1122    domain: Domain,
1123    ticks: Vec<f64>,
1124}
1125
1126#[derive(Clone, Copy, Debug)]
1127struct AxisMapping {
1128    domain: Domain,
1129    orientation: Orientation,
1130}
1131
1132#[derive(Clone, Copy, Debug, Default)]
1133struct GeometryOptions {
1134    category_orientation: Orientation,
1135    value: Option<AxisMapping>,
1136    scatter_x: Option<AxisMapping>,
1137    scatter_y: Option<AxisMapping>,
1138}
1139
1140#[derive(Default)]
1141struct ChartAnnotations {
1142    gridlines: Vec<PositionedElement>,
1143    axis_lines: Vec<PositionedElement>,
1144    ticks: Vec<PositionedElement>,
1145    labels: Vec<PositionedElement>,
1146}
1147
1148/// Converts one supported typed chart into a labelled backend-neutral group.
1149pub fn render_chart(
1150    chart: &CT_Chart,
1151    bounds: Rect,
1152    theme: &CT_OfficeStyleSheet,
1153    color_map: &ColorMap,
1154    fonts: &mut FontManager,
1155) -> Result<GroupElement> {
1156    let plot_bounds = geometry_plot_bounds(bounds)?;
1157    let plots = chart.plot_area.plots()?;
1158    let plot = plots
1159        .first()
1160        .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
1161    plot.validate()?;
1162    let series_colours = resolve_series_colours(plot, theme, color_map)?;
1163    let axes = chart.plot_area.axes()?;
1164    let options = geometry_options(plot, &axes, chart.disp_blanks_as)?;
1165    let plot_children = render_plot_geometry(
1166        plot,
1167        plot_bounds,
1168        chart.disp_blanks_as,
1169        options,
1170        &series_colours,
1171    )?;
1172    if plot_children.is_empty() && !plot_can_render_without_marks(plot) {
1173        return Err(ChartError::InvalidValue {
1174            element: "c:plotArea".to_owned(),
1175            value: "plot has no renderable cached data".to_owned(),
1176        });
1177    }
1178
1179    let mut chart_annotations =
1180        render_axes_and_labels(plot, &axes, plot_bounds, chart.disp_blanks_as, fonts)?;
1181    let mut annotations = Vec::new();
1182    render_data_labels(
1183        plot,
1184        plot_bounds,
1185        chart.disp_blanks_as,
1186        options,
1187        fonts,
1188        &mut chart_annotations.labels,
1189    )?;
1190    render_legend(
1191        chart,
1192        plot,
1193        plot_bounds,
1194        fonts,
1195        &series_colours,
1196        &mut annotations,
1197        &mut chart_annotations.labels,
1198    )?;
1199
1200    validate_geometry_coordinates(&chart_annotations.gridlines)?;
1201    validate_geometry_coordinates(&plot_children)?;
1202    validate_geometry_coordinates(&chart_annotations.axis_lines)?;
1203    validate_geometry_coordinates(&chart_annotations.ticks)?;
1204    validate_geometry_coordinates(&annotations)?;
1205    let mut children = chart_annotations.gridlines;
1206    children.push(PositionedElement::Group(GroupElement {
1207        transform: Transform::IDENTITY,
1208        clip: Some(Path::rect(plot_bounds)),
1209        opacity: 1.0,
1210        effects: Vec::new(),
1211        children: plot_children,
1212    }));
1213    children.extend(chart_annotations.axis_lines);
1214    children.extend(chart_annotations.ticks);
1215    children.extend(annotations);
1216    children.extend(chart_annotations.labels);
1217    Ok(GroupElement {
1218        transform: Transform::IDENTITY,
1219        clip: None,
1220        opacity: 1.0,
1221        effects: Vec::new(),
1222        children,
1223    })
1224}
1225
1226/// Shapes the stable label used inside a visible unsupported-chart fallback.
1227pub fn render_chart_placeholder(bounds: Rect, fonts: &mut FontManager) -> Result<GroupElement> {
1228    if !bounds.width.is_finite()
1229        || !bounds.height.is_finite()
1230        || bounds.width <= 0.0
1231        || bounds.height <= 0.0
1232    {
1233        return Err(ChartError::InvalidValue {
1234            element: "chart fallback bounds".to_owned(),
1235            value: "width and height must be finite and positive".to_owned(),
1236        });
1237    }
1238    let label = shape_label(
1239        fonts,
1240        "Unsupported chart",
1241        Point {
1242            x: 4.0,
1243            y: (bounds.height / 2.0).max(9.0),
1244        },
1245    )?;
1246    Ok(GroupElement {
1247        transform: Transform::IDENTITY,
1248        clip: Some(Path::rect(Rect {
1249            x: 0.0,
1250            y: 0.0,
1251            width: bounds.width,
1252            height: bounds.height,
1253        })),
1254        opacity: 1.0,
1255        effects: Vec::new(),
1256        children: vec![PositionedElement::Text(label)],
1257    })
1258}
1259
1260/// Converts one supported typed chart into chart-local backend-neutral paths.
1261pub fn render_geometry(
1262    chart: &CT_Chart,
1263    bounds: Rect,
1264    theme: &CT_OfficeStyleSheet,
1265    color_map: &ColorMap,
1266) -> Result<ChartGeometry> {
1267    let plot_bounds = geometry_plot_bounds(bounds)?;
1268    let plots = chart.plot_area.plots()?;
1269    let plot = plots
1270        .first()
1271        .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
1272    plot.validate()?;
1273    let series_colours = resolve_series_colours(plot, theme, color_map)?;
1274    let children = render_plot_geometry(
1275        plot,
1276        plot_bounds,
1277        chart.disp_blanks_as,
1278        geometry_options(plot, &[], chart.disp_blanks_as)?,
1279        &series_colours,
1280    )?;
1281    if children.is_empty() && !plot_can_render_without_marks(plot) {
1282        return Err(ChartError::InvalidValue {
1283            element: "c:plotArea".to_owned(),
1284            value: "plot has no renderable cached data".to_owned(),
1285        });
1286    }
1287    validate_geometry_coordinates(&children)?;
1288    Ok(ChartGeometry {
1289        plot_bounds,
1290        elements: vec![PositionedElement::Group(GroupElement {
1291            transform: Transform::IDENTITY,
1292            clip: None,
1293            opacity: 1.0,
1294            effects: Vec::new(),
1295            children,
1296        })],
1297    })
1298}
1299
1300fn plot_can_render_without_marks(plot: &Plot) -> bool {
1301    matches!(
1302        plot,
1303        Plot::Radar { series, .. }
1304            if series
1305                .iter()
1306                .any(|series| !series.values.values.is_empty())
1307    )
1308}
1309
1310fn geometry_plot_bounds(bounds: Rect) -> Result<Rect> {
1311    if ![bounds.x, bounds.y, bounds.width, bounds.height]
1312        .into_iter()
1313        .all(f64::is_finite)
1314        || bounds.width <= 0.0
1315        || bounds.height <= 0.0
1316        || !(bounds.x + bounds.width).is_finite()
1317        || !(bounds.y + bounds.height).is_finite()
1318    {
1319        return Err(ChartError::InvalidValue {
1320            element: "chart geometry bounds".to_owned(),
1321            value: format!(
1322                "x={}, y={}, width={}, height={}",
1323                bounds.x, bounds.y, bounds.width, bounds.height
1324            ),
1325        });
1326    }
1327    let plot_bounds = Rect {
1328        x: bounds.x + PLOT_MARGIN_LEFT,
1329        y: bounds.y + PLOT_MARGIN_TOP,
1330        width: bounds.width - PLOT_MARGIN_LEFT - PLOT_MARGIN_RIGHT,
1331        height: bounds.height - PLOT_MARGIN_TOP - PLOT_MARGIN_BOTTOM,
1332    };
1333    if ![
1334        plot_bounds.x,
1335        plot_bounds.y,
1336        plot_bounds.width,
1337        plot_bounds.height,
1338        plot_bounds.x + plot_bounds.width,
1339        plot_bounds.y + plot_bounds.height,
1340    ]
1341    .into_iter()
1342    .all(f64::is_finite)
1343        || plot_bounds.width <= 0.0
1344        || plot_bounds.height <= 0.0
1345    {
1346        return Err(ChartError::InvalidValue {
1347            element: "chart geometry bounds".to_owned(),
1348            value: "chart is too small for the fixed plot margins".to_owned(),
1349        });
1350    }
1351    Ok(plot_bounds)
1352}
1353
1354fn geometry_options(plot: &Plot, axes: &[Axis], blanks: DispBlanksAs) -> Result<GeometryOptions> {
1355    let mut options = GeometryOptions::default();
1356    if let Some(category_axis) = axes.iter().find(|axis| axis.kind != AxisKind::Value) {
1357        options.category_orientation = category_axis.scaling.orientation;
1358    }
1359
1360    if let Plot::Scatter { axis_ids, .. } = plot {
1361        let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
1362        if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[0]) {
1363            options.scatter_x = Some(axis_mapping(axis, x_domain)?);
1364        }
1365        if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[1]) {
1366            options.scatter_y = Some(axis_mapping(axis, y_domain)?);
1367        }
1368    } else if let Some(domain) = plot_value_domain(plot, blanks)? {
1369        options.value = Some(
1370            axes.iter()
1371                .find(|axis| axis.kind == AxisKind::Value)
1372                .map(|axis| axis_mapping(axis, domain))
1373                .transpose()?
1374                .unwrap_or(AxisMapping {
1375                    domain,
1376                    orientation: Orientation::MinMax,
1377                }),
1378        );
1379    }
1380    Ok(options)
1381}
1382
1383fn axis_mapping(axis: &Axis, extent: Domain) -> Result<AxisMapping> {
1384    if axis.scaling.log_base.is_some() {
1385        return Err(ChartError::InvalidValue {
1386            element: "c:scaling/c:logBase".to_owned(),
1387            value: "native chart rendering currently supports linear axes".to_owned(),
1388        });
1389    }
1390    let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
1391    Ok(AxisMapping {
1392        domain: scale.domain,
1393        orientation: axis.scaling.orientation,
1394    })
1395}
1396
1397fn plot_value_domain(plot: &Plot, blanks: DispBlanksAs) -> Result<Option<Domain>> {
1398    let domain = match plot {
1399        Plot::Bar {
1400            grouping, series, ..
1401        } => {
1402            let logical = logical_series_values(series)?;
1403            let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
1404            let layers =
1405                stacked_series_bounds(&logical, stacked, *grouping == BarGrouping::PercentStacked)?;
1406            domain_from_layers(&layers)?
1407        }
1408        Plot::Line {
1409            grouping, series, ..
1410        } => {
1411            let logical = logical_series_values(series)?;
1412            if *grouping == Grouping::Standard {
1413                let values = if blanks == DispBlanksAs::Zero {
1414                    zero_filled_series(&logical, series_category_count(series)?)
1415                        .into_iter()
1416                        .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
1417                        .collect::<Vec<_>>()
1418                } else {
1419                    logical
1420                        .into_iter()
1421                        .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
1422                        .collect::<Vec<_>>()
1423                };
1424                data_domain(&values, false)?
1425            } else {
1426                let layers =
1427                    stacked_series_bounds(&logical, true, *grouping == Grouping::PercentStacked)?;
1428                domain_from_layers(&layers)?
1429            }
1430        }
1431        Plot::Area {
1432            grouping, series, ..
1433        } => {
1434            let logical = logical_series_values(series)?;
1435            let layers = stacked_series_bounds(
1436                &logical,
1437                *grouping != Grouping::Standard,
1438                *grouping == Grouping::PercentStacked,
1439            )?;
1440            domain_from_layers(&layers)?
1441        }
1442        Plot::Radar { series, .. } => {
1443            let values = series
1444                .iter()
1445                .flat_map(|series| series.values.values.iter().copied())
1446                .collect::<Vec<_>>();
1447            data_domain(&values, true)?
1448        }
1449        Plot::Pie { .. } | Plot::Doughnut { .. } | Plot::Scatter { .. } => return Ok(None),
1450    };
1451    Ok(Some(domain))
1452}
1453
1454fn scatter_domains(plot: &Plot, blanks: DispBlanksAs) -> Result<(Domain, Domain)> {
1455    let Plot::Scatter { series, .. } = plot else {
1456        return Err(ChartError::UnexpectedElement("non-scatter plot".to_owned()));
1457    };
1458    let mut x_values = Vec::new();
1459    let mut y_values = Vec::new();
1460    for series in series {
1461        for (_, x, y) in scatter_render_points(series, blanks)? {
1462            x_values.push(x);
1463            y_values.push(y);
1464        }
1465    }
1466    Ok((
1467        data_domain(&x_values, false)?,
1468        data_domain(&y_values, false)?,
1469    ))
1470}
1471
1472fn render_axes_and_labels(
1473    plot: &Plot,
1474    axes: &[Axis],
1475    bounds: Rect,
1476    blanks: DispBlanksAs,
1477    fonts: &mut FontManager,
1478) -> Result<ChartAnnotations> {
1479    if matches!(plot, Plot::Radar { .. }) {
1480        return render_radar_axes_and_labels(plot, axes, bounds, blanks, fonts);
1481    }
1482    let mut annotations = ChartAnnotations::default();
1483    let series = plot_series(plot);
1484    let category_count = if series.is_empty() {
1485        1
1486    } else {
1487        series_category_count(series)?
1488    };
1489    for axis in axes {
1490        if axis.deleted {
1491            continue;
1492        }
1493        axis_line(&mut annotations.axis_lines, bounds, axis.position);
1494        if axis.kind == AxisKind::Value {
1495            let extent = axis_extent(plot, axis, blanks)?;
1496            let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
1497            for tick in &scale.ticks {
1498                let coordinate = axis_tick_coordinate(
1499                    *tick,
1500                    scale.domain,
1501                    bounds,
1502                    axis.position,
1503                    axis.scaling.orientation,
1504                )?;
1505                if axis.major_gridlines.is_some() {
1506                    major_gridline(
1507                        &mut annotations.gridlines,
1508                        bounds,
1509                        axis.position,
1510                        coordinate,
1511                    );
1512                }
1513                axis_tick(
1514                    &mut annotations.ticks,
1515                    bounds,
1516                    axis.position,
1517                    coordinate,
1518                    axis.major_tick_mark,
1519                );
1520                if axis.tick_label_position != TickLabelPosition::None {
1521                    let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
1522                    let origin = tick_label_origin(
1523                        bounds,
1524                        axis.position,
1525                        axis.tick_label_position,
1526                        coordinate,
1527                    );
1528                    annotations
1529                        .labels
1530                        .push(PositionedElement::Text(shape_label_with_properties(
1531                            fonts,
1532                            &text,
1533                            origin,
1534                            axis.tx_pr.as_ref(),
1535                        )?));
1536                }
1537            }
1538        } else {
1539            let count = category_count;
1540            if count > MAX_CATEGORY_ANNOTATIONS {
1541                return Err(ChartError::InvalidValue {
1542                    element: "c:catAx annotations".to_owned(),
1543                    value: format!(
1544                        "logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"
1545                    ),
1546                });
1547            }
1548            let category_labels: BTreeMap<_, _> =
1549                if axis.tick_label_position != TickLabelPosition::None {
1550                    series
1551                        .first()
1552                        .map(|series| series_categories(series, axis.number_format.as_ref()))
1553                        .transpose()?
1554                        .unwrap_or_default()
1555                        .into_iter()
1556                        .collect()
1557                } else {
1558                    BTreeMap::new()
1559                };
1560            for logical_index in 0..count {
1561                let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
1562                let fraction = (index as f64 + 0.5) / count as f64;
1563                let coordinate =
1564                    if matches!(axis.position, AxisPosition::Bottom | AxisPosition::Top) {
1565                        bounds.x + fraction * bounds.width
1566                    } else {
1567                        bounds.y + fraction * bounds.height
1568                    };
1569                if axis.major_gridlines.is_some() {
1570                    major_gridline(
1571                        &mut annotations.gridlines,
1572                        bounds,
1573                        axis.position,
1574                        coordinate,
1575                    );
1576                }
1577                axis_tick(
1578                    &mut annotations.ticks,
1579                    bounds,
1580                    axis.position,
1581                    coordinate,
1582                    axis.major_tick_mark,
1583                );
1584                if axis.tick_label_position != TickLabelPosition::None
1585                    && let Some(text) = category_labels.get(&logical_index)
1586                {
1587                    let origin = tick_label_origin(
1588                        bounds,
1589                        axis.position,
1590                        axis.tick_label_position,
1591                        coordinate,
1592                    );
1593                    annotations
1594                        .labels
1595                        .push(PositionedElement::Text(shape_label_with_properties(
1596                            fonts,
1597                            text,
1598                            origin,
1599                            axis.tx_pr.as_ref(),
1600                        )?));
1601                }
1602            }
1603        }
1604    }
1605    Ok(annotations)
1606}
1607
1608fn render_radar_axes_and_labels(
1609    plot: &Plot,
1610    axes: &[Axis],
1611    bounds: Rect,
1612    blanks: DispBlanksAs,
1613    fonts: &mut FontManager,
1614) -> Result<ChartAnnotations> {
1615    let mut annotations = ChartAnnotations::default();
1616    let series = plot_series(plot);
1617    let count = series_category_count(series)?;
1618    if count > MAX_CATEGORY_ANNOTATIONS {
1619        return Err(ChartError::InvalidValue {
1620            element: "c:radarChart annotations".to_owned(),
1621            value: format!("logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"),
1622        });
1623    }
1624    let center = Point {
1625        x: bounds.x + bounds.width / 2.0,
1626        y: bounds.y + bounds.height / 2.0,
1627    };
1628    let radius = bounds.width.min(bounds.height) / 2.0;
1629    for axis in axes {
1630        if axis.deleted {
1631            continue;
1632        }
1633        if axis.kind == AxisKind::Value {
1634            let scale = nice_number_scale(
1635                axis_extent(plot, axis, blanks)?,
1636                axis.scaling.minimum,
1637                axis.scaling.maximum,
1638                6,
1639            )?;
1640            annotations.axis_lines.push(stroked_segment(
1641                center,
1642                radial_point(center, radius, 0, count),
1643                Color::BLACK,
1644                1.0,
1645            ));
1646            for tick in &scale.ticks {
1647                let distance =
1648                    normalized_value_oriented(*tick, scale.domain, axis.scaling.orientation)?
1649                        * radius;
1650                if axis.major_gridlines.is_some() && distance > 0.0 {
1651                    let points: Vec<_> = (0..count)
1652                        .map(|index| radial_point(center, distance, index, count))
1653                        .collect();
1654                    annotations
1655                        .gridlines
1656                        .push(PositionedElement::Path(PathElement {
1657                            path: polyline_path(&points, true),
1658                            fill: None,
1659                            stroke: Some(Stroke::new(
1660                                Paint::Solid(Color {
1661                                    r: 0.82,
1662                                    g: 0.82,
1663                                    b: 0.82,
1664                                    a: 1.0,
1665                                }),
1666                                0.5,
1667                            )),
1668                        }));
1669                }
1670                radar_value_tick(
1671                    &mut annotations.ticks,
1672                    center,
1673                    distance,
1674                    axis.major_tick_mark,
1675                );
1676                let origin = match axis.tick_label_position {
1677                    TickLabelPosition::High => Some(Point {
1678                        x: center.x + radius + 4.0,
1679                        y: center.y - distance + 3.0,
1680                    }),
1681                    TickLabelPosition::Low => Some(Point {
1682                        x: center.x - radius - 30.0,
1683                        y: center.y - distance + 3.0,
1684                    }),
1685                    TickLabelPosition::NextTo => Some(Point {
1686                        x: center.x + 4.0,
1687                        y: center.y - distance + 3.0,
1688                    }),
1689                    TickLabelPosition::None => None,
1690                };
1691                if let Some(origin) = origin {
1692                    let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
1693                    annotations
1694                        .labels
1695                        .push(PositionedElement::Text(shape_label_with_properties(
1696                            fonts,
1697                            &text,
1698                            origin,
1699                            axis.tx_pr.as_ref(),
1700                        )?));
1701                }
1702            }
1703        } else {
1704            let category_labels: BTreeMap<_, _> =
1705                if axis.tick_label_position != TickLabelPosition::None {
1706                    series
1707                        .first()
1708                        .map(|series| series_categories(series, axis.number_format.as_ref()))
1709                        .transpose()?
1710                        .unwrap_or_default()
1711                        .into_iter()
1712                        .collect()
1713                } else {
1714                    BTreeMap::new()
1715                };
1716            for logical_index in 0..count {
1717                let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
1718                let end = radial_point(center, radius, index, count);
1719                annotations
1720                    .axis_lines
1721                    .push(stroked_segment(center, end, Color::BLACK, 1.0));
1722                radar_category_tick(
1723                    &mut annotations.ticks,
1724                    center,
1725                    radius,
1726                    index,
1727                    count,
1728                    axis.major_tick_mark,
1729                );
1730                if let Some(text) = category_labels.get(&logical_index) {
1731                    let Some(origin) = radar_category_label_origin(
1732                        bounds,
1733                        center,
1734                        radius,
1735                        index,
1736                        count,
1737                        axis.tick_label_position,
1738                    ) else {
1739                        continue;
1740                    };
1741                    annotations
1742                        .labels
1743                        .push(PositionedElement::Text(shape_label_with_properties(
1744                            fonts,
1745                            text,
1746                            origin,
1747                            axis.tx_pr.as_ref(),
1748                        )?));
1749                }
1750            }
1751        }
1752    }
1753    Ok(annotations)
1754}
1755
1756fn radar_category_label_origin(
1757    bounds: Rect,
1758    center: Point,
1759    radius: f64,
1760    index: usize,
1761    count: usize,
1762    position: TickLabelPosition,
1763) -> Option<Point> {
1764    let label_radius = match position {
1765        TickLabelPosition::High => radius + 18.0,
1766        TickLabelPosition::Low => (radius - 10.0).max(0.0),
1767        TickLabelPosition::NextTo => radius + 10.0,
1768        TickLabelPosition::None => return None,
1769    };
1770    let label = radial_point(center, label_radius, index, count);
1771    let mut origin = Point {
1772        x: label.x - 4.0,
1773        y: label.y + 3.0,
1774    };
1775    if position == TickLabelPosition::High {
1776        origin.x = origin.x.clamp(
1777            bounds.x - PLOT_MARGIN_LEFT,
1778            bounds.x + bounds.width + PLOT_MARGIN_RIGHT,
1779        );
1780        origin.y = origin.y.clamp(
1781            bounds.y - PLOT_MARGIN_TOP,
1782            bounds.y + bounds.height + PLOT_MARGIN_BOTTOM,
1783        );
1784    }
1785    Some(origin)
1786}
1787
1788fn radar_value_tick(
1789    elements: &mut Vec<PositionedElement>,
1790    center: Point,
1791    distance: f64,
1792    mark: TickMark,
1793) {
1794    let (left, right) = match mark {
1795        TickMark::None => return,
1796        TickMark::Inside => (4.0, 0.0),
1797        TickMark::Outside => (0.0, 4.0),
1798        TickMark::Cross => (2.0, 2.0),
1799    };
1800    let y = center.y - distance;
1801    elements.push(stroked_segment(
1802        Point {
1803            x: center.x - left,
1804            y,
1805        },
1806        Point {
1807            x: center.x + right,
1808            y,
1809        },
1810        Color::BLACK,
1811        1.0,
1812    ));
1813}
1814
1815fn radar_category_tick(
1816    elements: &mut Vec<PositionedElement>,
1817    center: Point,
1818    radius: f64,
1819    index: usize,
1820    count: usize,
1821    mark: TickMark,
1822) {
1823    let (inside, outside) = match mark {
1824        TickMark::None => return,
1825        TickMark::Inside => (4.0, 0.0),
1826        TickMark::Outside => (0.0, 4.0),
1827        TickMark::Cross => (2.0, 2.0),
1828    };
1829    elements.push(stroked_segment(
1830        radial_point(center, radius - inside, index, count),
1831        radial_point(center, radius + outside, index, count),
1832        Color::BLACK,
1833        1.0,
1834    ));
1835}
1836
1837fn axis_extent(plot: &Plot, axis: &Axis, blanks: DispBlanksAs) -> Result<Domain> {
1838    if let Plot::Scatter { axis_ids, .. } = plot {
1839        let (x, y) = scatter_domains(plot, blanks)?;
1840        return Ok(if axis.id == axis_ids[0] { x } else { y });
1841    }
1842    plot_value_domain(plot, blanks)?.ok_or_else(|| ChartError::InvalidValue {
1843        element: "chart axis".to_owned(),
1844        value: "axis-free plot supplied an axis".to_owned(),
1845    })
1846}
1847
1848fn axis_tick_coordinate(
1849    value: f64,
1850    domain: Domain,
1851    bounds: Rect,
1852    position: AxisPosition,
1853    orientation: Orientation,
1854) -> Result<f64> {
1855    if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
1856        map_x_oriented(value, domain, bounds, orientation)
1857    } else {
1858        map_y_oriented(value, domain, bounds, orientation)
1859    }
1860}
1861
1862fn axis_line(elements: &mut Vec<PositionedElement>, bounds: Rect, position: AxisPosition) {
1863    let (start, end) = match position {
1864        AxisPosition::Bottom => (
1865            Point {
1866                x: bounds.x,
1867                y: bounds.y + bounds.height,
1868            },
1869            Point {
1870                x: bounds.x + bounds.width,
1871                y: bounds.y + bounds.height,
1872            },
1873        ),
1874        AxisPosition::Top => (
1875            Point {
1876                x: bounds.x,
1877                y: bounds.y,
1878            },
1879            Point {
1880                x: bounds.x + bounds.width,
1881                y: bounds.y,
1882            },
1883        ),
1884        AxisPosition::Left => (
1885            Point {
1886                x: bounds.x,
1887                y: bounds.y,
1888            },
1889            Point {
1890                x: bounds.x,
1891                y: bounds.y + bounds.height,
1892            },
1893        ),
1894        AxisPosition::Right => (
1895            Point {
1896                x: bounds.x + bounds.width,
1897                y: bounds.y,
1898            },
1899            Point {
1900                x: bounds.x + bounds.width,
1901                y: bounds.y + bounds.height,
1902            },
1903        ),
1904    };
1905    elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
1906}
1907
1908fn major_gridline(
1909    elements: &mut Vec<PositionedElement>,
1910    bounds: Rect,
1911    position: AxisPosition,
1912    coordinate: f64,
1913) {
1914    let (start, end) = if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
1915        (
1916            Point {
1917                x: coordinate,
1918                y: bounds.y,
1919            },
1920            Point {
1921                x: coordinate,
1922                y: bounds.y + bounds.height,
1923            },
1924        )
1925    } else {
1926        (
1927            Point {
1928                x: bounds.x,
1929                y: coordinate,
1930            },
1931            Point {
1932                x: bounds.x + bounds.width,
1933                y: coordinate,
1934            },
1935        )
1936    };
1937    elements.push(stroked_segment(
1938        start,
1939        end,
1940        Color {
1941            r: 0.82,
1942            g: 0.82,
1943            b: 0.82,
1944            a: 1.0,
1945        },
1946        0.5,
1947    ));
1948}
1949
1950fn axis_tick(
1951    elements: &mut Vec<PositionedElement>,
1952    bounds: Rect,
1953    position: AxisPosition,
1954    coordinate: f64,
1955    mark: TickMark,
1956) {
1957    let (inside, outside) = match mark {
1958        TickMark::None => return,
1959        TickMark::Inside => (4.0, 0.0),
1960        TickMark::Outside => (0.0, 4.0),
1961        TickMark::Cross => (2.0, 2.0),
1962    };
1963    let (start, end) = match position {
1964        AxisPosition::Bottom => (
1965            Point {
1966                x: coordinate,
1967                y: bounds.y + bounds.height - inside,
1968            },
1969            Point {
1970                x: coordinate,
1971                y: bounds.y + bounds.height + outside,
1972            },
1973        ),
1974        AxisPosition::Top => (
1975            Point {
1976                x: coordinate,
1977                y: bounds.y + inside,
1978            },
1979            Point {
1980                x: coordinate,
1981                y: bounds.y - outside,
1982            },
1983        ),
1984        AxisPosition::Left => (
1985            Point {
1986                x: bounds.x + inside,
1987                y: coordinate,
1988            },
1989            Point {
1990                x: bounds.x - outside,
1991                y: coordinate,
1992            },
1993        ),
1994        AxisPosition::Right => (
1995            Point {
1996                x: bounds.x + bounds.width - inside,
1997                y: coordinate,
1998            },
1999            Point {
2000                x: bounds.x + bounds.width + outside,
2001                y: coordinate,
2002            },
2003        ),
2004    };
2005    elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
2006}
2007
2008fn tick_label_origin(
2009    bounds: Rect,
2010    axis_position: AxisPosition,
2011    label_position: TickLabelPosition,
2012    coordinate: f64,
2013) -> Point {
2014    let side = match label_position {
2015        TickLabelPosition::NextTo => axis_position,
2016        TickLabelPosition::High => {
2017            if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
2018                AxisPosition::Top
2019            } else {
2020                AxisPosition::Right
2021            }
2022        }
2023        TickLabelPosition::Low => {
2024            if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
2025                AxisPosition::Bottom
2026            } else {
2027                AxisPosition::Left
2028            }
2029        }
2030        TickLabelPosition::None => axis_position,
2031    };
2032    match side {
2033        AxisPosition::Bottom => Point {
2034            x: coordinate - 4.0,
2035            y: bounds.y + bounds.height + 12.0,
2036        },
2037        AxisPosition::Top => Point {
2038            x: coordinate - 4.0,
2039            y: bounds.y - 4.0,
2040        },
2041        AxisPosition::Left => Point {
2042            x: bounds.x - 30.0,
2043            y: coordinate + 3.0,
2044        },
2045        AxisPosition::Right => Point {
2046            x: bounds.x + bounds.width + 4.0,
2047            y: coordinate + 3.0,
2048        },
2049    }
2050}
2051
2052fn format_axis_value(format: Option<&NumberFormat>, value: f64) -> Result<String> {
2053    if let Some(format) = format {
2054        format.format_value(value)
2055    } else {
2056        NumberFormat::new("General".to_owned(), false)?.format_value(value)
2057    }
2058}
2059
2060fn shape_label(fonts: &mut FontManager, text: &str, origin: Point) -> Result<GlyphRun> {
2061    shape_label_with_properties(fonts, text, origin, None)
2062}
2063
2064fn shape_label_with_properties(
2065    fonts: &mut FontManager,
2066    text: &str,
2067    origin: Point,
2068    properties: Option<&CT_TextBody>,
2069) -> Result<GlyphRun> {
2070    const FAMILY: &str = "Carlito";
2071    const SIZE: f64 = 9.0;
2072    let properties = properties.and_then(chart_text_properties);
2073    let family = properties
2074        .and_then(|properties| properties.latin.as_ref())
2075        .map(|font| font.typeface.as_str())
2076        .unwrap_or(FAMILY);
2077    let size = properties
2078        .and_then(|properties| properties.font_size)
2079        .map(|size| f64::from(size) / 100.0)
2080        .unwrap_or(SIZE);
2081    let bold = properties
2082        .and_then(|properties| properties.bold)
2083        .unwrap_or(false);
2084    let italic = properties
2085        .and_then(|properties| properties.italic)
2086        .unwrap_or(false);
2087    let font_id = fonts
2088        .resolve_font_for_text(Some(family), bold, italic, text)
2089        .map_err(|error| ChartError::InvalidValue {
2090            element: "chart label font".to_owned(),
2091            value: error.to_string(),
2092        })?;
2093    let shaped =
2094        fonts
2095            .shape_text(font_id, text, size)
2096            .map_err(|error| ChartError::InvalidValue {
2097                element: "chart label shaping".to_owned(),
2098                value: error.to_string(),
2099            })?;
2100    Ok(GlyphRun {
2101        origin,
2102        font_id,
2103        font_size: size,
2104        glyph_ids: shaped.glyph_ids,
2105        advances: shaped.advances,
2106        text: text.to_owned(),
2107        source: None,
2108        color: Color::BLACK,
2109        bold,
2110        italic,
2111        field_kind: None,
2112        field_source: None,
2113        note: None,
2114        note_reference_source: None,
2115        tab_aligned: None,
2116    })
2117}
2118
2119fn chart_text_properties(body: &CT_TextBody) -> Option<&CT_TextCharacterProperties> {
2120    body.paragraphs()
2121        .first()
2122        .and_then(|paragraph| paragraph.properties.as_ref())
2123        .and_then(|properties| properties.default_run_properties.as_ref())
2124}
2125
2126fn stroked_segment(start: Point, end: Point, color: Color, width: f64) -> PositionedElement {
2127    PositionedElement::Path(PathElement {
2128        path: Path {
2129            commands: vec![PathCommand::MoveTo(start), PathCommand::LineTo(end)],
2130            fill_rule: FillRule::NonZero,
2131        },
2132        fill: None,
2133        stroke: Some(Stroke::new(Paint::Solid(color), width)),
2134    })
2135}
2136
2137fn plot_series(plot: &Plot) -> &[Series] {
2138    match plot {
2139        Plot::Bar { series, .. }
2140        | Plot::Line { series, .. }
2141        | Plot::Pie { series, .. }
2142        | Plot::Doughnut { series, .. }
2143        | Plot::Area { series, .. }
2144        | Plot::Scatter { series, .. }
2145        | Plot::Radar { series, .. } => series,
2146    }
2147}
2148
2149fn plot_data_labels(plot: &Plot) -> Option<&CT_DLbls> {
2150    match plot {
2151        Plot::Bar { data_labels, .. }
2152        | Plot::Line { data_labels, .. }
2153        | Plot::Pie { data_labels, .. }
2154        | Plot::Doughnut { data_labels, .. }
2155        | Plot::Area { data_labels, .. }
2156        | Plot::Scatter { data_labels, .. }
2157        | Plot::Radar { data_labels, .. } => data_labels.as_ref(),
2158    }
2159}
2160
2161fn series_categories(
2162    series: &Series,
2163    number_format: Option<&NumberFormat>,
2164) -> Result<Vec<(usize, String)>> {
2165    let Some(categories) = &series.categories else {
2166        return Ok(Vec::new());
2167    };
2168    match categories {
2169        AxisData::String(data) => {
2170            let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
2171            indexes
2172                .into_iter()
2173                .zip(&data.values)
2174                .map(|(index, value)| {
2175                    usize::try_from(index)
2176                        .map(|index| (index, value.clone()))
2177                        .map_err(|_| ChartError::InvalidValue {
2178                            element: "c:cat/c:pt/@idx".to_owned(),
2179                            value: index.to_string(),
2180                        })
2181                })
2182                .collect()
2183        }
2184        AxisData::Numeric(data) => {
2185            let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
2186            let cache_format;
2187            let format = if let Some(format) = number_format {
2188                format
2189            } else {
2190                cache_format = NumberFormat::new(data.format_code.clone(), false)?;
2191                &cache_format
2192            };
2193            indexes
2194                .into_iter()
2195                .zip(&data.values)
2196                .map(|(index, value)| {
2197                    let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
2198                        element: "c:cat/c:pt/@idx".to_owned(),
2199                        value: index.to_string(),
2200                    })?;
2201                    let text = format.format_value(*value)?;
2202                    Ok((index, text))
2203                })
2204                .collect()
2205        }
2206    }
2207}
2208
2209fn series_name(series: &Series, index: usize) -> String {
2210    series
2211        .name
2212        .as_ref()
2213        .and_then(|name| name.values.first())
2214        .cloned()
2215        .unwrap_or_else(|| format!("Series {}", index + 1))
2216}
2217
2218fn render_data_labels(
2219    plot: &Plot,
2220    bounds: Rect,
2221    blanks: DispBlanksAs,
2222    options: GeometryOptions,
2223    fonts: &mut FontManager,
2224    elements: &mut Vec<PositionedElement>,
2225) -> Result<()> {
2226    let plot_labels = plot_data_labels(plot);
2227    let anchors = plot_label_anchors(plot, bounds, blanks, options)?;
2228    for (series_index, series) in plot_series(plot).iter().enumerate() {
2229        let Some(labels) = series.data_labels.as_ref().or(plot_labels) else {
2230            continue;
2231        };
2232        let overrides = point_label_overrides(labels)?;
2233        let mut categories = None;
2234        let mut bubble_sizes = None;
2235        let (_, values) = logical_numeric_values(&series.values)?;
2236        let mut percentage_total = None;
2237        for (logical_index, value) in values {
2238            let point_override = overrides.get(&logical_index);
2239            if point_override.is_some_and(|point| point.deleted == Some(true)) {
2240                continue;
2241            }
2242            let effective_labels = point_override
2243                .map(|point| point.apply_to(labels))
2244                .unwrap_or_else(|| labels.clone());
2245            let labels = &effective_labels;
2246            let Some(anchor) = anchors.get(&(series_index, logical_index)).copied() else {
2247                continue;
2248            };
2249            let mut fields = Vec::new();
2250            if labels.show_series_name {
2251                fields.push(series_name(series, series_index));
2252            }
2253            if labels.show_category_name {
2254                if categories.is_none() {
2255                    categories = Some(
2256                        series_categories(series, None)?
2257                            .into_iter()
2258                            .collect::<BTreeMap<_, _>>(),
2259                    );
2260                }
2261                if let Some(category) = categories
2262                    .as_ref()
2263                    .and_then(|categories| categories.get(&logical_index))
2264                {
2265                    fields.push(category.clone());
2266                }
2267            }
2268            if labels.show_value {
2269                fields.push(format_axis_value(labels.number_format.as_ref(), value)?);
2270            }
2271            if labels.show_percent {
2272                let total = if let Some(total) = percentage_total {
2273                    total
2274                } else {
2275                    let total = series
2276                        .values
2277                        .values
2278                        .iter()
2279                        .copied()
2280                        .filter(|value| *value > 0.0)
2281                        .try_fold(0.0, |total, value| {
2282                            checked_geometry_sum(total, value, "data-label percentage total")
2283                        })?;
2284                    percentage_total = Some(total);
2285                    total
2286                };
2287                if total > 0.0 {
2288                    let text = if let Some(format) = &labels.number_format {
2289                        format.format_value(value / total)?
2290                    } else {
2291                        NumberFormat::new("0%".to_owned(), false)?.format_value(value / total)?
2292                    };
2293                    fields.push(text);
2294                }
2295            }
2296            if labels.show_bubble_size {
2297                if bubble_sizes.is_none() {
2298                    bubble_sizes = Some(
2299                        series
2300                            .bubble_size
2301                            .as_ref()
2302                            .map(logical_numeric_values)
2303                            .transpose()?
2304                            .map(|(_, values)| values.into_iter().collect::<BTreeMap<_, _>>())
2305                            .unwrap_or_default(),
2306                    );
2307                }
2308                if let Some(value) = bubble_sizes
2309                    .as_ref()
2310                    .and_then(|sizes| sizes.get(&logical_index))
2311                {
2312                    fields.push(format_axis_value(labels.number_format.as_ref(), *value)?);
2313                }
2314            }
2315            if fields.is_empty() {
2316                continue;
2317            }
2318            let separator = labels.separator.as_deref().unwrap_or(", ");
2319            let text = fields.join(separator);
2320            let origin = anchor.origin(labels.position);
2321            elements.push(PositionedElement::Text(shape_label(fonts, &text, origin)?));
2322        }
2323    }
2324    Ok(())
2325}
2326
2327#[derive(Clone, Debug, Default)]
2328struct PointLabelOverride {
2329    index: Option<usize>,
2330    deleted: Option<bool>,
2331    number_format: Option<NumberFormat>,
2332    position: Option<DataLabelPosition>,
2333    separator: Option<String>,
2334    show_legend_key: Option<bool>,
2335    show_value: Option<bool>,
2336    show_category_name: Option<bool>,
2337    show_series_name: Option<bool>,
2338    show_percent: Option<bool>,
2339    show_bubble_size: Option<bool>,
2340}
2341
2342impl PointLabelOverride {
2343    fn apply_to(&self, labels: &CT_DLbls) -> CT_DLbls {
2344        let mut effective = labels.clone();
2345        if let Some(value) = &self.number_format {
2346            effective.number_format = Some(value.clone());
2347        }
2348        if let Some(value) = self.position {
2349            effective.position = Some(value);
2350        }
2351        if let Some(value) = &self.separator {
2352            effective.separator = Some(value.clone());
2353        }
2354        if let Some(value) = self.show_legend_key {
2355            effective.show_legend_key = value;
2356        }
2357        if let Some(value) = self.show_value {
2358            effective.show_value = value;
2359        }
2360        if let Some(value) = self.show_category_name {
2361            effective.show_category_name = value;
2362        }
2363        if let Some(value) = self.show_series_name {
2364            effective.show_series_name = value;
2365        }
2366        if let Some(value) = self.show_percent {
2367            effective.show_percent = value;
2368        }
2369        if let Some(value) = self.show_bubble_size {
2370            effective.show_bubble_size = value;
2371        }
2372        effective
2373    }
2374}
2375
2376fn point_label_overrides(labels: &CT_DLbls) -> Result<BTreeMap<usize, PointLabelOverride>> {
2377    let mut namespaces = chart_namespace_defaults();
2378    for (name, namespace) in &labels.namespace_declarations {
2379        let prefix = if name == "xmlns" {
2380            Vec::new()
2381        } else if let Some(prefix) = name.strip_prefix("xmlns:") {
2382            prefix.as_bytes().to_vec()
2383        } else {
2384            continue;
2385        };
2386        upsert_namespace_binding(&mut namespaces, prefix, namespace.clone());
2387    }
2388    let mut overrides = BTreeMap::new();
2389    for raw in labels.raw_children.at(0) {
2390        if chart_root_local(raw, &namespaces)?.as_deref() != Some(b"dLbl") {
2391            continue;
2392        }
2393        let point = parse_point_label_override(raw, &namespaces)?;
2394        let index = point
2395            .index
2396            .ok_or_else(|| ChartError::MissingElement("c:dLbl/c:idx".to_owned()))?;
2397        if overrides.insert(index, point).is_some() {
2398            return Err(ChartError::DuplicateElement(format!(
2399                "c:dLbl[c:idx={index}]"
2400            )));
2401        }
2402    }
2403    Ok(overrides)
2404}
2405
2406fn parse_point_label_override(
2407    xml: &[u8],
2408    inherited: &NamespaceBindings,
2409) -> Result<PointLabelOverride> {
2410    let mut reader = Reader::from_reader(xml);
2411    let mut buffer = Vec::new();
2412    let mut namespaces = None;
2413    let mut state = PointLabelOverride::default();
2414    let mut order = 0;
2415    loop {
2416        match reader
2417            .read_event_into(&mut buffer)
2418            .map_err(OxmlError::from)?
2419        {
2420            Event::Start(element) if namespaces.is_none() => {
2421                if !matches_local_name(element.name().as_ref(), b"dLbl")
2422                    || !element_is_in_namespace(&element, C_NS, inherited)?
2423                {
2424                    return Err(ChartError::UnexpectedElement(element_name(&element)));
2425                }
2426                namespaces = Some(chart_bindings(inherited, &element)?);
2427            }
2428            Event::Start(element) => {
2429                let bindings = namespaces
2430                    .as_ref()
2431                    .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
2432                let name = chart_child_local(&element, bindings)?;
2433                let raw = capture_element(&mut reader, &element)?;
2434                parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
2435            }
2436            Event::Empty(element) => {
2437                let bindings = namespaces
2438                    .as_ref()
2439                    .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
2440                let name = chart_child_local(&element, bindings)?;
2441                let raw = capture_empty_element(&element)?;
2442                parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
2443            }
2444            Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbl") => {
2445                if state.index.is_none() {
2446                    return Err(ChartError::MissingElement("c:dLbl/c:idx".to_owned()));
2447                }
2448                return Ok(state);
2449            }
2450            Event::Eof => return Err(missing_end("c:dLbl")),
2451            _ => {}
2452        }
2453        buffer.clear();
2454    }
2455}
2456
2457fn parse_point_label_child(
2458    state: &mut PointLabelOverride,
2459    name: Option<&[u8]>,
2460    raw: &[u8],
2461    namespaces: &NamespaceBindings,
2462    order: &mut usize,
2463) -> Result<()> {
2464    let Some(name) = name else {
2465        return Ok(());
2466    };
2467    let rank = match name {
2468        b"idx" => 0,
2469        b"delete" => 1,
2470        b"layout" => 2,
2471        b"tx" => 3,
2472        b"numFmt" => 4,
2473        b"spPr" => 5,
2474        b"txPr" => 6,
2475        b"dLblPos" => 7,
2476        b"showLegendKey" => 8,
2477        b"showVal" => 9,
2478        b"showCatName" => 10,
2479        b"showSerName" => 11,
2480        b"showPercent" => 12,
2481        b"showBubbleSize" => 13,
2482        b"separator" => 14,
2483        b"showLeaderLines" => 15,
2484        b"leaderLines" => 16,
2485        b"extLst" => 17,
2486        _ => return Ok(()),
2487    };
2488    if rank < *order {
2489        return Err(ChartError::InvalidValue {
2490            element: "c:dLbl".to_owned(),
2491            value: format!("c:{} is out of schema order", String::from_utf8_lossy(name)),
2492        });
2493    }
2494    *order = rank;
2495    match name {
2496        b"idx" => {
2497            let (index, _) = parse_u32_scalar(raw, "idx")?;
2498            let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
2499                element: "c:dLbl/c:idx".to_owned(),
2500                value: index.to_string(),
2501            })?;
2502            set_once(&mut state.index, index, "c:dLbl/c:idx")?;
2503        }
2504        b"delete" => set_once(
2505            &mut state.deleted,
2506            parse_bool_value(raw, "delete")?.0,
2507            "c:dLbl/c:delete",
2508        )?,
2509        b"numFmt" => set_once(
2510            &mut state.number_format,
2511            NumberFormat::from_xml(raw)?,
2512            "c:dLbl/c:numFmt",
2513        )?,
2514        b"dLblPos" => {
2515            let (value, _) = scalar_value(raw, "dLblPos")?;
2516            let value =
2517                value.ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
2518            let position = DataLabelPosition::parse(&value)
2519                .ok_or_else(|| invalid_attribute("dLblPos", "val", value))?;
2520            set_once(&mut state.position, position, "c:dLbl/c:dLblPos")?;
2521        }
2522        b"showLegendKey" => set_once(
2523            &mut state.show_legend_key,
2524            parse_bool_value(raw, "showLegendKey")?.0,
2525            "c:dLbl/c:showLegendKey",
2526        )?,
2527        b"showVal" => set_once(
2528            &mut state.show_value,
2529            parse_bool_value(raw, "showVal")?.0,
2530            "c:dLbl/c:showVal",
2531        )?,
2532        b"showCatName" => set_once(
2533            &mut state.show_category_name,
2534            parse_bool_value(raw, "showCatName")?.0,
2535            "c:dLbl/c:showCatName",
2536        )?,
2537        b"showSerName" => set_once(
2538            &mut state.show_series_name,
2539            parse_bool_value(raw, "showSerName")?.0,
2540            "c:dLbl/c:showSerName",
2541        )?,
2542        b"showPercent" => set_once(
2543            &mut state.show_percent,
2544            parse_bool_value(raw, "showPercent")?.0,
2545            "c:dLbl/c:showPercent",
2546        )?,
2547        b"showBubbleSize" => set_once(
2548            &mut state.show_bubble_size,
2549            parse_bool_value(raw, "showBubbleSize")?.0,
2550            "c:dLbl/c:showBubbleSize",
2551        )?,
2552        b"separator" => set_once(
2553            &mut state.separator,
2554            parse_text_element(raw, b"separator", namespaces)?.0,
2555            "c:dLbl/c:separator",
2556        )?,
2557        _ => {}
2558    }
2559    Ok(())
2560}
2561
2562#[derive(Clone, Copy, Debug)]
2563struct LabelAnchor {
2564    center: Point,
2565    base: Point,
2566    end: Point,
2567    direction: Point,
2568}
2569
2570impl LabelAnchor {
2571    const fn point(center: Point) -> Self {
2572        Self {
2573            center,
2574            base: center,
2575            end: center,
2576            direction: Point { x: 0.0, y: 0.0 },
2577        }
2578    }
2579
2580    const fn segment(center: Point, base: Point, end: Point) -> Self {
2581        Self {
2582            center,
2583            base,
2584            end,
2585            direction: Point {
2586                x: end.x - base.x,
2587                y: end.y - base.y,
2588            },
2589        }
2590    }
2591
2592    const fn directed_segment(center: Point, base: Point, end: Point, direction: Point) -> Self {
2593        Self {
2594            center,
2595            base,
2596            end,
2597            direction,
2598        }
2599    }
2600
2601    fn origin(self, position: Option<DataLabelPosition>) -> Point {
2602        let mut origin = self.center;
2603        match position.unwrap_or(DataLabelPosition::BestFit) {
2604            DataLabelPosition::Bottom => origin.y += 8.0,
2605            DataLabelPosition::Top => origin.y -= 5.0,
2606            DataLabelPosition::Left => origin.x -= 8.0,
2607            DataLabelPosition::Right => origin.x += 8.0,
2608            DataLabelPosition::InsideBase => {
2609                origin = self.along_segment(self.base, 5.0, true).unwrap_or(Point {
2610                    x: origin.x,
2611                    y: origin.y + 8.0,
2612                });
2613            }
2614            DataLabelPosition::InsideEnd => {
2615                origin = self.along_segment(self.end, -5.0, true).unwrap_or(origin);
2616            }
2617            DataLabelPosition::OutsideEnd => {
2618                origin = self.along_segment(self.end, 5.0, false).unwrap_or(Point {
2619                    x: origin.x,
2620                    y: origin.y - 5.0,
2621                });
2622            }
2623            DataLabelPosition::BestFit | DataLabelPosition::Center => {}
2624        }
2625        origin
2626    }
2627
2628    fn along_segment(self, origin: Point, distance: f64, clamp_inside: bool) -> Option<Point> {
2629        let dx = self.end.x - self.base.x;
2630        let dy = self.end.y - self.base.y;
2631        let length = dx.hypot(dy);
2632        let (dx, dy, direction_length) = if length.is_finite() && length > 0.0 {
2633            (dx, dy, length)
2634        } else {
2635            let direction_length = self.direction.x.hypot(self.direction.y);
2636            if !direction_length.is_finite() || direction_length == 0.0 {
2637                return None;
2638            }
2639            (self.direction.x, self.direction.y, direction_length)
2640        };
2641        let distance = if clamp_inside {
2642            distance.clamp(-length / 2.0, length / 2.0)
2643        } else {
2644            distance
2645        };
2646        Some(Point {
2647            x: origin.x + dx / direction_length * distance,
2648            y: origin.y + dy / direction_length * distance,
2649        })
2650    }
2651}
2652
2653fn plot_label_anchors(
2654    plot: &Plot,
2655    bounds: Rect,
2656    blanks: DispBlanksAs,
2657    options: GeometryOptions,
2658) -> Result<BTreeMap<(usize, usize), LabelAnchor>> {
2659    let mut anchors = BTreeMap::new();
2660    match plot {
2661        Plot::Bar {
2662            direction,
2663            grouping,
2664            gap_width,
2665            overlap,
2666            series,
2667            ..
2668        } => {
2669            for bar in bar_rectangles(
2670                *direction, *grouping, *gap_width, *overlap, series, bounds, options,
2671            )? {
2672                if !point_in_rect(bar.end, bounds) {
2673                    continue;
2674                }
2675                let visible_base = Point {
2676                    x: bar.base.x.clamp(bounds.x, bounds.x + bounds.width),
2677                    y: bar.base.y.clamp(bounds.y, bounds.y + bounds.height),
2678                };
2679                let visible_end = Point {
2680                    x: bar.end.x.clamp(bounds.x, bounds.x + bounds.width),
2681                    y: bar.end.y.clamp(bounds.y, bounds.y + bounds.height),
2682                };
2683                anchors.insert(
2684                    (bar.series_index, bar.logical_index),
2685                    LabelAnchor::directed_segment(
2686                        Point {
2687                            x: (visible_base.x + visible_end.x) / 2.0,
2688                            y: (visible_base.y + visible_end.y) / 2.0,
2689                        },
2690                        visible_base,
2691                        visible_end,
2692                        Point {
2693                            x: bar.end.x - bar.base.x,
2694                            y: bar.end.y - bar.base.y,
2695                        },
2696                    ),
2697                );
2698            }
2699        }
2700        Plot::Line {
2701            grouping, series, ..
2702        }
2703        | Plot::Area {
2704            grouping, series, ..
2705        } => {
2706            for (series_index, points) in
2707                category_label_points(*grouping, series, bounds, blanks, options)?
2708                    .into_iter()
2709                    .enumerate()
2710            {
2711                for (logical_index, point) in points {
2712                    anchors.insert((series_index, logical_index), LabelAnchor::point(point));
2713                }
2714            }
2715        }
2716        Plot::Scatter { series, .. } => {
2717            let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
2718            let x_mapping = options.scatter_x.unwrap_or(AxisMapping {
2719                domain: x_domain,
2720                orientation: Orientation::MinMax,
2721            });
2722            let y_mapping = options.scatter_y.unwrap_or(AxisMapping {
2723                domain: y_domain,
2724                orientation: Orientation::MinMax,
2725            });
2726            for (series_index, series) in series.iter().enumerate() {
2727                let indexed = matched_scatter_points(series)?;
2728                let points = scatter_points(
2729                    &indexed,
2730                    x_mapping.domain,
2731                    y_mapping.domain,
2732                    bounds,
2733                    x_mapping.orientation,
2734                    y_mapping.orientation,
2735                )?;
2736                for ((logical_index, _, _), point) in indexed.into_iter().zip(points) {
2737                    anchors.insert((series_index, logical_index), LabelAnchor::point(point));
2738                }
2739            }
2740        }
2741        Plot::Radar { series, .. } => {
2742            let center = Point {
2743                x: bounds.x + bounds.width / 2.0,
2744                y: bounds.y + bounds.height / 2.0,
2745            };
2746            let category_count = series_category_count(series)?;
2747            for (series_index, points) in radar_series_points(series, bounds, options)?
2748                .into_iter()
2749                .enumerate()
2750            {
2751                for (logical_index, point) in points {
2752                    let spoke = radial_point(
2753                        center,
2754                        1.0,
2755                        oriented_category_index(
2756                            logical_index,
2757                            category_count,
2758                            options.category_orientation,
2759                        ),
2760                        category_count,
2761                    );
2762                    anchors.insert(
2763                        (series_index, logical_index),
2764                        LabelAnchor::directed_segment(
2765                            point,
2766                            center,
2767                            point,
2768                            Point {
2769                                x: spoke.x - center.x,
2770                                y: spoke.y - center.y,
2771                            },
2772                        ),
2773                    );
2774                }
2775            }
2776        }
2777        Plot::Pie {
2778            first_slice_angle,
2779            series,
2780            ..
2781        } => {
2782            for slice in pie_slices(*first_slice_angle, None, series, bounds)? {
2783                anchors.insert(
2784                    (slice.series_index, slice.logical_index),
2785                    LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2786                );
2787            }
2788        }
2789        Plot::Doughnut {
2790            first_slice_angle,
2791            hole_size,
2792            series,
2793            ..
2794        } => {
2795            for slice in pie_slices(*first_slice_angle, Some(*hole_size), series, bounds)? {
2796                anchors.insert(
2797                    (slice.series_index, slice.logical_index),
2798                    LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2799                );
2800            }
2801        }
2802    }
2803    anchors.retain(|_, anchor| point_in_rect(anchor.end, bounds));
2804    Ok(anchors)
2805}
2806
2807fn category_label_points(
2808    grouping: Grouping,
2809    series: &[Series],
2810    bounds: Rect,
2811    blanks: DispBlanksAs,
2812    options: GeometryOptions,
2813) -> Result<Vec<Vec<(usize, Point)>>> {
2814    validate_series_values(series)?;
2815    let logical_series = logical_series_values(series)?;
2816    let category_count = series_category_count(series)?;
2817    let calculated_series = if blanks == DispBlanksAs::Zero {
2818        zero_filled_series(&logical_series, category_count)
2819    } else {
2820        logical_series.clone()
2821    };
2822    let layers = stacked_series_bounds(
2823        &calculated_series,
2824        grouping != Grouping::Standard,
2825        grouping == Grouping::PercentStacked,
2826    )?;
2827    let domain = options
2828        .value
2829        .map(|mapping| mapping.domain)
2830        .unwrap_or(domain_from_layers(&layers)?);
2831    let value_orientation = options
2832        .value
2833        .map(|mapping| mapping.orientation)
2834        .unwrap_or(Orientation::MinMax);
2835    let mut point_series = Vec::with_capacity(layers.len());
2836    for (series_index, layer) in layers.iter().enumerate() {
2837        let present: BTreeSet<_> = logical_series[series_index]
2838            .1
2839            .iter()
2840            .map(|(index, _)| *index)
2841            .collect();
2842        let indexed: Vec<_> = layer
2843            .iter()
2844            .filter(|(index, _, _)| present.contains(index))
2845            .map(|(index, _, upper)| (*index, *upper))
2846            .collect();
2847        let points = category_points(
2848            &indexed,
2849            category_count,
2850            domain,
2851            bounds,
2852            options.category_orientation,
2853            value_orientation,
2854        )?;
2855        point_series.push(
2856            indexed
2857                .into_iter()
2858                .map(|(index, _)| index)
2859                .zip(points)
2860                .collect(),
2861        );
2862    }
2863    Ok(point_series)
2864}
2865
2866fn point_in_rect(point: Point, bounds: Rect) -> bool {
2867    const EPSILON: f64 = 1.0e-9;
2868    point.x >= bounds.x - EPSILON
2869        && point.x <= bounds.x + bounds.width + EPSILON
2870        && point.y >= bounds.y - EPSILON
2871        && point.y <= bounds.y + bounds.height + EPSILON
2872}
2873
2874fn resolve_series_colours(
2875    plot: &Plot,
2876    theme: &CT_OfficeStyleSheet,
2877    color_map: &ColorMap,
2878) -> Result<Vec<Color>> {
2879    let lookup = theme_colour_lookup(theme);
2880    plot_series(plot)
2881        .iter()
2882        .enumerate()
2883        .map(|(index, series)| {
2884            if let Some(properties) = series.sp_pr.as_ref() {
2885                if let Some(fill) = properties.fill.as_ref() {
2886                    return resolve_series_paint(fill, index, "fill", theme, color_map, &lookup);
2887                }
2888                if let Some(fill) = properties.line.as_ref().and_then(|line| line.fill.as_ref()) {
2889                    return resolve_series_paint(fill, index, "line", theme, color_map, &lookup);
2890                }
2891            }
2892            resolve_series_accent(index, theme, color_map, &lookup)
2893        })
2894        .collect()
2895}
2896
2897fn resolve_series_paint(
2898    fill: &Fill,
2899    series_index: usize,
2900    source: &str,
2901    theme: &CT_OfficeStyleSheet,
2902    color_map: &ColorMap,
2903    lookup: &[(&str, RgbColor)],
2904) -> Result<Color> {
2905    match fill {
2906        Fill::NoFill(_) => Ok(Color {
2907            r: 0.0,
2908            g: 0.0,
2909            b: 0.0,
2910            a: 0.0,
2911        }),
2912        Fill::Solid(solid) => {
2913            let choice = solid
2914                .color
2915                .as_ref()
2916                .ok_or_else(|| ChartError::InvalidValue {
2917                    element: format!("c:ser[{series_index}] direct {source} paint"),
2918                    value: "solid fill has no colour choice".to_owned(),
2919                })?;
2920            resolve_direct_chart_colour(choice, theme, color_map, lookup, || {
2921                format!("c:ser[{series_index}] direct {source} paint")
2922            })
2923        }
2924        Fill::Gradient(_) => unsupported_series_paint(series_index, source, "gradient"),
2925        Fill::Pattern(_) => unsupported_series_paint(series_index, source, "pattern"),
2926        Fill::Blip(_) => unsupported_series_paint(series_index, source, "picture"),
2927    }
2928}
2929
2930fn resolve_direct_chart_colour(
2931    choice: &ColorChoice,
2932    theme: &CT_OfficeStyleSheet,
2933    color_map: &ColorMap,
2934    lookup: &[(&str, RgbColor)],
2935    element: impl FnOnce() -> String,
2936) -> Result<Color> {
2937    let ColorChoice::Scheme {
2938        value, transforms, ..
2939    } = choice
2940    else {
2941        return resolve_chart_colour(choice, color_map, lookup, element);
2942    };
2943    let theme_slot =
2944        mapped_theme_slot(value, color_map).ok_or_else(|| ChartError::InvalidValue {
2945            element: element(),
2946            value: format!("no concrete theme slot is available for scheme colour {value}"),
2947        })?;
2948    let theme_choice = theme.theme_elements.color_scheme.color(theme_slot);
2949    let base = concrete_theme_colour(theme_choice).ok_or_else(|| ChartError::InvalidValue {
2950        element: format!("a:clrScheme/a:{}", theme_slot.as_str()),
2951        value: "selected theme slot has no concrete sRGB or system fallback colour".to_owned(),
2952    })?;
2953    let mut combined = theme_choice.transforms().to_vec();
2954    combined.extend_from_slice(transforms);
2955    Ok(resolved_layout_colour(apply_color_transforms(
2956        base, &combined,
2957    )))
2958}
2959
2960fn mapped_theme_slot(value: &str, color_map: &ColorMap) -> Option<ThemeColorSlot> {
2961    let mapped = match value {
2962        "bg1" => color_map.theme_slot(ColorMapSlot::Background1),
2963        "tx1" => color_map.theme_slot(ColorMapSlot::Text1),
2964        "bg2" => color_map.theme_slot(ColorMapSlot::Background2),
2965        "tx2" => color_map.theme_slot(ColorMapSlot::Text2),
2966        "accent1" => color_map.theme_slot(ColorMapSlot::Accent1),
2967        "accent2" => color_map.theme_slot(ColorMapSlot::Accent2),
2968        "accent3" => color_map.theme_slot(ColorMapSlot::Accent3),
2969        "accent4" => color_map.theme_slot(ColorMapSlot::Accent4),
2970        "accent5" => color_map.theme_slot(ColorMapSlot::Accent5),
2971        "accent6" => color_map.theme_slot(ColorMapSlot::Accent6),
2972        "hlink" => color_map.theme_slot(ColorMapSlot::Hyperlink),
2973        "folHlink" => color_map.theme_slot(ColorMapSlot::FollowedHyperlink),
2974        "dk1" => ThemeColorSlot::Dark1,
2975        "lt1" => ThemeColorSlot::Light1,
2976        "dk2" => ThemeColorSlot::Dark2,
2977        "lt2" => ThemeColorSlot::Light2,
2978        _ => return None,
2979    };
2980    Some(mapped)
2981}
2982
2983fn unsupported_series_paint(series_index: usize, source: &str, kind: &str) -> Result<Color> {
2984    Err(ChartError::InvalidValue {
2985        element: format!("c:ser[{series_index}] direct {source} paint"),
2986        value: format!("unsupported {kind} paint"),
2987    })
2988}
2989
2990fn resolve_series_accent(
2991    series_index: usize,
2992    theme: &CT_OfficeStyleSheet,
2993    color_map: &ColorMap,
2994    lookup: &[(&str, RgbColor)],
2995) -> Result<Color> {
2996    const ACCENTS: [ColorMapSlot; 6] = [
2997        ColorMapSlot::Accent1,
2998        ColorMapSlot::Accent2,
2999        ColorMapSlot::Accent3,
3000        ColorMapSlot::Accent4,
3001        ColorMapSlot::Accent5,
3002        ColorMapSlot::Accent6,
3003    ];
3004    let semantic_slot = ACCENTS[series_index % ACCENTS.len()];
3005    let theme_slot = color_map.theme_slot(semantic_slot);
3006    let choice = theme.theme_elements.color_scheme.color(theme_slot);
3007    resolve_chart_colour(choice, color_map, lookup, || {
3008        format!("c:ser[{series_index}] theme accent")
3009    })
3010}
3011
3012fn resolve_chart_colour(
3013    choice: &ColorChoice,
3014    color_map: &ColorMap,
3015    lookup: &[(&str, RgbColor)],
3016    element: impl FnOnce() -> String,
3017) -> Result<Color> {
3018    resolve_color(choice, color_map, lookup)
3019        .map(resolved_layout_colour)
3020        .map_err(|error| ChartError::InvalidValue {
3021            element: element(),
3022            value: error.to_string(),
3023        })
3024}
3025
3026fn theme_colour_lookup(theme: &CT_OfficeStyleSheet) -> Vec<(&'static str, RgbColor)> {
3027    theme
3028        .theme_elements
3029        .color_scheme
3030        .iter()
3031        .filter_map(|(slot, choice)| {
3032            concrete_theme_colour(choice).map(|colour| (slot.as_str(), colour))
3033        })
3034        .collect()
3035}
3036
3037fn concrete_theme_colour(choice: &ColorChoice) -> Option<RgbColor> {
3038    match choice {
3039        ColorChoice::Srgb { value, .. } => Some(*value),
3040        ColorChoice::System { last_color, .. } => *last_color,
3041        ColorChoice::Scheme { .. } | ColorChoice::Preset { .. } => None,
3042    }
3043}
3044
3045fn resolved_layout_colour(colour: ResolvedColor) -> Color {
3046    Color {
3047        r: f64::from(colour.red) / 255.0,
3048        g: f64::from(colour.green) / 255.0,
3049        b: f64::from(colour.blue) / 255.0,
3050        a: f64::from(colour.alpha) / 255.0,
3051    }
3052}
3053
3054fn render_legend(
3055    chart: &CT_Chart,
3056    plot: &Plot,
3057    bounds: Rect,
3058    fonts: &mut FontManager,
3059    series_colours: &[Color],
3060    paths: &mut Vec<PositionedElement>,
3061    labels: &mut Vec<PositionedElement>,
3062) -> Result<()> {
3063    if chart.legend.is_none() {
3064        return Ok(());
3065    }
3066    let x = bounds.x + bounds.width - 58.0;
3067    for (index, (series, colour)) in plot_series(plot).iter().zip(series_colours).enumerate() {
3068        let y = bounds.y + 6.0 + index as f64 * 13.0;
3069        paths.push(filled_path(
3070            Path::rect(Rect {
3071                x,
3072                y,
3073                width: 7.0,
3074                height: 7.0,
3075            }),
3076            *colour,
3077        ));
3078        labels.push(PositionedElement::Text(shape_label(
3079            fonts,
3080            &series_name(series, index),
3081            Point {
3082                x: x + 10.0,
3083                y: y + 7.0,
3084            },
3085        )?));
3086    }
3087    Ok(())
3088}
3089
3090fn validate_geometry_coordinates(elements: &[PositionedElement]) -> Result<()> {
3091    fn finite_point(point: Point) -> bool {
3092        point.x.is_finite() && point.y.is_finite()
3093    }
3094
3095    for element in elements {
3096        match element {
3097            PositionedElement::Path(path) => {
3098                for command in &path.path.commands {
3099                    let finite = match command {
3100                        PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
3101                            finite_point(*point)
3102                        }
3103                        PathCommand::CurveTo { c1, c2, to } => {
3104                            finite_point(*c1) && finite_point(*c2) && finite_point(*to)
3105                        }
3106                        PathCommand::Close => true,
3107                    };
3108                    if !finite {
3109                        return Err(ChartError::InvalidValue {
3110                            element: "chart geometry coordinate".to_owned(),
3111                            value: "generated path contains a nonfinite point".to_owned(),
3112                        });
3113                    }
3114                }
3115            }
3116            PositionedElement::Group(group) => validate_geometry_coordinates(&group.children)?,
3117            _ => {}
3118        }
3119    }
3120    Ok(())
3121}
3122
3123fn render_plot_geometry(
3124    plot: &Plot,
3125    bounds: Rect,
3126    blanks: DispBlanksAs,
3127    options: GeometryOptions,
3128    series_colours: &[Color],
3129) -> Result<Vec<PositionedElement>> {
3130    match plot {
3131        Plot::Bar {
3132            direction,
3133            grouping,
3134            gap_width,
3135            overlap,
3136            series,
3137            ..
3138        } => render_bar_geometry(
3139            *direction,
3140            *grouping,
3141            *gap_width,
3142            *overlap,
3143            series,
3144            bounds,
3145            options,
3146            series_colours,
3147        ),
3148        Plot::Line {
3149            grouping,
3150            marker,
3151            series,
3152            ..
3153        } => render_line_geometry(
3154            *grouping,
3155            *marker,
3156            series,
3157            bounds,
3158            blanks,
3159            options,
3160            series_colours,
3161        ),
3162        Plot::Pie {
3163            first_slice_angle,
3164            series,
3165            ..
3166        } => render_pie_geometry(*first_slice_angle, None, series, bounds, series_colours),
3167        Plot::Doughnut {
3168            first_slice_angle,
3169            hole_size,
3170            series,
3171            ..
3172        } => render_pie_geometry(
3173            *first_slice_angle,
3174            Some(*hole_size),
3175            series,
3176            bounds,
3177            series_colours,
3178        ),
3179        Plot::Area {
3180            grouping, series, ..
3181        } => render_area_geometry(*grouping, series, bounds, blanks, options, series_colours),
3182        Plot::Scatter { style, series, .. } => {
3183            render_scatter_geometry(*style, series, bounds, blanks, options, series_colours)
3184        }
3185        Plot::Radar { style, series, .. } => {
3186            render_radar_geometry(*style, series, bounds, options, series_colours)
3187        }
3188    }
3189}
3190
3191#[allow(clippy::too_many_arguments)]
3192fn render_bar_geometry(
3193    direction: BarDirection,
3194    grouping: BarGrouping,
3195    gap_width: u16,
3196    overlap: i8,
3197    series: &[Series],
3198    bounds: Rect,
3199    options: GeometryOptions,
3200    series_colours: &[Color],
3201) -> Result<Vec<PositionedElement>> {
3202    let bars = bar_rectangles(
3203        direction, grouping, gap_width, overlap, series, bounds, options,
3204    )?;
3205    bars.into_iter()
3206        .map(|bar| {
3207            Ok(filled_path(
3208                Path::rect(bar.rect),
3209                series_colour(series_colours, bar.series_index)?,
3210            ))
3211        })
3212        .collect()
3213}
3214
3215#[derive(Clone, Copy, Debug)]
3216struct BarRectangle {
3217    series_index: usize,
3218    logical_index: usize,
3219    rect: Rect,
3220    base: Point,
3221    end: Point,
3222}
3223
3224fn bar_rectangles(
3225    direction: BarDirection,
3226    grouping: BarGrouping,
3227    gap_width: u16,
3228    overlap: i8,
3229    series: &[Series],
3230    bounds: Rect,
3231    options: GeometryOptions,
3232) -> Result<Vec<BarRectangle>> {
3233    validate_series_values(series)?;
3234    let logical_series = logical_series_values(series)?;
3235    let category_count = series_category_count(series)?;
3236    let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
3237    let percent = grouping == BarGrouping::PercentStacked;
3238    let layers = stacked_series_bounds(&logical_series, stacked, percent)?;
3239    let domain = options
3240        .value
3241        .map(|mapping| mapping.domain)
3242        .unwrap_or(domain_from_layers(&layers)?);
3243    let value_orientation = options
3244        .value
3245        .map(|mapping| mapping.orientation)
3246        .unwrap_or(Orientation::MinMax);
3247    let category_extent = if direction == BarDirection::Column {
3248        bounds.width
3249    } else {
3250        bounds.height
3251    };
3252    let slot = category_extent / category_count as f64;
3253    let cluster_extent = slot * 100.0 / (100.0 + f64::from(gap_width));
3254    let bars_per_cluster = if stacked { 1 } else { series.len() };
3255    let overlap_fraction = if stacked {
3256        0.0
3257    } else {
3258        f64::from(overlap) / 100.0
3259    };
3260    let divisor =
3261        bars_per_cluster as f64 - (bars_per_cluster.saturating_sub(1)) as f64 * overlap_fraction;
3262    let bar_extent = cluster_extent / divisor;
3263    let advance = bar_extent * (1.0 - overlap_fraction);
3264    let mut bars = Vec::new();
3265    for (series_index, layer) in layers.iter().enumerate() {
3266        for &(logical_index, start, end) in layer {
3267            let category = oriented_category_index(
3268                logical_index,
3269                category_count,
3270                options.category_orientation,
3271            );
3272            let cluster_start = category as f64 / category_count as f64 * category_extent
3273                + (slot - cluster_extent) / 2.0;
3274            let category_start = if stacked {
3275                cluster_start
3276            } else {
3277                cluster_start + series_index as f64 * advance
3278            };
3279            let (rect, base, end) = if direction == BarDirection::Column {
3280                let y0 = map_y_oriented(start, domain, bounds, value_orientation)?;
3281                let y1 = map_y_oriented(end, domain, bounds, value_orientation)?;
3282                let center_x = bounds.x + category_start + bar_extent / 2.0;
3283                (
3284                    Rect {
3285                        x: bounds.x + category_start,
3286                        y: y0.min(y1),
3287                        width: bar_extent,
3288                        height: (y1 - y0).abs(),
3289                    },
3290                    Point { x: center_x, y: y0 },
3291                    Point { x: center_x, y: y1 },
3292                )
3293            } else {
3294                let x0 = map_x_oriented(start, domain, bounds, value_orientation)?;
3295                let x1 = map_x_oriented(end, domain, bounds, value_orientation)?;
3296                let center_y = bounds.y + category_start + bar_extent / 2.0;
3297                (
3298                    Rect {
3299                        x: x0.min(x1),
3300                        y: bounds.y + category_start,
3301                        width: (x1 - x0).abs(),
3302                        height: bar_extent,
3303                    },
3304                    Point { x: x0, y: center_y },
3305                    Point { x: x1, y: center_y },
3306                )
3307            };
3308            bars.push(BarRectangle {
3309                series_index,
3310                logical_index,
3311                rect,
3312                base,
3313                end,
3314            });
3315        }
3316    }
3317    Ok(bars)
3318}
3319
3320fn render_line_geometry(
3321    grouping: Grouping,
3322    marker: bool,
3323    series: &[Series],
3324    bounds: Rect,
3325    blanks: DispBlanksAs,
3326    options: GeometryOptions,
3327    series_colours: &[Color],
3328) -> Result<Vec<PositionedElement>> {
3329    validate_series_values(series)?;
3330    let logical_series = logical_series_values(series)?;
3331    let category_count = series_category_count(series)?;
3332    let calculated_series = if blanks == DispBlanksAs::Zero {
3333        zero_filled_series(&logical_series, category_count)
3334    } else {
3335        logical_series.clone()
3336    };
3337    let stacked = grouping != Grouping::Standard;
3338    let percent = grouping == Grouping::PercentStacked;
3339    let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
3340    let domain = options
3341        .value
3342        .map(|mapping| mapping.domain)
3343        .unwrap_or(domain_from_layers(&layers)?);
3344    let value_orientation = options
3345        .value
3346        .map(|mapping| mapping.orientation)
3347        .unwrap_or(Orientation::MinMax);
3348    let mut elements = Vec::new();
3349    for (series_index, layer) in layers.iter().enumerate() {
3350        let colour = series_colour(series_colours, series_index)?;
3351        let indexed: Vec<_> = layer
3352            .iter()
3353            .map(|(index, _, upper)| (*index, *upper))
3354            .collect();
3355        let indexes: Vec<_> = indexed.iter().map(|(index, _)| *index).collect();
3356        for (start, end) in contiguous_ranges(&indexes, blanks) {
3357            let points = category_points(
3358                &indexed[start..end],
3359                category_count,
3360                domain,
3361                bounds,
3362                options.category_orientation,
3363                value_orientation,
3364            )?;
3365            elements.push(stroked_path(polyline_path(&points, false), colour));
3366        }
3367        if marker {
3368            let present: BTreeSet<_> = logical_series[series_index]
3369                .1
3370                .iter()
3371                .map(|(index, _)| *index)
3372                .collect();
3373            let marker_values: Vec<_> = indexed
3374                .iter()
3375                .copied()
3376                .filter(|(index, _)| present.contains(index))
3377                .collect();
3378            let marker_points = category_points(
3379                &marker_values,
3380                category_count,
3381                domain,
3382                bounds,
3383                options.category_orientation,
3384                value_orientation,
3385            )?;
3386            push_markers(&mut elements, &marker_points, colour);
3387        }
3388    }
3389    Ok(elements)
3390}
3391
3392fn render_area_geometry(
3393    grouping: Grouping,
3394    series: &[Series],
3395    bounds: Rect,
3396    blanks: DispBlanksAs,
3397    options: GeometryOptions,
3398    series_colours: &[Color],
3399) -> Result<Vec<PositionedElement>> {
3400    validate_series_values(series)?;
3401    let logical_series = logical_series_values(series)?;
3402    let category_count = series_category_count(series)?;
3403    let calculated_series = if blanks == DispBlanksAs::Zero {
3404        zero_filled_series(&logical_series, category_count)
3405    } else {
3406        logical_series
3407    };
3408    let stacked = grouping != Grouping::Standard;
3409    let percent = grouping == Grouping::PercentStacked;
3410    let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
3411    let domain = options
3412        .value
3413        .map(|mapping| mapping.domain)
3414        .unwrap_or(domain_from_layers(&layers)?);
3415    let value_orientation = options
3416        .value
3417        .map(|mapping| mapping.orientation)
3418        .unwrap_or(Orientation::MinMax);
3419    let mut elements = Vec::new();
3420    for (series_index, layer) in layers.iter().enumerate() {
3421        let colour = series_colour(series_colours, series_index)?;
3422        let indexes: Vec<_> = layer.iter().map(|(index, _, _)| *index).collect();
3423        for (start, end) in contiguous_ranges(&indexes, blanks) {
3424            let segment = &layer[start..end];
3425            let top_values: Vec<_> = segment
3426                .iter()
3427                .map(|(index, _, upper)| (*index, *upper))
3428                .collect();
3429            let lower_values: Vec<_> = segment
3430                .iter()
3431                .map(|(index, lower, _)| (*index, *lower))
3432                .collect();
3433            let top = category_points(
3434                &top_values,
3435                category_count,
3436                domain,
3437                bounds,
3438                options.category_orientation,
3439                value_orientation,
3440            )?;
3441            let mut bottom = category_points(
3442                &lower_values,
3443                category_count,
3444                domain,
3445                bounds,
3446                options.category_orientation,
3447                value_orientation,
3448            )?;
3449            bottom.reverse();
3450            let mut commands = Vec::with_capacity(top.len() + bottom.len() + 2);
3451            commands.push(PathCommand::MoveTo(top[0]));
3452            commands.extend(top.iter().skip(1).copied().map(PathCommand::LineTo));
3453            commands.extend(bottom.into_iter().map(PathCommand::LineTo));
3454            commands.push(PathCommand::Close);
3455            elements.push(filled_and_stroked_path(
3456                Path {
3457                    commands,
3458                    fill_rule: FillRule::NonZero,
3459                },
3460                colour,
3461            ));
3462        }
3463    }
3464    Ok(elements)
3465}
3466
3467fn render_scatter_geometry(
3468    style: ScatterStyle,
3469    series: &[Series],
3470    bounds: Rect,
3471    blanks: DispBlanksAs,
3472    options: GeometryOptions,
3473    series_colours: &[Color],
3474) -> Result<Vec<PositionedElement>> {
3475    validate_series_values(series)?;
3476    let mut paired_series = Vec::with_capacity(series.len());
3477    let mut marker_series = Vec::with_capacity(series.len());
3478    let mut all_x = Vec::new();
3479    let mut all_y = Vec::new();
3480    for item in series {
3481        let markers = matched_scatter_points(item)?;
3482        let paired = scatter_render_points(item, blanks)?;
3483        all_x.extend(paired.iter().map(|(_, x, _)| *x));
3484        all_y.extend(paired.iter().map(|(_, _, y)| *y));
3485        paired_series.push(paired);
3486        marker_series.push(markers);
3487    }
3488    let x_domain = options
3489        .scatter_x
3490        .map(|mapping| mapping.domain)
3491        .unwrap_or(data_domain(&all_x, false)?);
3492    let y_domain = options
3493        .scatter_y
3494        .map(|mapping| mapping.domain)
3495        .unwrap_or(data_domain(&all_y, false)?);
3496    let x_orientation = options
3497        .scatter_x
3498        .map(|mapping| mapping.orientation)
3499        .unwrap_or(Orientation::MinMax);
3500    let y_orientation = options
3501        .scatter_y
3502        .map(|mapping| mapping.orientation)
3503        .unwrap_or(Orientation::MinMax);
3504    let draw_line = matches!(
3505        style,
3506        ScatterStyle::Line
3507            | ScatterStyle::LineMarker
3508            | ScatterStyle::Smooth
3509            | ScatterStyle::SmoothMarker
3510    );
3511    let draw_marker = matches!(
3512        style,
3513        ScatterStyle::Marker | ScatterStyle::LineMarker | ScatterStyle::SmoothMarker
3514    );
3515    let mut elements = Vec::new();
3516    for (series_index, paired) in paired_series.iter().enumerate() {
3517        let colour = series_colour(series_colours, series_index)?;
3518        if draw_line {
3519            let indexes: Vec<_> = paired.iter().map(|(index, _, _)| *index).collect();
3520            for (start, end) in contiguous_ranges(&indexes, blanks) {
3521                let points = scatter_points(
3522                    &paired[start..end],
3523                    x_domain,
3524                    y_domain,
3525                    bounds,
3526                    x_orientation,
3527                    y_orientation,
3528                )?;
3529                elements.push(stroked_path(polyline_path(&points, false), colour));
3530            }
3531        }
3532        if draw_marker {
3533            let marker_points = scatter_points(
3534                &marker_series[series_index],
3535                x_domain,
3536                y_domain,
3537                bounds,
3538                x_orientation,
3539                y_orientation,
3540            )?;
3541            push_markers(&mut elements, &marker_points, colour);
3542        }
3543    }
3544    Ok(elements)
3545}
3546
3547type ScatterPoint = (usize, f64, f64);
3548
3549struct ScatterPointMaps {
3550    x_count: usize,
3551    y_count: usize,
3552    x_by_index: BTreeMap<usize, f64>,
3553    y_by_index: BTreeMap<usize, f64>,
3554}
3555
3556fn scatter_point_maps(series: &Series) -> Result<ScatterPointMaps> {
3557    let Some(AxisData::Numeric(x_values)) = &series.categories else {
3558        return Err(ChartError::InvalidValue {
3559            element: "c:xVal".to_owned(),
3560            value: "scatter series requires numeric cached x values".to_owned(),
3561        });
3562    };
3563    validate_finite(&x_values.values, "c:xVal/c:numCache")?;
3564    let (x_count, x_points) = logical_numeric_values(x_values)?;
3565    let (y_count, y_points) = logical_numeric_values(&series.values)?;
3566    Ok(ScatterPointMaps {
3567        x_count,
3568        y_count,
3569        x_by_index: x_points.into_iter().collect(),
3570        y_by_index: y_points.into_iter().collect(),
3571    })
3572}
3573
3574fn matched_scatter_points(series: &Series) -> Result<Vec<ScatterPoint>> {
3575    let points = scatter_point_maps(series)?;
3576    Ok(points
3577        .y_by_index
3578        .iter()
3579        .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
3580        .collect())
3581}
3582
3583fn scatter_render_points(series: &Series, blanks: DispBlanksAs) -> Result<Vec<ScatterPoint>> {
3584    let points = scatter_point_maps(series)?;
3585    if blanks != DispBlanksAs::Zero {
3586        return Ok(points
3587            .y_by_index
3588            .iter()
3589            .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
3590            .collect());
3591    }
3592    let count = points.x_count.max(points.y_count);
3593    let present = points
3594        .x_by_index
3595        .keys()
3596        .chain(points.y_by_index.keys())
3597        .copied()
3598        .collect();
3599    Ok(zero_control_indexes(count, &present)
3600        .into_iter()
3601        .map(|index| {
3602            (
3603                index,
3604                points.x_by_index.get(&index).copied().unwrap_or(0.0),
3605                points.y_by_index.get(&index).copied().unwrap_or(0.0),
3606            )
3607        })
3608        .collect())
3609}
3610
3611fn scatter_points(
3612    values: &[(usize, f64, f64)],
3613    x_domain: Domain,
3614    y_domain: Domain,
3615    bounds: Rect,
3616    x_orientation: Orientation,
3617    y_orientation: Orientation,
3618) -> Result<Vec<Point>> {
3619    values
3620        .iter()
3621        .map(|(_, x, y)| {
3622            Ok(Point {
3623                x: map_x_oriented(*x, x_domain, bounds, x_orientation)?,
3624                y: map_y_oriented(*y, y_domain, bounds, y_orientation)?,
3625            })
3626        })
3627        .collect()
3628}
3629
3630fn render_radar_geometry(
3631    style: RadarStyle,
3632    series: &[Series],
3633    bounds: Rect,
3634    options: GeometryOptions,
3635    series_colours: &[Color],
3636) -> Result<Vec<PositionedElement>> {
3637    let point_series = radar_series_points(series, bounds, options)?;
3638    let mut elements = Vec::new();
3639    for (series_index, points) in point_series.iter().enumerate() {
3640        let colour = series_colour(series_colours, series_index)?;
3641        let plain_points: Vec<_> = points.iter().map(|(_, point)| *point).collect();
3642        if plain_points.is_empty() {
3643            continue;
3644        }
3645        let path = polyline_path(&plain_points, true);
3646        if style == RadarStyle::Filled {
3647            elements.push(filled_and_stroked_path(path, colour));
3648        } else {
3649            elements.push(stroked_path(path, colour));
3650        }
3651        if style == RadarStyle::Marker {
3652            push_markers(&mut elements, &plain_points, colour);
3653        }
3654    }
3655    Ok(elements)
3656}
3657
3658fn radar_series_points(
3659    series: &[Series],
3660    bounds: Rect,
3661    options: GeometryOptions,
3662) -> Result<Vec<Vec<(usize, Point)>>> {
3663    validate_series_values(series)?;
3664    let logical_series = logical_series_values(series)?;
3665    let category_count = series_category_count(series)?;
3666    let values = logical_series
3667        .iter()
3668        .flat_map(|(_, points)| points.iter().map(|(_, value)| *value))
3669        .collect::<Vec<_>>();
3670    let mapping = options.value.unwrap_or(AxisMapping {
3671        domain: data_domain(&values, true)?,
3672        orientation: Orientation::MinMax,
3673    });
3674    let center = Point {
3675        x: bounds.x + bounds.width / 2.0,
3676        y: bounds.y + bounds.height / 2.0,
3677    };
3678    let radius = bounds.width.min(bounds.height) / 2.0;
3679    let mut point_series = Vec::with_capacity(logical_series.len());
3680    for (_, logical) in &logical_series {
3681        let mut points = Vec::with_capacity(logical.len());
3682        for (category, value) in logical {
3683            if *value < mapping.domain.min || *value > mapping.domain.max {
3684                continue;
3685            }
3686            points.push((
3687                *category,
3688                radial_point(
3689                    center,
3690                    normalized_value_oriented(*value, mapping.domain, mapping.orientation)?
3691                        * radius,
3692                    oriented_category_index(
3693                        *category,
3694                        category_count,
3695                        options.category_orientation,
3696                    ),
3697                    category_count,
3698                ),
3699            ));
3700        }
3701        point_series.push(points);
3702    }
3703    Ok(point_series)
3704}
3705
3706fn render_pie_geometry(
3707    first_slice_angle: u16,
3708    hole_size: Option<u8>,
3709    series: &[Series],
3710    bounds: Rect,
3711    series_colours: &[Color],
3712) -> Result<Vec<PositionedElement>> {
3713    pie_slices(first_slice_angle, hole_size, series, bounds)?
3714        .into_iter()
3715        .map(|slice| {
3716            Ok(filled_path(
3717                slice.path,
3718                series_colour(series_colours, slice.series_index)?,
3719            ))
3720        })
3721        .collect()
3722}
3723
3724struct PieSliceGeometry {
3725    series_index: usize,
3726    logical_index: usize,
3727    path: Path,
3728    anchor: Point,
3729    base: Point,
3730    end: Point,
3731}
3732
3733fn pie_slices(
3734    first_slice_angle: u16,
3735    hole_size: Option<u8>,
3736    series: &[Series],
3737    bounds: Rect,
3738) -> Result<Vec<PieSliceGeometry>> {
3739    validate_series_values(series)?;
3740    let center = Point {
3741        x: bounds.x + bounds.width / 2.0,
3742        y: bounds.y + bounds.height / 2.0,
3743    };
3744    let radius = bounds.width.min(bounds.height) / 2.0;
3745    let mut elements = Vec::new();
3746    for (series_index, item) in series.iter().enumerate() {
3747        let (_, logical) = logical_numeric_values(&item.values)?;
3748        let total = logical
3749            .iter()
3750            .map(|(_, value)| *value)
3751            .filter(|value| *value > 0.0)
3752            .try_fold(0.0, |total, value| {
3753                checked_geometry_sum(total, value, "pie value total")
3754            })?;
3755        if total <= 0.0 {
3756            continue;
3757        }
3758        let mut angle = f64::from(first_slice_angle).to_radians() - std::f64::consts::FRAC_PI_2;
3759        for (logical_index, value) in logical {
3760            if value <= 0.0 {
3761                continue;
3762            }
3763            let next = angle + value / total * std::f64::consts::TAU;
3764            let path = if let Some(hole_size) = hole_size {
3765                doughnut_wedge(
3766                    center,
3767                    radius,
3768                    radius * f64::from(hole_size) / 100.0,
3769                    angle,
3770                    next,
3771                )
3772            } else {
3773                pie_wedge(center, radius, angle, next)
3774            };
3775            let inner_radius = hole_size
3776                .map(|hole| radius * f64::from(hole) / 100.0)
3777                .unwrap_or(0.0);
3778            let anchor_radius = hole_size
3779                .map(|_| (radius + inner_radius) / 2.0)
3780                .unwrap_or(radius * 2.0 / 3.0);
3781            let midpoint = (angle + next) / 2.0;
3782            elements.push(PieSliceGeometry {
3783                series_index,
3784                logical_index,
3785                path,
3786                anchor: circle_point(center, anchor_radius, midpoint),
3787                base: circle_point(center, inner_radius, midpoint),
3788                end: circle_point(center, radius, midpoint),
3789            });
3790            angle = next;
3791        }
3792    }
3793    Ok(elements)
3794}
3795
3796fn validate_series_values(series: &[Series]) -> Result<()> {
3797    if series.is_empty() {
3798        return Err(ChartError::MissingElement("c:ser".to_owned()));
3799    }
3800    for item in series {
3801        validate_finite(&item.values.values, "c:val/c:numCache")?;
3802    }
3803    Ok(())
3804}
3805
3806fn validate_finite(values: &[f64], element: &str) -> Result<()> {
3807    if let Some(value) = values.iter().find(|value| !value.is_finite()) {
3808        return Err(ChartError::InvalidValue {
3809            element: element.to_owned(),
3810            value: value.to_string(),
3811        });
3812    }
3813    Ok(())
3814}
3815
3816type IndexedValues = Vec<(usize, f64)>;
3817type LogicalSeries = (usize, IndexedValues);
3818type GeometryLayer = Vec<(usize, f64, f64)>;
3819
3820fn zero_control_indexes(count: usize, present: &BTreeSet<usize>) -> BTreeSet<usize> {
3821    let mut controls = present.clone();
3822    if count == 0 {
3823        return controls;
3824    }
3825    controls.insert(0);
3826    controls.insert(count - 1);
3827    for index in present {
3828        if *index > 0 {
3829            controls.insert(*index - 1);
3830        }
3831        if index + 1 < count {
3832            controls.insert(index + 1);
3833        }
3834    }
3835    controls
3836}
3837
3838fn zero_filled_series(series: &[LogicalSeries], count: usize) -> Vec<LogicalSeries> {
3839    let present = series
3840        .iter()
3841        .flat_map(|(_, points)| points.iter().map(|(index, _)| *index))
3842        .collect();
3843    let controls = zero_control_indexes(count, &present);
3844    series
3845        .iter()
3846        .map(|(declared, points)| {
3847            let values: BTreeMap<_, _> = points.iter().copied().collect();
3848            let filled = controls
3849                .iter()
3850                .map(|index| (*index, values.get(index).copied().unwrap_or(0.0)))
3851                .collect();
3852            (*declared, filled)
3853        })
3854        .collect()
3855}
3856
3857fn contiguous_ranges(indexes: &[usize], blanks: DispBlanksAs) -> Vec<(usize, usize)> {
3858    if indexes.is_empty() {
3859        return Vec::new();
3860    }
3861    if blanks != DispBlanksAs::Gap {
3862        return vec![(0, indexes.len())];
3863    }
3864    let mut ranges = Vec::new();
3865    let mut start = 0usize;
3866    for position in 1..indexes.len() {
3867        if indexes[position - 1].checked_add(1) != Some(indexes[position]) {
3868            ranges.push((start, position));
3869            start = position;
3870        }
3871    }
3872    ranges.push((start, indexes.len()));
3873    ranges
3874}
3875
3876fn logical_numeric_values(data: &NumericData) -> Result<LogicalSeries> {
3877    validate_finite(&data.values, "c:numCache")?;
3878    let (declared, indexes) = cache_layout(&data.markup, data.values.len())?;
3879    let declared = usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
3880        element: "c:ptCount".to_owned(),
3881        value: declared.to_string(),
3882    })?;
3883    let points = indexes
3884        .into_iter()
3885        .zip(data.values.iter().copied())
3886        .map(|(index, value)| {
3887            usize::try_from(index)
3888                .map(|index| (index, value))
3889                .map_err(|_| ChartError::InvalidValue {
3890                    element: "c:pt/@idx".to_owned(),
3891                    value: index.to_string(),
3892                })
3893        })
3894        .collect::<Result<Vec<_>>>()?;
3895    Ok((declared, points))
3896}
3897
3898fn logical_series_values(series: &[Series]) -> Result<Vec<LogicalSeries>> {
3899    series
3900        .iter()
3901        .map(|item| logical_numeric_values(&item.values))
3902        .collect()
3903}
3904
3905fn reference_logical_count(markup: &ReferenceMarkup, value_count: usize) -> Result<usize> {
3906    let (declared, _) = cache_layout(markup, value_count)?;
3907    usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
3908        element: "c:ptCount".to_owned(),
3909        value: declared.to_string(),
3910    })
3911}
3912
3913fn series_category_count(series: &[Series]) -> Result<usize> {
3914    let mut count = 0usize;
3915    for item in series {
3916        count = count.max(reference_logical_count(
3917            &item.values.markup,
3918            item.values.values.len(),
3919        )?);
3920        if let Some(categories) = &item.categories {
3921            let category_count = match categories {
3922                AxisData::String(data) => reference_logical_count(&data.markup, data.values.len())?,
3923                AxisData::Numeric(data) => {
3924                    reference_logical_count(&data.markup, data.values.len())?
3925                }
3926            };
3927            count = count.max(category_count);
3928        }
3929    }
3930    if count == 0 {
3931        return Err(ChartError::InvalidValue {
3932            element: "c:ser".to_owned(),
3933            value: "cached series contain no points".to_owned(),
3934        });
3935    }
3936    Ok(count)
3937}
3938
3939fn stacked_series_bounds(
3940    series: &[LogicalSeries],
3941    stacked: bool,
3942    percent: bool,
3943) -> Result<Vec<GeometryLayer>> {
3944    let mut positive_totals = BTreeMap::<usize, f64>::new();
3945    let mut negative_totals = BTreeMap::<usize, f64>::new();
3946    if percent {
3947        for (_, points) in series {
3948            for &(index, value) in points {
3949                let totals = if value >= 0.0 {
3950                    &mut positive_totals
3951                } else {
3952                    &mut negative_totals
3953                };
3954                let total = totals.entry(index).or_default();
3955                *total = checked_geometry_sum(*total, value.abs(), "stacked percentage total")?;
3956            }
3957        }
3958    }
3959    let mut positive = BTreeMap::<usize, f64>::new();
3960    let mut negative = BTreeMap::<usize, f64>::new();
3961    let mut layers = Vec::with_capacity(series.len());
3962    for (_, points) in series {
3963        let mut layer = Vec::with_capacity(points.len());
3964        for &(index, original) in points {
3965            let total = if original >= 0.0 {
3966                positive_totals.get(&index).copied().unwrap_or(0.0)
3967            } else {
3968                negative_totals.get(&index).copied().unwrap_or(0.0)
3969            };
3970            let value = if percent && total != 0.0 {
3971                original / total
3972            } else {
3973                original
3974            };
3975            if stacked {
3976                let accumulators = if value >= 0.0 {
3977                    &mut positive
3978                } else {
3979                    &mut negative
3980                };
3981                let accumulator = accumulators.entry(index).or_default();
3982                let lower = *accumulator;
3983                *accumulator = checked_geometry_sum(*accumulator, value, "stacked value total")?;
3984                layer.push((index, lower, *accumulator));
3985            } else {
3986                layer.push((index, 0.0, value));
3987            }
3988        }
3989        layers.push(layer);
3990    }
3991    Ok(layers)
3992}
3993
3994fn checked_geometry_sum(current: f64, value: f64, context: &str) -> Result<f64> {
3995    let sum = current + value;
3996    if sum.is_finite() {
3997        Ok(sum)
3998    } else {
3999        Err(ChartError::InvalidValue {
4000            element: "chart geometry aggregate".to_owned(),
4001            value: format!("{context} is nonfinite"),
4002        })
4003    }
4004}
4005
4006#[derive(Clone, Copy, Debug, PartialEq)]
4007struct Domain {
4008    min: f64,
4009    max: f64,
4010}
4011
4012fn nice_number_scale(
4013    extent: Domain,
4014    pinned_minimum: Option<f64>,
4015    pinned_maximum: Option<f64>,
4016    target_tick_count: usize,
4017) -> Result<AxisScale> {
4018    if target_tick_count < 2
4019        || !extent.min.is_finite()
4020        || !extent.max.is_finite()
4021        || pinned_minimum.is_some_and(|value| !value.is_finite())
4022        || pinned_maximum.is_some_and(|value| !value.is_finite())
4023    {
4024        return Err(ChartError::InvalidValue {
4025            element: "chart axis scale".to_owned(),
4026            value: "scale bounds and target tick count must be finite and valid".to_owned(),
4027        });
4028    }
4029
4030    let mut data_min = extent.min.min(extent.max);
4031    let mut data_max = extent.min.max(extent.max);
4032    if data_min == data_max {
4033        let expanded = expand_constant_domain(data_min)?;
4034        data_min = expanded.min;
4035        data_max = expanded.max;
4036    }
4037    let requested_min = match (pinned_minimum, pinned_maximum) {
4038        (Some(minimum), _) => minimum,
4039        (None, Some(maximum)) if data_min >= maximum => {
4040            extend_bound(maximum, stable_positive_step(data_min, data_max, 3)?, false)?
4041        }
4042        (None, _) => data_min,
4043    };
4044    let requested_max = match (pinned_minimum, pinned_maximum) {
4045        (_, Some(maximum)) => maximum,
4046        (Some(minimum), None) if data_max <= minimum => {
4047            extend_bound(minimum, stable_positive_step(data_min, data_max, 3)?, true)?
4048        }
4049        (_, None) => data_max,
4050    };
4051    if requested_min >= requested_max {
4052        return Err(ChartError::InvalidValue {
4053            element: "c:scaling".to_owned(),
4054            value: format!("minimum {requested_min} is not less than maximum {requested_max}"),
4055        });
4056    }
4057
4058    let raw_step = stable_positive_step(requested_min, requested_max, target_tick_count)?;
4059    let step = nice_step(raw_step)?;
4060    let rounded_minimum = (requested_min / step).floor() * step;
4061    let rounded_maximum = (requested_max / step).ceil() * step;
4062    let lower_fallback = pinned_minimum.is_none() && !rounded_minimum.is_finite();
4063    let upper_fallback = pinned_maximum.is_none() && !rounded_maximum.is_finite();
4064    let minimum = pinned_minimum.unwrap_or(if rounded_minimum.is_finite() {
4065        rounded_minimum
4066    } else {
4067        requested_min
4068    });
4069    let maximum = pinned_maximum.unwrap_or(if rounded_maximum.is_finite() {
4070        rounded_maximum
4071    } else {
4072        requested_max
4073    });
4074    if !minimum.is_finite() || !maximum.is_finite() || minimum >= maximum {
4075        return Err(ChartError::InvalidValue {
4076            element: "chart axis scale".to_owned(),
4077            value: "nice-number bounds are nonfinite or empty".to_owned(),
4078        });
4079    }
4080
4081    let first = (minimum / step).ceil();
4082    let last = (maximum / step).floor();
4083    let tick_count = if last >= first {
4084        (last - first).min(1024.0) as usize + 1
4085    } else {
4086        0
4087    };
4088    let mut ticks = Vec::with_capacity(tick_count);
4089    for index in 0..tick_count {
4090        let tick = (first + index as f64) * step;
4091        if tick.is_finite() && tick >= minimum && tick <= maximum {
4092            ticks.push(clean_tick(tick, step));
4093        }
4094    }
4095    if lower_fallback && ticks.first().is_none_or(|tick| *tick > minimum) {
4096        ticks.insert(0, minimum);
4097    }
4098    if upper_fallback && ticks.last().is_none_or(|tick| *tick < maximum) {
4099        ticks.push(maximum);
4100    }
4101    if ticks.len() < 2 {
4102        ticks.extend([minimum, maximum]);
4103        ticks.sort_by(f64::total_cmp);
4104    }
4105    ticks.dedup_by(|left, right| *left == *right);
4106    if ticks.len() < 2 || !ticks.windows(2).all(|pair| pair[0] < pair[1]) {
4107        return Err(ChartError::InvalidValue {
4108            element: "chart axis scale".to_owned(),
4109            value: "scale cannot produce two increasing finite ticks".to_owned(),
4110        });
4111    }
4112    Ok(AxisScale {
4113        domain: Domain {
4114            min: minimum,
4115            max: maximum,
4116        },
4117        ticks,
4118    })
4119}
4120
4121const MIN_POSITIVE_SUBNORMAL: f64 = f64::from_bits(1);
4122
4123fn stable_positive_step(minimum: f64, maximum: f64, target_tick_count: usize) -> Result<f64> {
4124    let scale = minimum.abs().max(maximum.abs());
4125    let scale = if scale == 0.0 {
4126        MIN_POSITIVE_SUBNORMAL
4127    } else {
4128        scale
4129    };
4130    let normalized =
4131        (maximum / scale - minimum / scale) / target_tick_count.saturating_sub(1) as f64;
4132    let step = normalized * scale;
4133    if normalized.is_finite() && normalized > 0.0 && step.is_finite() {
4134        Ok(step.max(MIN_POSITIVE_SUBNORMAL))
4135    } else {
4136        Err(ChartError::InvalidValue {
4137            element: "chart axis scale".to_owned(),
4138            value: "scale step is nonfinite or empty".to_owned(),
4139        })
4140    }
4141}
4142
4143fn nice_step(raw_step: f64) -> Result<f64> {
4144    if !raw_step.is_finite() || raw_step <= 0.0 {
4145        return Err(ChartError::InvalidValue {
4146            element: "chart axis scale".to_owned(),
4147            value: format!("invalid raw tick step {raw_step}"),
4148        });
4149    }
4150    let magnitude = 10.0_f64
4151        .powf(raw_step.log10().floor())
4152        .max(MIN_POSITIVE_SUBNORMAL);
4153    let fraction = raw_step / magnitude;
4154    let multiplier = if fraction <= 1.0 {
4155        1.0
4156    } else if fraction <= 2.0 {
4157        2.0
4158    } else if fraction <= 5.0 {
4159        5.0
4160    } else {
4161        10.0
4162    };
4163    let step = (multiplier * magnitude).max(MIN_POSITIVE_SUBNORMAL);
4164    if step.is_finite() && step > 0.0 {
4165        Ok(step)
4166    } else {
4167        Err(ChartError::InvalidValue {
4168            element: "chart axis scale".to_owned(),
4169            value: "nice-number step is nonfinite".to_owned(),
4170        })
4171    }
4172}
4173
4174fn expand_constant_domain(value: f64) -> Result<Domain> {
4175    if value == 0.0 {
4176        return Ok(Domain {
4177            min: -1.0,
4178            max: 1.0,
4179        });
4180    }
4181    let padding = (value.abs() * 0.5).max(MIN_POSITIVE_SUBNORMAL);
4182    let minimum = value - padding;
4183    let maximum = value + padding;
4184    if minimum.is_finite() && maximum.is_finite() && minimum < maximum {
4185        return Ok(Domain {
4186            min: minimum,
4187            max: maximum,
4188        });
4189    }
4190    let domain = if value.is_sign_positive() {
4191        Domain {
4192            min: 0.0,
4193            max: value,
4194        }
4195    } else {
4196        Domain {
4197            min: value,
4198            max: 0.0,
4199        }
4200    };
4201    if domain.min < domain.max {
4202        Ok(domain)
4203    } else {
4204        Err(ChartError::InvalidValue {
4205            element: "chart axis scale".to_owned(),
4206            value: format!("constant value {value} has no finite renderable extent"),
4207        })
4208    }
4209}
4210
4211fn extend_bound(bound: f64, span: f64, positive: bool) -> Result<f64> {
4212    let candidate = if positive { bound + span } else { bound - span };
4213    let extended = if candidate.is_finite()
4214        && if positive {
4215            candidate > bound
4216        } else {
4217            candidate < bound
4218        } {
4219        candidate
4220    } else if positive {
4221        bound.next_up()
4222    } else {
4223        bound.next_down()
4224    };
4225    if extended.is_finite()
4226        && if positive {
4227            extended > bound
4228        } else {
4229            extended < bound
4230        }
4231    {
4232        Ok(extended)
4233    } else {
4234        Err(ChartError::InvalidValue {
4235            element: "c:scaling".to_owned(),
4236            value: format!("one-sided bound {bound} leaves no finite automatic extent"),
4237        })
4238    }
4239}
4240
4241fn clean_tick(value: f64, step: f64) -> f64 {
4242    if value.abs() <= step.abs() * 1.0e-12 {
4243        0.0
4244    } else {
4245        value
4246    }
4247}
4248
4249fn domain_from_layers(layers: &[GeometryLayer]) -> Result<Domain> {
4250    let values = layers
4251        .iter()
4252        .flatten()
4253        .flat_map(|(_, lower, upper)| [*lower, *upper]);
4254    let mut min = 0.0_f64;
4255    let mut max = 0.0_f64;
4256    for value in values {
4257        if !value.is_finite() {
4258            return Err(ChartError::InvalidValue {
4259                element: "chart geometry domain".to_owned(),
4260                value: "aggregate produced a nonfinite value".to_owned(),
4261            });
4262        }
4263        min = min.min(value);
4264        max = max.max(value);
4265    }
4266    if min == max {
4267        max = 1.0;
4268    }
4269    Ok(Domain { min, max })
4270}
4271
4272fn data_domain(values: &[f64], include_zero: bool) -> Result<Domain> {
4273    validate_finite(values, "chart geometry cache")?;
4274    let Some(first) = values.first().copied() else {
4275        return Err(ChartError::InvalidValue {
4276            element: "c:numCache".to_owned(),
4277            value: "cached series contain no points".to_owned(),
4278        });
4279    };
4280    let mut min = first;
4281    let mut max = first;
4282    for value in values.iter().copied() {
4283        min = min.min(value);
4284        max = max.max(value);
4285    }
4286    if include_zero {
4287        min = min.min(0.0);
4288        max = max.max(0.0);
4289    }
4290    if min == max {
4291        if !include_zero {
4292            return expand_constant_domain(min);
4293        }
4294        if min > 0.0 {
4295            min = 0.0;
4296        } else if max < 0.0 {
4297            max = 0.0;
4298        } else {
4299            max = 1.0;
4300        }
4301    }
4302    Ok(Domain { min, max })
4303}
4304
4305fn normalized_value(value: f64, domain: Domain) -> Result<f64> {
4306    let scale = value
4307        .abs()
4308        .max(domain.min.abs())
4309        .max(domain.max.abs())
4310        .max(1.0);
4311    let value = value / scale;
4312    let min = domain.min / scale;
4313    let max = domain.max / scale;
4314    let range = max - min;
4315    let normalized = (value - min) / range;
4316    if !range.is_finite() || range <= 0.0 || !normalized.is_finite() {
4317        return Err(ChartError::InvalidValue {
4318            element: "chart geometry domain".to_owned(),
4319            value: format!("cannot map finite domain {} to {}", domain.min, domain.max),
4320        });
4321    }
4322    Ok(normalized)
4323}
4324
4325fn normalized_value_oriented(value: f64, domain: Domain, orientation: Orientation) -> Result<f64> {
4326    let normalized = normalized_value(value, domain)?;
4327    Ok(if orientation == Orientation::MaxMin {
4328        1.0 - normalized
4329    } else {
4330        normalized
4331    })
4332}
4333
4334fn map_x_oriented(
4335    value: f64,
4336    domain: Domain,
4337    bounds: Rect,
4338    orientation: Orientation,
4339) -> Result<f64> {
4340    let coordinate =
4341        bounds.x + normalized_value_oriented(value, domain, orientation)? * bounds.width;
4342    if coordinate.is_finite() {
4343        Ok(coordinate)
4344    } else {
4345        Err(ChartError::InvalidValue {
4346            element: "chart geometry coordinate".to_owned(),
4347            value: "x coordinate is nonfinite".to_owned(),
4348        })
4349    }
4350}
4351
4352fn map_y_oriented(
4353    value: f64,
4354    domain: Domain,
4355    bounds: Rect,
4356    orientation: Orientation,
4357) -> Result<f64> {
4358    let coordinate = bounds.y + bounds.height
4359        - normalized_value_oriented(value, domain, orientation)? * bounds.height;
4360    if coordinate.is_finite() {
4361        Ok(coordinate)
4362    } else {
4363        Err(ChartError::InvalidValue {
4364            element: "chart geometry coordinate".to_owned(),
4365            value: "y coordinate is nonfinite".to_owned(),
4366        })
4367    }
4368}
4369
4370fn oriented_category_index(index: usize, count: usize, orientation: Orientation) -> usize {
4371    if orientation == Orientation::MaxMin && index < count {
4372        count - index - 1
4373    } else {
4374        index
4375    }
4376}
4377
4378fn category_points(
4379    values: &[(usize, f64)],
4380    count: usize,
4381    domain: Domain,
4382    bounds: Rect,
4383    category_orientation: Orientation,
4384    value_orientation: Orientation,
4385) -> Result<Vec<Point>> {
4386    values
4387        .iter()
4388        .map(|(index, value)| {
4389            let index = oriented_category_index(*index, count, category_orientation);
4390            let x = bounds.x + (index as f64 + 0.5) / count as f64 * bounds.width;
4391            let y = map_y_oriented(*value, domain, bounds, value_orientation)?;
4392            if !x.is_finite() {
4393                return Err(ChartError::InvalidValue {
4394                    element: "chart geometry coordinate".to_owned(),
4395                    value: "category x coordinate is nonfinite".to_owned(),
4396                });
4397            }
4398            Ok(Point { x, y })
4399        })
4400        .collect()
4401}
4402
4403fn polyline_path(points: &[Point], close: bool) -> Path {
4404    let mut commands = Vec::with_capacity(points.len() + usize::from(close));
4405    if let Some(first) = points.first() {
4406        commands.push(PathCommand::MoveTo(*first));
4407        commands.extend(points.iter().skip(1).copied().map(PathCommand::LineTo));
4408        if close {
4409            commands.push(PathCommand::Close);
4410        }
4411    }
4412    Path {
4413        commands,
4414        fill_rule: FillRule::NonZero,
4415    }
4416}
4417
4418fn pie_wedge(center: Point, radius: f64, start: f64, end: f64) -> Path {
4419    let first = circle_point(center, radius, start);
4420    let mut commands = vec![PathCommand::MoveTo(center), PathCommand::LineTo(first)];
4421    append_arc(&mut commands, center, radius, start, end);
4422    commands.push(PathCommand::Close);
4423    Path {
4424        commands,
4425        fill_rule: FillRule::NonZero,
4426    }
4427}
4428
4429fn doughnut_wedge(
4430    center: Point,
4431    outer_radius: f64,
4432    inner_radius: f64,
4433    start: f64,
4434    end: f64,
4435) -> Path {
4436    let outer_start = circle_point(center, outer_radius, start);
4437    let inner_end = circle_point(center, inner_radius, end);
4438    let mut commands = vec![PathCommand::MoveTo(outer_start)];
4439    append_arc(&mut commands, center, outer_radius, start, end);
4440    commands.push(PathCommand::LineTo(inner_end));
4441    append_arc(&mut commands, center, inner_radius, end, start);
4442    commands.push(PathCommand::Close);
4443    Path {
4444        commands,
4445        fill_rule: FillRule::NonZero,
4446    }
4447}
4448
4449fn append_arc(commands: &mut Vec<PathCommand>, center: Point, radius: f64, start: f64, end: f64) {
4450    let segments = ((end - start).abs() / std::f64::consts::FRAC_PI_2)
4451        .ceil()
4452        .max(1.0) as usize;
4453    let sweep = (end - start) / segments as f64;
4454    for segment in 0..segments {
4455        let angle = start + segment as f64 * sweep;
4456        let next = angle + sweep;
4457        let factor = 4.0 / 3.0 * (sweep / 4.0).tan();
4458        let from = circle_point(center, radius, angle);
4459        let to = circle_point(center, radius, next);
4460        commands.push(PathCommand::CurveTo {
4461            c1: Point {
4462                x: from.x - factor * radius * angle.sin(),
4463                y: from.y + factor * radius * angle.cos(),
4464            },
4465            c2: Point {
4466                x: to.x + factor * radius * next.sin(),
4467                y: to.y - factor * radius * next.cos(),
4468            },
4469            to,
4470        });
4471    }
4472}
4473
4474fn circle_point(center: Point, radius: f64, angle: f64) -> Point {
4475    Point {
4476        x: center.x + radius * angle.cos(),
4477        y: center.y + radius * angle.sin(),
4478    }
4479}
4480
4481fn radial_point(center: Point, radius: f64, index: usize, count: usize) -> Point {
4482    circle_point(
4483        center,
4484        radius,
4485        index as f64 * std::f64::consts::TAU / count as f64 - std::f64::consts::FRAC_PI_2,
4486    )
4487}
4488
4489fn marker_path(center: Point) -> Path {
4490    const RADIUS: f64 = 2.5;
4491    const KAPPA: f64 = 0.552_284_749_830_793_6;
4492    let control = RADIUS * KAPPA;
4493    Path {
4494        commands: vec![
4495            PathCommand::MoveTo(Point {
4496                x: center.x + RADIUS,
4497                y: center.y,
4498            }),
4499            PathCommand::CurveTo {
4500                c1: Point {
4501                    x: center.x + RADIUS,
4502                    y: center.y + control,
4503                },
4504                c2: Point {
4505                    x: center.x + control,
4506                    y: center.y + RADIUS,
4507                },
4508                to: Point {
4509                    x: center.x,
4510                    y: center.y + RADIUS,
4511                },
4512            },
4513            PathCommand::CurveTo {
4514                c1: Point {
4515                    x: center.x - control,
4516                    y: center.y + RADIUS,
4517                },
4518                c2: Point {
4519                    x: center.x - RADIUS,
4520                    y: center.y + control,
4521                },
4522                to: Point {
4523                    x: center.x - RADIUS,
4524                    y: center.y,
4525                },
4526            },
4527            PathCommand::CurveTo {
4528                c1: Point {
4529                    x: center.x - RADIUS,
4530                    y: center.y - control,
4531                },
4532                c2: Point {
4533                    x: center.x - control,
4534                    y: center.y - RADIUS,
4535                },
4536                to: Point {
4537                    x: center.x,
4538                    y: center.y - RADIUS,
4539                },
4540            },
4541            PathCommand::CurveTo {
4542                c1: Point {
4543                    x: center.x + control,
4544                    y: center.y - RADIUS,
4545                },
4546                c2: Point {
4547                    x: center.x + RADIUS,
4548                    y: center.y - control,
4549                },
4550                to: Point {
4551                    x: center.x + RADIUS,
4552                    y: center.y,
4553                },
4554            },
4555            PathCommand::Close,
4556        ],
4557        fill_rule: FillRule::NonZero,
4558    }
4559}
4560
4561fn push_markers(elements: &mut Vec<PositionedElement>, points: &[Point], colour: Color) {
4562    for point in points {
4563        elements.push(filled_path(marker_path(*point), colour));
4564    }
4565}
4566
4567fn series_colour(series_colours: &[Color], index: usize) -> Result<Color> {
4568    series_colours
4569        .get(index)
4570        .copied()
4571        .ok_or_else(|| ChartError::InvalidValue {
4572            element: "chart series palette".to_owned(),
4573            value: format!("missing resolved colour for series {index}"),
4574        })
4575}
4576
4577fn filled_path(path: Path, color: Color) -> PositionedElement {
4578    PositionedElement::Path(PathElement {
4579        path,
4580        fill: Some(Paint::Solid(color)),
4581        stroke: None,
4582    })
4583}
4584
4585fn stroked_path(path: Path, color: Color) -> PositionedElement {
4586    PositionedElement::Path(PathElement {
4587        path,
4588        fill: None,
4589        stroke: Some(Stroke::new(Paint::Solid(color), 1.5)),
4590    })
4591}
4592
4593fn filled_and_stroked_path(path: Path, color: Color) -> PositionedElement {
4594    PositionedElement::Path(PathElement {
4595        path,
4596        fill: Some(Paint::Solid(Color {
4597            a: color.a * 0.55,
4598            ..color
4599        })),
4600        stroke: Some(Stroke::new(Paint::Solid(color), 1.0)),
4601    })
4602}
4603
4604type NamespaceBindings = Vec<(Vec<u8>, String)>;
4605type XmlAttributes = Vec<(String, String)>;
4606type RootAttributes = (XmlAttributes, XmlAttributes);
4607
4608#[derive(Clone, Debug, Default, Eq, PartialEq)]
4609struct ScalarMarkup {
4610    raw_attributes: XmlAttributes,
4611    raw_content: Vec<Vec<u8>>,
4612}
4613
4614#[derive(Clone, Debug, Default, Eq, PartialEq)]
4615struct TextMarkup {
4616    raw_attributes: XmlAttributes,
4617    raw_children: OrderedRawChildren,
4618}
4619
4620#[derive(Clone, Debug, Default, Eq, PartialEq)]
4621struct PointMarkup {
4622    raw_attributes: XmlAttributes,
4623    raw_children: OrderedRawChildren,
4624    value: TextMarkup,
4625}
4626
4627#[derive(Clone, Debug, Default, Eq, PartialEq)]
4628struct ReferenceMarkup {
4629    raw_attributes: XmlAttributes,
4630    raw_children: OrderedRawChildren,
4631    formula: TextMarkup,
4632    cache_attributes: XmlAttributes,
4633    cache_children: OrderedRawChildren,
4634    format_code: Option<TextMarkup>,
4635    format_code_omitted: bool,
4636    point_count: ScalarMarkup,
4637    declared_point_count: Option<u32>,
4638    point_indexes: Vec<u32>,
4639    points: Vec<PointMarkup>,
4640}
4641
4642#[derive(Clone, Debug, Default, Eq, PartialEq)]
4643struct WrapperMarkup {
4644    raw_attributes: XmlAttributes,
4645    raw_children: OrderedRawChildren,
4646}
4647
4648/// A formula-backed string cache whose count and point indexes are derived.
4649#[derive(Clone, Debug, PartialEq)]
4650pub struct StringRef {
4651    pub formula: String,
4652    pub values: Vec<String>,
4653    markup: ReferenceMarkup,
4654}
4655
4656impl StringRef {
4657    pub fn new(formula: String, values: Vec<String>) -> Result<Self> {
4658        let reference = Self {
4659            formula,
4660            values,
4661            markup: ReferenceMarkup::default(),
4662        };
4663        reference.validate()?;
4664        Ok(reference)
4665    }
4666
4667    pub fn from_xml(xml: &[u8]) -> Result<Self> {
4668        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
4669    }
4670
4671    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
4672        let parsed = parse_reference(xml, b"strRef", b"strCache", false, inherited)?;
4673        let values = parsed.values;
4674        let reference = Self {
4675            formula: parsed.formula,
4676            values,
4677            markup: parsed.markup,
4678        };
4679        reference.validate()?;
4680        Ok(reference)
4681    }
4682
4683    pub fn to_xml(&self) -> Result<Vec<u8>> {
4684        self.validate()?;
4685        let mut writer = Writer::new(Vec::new());
4686        self.write_xml(&mut writer, true)?;
4687        Ok(writer.into_inner())
4688    }
4689
4690    fn validate(&self) -> Result<()> {
4691        validate_formula(&self.formula, "c:strRef/c:f")?;
4692        for value in &self.values {
4693            validate_xml_text(value, "c:strCache/c:pt/c:v")?;
4694        }
4695        validate_point_count(self.values.len(), "c:strCache")
4696    }
4697
4698    fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
4699        self.validate()?;
4700        let mut start = BytesStart::new("c:strRef");
4701        if standalone {
4702            start.push_attribute(("xmlns:c", C_NS));
4703        }
4704        push_attributes(&mut start, &self.markup.raw_attributes);
4705        writer
4706            .write_event(Event::Start(start))
4707            .map_err(OxmlError::from)?;
4708        emit_raw(writer, self.markup.raw_children.at(0))?;
4709        write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
4710        emit_raw(writer, self.markup.raw_children.at(1))?;
4711        write_string_cache(writer, &self.values, &self.markup)?;
4712        emit_raw(writer, self.markup.raw_children.at(2))?;
4713        writer
4714            .write_event(Event::End(BytesEnd::new("c:strRef")))
4715            .map_err(OxmlError::from)?;
4716        Ok(())
4717    }
4718}
4719
4720/// A formula-backed numeric cache whose metadata and points are derived.
4721#[derive(Clone, Debug, PartialEq)]
4722pub struct NumericData {
4723    pub formula: String,
4724    pub format_code: String,
4725    pub values: Vec<f64>,
4726    markup: ReferenceMarkup,
4727}
4728
4729impl NumericData {
4730    pub fn new(formula: String, format_code: String, values: Vec<f64>) -> Result<Self> {
4731        validate_formula(&formula, "c:numRef/c:f")?;
4732        validate_format_code(&format_code)?;
4733        validate_numeric_values(&values)?;
4734        validate_point_count(values.len(), "c:numCache")?;
4735        Ok(Self {
4736            formula,
4737            format_code,
4738            values,
4739            markup: ReferenceMarkup::default(),
4740        })
4741    }
4742
4743    pub fn from_xml(xml: &[u8]) -> Result<Self> {
4744        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
4745    }
4746
4747    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
4748        let parsed = parse_reference(xml, b"numRef", b"numCache", true, inherited)?;
4749        let format_code = parsed.format_code.unwrap_or_else(|| "General".to_owned());
4750        let mut values = Vec::with_capacity(parsed.values.len());
4751        for value in parsed.values {
4752            let number = value.parse::<f64>().map_err(|_| ChartError::InvalidValue {
4753                element: "c:v".to_owned(),
4754                value: value.clone(),
4755            })?;
4756            if !number.is_finite() {
4757                return Err(ChartError::InvalidValue {
4758                    element: "c:v".to_owned(),
4759                    value,
4760                });
4761            }
4762            values.push(number);
4763        }
4764        let data = Self {
4765            formula: parsed.formula,
4766            format_code,
4767            values,
4768            markup: parsed.markup,
4769        };
4770        data.validate()?;
4771        Ok(data)
4772    }
4773
4774    pub fn to_xml(&self) -> Result<Vec<u8>> {
4775        self.validate()?;
4776        let mut writer = Writer::new(Vec::new());
4777        self.write_xml(&mut writer, true)?;
4778        Ok(writer.into_inner())
4779    }
4780
4781    fn validate(&self) -> Result<()> {
4782        validate_formula(&self.formula, "c:numRef/c:f")?;
4783        validate_format_code(&self.format_code)?;
4784        validate_numeric_values(&self.values)?;
4785        validate_point_count(self.values.len(), "c:numCache")
4786    }
4787
4788    fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
4789        self.validate()?;
4790        let mut start = BytesStart::new("c:numRef");
4791        if standalone {
4792            start.push_attribute(("xmlns:c", C_NS));
4793        }
4794        push_attributes(&mut start, &self.markup.raw_attributes);
4795        writer
4796            .write_event(Event::Start(start))
4797            .map_err(OxmlError::from)?;
4798        emit_raw(writer, self.markup.raw_children.at(0))?;
4799        write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
4800        emit_raw(writer, self.markup.raw_children.at(1))?;
4801        write_numeric_cache(writer, self, &self.markup)?;
4802        emit_raw(writer, self.markup.raw_children.at(2))?;
4803        writer
4804            .write_event(Event::End(BytesEnd::new("c:numRef")))
4805            .map_err(OxmlError::from)?;
4806        Ok(())
4807    }
4808}
4809
4810#[derive(Clone, Debug, PartialEq)]
4811pub enum AxisData {
4812    String(StringRef),
4813    Numeric(NumericData),
4814}
4815
4816#[derive(Clone, Debug, PartialEq)]
4817struct DataPoint {
4818    index: u32,
4819    sp_pr: CT_ShapeProperties,
4820}
4821
4822impl DataPoint {
4823    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
4824        writer
4825            .write_event(Event::Start(BytesStart::new("c:dPt")))
4826            .map_err(OxmlError::from)?;
4827        write_scalar(writer, "c:idx", &self.index.to_string(), None)?;
4828        self.sp_pr.write_xml_as(writer, "c:spPr")?;
4829        writer
4830            .write_event(Event::End(BytesEnd::new("c:dPt")))
4831            .map_err(OxmlError::from)?;
4832        Ok(())
4833    }
4834}
4835
4836/// The common formula-backed payload of one ChartML series.
4837#[derive(Clone, Debug, PartialEq)]
4838pub struct Series {
4839    pub index: u32,
4840    pub order: u32,
4841    pub name: Option<StringRef>,
4842    pub categories: Option<AxisData>,
4843    pub values: NumericData,
4844    pub bubble_size: Option<NumericData>,
4845    pub sp_pr: Option<CT_ShapeProperties>,
4846    pub data_labels: Option<CT_DLbls>,
4847    authored_data_points: Vec<DataPoint>,
4848    index_markup: ScalarMarkup,
4849    order_markup: ScalarMarkup,
4850    name_markup: Option<WrapperMarkup>,
4851    categories_markup: Option<WrapperMarkup>,
4852    values_markup: WrapperMarkup,
4853    bubble_size_markup: Option<WrapperMarkup>,
4854    opaque_name: bool,
4855    opaque_categories: bool,
4856    opaque_bubble_size: bool,
4857    uses_scatter_wrappers: bool,
4858    parsed_from_xml: bool,
4859    raw_attributes: XmlAttributes,
4860    namespace_declarations: XmlAttributes,
4861    raw_children: OrderedRawChildren,
4862}
4863
4864impl Series {
4865    pub fn new(index: u32, order: u32, values: NumericData) -> Self {
4866        Self {
4867            index,
4868            order,
4869            name: None,
4870            categories: None,
4871            values,
4872            bubble_size: None,
4873            sp_pr: None,
4874            data_labels: None,
4875            authored_data_points: Vec::new(),
4876            index_markup: ScalarMarkup::default(),
4877            order_markup: ScalarMarkup::default(),
4878            name_markup: None,
4879            categories_markup: None,
4880            values_markup: WrapperMarkup::default(),
4881            bubble_size_markup: None,
4882            opaque_name: false,
4883            opaque_categories: false,
4884            opaque_bubble_size: false,
4885            uses_scatter_wrappers: false,
4886            parsed_from_xml: false,
4887            raw_attributes: Vec::new(),
4888            namespace_declarations: Vec::new(),
4889            raw_children: OrderedRawChildren::default(),
4890        }
4891    }
4892
4893    pub fn from_xml(xml: &[u8]) -> Result<Self> {
4894        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
4895    }
4896
4897    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
4898        let mut reader = Reader::from_reader(xml);
4899        let mut buffer = Vec::new();
4900        loop {
4901            match reader
4902                .read_event_into(&mut buffer)
4903                .map_err(OxmlError::from)?
4904            {
4905                Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4906                    chart_root_prefix(&element)?;
4907                    if !element_is_in_namespace(&element, C_NS, inherited)? {
4908                        return Err(ChartError::UnexpectedElement(element_name(&element)));
4909                    }
4910                    return Self::from_element(&mut reader, &element, inherited);
4911                }
4912                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4913                    return Err(ChartError::MissingElement("c:idx".to_owned()));
4914                }
4915                Event::Start(element) | Event::Empty(element) => {
4916                    return Err(ChartError::UnexpectedElement(element_name(&element)));
4917                }
4918                Event::Eof => return Err(ChartError::MissingElement("c:ser".to_owned())),
4919                _ => {}
4920            }
4921            buffer.clear();
4922        }
4923    }
4924
4925    fn from_element(
4926        reader: &mut Reader<&[u8]>,
4927        start: &BytesStart<'_>,
4928        inherited: &NamespaceBindings,
4929    ) -> Result<Self> {
4930        reject_conflicting_prefix(start, b"a", A_NS)?;
4931        let namespaces = chart_bindings(inherited, start)?;
4932        require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
4933        require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
4934        require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
4935        let (raw_attributes, _) =
4936            capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
4937        let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
4938        let mut state = SeriesParseState::default();
4939        let mut buffer = Vec::new();
4940        loop {
4941            match reader
4942                .read_event_into(&mut buffer)
4943                .map_err(OxmlError::from)?
4944            {
4945                Event::Start(element) => {
4946                    let name = chart_child_local(&element, &namespaces)?;
4947                    let raw = capture_element(reader, &element)?;
4948                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
4949                }
4950                Event::Empty(element) => {
4951                    let name = chart_child_local(&element, &namespaces)?;
4952                    let raw = capture_empty_element(&element)?;
4953                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
4954                }
4955                event @ (Event::Text(_)
4956                | Event::CData(_)
4957                | Event::Comment(_)
4958                | Event::PI(_)
4959                | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
4960                Event::End(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4961                    break;
4962                }
4963                Event::Eof => return Err(missing_end("c:ser")),
4964                _ => {}
4965            }
4966            buffer.clear();
4967        }
4968        state.finish(raw_attributes, namespace_declarations)
4969    }
4970
4971    pub fn to_xml(&self) -> Result<Vec<u8>> {
4972        self.to_xml_for_plot(self.uses_scatter_wrappers)
4973    }
4974
4975    fn to_xml_for_plot(&self, scatter: bool) -> Result<Vec<u8>> {
4976        if self.opaque_name && self.name.is_some() {
4977            return Err(ChartError::DuplicateElement("c:tx".to_owned()));
4978        }
4979        if self.opaque_categories && self.categories.is_some() {
4980            return Err(ChartError::DuplicateElement("c:cat".to_owned()));
4981        }
4982        if self.opaque_bubble_size && self.bubble_size.is_some() {
4983            return Err(ChartError::DuplicateElement("c:bubbleSize".to_owned()));
4984        }
4985        self.values.validate()?;
4986        if let Some(name) = &self.name {
4987            name.validate()?;
4988        }
4989        if let Some(categories) = &self.categories {
4990            if scatter && !matches!(categories, AxisData::Numeric(_)) {
4991                return Err(ChartError::InvalidValue {
4992                    element: "c:xVal".to_owned(),
4993                    value: "scatter x values must use a numeric cache".to_owned(),
4994                });
4995            }
4996            match categories {
4997                AxisData::String(reference) => reference.validate()?,
4998                AxisData::Numeric(reference) => reference.validate()?,
4999            }
5000        }
5001        if scatter && self.categories.is_none() {
5002            return Err(ChartError::MissingElement("c:xVal".to_owned()));
5003        }
5004        if let Some(size) = &self.bubble_size {
5005            size.validate()?;
5006        }
5007        if let Some(labels) = &self.data_labels {
5008            labels.validate()?;
5009        }
5010        let mut writer = Writer::new(Vec::new());
5011        let mut start = BytesStart::new("c:ser");
5012        start.push_attribute(("xmlns:c", C_NS));
5013        start.push_attribute(("xmlns:a", A_NS));
5014        start.push_attribute(("xmlns:r", R_NS));
5015        push_attributes(&mut start, &self.namespace_declarations);
5016        push_attributes(&mut start, &self.raw_attributes);
5017        writer
5018            .write_event(Event::Start(start))
5019            .map_err(OxmlError::from)?;
5020        emit_raw(&mut writer, self.raw_children.at(0))?;
5021        write_scalar(
5022            &mut writer,
5023            "c:idx",
5024            &self.index.to_string(),
5025            Some(&self.index_markup),
5026        )?;
5027        emit_raw(&mut writer, self.raw_children.at(1))?;
5028        write_scalar(
5029            &mut writer,
5030            "c:order",
5031            &self.order.to_string(),
5032            Some(&self.order_markup),
5033        )?;
5034        emit_raw(&mut writer, self.raw_children.at(2))?;
5035        if let Some(name) = &self.name {
5036            write_wrapper_start(
5037                &mut writer,
5038                "c:tx",
5039                self.name_markup
5040                    .as_ref()
5041                    .unwrap_or(&WrapperMarkup::default()),
5042            )?;
5043            name.write_xml(&mut writer, false)?;
5044            write_wrapper_end(
5045                &mut writer,
5046                "c:tx",
5047                self.name_markup
5048                    .as_ref()
5049                    .unwrap_or(&WrapperMarkup::default()),
5050            )?;
5051        }
5052        emit_raw(&mut writer, self.raw_children.at(3))?;
5053        if let Some(properties) = &self.sp_pr {
5054            properties.write_xml_as(&mut writer, "c:spPr")?;
5055        }
5056        emit_raw(&mut writer, self.raw_children.at(4))?;
5057        for point in &self.authored_data_points {
5058            point.write_xml(&mut writer)?;
5059        }
5060        if let Some(labels) = &self.data_labels {
5061            labels.write_xml(&mut writer, false)?;
5062        }
5063        emit_raw(&mut writer, self.raw_children.at(5))?;
5064        if let Some(categories) = &self.categories {
5065            let default_markup = WrapperMarkup::default();
5066            let markup = self.categories_markup.as_ref().unwrap_or(&default_markup);
5067            write_wrapper_start(
5068                &mut writer,
5069                if scatter { "c:xVal" } else { "c:cat" },
5070                markup,
5071            )?;
5072            match categories {
5073                AxisData::String(reference) => reference.write_xml(&mut writer, false)?,
5074                AxisData::Numeric(reference) => reference.write_xml(&mut writer, false)?,
5075            }
5076            write_wrapper_end(
5077                &mut writer,
5078                if scatter { "c:xVal" } else { "c:cat" },
5079                markup,
5080            )?;
5081        }
5082        emit_raw(&mut writer, self.raw_children.at(6))?;
5083        write_wrapper_start(
5084            &mut writer,
5085            if scatter { "c:yVal" } else { "c:val" },
5086            &self.values_markup,
5087        )?;
5088        self.values.write_xml(&mut writer, false)?;
5089        write_wrapper_end(
5090            &mut writer,
5091            if scatter { "c:yVal" } else { "c:val" },
5092            &self.values_markup,
5093        )?;
5094        emit_raw(&mut writer, self.raw_children.at(7))?;
5095        if let Some(size) = &self.bubble_size {
5096            let default_markup = WrapperMarkup::default();
5097            let markup = self.bubble_size_markup.as_ref().unwrap_or(&default_markup);
5098            write_wrapper_start(&mut writer, "c:bubbleSize", markup)?;
5099            size.write_xml(&mut writer, false)?;
5100            write_wrapper_end(&mut writer, "c:bubbleSize", markup)?;
5101        }
5102        emit_raw(&mut writer, self.raw_children.at(8))?;
5103        writer
5104            .write_event(Event::End(BytesEnd::new("c:ser")))
5105            .map_err(OxmlError::from)?;
5106        Ok(writer.into_inner())
5107    }
5108
5109    pub fn raw_children(&self) -> &OrderedRawChildren {
5110        &self.raw_children
5111    }
5112}
5113
5114#[derive(Default)]
5115struct SeriesParseState {
5116    index: Option<(u32, ScalarMarkup)>,
5117    order: Option<(u32, ScalarMarkup)>,
5118    name: Option<(StringRef, WrapperMarkup)>,
5119    categories: Option<(AxisData, WrapperMarkup)>,
5120    values: Option<(NumericData, WrapperMarkup)>,
5121    bubble_size: Option<(NumericData, WrapperMarkup)>,
5122    data_labels: Option<CT_DLbls>,
5123    name_seen: bool,
5124    categories_seen: bool,
5125    bubble_size_seen: bool,
5126    opaque_name: bool,
5127    opaque_categories: bool,
5128    opaque_bubble_size: bool,
5129    saw_standard_wrapper: bool,
5130    saw_scatter_wrapper: bool,
5131    sp_pr: Option<CT_ShapeProperties>,
5132    raw_children: OrderedRawChildren,
5133    boundary: usize,
5134}
5135
5136impl SeriesParseState {
5137    fn capture_event(&mut self, raw: Vec<u8>) {
5138        self.raw_children.push(self.boundary, raw);
5139    }
5140
5141    fn parse_child(
5142        &mut self,
5143        name: &[u8],
5144        raw: Vec<u8>,
5145        namespaces: &NamespaceBindings,
5146    ) -> Result<()> {
5147        match name {
5148            b"idx" => {
5149                set_once(&mut self.index, parse_u32_scalar(&raw, "idx")?, "c:idx")?;
5150                self.boundary = self.boundary.max(1);
5151            }
5152            b"order" => {
5153                set_once(&mut self.order, parse_u32_scalar(&raw, "order")?, "c:order")?;
5154                self.boundary = self.boundary.max(2);
5155            }
5156            b"tx" => {
5157                mark_once(&mut self.name_seen, "c:tx")?;
5158                let parsed = parse_wrapper(&raw, b"tx", &[b"strRef"], namespaces)?;
5159                if let Some((_, reference)) = parsed.choice {
5160                    self.name = Some((
5161                        StringRef::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
5162                        parsed.markup,
5163                    ));
5164                } else {
5165                    self.opaque_name = true;
5166                    self.raw_children.push(2, raw);
5167                }
5168                self.boundary = self.boundary.max(3);
5169            }
5170            b"spPr" => {
5171                parse_chart_shape_properties(&raw, namespaces, &mut self.sp_pr)?;
5172                self.boundary = self.boundary.max(4);
5173            }
5174            b"dLbls" => {
5175                set_once(
5176                    &mut self.data_labels,
5177                    CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
5178                    "c:dLbls",
5179                )?;
5180                self.boundary = self.boundary.max(5);
5181            }
5182            b"cat" => {
5183                self.saw_standard_wrapper = true;
5184                mark_once(&mut self.categories_seen, "c:cat")?;
5185                let parsed = parse_wrapper(&raw, b"cat", &[b"strRef", b"numRef"], namespaces)?;
5186                if let Some((choice, reference)) = parsed.choice {
5187                    let categories = if choice == b"strRef" {
5188                        AxisData::String(StringRef::from_xml_with_namespaces(
5189                            &reference,
5190                            &parsed.namespaces,
5191                        )?)
5192                    } else {
5193                        AxisData::Numeric(NumericData::from_xml_with_namespaces(
5194                            &reference,
5195                            &parsed.namespaces,
5196                        )?)
5197                    };
5198                    self.categories = Some((categories, parsed.markup));
5199                } else {
5200                    self.opaque_categories = true;
5201                    self.raw_children.push(5, raw);
5202                }
5203                self.boundary = self.boundary.max(6);
5204            }
5205            b"xVal" => {
5206                self.saw_scatter_wrapper = true;
5207                mark_once(&mut self.categories_seen, "c:xVal")?;
5208                let parsed = parse_wrapper(&raw, b"xVal", &[b"numRef"], namespaces)?;
5209                let (_, reference) = parsed
5210                    .choice
5211                    .ok_or_else(|| ChartError::MissingElement("c:xVal/c:numRef".to_owned()))?;
5212                self.categories = Some((
5213                    AxisData::Numeric(NumericData::from_xml_with_namespaces(
5214                        &reference,
5215                        &parsed.namespaces,
5216                    )?),
5217                    parsed.markup,
5218                ));
5219                self.boundary = self.boundary.max(6);
5220            }
5221            b"val" | b"yVal" => {
5222                if name == b"yVal" {
5223                    self.saw_scatter_wrapper = true;
5224                } else {
5225                    self.saw_standard_wrapper = true;
5226                }
5227                let wrapper = if name == b"yVal" {
5228                    b"yVal".as_slice()
5229                } else {
5230                    b"val".as_slice()
5231                };
5232                let parsed = parse_wrapper(&raw, wrapper, &[b"numRef"], namespaces)?;
5233                let (_, reference) = parsed.choice.ok_or_else(|| {
5234                    ChartError::MissingElement(format!(
5235                        "c:{}/c:numRef",
5236                        String::from_utf8_lossy(wrapper)
5237                    ))
5238                })?;
5239                set_once(
5240                    &mut self.values,
5241                    (
5242                        NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
5243                        parsed.markup,
5244                    ),
5245                    "c:val or c:yVal",
5246                )?;
5247                self.boundary = self.boundary.max(7);
5248            }
5249            b"bubbleSize" => {
5250                mark_once(&mut self.bubble_size_seen, "c:bubbleSize")?;
5251                let parsed = parse_wrapper(&raw, b"bubbleSize", &[b"numRef"], namespaces)?;
5252                if let Some((_, reference)) = parsed.choice {
5253                    self.bubble_size = Some((
5254                        NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
5255                        parsed.markup,
5256                    ));
5257                } else {
5258                    self.opaque_bubble_size = true;
5259                    self.raw_children.push(7, raw);
5260                }
5261                self.boundary = self.boundary.max(8);
5262            }
5263            _ => {
5264                let boundary = series_raw_boundary(name, self.boundary);
5265                self.raw_children.push(boundary, raw);
5266                self.boundary = self.boundary.max(boundary);
5267            }
5268        }
5269        Ok(())
5270    }
5271
5272    fn finish(
5273        self,
5274        raw_attributes: XmlAttributes,
5275        namespace_declarations: XmlAttributes,
5276    ) -> Result<Series> {
5277        if self.saw_standard_wrapper && self.saw_scatter_wrapper {
5278            return Err(ChartError::InvalidValue {
5279                element: "c:ser".to_owned(),
5280                value: "category/value and x/y wrappers cannot be combined".to_owned(),
5281            });
5282        }
5283        let (index, index_markup) = self
5284            .index
5285            .ok_or_else(|| ChartError::MissingElement("c:idx".to_owned()))?;
5286        let (order, order_markup) = self
5287            .order
5288            .ok_or_else(|| ChartError::MissingElement("c:order".to_owned()))?;
5289        let (values, values_markup) = self
5290            .values
5291            .ok_or_else(|| ChartError::MissingElement("c:val".to_owned()))?;
5292        let (name, name_markup) = self
5293            .name
5294            .map(|(value, markup)| (Some(value), Some(markup)))
5295            .unwrap_or((None, None));
5296        let (categories, categories_markup) = self
5297            .categories
5298            .map(|(value, markup)| (Some(value), Some(markup)))
5299            .unwrap_or((None, None));
5300        let (bubble_size, bubble_size_markup) = self
5301            .bubble_size
5302            .map(|(value, markup)| (Some(value), Some(markup)))
5303            .unwrap_or((None, None));
5304        Ok(Series {
5305            index,
5306            order,
5307            name,
5308            categories,
5309            values,
5310            bubble_size,
5311            sp_pr: self.sp_pr,
5312            data_labels: self.data_labels,
5313            authored_data_points: Vec::new(),
5314            index_markup,
5315            order_markup,
5316            name_markup,
5317            categories_markup,
5318            values_markup,
5319            bubble_size_markup,
5320            opaque_name: self.opaque_name,
5321            opaque_categories: self.opaque_categories,
5322            opaque_bubble_size: self.opaque_bubble_size,
5323            uses_scatter_wrappers: self.saw_scatter_wrapper,
5324            parsed_from_xml: true,
5325            raw_attributes,
5326            namespace_declarations,
5327            raw_children: raw_children_in_schema_order(&self.raw_children, 8),
5328        })
5329    }
5330}
5331
5332fn series_raw_boundary(name: &[u8], current: usize) -> usize {
5333    match name {
5334        b"marker" | b"invertIfNegative" | b"pictureOptions" | b"explosion" | b"dPt" => 4,
5335        b"trendline" | b"errBars" => 5,
5336        b"shape" | b"smooth" => 7,
5337        b"extLst" => 8,
5338        _ => current,
5339    }
5340}
5341
5342struct ParsedReference {
5343    formula: String,
5344    format_code: Option<String>,
5345    values: Vec<String>,
5346    markup: ReferenceMarkup,
5347}
5348
5349fn parse_reference(
5350    xml: &[u8],
5351    reference_local: &[u8],
5352    cache_local: &[u8],
5353    numeric: bool,
5354    inherited: &NamespaceBindings,
5355) -> Result<ParsedReference> {
5356    let mut reader = Reader::from_reader(xml);
5357    let mut buffer = Vec::new();
5358    loop {
5359        match reader
5360            .read_event_into(&mut buffer)
5361            .map_err(OxmlError::from)?
5362        {
5363            Event::Start(element)
5364                if matches_local_name(element.name().as_ref(), reference_local) =>
5365            {
5366                let namespaces = typed_rewrite_bindings(&element, inherited)?;
5367                let raw_attributes = capture_fixed_attributes(&element, &["xmlns:c"])?;
5368                let mut formula: Option<(String, TextMarkup)> = None;
5369                let mut cache: Option<(Option<String>, Vec<String>, CacheMarkup)> = None;
5370                let mut raw_children = OrderedRawChildren::default();
5371                let mut boundary = 0usize;
5372                let mut inner = Vec::new();
5373                loop {
5374                    match reader
5375                        .read_event_into(&mut inner)
5376                        .map_err(OxmlError::from)?
5377                    {
5378                        Event::Start(child) => {
5379                            let name = chart_child_local(&child, &namespaces)?;
5380                            let raw = capture_element(&mut reader, &child)?;
5381                            match name.as_deref().unwrap_or_default() {
5382                                b"f" => {
5383                                    set_once(
5384                                        &mut formula,
5385                                        parse_text_element(&raw, b"f", &namespaces)?,
5386                                        "c:f",
5387                                    )?;
5388                                    boundary = boundary.max(1);
5389                                }
5390                                name if name == cache_local => {
5391                                    set_once(
5392                                        &mut cache,
5393                                        parse_cache(&raw, cache_local, numeric, &namespaces)?,
5394                                        &format!("c:{}", String::from_utf8_lossy(cache_local)),
5395                                    )?;
5396                                    boundary = boundary.max(2);
5397                                }
5398                                _ => raw_children.push(boundary, raw),
5399                            }
5400                        }
5401                        Event::Empty(child) => {
5402                            let name = chart_child_local(&child, &namespaces)?;
5403                            let raw = capture_empty_element(&child)?;
5404                            match name.as_deref().unwrap_or_default() {
5405                                b"f" => {
5406                                    set_once(
5407                                        &mut formula,
5408                                        parse_text_element(&raw, b"f", &namespaces)?,
5409                                        "c:f",
5410                                    )?;
5411                                    boundary = boundary.max(1);
5412                                }
5413                                name if name == cache_local => {
5414                                    set_once(
5415                                        &mut cache,
5416                                        parse_cache(&raw, cache_local, numeric, &namespaces)?,
5417                                        &format!("c:{}", String::from_utf8_lossy(cache_local)),
5418                                    )?;
5419                                    boundary = boundary.max(2);
5420                                }
5421                                _ => raw_children.push(boundary, raw),
5422                            }
5423                        }
5424                        event @ (Event::Text(_)
5425                        | Event::CData(_)
5426                        | Event::Comment(_)
5427                        | Event::PI(_)
5428                        | Event::GeneralRef(_)) => {
5429                            raw_children.push(boundary, capture_event(event)?);
5430                        }
5431                        Event::End(end)
5432                            if matches_local_name(end.name().as_ref(), reference_local) =>
5433                        {
5434                            let (formula, formula_markup) = formula
5435                                .ok_or_else(|| ChartError::MissingElement("c:f".to_owned()))?;
5436                            validate_formula(&formula, "c:f")?;
5437                            let (format_code, values, cache_markup) = cache.ok_or_else(|| {
5438                                ChartError::MissingElement(format!(
5439                                    "c:{}",
5440                                    String::from_utf8_lossy(cache_local)
5441                                ))
5442                            })?;
5443                            let markup = ReferenceMarkup {
5444                                raw_attributes,
5445                                raw_children: raw_children_in_schema_order(&raw_children, 2),
5446                                formula: formula_markup,
5447                                cache_attributes: cache_markup.raw_attributes,
5448                                cache_children: cache_markup.raw_children,
5449                                format_code: cache_markup.format_code,
5450                                format_code_omitted: cache_markup.format_code_omitted,
5451                                point_count: cache_markup.point_count,
5452                                declared_point_count: cache_markup.declared_point_count,
5453                                point_indexes: cache_markup.point_indexes,
5454                                points: cache_markup.points,
5455                            };
5456                            return Ok(ParsedReference {
5457                                formula,
5458                                format_code,
5459                                values,
5460                                markup,
5461                            });
5462                        }
5463                        Event::Eof => {
5464                            return Err(missing_end(&format!(
5465                                "c:{}",
5466                                String::from_utf8_lossy(reference_local)
5467                            )));
5468                        }
5469                        _ => {}
5470                    }
5471                    inner.clear();
5472                }
5473            }
5474            Event::Empty(element)
5475                if matches_local_name(element.name().as_ref(), reference_local) =>
5476            {
5477                return Err(ChartError::MissingElement("c:f".to_owned()));
5478            }
5479            Event::Start(element) | Event::Empty(element) => {
5480                return Err(ChartError::UnexpectedElement(element_name(&element)));
5481            }
5482            Event::Eof => {
5483                return Err(ChartError::MissingElement(format!(
5484                    "c:{}",
5485                    String::from_utf8_lossy(reference_local)
5486                )));
5487            }
5488            _ => {}
5489        }
5490        buffer.clear();
5491    }
5492}
5493
5494struct CacheMarkup {
5495    raw_attributes: XmlAttributes,
5496    raw_children: OrderedRawChildren,
5497    format_code: Option<TextMarkup>,
5498    format_code_omitted: bool,
5499    point_count: ScalarMarkup,
5500    declared_point_count: Option<u32>,
5501    point_indexes: Vec<u32>,
5502    points: Vec<PointMarkup>,
5503}
5504
5505fn parse_cache(
5506    xml: &[u8],
5507    cache_local: &[u8],
5508    numeric: bool,
5509    inherited: &NamespaceBindings,
5510) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
5511    let mut reader = Reader::from_reader(xml);
5512    let mut buffer = Vec::new();
5513    loop {
5514        match reader
5515            .read_event_into(&mut buffer)
5516            .map_err(OxmlError::from)?
5517        {
5518            Event::Start(element) if matches_local_name(element.name().as_ref(), cache_local) => {
5519                let namespaces = typed_rewrite_bindings(&element, inherited)?;
5520                let raw_attributes = capture_attributes(&element)?;
5521                let mut state = CacheParseState::new(numeric, namespaces);
5522                let mut inner = Vec::new();
5523                loop {
5524                    match reader
5525                        .read_event_into(&mut inner)
5526                        .map_err(OxmlError::from)?
5527                    {
5528                        Event::Start(child) => {
5529                            let name = chart_child_local(&child, &state.namespaces)?;
5530                            let raw = capture_element(&mut reader, &child)?;
5531                            state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
5532                        }
5533                        Event::Empty(child) => {
5534                            let name = chart_child_local(&child, &state.namespaces)?;
5535                            let raw = capture_empty_element(&child)?;
5536                            state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
5537                        }
5538                        event @ (Event::Text(_)
5539                        | Event::CData(_)
5540                        | Event::Comment(_)
5541                        | Event::PI(_)
5542                        | Event::GeneralRef(_)) => {
5543                            state.capture_event(capture_event(event)?);
5544                        }
5545                        Event::End(end) if matches_local_name(end.name().as_ref(), cache_local) => {
5546                            return state.finish(raw_attributes);
5547                        }
5548                        Event::Eof => {
5549                            return Err(missing_end(&format!(
5550                                "c:{}",
5551                                String::from_utf8_lossy(cache_local)
5552                            )));
5553                        }
5554                        _ => {}
5555                    }
5556                    inner.clear();
5557                }
5558            }
5559            Event::Empty(element) if matches_local_name(element.name().as_ref(), cache_local) => {
5560                return Err(ChartError::MissingElement("c:ptCount".to_owned()));
5561            }
5562            Event::Start(element) | Event::Empty(element) => {
5563                return Err(ChartError::UnexpectedElement(element_name(&element)));
5564            }
5565            Event::Eof => {
5566                return Err(ChartError::MissingElement(format!(
5567                    "c:{}",
5568                    String::from_utf8_lossy(cache_local)
5569                )));
5570            }
5571            _ => {}
5572        }
5573        buffer.clear();
5574    }
5575}
5576
5577struct CacheParseState {
5578    numeric: bool,
5579    namespaces: NamespaceBindings,
5580    point_base: usize,
5581    boundary: usize,
5582    format_code: Option<(String, TextMarkup)>,
5583    point_count: Option<(u32, ScalarMarkup)>,
5584    points: Vec<(u32, String, PointMarkup)>,
5585    raw_children: OrderedRawChildren,
5586}
5587
5588impl CacheParseState {
5589    fn new(numeric: bool, namespaces: NamespaceBindings) -> Self {
5590        Self {
5591            numeric,
5592            namespaces,
5593            point_base: usize::from(numeric) + 1,
5594            boundary: 0,
5595            format_code: None,
5596            point_count: None,
5597            points: Vec::new(),
5598            raw_children: OrderedRawChildren::default(),
5599        }
5600    }
5601
5602    fn capture_event(&mut self, raw: Vec<u8>) {
5603        self.raw_children.push(self.boundary, raw);
5604    }
5605
5606    fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
5607        match name {
5608            b"formatCode" if self.numeric => {
5609                set_once(
5610                    &mut self.format_code,
5611                    parse_text_element(&raw, b"formatCode", &self.namespaces)?,
5612                    "c:formatCode",
5613                )?;
5614                self.boundary = self.boundary.max(1);
5615            }
5616            b"ptCount" => {
5617                set_once(
5618                    &mut self.point_count,
5619                    parse_u32_scalar(&raw, "ptCount")?,
5620                    "c:ptCount",
5621                )?;
5622                self.boundary = self.boundary.max(self.point_base);
5623            }
5624            b"pt" => {
5625                self.points.push(parse_point(&raw, &self.namespaces)?);
5626                self.boundary = self.boundary.max(self.point_base + self.points.len());
5627            }
5628            _ => self.raw_children.push(self.boundary, raw),
5629        }
5630        Ok(())
5631    }
5632
5633    fn finish(
5634        self,
5635        raw_attributes: XmlAttributes,
5636    ) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
5637        let (declared, point_count_markup) = self
5638            .point_count
5639            .ok_or_else(|| ChartError::MissingElement("c:ptCount".to_owned()))?;
5640        let actual = u32::try_from(self.points.len()).map_err(|_| ChartError::InvalidValue {
5641            element: "c:ptCount".to_owned(),
5642            value: self.points.len().to_string(),
5643        })?;
5644        if declared < actual {
5645            return Err(invalid_attribute("ptCount", "val", declared.to_string()));
5646        }
5647        let mut values = Vec::with_capacity(self.points.len());
5648        let mut point_indexes = Vec::with_capacity(self.points.len());
5649        let mut point_markup = Vec::with_capacity(self.points.len());
5650        let mut previous = None;
5651        for (index, value, markup) in self.points {
5652            if index >= declared || previous.is_some_and(|last| index <= last) {
5653                return Err(invalid_attribute("pt", "idx", index.to_string()));
5654            }
5655            previous = Some(index);
5656            values.push(value);
5657            point_indexes.push(index);
5658            point_markup.push(markup);
5659        }
5660        let (format_code, format_markup) = self
5661            .format_code
5662            .map(|(value, markup)| (Some(value), Some(markup)))
5663            .unwrap_or((None, None));
5664        let format_code_omitted = self.numeric && format_code.is_none();
5665        Ok((
5666            format_code,
5667            values,
5668            CacheMarkup {
5669                raw_attributes,
5670                raw_children: raw_children_in_schema_order(
5671                    &self.raw_children,
5672                    self.point_base + point_markup.len(),
5673                ),
5674                format_code: format_markup,
5675                format_code_omitted,
5676                point_count: point_count_markup,
5677                declared_point_count: Some(declared),
5678                point_indexes,
5679                points: point_markup,
5680            },
5681        ))
5682    }
5683}
5684
5685fn parse_point(xml: &[u8], inherited: &NamespaceBindings) -> Result<(u32, String, PointMarkup)> {
5686    let mut reader = Reader::from_reader(xml);
5687    let mut buffer = Vec::new();
5688    loop {
5689        match reader
5690            .read_event_into(&mut buffer)
5691            .map_err(OxmlError::from)?
5692        {
5693            Event::Start(element) if matches_local_name(element.name().as_ref(), b"pt") => {
5694                let namespaces = typed_rewrite_bindings(&element, inherited)?;
5695                let index = required_u32_attribute(&element, "pt", b"idx")?;
5696                let raw_attributes = capture_attributes_except(&element, b"idx")?;
5697                let mut value: Option<(String, TextMarkup)> = None;
5698                let mut raw_children = OrderedRawChildren::default();
5699                let mut boundary = 0usize;
5700                let mut inner = Vec::new();
5701                loop {
5702                    match reader
5703                        .read_event_into(&mut inner)
5704                        .map_err(OxmlError::from)?
5705                    {
5706                        Event::Start(child) => {
5707                            let name = chart_child_local(&child, &namespaces)?;
5708                            let raw = capture_element(&mut reader, &child)?;
5709                            if name.as_deref() == Some(b"v") {
5710                                set_once(
5711                                    &mut value,
5712                                    parse_text_element(&raw, b"v", &namespaces)?,
5713                                    "c:v",
5714                                )?;
5715                                boundary = 1;
5716                            } else {
5717                                raw_children.push(boundary, raw);
5718                            }
5719                        }
5720                        Event::Empty(child) => {
5721                            let name = chart_child_local(&child, &namespaces)?;
5722                            let raw = capture_empty_element(&child)?;
5723                            if name.as_deref() == Some(b"v") {
5724                                set_once(
5725                                    &mut value,
5726                                    parse_text_element(&raw, b"v", &namespaces)?,
5727                                    "c:v",
5728                                )?;
5729                                boundary = 1;
5730                            } else {
5731                                raw_children.push(boundary, raw);
5732                            }
5733                        }
5734                        event @ (Event::Text(_)
5735                        | Event::CData(_)
5736                        | Event::Comment(_)
5737                        | Event::PI(_)
5738                        | Event::GeneralRef(_)) => {
5739                            raw_children.push(boundary, capture_event(event)?);
5740                        }
5741                        Event::End(end) if matches_local_name(end.name().as_ref(), b"pt") => {
5742                            let (value, value_markup) = value
5743                                .ok_or_else(|| ChartError::MissingElement("c:v".to_owned()))?;
5744                            return Ok((
5745                                index,
5746                                value,
5747                                PointMarkup {
5748                                    raw_attributes,
5749                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
5750                                    value: value_markup,
5751                                },
5752                            ));
5753                        }
5754                        Event::Eof => return Err(missing_end("c:pt")),
5755                        _ => {}
5756                    }
5757                    inner.clear();
5758                }
5759            }
5760            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pt") => {
5761                return Err(ChartError::MissingElement("c:v".to_owned()));
5762            }
5763            Event::Start(element) | Event::Empty(element) => {
5764                return Err(ChartError::UnexpectedElement(element_name(&element)));
5765            }
5766            Event::Eof => return Err(ChartError::MissingElement("c:pt".to_owned())),
5767            _ => {}
5768        }
5769        buffer.clear();
5770    }
5771}
5772
5773fn parse_text_element(
5774    xml: &[u8],
5775    local: &[u8],
5776    inherited: &NamespaceBindings,
5777) -> Result<(String, TextMarkup)> {
5778    let mut reader = Reader::from_reader(xml);
5779    let mut buffer = Vec::new();
5780    loop {
5781        match reader
5782            .read_event_into(&mut buffer)
5783            .map_err(OxmlError::from)?
5784        {
5785            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
5786                typed_rewrite_bindings(&element, inherited)?;
5787                let raw_attributes = capture_attributes(&element)?;
5788                let mut value = String::new();
5789                let mut raw_children = OrderedRawChildren::default();
5790                let mut seen_text = false;
5791                let mut inner = Vec::new();
5792                loop {
5793                    match reader
5794                        .read_event_into(&mut inner)
5795                        .map_err(OxmlError::from)?
5796                    {
5797                        Event::Text(text) => {
5798                            value.push_str(&decode_plain(&text));
5799                            seen_text = true;
5800                        }
5801                        Event::CData(text) => {
5802                            let decoded =
5803                                text.decode().map_err(|error| ChartError::InvalidValue {
5804                                    element: format!("c:{}", String::from_utf8_lossy(local)),
5805                                    value: error.to_string(),
5806                                })?;
5807                            value.push_str(&decoded);
5808                            seen_text = true;
5809                        }
5810                        Event::GeneralRef(reference) => {
5811                            value.push_str(&resolve_entity(&reference));
5812                            seen_text = true;
5813                        }
5814                        event @ (Event::Comment(_) | Event::PI(_)) => {
5815                            raw_children.push(usize::from(seen_text), capture_event(event)?);
5816                        }
5817                        Event::Start(child) | Event::Empty(child) => {
5818                            return Err(ChartError::UnexpectedElement(element_name(&child)));
5819                        }
5820                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
5821                            return Ok((
5822                                value,
5823                                TextMarkup {
5824                                    raw_attributes,
5825                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
5826                                },
5827                            ));
5828                        }
5829                        Event::Eof => {
5830                            return Err(missing_end(&format!(
5831                                "c:{}",
5832                                String::from_utf8_lossy(local)
5833                            )));
5834                        }
5835                        _ => {}
5836                    }
5837                    inner.clear();
5838                }
5839            }
5840            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
5841                typed_rewrite_bindings(&element, inherited)?;
5842                return Ok((
5843                    String::new(),
5844                    TextMarkup {
5845                        raw_attributes: capture_attributes(&element)?,
5846                        raw_children: OrderedRawChildren::default(),
5847                    },
5848                ));
5849            }
5850            Event::Start(element) | Event::Empty(element) => {
5851                return Err(ChartError::UnexpectedElement(element_name(&element)));
5852            }
5853            Event::Eof => {
5854                return Err(ChartError::MissingElement(format!(
5855                    "c:{}",
5856                    String::from_utf8_lossy(local)
5857                )));
5858            }
5859            _ => {}
5860        }
5861        buffer.clear();
5862    }
5863}
5864
5865struct ParsedWrapper {
5866    choice: Option<(Vec<u8>, Vec<u8>)>,
5867    markup: WrapperMarkup,
5868    namespaces: NamespaceBindings,
5869}
5870
5871fn parse_wrapper(
5872    xml: &[u8],
5873    local: &[u8],
5874    choices: &[&[u8]],
5875    inherited: &NamespaceBindings,
5876) -> Result<ParsedWrapper> {
5877    let mut reader = Reader::from_reader(xml);
5878    let mut buffer = Vec::new();
5879    loop {
5880        match reader
5881            .read_event_into(&mut buffer)
5882            .map_err(OxmlError::from)?
5883        {
5884            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
5885                let namespaces = typed_rewrite_bindings(&element, inherited)?;
5886                let raw_attributes = capture_attributes(&element)?;
5887                let mut choice = None;
5888                let mut raw_children = OrderedRawChildren::default();
5889                let mut boundary = 0usize;
5890                let mut inner = Vec::new();
5891                loop {
5892                    match reader
5893                        .read_event_into(&mut inner)
5894                        .map_err(OxmlError::from)?
5895                    {
5896                        Event::Start(child) => {
5897                            let name = chart_child_local(&child, &namespaces)?;
5898                            let raw = capture_element(&mut reader, &child)?;
5899                            if let Some(name) =
5900                                name.filter(|name| choices.contains(&name.as_slice()))
5901                            {
5902                                if choice.is_some() {
5903                                    return Err(ChartError::DuplicateElement(format!(
5904                                        "c:{} reference",
5905                                        String::from_utf8_lossy(local)
5906                                    )));
5907                                }
5908                                choice = Some((name, raw));
5909                                boundary = 1;
5910                            } else {
5911                                raw_children.push(boundary, raw);
5912                            }
5913                        }
5914                        Event::Empty(child) => {
5915                            let name = chart_child_local(&child, &namespaces)?;
5916                            let raw = capture_empty_element(&child)?;
5917                            if let Some(name) =
5918                                name.filter(|name| choices.contains(&name.as_slice()))
5919                            {
5920                                if choice.is_some() {
5921                                    return Err(ChartError::DuplicateElement(format!(
5922                                        "c:{} reference",
5923                                        String::from_utf8_lossy(local)
5924                                    )));
5925                                }
5926                                choice = Some((name, raw));
5927                                boundary = 1;
5928                            } else {
5929                                raw_children.push(boundary, raw);
5930                            }
5931                        }
5932                        event @ (Event::Text(_)
5933                        | Event::CData(_)
5934                        | Event::Comment(_)
5935                        | Event::PI(_)
5936                        | Event::GeneralRef(_)) => {
5937                            raw_children.push(boundary, capture_event(event)?);
5938                        }
5939                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
5940                            return Ok(ParsedWrapper {
5941                                choice,
5942                                markup: WrapperMarkup {
5943                                    raw_attributes,
5944                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
5945                                },
5946                                namespaces,
5947                            });
5948                        }
5949                        Event::Eof => {
5950                            return Err(missing_end(&format!(
5951                                "c:{}",
5952                                String::from_utf8_lossy(local)
5953                            )));
5954                        }
5955                        _ => {}
5956                    }
5957                    inner.clear();
5958                }
5959            }
5960            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
5961                let namespaces = chart_bindings(inherited, &element)?;
5962                return Ok(ParsedWrapper {
5963                    choice: None,
5964                    markup: WrapperMarkup {
5965                        raw_attributes: capture_attributes(&element)?,
5966                        raw_children: OrderedRawChildren::default(),
5967                    },
5968                    namespaces,
5969                });
5970            }
5971            Event::Start(element) | Event::Empty(element) => {
5972                return Err(ChartError::UnexpectedElement(element_name(&element)));
5973            }
5974            Event::Eof => {
5975                return Err(ChartError::MissingElement(format!(
5976                    "c:{}",
5977                    String::from_utf8_lossy(local)
5978                )));
5979            }
5980            _ => {}
5981        }
5982        buffer.clear();
5983    }
5984}
5985
5986fn write_string_cache(
5987    writer: &mut Writer<Vec<u8>>,
5988    values: &[String],
5989    markup: &ReferenceMarkup,
5990) -> Result<()> {
5991    let (declared_count, indexes) = cache_layout(markup, values.len())?;
5992    let mut start = BytesStart::new("c:strCache");
5993    push_attributes(&mut start, &markup.cache_attributes);
5994    writer
5995        .write_event(Event::Start(start))
5996        .map_err(OxmlError::from)?;
5997    emit_raw(writer, markup.cache_children.at(0))?;
5998    write_scalar(
5999        writer,
6000        "c:ptCount",
6001        &declared_count.to_string(),
6002        Some(&markup.point_count),
6003    )?;
6004    emit_cache_point_raw(writer, markup, 1, values.len(), 0)?;
6005    for (position, value) in values.iter().enumerate() {
6006        let default_markup = PointMarkup::default();
6007        write_point(
6008            writer,
6009            indexes[position],
6010            value,
6011            markup.points.get(position).unwrap_or(&default_markup),
6012        )?;
6013        emit_cache_point_raw(writer, markup, 1, values.len(), position + 1)?;
6014    }
6015    writer
6016        .write_event(Event::End(BytesEnd::new("c:strCache")))
6017        .map_err(OxmlError::from)?;
6018    Ok(())
6019}
6020
6021fn write_numeric_cache(
6022    writer: &mut Writer<Vec<u8>>,
6023    data: &NumericData,
6024    markup: &ReferenceMarkup,
6025) -> Result<()> {
6026    let (declared_count, indexes) = cache_layout(markup, data.values.len())?;
6027    let mut start = BytesStart::new("c:numCache");
6028    push_attributes(&mut start, &markup.cache_attributes);
6029    writer
6030        .write_event(Event::Start(start))
6031        .map_err(OxmlError::from)?;
6032    emit_raw(writer, markup.cache_children.at(0))?;
6033    if !markup.format_code_omitted || data.format_code != "General" {
6034        let default_format_markup = TextMarkup::default();
6035        write_text(
6036            writer,
6037            "c:formatCode",
6038            &data.format_code,
6039            markup
6040                .format_code
6041                .as_ref()
6042                .unwrap_or(&default_format_markup),
6043        )?;
6044    }
6045    emit_raw(writer, markup.cache_children.at(1))?;
6046    write_scalar(
6047        writer,
6048        "c:ptCount",
6049        &declared_count.to_string(),
6050        Some(&markup.point_count),
6051    )?;
6052    emit_cache_point_raw(writer, markup, 2, data.values.len(), 0)?;
6053    for (position, value) in data.values.iter().enumerate() {
6054        let default_markup = PointMarkup::default();
6055        write_point(
6056            writer,
6057            indexes[position],
6058            &value.to_string(),
6059            markup.points.get(position).unwrap_or(&default_markup),
6060        )?;
6061        emit_cache_point_raw(writer, markup, 2, data.values.len(), position + 1)?;
6062    }
6063    writer
6064        .write_event(Event::End(BytesEnd::new("c:numCache")))
6065        .map_err(OxmlError::from)?;
6066    Ok(())
6067}
6068
6069fn emit_cache_point_raw(
6070    writer: &mut Writer<Vec<u8>>,
6071    markup: &ReferenceMarkup,
6072    point_base: usize,
6073    current_count: usize,
6074    completed_points: usize,
6075) -> Result<()> {
6076    let original_count = markup.points.len();
6077    if current_count == 0 {
6078        for boundary in point_base..=point_base + original_count {
6079            emit_raw(writer, markup.cache_children.at(boundary))?;
6080        }
6081    } else if completed_points == 0 {
6082        if original_count > 0 {
6083            emit_raw(writer, markup.cache_children.at(point_base))?;
6084        }
6085    } else if completed_points < current_count {
6086        if completed_points < original_count {
6087            emit_raw(
6088                writer,
6089                markup.cache_children.at(point_base + completed_points),
6090            )?;
6091        }
6092    } else {
6093        let first_tail = if original_count == 0 {
6094            point_base
6095        } else {
6096            point_base + current_count.min(original_count)
6097        };
6098        for boundary in first_tail..=point_base + original_count {
6099            emit_raw(writer, markup.cache_children.at(boundary))?;
6100        }
6101    }
6102    Ok(())
6103}
6104
6105fn write_point(
6106    writer: &mut Writer<Vec<u8>>,
6107    index: u32,
6108    value: &str,
6109    markup: &PointMarkup,
6110) -> Result<()> {
6111    let index = index.to_string();
6112    let mut start = BytesStart::new("c:pt");
6113    start.push_attribute(("idx", index.as_str()));
6114    push_attributes(&mut start, &markup.raw_attributes);
6115    writer
6116        .write_event(Event::Start(start))
6117        .map_err(OxmlError::from)?;
6118    emit_raw(writer, markup.raw_children.at(0))?;
6119    write_text(writer, "c:v", value, &markup.value)?;
6120    emit_raw(writer, markup.raw_children.at(1))?;
6121    writer
6122        .write_event(Event::End(BytesEnd::new("c:pt")))
6123        .map_err(OxmlError::from)?;
6124    Ok(())
6125}
6126
6127fn cache_layout(markup: &ReferenceMarkup, value_count: usize) -> Result<(u32, Vec<u32>)> {
6128    if markup.point_indexes.len() == value_count
6129        && let Some(declared) = markup.declared_point_count
6130    {
6131        return Ok((declared, markup.point_indexes.clone()));
6132    }
6133    let declared = u32::try_from(value_count).map_err(|_| ChartError::InvalidValue {
6134        element: "c:ptCount".to_owned(),
6135        value: value_count.to_string(),
6136    })?;
6137    Ok((declared, (0..declared).collect()))
6138}
6139
6140fn write_text(
6141    writer: &mut Writer<Vec<u8>>,
6142    tag: &str,
6143    value: &str,
6144    markup: &TextMarkup,
6145) -> Result<()> {
6146    let mut start = BytesStart::new(tag);
6147    push_attributes(&mut start, &markup.raw_attributes);
6148    writer
6149        .write_event(Event::Start(start))
6150        .map_err(OxmlError::from)?;
6151    emit_raw(writer, markup.raw_children.at(0))?;
6152    writer
6153        .write_event(Event::Text(BytesText::new(value)))
6154        .map_err(OxmlError::from)?;
6155    emit_raw(writer, markup.raw_children.at(1))?;
6156    writer
6157        .write_event(Event::End(BytesEnd::new(tag)))
6158        .map_err(OxmlError::from)?;
6159    Ok(())
6160}
6161
6162fn write_wrapper_start(
6163    writer: &mut Writer<Vec<u8>>,
6164    tag: &str,
6165    markup: &WrapperMarkup,
6166) -> Result<()> {
6167    let mut start = BytesStart::new(tag);
6168    push_attributes(&mut start, &markup.raw_attributes);
6169    writer
6170        .write_event(Event::Start(start))
6171        .map_err(OxmlError::from)?;
6172    emit_raw(writer, markup.raw_children.at(0))
6173}
6174
6175fn write_wrapper_end(
6176    writer: &mut Writer<Vec<u8>>,
6177    tag: &str,
6178    markup: &WrapperMarkup,
6179) -> Result<()> {
6180    emit_raw(writer, markup.raw_children.at(1))?;
6181    writer
6182        .write_event(Event::End(BytesEnd::new(tag)))
6183        .map_err(OxmlError::from)?;
6184    Ok(())
6185}
6186
6187fn validate_formula(formula: &str, element: &str) -> Result<()> {
6188    if formula.trim().is_empty() {
6189        return Err(ChartError::InvalidValue {
6190            element: element.to_owned(),
6191            value: formula.to_owned(),
6192        });
6193    }
6194    validate_xml_text(formula, element)
6195}
6196
6197fn validate_format_code(format_code: &str) -> Result<()> {
6198    if format_code.is_empty() {
6199        return Err(ChartError::InvalidValue {
6200            element: "c:formatCode".to_owned(),
6201            value: format_code.to_owned(),
6202        });
6203    }
6204    validate_xml_text(format_code, "c:formatCode")
6205}
6206
6207fn validate_xml_text(value: &str, element: &str) -> Result<()> {
6208    if value.chars().all(|character| {
6209        matches!(
6210            character,
6211            '\u{9}' | '\u{A}' | '\u{D}' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}'
6212        )
6213    }) {
6214        return Ok(());
6215    }
6216    Err(ChartError::InvalidValue {
6217        element: element.to_owned(),
6218        value: value.to_owned(),
6219    })
6220}
6221
6222fn validate_numeric_values(values: &[f64]) -> Result<()> {
6223    if let Some(value) = values.iter().find(|value| !value.is_finite()) {
6224        return Err(ChartError::InvalidValue {
6225            element: "c:v".to_owned(),
6226            value: value.to_string(),
6227        });
6228    }
6229    Ok(())
6230}
6231
6232fn validate_point_count(count: usize, element: &str) -> Result<()> {
6233    u32::try_from(count)
6234        .map(|_| ())
6235        .map_err(|_| ChartError::InvalidValue {
6236            element: element.to_owned(),
6237            value: count.to_string(),
6238        })
6239}
6240
6241fn parse_u32_scalar(xml: &[u8], local: &str) -> Result<(u32, ScalarMarkup)> {
6242    let (value, markup) = scalar_value(xml, local)?;
6243    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
6244    let parsed = value
6245        .parse::<u32>()
6246        .map_err(|_| invalid_attribute(local, "val", value))?;
6247    Ok((parsed, markup))
6248}
6249
6250fn required_u32_attribute(element: &BytesStart<'_>, local: &str, attribute: &[u8]) -> Result<u32> {
6251    let value = attribute_value(element, attribute)?.ok_or_else(|| {
6252        invalid_attribute(
6253            local,
6254            &String::from_utf8_lossy(attribute),
6255            "<missing>".to_owned(),
6256        )
6257    })?;
6258    value
6259        .parse::<u32>()
6260        .map_err(|_| invalid_attribute(local, &String::from_utf8_lossy(attribute), value))
6261}
6262
6263fn chart_root_prefix(element: &BytesStart<'_>) -> Result<Vec<u8>> {
6264    let name = element.name();
6265    let qualified = name.as_ref();
6266    let prefix = qualified
6267        .iter()
6268        .position(|byte| *byte == b':')
6269        .map(|position| qualified[..position].to_vec())
6270        .unwrap_or_default();
6271    let declaration = if prefix.is_empty() {
6272        b"xmlns".to_vec()
6273    } else {
6274        let mut declaration = b"xmlns:".to_vec();
6275        declaration.extend_from_slice(&prefix);
6276        declaration
6277    };
6278    if let Some(namespace) = attribute_value(element, &declaration)?
6279        && namespace != C_NS
6280    {
6281        return Err(invalid_attribute(
6282            &element_name(element),
6283            "namespace",
6284            namespace,
6285        ));
6286    }
6287    reject_conflicting_prefix(element, b"c", C_NS)?;
6288    Ok(prefix)
6289}
6290
6291fn typed_rewrite_bindings(
6292    element: &BytesStart<'_>,
6293    inherited: &NamespaceBindings,
6294) -> Result<NamespaceBindings> {
6295    chart_root_prefix(element)?;
6296    if !element_is_in_namespace(element, C_NS, inherited)? {
6297        return Err(ChartError::UnexpectedElement(element_name(element)));
6298    }
6299    let bindings = chart_bindings(inherited, element)?;
6300    require_fixed_namespace(&bindings, b"c", C_NS, element)?;
6301    Ok(bindings)
6302}
6303
6304fn chart_namespace_defaults() -> NamespaceBindings {
6305    vec![
6306        (b"c".to_vec(), C_NS.to_owned()),
6307        (b"a".to_vec(), A_NS.to_owned()),
6308        (b"r".to_vec(), R_NS.to_owned()),
6309    ]
6310}
6311
6312fn chart_bindings(
6313    inherited: &NamespaceBindings,
6314    element: &BytesStart<'_>,
6315) -> Result<NamespaceBindings> {
6316    let mut bindings = chart_namespace_defaults();
6317    for (prefix, namespace) in inherited
6318        .iter()
6319        .cloned()
6320        .chain(namespace_bindings(element)?)
6321    {
6322        upsert_namespace_binding(&mut bindings, prefix, namespace);
6323    }
6324    Ok(bindings)
6325}
6326
6327fn root_chart_bindings(
6328    xml: &[u8],
6329    local: &[u8],
6330    inherited: &NamespaceBindings,
6331) -> Result<NamespaceBindings> {
6332    let mut reader = Reader::from_reader(xml);
6333    let mut buffer = Vec::new();
6334    loop {
6335        match reader
6336            .read_event_into(&mut buffer)
6337            .map_err(OxmlError::from)?
6338        {
6339            Event::Start(element) | Event::Empty(element)
6340                if matches_local_name(element.name().as_ref(), local) =>
6341            {
6342                if !element_is_in_namespace(&element, C_NS, inherited)? {
6343                    return Err(ChartError::UnexpectedElement(element_name(&element)));
6344                }
6345                return chart_bindings(inherited, &element);
6346            }
6347            Event::Start(element) | Event::Empty(element) => {
6348                return Err(ChartError::UnexpectedElement(element_name(&element)));
6349            }
6350            Event::Eof => {
6351                return Err(ChartError::MissingElement(format!(
6352                    "c:{}",
6353                    String::from_utf8_lossy(local)
6354                )));
6355            }
6356            _ => {}
6357        }
6358        buffer.clear();
6359    }
6360}
6361
6362fn chart_child_local(
6363    element: &BytesStart<'_>,
6364    inherited: &NamespaceBindings,
6365) -> Result<Option<Vec<u8>>> {
6366    Ok(element_is_in_namespace(element, C_NS, inherited)?
6367        .then(|| local_name(element.name().as_ref()).to_vec()))
6368}
6369
6370fn capture_fixed_root_attributes(
6371    start: &BytesStart<'_>,
6372    fixed: &[&str],
6373) -> Result<(XmlAttributes, XmlAttributes)> {
6374    let mut attributes = Vec::new();
6375    let mut namespaces = Vec::new();
6376    for attribute in start.attributes() {
6377        let attribute = attribute.map_err(OxmlError::from)?;
6378        let name = std::str::from_utf8(attribute.key.as_ref())
6379            .map_err(OxmlError::from)?
6380            .to_owned();
6381        let value = attribute
6382            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
6383            .map_err(OxmlError::from)?
6384            .into_owned();
6385        if fixed.contains(&name.as_str()) {
6386            continue;
6387        }
6388        if name == "xmlns" || name.starts_with("xmlns:") {
6389            namespaces.push((name, value));
6390        } else {
6391            attributes.push((name, value));
6392        }
6393    }
6394    Ok((attributes, namespaces))
6395}
6396
6397fn require_fixed_namespace(
6398    bindings: &NamespaceBindings,
6399    prefix: &[u8],
6400    expected: &str,
6401    element: &BytesStart<'_>,
6402) -> Result<()> {
6403    let actual = bindings
6404        .iter()
6405        .find(|(candidate, _)| candidate.as_slice() == prefix)
6406        .map(|(_, namespace)| namespace.as_str());
6407    if actual == Some(expected) {
6408        return Ok(());
6409    }
6410    Err(invalid_attribute(
6411        &element_name(element),
6412        &format!("xmlns:{}", String::from_utf8_lossy(prefix)),
6413        actual.unwrap_or("<missing>").to_owned(),
6414    ))
6415}
6416
6417fn standalone_namespace_declarations(bindings: &NamespaceBindings) -> Result<XmlAttributes> {
6418    bindings
6419        .iter()
6420        .filter(|(prefix, _)| prefix != b"c" && prefix != b"a" && prefix != b"r")
6421        .map(|(prefix, namespace)| {
6422            let prefix = std::str::from_utf8(prefix).map_err(OxmlError::from)?;
6423            let name = if prefix.is_empty() {
6424                "xmlns".to_owned()
6425            } else {
6426                format!("xmlns:{prefix}")
6427            };
6428            Ok((name, namespace.clone()))
6429        })
6430        .collect()
6431}
6432
6433fn capture_fixed_attributes(start: &BytesStart<'_>, fixed: &[&str]) -> Result<XmlAttributes> {
6434    let mut attributes = Vec::new();
6435    for attribute in start.attributes() {
6436        let attribute = attribute.map_err(OxmlError::from)?;
6437        let name = std::str::from_utf8(attribute.key.as_ref())
6438            .map_err(OxmlError::from)?
6439            .to_owned();
6440        if fixed.contains(&name.as_str()) {
6441            continue;
6442        }
6443        let value = attribute
6444            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
6445            .map_err(OxmlError::from)?
6446            .into_owned();
6447        attributes.push((name, value));
6448    }
6449    Ok(attributes)
6450}
6451
6452/// A producer-compatible ChartML axis identifier.
6453///
6454/// ECMA-376 names an unsigned value, but PowerPoint emits signed 32-bit
6455/// lexical values in real chart parts. The accepted domain covers both forms.
6456#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
6457pub struct AxisId(i64);
6458
6459impl AxisId {
6460    pub fn new(value: i64) -> Result<Self> {
6461        if (i64::from(i32::MIN)..=i64::from(u32::MAX)).contains(&value) {
6462            Ok(Self(value))
6463        } else {
6464            Err(ChartError::InvalidValue {
6465                element: "c:axId".to_owned(),
6466                value: value.to_string(),
6467            })
6468        }
6469    }
6470
6471    pub const fn value(self) -> i64 {
6472        self.0
6473    }
6474}
6475
6476/// Whether bars extend along the category or value axis.
6477#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6478pub enum BarDirection {
6479    Bar,
6480    Column,
6481}
6482
6483impl BarDirection {
6484    fn parse(value: &str) -> Option<Self> {
6485        match value {
6486            "bar" => Some(Self::Bar),
6487            "col" => Some(Self::Column),
6488            _ => None,
6489        }
6490    }
6491
6492    const fn as_str(self) -> &'static str {
6493        match self {
6494            Self::Bar => "bar",
6495            Self::Column => "col",
6496        }
6497    }
6498}
6499
6500/// Grouping modes supported by a two-dimensional bar plot.
6501#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6502pub enum BarGrouping {
6503    Clustered,
6504    PercentStacked,
6505    Stacked,
6506    Standard,
6507}
6508
6509impl BarGrouping {
6510    fn parse(value: &str) -> Option<Self> {
6511        match value {
6512            "clustered" => Some(Self::Clustered),
6513            "percentStacked" => Some(Self::PercentStacked),
6514            "stacked" => Some(Self::Stacked),
6515            "standard" => Some(Self::Standard),
6516            _ => None,
6517        }
6518    }
6519
6520    const fn as_str(self) -> &'static str {
6521        match self {
6522            Self::Clustered => "clustered",
6523            Self::PercentStacked => "percentStacked",
6524            Self::Stacked => "stacked",
6525            Self::Standard => "standard",
6526        }
6527    }
6528}
6529
6530/// Grouping modes shared by line and later area plots.
6531#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6532pub enum Grouping {
6533    PercentStacked,
6534    Stacked,
6535    Standard,
6536}
6537
6538impl Grouping {
6539    fn parse(value: &str) -> Option<Self> {
6540        match value {
6541            "percentStacked" => Some(Self::PercentStacked),
6542            "stacked" => Some(Self::Stacked),
6543            "standard" => Some(Self::Standard),
6544            _ => None,
6545        }
6546    }
6547
6548    const fn as_str(self) -> &'static str {
6549        match self {
6550            Self::PercentStacked => "percentStacked",
6551            Self::Stacked => "stacked",
6552            Self::Standard => "standard",
6553        }
6554    }
6555}
6556
6557/// Display style for a two-dimensional scatter plot.
6558#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6559pub enum ScatterStyle {
6560    Line,
6561    LineMarker,
6562    Marker,
6563    None,
6564    Smooth,
6565    SmoothMarker,
6566}
6567
6568impl ScatterStyle {
6569    fn parse(value: &str) -> Option<Self> {
6570        match value {
6571            "line" => Some(Self::Line),
6572            "lineMarker" => Some(Self::LineMarker),
6573            "marker" => Some(Self::Marker),
6574            "none" => Some(Self::None),
6575            "smooth" => Some(Self::Smooth),
6576            "smoothMarker" => Some(Self::SmoothMarker),
6577            _ => None,
6578        }
6579    }
6580
6581    const fn as_str(self) -> &'static str {
6582        match self {
6583            Self::Line => "line",
6584            Self::LineMarker => "lineMarker",
6585            Self::Marker => "marker",
6586            Self::None => "none",
6587            Self::Smooth => "smooth",
6588            Self::SmoothMarker => "smoothMarker",
6589        }
6590    }
6591}
6592
6593/// Display style for a two-dimensional radar plot.
6594#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6595pub enum RadarStyle {
6596    Filled,
6597    Marker,
6598    Standard,
6599}
6600
6601impl RadarStyle {
6602    fn parse(value: &str) -> Option<Self> {
6603        match value {
6604            "filled" => Some(Self::Filled),
6605            "marker" => Some(Self::Marker),
6606            "standard" => Some(Self::Standard),
6607            _ => None,
6608        }
6609    }
6610
6611    const fn as_str(self) -> &'static str {
6612        match self {
6613            Self::Filled => "filled",
6614            Self::Marker => "marker",
6615            Self::Standard => "standard",
6616        }
6617    }
6618}
6619
6620/// A supported two-dimensional plot owned by one plot area.
6621#[derive(Clone, Debug, PartialEq)]
6622pub enum Plot {
6623    Bar {
6624        direction: BarDirection,
6625        grouping: BarGrouping,
6626        gap_width: u16,
6627        overlap: i8,
6628        series: Vec<Series>,
6629        data_labels: Option<CT_DLbls>,
6630        axis_ids: [AxisId; 2],
6631    },
6632    Line {
6633        grouping: Grouping,
6634        marker: bool,
6635        smooth: bool,
6636        series: Vec<Series>,
6637        data_labels: Option<CT_DLbls>,
6638        axis_ids: [AxisId; 2],
6639    },
6640    Pie {
6641        first_slice_angle: u16,
6642        series: Vec<Series>,
6643        data_labels: Option<CT_DLbls>,
6644    },
6645    Doughnut {
6646        first_slice_angle: u16,
6647        hole_size: u8,
6648        series: Vec<Series>,
6649        data_labels: Option<CT_DLbls>,
6650    },
6651    Area {
6652        grouping: Grouping,
6653        series: Vec<Series>,
6654        data_labels: Option<CT_DLbls>,
6655        axis_ids: [AxisId; 2],
6656    },
6657    Scatter {
6658        style: ScatterStyle,
6659        series: Vec<Series>,
6660        data_labels: Option<CT_DLbls>,
6661        axis_ids: [AxisId; 2],
6662    },
6663    Radar {
6664        style: RadarStyle,
6665        series: Vec<Series>,
6666        data_labels: Option<CT_DLbls>,
6667        axis_ids: [AxisId; 2],
6668    },
6669}
6670
6671impl Plot {
6672    pub fn bar(
6673        direction: BarDirection,
6674        grouping: BarGrouping,
6675        series: Vec<Series>,
6676        axis_ids: [AxisId; 2],
6677    ) -> Result<Self> {
6678        let plot = Self::Bar {
6679            direction,
6680            grouping,
6681            gap_width: 150,
6682            overlap: 0,
6683            series,
6684            data_labels: None,
6685            axis_ids,
6686        };
6687        plot.validate()?;
6688        Ok(plot)
6689    }
6690
6691    pub fn line(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
6692        let plot = Self::Line {
6693            grouping,
6694            marker: false,
6695            smooth: false,
6696            series,
6697            data_labels: None,
6698            axis_ids,
6699        };
6700        plot.validate()?;
6701        Ok(plot)
6702    }
6703
6704    pub fn pie(series: Vec<Series>) -> Result<Self> {
6705        let plot = Self::Pie {
6706            first_slice_angle: 0,
6707            series,
6708            data_labels: None,
6709        };
6710        plot.validate()?;
6711        Ok(plot)
6712    }
6713
6714    pub fn doughnut(series: Vec<Series>) -> Result<Self> {
6715        let plot = Self::Doughnut {
6716            first_slice_angle: 0,
6717            hole_size: 50,
6718            series,
6719            data_labels: None,
6720        };
6721        plot.validate()?;
6722        Ok(plot)
6723    }
6724
6725    pub fn area(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
6726        let plot = Self::Area {
6727            grouping,
6728            series,
6729            data_labels: None,
6730            axis_ids,
6731        };
6732        plot.validate()?;
6733        Ok(plot)
6734    }
6735
6736    pub fn scatter(
6737        style: ScatterStyle,
6738        series: Vec<Series>,
6739        axis_ids: [AxisId; 2],
6740    ) -> Result<Self> {
6741        let plot = Self::Scatter {
6742            style,
6743            series,
6744            data_labels: None,
6745            axis_ids,
6746        };
6747        plot.validate()?;
6748        Ok(plot)
6749    }
6750
6751    pub fn radar(style: RadarStyle, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
6752        let plot = Self::Radar {
6753            style,
6754            series,
6755            data_labels: None,
6756            axis_ids,
6757        };
6758        plot.validate()?;
6759        Ok(plot)
6760    }
6761
6762    fn axis_ids(&self) -> Option<[AxisId; 2]> {
6763        match self {
6764            Self::Bar { axis_ids, .. }
6765            | Self::Line { axis_ids, .. }
6766            | Self::Area { axis_ids, .. }
6767            | Self::Scatter { axis_ids, .. }
6768            | Self::Radar { axis_ids, .. } => Some(*axis_ids),
6769            Self::Pie { .. } | Self::Doughnut { .. } => None,
6770        }
6771    }
6772
6773    fn validate(&self) -> Result<()> {
6774        let (series, labels, axis_ids) = match self {
6775            Self::Bar {
6776                gap_width,
6777                overlap,
6778                series,
6779                data_labels,
6780                axis_ids,
6781                ..
6782            } => {
6783                if *gap_width > 500 {
6784                    return Err(ChartError::InvalidValue {
6785                        element: "c:gapWidth".to_owned(),
6786                        value: gap_width.to_string(),
6787                    });
6788                }
6789                if !(-100..=100).contains(overlap) {
6790                    return Err(ChartError::InvalidValue {
6791                        element: "c:overlap".to_owned(),
6792                        value: overlap.to_string(),
6793                    });
6794                }
6795                (series, data_labels, Some(axis_ids))
6796            }
6797            Self::Line {
6798                series,
6799                data_labels,
6800                axis_ids,
6801                ..
6802            } => (series, data_labels, Some(axis_ids)),
6803            Self::Pie {
6804                first_slice_angle,
6805                series,
6806                data_labels,
6807            } => {
6808                if *first_slice_angle > 360 {
6809                    return Err(ChartError::InvalidValue {
6810                        element: "c:firstSliceAng".to_owned(),
6811                        value: first_slice_angle.to_string(),
6812                    });
6813                }
6814                (series, data_labels, None)
6815            }
6816            Self::Doughnut {
6817                first_slice_angle,
6818                hole_size,
6819                series,
6820                data_labels,
6821            } => {
6822                if *first_slice_angle > 360 {
6823                    return Err(ChartError::InvalidValue {
6824                        element: "c:firstSliceAng".to_owned(),
6825                        value: first_slice_angle.to_string(),
6826                    });
6827                }
6828                if !(10..=90).contains(hole_size) {
6829                    return Err(ChartError::InvalidValue {
6830                        element: "c:holeSize".to_owned(),
6831                        value: hole_size.to_string(),
6832                    });
6833                }
6834                (series, data_labels, None)
6835            }
6836            Self::Area {
6837                series,
6838                data_labels,
6839                axis_ids,
6840                ..
6841            }
6842            | Self::Radar {
6843                series,
6844                data_labels,
6845                axis_ids,
6846                ..
6847            } => (series, data_labels, Some(axis_ids)),
6848            Self::Scatter {
6849                series,
6850                data_labels,
6851                axis_ids,
6852                ..
6853            } => {
6854                for item in series {
6855                    if !matches!(item.categories, Some(AxisData::Numeric(_))) {
6856                        return Err(ChartError::InvalidValue {
6857                            element: "c:xVal".to_owned(),
6858                            value: "scatter series requires numeric x values".to_owned(),
6859                        });
6860                    }
6861                }
6862                (series, data_labels, Some(axis_ids))
6863            }
6864        };
6865        if series.is_empty() {
6866            return Err(ChartError::MissingElement("c:ser".to_owned()));
6867        }
6868        let scatter_plot = matches!(self, Self::Scatter { .. });
6869        for item in series {
6870            if item.bubble_size.is_some() || item.opaque_bubble_size {
6871                return Err(ChartError::InvalidValue {
6872                    element: "c:bubbleSize".to_owned(),
6873                    value: "bubble size is only valid in an opaque bubble plot".to_owned(),
6874                });
6875            }
6876            if item.uses_scatter_wrappers && !scatter_plot {
6877                return Err(ChartError::InvalidValue {
6878                    element: "c:ser".to_owned(),
6879                    value: "x/y wrappers are only valid in scatter plots".to_owned(),
6880                });
6881            }
6882            if scatter_plot && item.parsed_from_xml && !item.uses_scatter_wrappers {
6883                return Err(ChartError::InvalidValue {
6884                    element: "c:ser".to_owned(),
6885                    value: "scatter plots require xVal/yVal wrappers".to_owned(),
6886                });
6887            }
6888        }
6889        if let Some(axis_ids) = axis_ids {
6890            if axis_ids[0] == axis_ids[1] {
6891                return Err(ChartError::DuplicateElement(format!(
6892                    "c:axId {}",
6893                    axis_ids[0].value()
6894                )));
6895            }
6896            for id in axis_ids {
6897                AxisId::new(id.value())?;
6898            }
6899        }
6900        if let Some(labels) = labels {
6901            labels.validate()?;
6902        }
6903        for item in series {
6904            item.values.validate()?;
6905        }
6906        Ok(())
6907    }
6908}
6909
6910/// The concrete ChartML root used by an axis.
6911#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
6912pub enum AxisKind {
6913    Category,
6914    Value,
6915    Date,
6916    Series,
6917}
6918
6919impl AxisKind {
6920    fn parse(local: &[u8]) -> Option<Self> {
6921        match local {
6922            b"catAx" => Some(Self::Category),
6923            b"valAx" => Some(Self::Value),
6924            b"dateAx" => Some(Self::Date),
6925            b"serAx" => Some(Self::Series),
6926            _ => None,
6927        }
6928    }
6929
6930    const fn local(self) -> &'static str {
6931        match self {
6932            Self::Category => "catAx",
6933            Self::Value => "valAx",
6934            Self::Date => "dateAx",
6935            Self::Series => "serAx",
6936        }
6937    }
6938}
6939
6940/// Direction of values along an axis scale.
6941#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6942pub enum Orientation {
6943    #[default]
6944    MinMax,
6945    MaxMin,
6946}
6947
6948impl Orientation {
6949    fn parse(value: &str) -> Option<Self> {
6950        match value {
6951            "minMax" => Some(Self::MinMax),
6952            "maxMin" => Some(Self::MaxMin),
6953            _ => None,
6954        }
6955    }
6956
6957    const fn as_str(self) -> &'static str {
6958        match self {
6959            Self::MinMax => "minMax",
6960            Self::MaxMin => "maxMin",
6961        }
6962    }
6963}
6964
6965/// Which side of the plot area carries an axis.
6966#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6967pub enum AxisPosition {
6968    Bottom,
6969    Left,
6970    Right,
6971    Top,
6972}
6973
6974impl AxisPosition {
6975    fn parse(value: &str) -> Option<Self> {
6976        match value {
6977            "b" => Some(Self::Bottom),
6978            "l" => Some(Self::Left),
6979            "r" => Some(Self::Right),
6980            "t" => Some(Self::Top),
6981            _ => None,
6982        }
6983    }
6984
6985    const fn as_str(self) -> &'static str {
6986        match self {
6987            Self::Bottom => "b",
6988            Self::Left => "l",
6989            Self::Right => "r",
6990            Self::Top => "t",
6991        }
6992    }
6993}
6994
6995/// Placement of a major or minor tick relative to its axis line.
6996#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6997pub enum TickMark {
6998    Cross,
6999    Inside,
7000    #[default]
7001    None,
7002    Outside,
7003}
7004
7005impl TickMark {
7006    fn parse(value: &str) -> Option<Self> {
7007        match value {
7008            "cross" => Some(Self::Cross),
7009            "in" => Some(Self::Inside),
7010            "none" => Some(Self::None),
7011            "out" => Some(Self::Outside),
7012            _ => None,
7013        }
7014    }
7015
7016    const fn as_str(self) -> &'static str {
7017        match self {
7018            Self::Cross => "cross",
7019            Self::Inside => "in",
7020            Self::None => "none",
7021            Self::Outside => "out",
7022        }
7023    }
7024}
7025
7026/// Placement of tick labels relative to an axis.
7027#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
7028pub enum TickLabelPosition {
7029    High,
7030    Low,
7031    #[default]
7032    NextTo,
7033    None,
7034}
7035
7036impl TickLabelPosition {
7037    fn parse(value: &str) -> Option<Self> {
7038        match value {
7039            "high" => Some(Self::High),
7040            "low" => Some(Self::Low),
7041            "nextTo" => Some(Self::NextTo),
7042            "none" => Some(Self::None),
7043            _ => None,
7044        }
7045    }
7046
7047    const fn as_str(self) -> &'static str {
7048        match self {
7049            Self::High => "high",
7050            Self::Low => "low",
7051            Self::NextTo => "nextTo",
7052            Self::None => "none",
7053        }
7054    }
7055}
7056
7057/// The modelled values of `c:scaling`.
7058#[derive(Clone, Debug)]
7059pub struct Scaling {
7060    pub log_base: Option<f64>,
7061    pub orientation: Orientation,
7062    pub maximum: Option<f64>,
7063    pub minimum: Option<f64>,
7064    log_base_markup: Option<ScalarMarkup>,
7065    orientation_markup: Option<ScalarMarkup>,
7066    maximum_markup: Option<ScalarMarkup>,
7067    minimum_markup: Option<ScalarMarkup>,
7068    raw_attributes: XmlAttributes,
7069    raw_children: OrderedRawChildren,
7070}
7071
7072impl PartialEq for Scaling {
7073    fn eq(&self, other: &Self) -> bool {
7074        self.log_base == other.log_base
7075            && self.orientation == other.orientation
7076            && self.maximum == other.maximum
7077            && self.minimum == other.minimum
7078            && optional_scalar_markup_eq(&self.log_base_markup, &other.log_base_markup)
7079            && optional_scalar_markup_eq(&self.orientation_markup, &other.orientation_markup)
7080            && optional_scalar_markup_eq(&self.maximum_markup, &other.maximum_markup)
7081            && optional_scalar_markup_eq(&self.minimum_markup, &other.minimum_markup)
7082            && self.raw_attributes == other.raw_attributes
7083            && self.raw_children == other.raw_children
7084    }
7085}
7086
7087impl Default for Scaling {
7088    fn default() -> Self {
7089        Self {
7090            log_base: None,
7091            orientation: Orientation::MinMax,
7092            maximum: None,
7093            minimum: None,
7094            log_base_markup: None,
7095            orientation_markup: None,
7096            maximum_markup: None,
7097            minimum_markup: None,
7098            raw_attributes: Vec::new(),
7099            raw_children: OrderedRawChildren::default(),
7100        }
7101    }
7102}
7103
7104/// Shape properties on a major or minor gridline container.
7105#[derive(Clone, Debug, Default, PartialEq)]
7106pub struct ChartLines {
7107    pub sp_pr: Option<CT_ShapeProperties>,
7108    raw_attributes: XmlAttributes,
7109    raw_children: OrderedRawChildren,
7110}
7111
7112/// Number format applied to axis values and labels.
7113#[derive(Clone, Debug, Eq, PartialEq)]
7114pub struct NumberFormat {
7115    pub format_code: String,
7116    pub source_linked: bool,
7117    raw_attributes: XmlAttributes,
7118    raw_content: Vec<Vec<u8>>,
7119}
7120
7121/// Placement of a chart data label relative to its data point.
7122#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7123pub enum DataLabelPosition {
7124    BestFit,
7125    Bottom,
7126    Center,
7127    InsideBase,
7128    InsideEnd,
7129    Left,
7130    OutsideEnd,
7131    Right,
7132    Top,
7133}
7134
7135impl DataLabelPosition {
7136    fn parse(value: &str) -> Option<Self> {
7137        match value {
7138            "bestFit" => Some(Self::BestFit),
7139            "b" => Some(Self::Bottom),
7140            "ctr" => Some(Self::Center),
7141            "inBase" => Some(Self::InsideBase),
7142            "inEnd" => Some(Self::InsideEnd),
7143            "l" => Some(Self::Left),
7144            "outEnd" => Some(Self::OutsideEnd),
7145            "r" => Some(Self::Right),
7146            "t" => Some(Self::Top),
7147            _ => None,
7148        }
7149    }
7150
7151    const fn as_str(self) -> &'static str {
7152        match self {
7153            Self::BestFit => "bestFit",
7154            Self::Bottom => "b",
7155            Self::Center => "ctr",
7156            Self::InsideBase => "inBase",
7157            Self::InsideEnd => "inEnd",
7158            Self::Left => "l",
7159            Self::OutsideEnd => "outEnd",
7160            Self::Right => "r",
7161            Self::Top => "t",
7162        }
7163    }
7164}
7165
7166/// Collection-level data-label defaults for one chart series or plot.
7167#[derive(Clone, Debug, Default, PartialEq)]
7168pub struct CT_DLbls {
7169    pub number_format: Option<NumberFormat>,
7170    pub position: Option<DataLabelPosition>,
7171    pub separator: Option<String>,
7172    pub show_legend_key: bool,
7173    pub show_value: bool,
7174    pub show_category_name: bool,
7175    pub show_series_name: bool,
7176    pub show_percent: bool,
7177    pub show_bubble_size: bool,
7178    position_markup: Option<ScalarMarkup>,
7179    show_legend_key_markup: Option<ScalarMarkup>,
7180    show_value_markup: Option<ScalarMarkup>,
7181    show_category_name_markup: Option<ScalarMarkup>,
7182    show_series_name_markup: Option<ScalarMarkup>,
7183    show_percent_markup: Option<ScalarMarkup>,
7184    show_bubble_size_markup: Option<ScalarMarkup>,
7185    separator_markup: Option<TextMarkup>,
7186    raw_attributes: XmlAttributes,
7187    namespace_declarations: XmlAttributes,
7188    raw_children: OrderedRawChildren,
7189}
7190
7191/// How a chart displays cells whose cached values are blank.
7192#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
7193pub enum DispBlanksAs {
7194    #[default]
7195    Gap,
7196    Zero,
7197    Span,
7198}
7199
7200impl DispBlanksAs {
7201    fn parse(value: &str) -> Option<Self> {
7202        match value {
7203            "gap" => Some(Self::Gap),
7204            "zero" => Some(Self::Zero),
7205            "span" => Some(Self::Span),
7206            _ => None,
7207        }
7208    }
7209
7210    const fn as_str(self) -> &'static str {
7211        match self {
7212            Self::Gap => "gap",
7213            Self::Zero => "zero",
7214            Self::Span => "span",
7215        }
7216    }
7217}
7218
7219/// A chart title with plain-text authoring and preserved unsupported children.
7220#[derive(Clone, Debug, Default, Eq, PartialEq)]
7221pub struct CT_Title {
7222    text: Option<CT_TextBody>,
7223    raw_attributes: Vec<(String, String)>,
7224    raw_children: OrderedRawChildren,
7225}
7226
7227/// Preserved lexical and ordered-raw state for one typed plot.
7228#[derive(Clone, Debug, Default, PartialEq)]
7229struct PlotMarkup {
7230    raw_attributes: XmlAttributes,
7231    raw_children: OrderedRawChildren,
7232    direction: Option<ScalarMarkup>,
7233    grouping: Option<ScalarMarkup>,
7234    gap_width: Option<ScalarMarkup>,
7235    overlap: Option<ScalarMarkup>,
7236    marker: Option<ScalarMarkup>,
7237    smooth: Option<ScalarMarkup>,
7238    first_slice_angle: Option<ScalarMarkup>,
7239    hole_size: Option<ScalarMarkup>,
7240    style: Option<ScalarMarkup>,
7241    axis_ids: Vec<AxisIdMarkup>,
7242    original_series_keys: Vec<(u32, u32)>,
7243    original_axis_ids: Vec<AxisId>,
7244    parsed_bar: Option<bool>,
7245    parsed_remaining: Option<&'static str>,
7246}
7247
7248/// A plot-area shell. F-119 through F-122 replace selected raw slots with types.
7249#[derive(Clone, Debug, PartialEq)]
7250pub struct CT_PlotArea {
7251    raw_attributes: Vec<(String, String)>,
7252    raw_children: OrderedRawChildren,
7253    namespace_bindings: NamespaceBindings,
7254    plots: Option<Vec<Plot>>,
7255    plot_markup: Vec<PlotMarkup>,
7256    axes: Vec<Axis>,
7257}
7258
7259/// A chart legend shell whose current children remain opaque.
7260#[derive(Clone, Debug, Default, Eq, PartialEq)]
7261pub struct CT_Legend {
7262    raw_attributes: Vec<(String, String)>,
7263    raw_children: OrderedRawChildren,
7264}
7265
7266#[derive(Clone, Debug, PartialEq)]
7267struct AxisIdMarkup {
7268    scalar: ScalarMarkup,
7269    parsed: AxisId,
7270    lexical: String,
7271}
7272
7273/// The common modelled sequence of one ChartML axis.
7274#[derive(Clone, Debug)]
7275pub struct Axis {
7276    pub kind: AxisKind,
7277    pub id: AxisId,
7278    pub scaling: Scaling,
7279    pub deleted: bool,
7280    pub position: AxisPosition,
7281    pub major_gridlines: Option<ChartLines>,
7282    pub minor_gridlines: Option<ChartLines>,
7283    pub title: Option<CT_Title>,
7284    pub number_format: Option<NumberFormat>,
7285    pub major_tick_mark: TickMark,
7286    pub minor_tick_mark: TickMark,
7287    pub tick_label_position: TickLabelPosition,
7288    pub sp_pr: Option<CT_ShapeProperties>,
7289    pub tx_pr: Option<CT_TextBody>,
7290    pub cross_axis: AxisId,
7291    parsed_kind: Option<AxisKind>,
7292    id_markup: AxisIdMarkup,
7293    delete_markup: Option<ScalarMarkup>,
7294    position_markup: ScalarMarkup,
7295    major_tick_mark_markup: Option<ScalarMarkup>,
7296    minor_tick_mark_markup: Option<ScalarMarkup>,
7297    tick_label_position_markup: Option<ScalarMarkup>,
7298    cross_axis_markup: AxisIdMarkup,
7299    raw_attributes: XmlAttributes,
7300    namespace_declarations: XmlAttributes,
7301    raw_children: OrderedRawChildren,
7302}
7303
7304impl PartialEq for Axis {
7305    fn eq(&self, other: &Self) -> bool {
7306        self.kind == other.kind
7307            && self.id == other.id
7308            && axis_id_markup_eq(&self.id_markup, &other.id_markup)
7309            && self.scaling == other.scaling
7310            && self.deleted == other.deleted
7311            && optional_scalar_markup_eq(&self.delete_markup, &other.delete_markup)
7312            && self.position == other.position
7313            && self.position_markup == other.position_markup
7314            && self.major_gridlines == other.major_gridlines
7315            && self.minor_gridlines == other.minor_gridlines
7316            && self.title == other.title
7317            && self.number_format == other.number_format
7318            && self.major_tick_mark == other.major_tick_mark
7319            && optional_scalar_markup_eq(
7320                &self.major_tick_mark_markup,
7321                &other.major_tick_mark_markup,
7322            )
7323            && self.minor_tick_mark == other.minor_tick_mark
7324            && optional_scalar_markup_eq(
7325                &self.minor_tick_mark_markup,
7326                &other.minor_tick_mark_markup,
7327            )
7328            && self.tick_label_position == other.tick_label_position
7329            && optional_scalar_markup_eq(
7330                &self.tick_label_position_markup,
7331                &other.tick_label_position_markup,
7332            )
7333            && self.sp_pr == other.sp_pr
7334            && self.tx_pr == other.tx_pr
7335            && self.cross_axis == other.cross_axis
7336            && axis_id_markup_eq(&self.cross_axis_markup, &other.cross_axis_markup)
7337            && self.raw_attributes == other.raw_attributes
7338            && self.namespace_declarations == other.namespace_declarations
7339            && self.raw_children == other.raw_children
7340    }
7341}
7342
7343impl Axis {
7344    pub fn new(kind: AxisKind, id: AxisId, position: AxisPosition, cross_axis: AxisId) -> Self {
7345        Self {
7346            kind,
7347            id,
7348            scaling: Scaling::default(),
7349            deleted: false,
7350            position,
7351            major_gridlines: None,
7352            minor_gridlines: None,
7353            title: None,
7354            number_format: None,
7355            major_tick_mark: TickMark::None,
7356            minor_tick_mark: TickMark::None,
7357            tick_label_position: TickLabelPosition::NextTo,
7358            sp_pr: None,
7359            tx_pr: None,
7360            cross_axis,
7361            parsed_kind: None,
7362            id_markup: default_axis_id_markup(id),
7363            delete_markup: None,
7364            position_markup: ScalarMarkup::default(),
7365            major_tick_mark_markup: None,
7366            minor_tick_mark_markup: None,
7367            tick_label_position_markup: None,
7368            cross_axis_markup: default_axis_id_markup(cross_axis),
7369            raw_attributes: Vec::new(),
7370            namespace_declarations: Vec::new(),
7371            raw_children: OrderedRawChildren::default(),
7372        }
7373    }
7374
7375    /// Sets axis visibility and writes the explicit ChartML deletion flag.
7376    pub fn set_deleted(&mut self, deleted: bool) {
7377        self.deleted = deleted;
7378        self.delete_markup.get_or_insert_with(ScalarMarkup::default);
7379    }
7380
7381    pub fn from_xml(xml: &[u8]) -> Result<Self> {
7382        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
7383    }
7384
7385    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7386        let mut reader = Reader::from_reader(xml);
7387        let mut buffer = Vec::new();
7388        loop {
7389            match reader
7390                .read_event_into(&mut buffer)
7391                .map_err(OxmlError::from)?
7392            {
7393                Event::Start(element) => {
7394                    let name = element.name();
7395                    let local = local_name(name.as_ref());
7396                    let Some(kind) = AxisKind::parse(local) else {
7397                        return Err(ChartError::UnexpectedElement(element_name(&element)));
7398                    };
7399                    chart_root_prefix(&element)?;
7400                    if !element_is_in_namespace(&element, C_NS, inherited)? {
7401                        return Err(ChartError::UnexpectedElement(element_name(&element)));
7402                    }
7403                    return Self::from_element(&mut reader, &element, inherited, kind);
7404                }
7405                Event::Empty(element) => {
7406                    let name = element.name();
7407                    let local = local_name(name.as_ref());
7408                    if AxisKind::parse(local).is_some() {
7409                        return Err(ChartError::MissingElement("c:axId".to_owned()));
7410                    }
7411                    return Err(ChartError::UnexpectedElement(element_name(&element)));
7412                }
7413                Event::Eof => return Err(ChartError::MissingElement("ChartML axis".to_owned())),
7414                _ => {}
7415            }
7416            buffer.clear();
7417        }
7418    }
7419
7420    fn from_element(
7421        reader: &mut Reader<&[u8]>,
7422        start: &BytesStart<'_>,
7423        inherited: &NamespaceBindings,
7424        kind: AxisKind,
7425    ) -> Result<Self> {
7426        reject_conflicting_prefix(start, b"a", A_NS)?;
7427        reject_conflicting_prefix(start, b"r", R_NS)?;
7428        let namespaces = chart_bindings(inherited, start)?;
7429        require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
7430        require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
7431        require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
7432        let (raw_attributes, _) =
7433            capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
7434        let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
7435        let mut state = AxisParseState::default();
7436        let mut buffer = Vec::new();
7437        loop {
7438            match reader
7439                .read_event_into(&mut buffer)
7440                .map_err(OxmlError::from)?
7441            {
7442                Event::Start(element) => {
7443                    let name = chart_child_local(&element, &namespaces)?;
7444                    let raw = capture_element(reader, &element)?;
7445                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
7446                }
7447                Event::Empty(element) => {
7448                    let name = chart_child_local(&element, &namespaces)?;
7449                    let raw = capture_empty_element(&element)?;
7450                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
7451                }
7452                event @ (Event::Text(_)
7453                | Event::CData(_)
7454                | Event::Comment(_)
7455                | Event::PI(_)
7456                | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
7457                Event::End(element)
7458                    if matches_local_name(element.name().as_ref(), kind.local().as_bytes()) =>
7459                {
7460                    break;
7461                }
7462                Event::Eof => return Err(missing_end(&format!("c:{}", kind.local()))),
7463                _ => {}
7464            }
7465            buffer.clear();
7466        }
7467        state.finish(kind, raw_attributes, namespace_declarations)
7468    }
7469
7470    pub fn to_xml(&self) -> Result<Vec<u8>> {
7471        self.validate()?;
7472        let mut writer = Writer::new(Vec::new());
7473        let root = format!("c:{}", self.kind.local());
7474        let mut start = BytesStart::new(&root);
7475        start.push_attribute(("xmlns:c", C_NS));
7476        start.push_attribute(("xmlns:a", A_NS));
7477        start.push_attribute(("xmlns:r", R_NS));
7478        push_attributes(&mut start, &self.namespace_declarations);
7479        push_attributes(&mut start, &self.raw_attributes);
7480        writer
7481            .write_event(Event::Start(start))
7482            .map_err(OxmlError::from)?;
7483        emit_raw(&mut writer, self.raw_children.at(0))?;
7484        write_axis_id(&mut writer, "c:axId", self.id, &self.id_markup)?;
7485        emit_raw(&mut writer, self.raw_children.at(1))?;
7486        self.scaling.write_xml(&mut writer)?;
7487        emit_raw(&mut writer, self.raw_children.at(2))?;
7488        if self.deleted || self.delete_markup.is_some() {
7489            write_scalar(
7490                &mut writer,
7491                "c:delete",
7492                bool_lexical(self.deleted),
7493                self.delete_markup.as_ref(),
7494            )?;
7495        }
7496        emit_raw(&mut writer, self.raw_children.at(3))?;
7497        write_scalar(
7498            &mut writer,
7499            "c:axPos",
7500            self.position.as_str(),
7501            Some(&self.position_markup),
7502        )?;
7503        emit_raw(&mut writer, self.raw_children.at(4))?;
7504        if let Some(lines) = &self.major_gridlines {
7505            lines.write_xml(&mut writer, "c:majorGridlines")?;
7506        }
7507        emit_raw(&mut writer, self.raw_children.at(5))?;
7508        if let Some(lines) = &self.minor_gridlines {
7509            lines.write_xml(&mut writer, "c:minorGridlines")?;
7510        }
7511        emit_raw(&mut writer, self.raw_children.at(6))?;
7512        if let Some(title) = &self.title {
7513            title.write_xml(&mut writer)?;
7514        }
7515        emit_raw(&mut writer, self.raw_children.at(7))?;
7516        if let Some(number_format) = &self.number_format {
7517            number_format.write_xml(&mut writer)?;
7518        }
7519        emit_raw(&mut writer, self.raw_children.at(8))?;
7520        if self.major_tick_mark != TickMark::None || self.major_tick_mark_markup.is_some() {
7521            write_scalar(
7522                &mut writer,
7523                "c:majorTickMark",
7524                self.major_tick_mark.as_str(),
7525                self.major_tick_mark_markup.as_ref(),
7526            )?;
7527        }
7528        emit_raw(&mut writer, self.raw_children.at(9))?;
7529        if self.minor_tick_mark != TickMark::None || self.minor_tick_mark_markup.is_some() {
7530            write_scalar(
7531                &mut writer,
7532                "c:minorTickMark",
7533                self.minor_tick_mark.as_str(),
7534                self.minor_tick_mark_markup.as_ref(),
7535            )?;
7536        }
7537        emit_raw(&mut writer, self.raw_children.at(10))?;
7538        if self.tick_label_position != TickLabelPosition::NextTo
7539            || self.tick_label_position_markup.is_some()
7540        {
7541            write_scalar(
7542                &mut writer,
7543                "c:tickLblPos",
7544                self.tick_label_position.as_str(),
7545                self.tick_label_position_markup.as_ref(),
7546            )?;
7547        }
7548        emit_raw(&mut writer, self.raw_children.at(11))?;
7549        if let Some(properties) = &self.sp_pr {
7550            properties.write_xml_as(&mut writer, "c:spPr")?;
7551        }
7552        emit_raw(&mut writer, self.raw_children.at(12))?;
7553        if let Some(text) = &self.tx_pr {
7554            text.write_xml_as(&mut writer, "c:txPr")?;
7555        }
7556        emit_raw(&mut writer, self.raw_children.at(13))?;
7557        write_axis_id(
7558            &mut writer,
7559            "c:crossAx",
7560            self.cross_axis,
7561            &self.cross_axis_markup,
7562        )?;
7563        emit_raw(&mut writer, self.raw_children.at(14))?;
7564        writer
7565            .write_event(Event::End(BytesEnd::new(&root)))
7566            .map_err(OxmlError::from)?;
7567        Ok(writer.into_inner())
7568    }
7569
7570    pub fn raw_children(&self) -> &OrderedRawChildren {
7571        &self.raw_children
7572    }
7573
7574    fn validate(&self) -> Result<()> {
7575        if let Some(parsed_kind) = self.parsed_kind
7576            && self.kind != parsed_kind
7577        {
7578            return Err(ChartError::InvalidValue {
7579                element: "ChartML axis root".to_owned(),
7580                value: format!(
7581                    "parsed c:{} cannot be relabelled c:{}",
7582                    parsed_kind.local(),
7583                    self.kind.local()
7584                ),
7585            });
7586        }
7587        AxisId::new(self.id.value())?;
7588        AxisId::new(self.cross_axis.value())?;
7589        self.scaling.validate()?;
7590        if let Some(number_format) = &self.number_format {
7591            number_format.validate()?;
7592        }
7593        Ok(())
7594    }
7595}
7596
7597#[derive(Default)]
7598struct AxisParseState {
7599    id: Option<(AxisId, AxisIdMarkup)>,
7600    scaling: Option<Scaling>,
7601    deleted: Option<(bool, ScalarMarkup)>,
7602    position: Option<(AxisPosition, ScalarMarkup)>,
7603    major_gridlines: Option<ChartLines>,
7604    minor_gridlines: Option<ChartLines>,
7605    title: Option<CT_Title>,
7606    number_format: Option<NumberFormat>,
7607    major_tick_mark: Option<(TickMark, ScalarMarkup)>,
7608    minor_tick_mark: Option<(TickMark, ScalarMarkup)>,
7609    tick_label_position: Option<(TickLabelPosition, ScalarMarkup)>,
7610    sp_pr: Option<CT_ShapeProperties>,
7611    tx_pr: Option<CT_TextBody>,
7612    cross_axis: Option<(AxisId, AxisIdMarkup)>,
7613    raw_children: OrderedRawChildren,
7614    boundary: usize,
7615}
7616
7617impl AxisParseState {
7618    fn capture_event(&mut self, raw: Vec<u8>) {
7619        self.raw_children.push(self.boundary, raw);
7620    }
7621
7622    fn parse_child(
7623        &mut self,
7624        name: &[u8],
7625        raw: Vec<u8>,
7626        namespaces: &NamespaceBindings,
7627    ) -> Result<()> {
7628        match name {
7629            b"axId" => {
7630                set_once(&mut self.id, parse_axis_id_scalar(&raw, "axId")?, "c:axId")?;
7631                self.boundary = self.boundary.max(1);
7632            }
7633            b"scaling" => {
7634                set_once(
7635                    &mut self.scaling,
7636                    Scaling::from_xml(&raw, namespaces)?,
7637                    "c:scaling",
7638                )?;
7639                self.boundary = self.boundary.max(2);
7640            }
7641            b"delete" => {
7642                set_once(
7643                    &mut self.deleted,
7644                    parse_bool_value(&raw, "delete")?,
7645                    "c:delete",
7646                )?;
7647                self.boundary = self.boundary.max(3);
7648            }
7649            b"axPos" => {
7650                set_once(&mut self.position, parse_axis_position(&raw)?, "c:axPos")?;
7651                self.boundary = self.boundary.max(4);
7652            }
7653            b"majorGridlines" => {
7654                set_once(
7655                    &mut self.major_gridlines,
7656                    ChartLines::from_xml(&raw, b"majorGridlines", namespaces)?,
7657                    "c:majorGridlines",
7658                )?;
7659                self.boundary = self.boundary.max(5);
7660            }
7661            b"minorGridlines" => {
7662                set_once(
7663                    &mut self.minor_gridlines,
7664                    ChartLines::from_xml(&raw, b"minorGridlines", namespaces)?,
7665                    "c:minorGridlines",
7666                )?;
7667                self.boundary = self.boundary.max(6);
7668            }
7669            b"title" => {
7670                set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
7671                self.boundary = self.boundary.max(7);
7672            }
7673            b"numFmt" => {
7674                set_once(
7675                    &mut self.number_format,
7676                    NumberFormat::from_xml(&raw)?,
7677                    "c:numFmt",
7678                )?;
7679                self.boundary = self.boundary.max(8);
7680            }
7681            b"majorTickMark" => {
7682                set_once(
7683                    &mut self.major_tick_mark,
7684                    parse_tick_mark(&raw, "majorTickMark")?,
7685                    "c:majorTickMark",
7686                )?;
7687                self.boundary = self.boundary.max(9);
7688            }
7689            b"minorTickMark" => {
7690                set_once(
7691                    &mut self.minor_tick_mark,
7692                    parse_tick_mark(&raw, "minorTickMark")?,
7693                    "c:minorTickMark",
7694                )?;
7695                self.boundary = self.boundary.max(10);
7696            }
7697            b"tickLblPos" => {
7698                set_once(
7699                    &mut self.tick_label_position,
7700                    parse_tick_label_position(&raw)?,
7701                    "c:tickLblPos",
7702                )?;
7703                self.boundary = self.boundary.max(11);
7704            }
7705            b"spPr" => {
7706                parse_chart_shape_properties(&raw, namespaces, &mut self.sp_pr)?;
7707                self.boundary = self.boundary.max(12);
7708            }
7709            b"txPr" => {
7710                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
7711                let inherited = namespaces
7712                    .iter()
7713                    .map(|(prefix, uri)| {
7714                        Ok((
7715                            std::str::from_utf8(prefix)
7716                                .map_err(OxmlError::from)?
7717                                .to_owned(),
7718                            uri.clone(),
7719                        ))
7720                    })
7721                    .collect::<Result<Vec<_>>>()?;
7722                let text = CT_TextBody::from_xml_as_with_namespaces(&raw, b"txPr", &inherited)?;
7723                let mut writer = Writer::new(Vec::new());
7724                text.write_xml_as(&mut writer, "c:txPr")?;
7725                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
7726                set_once(&mut self.tx_pr, text, "c:txPr")?;
7727                self.boundary = self.boundary.max(13);
7728            }
7729            b"crossAx" => {
7730                set_once(
7731                    &mut self.cross_axis,
7732                    parse_axis_id_scalar(&raw, "crossAx")?,
7733                    "c:crossAx",
7734                )?;
7735                self.boundary = self.boundary.max(14);
7736            }
7737            _ => self.raw_children.push(self.boundary, raw),
7738        }
7739        Ok(())
7740    }
7741
7742    fn finish(
7743        self,
7744        kind: AxisKind,
7745        raw_attributes: XmlAttributes,
7746        namespace_declarations: XmlAttributes,
7747    ) -> Result<Axis> {
7748        let (id, id_markup) = self
7749            .id
7750            .ok_or_else(|| ChartError::MissingElement("c:axId".to_owned()))?;
7751        let scaling = self
7752            .scaling
7753            .ok_or_else(|| ChartError::MissingElement("c:scaling".to_owned()))?;
7754        let (position, position_markup) = self
7755            .position
7756            .ok_or_else(|| ChartError::MissingElement("c:axPos".to_owned()))?;
7757        let (cross_axis, cross_axis_markup) = self
7758            .cross_axis
7759            .ok_or_else(|| ChartError::MissingElement("c:crossAx".to_owned()))?;
7760        let (deleted, delete_markup) = self
7761            .deleted
7762            .map(|(value, markup)| (value, Some(markup)))
7763            .unwrap_or((false, None));
7764        let (major_tick_mark, major_tick_mark_markup) = self
7765            .major_tick_mark
7766            .map(|(value, markup)| (value, Some(markup)))
7767            .unwrap_or((TickMark::None, None));
7768        let (minor_tick_mark, minor_tick_mark_markup) = self
7769            .minor_tick_mark
7770            .map(|(value, markup)| (value, Some(markup)))
7771            .unwrap_or((TickMark::None, None));
7772        let (tick_label_position, tick_label_position_markup) = self
7773            .tick_label_position
7774            .map(|(value, markup)| (value, Some(markup)))
7775            .unwrap_or((TickLabelPosition::NextTo, None));
7776        let axis = Axis {
7777            kind,
7778            id,
7779            scaling,
7780            deleted,
7781            position,
7782            major_gridlines: self.major_gridlines,
7783            minor_gridlines: self.minor_gridlines,
7784            title: self.title,
7785            number_format: self.number_format,
7786            major_tick_mark,
7787            minor_tick_mark,
7788            tick_label_position,
7789            sp_pr: self.sp_pr,
7790            tx_pr: self.tx_pr,
7791            cross_axis,
7792            parsed_kind: Some(kind),
7793            id_markup,
7794            delete_markup,
7795            position_markup,
7796            major_tick_mark_markup,
7797            minor_tick_mark_markup,
7798            tick_label_position_markup,
7799            cross_axis_markup,
7800            raw_attributes,
7801            namespace_declarations,
7802            raw_children: raw_children_in_schema_order(&self.raw_children, 14),
7803        };
7804        axis.validate()?;
7805        Ok(axis)
7806    }
7807}
7808
7809impl Scaling {
7810    fn from_xml(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7811        let mut reader = Reader::from_reader(xml);
7812        let mut buffer = Vec::new();
7813        loop {
7814            match reader
7815                .read_event_into(&mut buffer)
7816                .map_err(OxmlError::from)?
7817            {
7818                Event::Start(element)
7819                    if matches_local_name(element.name().as_ref(), b"scaling") =>
7820                {
7821                    let namespaces = typed_rewrite_bindings(&element, inherited)?;
7822                    let raw_attributes = capture_attributes(&element)?;
7823                    let mut log_base = None;
7824                    let mut orientation = None;
7825                    let mut maximum = None;
7826                    let mut minimum = None;
7827                    let mut raw_children = OrderedRawChildren::default();
7828                    let mut boundary = 0usize;
7829                    let mut inner = Vec::new();
7830                    loop {
7831                        match reader
7832                            .read_event_into(&mut inner)
7833                            .map_err(OxmlError::from)?
7834                        {
7835                            Event::Start(child) => {
7836                                let name = chart_child_local(&child, &namespaces)?;
7837                                let raw = capture_element(&mut reader, &child)?;
7838                                parse_scaling_child(
7839                                    name.as_deref().unwrap_or_default(),
7840                                    raw,
7841                                    &mut log_base,
7842                                    &mut orientation,
7843                                    &mut maximum,
7844                                    &mut minimum,
7845                                    &mut raw_children,
7846                                    &mut boundary,
7847                                )?;
7848                            }
7849                            Event::Empty(child) => {
7850                                let name = chart_child_local(&child, &namespaces)?;
7851                                let raw = capture_empty_element(&child)?;
7852                                parse_scaling_child(
7853                                    name.as_deref().unwrap_or_default(),
7854                                    raw,
7855                                    &mut log_base,
7856                                    &mut orientation,
7857                                    &mut maximum,
7858                                    &mut minimum,
7859                                    &mut raw_children,
7860                                    &mut boundary,
7861                                )?;
7862                            }
7863                            event @ (Event::Text(_)
7864                            | Event::CData(_)
7865                            | Event::Comment(_)
7866                            | Event::PI(_)
7867                            | Event::GeneralRef(_)) => {
7868                                raw_children.push(boundary, capture_event(event)?);
7869                            }
7870                            Event::End(end)
7871                                if matches_local_name(end.name().as_ref(), b"scaling") =>
7872                            {
7873                                let (log_base, log_base_markup) = optional_markup(log_base);
7874                                let (orientation, orientation_markup) = orientation
7875                                    .map(|(value, markup)| (value, Some(markup)))
7876                                    .unwrap_or((Orientation::MinMax, None));
7877                                let (maximum, maximum_markup) = optional_markup(maximum);
7878                                let (minimum, minimum_markup) = optional_markup(minimum);
7879                                let scaling = Self {
7880                                    log_base,
7881                                    orientation,
7882                                    maximum,
7883                                    minimum,
7884                                    log_base_markup,
7885                                    orientation_markup,
7886                                    maximum_markup,
7887                                    minimum_markup,
7888                                    raw_attributes,
7889                                    raw_children: raw_children_in_schema_order(&raw_children, 4),
7890                                };
7891                                scaling.validate()?;
7892                                return Ok(scaling);
7893                            }
7894                            Event::Eof => return Err(missing_end("c:scaling")),
7895                            _ => {}
7896                        }
7897                        inner.clear();
7898                    }
7899                }
7900                Event::Empty(element)
7901                    if matches_local_name(element.name().as_ref(), b"scaling") =>
7902                {
7903                    typed_rewrite_bindings(&element, inherited)?;
7904                    return Ok(Self {
7905                        raw_attributes: capture_attributes(&element)?,
7906                        ..Self::default()
7907                    });
7908                }
7909                Event::Start(element) | Event::Empty(element) => {
7910                    return Err(ChartError::UnexpectedElement(element_name(&element)));
7911                }
7912                Event::Eof => return Err(ChartError::MissingElement("c:scaling".to_owned())),
7913                _ => {}
7914            }
7915            buffer.clear();
7916        }
7917    }
7918
7919    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7920        self.validate()?;
7921        let mut start = BytesStart::new("c:scaling");
7922        push_attributes(&mut start, &self.raw_attributes);
7923        writer
7924            .write_event(Event::Start(start))
7925            .map_err(OxmlError::from)?;
7926        emit_raw(writer, self.raw_children.at(0))?;
7927        if let Some(value) = self.log_base {
7928            write_scalar(
7929                writer,
7930                "c:logBase",
7931                &value.to_string(),
7932                self.log_base_markup.as_ref(),
7933            )?;
7934        }
7935        emit_raw(writer, self.raw_children.at(1))?;
7936        if self.orientation != Orientation::MinMax || self.orientation_markup.is_some() {
7937            write_scalar(
7938                writer,
7939                "c:orientation",
7940                self.orientation.as_str(),
7941                self.orientation_markup.as_ref(),
7942            )?;
7943        }
7944        emit_raw(writer, self.raw_children.at(2))?;
7945        if let Some(value) = self.maximum {
7946            write_scalar(
7947                writer,
7948                "c:max",
7949                &value.to_string(),
7950                self.maximum_markup.as_ref(),
7951            )?;
7952        }
7953        emit_raw(writer, self.raw_children.at(3))?;
7954        if let Some(value) = self.minimum {
7955            write_scalar(
7956                writer,
7957                "c:min",
7958                &value.to_string(),
7959                self.minimum_markup.as_ref(),
7960            )?;
7961        }
7962        emit_raw(writer, self.raw_children.at(4))?;
7963        writer
7964            .write_event(Event::End(BytesEnd::new("c:scaling")))
7965            .map_err(OxmlError::from)?;
7966        Ok(())
7967    }
7968
7969    fn validate(&self) -> Result<()> {
7970        if let Some(value) = self.log_base
7971            && (!value.is_finite() || !(2.0..=1000.0).contains(&value))
7972        {
7973            return Err(ChartError::InvalidValue {
7974                element: "c:logBase".to_owned(),
7975                value: value.to_string(),
7976            });
7977        }
7978        for (element, value) in [("c:max", self.maximum), ("c:min", self.minimum)] {
7979            if let Some(value) = value
7980                && !value.is_finite()
7981            {
7982                return Err(ChartError::InvalidValue {
7983                    element: element.to_owned(),
7984                    value: value.to_string(),
7985                });
7986            }
7987        }
7988        if let (Some(minimum), Some(maximum)) = (self.minimum, self.maximum)
7989            && minimum > maximum
7990        {
7991            return Err(ChartError::InvalidValue {
7992                element: "c:scaling".to_owned(),
7993                value: format!("minimum {minimum} exceeds maximum {maximum}"),
7994            });
7995        }
7996        Ok(())
7997    }
7998}
7999
8000#[allow(clippy::too_many_arguments)]
8001fn parse_scaling_child(
8002    name: &[u8],
8003    raw: Vec<u8>,
8004    log_base: &mut Option<(f64, ScalarMarkup)>,
8005    orientation: &mut Option<(Orientation, ScalarMarkup)>,
8006    maximum: &mut Option<(f64, ScalarMarkup)>,
8007    minimum: &mut Option<(f64, ScalarMarkup)>,
8008    raw_children: &mut OrderedRawChildren,
8009    boundary: &mut usize,
8010) -> Result<()> {
8011    match name {
8012        b"logBase" => {
8013            set_once(log_base, parse_f64_scalar(&raw, "logBase")?, "c:logBase")?;
8014            *boundary = (*boundary).max(1);
8015        }
8016        b"orientation" => {
8017            let (value, markup) = scalar_value(&raw, "orientation")?;
8018            let value = value
8019                .ok_or_else(|| invalid_attribute("orientation", "val", "<missing>".to_owned()))?;
8020            let parsed = Orientation::parse(&value)
8021                .ok_or_else(|| invalid_attribute("orientation", "val", value))?;
8022            set_once(orientation, (parsed, markup), "c:orientation")?;
8023            *boundary = (*boundary).max(2);
8024        }
8025        b"max" => {
8026            set_once(maximum, parse_f64_scalar(&raw, "max")?, "c:max")?;
8027            *boundary = (*boundary).max(3);
8028        }
8029        b"min" => {
8030            set_once(minimum, parse_f64_scalar(&raw, "min")?, "c:min")?;
8031            *boundary = (*boundary).max(4);
8032        }
8033        _ => raw_children.push(*boundary, raw),
8034    }
8035    Ok(())
8036}
8037
8038impl ChartLines {
8039    fn from_xml(xml: &[u8], local: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
8040        let mut reader = Reader::from_reader(xml);
8041        let mut buffer = Vec::new();
8042        loop {
8043            match reader
8044                .read_event_into(&mut buffer)
8045                .map_err(OxmlError::from)?
8046            {
8047                Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
8048                    let namespaces = typed_rewrite_bindings(&element, inherited)?;
8049                    let raw_attributes = capture_attributes(&element)?;
8050                    let mut sp_pr = None;
8051                    let mut raw_children = OrderedRawChildren::default();
8052                    let mut boundary = 0usize;
8053                    let mut inner = Vec::new();
8054                    loop {
8055                        match reader
8056                            .read_event_into(&mut inner)
8057                            .map_err(OxmlError::from)?
8058                        {
8059                            Event::Start(child) => {
8060                                let name = chart_child_local(&child, &namespaces)?;
8061                                let raw = capture_element(&mut reader, &child)?;
8062                                if name.as_deref() == Some(b"spPr") {
8063                                    parse_chart_shape_properties(&raw, &namespaces, &mut sp_pr)?;
8064                                    boundary = 1;
8065                                } else {
8066                                    raw_children.push(boundary, raw);
8067                                }
8068                            }
8069                            Event::Empty(child) => {
8070                                let name = chart_child_local(&child, &namespaces)?;
8071                                let raw = capture_empty_element(&child)?;
8072                                if name.as_deref() == Some(b"spPr") {
8073                                    parse_chart_shape_properties(&raw, &namespaces, &mut sp_pr)?;
8074                                    boundary = 1;
8075                                } else {
8076                                    raw_children.push(boundary, raw);
8077                                }
8078                            }
8079                            event @ (Event::Text(_)
8080                            | Event::CData(_)
8081                            | Event::Comment(_)
8082                            | Event::PI(_)
8083                            | Event::GeneralRef(_)) => {
8084                                raw_children.push(boundary, capture_event(event)?);
8085                            }
8086                            Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
8087                                return Ok(Self {
8088                                    sp_pr,
8089                                    raw_attributes,
8090                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
8091                                });
8092                            }
8093                            Event::Eof => {
8094                                return Err(missing_end(&format!(
8095                                    "c:{}",
8096                                    String::from_utf8_lossy(local)
8097                                )));
8098                            }
8099                            _ => {}
8100                        }
8101                        inner.clear();
8102                    }
8103                }
8104                Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
8105                    typed_rewrite_bindings(&element, inherited)?;
8106                    return Ok(Self {
8107                        sp_pr: None,
8108                        raw_attributes: capture_attributes(&element)?,
8109                        raw_children: OrderedRawChildren::default(),
8110                    });
8111                }
8112                Event::Start(element) | Event::Empty(element) => {
8113                    return Err(ChartError::UnexpectedElement(element_name(&element)));
8114                }
8115                Event::Eof => {
8116                    return Err(ChartError::MissingElement(format!(
8117                        "c:{}",
8118                        String::from_utf8_lossy(local)
8119                    )));
8120                }
8121                _ => {}
8122            }
8123            buffer.clear();
8124        }
8125    }
8126
8127    fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
8128        let mut start = BytesStart::new(tag);
8129        push_attributes(&mut start, &self.raw_attributes);
8130        if self.sp_pr.is_none() && self.raw_children.is_empty() {
8131            writer
8132                .write_event(Event::Empty(start))
8133                .map_err(OxmlError::from)?;
8134            return Ok(());
8135        }
8136        writer
8137            .write_event(Event::Start(start))
8138            .map_err(OxmlError::from)?;
8139        emit_raw(writer, self.raw_children.at(0))?;
8140        if let Some(properties) = &self.sp_pr {
8141            properties.write_xml_as(writer, "c:spPr")?;
8142        }
8143        emit_raw(writer, self.raw_children.at(1))?;
8144        writer
8145            .write_event(Event::End(BytesEnd::new(tag)))
8146            .map_err(OxmlError::from)?;
8147        Ok(())
8148    }
8149}
8150
8151fn parse_chart_shape_properties(
8152    raw: &[u8],
8153    namespaces: &NamespaceBindings,
8154    slot: &mut Option<CT_ShapeProperties>,
8155) -> Result<()> {
8156    reject_conflicting_prefix_in_xml(raw, b"a", A_NS)?;
8157    let inherited = namespaces
8158        .iter()
8159        .map(|(prefix, uri)| {
8160            Ok((
8161                std::str::from_utf8(prefix)
8162                    .map_err(OxmlError::from)?
8163                    .to_owned(),
8164                uri.clone(),
8165            ))
8166        })
8167        .collect::<Result<Vec<_>>>()?;
8168    let properties = CT_ShapeProperties::from_xml_with_inherited_namespaces(raw, &inherited)?;
8169    let mut writer = Writer::new(Vec::new());
8170    properties.write_xml_as(&mut writer, "c:spPr")?;
8171    reject_rewritten_foreign_elements(raw, &writer.into_inner(), namespaces, b"spPr")?;
8172    set_once(slot, properties, "c:spPr")
8173}
8174
8175impl NumberFormat {
8176    pub fn new(format_code: String, source_linked: bool) -> Result<Self> {
8177        let number_format = Self {
8178            format_code,
8179            source_linked,
8180            raw_attributes: Vec::new(),
8181            raw_content: Vec::new(),
8182        };
8183        number_format.validate()?;
8184        Ok(number_format)
8185    }
8186
8187    fn from_xml(xml: &[u8]) -> Result<Self> {
8188        let mut reader = Reader::from_reader(xml);
8189        let mut buffer = Vec::new();
8190        loop {
8191            match reader
8192                .read_event_into(&mut buffer)
8193                .map_err(OxmlError::from)?
8194            {
8195                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
8196                    return Self::from_element(&element, Vec::new());
8197                }
8198                Event::Start(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
8199                    let mut raw_content = Vec::new();
8200                    let mut inner = Vec::new();
8201                    loop {
8202                        match reader
8203                            .read_event_into(&mut inner)
8204                            .map_err(OxmlError::from)?
8205                        {
8206                            Event::End(end)
8207                                if matches_local_name(end.name().as_ref(), b"numFmt") =>
8208                            {
8209                                return Self::from_element(&element, raw_content);
8210                            }
8211                            event @ (Event::Text(_)
8212                            | Event::CData(_)
8213                            | Event::Comment(_)
8214                            | Event::PI(_)
8215                            | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
8216                            Event::Eof => return Err(missing_end("c:numFmt")),
8217                            _ => return Err(ChartError::UnexpectedElement("c:numFmt".to_owned())),
8218                        }
8219                        inner.clear();
8220                    }
8221                }
8222                Event::Start(element) | Event::Empty(element) => {
8223                    return Err(ChartError::UnexpectedElement(element_name(&element)));
8224                }
8225                Event::Eof => return Err(ChartError::MissingElement("c:numFmt".to_owned())),
8226                _ => {}
8227            }
8228            buffer.clear();
8229        }
8230    }
8231
8232    fn from_element(element: &BytesStart<'_>, raw_content: Vec<Vec<u8>>) -> Result<Self> {
8233        reject_conflicting_prefix(element, b"c", C_NS)?;
8234        let format_code = attribute_value(element, b"formatCode")?
8235            .ok_or_else(|| invalid_attribute("numFmt", "formatCode", "<missing>".to_owned()))?;
8236        let source_linked = attribute_value(element, b"sourceLinked")?
8237            .ok_or_else(|| invalid_attribute("numFmt", "sourceLinked", "<missing>".to_owned()))?;
8238        let source_linked = parse_bool_lexical("numFmt", "sourceLinked", &source_linked)?;
8239        let number_format = Self {
8240            format_code,
8241            source_linked,
8242            raw_attributes: capture_attributes_excluding(
8243                element,
8244                &[b"formatCode", b"sourceLinked"],
8245            )?,
8246            raw_content,
8247        };
8248        number_format.validate()?;
8249        Ok(number_format)
8250    }
8251
8252    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8253        self.validate()?;
8254        let mut start = BytesStart::new("c:numFmt");
8255        start.push_attribute(("formatCode", self.format_code.as_str()));
8256        start.push_attribute(("sourceLinked", bool_lexical(self.source_linked)));
8257        push_attributes(&mut start, &self.raw_attributes);
8258        if self.raw_content.is_empty() {
8259            writer
8260                .write_event(Event::Empty(start))
8261                .map_err(OxmlError::from)?;
8262            return Ok(());
8263        }
8264        writer
8265            .write_event(Event::Start(start))
8266            .map_err(OxmlError::from)?;
8267        for raw in &self.raw_content {
8268            writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
8269        }
8270        writer
8271            .write_event(Event::End(BytesEnd::new("c:numFmt")))
8272            .map_err(OxmlError::from)?;
8273        Ok(())
8274    }
8275
8276    /// Projects one cached value through the supported deterministic subset.
8277    pub fn format_value(&self, value: f64) -> Result<String> {
8278        if !value.is_finite() {
8279            return Err(ChartError::InvalidValue {
8280                element: "c:numFmt value".to_owned(),
8281                value: value.to_string(),
8282            });
8283        }
8284        if self.format_code == "General" {
8285            return Ok(value.to_string());
8286        }
8287
8288        let (numeric_code, percentage) = self
8289            .format_code
8290            .strip_suffix('%')
8291            .map(|code| (code, true))
8292            .unwrap_or((&self.format_code, false));
8293        let precision = if numeric_code == "0" {
8294            Some(0)
8295        } else if let Some(decimals) = numeric_code.strip_prefix("0.")
8296            && !decimals.is_empty()
8297            && decimals.bytes().all(|byte| byte == b'0')
8298        {
8299            Some(decimals.len())
8300        } else {
8301            None
8302        };
8303        let Some(precision) = precision else {
8304            return Err(ChartError::InvalidValue {
8305                element: "c:numFmt/@formatCode projection".to_owned(),
8306                value: self.format_code.clone(),
8307            });
8308        };
8309        let projected = if percentage { value * 100.0 } else { value };
8310        if !projected.is_finite() {
8311            return Err(ChartError::InvalidValue {
8312                element: "c:numFmt value".to_owned(),
8313                value: projected.to_string(),
8314            });
8315        }
8316        let mut formatted = format!("{projected:.precision$}");
8317        if percentage {
8318            formatted.push('%');
8319        }
8320        Ok(formatted)
8321    }
8322
8323    fn validate(&self) -> Result<()> {
8324        if self.format_code.is_empty() {
8325            return Err(ChartError::InvalidValue {
8326                element: "c:numFmt/@formatCode".to_owned(),
8327                value: self.format_code.clone(),
8328            });
8329        }
8330        validate_xml_text(&self.format_code, "c:numFmt/@formatCode")
8331    }
8332}
8333
8334impl CT_DLbls {
8335    pub fn from_xml(xml: &[u8]) -> Result<Self> {
8336        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
8337    }
8338
8339    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
8340        let mut reader = Reader::from_reader(xml);
8341        let mut buffer = Vec::new();
8342        loop {
8343            match reader
8344                .read_event_into(&mut buffer)
8345                .map_err(OxmlError::from)?
8346            {
8347                Event::Start(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
8348                    chart_root_prefix(&element)?;
8349                    if !element_is_in_namespace(&element, C_NS, inherited)? {
8350                        return Err(ChartError::UnexpectedElement(element_name(&element)));
8351                    }
8352                    return Self::from_element(&mut reader, &element, inherited);
8353                }
8354                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
8355                    chart_root_prefix(&element)?;
8356                    let namespaces = chart_bindings(inherited, &element)?;
8357                    if !element_is_in_namespace(&element, C_NS, inherited)? {
8358                        return Err(ChartError::UnexpectedElement(element_name(&element)));
8359                    }
8360                    let (raw_attributes, _) = capture_fixed_root_attributes(
8361                        &element,
8362                        &["xmlns:c", "xmlns:a", "xmlns:r"],
8363                    )?;
8364                    return Ok(Self {
8365                        raw_attributes,
8366                        namespace_declarations: standalone_namespace_declarations(&namespaces)?,
8367                        ..Self::default()
8368                    });
8369                }
8370                Event::Start(element) | Event::Empty(element) => {
8371                    return Err(ChartError::UnexpectedElement(element_name(&element)));
8372                }
8373                Event::Eof => return Err(ChartError::MissingElement("c:dLbls".to_owned())),
8374                _ => {}
8375            }
8376            buffer.clear();
8377        }
8378    }
8379
8380    fn from_element(
8381        reader: &mut Reader<&[u8]>,
8382        start: &BytesStart<'_>,
8383        inherited: &NamespaceBindings,
8384    ) -> Result<Self> {
8385        reject_conflicting_prefix(start, b"a", A_NS)?;
8386        let namespaces = chart_bindings(inherited, start)?;
8387        require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
8388        let (raw_attributes, _) =
8389            capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8390        let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
8391        let mut state = DataLabelsParseState::new(namespaces);
8392        let mut buffer = Vec::new();
8393        loop {
8394            match reader
8395                .read_event_into(&mut buffer)
8396                .map_err(OxmlError::from)?
8397            {
8398                Event::Start(element) => {
8399                    let name = chart_child_local(&element, &state.namespaces)?;
8400                    let raw = capture_element(reader, &element)?;
8401                    state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
8402                }
8403                Event::Empty(element) => {
8404                    let name = chart_child_local(&element, &state.namespaces)?;
8405                    let raw = capture_empty_element(&element)?;
8406                    state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
8407                }
8408                event @ (Event::Text(_)
8409                | Event::CData(_)
8410                | Event::Comment(_)
8411                | Event::PI(_)
8412                | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
8413                Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
8414                    return state.finish(raw_attributes, namespace_declarations);
8415                }
8416                Event::Eof => return Err(missing_end("c:dLbls")),
8417                _ => {}
8418            }
8419            buffer.clear();
8420        }
8421    }
8422
8423    pub fn to_xml(&self) -> Result<Vec<u8>> {
8424        let mut writer = Writer::new(Vec::new());
8425        self.write_xml(&mut writer, true)?;
8426        Ok(writer.into_inner())
8427    }
8428
8429    fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
8430        self.validate()?;
8431        let mut start = BytesStart::new("c:dLbls");
8432        if standalone {
8433            start.push_attribute(("xmlns:c", C_NS));
8434            start.push_attribute(("xmlns:a", A_NS));
8435            start.push_attribute(("xmlns:r", R_NS));
8436        }
8437        push_attributes(&mut start, &self.namespace_declarations);
8438        push_attributes(&mut start, &self.raw_attributes);
8439        writer
8440            .write_event(Event::Start(start))
8441            .map_err(OxmlError::from)?;
8442        emit_raw(writer, self.raw_children.at(0))?;
8443        if let Some(number_format) = &self.number_format {
8444            number_format.write_xml(writer)?;
8445        }
8446        emit_raw(writer, self.raw_children.at(1))?;
8447        if let Some(position) = self.position {
8448            write_scalar(
8449                writer,
8450                "c:dLblPos",
8451                position.as_str(),
8452                self.position_markup.as_ref(),
8453            )?;
8454        }
8455        emit_raw(writer, self.raw_children.at(2))?;
8456        write_optional_bool(
8457            writer,
8458            "c:showLegendKey",
8459            self.show_legend_key,
8460            self.show_legend_key_markup.as_ref(),
8461        )?;
8462        emit_raw(writer, self.raw_children.at(3))?;
8463        write_optional_bool(
8464            writer,
8465            "c:showVal",
8466            self.show_value,
8467            self.show_value_markup.as_ref(),
8468        )?;
8469        emit_raw(writer, self.raw_children.at(4))?;
8470        write_optional_bool(
8471            writer,
8472            "c:showCatName",
8473            self.show_category_name,
8474            self.show_category_name_markup.as_ref(),
8475        )?;
8476        emit_raw(writer, self.raw_children.at(5))?;
8477        write_optional_bool(
8478            writer,
8479            "c:showSerName",
8480            self.show_series_name,
8481            self.show_series_name_markup.as_ref(),
8482        )?;
8483        emit_raw(writer, self.raw_children.at(6))?;
8484        write_optional_bool(
8485            writer,
8486            "c:showPercent",
8487            self.show_percent,
8488            self.show_percent_markup.as_ref(),
8489        )?;
8490        emit_raw(writer, self.raw_children.at(7))?;
8491        write_optional_bool(
8492            writer,
8493            "c:showBubbleSize",
8494            self.show_bubble_size,
8495            self.show_bubble_size_markup.as_ref(),
8496        )?;
8497        emit_raw(writer, self.raw_children.at(8))?;
8498        if let Some(separator) = &self.separator {
8499            write_text(
8500                writer,
8501                "c:separator",
8502                separator,
8503                self.separator_markup
8504                    .as_ref()
8505                    .unwrap_or(&TextMarkup::default()),
8506            )?;
8507        }
8508        emit_raw(writer, self.raw_children.at(9))?;
8509        writer
8510            .write_event(Event::End(BytesEnd::new("c:dLbls")))
8511            .map_err(OxmlError::from)?;
8512        Ok(())
8513    }
8514
8515    fn validate(&self) -> Result<()> {
8516        if let Some(number_format) = &self.number_format {
8517            number_format.validate()?;
8518        }
8519        if let Some(separator) = &self.separator {
8520            validate_xml_text(separator, "c:separator")?;
8521        }
8522        Ok(())
8523    }
8524
8525    pub fn raw_children(&self) -> &OrderedRawChildren {
8526        &self.raw_children
8527    }
8528}
8529
8530struct DataLabelsParseState {
8531    namespaces: NamespaceBindings,
8532    number_format: Option<NumberFormat>,
8533    position: Option<(DataLabelPosition, ScalarMarkup)>,
8534    show_legend_key: Option<(bool, ScalarMarkup)>,
8535    show_value: Option<(bool, ScalarMarkup)>,
8536    show_category_name: Option<(bool, ScalarMarkup)>,
8537    show_series_name: Option<(bool, ScalarMarkup)>,
8538    show_percent: Option<(bool, ScalarMarkup)>,
8539    show_bubble_size: Option<(bool, ScalarMarkup)>,
8540    separator: Option<(String, TextMarkup)>,
8541    raw_children: OrderedRawChildren,
8542    boundary: usize,
8543}
8544
8545impl DataLabelsParseState {
8546    fn new(namespaces: NamespaceBindings) -> Self {
8547        Self {
8548            namespaces,
8549            number_format: None,
8550            position: None,
8551            show_legend_key: None,
8552            show_value: None,
8553            show_category_name: None,
8554            show_series_name: None,
8555            show_percent: None,
8556            show_bubble_size: None,
8557            separator: None,
8558            raw_children: OrderedRawChildren::default(),
8559            boundary: 0,
8560        }
8561    }
8562
8563    fn capture_event(&mut self, raw: Vec<u8>) {
8564        self.raw_children.push(self.boundary, raw);
8565    }
8566
8567    fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
8568        match name {
8569            b"numFmt" => {
8570                set_once(
8571                    &mut self.number_format,
8572                    NumberFormat::from_xml(&raw)?,
8573                    "c:numFmt",
8574                )?;
8575                self.boundary = self.boundary.max(1);
8576            }
8577            b"dLblPos" => {
8578                let (value, markup) = scalar_value(&raw, "dLblPos")?;
8579                let lexical = value
8580                    .ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
8581                let position = DataLabelPosition::parse(&lexical)
8582                    .ok_or_else(|| invalid_attribute("dLblPos", "val", lexical))?;
8583                set_once(&mut self.position, (position, markup), "c:dLblPos")?;
8584                self.boundary = self.boundary.max(2);
8585            }
8586            b"showLegendKey" => {
8587                set_once(
8588                    &mut self.show_legend_key,
8589                    parse_bool_value(&raw, "showLegendKey")?,
8590                    "c:showLegendKey",
8591                )?;
8592                self.boundary = self.boundary.max(3);
8593            }
8594            b"showVal" => {
8595                set_once(
8596                    &mut self.show_value,
8597                    parse_bool_value(&raw, "showVal")?,
8598                    "c:showVal",
8599                )?;
8600                self.boundary = self.boundary.max(4);
8601            }
8602            b"showCatName" => {
8603                set_once(
8604                    &mut self.show_category_name,
8605                    parse_bool_value(&raw, "showCatName")?,
8606                    "c:showCatName",
8607                )?;
8608                self.boundary = self.boundary.max(5);
8609            }
8610            b"showSerName" => {
8611                set_once(
8612                    &mut self.show_series_name,
8613                    parse_bool_value(&raw, "showSerName")?,
8614                    "c:showSerName",
8615                )?;
8616                self.boundary = self.boundary.max(6);
8617            }
8618            b"showPercent" => {
8619                set_once(
8620                    &mut self.show_percent,
8621                    parse_bool_value(&raw, "showPercent")?,
8622                    "c:showPercent",
8623                )?;
8624                self.boundary = self.boundary.max(7);
8625            }
8626            b"showBubbleSize" => {
8627                set_once(
8628                    &mut self.show_bubble_size,
8629                    parse_bool_value(&raw, "showBubbleSize")?,
8630                    "c:showBubbleSize",
8631                )?;
8632                self.boundary = self.boundary.max(8);
8633            }
8634            b"separator" => {
8635                set_once(
8636                    &mut self.separator,
8637                    parse_text_element(&raw, b"separator", &self.namespaces)?,
8638                    "c:separator",
8639                )?;
8640                self.boundary = self.boundary.max(9);
8641            }
8642            _ => {
8643                let boundary = data_labels_raw_boundary(name, self.boundary);
8644                self.raw_children.push(boundary, raw);
8645                self.boundary = self.boundary.max(boundary);
8646            }
8647        }
8648        Ok(())
8649    }
8650
8651    fn finish(
8652        self,
8653        raw_attributes: XmlAttributes,
8654        namespace_declarations: XmlAttributes,
8655    ) -> Result<CT_DLbls> {
8656        let (position, position_markup) = optional_markup(self.position);
8657        let (show_legend_key, show_legend_key_markup) = optional_bool_markup(self.show_legend_key);
8658        let (show_value, show_value_markup) = optional_bool_markup(self.show_value);
8659        let (show_category_name, show_category_name_markup) =
8660            optional_bool_markup(self.show_category_name);
8661        let (show_series_name, show_series_name_markup) =
8662            optional_bool_markup(self.show_series_name);
8663        let (show_percent, show_percent_markup) = optional_bool_markup(self.show_percent);
8664        let (show_bubble_size, show_bubble_size_markup) =
8665            optional_bool_markup(self.show_bubble_size);
8666        let (separator, separator_markup) = self
8667            .separator
8668            .map(|(value, markup)| (Some(value), Some(markup)))
8669            .unwrap_or((None, None));
8670        let labels = CT_DLbls {
8671            number_format: self.number_format,
8672            position,
8673            separator,
8674            show_legend_key,
8675            show_value,
8676            show_category_name,
8677            show_series_name,
8678            show_percent,
8679            show_bubble_size,
8680            position_markup,
8681            show_legend_key_markup,
8682            show_value_markup,
8683            show_category_name_markup,
8684            show_series_name_markup,
8685            show_percent_markup,
8686            show_bubble_size_markup,
8687            separator_markup,
8688            raw_attributes,
8689            namespace_declarations,
8690            raw_children: raw_children_in_schema_order(&self.raw_children, 9),
8691        };
8692        labels.validate()?;
8693        Ok(labels)
8694    }
8695}
8696
8697fn data_labels_raw_boundary(name: &[u8], current: usize) -> usize {
8698    match name {
8699        b"dLbl" | b"delete" => 0,
8700        b"spPr" | b"txPr" => 1,
8701        b"showLeaderLines" | b"leaderLines" | b"extLst" => 9,
8702        _ => current,
8703    }
8704}
8705
8706fn optional_bool_markup(value: Option<(bool, ScalarMarkup)>) -> (bool, Option<ScalarMarkup>) {
8707    value
8708        .map(|(value, markup)| (value, Some(markup)))
8709        .unwrap_or((false, None))
8710}
8711
8712fn write_optional_bool(
8713    writer: &mut Writer<Vec<u8>>,
8714    tag: &str,
8715    value: bool,
8716    markup: Option<&ScalarMarkup>,
8717) -> Result<()> {
8718    if value || markup.is_some() {
8719        write_scalar(writer, tag, bool_lexical(value), markup)?;
8720    }
8721    Ok(())
8722}
8723
8724fn default_axis_id_markup(id: AxisId) -> AxisIdMarkup {
8725    AxisIdMarkup {
8726        scalar: ScalarMarkup::default(),
8727        parsed: id,
8728        lexical: id.value().to_string(),
8729    }
8730}
8731
8732fn parse_axis_id_scalar(xml: &[u8], local: &str) -> Result<(AxisId, AxisIdMarkup)> {
8733    let (value, scalar) = scalar_value(xml, local)?;
8734    let lexical = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8735    let parsed = lexical
8736        .parse::<i64>()
8737        .map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
8738    let id = AxisId::new(parsed).map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
8739    Ok((
8740        id,
8741        AxisIdMarkup {
8742            scalar,
8743            parsed: id,
8744            lexical,
8745        },
8746    ))
8747}
8748
8749fn write_axis_id<W: Write>(
8750    writer: &mut Writer<W>,
8751    tag: &str,
8752    id: AxisId,
8753    markup: &AxisIdMarkup,
8754) -> Result<()> {
8755    AxisId::new(id.value())?;
8756    let normalized;
8757    let value = if id == markup.parsed {
8758        markup.lexical.as_str()
8759    } else {
8760        normalized = id.value().to_string();
8761        normalized.as_str()
8762    };
8763    write_scalar(writer, tag, value, Some(&markup.scalar))
8764}
8765
8766fn parse_f64_scalar(xml: &[u8], local: &str) -> Result<(f64, ScalarMarkup)> {
8767    let (value, markup) = scalar_value(xml, local)?;
8768    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8769    let parsed = value
8770        .parse::<f64>()
8771        .map_err(|_| invalid_attribute(local, "val", value.clone()))?;
8772    if !parsed.is_finite() {
8773        return Err(invalid_attribute(local, "val", value));
8774    }
8775    Ok((parsed, markup))
8776}
8777
8778fn parse_axis_position(xml: &[u8]) -> Result<(AxisPosition, ScalarMarkup)> {
8779    let (value, markup) = scalar_value(xml, "axPos")?;
8780    let value = value.ok_or_else(|| invalid_attribute("axPos", "val", "<missing>".to_owned()))?;
8781    let parsed =
8782        AxisPosition::parse(&value).ok_or_else(|| invalid_attribute("axPos", "val", value))?;
8783    Ok((parsed, markup))
8784}
8785
8786fn parse_tick_mark(xml: &[u8], local: &str) -> Result<(TickMark, ScalarMarkup)> {
8787    let (value, markup) = scalar_value(xml, local)?;
8788    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8789    let parsed = TickMark::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
8790    Ok((parsed, markup))
8791}
8792
8793fn parse_tick_label_position(xml: &[u8]) -> Result<(TickLabelPosition, ScalarMarkup)> {
8794    let (value, markup) = scalar_value(xml, "tickLblPos")?;
8795    let value =
8796        value.ok_or_else(|| invalid_attribute("tickLblPos", "val", "<missing>".to_owned()))?;
8797    let parsed = TickLabelPosition::parse(&value)
8798        .ok_or_else(|| invalid_attribute("tickLblPos", "val", value))?;
8799    Ok((parsed, markup))
8800}
8801
8802fn optional_markup<T>(value: Option<(T, ScalarMarkup)>) -> (Option<T>, Option<ScalarMarkup>) {
8803    value
8804        .map(|(value, markup)| (Some(value), Some(markup)))
8805        .unwrap_or((None, None))
8806}
8807
8808fn optional_scalar_markup_eq(left: &Option<ScalarMarkup>, right: &Option<ScalarMarkup>) -> bool {
8809    let empty = ScalarMarkup::default();
8810    left.as_ref().unwrap_or(&empty) == right.as_ref().unwrap_or(&empty)
8811}
8812
8813fn axis_id_markup_eq(left: &AxisIdMarkup, right: &AxisIdMarkup) -> bool {
8814    left.scalar == right.scalar
8815}
8816
8817const fn bool_lexical(value: bool) -> &'static str {
8818    if value { "1" } else { "0" }
8819}
8820
8821fn parse_bool_lexical(element: &str, attribute: &str, value: &str) -> Result<bool> {
8822    match value {
8823        "1" | "true" => Ok(true),
8824        "0" | "false" => Ok(false),
8825        _ => Err(invalid_attribute(element, attribute, value.to_owned())),
8826    }
8827}
8828
8829impl CT_Title {
8830    fn plain_text(text: &str) -> Self {
8831        let mut body = CT_TextBody::new();
8832        body.set_text(text);
8833        let mut raw_children = OrderedRawChildren::default();
8834        raw_children.push(1, br#"<c:layout/><c:overlay val="0"/>"#.to_vec());
8835        Self {
8836            text: Some(body),
8837            raw_children,
8838            ..Self::default()
8839        }
8840    }
8841
8842    fn from_xml(xml: &[u8]) -> Result<Self> {
8843        let (raw_attributes, raw_children) = parse_raw_shell(xml, b"title", "c:title")?;
8844        Ok(Self {
8845            text: None,
8846            raw_attributes,
8847            raw_children,
8848        })
8849    }
8850
8851    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8852        if self.text.is_none() {
8853            return write_raw_shell(writer, "c:title", &self.raw_attributes, &self.raw_children);
8854        }
8855        let mut start = BytesStart::new("c:title");
8856        push_attributes(&mut start, &self.raw_attributes);
8857        writer
8858            .write_event(Event::Start(start))
8859            .map_err(OxmlError::from)?;
8860        emit_raw(writer, self.raw_children.at(0))?;
8861        writer
8862            .write_event(Event::Start(BytesStart::new("c:tx")))
8863            .map_err(OxmlError::from)?;
8864        if let Some(text) = &self.text {
8865            text.write_xml_as(writer, "c:rich")?;
8866        }
8867        writer
8868            .write_event(Event::End(BytesEnd::new("c:tx")))
8869            .map_err(OxmlError::from)?;
8870        emit_raw(writer, self.raw_children.at(1))?;
8871        writer
8872            .write_event(Event::End(BytesEnd::new("c:title")))
8873            .map_err(OxmlError::from)?;
8874        Ok(())
8875    }
8876
8877    pub fn raw_children(&self) -> &OrderedRawChildren {
8878        &self.raw_children
8879    }
8880}
8881
8882impl CT_PlotArea {
8883    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
8884        let (raw_attributes, captured_children) = parse_raw_shell(xml, b"plotArea", "c:plotArea")?;
8885        let namespace_bindings = root_chart_bindings(xml, b"plotArea", inherited)?;
8886        let direct_roots: Vec<_> = captured_children
8887            .at(0)
8888            .filter_map(|raw| chart_root_local(raw, &namespace_bindings).transpose())
8889            .collect::<Result<_>>()?;
8890        let plot_roots: Vec<_> = direct_roots
8891            .iter()
8892            .filter(|local| is_plot_container(local))
8893            .cloned()
8894            .collect();
8895        let typed_local = (plot_roots.len() == 1
8896            && matches!(
8897                plot_roots[0].as_slice(),
8898                b"barChart"
8899                    | b"lineChart"
8900                    | b"pieChart"
8901                    | b"doughnutChart"
8902                    | b"areaChart"
8903                    | b"scatterChart"
8904                    | b"radarChart"
8905            ))
8906        .then(|| plot_roots[0].clone());
8907
8908        let Some(typed_local) = typed_local else {
8909            return Ok(Self {
8910                raw_attributes,
8911                raw_children: captured_children,
8912                namespace_bindings,
8913                plots: None,
8914                plot_markup: Vec::new(),
8915                axes: Vec::new(),
8916            });
8917        };
8918
8919        let mut raw_children = OrderedRawChildren::default();
8920        let mut plots = Vec::new();
8921        let mut plot_markup = Vec::new();
8922        let mut axes = Vec::new();
8923        let mut boundary = 0usize;
8924        for raw in captured_children.at(0) {
8925            let local = chart_root_local(raw, &namespace_bindings)?;
8926            if local.as_deref() == Some(typed_local.as_slice()) {
8927                let (plot, markup) = parse_plot(raw, &namespace_bindings)?;
8928                plots.push(plot);
8929                plot_markup.push(markup);
8930                boundary = 1;
8931            } else if local.as_deref().and_then(AxisKind::parse).is_some() {
8932                axes.push(Axis::from_xml_with_namespaces(raw, &namespace_bindings)?);
8933                boundary = axes.len() + 1;
8934            } else {
8935                raw_children.push(boundary, raw.to_vec());
8936            }
8937        }
8938        let plot_area = Self {
8939            raw_attributes,
8940            raw_children,
8941            namespace_bindings,
8942            plots: Some(plots),
8943            plot_markup,
8944            axes,
8945        };
8946        plot_area.validate_typed()?;
8947        Ok(plot_area)
8948    }
8949
8950    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8951        let Some(plots) = &self.plots else {
8952            return write_raw_shell(
8953                writer,
8954                "c:plotArea",
8955                &self.raw_attributes,
8956                &self.raw_children,
8957            );
8958        };
8959        self.validate_typed()?;
8960        let mut start = BytesStart::new("c:plotArea");
8961        push_attributes(&mut start, &self.raw_attributes);
8962        writer
8963            .write_event(Event::Start(start))
8964            .map_err(OxmlError::from)?;
8965        emit_raw(writer, self.raw_children.at(0))?;
8966        for (index, plot) in plots.iter().enumerate() {
8967            write_plot(
8968                writer,
8969                plot,
8970                self.plot_markup
8971                    .get(index)
8972                    .unwrap_or(&PlotMarkup::default()),
8973            )?;
8974        }
8975        emit_raw(writer, self.raw_children.at(1))?;
8976        for (index, axis) in self.axes.iter().enumerate() {
8977            writer
8978                .get_mut()
8979                .write_all(&axis.to_xml()?)
8980                .map_err(OxmlError::from)?;
8981            emit_raw(writer, self.raw_children.at(index + 2))?;
8982        }
8983        writer
8984            .write_event(Event::End(BytesEnd::new("c:plotArea")))
8985            .map_err(OxmlError::from)?;
8986        Ok(())
8987    }
8988
8989    pub fn raw_children(&self) -> &OrderedRawChildren {
8990        &self.raw_children
8991    }
8992
8993    /// Creates a supported single-family plot area with owned axes.
8994    pub fn new(plots: Vec<Plot>, axes: Vec<Axis>) -> Result<Self> {
8995        let plot_markup = plots
8996            .iter()
8997            .map(|plot| {
8998                let mut markup = PlotMarkup::default();
8999                if matches!(plot, Plot::Line { .. }) {
9000                    markup.marker = Some(ScalarMarkup::default());
9001                    markup.smooth = Some(ScalarMarkup::default());
9002                }
9003                if matches!(plot, Plot::Doughnut { .. }) {
9004                    markup.hole_size = Some(ScalarMarkup::default());
9005                }
9006                markup
9007            })
9008            .collect();
9009        let plot_area = Self {
9010            raw_attributes: Vec::new(),
9011            raw_children: OrderedRawChildren::default(),
9012            namespace_bindings: chart_namespace_defaults(),
9013            plot_markup,
9014            plots: Some(plots),
9015            axes,
9016        };
9017        plot_area.validate_typed()?;
9018        Ok(plot_area)
9019    }
9020
9021    /// Returns the owned supported plots, or an error for an opaque choice.
9022    pub fn plots(&self) -> Result<&[Plot]> {
9023        self.plots
9024            .as_deref()
9025            .ok_or_else(|| ChartError::InvalidValue {
9026                element: "c:plotArea".to_owned(),
9027                value: "unsupported or combination plot area is opaque".to_owned(),
9028            })
9029    }
9030
9031    /// Returns mutable supported plots without exposing opaque content to edits.
9032    pub fn plots_mut(&mut self) -> Result<&mut [Plot]> {
9033        self.plots
9034            .as_deref_mut()
9035            .ok_or_else(|| ChartError::InvalidValue {
9036                element: "c:plotArea".to_owned(),
9037                value: "cannot combine typed plots with an opaque plot choice".to_owned(),
9038            })
9039    }
9040
9041    /// Parses the common series payloads nested in category-based plot shells.
9042    pub fn series(&self) -> Result<Vec<Series>> {
9043        if let Some(plots) = &self.plots {
9044            return Ok(plots
9045                .iter()
9046                .flat_map(|plot| match plot {
9047                    Plot::Bar { series, .. }
9048                    | Plot::Line { series, .. }
9049                    | Plot::Pie { series, .. }
9050                    | Plot::Doughnut { series, .. }
9051                    | Plot::Area { series, .. }
9052                    | Plot::Scatter { series, .. }
9053                    | Plot::Radar { series, .. } => series.clone(),
9054                })
9055                .collect());
9056        }
9057        let mut series = Vec::new();
9058        for raw in self.raw_children.at(0) {
9059            parse_plot_series(raw, &self.namespace_bindings, &mut series)?;
9060        }
9061        Ok(series)
9062    }
9063
9064    /// Parses and validates the direct axis children of this plot area.
9065    pub fn axes(&self) -> Result<Vec<Axis>> {
9066        if self.plots.is_some() {
9067            self.validate_typed()?;
9068            return Ok(self.axes.clone());
9069        }
9070        let mut axes = Vec::new();
9071        for raw in self.raw_children.at(0) {
9072            if let Some(axis) = parse_plot_axis(raw, &self.namespace_bindings)? {
9073                axes.push(axis);
9074            }
9075        }
9076        validate_axis_pairs(&axes)?;
9077        Ok(axes)
9078    }
9079
9080    fn validate_typed(&self) -> Result<()> {
9081        let plots = self
9082            .plots
9083            .as_ref()
9084            .ok_or_else(|| ChartError::InvalidValue {
9085                element: "c:plotArea".to_owned(),
9086                value: "opaque plot area has no typed validation view".to_owned(),
9087            })?;
9088        if plots.len() != 1 {
9089            return Err(ChartError::InvalidValue {
9090                element: "c:plotArea".to_owned(),
9091                value: "typed plot area requires exactly one plot family".to_owned(),
9092            });
9093        }
9094        validate_axis_pairs(&self.axes)?;
9095        for plot in plots {
9096            plot.validate()?;
9097            if let Some(axis_ids) = plot.axis_ids() {
9098                for id in axis_ids {
9099                    if !self.axes.iter().any(|axis| axis.id == id) {
9100                        return Err(ChartError::InvalidValue {
9101                            element: "c:axId".to_owned(),
9102                            value: format!("plot references missing axis {}", id.value()),
9103                        });
9104                    }
9105                }
9106            } else if !self.axes.is_empty() {
9107                return Err(ChartError::InvalidValue {
9108                    element: "c:plotArea".to_owned(),
9109                    value: "pie and doughnut plot areas must not own axes".to_owned(),
9110                });
9111            }
9112            if plot.axis_ids().is_some() && self.axes.len() != 2 {
9113                return Err(ChartError::InvalidValue {
9114                    element: "c:plotArea".to_owned(),
9115                    value: format!(
9116                        "axis-owned plot requires exactly 2 axes, found {}",
9117                        self.axes.len()
9118                    ),
9119                });
9120            }
9121        }
9122        Ok(())
9123    }
9124}
9125
9126impl Default for CT_PlotArea {
9127    fn default() -> Self {
9128        Self {
9129            raw_attributes: Vec::new(),
9130            raw_children: OrderedRawChildren::default(),
9131            namespace_bindings: chart_namespace_defaults(),
9132            plots: Some(Vec::new()),
9133            plot_markup: Vec::new(),
9134            axes: Vec::new(),
9135        }
9136    }
9137}
9138
9139fn chart_root_local(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Vec<u8>>> {
9140    let mut reader = Reader::from_reader(xml);
9141    let mut buffer = Vec::new();
9142    loop {
9143        match reader
9144            .read_event_into(&mut buffer)
9145            .map_err(OxmlError::from)?
9146        {
9147            Event::Start(element) | Event::Empty(element) => {
9148                return Ok(element_is_in_namespace(&element, C_NS, inherited)?
9149                    .then(|| local_name(element.name().as_ref()).to_vec()));
9150            }
9151            Event::Eof => return Ok(None),
9152            _ => {}
9153        }
9154        buffer.clear();
9155    }
9156}
9157
9158fn is_plot_container(local: &[u8]) -> bool {
9159    matches!(
9160        local,
9161        b"areaChart"
9162            | b"area3DChart"
9163            | b"barChart"
9164            | b"bar3DChart"
9165            | b"bubbleChart"
9166            | b"doughnutChart"
9167            | b"lineChart"
9168            | b"line3DChart"
9169            | b"ofPieChart"
9170            | b"pieChart"
9171            | b"pie3DChart"
9172            | b"radarChart"
9173            | b"scatterChart"
9174            | b"stockChart"
9175            | b"surfaceChart"
9176            | b"surface3DChart"
9177    )
9178}
9179
9180fn parse_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
9181    let local = chart_root_local(xml, inherited)?
9182        .ok_or_else(|| ChartError::MissingElement("c:barChart or c:lineChart".to_owned()))?;
9183    match local.as_slice() {
9184        b"barChart" => parse_bar_plot(xml, inherited),
9185        b"lineChart" => parse_line_plot(xml, inherited),
9186        b"pieChart" | b"doughnutChart" | b"areaChart" | b"scatterChart" | b"radarChart" => {
9187            parse_remaining_plot(xml, inherited, &local)
9188        }
9189        _ => Err(ChartError::UnexpectedElement(format!(
9190            "c:{}",
9191            String::from_utf8_lossy(&local)
9192        ))),
9193    }
9194}
9195
9196fn parse_remaining_plot(
9197    xml: &[u8],
9198    inherited: &NamespaceBindings,
9199    local: &[u8],
9200) -> Result<(Plot, PlotMarkup)> {
9201    let root = String::from_utf8_lossy(local).into_owned();
9202    let mut reader = Reader::from_reader(xml);
9203    let mut buffer = Vec::new();
9204    loop {
9205        match reader
9206            .read_event_into(&mut buffer)
9207            .map_err(OxmlError::from)?
9208        {
9209            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
9210                let namespaces = typed_rewrite_bindings(&element, inherited)?;
9211                let (mut raw_attributes, namespace_declarations) =
9212                    capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
9213                raw_attributes.splice(0..0, namespace_declarations);
9214                let mut grouping = None;
9215                let mut style = None;
9216                let mut first_slice_angle = None;
9217                let mut hole_size = None;
9218                let mut series = Vec::new();
9219                let mut data_labels = None;
9220                let mut axis_ids = Vec::new();
9221                let mut raw_children = OrderedRawChildren::default();
9222                let mut boundary = 0usize;
9223                let mut inner = Vec::new();
9224                loop {
9225                    match reader
9226                        .read_event_into(&mut inner)
9227                        .map_err(OxmlError::from)?
9228                    {
9229                        Event::Start(child) => {
9230                            let name = chart_child_local(&child, &namespaces)?;
9231                            let raw = capture_element(&mut reader, &child)?;
9232                            parse_remaining_plot_child(
9233                                local,
9234                                name.as_deref().unwrap_or_default(),
9235                                raw,
9236                                &namespaces,
9237                                &mut grouping,
9238                                &mut style,
9239                                &mut first_slice_angle,
9240                                &mut hole_size,
9241                                &mut series,
9242                                &mut data_labels,
9243                                &mut axis_ids,
9244                                &mut raw_children,
9245                                &mut boundary,
9246                            )?;
9247                        }
9248                        Event::Empty(child) => {
9249                            let name = chart_child_local(&child, &namespaces)?;
9250                            let raw = capture_empty_element(&child)?;
9251                            parse_remaining_plot_child(
9252                                local,
9253                                name.as_deref().unwrap_or_default(),
9254                                raw,
9255                                &namespaces,
9256                                &mut grouping,
9257                                &mut style,
9258                                &mut first_slice_angle,
9259                                &mut hole_size,
9260                                &mut series,
9261                                &mut data_labels,
9262                                &mut axis_ids,
9263                                &mut raw_children,
9264                                &mut boundary,
9265                            )?;
9266                        }
9267                        event @ (Event::Text(_)
9268                        | Event::CData(_)
9269                        | Event::Comment(_)
9270                        | Event::PI(_)
9271                        | Event::GeneralRef(_)) => {
9272                            raw_children.push(boundary, capture_event(event)?);
9273                        }
9274                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
9275                            let needs_axes =
9276                                matches!(local, b"areaChart" | b"scatterChart" | b"radarChart");
9277                            if needs_axes && axis_ids.len() != 2 {
9278                                return Err(ChartError::InvalidValue {
9279                                    element: format!("c:{root}/c:axId"),
9280                                    value: format!("expected 2, found {}", axis_ids.len()),
9281                                });
9282                            }
9283                            if !needs_axes && !axis_ids.is_empty() {
9284                                return Err(ChartError::InvalidValue {
9285                                    element: format!("c:{root}/c:axId"),
9286                                    value: "axis-free plot contains axis references".to_owned(),
9287                                });
9288                            }
9289                            let parsed_ids = needs_axes.then(|| [axis_ids[0].0, axis_ids[1].0]);
9290                            let original_series_keys = series
9291                                .iter()
9292                                .map(|item| (item.index, item.order))
9293                                .collect::<Vec<_>>();
9294                            let plot = match local {
9295                                b"pieChart" => Plot::Pie {
9296                                    first_slice_angle: first_slice_angle
9297                                        .as_ref()
9298                                        .map_or(0, |item| item.0),
9299                                    series,
9300                                    data_labels,
9301                                },
9302                                b"doughnutChart" => Plot::Doughnut {
9303                                    first_slice_angle: first_slice_angle
9304                                        .as_ref()
9305                                        .map_or(0, |item| item.0),
9306                                    hole_size: hole_size.as_ref().map_or(50, |item| item.0),
9307                                    series,
9308                                    data_labels,
9309                                },
9310                                b"areaChart" => Plot::Area {
9311                                    grouping: grouping
9312                                        .as_ref()
9313                                        .ok_or_else(|| {
9314                                            ChartError::MissingElement("c:grouping".to_owned())
9315                                        })?
9316                                        .0,
9317                                    series,
9318                                    data_labels,
9319                                    axis_ids: parsed_ids.ok_or_else(|| {
9320                                        ChartError::MissingElement("c:areaChart/c:axId".to_owned())
9321                                    })?,
9322                                },
9323                                b"scatterChart" => {
9324                                    let lexical = style
9325                                        .as_ref()
9326                                        .ok_or_else(|| {
9327                                            ChartError::MissingElement("c:scatterStyle".to_owned())
9328                                        })?
9329                                        .0
9330                                        .as_str();
9331                                    Plot::Scatter {
9332                                        style: ScatterStyle::parse(lexical).ok_or_else(|| {
9333                                            invalid_attribute(
9334                                                "scatterStyle",
9335                                                "val",
9336                                                lexical.to_owned(),
9337                                            )
9338                                        })?,
9339                                        series,
9340                                        data_labels,
9341                                        axis_ids: parsed_ids.ok_or_else(|| {
9342                                            ChartError::MissingElement(
9343                                                "c:scatterChart/c:axId".to_owned(),
9344                                            )
9345                                        })?,
9346                                    }
9347                                }
9348                                b"radarChart" => {
9349                                    let lexical = style
9350                                        .as_ref()
9351                                        .ok_or_else(|| {
9352                                            ChartError::MissingElement("c:radarStyle".to_owned())
9353                                        })?
9354                                        .0
9355                                        .as_str();
9356                                    Plot::Radar {
9357                                        style: RadarStyle::parse(lexical).ok_or_else(|| {
9358                                            invalid_attribute(
9359                                                "radarStyle",
9360                                                "val",
9361                                                lexical.to_owned(),
9362                                            )
9363                                        })?,
9364                                        series,
9365                                        data_labels,
9366                                        axis_ids: parsed_ids.ok_or_else(|| {
9367                                            ChartError::MissingElement(
9368                                                "c:radarChart/c:axId".to_owned(),
9369                                            )
9370                                        })?,
9371                                    }
9372                                }
9373                                _ => {
9374                                    return Err(ChartError::UnexpectedElement(format!(
9375                                        "c:{}",
9376                                        String::from_utf8_lossy(local)
9377                                    )));
9378                                }
9379                            };
9380                            plot.validate()?;
9381                            let final_boundary = boundary.max(original_series_keys.len() + 6);
9382                            return Ok((
9383                                plot,
9384                                PlotMarkup {
9385                                    raw_attributes,
9386                                    raw_children: raw_children_in_schema_order(
9387                                        &raw_children,
9388                                        final_boundary,
9389                                    ),
9390                                    grouping: grouping.map(|item| item.1),
9391                                    first_slice_angle: first_slice_angle.map(|item| item.1),
9392                                    hole_size: hole_size.map(|item| item.1),
9393                                    style: style.map(|item| item.1),
9394                                    axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
9395                                    original_series_keys,
9396                                    original_axis_ids: parsed_ids
9397                                        .map_or_else(Vec::new, |ids| ids.to_vec()),
9398                                    parsed_remaining: match local {
9399                                        b"pieChart" => Some("pieChart"),
9400                                        b"doughnutChart" => Some("doughnutChart"),
9401                                        b"areaChart" => Some("areaChart"),
9402                                        b"scatterChart" => Some("scatterChart"),
9403                                        b"radarChart" => Some("radarChart"),
9404                                        _ => None,
9405                                    },
9406                                    ..PlotMarkup::default()
9407                                },
9408                            ));
9409                        }
9410                        Event::Eof => return Err(missing_end(&format!("c:{root}"))),
9411                        _ => {}
9412                    }
9413                    inner.clear();
9414                }
9415            }
9416            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
9417                return Err(ChartError::MissingElement("c:ser".to_owned()));
9418            }
9419            Event::Start(element) | Event::Empty(element) => {
9420                return Err(ChartError::UnexpectedElement(element_name(&element)));
9421            }
9422            Event::Eof => return Err(ChartError::MissingElement(format!("c:{root}"))),
9423            _ => {}
9424        }
9425        buffer.clear();
9426    }
9427}
9428
9429#[allow(clippy::too_many_arguments)]
9430fn parse_remaining_plot_child(
9431    root: &[u8],
9432    name: &[u8],
9433    raw: Vec<u8>,
9434    namespaces: &NamespaceBindings,
9435    grouping: &mut Option<(Grouping, ScalarMarkup)>,
9436    style: &mut Option<(String, ScalarMarkup)>,
9437    first_slice_angle: &mut Option<(u16, ScalarMarkup)>,
9438    hole_size: &mut Option<(u8, ScalarMarkup)>,
9439    series: &mut Vec<Series>,
9440    data_labels: &mut Option<CT_DLbls>,
9441    axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
9442    raw_children: &mut OrderedRawChildren,
9443    boundary: &mut usize,
9444) -> Result<()> {
9445    let property_first = matches!(root, b"areaChart" | b"scatterChart" | b"radarChart");
9446    let base = usize::from(property_first);
9447    match name {
9448        b"grouping" if root == b"areaChart" => {
9449            let (value, markup) = required_scalar(&raw, "grouping")?;
9450            let parsed = Grouping::parse(&value)
9451                .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
9452            set_once(grouping, (parsed, markup), "c:grouping")?;
9453            *boundary = (*boundary).max(1);
9454        }
9455        b"scatterStyle" if root == b"scatterChart" => {
9456            let (value, markup) = required_scalar(&raw, "scatterStyle")?;
9457            if ScatterStyle::parse(&value).is_none() {
9458                return Err(invalid_attribute("scatterStyle", "val", value));
9459            }
9460            set_once(style, (value, markup), "c:scatterStyle")?;
9461            *boundary = (*boundary).max(1);
9462        }
9463        b"radarStyle" if root == b"radarChart" => {
9464            let (value, markup) = required_scalar(&raw, "radarStyle")?;
9465            if RadarStyle::parse(&value).is_none() {
9466                return Err(invalid_attribute("radarStyle", "val", value));
9467            }
9468            set_once(style, (value, markup), "c:radarStyle")?;
9469            *boundary = (*boundary).max(1);
9470        }
9471        b"ser" => {
9472            series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
9473            *boundary = (*boundary).max(base + series.len());
9474        }
9475        b"dLbls" => {
9476            set_once(
9477                data_labels,
9478                CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
9479                "c:dLbls",
9480            )?;
9481            *boundary = (*boundary).max(base + series.len() + 1);
9482        }
9483        b"firstSliceAng" if matches!(root, b"pieChart" | b"doughnutChart") => {
9484            let (value, markup) = required_scalar(&raw, "firstSliceAng")?;
9485            let parsed = value
9486                .parse::<u16>()
9487                .map_err(|_| invalid_attribute("firstSliceAng", "val", value.clone()))?;
9488            if parsed > 360 {
9489                return Err(invalid_attribute("firstSliceAng", "val", value));
9490            }
9491            set_once(first_slice_angle, (parsed, markup), "c:firstSliceAng")?;
9492            *boundary = (*boundary).max(series.len() + 2);
9493        }
9494        b"holeSize" if root == b"doughnutChart" => {
9495            let (value, markup) = required_scalar(&raw, "holeSize")?;
9496            let parsed = value
9497                .parse::<u8>()
9498                .map_err(|_| invalid_attribute("holeSize", "val", value.clone()))?;
9499            if !(10..=90).contains(&parsed) {
9500                return Err(invalid_attribute("holeSize", "val", value));
9501            }
9502            set_once(hole_size, (parsed, markup), "c:holeSize")?;
9503            *boundary = (*boundary).max(series.len() + 3);
9504        }
9505        b"axId" if property_first => {
9506            if axis_ids.len() == 2 {
9507                return Err(ChartError::DuplicateElement("c:axId".to_owned()));
9508            }
9509            axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
9510            *boundary = (*boundary).max(base + series.len() + 1 + axis_ids.len());
9511        }
9512        b"axId" => {
9513            return Err(ChartError::InvalidValue {
9514                element: "c:axId".to_owned(),
9515                value: "axis-free plot contains axis reference".to_owned(),
9516            });
9517        }
9518        _ => {
9519            let raw_boundary = match (root, name) {
9520                (_, b"varyColors") => base,
9521                (b"pieChart", b"extLst") => series.len() + 2,
9522                (b"doughnutChart", b"extLst") => series.len() + 3,
9523                (b"areaChart" | b"scatterChart" | b"radarChart", b"dropLines") => {
9524                    base + series.len() + 1
9525                }
9526                (b"areaChart" | b"scatterChart" | b"radarChart", b"extLst") => {
9527                    base + series.len() + 3
9528                }
9529                _ => *boundary,
9530            };
9531            raw_children.push(raw_boundary, raw);
9532            *boundary = (*boundary).max(raw_boundary);
9533        }
9534    }
9535    Ok(())
9536}
9537
9538fn parse_bar_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
9539    let mut reader = Reader::from_reader(xml);
9540    let mut buffer = Vec::new();
9541    loop {
9542        match reader
9543            .read_event_into(&mut buffer)
9544            .map_err(OxmlError::from)?
9545        {
9546            Event::Start(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
9547                let namespaces = typed_rewrite_bindings(&element, inherited)?;
9548                let (mut raw_attributes, namespace_declarations) =
9549                    capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
9550                raw_attributes.splice(0..0, namespace_declarations);
9551                let mut direction = None;
9552                let mut grouping = None;
9553                let mut gap_width = None;
9554                let mut overlap = None;
9555                let mut series = Vec::new();
9556                let mut data_labels = None;
9557                let mut axis_ids = Vec::new();
9558                let mut raw_children = OrderedRawChildren::default();
9559                let mut boundary = 0usize;
9560                let mut inner = Vec::new();
9561                loop {
9562                    match reader
9563                        .read_event_into(&mut inner)
9564                        .map_err(OxmlError::from)?
9565                    {
9566                        Event::Start(child) => {
9567                            let name = chart_child_local(&child, &namespaces)?;
9568                            let raw = capture_element(&mut reader, &child)?;
9569                            parse_bar_child(
9570                                name.as_deref().unwrap_or_default(),
9571                                raw,
9572                                &namespaces,
9573                                &mut direction,
9574                                &mut grouping,
9575                                &mut gap_width,
9576                                &mut overlap,
9577                                &mut series,
9578                                &mut data_labels,
9579                                &mut axis_ids,
9580                                &mut raw_children,
9581                                &mut boundary,
9582                            )?;
9583                        }
9584                        Event::Empty(child) => {
9585                            let name = chart_child_local(&child, &namespaces)?;
9586                            let raw = capture_empty_element(&child)?;
9587                            parse_bar_child(
9588                                name.as_deref().unwrap_or_default(),
9589                                raw,
9590                                &namespaces,
9591                                &mut direction,
9592                                &mut grouping,
9593                                &mut gap_width,
9594                                &mut overlap,
9595                                &mut series,
9596                                &mut data_labels,
9597                                &mut axis_ids,
9598                                &mut raw_children,
9599                                &mut boundary,
9600                            )?;
9601                        }
9602                        event @ (Event::Text(_)
9603                        | Event::CData(_)
9604                        | Event::Comment(_)
9605                        | Event::PI(_)
9606                        | Event::GeneralRef(_)) => {
9607                            raw_children.push(boundary, capture_event(event)?);
9608                        }
9609                        Event::End(end) if matches_local_name(end.name().as_ref(), b"barChart") => {
9610                            let (direction, direction_markup) = direction
9611                                .ok_or_else(|| ChartError::MissingElement("c:barDir".to_owned()))?;
9612                            let (grouping, grouping_markup) = grouping.ok_or_else(|| {
9613                                ChartError::MissingElement("c:grouping".to_owned())
9614                            })?;
9615                            if axis_ids.len() != 2 {
9616                                return Err(ChartError::InvalidValue {
9617                                    element: "c:barChart/c:axId".to_owned(),
9618                                    value: format!("expected 2, found {}", axis_ids.len()),
9619                                });
9620                            }
9621                            let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
9622                            let original_series_keys: Vec<(u32, u32)> =
9623                                series.iter().map(|item| (item.index, item.order)).collect();
9624                            let plot = Plot::Bar {
9625                                direction,
9626                                grouping,
9627                                gap_width: gap_width.as_ref().map_or(150, |item| item.0),
9628                                overlap: overlap.as_ref().map_or(0, |item| item.0),
9629                                series,
9630                                data_labels,
9631                                axis_ids: parsed_ids,
9632                            };
9633                            plot.validate()?;
9634                            return Ok((
9635                                plot,
9636                                PlotMarkup {
9637                                    raw_attributes,
9638                                    raw_children: raw_children_in_schema_order(
9639                                        &raw_children,
9640                                        original_series_keys.len() + 7,
9641                                    ),
9642                                    direction: Some(direction_markup),
9643                                    grouping: Some(grouping_markup),
9644                                    gap_width: gap_width.map(|item| item.1),
9645                                    overlap: overlap.map(|item| item.1),
9646                                    marker: None,
9647                                    smooth: None,
9648                                    first_slice_angle: None,
9649                                    hole_size: None,
9650                                    style: None,
9651                                    axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
9652                                    original_series_keys,
9653                                    original_axis_ids: parsed_ids.to_vec(),
9654                                    parsed_bar: Some(true),
9655                                    parsed_remaining: None,
9656                                },
9657                            ));
9658                        }
9659                        Event::Eof => return Err(missing_end("c:barChart")),
9660                        _ => {}
9661                    }
9662                    inner.clear();
9663                }
9664            }
9665            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
9666                return Err(ChartError::MissingElement("c:barDir".to_owned()));
9667            }
9668            Event::Start(element) | Event::Empty(element) => {
9669                return Err(ChartError::UnexpectedElement(element_name(&element)));
9670            }
9671            Event::Eof => return Err(ChartError::MissingElement("c:barChart".to_owned())),
9672            _ => {}
9673        }
9674        buffer.clear();
9675    }
9676}
9677
9678#[allow(clippy::too_many_arguments)]
9679fn parse_bar_child(
9680    name: &[u8],
9681    raw: Vec<u8>,
9682    namespaces: &NamespaceBindings,
9683    direction: &mut Option<(BarDirection, ScalarMarkup)>,
9684    grouping: &mut Option<(BarGrouping, ScalarMarkup)>,
9685    gap_width: &mut Option<(u16, ScalarMarkup)>,
9686    overlap: &mut Option<(i8, ScalarMarkup)>,
9687    series: &mut Vec<Series>,
9688    data_labels: &mut Option<CT_DLbls>,
9689    axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
9690    raw_children: &mut OrderedRawChildren,
9691    boundary: &mut usize,
9692) -> Result<()> {
9693    match name {
9694        b"barDir" => {
9695            let (value, markup) = required_scalar(&raw, "barDir")?;
9696            let value = BarDirection::parse(&value)
9697                .ok_or_else(|| invalid_attribute("barDir", "val", value))?;
9698            set_once(direction, (value, markup), "c:barDir")?;
9699            *boundary = (*boundary).max(1);
9700        }
9701        b"grouping" => {
9702            let (value, markup) = required_scalar(&raw, "grouping")?;
9703            let value = BarGrouping::parse(&value)
9704                .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
9705            set_once(grouping, (value, markup), "c:grouping")?;
9706            *boundary = (*boundary).max(2);
9707        }
9708        b"ser" => {
9709            series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
9710            *boundary = (*boundary).max(series.len() + 2);
9711        }
9712        b"dLbls" => {
9713            set_once(
9714                data_labels,
9715                CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
9716                "c:dLbls",
9717            )?;
9718            *boundary = (*boundary).max(series.len() + 3);
9719        }
9720        b"gapWidth" => {
9721            let (value, markup) = required_scalar(&raw, "gapWidth")?;
9722            let parsed = value
9723                .parse::<u16>()
9724                .map_err(|_| invalid_attribute("gapWidth", "val", value.clone()))?;
9725            if parsed > 500 {
9726                return Err(invalid_attribute("gapWidth", "val", value));
9727            }
9728            set_once(gap_width, (parsed, markup), "c:gapWidth")?;
9729            *boundary = (*boundary).max(series.len() + 4);
9730        }
9731        b"overlap" => {
9732            let (value, markup) = required_scalar(&raw, "overlap")?;
9733            let parsed = value
9734                .parse::<i8>()
9735                .map_err(|_| invalid_attribute("overlap", "val", value.clone()))?;
9736            if !(-100..=100).contains(&parsed) {
9737                return Err(invalid_attribute("overlap", "val", value));
9738            }
9739            set_once(overlap, (parsed, markup), "c:overlap")?;
9740            *boundary = (*boundary).max(series.len() + 5);
9741        }
9742        b"axId" => {
9743            if axis_ids.len() == 2 {
9744                return Err(ChartError::DuplicateElement("c:axId".to_owned()));
9745            }
9746            axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
9747            *boundary = (*boundary).max(series.len() + 5 + axis_ids.len());
9748        }
9749        _ => raw_children.push(*boundary, raw),
9750    }
9751    Ok(())
9752}
9753
9754fn parse_line_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
9755    let mut reader = Reader::from_reader(xml);
9756    let mut buffer = Vec::new();
9757    loop {
9758        match reader
9759            .read_event_into(&mut buffer)
9760            .map_err(OxmlError::from)?
9761        {
9762            Event::Start(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
9763                let namespaces = typed_rewrite_bindings(&element, inherited)?;
9764                let (mut raw_attributes, namespace_declarations) =
9765                    capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
9766                raw_attributes.splice(0..0, namespace_declarations);
9767                let mut grouping = None;
9768                let mut marker = None;
9769                let mut smooth = None;
9770                let mut series = Vec::new();
9771                let mut data_labels = None;
9772                let mut axis_ids = Vec::new();
9773                let mut raw_children = OrderedRawChildren::default();
9774                let mut boundary = 0usize;
9775                let mut inner = Vec::new();
9776                loop {
9777                    match reader
9778                        .read_event_into(&mut inner)
9779                        .map_err(OxmlError::from)?
9780                    {
9781                        Event::Start(child) => {
9782                            let name = chart_child_local(&child, &namespaces)?;
9783                            let raw = capture_element(&mut reader, &child)?;
9784                            parse_line_child(
9785                                name.as_deref().unwrap_or_default(),
9786                                raw,
9787                                &namespaces,
9788                                &mut grouping,
9789                                &mut marker,
9790                                &mut smooth,
9791                                &mut series,
9792                                &mut data_labels,
9793                                &mut axis_ids,
9794                                &mut raw_children,
9795                                &mut boundary,
9796                            )?;
9797                        }
9798                        Event::Empty(child) => {
9799                            let name = chart_child_local(&child, &namespaces)?;
9800                            let raw = capture_empty_element(&child)?;
9801                            parse_line_child(
9802                                name.as_deref().unwrap_or_default(),
9803                                raw,
9804                                &namespaces,
9805                                &mut grouping,
9806                                &mut marker,
9807                                &mut smooth,
9808                                &mut series,
9809                                &mut data_labels,
9810                                &mut axis_ids,
9811                                &mut raw_children,
9812                                &mut boundary,
9813                            )?;
9814                        }
9815                        event @ (Event::Text(_)
9816                        | Event::CData(_)
9817                        | Event::Comment(_)
9818                        | Event::PI(_)
9819                        | Event::GeneralRef(_)) => {
9820                            raw_children.push(boundary, capture_event(event)?);
9821                        }
9822                        Event::End(end)
9823                            if matches_local_name(end.name().as_ref(), b"lineChart") =>
9824                        {
9825                            let (grouping, grouping_markup) = grouping.ok_or_else(|| {
9826                                ChartError::MissingElement("c:grouping".to_owned())
9827                            })?;
9828                            if axis_ids.len() != 2 {
9829                                return Err(ChartError::InvalidValue {
9830                                    element: "c:lineChart/c:axId".to_owned(),
9831                                    value: format!("expected 2, found {}", axis_ids.len()),
9832                                });
9833                            }
9834                            let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
9835                            let original_series_keys: Vec<(u32, u32)> =
9836                                series.iter().map(|item| (item.index, item.order)).collect();
9837                            let plot = Plot::Line {
9838                                grouping,
9839                                marker: marker.as_ref().is_some_and(|item| item.0),
9840                                smooth: smooth.as_ref().is_some_and(|item| item.0),
9841                                series,
9842                                data_labels,
9843                                axis_ids: parsed_ids,
9844                            };
9845                            plot.validate()?;
9846                            return Ok((
9847                                plot,
9848                                PlotMarkup {
9849                                    raw_attributes,
9850                                    raw_children: raw_children_in_schema_order(
9851                                        &raw_children,
9852                                        original_series_keys.len() + 6,
9853                                    ),
9854                                    direction: None,
9855                                    grouping: Some(grouping_markup),
9856                                    gap_width: None,
9857                                    overlap: None,
9858                                    marker: marker.map(|item| item.1),
9859                                    smooth: smooth.map(|item| item.1),
9860                                    first_slice_angle: None,
9861                                    hole_size: None,
9862                                    style: None,
9863                                    axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
9864                                    original_series_keys,
9865                                    original_axis_ids: parsed_ids.to_vec(),
9866                                    parsed_bar: Some(false),
9867                                    parsed_remaining: None,
9868                                },
9869                            ));
9870                        }
9871                        Event::Eof => return Err(missing_end("c:lineChart")),
9872                        _ => {}
9873                    }
9874                    inner.clear();
9875                }
9876            }
9877            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
9878                return Err(ChartError::MissingElement("c:grouping".to_owned()));
9879            }
9880            Event::Start(element) | Event::Empty(element) => {
9881                return Err(ChartError::UnexpectedElement(element_name(&element)));
9882            }
9883            Event::Eof => return Err(ChartError::MissingElement("c:lineChart".to_owned())),
9884            _ => {}
9885        }
9886        buffer.clear();
9887    }
9888}
9889
9890#[allow(clippy::too_many_arguments)]
9891fn parse_line_child(
9892    name: &[u8],
9893    raw: Vec<u8>,
9894    namespaces: &NamespaceBindings,
9895    grouping: &mut Option<(Grouping, ScalarMarkup)>,
9896    marker: &mut Option<(bool, ScalarMarkup)>,
9897    smooth: &mut Option<(bool, ScalarMarkup)>,
9898    series: &mut Vec<Series>,
9899    data_labels: &mut Option<CT_DLbls>,
9900    axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
9901    raw_children: &mut OrderedRawChildren,
9902    boundary: &mut usize,
9903) -> Result<()> {
9904    match name {
9905        b"grouping" => {
9906            let (value, markup) = required_scalar(&raw, "grouping")?;
9907            let value = Grouping::parse(&value)
9908                .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
9909            set_once(grouping, (value, markup), "c:grouping")?;
9910            *boundary = (*boundary).max(1);
9911        }
9912        b"ser" => {
9913            series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
9914            *boundary = (*boundary).max(series.len() + 1);
9915        }
9916        b"dLbls" => {
9917            set_once(
9918                data_labels,
9919                CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
9920                "c:dLbls",
9921            )?;
9922            *boundary = (*boundary).max(series.len() + 2);
9923        }
9924        b"marker" => {
9925            set_once(marker, parse_bool_value(&raw, "marker")?, "c:marker")?;
9926            *boundary = (*boundary).max(series.len() + 3);
9927        }
9928        b"smooth" => {
9929            set_once(smooth, parse_bool_value(&raw, "smooth")?, "c:smooth")?;
9930            *boundary = (*boundary).max(series.len() + 4);
9931        }
9932        b"axId" => {
9933            if axis_ids.len() == 2 {
9934                return Err(ChartError::DuplicateElement("c:axId".to_owned()));
9935            }
9936            axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
9937            *boundary = (*boundary).max(series.len() + 4 + axis_ids.len());
9938        }
9939        _ => raw_children.push(*boundary, raw),
9940    }
9941    Ok(())
9942}
9943
9944fn required_scalar(xml: &[u8], local: &str) -> Result<(String, ScalarMarkup)> {
9945    let (value, markup) = scalar_value(xml, local)?;
9946    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
9947    Ok((value, markup))
9948}
9949
9950fn write_plot<W: Write>(writer: &mut Writer<W>, plot: &Plot, markup: &PlotMarkup) -> Result<()> {
9951    plot.validate()?;
9952    if let Some(parsed_bar) = markup.parsed_bar
9953        && parsed_bar != matches!(plot, Plot::Bar { .. })
9954    {
9955        return Err(ChartError::InvalidValue {
9956            element: "c:plotArea".to_owned(),
9957            value: "a parsed plot family cannot be replaced while preserved payload remains"
9958                .to_owned(),
9959        });
9960    }
9961    let current_remaining = match plot {
9962        Plot::Pie { .. } => Some("pieChart"),
9963        Plot::Doughnut { .. } => Some("doughnutChart"),
9964        Plot::Area { .. } => Some("areaChart"),
9965        Plot::Scatter { .. } => Some("scatterChart"),
9966        Plot::Radar { .. } => Some("radarChart"),
9967        Plot::Bar { .. } | Plot::Line { .. } => None,
9968    };
9969    if let Some(parsed) = markup.parsed_remaining
9970        && current_remaining != Some(parsed)
9971    {
9972        return Err(ChartError::InvalidValue {
9973            element: "c:plotArea".to_owned(),
9974            value: "a parsed plot family cannot be replaced while preserved payload remains"
9975                .to_owned(),
9976        });
9977    }
9978    match plot {
9979        Plot::Bar {
9980            direction,
9981            grouping,
9982            gap_width,
9983            overlap,
9984            series,
9985            data_labels,
9986            axis_ids,
9987        } => {
9988            let mut start = BytesStart::new("c:barChart");
9989            push_attributes(&mut start, &markup.raw_attributes);
9990            writer
9991                .write_event(Event::Start(start))
9992                .map_err(OxmlError::from)?;
9993            emit_raw(writer, markup.raw_children.at(0))?;
9994            write_scalar(
9995                writer,
9996                "c:barDir",
9997                direction.as_str(),
9998                markup.direction.as_ref(),
9999            )?;
10000            emit_raw(writer, markup.raw_children.at(1))?;
10001            write_scalar(
10002                writer,
10003                "c:grouping",
10004                grouping.as_str(),
10005                markup.grouping.as_ref(),
10006            )?;
10007            let original_to_current =
10008                series_original_to_current(&markup.original_series_keys, series);
10009            emit_repeated_raw(
10010                writer,
10011                &markup.raw_children,
10012                2,
10013                &original_to_current,
10014                series.len(),
10015                0,
10016            )?;
10017            for (index, item) in series.iter().enumerate() {
10018                writer
10019                    .get_mut()
10020                    .write_all(&item.to_xml()?)
10021                    .map_err(OxmlError::from)?;
10022                emit_repeated_raw(
10023                    writer,
10024                    &markup.raw_children,
10025                    2,
10026                    &original_to_current,
10027                    series.len(),
10028                    index + 1,
10029                )?;
10030            }
10031            if let Some(labels) = data_labels {
10032                writer
10033                    .get_mut()
10034                    .write_all(&labels.to_xml()?)
10035                    .map_err(OxmlError::from)?;
10036            }
10037            let trailing = markup.original_series_keys.len();
10038            emit_raw(writer, markup.raw_children.at(trailing + 3))?;
10039            if *gap_width != 150 || markup.gap_width.is_some() {
10040                write_scalar(
10041                    writer,
10042                    "c:gapWidth",
10043                    &gap_width.to_string(),
10044                    markup.gap_width.as_ref(),
10045                )?;
10046            }
10047            emit_raw(writer, markup.raw_children.at(trailing + 4))?;
10048            if *overlap != 0 || markup.overlap.is_some() {
10049                write_scalar(
10050                    writer,
10051                    "c:overlap",
10052                    &overlap.to_string(),
10053                    markup.overlap.as_ref(),
10054                )?;
10055            }
10056            let axis_original_to_current =
10057                axis_original_to_current(&markup.original_axis_ids, axis_ids);
10058            let axis_current_to_original =
10059                invert_original_to_current(&axis_original_to_current, axis_ids.len());
10060            emit_repeated_raw(
10061                writer,
10062                &markup.raw_children,
10063                trailing + 5,
10064                &axis_original_to_current,
10065                axis_ids.len(),
10066                0,
10067            )?;
10068            for (index, id) in axis_ids.iter().enumerate() {
10069                let default_markup = default_axis_id_markup(*id);
10070                let original_markup = axis_current_to_original[index]
10071                    .and_then(|original| markup.axis_ids.get(original));
10072                write_axis_id(
10073                    writer,
10074                    "c:axId",
10075                    *id,
10076                    original_markup.unwrap_or(&default_markup),
10077                )?;
10078                emit_repeated_raw(
10079                    writer,
10080                    &markup.raw_children,
10081                    trailing + 5,
10082                    &axis_original_to_current,
10083                    axis_ids.len(),
10084                    index + 1,
10085                )?;
10086            }
10087            writer
10088                .write_event(Event::End(BytesEnd::new("c:barChart")))
10089                .map_err(OxmlError::from)?;
10090        }
10091        Plot::Line {
10092            grouping,
10093            marker,
10094            smooth,
10095            series,
10096            data_labels,
10097            axis_ids,
10098        } => {
10099            let mut start = BytesStart::new("c:lineChart");
10100            push_attributes(&mut start, &markup.raw_attributes);
10101            writer
10102                .write_event(Event::Start(start))
10103                .map_err(OxmlError::from)?;
10104            emit_raw(writer, markup.raw_children.at(0))?;
10105            write_scalar(
10106                writer,
10107                "c:grouping",
10108                grouping.as_str(),
10109                markup.grouping.as_ref(),
10110            )?;
10111            let original_to_current =
10112                series_original_to_current(&markup.original_series_keys, series);
10113            emit_repeated_raw(
10114                writer,
10115                &markup.raw_children,
10116                1,
10117                &original_to_current,
10118                series.len(),
10119                0,
10120            )?;
10121            for (index, item) in series.iter().enumerate() {
10122                writer
10123                    .get_mut()
10124                    .write_all(&item.to_xml()?)
10125                    .map_err(OxmlError::from)?;
10126                emit_repeated_raw(
10127                    writer,
10128                    &markup.raw_children,
10129                    1,
10130                    &original_to_current,
10131                    series.len(),
10132                    index + 1,
10133                )?;
10134            }
10135            if let Some(labels) = data_labels {
10136                writer
10137                    .get_mut()
10138                    .write_all(&labels.to_xml()?)
10139                    .map_err(OxmlError::from)?;
10140            }
10141            let trailing = markup.original_series_keys.len();
10142            emit_raw(writer, markup.raw_children.at(trailing + 2))?;
10143            if *marker || markup.marker.is_some() {
10144                write_scalar(
10145                    writer,
10146                    "c:marker",
10147                    bool_lexical(*marker),
10148                    markup.marker.as_ref(),
10149                )?;
10150            }
10151            emit_raw(writer, markup.raw_children.at(trailing + 3))?;
10152            if *smooth || markup.smooth.is_some() {
10153                write_scalar(
10154                    writer,
10155                    "c:smooth",
10156                    bool_lexical(*smooth),
10157                    markup.smooth.as_ref(),
10158                )?;
10159            }
10160            let axis_original_to_current =
10161                axis_original_to_current(&markup.original_axis_ids, axis_ids);
10162            let axis_current_to_original =
10163                invert_original_to_current(&axis_original_to_current, axis_ids.len());
10164            emit_repeated_raw(
10165                writer,
10166                &markup.raw_children,
10167                trailing + 4,
10168                &axis_original_to_current,
10169                axis_ids.len(),
10170                0,
10171            )?;
10172            for (index, id) in axis_ids.iter().enumerate() {
10173                let default_markup = default_axis_id_markup(*id);
10174                let original_markup = axis_current_to_original[index]
10175                    .and_then(|original| markup.axis_ids.get(original));
10176                write_axis_id(
10177                    writer,
10178                    "c:axId",
10179                    *id,
10180                    original_markup.unwrap_or(&default_markup),
10181                )?;
10182                emit_repeated_raw(
10183                    writer,
10184                    &markup.raw_children,
10185                    trailing + 4,
10186                    &axis_original_to_current,
10187                    axis_ids.len(),
10188                    index + 1,
10189                )?;
10190            }
10191            writer
10192                .write_event(Event::End(BytesEnd::new("c:lineChart")))
10193                .map_err(OxmlError::from)?;
10194        }
10195        remaining => {
10196            let (
10197                root,
10198                property,
10199                series,
10200                data_labels,
10201                axis_ids,
10202                first_slice_angle,
10203                hole_size,
10204                scatter,
10205            ) = match remaining {
10206                Plot::Pie {
10207                    first_slice_angle,
10208                    series,
10209                    data_labels,
10210                } => (
10211                    "pieChart",
10212                    None,
10213                    series,
10214                    data_labels,
10215                    None,
10216                    Some(*first_slice_angle),
10217                    None,
10218                    false,
10219                ),
10220                Plot::Doughnut {
10221                    first_slice_angle,
10222                    hole_size,
10223                    series,
10224                    data_labels,
10225                } => (
10226                    "doughnutChart",
10227                    None,
10228                    series,
10229                    data_labels,
10230                    None,
10231                    Some(*first_slice_angle),
10232                    Some(*hole_size),
10233                    false,
10234                ),
10235                Plot::Area {
10236                    grouping,
10237                    series,
10238                    data_labels,
10239                    axis_ids,
10240                } => (
10241                    "areaChart",
10242                    Some(("c:grouping", grouping.as_str(), markup.grouping.as_ref())),
10243                    series,
10244                    data_labels,
10245                    Some(axis_ids),
10246                    None,
10247                    None,
10248                    false,
10249                ),
10250                Plot::Scatter {
10251                    style,
10252                    series,
10253                    data_labels,
10254                    axis_ids,
10255                } => (
10256                    "scatterChart",
10257                    Some(("c:scatterStyle", style.as_str(), markup.style.as_ref())),
10258                    series,
10259                    data_labels,
10260                    Some(axis_ids),
10261                    None,
10262                    None,
10263                    true,
10264                ),
10265                Plot::Radar {
10266                    style,
10267                    series,
10268                    data_labels,
10269                    axis_ids,
10270                } => (
10271                    "radarChart",
10272                    Some(("c:radarStyle", style.as_str(), markup.style.as_ref())),
10273                    series,
10274                    data_labels,
10275                    Some(axis_ids),
10276                    None,
10277                    None,
10278                    false,
10279                ),
10280                Plot::Bar { .. } | Plot::Line { .. } => unreachable!("handled above"),
10281            };
10282            let tag = format!("c:{root}");
10283            let mut start = BytesStart::new(&tag);
10284            push_attributes(&mut start, &markup.raw_attributes);
10285            writer
10286                .write_event(Event::Start(start))
10287                .map_err(OxmlError::from)?;
10288            let base = usize::from(property.is_some());
10289            if let Some((name, value, scalar_markup)) = property {
10290                emit_raw(writer, markup.raw_children.at(0))?;
10291                write_scalar(writer, name, value, scalar_markup)?;
10292            }
10293            let original_to_current =
10294                series_original_to_current(&markup.original_series_keys, series);
10295            emit_repeated_raw(
10296                writer,
10297                &markup.raw_children,
10298                base,
10299                &original_to_current,
10300                series.len(),
10301                0,
10302            )?;
10303            for (index, item) in series.iter().enumerate() {
10304                writer
10305                    .get_mut()
10306                    .write_all(&item.to_xml_for_plot(scatter)?)
10307                    .map_err(OxmlError::from)?;
10308                emit_repeated_raw(
10309                    writer,
10310                    &markup.raw_children,
10311                    base,
10312                    &original_to_current,
10313                    series.len(),
10314                    index + 1,
10315                )?;
10316            }
10317            if let Some(labels) = data_labels {
10318                writer
10319                    .get_mut()
10320                    .write_all(&labels.to_xml()?)
10321                    .map_err(OxmlError::from)?;
10322            }
10323            let trailing = base + markup.original_series_keys.len();
10324            if let Some(axis_ids) = axis_ids {
10325                let axis_original_to_current =
10326                    axis_original_to_current(&markup.original_axis_ids, axis_ids);
10327                let axis_current_to_original =
10328                    invert_original_to_current(&axis_original_to_current, axis_ids.len());
10329                emit_repeated_raw(
10330                    writer,
10331                    &markup.raw_children,
10332                    trailing + 1,
10333                    &axis_original_to_current,
10334                    axis_ids.len(),
10335                    0,
10336                )?;
10337                for (index, id) in axis_ids.iter().enumerate() {
10338                    let default_markup = default_axis_id_markup(*id);
10339                    let original_markup = axis_current_to_original[index]
10340                        .and_then(|original| markup.axis_ids.get(original));
10341                    write_axis_id(
10342                        writer,
10343                        "c:axId",
10344                        *id,
10345                        original_markup.unwrap_or(&default_markup),
10346                    )?;
10347                    emit_repeated_raw(
10348                        writer,
10349                        &markup.raw_children,
10350                        trailing + 1,
10351                        &axis_original_to_current,
10352                        axis_ids.len(),
10353                        index + 1,
10354                    )?;
10355                }
10356            } else {
10357                emit_raw(writer, markup.raw_children.at(trailing + 1))?;
10358                if let Some(angle) = first_slice_angle {
10359                    if angle != 0 || markup.first_slice_angle.is_some() {
10360                        write_scalar(
10361                            writer,
10362                            "c:firstSliceAng",
10363                            &angle.to_string(),
10364                            markup.first_slice_angle.as_ref(),
10365                        )?;
10366                    }
10367                    emit_raw(writer, markup.raw_children.at(trailing + 2))?;
10368                }
10369                if let Some(size) = hole_size {
10370                    if size != 50 || markup.hole_size.is_some() {
10371                        write_scalar(
10372                            writer,
10373                            "c:holeSize",
10374                            &size.to_string(),
10375                            markup.hole_size.as_ref(),
10376                        )?;
10377                    }
10378                    emit_raw(writer, markup.raw_children.at(trailing + 3))?;
10379                }
10380            }
10381            writer
10382                .write_event(Event::End(BytesEnd::new(&tag)))
10383                .map_err(OxmlError::from)?;
10384        }
10385    }
10386    Ok(())
10387}
10388
10389fn series_original_to_current(original: &[(u32, u32)], current: &[Series]) -> Vec<Option<usize>> {
10390    let mut used = vec![false; current.len()];
10391    let mut matches = vec![None; original.len()];
10392    for original_index in 0..original.len().min(current.len()) {
10393        if original[original_index]
10394            == (current[original_index].index, current[original_index].order)
10395        {
10396            matches[original_index] = Some(original_index);
10397            used[original_index] = true;
10398        }
10399    }
10400    for (original_index, key) in original.iter().enumerate() {
10401        if matches[original_index].is_some() {
10402            continue;
10403        }
10404        if let Some(current_index) = current
10405            .iter()
10406            .enumerate()
10407            .position(|(index, item)| !used[index] && (item.index, item.order) == *key)
10408        {
10409            matches[original_index] = Some(current_index);
10410            used[current_index] = true;
10411        }
10412    }
10413    match_unidentified_by_position(&mut matches, &mut used);
10414    matches
10415}
10416
10417fn axis_original_to_current(original: &[AxisId], current: &[AxisId]) -> Vec<Option<usize>> {
10418    let mut used = vec![false; current.len()];
10419    let mut matches = vec![None; original.len()];
10420    for (original_index, id) in original.iter().enumerate() {
10421        if let Some(current_index) = current
10422            .iter()
10423            .enumerate()
10424            .position(|(index, current_id)| !used[index] && current_id == id)
10425        {
10426            matches[original_index] = Some(current_index);
10427            used[current_index] = true;
10428        }
10429    }
10430    match_unidentified_by_position(&mut matches, &mut used);
10431    matches
10432}
10433
10434fn match_unidentified_by_position(matches: &mut [Option<usize>], used: &mut [bool]) {
10435    for original_index in 0..matches.len() {
10436        if matches[original_index].is_none() && original_index < used.len() && !used[original_index]
10437        {
10438            matches[original_index] = Some(original_index);
10439            used[original_index] = true;
10440        }
10441    }
10442    for matched in matches.iter_mut().filter(|matched| matched.is_none()) {
10443        if let Some(current_index) = used.iter().position(|used| !used) {
10444            *matched = Some(current_index);
10445            used[current_index] = true;
10446        }
10447    }
10448}
10449
10450fn invert_original_to_current(
10451    original_to_current: &[Option<usize>],
10452    current_len: usize,
10453) -> Vec<Option<usize>> {
10454    let mut current_to_original = vec![None; current_len];
10455    for (original, current) in original_to_current.iter().enumerate() {
10456        if let Some(current) = current {
10457            current_to_original[*current] = Some(original);
10458        }
10459    }
10460    current_to_original
10461}
10462
10463fn emit_repeated_raw<W: Write>(
10464    writer: &mut Writer<W>,
10465    raw_children: &OrderedRawChildren,
10466    offset: usize,
10467    original_to_current: &[Option<usize>],
10468    current_len: usize,
10469    current_boundary: usize,
10470) -> Result<()> {
10471    if current_boundary == 0 {
10472        emit_raw(writer, raw_children.at(offset))?;
10473    }
10474    for original_boundary in 1..=original_to_current.len() {
10475        let effective = original_to_current
10476            .iter()
10477            .skip(original_boundary)
10478            .flatten()
10479            .copied()
10480            .next()
10481            .unwrap_or(current_len);
10482        if effective == current_boundary {
10483            emit_raw(writer, raw_children.at(offset + original_boundary))?;
10484        }
10485    }
10486    Ok(())
10487}
10488
10489fn parse_plot_axis(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Axis>> {
10490    let mut reader = Reader::from_reader(xml);
10491    let mut buffer = Vec::new();
10492    loop {
10493        match reader
10494            .read_event_into(&mut buffer)
10495            .map_err(OxmlError::from)?
10496        {
10497            Event::Start(element) | Event::Empty(element) => {
10498                if !element_is_in_namespace(&element, C_NS, inherited)? {
10499                    return Ok(None);
10500                }
10501                let name = element.name();
10502                if AxisKind::parse(local_name(name.as_ref())).is_none() {
10503                    return Ok(None);
10504                }
10505                return Axis::from_xml_with_namespaces(xml, inherited).map(Some);
10506            }
10507            Event::Eof => return Ok(None),
10508            _ => {}
10509        }
10510        buffer.clear();
10511    }
10512}
10513
10514fn validate_axis_pairs(axes: &[Axis]) -> Result<()> {
10515    for (index, axis) in axes.iter().enumerate() {
10516        if axes[..index].iter().any(|other| other.id == axis.id) {
10517            return Err(ChartError::DuplicateElement(format!(
10518                "c:axId {}",
10519                axis.id.value()
10520            )));
10521        }
10522    }
10523    for axis in axes {
10524        if axis.id == axis.cross_axis {
10525            return Err(ChartError::InvalidValue {
10526                element: "c:crossAx".to_owned(),
10527                value: format!("axis {} crosses itself", axis.id.value()),
10528            });
10529        }
10530        let crossed = axes
10531            .iter()
10532            .find(|candidate| candidate.id == axis.cross_axis)
10533            .ok_or_else(|| ChartError::InvalidValue {
10534                element: "c:crossAx".to_owned(),
10535                value: format!(
10536                    "axis {} references missing axis {}",
10537                    axis.id.value(),
10538                    axis.cross_axis.value()
10539                ),
10540            })?;
10541        if crossed.cross_axis != axis.id {
10542            return Err(ChartError::InvalidValue {
10543                element: "c:crossAx".to_owned(),
10544                value: format!(
10545                    "axis {} references {}, which references {}",
10546                    axis.id.value(),
10547                    crossed.id.value(),
10548                    crossed.cross_axis.value()
10549                ),
10550            });
10551        }
10552    }
10553    Ok(())
10554}
10555
10556fn parse_plot_series(
10557    xml: &[u8],
10558    inherited: &NamespaceBindings,
10559    series: &mut Vec<Series>,
10560) -> Result<()> {
10561    let mut reader = Reader::from_reader(xml);
10562    let mut buffer = Vec::new();
10563    loop {
10564        match reader
10565            .read_event_into(&mut buffer)
10566            .map_err(OxmlError::from)?
10567        {
10568            Event::Start(element) => {
10569                if !element_is_in_namespace(&element, C_NS, inherited)? {
10570                    return Ok(());
10571                }
10572                let local = local_name(element.name().as_ref()).to_vec();
10573                if !is_supported_series_plot(&local) {
10574                    return Ok(());
10575                }
10576                let namespaces = chart_bindings(inherited, &element)?;
10577                let mut inner = Vec::new();
10578                loop {
10579                    match reader
10580                        .read_event_into(&mut inner)
10581                        .map_err(OxmlError::from)?
10582                    {
10583                        Event::Start(child) => {
10584                            let name = chart_child_local(&child, &namespaces)?;
10585                            let raw = capture_element(&mut reader, &child)?;
10586                            if name.as_deref() == Some(b"ser") {
10587                                series.push(Series::from_xml_with_namespaces(&raw, &namespaces)?);
10588                            }
10589                        }
10590                        Event::Empty(child) => {
10591                            let name = chart_child_local(&child, &namespaces)?;
10592                            if name.as_deref() == Some(b"ser") {
10593                                return Err(ChartError::MissingElement("c:ser/c:idx".to_owned()));
10594                            }
10595                        }
10596                        Event::End(end) if matches_local_name(end.name().as_ref(), &local) => {
10597                            return Ok(());
10598                        }
10599                        Event::Eof => {
10600                            return Err(missing_end(&format!(
10601                                "c:{}",
10602                                String::from_utf8_lossy(&local)
10603                            )));
10604                        }
10605                        _ => {}
10606                    }
10607                    inner.clear();
10608                }
10609            }
10610            Event::Empty(_) | Event::Eof => return Ok(()),
10611            _ => {}
10612        }
10613        buffer.clear();
10614    }
10615}
10616
10617fn is_supported_series_plot(local: &[u8]) -> bool {
10618    matches!(
10619        local,
10620        b"areaChart"
10621            | b"area3DChart"
10622            | b"barChart"
10623            | b"bar3DChart"
10624            | b"doughnutChart"
10625            | b"lineChart"
10626            | b"line3DChart"
10627            | b"ofPieChart"
10628            | b"pieChart"
10629            | b"pie3DChart"
10630            | b"radarChart"
10631            | b"stockChart"
10632            | b"surfaceChart"
10633            | b"surface3DChart"
10634    )
10635}
10636
10637impl CT_Legend {
10638    fn from_xml(xml: &[u8]) -> Result<Self> {
10639        let (raw_attributes, raw_children) = parse_raw_shell(xml, b"legend", "c:legend")?;
10640        Ok(Self {
10641            raw_attributes,
10642            raw_children,
10643        })
10644    }
10645
10646    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
10647        write_raw_shell(writer, "c:legend", &self.raw_attributes, &self.raw_children)
10648    }
10649
10650    pub fn raw_children(&self) -> &OrderedRawChildren {
10651        &self.raw_children
10652    }
10653}
10654
10655fn parse_raw_shell(
10656    xml: &[u8],
10657    local: &[u8],
10658    tag: &str,
10659) -> Result<(XmlAttributes, OrderedRawChildren)> {
10660    let mut reader = Reader::from_reader(xml);
10661    let mut buffer = Vec::new();
10662    loop {
10663        match reader
10664            .read_event_into(&mut buffer)
10665            .map_err(OxmlError::from)?
10666        {
10667            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
10668                reject_conflicting_prefix(&element, b"c", C_NS)?;
10669                let raw_attributes = capture_attributes(&element)?;
10670                let mut raw_children = OrderedRawChildren::default();
10671                let mut child_buffer = Vec::new();
10672                loop {
10673                    match reader
10674                        .read_event_into(&mut child_buffer)
10675                        .map_err(OxmlError::from)?
10676                    {
10677                        Event::Start(child) => {
10678                            raw_children.push(0, capture_element(&mut reader, &child)?)
10679                        }
10680                        Event::Empty(child) => raw_children.push(0, capture_empty_element(&child)?),
10681                        event @ (Event::Text(_)
10682                        | Event::CData(_)
10683                        | Event::Comment(_)
10684                        | Event::PI(_)
10685                        | Event::GeneralRef(_)) => {
10686                            raw_children.push(0, capture_event(event)?);
10687                        }
10688                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => break,
10689                        Event::Eof => return Err(missing_end(tag)),
10690                        _ => {}
10691                    }
10692                    child_buffer.clear();
10693                }
10694                return Ok((raw_attributes, raw_children));
10695            }
10696            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
10697                reject_conflicting_prefix(&element, b"c", C_NS)?;
10698                return Ok((capture_attributes(&element)?, OrderedRawChildren::default()));
10699            }
10700            Event::Start(element) | Event::Empty(element) => {
10701                return Err(ChartError::UnexpectedElement(element_name(&element)));
10702            }
10703            Event::Eof => return Err(ChartError::MissingElement(tag.to_owned())),
10704            _ => {}
10705        }
10706        buffer.clear();
10707    }
10708}
10709
10710fn write_raw_shell<W: Write>(
10711    writer: &mut Writer<W>,
10712    tag: &str,
10713    raw_attributes: &XmlAttributes,
10714    raw_children: &OrderedRawChildren,
10715) -> Result<()> {
10716    let mut start = BytesStart::new(tag);
10717    push_attributes(&mut start, raw_attributes);
10718    if raw_children.is_empty() {
10719        writer
10720            .write_event(Event::Empty(start))
10721            .map_err(OxmlError::from)?;
10722        return Ok(());
10723    }
10724    writer
10725        .write_event(Event::Start(start))
10726        .map_err(OxmlError::from)?;
10727    emit_raw(writer, raw_children.at(0))?;
10728    writer
10729        .write_event(Event::End(BytesEnd::new(tag)))
10730        .map_err(OxmlError::from)?;
10731    Ok(())
10732}
10733
10734/// The implemented behavior-bearing core of one `c:chart`.
10735#[derive(Clone, Debug, PartialEq)]
10736pub struct CT_Chart {
10737    pub title: Option<CT_Title>,
10738    pub auto_title_deleted: bool,
10739    pub plot_area: CT_PlotArea,
10740    pub legend: Option<CT_Legend>,
10741    pub plot_vis_only: bool,
10742    pub disp_blanks_as: DispBlanksAs,
10743    auto_title_deleted_markup: Option<ScalarMarkup>,
10744    plot_vis_only_markup: Option<ScalarMarkup>,
10745    disp_blanks_as_markup: Option<ScalarMarkup>,
10746    raw_attributes: Vec<(String, String)>,
10747    raw_children: OrderedRawChildren,
10748}
10749
10750impl CT_Chart {
10751    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
10752        let mut reader = Reader::from_reader(xml);
10753        let mut buffer = Vec::new();
10754        loop {
10755            match reader
10756                .read_event_into(&mut buffer)
10757                .map_err(OxmlError::from)?
10758            {
10759                Event::Start(element) if matches_local_name(element.name().as_ref(), b"chart") => {
10760                    reject_conflicting_prefix(&element, b"c", C_NS)?;
10761                    return Self::from_element(&mut reader, &element, inherited);
10762                }
10763                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"chart") => {
10764                    return Err(ChartError::MissingElement("c:plotArea".to_owned()));
10765                }
10766                Event::Start(element) | Event::Empty(element) => {
10767                    return Err(ChartError::UnexpectedElement(element_name(&element)));
10768                }
10769                Event::Eof => return Err(ChartError::MissingElement("c:chart".to_owned())),
10770                _ => {}
10771            }
10772            buffer.clear();
10773        }
10774    }
10775
10776    fn from_element(
10777        reader: &mut Reader<&[u8]>,
10778        start: &BytesStart<'_>,
10779        inherited: &NamespaceBindings,
10780    ) -> Result<Self> {
10781        let namespaces = bindings_with_local(inherited, start)?;
10782        let raw_attributes = capture_attributes(start)?;
10783        let mut state = ChartParseState::default();
10784        let mut buffer = Vec::new();
10785
10786        loop {
10787            match reader
10788                .read_event_into(&mut buffer)
10789                .map_err(OxmlError::from)?
10790            {
10791                Event::Start(element) => {
10792                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10793                        local_name(element.name().as_ref()).to_vec()
10794                    } else {
10795                        Vec::new()
10796                    };
10797                    let raw = capture_element(reader, &element)?;
10798                    state.parse_child(&name, raw, &namespaces)?;
10799                }
10800                Event::Empty(element) => {
10801                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10802                        local_name(element.name().as_ref()).to_vec()
10803                    } else {
10804                        Vec::new()
10805                    };
10806                    let raw = capture_empty_element(&element)?;
10807                    state.parse_child(&name, raw, &namespaces)?;
10808                }
10809                event @ (Event::Text(_)
10810                | Event::CData(_)
10811                | Event::Comment(_)
10812                | Event::PI(_)
10813                | Event::GeneralRef(_)) => {
10814                    state.capture_event(capture_event(event)?);
10815                }
10816                Event::End(element) if matches_local_name(element.name().as_ref(), b"chart") => {
10817                    break;
10818                }
10819                Event::Eof => return Err(missing_end("c:chart")),
10820                _ => {}
10821            }
10822            buffer.clear();
10823        }
10824
10825        state.finish(raw_attributes)
10826    }
10827
10828    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
10829        let mut start = BytesStart::new("c:chart");
10830        push_attributes(&mut start, &self.raw_attributes);
10831        writer
10832            .write_event(Event::Start(start))
10833            .map_err(OxmlError::from)?;
10834        emit_raw(writer, self.raw_children.at(0))?;
10835        if let Some(title) = &self.title {
10836            title.write_xml(writer)?;
10837        }
10838        emit_raw(writer, self.raw_children.at(1))?;
10839        if self.auto_title_deleted_markup.is_some() || self.auto_title_deleted {
10840            write_scalar(
10841                writer,
10842                "c:autoTitleDeleted",
10843                if self.auto_title_deleted { "1" } else { "0" },
10844                self.auto_title_deleted_markup.as_ref(),
10845            )?;
10846        }
10847        for raw_boundary in 2..8 {
10848            emit_raw(writer, self.raw_children.at(raw_boundary))?;
10849        }
10850        self.plot_area.write_xml(writer)?;
10851        emit_raw(writer, self.raw_children.at(8))?;
10852        if let Some(legend) = &self.legend {
10853            legend.write_xml(writer)?;
10854        }
10855        emit_raw(writer, self.raw_children.at(9))?;
10856        if self.plot_vis_only_markup.is_some() || !self.plot_vis_only {
10857            write_scalar(
10858                writer,
10859                "c:plotVisOnly",
10860                if self.plot_vis_only { "1" } else { "0" },
10861                self.plot_vis_only_markup.as_ref(),
10862            )?;
10863        }
10864        emit_raw(writer, self.raw_children.at(10))?;
10865        if self.disp_blanks_as_markup.is_some() || self.disp_blanks_as != DispBlanksAs::Gap {
10866            write_scalar(
10867                writer,
10868                "c:dispBlanksAs",
10869                self.disp_blanks_as.as_str(),
10870                self.disp_blanks_as_markup.as_ref(),
10871            )?;
10872        }
10873        for raw_boundary in 11..=13 {
10874            emit_raw(writer, self.raw_children.at(raw_boundary))?;
10875        }
10876        writer
10877            .write_event(Event::End(BytesEnd::new("c:chart")))
10878            .map_err(OxmlError::from)?;
10879        Ok(())
10880    }
10881
10882    pub fn raw_children(&self) -> &OrderedRawChildren {
10883        &self.raw_children
10884    }
10885}
10886
10887#[derive(Default)]
10888struct ChartParseState {
10889    title: Option<CT_Title>,
10890    auto_title_deleted: Option<(bool, ScalarMarkup)>,
10891    plot_area: Option<CT_PlotArea>,
10892    legend: Option<CT_Legend>,
10893    plot_vis_only: Option<(bool, ScalarMarkup)>,
10894    disp_blanks_as: Option<(DispBlanksAs, ScalarMarkup)>,
10895    raw_children: OrderedRawChildren,
10896    boundary: usize,
10897}
10898
10899impl ChartParseState {
10900    fn capture_event(&mut self, raw: Vec<u8>) {
10901        self.raw_children.push(self.boundary, raw);
10902    }
10903
10904    fn parse_child(
10905        &mut self,
10906        name: &[u8],
10907        raw: Vec<u8>,
10908        namespaces: &NamespaceBindings,
10909    ) -> Result<()> {
10910        match name {
10911            b"title" => {
10912                set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
10913                self.boundary = self.boundary.max(1);
10914            }
10915            b"autoTitleDeleted" => {
10916                set_once(
10917                    &mut self.auto_title_deleted,
10918                    parse_bool_value(&raw, "autoTitleDeleted")?,
10919                    "c:autoTitleDeleted",
10920                )?;
10921                self.boundary = self.boundary.max(2);
10922            }
10923            b"plotArea" => {
10924                set_once(
10925                    &mut self.plot_area,
10926                    CT_PlotArea::from_xml_with_namespaces(&raw, namespaces)?,
10927                    "c:plotArea",
10928                )?;
10929                self.boundary = self.boundary.max(8);
10930            }
10931            b"legend" => {
10932                set_once(&mut self.legend, CT_Legend::from_xml(&raw)?, "c:legend")?;
10933                self.boundary = self.boundary.max(9);
10934            }
10935            b"plotVisOnly" => {
10936                set_once(
10937                    &mut self.plot_vis_only,
10938                    parse_bool_value(&raw, "plotVisOnly")?,
10939                    "c:plotVisOnly",
10940                )?;
10941                self.boundary = self.boundary.max(10);
10942            }
10943            b"dispBlanksAs" => {
10944                set_once(
10945                    &mut self.disp_blanks_as,
10946                    parse_enum_value(&raw, "dispBlanksAs")?,
10947                    "c:dispBlanksAs",
10948                )?;
10949                self.boundary = self.boundary.max(11);
10950            }
10951            _ => {
10952                self.raw_children
10953                    .push(chart_raw_slot(name).unwrap_or(self.boundary), raw);
10954                self.boundary = self.boundary.max(chart_raw_boundary_after(name));
10955            }
10956        }
10957        Ok(())
10958    }
10959
10960    fn finish(self, raw_attributes: XmlAttributes) -> Result<CT_Chart> {
10961        let (auto_title_deleted, auto_title_deleted_markup) = self
10962            .auto_title_deleted
10963            .map(|(value, markup)| (value, Some(markup)))
10964            .unwrap_or((false, None));
10965        let (plot_vis_only, plot_vis_only_markup) = self
10966            .plot_vis_only
10967            .map(|(value, markup)| (value, Some(markup)))
10968            .unwrap_or((true, None));
10969        let (disp_blanks_as, disp_blanks_as_markup) = self
10970            .disp_blanks_as
10971            .map(|(value, markup)| (value, Some(markup)))
10972            .unwrap_or((DispBlanksAs::Gap, None));
10973        Ok(CT_Chart {
10974            title: self.title,
10975            auto_title_deleted,
10976            plot_area: self
10977                .plot_area
10978                .ok_or_else(|| ChartError::MissingElement("c:plotArea".to_owned()))?,
10979            legend: self.legend,
10980            plot_vis_only,
10981            disp_blanks_as,
10982            auto_title_deleted_markup,
10983            plot_vis_only_markup,
10984            disp_blanks_as_markup,
10985            raw_attributes,
10986            raw_children: raw_children_in_schema_order(&self.raw_children, 13),
10987        })
10988    }
10989}
10990
10991fn chart_raw_slot(name: &[u8]) -> Option<usize> {
10992    match name {
10993        b"pivotFmts" => Some(2),
10994        b"view3D" => Some(3),
10995        b"floor" => Some(4),
10996        b"sideWall" => Some(5),
10997        b"backWall" => Some(6),
10998        // Both unmodelled tail children share a slot so producer sibling order
10999        // remains byte-stable until a later story types either element.
11000        b"showDLblsOverMax" | b"extLst" => Some(11),
11001        _ => None,
11002    }
11003}
11004
11005fn chart_raw_boundary_after(name: &[u8]) -> usize {
11006    match name {
11007        b"title" => 1,
11008        b"autoTitleDeleted" => 2,
11009        b"pivotFmts" => 3,
11010        b"view3D" => 4,
11011        b"floor" => 5,
11012        b"sideWall" => 6,
11013        b"backWall" => 7,
11014        b"plotArea" => 8,
11015        b"legend" => 9,
11016        b"plotVisOnly" => 10,
11017        b"dispBlanksAs" => 11,
11018        b"showDLblsOverMax" | b"extLst" => 11,
11019        _ => 0,
11020    }
11021}
11022
11023/// One complete `c:chartSpace` part with the F-118 core typed.
11024#[derive(Clone, Debug, PartialEq)]
11025pub struct CT_ChartSpace {
11026    pub chart: CT_Chart,
11027    pub sp_pr: Option<CT_ShapeProperties>,
11028    pub tx_pr: Option<CT_TextBody>,
11029    raw_attributes: Vec<(String, String)>,
11030    namespace_declarations: Vec<(String, String)>,
11031    raw_children: OrderedRawChildren,
11032}
11033
11034impl CT_ChartSpace {
11035    /// Parses one complete ChartML chart-space part.
11036    pub fn from_xml(xml: &[u8]) -> Result<Self> {
11037        let mut reader = Reader::from_reader(xml);
11038        let mut buffer = Vec::new();
11039        loop {
11040            match reader
11041                .read_event_into(&mut buffer)
11042                .map_err(OxmlError::from)?
11043            {
11044                Event::Start(element)
11045                    if matches_local_name(element.name().as_ref(), b"chartSpace") =>
11046                {
11047                    validate_chart_space_namespace(&element)?;
11048                    return Self::from_element(&mut reader, &element);
11049                }
11050                Event::Empty(element)
11051                    if matches_local_name(element.name().as_ref(), b"chartSpace") =>
11052                {
11053                    validate_chart_space_namespace(&element)?;
11054                    return Err(ChartError::MissingElement("c:chart".to_owned()));
11055                }
11056                Event::Start(element) | Event::Empty(element) => {
11057                    return Err(ChartError::UnexpectedElement(element_name(&element)));
11058                }
11059                Event::Eof => return Err(ChartError::MissingElement("c:chartSpace".to_owned())),
11060                _ => {}
11061            }
11062            buffer.clear();
11063        }
11064    }
11065
11066    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
11067        let namespaces = namespace_bindings(start)?;
11068        let (raw_attributes, namespace_declarations) = capture_root_attributes(start)?;
11069        let mut state = ChartSpaceParseState::default();
11070        let mut buffer = Vec::new();
11071
11072        loop {
11073            match reader
11074                .read_event_into(&mut buffer)
11075                .map_err(OxmlError::from)?
11076            {
11077                Event::Start(element) => {
11078                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
11079                        local_name(element.name().as_ref()).to_vec()
11080                    } else {
11081                        Vec::new()
11082                    };
11083                    let raw = capture_element(reader, &element)?;
11084                    state.parse_child(&name, raw, &namespaces)?;
11085                }
11086                Event::Empty(element) => {
11087                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
11088                        local_name(element.name().as_ref()).to_vec()
11089                    } else {
11090                        Vec::new()
11091                    };
11092                    let raw = capture_empty_element(&element)?;
11093                    state.parse_child(&name, raw, &namespaces)?;
11094                }
11095                event @ (Event::Text(_)
11096                | Event::CData(_)
11097                | Event::Comment(_)
11098                | Event::PI(_)
11099                | Event::GeneralRef(_)) => {
11100                    state.capture_event(capture_event(event)?);
11101                }
11102                Event::End(element)
11103                    if matches_local_name(element.name().as_ref(), b"chartSpace") =>
11104                {
11105                    break;
11106                }
11107                Event::Eof => return Err(missing_end("c:chartSpace")),
11108                _ => {}
11109            }
11110            buffer.clear();
11111        }
11112
11113        state.finish(raw_attributes, namespace_declarations)
11114    }
11115
11116    /// Writes one chart-space part using fixed OOXML prefixes and schema order.
11117    pub fn to_xml(&self) -> Result<Vec<u8>> {
11118        let mut writer = Writer::new(Vec::new());
11119        let mut start = BytesStart::new("c:chartSpace");
11120        start.push_attribute(("xmlns:c", C_NS));
11121        start.push_attribute(("xmlns:a", A_NS));
11122        start.push_attribute(("xmlns:r", R_NS));
11123        push_attributes(&mut start, &self.namespace_declarations);
11124        push_attributes(&mut start, &self.raw_attributes);
11125        writer
11126            .write_event(Event::Start(start))
11127            .map_err(OxmlError::from)?;
11128        for raw_boundary in 0..8 {
11129            emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
11130        }
11131        self.chart.write_xml(&mut writer)?;
11132        emit_raw(&mut writer, self.raw_children.at(8))?;
11133        if let Some(properties) = &self.sp_pr {
11134            properties.write_xml_as(&mut writer, "c:spPr")?;
11135        }
11136        emit_raw(&mut writer, self.raw_children.at(9))?;
11137        if let Some(text) = &self.tx_pr {
11138            text.write_xml_as(&mut writer, "c:txPr")?;
11139        }
11140        for raw_boundary in 10..=14 {
11141            emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
11142        }
11143        writer
11144            .write_event(Event::End(BytesEnd::new("c:chartSpace")))
11145            .map_err(OxmlError::from)?;
11146        Ok(writer.into_inner())
11147    }
11148
11149    pub fn raw_children(&self) -> &OrderedRawChildren {
11150        &self.raw_children
11151    }
11152}
11153
11154#[derive(Default)]
11155struct ChartSpaceParseState {
11156    chart: Option<CT_Chart>,
11157    sp_pr: Option<CT_ShapeProperties>,
11158    tx_pr: Option<CT_TextBody>,
11159    raw_children: OrderedRawChildren,
11160    boundary: usize,
11161}
11162
11163impl ChartSpaceParseState {
11164    fn capture_event(&mut self, raw: Vec<u8>) {
11165        self.raw_children.push(self.boundary, raw);
11166    }
11167
11168    fn parse_child(
11169        &mut self,
11170        name: &[u8],
11171        raw: Vec<u8>,
11172        namespaces: &NamespaceBindings,
11173    ) -> Result<()> {
11174        match name {
11175            b"chart" => {
11176                set_once(
11177                    &mut self.chart,
11178                    CT_Chart::from_xml_with_namespaces(&raw, namespaces)?,
11179                    "c:chart",
11180                )?;
11181                self.boundary = self.boundary.max(8);
11182            }
11183            b"spPr" => {
11184                parse_chart_shape_properties(&raw, namespaces, &mut self.sp_pr)?;
11185                self.boundary = self.boundary.max(9);
11186            }
11187            b"txPr" => {
11188                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
11189                let inherited = namespaces
11190                    .iter()
11191                    .map(|(prefix, uri)| {
11192                        Ok((
11193                            std::str::from_utf8(prefix)
11194                                .map_err(OxmlError::from)?
11195                                .to_owned(),
11196                            uri.clone(),
11197                        ))
11198                    })
11199                    .collect::<Result<Vec<_>>>()?;
11200                let text = CT_TextBody::from_xml_as_with_namespaces(&raw, b"txPr", &inherited)?;
11201                let mut writer = Writer::new(Vec::new());
11202                text.write_xml_as(&mut writer, "c:txPr")?;
11203                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
11204                set_once(&mut self.tx_pr, text, "c:txPr")?;
11205                self.boundary = self.boundary.max(10);
11206            }
11207            _ => {
11208                self.raw_children
11209                    .push(chart_space_raw_slot(name).unwrap_or(self.boundary), raw);
11210                self.boundary = self.boundary.max(chart_space_raw_boundary_after(name));
11211            }
11212        }
11213        Ok(())
11214    }
11215
11216    fn finish(
11217        self,
11218        raw_attributes: XmlAttributes,
11219        namespace_declarations: XmlAttributes,
11220    ) -> Result<CT_ChartSpace> {
11221        Ok(CT_ChartSpace {
11222            chart: self
11223                .chart
11224                .ok_or_else(|| ChartError::MissingElement("c:chart".to_owned()))?,
11225            sp_pr: self.sp_pr,
11226            tx_pr: self.tx_pr,
11227            raw_attributes,
11228            namespace_declarations,
11229            raw_children: raw_children_in_schema_order(&self.raw_children, 14),
11230        })
11231    }
11232}
11233
11234fn raw_children_in_schema_order(
11235    raw_children: &OrderedRawChildren,
11236    final_boundary: usize,
11237) -> OrderedRawChildren {
11238    let mut ordered = OrderedRawChildren::default();
11239    for boundary in 0..=final_boundary {
11240        for raw in raw_children.at(boundary) {
11241            ordered.push(boundary, raw.to_vec());
11242        }
11243    }
11244    ordered
11245}
11246
11247fn chart_space_raw_slot(name: &[u8]) -> Option<usize> {
11248    match name {
11249        b"date1904" => Some(0),
11250        b"lang" => Some(1),
11251        b"roundedCorners" => Some(2),
11252        b"style" => Some(3),
11253        b"clrMapOvr" => Some(4),
11254        b"pivotSource" => Some(5),
11255        b"protection" => Some(6),
11256        b"externalData" => Some(10),
11257        b"printSettings" => Some(11),
11258        b"userShapes" => Some(12),
11259        b"extLst" => Some(13),
11260        _ => None,
11261    }
11262}
11263
11264fn chart_space_raw_boundary_after(name: &[u8]) -> usize {
11265    match name {
11266        b"date1904" => 1,
11267        b"lang" => 2,
11268        b"roundedCorners" => 3,
11269        b"style" => 4,
11270        b"clrMapOvr" => 5,
11271        b"pivotSource" => 6,
11272        b"protection" => 7,
11273        b"chart" => 8,
11274        b"spPr" => 9,
11275        b"txPr" => 10,
11276        b"externalData" => 11,
11277        b"printSettings" => 12,
11278        b"userShapes" => 13,
11279        b"extLst" => 14,
11280        _ => 0,
11281    }
11282}
11283
11284fn parse_bool_value(xml: &[u8], local: &str) -> Result<(bool, ScalarMarkup)> {
11285    let (value, markup) = scalar_value(xml, local)?;
11286    let value = match value.as_deref().unwrap_or("1") {
11287        "1" | "true" => true,
11288        "0" | "false" => false,
11289        invalid => return Err(invalid_attribute(local, "val", invalid.to_owned())),
11290    };
11291    Ok((value, markup))
11292}
11293
11294fn parse_enum_value(xml: &[u8], local: &str) -> Result<(DispBlanksAs, ScalarMarkup)> {
11295    let (value, markup) = scalar_value(xml, local)?;
11296    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
11297    let parsed =
11298        DispBlanksAs::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
11299    Ok((parsed, markup))
11300}
11301
11302fn scalar_value(xml: &[u8], expected: &str) -> Result<(Option<String>, ScalarMarkup)> {
11303    let mut reader = Reader::from_reader(xml);
11304    let mut buffer = Vec::new();
11305    loop {
11306        match reader
11307            .read_event_into(&mut buffer)
11308            .map_err(OxmlError::from)?
11309        {
11310            Event::Empty(element)
11311                if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
11312            {
11313                reject_conflicting_prefix(&element, b"c", C_NS)?;
11314                return Ok((
11315                    attribute_value(&element, b"val")?,
11316                    ScalarMarkup {
11317                        raw_attributes: capture_attributes_except(&element, b"val")?,
11318                        raw_content: Vec::new(),
11319                    },
11320                ));
11321            }
11322            Event::Start(element)
11323                if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
11324            {
11325                reject_conflicting_prefix(&element, b"c", C_NS)?;
11326                let value = attribute_value(&element, b"val")?;
11327                let raw_attributes = capture_attributes_except(&element, b"val")?;
11328                let mut raw_content = Vec::new();
11329                let mut inner = Vec::new();
11330                loop {
11331                    match reader
11332                        .read_event_into(&mut inner)
11333                        .map_err(OxmlError::from)?
11334                    {
11335                        Event::End(end)
11336                            if matches_local_name(end.name().as_ref(), expected.as_bytes()) =>
11337                        {
11338                            return Ok((
11339                                value,
11340                                ScalarMarkup {
11341                                    raw_attributes,
11342                                    raw_content,
11343                                },
11344                            ));
11345                        }
11346                        event @ (Event::Text(_)
11347                        | Event::CData(_)
11348                        | Event::Comment(_)
11349                        | Event::PI(_)
11350                        | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
11351                        Event::Eof => return Err(missing_end(expected)),
11352                        _ => return Err(ChartError::UnexpectedElement(expected.to_owned())),
11353                    }
11354                    inner.clear();
11355                }
11356            }
11357            Event::Start(element) | Event::Empty(element) => {
11358                return Err(ChartError::UnexpectedElement(element_name(&element)));
11359            }
11360            Event::Eof => return Err(ChartError::MissingElement(expected.to_owned())),
11361            _ => {}
11362        }
11363        buffer.clear();
11364    }
11365}
11366
11367fn set_once<T>(slot: &mut Option<T>, value: T, name: &str) -> Result<()> {
11368    if slot.is_some() {
11369        return Err(ChartError::DuplicateElement(name.to_owned()));
11370    }
11371    *slot = Some(value);
11372    Ok(())
11373}
11374
11375fn mark_once(seen: &mut bool, name: &str) -> Result<()> {
11376    if *seen {
11377        return Err(ChartError::DuplicateElement(name.to_owned()));
11378    }
11379    *seen = true;
11380    Ok(())
11381}
11382
11383fn validate_chart_space_namespace(element: &BytesStart<'_>) -> Result<()> {
11384    let name = element.name();
11385    let qualified = name.as_ref();
11386    let declaration = match qualified.iter().position(|byte| *byte == b':') {
11387        Some(position) => {
11388            let mut declaration = b"xmlns:".to_vec();
11389            declaration.extend_from_slice(&qualified[..position]);
11390            declaration
11391        }
11392        None => b"xmlns".to_vec(),
11393    };
11394    let namespace = attribute_value(element, &declaration)?
11395        .ok_or_else(|| invalid_attribute("chartSpace", "namespace", "<missing>".to_owned()))?;
11396    if namespace != C_NS {
11397        return Err(invalid_attribute("chartSpace", "namespace", namespace));
11398    }
11399    reject_conflicting_prefix(element, b"c", C_NS)?;
11400    reject_conflicting_prefix(element, b"a", A_NS)?;
11401    reject_conflicting_prefix(element, b"r", R_NS)
11402}
11403
11404fn reject_conflicting_prefix_in_xml(xml: &[u8], prefix: &[u8], namespace: &str) -> Result<()> {
11405    let mut reader = Reader::from_reader(xml);
11406    let mut buffer = Vec::new();
11407    loop {
11408        match reader
11409            .read_event_into(&mut buffer)
11410            .map_err(OxmlError::from)?
11411        {
11412            Event::Start(element) | Event::Empty(element) => {
11413                return reject_conflicting_prefix(&element, prefix, namespace);
11414            }
11415            Event::Eof => return Err(ChartError::MissingElement("XML root".to_owned())),
11416            _ => {}
11417        }
11418        buffer.clear();
11419    }
11420}
11421
11422fn reject_rewritten_foreign_elements(
11423    original: &[u8],
11424    written: &[u8],
11425    inherited: &NamespaceBindings,
11426    root_local: &[u8],
11427) -> Result<()> {
11428    let original = foreign_element_regions(original, inherited, root_local)?;
11429    let mut written_namespaces = inherited.clone();
11430    written_namespaces.retain(|(prefix, _)| prefix != b"a");
11431    written_namespaces.push((b"a".to_vec(), A_NS.to_owned()));
11432    let written = foreign_element_regions(written, &written_namespaces, root_local)?;
11433    if original != written {
11434        return Err(invalid_attribute(
11435            &String::from_utf8_lossy(root_local),
11436            "namespace",
11437            "foreign DrawingML content would be converted to typed content".to_owned(),
11438        ));
11439    }
11440    Ok(())
11441}
11442
11443fn foreign_element_regions(
11444    xml: &[u8],
11445    inherited: &NamespaceBindings,
11446    root_local: &[u8],
11447) -> Result<Vec<Vec<u8>>> {
11448    let mut reader = Reader::from_reader(xml);
11449    let mut buffer = Vec::new();
11450    let root_namespaces = loop {
11451        match reader
11452            .read_event_into(&mut buffer)
11453            .map_err(OxmlError::from)?
11454        {
11455            Event::Start(element) if matches_local_name(element.name().as_ref(), root_local) => {
11456                break bindings_with_local(inherited, &element)?;
11457            }
11458            Event::Empty(element) if matches_local_name(element.name().as_ref(), root_local) => {
11459                return Ok(Vec::new());
11460            }
11461            Event::Start(element) | Event::Empty(element) => {
11462                return Err(ChartError::UnexpectedElement(element_name(&element)));
11463            }
11464            Event::Eof => {
11465                return Err(ChartError::MissingElement(
11466                    String::from_utf8_lossy(root_local).into_owned(),
11467                ));
11468            }
11469            _ => {}
11470        }
11471        buffer.clear();
11472    };
11473
11474    let mut namespace_stack = vec![root_namespaces];
11475    let mut foreign = Vec::new();
11476    let mut child_buffer = Vec::new();
11477    loop {
11478        match reader
11479            .read_event_into(&mut child_buffer)
11480            .map_err(OxmlError::from)?
11481        {
11482            Event::Start(element) => {
11483                let namespaces = namespace_stack.last().expect("root namespace binding");
11484                if element_is_in_namespace(&element, A_NS, namespaces)? {
11485                    namespace_stack.push(bindings_with_local(namespaces, &element)?);
11486                } else {
11487                    foreign.push(capture_element(&mut reader, &element)?);
11488                }
11489            }
11490            Event::Empty(element) => {
11491                let namespaces = namespace_stack.last().expect("root namespace binding");
11492                if !element_is_in_namespace(&element, A_NS, namespaces)? {
11493                    foreign.push(capture_empty_element(&element)?);
11494                }
11495            }
11496            Event::End(element) if matches_local_name(element.name().as_ref(), root_local) => {
11497                return Ok(foreign);
11498            }
11499            Event::End(_) => {
11500                namespace_stack.pop();
11501            }
11502            Event::Eof => {
11503                return Err(missing_end(&String::from_utf8_lossy(root_local)));
11504            }
11505            _ => {}
11506        }
11507        child_buffer.clear();
11508    }
11509}
11510
11511fn reject_conflicting_prefix(
11512    element: &BytesStart<'_>,
11513    prefix: &[u8],
11514    namespace: &str,
11515) -> Result<()> {
11516    let mut key = b"xmlns:".to_vec();
11517    key.extend_from_slice(prefix);
11518    if let Some(value) = attribute_value(element, &key)?
11519        && value != namespace
11520    {
11521        return Err(invalid_attribute(
11522            &element_name(element),
11523            &String::from_utf8_lossy(&key),
11524            value,
11525        ));
11526    }
11527    Ok(())
11528}
11529
11530fn namespace_bindings(element: &BytesStart<'_>) -> Result<NamespaceBindings> {
11531    let mut bindings = Vec::new();
11532    for attribute in element.attributes() {
11533        let attribute = attribute.map_err(OxmlError::from)?;
11534        let key = attribute.key.as_ref();
11535        let prefix = if key == b"xmlns" {
11536            Vec::new()
11537        } else if let Some(prefix) = key.strip_prefix(b"xmlns:") {
11538            prefix.to_vec()
11539        } else {
11540            continue;
11541        };
11542        let namespace = attribute
11543            .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
11544            .map_err(OxmlError::from)?
11545            .into_owned();
11546        bindings.push((prefix, namespace));
11547    }
11548    Ok(bindings)
11549}
11550
11551fn bindings_with_local(
11552    inherited: &NamespaceBindings,
11553    element: &BytesStart<'_>,
11554) -> Result<NamespaceBindings> {
11555    let mut bindings = inherited.clone();
11556    for (prefix, namespace) in namespace_bindings(element)? {
11557        upsert_namespace_binding(&mut bindings, prefix, namespace);
11558    }
11559    Ok(bindings)
11560}
11561
11562fn upsert_namespace_binding(bindings: &mut NamespaceBindings, prefix: Vec<u8>, namespace: String) {
11563    if let Some((_, current)) = bindings.iter_mut().find(|(current, _)| current == &prefix) {
11564        *current = namespace;
11565    } else {
11566        bindings.push((prefix, namespace));
11567    }
11568}
11569
11570fn element_is_in_namespace(
11571    element: &BytesStart<'_>,
11572    expected: &str,
11573    inherited: &NamespaceBindings,
11574) -> Result<bool> {
11575    let name = element.name();
11576    let qualified = name.as_ref();
11577    let prefix = qualified
11578        .iter()
11579        .position(|byte| *byte == b':')
11580        .map(|position| &qualified[..position])
11581        .unwrap_or_default();
11582    let local = namespace_bindings(element)?;
11583    let namespace = local
11584        .iter()
11585        .rev()
11586        .chain(inherited.iter().rev())
11587        .find(|(candidate, _)| candidate.as_slice() == prefix)
11588        .map(|(_, namespace)| namespace.as_str());
11589    Ok(namespace == Some(expected))
11590}
11591
11592fn attribute_value(element: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
11593    for attribute in element.attributes() {
11594        let attribute = attribute.map_err(OxmlError::from)?;
11595        if attribute.key.as_ref() == name {
11596            return Ok(Some(
11597                attribute
11598                    .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
11599                    .map_err(OxmlError::from)?
11600                    .into_owned(),
11601            ));
11602        }
11603    }
11604    Ok(None)
11605}
11606
11607fn capture_root_attributes(start: &BytesStart<'_>) -> Result<RootAttributes> {
11608    let mut attributes = Vec::new();
11609    let mut namespaces = Vec::new();
11610    for attribute in start.attributes() {
11611        let attribute = attribute.map_err(OxmlError::from)?;
11612        let name = std::str::from_utf8(attribute.key.as_ref())
11613            .map_err(OxmlError::from)?
11614            .to_owned();
11615        let value = attribute
11616            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
11617            .map_err(OxmlError::from)?
11618            .into_owned();
11619        if name == "xmlns:c" || name == "xmlns:a" || name == "xmlns:r" {
11620            continue;
11621        }
11622        if name == "xmlns" || name.starts_with("xmlns:") {
11623            namespaces.push((name, value));
11624        } else {
11625            attributes.push((name, value));
11626        }
11627    }
11628    Ok((attributes, namespaces))
11629}
11630
11631fn capture_attributes(start: &BytesStart<'_>) -> Result<XmlAttributes> {
11632    let mut attributes = Vec::new();
11633    for attribute in start.attributes() {
11634        let attribute = attribute.map_err(OxmlError::from)?;
11635        attributes.push((
11636            std::str::from_utf8(attribute.key.as_ref())
11637                .map_err(OxmlError::from)?
11638                .to_owned(),
11639            attribute
11640                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
11641                .map_err(OxmlError::from)?
11642                .into_owned(),
11643        ));
11644    }
11645    Ok(attributes)
11646}
11647
11648fn capture_attributes_except(start: &BytesStart<'_>, excluded: &[u8]) -> Result<XmlAttributes> {
11649    let mut attributes = Vec::new();
11650    for attribute in start.attributes() {
11651        let attribute = attribute.map_err(OxmlError::from)?;
11652        if attribute.key.as_ref() == excluded {
11653            continue;
11654        }
11655        attributes.push((
11656            std::str::from_utf8(attribute.key.as_ref())
11657                .map_err(OxmlError::from)?
11658                .to_owned(),
11659            attribute
11660                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
11661                .map_err(OxmlError::from)?
11662                .into_owned(),
11663        ));
11664    }
11665    Ok(attributes)
11666}
11667
11668fn capture_attributes_excluding(
11669    start: &BytesStart<'_>,
11670    excluded: &[&[u8]],
11671) -> Result<XmlAttributes> {
11672    let mut attributes = Vec::new();
11673    for attribute in start.attributes() {
11674        let attribute = attribute.map_err(OxmlError::from)?;
11675        if excluded.contains(&attribute.key.as_ref()) {
11676            continue;
11677        }
11678        attributes.push((
11679            std::str::from_utf8(attribute.key.as_ref())
11680                .map_err(OxmlError::from)?
11681                .to_owned(),
11682            attribute
11683                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
11684                .map_err(OxmlError::from)?
11685                .into_owned(),
11686        ));
11687    }
11688    Ok(attributes)
11689}
11690
11691fn push_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
11692    for (name, value) in attributes {
11693        start.push_attribute((name.as_str(), value.as_str()));
11694    }
11695}
11696
11697fn write_scalar<W: Write>(
11698    writer: &mut Writer<W>,
11699    name: &str,
11700    value: &str,
11701    markup: Option<&ScalarMarkup>,
11702) -> Result<()> {
11703    let mut start = BytesStart::new(name);
11704    start.push_attribute(("val", value));
11705    if let Some(markup) = markup {
11706        push_attributes(&mut start, &markup.raw_attributes);
11707    }
11708    let has_content = markup.is_some_and(|markup| !markup.raw_content.is_empty());
11709    if !has_content {
11710        writer
11711            .write_event(Event::Empty(start))
11712            .map_err(OxmlError::from)?;
11713        return Ok(());
11714    }
11715    writer
11716        .write_event(Event::Start(start))
11717        .map_err(OxmlError::from)?;
11718    if let Some(markup) = markup {
11719        for raw in &markup.raw_content {
11720            writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
11721        }
11722    }
11723    writer
11724        .write_event(Event::End(BytesEnd::new(name)))
11725        .map_err(OxmlError::from)?;
11726    Ok(())
11727}
11728
11729fn capture_event(event: Event<'_>) -> Result<Vec<u8>> {
11730    let mut writer = Writer::new(Vec::new());
11731    writer
11732        .write_event(event.into_owned())
11733        .map_err(OxmlError::from)?;
11734    Ok(writer.into_inner())
11735}
11736
11737fn emit_raw<'a, W: Write>(
11738    writer: &mut Writer<W>,
11739    children: impl Iterator<Item = &'a [u8]>,
11740) -> Result<()> {
11741    for child in children {
11742        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
11743    }
11744    Ok(())
11745}
11746
11747fn invalid_attribute(element: &str, attribute: &str, value: String) -> ChartError {
11748    ChartError::InvalidAttribute {
11749        element: element.to_owned(),
11750        attribute: attribute.to_owned(),
11751        value,
11752    }
11753}
11754
11755fn element_name(element: &BytesStart<'_>) -> String {
11756    String::from_utf8_lossy(element.name().as_ref()).into_owned()
11757}
11758
11759fn missing_end(element: &str) -> ChartError {
11760    ChartError::Xml(OxmlError::MissingElement(format!("closing {element}")))
11761}
11762
11763#[cfg(test)]
11764mod tests {
11765    use std::collections::HashSet;
11766    use std::fs;
11767    use std::path::{Path, PathBuf};
11768    use std::process::Command;
11769
11770    use oxml_core::raw_xml::{capture_element, capture_empty_element};
11771    use oxml_drawing::color::{ColorMap, ColorMapSlot, RgbColor, ThemeColorSlot};
11772    use oxml_drawing::theme::CT_OfficeStyleSheet;
11773    use oxml_layout::{
11774        Color, FontManager, LayoutResult, PageFrame, Paint, PathCommand, Point, PositionedElement,
11775        Rect,
11776    };
11777    use oxml_opc::OpcPackage;
11778    use quick_xml::Reader;
11779    use quick_xml::events::Event;
11780
11781    use super::{
11782        A_NS, Axis, AxisData, AxisId, AxisKind, AxisPosition, BarDirection, BarGrouping, C_NS,
11783        CT_ChartSpace, CT_DLbls, CT_ShapeProperties, CT_TextBody, ChartData, ChartGeometry,
11784        ChartKind, DataLabelPosition, DispBlanksAs, Domain, Grouping, NumberFormat, NumericData,
11785        Orientation, Plot, R_NS, ScatterStyle, Series, StringRef, TickLabelPosition, TickMark,
11786        authored_chart_parts, capture_event, chart_workbook_relationship_id, local_name,
11787        matches_local_name, nice_number_scale, read_chart_data,
11788        render_chart as render_chart_with_theme, render_geometry as render_geometry_with_theme,
11789        replace_chart_data,
11790    };
11791
11792    const MANIFEST: &str = include_str!("../../../scripts/pptx-corpus-manifest.tsv");
11793    const EXPECTED_DECKS: usize = 50;
11794    const LIBREOFFICE_VERSION: &str =
11795        "LibreOffice 26.2.5.2 cd7284b4cbbfeb507e630c1aac019f4157393acb";
11796    const PDFTOTEXT_VERSION: &str = "pdftotext version 26.01.0";
11797    const PDFTOPPM_VERSION: &str = "pdftoppm version 26.01.0";
11798    const PLOT_RENDER_NORMALIZED_MAE_THRESHOLD: f64 = 0.0;
11799
11800    #[test]
11801    fn authored_charts_emit_portable_viewer_defaults() {
11802        let data = ChartData {
11803            categories: vec!["August".to_owned(), "September".to_owned()],
11804            series: vec![("Price".to_owned(), vec![12.5, 19.2])],
11805            number_format: Some(r"0.##\%".to_owned()),
11806            category_axis_title: Some("Month".to_owned()),
11807            value_axis_title: Some("Change".to_owned()),
11808            palette: vec![
11809                RgbColor::parse("2B6FE3").unwrap(),
11810                RgbColor::parse("F0761F").unwrap(),
11811            ],
11812        };
11813        let (line, line_workbook) = authored_chart_parts(ChartKind::Line, &data, "rId1").unwrap();
11814        let line = std::str::from_utf8(&line).unwrap();
11815        assert_eq!(line.matches(r#"<c:delete val="0"/>"#).count(), 2);
11816        assert!(line.contains(r#"<c:numFmt formatCode="0.##\%" sourceLinked="0"/>"#));
11817        assert!(line.contains("<a:t>Month</a:t>"));
11818        assert!(line.contains("<a:t>Change</a:t>"));
11819        assert!(line.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
11820        let assert_portable_line = |xml: &str| {
11821            let titles = xml
11822                .split("<c:title>")
11823                .skip(1)
11824                .map(|tail| tail.split_once("</c:title>").unwrap().0)
11825                .collect::<Vec<_>>();
11826            assert_eq!(titles.len(), 2);
11827            for title in titles {
11828                let text = title.find("<c:tx>").unwrap();
11829                let layout = title.find("<c:layout/>").unwrap();
11830                let overlay = title.find(r#"<c:overlay val="0"/>"#).unwrap();
11831                assert!(text < layout && layout < overlay);
11832            }
11833
11834            let line_plot = xml
11835                .split_once("<c:lineChart>")
11836                .unwrap()
11837                .1
11838                .split_once("</c:lineChart>")
11839                .unwrap()
11840                .0;
11841            assert_eq!(line_plot.matches(r#"<c:marker val="0"/>"#).count(), 1);
11842            assert_eq!(line_plot.matches(r#"<c:smooth val="0"/>"#).count(), 1);
11843            let marker = line_plot.find(r#"<c:marker val="0"/>"#).unwrap();
11844            let smooth = line_plot.find(r#"<c:smooth val="0"/>"#).unwrap();
11845            let axis_id = line_plot.find("<c:axId").unwrap();
11846            assert!(marker < smooth && smooth < axis_id);
11847        };
11848        assert_portable_line(line);
11849        for axis in line.split("<c:crossAx").take(2) {
11850            let scaling = axis.rfind("<c:scaling").unwrap();
11851            let deleted = axis.rfind("<c:delete").unwrap();
11852            let position = axis.rfind("<c:axPos").unwrap();
11853            assert!(scaling < deleted && deleted < position);
11854        }
11855
11856        let parsed = CT_ChartSpace::from_xml(line.as_bytes()).unwrap();
11857        let rewritten = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
11858        assert_portable_line(&rewritten);
11859        assert!(rewritten.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
11860        assert!(rewritten.contains(r#"<c:externalData r:id="rId1">"#));
11861
11862        for kind in [
11863            ChartKind::Bar,
11864            ChartKind::Pie,
11865            ChartKind::Doughnut,
11866            ChartKind::Area,
11867            ChartKind::Scatter,
11868            ChartKind::Radar,
11869        ] {
11870            let mut kind_data = data.clone();
11871            if kind == ChartKind::Scatter {
11872                kind_data.categories = vec!["1".to_owned(), "2".to_owned()];
11873            }
11874            let (chart, workbook) = authored_chart_parts(kind, &kind_data, "rId1").unwrap();
11875            let chart = std::str::from_utf8(&chart).unwrap();
11876            assert!(!chart.contains(r#"<c:marker val="0"/>"#));
11877            assert!(!chart.contains(r#"<c:smooth val="0"/>"#));
11878            if kind != ChartKind::Scatter {
11879                assert_eq!(workbook, line_workbook);
11880            }
11881            if matches!(kind, ChartKind::Pie | ChartKind::Doughnut) {
11882                assert!(chart.contains("<c:legend"));
11883                assert!(chart.contains(r#"<c:showPercent val="1"/>"#));
11884            }
11885            if kind == ChartKind::Doughnut {
11886                assert!(chart.contains(r#"<c:holeSize val="50"/>"#));
11887            }
11888            if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) {
11889                assert_eq!(chart.matches("<c:dPt>").count(), 2);
11890                assert!(chart.contains(
11891                    r#"<c:dPt><c:idx val="0"/><c:spPr><a:solidFill><a:srgbClr val="2B6FE3"/>"#
11892                ));
11893                assert!(chart.contains(
11894                    r#"<c:dPt><c:idx val="1"/><c:spPr><a:solidFill><a:srgbClr val="F0761F"/>"#
11895                ));
11896            }
11897
11898            let parsed = CT_ChartSpace::from_xml(chart.as_bytes()).unwrap();
11899            let rewritten = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
11900            assert_eq!(
11901                rewritten.matches("<c:dPt>").count(),
11902                usize::from(matches!(
11903                    kind,
11904                    ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut
11905                )) * 2
11906            );
11907            assert!(rewritten.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
11908            if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) {
11909                assert!(rewritten.contains(r#"<a:srgbClr val="F0761F"/>"#));
11910            }
11911        }
11912    }
11913
11914    #[test]
11915    fn optional_numeric_cache_format_remains_omitted() {
11916        let xml = br#"<c:numRef xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:f>Sheet1!$B$2:$B$3</c:f><c:numCache><c:ptCount val="2"/><c:pt idx="0"><c:v>12.5</c:v></c:pt><c:pt idx="1"><c:v>19.2</c:v></c:pt></c:numCache></c:numRef>"#;
11917        let mut parsed = NumericData::from_xml(xml).unwrap();
11918        assert_eq!(parsed.format_code, "General");
11919        let written = parsed.to_xml().unwrap();
11920        assert!(!String::from_utf8_lossy(&written).contains("formatCode"));
11921        assert_eq!(NumericData::from_xml(&written).unwrap(), parsed);
11922
11923        parsed.format_code = "0.00".to_owned();
11924        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
11925        assert!(written.contains("<c:formatCode>0.00</c:formatCode>"));
11926    }
11927
11928    fn render_geometry(chart: &super::CT_Chart, bounds: Rect) -> super::Result<ChartGeometry> {
11929        render_geometry_with_theme(
11930            chart,
11931            bounds,
11932            &CT_OfficeStyleSheet::office_default(),
11933            &ColorMap::default(),
11934        )
11935    }
11936
11937    fn render_chart(
11938        chart: &super::CT_Chart,
11939        bounds: Rect,
11940        fonts: &mut FontManager,
11941    ) -> super::Result<oxml_layout::GroupElement> {
11942        render_chart_with_theme(
11943            chart,
11944            bounds,
11945            &CT_OfficeStyleSheet::office_default(),
11946            &ColorMap::default(),
11947            fonts,
11948        )
11949    }
11950
11951    #[test]
11952    fn bar_chart_rasterises_at_computed_positions() {
11953        let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11954        let geometry = render_geometry(
11955            &chart.chart,
11956            Rect {
11957                x: 0.0,
11958                y: 0.0,
11959                width: 200.0,
11960                height: 140.0,
11961            },
11962        )
11963        .unwrap();
11964        assert_eq!(
11965            geometry.plot_bounds,
11966            Rect {
11967                x: 36.0,
11968                y: 12.0,
11969                width: 152.0,
11970                height: 100.0,
11971            }
11972        );
11973        let bounds = path_bounds(&geometry);
11974        assert_eq!(bounds.len(), 2);
11975        assert_rect_near(
11976            bounds[0],
11977            Rect {
11978                x: 58.8,
11979                y: 62.0,
11980                width: 30.4,
11981                height: 50.0,
11982            },
11983        );
11984        assert_rect_near(
11985            bounds[1],
11986            Rect {
11987                x: 134.8,
11988                y: 12.0,
11989                width: 30.4,
11990                height: 100.0,
11991            },
11992        );
11993
11994        let layout = LayoutResult::new(
11995            vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
11996            Vec::new(),
11997            None,
11998            Vec::new(),
11999        );
12000        let png = oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap();
12001        let pixmap = tiny_skia::Pixmap::decode_png(&png).unwrap();
12002        for (x, y) in [(74, 87), (150, 37)] {
12003            let pixel = pixmap.pixel(x, y).unwrap();
12004            assert_ne!((pixel.red(), pixel.green(), pixel.blue()), (255, 255, 255));
12005        }
12006    }
12007
12008    #[test]
12009    fn unstyled_four_series_use_accent_one_through_four() {
12010        let chart = four_series_bar_chart();
12011        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12012        let colours = path_colours(&geometry);
12013
12014        assert_eq!(
12015            &colours[..4],
12016            &[
12017                Color::from_hex("156082"),
12018                Color::from_hex("E97132"),
12019                Color::from_hex("196B24"),
12020                Color::from_hex("0F9ED5"),
12021            ]
12022        );
12023    }
12024
12025    #[test]
12026    fn direct_series_solid_colour_overrides_theme_accent() {
12027        for (paint, expected) in [
12028            (
12029                "<a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill>",
12030                Color::from_hex("123456"),
12031            ),
12032            (
12033                "<a:ln><a:solidFill><a:srgbClr val=\"654321\"/></a:solidFill></a:ln>",
12034                Color::from_hex("654321"),
12035            ),
12036            (
12037                "<a:noFill/>",
12038                Color {
12039                    r: 0.0,
12040                    g: 0.0,
12041                    b: 0.0,
12042                    a: 0.0,
12043                },
12044            ),
12045        ] {
12046            let styled =
12047                plot_series(0).replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
12048            let plot = bar_plot("").replace(&plot_series(0), &styled);
12049            let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
12050            let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12051
12052            assert_eq!(path_colours(&geometry)[0], expected);
12053        }
12054
12055        let styled = plot_series(0).replace(
12056            "<q:cat>",
12057            "<q:spPr><a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill></q:spPr><q:cat>",
12058        );
12059        let plot = bar_plot("").replace(&plot_series(0), &styled);
12060        let xml = chart_with_plot(&plot).replace(
12061            "</q:plotArea></q:chart>",
12062            "</q:plotArea><q:legend/></q:chart>",
12063        );
12064        let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
12065        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12066        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12067        assert!(group.children.iter().any(|element| matches!(
12068            element,
12069            PositionedElement::Path(path)
12070                if path.fill == Some(Paint::Solid(Color::from_hex("123456")))
12071        )));
12072
12073        let mut theme = CT_OfficeStyleSheet::office_default();
12074        let preset = oxml_drawing::fill::Fill::from_xml(
12075            br#"<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:prstClr val="red"/></a:solidFill>"#,
12076        )
12077        .unwrap();
12078        let oxml_drawing::fill::Fill::Solid(preset) = preset else {
12079            panic!("expected solid fill");
12080        };
12081        theme.theme_elements.color_scheme.hyperlink = preset.color.unwrap();
12082        let geometry =
12083            render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &ColorMap::default())
12084                .unwrap();
12085        assert_eq!(path_colours(&geometry)[0], Color::from_hex("123456"));
12086
12087        for (paint, expected_alpha) in [
12088            ("<a:noFill/>", 0.0),
12089            (
12090                "<a:solidFill><a:srgbClr val=\"123456\"><a:alpha val=\"50000\"/></a:srgbClr></a:solidFill>",
12091                (128.0 / 255.0) * 0.55,
12092            ),
12093        ] {
12094            let (_, area, _) = remaining_plot_fixtures()
12095                .into_iter()
12096                .find(|(kind, _, _)| *kind == "areaChart")
12097                .unwrap();
12098            let styled_area = area.replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
12099            let chart =
12100                CT_ChartSpace::from_xml(chart_with_optional_axes(&styled_area, true).as_bytes())
12101                    .unwrap();
12102            let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12103            let actual_alpha = path_colours(&geometry)[0].a;
12104
12105            assert!((actual_alpha - expected_alpha).abs() < 1.0e-12);
12106        }
12107    }
12108
12109    #[test]
12110    fn series_accent_cycle_repeats_after_six() {
12111        let chart = seven_series_bar_chart();
12112        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12113        let colours = path_colours(&geometry);
12114
12115        assert_eq!(colours[0], Color::from_hex("156082"));
12116        assert_eq!(colours[6], colours[0]);
12117    }
12118
12119    #[test]
12120    fn series_colours_honor_colour_map_and_transform_order() {
12121        let styled = plot_series(0).replace(
12122            "<q:cat>",
12123            "<q:spPr><a:solidFill><a:schemeClr val=\"accent1\"><a:shade val=\"80000\"/><a:lumOff val=\"5000\"/><a:alpha val=\"50000\"/></a:schemeClr></a:solidFill></q:spPr><q:cat>",
12124        );
12125        let plot = bar_plot("").replace(&plot_series(0), &styled);
12126        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
12127        let color_map =
12128            ColorMap::default().with_overrides(&[(ColorMapSlot::Accent1, ThemeColorSlot::Accent4)]);
12129        let mut theme = CT_OfficeStyleSheet::office_default();
12130        let theme_fill = oxml_drawing::fill::Fill::from_xml(
12131            br#"<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:srgbClr val="336699"><a:tint val="20000"/><a:lumMod val="90000"/></a:srgbClr></a:solidFill>"#,
12132        )
12133        .unwrap();
12134        let oxml_drawing::fill::Fill::Solid(theme_fill) = theme_fill else {
12135            panic!("expected solid fill");
12136        };
12137        let theme_colour = theme_fill.color.unwrap();
12138        theme.theme_elements.color_scheme.accent4 = theme_colour.clone();
12139        let geometry =
12140            render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &color_map).unwrap();
12141        let series_fill = oxml_drawing::fill::Fill::from_xml(
12142            br#"<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:schemeClr val="accent1"><a:shade val="80000"/><a:lumOff val="5000"/><a:alpha val="50000"/></a:schemeClr></a:solidFill>"#,
12143        )
12144        .unwrap();
12145        let oxml_drawing::fill::Fill::Solid(series_fill) = series_fill else {
12146            panic!("expected solid fill");
12147        };
12148        let mut transforms = theme_colour.transforms().to_vec();
12149        transforms.extend_from_slice(series_fill.color.as_ref().unwrap().transforms());
12150        let expected = oxml_drawing::color::apply_color_transforms(
12151            RgbColor::parse("336699").unwrap(),
12152            &transforms,
12153        );
12154
12155        assert_eq!(path_colours(&geometry)[0], resolved_layout_colour(expected));
12156    }
12157
12158    #[test]
12159    fn chart_colours_do_not_use_word_tint_shade() {
12160        let styled = plot_series(0).replace(
12161            "<q:cat>",
12162            "<q:spPr><a:solidFill><a:srgbClr val=\"808080\"><a:tint val=\"50000\"/></a:srgbClr></a:solidFill></q:spPr><q:cat>",
12163        );
12164        let plot = bar_plot("").replace(&plot_series(0), &styled);
12165        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
12166        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12167
12168        assert_eq!(
12169            path_colours(&geometry)[0],
12170            Color {
12171                r: 205.0 / 255.0,
12172                g: 205.0 / 255.0,
12173                b: 205.0 / 255.0,
12174                a: 1.0,
12175            }
12176        );
12177    }
12178
12179    #[test]
12180    fn unsupported_direct_series_paint_is_contextual() {
12181        let styled = plot_series(0).replace(
12182            "<q:cat>",
12183            "<q:spPr><a:gradFill><a:gsLst><a:gs pos=\"0\"><a:srgbClr val=\"FF0000\"/></a:gs><a:gs pos=\"100000\"><a:srgbClr val=\"0000FF\"/></a:gs></a:gsLst></a:gradFill></q:spPr><q:cat>",
12184        );
12185        let plot = bar_plot("").replace(&plot_series(0), &styled);
12186        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
12187        let error = render_geometry(&chart.chart, chart_bounds()).unwrap_err();
12188
12189        assert!(error.to_string().contains("c:ser[0] direct fill paint"));
12190        assert!(error.to_string().contains("gradient"));
12191    }
12192
12193    #[test]
12194    fn resolved_chart_palette_raster_is_deterministic() {
12195        let raster = || {
12196            let chart = four_series_bar_chart();
12197            let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12198            let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12199            let layout = LayoutResult::new(
12200                vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
12201                fonts.all_font_data(),
12202                None,
12203                Vec::new(),
12204            );
12205            oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
12206        };
12207        let chart = four_series_bar_chart();
12208        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12209
12210        assert_eq!(path_colours(&geometry)[0], Color::from_hex("156082"));
12211        assert_eq!(raster(), raster());
12212    }
12213
12214    #[test]
12215    fn bar_geometry_handles_direction_grouping_gap_and_overlap() {
12216        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12217        let Plot::Bar {
12218            overlap, series, ..
12219        } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
12220        else {
12221            panic!("expected bar plot");
12222        };
12223        let mut second = series[0].clone();
12224        second.index = 1;
12225        second.order = 1;
12226        second.values.values = vec![2.0, 1.0];
12227        series.push(second);
12228
12229        *overlap = 25;
12230        let clustered = render_geometry(&chart.chart, chart_bounds()).unwrap();
12231        let clustered_bounds = path_bounds(&clustered);
12232        let clustered_width = 30.4 / 1.75;
12233        let clustered_advance = clustered_width * 0.75;
12234        assert_rects_near(
12235            &clustered_bounds,
12236            &[
12237                Rect {
12238                    x: 58.8,
12239                    y: 62.0,
12240                    width: clustered_width,
12241                    height: 50.0,
12242                },
12243                Rect {
12244                    x: 134.8,
12245                    y: 12.0,
12246                    width: clustered_width,
12247                    height: 100.0,
12248                },
12249                Rect {
12250                    x: 58.8 + clustered_advance,
12251                    y: 12.0,
12252                    width: clustered_width,
12253                    height: 100.0,
12254                },
12255                Rect {
12256                    x: 134.8 + clustered_advance,
12257                    y: 62.0,
12258                    width: clustered_width,
12259                    height: 50.0,
12260                },
12261            ],
12262        );
12263
12264        let Plot::Bar { direction, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12265            unreachable!()
12266        };
12267        *direction = BarDirection::Bar;
12268        let horizontal = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
12269        let horizontal_width = 20.0 / 1.75;
12270        let horizontal_advance = horizontal_width * 0.75;
12271        assert_rects_near(
12272            &horizontal,
12273            &[
12274                Rect {
12275                    x: 36.0,
12276                    y: 27.0,
12277                    width: 76.0,
12278                    height: horizontal_width,
12279                },
12280                Rect {
12281                    x: 36.0,
12282                    y: 77.0,
12283                    width: 152.0,
12284                    height: horizontal_width,
12285                },
12286                Rect {
12287                    x: 36.0,
12288                    y: 27.0 + horizontal_advance,
12289                    width: 152.0,
12290                    height: horizontal_width,
12291                },
12292                Rect {
12293                    x: 36.0,
12294                    y: 77.0 + horizontal_advance,
12295                    width: 76.0,
12296                    height: horizontal_width,
12297                },
12298            ],
12299        );
12300
12301        let Plot::Bar {
12302            direction,
12303            grouping,
12304            ..
12305        } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
12306        else {
12307            unreachable!()
12308        };
12309        *direction = BarDirection::Column;
12310        *grouping = BarGrouping::Stacked;
12311        let stacked = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
12312        assert_rects_near(
12313            &stacked,
12314            &[
12315                Rect {
12316                    x: 58.8,
12317                    y: 112.0 - 100.0 / 3.0,
12318                    width: 30.4,
12319                    height: 100.0 / 3.0,
12320                },
12321                Rect {
12322                    x: 134.8,
12323                    y: 112.0 - 200.0 / 3.0,
12324                    width: 30.4,
12325                    height: 200.0 / 3.0,
12326                },
12327                Rect {
12328                    x: 58.8,
12329                    y: 12.0,
12330                    width: 30.4,
12331                    height: 200.0 / 3.0,
12332                },
12333                Rect {
12334                    x: 134.8,
12335                    y: 12.0,
12336                    width: 30.4,
12337                    height: 100.0 / 3.0,
12338                },
12339            ],
12340        );
12341
12342        let Plot::Bar {
12343            grouping, series, ..
12344        } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
12345        else {
12346            unreachable!()
12347        };
12348        *grouping = BarGrouping::PercentStacked;
12349        series[1].values.values = vec![2.0, 2.0];
12350        let percent = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
12351        assert_rects_near(
12352            &percent,
12353            &[
12354                Rect {
12355                    x: 58.8,
12356                    y: 112.0 - 100.0 / 3.0,
12357                    width: 30.4,
12358                    height: 100.0 / 3.0,
12359                },
12360                Rect {
12361                    x: 134.8,
12362                    y: 62.0,
12363                    width: 30.4,
12364                    height: 50.0,
12365                },
12366                Rect {
12367                    x: 58.8,
12368                    y: 12.0,
12369                    width: 30.4,
12370                    height: 200.0 / 3.0,
12371                },
12372                Rect {
12373                    x: 134.8,
12374                    y: 12.0,
12375                    width: 30.4,
12376                    height: 50.0,
12377                },
12378            ],
12379        );
12380    }
12381
12382    #[test]
12383    fn line_scatter_and_radar_emit_paths_and_markers() {
12384        let line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
12385        let line_geometry = render_geometry(&line.chart, chart_bounds()).unwrap();
12386        assert_path_points(
12387            path_commands(&line_geometry)[0],
12388            &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
12389            false,
12390        );
12391        assert_rects_near(
12392            &path_bounds(&line_geometry)[1..],
12393            &[
12394                Rect {
12395                    x: 71.5,
12396                    y: 109.5,
12397                    width: 5.0,
12398                    height: 5.0,
12399                },
12400                Rect {
12401                    x: 147.5,
12402                    y: 9.5,
12403                    width: 5.0,
12404                    height: 5.0,
12405                },
12406            ],
12407        );
12408
12409        let (scatter_plot, _) = remaining_plot_fixtures()
12410            .into_iter()
12411            .find(|(kind, _, _)| *kind == "scatterChart")
12412            .map(|(_, plot, axes)| (plot, axes))
12413            .unwrap();
12414        let mut scatter =
12415            CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
12416                .unwrap();
12417        let Plot::Scatter { style, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
12418        else {
12419            unreachable!()
12420        };
12421        *style = ScatterStyle::LineMarker;
12422        let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12423        assert_path_points(
12424            path_commands(&scatter_geometry)[0],
12425            &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
12426            false,
12427        );
12428        assert_rects_near(
12429            &path_bounds(&scatter_geometry)[1..],
12430            &[
12431                Rect {
12432                    x: 33.5,
12433                    y: 109.5,
12434                    width: 5.0,
12435                    height: 5.0,
12436                },
12437                Rect {
12438                    x: 185.5,
12439                    y: 9.5,
12440                    width: 5.0,
12441                    height: 5.0,
12442                },
12443            ],
12444        );
12445
12446        let (radar_plot, _) = remaining_plot_fixtures()
12447            .into_iter()
12448            .find(|(kind, _, _)| *kind == "radarChart")
12449            .map(|(_, plot, axes)| (plot, axes))
12450            .unwrap();
12451        let radar = CT_ChartSpace::from_xml(chart_with_optional_axes(&radar_plot, true).as_bytes())
12452            .unwrap();
12453        let radar_geometry = render_geometry(&radar.chart, chart_bounds()).unwrap();
12454        assert_path_points(
12455            path_commands(&radar_geometry)[0],
12456            &[Point { x: 112.0, y: 37.0 }, Point { x: 112.0, y: 112.0 }],
12457            true,
12458        );
12459        assert_rects_near(
12460            &path_bounds(&radar_geometry)[1..],
12461            &[
12462                Rect {
12463                    x: 109.5,
12464                    y: 34.5,
12465                    width: 5.0,
12466                    height: 5.0,
12467                },
12468                Rect {
12469                    x: 109.5,
12470                    y: 109.5,
12471                    width: 5.0,
12472                    height: 5.0,
12473                },
12474            ],
12475        );
12476    }
12477
12478    #[test]
12479    fn pie_doughnut_and_area_emit_closed_paths() {
12480        let fixtures = remaining_plot_fixtures();
12481        let pie_plot = fixtures
12482            .iter()
12483            .find(|(kind, _, _)| *kind == "pieChart")
12484            .unwrap();
12485        let pie =
12486            CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot.1, pie_plot.2).as_bytes())
12487                .unwrap();
12488        let pie_geometry = render_geometry(&pie.chart, chart_bounds()).unwrap();
12489        let pie_commands = path_commands(&pie_geometry);
12490        assert_eq!(pie_commands.len(), 2);
12491        let [
12492            PathCommand::MoveTo(center),
12493            PathCommand::LineTo(start),
12494            ..,
12495            PathCommand::CurveTo { to: end, .. },
12496            PathCommand::Close,
12497        ] = pie_commands[0]
12498        else {
12499            panic!("expected closed cubic pie wedge");
12500        };
12501        assert_point_near(*center, Point { x: 112.0, y: 62.0 });
12502        assert_point_near(
12503            *start,
12504            Point {
12505                x: 137.0,
12506                y: 62.0 - 25.0 * 3.0_f64.sqrt(),
12507            },
12508        );
12509        assert_point_near(
12510            *end,
12511            Point {
12512                x: 137.0,
12513                y: 62.0 + 25.0 * 3.0_f64.sqrt(),
12514            },
12515        );
12516
12517        let doughnut_plot = fixtures
12518            .iter()
12519            .find(|(kind, _, _)| *kind == "doughnutChart")
12520            .unwrap();
12521        let doughnut = CT_ChartSpace::from_xml(
12522            chart_with_optional_axes(&doughnut_plot.1, doughnut_plot.2).as_bytes(),
12523        )
12524        .unwrap();
12525        let doughnut_geometry = render_geometry(&doughnut.chart, chart_bounds()).unwrap();
12526        let doughnut_commands = path_commands(&doughnut_geometry);
12527        assert_eq!(doughnut_commands.len(), 2);
12528        let commands = doughnut_commands[0];
12529        assert_eq!(commands.len(), 7);
12530        let PathCommand::MoveTo(outer_start) = commands[0] else {
12531            panic!("expected doughnut outer start");
12532        };
12533        let PathCommand::LineTo(inner_end) = commands[3] else {
12534            panic!("expected doughnut inner-radius join");
12535        };
12536        let PathCommand::CurveTo {
12537            to: inner_start, ..
12538        } = commands[5]
12539        else {
12540            panic!("expected reverse inner arc");
12541        };
12542        let diagonal = 25.0 * 2.0_f64.sqrt();
12543        assert_point_near(
12544            outer_start,
12545            Point {
12546                x: 112.0 + diagonal,
12547                y: 62.0 - diagonal,
12548            },
12549        );
12550        assert_point_near(
12551            inner_end,
12552            Point {
12553                x: 112.0 + 30.0 * 75.0_f64.to_radians().cos(),
12554                y: 62.0 + 30.0 * 75.0_f64.to_radians().sin(),
12555            },
12556        );
12557        let inner_diagonal = 15.0 * 2.0_f64.sqrt();
12558        assert_point_near(
12559            inner_start,
12560            Point {
12561                x: 112.0 + inner_diagonal,
12562                y: 62.0 - inner_diagonal,
12563            },
12564        );
12565        assert_eq!(commands.last(), Some(&PathCommand::Close));
12566
12567        let area_plot = fixtures
12568            .iter()
12569            .find(|(kind, _, _)| *kind == "areaChart")
12570            .unwrap();
12571        let mut area =
12572            CT_ChartSpace::from_xml(chart_with_optional_axes(&area_plot.1, area_plot.2).as_bytes())
12573                .unwrap();
12574        let area_geometry = render_geometry(&area.chart, chart_bounds()).unwrap();
12575        assert_path_points(
12576            path_commands(&area_geometry)[0],
12577            &[
12578                Point { x: 74.0, y: 62.0 },
12579                Point { x: 150.0, y: 12.0 },
12580                Point { x: 150.0, y: 112.0 },
12581                Point { x: 74.0, y: 112.0 },
12582            ],
12583            true,
12584        );
12585
12586        let Plot::Area {
12587            grouping, series, ..
12588        } = &mut area.chart.plot_area.plots_mut().unwrap()[0]
12589        else {
12590            unreachable!()
12591        };
12592        *grouping = Grouping::Stacked;
12593        let mut second = series[0].clone();
12594        second.index = 1;
12595        second.order = 1;
12596        second.values.values = vec![2.0, 1.0];
12597        series.push(second);
12598        let stacked_area = render_geometry(&area.chart, chart_bounds()).unwrap();
12599        assert_path_points(
12600            path_commands(&stacked_area)[1],
12601            &[
12602                Point { x: 74.0, y: 12.0 },
12603                Point { x: 150.0, y: 12.0 },
12604                Point {
12605                    x: 150.0,
12606                    y: 112.0 - 200.0 / 3.0,
12607                },
12608                Point {
12609                    x: 74.0,
12610                    y: 112.0 - 100.0 / 3.0,
12611                },
12612            ],
12613            true,
12614        );
12615    }
12616
12617    #[test]
12618    fn sparse_cache_indexes_preserve_slots_and_scatter_pairing() {
12619        let series = sparse_category_series();
12620        let bar = format!(
12621            r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{series}<q:gapWidth val="150"/><q:overlap val="0"/><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:barChart>"#
12622        );
12623        let bar = CT_ChartSpace::from_xml(chart_with_plot(&bar).as_bytes()).unwrap();
12624        assert_rects_near(
12625            &path_bounds(&render_geometry(&bar.chart, chart_bounds()).unwrap()),
12626            &[
12627                Rect {
12628                    x: 51.2,
12629                    y: 62.0,
12630                    width: 304.0 / 15.0,
12631                    height: 50.0,
12632                },
12633                Rect {
12634                    x: 36.0 + 304.0 / 3.0 + 15.2,
12635                    y: 12.0,
12636                    width: 304.0 / 15.0,
12637                    height: 100.0,
12638                },
12639            ],
12640        );
12641
12642        let line = format!(
12643            r#"<q:lineChart><q:grouping val="standard"/>{series}<q:marker val="0"/><q:smooth val="0"/><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:lineChart>"#
12644        );
12645        let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line).as_bytes()).unwrap();
12646        let first = Point {
12647            x: 36.0 + 76.0 / 3.0,
12648            y: 62.0,
12649        };
12650        let missing = Point { x: 112.0, y: 112.0 };
12651        let last = Point {
12652            x: 36.0 + 380.0 / 3.0,
12653            y: 12.0,
12654        };
12655        let gap = render_geometry(&line.chart, chart_bounds()).unwrap();
12656        let gap_commands = path_commands(&gap);
12657        assert_eq!(gap_commands.len(), 2);
12658        assert_path_points(
12659            gap_commands[0],
12660            &[Point {
12661                x: first.x,
12662                y: 112.0,
12663            }],
12664            false,
12665        );
12666        assert_path_points(gap_commands[1], &[last], false);
12667        line.chart.disp_blanks_as = DispBlanksAs::Zero;
12668        assert_path_points(
12669            path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
12670            &[first, missing, last],
12671            false,
12672        );
12673        line.chart.disp_blanks_as = DispBlanksAs::Span;
12674        assert_path_points(
12675            path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
12676            &[
12677                Point {
12678                    x: first.x,
12679                    y: 112.0,
12680                },
12681                last,
12682            ],
12683            false,
12684        );
12685
12686        let area = format!(
12687            r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:areaChart>"#
12688        );
12689        let mut area = CT_ChartSpace::from_xml(chart_with_plot(&area).as_bytes()).unwrap();
12690        let area_gap = render_geometry(&area.chart, chart_bounds()).unwrap();
12691        let area_gap_commands = path_commands(&area_gap);
12692        assert_eq!(area_gap_commands.len(), 2);
12693        assert_path_points(
12694            area_gap_commands[0],
12695            &[
12696                first,
12697                Point {
12698                    x: first.x,
12699                    y: 112.0,
12700                },
12701            ],
12702            true,
12703        );
12704        assert_path_points(
12705            area_gap_commands[1],
12706            &[
12707                last,
12708                Point {
12709                    x: last.x,
12710                    y: 112.0,
12711                },
12712            ],
12713            true,
12714        );
12715        area.chart.disp_blanks_as = DispBlanksAs::Zero;
12716        assert_path_points(
12717            path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
12718            &[
12719                first,
12720                missing,
12721                last,
12722                Point {
12723                    x: last.x,
12724                    y: 112.0,
12725                },
12726                missing,
12727                Point {
12728                    x: first.x,
12729                    y: 112.0,
12730                },
12731            ],
12732            true,
12733        );
12734        area.chart.disp_blanks_as = DispBlanksAs::Span;
12735        assert_path_points(
12736            path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
12737            &[
12738                first,
12739                last,
12740                Point {
12741                    x: last.x,
12742                    y: 112.0,
12743                },
12744                Point {
12745                    x: first.x,
12746                    y: 112.0,
12747                },
12748            ],
12749            true,
12750        );
12751
12752        let radar = format!(
12753            r#"<q:radarChart><q:radarStyle val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#
12754        );
12755        let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
12756        assert_path_points(
12757            path_commands(&render_geometry(&radar.chart, chart_bounds()).unwrap())[0],
12758            &[
12759                Point { x: 112.0, y: 37.0 },
12760                Point {
12761                    x: 112.0 - 25.0 * 3.0_f64.sqrt(),
12762                    y: 87.0,
12763                },
12764            ],
12765            true,
12766        );
12767
12768        let scatter = format!(
12769            r#"<q:scatterChart><q:scatterStyle val="lineMarker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:scatterChart>"#,
12770            sparse_scatter_series()
12771        );
12772        let mut scatter = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
12773        let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12774        assert_eq!(geometry_children(&scatter_geometry).len(), 4);
12775        let gap_commands = path_commands(&scatter_geometry);
12776        assert_path_points(gap_commands[0], &[Point { x: 36.0, y: 112.0 }], false);
12777        assert_path_points(gap_commands[1], &[Point { x: 188.0, y: 12.0 }], false);
12778
12779        scatter.chart.disp_blanks_as = DispBlanksAs::Zero;
12780        let zero = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12781        assert_path_points(
12782            path_commands(&zero)[0],
12783            &[
12784                Point { x: 74.0, y: 87.0 },
12785                Point { x: 36.0, y: 62.0 },
12786                Point { x: 150.0, y: 112.0 },
12787                Point { x: 188.0, y: 12.0 },
12788            ],
12789            false,
12790        );
12791
12792        scatter.chart.disp_blanks_as = DispBlanksAs::Span;
12793        assert_path_points(
12794            path_commands(&render_geometry(&scatter.chart, chart_bounds()).unwrap())[0],
12795            &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
12796            false,
12797        );
12798    }
12799
12800    #[test]
12801    fn sparse_scatter_domains_and_labels_use_only_matched_points() {
12802        let scatter = format!(
12803            r#"<q:scatterChart><q:scatterStyle val="lineMarker"/>{}<q:dLbls><q:showVal val="1"/></q:dLbls><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:scatterChart>"#,
12804            sparse_scatter_series()
12805        );
12806        let chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
12807        let plot = &chart.chart.plot_area.plots().unwrap()[0];
12808        assert_eq!(
12809            super::scatter_domains(plot, DispBlanksAs::Gap).unwrap(),
12810            (
12811                Domain {
12812                    min: 10.0,
12813                    max: 40.0,
12814                },
12815                Domain {
12816                    min: 100.0,
12817                    max: 400.0,
12818                },
12819            )
12820        );
12821        let axes = chart.chart.plot_area.axes().unwrap();
12822        let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
12823        let anchors =
12824            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12825        assert_eq!(
12826            anchors.keys().copied().collect::<Vec<_>>(),
12827            vec![(0, 0), (0, 3)]
12828        );
12829        assert_eq!(anchors[&(0, 0)].center, Point { x: 0.0, y: 140.0 });
12830        assert_eq!(anchors[&(0, 3)].center, Point { x: 200.0, y: 0.0 });
12831    }
12832
12833    #[test]
12834    fn sparse_bubble_size_labels_resolve_logical_indexes() {
12835        let scatter = format!(
12836            r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:dLbls><q:showBubbleSize val="1"/></q:dLbls><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:scatterChart>"#,
12837            sparse_scatter_series()
12838        );
12839        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
12840        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12841        let Plot::Scatter { series, .. } = plot else {
12842            panic!("expected scatter plot");
12843        };
12844        series[0].bubble_size = Some(
12845            NumericData::from_xml(
12846                format!(
12847                    r#"<q:numRef xmlns:q="{C_NS}"><q:f>Sheet1!$C$2:$C$5</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="4"/><q:pt idx="0"><q:v>5</q:v></q:pt><q:pt idx="3"><q:v>9</q:v></q:pt></q:numCache></q:numRef>"#
12848                )
12849                .as_bytes(),
12850            )
12851            .unwrap(),
12852        );
12853        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12854        let mut rendered = Vec::new();
12855        super::render_data_labels(
12856            plot,
12857            chart_bounds(),
12858            DispBlanksAs::Gap,
12859            super::GeometryOptions::default(),
12860            &mut fonts,
12861            &mut rendered,
12862        )
12863        .unwrap();
12864        let text: Vec<_> = rendered
12865            .iter()
12866            .filter_map(|element| match element {
12867                PositionedElement::Text(run) => Some(run.text.as_str()),
12868                _ => None,
12869            })
12870            .collect();
12871        assert_eq!(text, vec!["5", "9"]);
12872    }
12873
12874    #[test]
12875    fn geometry_rejects_invalid_bounds_and_opaque_plots() {
12876        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12877        for bounds in [
12878            Rect {
12879                x: 0.0,
12880                y: 0.0,
12881                width: f64::NAN,
12882                height: 100.0,
12883            },
12884            Rect {
12885                x: 0.0,
12886                y: 0.0,
12887                width: 0.0,
12888                height: 100.0,
12889            },
12890            Rect {
12891                x: 0.0,
12892                y: 0.0,
12893                width: 40.0,
12894                height: 30.0,
12895            },
12896        ] {
12897            assert!(render_geometry(&chart.chart, bounds).is_err());
12898        }
12899
12900        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12901            unreachable!()
12902        };
12903        series[0].values.values.clear();
12904        assert!(render_geometry(&chart.chart, chart_bounds()).is_err());
12905
12906        let opaque = CT_ChartSpace::from_xml(
12907            chart_with_plot(r#"<q:bar3DChart><q:barDir val="col"/></q:bar3DChart>"#).as_bytes(),
12908        )
12909        .unwrap();
12910        let error = render_geometry(&opaque.chart, chart_bounds()).unwrap_err();
12911        assert!(error.to_string().contains("unsupported or combination"));
12912
12913        let (scatter_plot, _) = remaining_plot_fixtures()
12914            .into_iter()
12915            .find(|(kind, _, _)| *kind == "scatterChart")
12916            .map(|(_, plot, axes)| (plot, axes))
12917            .unwrap();
12918        let mut scatter =
12919            CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
12920                .unwrap();
12921        for invalid in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
12922            let Plot::Scatter { series, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
12923            else {
12924                unreachable!()
12925            };
12926            let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
12927                unreachable!()
12928            };
12929            x_values.values[0] = invalid;
12930            let error = render_geometry(&scatter.chart, chart_bounds()).unwrap_err();
12931            assert!(error.to_string().contains("xVal"));
12932        }
12933    }
12934
12935    #[test]
12936    fn finite_extremes_never_produce_nonfinite_geometry() {
12937        let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
12938        let Plot::Line { series, .. } = &mut line.chart.plot_area.plots_mut().unwrap()[0] else {
12939            unreachable!()
12940        };
12941        series[0].values.values = vec![-f64::MAX, f64::MAX];
12942        let extreme_line = render_geometry(&line.chart, chart_bounds()).unwrap();
12943        assert_path_points(
12944            path_commands(&extreme_line)[0],
12945            &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
12946            false,
12947        );
12948
12949        let mut stacked =
12950            CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12951        let Plot::Bar {
12952            grouping, series, ..
12953        } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
12954        else {
12955            unreachable!()
12956        };
12957        *grouping = BarGrouping::Stacked;
12958        series[0].values.values = vec![f64::MAX, 1.0];
12959        let mut second = series[0].clone();
12960        second.index = 1;
12961        second.order = 1;
12962        series.push(second);
12963        let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
12964        assert!(error.to_string().contains("stacked value total"));
12965
12966        let Plot::Bar { grouping, .. } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
12967        else {
12968            unreachable!()
12969        };
12970        *grouping = BarGrouping::PercentStacked;
12971        let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
12972        assert!(error.to_string().contains("stacked percentage total"));
12973
12974        let (pie_plot, _) = remaining_plot_fixtures()
12975            .into_iter()
12976            .find(|(kind, _, _)| *kind == "pieChart")
12977            .map(|(_, plot, axes)| (plot, axes))
12978            .unwrap();
12979        let mut pie =
12980            CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot, false).as_bytes()).unwrap();
12981        let Plot::Pie { series, .. } = &mut pie.chart.plot_area.plots_mut().unwrap()[0] else {
12982            unreachable!()
12983        };
12984        series[0].values.values = vec![f64::MAX, f64::MAX];
12985        let error = render_geometry(&pie.chart, chart_bounds()).unwrap_err();
12986        assert!(error.to_string().contains("pie value total"));
12987
12988        let (scatter_plot, _) = remaining_plot_fixtures()
12989            .into_iter()
12990            .find(|(kind, _, _)| *kind == "scatterChart")
12991            .map(|(_, plot, axes)| (plot, axes))
12992            .unwrap();
12993        let mut scatter =
12994            CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
12995                .unwrap();
12996        let Plot::Scatter { style, series, .. } =
12997            &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
12998        else {
12999            unreachable!()
13000        };
13001        *style = ScatterStyle::Line;
13002        let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
13003            unreachable!()
13004        };
13005        x_values.values = vec![-f64::MAX, f64::MAX];
13006        series[0].values.values = vec![-f64::MAX, f64::MAX];
13007        let extreme_scatter = render_geometry(&scatter.chart, chart_bounds()).unwrap();
13008        assert_path_points(
13009            path_commands(&extreme_scatter)[0],
13010            &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
13011            false,
13012        );
13013    }
13014
13015    #[test]
13016    fn geometry_is_backend_neutral_and_deterministic() {
13017        let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13018        let first = render_geometry(&chart.chart, chart_bounds()).unwrap();
13019        let second = render_geometry(&chart.chart, chart_bounds()).unwrap();
13020        assert_eq!(
13021            format!("{:?}", first.elements),
13022            format!("{:?}", second.elements)
13023        );
13024
13025        let raster = |geometry: ChartGeometry| {
13026            let layout = LayoutResult::new(
13027                vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
13028                Vec::new(),
13029                None,
13030                Vec::new(),
13031            );
13032            oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
13033        };
13034        assert_eq!(raster(first), raster(second));
13035    }
13036
13037    #[test]
13038    fn zero_to_one_hundred_axis_uses_expected_ticks() {
13039        let scale = nice_number_scale(
13040            Domain {
13041                min: 0.0,
13042                max: 100.0,
13043            },
13044            None,
13045            None,
13046            6,
13047        )
13048        .unwrap();
13049        assert_eq!(scale.ticks, vec![0.0, 20.0, 40.0, 60.0, 80.0, 100.0]);
13050    }
13051
13052    #[test]
13053    fn nice_number_ticks_cover_unpinned_extents() {
13054        for domain in [
13055            Domain {
13056                min: 3.0,
13057                max: 97.0,
13058            },
13059            Domain {
13060                min: -97.0,
13061                max: -3.0,
13062            },
13063            Domain {
13064                min: -3.5,
13065                max: 8.25,
13066            },
13067            Domain {
13068                min: 0.125,
13069                max: 0.875,
13070            },
13071            Domain {
13072                min: 1.0e12,
13073                max: 7.0e12,
13074            },
13075            Domain {
13076                min: -f64::MAX,
13077                max: f64::MAX,
13078            },
13079            Domain {
13080                min: f64::from_bits(1),
13081                max: f64::from_bits(5),
13082            },
13083        ] {
13084            let scale = nice_number_scale(domain, None, None, 6).unwrap();
13085            assert!(scale.domain.min <= domain.min);
13086            assert!(scale.domain.max >= domain.max);
13087            assert!(scale.ticks.iter().all(|tick| tick.is_finite()));
13088            assert!(scale.ticks.windows(2).all(|pair| pair[0] < pair[1]));
13089        }
13090
13091        let constant = nice_number_scale(Domain { min: 4.0, max: 4.0 }, None, None, 6).unwrap();
13092        assert!(constant.domain.min < 4.0 && constant.domain.max > 4.0);
13093    }
13094
13095    #[test]
13096    fn one_sided_explicit_scale_beyond_data_derives_a_valid_opposite_bound() {
13097        let minimum = nice_number_scale(
13098            Domain {
13099                min: 0.0,
13100                max: 10.0,
13101            },
13102            Some(20.0),
13103            None,
13104            6,
13105        )
13106        .unwrap();
13107        assert_eq!(minimum.domain.min, 20.0);
13108        assert!(minimum.domain.max > 20.0);
13109        assert!(minimum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
13110
13111        let maximum = nice_number_scale(
13112            Domain {
13113                min: 0.0,
13114                max: 10.0,
13115            },
13116            None,
13117            Some(-20.0),
13118            6,
13119        )
13120        .unwrap();
13121        assert_eq!(maximum.domain.max, -20.0);
13122        assert!(maximum.domain.min < -20.0);
13123        assert!(maximum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
13124    }
13125
13126    #[test]
13127    fn standard_line_domain_does_not_force_zero() {
13128        let mut chart =
13129            CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
13130        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13131        let Plot::Line { series, .. } = plot else {
13132            panic!("expected line plot");
13133        };
13134        series[0].values.values = vec![1_000.0, 1_010.0];
13135        assert_eq!(
13136            super::plot_value_domain(plot, DispBlanksAs::Gap).unwrap(),
13137            Some(Domain {
13138                min: 1_000.0,
13139                max: 1_010.0,
13140            })
13141        );
13142        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
13143        assert_path_points(
13144            path_commands(&geometry)[0],
13145            &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
13146            false,
13147        );
13148    }
13149
13150    #[test]
13151    fn explicit_scaling_bounds_and_orientation_are_honored() {
13152        let scale = nice_number_scale(
13153            Domain {
13154                min: 25.0,
13155                max: 75.0,
13156            },
13157            Some(0.0),
13158            Some(100.0),
13159            6,
13160        )
13161        .unwrap();
13162        assert_eq!(
13163            scale.domain,
13164            Domain {
13165                min: 0.0,
13166                max: 100.0
13167            }
13168        );
13169        assert_eq!(
13170            super::map_y_oriented(0.0, scale.domain, chart_bounds(), Orientation::MaxMin).unwrap(),
13171            0.0
13172        );
13173        assert_eq!(
13174            super::map_y_oriented(100.0, scale.domain, chart_bounds(), Orientation::MaxMin)
13175                .unwrap(),
13176            140.0
13177        );
13178
13179        let mut chart = labelled_bar_chart();
13180        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
13181            panic!("expected bar plot");
13182        };
13183        series[0].values.values = vec![25.0, 75.0];
13184        let value_axis = chart
13185            .chart
13186            .plot_area
13187            .axes
13188            .iter_mut()
13189            .find(|axis| axis.kind == AxisKind::Value)
13190            .unwrap();
13191        value_axis.scaling.orientation = Orientation::MaxMin;
13192        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13193        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13194        let plot = group
13195            .children
13196            .iter()
13197            .find_map(|element| match element {
13198                PositionedElement::Group(group) => Some(group),
13199                _ => None,
13200            })
13201            .unwrap();
13202        let bars: Vec<_> = plot
13203            .children
13204            .iter()
13205            .filter_map(|element| match element {
13206                PositionedElement::Path(path) => path.path.bounds(),
13207                _ => None,
13208            })
13209            .collect();
13210        assert_eq!(bars.len(), 2);
13211        assert!((bars[0].y - 12.0).abs() < 1.0e-9);
13212        assert!((bars[0].height - 25.0).abs() < 1.0e-9);
13213        assert!((bars[1].height - 75.0).abs() < 1.0e-9);
13214    }
13215
13216    #[test]
13217    fn family_label_anchors_share_rendered_geometry() {
13218        let mut bar = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13219        let bar_plot = &mut bar.chart.plot_area.plots_mut().unwrap()[0];
13220        let Plot::Bar { series, .. } = bar_plot else {
13221            panic!("expected bar plot");
13222        };
13223        let mut second = series[0].clone();
13224        second.index = 1;
13225        second.order = 1;
13226        series.push(second);
13227        let anchors = super::plot_label_anchors(
13228            bar_plot,
13229            chart_bounds(),
13230            DispBlanksAs::Gap,
13231            super::GeometryOptions::default(),
13232        )
13233        .unwrap();
13234        assert_point_near(anchors[&(0, 0)].center, Point { x: 40.0, y: 105.0 });
13235        assert_point_near(anchors[&(1, 0)].center, Point { x: 60.0, y: 105.0 });
13236
13237        let pie = CT_ChartSpace::from_xml(
13238            chart_with_optional_axes(
13239                &remaining_plot_fixtures()
13240                    .into_iter()
13241                    .find(|(kind, _, _)| *kind == "pieChart")
13242                    .unwrap()
13243                    .1,
13244                false,
13245            )
13246            .as_bytes(),
13247        )
13248        .unwrap();
13249        let Plot::Pie {
13250            first_slice_angle,
13251            series,
13252            ..
13253        } = &pie.chart.plot_area.plots().unwrap()[0]
13254        else {
13255            panic!("expected pie plot");
13256        };
13257        let slices = super::pie_slices(*first_slice_angle, None, series, chart_bounds()).unwrap();
13258        assert_eq!(slices.len(), 2);
13259        let first_span = std::f64::consts::TAU / 3.0;
13260        let first_midpoint = f64::from(*first_slice_angle).to_radians()
13261            - std::f64::consts::FRAC_PI_2
13262            + first_span / 2.0;
13263        assert_point_near(
13264            slices[0].anchor,
13265            super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, first_midpoint),
13266        );
13267        let second_midpoint = first_midpoint + std::f64::consts::PI;
13268        assert_point_near(
13269            slices[1].anchor,
13270            super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, second_midpoint),
13271        );
13272
13273        let radar = format!(
13274            r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
13275            plot_series(0)
13276        );
13277        let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
13278        let Plot::Radar { series, .. } = &radar.chart.plot_area.plots().unwrap()[0] else {
13279            panic!("expected radar plot");
13280        };
13281        let radar_points =
13282            super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
13283                .unwrap();
13284        assert_eq!(radar_points[0][0], (0, Point { x: 100.0, y: 35.0 }));
13285        assert_eq!(radar_points[0][1], (1, Point { x: 100.0, y: 140.0 }));
13286    }
13287
13288    #[test]
13289    fn radar_annotations_use_spokes_perimeter_labels_and_radial_gridlines() {
13290        let radar_plot = format!(
13291            r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
13292            sparse_category_series()
13293        );
13294        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
13295        for axis in &mut chart.chart.plot_area.axes {
13296            axis.major_tick_mark = TickMark::Cross;
13297            if axis.kind == AxisKind::Value {
13298                axis.major_gridlines = Some(super::ChartLines::default());
13299            }
13300        }
13301        let plot = &chart.chart.plot_area.plots().unwrap()[0];
13302        let axes = chart.chart.plot_area.axes().unwrap();
13303        let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
13304        let center = Point { x: 112.0, y: 62.0 };
13305        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13306        let annotations =
13307            super::render_axes_and_labels(plot, &axes, bounds, DispBlanksAs::Gap, &mut fonts)
13308                .unwrap();
13309        assert_eq!(annotations.axis_lines.len(), 4);
13310        for spoke in annotations.axis_lines.iter().take(3) {
13311            let PositionedElement::Path(spoke) = spoke else {
13312                panic!("radar spoke must be a path");
13313            };
13314            assert_eq!(
13315                spoke.path.commands.first(),
13316                Some(&PathCommand::MoveTo(center))
13317            );
13318        }
13319        assert!(!annotations.gridlines.is_empty());
13320        for gridline in &annotations.gridlines {
13321            let PositionedElement::Path(gridline) = gridline else {
13322                panic!("radar gridline must be a path");
13323            };
13324            assert_eq!(gridline.path.commands.last(), Some(&PathCommand::Close));
13325            let radii: Vec<_> = gridline
13326                .path
13327                .commands
13328                .iter()
13329                .filter_map(|command| match command {
13330                    PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
13331                        Some((point.x - center.x).hypot(point.y - center.y))
13332                    }
13333                    _ => None,
13334                })
13335                .collect();
13336            assert_eq!(radii.len(), 3);
13337            assert!(
13338                radii
13339                    .windows(2)
13340                    .all(|pair| (pair[0] - pair[1]).abs() < 1.0e-9)
13341            );
13342        }
13343        let text: Vec<_> = annotations
13344            .labels
13345            .iter()
13346            .filter_map(|element| match element {
13347                PositionedElement::Text(run) => Some(run.text.as_str()),
13348                _ => None,
13349            })
13350            .collect();
13351        assert!(text.contains(&"North"));
13352        assert!(text.contains(&"West"));
13353    }
13354
13355    #[test]
13356    fn radar_tick_label_positions_are_distinct() {
13357        let radar_plot = format!(
13358            r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
13359            plot_series(0)
13360        );
13361        let chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
13362        let plot = &chart.chart.plot_area.plots().unwrap()[0];
13363        let axes = chart.chart.plot_area.axes().unwrap();
13364        let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
13365        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13366
13367        let category_axis = axes
13368            .iter()
13369            .find(|axis| axis.kind == AxisKind::Category)
13370            .unwrap();
13371        for (position, expected) in [
13372            (TickLabelPosition::High, Point { x: 108.0, y: 0.0 }),
13373            (TickLabelPosition::Low, Point { x: 108.0, y: 25.0 }),
13374            (TickLabelPosition::NextTo, Point { x: 108.0, y: 5.0 }),
13375        ] {
13376            let mut axis = category_axis.clone();
13377            axis.tick_label_position = position;
13378            let annotations =
13379                super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
13380                    .unwrap();
13381            let north = annotations
13382                .labels
13383                .iter()
13384                .find_map(|element| match element {
13385                    PositionedElement::Text(run) if run.text == "North" => Some(run),
13386                    _ => None,
13387                })
13388                .unwrap();
13389            assert_point_near(north.origin, expected);
13390        }
13391        let mut hidden_category = category_axis.clone();
13392        hidden_category.tick_label_position = TickLabelPosition::None;
13393        let annotations = super::render_axes_and_labels(
13394            plot,
13395            &[hidden_category],
13396            bounds,
13397            DispBlanksAs::Gap,
13398            &mut fonts,
13399        )
13400        .unwrap();
13401        assert!(annotations.labels.is_empty());
13402
13403        let narrow_bounds = Rect {
13404            x: 36.0,
13405            y: 12.0,
13406            width: 24.0,
13407            height: 100.0,
13408        };
13409        let narrow_center = Point { x: 48.0, y: 62.0 };
13410        assert_eq!(
13411            super::radar_category_label_origin(
13412                narrow_bounds,
13413                narrow_center,
13414                12.0,
13415                1,
13416                4,
13417                TickLabelPosition::High,
13418            ),
13419            Some(Point { x: 72.0, y: 65.0 })
13420        );
13421        assert_eq!(
13422            super::radar_category_label_origin(
13423                narrow_bounds,
13424                narrow_center,
13425                12.0,
13426                1,
13427                4,
13428                TickLabelPosition::NextTo,
13429            ),
13430            Some(Point { x: 66.0, y: 65.0 })
13431        );
13432
13433        let value_axis = axes
13434            .iter()
13435            .find(|axis| axis.kind == AxisKind::Value)
13436            .unwrap();
13437        for (position, expected) in [
13438            (TickLabelPosition::High, Point { x: 166.0, y: 65.0 }),
13439            (TickLabelPosition::Low, Point { x: 32.0, y: 65.0 }),
13440            (TickLabelPosition::NextTo, Point { x: 116.0, y: 65.0 }),
13441        ] {
13442            let mut axis = value_axis.clone();
13443            axis.tick_label_position = position;
13444            let annotations =
13445                super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
13446                    .unwrap();
13447            let zero = annotations
13448                .labels
13449                .iter()
13450                .find_map(|element| match element {
13451                    PositionedElement::Text(run) if run.text == "0" => Some(run),
13452                    _ => None,
13453                })
13454                .unwrap();
13455            assert_point_near(zero.origin, expected);
13456        }
13457        let mut hidden_value = value_axis.clone();
13458        hidden_value.tick_label_position = TickLabelPosition::None;
13459        let annotations = super::render_axes_and_labels(
13460            plot,
13461            &[hidden_value],
13462            bounds,
13463            DispBlanksAs::Gap,
13464            &mut fonts,
13465        )
13466        .unwrap();
13467        assert!(annotations.labels.is_empty());
13468    }
13469
13470    #[test]
13471    fn radar_negative_and_mixed_sign_domains_preserve_values() {
13472        let radar_plot = format!(
13473            r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
13474            plot_series(0)
13475        );
13476        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
13477        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13478        let Plot::Radar { series, .. } = plot else {
13479            panic!("expected radar plot");
13480        };
13481        series[0].values.values = vec![-10.0, -5.0];
13482        let points =
13483            super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
13484                .unwrap();
13485        assert_eq!(points[0][0], (0, Point { x: 100.0, y: 70.0 }));
13486        assert_eq!(points[0][1], (1, Point { x: 100.0, y: 105.0 }));
13487
13488        series[0].values.values = vec![-5.0, 10.0];
13489        let options = super::GeometryOptions {
13490            value: Some(super::AxisMapping {
13491                domain: Domain {
13492                    min: -10.0,
13493                    max: 10.0,
13494                },
13495                orientation: Orientation::MinMax,
13496            }),
13497            ..super::GeometryOptions::default()
13498        };
13499        let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
13500        assert_eq!(points[0][0], (0, Point { x: 100.0, y: 52.5 }));
13501        assert_eq!(points[0][1], (1, Point { x: 100.0, y: 140.0 }));
13502    }
13503
13504    #[test]
13505    fn radar_explicit_bounds_suppress_out_of_range_points_and_labels() {
13506        let radar_plot = format!(
13507            r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:dLbls><q:showVal val="1"/></q:dLbls><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
13508            plot_series(0)
13509        );
13510        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
13511        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13512        let Plot::Radar { series, .. } = plot else {
13513            panic!("expected radar plot");
13514        };
13515        series[0].values.values = vec![25.0, 75.0];
13516        let options = super::GeometryOptions {
13517            value: Some(super::AxisMapping {
13518                domain: Domain {
13519                    min: 50.0,
13520                    max: 100.0,
13521                },
13522                orientation: Orientation::MinMax,
13523            }),
13524            ..super::GeometryOptions::default()
13525        };
13526        let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
13527        assert_eq!(points[0], vec![(1, Point { x: 100.0, y: 105.0 })]);
13528        let anchors =
13529            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
13530        assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 1)]);
13531
13532        let Plot::Radar { series, .. } = plot else {
13533            panic!("expected radar plot");
13534        };
13535        series[0].values.values = vec![75.0, 125.0];
13536        let reversed = super::GeometryOptions {
13537            value: Some(super::AxisMapping {
13538                domain: Domain {
13539                    min: 50.0,
13540                    max: 100.0,
13541                },
13542                orientation: Orientation::MaxMin,
13543            }),
13544            ..super::GeometryOptions::default()
13545        };
13546        let points = super::radar_series_points(series, chart_bounds(), reversed).unwrap();
13547        assert_eq!(points[0], vec![(0, Point { x: 100.0, y: 35.0 })]);
13548        let anchors =
13549            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
13550        assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
13551
13552        let value_axis = chart
13553            .chart
13554            .plot_area
13555            .axes
13556            .iter_mut()
13557            .find(|axis| axis.kind == AxisKind::Value)
13558            .unwrap();
13559        value_axis.scaling.minimum = Some(50.0);
13560        value_axis.scaling.maximum = Some(100.0);
13561        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13562        let Plot::Radar { series, .. } = plot else {
13563            panic!("expected radar plot");
13564        };
13565        series[0].values.values = vec![25.0, 25.0];
13566        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13567        let rendered = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13568        assert!(rendered.children.iter().any(|element| matches!(
13569            element,
13570            PositionedElement::Group(group) if group.clip.is_some() && group.children.is_empty()
13571        )));
13572        assert!(
13573            rendered
13574                .children
13575                .iter()
13576                .any(|element| matches!(element, PositionedElement::Path(_)))
13577        );
13578        assert!(!rendered.children.iter().any(|element| matches!(
13579            element,
13580            PositionedElement::Text(run) if run.text == "25"
13581        )));
13582    }
13583
13584    #[test]
13585    fn inside_and_outside_label_positions_follow_family_geometry() {
13586        let mut horizontal =
13587            CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13588        let plot = &mut horizontal.chart.plot_area.plots_mut().unwrap()[0];
13589        let Plot::Bar { direction, .. } = plot else {
13590            panic!("expected bar plot");
13591        };
13592        *direction = BarDirection::Bar;
13593        let anchors = super::plot_label_anchors(
13594            plot,
13595            chart_bounds(),
13596            DispBlanksAs::Gap,
13597            super::GeometryOptions::default(),
13598        )
13599        .unwrap();
13600        let anchor = anchors[&(0, 0)];
13601        assert_point_near(
13602            anchor.origin(Some(DataLabelPosition::InsideBase)),
13603            Point { x: 5.0, y: 35.0 },
13604        );
13605        assert_point_near(
13606            anchor.origin(Some(DataLabelPosition::InsideEnd)),
13607            Point { x: 95.0, y: 35.0 },
13608        );
13609        assert_point_near(
13610            anchor.origin(Some(DataLabelPosition::OutsideEnd)),
13611            Point { x: 105.0, y: 35.0 },
13612        );
13613
13614        let mut negative =
13615            CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13616        let plot = &mut negative.chart.plot_area.plots_mut().unwrap()[0];
13617        let Plot::Bar { series, .. } = plot else {
13618            panic!("expected bar plot");
13619        };
13620        series[0].values.values = vec![-1.0, -2.0];
13621        let anchors = super::plot_label_anchors(
13622            plot,
13623            chart_bounds(),
13624            DispBlanksAs::Gap,
13625            super::GeometryOptions::default(),
13626        )
13627        .unwrap();
13628        assert_point_near(
13629            anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
13630            Point { x: 50.0, y: 75.0 },
13631        );
13632        let reversed = super::GeometryOptions {
13633            value: Some(super::AxisMapping {
13634                domain: Domain {
13635                    min: -2.0,
13636                    max: 0.0,
13637                },
13638                orientation: Orientation::MaxMin,
13639            }),
13640            ..super::GeometryOptions::default()
13641        };
13642        let anchors =
13643            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
13644        assert_point_near(
13645            anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
13646            Point { x: 50.0, y: 65.0 },
13647        );
13648
13649        let pie = CT_ChartSpace::from_xml(
13650            chart_with_optional_axes(
13651                &remaining_plot_fixtures()
13652                    .into_iter()
13653                    .find(|(kind, _, _)| *kind == "pieChart")
13654                    .unwrap()
13655                    .1,
13656                false,
13657            )
13658            .as_bytes(),
13659        )
13660        .unwrap();
13661        let Plot::Pie { series, .. } = &pie.chart.plot_area.plots().unwrap()[0] else {
13662            panic!("expected pie plot");
13663        };
13664        let pie_slice = &super::pie_slices(30, None, series, chart_bounds()).unwrap()[0];
13665        assert_point_near(
13666            super::LabelAnchor::segment(pie_slice.anchor, pie_slice.base, pie_slice.end)
13667                .origin(Some(DataLabelPosition::OutsideEnd)),
13668            Point { x: 175.0, y: 70.0 },
13669        );
13670        let doughnut_slice = &super::pie_slices(30, Some(60), series, chart_bounds()).unwrap()[0];
13671        assert_point_near(
13672            super::LabelAnchor::segment(
13673                doughnut_slice.anchor,
13674                doughnut_slice.base,
13675                doughnut_slice.end,
13676            )
13677            .origin(Some(DataLabelPosition::InsideBase)),
13678            Point { x: 147.0, y: 70.0 },
13679        );
13680
13681        let short = super::LabelAnchor::segment(
13682            Point { x: 2.0, y: 0.0 },
13683            Point { x: 0.0, y: 0.0 },
13684            Point { x: 4.0, y: 0.0 },
13685        );
13686        assert_eq!(
13687            short.origin(Some(DataLabelPosition::InsideBase)),
13688            Point { x: 2.0, y: 0.0 }
13689        );
13690        assert_eq!(
13691            short.origin(Some(DataLabelPosition::InsideEnd)),
13692            Point { x: 2.0, y: 0.0 }
13693        );
13694        let thin_bounds = Rect {
13695            x: 0.0,
13696            y: 0.0,
13697            width: 80.0,
13698            height: 80.0,
13699        };
13700        let thin = &super::pie_slices(30, Some(90), series, thin_bounds).unwrap()[0];
13701        let thin = super::LabelAnchor::segment(thin.anchor, thin.base, thin.end);
13702        assert_point_near(
13703            thin.origin(Some(DataLabelPosition::InsideBase)),
13704            Point { x: 78.0, y: 40.0 },
13705        );
13706        assert_point_near(
13707            thin.origin(Some(DataLabelPosition::InsideEnd)),
13708            Point { x: 78.0, y: 40.0 },
13709        );
13710    }
13711
13712    #[test]
13713    fn degenerate_bar_and_radar_anchors_preserve_family_direction() {
13714        let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13715        let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
13716        let Plot::Bar { series, .. } = plot else {
13717            panic!("expected bar plot");
13718        };
13719        series[0].values.values = vec![80.0, 125.0];
13720        for (direction, orientation, boundary, outside) in [
13721            (
13722                BarDirection::Column,
13723                Orientation::MinMax,
13724                Point { x: 50.0, y: 140.0 },
13725                Point { x: 50.0, y: 135.0 },
13726            ),
13727            (
13728                BarDirection::Column,
13729                Orientation::MaxMin,
13730                Point { x: 50.0, y: 0.0 },
13731                Point { x: 50.0, y: 5.0 },
13732            ),
13733            (
13734                BarDirection::Bar,
13735                Orientation::MinMax,
13736                Point { x: 0.0, y: 35.0 },
13737                Point { x: 5.0, y: 35.0 },
13738            ),
13739            (
13740                BarDirection::Bar,
13741                Orientation::MaxMin,
13742                Point { x: 200.0, y: 35.0 },
13743                Point { x: 195.0, y: 35.0 },
13744            ),
13745        ] {
13746            let Plot::Bar {
13747                direction: plot_direction,
13748                ..
13749            } = plot
13750            else {
13751                panic!("expected bar plot");
13752            };
13753            *plot_direction = direction;
13754            let options = super::GeometryOptions {
13755                value: Some(super::AxisMapping {
13756                    domain: Domain {
13757                        min: 80.0,
13758                        max: 100.0,
13759                    },
13760                    orientation,
13761                }),
13762                ..super::GeometryOptions::default()
13763            };
13764            let anchors =
13765                super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options)
13766                    .unwrap();
13767            let anchor = anchors[&(0, 0)];
13768            assert_point_near(anchor.center, boundary);
13769            assert_point_near(anchor.base, boundary);
13770            assert_point_near(anchor.end, boundary);
13771            assert_point_near(anchor.origin(Some(DataLabelPosition::InsideBase)), boundary);
13772            assert_point_near(anchor.origin(Some(DataLabelPosition::InsideEnd)), boundary);
13773            assert_point_near(anchor.origin(Some(DataLabelPosition::OutsideEnd)), outside);
13774        }
13775
13776        let radar_plot = format!(
13777            r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
13778            plot_series(0)
13779        );
13780        let mut radar = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
13781        let plot = &mut radar.chart.plot_area.plots_mut().unwrap()[0];
13782        let Plot::Radar { series, .. } = plot else {
13783            panic!("expected radar plot");
13784        };
13785        series[0].values.values = vec![0.0, 0.0, 0.0, 0.0];
13786        let anchors = super::plot_label_anchors(
13787            plot,
13788            chart_bounds(),
13789            DispBlanksAs::Gap,
13790            super::GeometryOptions::default(),
13791        )
13792        .unwrap();
13793        let center = Point { x: 100.0, y: 70.0 };
13794        for logical_index in 0..4 {
13795            assert_point_near(
13796                anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideBase)),
13797                center,
13798            );
13799            assert_point_near(
13800                anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideEnd)),
13801                center,
13802            );
13803        }
13804        assert_point_near(
13805            anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
13806            Point { x: 100.0, y: 65.0 },
13807        );
13808        assert_point_near(
13809            anchors[&(0, 1)].origin(Some(DataLabelPosition::OutsideEnd)),
13810            Point { x: 105.0, y: 70.0 },
13811        );
13812        assert_point_near(
13813            anchors[&(0, 2)].origin(Some(DataLabelPosition::OutsideEnd)),
13814            Point { x: 100.0, y: 75.0 },
13815        );
13816        assert_point_near(
13817            anchors[&(0, 3)].origin(Some(DataLabelPosition::OutsideEnd)),
13818            Point { x: 95.0, y: 70.0 },
13819        );
13820    }
13821
13822    #[test]
13823    fn labels_outside_explicit_axis_bounds_are_suppressed() {
13824        let mut chart =
13825            CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
13826        {
13827            let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13828            let Plot::Line { series, .. } = plot else {
13829                panic!("expected line plot");
13830            };
13831            series[0].values.values = vec![25.0, 75.0];
13832        }
13833        let value_axis = chart
13834            .chart
13835            .plot_area
13836            .axes
13837            .iter_mut()
13838            .find(|axis| axis.kind == AxisKind::Value)
13839            .unwrap();
13840        value_axis.scaling.minimum = Some(50.0);
13841        value_axis.scaling.maximum = Some(100.0);
13842        value_axis.scaling.orientation = Orientation::MaxMin;
13843        let plot = &chart.chart.plot_area.plots().unwrap()[0];
13844        let axes = chart.chart.plot_area.axes().unwrap();
13845        let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
13846        let anchors =
13847            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
13848        assert!(!anchors.contains_key(&(0, 0)));
13849        assert_point_near(anchors[&(0, 1)].center, Point { x: 150.0, y: 70.0 });
13850
13851        let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13852        let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
13853        let Plot::Bar { series, .. } = plot else {
13854            panic!("expected bar plot");
13855        };
13856        series[0].values.values = vec![90.0, 125.0];
13857        let options = super::GeometryOptions {
13858            value: Some(super::AxisMapping {
13859                domain: Domain {
13860                    min: 80.0,
13861                    max: 100.0,
13862                },
13863                orientation: Orientation::MinMax,
13864            }),
13865            ..super::GeometryOptions::default()
13866        };
13867        let anchors =
13868            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
13869        assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
13870        assert_point_near(anchors[&(0, 0)].base, Point { x: 50.0, y: 140.0 });
13871        assert_point_near(anchors[&(0, 0)].end, Point { x: 50.0, y: 70.0 });
13872        assert_point_near(anchors[&(0, 0)].center, Point { x: 50.0, y: 105.0 });
13873        assert_point_near(
13874            anchors[&(0, 0)].origin(Some(DataLabelPosition::InsideBase)),
13875            Point { x: 50.0, y: 135.0 },
13876        );
13877    }
13878
13879    #[test]
13880    fn zero_bars_render_and_emit_requested_value_labels() {
13881        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13882        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13883        let Plot::Bar { series, .. } = plot else {
13884            panic!("expected bar plot");
13885        };
13886        series[0].values.values = vec![0.0, 0.0];
13887        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
13888        assert_eq!(path_commands(&geometry).len(), 2);
13889        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13890        render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13891        let plot = &chart.chart.plot_area.plots().unwrap()[0];
13892        let mut labels = Vec::new();
13893        super::render_data_labels(
13894            plot,
13895            chart_bounds(),
13896            DispBlanksAs::Gap,
13897            super::GeometryOptions::default(),
13898            &mut fonts,
13899            &mut labels,
13900        )
13901        .unwrap();
13902        let text: Vec<_> = labels
13903            .iter()
13904            .filter_map(|element| match element {
13905                PositionedElement::Text(run) => Some(run.text.as_str()),
13906                _ => None,
13907            })
13908            .collect();
13909        assert_eq!(text, vec!["0", "0"]);
13910    }
13911
13912    #[test]
13913    fn axes_gridlines_and_tick_marks_follow_model_state() {
13914        let chart = labelled_bar_chart();
13915        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13916        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13917        let ranks: Vec<_> = group
13918            .children
13919            .iter()
13920            .map(|element| match element {
13921                PositionedElement::Group(group) => {
13922                    assert!(group.clip.is_some());
13923                    1
13924                }
13925                PositionedElement::Path(path)
13926                    if path
13927                        .stroke
13928                        .as_ref()
13929                        .is_some_and(|stroke| stroke.width == 0.5) =>
13930                {
13931                    0
13932                }
13933                PositionedElement::Path(path) if path.fill.is_some() => 4,
13934                PositionedElement::Path(path) => {
13935                    let bounds = path.path.bounds().expect("axis or tick bounds");
13936                    assert_eq!(path.stroke.as_ref().map(|stroke| stroke.width), Some(1.0));
13937                    if bounds.width > 4.0 || bounds.height > 4.0 {
13938                        2
13939                    } else {
13940                        3
13941                    }
13942                }
13943                PositionedElement::Text(_) => 5,
13944                _ => panic!("unexpected labelled chart element"),
13945            })
13946            .collect();
13947        assert!(ranks.windows(2).all(|pair| pair[0] <= pair[1]), "{ranks:?}");
13948        for required in 0..=5 {
13949            assert!(
13950                ranks.contains(&required),
13951                "missing z-order rank {required}: {ranks:?}"
13952            );
13953        }
13954        assert_eq!(ranks.iter().filter(|rank| **rank == 1).count(), 1);
13955        assert_eq!(ranks.iter().filter(|rank| **rank == 2).count(), 2);
13956
13957        let mut deleted = chart.clone();
13958        for axis in &mut deleted.chart.plot_area.axes {
13959            axis.deleted = true;
13960        }
13961        let deleted_group = render_chart(&deleted.chart, chart_bounds(), &mut fonts).unwrap();
13962        assert_eq!(
13963            deleted_group
13964                .children
13965                .iter()
13966                .filter(|element| {
13967                    matches!(
13968                        element,
13969                        PositionedElement::Path(path) if path.stroke.is_some()
13970                    )
13971                })
13972                .count(),
13973            0
13974        );
13975    }
13976
13977    #[test]
13978    fn category_ticks_cover_unlabelled_and_sparse_logical_slots() {
13979        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13980        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
13981            panic!("expected bar plot");
13982        };
13983        series[0].categories = None;
13984        let plot = &chart.chart.plot_area.plots().unwrap()[0];
13985        let mut category_axis = chart
13986            .chart
13987            .plot_area
13988            .axes()
13989            .unwrap()
13990            .into_iter()
13991            .find(|axis| axis.kind == AxisKind::Category)
13992            .unwrap();
13993        category_axis.major_gridlines = Some(super::ChartLines::default());
13994        category_axis.major_tick_mark = TickMark::Cross;
13995        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13996        let annotations = super::render_axes_and_labels(
13997            plot,
13998            &[category_axis],
13999            chart_bounds(),
14000            DispBlanksAs::Gap,
14001            &mut fonts,
14002        )
14003        .unwrap();
14004        assert_eq!(annotations.gridlines.len(), 2);
14005        assert_eq!(annotations.ticks.len(), 2);
14006        assert!(annotations.labels.is_empty());
14007
14008        let sparse_plot = format!(
14009            r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{}<q:gapWidth val="150"/><q:overlap val="0"/><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:barChart>"#,
14010            sparse_category_series()
14011        );
14012        let sparse = CT_ChartSpace::from_xml(chart_with_plot(&sparse_plot).as_bytes()).unwrap();
14013        let plot = &sparse.chart.plot_area.plots().unwrap()[0];
14014        let mut category_axis = sparse
14015            .chart
14016            .plot_area
14017            .axes()
14018            .unwrap()
14019            .into_iter()
14020            .find(|axis| axis.kind == AxisKind::Category)
14021            .unwrap();
14022        category_axis.major_gridlines = Some(super::ChartLines::default());
14023        category_axis.major_tick_mark = TickMark::Cross;
14024        let annotations = super::render_axes_and_labels(
14025            plot,
14026            &[category_axis],
14027            chart_bounds(),
14028            DispBlanksAs::Gap,
14029            &mut fonts,
14030        )
14031        .unwrap();
14032        assert_eq!(annotations.gridlines.len(), 3);
14033        assert_eq!(annotations.ticks.len(), 3);
14034        assert_eq!(annotations.labels.len(), 2);
14035
14036        let oversized_plot = sparse_plot.replacen(
14037            r#"<q:ptCount val="3"/>"#,
14038            r#"<q:ptCount val="4294967295"/>"#,
14039            1,
14040        );
14041        let oversized =
14042            CT_ChartSpace::from_xml(chart_with_plot(&oversized_plot).as_bytes()).unwrap();
14043        let plot = &oversized.chart.plot_area.plots().unwrap()[0];
14044        let category_axis = oversized
14045            .chart
14046            .plot_area
14047            .axes()
14048            .unwrap()
14049            .into_iter()
14050            .find(|axis| axis.kind == AxisKind::Category)
14051            .unwrap();
14052        let result = super::render_axes_and_labels(
14053            plot,
14054            &[category_axis],
14055            chart_bounds(),
14056            DispBlanksAs::Gap,
14057            &mut fonts,
14058        );
14059        let error = match result {
14060            Err(error) => error,
14061            Ok(_) => panic!("oversized category annotations rendered"),
14062        };
14063        assert!(
14064            error
14065                .to_string()
14066                .contains("logical category count 4294967295")
14067        );
14068        assert!(error.to_string().contains("exceeds 16384"));
14069    }
14070
14071    #[test]
14072    fn unsupported_numeric_category_formats_return_projection_errors() {
14073        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14074        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
14075            panic!("expected bar plot");
14076        };
14077        series[0].categories = Some(AxisData::Numeric(
14078            NumericData::new(
14079                "Sheet1!$A$2:$A$3".to_owned(),
14080                "General".to_owned(),
14081                vec![1.0, 2.0],
14082            )
14083            .unwrap(),
14084        ));
14085        let plot = &chart.chart.plot_area.plots().unwrap()[0];
14086        let mut category_axis = chart
14087            .chart
14088            .plot_area
14089            .axes()
14090            .unwrap()
14091            .into_iter()
14092            .find(|axis| axis.kind == AxisKind::Category)
14093            .unwrap();
14094        category_axis.number_format = Some(NumberFormat::new("0.00".to_owned(), false).unwrap());
14095        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14096        let annotations = super::render_axes_and_labels(
14097            plot,
14098            &[category_axis.clone()],
14099            chart_bounds(),
14100            DispBlanksAs::Gap,
14101            &mut fonts,
14102        )
14103        .unwrap();
14104        let text: Vec<_> = annotations
14105            .labels
14106            .iter()
14107            .filter_map(|element| match element {
14108                PositionedElement::Text(run) => Some(run.text.as_str()),
14109                _ => None,
14110            })
14111            .collect();
14112        assert_eq!(text, vec!["1.00", "2.00"]);
14113
14114        category_axis.number_format = Some(NumberFormat::new("#,##0".to_owned(), false).unwrap());
14115        let result = super::render_axes_and_labels(
14116            plot,
14117            &[category_axis],
14118            chart_bounds(),
14119            DispBlanksAs::Gap,
14120            &mut fonts,
14121        );
14122        let error = match result {
14123            Err(error) => error,
14124            Ok(_) => panic!("unsupported category-axis format rendered"),
14125        };
14126        assert!(
14127            error
14128                .to_string()
14129                .contains("c:numFmt/@formatCode projection")
14130        );
14131        assert!(error.to_string().contains("#,##0"));
14132    }
14133
14134    #[test]
14135    fn labels_and_legend_shape_with_deterministic_fonts() {
14136        let chart = labelled_bar_chart();
14137        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14138        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
14139        let labels: Vec<_> = group
14140            .children
14141            .iter()
14142            .filter_map(|element| match element {
14143                PositionedElement::Text(run) => Some(run),
14144                _ => None,
14145            })
14146            .collect();
14147        for text in ["North", "South", "20", "Sales", "100"] {
14148            assert!(labels.iter().any(|run| run.text == text), "missing {text}");
14149        }
14150        let styled_tick = labels.iter().find(|run| run.text == "20").unwrap();
14151        assert_eq!(styled_tick.font_size, 12.0);
14152        assert!(styled_tick.bold);
14153        assert!(
14154            labels
14155                .iter()
14156                .all(|run| !run.glyph_ids.is_empty() && run.glyph_ids.len() == run.advances.len())
14157        );
14158    }
14159
14160    #[test]
14161    fn labelled_chart_raster_is_deterministic() {
14162        let raster = || {
14163            let chart = labelled_bar_chart();
14164            let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14165            let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
14166            let layout = LayoutResult::new(
14167                vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
14168                fonts.all_font_data(),
14169                None,
14170                Vec::new(),
14171            );
14172            oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
14173        };
14174        assert_eq!(raster(), raster());
14175    }
14176
14177    fn labelled_bar_chart() -> CT_ChartSpace {
14178        let series = plot_series(0)
14179            .replace(
14180                "<q:cat>",
14181                "<q:tx><q:strRef><q:f>Sheet1!$B$1</q:f><q:strCache><q:ptCount val=\"1\"/><q:pt idx=\"0\"><q:v>Sales</q:v></q:pt></q:strCache></q:strRef></q:tx><q:cat>",
14182            )
14183            .replace("<q:v>1</q:v>", "<q:v>0</q:v>")
14184            .replace("<q:v>2</q:v>", "<q:v>100</q:v>");
14185        let plot = format!(
14186            r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{series}<q:dLbls><q:numFmt formatCode="0" sourceLinked="0"/><q:showVal val="1"/></q:dLbls><q:gapWidth val="150"/><q:overlap val="0"/><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:barChart>"#
14187        );
14188        let xml = chart_with_plot(&plot)
14189            .replacen(
14190                "<q:scaling/>",
14191                "<q:scaling><q:orientation val=\"minMax\"/></q:scaling>",
14192                1,
14193            )
14194            .replacen(
14195                "<q:scaling/>",
14196                "<q:scaling><q:max val=\"100\"/><q:min val=\"0\"/></q:scaling>",
14197                1,
14198            )
14199            .replace(
14200                "<q:axPos val=\"b\"/>",
14201                "<q:axPos val=\"b\"/><q:majorTickMark val=\"cross\"/>",
14202            )
14203            .replace(
14204                "<q:axPos val=\"l\"/>",
14205                "<q:axPos val=\"l\"/><q:majorGridlines/><q:numFmt formatCode=\"0\" sourceLinked=\"0\"/><q:majorTickMark val=\"out\"/><q:txPr><a:bodyPr/><a:p><a:pPr><a:defRPr sz=\"1200\" b=\"1\"><a:latin typeface=\"Carlito\"/></a:defRPr></a:pPr></a:p></q:txPr>",
14206            )
14207            .replace("</q:plotArea></q:chart>", "</q:plotArea><q:legend/></q:chart>");
14208        CT_ChartSpace::from_xml(xml.as_bytes()).unwrap()
14209    }
14210
14211    fn chart_bounds() -> Rect {
14212        Rect {
14213            x: 0.0,
14214            y: 0.0,
14215            width: 200.0,
14216            height: 140.0,
14217        }
14218    }
14219
14220    fn four_series_bar_chart() -> CT_ChartSpace {
14221        let series = (0..4).map(plot_series).collect::<String>();
14222        let plot = bar_plot("").replace(&plot_series(0), &series);
14223        CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
14224    }
14225
14226    fn seven_series_bar_chart() -> CT_ChartSpace {
14227        let series = (0..7).map(plot_series).collect::<String>();
14228        let plot = bar_plot("").replace(&plot_series(0), &series);
14229        CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
14230    }
14231
14232    fn path_colours(geometry: &ChartGeometry) -> Vec<Color> {
14233        let mut colours = geometry_children(geometry)
14234            .iter()
14235            .filter_map(|element| match element {
14236                PositionedElement::Path(path) => match path
14237                    .fill
14238                    .as_ref()
14239                    .or_else(|| path.stroke.as_ref().map(|stroke| &stroke.paint))
14240                {
14241                    Some(Paint::Solid(colour)) => Some(*colour),
14242                    _ => None,
14243                },
14244                _ => None,
14245            })
14246            .collect::<Vec<_>>();
14247        colours.dedup_by(|left, right| left == right);
14248        colours
14249    }
14250
14251    fn resolved_layout_colour(colour: oxml_drawing::color::ResolvedColor) -> Color {
14252        Color {
14253            r: f64::from(colour.red) / 255.0,
14254            g: f64::from(colour.green) / 255.0,
14255            b: f64::from(colour.blue) / 255.0,
14256            a: f64::from(colour.alpha) / 255.0,
14257        }
14258    }
14259
14260    fn geometry_children(geometry: &ChartGeometry) -> &[PositionedElement] {
14261        let [PositionedElement::Group(group)] = geometry.elements.as_slice() else {
14262            panic!("chart geometry must contain one group");
14263        };
14264        &group.children
14265    }
14266
14267    fn path_commands(geometry: &ChartGeometry) -> Vec<&[PathCommand]> {
14268        geometry_children(geometry)
14269            .iter()
14270            .map(|element| {
14271                let PositionedElement::Path(path) = element else {
14272                    panic!("chart geometry child must be a path");
14273                };
14274                path.path.commands.as_slice()
14275            })
14276            .collect()
14277    }
14278
14279    fn path_bounds(geometry: &ChartGeometry) -> Vec<Rect> {
14280        geometry_children(geometry)
14281            .iter()
14282            .map(|element| {
14283                let PositionedElement::Path(path) = element else {
14284                    panic!("chart geometry child must be a path");
14285                };
14286                path.path.bounds().expect("chart path has bounds")
14287            })
14288            .collect()
14289    }
14290
14291    fn assert_rect_near(actual: Rect, expected: Rect) {
14292        assert_near(actual.x, expected.x);
14293        assert_near(actual.y, expected.y);
14294        assert_near(actual.width, expected.width);
14295        assert_near(actual.height, expected.height);
14296    }
14297
14298    fn assert_rects_near(actual: &[Rect], expected: &[Rect]) {
14299        assert_eq!(actual.len(), expected.len());
14300        for (actual, expected) in actual.iter().copied().zip(expected.iter().copied()) {
14301            assert_rect_near(actual, expected);
14302        }
14303    }
14304
14305    fn assert_path_points(commands: &[PathCommand], expected: &[Point], closed: bool) {
14306        let points: Vec<_> = commands
14307            .iter()
14308            .filter_map(|command| match command {
14309                PathCommand::MoveTo(point) | PathCommand::LineTo(point) => Some(*point),
14310                PathCommand::Close => None,
14311                PathCommand::CurveTo { .. } => panic!("expected polygonal path"),
14312            })
14313            .collect();
14314        assert_eq!(points.len(), expected.len());
14315        for (actual, expected) in points.into_iter().zip(expected.iter().copied()) {
14316            assert_point_near(actual, expected);
14317        }
14318        assert_eq!(commands.last() == Some(&PathCommand::Close), closed);
14319    }
14320
14321    fn assert_point_near(actual: Point, expected: Point) {
14322        assert_near(actual.x, expected.x);
14323        assert_near(actual.y, expected.y);
14324    }
14325
14326    fn assert_near(actual: f64, expected: f64) {
14327        assert!(
14328            (actual - expected).abs() < 1e-9,
14329            "expected {expected}, got {actual}"
14330        );
14331    }
14332
14333    #[test]
14334    fn remaining_v1_plots_round_trip_and_render() {
14335        for (kind, plot, axes) in remaining_plot_fixtures() {
14336            let xml = chart_with_optional_axes(&plot, axes);
14337            let parsed = CT_ChartSpace::from_xml(xml.as_bytes())
14338                .unwrap_or_else(|error| panic!("{kind}: parse failed: {error}"));
14339            assert_eq!(parsed.chart.plot_area.plots().unwrap().len(), 1);
14340            assert_eq!(
14341                parsed.chart.plot_area.axes().unwrap().len(),
14342                usize::from(axes) * 2
14343            );
14344            let written = parsed
14345                .to_xml()
14346                .unwrap_or_else(|error| panic!("{kind}: write failed: {error}"));
14347            assert_eq!(
14348                parsed,
14349                CT_ChartSpace::from_xml(&written)
14350                    .unwrap_or_else(|error| panic!("{kind}: reparse failed: {error}"))
14351            );
14352        }
14353        if let Some(corpus) = require_or_skip_corpus() {
14354            verify_remaining_plot_viewer_gate(&corpus);
14355        }
14356    }
14357
14358    #[test]
14359    fn remaining_plot_families_write_fixed_prefixes_in_schema_order() {
14360        for (kind, plot, axes) in remaining_plot_fixtures() {
14361            let parsed =
14362                CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
14363            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14364            assert!(written.contains(&format!("<c:{kind}")));
14365            assert!(!written.contains(&format!("<q:{kind}")));
14366            let series = written.find("<c:ser").unwrap();
14367            let labels = written.find("<c:dLbls").unwrap();
14368            assert!(series < labels, "{kind}: series must precede labels");
14369            if matches!(kind, "areaChart" | "scatterChart" | "radarChart") {
14370                let property = match kind {
14371                    "areaChart" => "<c:grouping",
14372                    "scatterChart" => "<c:scatterStyle",
14373                    "radarChart" => "<c:radarStyle",
14374                    _ => unreachable!(),
14375                };
14376                assert!(written.find(property).unwrap() < series);
14377                assert!(labels < written.find("<c:axId").unwrap());
14378            } else {
14379                assert!(labels < written.find("<c:firstSliceAng").unwrap());
14380            }
14381            if kind == "doughnutChart" {
14382                assert!(
14383                    written.find("<c:firstSliceAng").unwrap()
14384                        < written.find("<c:holeSize").unwrap()
14385                );
14386            }
14387        }
14388    }
14389
14390    #[test]
14391    fn scatter_series_map_numeric_categories_and_values_to_x_and_y() {
14392        let plot = remaining_plot_fixtures()
14393            .into_iter()
14394            .find(|(kind, _, _)| *kind == "scatterChart")
14395            .unwrap()
14396            .1;
14397        let mut parsed =
14398            CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, true).as_bytes()).unwrap();
14399        let standalone_series = {
14400            let Plot::Scatter { series, .. } = &mut parsed.chart.plot_area.plots_mut().unwrap()[0]
14401            else {
14402                panic!("expected scatter plot");
14403            };
14404            let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
14405                panic!("expected numeric x values");
14406            };
14407            x_values.values[0] = 3.5;
14408            series[0].values.values[0] = 7.5;
14409            series[0].clone()
14410        };
14411        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14412        assert!(written.contains("<c:xVal>"));
14413        assert!(written.contains("<c:yVal>"));
14414        assert!(!written.contains("<c:cat>"));
14415        assert!(!written.contains("<c:val>"));
14416        assert!(written.contains("<c:v>3.5</c:v>"));
14417        assert!(written.contains("<c:v>7.5</c:v>"));
14418        let standalone = String::from_utf8(standalone_series.to_xml().unwrap()).unwrap();
14419        assert!(standalone.contains("<c:xVal>"));
14420        assert!(standalone.contains("<c:yVal>"));
14421        assert!(!standalone.contains("<c:cat>"));
14422        assert!(!standalone.contains("<c:val>"));
14423    }
14424
14425    #[test]
14426    fn malformed_remaining_plots_return_errors_without_panicking() {
14427        let series = plot_series(0);
14428        let scatter = numeric_plot_series(0);
14429        let opaque_bubble = series.replace(
14430            "</q:val></q:ser>",
14431            "</q:val><q:bubbleSize><x:opaque/></q:bubbleSize></q:ser>",
14432        );
14433        let cases = [
14434            r#"<q:pieChart/>"#.to_owned(),
14435            format!(r#"<q:pieChart>{series}<q:firstSliceAng val="361"/></q:pieChart>"#),
14436            format!(
14437                r#"<q:pieChart>{series}<q:firstSliceAng val="1"/><q:firstSliceAng val="2"/></q:pieChart>"#
14438            ),
14439            format!(r#"<q:doughnutChart>{series}<q:holeSize val="9"/></q:doughnutChart>"#),
14440            format!(
14441                r#"<q:doughnutChart>{series}<q:holeSize val="50"/><q:holeSize val="60"/></q:doughnutChart>"#
14442            ),
14443            format!(
14444                r#"<q:areaChart><q:grouping val="clustered"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
14445            ),
14446            format!(
14447                r#"<q:areaChart><q:grouping val="standard"/><q:grouping val="stacked"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
14448            ),
14449            format!(
14450                r#"<q:scatterChart><q:scatterStyle val="dots"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
14451            ),
14452            format!(
14453                r#"<q:scatterChart><q:scatterStyle val="marker"/><q:scatterStyle val="line"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
14454            ),
14455            format!(
14456                r#"<q:radarChart><q:radarStyle val="smooth"/>{series}<q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
14457            ),
14458            format!(
14459                r#"<q:radarChart><q:radarStyle val="marker"/>{series}<q:dLbls/><q:dLbls/><q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
14460            ),
14461            format!(r#"<q:pieChart>{series}<q:axId val="1"/></q:pieChart>"#),
14462            format!(
14463                r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/></q:areaChart>"#
14464            ),
14465            r#"<q:areaChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:areaChart>"#.to_owned(),
14466            format!(
14467                r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/><q:axId val="2"/><q:axId val="3"/></q:areaChart>"#
14468            ),
14469            format!(
14470                r#"<q:scatterChart><q:scatterStyle val="marker"/>{series}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
14471            ),
14472            format!(
14473                r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
14474                scatter
14475                    .replace("<q:yVal>", "<q:val>")
14476                    .replace("</q:yVal>", "</q:val>")
14477            ),
14478            format!(
14479                r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
14480                scatter_without_x(0)
14481            ),
14482            format!(
14483                r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
14484                scatter_without_y(0)
14485            ),
14486            format!(r#"<q:pieChart>{opaque_bubble}</q:pieChart>"#),
14487        ];
14488        for plot in cases {
14489            let xml = chart_with_optional_axes(&plot, false);
14490            let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
14491            assert!(result.is_ok(), "parser panicked for {plot}");
14492            assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
14493        }
14494
14495        let missing_axis = format!(
14496            r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="7"/></q:areaChart>"#
14497        );
14498        let missing_axis =
14499            CT_ChartSpace::from_xml(chart_with_optional_axes(&missing_axis, true).as_bytes())
14500                .unwrap_err();
14501        assert!(missing_axis.to_string().contains("missing axis 7"));
14502
14503        let values =
14504            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
14505        let mut bubble_series = Series::new(0, 0, values.clone());
14506        bubble_series.bubble_size = Some(values);
14507        assert!(Plot::pie(vec![bubble_series]).is_err());
14508    }
14509
14510    #[test]
14511    fn unsupported_plot_families_and_children_remain_byte_preserved() {
14512        for raw in [
14513            r#"<q:pie3DChart x:keep="3d"><q:ser><x:opaque/></q:ser></q:pie3DChart>"#,
14514            r#"<q:ofPieChart x:keep="of"><x:opaque/></q:ofPieChart>"#,
14515            r#"<q:stockChart x:keep="stock"><x:opaque/></q:stockChart>"#,
14516            r#"<q:surfaceChart x:keep="surface"><x:opaque/></q:surfaceChart>"#,
14517            r#"<q:bubbleChart x:keep="bubble"><x:opaque/></q:bubbleChart>"#,
14518        ] {
14519            let parsed =
14520                CT_ChartSpace::from_xml(chart_with_optional_axes(raw, false).as_bytes()).unwrap();
14521            assert!(parsed.chart.plot_area.plots().is_err());
14522            let written = parsed.to_xml().unwrap();
14523            assert!(
14524                written
14525                    .windows(raw.len())
14526                    .any(|window| window == raw.as_bytes())
14527            );
14528        }
14529
14530        let pie = remaining_plot_fixtures()[0].1.clone();
14531        let preserved = r#"<!--point--><q:dPt x:id="1"><q:explosion val="7"/><x:tail/></q:dPt>"#;
14532        let plot = pie.replace("<q:dLbls>", &format!("{preserved}<q:dLbls>"));
14533        let parsed =
14534            CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, false).as_bytes()).unwrap();
14535        let written = parsed.to_xml().unwrap();
14536        assert!(
14537            written
14538                .windows(preserved.len())
14539                .any(|window| window == preserved.as_bytes())
14540        );
14541
14542        for (kind, mut plot, axes) in [
14543            ("pieChart", remaining_plot_fixtures()[0].1.clone(), false),
14544            ("areaChart", remaining_plot_fixtures()[2].1.clone(), true),
14545        ] {
14546            plot = plot.replace(r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#, "");
14547            if kind == "pieChart" {
14548                plot = plot.replace(
14549                    "</q:extLst></q:pieChart>",
14550                    "</q:extLst><!--after-ext--></q:pieChart>",
14551                );
14552            } else {
14553                plot = plot.replace(
14554                    "</q:dropLines><q:axId",
14555                    "</q:dropLines><!--after-drop-lines--><q:axId",
14556                );
14557            }
14558            let mut parsed =
14559                CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
14560            let labels =
14561                CT_DLbls::from_xml(format!(r#"<c:dLbls xmlns:c="{C_NS}"/>"#).as_bytes()).unwrap();
14562            match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
14563                Plot::Pie { data_labels, .. } | Plot::Area { data_labels, .. } => {
14564                    *data_labels = Some(labels)
14565                }
14566                _ => panic!("expected pie or area"),
14567            }
14568            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14569            assert!(
14570                written.find("<c:dLbls").unwrap() < written.find("<q:extLst").unwrap(),
14571                "{kind}: inserted labels must precede the preserved extension"
14572            );
14573            if kind == "areaChart" {
14574                assert!(written.find("<c:dLbls").unwrap() < written.find("<q:dropLines").unwrap());
14575                assert!(
14576                    written.find("<q:dropLines").unwrap()
14577                        < written.find("<!--after-drop-lines-->").unwrap()
14578                );
14579            } else {
14580                assert!(
14581                    written.find("<q:extLst").unwrap() < written.find("<!--after-ext-->").unwrap()
14582                );
14583            }
14584        }
14585    }
14586
14587    #[test]
14588    fn every_supported_corpus_plot_round_trips_structurally() {
14589        let Some(corpus) = require_or_skip_corpus() else {
14590            return;
14591        };
14592        verify_fetched_corpus(&corpus);
14593        let mut pie_count = 0usize;
14594        let mut bar_count = 0usize;
14595        let mut line_count = 0usize;
14596        for path in manifest_paths() {
14597            let package = OpcPackage::open(corpus.join(path)).unwrap();
14598            for (part, xml) in &package.parts {
14599                if !is_chart_part(part) {
14600                    continue;
14601                }
14602                let parsed = CT_ChartSpace::from_xml(xml).unwrap();
14603                if let Ok(plots) = parsed.chart.plot_area.plots() {
14604                    for plot in plots {
14605                        match plot {
14606                            Plot::Pie { .. } => pie_count += 1,
14607                            Plot::Bar { .. } => bar_count += 1,
14608                            Plot::Line { .. } => line_count += 1,
14609                            _ => {}
14610                        }
14611                    }
14612                }
14613                let written = parsed.to_xml().unwrap();
14614                assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
14615            }
14616        }
14617        assert_eq!(pie_count, 1, "typed corpus pie coverage changed");
14618        assert_eq!(bar_count, 11, "typed corpus bar coverage changed");
14619        assert_eq!(line_count, 2, "typed corpus line coverage changed");
14620    }
14621
14622    fn standalone_axis(local: &str, children: &str) -> String {
14623        format!(
14624            r#"<c:{local} xmlns:c="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}">{children}</c:{local}>"#
14625        )
14626    }
14627
14628    fn minimal_axis(local: &str, id: &str, position: &str, cross_axis: &str) -> String {
14629        format!(
14630            r#"<c:{local}><c:axId val="{id}"/><c:scaling/><c:axPos val="{position}"/><c:crossAx val="{cross_axis}"/></c:{local}>"#
14631        )
14632    }
14633
14634    fn chart_with_axes(axes: &[String]) -> String {
14635        format!(
14636            r#"<c:chartSpace xmlns:c="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}"><c:chart><c:plotArea>{}</c:plotArea></c:chart></c:chartSpace>"#,
14637            axes.join("")
14638        )
14639    }
14640
14641    #[test]
14642    fn data_labels_write_fixed_prefixes_in_schema_order() {
14643        let xml = format!(
14644            r#"<q:dLbls xmlns:q="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}" xmlns:x="urn:producer"><q:dLbl><q:idx val="0"/><x:point/></q:dLbl><q:numFmt formatCode="0.0%" sourceLinked="0"/><q:spPr><x:shape/></q:spPr><q:txPr><x:text/></q:txPr><q:dLblPos val="outEnd"/><q:showLegendKey val="1"/><q:showVal val="true"/><q:showCatName val="0"/><q:showSerName val="1"/><q:showPercent val="0"/><q:showBubbleSize val="1"/><q:separator> / </q:separator><q:leaderLines><x:line/></q:leaderLines><q:extLst><q:ext uri="labels"><x:tail/></q:ext></q:extLst></q:dLbls>"#
14645        );
14646        let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
14647        assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0%");
14648        assert_eq!(parsed.position, Some(DataLabelPosition::OutsideEnd));
14649        assert_eq!(parsed.separator.as_deref(), Some(" / "));
14650        assert!(parsed.show_legend_key);
14651        assert!(parsed.show_value);
14652        assert!(!parsed.show_category_name);
14653        assert!(parsed.show_series_name);
14654        assert!(!parsed.show_percent);
14655        assert!(parsed.show_bubble_size);
14656
14657        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14658        assert!(written.starts_with(&format!(
14659            "<c:dLbls xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
14660        )));
14661        let mut cursor = 0usize;
14662        for tag in [
14663            "<q:dLbl",
14664            "<c:numFmt",
14665            "<q:spPr",
14666            "<q:txPr",
14667            "<c:dLblPos",
14668            "<c:showLegendKey",
14669            "<c:showVal",
14670            "<c:showCatName",
14671            "<c:showSerName",
14672            "<c:showPercent",
14673            "<c:showBubbleSize",
14674            "<c:separator",
14675            "<q:leaderLines",
14676            "<q:extLst",
14677        ] {
14678            let position = written[cursor..]
14679                .find(tag)
14680                .unwrap_or_else(|| panic!("missing ordered data-label tag {tag}"));
14681            cursor += position + tag.len();
14682        }
14683        assert_eq!(parsed, CT_DLbls::from_xml(written.as_bytes()).unwrap());
14684    }
14685
14686    #[test]
14687    fn percentage_formatted_label_renders_with_correct_text() {
14688        let Some(corpus) = require_or_skip_corpus() else {
14689            return;
14690        };
14691        verify_fetched_corpus(&corpus);
14692        assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
14693        assert_command_version("pdftotext", &["-v"], PDFTOTEXT_VERSION);
14694
14695        let source = corpus.join("bar-chart.pptx");
14696        let mut package = OpcPackage::open(&source).expect("open percentage-label source deck");
14697        let chart_part = "/ppt/charts/chart1.xml";
14698        let original_parts = package.parts.clone();
14699        let chart_xml = package
14700            .get_part(chart_part)
14701            .expect("bar chart part")
14702            .to_vec();
14703        let original_series = first_series_xml(&chart_xml).expect("bar chart series");
14704        let mut series = Series::from_xml(&original_series).expect("parse bar chart series");
14705        series.values.values.fill(0.25);
14706        let labels = CT_DLbls {
14707            number_format: Some(NumberFormat::new("0%".to_owned(), false).unwrap()),
14708            position: Some(DataLabelPosition::OutsideEnd),
14709            show_value: true,
14710            ..CT_DLbls::default()
14711        };
14712        series.data_labels = Some(labels);
14713        let replacement = series.to_xml().expect("serialize typed percentage labels");
14714        let candidate_chart = replace_once(&chart_xml, &original_series, &replacement);
14715        package.set_part(chart_part, candidate_chart);
14716        for (part, bytes) in &original_parts {
14717            if part == chart_part {
14718                assert_ne!(package.parts[part], *bytes);
14719            } else {
14720                assert_eq!(
14721                    package.parts[part], *bytes,
14722                    "unexpected changed part {part}"
14723                );
14724            }
14725        }
14726
14727        let temp_root =
14728            std::env::temp_dir().join(format!("oxml-chart-f123-label-gate-{}", std::process::id()));
14729        let profile = std::env::temp_dir().join(format!(
14730            "oxml-chart-f123-label-profile-{}",
14731            std::process::id()
14732        ));
14733        if temp_root.exists() {
14734            fs::remove_dir_all(&temp_root).expect("remove stale F-123 evidence");
14735        }
14736        if profile.exists() {
14737            fs::remove_dir_all(&profile).expect("remove stale F-123 profile");
14738        }
14739        fs::create_dir_all(&temp_root).expect("create F-123 evidence root");
14740        let unbound_candidate = temp_root.join("f123-percentage-label.pptx");
14741        package
14742            .save(&unbound_candidate)
14743            .expect("save percentage-label candidate");
14744        let candidate_sha = sha256(&unbound_candidate);
14745        let evidence_dir = temp_root.join(&candidate_sha);
14746        fs::create_dir(&evidence_dir).expect("create SHA-bound evidence directory");
14747        let candidate = evidence_dir.join("f123-percentage-label.pptx");
14748        fs::rename(&unbound_candidate, &candidate).expect("bind candidate to evidence SHA");
14749        assert_eq!(sha256(&candidate), candidate_sha);
14750
14751        let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
14752        let conversion = Command::new("soffice")
14753            .args([
14754                "--headless",
14755                &profile_argument,
14756                "--convert-to",
14757                "pdf:impress_pdf_Export",
14758                "--outdir",
14759            ])
14760            .arg(&evidence_dir)
14761            .arg(&candidate)
14762            .output()
14763            .expect("run pinned LibreOffice percentage-label gate");
14764        assert!(
14765            conversion.status.success(),
14766            "LibreOffice conversion failed: {}",
14767            String::from_utf8_lossy(&conversion.stderr)
14768        );
14769        let pdf = evidence_dir.join("f123-percentage-label.pdf");
14770        assert!(
14771            pdf.is_file(),
14772            "LibreOffice did not create {}",
14773            pdf.display()
14774        );
14775        let text_path = evidence_dir.join("f123-percentage-label.txt");
14776        let extraction = Command::new("pdftotext")
14777            .arg(&pdf)
14778            .arg(&text_path)
14779            .output()
14780            .expect("run pinned Poppler percentage-label extraction");
14781        assert!(
14782            extraction.status.success(),
14783            "pdftotext failed: {}",
14784            String::from_utf8_lossy(&extraction.stderr)
14785        );
14786        let extracted = fs::read_to_string(&text_path).expect("read percentage-label text");
14787        assert!(
14788            extracted.contains("25%"),
14789            "SHA-bound viewer text lacks 25%: {extracted:?}"
14790        );
14791        for artifact in [&candidate, &pdf, &text_path] {
14792            assert_eq!(
14793                artifact.parent(),
14794                Some(evidence_dir.as_path()),
14795                "evidence escaped candidate SHA"
14796            );
14797        }
14798        eprintln!("F-123 candidate/render/text evidence SHA-256 {candidate_sha}, extracted 25%");
14799        fs::remove_dir_all(&temp_root).expect("remove F-123 evidence");
14800        if profile.exists() {
14801            fs::remove_dir_all(&profile).expect("remove F-123 profile");
14802        }
14803    }
14804
14805    #[test]
14806    fn common_number_formats_project_cached_values_deterministically() {
14807        for (code, value, expected) in [
14808            ("General", 0.25, "0.25"),
14809            ("0", 12.6, "13"),
14810            ("0.0", 12.64, "12.6"),
14811            ("0.00", -12.645, "-12.64"),
14812            ("0%", 0.25, "25%"),
14813            ("0.0%", 0.256, "25.6%"),
14814            ("0.00%", 0.256, "25.60%"),
14815        ] {
14816            let format = NumberFormat::new(code.to_owned(), false).unwrap();
14817            assert_eq!(format.format_value(value).unwrap(), expected, "{code}");
14818        }
14819    }
14820
14821    #[test]
14822    fn malformed_data_labels_and_number_formats_return_errors_without_panicking() {
14823        let cases = [
14824            format!(
14825                r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="1"/><c:showVal val="0"/></c:dLbls>"#
14826            ),
14827            format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="yes"/></c:dLbls>"#),
14828            format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:dLblPos val="middle"/></c:dLbls>"#),
14829            format!(
14830                r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="" sourceLinked="1"/></c:dLbls>"#
14831            ),
14832            format!(
14833                r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="0" sourceLinked="maybe"/></c:dLbls>"#
14834            ),
14835        ];
14836        for xml in cases {
14837            let result = std::panic::catch_unwind(|| CT_DLbls::from_xml(xml.as_bytes()));
14838            assert!(result.is_ok(), "data-label parser panicked for {xml}");
14839            assert!(result.unwrap().is_err(), "malformed labels parsed: {xml}");
14840        }
14841
14842        let invalid_code = NumberFormat::new("bad\u{0}code".to_owned(), false);
14843        assert!(invalid_code.is_err());
14844        let unsupported = NumberFormat::new("#,##0".to_owned(), false).unwrap();
14845        assert!(unsupported.format_value(12.0).is_err());
14846        let general = NumberFormat::new("General".to_owned(), false).unwrap();
14847        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
14848            assert!(general.format_value(value).is_err());
14849        }
14850
14851        let invalid_separator = CT_DLbls {
14852            separator: Some("bad\u{0}separator".to_owned()),
14853            ..CT_DLbls::default()
14854        };
14855        assert!(invalid_separator.to_xml().is_err());
14856    }
14857
14858    #[test]
14859    fn data_labels_preserve_point_overrides_and_extensions_byte_for_byte() {
14860        let xml = format!(
14861            r#"<q:dLbls xmlns:q="{C_NS}" xmlns:x="urn:producer" x:keep="labels"><!--before--><q:dLbl x:keep="point"><q:idx val="0"/><q:spPr><x:shape/></q:spPr><q:txPr><x:text/></q:txPr><q:extLst><q:ext uri="point"><x:point/></q:ext></q:extLst></q:dLbl><?after-point?><q:numFmt formatCode="0%" sourceLinked="0"/><q:spPr><x:collection-shape/></q:spPr><q:txPr><x:collection-text/></q:txPr><q:showVal val="1"/><q:separator><![CDATA[ | ]]></q:separator><q:showLeaderLines val="1"/><q:leaderLines><q:spPr><x:leader-shape/></q:spPr></q:leaderLines><q:extLst><q:ext uri="collection"><x:tail/></q:ext></q:extLst><!--after--></q:dLbls>"#
14862        );
14863        let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
14864        let written = parsed.to_xml().unwrap();
14865        for raw in [
14866            br#"<!--before-->"#.as_slice(),
14867            br#"<q:dLbl x:keep="point"><q:idx val="0"/><q:spPr><x:shape/></q:spPr><q:txPr><x:text/></q:txPr><q:extLst><q:ext uri="point"><x:point/></q:ext></q:extLst></q:dLbl>"#.as_slice(),
14868            br#"<?after-point?>"#.as_slice(),
14869            br#"<q:spPr><x:collection-shape/></q:spPr>"#.as_slice(),
14870            br#"<q:txPr><x:collection-text/></q:txPr>"#.as_slice(),
14871            br#"<q:showLeaderLines val="1"/>"#.as_slice(),
14872            br#"<q:leaderLines><q:spPr><x:leader-shape/></q:spPr></q:leaderLines>"#.as_slice(),
14873            br#"<q:extLst><q:ext uri="collection"><x:tail/></q:ext></q:extLst>"#.as_slice(),
14874            br#"<!--after-->"#.as_slice(),
14875        ] {
14876            assert!(
14877                written.windows(raw.len()).any(|window| window == raw),
14878                "preserved data-label bytes changed: {}",
14879                String::from_utf8_lossy(raw)
14880            );
14881        }
14882        assert_eq!(parsed, CT_DLbls::from_xml(&written).unwrap());
14883    }
14884
14885    #[test]
14886    fn every_corpus_data_label_collection_round_trips_structurally() {
14887        let Some(corpus) = require_or_skip_corpus() else {
14888            return;
14889        };
14890        verify_fetched_corpus(&corpus);
14891        let mut label_collections = 0usize;
14892        let mut axis_number_formats = 0usize;
14893        for path in manifest_paths() {
14894            let package = OpcPackage::open(corpus.join(path))
14895                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14896            for (part, xml) in &package.parts {
14897                if !is_chart_part(part) {
14898                    continue;
14899                }
14900                let chart = CT_ChartSpace::from_xml(xml)
14901                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14902                for series in chart.chart.plot_area.series().unwrap_or_else(|error| {
14903                    panic!("{path} {part}: series projection failed: {error}")
14904                }) {
14905                    if let Some(labels) = series.data_labels {
14906                        let written = labels.to_xml().unwrap_or_else(|error| {
14907                            panic!("{path} {part}: label write failed: {error}")
14908                        });
14909                        let reparsed = CT_DLbls::from_xml(&written).unwrap_or_else(|error| {
14910                            panic!("{path} {part}: written labels parse failed: {error}")
14911                        });
14912                        assert_eq!(labels, reparsed, "{path} {part}: labels changed");
14913                        label_collections += 1;
14914                    }
14915                }
14916                for axis in chart.chart.plot_area.axes().unwrap_or_else(|error| {
14917                    panic!("{path} {part}: axis projection failed: {error}")
14918                }) {
14919                    if axis.number_format.is_some() {
14920                        let written = axis.to_xml().unwrap_or_else(|error| {
14921                            panic!("{path} {part}: axis write failed: {error}")
14922                        });
14923                        let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
14924                            panic!("{path} {part}: written axis parse failed: {error}")
14925                        });
14926                        assert_eq!(axis, reparsed, "{path} {part}: axis changed");
14927                        axis_number_formats += 1;
14928                    }
14929                }
14930            }
14931        }
14932        assert!(label_collections > 0, "the pinned corpus has no c:dLbls");
14933        assert!(
14934            axis_number_formats > 0,
14935            "the pinned corpus has no axis c:numFmt"
14936        );
14937        eprintln!(
14938            "ChartML data-label corpus gate checked {label_collections} collections and {axis_number_formats} axis number formats"
14939        );
14940    }
14941
14942    fn full_axis(local: &str, position: &str) -> String {
14943        format!(
14944            r#"<q:{local} xmlns:q="{C_NS}" xmlns:d="{A_NS}" xmlns:r="{R_NS}" xmlns:x="urn:producer"><q:axId val="-1125255920"/><q:scaling><q:logBase val="10"/><q:orientation val="maxMin"/><q:max val="100"/><q:min val="-10"/></q:scaling><q:delete val="1"/><q:axPos val="{position}"/><q:majorGridlines><q:spPr><d:ln/></q:spPr></q:majorGridlines><q:minorGridlines/><q:title><x:title/></q:title><q:numFmt formatCode="0.0" sourceLinked="0"/><q:majorTickMark val="cross"/><q:minorTickMark val="in"/><q:tickLblPos val="high"/><q:spPr><d:solidFill><d:srgbClr val="112233"/></d:solidFill></q:spPr><q:txPr><d:bodyPr/><d:p/></q:txPr><q:crossAx val="-1004303696"/></q:{local}>"#
14945        )
14946    }
14947
14948    #[test]
14949    fn axis_id_pairs_are_reciprocal() {
14950        assert_eq!(
14951            AxisId::new(i64::from(i32::MIN)).unwrap().value(),
14952            i64::from(i32::MIN)
14953        );
14954        assert_eq!(
14955            AxisId::new(i64::from(u32::MAX)).unwrap().value(),
14956            i64::from(u32::MAX)
14957        );
14958        assert!(AxisId::new(i64::from(i32::MIN) - 1).is_err());
14959        assert!(AxisId::new(i64::from(u32::MAX) + 1).is_err());
14960
14961        let valid = chart_with_axes(&[
14962            minimal_axis("catAx", "-1884094432", "b", "-1884097184"),
14963            minimal_axis("valAx", "-1884097184", "l", "-1884094432"),
14964        ]);
14965        let axes = CT_ChartSpace::from_xml(valid.as_bytes())
14966            .unwrap()
14967            .chart
14968            .plot_area
14969            .axes()
14970            .unwrap();
14971        assert_eq!(axes.len(), 2);
14972        assert_eq!(axes[0].id.value(), -1_884_094_432);
14973        assert_eq!(axes[0].cross_axis, axes[1].id);
14974        assert_eq!(axes[1].cross_axis, axes[0].id);
14975
14976        let invalid_sets = [
14977            vec![
14978                minimal_axis("catAx", "1", "b", "2"),
14979                minimal_axis("valAx", "1", "l", "2"),
14980            ],
14981            vec![minimal_axis("catAx", "1", "b", "1")],
14982            vec![minimal_axis("catAx", "1", "b", "2")],
14983            vec![
14984                minimal_axis("catAx", "1", "b", "2"),
14985                minimal_axis("valAx", "2", "l", "3"),
14986                minimal_axis("serAx", "3", "r", "2"),
14987            ],
14988        ];
14989        for axes in invalid_sets {
14990            let chart = CT_ChartSpace::from_xml(chart_with_axes(&axes).as_bytes()).unwrap();
14991            assert!(chart.chart.plot_area.axes().is_err());
14992        }
14993
14994        let empty = CT_ChartSpace::from_xml(chart_with_axes(&[]).as_bytes()).unwrap();
14995        assert!(empty.chart.plot_area.axes().unwrap().is_empty());
14996    }
14997
14998    #[test]
14999    fn all_axis_forms_write_fixed_prefixes_in_schema_order() {
15000        for (local, kind, position) in [
15001            ("catAx", AxisKind::Category, AxisPosition::Bottom),
15002            ("valAx", AxisKind::Value, AxisPosition::Left),
15003            ("dateAx", AxisKind::Date, AxisPosition::Top),
15004            ("serAx", AxisKind::Series, AxisPosition::Right),
15005        ] {
15006            let xml = full_axis(local, position.as_str());
15007            let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
15008            assert_eq!(parsed.kind, kind);
15009            assert_eq!(parsed.id.value(), -1_125_255_920);
15010            assert_eq!(parsed.scaling.log_base, Some(10.0));
15011            assert_eq!(parsed.scaling.orientation, Orientation::MaxMin);
15012            assert_eq!(parsed.scaling.maximum, Some(100.0));
15013            assert_eq!(parsed.scaling.minimum, Some(-10.0));
15014            assert!(parsed.deleted);
15015            assert_eq!(parsed.position, position);
15016            assert!(parsed.major_gridlines.is_some());
15017            assert!(parsed.minor_gridlines.is_some());
15018            assert!(parsed.title.is_some());
15019            assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0");
15020            assert_eq!(parsed.major_tick_mark, TickMark::Cross);
15021            assert_eq!(parsed.minor_tick_mark, TickMark::Inside);
15022            assert_eq!(parsed.tick_label_position, TickLabelPosition::High);
15023            assert!(parsed.sp_pr.is_some());
15024            assert!(parsed.tx_pr.is_some());
15025            assert_eq!(parsed.cross_axis.value(), -1_004_303_696);
15026
15027            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15028            assert!(written.starts_with(&format!(
15029                "<c:{local} xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
15030            )));
15031            assert!(written.contains("<c:axId val=\"-1125255920\""));
15032            let mut cursor = 0usize;
15033            for tag in [
15034                "<c:axId",
15035                "<c:scaling",
15036                "<c:delete",
15037                "<c:axPos",
15038                "<c:majorGridlines",
15039                "<c:minorGridlines",
15040                "<c:title",
15041                "<c:numFmt",
15042                "<c:majorTickMark",
15043                "<c:minorTickMark",
15044                "<c:tickLblPos",
15045                "<c:spPr",
15046                "<c:txPr",
15047                "<c:crossAx",
15048            ] {
15049                let position = written[cursor..]
15050                    .find(tag)
15051                    .unwrap_or_else(|| panic!("missing ordered axis tag {tag}"));
15052                cursor += position + tag.len();
15053            }
15054            assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
15055        }
15056
15057        let mut constructed = Axis::new(
15058            AxisKind::Value,
15059            AxisId::new(1).unwrap(),
15060            AxisPosition::Left,
15061            AxisId::new(2).unwrap(),
15062        );
15063        assert_eq!(
15064            constructed,
15065            Axis::from_xml(&constructed.to_xml().unwrap()).unwrap()
15066        );
15067        constructed.kind = AxisKind::Category;
15068        constructed.id = AxisId::new(3).unwrap();
15069        constructed.cross_axis = AxisId::new(4).unwrap();
15070        constructed.scaling.minimum = Some(0.0);
15071        constructed.scaling.maximum = Some(10.0);
15072        let written = constructed.to_xml().unwrap();
15073        assert!(written.starts_with(b"<c:catAx"));
15074        assert_eq!(constructed, Axis::from_xml(&written).unwrap());
15075    }
15076
15077    #[test]
15078    fn axis_equality_normalizes_ids_without_losing_lexical_preservation() {
15079        let padded = Axis::from_xml(
15080            standalone_axis(
15081                "catAx",
15082                "<c:axId val=\"01\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"002\"/>",
15083            )
15084            .as_bytes(),
15085        )
15086        .unwrap();
15087        let normalized = Axis::from_xml(
15088            standalone_axis(
15089                "catAx",
15090                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15091            )
15092            .as_bytes(),
15093        )
15094        .unwrap();
15095        assert_eq!(padded, normalized);
15096
15097        let preserved = String::from_utf8(padded.to_xml().unwrap()).unwrap();
15098        assert!(preserved.contains("<c:axId val=\"01\""));
15099        assert!(preserved.contains("<c:crossAx val=\"002\""));
15100
15101        let mut mutated = padded;
15102        mutated.id = AxisId::new(3).unwrap();
15103        mutated.cross_axis = AxisId::new(4).unwrap();
15104        let rewritten = String::from_utf8(mutated.to_xml().unwrap()).unwrap();
15105        assert!(rewritten.contains("<c:axId val=\"3\""));
15106        assert!(rewritten.contains("<c:crossAx val=\"4\""));
15107        assert!(!rewritten.contains("val=\"01\""));
15108        assert!(!rewritten.contains("val=\"002\""));
15109    }
15110
15111    #[test]
15112    fn malformed_axis_values_return_errors_without_panicking() {
15113        let cases = [
15114            standalone_axis(
15115                "catAx",
15116                "<c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15117            ),
15118            standalone_axis(
15119                "catAx",
15120                "<c:axId val=\"1\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15121            ),
15122            standalone_axis(
15123                "catAx",
15124                "<c:axId val=\"1\"/><c:scaling/><c:crossAx val=\"2\"/>",
15125            ),
15126            standalone_axis(
15127                "catAx",
15128                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/>",
15129            ),
15130            standalone_axis(
15131                "catAx",
15132                "<c:axId val=\"-2147483649\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15133            ),
15134            standalone_axis(
15135                "catAx",
15136                "<c:axId val=\"4294967296\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15137            ),
15138            standalone_axis(
15139                "catAx",
15140                "<c:axId val=\"1\"/><c:scaling><c:logBase val=\"1\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15141            ),
15142            standalone_axis(
15143                "catAx",
15144                "<c:axId val=\"1\"/><c:scaling><c:max val=\"NaN\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15145            ),
15146            standalone_axis(
15147                "catAx",
15148                "<c:axId val=\"1\"/><c:scaling><c:max val=\"1\"/><c:min val=\"2\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15149            ),
15150            standalone_axis(
15151                "catAx",
15152                "<c:axId val=\"1\"/><c:scaling><c:orientation val=\"sideways\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15153            ),
15154            standalone_axis(
15155                "catAx",
15156                "<c:axId val=\"1\"/><c:scaling/><c:delete val=\"yes\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15157            ),
15158            standalone_axis(
15159                "catAx",
15160                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"middle\"/><c:crossAx val=\"2\"/>",
15161            ),
15162            standalone_axis(
15163                "catAx",
15164                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:majorTickMark val=\"near\"/><c:crossAx val=\"2\"/>",
15165            ),
15166            standalone_axis(
15167                "catAx",
15168                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:tickLblPos val=\"middle\"/><c:crossAx val=\"2\"/>",
15169            ),
15170            standalone_axis(
15171                "catAx",
15172                "<c:axId val=\"1\"/><c:axId val=\"2\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15173            ),
15174            standalone_axis(
15175                "catAx",
15176                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt sourceLinked=\"1\"/><c:crossAx val=\"2\"/>",
15177            ),
15178            standalone_axis(
15179                "catAx",
15180                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt formatCode=\"0\" sourceLinked=\"maybe\"/><c:crossAx val=\"2\"/>",
15181            ),
15182        ];
15183        for xml in cases {
15184            let result = std::panic::catch_unwind(|| Axis::from_xml(xml.as_bytes()));
15185            assert!(result.is_ok(), "axis parser panicked for {xml}");
15186            assert!(result.unwrap().is_err(), "malformed axis parsed: {xml}");
15187        }
15188
15189        let mut axis = Axis::new(
15190            AxisKind::Value,
15191            AxisId::new(1).unwrap(),
15192            AxisPosition::Left,
15193            AxisId::new(2).unwrap(),
15194        );
15195        axis.scaling.maximum = Some(f64::INFINITY);
15196        assert!(axis.to_xml().is_err());
15197    }
15198
15199    #[test]
15200    fn axes_preserve_unmodelled_children_byte_for_byte() {
15201        let xml = format!(
15202            r#"<q:catAx xmlns:q="{C_NS}" xmlns:d="{A_NS}" xmlns:x="urn:producer" x:keep="axis"><!--before--><q:axId val="-1884094432" x:keep="id"/><?after-id?><q:scaling x:keep="scaling"><!--scale--><q:orientation val="minMax"/><q:extLst><q:ext uri="scale"><x:data/></q:ext></q:extLst></q:scaling><q:axPos val="b"/><q:majorGridlines x:keep="grid"><x:grid/></q:majorGridlines><q:title><x:title/></q:title><q:crossAx val="-1884097184"/><q:crosses val="autoZero"/><q:auto val="1"><x:auto/></q:auto><q:extLst><q:ext uri="axis"><x:tail/></q:ext></q:extLst><!--after--></q:catAx>"#
15203        );
15204        let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
15205        let mut relabelled = parsed.clone();
15206        relabelled.kind = AxisKind::Value;
15207        assert!(
15208            relabelled.to_xml().is_err(),
15209            "a parsed category axis must not retain its tail under a value-axis root"
15210        );
15211        let written = parsed.to_xml().unwrap();
15212        for raw in [
15213            br#"<!--before-->"#.as_slice(),
15214            br#"<?after-id?>"#.as_slice(),
15215            br#"<!--scale-->"#.as_slice(),
15216            br#"<q:extLst><q:ext uri="scale"><x:data/></q:ext></q:extLst>"#.as_slice(),
15217            br#"<x:grid/>"#.as_slice(),
15218            br#"<x:title/>"#.as_slice(),
15219            br#"<q:crosses val="autoZero"/>"#.as_slice(),
15220            br#"<q:auto val="1"><x:auto/></q:auto>"#.as_slice(),
15221            br#"<q:extLst><q:ext uri="axis"><x:tail/></q:ext></q:extLst>"#.as_slice(),
15222            br#"<!--after-->"#.as_slice(),
15223        ] {
15224            assert!(
15225                written.windows(raw.len()).any(|window| window == raw),
15226                "preserved axis bytes changed: {}",
15227                String::from_utf8_lossy(raw)
15228            );
15229        }
15230        let written = String::from_utf8(written).unwrap();
15231        assert!(written.starts_with("<c:catAx"));
15232        assert!(written.contains("x:keep=\"axis\""));
15233        assert!(written.contains("x:keep=\"id\""));
15234        assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
15235    }
15236
15237    #[test]
15238    fn every_corpus_axis_round_trips_structurally() {
15239        let Some(corpus) = require_or_skip_corpus() else {
15240            return;
15241        };
15242        verify_fetched_corpus(&corpus);
15243        let mut axis_count = 0usize;
15244        let mut chart_parts = 0usize;
15245        let mut kinds = HashSet::new();
15246        for path in manifest_paths() {
15247            let package = OpcPackage::open(corpus.join(path))
15248                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
15249            for (part, xml) in &package.parts {
15250                if !is_chart_part(part) {
15251                    continue;
15252                }
15253                let chart = CT_ChartSpace::from_xml(xml)
15254                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
15255                let axes = chart.chart.plot_area.axes().unwrap_or_else(|error| {
15256                    panic!("{path} {part}: axis projection failed: {error}")
15257                });
15258                for axis in axes {
15259                    kinds.insert(axis.kind);
15260                    let written = axis.to_xml().unwrap_or_else(|error| {
15261                        panic!("{path} {part}: axis write failed: {error}")
15262                    });
15263                    let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
15264                        panic!("{path} {part}: written axis parse failed: {error}")
15265                    });
15266                    assert_eq!(axis, reparsed, "{path} {part}: axis model changed");
15267                    axis_count += 1;
15268                }
15269                chart_parts += 1;
15270            }
15271        }
15272        assert!(
15273            axis_count > 0,
15274            "the pinned corpus contained no ChartML axes"
15275        );
15276        assert!(kinds.contains(&AxisKind::Category));
15277        assert!(kinds.contains(&AxisKind::Value));
15278
15279        for kind in ["dateAx", "serAx"] {
15280            let parsed = Axis::from_xml(
15281                standalone_axis(
15282                    kind,
15283                    "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15284                )
15285                .as_bytes(),
15286            )
15287            .unwrap();
15288            assert_eq!(parsed, Axis::from_xml(&parsed.to_xml().unwrap()).unwrap());
15289        }
15290        eprintln!(
15291            "ChartML axis corpus gate checked {axis_count} axes across {chart_parts} chart parts"
15292        );
15293    }
15294
15295    #[test]
15296    fn series_formula_and_cache_are_consistent_with_one_source() {
15297        let values = NumericData::new(
15298            "'Sales 24'!$B$2:$B$4".to_owned(),
15299            "0.0".to_owned(),
15300            vec![4.25, 8.5, 17.0],
15301        )
15302        .unwrap();
15303        let mut series = Series::new(3, 1, values);
15304        series.categories = Some(AxisData::String(
15305            StringRef::new(
15306                "'Sales 24'!$A$2:$A$4".to_owned(),
15307                vec!["North".to_owned(), "South".to_owned(), "West".to_owned()],
15308            )
15309            .unwrap(),
15310        ));
15311
15312        let written = String::from_utf8(series.to_xml().unwrap()).unwrap();
15313        assert!(written.contains("<c:f>&apos;Sales 24&apos;!$B$2:$B$4</c:f>"));
15314        assert!(written.contains("<c:formatCode>0.0</c:formatCode>"));
15315        assert_eq!(written.matches("<c:ptCount val=\"3\"").count(), 2);
15316        for (index, value) in ["4.25", "8.5", "17"].iter().enumerate() {
15317            assert!(written.contains(&format!("<c:pt idx=\"{index}\"><c:v>{value}</c:v></c:pt>")));
15318        }
15319        let reparsed = Series::from_xml(written.as_bytes()).unwrap();
15320        assert_eq!(reparsed.index, series.index);
15321        assert_eq!(reparsed.order, series.order);
15322        match (&reparsed.categories, &series.categories) {
15323            (Some(AxisData::String(left)), Some(AxisData::String(right))) => {
15324                assert_eq!(left.formula, right.formula);
15325                assert_eq!(left.values, right.values);
15326            }
15327            _ => panic!("expected string categories"),
15328        }
15329        assert_eq!(reparsed.values.formula, series.values.formula);
15330        assert_eq!(reparsed.values.format_code, series.values.format_code);
15331        assert_eq!(reparsed.values.values, series.values.values);
15332    }
15333
15334    #[test]
15335    fn string_and_numeric_references_write_fixed_prefixes_in_schema_order() {
15336        let string_xml = br#"<q:strRef xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:strCache><q:pt idx="0"><q:v>West</q:v></q:pt><q:ptCount val="1"/></q:strCache><q:f>Sheet1!$A$2</q:f></q:strRef>"#;
15337        let string_ref = StringRef::from_xml(string_xml).unwrap();
15338        let written = String::from_utf8(string_ref.to_xml().unwrap()).unwrap();
15339        assert!(written.starts_with("<c:strRef xmlns:c="));
15340        assert!(written.find("<c:f>").unwrap() < written.find("<c:strCache>").unwrap());
15341        assert!(written.find("<c:ptCount").unwrap() < written.find("<c:pt idx=").unwrap());
15342
15343        let numeric_xml = br#"<q:numRef xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:numCache><q:ptCount val="2"/><q:pt idx="0"><q:v>1.5</q:v></q:pt><q:pt idx="1"><q:v>2.5</q:v></q:pt><q:formatCode>0.00</q:formatCode></q:numCache><q:f>Sheet1!$B$2:$B$3</q:f></q:numRef>"#;
15344        let numeric = NumericData::from_xml(numeric_xml).unwrap();
15345        let written = String::from_utf8(numeric.to_xml().unwrap()).unwrap();
15346        let positions: Vec<_> = [
15347            "<c:f>",
15348            "<c:numCache>",
15349            "<c:formatCode>",
15350            "<c:ptCount",
15351            "<c:pt idx=",
15352        ]
15353        .iter()
15354        .map(|tag| written.find(tag).unwrap())
15355        .collect();
15356        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15357        assert_eq!(numeric, NumericData::from_xml(written.as_bytes()).unwrap());
15358    }
15359
15360    #[test]
15361    fn mixed_chartml_aliases_resolve_by_namespace_uri() {
15362        let xml = br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="4"/><q:order val="2"/><c:cat><q:strRef><c:f>Sheet1!$A$2:$A$3</c:f><q:strCache><c:ptCount val="2"/><q:pt idx="0"><c:v>North</c:v></q:pt><c:pt idx="1"><q:v>West</q:v></c:pt></q:strCache></q:strRef></c:cat><q:val><c:numRef><q:f>Sheet1!$B$2:$B$3</q:f><c:numCache><q:formatCode>0.0</q:formatCode><c:ptCount val="2"/><q:pt idx="0"><c:v>1.5</c:v></q:pt><c:pt idx="1"><q:v>2.5</q:v></c:pt></c:numCache></c:numRef></q:val></q:ser>"#;
15363        let parsed = Series::from_xml(xml).unwrap();
15364        assert_eq!(parsed.index, 4);
15365        assert_eq!(parsed.order, 2);
15366        assert_eq!(parsed.values.values, vec![1.5, 2.5]);
15367        assert!(matches!(parsed.categories, Some(AxisData::String(_))));
15368        assert_eq!(parsed, Series::from_xml(&parsed.to_xml().unwrap()).unwrap());
15369
15370        let foreign = br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:producer"><c:idx val="0"/><c:order val="0"/><c:spPr><x:solidFill><x:srgbClr val="112233"/></x:solidFill></c:spPr><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#;
15371        let parsed = Series::from_xml(foreign).unwrap();
15372        assert!(parsed.sp_pr.as_ref().unwrap().fill.is_none());
15373        let written = parsed.to_xml().unwrap();
15374        assert!(
15375            std::str::from_utf8(&written)
15376                .unwrap()
15377                .contains(r#"<x:solidFill><x:srgbClr val="112233"/></x:solidFill>"#)
15378        );
15379        assert_eq!(parsed, Series::from_xml(&written).unwrap());
15380    }
15381
15382    #[test]
15383    fn plot_area_series_ignores_inherited_foreign_plot_aliases() {
15384        let xml = br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:producer"><c:chart><c:plotArea><x:barChart><x:ser><x:idx val="9"/><x:order val="9"/><x:val><x:numRef><x:f>foreign</x:f><x:numCache><x:formatCode>General</x:formatCode><x:ptCount val="0"/></x:numCache></x:numRef></x:val></x:ser></x:barChart><c:barChart><q:ser><q:idx val="1"/><q:order val="0"/><q:marker><x:data/></q:marker><q:val><q:numRef><q:f>Sheet1!$B$2</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="1"/><q:pt idx="0"><q:v>3</q:v></q:pt></q:numCache></q:numRef></q:val></q:ser></c:barChart><c:pieChart/></c:plotArea></c:chart></c:chartSpace>"#;
15385        let chart = CT_ChartSpace::from_xml(xml).unwrap();
15386        let series = chart.chart.plot_area.series().unwrap();
15387        assert_eq!(series.len(), 1);
15388        assert_eq!(series[0].index, 1);
15389        let written = String::from_utf8(series[0].to_xml().unwrap()).unwrap();
15390        assert!(
15391            written.contains(r#"xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart""#)
15392        );
15393        assert!(written.contains(r#"xmlns:x="urn:producer""#));
15394        assert!(written.contains("<q:marker><x:data/></q:marker>"));
15395        assert_eq!(series[0], Series::from_xml(written.as_bytes()).unwrap());
15396
15397        let conflicting = br#"<q:chartSpace xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:chart><q:plotArea><q:barChart xmlns:c="urn:foreign"><q:ser><q:idx val="0"/><q:order val="0"/><q:val><q:numRef><q:f>Sheet1!$A$1</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="0"/></q:numCache></q:numRef></q:val></q:ser></q:barChart><q:pieChart/></q:plotArea></q:chart></q:chartSpace>"#;
15398        let chart = CT_ChartSpace::from_xml(conflicting).unwrap();
15399        assert!(chart.chart.plot_area.series().is_err());
15400    }
15401
15402    #[test]
15403    fn malformed_series_and_cache_values_return_errors_without_panicking() {
15404        let cases: &[&[u8]] = &[
15405            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:order val="0"/><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#,
15406            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/></c:ser>"#,
15407            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:idx val="1"/><c:order val="0"/><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#,
15408            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:val><c:numRef><c:f></c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#,
15409            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/><c:pt idx="0"><c:v>1</c:v></c:pt></c:numCache></c:numRef></c:val></c:ser>"#,
15410            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:val><c:numRef><c:f>S!$A$1:$A$2</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="2"/><c:pt idx="0"><c:v>1</c:v></c:pt><c:pt idx="2"><c:v>2</c:v></c:pt></c:numCache></c:numRef></c:val></c:ser>"#,
15411            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="1"/><c:pt idx="0"><c:v>NaN</c:v></c:pt></c:numCache></c:numRef></c:val></c:ser>"#,
15412            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="urn:producer"><c:idx val="0"/><c:order val="0"/><a:marker/><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#,
15413            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:tx><c:v>opaque</c:v></c:tx><c:tx><c:strRef><c:f>S!$A$1</c:f><c:strCache><c:ptCount val="0"/></c:strCache></c:strRef></c:tx><c:val><c:numRef><c:f>S!$B$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#,
15414            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:cat><c:multiLvlStrRef/></c:cat><c:cat><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:cat><c:val><c:numRef><c:f>S!$B$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#,
15415            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:val><c:numRef><c:f>S!$B$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val><c:bubbleSize><c:numLit/></c:bubbleSize><c:bubbleSize><c:numRef><c:f>S!$C$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:bubbleSize></c:ser>"#,
15416            br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:idx val="0"/><q:order val="0"/><q:val xmlns:c="urn:producer"><q:numRef><q:f>S!$A$1</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="0"/></q:numCache></q:numRef></q:val></q:ser>"#,
15417            br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:idx val="0"/><q:order val="0"/><q:val><q:numRef><q:f>S!$A$1</q:f><q:numCache xmlns:c="urn:producer"><q:formatCode>General</q:formatCode><q:ptCount val="0"/></q:numCache></q:numRef></q:val></q:ser>"#,
15418            br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:idx val="0"/><q:order val="0"/><q:val><q:numRef><q:f>S!$A$1</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="1"/><q:pt xmlns:c="urn:producer" idx="0"><q:v>1</q:v></q:pt></q:numCache></q:numRef></q:val></q:ser>"#,
15419            br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:idx val="0"/><q:order val="0"/><q:val><q:numRef><q:f>S!$A$1</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount xmlns:c="urn:producer" val="0"/></q:numCache></q:numRef></q:val></q:ser>"#,
15420            br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:idx val="0"/><q:order val="0"/><q:val><q:numRef><q:f xmlns:c="urn:producer">S!$A$1</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="0"/></q:numCache></q:numRef></q:val></q:ser>"#,
15421        ];
15422        for xml in cases {
15423            let result = std::panic::catch_unwind(|| Series::from_xml(xml));
15424            assert!(result.is_ok(), "series parser panicked");
15425            assert!(
15426                result.unwrap().is_err(),
15427                "malformed series parsed: {}",
15428                String::from_utf8_lossy(xml)
15429            );
15430        }
15431        assert!(
15432            NumericData::new(
15433                "S!$A$1".to_owned(),
15434                "General".to_owned(),
15435                vec![f64::INFINITY]
15436            )
15437            .is_err()
15438        );
15439    }
15440
15441    #[test]
15442    fn chart_text_rejects_xml_forbidden_characters_and_escapes_metacharacters() {
15443        assert!(StringRef::new("S!$A$1\u{1}".to_owned(), Vec::new()).is_err());
15444        assert!(StringRef::new("S!$A$1".to_owned(), vec!["invalid\u{1}".to_owned()]).is_err());
15445        assert!(
15446            NumericData::new("S!$A$1\u{1}".to_owned(), "General".to_owned(), Vec::new()).is_err()
15447        );
15448        assert!(
15449            NumericData::new("S!$A$1".to_owned(), "General\u{1}".to_owned(), Vec::new()).is_err()
15450        );
15451
15452        let mut strings = StringRef::new("S!$A$1".to_owned(), vec!["valid".to_owned()]).unwrap();
15453        strings.formula = "invalid\u{1}".to_owned();
15454        assert!(strings.to_xml().is_err());
15455        strings.formula = "S!$A$1".to_owned();
15456        strings.values[0] = "invalid\u{1}".to_owned();
15457        assert!(strings.to_xml().is_err());
15458
15459        let mut numbers =
15460            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap();
15461        numbers.formula = "invalid\u{1}".to_owned();
15462        assert!(numbers.to_xml().is_err());
15463        numbers.formula = "S!$A$1".to_owned();
15464        numbers.format_code = "invalid\u{1}".to_owned();
15465        assert!(numbers.to_xml().is_err());
15466
15467        let strings =
15468            StringRef::new("S!$A$1&".to_owned(), vec!["North < West & East".to_owned()]).unwrap();
15469        let written = String::from_utf8(strings.to_xml().unwrap()).unwrap();
15470        assert!(written.contains("S!$A$1&amp;"));
15471        assert!(written.contains("North &lt; West &amp; East"));
15472    }
15473
15474    #[test]
15475    fn series_preserves_unmodelled_children_byte_for_byte() {
15476        let xml = br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:d="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer" x:keep="series"><!--before--><q:idx val="0" x:keep="idx"/><x:between/><q:order val="0"/><q:tx x:keep="tx"><q:strRef x:keep="ref"><q:f x:keep="formula">S!$B$1</q:f><q:strCache x:keep="cache"><q:ptCount val="1" x:keep="count"/><!--point--><q:pt idx="0" x:keep="point"><q:v x:keep="value">Revenue</q:v></q:pt><x:cacheExt/></q:strCache></q:strRef></q:tx><q:spPr><d:noFill/><x:shapeExt/></q:spPr><q:dPt x:id="one"><q:idx val="0"/></q:dPt><q:dLbls x:id="labels"/><q:cat><q:strRef><q:f>S!$A$2:$A$3</q:f><q:strCache><q:ptCount val="2"/><q:pt idx="0"><q:v>North</q:v></q:pt><q:pt idx="1"><q:v>West</q:v></q:pt></q:strCache></q:strRef></q:cat><q:trendline x:id="trend"/><q:val><q:numRef><q:f>S!$B$2:$B$3</q:f><q:numCache><q:formatCode>0.0</q:formatCode><q:ptCount val="2"/><q:pt idx="0"><q:v>1.5</q:v></q:pt><q:pt idx="1"><q:v>2.5</q:v></q:pt></q:numCache></q:numRef></q:val><q:extLst><q:ext uri="keep"><x:data/></q:ext></q:extLst></q:ser>"#;
15477        let parsed = Series::from_xml(xml).unwrap();
15478        let written = parsed.to_xml().unwrap();
15479        for raw in [
15480            br#"<!--before-->"#.as_slice(),
15481            br#"<x:between/>"#.as_slice(),
15482            br#"<q:dPt x:id="one"><q:idx val="0"/></q:dPt>"#.as_slice(),
15483            br#"<q:trendline x:id="trend"/>"#.as_slice(),
15484            br#"<q:extLst><q:ext uri="keep"><x:data/></q:ext></q:extLst>"#.as_slice(),
15485            br#"<!--point-->"#.as_slice(),
15486            br#"<x:cacheExt/>"#.as_slice(),
15487        ] {
15488            assert!(
15489                written.windows(raw.len()).any(|window| window == raw),
15490                "preserved series bytes changed: {}",
15491                String::from_utf8_lossy(raw)
15492            );
15493        }
15494        let written = String::from_utf8(written.clone()).unwrap();
15495        assert!(written.contains(r#"x:keep="series""#));
15496        assert!(written.contains(r#"x:keep="formula""#));
15497        assert!(written.contains("<c:dLbls"));
15498        assert!(written.contains(r#"x:id="labels""#));
15499        assert!(parsed.data_labels.is_some());
15500        assert_eq!(parsed, Series::from_xml(written.as_bytes()).unwrap());
15501    }
15502
15503    #[test]
15504    fn public_series_edits_do_not_duplicate_or_drop_preserved_payloads() {
15505        let opaque_xml = br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:tx><c:v>opaque</c:v></c:tx><c:cat><c:multiLvlStrRef/></c:cat><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val><c:bubbleSize><c:numLit/></c:bubbleSize></c:ser>"#;
15506        let parsed = Series::from_xml(opaque_xml).unwrap();
15507
15508        let mut edited = parsed.clone();
15509        edited.name = Some(StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap());
15510        assert!(edited.to_xml().is_err());
15511
15512        let mut edited = parsed.clone();
15513        edited.categories = Some(AxisData::Numeric(
15514            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap(),
15515        ));
15516        assert!(edited.to_xml().is_err());
15517
15518        let mut edited = parsed;
15519        edited.bubble_size =
15520            Some(NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
15521        assert!(edited.to_xml().is_err());
15522
15523        let cache_xml = br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:producer"><c:idx val="0"/><c:order val="0"/><c:val><c:numRef><c:f>S!$A$1:$A$2</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="2"/><c:pt idx="0"><c:v>1</c:v></c:pt><c:pt idx="1"><c:v>2</c:v></c:pt><c:extLst><c:ext uri="keep"><x:data/></c:ext></c:extLst></c:numCache></c:numRef></c:val></c:ser>"#;
15524        let parsed = Series::from_xml(cache_xml).unwrap();
15525        let tail = "<c:extLst><c:ext uri=\"keep\"><x:data/></c:ext></c:extLst>";
15526
15527        let mut shortened = parsed.clone();
15528        shortened.values.values.truncate(1);
15529        let written = String::from_utf8(shortened.to_xml().unwrap()).unwrap();
15530        assert!(!written.contains("<c:pt idx=\"1\""));
15531        assert!(written.find("<c:pt idx=\"0\"").unwrap() < written.find(tail).unwrap());
15532        assert!(Series::from_xml(written.as_bytes()).is_ok());
15533
15534        let mut grown = parsed;
15535        grown.values.values.push(3.0);
15536        let written = String::from_utf8(grown.to_xml().unwrap()).unwrap();
15537        assert!(written.find("<c:pt idx=\"2\"").unwrap() < written.find(tail).unwrap());
15538        assert!(Series::from_xml(written.as_bytes()).is_ok());
15539
15540        let ordered_xml = br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><c:idx val="0"/><c:order val="0"/><c:marker><c:symbol val="circle"/></c:marker><c:val><c:numRef><c:f>S!$B$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val><c:extLst><c:ext uri="keep"/></c:extLst></c:ser>"#;
15541        let mut edited = Series::from_xml(ordered_xml).unwrap();
15542        edited.name = Some(StringRef::new("S!$B$1".to_owned(), Vec::new()).unwrap());
15543        edited.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
15544        edited.categories = Some(AxisData::String(
15545            StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap(),
15546        ));
15547        edited.bubble_size =
15548            Some(NumericData::new("S!$C$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
15549        let written = String::from_utf8(edited.to_xml().unwrap()).unwrap();
15550        let positions: Vec<_> = [
15551            "<c:tx>",
15552            "<c:spPr",
15553            "<c:marker>",
15554            "<c:cat>",
15555            "<c:val>",
15556            "<c:bubbleSize>",
15557            "<c:extLst>",
15558        ]
15559        .iter()
15560        .map(|tag| written.find(tag).unwrap())
15561        .collect();
15562        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15563        assert!(Series::from_xml(written.as_bytes()).is_ok());
15564    }
15565
15566    #[test]
15567    fn every_corpus_series_round_trips_structurally() {
15568        let Some(corpus) = require_or_skip_corpus() else {
15569            return;
15570        };
15571        verify_fetched_corpus(&corpus);
15572        let mut series_count = 0usize;
15573        let mut chart_parts = 0usize;
15574        for path in manifest_paths() {
15575            let package = OpcPackage::open(corpus.join(path))
15576                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
15577            for (part, xml) in &package.parts {
15578                if !is_chart_part(part) {
15579                    continue;
15580                }
15581                let chart = CT_ChartSpace::from_xml(xml)
15582                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
15583                let parsed_series =
15584                    chart.chart.plot_area.series().unwrap_or_else(|error| {
15585                        panic!("{path} {part}: series parse failed: {error}")
15586                    });
15587                for series in parsed_series {
15588                    let written = series.to_xml().unwrap_or_else(|error| {
15589                        panic!("{path} {part}: series write failed: {error}")
15590                    });
15591                    let reparsed = Series::from_xml(&written).unwrap_or_else(|error| {
15592                        panic!("{path} {part}: written series parse failed: {error}")
15593                    });
15594                    assert_eq!(series, reparsed, "{path} {part}: series model changed");
15595                    series_count += 1;
15596                }
15597                chart_parts += 1;
15598            }
15599        }
15600        assert!(
15601            series_count > 0,
15602            "the pinned corpus contained no supported series"
15603        );
15604        eprintln!(
15605            "ChartML series corpus gate checked {series_count} series across {chart_parts} chart parts"
15606        );
15607    }
15608
15609    #[test]
15610    fn chart_space_reads_aliases_and_writes_fixed_prefixes_in_schema_order() {
15611        let xml = br#"<ch:chartSpace xmlns:ch="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:d="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:rel="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:x="urn:producer"><x:before/><ch:chart><ch:plotVisOnly val="0"/><ch:legend><x:legend/></ch:legend><ch:title><x:title/></ch:title><ch:plotArea><x:plot/></ch:plotArea><ch:dispBlanksAs val="span"/></ch:chart><ch:txPr><d:bodyPr/><d:p/></ch:txPr><ch:spPr><d:solidFill><d:srgbClr val="112233"/></d:solidFill></ch:spPr><x:after/></ch:chartSpace>"#;
15612        let parsed = CT_ChartSpace::from_xml(xml).unwrap();
15613        assert!(!parsed.chart.auto_title_deleted);
15614        assert!(!parsed.chart.plot_vis_only);
15615        assert_eq!(parsed.chart.disp_blanks_as, DispBlanksAs::Span);
15616        assert!(parsed.chart.title.is_some());
15617        assert!(parsed.chart.legend.is_some());
15618        assert!(parsed.sp_pr.as_ref().unwrap().fill.is_some());
15619        assert!(parsed.tx_pr.is_some());
15620
15621        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15622        assert!(written.starts_with(
15623            r#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships""#
15624        ));
15625        for tag in [
15626            "<c:chart>",
15627            "<c:title>",
15628            "<c:plotArea>",
15629            "<c:legend>",
15630            "<c:plotVisOnly",
15631            "<c:dispBlanksAs",
15632            "<c:spPr>",
15633            "<c:txPr",
15634        ] {
15635            assert!(written.contains(tag), "missing fixed-prefix tag {tag}");
15636        }
15637        let positions: Vec<_> = [
15638            "<c:title>",
15639            "<c:plotArea>",
15640            "<c:legend>",
15641            "<c:plotVisOnly",
15642            "<c:dispBlanksAs",
15643            "<c:spPr>",
15644            "<c:txPr",
15645        ]
15646        .iter()
15647        .map(|tag| written.find(tag).unwrap())
15648        .collect();
15649        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15650        assert!(written.contains("<a:solidFill><a:srgbClr"));
15651        assert!(written.contains("<d:bodyPr/><d:p"));
15652        assert!(written.contains(&format!("xmlns:d=\"{A_NS}\"")));
15653        assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
15654    }
15655
15656    #[test]
15657    fn public_core_edits_keep_preserved_children_in_their_schema_slots() {
15658        let xml = br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><c:chart><c:pivotFmts><c:pivotFmt/></c:pivotFmts><c:plotArea/></c:chart><c:externalData r:id="rId1"/></c:chartSpace>"#;
15659        let mut parsed = CT_ChartSpace::from_xml(xml).unwrap();
15660        parsed.chart.auto_title_deleted = true;
15661        parsed.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
15662        parsed.tx_pr =
15663            Some(CT_TextBody::from_xml(br#"<a:txBody><a:bodyPr/><a:p/></a:txBody>"#).unwrap());
15664        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15665        let positions: Vec<_> = [
15666            "<c:autoTitleDeleted",
15667            "<c:pivotFmts>",
15668            "<c:plotArea",
15669            "<c:spPr",
15670            "<c:txPr",
15671            "<c:externalData",
15672        ]
15673        .iter()
15674        .map(|tag| written.find(tag).unwrap())
15675        .collect();
15676        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15677        assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
15678    }
15679
15680    #[test]
15681    fn malformed_core_chart_values_return_errors_without_panicking() {
15682        let cases: &[&[u8]] = &[
15683            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"/>"#,
15684            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart/></c:chartSpace>"#,
15685            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:plotArea/></c:chart></c:chartSpace>"#,
15686            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:autoTitleDeleted val="yes"/><c:plotArea/></c:chart></c:chartSpace>"#,
15687            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:dispBlanksAs val="empty"/></c:chart></c:chartSpace>"#,
15688            br#"<c:chartSpace xmlns:c="urn:wrong"><c:chart><c:plotArea/></c:chart></c:chartSpace>"#,
15689            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:foreign"><x:chart><x:plotArea/></x:chart></c:chartSpace>"#,
15690            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/></c:chart><c:chart><c:plotArea/></c:chart></c:chartSpace>"#,
15691            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:title/><c:title/><c:plotArea/></c:chart></c:chartSpace>"#,
15692            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:plotVisOnly><c:child/></c:plotVisOnly></c:chart></c:chartSpace>"#,
15693            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:foreign"><c:chart><c:plotArea/></c:chart><c:txPr><x:bodyPr/><a:p/></c:txPr></c:chartSpace>"#,
15694            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:foreign"><c:chart><c:plotArea/></c:chart><c:spPr><a:solidFill><x:srgbClr val="112233"/></a:solidFill></c:spPr></c:chartSpace>"#,
15695        ];
15696        for xml in cases {
15697            let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml));
15698            assert!(result.is_ok(), "ChartML parser panicked");
15699            assert!(
15700                result.unwrap().is_err(),
15701                "malformed ChartML parsed: {}",
15702                String::from_utf8_lossy(xml)
15703            );
15704        }
15705    }
15706
15707    #[test]
15708    fn fx215_chart_text_adapter_preserves_qualified_and_foreign_owners() {
15709        for (prefix, declarations) in [
15710            ("d:", format!("xmlns:d=\"{A_NS}\"")),
15711            ("", format!("xmlns=\"{A_NS}\"")),
15712        ] {
15713            let opaque = "<x:r><x:t>foreign</x:t></x:r>";
15714            let xml = format!(
15715                "<c:chartSpace xmlns:c=\"{C_NS}\" {declarations} xmlns:x=\"urn:foreign\"><c:chart><c:plotArea/></c:chart><c:txPr><{prefix}bodyPr/><{prefix}p>{opaque}<{prefix}r><{prefix}t>owned</{prefix}t></{prefix}r></{prefix}p></c:txPr></c:chartSpace>"
15716            );
15717            let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15718            let text = chart.tx_pr.as_ref().unwrap();
15719            assert_eq!(text.paragraphs().len(), 1);
15720            assert_eq!(text.paragraphs()[0].runs.len(), 1);
15721            let written = chart.to_xml().unwrap();
15722            assert!(std::str::from_utf8(&written).unwrap().contains(opaque));
15723            let reopened = CT_ChartSpace::from_xml(&written).unwrap();
15724            assert_eq!(
15725                reopened.tx_pr.as_ref().unwrap().paragraphs()[0].runs.len(),
15726                1
15727            );
15728            assert_eq!(
15729                text.body_properties,
15730                reopened.tx_pr.as_ref().unwrap().body_properties
15731            );
15732            for body in [text, reopened.tx_pr.as_ref().unwrap()] {
15733                let oxml_drawing::text::TextRun::Run(run) = &body.paragraphs()[0].runs[0] else {
15734                    panic!("qualified text must remain a regular run");
15735                };
15736                assert_eq!(run.text.value, "owned");
15737            }
15738        }
15739        let xml = format!(
15740            "<c:chartSpace xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:x=\"urn:foreign\"><c:chart><c:plotArea/></c:chart><c:txPr><a:bodyPr/><a:p><x:r><x:t>foreign</x:t></x:r></a:p></c:txPr></c:chartSpace>"
15741        );
15742        let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15743        assert!(
15744            chart.tx_pr.as_ref().unwrap().paragraphs()[0]
15745                .runs
15746                .is_empty()
15747        );
15748        let saved = chart.to_xml().unwrap();
15749        assert!(
15750            std::str::from_utf8(&saved)
15751                .unwrap()
15752                .contains("<x:r><x:t>foreign</x:t></x:r>")
15753        );
15754        assert_eq!(chart, CT_ChartSpace::from_xml(&saved).unwrap());
15755    }
15756
15757    #[test]
15758    fn point_label_overrides_render_without_changing_preserved_xml() {
15759        let raw_overrides = r#"<q:dLbl><q:idx val="1"/><q:numFmt formatCode="0.0" sourceLinked="0"/><q:dLblPos val="r"/><q:showVal val="1"/><q:showCatName val="1"/><q:separator> / </q:separator><q:extLst><x:point/></q:extLst></q:dLbl>"#;
15760        let plot = bar_plot("").replace(
15761            r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
15762            &format!(
15763                r#"<q:dLbls>{raw_overrides}<q:numFmt formatCode="0" sourceLinked="0"/><q:showVal val="1"/><q:showCatName val="1"/><q:separator>, </q:separator></q:dLbls>"#
15764            ),
15765        );
15766        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
15767        let plot = &chart.chart.plot_area.plots().unwrap()[0];
15768        let Plot::Bar { data_labels, .. } = plot else {
15769            panic!("expected bar plot");
15770        };
15771        let labels = data_labels.as_ref().unwrap();
15772        let written = labels.to_xml().unwrap();
15773        assert!(
15774            written
15775                .windows(raw_overrides.len())
15776                .any(|window| window == raw_overrides.as_bytes())
15777        );
15778        let overrides = super::point_label_overrides(labels).unwrap();
15779        assert_eq!(overrides.keys().copied().collect::<Vec<_>>(), vec![1]);
15780
15781        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
15782        let mut rendered = Vec::new();
15783        super::render_data_labels(
15784            plot,
15785            chart_bounds(),
15786            DispBlanksAs::Gap,
15787            super::GeometryOptions::default(),
15788            &mut fonts,
15789            &mut rendered,
15790        )
15791        .unwrap();
15792        let labels: Vec<_> = rendered
15793            .iter()
15794            .filter_map(|element| match element {
15795                PositionedElement::Text(run) => Some(run),
15796                _ => None,
15797            })
15798            .collect();
15799        assert_eq!(labels.len(), 2);
15800        assert_eq!(labels[0].text, "North, 1");
15801        assert_eq!(labels[1].text, "South / 2.0");
15802        assert_point_near(labels[0].origin, Point { x: 50.0, y: 105.0 });
15803        assert_point_near(labels[1].origin, Point { x: 158.0, y: 70.0 });
15804        assert_eq!(
15805            CT_DLbls::from_xml(&written).unwrap().to_xml().unwrap(),
15806            written
15807        );
15808    }
15809
15810    #[test]
15811    fn malformed_point_label_overrides_return_contextual_errors() {
15812        let cases = [
15813            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:showVal val="1"/></q:dLbl></q:dLbls>"#,
15814            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="0"/><q:idx val="1"/></q:dLbl></q:dLbls>"#,
15815            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="point"/></q:dLbl></q:dLbls>"#,
15816            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="0"/><q:showVal val="maybe"/></q:dLbl></q:dLbls>"#,
15817            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="0"/><q:dLblPos val="middle"/></q:dLbl></q:dLbls>"#,
15818            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="0"/><q:showVal val="1"/><q:dLblPos val="r"/></q:dLbl></q:dLbls>"#,
15819            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="0"/></q:dLbl><q:dLbl><q:idx val="0"/></q:dLbl></q:dLbls>"#,
15820        ];
15821        for xml in cases {
15822            let labels = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
15823            let result = std::panic::catch_unwind(|| super::point_label_overrides(&labels));
15824            assert!(result.is_ok(), "point-label parser panicked for {xml}");
15825            assert!(
15826                result.unwrap().is_err(),
15827                "malformed point labels parsed: {xml}"
15828            );
15829        }
15830
15831        let foreign = format!(
15832            r#"<q:dLbls xmlns:q="{C_NS}" xmlns:x="urn:foreign"><x:dLbl><x:idx val="0"/></x:dLbl></q:dLbls>"#
15833        );
15834        let labels = CT_DLbls::from_xml(foreign.as_bytes()).unwrap();
15835        assert!(super::point_label_overrides(&labels).unwrap().is_empty());
15836    }
15837
15838    #[test]
15839    fn percentage_label_aggregation_rejects_nonfinite_totals() {
15840        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
15841        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
15842        let Plot::Bar { series, .. } = plot else {
15843            panic!("expected bar plot");
15844        };
15845        series[0].values.values = vec![f64::MAX, f64::MAX];
15846        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
15847        super::render_data_labels(
15848            plot,
15849            chart_bounds(),
15850            DispBlanksAs::Gap,
15851            super::GeometryOptions::default(),
15852            &mut fonts,
15853            &mut Vec::new(),
15854        )
15855        .expect("value-only labels do not need a percentage total");
15856        let Plot::Bar { data_labels, .. } = plot else {
15857            panic!("expected bar plot");
15858        };
15859        data_labels.as_mut().unwrap().show_percent = true;
15860        let error = super::render_data_labels(
15861            plot,
15862            chart_bounds(),
15863            DispBlanksAs::Gap,
15864            super::GeometryOptions::default(),
15865            &mut fonts,
15866            &mut Vec::new(),
15867        )
15868        .unwrap_err();
15869        assert!(error.to_string().contains("data-label percentage total"));
15870
15871        let overrides = r#"<q:dLbl><q:idx val="0"/><q:showPercent val="0"/></q:dLbl><q:dLbl><q:idx val="1"/><q:showPercent val="0"/></q:dLbl>"#;
15872        let plot_xml = bar_plot("").replace(
15873            r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
15874            &format!(
15875                r#"<q:dLbls>{overrides}<q:showVal val="1"/><q:showPercent val="1"/></q:dLbls>"#
15876            ),
15877        );
15878        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
15879        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
15880        let Plot::Bar { series, .. } = plot else {
15881            panic!("expected bar plot");
15882        };
15883        series[0].values.values = vec![f64::MAX, f64::MAX];
15884        super::render_data_labels(
15885            plot,
15886            chart_bounds(),
15887            DispBlanksAs::Gap,
15888            super::GeometryOptions::default(),
15889            &mut fonts,
15890            &mut Vec::new(),
15891        )
15892        .expect("effective point overrides disable percentage aggregation");
15893    }
15894
15895    #[test]
15896    fn percentage_labels_use_effective_number_format_precision() {
15897        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
15898        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
15899        let Plot::Bar {
15900            series,
15901            data_labels,
15902            ..
15903        } = plot
15904        else {
15905            panic!("expected bar plot");
15906        };
15907        series[0].values.values = vec![1.0, 2.0];
15908        let labels = data_labels.as_mut().unwrap();
15909        labels.show_value = false;
15910        labels.show_percent = true;
15911        labels.number_format = Some(NumberFormat::new("0.00%".to_owned(), false).unwrap());
15912        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
15913        let mut rendered = Vec::new();
15914        super::render_data_labels(
15915            plot,
15916            chart_bounds(),
15917            DispBlanksAs::Gap,
15918            super::GeometryOptions::default(),
15919            &mut fonts,
15920            &mut rendered,
15921        )
15922        .unwrap();
15923        let text: Vec<_> = rendered
15924            .iter()
15925            .filter_map(|element| match element {
15926                PositionedElement::Text(run) => Some(run.text.as_str()),
15927                _ => None,
15928            })
15929            .collect();
15930        assert_eq!(text, vec!["33.33%", "66.67%"]);
15931
15932        let point_override = r#"<q:dLbl><q:idx val="0"/><q:numFmt formatCode="0.00%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbl>"#;
15933        let plot_xml = bar_plot("").replace(
15934            r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
15935            &format!(
15936                r#"<q:dLbls>{point_override}<q:numFmt formatCode="0%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbls>"#
15937            ),
15938        );
15939        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
15940        let plot = &chart.chart.plot_area.plots().unwrap()[0];
15941        let mut rendered = Vec::new();
15942        super::render_data_labels(
15943            plot,
15944            chart_bounds(),
15945            DispBlanksAs::Gap,
15946            super::GeometryOptions::default(),
15947            &mut fonts,
15948            &mut rendered,
15949        )
15950        .unwrap();
15951        let text: Vec<_> = rendered
15952            .iter()
15953            .filter_map(|element| match element {
15954                PositionedElement::Text(run) => Some(run.text.as_str()),
15955                _ => None,
15956            })
15957            .collect();
15958        assert_eq!(text, vec!["33.33%", "67%"]);
15959    }
15960
15961    #[test]
15962    fn core_chart_shells_preserve_unmodelled_children_byte_for_byte() {
15963        let preserved = [
15964            br#"<x:root x:id="1"><x:nested>one &amp; two</x:nested><!--root--></x:root>"#
15965                .as_slice(),
15966            br#"<x:title x:id="2"><x:nested><![CDATA[title]]></x:nested></x:title>"#.as_slice(),
15967            br#"<x:plot x:id="3"><x:nested>plot</x:nested></x:plot>"#.as_slice(),
15968            br#"<x:legend x:id="4"><x:nested>legend</x:nested></x:legend>"#.as_slice(),
15969            br#"<x:extension x:id="5"><x:nested>extension</x:nested></x:extension>"#.as_slice(),
15970            br#"<!--space-comment-->"#.as_slice(),
15971            br#"<?space processing?>"#.as_slice(),
15972            br#"<!--chart-comment-->"#.as_slice(),
15973            br#"<?title processing?>"#.as_slice(),
15974            br#"<!--flag-comment-->"#.as_slice(),
15975            br#"<?flag processing?>"#.as_slice(),
15976            br#"<x:producerShape/>"#.as_slice(),
15977            br#"<x:producerText/>"#.as_slice(),
15978        ];
15979        let xml = br#"<q:chartSpace xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer"><!--space-comment--><?space processing?><x:root x:id="1"><x:nested>one &amp; two</x:nested><!--root--></x:root><q:chart><!--chart-comment--><q:autoTitleDeleted val="0" x:keep="one &amp; two"><!--flag-comment--><?flag processing?></q:autoTitleDeleted><q:title data="keep"><?title processing?><x:title x:id="2"><x:nested><![CDATA[title]]></x:nested></x:title></q:title><q:plotArea data="keep"><x:plot x:id="3"><x:nested>plot</x:nested></x:plot></q:plotArea><q:legend data="keep"><x:legend x:id="4"><x:nested>legend</x:nested></x:legend></q:legend><x:extension x:id="5"><x:nested>extension</x:nested></x:extension></q:chart><q:spPr><x:producerShape/></q:spPr><q:txPr><a:bodyPr/><a:p><x:producerText/></a:p></q:txPr></q:chartSpace>"#;
15980        let parsed = CT_ChartSpace::from_xml(xml).unwrap();
15981        let written = parsed.to_xml().unwrap();
15982        for raw in preserved {
15983            assert!(
15984                written.windows(raw.len()).any(|window| window == raw),
15985                "preserved subtree changed: {}",
15986                String::from_utf8_lossy(raw)
15987            );
15988        }
15989        let written_text = String::from_utf8(written.clone()).unwrap();
15990        assert!(written_text.contains(r#"x:keep="one &amp; two""#));
15991        assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
15992    }
15993
15994    #[test]
15995    fn every_corpus_chart_part_round_trips_structurally() {
15996        let Some(corpus) = require_or_skip_corpus() else {
15997            return;
15998        };
15999        verify_fetched_corpus(&corpus);
16000        let mut chart_parts = 0usize;
16001        let mut decks_with_charts = HashSet::new();
16002        for path in manifest_paths() {
16003            let package = OpcPackage::open(corpus.join(path))
16004                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
16005            for (part, xml) in &package.parts {
16006                if !is_chart_part(part) {
16007                    continue;
16008                }
16009                let parsed = CT_ChartSpace::from_xml(xml)
16010                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
16011                let written = parsed
16012                    .to_xml()
16013                    .unwrap_or_else(|error| panic!("{path} {part}: write failed: {error}"));
16014                let original_preserved = preserved_chartml_regions(xml)
16015                    .unwrap_or_else(|error| panic!("{path} {part}: original scan failed: {error}"));
16016                let written_preserved = preserved_chartml_regions(&written)
16017                    .unwrap_or_else(|error| panic!("{path} {part}: written scan failed: {error}"));
16018                assert_eq!(
16019                    original_preserved.len(),
16020                    written_preserved.len(),
16021                    "{path} {part}: preserved ChartML region count changed"
16022                );
16023                for (index, (original, written)) in original_preserved
16024                    .iter()
16025                    .zip(&written_preserved)
16026                    .enumerate()
16027                {
16028                    assert_eq!(
16029                        original.parent_path, written.parent_path,
16030                        "{path} {part}: preserved region {index} parent changed"
16031                    );
16032                    assert_eq!(
16033                        original.boundary, written.boundary,
16034                        "{path} {part}: preserved region {index} boundary changed"
16035                    );
16036                    assert_eq!(
16037                        original.sibling_order, written.sibling_order,
16038                        "{path} {part}: preserved region {index} order changed"
16039                    );
16040                    assert_eq!(
16041                        original.xml, written.xml,
16042                        "{path} {part}: preserved region {index} bytes changed"
16043                    );
16044                }
16045                let reparsed = CT_ChartSpace::from_xml(&written)
16046                    .unwrap_or_else(|error| panic!("{path} {part}: written parse failed: {error}"));
16047                assert_eq!(parsed, reparsed, "{path} {part}: core model changed");
16048                chart_parts += 1;
16049                decks_with_charts.insert(path);
16050            }
16051        }
16052        assert!(
16053            chart_parts > 0,
16054            "the pinned corpus contained no chart parts"
16055        );
16056        eprintln!(
16057            "ChartML corpus gate checked {chart_parts} chart parts across {} decks",
16058            decks_with_charts.len()
16059        );
16060    }
16061
16062    #[derive(Debug, Eq, PartialEq)]
16063    struct PreservedChartRegion {
16064        parent_path: Vec<Vec<u8>>,
16065        boundary: usize,
16066        sibling_order: usize,
16067        xml: Vec<u8>,
16068    }
16069
16070    struct PreservationFrame {
16071        local_name: Vec<u8>,
16072        boundary: usize,
16073        next_sibling: usize,
16074    }
16075
16076    fn preserved_chartml_regions(xml: &[u8]) -> Result<Vec<PreservedChartRegion>, String> {
16077        let mut reader = Reader::from_reader(xml);
16078        let mut buffer = Vec::new();
16079        let mut parents: Vec<PreservationFrame> = Vec::new();
16080        let mut preserved = Vec::new();
16081        loop {
16082            match reader.read_event_into(&mut buffer) {
16083                Ok(Event::Start(element)) => {
16084                    let child = local_name(element.name().as_ref()).to_vec();
16085                    if parents.is_empty() && child == b"chartSpace" {
16086                        parents.push(PreservationFrame {
16087                            local_name: child,
16088                            boundary: 0,
16089                            next_sibling: 0,
16090                        });
16091                    } else if let Some(parent) = parents.last()
16092                        && is_opaque_chartml_child(&parent.local_name, &child)
16093                    {
16094                        let boundary =
16095                            preserved_child_boundary(&parent.local_name, &child, parent.boundary);
16096                        let xml = capture_element(&mut reader, &element)
16097                            .map_err(|error| error.to_string())?;
16098                        record_preserved_region(&mut parents, &mut preserved, boundary, xml);
16099                        advance_preservation_boundary(&mut parents, &child);
16100                    } else {
16101                        advance_preservation_boundary(&mut parents, &child);
16102                        if is_chartml_preservation_parent(&child) {
16103                            parents.push(PreservationFrame {
16104                                local_name: child,
16105                                boundary: 0,
16106                                next_sibling: 0,
16107                            });
16108                        } else {
16109                            capture_element(&mut reader, &element)
16110                                .map_err(|error| error.to_string())?;
16111                        }
16112                    }
16113                }
16114                Ok(Event::Empty(element)) => {
16115                    let name = element.name();
16116                    let child = local_name(name.as_ref()).to_vec();
16117                    if let Some(parent) = parents.last()
16118                        && is_opaque_chartml_child(&parent.local_name, &child)
16119                    {
16120                        let boundary =
16121                            preserved_child_boundary(&parent.local_name, &child, parent.boundary);
16122                        let xml =
16123                            capture_empty_element(&element).map_err(|error| error.to_string())?;
16124                        record_preserved_region(&mut parents, &mut preserved, boundary, xml);
16125                    }
16126                    advance_preservation_boundary(&mut parents, &child);
16127                }
16128                Ok(
16129                    event @ (Event::Comment(_)
16130                    | Event::PI(_)
16131                    | Event::CData(_)
16132                    | Event::GeneralRef(_)),
16133                ) if !parents.is_empty() => {
16134                    let boundary = parents.last().expect("preservation parent").boundary;
16135                    let xml = capture_event(event).map_err(|error| error.to_string())?;
16136                    record_preserved_region(&mut parents, &mut preserved, boundary, xml);
16137                }
16138                Ok(Event::End(_)) => {
16139                    parents.pop();
16140                }
16141                Ok(Event::Eof) => break,
16142                Ok(_) => {}
16143                Err(error) => return Err(error.to_string()),
16144            }
16145            buffer.clear();
16146        }
16147        Ok(preserved)
16148    }
16149
16150    fn record_preserved_region(
16151        parents: &mut [PreservationFrame],
16152        preserved: &mut Vec<PreservedChartRegion>,
16153        boundary: usize,
16154        xml: Vec<u8>,
16155    ) {
16156        let sibling_order = parents.last().expect("preservation parent").next_sibling;
16157        parents
16158            .last_mut()
16159            .expect("preservation parent")
16160            .next_sibling += 1;
16161        preserved.push(PreservedChartRegion {
16162            parent_path: parents
16163                .iter()
16164                .map(|parent| parent.local_name.clone())
16165                .collect(),
16166            boundary,
16167            sibling_order,
16168            xml,
16169        });
16170    }
16171
16172    fn advance_preservation_boundary(parents: &mut [PreservationFrame], child: &[u8]) {
16173        let Some(parent) = parents.last_mut() else {
16174            return;
16175        };
16176        parent.boundary = parent
16177            .boundary
16178            .max(preserved_boundary_after(&parent.local_name, child));
16179    }
16180
16181    fn preserved_child_boundary(parent: &[u8], child: &[u8], current: usize) -> usize {
16182        match parent {
16183            b"chartSpace" => match child {
16184                b"date1904" => 0,
16185                b"lang" => 1,
16186                b"roundedCorners" => 2,
16187                b"style" => 3,
16188                b"clrMapOvr" => 4,
16189                b"pivotSource" => 5,
16190                b"protection" => 6,
16191                b"externalData" => 10,
16192                b"printSettings" => 11,
16193                b"userShapes" => 12,
16194                b"extLst" => 13,
16195                _ => current,
16196            },
16197            b"chart" => match child {
16198                b"pivotFmts" => 2,
16199                b"view3D" => 3,
16200                b"floor" => 4,
16201                b"sideWall" => 5,
16202                b"backWall" => 6,
16203                b"showDLblsOverMax" | b"extLst" => 11,
16204                _ => current,
16205            },
16206            _ => current,
16207        }
16208    }
16209
16210    fn preserved_boundary_after(parent: &[u8], child: &[u8]) -> usize {
16211        match parent {
16212            b"chartSpace" => match child {
16213                b"date1904" => 1,
16214                b"lang" => 2,
16215                b"roundedCorners" => 3,
16216                b"style" => 4,
16217                b"clrMapOvr" => 5,
16218                b"pivotSource" => 6,
16219                b"protection" => 7,
16220                b"chart" => 8,
16221                b"spPr" => 9,
16222                b"txPr" => 10,
16223                b"externalData" => 11,
16224                b"printSettings" => 12,
16225                b"userShapes" => 13,
16226                b"extLst" => 14,
16227                _ => 0,
16228            },
16229            b"chart" => match child {
16230                b"title" => 1,
16231                b"autoTitleDeleted" => 2,
16232                b"pivotFmts" => 3,
16233                b"view3D" => 4,
16234                b"floor" => 5,
16235                b"sideWall" => 6,
16236                b"backWall" => 7,
16237                b"plotArea" => 8,
16238                b"legend" => 9,
16239                b"plotVisOnly" => 10,
16240                b"dispBlanksAs" => 11,
16241                b"showDLblsOverMax" | b"extLst" => 11,
16242                _ => 0,
16243            },
16244            b"pieChart" => match child {
16245                b"ser" => 1,
16246                b"dLbls" => 2,
16247                b"firstSliceAng" => 3,
16248                _ => 0,
16249            },
16250            b"doughnutChart" => match child {
16251                b"ser" => 1,
16252                b"dLbls" => 2,
16253                b"firstSliceAng" => 3,
16254                b"holeSize" => 4,
16255                _ => 0,
16256            },
16257            b"areaChart" => match child {
16258                b"grouping" => 1,
16259                b"ser" => 2,
16260                b"dLbls" => 3,
16261                b"axId" => 4,
16262                _ => 0,
16263            },
16264            b"scatterChart" | b"radarChart" => match child {
16265                b"scatterStyle" | b"radarStyle" => 1,
16266                b"ser" => 2,
16267                b"dLbls" => 3,
16268                b"axId" => 4,
16269                _ => 0,
16270            },
16271            _ => 0,
16272        }
16273    }
16274
16275    fn is_opaque_chartml_child(parent: &[u8], child: &[u8]) -> bool {
16276        match parent {
16277            b"chartSpace" => !matches!(child, b"chart" | b"spPr" | b"txPr"),
16278            b"chart" => !matches!(
16279                child,
16280                b"title"
16281                    | b"autoTitleDeleted"
16282                    | b"plotArea"
16283                    | b"legend"
16284                    | b"plotVisOnly"
16285                    | b"dispBlanksAs"
16286            ),
16287            b"plotArea" => !matches!(
16288                child,
16289                b"barChart"
16290                    | b"lineChart"
16291                    | b"pieChart"
16292                    | b"doughnutChart"
16293                    | b"areaChart"
16294                    | b"scatterChart"
16295                    | b"radarChart"
16296                    | b"catAx"
16297                    | b"valAx"
16298                    | b"dateAx"
16299                    | b"serAx"
16300            ),
16301            b"pieChart" => !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng"),
16302            b"doughnutChart" => {
16303                !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng" | b"holeSize")
16304            }
16305            b"areaChart" => !matches!(child, b"grouping" | b"ser" | b"dLbls" | b"axId"),
16306            b"scatterChart" => !matches!(child, b"scatterStyle" | b"ser" | b"dLbls" | b"axId"),
16307            b"radarChart" => !matches!(child, b"radarStyle" | b"ser" | b"dLbls" | b"axId"),
16308            b"title" | b"legend" => true,
16309            _ => false,
16310        }
16311    }
16312
16313    fn is_chartml_preservation_parent(parent: &[u8]) -> bool {
16314        matches!(
16315            parent,
16316            b"chartSpace"
16317                | b"chart"
16318                | b"title"
16319                | b"plotArea"
16320                | b"pieChart"
16321                | b"doughnutChart"
16322                | b"areaChart"
16323                | b"scatterChart"
16324                | b"radarChart"
16325                | b"legend"
16326                | b"autoTitleDeleted"
16327                | b"plotVisOnly"
16328                | b"dispBlanksAs"
16329        )
16330    }
16331
16332    #[test]
16333    fn oxml_chart_is_an_explicit_publication_candidate() {
16334        let manifest = include_str!("../Cargo.toml");
16335        assert!(manifest.contains("name = \"oxml-chart\""));
16336        assert!(manifest.contains("version = \"0.14.1\""));
16337        assert!(manifest.contains("publish = true"));
16338        for dependency in [
16339            "oxml-core.workspace",
16340            "oxml-drawing.workspace",
16341            "quick-xml.workspace",
16342        ] {
16343            assert!(manifest.contains(dependency));
16344        }
16345        assert!(!manifest.contains("rpptx.workspace"));
16346        assert!(!manifest.contains("rdocx.workspace"));
16347    }
16348
16349    fn first_series_xml(xml: &[u8]) -> Option<Vec<u8>> {
16350        let mut reader = Reader::from_reader(xml);
16351        let mut buffer = Vec::new();
16352        loop {
16353            match reader.read_event_into(&mut buffer).ok()? {
16354                Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
16355                    return capture_element(&mut reader, &element).ok();
16356                }
16357                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
16358                    return capture_empty_element(&element).ok();
16359                }
16360                Event::Eof => return None,
16361                _ => {}
16362            }
16363            buffer.clear();
16364        }
16365    }
16366
16367    fn replace_once(haystack: &[u8], needle: &[u8], replacement: &[u8]) -> Vec<u8> {
16368        let offset = haystack
16369            .windows(needle.len())
16370            .position(|window| window == needle)
16371            .expect("serialized source series occurs in chart part");
16372        assert!(
16373            !haystack[offset + needle.len()..]
16374                .windows(needle.len())
16375                .any(|window| window == needle),
16376            "source series unexpectedly occurs more than once"
16377        );
16378        let mut result = Vec::with_capacity(haystack.len() - needle.len() + replacement.len());
16379        result.extend_from_slice(&haystack[..offset]);
16380        result.extend_from_slice(replacement);
16381        result.extend_from_slice(&haystack[offset + needle.len()..]);
16382        result
16383    }
16384
16385    fn assert_command_version(program: &str, arguments: &[&str], expected: &str) {
16386        let output = Command::new(program)
16387            .args(arguments)
16388            .output()
16389            .unwrap_or_else(|error| panic!("run {program} version check: {error}"));
16390        assert!(
16391            output.status.success(),
16392            "{program} version check failed: {}",
16393            String::from_utf8_lossy(&output.stderr)
16394        );
16395        let stdout = String::from_utf8(output.stdout).expect("version stdout is UTF-8");
16396        let stderr = String::from_utf8(output.stderr).expect("version stderr is UTF-8");
16397        let actual = if stdout.trim().is_empty() {
16398            stderr.trim()
16399        } else {
16400            stdout.trim()
16401        };
16402        assert_eq!(
16403            actual.lines().next(),
16404            Some(expected),
16405            "{program} version drift"
16406        );
16407    }
16408
16409    fn sha256(path: &Path) -> String {
16410        let output = Command::new("shasum")
16411            .args(["-a", "256"])
16412            .arg(path)
16413            .output()
16414            .unwrap_or_else(|error| panic!("{}: run shasum: {error}", path.display()));
16415        assert!(
16416            output.status.success(),
16417            "{}: shasum failed: {}",
16418            path.display(),
16419            String::from_utf8_lossy(&output.stderr)
16420        );
16421        String::from_utf8(output.stdout)
16422            .expect("shasum output is UTF-8")
16423            .split_whitespace()
16424            .next()
16425            .expect("shasum digest")
16426            .to_owned()
16427    }
16428
16429    fn workspace_root() -> PathBuf {
16430        Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
16431    }
16432
16433    fn corpus_dir() -> PathBuf {
16434        std::env::var_os("RDOCX_PPTX_CORPUS_DIR")
16435            .map(PathBuf::from)
16436            .unwrap_or_else(|| workspace_root().join("corpus/pptx"))
16437    }
16438
16439    fn require_or_skip_corpus() -> Option<PathBuf> {
16440        let corpus = corpus_dir();
16441        if corpus.is_dir() {
16442            return Some(corpus);
16443        }
16444        assert_ne!(
16445            std::env::var_os("RDOCX_PPTX_CORPUS_REQUIRED").as_deref(),
16446            Some(std::ffi::OsStr::new("1")),
16447            "the pinned corpus is required but {} does not exist",
16448            corpus.display()
16449        );
16450        eprintln!(
16451            "ChartML corpus gate skipped because {} is absent",
16452            corpus.display()
16453        );
16454        None
16455    }
16456
16457    fn verify_fetched_corpus(corpus: &Path) {
16458        let status = Command::new("python3")
16459            .arg(workspace_root().join("scripts/fetch_pptx_corpus.py"))
16460            .arg("--check")
16461            .env("RDOCX_PPTX_CORPUS_DIR", corpus)
16462            .status()
16463            .expect("run corpus verifier");
16464        assert!(status.success(), "pinned corpus verification failed");
16465    }
16466
16467    fn manifest_paths() -> Vec<&'static str> {
16468        let mut lines = MANIFEST.lines();
16469        assert_eq!(lines.next(), Some("path\tproducer\tsha256\turl"));
16470        let paths: Vec<_> = lines
16471            .map(|line| line.split('\t').next().expect("manifest path"))
16472            .collect();
16473        assert_eq!(paths.len(), EXPECTED_DECKS);
16474        paths
16475    }
16476
16477    fn is_chart_part(part: &str) -> bool {
16478        let Some(stem) = part
16479            .strip_prefix("/ppt/charts/chart")
16480            .and_then(|name| name.strip_suffix(".xml"))
16481        else {
16482            return false;
16483        };
16484        !stem.is_empty() && stem.bytes().all(|byte| byte.is_ascii_digit())
16485    }
16486
16487    fn plot_series(index: u32) -> String {
16488        format!(
16489            r#"<q:ser><q:idx val="{index}"/><q:order val="{index}"/><q:cat><q:strRef><q:f>Sheet1!$A$2:$A$3</q:f><q:strCache><q:ptCount val="2"/><q:pt idx="0"><q:v>North</q:v></q:pt><q:pt idx="1"><q:v>South</q:v></q:pt></q:strCache></q:strRef></q:cat><q:val><q:numRef><q:f>Sheet1!$B$2:$B$3</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="2"/><q:pt idx="0"><q:v>1</q:v></q:pt><q:pt idx="1"><q:v>2</q:v></q:pt></q:numCache></q:numRef></q:val></q:ser>"#
16490        )
16491    }
16492
16493    fn sparse_category_series() -> &'static str {
16494        r#"<q:ser><q:idx val="0"/><q:order val="0"/><q:cat><q:strRef><q:f>Sheet1!$A$2:$A$4</q:f><q:strCache><q:ptCount val="3"/><q:pt idx="0"><q:v>North</q:v></q:pt><q:pt idx="2"><q:v>West</q:v></q:pt></q:strCache></q:strRef></q:cat><q:val><q:numRef><q:f>Sheet1!$B$2:$B$4</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="3"/><q:pt idx="0"><q:v>1</q:v></q:pt><q:pt idx="2"><q:v>2</q:v></q:pt></q:numCache></q:numRef></q:val></q:ser>"#
16495    }
16496
16497    fn sparse_scatter_series() -> &'static str {
16498        r#"<q:ser><q:idx val="0"/><q:order val="0"/><q:xVal><q:numRef><q:f>Sheet1!$A$2:$A$5</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="4"/><q:pt idx="0"><q:v>10</q:v></q:pt><q:pt idx="2"><q:v>30</q:v></q:pt><q:pt idx="3"><q:v>40</q:v></q:pt></q:numCache></q:numRef></q:xVal><q:yVal><q:numRef><q:f>Sheet1!$B$2:$B$5</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="4"/><q:pt idx="0"><q:v>100</q:v></q:pt><q:pt idx="1"><q:v>200</q:v></q:pt><q:pt idx="3"><q:v>400</q:v></q:pt></q:numCache></q:numRef></q:yVal></q:ser>"#
16499    }
16500
16501    fn chart_with_plot(plot: &str) -> String {
16502        format!(
16503            r#"<q:chartSpace xmlns:q="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}" xmlns:x="urn:producer"><q:chart><q:plotArea>{plot}<q:catAx><q:axId val="-1884094432"/><q:scaling/><q:axPos val="b"/><q:crossAx val="-1884097184"/></q:catAx><q:valAx><q:axId val="-1884097184"/><q:scaling/><q:axPos val="l"/><q:crossAx val="-1884094432"/></q:valAx></q:plotArea></q:chart></q:chartSpace>"#
16504        )
16505    }
16506
16507    fn chart_with_optional_axes(plot: &str, axes: bool) -> String {
16508        let axis_xml = if axes {
16509            r#"<q:catAx><q:axId val="-1884094432"/><q:scaling/><q:axPos val="b"/><q:crossAx val="-1884097184"/></q:catAx><q:valAx><q:axId val="-1884097184"/><q:scaling/><q:axPos val="l"/><q:crossAx val="-1884094432"/></q:valAx>"#
16510        } else {
16511            ""
16512        };
16513        format!(
16514            r#"<q:chartSpace xmlns:q="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}" xmlns:x="urn:producer"><q:chart><q:plotArea>{plot}{axis_xml}</q:plotArea></q:chart></q:chartSpace>"#
16515        )
16516    }
16517
16518    fn numeric_plot_series(index: u32) -> String {
16519        format!(
16520            r#"<q:ser><q:idx val="{index}"/><q:order val="{index}"/><q:xVal><q:numRef><q:f>Sheet1!$A$2:$A$3</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="2"/><q:pt idx="0"><q:v>1</q:v></q:pt><q:pt idx="1"><q:v>2</q:v></q:pt></q:numCache></q:numRef></q:xVal><q:yVal><q:numRef><q:f>Sheet1!$B$2:$B$3</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="2"/><q:pt idx="0"><q:v>4</q:v></q:pt><q:pt idx="1"><q:v>8</q:v></q:pt></q:numCache></q:numRef></q:yVal></q:ser>"#
16521        )
16522    }
16523
16524    fn scatter_without_x(index: u32) -> String {
16525        format!(
16526            r#"<q:ser><q:idx val="{index}"/><q:order val="{index}"/><q:yVal><q:numRef><q:f>Sheet1!$B$2</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="1"/><q:pt idx="0"><q:v>4</q:v></q:pt></q:numCache></q:numRef></q:yVal></q:ser>"#
16527        )
16528    }
16529
16530    fn scatter_without_y(index: u32) -> String {
16531        format!(
16532            r#"<q:ser><q:idx val="{index}"/><q:order val="{index}"/><q:xVal><q:numRef><q:f>Sheet1!$A$2</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="1"/><q:pt idx="0"><q:v>1</q:v></q:pt></q:numCache></q:numRef></q:xVal></q:ser>"#
16533        )
16534    }
16535
16536    fn remaining_plot_fixtures() -> Vec<(&'static str, String, bool)> {
16537        let series = plot_series(0);
16538        let scatter = numeric_plot_series(0);
16539        vec![
16540            (
16541                "pieChart",
16542                format!(
16543                    r#"<q:pieChart x:keep="pie"><q:varyColors val="1"/>{series}<q:dLbls><q:showVal val="1"/></q:dLbls><q:firstSliceAng val="30"/><q:extLst><x:pie/></q:extLst></q:pieChart>"#
16544                ),
16545                false,
16546            ),
16547            (
16548                "doughnutChart",
16549                format!(
16550                    r#"<q:doughnutChart x:keep="doughnut"><q:varyColors val="1"/>{series}<q:dLbls><q:showPercent val="1"/></q:dLbls><q:firstSliceAng val="45"/><q:holeSize val="60"/><q:extLst><x:doughnut/></q:extLst></q:doughnutChart>"#
16551                ),
16552                false,
16553            ),
16554            (
16555                "areaChart",
16556                format!(
16557                    r#"<q:areaChart x:keep="area"><q:grouping val="standard"/><q:varyColors val="0"/>{series}<q:dLbls><q:showVal val="1"/></q:dLbls><q:dropLines><x:line/></q:dropLines><q:axId val="-1884094432"/><q:axId val="-1884097184"/><q:extLst><x:area/></q:extLst></q:areaChart>"#
16558                ),
16559                true,
16560            ),
16561            (
16562                "scatterChart",
16563                format!(
16564                    r#"<q:scatterChart x:keep="scatter"><q:scatterStyle val="marker"/><q:varyColors val="0"/>{scatter}<q:dLbls><q:showVal val="1"/></q:dLbls><q:axId val="-1884094432"/><q:axId val="-1884097184"/><q:extLst><x:scatter/></q:extLst></q:scatterChart>"#
16565                ),
16566                true,
16567            ),
16568            (
16569                "radarChart",
16570                format!(
16571                    r#"<q:radarChart x:keep="radar"><q:radarStyle val="marker"/><q:varyColors val="0"/>{series}<q:dLbls><q:showVal val="1"/></q:dLbls><q:axId val="-1884094432"/><q:axId val="-1884097184"/><q:extLst><x:radar/></q:extLst></q:radarChart>"#
16572                ),
16573                true,
16574            ),
16575        ]
16576    }
16577
16578    fn bar_plot(extra: &str) -> String {
16579        format!(
16580            r#"<q:barChart x:keep="bar"><q:barDir val="col"/><q:grouping val="clustered"/><q:varyColors val="1"/>{}<q:dLbls><q:showVal val="1"/></q:dLbls><q:gapWidth val="150"/><q:overlap val="0"/><q:serLines><x:line/></q:serLines><q:axId val="-1884094432"/><q:axId val="-1884097184"/>{extra}</q:barChart>"#,
16581            plot_series(0)
16582        )
16583    }
16584
16585    fn line_plot(extra: &str) -> String {
16586        format!(
16587            r#"<q:lineChart x:keep="line"><q:grouping val="standard"/><q:varyColors val="0"/>{}<q:dLbls><q:showVal val="1"/></q:dLbls><q:dropLines><x:line/></q:dropLines><q:marker val="1"/><q:smooth val="0"/><q:axId val="-1884094432"/><q:axId val="-1884097184"/>{extra}</q:lineChart>"#,
16588            plot_series(0)
16589        )
16590    }
16591
16592    #[test]
16593    fn bar_and_line_plots_round_trip_and_render() {
16594        for (xml, is_bar) in [
16595            (chart_with_plot(&bar_plot("")), true),
16596            (chart_with_plot(&line_plot("")), false),
16597        ] {
16598            let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16599            let plots = parsed.chart.plot_area.plots().unwrap();
16600            assert_eq!(plots.len(), 1);
16601            assert_eq!(matches!(plots[0], Plot::Bar { .. }), is_bar);
16602            assert_eq!(matches!(plots[0], Plot::Line { .. }), !is_bar);
16603            assert_eq!(parsed.chart.plot_area.axes().unwrap().len(), 2);
16604            let written = parsed.to_xml().unwrap();
16605            let reparsed = CT_ChartSpace::from_xml(&written).unwrap();
16606            assert_eq!(parsed, reparsed);
16607        }
16608        if let Some(corpus) = require_or_skip_corpus() {
16609            verify_bar_and_line_viewer_gate(&corpus);
16610        }
16611    }
16612
16613    #[test]
16614    fn ppm_parser_preserves_whitespace_valued_first_pixels() {
16615        for first_pixel in *b" \n\r\t" {
16616            let mut ppm = b"P6\n1 1\n255\n".to_vec();
16617            ppm.extend_from_slice(&[first_pixel, 0x7f, 0xff]);
16618            let (width, height, pixels) = ppm_pixels(&ppm);
16619            assert_eq!((width, height), (1, 1));
16620            assert_eq!(pixels, [first_pixel, 0x7f, 0xff]);
16621        }
16622
16623        let ppm = b"P6\r\n1 1\r\n255\r\n\x7f\xff";
16624        assert_eq!(ppm_pixels(ppm).2, [b'\n', 0x7f, 0xff]);
16625    }
16626
16627    #[test]
16628    fn bar_and_line_plots_write_fixed_prefixes_in_schema_order() {
16629        for xml in [
16630            chart_with_plot(&bar_plot(r#"<q:extLst><x:bar-tail/></q:extLst>"#)),
16631            chart_with_plot(&line_plot(r#"<q:extLst><x:line-tail/></q:extLst>"#)),
16632        ] {
16633            let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16634            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
16635            assert!(written.contains("<c:barChart") || written.contains("<c:lineChart"));
16636            assert!(!written.contains("<q:barChart"));
16637            assert!(!written.contains("<q:lineChart"));
16638            if written.contains("<c:barChart") {
16639                let positions: Vec<_> = [
16640                    "<c:barDir",
16641                    "<c:grouping",
16642                    "<q:varyColors",
16643                    "<c:ser",
16644                    "<c:dLbls",
16645                    "<c:gapWidth",
16646                    "<c:overlap",
16647                    "<q:serLines",
16648                    "<c:axId",
16649                    "<q:extLst",
16650                ]
16651                .iter()
16652                .map(|tag| written.find(tag).unwrap())
16653                .collect();
16654                assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
16655            } else {
16656                let positions: Vec<_> = [
16657                    "<c:grouping",
16658                    "<q:varyColors",
16659                    "<c:ser",
16660                    "<c:dLbls",
16661                    "<q:dropLines",
16662                    "<c:marker",
16663                    "<c:smooth",
16664                    "<c:axId",
16665                    "<q:extLst",
16666                ]
16667                .iter()
16668                .map(|tag| written.find(tag).unwrap())
16669                .collect();
16670                assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
16671            }
16672            assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
16673        }
16674
16675        for plot in [bar_plot(""), line_plot("")] {
16676            let first = plot_series(0);
16677            let with_series_boundary = plot.replacen(
16678                &first,
16679                &format!(
16680                    "{first}<!--between-series--><?series-boundary?>\n  {}",
16681                    plot_series(1)
16682                ),
16683                1,
16684            );
16685            let with_axis_boundary = with_series_boundary.replacen(
16686                r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
16687                r#"<q:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
16688  <q:axId val="-1884097184"/>"#,
16689                1,
16690            );
16691            let parsed =
16692                CT_ChartSpace::from_xml(chart_with_plot(&with_axis_boundary).as_bytes()).unwrap();
16693            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
16694            assert!(written.contains("</c:ser><!--between-series--><?series-boundary?>\n  <c:ser"));
16695            assert!(written.contains(
16696                r#"<c:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
16697  <c:axId val="-1884097184"/>"#
16698            ));
16699            assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
16700        }
16701    }
16702
16703    #[test]
16704    fn malformed_bar_and_line_plots_return_errors_without_panicking() {
16705        let valid_series = plot_series(0);
16706        let cases = [
16707            format!(r#"<q:barChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
16708            format!(r#"<q:barChart><q:barDir val="diagonal"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
16709            format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{valid_series}<q:gapWidth val="501"/><q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
16710            format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{valid_series}<q:overlap val="-101"/><q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
16711            format!(r#"<q:lineChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
16712            format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:marker val="maybe"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
16713            r#"<q:lineChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#.to_owned(),
16714            format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:axId val="1"/><q:axId val="1"/></q:lineChart>"#),
16715            format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/><q:grouping val="stacked"/>{valid_series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:barChart>"#),
16716            format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
16717        ];
16718        for plot in cases {
16719            let xml = chart_with_plot(&plot);
16720            let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
16721            assert!(result.is_ok(), "plot parser panicked for {plot}");
16722            assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
16723        }
16724
16725        let missing_axis = format!(
16726            r#"<q:chartSpace xmlns:q="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}"><q:chart><q:plotArea>{}<q:catAx><q:axId val="-1884094432"/><q:scaling/><q:axPos val="b"/><q:crossAx val="-1884097184"/></q:catAx></q:plotArea></q:chart></q:chartSpace>"#,
16727            bar_plot("")
16728        );
16729        assert!(CT_ChartSpace::from_xml(missing_axis.as_bytes()).is_err());
16730
16731        let values =
16732            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
16733        let series = vec![Series::new(0, 0, values)];
16734        let ids = [AxisId::new(1).unwrap(), AxisId::new(2).unwrap()];
16735        let mut invalid =
16736            Plot::bar(BarDirection::Column, BarGrouping::Clustered, series, ids).unwrap();
16737        if let Plot::Bar { gap_width, .. } = &mut invalid {
16738            *gap_width = 501;
16739        }
16740        assert!(invalid.validate().is_err());
16741    }
16742
16743    #[test]
16744    fn unsupported_and_combo_plots_remain_byte_preserved() {
16745        let bar = bar_plot("");
16746        let line = line_plot("");
16747        let combo = chart_with_plot(&format!("<!--before-->{bar}<?between?>{line}<!--after-->"));
16748        let parsed = CT_ChartSpace::from_xml(combo.as_bytes()).unwrap();
16749        assert!(parsed.chart.plot_area.plots().is_err());
16750        let written = parsed.to_xml().unwrap();
16751        for raw in [
16752            bar.as_bytes(),
16753            line.as_bytes(),
16754            br#"<!--before-->"#.as_slice(),
16755            br#"<?between?>"#.as_slice(),
16756            br#"<!--after-->"#.as_slice(),
16757        ] {
16758            assert!(written.windows(raw.len()).any(|window| window == raw));
16759        }
16760
16761        let unsupported = chart_with_plot(
16762            r#"<q:bar3DChart x:keep="three"><q:barDir val="col"/><q:ser><x:opaque/></q:ser></q:bar3DChart>"#,
16763        );
16764        let parsed = CT_ChartSpace::from_xml(unsupported.as_bytes()).unwrap();
16765        assert!(parsed.chart.plot_area.plots().is_err());
16766        assert!(
16767            String::from_utf8(parsed.to_xml().unwrap())
16768                .unwrap()
16769                .contains(r#"<q:bar3DChart x:keep="three">"#)
16770        );
16771    }
16772
16773    #[test]
16774    fn public_plot_edits_preserve_axes_and_unselected_payloads() {
16775        let xml = chart_with_plot(&bar_plot(
16776            r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#,
16777        ));
16778        let mut parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16779        let axes_before = parsed.chart.plot_area.axes().unwrap();
16780        match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
16781            Plot::Bar {
16782                gap_width,
16783                overlap,
16784                series,
16785                ..
16786            } => {
16787                *gap_width = 225;
16788                *overlap = -25;
16789                series[0].index = 7;
16790                series[0].values.values[0] = 9.0;
16791            }
16792            _ => panic!("expected bar plot"),
16793        }
16794        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
16795        assert!(written.contains(r#"<c:gapWidth val="225"/>"#));
16796        assert!(written.contains(r#"<c:overlap val="-25"/>"#));
16797        assert!(written.contains("<c:v>9</c:v>"));
16798        assert!(written.contains("<q:varyColors val=\"1\"/>"));
16799        assert!(written.find("<q:varyColors").unwrap() < written.find("<c:ser").unwrap());
16800        assert!(written.contains("<q:serLines><x:line/></q:serLines>"));
16801        assert!(written.contains(r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#));
16802        let reparsed = CT_ChartSpace::from_xml(written.as_bytes()).unwrap();
16803        assert_eq!(axes_before, reparsed.chart.plot_area.axes().unwrap());
16804
16805        let first = plot_series(0);
16806        let colliding_series =
16807            bar_plot("").replacen(&first, &format!("{first}{}", plot_series(1)), 1);
16808        let mut collision =
16809            CT_ChartSpace::from_xml(chart_with_plot(&colliding_series).as_bytes()).unwrap();
16810        if let Plot::Bar { series, .. } = &mut collision.chart.plot_area.plots_mut().unwrap()[0] {
16811            series[0].index = series[1].index;
16812            series[0].order = series[1].order;
16813        }
16814        let collision = String::from_utf8(collision.to_xml().unwrap()).unwrap();
16815        assert!(collision.find("<q:varyColors").unwrap() < collision.find("<c:ser").unwrap());
16816
16817        let mut inserted = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16818        if let Plot::Bar { series, .. } = &mut inserted.chart.plot_area.plots_mut().unwrap()[0] {
16819            let mut new_series = series[0].clone();
16820            new_series.index = 99;
16821            new_series.order = 99;
16822            series.insert(0, new_series);
16823        }
16824        let inserted = String::from_utf8(inserted.to_xml().unwrap()).unwrap();
16825        assert!(inserted.find("<q:varyColors").unwrap() < inserted.find("<c:ser").unwrap());
16826
16827        let axis_payload = bar_plot("").replacen(
16828            r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
16829            r#"<q:axId val="-1884094432" x:slot="category"/><!--axis-anchor--><q:axId val="-1884097184" x:slot="value"/>"#,
16830            1,
16831        );
16832        let mut swapped =
16833            CT_ChartSpace::from_xml(chart_with_plot(&axis_payload).as_bytes()).unwrap();
16834        if let Plot::Bar { axis_ids, .. } = &mut swapped.chart.plot_area.plots_mut().unwrap()[0] {
16835            axis_ids.swap(0, 1);
16836        }
16837        let swapped = String::from_utf8(swapped.to_xml().unwrap()).unwrap();
16838        let anchor = swapped.find("<!--axis-anchor-->").unwrap();
16839        let value_axis = swapped
16840            .find(r#"<c:axId val="-1884097184" x:slot="value"/>"#)
16841            .unwrap();
16842        let category_axis = swapped
16843            .find(r#"<c:axId val="-1884094432" x:slot="category"/>"#)
16844            .unwrap();
16845        let series_lines = swapped.find("<q:serLines").unwrap();
16846        assert!(series_lines < value_axis && series_lines < category_axis);
16847        assert!(anchor < value_axis && value_axis < category_axis);
16848
16849        let mut replaced = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16850        let Plot::Bar {
16851            series, axis_ids, ..
16852        } = &replaced.chart.plot_area.plots().unwrap()[0]
16853        else {
16854            panic!("expected bar plot");
16855        };
16856        let line = Plot::line(Grouping::Standard, series.clone(), *axis_ids).unwrap();
16857        replaced.chart.plot_area.plots_mut().unwrap()[0] = line;
16858        assert!(replaced.to_xml().is_err());
16859    }
16860
16861    #[test]
16862    fn every_corpus_bar_and_line_plot_round_trips_structurally() {
16863        let Some(corpus) = require_or_skip_corpus() else {
16864            return;
16865        };
16866        verify_fetched_corpus(&corpus);
16867        let mut typed_bar_count = 0usize;
16868        let mut typed_line_count = 0usize;
16869        let mut opaque_bar_count = 0usize;
16870        let mut opaque_line_count = 0usize;
16871        let mut opaque_combo_count = 0usize;
16872        for path in manifest_paths() {
16873            let package = OpcPackage::open(corpus.join(path))
16874                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
16875            for (part, xml) in &package.parts {
16876                if !is_chart_part(part) {
16877                    continue;
16878                }
16879                let chart = CT_ChartSpace::from_xml(xml)
16880                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
16881                match chart.chart.plot_area.plots() {
16882                    Ok(plots) => {
16883                        for plot in plots {
16884                            match plot {
16885                                Plot::Bar { .. } => typed_bar_count += 1,
16886                                Plot::Line { .. } => typed_line_count += 1,
16887                                _ => {}
16888                            }
16889                        }
16890                    }
16891                    Err(_) => {
16892                        let mut preserved_plot_count = 0usize;
16893                        for raw in chart.chart.plot_area.raw_children.at(0) {
16894                            if let Ok(Some(local)) = super::chart_root_local(
16895                                raw,
16896                                &chart.chart.plot_area.namespace_bindings,
16897                            ) {
16898                                match local.as_slice() {
16899                                    b"barChart" => {
16900                                        opaque_bar_count += 1;
16901                                        preserved_plot_count += 1;
16902                                    }
16903                                    b"lineChart" => {
16904                                        opaque_line_count += 1;
16905                                        preserved_plot_count += 1;
16906                                    }
16907                                    _ => {}
16908                                }
16909                            }
16910                        }
16911                        if preserved_plot_count > 1 {
16912                            opaque_combo_count += 1;
16913                        }
16914                    }
16915                }
16916                let written = chart
16917                    .to_xml()
16918                    .unwrap_or_else(|error| panic!("{path} {part}: chart write failed: {error}"));
16919                let reparsed = CT_ChartSpace::from_xml(&written).unwrap_or_else(|error| {
16920                    panic!("{path} {part}: written chart parse failed: {error}")
16921                });
16922                assert_eq!(chart, reparsed, "{path} {part}: chart model changed");
16923            }
16924        }
16925        assert_eq!(typed_bar_count, 11, "typed corpus bar coverage changed");
16926        assert_eq!(typed_line_count, 2, "typed corpus line coverage changed");
16927        assert_eq!(opaque_bar_count, 1, "preserved combo bar coverage changed");
16928        assert_eq!(
16929            opaque_line_count, 1,
16930            "preserved combo line coverage changed"
16931        );
16932        assert_eq!(opaque_combo_count, 1, "preserved combo coverage changed");
16933        assert_eq!(typed_bar_count + opaque_bar_count, 12);
16934        assert_eq!(typed_line_count + opaque_line_count, 3);
16935        eprintln!(
16936            "ChartML plot corpus gate checked {typed_bar_count} typed bar, {typed_line_count} typed line, and one preserved bar-line combination"
16937        );
16938    }
16939
16940    fn verify_remaining_plot_viewer_gate(corpus: &Path) {
16941        verify_fetched_corpus(corpus);
16942        assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
16943        assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
16944        let source = corpus.join("bar-chart.pptx");
16945        let source_sha = sha256(&source);
16946        let temp_root = std::env::temp_dir().join(format!(
16947            "oxml-chart-f122-viewer-gate-{}",
16948            std::process::id()
16949        ));
16950        if temp_root.exists() {
16951            fs::remove_dir_all(&temp_root).expect("remove stale F-122 evidence");
16952        }
16953        fs::create_dir_all(&temp_root).expect("create F-122 evidence root");
16954
16955        for (kind, plot, axes) in remaining_plot_fixtures() {
16956            let mut package = OpcPackage::open(&source).expect("open F-122 viewer source");
16957            package.set_part(
16958                "/ppt/charts/chart1.xml",
16959                CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes())
16960                    .expect("parse F-122 viewer chart")
16961                    .to_xml()
16962                    .expect("serialize F-122 viewer chart"),
16963            );
16964            let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
16965            package.save(&unbound).expect("save F-122 viewer candidate");
16966            let candidate_sha = sha256(&unbound);
16967            let evidence = temp_root.join(format!("{source_sha}-{candidate_sha}"));
16968            fs::create_dir(&evidence).expect("create SHA-bound F-122 evidence directory");
16969            let candidate = evidence.join(format!("{kind}-candidate.pptx"));
16970            fs::rename(&unbound, &candidate).expect("bind F-122 candidate to SHA");
16971            assert_eq!(sha256(&candidate), candidate_sha);
16972            let render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
16973            let bytes = fs::read(&render).expect("read F-122 viewer PPM");
16974            let (width, height, pixels) = ppm_pixels(&bytes);
16975            let left = width / 5;
16976            let right = width * 4 / 5;
16977            let top = height * 3 / 20;
16978            let bottom = height * 17 / 20;
16979            let mut nonblank = 0usize;
16980            for y in top..bottom {
16981                for x in left..right {
16982                    let offset = (y * width + x) * 3;
16983                    if pixels[offset..offset + 3]
16984                        .iter()
16985                        .any(|channel| *channel < 245)
16986                    {
16987                        nonblank += 1;
16988                    }
16989                }
16990            }
16991            const CHART_RECTANGLE_NONBLANK_THRESHOLD: usize = 1_000;
16992            assert!(
16993                nonblank >= CHART_RECTANGLE_NONBLANK_THRESHOLD,
16994                "{kind}: chart rectangle [{left},{top}) to [{right},{bottom}) contains {nonblank} nonblank pixels, below {CHART_RECTANGLE_NONBLANK_THRESHOLD}"
16995            );
16996            eprintln!(
16997                "F-122 {kind} viewer gate source deck {source_sha}, candidate deck {candidate_sha}, render {}, chart rectangle [{left},{top}) to [{right},{bottom}), RGB<245 nonblank pixels {nonblank} >= {CHART_RECTANGLE_NONBLANK_THRESHOLD}",
16998                sha256(&render)
16999            );
17000        }
17001        fs::remove_dir_all(&temp_root).expect("remove F-122 viewer evidence");
17002    }
17003
17004    fn verify_bar_and_line_viewer_gate(corpus: &Path) {
17005        verify_fetched_corpus(corpus);
17006        assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
17007        assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
17008        let temp_root = std::env::temp_dir().join(format!(
17009            "oxml-chart-f121-viewer-gate-{}",
17010            std::process::id()
17011        ));
17012        if temp_root.exists() {
17013            fs::remove_dir_all(&temp_root).expect("remove stale F-121 evidence");
17014        }
17015        fs::create_dir_all(&temp_root).expect("create F-121 evidence root");
17016
17017        for (kind, deck) in [("bar", "bar-chart.pptx"), ("line", "line-chart.pptx")] {
17018            let source = corpus.join(deck);
17019            let original_sha = sha256(&source);
17020            let mut package = OpcPackage::open(&source)
17021                .unwrap_or_else(|error| panic!("{deck}: open viewer source: {error}"));
17022            let chart_part = "/ppt/charts/chart1.xml";
17023            let original_parts = package.parts.clone();
17024            let parsed = CT_ChartSpace::from_xml(
17025                package
17026                    .get_part(chart_part)
17027                    .unwrap_or_else(|| panic!("{deck}: missing {chart_part}")),
17028            )
17029            .unwrap_or_else(|error| panic!("{deck}: parse viewer chart: {error}"));
17030            assert!(
17031                parsed.chart.plot_area.plots().is_ok(),
17032                "{deck}: plot is opaque"
17033            );
17034            package.set_part(
17035                chart_part,
17036                parsed
17037                    .to_xml()
17038                    .unwrap_or_else(|error| panic!("{deck}: serialize viewer chart: {error}")),
17039            );
17040            for (part, bytes) in &original_parts {
17041                if part == chart_part {
17042                    assert_ne!(package.parts[part], *bytes, "{deck}: chart did not rewrite");
17043                } else {
17044                    assert_eq!(package.parts[part], *bytes, "{deck}: changed part {part}");
17045                }
17046            }
17047
17048            let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
17049            package
17050                .save(&unbound)
17051                .unwrap_or_else(|error| panic!("{deck}: save viewer candidate: {error}"));
17052            let candidate_sha = sha256(&unbound);
17053            let evidence = temp_root.join(format!("{original_sha}-{candidate_sha}"));
17054            fs::create_dir(&evidence).expect("create SHA-bound F-121 evidence directory");
17055            let original = evidence.join(format!("{kind}-original.pptx"));
17056            let candidate = evidence.join(format!("{kind}-candidate.pptx"));
17057            fs::copy(&source, &original).expect("copy SHA-bound viewer original");
17058            fs::rename(&unbound, &candidate).expect("bind viewer candidate to SHA");
17059            assert_eq!(sha256(&original), original_sha);
17060            assert_eq!(sha256(&candidate), candidate_sha);
17061
17062            let original_render = render_deck_to_ppm(&original, &evidence, kind, "original");
17063            let candidate_render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
17064            let original_bytes = fs::read(&original_render).expect("read original PPM");
17065            let candidate_bytes = fs::read(&candidate_render).expect("read candidate PPM");
17066            let normalized_mae = normalized_ppm_mae(&original_bytes, &candidate_bytes);
17067            assert!(
17068                normalized_mae <= PLOT_RENDER_NORMALIZED_MAE_THRESHOLD,
17069                "{deck}: normalized RGB MAE {normalized_mae:.8} exceeds {:.8}",
17070                PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
17071            );
17072            eprintln!(
17073                "F-121 {kind} viewer gate original deck {original_sha}, candidate deck {candidate_sha}, original render {}, candidate render {}, normalized RGB MAE {normalized_mae:.8} <= {:.8}",
17074                sha256(&original_render),
17075                sha256(&candidate_render),
17076                PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
17077            );
17078        }
17079        fs::remove_dir_all(&temp_root).expect("remove F-121 viewer evidence");
17080    }
17081
17082    fn render_deck_to_ppm(deck: &Path, evidence: &Path, kind: &str, side: &str) -> PathBuf {
17083        let profile = std::env::temp_dir().join(format!(
17084            "oxml-chart-f121-{kind}-{side}-profile-{}",
17085            std::process::id()
17086        ));
17087        if profile.exists() {
17088            fs::remove_dir_all(&profile).expect("remove stale F-121 viewer profile");
17089        }
17090        let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
17091        let conversion = Command::new("soffice")
17092            .args([
17093                "--headless",
17094                &profile_argument,
17095                "--convert-to",
17096                "pdf:impress_pdf_Export",
17097                "--outdir",
17098            ])
17099            .arg(evidence)
17100            .arg(deck)
17101            .output()
17102            .expect("run pinned LibreOffice F-121 viewer gate");
17103        assert!(
17104            conversion.status.success(),
17105            "{}: LibreOffice conversion failed: {}",
17106            deck.display(),
17107            String::from_utf8_lossy(&conversion.stderr)
17108        );
17109        let pdf = deck.with_extension("pdf");
17110        assert!(
17111            pdf.is_file(),
17112            "LibreOffice did not create {}",
17113            pdf.display()
17114        );
17115        let prefix = evidence.join(format!("{kind}-{side}"));
17116        let raster = Command::new("pdftoppm")
17117            .args(["-f", "1", "-singlefile", "-r", "150"])
17118            .arg(&pdf)
17119            .arg(&prefix)
17120            .output()
17121            .expect("run pinned Poppler F-121 raster gate");
17122        assert!(
17123            raster.status.success(),
17124            "{}: pdftoppm failed: {}",
17125            pdf.display(),
17126            String::from_utf8_lossy(&raster.stderr)
17127        );
17128        if profile.exists() {
17129            fs::remove_dir_all(&profile).expect("remove F-121 viewer profile");
17130        }
17131        prefix.with_extension("ppm")
17132    }
17133
17134    fn normalized_ppm_mae(left: &[u8], right: &[u8]) -> f64 {
17135        let (left_width, left_height, left_pixels) = ppm_pixels(left);
17136        let (right_width, right_height, right_pixels) = ppm_pixels(right);
17137        assert_eq!((left_width, left_height), (right_width, right_height));
17138        assert_eq!(left_pixels.len(), right_pixels.len());
17139        let difference: u64 = left_pixels
17140            .iter()
17141            .zip(right_pixels)
17142            .map(|(left, right)| u64::from(left.abs_diff(*right)))
17143            .sum();
17144        difference as f64 / (left_pixels.len() as f64 * 255.0)
17145    }
17146
17147    fn ppm_pixels(bytes: &[u8]) -> (usize, usize, &[u8]) {
17148        let mut cursor = 0usize;
17149        let mut tokens = Vec::new();
17150        while tokens.len() < 4 {
17151            while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
17152                cursor += 1;
17153            }
17154            if cursor < bytes.len() && bytes[cursor] == b'#' {
17155                while cursor < bytes.len() && bytes[cursor] != b'\n' {
17156                    cursor += 1;
17157                }
17158                continue;
17159            }
17160            let start = cursor;
17161            while cursor < bytes.len() && !bytes[cursor].is_ascii_whitespace() {
17162                cursor += 1;
17163            }
17164            tokens.push(std::str::from_utf8(&bytes[start..cursor]).expect("PPM header UTF-8"));
17165        }
17166        assert_eq!(tokens[0], "P6");
17167        let width = tokens[1].parse::<usize>().expect("PPM width");
17168        let height = tokens[2].parse::<usize>().expect("PPM height");
17169        assert_eq!(tokens[3], "255");
17170        assert!(
17171            cursor < bytes.len() && bytes[cursor].is_ascii_whitespace(),
17172            "PPM header must end with whitespace"
17173        );
17174        cursor += 1;
17175        let pixels = &bytes[cursor..];
17176        assert_eq!(pixels.len(), width * height * 3, "PPM pixel length");
17177        (width, height, pixels)
17178    }
17179    #[test]
17180    fn fx215_existing_chart_data_projection() {
17181        let data = ChartData {
17182            categories: vec!["1".into(), "2".into()],
17183            series: vec![("Revenue & margin".into(), vec![1.25, 2.5])],
17184            number_format: Some("0.00".into()),
17185            ..ChartData::default()
17186        };
17187        for kind in [
17188            ChartKind::Bar,
17189            ChartKind::Line,
17190            ChartKind::Pie,
17191            ChartKind::Doughnut,
17192            ChartKind::Area,
17193            ChartKind::Scatter,
17194            ChartKind::Radar,
17195        ] {
17196            let (xml, _) = authored_chart_parts(kind, &data, "rIdWorkbook").unwrap();
17197            let original = xml.clone();
17198            let (actual_kind, actual) = read_chart_data(&xml).unwrap();
17199            assert_eq!(actual_kind, kind);
17200            assert_eq!(actual.categories, data.categories);
17201            assert_eq!(actual.series, data.series);
17202            assert_eq!(actual.number_format, data.number_format);
17203            assert_eq!(xml, original);
17204            assert_eq!(
17205                chart_workbook_relationship_id(&xml).unwrap().as_deref(),
17206                Some("rIdWorkbook")
17207            );
17208        }
17209    }
17210
17211    #[test]
17212    fn fx215_workbook_relationship_ignores_foreign_id_and_accepts_alias() {
17213        let data = ChartData {
17214            categories: vec!["One".into()],
17215            series: vec![("Values".into(), vec![2.0])],
17216            ..ChartData::default()
17217        };
17218        let (xml, _) = authored_chart_parts(ChartKind::Bar, &data, "rIdWorkbook").unwrap();
17219        let xml = String::from_utf8(xml)
17220            .unwrap()
17221            .replace("r:id=", "rr:id=")
17222            .replace(
17223                "<c:externalData",
17224                &format!(
17225                    "<c:externalData xmlns:rr=\"{R_NS}\" xmlns:f=\"urn:foreign\" f:id=\"wrong\""
17226                ),
17227            );
17228        assert_eq!(
17229            chart_workbook_relationship_id(xml.as_bytes())
17230                .unwrap()
17231                .as_deref(),
17232            Some("rIdWorkbook")
17233        );
17234        let foreign = xml.replace(&format!("xmlns:rr=\"{R_NS}\""), "xmlns:rr=\"urn:foreign\"");
17235        assert!(chart_workbook_relationship_id(foreign.as_bytes()).is_err());
17236    }
17237
17238    #[test]
17239    fn fx215_replace_data_updates_cache_formula_and_workbook() {
17240        let original = ChartData {
17241            categories: vec!["North".into(), "South".into()],
17242            series: vec![
17243                ("Revenue".into(), vec![1.0, 2.0]),
17244                ("Cost".into(), vec![0.5, 1.0]),
17245            ],
17246            ..ChartData::default()
17247        };
17248        for kind in [ChartKind::Bar, ChartKind::Line] {
17249            let (xml, workbook) = authored_chart_parts(kind, &original, "rIdWorkbook").unwrap();
17250            for series in [
17251                vec![("New revenue".into(), vec![8.0, 9.0, 10.0])],
17252                vec![
17253                    ("New revenue".into(), vec![8.0, 9.0, 10.0]),
17254                    ("New cost".into(), vec![1.0, 2.0, 3.0]),
17255                    ("Profit".into(), vec![7.0, 7.0, 7.0]),
17256                ],
17257            ] {
17258                let data = ChartData {
17259                    categories: vec!["East".into(), "West".into(), "Central".into()],
17260                    series,
17261                    ..ChartData::default()
17262                };
17263                let (chart, book) = replace_chart_data(&xml, &workbook, &data).unwrap();
17264                assert_eq!(read_chart_data(&chart).unwrap().1.series, data.series);
17265                assert_eq!(
17266                    read_chart_data(&chart).unwrap().1.categories,
17267                    data.categories
17268                );
17269                let package = OpcPackage::from_reader(std::io::Cursor::new(book)).unwrap();
17270                let sheet = String::from_utf8(
17271                    package
17272                        .get_part("/xl/worksheets/sheet1.xml")
17273                        .unwrap()
17274                        .to_vec(),
17275                )
17276                .unwrap();
17277                assert!(sheet.contains("<v>8</v>"));
17278                assert!(sheet.contains("<v>10</v>"));
17279                assert!(sheet.contains("East"));
17280            }
17281        }
17282    }
17283
17284    #[test]
17285    fn fx215_surviving_series_and_workbook_opaque_payload() {
17286        let original = ChartData {
17287            categories: vec!["North".into(), "South".into()],
17288            series: vec![("Revenue".into(), vec![1.0, 2.0])],
17289            ..ChartData::default()
17290        };
17291        let (xml, workbook) =
17292            authored_chart_parts(ChartKind::Bar, &original, "rIdWorkbook").unwrap();
17293        let source=String::from_utf8(xml).unwrap().replace("<c:chartSpace ","<c:chartSpace xmlns:f=\"urn:foreign\" f:keep='root' ").replace("<c:ser ","<c:ser f:keep='series' ").replace("<c:idx", "<f:opaque data='exact'/><c:idx").replace("</c:ser>","<c:extLst><c:ext uri='producer'><f:payload value='retain'/></c:ext></c:extLst></c:ser>");
17294        assert!(source.contains("f:keep='series'"));
17295        assert!(source.contains("<f:opaque data='exact'/>"));
17296        let data = ChartData {
17297            categories: vec!["West".into()],
17298            series: vec![("Changed".into(), vec![9.0])],
17299            ..ChartData::default()
17300        };
17301        let (changed, _) = replace_chart_data(source.as_bytes(), &workbook, &data).unwrap();
17302        let changed = String::from_utf8(changed).unwrap();
17303        for exact in [
17304            "f:keep='root'",
17305            "f:keep='series'",
17306            "<f:opaque data='exact'/>",
17307            "<c:extLst><c:ext uri='producer'><f:payload value='retain'/></c:ext></c:extLst>",
17308        ] {
17309            assert!(changed.contains(exact), "{exact}");
17310        }
17311        let space = CT_ChartSpace::from_xml(changed.as_bytes()).unwrap();
17312        assert_eq!(space.chart.plot_area.series().unwrap()[0].index, 0);
17313        let no_op = replace_chart_data(source.as_bytes(), &workbook, &original).unwrap();
17314        assert_eq!(no_op.0, source.as_bytes());
17315        assert_eq!(no_op.1, workbook);
17316    }
17317
17318    #[test]
17319    fn fx215_all_seven_families_replace_data_without_changing_kind() {
17320        let original = ChartData {
17321            categories: vec!["1".into(), "2".into()],
17322            series: vec![("Old".into(), vec![1.0, 2.0])],
17323            ..ChartData::default()
17324        };
17325        let data = ChartData {
17326            categories: vec!["3".into(), "4".into(), "5".into()],
17327            series: vec![("New".into(), vec![0.25, 0.5, 0.75])],
17328            number_format: Some("0.0%".into()),
17329            ..ChartData::default()
17330        };
17331        for kind in [
17332            ChartKind::Bar,
17333            ChartKind::Line,
17334            ChartKind::Pie,
17335            ChartKind::Doughnut,
17336            ChartKind::Area,
17337            ChartKind::Scatter,
17338            ChartKind::Radar,
17339        ] {
17340            let (chart, book) = authored_chart_parts(kind, &original, "rIdWorkbook").unwrap();
17341            let (updated, workbook) = replace_chart_data(&chart, &book, &data).unwrap();
17342            let (actual, projected) = read_chart_data(&updated).unwrap();
17343            assert_eq!(actual, kind);
17344            assert_eq!(projected.categories, data.categories);
17345            assert_eq!(projected.series, data.series);
17346            assert_eq!(projected.number_format, data.number_format);
17347            let package = OpcPackage::from_reader(std::io::Cursor::new(workbook)).unwrap();
17348            let styles =
17349                String::from_utf8(package.get_part("/xl/styles.xml").unwrap().to_vec()).unwrap();
17350            assert!(styles.contains("formatCode=\"0.0%\""));
17351        }
17352    }
17353    #[test]
17354    fn fx215_qualified_corpus_replacement_cardinalities() {
17355        let corpus = corpus_dir();
17356        assert!(corpus.is_dir(), "F-X215 required corpus is missing");
17357        for deck in ["bar-chart.pptx", "line-chart.pptx", "chart-picture-bg.pptx"] {
17358            let path = corpus.join(deck);
17359            let expected = MANIFEST
17360                .lines()
17361                .find(|line| line.starts_with(&format!("{deck}\t")))
17362                .unwrap()
17363                .split('\t')
17364                .nth(2)
17365                .unwrap();
17366            assert_eq!(sha256(&path), expected);
17367            let package = OpcPackage::open(&path).unwrap();
17368            let chart_part = "/ppt/charts/chart1.xml";
17369            let chart = package.get_part(chart_part).unwrap();
17370            let (kind, original) = read_chart_data(chart).unwrap();
17371            let workbook_id = chart_workbook_relationship_id(chart).unwrap().unwrap();
17372            let rel = package
17373                .get_part_rels(chart_part)
17374                .unwrap()
17375                .items
17376                .iter()
17377                .find(|rel| rel.id == workbook_id)
17378                .unwrap();
17379            assert_eq!(rel.rel_type, oxml_opc::relationship::rel_types::PACKAGE);
17380            assert_ne!(rel.target_mode.as_deref(), Some("External"));
17381            let workbook_part = OpcPackage::resolve_rel_target(chart_part, &rel.target);
17382            let workbook = package.get_part(&workbook_part).unwrap();
17383            let mut variants = Vec::new();
17384            if original.series.len() > 1 {
17385                let mut fewer = original.clone();
17386                fewer.series.pop();
17387                variants.push(("fewer", fewer));
17388            }
17389            let mut more = original.clone();
17390            more.series.push((
17391                "Added exact values".into(),
17392                (0..more.categories.len())
17393                    .map(|index| 100.0 + index as f64)
17394                    .collect(),
17395            ));
17396            variants.push(("more", more));
17397            let mut categories = original.clone();
17398            categories.categories.push("Added category".into());
17399            for (_, values) in &mut categories.series {
17400                values.push(777.0);
17401            }
17402            variants.push(("categories", categories));
17403            for (label, data) in variants {
17404                let (changed, book) = replace_chart_data(chart, workbook, &data)
17405                    .unwrap_or_else(|error| panic!("{deck}/{label}: {error}"));
17406                let (actual, projection) = read_chart_data(&changed).unwrap();
17407                assert_eq!(actual, kind);
17408                assert_eq!(projection.categories, data.categories);
17409                assert_eq!(projection.series, data.series);
17410                let mut candidate = package.clone();
17411                candidate.set_part(chart_part, changed);
17412                candidate.set_part(&workbook_part, book);
17413                let mut bytes = std::io::Cursor::new(Vec::new());
17414                candidate.write_to(&mut bytes).unwrap();
17415                if let Some(directory) = std::env::var_os("RDOCX_FX215_CANDIDATES_DIR") {
17416                    let directory = PathBuf::from(directory);
17417                    fs::create_dir_all(&directory).unwrap();
17418                    fs::write(
17419                        directory.join(format!("{deck}-{label}.pptx")),
17420                        bytes.get_ref(),
17421                    )
17422                    .unwrap();
17423                }
17424                let reopened =
17425                    OpcPackage::from_reader(std::io::Cursor::new(bytes.into_inner())).unwrap();
17426                for (part, source) in &package.parts {
17427                    if part != chart_part && part != &workbook_part {
17428                        assert_eq!(
17429                            reopened.get_part(part).unwrap(),
17430                            source,
17431                            "{deck}/{label}: {part}"
17432                        );
17433                    }
17434                }
17435                for (owner, relationships) in &package.part_rels {
17436                    assert_eq!(
17437                        reopened.get_part_rels(owner).unwrap().to_xml().unwrap(),
17438                        relationships.to_xml().unwrap()
17439                    );
17440                }
17441                let old_book = OpcPackage::from_reader(std::io::Cursor::new(workbook)).unwrap();
17442                let new_book = OpcPackage::from_reader(std::io::Cursor::new(
17443                    reopened.get_part(&workbook_part).unwrap(),
17444                ))
17445                .unwrap();
17446                for (part, source) in &old_book.parts {
17447                    if part != "/xl/worksheets/sheet1.xml" && part != "/xl/styles.xml" {
17448                        assert_eq!(
17449                            new_book.get_part(part).unwrap(),
17450                            source,
17451                            "{deck}/{label}: workbook {part}"
17452                        );
17453                    }
17454                }
17455                eprintln!("F-X215 corpus {deck}/{label} qualified cache and workbook candidate");
17456            }
17457        }
17458    }
17459    #[test]
17460    fn fx215_sparse_cache_projection_retains_missing_values() {
17461        let plot = format!(
17462            r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:barChart>"#,
17463            sparse_category_series()
17464        );
17465        let xml = chart_with_plot(&plot);
17466        let (_, data) = read_chart_data(xml.as_bytes()).unwrap();
17467        assert_eq!(data.categories, vec!["North", "", "West"]);
17468        assert_eq!(data.series[0].1[0], 1.0);
17469        assert!(data.series[0].1[1].is_nan());
17470        assert_eq!(data.series[0].1[2], 2.0);
17471    }
17472
17473    #[test]
17474    fn fx215_invalid_data_and_external_formula_preserve_inputs() {
17475        let original = ChartData {
17476            categories: vec!["North".into(), "South".into()],
17477            series: vec![("Revenue".into(), vec![1.0, 2.0])],
17478            ..ChartData::default()
17479        };
17480        let (xml, book) = authored_chart_parts(ChartKind::Bar, &original, "rIdWorkbook").unwrap();
17481        let chart_source = xml.clone();
17482        let book_source = book.clone();
17483        let invalid = ChartData {
17484            categories: original.categories.clone(),
17485            series: vec![("Bad".into(), vec![f64::NAN, 1.0])],
17486            ..ChartData::default()
17487        };
17488        assert!(replace_chart_data(&xml, &book, &invalid).is_err());
17489        assert_eq!(xml, chart_source);
17490        assert_eq!(book, book_source);
17491        let external = String::from_utf8(xml.clone())
17492            .unwrap()
17493            .replace("Sheet1!", "'[1]Sheet1'!");
17494        let changed = ChartData {
17495            categories: original.categories.clone(),
17496            series: vec![("New".into(), vec![8.0, 9.0])],
17497            ..ChartData::default()
17498        };
17499        let error = replace_chart_data(external.as_bytes(), &book, &changed).unwrap_err();
17500        assert!(error.to_string().contains("external workbook formula"));
17501        assert_eq!(book, book_source);
17502    }
17503}