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
340/// Reads a bounded XL_CHART_TYPE value from one owned ChartML plot.
341/// Valid unsupported or combination plots return None. Reading does not project
342/// dense caches, resolve an embedded workbook, or rewrite retained source.
343pub fn read_chart_type(chart_xml: &[u8]) -> Result<Option<i64>> {
344    oxml_core::xml::validate_strict_xml_1_0(chart_xml)
345        .map_err(|error| invalid_authoring_value(format!("invalid chart XML: {error:?}")))?;
346    let nodes = chart_source_nodes(chart_xml)?;
347    if !nodes
348        .first()
349        .is_some_and(|node| node.qualified && node.local == b"chartSpace")
350        || nodes.iter().filter(|node| node.parent.is_none()).count() != 1
351    {
352        return Err(invalid_authoring_value("owned chartSpace required"));
353    }
354    let chart = chart_child_node(&nodes, 0, b"chart")?;
355    let area = chart_child_node(&nodes, chart, b"plotArea")?;
356    let mut plots = nodes.iter().enumerate().filter(|(_, node)| {
357        node.parent == Some(area) && node.qualified && node.local.ends_with(b"Chart")
358    });
359    let Some((source_plot, plot)) = plots.next() else {
360        return Ok(None);
361    };
362    if plots.next().is_some() {
363        return Ok(None);
364    }
365    // Index physical children once. Each direct child is visited at most twice
366    // for series metadata and twice for plot scalars, independently of series count.
367    let mut children = vec![Vec::new(); nodes.len()];
368    for (index, node) in nodes.iter().enumerate() {
369        if let Some(parent) = node.parent {
370            children[parent].push(index);
371        }
372    }
373    let mut remaining_work = nodes.len().saturating_mul(4);
374    let mut marker_none = false;
375    let mut exploded = false;
376    for (index, _) in nodes.iter().enumerate().filter(|(_, node)| {
377        node.parent == Some(source_plot) && node.qualified && node.local == b"ser"
378    }) {
379        if let Some(marker) =
380            chart_optional_child_node(&nodes, &children, index, b"marker", &mut remaining_work)?
381            && let Some(symbol) = chart_optional_child_node(
382                &nodes,
383                &children,
384                marker,
385                b"symbol",
386                &mut remaining_work,
387            )?
388        {
389            let symbol = chart_type_scalar(chart_xml, &nodes[symbol])?;
390            if !matches!(
391                symbol.as_str(),
392                "auto"
393                    | "circle"
394                    | "dash"
395                    | "diamond"
396                    | "dot"
397                    | "none"
398                    | "picture"
399                    | "plus"
400                    | "square"
401                    | "star"
402                    | "triangle"
403                    | "x"
404            ) {
405                return Err(invalid_authoring_value("invalid marker symbol"));
406            }
407            marker_none |= symbol == "none";
408        }
409        if let Some(explosion) =
410            chart_optional_child_node(&nodes, &children, index, b"explosion", &mut remaining_work)?
411        {
412            chart_type_scalar(chart_xml, &nodes[explosion])?
413                .parse::<u32>()
414                .map_err(|_| invalid_authoring_value("invalid series explosion"))?;
415            exploded = true;
416        }
417    }
418    let mut scalar = |local: &[u8]| -> Result<Option<String>> {
419        chart_optional_child_node(&nodes, &children, source_plot, local, &mut remaining_work)?
420            .map(|index| chart_type_scalar(chart_xml, &nodes[index]))
421            .transpose()
422    };
423    Ok(match plot.local.as_slice() {
424        b"barChart" => {
425            let direction =
426                scalar(b"barDir")?.ok_or_else(|| ChartError::MissingElement("c:barDir".into()))?;
427            let direction = BarDirection::parse(&direction)
428                .ok_or_else(|| invalid_authoring_value("invalid bar direction"))?;
429            let grouping = scalar(b"grouping")?.unwrap_or_else(|| "standard".into());
430            let grouping = BarGrouping::parse(&grouping)
431                .ok_or_else(|| invalid_authoring_value("invalid bar grouping"))?;
432            match (direction, grouping) {
433                (BarDirection::Bar, BarGrouping::Clustered) => Some(57),
434                (BarDirection::Bar, BarGrouping::Stacked) => Some(58),
435                (BarDirection::Bar, BarGrouping::PercentStacked) => Some(59),
436                (BarDirection::Column, BarGrouping::Clustered) => Some(51),
437                (BarDirection::Column, BarGrouping::Stacked) => Some(52),
438                (BarDirection::Column, BarGrouping::PercentStacked) => Some(53),
439                (_, BarGrouping::Standard) => None,
440            }
441        }
442        b"lineChart" | b"areaChart" => {
443            let grouping = scalar(b"grouping")?.unwrap_or_else(|| "standard".into());
444            let grouping = Grouping::parse(&grouping)
445                .ok_or_else(|| invalid_authoring_value("invalid chart grouping"))?;
446            Some(if plot.local == b"areaChart" {
447                match grouping {
448                    Grouping::Standard => 1,
449                    Grouping::Stacked => 76,
450                    Grouping::PercentStacked => 77,
451                }
452            } else {
453                match (grouping, marker_none) {
454                    (Grouping::Standard, true) => 4,
455                    (Grouping::Stacked, true) => 63,
456                    (Grouping::PercentStacked, true) => 64,
457                    (Grouping::Standard, false) => 65,
458                    (Grouping::Stacked, false) => 66,
459                    (Grouping::PercentStacked, false) => 67,
460                }
461            })
462        }
463        b"pieChart" => Some(if exploded { 69 } else { 5 }),
464        b"doughnutChart" => Some(if exploded { 80 } else { -4120 }),
465        b"scatterChart" => {
466            let value = scalar(b"scatterStyle")?
467                .ok_or_else(|| ChartError::MissingElement("c:scatterStyle".into()))?;
468            let style = ScatterStyle::parse(&value)
469                .ok_or_else(|| invalid_authoring_value("invalid scatter style"))?;
470            (style == ScatterStyle::LineMarker && !marker_none).then_some(74)
471        }
472        b"radarChart" => {
473            let value = scalar(b"radarStyle")?
474                .ok_or_else(|| ChartError::MissingElement("c:radarStyle".into()))?;
475            let style = RadarStyle::parse(&value)
476                .ok_or_else(|| invalid_authoring_value("invalid radar style"))?;
477            (style == RadarStyle::Standard && !marker_none).then_some(81)
478        }
479        _ => None,
480    })
481}
482
483fn chart_optional_child_node(
484    nodes: &[ChartSourceNode],
485    children: &[Vec<usize>],
486    parent: usize,
487    local: &[u8],
488    remaining_work: &mut usize,
489) -> Result<Option<usize>> {
490    let mut found = None;
491    for &index in &children[parent] {
492        *remaining_work = remaining_work
493            .checked_sub(1)
494            .ok_or_else(|| invalid_authoring_value("chart metadata work limit"))?;
495        let node = &nodes[index];
496        if node.qualified && node.local == local && found.replace(index).is_some() {
497            return Err(ChartError::DuplicateElement(format!(
498                "c:{}",
499                String::from_utf8_lossy(local)
500            )));
501        }
502    }
503    Ok(found)
504}
505
506fn chart_type_scalar(xml: &[u8], node: &ChartSourceNode) -> Result<String> {
507    let mut reader = Reader::from_reader(&xml[node.start..node.end]);
508    let value = match reader.read_event().map_err(OxmlError::from)? {
509        Event::Start(element) | Event::Empty(element) => attribute_value(&element, b"val")?
510            .ok_or_else(|| invalid_authoring_value("chart type scalar requires val"))?,
511        _ => return Err(invalid_authoring_value("chart type scalar root")),
512    };
513    loop {
514        match reader.read_event().map_err(OxmlError::from)? {
515            Event::End(_) | Event::Comment(_) | Event::PI(_) => {}
516            Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
517            Event::Eof => return Ok(value),
518            _ => {
519                return Err(invalid_authoring_value(
520                    "chart type scalar has non-scalar content",
521                ));
522            }
523        }
524    }
525}
526
527fn chart_dense_numbers(data: &NumericData) -> Result<Vec<f64>> {
528    let (count, indexes) = cache_layout(&data.markup, data.values.len())?;
529    let count = chart_projection_count(count)?;
530    let mut values = vec![f64::NAN; count];
531    for (index, value) in indexes.into_iter().zip(&data.values) {
532        values[index as usize] = *value;
533    }
534    Ok(values)
535}
536
537fn chart_projection_count(count: u32) -> Result<usize> {
538    if count > 1_048_576 {
539        return Err(invalid_attribute("ptCount", "val", count.to_string()));
540    }
541    Ok(count as usize)
542}
543
544fn chart_category_labels(series: &Series) -> Result<Vec<String>> {
545    let (markup, labels) = match &series.categories {
546        Some(AxisData::String(data)) => (&data.markup, data.values.clone()),
547        Some(AxisData::Numeric(data)) => (
548            &data.markup,
549            data.values.iter().map(|value| value.to_string()).collect(),
550        ),
551        None if series.opaque_categories => {
552            return Err(invalid_authoring_value(
553                "unsupported literal or multilevel category projection",
554            ));
555        }
556        None => return Ok(Vec::new()),
557    };
558    let (count, indexes) = cache_layout(markup, labels.len())?;
559    let mut values = vec![String::new(); chart_projection_count(count)?];
560    for (index, value) in indexes.into_iter().zip(labels) {
561        values[index as usize] = value;
562    }
563    Ok(values)
564}
565
566fn chart_literal_name(series: &Series) -> Result<String> {
567    for raw in series.raw_children.at(2) {
568        let inherited = series
569            .namespace_declarations
570            .iter()
571            .filter_map(|(name, value)| {
572                let prefix = if name == "xmlns" {
573                    ""
574                } else {
575                    name.strip_prefix("xmlns:")?
576                };
577                Some((prefix.as_bytes().to_vec(), value.clone()))
578            })
579            .collect::<NamespaceBindings>();
580        let mut reader = Reader::from_reader(raw);
581        let mut buffer = Vec::new();
582        let mut namespaces = inherited;
583        let mut depth = 0usize;
584        loop {
585            match reader
586                .read_event_into(&mut buffer)
587                .map_err(OxmlError::from)?
588            {
589                Event::Start(element) => {
590                    if depth == 0 {
591                        if chart_child_local(&element, &namespaces)?.as_deref() != Some(b"tx") {
592                            break;
593                        }
594                        namespaces = chart_bindings(&namespaces, &element)?;
595                    }
596                    if depth == 1
597                        && chart_child_local(&element, &namespaces)?.as_deref() == Some(b"v")
598                    {
599                        let value = capture_element(&mut reader, &element)?;
600                        return Ok(parse_text_element(&value, b"v", &namespaces)?.0);
601                    }
602                    depth += 1;
603                }
604                Event::End(_) => {
605                    depth = depth.saturating_sub(1);
606                }
607                Event::Eof => break,
608                _ => {}
609            }
610            buffer.clear();
611        }
612    }
613    Err(invalid_authoring_value(
614        "unsupported series name projection",
615    ))
616}
617
618/// Resolve only the root-owned ChartML externalData relationships attribute.
619pub fn chart_workbook_relationship_id(chart_xml: &[u8]) -> Result<Option<String>> {
620    use quick_xml::name::ResolveResult;
621    use quick_xml::reader::NsReader;
622    CT_ChartSpace::from_xml(chart_xml)?;
623    let mut reader = NsReader::from_reader(chart_xml);
624    let mut buffer = Vec::new();
625    let mut depth = 0usize;
626    let mut result = None;
627    loop {
628        let (namespace, event) = reader
629            .read_resolved_event_into(&mut buffer)
630            .map_err(OxmlError::from)?;
631        let empty = matches!(&event, Event::Empty(_));
632        match (namespace, event) {
633            (namespace, Event::Start(element) | Event::Empty(element)) => {
634                if depth == 1
635                    && matches!(namespace, ResolveResult::Bound(value) if value.as_ref() == C_NS.as_bytes())
636                    && element.local_name().as_ref() == b"externalData"
637                {
638                    if result.is_some() {
639                        return Err(ChartError::DuplicateElement("c:externalData".into()));
640                    }
641                    for attribute in element.attributes() {
642                        let attribute = attribute.map_err(OxmlError::from)?;
643                        let (namespace, local) = reader.resolver().resolve_attribute(attribute.key);
644                        if matches!(namespace, ResolveResult::Bound(value) if value.as_ref() == R_NS.as_bytes())
645                            && local.as_ref() == b"id"
646                        {
647                            result = Some(
648                                attribute
649                                    .decoded_and_normalized_value(
650                                        XmlVersion::Implicit1_0,
651                                        reader.decoder(),
652                                    )
653                                    .map_err(OxmlError::from)?
654                                    .into_owned(),
655                            );
656                        }
657                    }
658                    if result.is_none() {
659                        return Err(ChartError::MissingElement("c:externalData/@r:id".into()));
660                    }
661                }
662                if !empty {
663                    depth += 1;
664                }
665            }
666            (_, Event::End(_)) => {
667                depth = depth.saturating_sub(1);
668            }
669            (_, Event::DocType(_)) => {
670                return Err(invalid_authoring_value(
671                    "DOCTYPE is unsupported in chart data",
672                ));
673            }
674            (_, Event::Eof) => break,
675            _ => {}
676        }
677        buffer.clear();
678    }
679    Ok(result)
680}
681
682#[derive(Debug)]
683struct ChartSourceNode {
684    local: Vec<u8>,
685    qualified: bool,
686    parent: Option<usize>,
687    start: usize,
688    end: usize,
689}
690
691fn chart_source_nodes(xml: &[u8]) -> Result<Vec<ChartSourceNode>> {
692    use quick_xml::name::ResolveResult;
693    use quick_xml::reader::NsReader;
694    let mut reader = NsReader::from_reader(xml);
695    let mut nodes: Vec<ChartSourceNode> = Vec::new();
696    let mut stack = Vec::new();
697    let mut buffer = Vec::new();
698    loop {
699        let start = reader.buffer_position() as usize;
700        let event = reader
701            .read_event_into(&mut buffer)
702            .map_err(OxmlError::from)?;
703        let end = reader.buffer_position() as usize;
704        let empty = matches!(&event, Event::Empty(_));
705        match event {
706            Event::Start(element) | Event::Empty(element) => {
707                let (namespace, local) = reader.resolver().resolve_element(element.name());
708                let qualified = matches!(namespace,ResolveResult::Bound(value) if value.as_ref()==C_NS.as_bytes());
709                let local = local.as_ref().to_vec();
710                let index = nodes.len();
711                if index >= 1_000_000 {
712                    return Err(invalid_authoring_value("chart XML node limit"));
713                }
714                nodes.push(ChartSourceNode {
715                    local,
716                    qualified,
717                    parent: stack.last().copied(),
718                    start,
719                    end,
720                });
721                if !empty {
722                    stack.push(index);
723                }
724            }
725            Event::End(_) => {
726                let index = stack
727                    .pop()
728                    .ok_or_else(|| invalid_authoring_value("unbalanced chart source"))?;
729                nodes[index].end = end;
730            }
731            Event::Text(text) if stack.is_empty() && !text.iter().all(u8::is_ascii_whitespace) => {
732                return Err(invalid_authoring_value("text outside chart XML root"));
733            }
734            Event::CData(_) | Event::GeneralRef(_) if stack.is_empty() => {
735                return Err(invalid_authoring_value("content outside chart XML root"));
736            }
737            Event::Decl(_) if start != 0 && !(start == 3 && xml.starts_with(b"\xef\xbb\xbf")) => {
738                return Err(invalid_authoring_value("misplaced chart XML declaration"));
739            }
740            Event::DocType(_) => {
741                return Err(invalid_authoring_value(
742                    "DOCTYPE is unsupported in chart source",
743                ));
744            }
745            Event::Eof => break,
746            _ => {}
747        }
748        buffer.clear();
749    }
750    if !stack.is_empty() {
751        return Err(invalid_authoring_value("truncated chart source"));
752    }
753    Ok(nodes)
754}
755
756fn chart_child_node(nodes: &[ChartSourceNode], parent: usize, local: &[u8]) -> Result<usize> {
757    let found = nodes
758        .iter()
759        .enumerate()
760        .filter(|(_, node)| node.parent == Some(parent) && node.qualified && node.local == local)
761        .map(|(index, _)| index)
762        .collect::<Vec<_>>();
763    let [index] = found.as_slice() else {
764        return Err(invalid_authoring_value(format!(
765            "chart data requires one owned {}",
766            String::from_utf8_lossy(local)
767        )));
768    };
769    Ok(*index)
770}
771
772fn chart_source_series(xml: &[u8]) -> Result<Vec<Series>> {
773    CT_ChartSpace::from_xml(xml)?.chart.plot_area.series()
774}
775
776fn resized_chart_range(original: &ChartRange, count: usize) -> Result<ChartRange> {
777    let mut range = original.clone();
778    range.last_row = range
779        .first_row
780        .checked_add(
781            count
782                .checked_sub(1)
783                .ok_or_else(|| invalid_authoring_value("empty chart data range"))?,
784        )
785        .ok_or_else(|| invalid_authoring_value("chart data row overflow"))?;
786    range.formula().map_err(invalid_authoring_value)?;
787    Ok(range)
788}
789
790fn replaced_string_reference(
791    original: &StringRef,
792    formula: String,
793    values: Vec<String>,
794) -> StringRef {
795    let mut result = original.clone();
796    result.formula = formula;
797    result.values = values;
798    result.markup.declared_point_count = None;
799    result.markup.point_indexes.clear();
800    result
801}
802
803fn replaced_numeric_reference(
804    original: &NumericData,
805    formula: String,
806    values: Vec<f64>,
807    number_format: Option<&str>,
808) -> NumericData {
809    let mut result = original.clone();
810    result.formula = formula;
811    result.values = values;
812    if let Some(format) = number_format {
813        result.format_code = format.to_owned();
814    }
815    result.markup.declared_point_count = None;
816    result.markup.point_indexes.clear();
817    result
818}
819
820/// Stage replacement chart caches and only their formula-owned workbook cells.
821/// The caller publishes both returned parts together after proving package owners.
822/// Original root, plot, axes, legend and surviving series formatting stay exact.
823pub fn replace_chart_data(
824    chart_xml: &[u8],
825    workbook: &[u8],
826    data: &ChartData,
827) -> Result<(Vec<u8>, Vec<u8>)> {
828    let (kind, current) = read_chart_data(chart_xml)?;
829    validate_chart_data(kind, data)?;
830    if data.category_axis_title.is_some()
831        || data.value_axis_title.is_some()
832        || !data.palette.is_empty()
833    {
834        return Err(invalid_authoring_value(
835            "replace_data changes chart data, not axis titles or palette",
836        ));
837    }
838    let workbook_id = chart_workbook_relationship_id(chart_xml)?
839        .ok_or_else(|| invalid_authoring_value("chart has no embedded workbook reference"))?;
840    let old_series = chart_source_series(chart_xml)?;
841    let first = old_series
842        .first()
843        .ok_or_else(|| invalid_authoring_value("chart has no editable series"))?;
844    let first_name = first
845        .name
846        .as_ref()
847        .ok_or_else(|| invalid_authoring_value("series title needs an owned workbook formula"))?;
848    let name_origin =
849        ChartRange::from_formula(&first_name.formula).map_err(invalid_authoring_value)?;
850    let values_origin =
851        ChartRange::from_formula(&first.values.formula).map_err(invalid_authoring_value)?;
852    if name_origin.first_row != name_origin.last_row || name_origin.sheet != values_origin.sheet {
853        return Err(invalid_authoring_value(
854            "series title and values must own one embedded worksheet",
855        ));
856    }
857    let category_origin = match first.categories.as_ref() {
858        Some(AxisData::String(reference)) => ChartRange::from_formula(&reference.formula),
859        Some(AxisData::Numeric(reference)) => ChartRange::from_formula(&reference.formula),
860        None => {
861            return Err(invalid_authoring_value(
862                "categories need an owned workbook formula",
863            ));
864        }
865    }
866    .map_err(invalid_authoring_value)?;
867    if category_origin.sheet != values_origin.sheet {
868        return Err(invalid_authoring_value(
869            "chart data must own one embedded worksheet",
870        ));
871    }
872    // Validate every formula boundary before an unchanged-data fast path.
873    for series in &old_series {
874        for formula in [
875            series.name.as_ref().map(|r| r.formula.as_str()),
876            Some(series.values.formula.as_str()),
877            series.categories.as_ref().map(|r| match r {
878                AxisData::String(r) => r.formula.as_str(),
879                AxisData::Numeric(r) => r.formula.as_str(),
880            }),
881        ] {
882            let formula = formula.ok_or_else(|| {
883                invalid_authoring_value("series needs ordinary formula-backed data")
884            })?;
885            let range = ChartRange::from_formula(formula).map_err(invalid_authoring_value)?;
886            if range.sheet != values_origin.sheet {
887                return Err(invalid_authoring_value(
888                    "series formula refers to a different worksheet",
889                ));
890            }
891        }
892    }
893    if data.categories == current.categories
894        && data.series == current.series
895        && data.number_format.as_ref().is_none_or(|format| {
896            Some(format) == current.number_format.as_ref()
897                || (format == "General" && current.number_format.is_none())
898        })
899    {
900        oxml_sml::chart_unused_columns(workbook, &values_origin.sheet, 0)
901            .map_err(invalid_authoring_value)?;
902        return Ok((chart_xml.to_vec(), workbook.to_vec()));
903    }
904    let nodes = chart_source_nodes(chart_xml)?;
905    let chart = chart_child_node(&nodes, 0, b"chart")?;
906    let plot_area = chart_child_node(&nodes, chart, b"plotArea")?;
907    let plot_local = match kind {
908        ChartKind::Bar => b"barChart".as_slice(),
909        ChartKind::Line => b"lineChart",
910        ChartKind::Pie => b"pieChart",
911        ChartKind::Doughnut => b"doughnutChart",
912        ChartKind::Area => b"areaChart",
913        ChartKind::Scatter => b"scatterChart",
914        ChartKind::Radar => b"radarChart",
915    };
916    let plot = chart_child_node(&nodes, plot_area, plot_local)?;
917    let series_nodes = nodes
918        .iter()
919        .enumerate()
920        .filter(|(_, node)| node.parent == Some(plot) && node.qualified && node.local == b"ser")
921        .map(|(index, _)| index)
922        .collect::<Vec<_>>();
923    if series_nodes.len() != old_series.len() {
924        return Err(invalid_authoring_value(
925            "editable series source inventory disagrees",
926        ));
927    }
928    let mut updates = Vec::new();
929    let mut patches = Vec::new();
930    for (ordinal, (old, &source_index)) in old_series.iter().zip(&series_nodes).enumerate() {
931        let old_name = old
932            .name
933            .as_ref()
934            .ok_or_else(|| invalid_authoring_value("series title needs formula-backed data"))?;
935        let old_name_range =
936            ChartRange::from_formula(&old_name.formula).map_err(invalid_authoring_value)?;
937        if old_name_range.first_row != old_name_range.last_row {
938            return Err(invalid_authoring_value(
939                "series title formula must own one cell",
940            ));
941        }
942        let old_value_range =
943            ChartRange::from_formula(&old.values.formula).map_err(invalid_authoring_value)?;
944        let categories = old
945            .categories
946            .as_ref()
947            .ok_or_else(|| invalid_authoring_value("series categories are missing"))?;
948        let old_category_range = ChartRange::from_formula(match categories {
949            AxisData::String(r) => &r.formula,
950            AxisData::Numeric(r) => &r.formula,
951        })
952        .map_err(invalid_authoring_value)?;
953        let Some((name, values)) = data.series.get(ordinal) else {
954            for original in [old_name_range, old_value_range, old_category_range] {
955                updates.push(ChartRangeUpdate {
956                    number_format: None,
957                    original: Some(original),
958                    target: None,
959                    values: Vec::new(),
960                });
961            }
962            patches.push((
963                nodes[source_index].start,
964                nodes[source_index].end,
965                Vec::new(),
966            ));
967            continue;
968        };
969        let new_value_range = resized_chart_range(&old_value_range, values.len())?;
970        let new_category_range = resized_chart_range(&old_category_range, data.categories.len())?;
971        updates.push(ChartRangeUpdate {
972            number_format: None,
973            original: Some(old_name_range.clone()),
974            target: Some(old_name_range.clone()),
975            values: vec![ChartCellValue::Text(name.clone())],
976        });
977        updates.push(ChartRangeUpdate {
978            number_format: Some(
979                data.number_format
980                    .as_deref()
981                    .unwrap_or(&old.values.format_code)
982                    .to_owned(),
983            ),
984            original: Some(old_value_range),
985            target: Some(new_value_range.clone()),
986            values: values.iter().copied().map(ChartCellValue::Number).collect(),
987        });
988        let category_cells = if matches!(categories, AxisData::Numeric(_)) {
989            data.categories
990                .iter()
991                .map(|value| {
992                    value
993                        .parse::<f64>()
994                        .map(ChartCellValue::Number)
995                        .map_err(invalid_authoring_value)
996                })
997                .collect::<Result<Vec<_>>>()?
998        } else {
999            data.categories
1000                .iter()
1001                .cloned()
1002                .map(ChartCellValue::Text)
1003                .collect()
1004        };
1005        updates.push(ChartRangeUpdate {
1006            number_format: None,
1007            original: Some(old_category_range),
1008            target: Some(new_category_range.clone()),
1009            values: category_cells,
1010        });
1011        let tx = chart_child_node(&nodes, source_index, b"tx")?;
1012        let name_ref = chart_child_node(&nodes, tx, b"strRef")?;
1013        let name_reference = replaced_string_reference(
1014            old_name,
1015            old_name_range.formula().map_err(invalid_authoring_value)?,
1016            vec![name.clone()],
1017        );
1018        patches.push((
1019            nodes[name_ref].start,
1020            nodes[name_ref].end,
1021            name_reference.to_xml()?,
1022        ));
1023        let value_wrapper = chart_child_node(
1024            &nodes,
1025            source_index,
1026            if kind == ChartKind::Scatter {
1027                b"yVal"
1028            } else {
1029                b"val"
1030            },
1031        )?;
1032        let value_ref = chart_child_node(&nodes, value_wrapper, b"numRef")?;
1033        let value_reference = replaced_numeric_reference(
1034            &old.values,
1035            new_value_range.formula().map_err(invalid_authoring_value)?,
1036            values.clone(),
1037            data.number_format.as_deref(),
1038        );
1039        patches.push((
1040            nodes[value_ref].start,
1041            nodes[value_ref].end,
1042            value_reference.to_xml()?,
1043        ));
1044        let category_wrapper = chart_child_node(
1045            &nodes,
1046            source_index,
1047            if kind == ChartKind::Scatter {
1048                b"xVal"
1049            } else {
1050                b"cat"
1051            },
1052        )?;
1053        let (local, bytes) = match categories {
1054            AxisData::String(reference) => (
1055                b"strRef".as_slice(),
1056                replaced_string_reference(
1057                    reference,
1058                    new_category_range
1059                        .formula()
1060                        .map_err(invalid_authoring_value)?,
1061                    data.categories.clone(),
1062                )
1063                .to_xml()?,
1064            ),
1065            AxisData::Numeric(reference) => (
1066                b"numRef".as_slice(),
1067                replaced_numeric_reference(
1068                    reference,
1069                    new_category_range
1070                        .formula()
1071                        .map_err(invalid_authoring_value)?,
1072                    data.categories
1073                        .iter()
1074                        .map(|v| v.parse::<f64>().map_err(invalid_authoring_value))
1075                        .collect::<Result<Vec<_>>>()?,
1076                    None,
1077                )
1078                .to_xml()?,
1079            ),
1080        };
1081        let category_ref = chart_child_node(&nodes, category_wrapper, local)?;
1082        patches.push((nodes[category_ref].start, nodes[category_ref].end, bytes));
1083    }
1084    let added = data.series.len().saturating_sub(old_series.len());
1085    if added > 0 {
1086        let free = oxml_sml::chart_unused_columns(workbook, &values_origin.sheet, added)
1087            .map_err(invalid_authoring_value)?;
1088        let (default_xml, _) = authored_chart_parts(kind, data, &workbook_id)?;
1089        let defaults = chart_source_series(&default_xml)?;
1090        let max_index = old_series.iter().map(|s| s.index).max().unwrap();
1091        let max_order = old_series.iter().map(|s| s.order).max().unwrap();
1092        let mut bytes = Vec::new();
1093        for (offset, column) in free.into_iter().enumerate() {
1094            let ordinal = old_series.len() + offset;
1095            let (name, values) = &data.series[ordinal];
1096            let mut series = defaults[ordinal].clone();
1097            let step = u32::try_from(offset + 1).map_err(invalid_authoring_value)?;
1098            series.index = max_index
1099                .checked_add(step)
1100                .ok_or_else(|| invalid_authoring_value("chart series index overflow"))?;
1101            series.order = max_order
1102                .checked_add(step)
1103                .ok_or_else(|| invalid_authoring_value("chart series order overflow"))?;
1104            let mut title = name_origin.clone();
1105            title.column = column;
1106            let mut numbers = resized_chart_range(&values_origin, values.len())?;
1107            numbers.column = column;
1108            let category = resized_chart_range(&category_origin, data.categories.len())?;
1109            series.name = Some(StringRef::new(
1110                title.formula().map_err(invalid_authoring_value)?,
1111                vec![name.clone()],
1112            )?);
1113            series.values = NumericData::new(
1114                numbers.formula().map_err(invalid_authoring_value)?,
1115                data.number_format
1116                    .as_deref()
1117                    .unwrap_or(&first.values.format_code)
1118                    .to_owned(),
1119                values.clone(),
1120            )?;
1121            series.categories = Some(if kind == ChartKind::Scatter {
1122                AxisData::Numeric(NumericData::new(
1123                    category.formula().map_err(invalid_authoring_value)?,
1124                    "General".into(),
1125                    data.categories
1126                        .iter()
1127                        .map(|v| v.parse::<f64>().map_err(invalid_authoring_value))
1128                        .collect::<Result<Vec<_>>>()?,
1129                )?)
1130            } else {
1131                AxisData::String(StringRef::new(
1132                    category.formula().map_err(invalid_authoring_value)?,
1133                    data.categories.clone(),
1134                )?)
1135            });
1136            updates.push(ChartRangeUpdate {
1137                number_format: None,
1138                original: None,
1139                target: Some(title),
1140                values: vec![ChartCellValue::Text(name.clone())],
1141            });
1142            updates.push(ChartRangeUpdate {
1143                number_format: Some(
1144                    data.number_format
1145                        .as_deref()
1146                        .unwrap_or(&first.values.format_code)
1147                        .to_owned(),
1148                ),
1149                original: None,
1150                target: Some(numbers),
1151                values: values.iter().copied().map(ChartCellValue::Number).collect(),
1152            });
1153            bytes.extend(series.to_xml_for_plot(kind == ChartKind::Scatter)?);
1154        }
1155        let position = nodes[*series_nodes.last().unwrap()].end;
1156        patches.push((position, position, bytes));
1157    }
1158    let updated_workbook =
1159        oxml_sml::merge_chart_ranges(workbook, &updates).map_err(invalid_authoring_value)?;
1160    patches.sort_by_key(|(start, end, _)| (*start, *end));
1161    for pair in patches.windows(2) {
1162        if pair[0].1 > pair[1].0 {
1163            return Err(invalid_authoring_value(
1164                "overlapping chart data source edits",
1165            ));
1166        }
1167    }
1168    let mut updated_chart = chart_xml.to_vec();
1169    for (start, end, bytes) in patches.into_iter().rev() {
1170        updated_chart.splice(start..end, bytes);
1171    }
1172    let (_, projected) = read_chart_data(&updated_chart)?;
1173    if projected.categories != data.categories || projected.series != data.series {
1174        return Err(invalid_authoring_value(
1175            "replacement chart cache projection disagrees with requested data",
1176        ));
1177    }
1178    Ok((updated_chart, updated_workbook))
1179}
1180
1181fn validate_chart_data(kind: ChartKind, data: &ChartData) -> Result<()> {
1182    if data.categories.is_empty() {
1183        return Err(invalid_authoring_value("at least one category is required"));
1184    }
1185    if data.series.is_empty() {
1186        return Err(invalid_authoring_value("at least one series is required"));
1187    }
1188    if matches!(kind, ChartKind::Pie | ChartKind::Doughnut) && data.series.len() != 1 {
1189        return Err(invalid_authoring_value(
1190            "pie and doughnut charts require exactly one series",
1191        ));
1192    }
1193    for (name, values) in &data.series {
1194        if values.len() != data.categories.len() {
1195            return Err(invalid_authoring_value(format!(
1196                "series {name:?} has {} values for {} categories",
1197                values.len(),
1198                data.categories.len()
1199            )));
1200        }
1201        if values.iter().any(|value| !value.is_finite()) {
1202            return Err(invalid_authoring_value(format!(
1203                "series {name:?} contains a nonfinite value"
1204            )));
1205        }
1206    }
1207    if kind == ChartKind::Scatter {
1208        for category in &data.categories {
1209            if category
1210                .parse::<f64>()
1211                .ok()
1212                .is_none_or(|value| !value.is_finite())
1213            {
1214                return Err(invalid_authoring_value(format!(
1215                    "scatter category {category:?} is not a finite number"
1216                )));
1217            }
1218        }
1219    }
1220    if let Some(format) = &data.number_format {
1221        NumericData::new("Sheet1!$A$2".to_owned(), format.clone(), vec![0.0])?;
1222    }
1223    Ok(())
1224}
1225
1226fn invalid_authoring_value(error: impl fmt::Display) -> ChartError {
1227    ChartError::InvalidValue {
1228        element: "chart authoring".to_owned(),
1229        value: error.to_string(),
1230    }
1231}
1232
1233fn category_value_axes(category: AxisId, value: AxisId, data: &ChartData) -> Result<Vec<Axis>> {
1234    authored_axes(
1235        [
1236            Axis::new(AxisKind::Category, category, AxisPosition::Bottom, value),
1237            Axis::new(AxisKind::Value, value, AxisPosition::Left, category),
1238        ],
1239        data,
1240    )
1241}
1242
1243fn authored_axes(mut axes: [Axis; 2], data: &ChartData) -> Result<Vec<Axis>> {
1244    for axis in &mut axes {
1245        axis.set_deleted(false);
1246    }
1247    axes[0].title = data
1248        .category_axis_title
1249        .as_deref()
1250        .map(CT_Title::plain_text);
1251    axes[1].title = data.value_axis_title.as_deref().map(CT_Title::plain_text);
1252    axes[1].number_format = data
1253        .number_format
1254        .as_ref()
1255        .map(|format| NumberFormat::new(format.clone(), false))
1256        .transpose()?;
1257    Ok(axes.into())
1258}
1259
1260fn apply_palette(kind: ChartKind, data: &ChartData, series: &mut [Series]) {
1261    if data.palette.is_empty() {
1262        return;
1263    }
1264    for (index, item) in series.iter_mut().enumerate() {
1265        item.sp_pr = Some(series_shape(kind, data.palette[index % data.palette.len()]));
1266    }
1267    if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) && series.len() == 1 {
1268        series[0].authored_data_points = data
1269            .categories
1270            .iter()
1271            .enumerate()
1272            .map(|(index, _)| DataPoint {
1273                index: index as u32,
1274                sp_pr: series_shape(kind, data.palette[index % data.palette.len()]),
1275            })
1276            .collect();
1277    }
1278}
1279
1280fn series_shape(kind: ChartKind, color: RgbColor) -> CT_ShapeProperties {
1281    let mut solid = SolidFill::default();
1282    solid.color = Some(ColorChoice::srgb(color));
1283    let fill = Fill::Solid(solid);
1284    let mut properties = CT_ShapeProperties::default();
1285    if matches!(
1286        kind,
1287        ChartKind::Line | ChartKind::Scatter | ChartKind::Radar
1288    ) {
1289        let mut line = CT_LineProperties::default();
1290        line.fill = Some(fill);
1291        properties.line = Some(line);
1292    } else {
1293        properties.fill = Some(fill);
1294    }
1295    properties
1296}
1297
1298fn spreadsheet_column_name(mut index: usize) -> String {
1299    let mut name = Vec::new();
1300    index += 1;
1301    while index != 0 {
1302        let remainder = (index - 1) % 26;
1303        name.push((b'A' + remainder as u8) as char);
1304        index = (index - 1) / 26;
1305    }
1306    name.iter().rev().collect()
1307}
1308
1309/// Backend-neutral plot geometry and the plot rectangle reserved inside a chart.
1310#[derive(Clone, Debug)]
1311pub struct ChartGeometry {
1312    pub plot_bounds: Rect,
1313    pub elements: Vec<PositionedElement>,
1314}
1315
1316#[derive(Clone, Debug)]
1317struct AxisScale {
1318    domain: Domain,
1319    ticks: Vec<f64>,
1320}
1321
1322#[derive(Clone, Copy, Debug)]
1323struct AxisMapping {
1324    domain: Domain,
1325    orientation: Orientation,
1326}
1327
1328#[derive(Clone, Copy, Debug, Default)]
1329struct GeometryOptions {
1330    category_orientation: Orientation,
1331    value: Option<AxisMapping>,
1332    scatter_x: Option<AxisMapping>,
1333    scatter_y: Option<AxisMapping>,
1334}
1335
1336#[derive(Default)]
1337struct ChartAnnotations {
1338    gridlines: Vec<PositionedElement>,
1339    axis_lines: Vec<PositionedElement>,
1340    ticks: Vec<PositionedElement>,
1341    labels: Vec<PositionedElement>,
1342}
1343
1344/// Converts one supported typed chart into a labelled backend-neutral group.
1345pub fn render_chart(
1346    chart: &CT_Chart,
1347    bounds: Rect,
1348    theme: &CT_OfficeStyleSheet,
1349    color_map: &ColorMap,
1350    fonts: &mut FontManager,
1351) -> Result<GroupElement> {
1352    let plots = chart.plot_area.plots()?;
1353    let plot = plots
1354        .first()
1355        .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
1356    plot.validate()?;
1357    let legend_layout = layout_legend(chart, plot, bounds, fonts)?;
1358    let plot_bounds = legend_layout.plot;
1359    let series_colours = resolve_series_colours(plot, theme, color_map)?;
1360    let axes = chart.plot_area.axes()?;
1361    let options = geometry_options(plot, &axes, chart.disp_blanks_as)?;
1362    let plot_children = render_plot_geometry(
1363        plot,
1364        plot_bounds,
1365        chart.disp_blanks_as,
1366        options,
1367        &series_colours,
1368    )?;
1369    if plot_children.is_empty() && !plot_can_render_without_marks(plot) {
1370        return Err(ChartError::InvalidValue {
1371            element: "c:plotArea".to_owned(),
1372            value: "plot has no renderable cached data".to_owned(),
1373        });
1374    }
1375
1376    let mut chart_annotations =
1377        render_axes_and_labels(plot, &axes, plot_bounds, chart.disp_blanks_as, fonts)?;
1378    let mut annotations = Vec::new();
1379    render_data_labels(
1380        plot,
1381        plot_bounds,
1382        chart.disp_blanks_as,
1383        options,
1384        fonts,
1385        &mut chart_annotations.labels,
1386    )?;
1387    render_legend(
1388        legend_layout,
1389        &series_colours,
1390        &mut annotations,
1391        &mut chart_annotations.labels,
1392    );
1393
1394    validate_geometry_coordinates(&chart_annotations.gridlines)?;
1395    validate_geometry_coordinates(&plot_children)?;
1396    validate_geometry_coordinates(&chart_annotations.axis_lines)?;
1397    validate_geometry_coordinates(&chart_annotations.ticks)?;
1398    validate_geometry_coordinates(&annotations)?;
1399    let mut children = chart_annotations.gridlines;
1400    children.push(PositionedElement::Group(GroupElement {
1401        transform: Transform::IDENTITY,
1402        clip: Some(Path::rect(plot_bounds)),
1403        opacity: 1.0,
1404        effects: Vec::new(),
1405        children: plot_children,
1406    }));
1407    children.extend(chart_annotations.axis_lines);
1408    children.extend(chart_annotations.ticks);
1409    children.extend(annotations);
1410    children.extend(chart_annotations.labels);
1411    Ok(GroupElement {
1412        transform: Transform::IDENTITY,
1413        clip: None,
1414        opacity: 1.0,
1415        effects: Vec::new(),
1416        children,
1417    })
1418}
1419
1420/// Shapes the stable label used inside a visible unsupported-chart fallback.
1421pub fn render_chart_placeholder(bounds: Rect, fonts: &mut FontManager) -> Result<GroupElement> {
1422    if !bounds.width.is_finite()
1423        || !bounds.height.is_finite()
1424        || bounds.width <= 0.0
1425        || bounds.height <= 0.0
1426    {
1427        return Err(ChartError::InvalidValue {
1428            element: "chart fallback bounds".to_owned(),
1429            value: "width and height must be finite and positive".to_owned(),
1430        });
1431    }
1432    let label = shape_label(
1433        fonts,
1434        "Unsupported chart",
1435        Point {
1436            x: 4.0,
1437            y: (bounds.height / 2.0).max(9.0),
1438        },
1439    )?;
1440    Ok(GroupElement {
1441        transform: Transform::IDENTITY,
1442        clip: Some(Path::rect(Rect {
1443            x: 0.0,
1444            y: 0.0,
1445            width: bounds.width,
1446            height: bounds.height,
1447        })),
1448        opacity: 1.0,
1449        effects: Vec::new(),
1450        children: vec![PositionedElement::Text(label)],
1451    })
1452}
1453
1454/// Converts one supported typed chart into chart-local backend-neutral paths.
1455pub fn render_geometry(
1456    chart: &CT_Chart,
1457    bounds: Rect,
1458    theme: &CT_OfficeStyleSheet,
1459    color_map: &ColorMap,
1460) -> Result<ChartGeometry> {
1461    let plot_bounds = geometry_plot_bounds(bounds)?;
1462    let plots = chart.plot_area.plots()?;
1463    let plot = plots
1464        .first()
1465        .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
1466    plot.validate()?;
1467    let series_colours = resolve_series_colours(plot, theme, color_map)?;
1468    let children = render_plot_geometry(
1469        plot,
1470        plot_bounds,
1471        chart.disp_blanks_as,
1472        geometry_options(plot, &[], chart.disp_blanks_as)?,
1473        &series_colours,
1474    )?;
1475    if children.is_empty() && !plot_can_render_without_marks(plot) {
1476        return Err(ChartError::InvalidValue {
1477            element: "c:plotArea".to_owned(),
1478            value: "plot has no renderable cached data".to_owned(),
1479        });
1480    }
1481    validate_geometry_coordinates(&children)?;
1482    Ok(ChartGeometry {
1483        plot_bounds,
1484        elements: vec![PositionedElement::Group(GroupElement {
1485            transform: Transform::IDENTITY,
1486            clip: None,
1487            opacity: 1.0,
1488            effects: Vec::new(),
1489            children,
1490        })],
1491    })
1492}
1493
1494fn plot_can_render_without_marks(plot: &Plot) -> bool {
1495    matches!(
1496        plot,
1497        Plot::Radar { series, .. }
1498            if series
1499                .iter()
1500                .any(|series| !series.values.values.is_empty())
1501    )
1502}
1503
1504fn geometry_plot_bounds(bounds: Rect) -> Result<Rect> {
1505    if ![bounds.x, bounds.y, bounds.width, bounds.height]
1506        .into_iter()
1507        .all(f64::is_finite)
1508        || bounds.width <= 0.0
1509        || bounds.height <= 0.0
1510        || !(bounds.x + bounds.width).is_finite()
1511        || !(bounds.y + bounds.height).is_finite()
1512    {
1513        return Err(ChartError::InvalidValue {
1514            element: "chart geometry bounds".to_owned(),
1515            value: format!(
1516                "x={}, y={}, width={}, height={}",
1517                bounds.x, bounds.y, bounds.width, bounds.height
1518            ),
1519        });
1520    }
1521    let plot_bounds = Rect {
1522        x: bounds.x + PLOT_MARGIN_LEFT,
1523        y: bounds.y + PLOT_MARGIN_TOP,
1524        width: bounds.width - PLOT_MARGIN_LEFT - PLOT_MARGIN_RIGHT,
1525        height: bounds.height - PLOT_MARGIN_TOP - PLOT_MARGIN_BOTTOM,
1526    };
1527    if ![
1528        plot_bounds.x,
1529        plot_bounds.y,
1530        plot_bounds.width,
1531        plot_bounds.height,
1532        plot_bounds.x + plot_bounds.width,
1533        plot_bounds.y + plot_bounds.height,
1534    ]
1535    .into_iter()
1536    .all(f64::is_finite)
1537        || plot_bounds.width <= 0.0
1538        || plot_bounds.height <= 0.0
1539    {
1540        return Err(ChartError::InvalidValue {
1541            element: "chart geometry bounds".to_owned(),
1542            value: "chart is too small for the fixed plot margins".to_owned(),
1543        });
1544    }
1545    Ok(plot_bounds)
1546}
1547
1548fn geometry_options(plot: &Plot, axes: &[Axis], blanks: DispBlanksAs) -> Result<GeometryOptions> {
1549    let mut options = GeometryOptions::default();
1550    if let Some(category_axis) = axes.iter().find(|axis| axis.kind != AxisKind::Value) {
1551        options.category_orientation = category_axis.scaling.orientation;
1552    }
1553
1554    if let Plot::Scatter { axis_ids, .. } = plot {
1555        let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
1556        if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[0]) {
1557            options.scatter_x = Some(axis_mapping(axis, x_domain)?);
1558        }
1559        if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[1]) {
1560            options.scatter_y = Some(axis_mapping(axis, y_domain)?);
1561        }
1562    } else if let Some(domain) = plot_value_domain(plot, blanks)? {
1563        options.value = Some(
1564            axes.iter()
1565                .find(|axis| axis.kind == AxisKind::Value)
1566                .map(|axis| axis_mapping(axis, domain))
1567                .transpose()?
1568                .unwrap_or(AxisMapping {
1569                    domain,
1570                    orientation: Orientation::MinMax,
1571                }),
1572        );
1573    }
1574    Ok(options)
1575}
1576
1577fn axis_mapping(axis: &Axis, extent: Domain) -> Result<AxisMapping> {
1578    if axis.scaling.log_base.is_some() {
1579        return Err(ChartError::InvalidValue {
1580            element: "c:scaling/c:logBase".to_owned(),
1581            value: "native chart rendering currently supports linear axes".to_owned(),
1582        });
1583    }
1584    let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
1585    Ok(AxisMapping {
1586        domain: scale.domain,
1587        orientation: axis.scaling.orientation,
1588    })
1589}
1590
1591fn plot_value_domain(plot: &Plot, blanks: DispBlanksAs) -> Result<Option<Domain>> {
1592    let domain = match plot {
1593        Plot::Bar {
1594            grouping, series, ..
1595        } => {
1596            let logical = logical_series_values(series)?;
1597            let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
1598            let layers =
1599                stacked_series_bounds(&logical, stacked, *grouping == BarGrouping::PercentStacked)?;
1600            domain_from_layers(&layers)?
1601        }
1602        Plot::Line {
1603            grouping, series, ..
1604        } => {
1605            let logical = logical_series_values(series)?;
1606            if *grouping == Grouping::Standard {
1607                let values = if blanks == DispBlanksAs::Zero {
1608                    zero_filled_series(&logical, series_category_count(series)?)
1609                        .into_iter()
1610                        .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
1611                        .collect::<Vec<_>>()
1612                } else {
1613                    logical
1614                        .into_iter()
1615                        .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
1616                        .collect::<Vec<_>>()
1617                };
1618                data_domain(&values, false)?
1619            } else {
1620                let layers =
1621                    stacked_series_bounds(&logical, true, *grouping == Grouping::PercentStacked)?;
1622                domain_from_layers(&layers)?
1623            }
1624        }
1625        Plot::Area {
1626            grouping, series, ..
1627        } => {
1628            let logical = logical_series_values(series)?;
1629            let layers = stacked_series_bounds(
1630                &logical,
1631                *grouping != Grouping::Standard,
1632                *grouping == Grouping::PercentStacked,
1633            )?;
1634            domain_from_layers(&layers)?
1635        }
1636        Plot::Radar { series, .. } => {
1637            let values = series
1638                .iter()
1639                .flat_map(|series| series.values.values.iter().copied())
1640                .collect::<Vec<_>>();
1641            data_domain(&values, true)?
1642        }
1643        Plot::Pie { .. } | Plot::Doughnut { .. } | Plot::Scatter { .. } => return Ok(None),
1644    };
1645    Ok(Some(domain))
1646}
1647
1648fn scatter_domains(plot: &Plot, blanks: DispBlanksAs) -> Result<(Domain, Domain)> {
1649    let Plot::Scatter { series, .. } = plot else {
1650        return Err(ChartError::UnexpectedElement("non-scatter plot".to_owned()));
1651    };
1652    let mut x_values = Vec::new();
1653    let mut y_values = Vec::new();
1654    for series in series {
1655        for (_, x, y) in scatter_render_points(series, blanks)? {
1656            x_values.push(x);
1657            y_values.push(y);
1658        }
1659    }
1660    Ok((
1661        data_domain(&x_values, false)?,
1662        data_domain(&y_values, false)?,
1663    ))
1664}
1665
1666fn render_axes_and_labels(
1667    plot: &Plot,
1668    axes: &[Axis],
1669    bounds: Rect,
1670    blanks: DispBlanksAs,
1671    fonts: &mut FontManager,
1672) -> Result<ChartAnnotations> {
1673    if matches!(plot, Plot::Radar { .. }) {
1674        return render_radar_axes_and_labels(plot, axes, bounds, blanks, fonts);
1675    }
1676    let mut annotations = ChartAnnotations::default();
1677    let series = plot_series(plot);
1678    let category_count = if series.is_empty() {
1679        1
1680    } else {
1681        series_category_count(series)?
1682    };
1683    for axis in axes {
1684        if axis.deleted {
1685            continue;
1686        }
1687        axis_line(&mut annotations.axis_lines, bounds, axis.position);
1688        if axis.kind == AxisKind::Value {
1689            let extent = axis_extent(plot, axis, blanks)?;
1690            let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
1691            for tick in &scale.ticks {
1692                let coordinate = axis_tick_coordinate(
1693                    *tick,
1694                    scale.domain,
1695                    bounds,
1696                    axis.position,
1697                    axis.scaling.orientation,
1698                )?;
1699                if axis.major_gridlines.is_some() {
1700                    major_gridline(
1701                        &mut annotations.gridlines,
1702                        bounds,
1703                        axis.position,
1704                        coordinate,
1705                    );
1706                }
1707                axis_tick(
1708                    &mut annotations.ticks,
1709                    bounds,
1710                    axis.position,
1711                    coordinate,
1712                    axis.major_tick_mark,
1713                );
1714                if axis.tick_label_position != TickLabelPosition::None {
1715                    let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
1716                    let origin = tick_label_origin(
1717                        bounds,
1718                        axis.position,
1719                        axis.tick_label_position,
1720                        coordinate,
1721                    );
1722                    annotations
1723                        .labels
1724                        .push(PositionedElement::Text(shape_label_with_properties(
1725                            fonts,
1726                            &text,
1727                            origin,
1728                            axis.tx_pr.as_ref(),
1729                        )?));
1730                }
1731            }
1732        } else {
1733            let count = category_count;
1734            if count > MAX_CATEGORY_ANNOTATIONS {
1735                return Err(ChartError::InvalidValue {
1736                    element: "c:catAx annotations".to_owned(),
1737                    value: format!(
1738                        "logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"
1739                    ),
1740                });
1741            }
1742            let category_labels: BTreeMap<_, _> =
1743                if axis.tick_label_position != TickLabelPosition::None {
1744                    series
1745                        .first()
1746                        .map(|series| series_categories(series, axis.number_format.as_ref()))
1747                        .transpose()?
1748                        .unwrap_or_default()
1749                        .into_iter()
1750                        .collect()
1751                } else {
1752                    BTreeMap::new()
1753                };
1754            for logical_index in 0..count {
1755                let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
1756                let fraction = (index as f64 + 0.5) / count as f64;
1757                let coordinate =
1758                    if matches!(axis.position, AxisPosition::Bottom | AxisPosition::Top) {
1759                        bounds.x + fraction * bounds.width
1760                    } else {
1761                        bounds.y + fraction * bounds.height
1762                    };
1763                if axis.major_gridlines.is_some() {
1764                    major_gridline(
1765                        &mut annotations.gridlines,
1766                        bounds,
1767                        axis.position,
1768                        coordinate,
1769                    );
1770                }
1771                axis_tick(
1772                    &mut annotations.ticks,
1773                    bounds,
1774                    axis.position,
1775                    coordinate,
1776                    axis.major_tick_mark,
1777                );
1778                if axis.tick_label_position != TickLabelPosition::None
1779                    && let Some(text) = category_labels.get(&logical_index)
1780                {
1781                    let origin = tick_label_origin(
1782                        bounds,
1783                        axis.position,
1784                        axis.tick_label_position,
1785                        coordinate,
1786                    );
1787                    annotations
1788                        .labels
1789                        .push(PositionedElement::Text(shape_label_with_properties(
1790                            fonts,
1791                            text,
1792                            origin,
1793                            axis.tx_pr.as_ref(),
1794                        )?));
1795                }
1796            }
1797        }
1798    }
1799    Ok(annotations)
1800}
1801
1802fn render_radar_axes_and_labels(
1803    plot: &Plot,
1804    axes: &[Axis],
1805    bounds: Rect,
1806    blanks: DispBlanksAs,
1807    fonts: &mut FontManager,
1808) -> Result<ChartAnnotations> {
1809    let mut annotations = ChartAnnotations::default();
1810    let series = plot_series(plot);
1811    let count = series_category_count(series)?;
1812    if count > MAX_CATEGORY_ANNOTATIONS {
1813        return Err(ChartError::InvalidValue {
1814            element: "c:radarChart annotations".to_owned(),
1815            value: format!("logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"),
1816        });
1817    }
1818    let center = Point {
1819        x: bounds.x + bounds.width / 2.0,
1820        y: bounds.y + bounds.height / 2.0,
1821    };
1822    let radius = bounds.width.min(bounds.height) / 2.0;
1823    for axis in axes {
1824        if axis.deleted {
1825            continue;
1826        }
1827        if axis.kind == AxisKind::Value {
1828            let scale = nice_number_scale(
1829                axis_extent(plot, axis, blanks)?,
1830                axis.scaling.minimum,
1831                axis.scaling.maximum,
1832                6,
1833            )?;
1834            annotations.axis_lines.push(stroked_segment(
1835                center,
1836                radial_point(center, radius, 0, count),
1837                Color::BLACK,
1838                1.0,
1839            ));
1840            for tick in &scale.ticks {
1841                let distance =
1842                    normalized_value_oriented(*tick, scale.domain, axis.scaling.orientation)?
1843                        * radius;
1844                if axis.major_gridlines.is_some() && distance > 0.0 {
1845                    let points: Vec<_> = (0..count)
1846                        .map(|index| radial_point(center, distance, index, count))
1847                        .collect();
1848                    annotations
1849                        .gridlines
1850                        .push(PositionedElement::Path(PathElement {
1851                            path: polyline_path(&points, true),
1852                            fill: None,
1853                            stroke: Some(Stroke::new(
1854                                Paint::Solid(Color {
1855                                    r: 0.82,
1856                                    g: 0.82,
1857                                    b: 0.82,
1858                                    a: 1.0,
1859                                }),
1860                                0.5,
1861                            )),
1862                        }));
1863                }
1864                radar_value_tick(
1865                    &mut annotations.ticks,
1866                    center,
1867                    distance,
1868                    axis.major_tick_mark,
1869                );
1870                let origin = match axis.tick_label_position {
1871                    TickLabelPosition::High => Some(Point {
1872                        x: center.x + radius + 4.0,
1873                        y: center.y - distance + 3.0,
1874                    }),
1875                    TickLabelPosition::Low => Some(Point {
1876                        x: center.x - radius - 30.0,
1877                        y: center.y - distance + 3.0,
1878                    }),
1879                    TickLabelPosition::NextTo => Some(Point {
1880                        x: center.x + 4.0,
1881                        y: center.y - distance + 3.0,
1882                    }),
1883                    TickLabelPosition::None => None,
1884                };
1885                if let Some(origin) = origin {
1886                    let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
1887                    annotations
1888                        .labels
1889                        .push(PositionedElement::Text(shape_label_with_properties(
1890                            fonts,
1891                            &text,
1892                            origin,
1893                            axis.tx_pr.as_ref(),
1894                        )?));
1895                }
1896            }
1897        } else {
1898            let category_labels: BTreeMap<_, _> =
1899                if axis.tick_label_position != TickLabelPosition::None {
1900                    series
1901                        .first()
1902                        .map(|series| series_categories(series, axis.number_format.as_ref()))
1903                        .transpose()?
1904                        .unwrap_or_default()
1905                        .into_iter()
1906                        .collect()
1907                } else {
1908                    BTreeMap::new()
1909                };
1910            for logical_index in 0..count {
1911                let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
1912                let end = radial_point(center, radius, index, count);
1913                annotations
1914                    .axis_lines
1915                    .push(stroked_segment(center, end, Color::BLACK, 1.0));
1916                radar_category_tick(
1917                    &mut annotations.ticks,
1918                    center,
1919                    radius,
1920                    index,
1921                    count,
1922                    axis.major_tick_mark,
1923                );
1924                if let Some(text) = category_labels.get(&logical_index) {
1925                    let Some(origin) = radar_category_label_origin(
1926                        bounds,
1927                        center,
1928                        radius,
1929                        index,
1930                        count,
1931                        axis.tick_label_position,
1932                    ) else {
1933                        continue;
1934                    };
1935                    annotations
1936                        .labels
1937                        .push(PositionedElement::Text(shape_label_with_properties(
1938                            fonts,
1939                            text,
1940                            origin,
1941                            axis.tx_pr.as_ref(),
1942                        )?));
1943                }
1944            }
1945        }
1946    }
1947    Ok(annotations)
1948}
1949
1950fn radar_category_label_origin(
1951    bounds: Rect,
1952    center: Point,
1953    radius: f64,
1954    index: usize,
1955    count: usize,
1956    position: TickLabelPosition,
1957) -> Option<Point> {
1958    let label_radius = match position {
1959        TickLabelPosition::High => radius + 18.0,
1960        TickLabelPosition::Low => (radius - 10.0).max(0.0),
1961        TickLabelPosition::NextTo => radius + 10.0,
1962        TickLabelPosition::None => return None,
1963    };
1964    let label = radial_point(center, label_radius, index, count);
1965    let mut origin = Point {
1966        x: label.x - 4.0,
1967        y: label.y + 3.0,
1968    };
1969    if position == TickLabelPosition::High {
1970        origin.x = origin.x.clamp(
1971            bounds.x - PLOT_MARGIN_LEFT,
1972            bounds.x + bounds.width + PLOT_MARGIN_RIGHT,
1973        );
1974        origin.y = origin.y.clamp(
1975            bounds.y - PLOT_MARGIN_TOP,
1976            bounds.y + bounds.height + PLOT_MARGIN_BOTTOM,
1977        );
1978    }
1979    Some(origin)
1980}
1981
1982fn radar_value_tick(
1983    elements: &mut Vec<PositionedElement>,
1984    center: Point,
1985    distance: f64,
1986    mark: TickMark,
1987) {
1988    let (left, right) = match mark {
1989        TickMark::None => return,
1990        TickMark::Inside => (4.0, 0.0),
1991        TickMark::Outside => (0.0, 4.0),
1992        TickMark::Cross => (2.0, 2.0),
1993    };
1994    let y = center.y - distance;
1995    elements.push(stroked_segment(
1996        Point {
1997            x: center.x - left,
1998            y,
1999        },
2000        Point {
2001            x: center.x + right,
2002            y,
2003        },
2004        Color::BLACK,
2005        1.0,
2006    ));
2007}
2008
2009fn radar_category_tick(
2010    elements: &mut Vec<PositionedElement>,
2011    center: Point,
2012    radius: f64,
2013    index: usize,
2014    count: usize,
2015    mark: TickMark,
2016) {
2017    let (inside, outside) = match mark {
2018        TickMark::None => return,
2019        TickMark::Inside => (4.0, 0.0),
2020        TickMark::Outside => (0.0, 4.0),
2021        TickMark::Cross => (2.0, 2.0),
2022    };
2023    elements.push(stroked_segment(
2024        radial_point(center, radius - inside, index, count),
2025        radial_point(center, radius + outside, index, count),
2026        Color::BLACK,
2027        1.0,
2028    ));
2029}
2030
2031fn axis_extent(plot: &Plot, axis: &Axis, blanks: DispBlanksAs) -> Result<Domain> {
2032    if let Plot::Scatter { axis_ids, .. } = plot {
2033        let (x, y) = scatter_domains(plot, blanks)?;
2034        return Ok(if axis.id == axis_ids[0] { x } else { y });
2035    }
2036    plot_value_domain(plot, blanks)?.ok_or_else(|| ChartError::InvalidValue {
2037        element: "chart axis".to_owned(),
2038        value: "axis-free plot supplied an axis".to_owned(),
2039    })
2040}
2041
2042fn axis_tick_coordinate(
2043    value: f64,
2044    domain: Domain,
2045    bounds: Rect,
2046    position: AxisPosition,
2047    orientation: Orientation,
2048) -> Result<f64> {
2049    if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
2050        map_x_oriented(value, domain, bounds, orientation)
2051    } else {
2052        map_y_oriented(value, domain, bounds, orientation)
2053    }
2054}
2055
2056fn axis_line(elements: &mut Vec<PositionedElement>, bounds: Rect, position: AxisPosition) {
2057    let (start, end) = match position {
2058        AxisPosition::Bottom => (
2059            Point {
2060                x: bounds.x,
2061                y: bounds.y + bounds.height,
2062            },
2063            Point {
2064                x: bounds.x + bounds.width,
2065                y: bounds.y + bounds.height,
2066            },
2067        ),
2068        AxisPosition::Top => (
2069            Point {
2070                x: bounds.x,
2071                y: bounds.y,
2072            },
2073            Point {
2074                x: bounds.x + bounds.width,
2075                y: bounds.y,
2076            },
2077        ),
2078        AxisPosition::Left => (
2079            Point {
2080                x: bounds.x,
2081                y: bounds.y,
2082            },
2083            Point {
2084                x: bounds.x,
2085                y: bounds.y + bounds.height,
2086            },
2087        ),
2088        AxisPosition::Right => (
2089            Point {
2090                x: bounds.x + bounds.width,
2091                y: bounds.y,
2092            },
2093            Point {
2094                x: bounds.x + bounds.width,
2095                y: bounds.y + bounds.height,
2096            },
2097        ),
2098    };
2099    elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
2100}
2101
2102fn major_gridline(
2103    elements: &mut Vec<PositionedElement>,
2104    bounds: Rect,
2105    position: AxisPosition,
2106    coordinate: f64,
2107) {
2108    let (start, end) = if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
2109        (
2110            Point {
2111                x: coordinate,
2112                y: bounds.y,
2113            },
2114            Point {
2115                x: coordinate,
2116                y: bounds.y + bounds.height,
2117            },
2118        )
2119    } else {
2120        (
2121            Point {
2122                x: bounds.x,
2123                y: coordinate,
2124            },
2125            Point {
2126                x: bounds.x + bounds.width,
2127                y: coordinate,
2128            },
2129        )
2130    };
2131    elements.push(stroked_segment(
2132        start,
2133        end,
2134        Color {
2135            r: 0.82,
2136            g: 0.82,
2137            b: 0.82,
2138            a: 1.0,
2139        },
2140        0.5,
2141    ));
2142}
2143
2144fn axis_tick(
2145    elements: &mut Vec<PositionedElement>,
2146    bounds: Rect,
2147    position: AxisPosition,
2148    coordinate: f64,
2149    mark: TickMark,
2150) {
2151    let (inside, outside) = match mark {
2152        TickMark::None => return,
2153        TickMark::Inside => (4.0, 0.0),
2154        TickMark::Outside => (0.0, 4.0),
2155        TickMark::Cross => (2.0, 2.0),
2156    };
2157    let (start, end) = match position {
2158        AxisPosition::Bottom => (
2159            Point {
2160                x: coordinate,
2161                y: bounds.y + bounds.height - inside,
2162            },
2163            Point {
2164                x: coordinate,
2165                y: bounds.y + bounds.height + outside,
2166            },
2167        ),
2168        AxisPosition::Top => (
2169            Point {
2170                x: coordinate,
2171                y: bounds.y + inside,
2172            },
2173            Point {
2174                x: coordinate,
2175                y: bounds.y - outside,
2176            },
2177        ),
2178        AxisPosition::Left => (
2179            Point {
2180                x: bounds.x + inside,
2181                y: coordinate,
2182            },
2183            Point {
2184                x: bounds.x - outside,
2185                y: coordinate,
2186            },
2187        ),
2188        AxisPosition::Right => (
2189            Point {
2190                x: bounds.x + bounds.width - inside,
2191                y: coordinate,
2192            },
2193            Point {
2194                x: bounds.x + bounds.width + outside,
2195                y: coordinate,
2196            },
2197        ),
2198    };
2199    elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
2200}
2201
2202fn tick_label_origin(
2203    bounds: Rect,
2204    axis_position: AxisPosition,
2205    label_position: TickLabelPosition,
2206    coordinate: f64,
2207) -> Point {
2208    let side = match label_position {
2209        TickLabelPosition::NextTo => axis_position,
2210        TickLabelPosition::High => {
2211            if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
2212                AxisPosition::Top
2213            } else {
2214                AxisPosition::Right
2215            }
2216        }
2217        TickLabelPosition::Low => {
2218            if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
2219                AxisPosition::Bottom
2220            } else {
2221                AxisPosition::Left
2222            }
2223        }
2224        TickLabelPosition::None => axis_position,
2225    };
2226    match side {
2227        AxisPosition::Bottom => Point {
2228            x: coordinate - 4.0,
2229            y: bounds.y + bounds.height + 12.0,
2230        },
2231        AxisPosition::Top => Point {
2232            x: coordinate - 4.0,
2233            y: bounds.y - 4.0,
2234        },
2235        AxisPosition::Left => Point {
2236            x: bounds.x - 30.0,
2237            y: coordinate + 3.0,
2238        },
2239        AxisPosition::Right => Point {
2240            x: bounds.x + bounds.width + 4.0,
2241            y: coordinate + 3.0,
2242        },
2243    }
2244}
2245
2246fn format_axis_value(format: Option<&NumberFormat>, value: f64) -> Result<String> {
2247    if let Some(format) = format {
2248        format.format_value(value)
2249    } else {
2250        NumberFormat::new("General".to_owned(), false)?.format_value(value)
2251    }
2252}
2253
2254fn shape_label(fonts: &mut FontManager, text: &str, origin: Point) -> Result<GlyphRun> {
2255    shape_label_with_properties(fonts, text, origin, None)
2256}
2257
2258fn shape_label_with_properties(
2259    fonts: &mut FontManager,
2260    text: &str,
2261    origin: Point,
2262    properties: Option<&CT_TextBody>,
2263) -> Result<GlyphRun> {
2264    const FAMILY: &str = "Carlito";
2265    const SIZE: f64 = 9.0;
2266    let properties = properties.and_then(chart_text_properties);
2267    let family = properties
2268        .and_then(|properties| properties.latin.as_ref())
2269        .map(|font| font.typeface.as_str())
2270        .unwrap_or(FAMILY);
2271    let size = properties
2272        .and_then(|properties| properties.font_size)
2273        .map(|size| f64::from(size) / 100.0)
2274        .unwrap_or(SIZE);
2275    let bold = properties
2276        .and_then(|properties| properties.bold)
2277        .unwrap_or(false);
2278    let italic = properties
2279        .and_then(|properties| properties.italic)
2280        .unwrap_or(false);
2281    let font_id = fonts
2282        .resolve_font_for_text(Some(family), bold, italic, text)
2283        .map_err(|error| ChartError::InvalidValue {
2284            element: "chart label font".to_owned(),
2285            value: error.to_string(),
2286        })?;
2287    let shaped =
2288        fonts
2289            .shape_text(font_id, text, size)
2290            .map_err(|error| ChartError::InvalidValue {
2291                element: "chart label shaping".to_owned(),
2292                value: error.to_string(),
2293            })?;
2294    Ok(GlyphRun {
2295        origin,
2296        font_id,
2297        font_size: size,
2298        glyph_ids: shaped.glyph_ids,
2299        advances: shaped.advances,
2300        text: text.to_owned(),
2301        source: None,
2302        color: Color::BLACK,
2303        bold,
2304        italic,
2305        field_kind: None,
2306        field_source: None,
2307        note: None,
2308        note_reference_source: None,
2309        tab_aligned: None,
2310    })
2311}
2312
2313fn chart_text_properties(body: &CT_TextBody) -> Option<&CT_TextCharacterProperties> {
2314    body.paragraphs()
2315        .first()
2316        .and_then(|paragraph| paragraph.properties.as_ref())
2317        .and_then(|properties| properties.default_run_properties.as_ref())
2318}
2319
2320fn stroked_segment(start: Point, end: Point, color: Color, width: f64) -> PositionedElement {
2321    PositionedElement::Path(PathElement {
2322        path: Path {
2323            commands: vec![PathCommand::MoveTo(start), PathCommand::LineTo(end)],
2324            fill_rule: FillRule::NonZero,
2325        },
2326        fill: None,
2327        stroke: Some(Stroke::new(Paint::Solid(color), width)),
2328    })
2329}
2330
2331fn plot_series(plot: &Plot) -> &[Series] {
2332    match plot {
2333        Plot::Bar { series, .. }
2334        | Plot::Line { series, .. }
2335        | Plot::Pie { series, .. }
2336        | Plot::Doughnut { series, .. }
2337        | Plot::Area { series, .. }
2338        | Plot::Scatter { series, .. }
2339        | Plot::Radar { series, .. } => series,
2340    }
2341}
2342
2343fn plot_data_labels(plot: &Plot) -> Option<&CT_DLbls> {
2344    match plot {
2345        Plot::Bar { data_labels, .. }
2346        | Plot::Line { data_labels, .. }
2347        | Plot::Pie { data_labels, .. }
2348        | Plot::Doughnut { data_labels, .. }
2349        | Plot::Area { data_labels, .. }
2350        | Plot::Scatter { data_labels, .. }
2351        | Plot::Radar { data_labels, .. } => data_labels.as_ref(),
2352    }
2353}
2354
2355fn series_categories(
2356    series: &Series,
2357    number_format: Option<&NumberFormat>,
2358) -> Result<Vec<(usize, String)>> {
2359    let Some(categories) = &series.categories else {
2360        return Ok(Vec::new());
2361    };
2362    match categories {
2363        AxisData::String(data) => {
2364            let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
2365            indexes
2366                .into_iter()
2367                .zip(&data.values)
2368                .map(|(index, value)| {
2369                    usize::try_from(index)
2370                        .map(|index| (index, value.clone()))
2371                        .map_err(|_| ChartError::InvalidValue {
2372                            element: "c:cat/c:pt/@idx".to_owned(),
2373                            value: index.to_string(),
2374                        })
2375                })
2376                .collect()
2377        }
2378        AxisData::Numeric(data) => {
2379            let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
2380            let cache_format;
2381            let format = if let Some(format) = number_format {
2382                format
2383            } else {
2384                cache_format = NumberFormat::new(data.format_code.clone(), false)?;
2385                &cache_format
2386            };
2387            indexes
2388                .into_iter()
2389                .zip(&data.values)
2390                .map(|(index, value)| {
2391                    let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
2392                        element: "c:cat/c:pt/@idx".to_owned(),
2393                        value: index.to_string(),
2394                    })?;
2395                    let text = format.format_value(*value)?;
2396                    Ok((index, text))
2397                })
2398                .collect()
2399        }
2400    }
2401}
2402
2403fn series_name(series: &Series, index: usize) -> String {
2404    series
2405        .name
2406        .as_ref()
2407        .and_then(|name| name.values.first())
2408        .cloned()
2409        .unwrap_or_else(|| format!("Series {}", index + 1))
2410}
2411
2412fn render_data_labels(
2413    plot: &Plot,
2414    bounds: Rect,
2415    blanks: DispBlanksAs,
2416    options: GeometryOptions,
2417    fonts: &mut FontManager,
2418    elements: &mut Vec<PositionedElement>,
2419) -> Result<()> {
2420    let plot_labels = plot_data_labels(plot);
2421    let anchors = plot_label_anchors(plot, bounds, blanks, options)?;
2422    for (series_index, series) in plot_series(plot).iter().enumerate() {
2423        let Some(labels) = series.data_labels.as_ref().or(plot_labels) else {
2424            continue;
2425        };
2426        let overrides = point_label_overrides(labels)?;
2427        let mut categories = None;
2428        let mut bubble_sizes = None;
2429        let (_, values) = logical_numeric_values(&series.values)?;
2430        let mut percentage_total = None;
2431        for (logical_index, value) in values {
2432            let point_override = overrides.get(&logical_index);
2433            if point_override.is_some_and(|point| point.deleted == Some(true)) {
2434                continue;
2435            }
2436            let effective_labels = point_override
2437                .map(|point| point.apply_to(labels))
2438                .unwrap_or_else(|| labels.clone());
2439            let labels = &effective_labels;
2440            let Some(anchor) = anchors.get(&(series_index, logical_index)).copied() else {
2441                continue;
2442            };
2443            let mut fields = Vec::new();
2444            if labels.show_series_name {
2445                fields.push(series_name(series, series_index));
2446            }
2447            if labels.show_category_name {
2448                if categories.is_none() {
2449                    categories = Some(
2450                        series_categories(series, None)?
2451                            .into_iter()
2452                            .collect::<BTreeMap<_, _>>(),
2453                    );
2454                }
2455                if let Some(category) = categories
2456                    .as_ref()
2457                    .and_then(|categories| categories.get(&logical_index))
2458                {
2459                    fields.push(category.clone());
2460                }
2461            }
2462            if labels.show_value {
2463                fields.push(format_axis_value(labels.number_format.as_ref(), value)?);
2464            }
2465            if labels.show_percent {
2466                let total = if let Some(total) = percentage_total {
2467                    total
2468                } else {
2469                    let total = series
2470                        .values
2471                        .values
2472                        .iter()
2473                        .copied()
2474                        .filter(|value| *value > 0.0)
2475                        .try_fold(0.0, |total, value| {
2476                            checked_geometry_sum(total, value, "data-label percentage total")
2477                        })?;
2478                    percentage_total = Some(total);
2479                    total
2480                };
2481                if total > 0.0 {
2482                    let text = if let Some(format) = &labels.number_format {
2483                        format.format_value(value / total)?
2484                    } else {
2485                        NumberFormat::new("0%".to_owned(), false)?.format_value(value / total)?
2486                    };
2487                    fields.push(text);
2488                }
2489            }
2490            if labels.show_bubble_size {
2491                if bubble_sizes.is_none() {
2492                    bubble_sizes = Some(
2493                        series
2494                            .bubble_size
2495                            .as_ref()
2496                            .map(logical_numeric_values)
2497                            .transpose()?
2498                            .map(|(_, values)| values.into_iter().collect::<BTreeMap<_, _>>())
2499                            .unwrap_or_default(),
2500                    );
2501                }
2502                if let Some(value) = bubble_sizes
2503                    .as_ref()
2504                    .and_then(|sizes| sizes.get(&logical_index))
2505                {
2506                    fields.push(format_axis_value(labels.number_format.as_ref(), *value)?);
2507                }
2508            }
2509            if fields.is_empty() {
2510                continue;
2511            }
2512            let separator = labels.separator.as_deref().unwrap_or(", ");
2513            let text = fields.join(separator);
2514            let origin = anchor.origin(labels.position);
2515            elements.push(PositionedElement::Text(shape_label(fonts, &text, origin)?));
2516        }
2517    }
2518    Ok(())
2519}
2520
2521#[derive(Clone, Debug, Default)]
2522struct PointLabelOverride {
2523    index: Option<usize>,
2524    deleted: Option<bool>,
2525    number_format: Option<NumberFormat>,
2526    position: Option<DataLabelPosition>,
2527    separator: Option<String>,
2528    show_legend_key: Option<bool>,
2529    show_value: Option<bool>,
2530    show_category_name: Option<bool>,
2531    show_series_name: Option<bool>,
2532    show_percent: Option<bool>,
2533    show_bubble_size: Option<bool>,
2534}
2535
2536impl PointLabelOverride {
2537    fn apply_to(&self, labels: &CT_DLbls) -> CT_DLbls {
2538        let mut effective = labels.clone();
2539        if let Some(value) = &self.number_format {
2540            effective.number_format = Some(value.clone());
2541        }
2542        if let Some(value) = self.position {
2543            effective.position = Some(value);
2544        }
2545        if let Some(value) = &self.separator {
2546            effective.separator = Some(value.clone());
2547        }
2548        if let Some(value) = self.show_legend_key {
2549            effective.show_legend_key = value;
2550        }
2551        if let Some(value) = self.show_value {
2552            effective.show_value = value;
2553        }
2554        if let Some(value) = self.show_category_name {
2555            effective.show_category_name = value;
2556        }
2557        if let Some(value) = self.show_series_name {
2558            effective.show_series_name = value;
2559        }
2560        if let Some(value) = self.show_percent {
2561            effective.show_percent = value;
2562        }
2563        if let Some(value) = self.show_bubble_size {
2564            effective.show_bubble_size = value;
2565        }
2566        effective
2567    }
2568}
2569
2570fn point_label_overrides(labels: &CT_DLbls) -> Result<BTreeMap<usize, PointLabelOverride>> {
2571    let mut namespaces = chart_namespace_defaults();
2572    for (name, namespace) in &labels.namespace_declarations {
2573        let prefix = if name == "xmlns" {
2574            Vec::new()
2575        } else if let Some(prefix) = name.strip_prefix("xmlns:") {
2576            prefix.as_bytes().to_vec()
2577        } else {
2578            continue;
2579        };
2580        upsert_namespace_binding(&mut namespaces, prefix, namespace.clone());
2581    }
2582    let mut overrides = BTreeMap::new();
2583    for raw in labels.raw_children.at(0) {
2584        if chart_root_local(raw, &namespaces)?.as_deref() != Some(b"dLbl") {
2585            continue;
2586        }
2587        let point = parse_point_label_override(raw, &namespaces)?;
2588        let index = point
2589            .index
2590            .ok_or_else(|| ChartError::MissingElement("c:dLbl/c:idx".to_owned()))?;
2591        if overrides.insert(index, point).is_some() {
2592            return Err(ChartError::DuplicateElement(format!(
2593                "c:dLbl[c:idx={index}]"
2594            )));
2595        }
2596    }
2597    Ok(overrides)
2598}
2599
2600fn parse_point_label_override(
2601    xml: &[u8],
2602    inherited: &NamespaceBindings,
2603) -> Result<PointLabelOverride> {
2604    let mut reader = Reader::from_reader(xml);
2605    let mut buffer = Vec::new();
2606    let mut namespaces = None;
2607    let mut state = PointLabelOverride::default();
2608    let mut order = 0;
2609    loop {
2610        match reader
2611            .read_event_into(&mut buffer)
2612            .map_err(OxmlError::from)?
2613        {
2614            Event::Start(element) if namespaces.is_none() => {
2615                if !matches_local_name(element.name().as_ref(), b"dLbl")
2616                    || !element_is_in_namespace(&element, C_NS, inherited)?
2617                {
2618                    return Err(ChartError::UnexpectedElement(element_name(&element)));
2619                }
2620                namespaces = Some(chart_bindings(inherited, &element)?);
2621            }
2622            Event::Start(element) => {
2623                let bindings = namespaces
2624                    .as_ref()
2625                    .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
2626                let name = chart_child_local(&element, bindings)?;
2627                let raw = capture_element(&mut reader, &element)?;
2628                parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
2629            }
2630            Event::Empty(element) => {
2631                let bindings = namespaces
2632                    .as_ref()
2633                    .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
2634                let name = chart_child_local(&element, bindings)?;
2635                let raw = capture_empty_element(&element)?;
2636                parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
2637            }
2638            Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbl") => {
2639                if state.index.is_none() {
2640                    return Err(ChartError::MissingElement("c:dLbl/c:idx".to_owned()));
2641                }
2642                return Ok(state);
2643            }
2644            Event::Eof => return Err(missing_end("c:dLbl")),
2645            _ => {}
2646        }
2647        buffer.clear();
2648    }
2649}
2650
2651fn parse_point_label_child(
2652    state: &mut PointLabelOverride,
2653    name: Option<&[u8]>,
2654    raw: &[u8],
2655    namespaces: &NamespaceBindings,
2656    order: &mut usize,
2657) -> Result<()> {
2658    let Some(name) = name else {
2659        return Ok(());
2660    };
2661    let rank = match name {
2662        b"idx" => 0,
2663        b"delete" => 1,
2664        b"layout" => 2,
2665        b"tx" => 3,
2666        b"numFmt" => 4,
2667        b"spPr" => 5,
2668        b"txPr" => 6,
2669        b"dLblPos" => 7,
2670        b"showLegendKey" => 8,
2671        b"showVal" => 9,
2672        b"showCatName" => 10,
2673        b"showSerName" => 11,
2674        b"showPercent" => 12,
2675        b"showBubbleSize" => 13,
2676        b"separator" => 14,
2677        b"showLeaderLines" => 15,
2678        b"leaderLines" => 16,
2679        b"extLst" => 17,
2680        _ => return Ok(()),
2681    };
2682    if rank < *order {
2683        return Err(ChartError::InvalidValue {
2684            element: "c:dLbl".to_owned(),
2685            value: format!("c:{} is out of schema order", String::from_utf8_lossy(name)),
2686        });
2687    }
2688    *order = rank;
2689    match name {
2690        b"idx" => {
2691            let (index, _) = parse_u32_scalar(raw, "idx")?;
2692            let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
2693                element: "c:dLbl/c:idx".to_owned(),
2694                value: index.to_string(),
2695            })?;
2696            set_once(&mut state.index, index, "c:dLbl/c:idx")?;
2697        }
2698        b"delete" => set_once(
2699            &mut state.deleted,
2700            parse_bool_value(raw, "delete")?.0,
2701            "c:dLbl/c:delete",
2702        )?,
2703        b"numFmt" => set_once(
2704            &mut state.number_format,
2705            NumberFormat::from_xml(raw)?,
2706            "c:dLbl/c:numFmt",
2707        )?,
2708        b"dLblPos" => {
2709            let (value, _) = scalar_value(raw, "dLblPos")?;
2710            let value =
2711                value.ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
2712            let position = DataLabelPosition::parse(&value)
2713                .ok_or_else(|| invalid_attribute("dLblPos", "val", value))?;
2714            set_once(&mut state.position, position, "c:dLbl/c:dLblPos")?;
2715        }
2716        b"showLegendKey" => set_once(
2717            &mut state.show_legend_key,
2718            parse_bool_value(raw, "showLegendKey")?.0,
2719            "c:dLbl/c:showLegendKey",
2720        )?,
2721        b"showVal" => set_once(
2722            &mut state.show_value,
2723            parse_bool_value(raw, "showVal")?.0,
2724            "c:dLbl/c:showVal",
2725        )?,
2726        b"showCatName" => set_once(
2727            &mut state.show_category_name,
2728            parse_bool_value(raw, "showCatName")?.0,
2729            "c:dLbl/c:showCatName",
2730        )?,
2731        b"showSerName" => set_once(
2732            &mut state.show_series_name,
2733            parse_bool_value(raw, "showSerName")?.0,
2734            "c:dLbl/c:showSerName",
2735        )?,
2736        b"showPercent" => set_once(
2737            &mut state.show_percent,
2738            parse_bool_value(raw, "showPercent")?.0,
2739            "c:dLbl/c:showPercent",
2740        )?,
2741        b"showBubbleSize" => set_once(
2742            &mut state.show_bubble_size,
2743            parse_bool_value(raw, "showBubbleSize")?.0,
2744            "c:dLbl/c:showBubbleSize",
2745        )?,
2746        b"separator" => set_once(
2747            &mut state.separator,
2748            parse_text_element(raw, b"separator", namespaces)?.0,
2749            "c:dLbl/c:separator",
2750        )?,
2751        _ => {}
2752    }
2753    Ok(())
2754}
2755
2756#[derive(Clone, Copy, Debug)]
2757struct LabelAnchor {
2758    center: Point,
2759    base: Point,
2760    end: Point,
2761    direction: Point,
2762}
2763
2764impl LabelAnchor {
2765    const fn point(center: Point) -> Self {
2766        Self {
2767            center,
2768            base: center,
2769            end: center,
2770            direction: Point { x: 0.0, y: 0.0 },
2771        }
2772    }
2773
2774    const fn segment(center: Point, base: Point, end: Point) -> Self {
2775        Self {
2776            center,
2777            base,
2778            end,
2779            direction: Point {
2780                x: end.x - base.x,
2781                y: end.y - base.y,
2782            },
2783        }
2784    }
2785
2786    const fn directed_segment(center: Point, base: Point, end: Point, direction: Point) -> Self {
2787        Self {
2788            center,
2789            base,
2790            end,
2791            direction,
2792        }
2793    }
2794
2795    fn origin(self, position: Option<DataLabelPosition>) -> Point {
2796        let mut origin = self.center;
2797        match position.unwrap_or(DataLabelPosition::BestFit) {
2798            DataLabelPosition::Bottom => origin.y += 8.0,
2799            DataLabelPosition::Top => origin.y -= 5.0,
2800            DataLabelPosition::Left => origin.x -= 8.0,
2801            DataLabelPosition::Right => origin.x += 8.0,
2802            DataLabelPosition::InsideBase => {
2803                origin = self.along_segment(self.base, 5.0, true).unwrap_or(Point {
2804                    x: origin.x,
2805                    y: origin.y + 8.0,
2806                });
2807            }
2808            DataLabelPosition::InsideEnd => {
2809                origin = self.along_segment(self.end, -5.0, true).unwrap_or(origin);
2810            }
2811            DataLabelPosition::OutsideEnd => {
2812                origin = self.along_segment(self.end, 5.0, false).unwrap_or(Point {
2813                    x: origin.x,
2814                    y: origin.y - 5.0,
2815                });
2816            }
2817            DataLabelPosition::BestFit | DataLabelPosition::Center => {}
2818        }
2819        origin
2820    }
2821
2822    fn along_segment(self, origin: Point, distance: f64, clamp_inside: bool) -> Option<Point> {
2823        let dx = self.end.x - self.base.x;
2824        let dy = self.end.y - self.base.y;
2825        let length = dx.hypot(dy);
2826        let (dx, dy, direction_length) = if length.is_finite() && length > 0.0 {
2827            (dx, dy, length)
2828        } else {
2829            let direction_length = self.direction.x.hypot(self.direction.y);
2830            if !direction_length.is_finite() || direction_length == 0.0 {
2831                return None;
2832            }
2833            (self.direction.x, self.direction.y, direction_length)
2834        };
2835        let distance = if clamp_inside {
2836            distance.clamp(-length / 2.0, length / 2.0)
2837        } else {
2838            distance
2839        };
2840        Some(Point {
2841            x: origin.x + dx / direction_length * distance,
2842            y: origin.y + dy / direction_length * distance,
2843        })
2844    }
2845}
2846
2847fn plot_label_anchors(
2848    plot: &Plot,
2849    bounds: Rect,
2850    blanks: DispBlanksAs,
2851    options: GeometryOptions,
2852) -> Result<BTreeMap<(usize, usize), LabelAnchor>> {
2853    let mut anchors = BTreeMap::new();
2854    match plot {
2855        Plot::Bar {
2856            direction,
2857            grouping,
2858            gap_width,
2859            overlap,
2860            series,
2861            ..
2862        } => {
2863            for bar in bar_rectangles(
2864                *direction, *grouping, *gap_width, *overlap, series, bounds, options,
2865            )? {
2866                if !point_in_rect(bar.end, bounds) {
2867                    continue;
2868                }
2869                let visible_base = Point {
2870                    x: bar.base.x.clamp(bounds.x, bounds.x + bounds.width),
2871                    y: bar.base.y.clamp(bounds.y, bounds.y + bounds.height),
2872                };
2873                let visible_end = Point {
2874                    x: bar.end.x.clamp(bounds.x, bounds.x + bounds.width),
2875                    y: bar.end.y.clamp(bounds.y, bounds.y + bounds.height),
2876                };
2877                anchors.insert(
2878                    (bar.series_index, bar.logical_index),
2879                    LabelAnchor::directed_segment(
2880                        Point {
2881                            x: (visible_base.x + visible_end.x) / 2.0,
2882                            y: (visible_base.y + visible_end.y) / 2.0,
2883                        },
2884                        visible_base,
2885                        visible_end,
2886                        Point {
2887                            x: bar.end.x - bar.base.x,
2888                            y: bar.end.y - bar.base.y,
2889                        },
2890                    ),
2891                );
2892            }
2893        }
2894        Plot::Line {
2895            grouping, series, ..
2896        }
2897        | Plot::Area {
2898            grouping, series, ..
2899        } => {
2900            for (series_index, points) in
2901                category_label_points(*grouping, series, bounds, blanks, options)?
2902                    .into_iter()
2903                    .enumerate()
2904            {
2905                for (logical_index, point) in points {
2906                    anchors.insert((series_index, logical_index), LabelAnchor::point(point));
2907                }
2908            }
2909        }
2910        Plot::Scatter { series, .. } => {
2911            let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
2912            let x_mapping = options.scatter_x.unwrap_or(AxisMapping {
2913                domain: x_domain,
2914                orientation: Orientation::MinMax,
2915            });
2916            let y_mapping = options.scatter_y.unwrap_or(AxisMapping {
2917                domain: y_domain,
2918                orientation: Orientation::MinMax,
2919            });
2920            for (series_index, series) in series.iter().enumerate() {
2921                let indexed = matched_scatter_points(series)?;
2922                let points = scatter_points(
2923                    &indexed,
2924                    x_mapping.domain,
2925                    y_mapping.domain,
2926                    bounds,
2927                    x_mapping.orientation,
2928                    y_mapping.orientation,
2929                )?;
2930                for ((logical_index, _, _), point) in indexed.into_iter().zip(points) {
2931                    anchors.insert((series_index, logical_index), LabelAnchor::point(point));
2932                }
2933            }
2934        }
2935        Plot::Radar { series, .. } => {
2936            let center = Point {
2937                x: bounds.x + bounds.width / 2.0,
2938                y: bounds.y + bounds.height / 2.0,
2939            };
2940            let category_count = series_category_count(series)?;
2941            for (series_index, points) in radar_series_points(series, bounds, options)?
2942                .into_iter()
2943                .enumerate()
2944            {
2945                for (logical_index, point) in points {
2946                    let spoke = radial_point(
2947                        center,
2948                        1.0,
2949                        oriented_category_index(
2950                            logical_index,
2951                            category_count,
2952                            options.category_orientation,
2953                        ),
2954                        category_count,
2955                    );
2956                    anchors.insert(
2957                        (series_index, logical_index),
2958                        LabelAnchor::directed_segment(
2959                            point,
2960                            center,
2961                            point,
2962                            Point {
2963                                x: spoke.x - center.x,
2964                                y: spoke.y - center.y,
2965                            },
2966                        ),
2967                    );
2968                }
2969            }
2970        }
2971        Plot::Pie {
2972            first_slice_angle,
2973            series,
2974            ..
2975        } => {
2976            for slice in pie_slices(*first_slice_angle, None, series, bounds)? {
2977                anchors.insert(
2978                    (slice.series_index, slice.logical_index),
2979                    LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2980                );
2981            }
2982        }
2983        Plot::Doughnut {
2984            first_slice_angle,
2985            hole_size,
2986            series,
2987            ..
2988        } => {
2989            for slice in pie_slices(*first_slice_angle, Some(*hole_size), series, bounds)? {
2990                anchors.insert(
2991                    (slice.series_index, slice.logical_index),
2992                    LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2993                );
2994            }
2995        }
2996    }
2997    anchors.retain(|_, anchor| point_in_rect(anchor.end, bounds));
2998    Ok(anchors)
2999}
3000
3001fn category_label_points(
3002    grouping: Grouping,
3003    series: &[Series],
3004    bounds: Rect,
3005    blanks: DispBlanksAs,
3006    options: GeometryOptions,
3007) -> Result<Vec<Vec<(usize, Point)>>> {
3008    validate_series_values(series)?;
3009    let logical_series = logical_series_values(series)?;
3010    let category_count = series_category_count(series)?;
3011    let calculated_series = if blanks == DispBlanksAs::Zero {
3012        zero_filled_series(&logical_series, category_count)
3013    } else {
3014        logical_series.clone()
3015    };
3016    let layers = stacked_series_bounds(
3017        &calculated_series,
3018        grouping != Grouping::Standard,
3019        grouping == Grouping::PercentStacked,
3020    )?;
3021    let domain = options
3022        .value
3023        .map(|mapping| mapping.domain)
3024        .unwrap_or(domain_from_layers(&layers)?);
3025    let value_orientation = options
3026        .value
3027        .map(|mapping| mapping.orientation)
3028        .unwrap_or(Orientation::MinMax);
3029    let mut point_series = Vec::with_capacity(layers.len());
3030    for (series_index, layer) in layers.iter().enumerate() {
3031        let present: BTreeSet<_> = logical_series[series_index]
3032            .1
3033            .iter()
3034            .map(|(index, _)| *index)
3035            .collect();
3036        let indexed: Vec<_> = layer
3037            .iter()
3038            .filter(|(index, _, _)| present.contains(index))
3039            .map(|(index, _, upper)| (*index, *upper))
3040            .collect();
3041        let points = category_points(
3042            &indexed,
3043            category_count,
3044            domain,
3045            bounds,
3046            options.category_orientation,
3047            value_orientation,
3048        )?;
3049        point_series.push(
3050            indexed
3051                .into_iter()
3052                .map(|(index, _)| index)
3053                .zip(points)
3054                .collect(),
3055        );
3056    }
3057    Ok(point_series)
3058}
3059
3060fn point_in_rect(point: Point, bounds: Rect) -> bool {
3061    const EPSILON: f64 = 1.0e-9;
3062    point.x >= bounds.x - EPSILON
3063        && point.x <= bounds.x + bounds.width + EPSILON
3064        && point.y >= bounds.y - EPSILON
3065        && point.y <= bounds.y + bounds.height + EPSILON
3066}
3067
3068fn resolve_series_colours(
3069    plot: &Plot,
3070    theme: &CT_OfficeStyleSheet,
3071    color_map: &ColorMap,
3072) -> Result<Vec<Color>> {
3073    let lookup = theme_colour_lookup(theme);
3074    plot_series(plot)
3075        .iter()
3076        .enumerate()
3077        .map(|(index, series)| {
3078            if let Some(properties) = series.sp_pr.as_ref() {
3079                if let Some(fill) = properties.fill.as_ref() {
3080                    return resolve_series_paint(fill, index, "fill", theme, color_map, &lookup);
3081                }
3082                if let Some(fill) = properties.line.as_ref().and_then(|line| line.fill.as_ref()) {
3083                    return resolve_series_paint(fill, index, "line", theme, color_map, &lookup);
3084                }
3085            }
3086            resolve_series_accent(index, theme, color_map, &lookup)
3087        })
3088        .collect()
3089}
3090
3091fn resolve_series_paint(
3092    fill: &Fill,
3093    series_index: usize,
3094    source: &str,
3095    theme: &CT_OfficeStyleSheet,
3096    color_map: &ColorMap,
3097    lookup: &[(&str, RgbColor)],
3098) -> Result<Color> {
3099    match fill {
3100        Fill::NoFill(_) => Ok(Color {
3101            r: 0.0,
3102            g: 0.0,
3103            b: 0.0,
3104            a: 0.0,
3105        }),
3106        Fill::Solid(solid) => {
3107            let choice = solid
3108                .color
3109                .as_ref()
3110                .ok_or_else(|| ChartError::InvalidValue {
3111                    element: format!("c:ser[{series_index}] direct {source} paint"),
3112                    value: "solid fill has no colour choice".to_owned(),
3113                })?;
3114            resolve_direct_chart_colour(choice, theme, color_map, lookup, || {
3115                format!("c:ser[{series_index}] direct {source} paint")
3116            })
3117        }
3118        Fill::Gradient(_) => unsupported_series_paint(series_index, source, "gradient"),
3119        Fill::Pattern(_) => unsupported_series_paint(series_index, source, "pattern"),
3120        Fill::Blip(_) => unsupported_series_paint(series_index, source, "picture"),
3121    }
3122}
3123
3124fn resolve_direct_chart_colour(
3125    choice: &ColorChoice,
3126    theme: &CT_OfficeStyleSheet,
3127    color_map: &ColorMap,
3128    lookup: &[(&str, RgbColor)],
3129    element: impl FnOnce() -> String,
3130) -> Result<Color> {
3131    let ColorChoice::Scheme {
3132        value, transforms, ..
3133    } = choice
3134    else {
3135        return resolve_chart_colour(choice, color_map, lookup, element);
3136    };
3137    let theme_slot =
3138        mapped_theme_slot(value, color_map).ok_or_else(|| ChartError::InvalidValue {
3139            element: element(),
3140            value: format!("no concrete theme slot is available for scheme colour {value}"),
3141        })?;
3142    let theme_choice = theme.theme_elements.color_scheme.color(theme_slot);
3143    let base = concrete_theme_colour(theme_choice).ok_or_else(|| ChartError::InvalidValue {
3144        element: format!("a:clrScheme/a:{}", theme_slot.as_str()),
3145        value: "selected theme slot has no concrete sRGB or system fallback colour".to_owned(),
3146    })?;
3147    let mut combined = theme_choice.transforms().to_vec();
3148    combined.extend_from_slice(transforms);
3149    Ok(resolved_layout_colour(apply_color_transforms(
3150        base, &combined,
3151    )))
3152}
3153
3154fn mapped_theme_slot(value: &str, color_map: &ColorMap) -> Option<ThemeColorSlot> {
3155    let mapped = match value {
3156        "bg1" => color_map.theme_slot(ColorMapSlot::Background1),
3157        "tx1" => color_map.theme_slot(ColorMapSlot::Text1),
3158        "bg2" => color_map.theme_slot(ColorMapSlot::Background2),
3159        "tx2" => color_map.theme_slot(ColorMapSlot::Text2),
3160        "accent1" => color_map.theme_slot(ColorMapSlot::Accent1),
3161        "accent2" => color_map.theme_slot(ColorMapSlot::Accent2),
3162        "accent3" => color_map.theme_slot(ColorMapSlot::Accent3),
3163        "accent4" => color_map.theme_slot(ColorMapSlot::Accent4),
3164        "accent5" => color_map.theme_slot(ColorMapSlot::Accent5),
3165        "accent6" => color_map.theme_slot(ColorMapSlot::Accent6),
3166        "hlink" => color_map.theme_slot(ColorMapSlot::Hyperlink),
3167        "folHlink" => color_map.theme_slot(ColorMapSlot::FollowedHyperlink),
3168        "dk1" => ThemeColorSlot::Dark1,
3169        "lt1" => ThemeColorSlot::Light1,
3170        "dk2" => ThemeColorSlot::Dark2,
3171        "lt2" => ThemeColorSlot::Light2,
3172        _ => return None,
3173    };
3174    Some(mapped)
3175}
3176
3177fn unsupported_series_paint(series_index: usize, source: &str, kind: &str) -> Result<Color> {
3178    Err(ChartError::InvalidValue {
3179        element: format!("c:ser[{series_index}] direct {source} paint"),
3180        value: format!("unsupported {kind} paint"),
3181    })
3182}
3183
3184fn resolve_series_accent(
3185    series_index: usize,
3186    theme: &CT_OfficeStyleSheet,
3187    color_map: &ColorMap,
3188    lookup: &[(&str, RgbColor)],
3189) -> Result<Color> {
3190    const ACCENTS: [ColorMapSlot; 6] = [
3191        ColorMapSlot::Accent1,
3192        ColorMapSlot::Accent2,
3193        ColorMapSlot::Accent3,
3194        ColorMapSlot::Accent4,
3195        ColorMapSlot::Accent5,
3196        ColorMapSlot::Accent6,
3197    ];
3198    let semantic_slot = ACCENTS[series_index % ACCENTS.len()];
3199    let theme_slot = color_map.theme_slot(semantic_slot);
3200    let choice = theme.theme_elements.color_scheme.color(theme_slot);
3201    resolve_chart_colour(choice, color_map, lookup, || {
3202        format!("c:ser[{series_index}] theme accent")
3203    })
3204}
3205
3206fn resolve_chart_colour(
3207    choice: &ColorChoice,
3208    color_map: &ColorMap,
3209    lookup: &[(&str, RgbColor)],
3210    element: impl FnOnce() -> String,
3211) -> Result<Color> {
3212    resolve_color(choice, color_map, lookup)
3213        .map(resolved_layout_colour)
3214        .map_err(|error| ChartError::InvalidValue {
3215            element: element(),
3216            value: error.to_string(),
3217        })
3218}
3219
3220fn theme_colour_lookup(theme: &CT_OfficeStyleSheet) -> Vec<(&'static str, RgbColor)> {
3221    theme
3222        .theme_elements
3223        .color_scheme
3224        .iter()
3225        .filter_map(|(slot, choice)| {
3226            concrete_theme_colour(choice).map(|colour| (slot.as_str(), colour))
3227        })
3228        .collect()
3229}
3230
3231fn concrete_theme_colour(choice: &ColorChoice) -> Option<RgbColor> {
3232    match choice {
3233        ColorChoice::Srgb { value, .. } => Some(*value),
3234        ColorChoice::System { last_color, .. } => *last_color,
3235        ColorChoice::Scheme { .. } | ColorChoice::Preset { .. } => None,
3236    }
3237}
3238
3239fn resolved_layout_colour(colour: ResolvedColor) -> Color {
3240    Color {
3241        r: f64::from(colour.red) / 255.0,
3242        g: f64::from(colour.green) / 255.0,
3243        b: f64::from(colour.blue) / 255.0,
3244        a: f64::from(colour.alpha) / 255.0,
3245    }
3246}
3247
3248struct LegendLayout {
3249    plot: Rect,
3250    labels: Vec<GlyphRun>,
3251    keys: Vec<Rect>,
3252}
3253
3254fn layout_legend(
3255    chart: &CT_Chart,
3256    plot: &Plot,
3257    bounds: Rect,
3258    fonts: &mut FontManager,
3259) -> Result<LegendLayout> {
3260    let full_plot = geometry_plot_bounds(bounds)?;
3261    let Some(legend) = &chart.legend else {
3262        return Ok(LegendLayout {
3263            plot: full_plot,
3264            labels: Vec::new(),
3265            keys: Vec::new(),
3266        });
3267    };
3268    let settings = legend.settings;
3269    let horizontal = matches!(
3270        settings.position,
3271        LegendPosition::Top | LegendPosition::Bottom
3272    );
3273    if plot_series(plot).len() > MAX_CATEGORY_ANNOTATIONS {
3274        return Err(invalid_authoring_value("legend entry limit"));
3275    }
3276    let mut labels = plot_series(plot)
3277        .iter()
3278        .enumerate()
3279        .map(|(index, series)| {
3280            shape_label(fonts, &series_name(series, index), Point { x: 0.0, y: 0.0 })
3281        })
3282        .collect::<Result<Vec<_>>>()?;
3283    let available = if settings.overlay { full_plot } else { bounds };
3284    let capacity = available.width - 12.0;
3285    // Frozen native Word/PowerPoint 9 point legend controls qualify these
3286    // nominal-font metrics within 1 point and the unchanged cropped paint gate.
3287    let row_height = labels.first().map_or(0.0, |label| label.font_size * 1.875);
3288    let mut positions = Vec::with_capacity(labels.len());
3289    let mut width: f64 = 0.0;
3290    let mut x = 0.0;
3291    let mut y = 0.0;
3292    for label in &labels {
3293        let entry_width = label.advances.iter().sum::<f64>() + label.font_size * 7.0 / 9.0 + 4.0;
3294        if !entry_width.is_finite() || entry_width > capacity {
3295            return Err(invalid_authoring_value("legend label exceeds chart width"));
3296        }
3297        if horizontal && x > 0.0 && x + entry_width > capacity {
3298            x = 0.0;
3299            y += row_height;
3300        }
3301        positions.push(Point { x, y });
3302        width = width.max(x + entry_width);
3303        if horizontal {
3304            x += entry_width + 4.0;
3305        } else {
3306            y += row_height;
3307        }
3308    }
3309    let height = if labels.is_empty() {
3310        0.0
3311    } else if horizontal {
3312        y + row_height
3313    } else {
3314        y
3315    };
3316    if height > available.height - 12.0 {
3317        return Err(invalid_authoring_value(
3318            "legend entries exceed chart height",
3319        ));
3320    }
3321    let mut content = bounds;
3322    if !settings.overlay && !labels.is_empty() {
3323        match settings.position {
3324            LegendPosition::Right | LegendPosition::TopRight => content.width -= width + 10.0,
3325            LegendPosition::Left => {
3326                content.x += width + 10.0;
3327                content.width -= width + 10.0;
3328            }
3329            LegendPosition::Top => {
3330                content.y += height + 10.0;
3331                content.height -= height + 10.0;
3332            }
3333            LegendPosition::Bottom => content.height -= height + 10.0,
3334        }
3335    }
3336    let plot_bounds = geometry_plot_bounds(content)?;
3337    let area = if settings.overlay {
3338        plot_bounds
3339    } else {
3340        bounds
3341    };
3342    let origin = match settings.position {
3343        LegendPosition::Right => Point {
3344            x: area.x + area.width - width - 6.0,
3345            y: area.y + (area.height - height) / 2.0,
3346        },
3347        LegendPosition::TopRight => Point {
3348            x: area.x + area.width - width - 6.0,
3349            y: area.y + 6.0,
3350        },
3351        LegendPosition::Left => Point {
3352            x: area.x + 6.0,
3353            y: area.y + (area.height - height) / 2.0,
3354        },
3355        LegendPosition::Top => Point {
3356            x: area.x + (area.width - width) / 2.0,
3357            y: area.y + 6.0,
3358        },
3359        LegendPosition::Bottom => Point {
3360            x: area.x + (area.width - width) / 2.0,
3361            y: area.y + area.height - height - 6.0,
3362        },
3363    };
3364    let mut keys = Vec::with_capacity(labels.len());
3365    for (label, offset) in labels.iter_mut().zip(positions) {
3366        let x = origin.x + offset.x;
3367        let y = origin.y + offset.y;
3368        keys.push(Rect {
3369            x,
3370            y: y + (row_height - label.font_size * 5.0 / 9.0) / 2.0,
3371            width: label.font_size * 5.0 / 9.0,
3372            height: label.font_size * 5.0 / 9.0,
3373        });
3374        label.origin = Point {
3375            x: x + label.font_size * 7.0 / 9.0,
3376            y: y + label.font_size * 7.0 / 6.0,
3377        };
3378    }
3379    Ok(LegendLayout {
3380        plot: plot_bounds,
3381        labels,
3382        keys,
3383    })
3384}
3385
3386fn render_legend(
3387    layout: LegendLayout,
3388    series_colours: &[Color],
3389    paths: &mut Vec<PositionedElement>,
3390    labels: &mut Vec<PositionedElement>,
3391) {
3392    for (key, colour) in layout.keys.into_iter().zip(series_colours) {
3393        paths.push(filled_path(Path::rect(key), *colour));
3394    }
3395    labels.extend(layout.labels.into_iter().map(PositionedElement::Text));
3396}
3397
3398fn validate_geometry_coordinates(elements: &[PositionedElement]) -> Result<()> {
3399    fn finite_point(point: Point) -> bool {
3400        point.x.is_finite() && point.y.is_finite()
3401    }
3402
3403    for element in elements {
3404        match element {
3405            PositionedElement::Path(path) => {
3406                for command in &path.path.commands {
3407                    let finite = match command {
3408                        PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
3409                            finite_point(*point)
3410                        }
3411                        PathCommand::CurveTo { c1, c2, to } => {
3412                            finite_point(*c1) && finite_point(*c2) && finite_point(*to)
3413                        }
3414                        PathCommand::Close => true,
3415                    };
3416                    if !finite {
3417                        return Err(ChartError::InvalidValue {
3418                            element: "chart geometry coordinate".to_owned(),
3419                            value: "generated path contains a nonfinite point".to_owned(),
3420                        });
3421                    }
3422                }
3423            }
3424            PositionedElement::Group(group) => validate_geometry_coordinates(&group.children)?,
3425            _ => {}
3426        }
3427    }
3428    Ok(())
3429}
3430
3431fn render_plot_geometry(
3432    plot: &Plot,
3433    bounds: Rect,
3434    blanks: DispBlanksAs,
3435    options: GeometryOptions,
3436    series_colours: &[Color],
3437) -> Result<Vec<PositionedElement>> {
3438    match plot {
3439        Plot::Bar {
3440            direction,
3441            grouping,
3442            gap_width,
3443            overlap,
3444            series,
3445            ..
3446        } => render_bar_geometry(
3447            *direction,
3448            *grouping,
3449            *gap_width,
3450            *overlap,
3451            series,
3452            bounds,
3453            options,
3454            series_colours,
3455        ),
3456        Plot::Line {
3457            grouping,
3458            marker,
3459            series,
3460            ..
3461        } => render_line_geometry(
3462            *grouping,
3463            *marker,
3464            series,
3465            bounds,
3466            blanks,
3467            options,
3468            series_colours,
3469        ),
3470        Plot::Pie {
3471            first_slice_angle,
3472            series,
3473            ..
3474        } => render_pie_geometry(*first_slice_angle, None, series, bounds, series_colours),
3475        Plot::Doughnut {
3476            first_slice_angle,
3477            hole_size,
3478            series,
3479            ..
3480        } => render_pie_geometry(
3481            *first_slice_angle,
3482            Some(*hole_size),
3483            series,
3484            bounds,
3485            series_colours,
3486        ),
3487        Plot::Area {
3488            grouping, series, ..
3489        } => render_area_geometry(*grouping, series, bounds, blanks, options, series_colours),
3490        Plot::Scatter { style, series, .. } => {
3491            render_scatter_geometry(*style, series, bounds, blanks, options, series_colours)
3492        }
3493        Plot::Radar { style, series, .. } => {
3494            render_radar_geometry(*style, series, bounds, options, series_colours)
3495        }
3496    }
3497}
3498
3499#[allow(clippy::too_many_arguments)]
3500fn render_bar_geometry(
3501    direction: BarDirection,
3502    grouping: BarGrouping,
3503    gap_width: u16,
3504    overlap: i8,
3505    series: &[Series],
3506    bounds: Rect,
3507    options: GeometryOptions,
3508    series_colours: &[Color],
3509) -> Result<Vec<PositionedElement>> {
3510    let bars = bar_rectangles(
3511        direction, grouping, gap_width, overlap, series, bounds, options,
3512    )?;
3513    bars.into_iter()
3514        .map(|bar| {
3515            Ok(filled_path(
3516                Path::rect(bar.rect),
3517                series_colour(series_colours, bar.series_index)?,
3518            ))
3519        })
3520        .collect()
3521}
3522
3523#[derive(Clone, Copy, Debug)]
3524struct BarRectangle {
3525    series_index: usize,
3526    logical_index: usize,
3527    rect: Rect,
3528    base: Point,
3529    end: Point,
3530}
3531
3532fn bar_rectangles(
3533    direction: BarDirection,
3534    grouping: BarGrouping,
3535    gap_width: u16,
3536    overlap: i8,
3537    series: &[Series],
3538    bounds: Rect,
3539    options: GeometryOptions,
3540) -> Result<Vec<BarRectangle>> {
3541    validate_series_values(series)?;
3542    let logical_series = logical_series_values(series)?;
3543    let category_count = series_category_count(series)?;
3544    let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
3545    let percent = grouping == BarGrouping::PercentStacked;
3546    let layers = stacked_series_bounds(&logical_series, stacked, percent)?;
3547    let domain = options
3548        .value
3549        .map(|mapping| mapping.domain)
3550        .unwrap_or(domain_from_layers(&layers)?);
3551    let value_orientation = options
3552        .value
3553        .map(|mapping| mapping.orientation)
3554        .unwrap_or(Orientation::MinMax);
3555    let category_extent = if direction == BarDirection::Column {
3556        bounds.width
3557    } else {
3558        bounds.height
3559    };
3560    let slot = category_extent / category_count as f64;
3561    let cluster_extent = slot * 100.0 / (100.0 + f64::from(gap_width));
3562    let bars_per_cluster = if stacked { 1 } else { series.len() };
3563    let overlap_fraction = if stacked {
3564        0.0
3565    } else {
3566        f64::from(overlap) / 100.0
3567    };
3568    let divisor =
3569        bars_per_cluster as f64 - (bars_per_cluster.saturating_sub(1)) as f64 * overlap_fraction;
3570    let bar_extent = cluster_extent / divisor;
3571    let advance = bar_extent * (1.0 - overlap_fraction);
3572    let mut bars = Vec::new();
3573    for (series_index, layer) in layers.iter().enumerate() {
3574        for &(logical_index, start, end) in layer {
3575            let category = oriented_category_index(
3576                logical_index,
3577                category_count,
3578                options.category_orientation,
3579            );
3580            let cluster_start = category as f64 / category_count as f64 * category_extent
3581                + (slot - cluster_extent) / 2.0;
3582            let category_start = if stacked {
3583                cluster_start
3584            } else {
3585                cluster_start + series_index as f64 * advance
3586            };
3587            let (rect, base, end) = if direction == BarDirection::Column {
3588                let y0 = map_y_oriented(start, domain, bounds, value_orientation)?;
3589                let y1 = map_y_oriented(end, domain, bounds, value_orientation)?;
3590                let center_x = bounds.x + category_start + bar_extent / 2.0;
3591                (
3592                    Rect {
3593                        x: bounds.x + category_start,
3594                        y: y0.min(y1),
3595                        width: bar_extent,
3596                        height: (y1 - y0).abs(),
3597                    },
3598                    Point { x: center_x, y: y0 },
3599                    Point { x: center_x, y: y1 },
3600                )
3601            } else {
3602                let x0 = map_x_oriented(start, domain, bounds, value_orientation)?;
3603                let x1 = map_x_oriented(end, domain, bounds, value_orientation)?;
3604                let center_y = bounds.y + category_start + bar_extent / 2.0;
3605                (
3606                    Rect {
3607                        x: x0.min(x1),
3608                        y: bounds.y + category_start,
3609                        width: (x1 - x0).abs(),
3610                        height: bar_extent,
3611                    },
3612                    Point { x: x0, y: center_y },
3613                    Point { x: x1, y: center_y },
3614                )
3615            };
3616            bars.push(BarRectangle {
3617                series_index,
3618                logical_index,
3619                rect,
3620                base,
3621                end,
3622            });
3623        }
3624    }
3625    Ok(bars)
3626}
3627
3628fn render_line_geometry(
3629    grouping: Grouping,
3630    marker: bool,
3631    series: &[Series],
3632    bounds: Rect,
3633    blanks: DispBlanksAs,
3634    options: GeometryOptions,
3635    series_colours: &[Color],
3636) -> Result<Vec<PositionedElement>> {
3637    validate_series_values(series)?;
3638    let logical_series = logical_series_values(series)?;
3639    let category_count = series_category_count(series)?;
3640    let calculated_series = if blanks == DispBlanksAs::Zero {
3641        zero_filled_series(&logical_series, category_count)
3642    } else {
3643        logical_series.clone()
3644    };
3645    let stacked = grouping != Grouping::Standard;
3646    let percent = grouping == Grouping::PercentStacked;
3647    let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
3648    let domain = options
3649        .value
3650        .map(|mapping| mapping.domain)
3651        .unwrap_or(domain_from_layers(&layers)?);
3652    let value_orientation = options
3653        .value
3654        .map(|mapping| mapping.orientation)
3655        .unwrap_or(Orientation::MinMax);
3656    let mut elements = Vec::new();
3657    for (series_index, layer) in layers.iter().enumerate() {
3658        let colour = series_colour(series_colours, series_index)?;
3659        let indexed: Vec<_> = layer
3660            .iter()
3661            .map(|(index, _, upper)| (*index, *upper))
3662            .collect();
3663        let indexes: Vec<_> = indexed.iter().map(|(index, _)| *index).collect();
3664        for (start, end) in contiguous_ranges(&indexes, blanks) {
3665            let points = category_points(
3666                &indexed[start..end],
3667                category_count,
3668                domain,
3669                bounds,
3670                options.category_orientation,
3671                value_orientation,
3672            )?;
3673            elements.push(stroked_path(polyline_path(&points, false), colour));
3674        }
3675        if marker {
3676            let present: BTreeSet<_> = logical_series[series_index]
3677                .1
3678                .iter()
3679                .map(|(index, _)| *index)
3680                .collect();
3681            let marker_values: Vec<_> = indexed
3682                .iter()
3683                .copied()
3684                .filter(|(index, _)| present.contains(index))
3685                .collect();
3686            let marker_points = category_points(
3687                &marker_values,
3688                category_count,
3689                domain,
3690                bounds,
3691                options.category_orientation,
3692                value_orientation,
3693            )?;
3694            push_markers(&mut elements, &marker_points, colour);
3695        }
3696    }
3697    Ok(elements)
3698}
3699
3700fn render_area_geometry(
3701    grouping: Grouping,
3702    series: &[Series],
3703    bounds: Rect,
3704    blanks: DispBlanksAs,
3705    options: GeometryOptions,
3706    series_colours: &[Color],
3707) -> Result<Vec<PositionedElement>> {
3708    validate_series_values(series)?;
3709    let logical_series = logical_series_values(series)?;
3710    let category_count = series_category_count(series)?;
3711    let calculated_series = if blanks == DispBlanksAs::Zero {
3712        zero_filled_series(&logical_series, category_count)
3713    } else {
3714        logical_series
3715    };
3716    let stacked = grouping != Grouping::Standard;
3717    let percent = grouping == Grouping::PercentStacked;
3718    let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
3719    let domain = options
3720        .value
3721        .map(|mapping| mapping.domain)
3722        .unwrap_or(domain_from_layers(&layers)?);
3723    let value_orientation = options
3724        .value
3725        .map(|mapping| mapping.orientation)
3726        .unwrap_or(Orientation::MinMax);
3727    let mut elements = Vec::new();
3728    for (series_index, layer) in layers.iter().enumerate() {
3729        let colour = series_colour(series_colours, series_index)?;
3730        let indexes: Vec<_> = layer.iter().map(|(index, _, _)| *index).collect();
3731        for (start, end) in contiguous_ranges(&indexes, blanks) {
3732            let segment = &layer[start..end];
3733            let top_values: Vec<_> = segment
3734                .iter()
3735                .map(|(index, _, upper)| (*index, *upper))
3736                .collect();
3737            let lower_values: Vec<_> = segment
3738                .iter()
3739                .map(|(index, lower, _)| (*index, *lower))
3740                .collect();
3741            let top = category_points(
3742                &top_values,
3743                category_count,
3744                domain,
3745                bounds,
3746                options.category_orientation,
3747                value_orientation,
3748            )?;
3749            let mut bottom = category_points(
3750                &lower_values,
3751                category_count,
3752                domain,
3753                bounds,
3754                options.category_orientation,
3755                value_orientation,
3756            )?;
3757            bottom.reverse();
3758            let mut commands = Vec::with_capacity(top.len() + bottom.len() + 2);
3759            commands.push(PathCommand::MoveTo(top[0]));
3760            commands.extend(top.iter().skip(1).copied().map(PathCommand::LineTo));
3761            commands.extend(bottom.into_iter().map(PathCommand::LineTo));
3762            commands.push(PathCommand::Close);
3763            elements.push(filled_and_stroked_path(
3764                Path {
3765                    commands,
3766                    fill_rule: FillRule::NonZero,
3767                },
3768                colour,
3769            ));
3770        }
3771    }
3772    Ok(elements)
3773}
3774
3775fn render_scatter_geometry(
3776    style: ScatterStyle,
3777    series: &[Series],
3778    bounds: Rect,
3779    blanks: DispBlanksAs,
3780    options: GeometryOptions,
3781    series_colours: &[Color],
3782) -> Result<Vec<PositionedElement>> {
3783    validate_series_values(series)?;
3784    let mut paired_series = Vec::with_capacity(series.len());
3785    let mut marker_series = Vec::with_capacity(series.len());
3786    let mut all_x = Vec::new();
3787    let mut all_y = Vec::new();
3788    for item in series {
3789        let markers = matched_scatter_points(item)?;
3790        let paired = scatter_render_points(item, blanks)?;
3791        all_x.extend(paired.iter().map(|(_, x, _)| *x));
3792        all_y.extend(paired.iter().map(|(_, _, y)| *y));
3793        paired_series.push(paired);
3794        marker_series.push(markers);
3795    }
3796    let x_domain = options
3797        .scatter_x
3798        .map(|mapping| mapping.domain)
3799        .unwrap_or(data_domain(&all_x, false)?);
3800    let y_domain = options
3801        .scatter_y
3802        .map(|mapping| mapping.domain)
3803        .unwrap_or(data_domain(&all_y, false)?);
3804    let x_orientation = options
3805        .scatter_x
3806        .map(|mapping| mapping.orientation)
3807        .unwrap_or(Orientation::MinMax);
3808    let y_orientation = options
3809        .scatter_y
3810        .map(|mapping| mapping.orientation)
3811        .unwrap_or(Orientation::MinMax);
3812    let draw_line = matches!(
3813        style,
3814        ScatterStyle::Line
3815            | ScatterStyle::LineMarker
3816            | ScatterStyle::Smooth
3817            | ScatterStyle::SmoothMarker
3818    );
3819    let draw_marker = matches!(
3820        style,
3821        ScatterStyle::Marker | ScatterStyle::LineMarker | ScatterStyle::SmoothMarker
3822    );
3823    let mut elements = Vec::new();
3824    for (series_index, paired) in paired_series.iter().enumerate() {
3825        let colour = series_colour(series_colours, series_index)?;
3826        if draw_line {
3827            let indexes: Vec<_> = paired.iter().map(|(index, _, _)| *index).collect();
3828            for (start, end) in contiguous_ranges(&indexes, blanks) {
3829                let points = scatter_points(
3830                    &paired[start..end],
3831                    x_domain,
3832                    y_domain,
3833                    bounds,
3834                    x_orientation,
3835                    y_orientation,
3836                )?;
3837                elements.push(stroked_path(polyline_path(&points, false), colour));
3838            }
3839        }
3840        if draw_marker {
3841            let marker_points = scatter_points(
3842                &marker_series[series_index],
3843                x_domain,
3844                y_domain,
3845                bounds,
3846                x_orientation,
3847                y_orientation,
3848            )?;
3849            push_markers(&mut elements, &marker_points, colour);
3850        }
3851    }
3852    Ok(elements)
3853}
3854
3855type ScatterPoint = (usize, f64, f64);
3856
3857struct ScatterPointMaps {
3858    x_count: usize,
3859    y_count: usize,
3860    x_by_index: BTreeMap<usize, f64>,
3861    y_by_index: BTreeMap<usize, f64>,
3862}
3863
3864fn scatter_point_maps(series: &Series) -> Result<ScatterPointMaps> {
3865    let Some(AxisData::Numeric(x_values)) = &series.categories else {
3866        return Err(ChartError::InvalidValue {
3867            element: "c:xVal".to_owned(),
3868            value: "scatter series requires numeric cached x values".to_owned(),
3869        });
3870    };
3871    validate_finite(&x_values.values, "c:xVal/c:numCache")?;
3872    let (x_count, x_points) = logical_numeric_values(x_values)?;
3873    let (y_count, y_points) = logical_numeric_values(&series.values)?;
3874    Ok(ScatterPointMaps {
3875        x_count,
3876        y_count,
3877        x_by_index: x_points.into_iter().collect(),
3878        y_by_index: y_points.into_iter().collect(),
3879    })
3880}
3881
3882fn matched_scatter_points(series: &Series) -> Result<Vec<ScatterPoint>> {
3883    let points = scatter_point_maps(series)?;
3884    Ok(points
3885        .y_by_index
3886        .iter()
3887        .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
3888        .collect())
3889}
3890
3891fn scatter_render_points(series: &Series, blanks: DispBlanksAs) -> Result<Vec<ScatterPoint>> {
3892    let points = scatter_point_maps(series)?;
3893    if blanks != DispBlanksAs::Zero {
3894        return Ok(points
3895            .y_by_index
3896            .iter()
3897            .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
3898            .collect());
3899    }
3900    let count = points.x_count.max(points.y_count);
3901    let present = points
3902        .x_by_index
3903        .keys()
3904        .chain(points.y_by_index.keys())
3905        .copied()
3906        .collect();
3907    Ok(zero_control_indexes(count, &present)
3908        .into_iter()
3909        .map(|index| {
3910            (
3911                index,
3912                points.x_by_index.get(&index).copied().unwrap_or(0.0),
3913                points.y_by_index.get(&index).copied().unwrap_or(0.0),
3914            )
3915        })
3916        .collect())
3917}
3918
3919fn scatter_points(
3920    values: &[(usize, f64, f64)],
3921    x_domain: Domain,
3922    y_domain: Domain,
3923    bounds: Rect,
3924    x_orientation: Orientation,
3925    y_orientation: Orientation,
3926) -> Result<Vec<Point>> {
3927    values
3928        .iter()
3929        .map(|(_, x, y)| {
3930            Ok(Point {
3931                x: map_x_oriented(*x, x_domain, bounds, x_orientation)?,
3932                y: map_y_oriented(*y, y_domain, bounds, y_orientation)?,
3933            })
3934        })
3935        .collect()
3936}
3937
3938fn render_radar_geometry(
3939    style: RadarStyle,
3940    series: &[Series],
3941    bounds: Rect,
3942    options: GeometryOptions,
3943    series_colours: &[Color],
3944) -> Result<Vec<PositionedElement>> {
3945    let point_series = radar_series_points(series, bounds, options)?;
3946    let mut elements = Vec::new();
3947    for (series_index, points) in point_series.iter().enumerate() {
3948        let colour = series_colour(series_colours, series_index)?;
3949        let plain_points: Vec<_> = points.iter().map(|(_, point)| *point).collect();
3950        if plain_points.is_empty() {
3951            continue;
3952        }
3953        let path = polyline_path(&plain_points, true);
3954        if style == RadarStyle::Filled {
3955            elements.push(filled_and_stroked_path(path, colour));
3956        } else {
3957            elements.push(stroked_path(path, colour));
3958        }
3959        if style == RadarStyle::Marker {
3960            push_markers(&mut elements, &plain_points, colour);
3961        }
3962    }
3963    Ok(elements)
3964}
3965
3966fn radar_series_points(
3967    series: &[Series],
3968    bounds: Rect,
3969    options: GeometryOptions,
3970) -> Result<Vec<Vec<(usize, Point)>>> {
3971    validate_series_values(series)?;
3972    let logical_series = logical_series_values(series)?;
3973    let category_count = series_category_count(series)?;
3974    let values = logical_series
3975        .iter()
3976        .flat_map(|(_, points)| points.iter().map(|(_, value)| *value))
3977        .collect::<Vec<_>>();
3978    let mapping = options.value.unwrap_or(AxisMapping {
3979        domain: data_domain(&values, true)?,
3980        orientation: Orientation::MinMax,
3981    });
3982    let center = Point {
3983        x: bounds.x + bounds.width / 2.0,
3984        y: bounds.y + bounds.height / 2.0,
3985    };
3986    let radius = bounds.width.min(bounds.height) / 2.0;
3987    let mut point_series = Vec::with_capacity(logical_series.len());
3988    for (_, logical) in &logical_series {
3989        let mut points = Vec::with_capacity(logical.len());
3990        for (category, value) in logical {
3991            if *value < mapping.domain.min || *value > mapping.domain.max {
3992                continue;
3993            }
3994            points.push((
3995                *category,
3996                radial_point(
3997                    center,
3998                    normalized_value_oriented(*value, mapping.domain, mapping.orientation)?
3999                        * radius,
4000                    oriented_category_index(
4001                        *category,
4002                        category_count,
4003                        options.category_orientation,
4004                    ),
4005                    category_count,
4006                ),
4007            ));
4008        }
4009        point_series.push(points);
4010    }
4011    Ok(point_series)
4012}
4013
4014fn render_pie_geometry(
4015    first_slice_angle: u16,
4016    hole_size: Option<u8>,
4017    series: &[Series],
4018    bounds: Rect,
4019    series_colours: &[Color],
4020) -> Result<Vec<PositionedElement>> {
4021    pie_slices(first_slice_angle, hole_size, series, bounds)?
4022        .into_iter()
4023        .map(|slice| {
4024            Ok(filled_path(
4025                slice.path,
4026                series_colour(series_colours, slice.series_index)?,
4027            ))
4028        })
4029        .collect()
4030}
4031
4032struct PieSliceGeometry {
4033    series_index: usize,
4034    logical_index: usize,
4035    path: Path,
4036    anchor: Point,
4037    base: Point,
4038    end: Point,
4039}
4040
4041fn pie_slices(
4042    first_slice_angle: u16,
4043    hole_size: Option<u8>,
4044    series: &[Series],
4045    bounds: Rect,
4046) -> Result<Vec<PieSliceGeometry>> {
4047    validate_series_values(series)?;
4048    let center = Point {
4049        x: bounds.x + bounds.width / 2.0,
4050        y: bounds.y + bounds.height / 2.0,
4051    };
4052    let radius = bounds.width.min(bounds.height) / 2.0;
4053    let mut elements = Vec::new();
4054    for (series_index, item) in series.iter().enumerate() {
4055        let (_, logical) = logical_numeric_values(&item.values)?;
4056        let total = logical
4057            .iter()
4058            .map(|(_, value)| *value)
4059            .filter(|value| *value > 0.0)
4060            .try_fold(0.0, |total, value| {
4061                checked_geometry_sum(total, value, "pie value total")
4062            })?;
4063        if total <= 0.0 {
4064            continue;
4065        }
4066        let mut angle = f64::from(first_slice_angle).to_radians() - std::f64::consts::FRAC_PI_2;
4067        for (logical_index, value) in logical {
4068            if value <= 0.0 {
4069                continue;
4070            }
4071            let next = angle + value / total * std::f64::consts::TAU;
4072            let path = if let Some(hole_size) = hole_size {
4073                doughnut_wedge(
4074                    center,
4075                    radius,
4076                    radius * f64::from(hole_size) / 100.0,
4077                    angle,
4078                    next,
4079                )
4080            } else {
4081                pie_wedge(center, radius, angle, next)
4082            };
4083            let inner_radius = hole_size
4084                .map(|hole| radius * f64::from(hole) / 100.0)
4085                .unwrap_or(0.0);
4086            let anchor_radius = hole_size
4087                .map(|_| (radius + inner_radius) / 2.0)
4088                .unwrap_or(radius * 2.0 / 3.0);
4089            let midpoint = (angle + next) / 2.0;
4090            elements.push(PieSliceGeometry {
4091                series_index,
4092                logical_index,
4093                path,
4094                anchor: circle_point(center, anchor_radius, midpoint),
4095                base: circle_point(center, inner_radius, midpoint),
4096                end: circle_point(center, radius, midpoint),
4097            });
4098            angle = next;
4099        }
4100    }
4101    Ok(elements)
4102}
4103
4104fn validate_series_values(series: &[Series]) -> Result<()> {
4105    if series.is_empty() {
4106        return Err(ChartError::MissingElement("c:ser".to_owned()));
4107    }
4108    for item in series {
4109        validate_finite(&item.values.values, "c:val/c:numCache")?;
4110    }
4111    Ok(())
4112}
4113
4114fn validate_finite(values: &[f64], element: &str) -> Result<()> {
4115    if let Some(value) = values.iter().find(|value| !value.is_finite()) {
4116        return Err(ChartError::InvalidValue {
4117            element: element.to_owned(),
4118            value: value.to_string(),
4119        });
4120    }
4121    Ok(())
4122}
4123
4124type IndexedValues = Vec<(usize, f64)>;
4125type LogicalSeries = (usize, IndexedValues);
4126type GeometryLayer = Vec<(usize, f64, f64)>;
4127
4128fn zero_control_indexes(count: usize, present: &BTreeSet<usize>) -> BTreeSet<usize> {
4129    let mut controls = present.clone();
4130    if count == 0 {
4131        return controls;
4132    }
4133    controls.insert(0);
4134    controls.insert(count - 1);
4135    for index in present {
4136        if *index > 0 {
4137            controls.insert(*index - 1);
4138        }
4139        if index + 1 < count {
4140            controls.insert(index + 1);
4141        }
4142    }
4143    controls
4144}
4145
4146fn zero_filled_series(series: &[LogicalSeries], count: usize) -> Vec<LogicalSeries> {
4147    let present = series
4148        .iter()
4149        .flat_map(|(_, points)| points.iter().map(|(index, _)| *index))
4150        .collect();
4151    let controls = zero_control_indexes(count, &present);
4152    series
4153        .iter()
4154        .map(|(declared, points)| {
4155            let values: BTreeMap<_, _> = points.iter().copied().collect();
4156            let filled = controls
4157                .iter()
4158                .map(|index| (*index, values.get(index).copied().unwrap_or(0.0)))
4159                .collect();
4160            (*declared, filled)
4161        })
4162        .collect()
4163}
4164
4165fn contiguous_ranges(indexes: &[usize], blanks: DispBlanksAs) -> Vec<(usize, usize)> {
4166    if indexes.is_empty() {
4167        return Vec::new();
4168    }
4169    if blanks != DispBlanksAs::Gap {
4170        return vec![(0, indexes.len())];
4171    }
4172    let mut ranges = Vec::new();
4173    let mut start = 0usize;
4174    for position in 1..indexes.len() {
4175        if indexes[position - 1].checked_add(1) != Some(indexes[position]) {
4176            ranges.push((start, position));
4177            start = position;
4178        }
4179    }
4180    ranges.push((start, indexes.len()));
4181    ranges
4182}
4183
4184fn logical_numeric_values(data: &NumericData) -> Result<LogicalSeries> {
4185    validate_finite(&data.values, "c:numCache")?;
4186    let (declared, indexes) = cache_layout(&data.markup, data.values.len())?;
4187    let declared = usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
4188        element: "c:ptCount".to_owned(),
4189        value: declared.to_string(),
4190    })?;
4191    let points = indexes
4192        .into_iter()
4193        .zip(data.values.iter().copied())
4194        .map(|(index, value)| {
4195            usize::try_from(index)
4196                .map(|index| (index, value))
4197                .map_err(|_| ChartError::InvalidValue {
4198                    element: "c:pt/@idx".to_owned(),
4199                    value: index.to_string(),
4200                })
4201        })
4202        .collect::<Result<Vec<_>>>()?;
4203    Ok((declared, points))
4204}
4205
4206fn logical_series_values(series: &[Series]) -> Result<Vec<LogicalSeries>> {
4207    series
4208        .iter()
4209        .map(|item| logical_numeric_values(&item.values))
4210        .collect()
4211}
4212
4213fn reference_logical_count(markup: &ReferenceMarkup, value_count: usize) -> Result<usize> {
4214    let (declared, _) = cache_layout(markup, value_count)?;
4215    usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
4216        element: "c:ptCount".to_owned(),
4217        value: declared.to_string(),
4218    })
4219}
4220
4221fn series_category_count(series: &[Series]) -> Result<usize> {
4222    let mut count = 0usize;
4223    for item in series {
4224        count = count.max(reference_logical_count(
4225            &item.values.markup,
4226            item.values.values.len(),
4227        )?);
4228        if let Some(categories) = &item.categories {
4229            let category_count = match categories {
4230                AxisData::String(data) => reference_logical_count(&data.markup, data.values.len())?,
4231                AxisData::Numeric(data) => {
4232                    reference_logical_count(&data.markup, data.values.len())?
4233                }
4234            };
4235            count = count.max(category_count);
4236        }
4237    }
4238    if count == 0 {
4239        return Err(ChartError::InvalidValue {
4240            element: "c:ser".to_owned(),
4241            value: "cached series contain no points".to_owned(),
4242        });
4243    }
4244    Ok(count)
4245}
4246
4247fn stacked_series_bounds(
4248    series: &[LogicalSeries],
4249    stacked: bool,
4250    percent: bool,
4251) -> Result<Vec<GeometryLayer>> {
4252    let mut positive_totals = BTreeMap::<usize, f64>::new();
4253    let mut negative_totals = BTreeMap::<usize, f64>::new();
4254    if percent {
4255        for (_, points) in series {
4256            for &(index, value) in points {
4257                let totals = if value >= 0.0 {
4258                    &mut positive_totals
4259                } else {
4260                    &mut negative_totals
4261                };
4262                let total = totals.entry(index).or_default();
4263                *total = checked_geometry_sum(*total, value.abs(), "stacked percentage total")?;
4264            }
4265        }
4266    }
4267    let mut positive = BTreeMap::<usize, f64>::new();
4268    let mut negative = BTreeMap::<usize, f64>::new();
4269    let mut layers = Vec::with_capacity(series.len());
4270    for (_, points) in series {
4271        let mut layer = Vec::with_capacity(points.len());
4272        for &(index, original) in points {
4273            let total = if original >= 0.0 {
4274                positive_totals.get(&index).copied().unwrap_or(0.0)
4275            } else {
4276                negative_totals.get(&index).copied().unwrap_or(0.0)
4277            };
4278            let value = if percent && total != 0.0 {
4279                original / total
4280            } else {
4281                original
4282            };
4283            if stacked {
4284                let accumulators = if value >= 0.0 {
4285                    &mut positive
4286                } else {
4287                    &mut negative
4288                };
4289                let accumulator = accumulators.entry(index).or_default();
4290                let lower = *accumulator;
4291                *accumulator = checked_geometry_sum(*accumulator, value, "stacked value total")?;
4292                layer.push((index, lower, *accumulator));
4293            } else {
4294                layer.push((index, 0.0, value));
4295            }
4296        }
4297        layers.push(layer);
4298    }
4299    Ok(layers)
4300}
4301
4302fn checked_geometry_sum(current: f64, value: f64, context: &str) -> Result<f64> {
4303    let sum = current + value;
4304    if sum.is_finite() {
4305        Ok(sum)
4306    } else {
4307        Err(ChartError::InvalidValue {
4308            element: "chart geometry aggregate".to_owned(),
4309            value: format!("{context} is nonfinite"),
4310        })
4311    }
4312}
4313
4314#[derive(Clone, Copy, Debug, PartialEq)]
4315struct Domain {
4316    min: f64,
4317    max: f64,
4318}
4319
4320fn nice_number_scale(
4321    extent: Domain,
4322    pinned_minimum: Option<f64>,
4323    pinned_maximum: Option<f64>,
4324    target_tick_count: usize,
4325) -> Result<AxisScale> {
4326    if target_tick_count < 2
4327        || !extent.min.is_finite()
4328        || !extent.max.is_finite()
4329        || pinned_minimum.is_some_and(|value| !value.is_finite())
4330        || pinned_maximum.is_some_and(|value| !value.is_finite())
4331    {
4332        return Err(ChartError::InvalidValue {
4333            element: "chart axis scale".to_owned(),
4334            value: "scale bounds and target tick count must be finite and valid".to_owned(),
4335        });
4336    }
4337
4338    let mut data_min = extent.min.min(extent.max);
4339    let mut data_max = extent.min.max(extent.max);
4340    if data_min == data_max {
4341        let expanded = expand_constant_domain(data_min)?;
4342        data_min = expanded.min;
4343        data_max = expanded.max;
4344    }
4345    let requested_min = match (pinned_minimum, pinned_maximum) {
4346        (Some(minimum), _) => minimum,
4347        (None, Some(maximum)) if data_min >= maximum => {
4348            extend_bound(maximum, stable_positive_step(data_min, data_max, 3)?, false)?
4349        }
4350        (None, _) => data_min,
4351    };
4352    let requested_max = match (pinned_minimum, pinned_maximum) {
4353        (_, Some(maximum)) => maximum,
4354        (Some(minimum), None) if data_max <= minimum => {
4355            extend_bound(minimum, stable_positive_step(data_min, data_max, 3)?, true)?
4356        }
4357        (_, None) => data_max,
4358    };
4359    if requested_min >= requested_max {
4360        return Err(ChartError::InvalidValue {
4361            element: "c:scaling".to_owned(),
4362            value: format!("minimum {requested_min} is not less than maximum {requested_max}"),
4363        });
4364    }
4365
4366    let raw_step = stable_positive_step(requested_min, requested_max, target_tick_count)?;
4367    let step = nice_step(raw_step)?;
4368    let rounded_minimum = (requested_min / step).floor() * step;
4369    let rounded_maximum = (requested_max / step).ceil() * step;
4370    let lower_fallback = pinned_minimum.is_none() && !rounded_minimum.is_finite();
4371    let upper_fallback = pinned_maximum.is_none() && !rounded_maximum.is_finite();
4372    let minimum = pinned_minimum.unwrap_or(if rounded_minimum.is_finite() {
4373        rounded_minimum
4374    } else {
4375        requested_min
4376    });
4377    let maximum = pinned_maximum.unwrap_or(if rounded_maximum.is_finite() {
4378        rounded_maximum
4379    } else {
4380        requested_max
4381    });
4382    if !minimum.is_finite() || !maximum.is_finite() || minimum >= maximum {
4383        return Err(ChartError::InvalidValue {
4384            element: "chart axis scale".to_owned(),
4385            value: "nice-number bounds are nonfinite or empty".to_owned(),
4386        });
4387    }
4388
4389    let first = (minimum / step).ceil();
4390    let last = (maximum / step).floor();
4391    let tick_count = if last >= first {
4392        (last - first).min(1024.0) as usize + 1
4393    } else {
4394        0
4395    };
4396    let mut ticks = Vec::with_capacity(tick_count);
4397    for index in 0..tick_count {
4398        let tick = (first + index as f64) * step;
4399        if tick.is_finite() && tick >= minimum && tick <= maximum {
4400            ticks.push(clean_tick(tick, step));
4401        }
4402    }
4403    if lower_fallback && ticks.first().is_none_or(|tick| *tick > minimum) {
4404        ticks.insert(0, minimum);
4405    }
4406    if upper_fallback && ticks.last().is_none_or(|tick| *tick < maximum) {
4407        ticks.push(maximum);
4408    }
4409    if ticks.len() < 2 {
4410        ticks.extend([minimum, maximum]);
4411        ticks.sort_by(f64::total_cmp);
4412    }
4413    ticks.dedup_by(|left, right| *left == *right);
4414    if ticks.len() < 2 || !ticks.windows(2).all(|pair| pair[0] < pair[1]) {
4415        return Err(ChartError::InvalidValue {
4416            element: "chart axis scale".to_owned(),
4417            value: "scale cannot produce two increasing finite ticks".to_owned(),
4418        });
4419    }
4420    Ok(AxisScale {
4421        domain: Domain {
4422            min: minimum,
4423            max: maximum,
4424        },
4425        ticks,
4426    })
4427}
4428
4429const MIN_POSITIVE_SUBNORMAL: f64 = f64::from_bits(1);
4430
4431fn stable_positive_step(minimum: f64, maximum: f64, target_tick_count: usize) -> Result<f64> {
4432    let scale = minimum.abs().max(maximum.abs());
4433    let scale = if scale == 0.0 {
4434        MIN_POSITIVE_SUBNORMAL
4435    } else {
4436        scale
4437    };
4438    let normalized =
4439        (maximum / scale - minimum / scale) / target_tick_count.saturating_sub(1) as f64;
4440    let step = normalized * scale;
4441    if normalized.is_finite() && normalized > 0.0 && step.is_finite() {
4442        Ok(step.max(MIN_POSITIVE_SUBNORMAL))
4443    } else {
4444        Err(ChartError::InvalidValue {
4445            element: "chart axis scale".to_owned(),
4446            value: "scale step is nonfinite or empty".to_owned(),
4447        })
4448    }
4449}
4450
4451fn nice_step(raw_step: f64) -> Result<f64> {
4452    if !raw_step.is_finite() || raw_step <= 0.0 {
4453        return Err(ChartError::InvalidValue {
4454            element: "chart axis scale".to_owned(),
4455            value: format!("invalid raw tick step {raw_step}"),
4456        });
4457    }
4458    let magnitude = 10.0_f64
4459        .powf(raw_step.log10().floor())
4460        .max(MIN_POSITIVE_SUBNORMAL);
4461    let fraction = raw_step / magnitude;
4462    let multiplier = if fraction <= 1.0 {
4463        1.0
4464    } else if fraction <= 2.0 {
4465        2.0
4466    } else if fraction <= 5.0 {
4467        5.0
4468    } else {
4469        10.0
4470    };
4471    let step = (multiplier * magnitude).max(MIN_POSITIVE_SUBNORMAL);
4472    if step.is_finite() && step > 0.0 {
4473        Ok(step)
4474    } else {
4475        Err(ChartError::InvalidValue {
4476            element: "chart axis scale".to_owned(),
4477            value: "nice-number step is nonfinite".to_owned(),
4478        })
4479    }
4480}
4481
4482fn expand_constant_domain(value: f64) -> Result<Domain> {
4483    if value == 0.0 {
4484        return Ok(Domain {
4485            min: -1.0,
4486            max: 1.0,
4487        });
4488    }
4489    let padding = (value.abs() * 0.5).max(MIN_POSITIVE_SUBNORMAL);
4490    let minimum = value - padding;
4491    let maximum = value + padding;
4492    if minimum.is_finite() && maximum.is_finite() && minimum < maximum {
4493        return Ok(Domain {
4494            min: minimum,
4495            max: maximum,
4496        });
4497    }
4498    let domain = if value.is_sign_positive() {
4499        Domain {
4500            min: 0.0,
4501            max: value,
4502        }
4503    } else {
4504        Domain {
4505            min: value,
4506            max: 0.0,
4507        }
4508    };
4509    if domain.min < domain.max {
4510        Ok(domain)
4511    } else {
4512        Err(ChartError::InvalidValue {
4513            element: "chart axis scale".to_owned(),
4514            value: format!("constant value {value} has no finite renderable extent"),
4515        })
4516    }
4517}
4518
4519fn extend_bound(bound: f64, span: f64, positive: bool) -> Result<f64> {
4520    let candidate = if positive { bound + span } else { bound - span };
4521    let extended = if candidate.is_finite()
4522        && if positive {
4523            candidate > bound
4524        } else {
4525            candidate < bound
4526        } {
4527        candidate
4528    } else if positive {
4529        bound.next_up()
4530    } else {
4531        bound.next_down()
4532    };
4533    if extended.is_finite()
4534        && if positive {
4535            extended > bound
4536        } else {
4537            extended < bound
4538        }
4539    {
4540        Ok(extended)
4541    } else {
4542        Err(ChartError::InvalidValue {
4543            element: "c:scaling".to_owned(),
4544            value: format!("one-sided bound {bound} leaves no finite automatic extent"),
4545        })
4546    }
4547}
4548
4549fn clean_tick(value: f64, step: f64) -> f64 {
4550    if value.abs() <= step.abs() * 1.0e-12 {
4551        0.0
4552    } else {
4553        value
4554    }
4555}
4556
4557fn domain_from_layers(layers: &[GeometryLayer]) -> Result<Domain> {
4558    let values = layers
4559        .iter()
4560        .flatten()
4561        .flat_map(|(_, lower, upper)| [*lower, *upper]);
4562    let mut min = 0.0_f64;
4563    let mut max = 0.0_f64;
4564    for value in values {
4565        if !value.is_finite() {
4566            return Err(ChartError::InvalidValue {
4567                element: "chart geometry domain".to_owned(),
4568                value: "aggregate produced a nonfinite value".to_owned(),
4569            });
4570        }
4571        min = min.min(value);
4572        max = max.max(value);
4573    }
4574    if min == max {
4575        max = 1.0;
4576    }
4577    Ok(Domain { min, max })
4578}
4579
4580fn data_domain(values: &[f64], include_zero: bool) -> Result<Domain> {
4581    validate_finite(values, "chart geometry cache")?;
4582    let Some(first) = values.first().copied() else {
4583        return Err(ChartError::InvalidValue {
4584            element: "c:numCache".to_owned(),
4585            value: "cached series contain no points".to_owned(),
4586        });
4587    };
4588    let mut min = first;
4589    let mut max = first;
4590    for value in values.iter().copied() {
4591        min = min.min(value);
4592        max = max.max(value);
4593    }
4594    if include_zero {
4595        min = min.min(0.0);
4596        max = max.max(0.0);
4597    }
4598    if min == max {
4599        if !include_zero {
4600            return expand_constant_domain(min);
4601        }
4602        if min > 0.0 {
4603            min = 0.0;
4604        } else if max < 0.0 {
4605            max = 0.0;
4606        } else {
4607            max = 1.0;
4608        }
4609    }
4610    Ok(Domain { min, max })
4611}
4612
4613fn normalized_value(value: f64, domain: Domain) -> Result<f64> {
4614    let scale = value
4615        .abs()
4616        .max(domain.min.abs())
4617        .max(domain.max.abs())
4618        .max(1.0);
4619    let value = value / scale;
4620    let min = domain.min / scale;
4621    let max = domain.max / scale;
4622    let range = max - min;
4623    let normalized = (value - min) / range;
4624    if !range.is_finite() || range <= 0.0 || !normalized.is_finite() {
4625        return Err(ChartError::InvalidValue {
4626            element: "chart geometry domain".to_owned(),
4627            value: format!("cannot map finite domain {} to {}", domain.min, domain.max),
4628        });
4629    }
4630    Ok(normalized)
4631}
4632
4633fn normalized_value_oriented(value: f64, domain: Domain, orientation: Orientation) -> Result<f64> {
4634    let normalized = normalized_value(value, domain)?;
4635    Ok(if orientation == Orientation::MaxMin {
4636        1.0 - normalized
4637    } else {
4638        normalized
4639    })
4640}
4641
4642fn map_x_oriented(
4643    value: f64,
4644    domain: Domain,
4645    bounds: Rect,
4646    orientation: Orientation,
4647) -> Result<f64> {
4648    let coordinate =
4649        bounds.x + normalized_value_oriented(value, domain, orientation)? * bounds.width;
4650    if coordinate.is_finite() {
4651        Ok(coordinate)
4652    } else {
4653        Err(ChartError::InvalidValue {
4654            element: "chart geometry coordinate".to_owned(),
4655            value: "x coordinate is nonfinite".to_owned(),
4656        })
4657    }
4658}
4659
4660fn map_y_oriented(
4661    value: f64,
4662    domain: Domain,
4663    bounds: Rect,
4664    orientation: Orientation,
4665) -> Result<f64> {
4666    let coordinate = bounds.y + bounds.height
4667        - normalized_value_oriented(value, domain, orientation)? * bounds.height;
4668    if coordinate.is_finite() {
4669        Ok(coordinate)
4670    } else {
4671        Err(ChartError::InvalidValue {
4672            element: "chart geometry coordinate".to_owned(),
4673            value: "y coordinate is nonfinite".to_owned(),
4674        })
4675    }
4676}
4677
4678fn oriented_category_index(index: usize, count: usize, orientation: Orientation) -> usize {
4679    if orientation == Orientation::MaxMin && index < count {
4680        count - index - 1
4681    } else {
4682        index
4683    }
4684}
4685
4686fn category_points(
4687    values: &[(usize, f64)],
4688    count: usize,
4689    domain: Domain,
4690    bounds: Rect,
4691    category_orientation: Orientation,
4692    value_orientation: Orientation,
4693) -> Result<Vec<Point>> {
4694    values
4695        .iter()
4696        .map(|(index, value)| {
4697            let index = oriented_category_index(*index, count, category_orientation);
4698            let x = bounds.x + (index as f64 + 0.5) / count as f64 * bounds.width;
4699            let y = map_y_oriented(*value, domain, bounds, value_orientation)?;
4700            if !x.is_finite() {
4701                return Err(ChartError::InvalidValue {
4702                    element: "chart geometry coordinate".to_owned(),
4703                    value: "category x coordinate is nonfinite".to_owned(),
4704                });
4705            }
4706            Ok(Point { x, y })
4707        })
4708        .collect()
4709}
4710
4711fn polyline_path(points: &[Point], close: bool) -> Path {
4712    let mut commands = Vec::with_capacity(points.len() + usize::from(close));
4713    if let Some(first) = points.first() {
4714        commands.push(PathCommand::MoveTo(*first));
4715        commands.extend(points.iter().skip(1).copied().map(PathCommand::LineTo));
4716        if close {
4717            commands.push(PathCommand::Close);
4718        }
4719    }
4720    Path {
4721        commands,
4722        fill_rule: FillRule::NonZero,
4723    }
4724}
4725
4726fn pie_wedge(center: Point, radius: f64, start: f64, end: f64) -> Path {
4727    let first = circle_point(center, radius, start);
4728    let mut commands = vec![PathCommand::MoveTo(center), PathCommand::LineTo(first)];
4729    append_arc(&mut commands, center, radius, start, end);
4730    commands.push(PathCommand::Close);
4731    Path {
4732        commands,
4733        fill_rule: FillRule::NonZero,
4734    }
4735}
4736
4737fn doughnut_wedge(
4738    center: Point,
4739    outer_radius: f64,
4740    inner_radius: f64,
4741    start: f64,
4742    end: f64,
4743) -> Path {
4744    let outer_start = circle_point(center, outer_radius, start);
4745    let inner_end = circle_point(center, inner_radius, end);
4746    let mut commands = vec![PathCommand::MoveTo(outer_start)];
4747    append_arc(&mut commands, center, outer_radius, start, end);
4748    commands.push(PathCommand::LineTo(inner_end));
4749    append_arc(&mut commands, center, inner_radius, end, start);
4750    commands.push(PathCommand::Close);
4751    Path {
4752        commands,
4753        fill_rule: FillRule::NonZero,
4754    }
4755}
4756
4757fn append_arc(commands: &mut Vec<PathCommand>, center: Point, radius: f64, start: f64, end: f64) {
4758    let segments = ((end - start).abs() / std::f64::consts::FRAC_PI_2)
4759        .ceil()
4760        .max(1.0) as usize;
4761    let sweep = (end - start) / segments as f64;
4762    for segment in 0..segments {
4763        let angle = start + segment as f64 * sweep;
4764        let next = angle + sweep;
4765        let factor = 4.0 / 3.0 * (sweep / 4.0).tan();
4766        let from = circle_point(center, radius, angle);
4767        let to = circle_point(center, radius, next);
4768        commands.push(PathCommand::CurveTo {
4769            c1: Point {
4770                x: from.x - factor * radius * angle.sin(),
4771                y: from.y + factor * radius * angle.cos(),
4772            },
4773            c2: Point {
4774                x: to.x + factor * radius * next.sin(),
4775                y: to.y - factor * radius * next.cos(),
4776            },
4777            to,
4778        });
4779    }
4780}
4781
4782fn circle_point(center: Point, radius: f64, angle: f64) -> Point {
4783    Point {
4784        x: center.x + radius * angle.cos(),
4785        y: center.y + radius * angle.sin(),
4786    }
4787}
4788
4789fn radial_point(center: Point, radius: f64, index: usize, count: usize) -> Point {
4790    circle_point(
4791        center,
4792        radius,
4793        index as f64 * std::f64::consts::TAU / count as f64 - std::f64::consts::FRAC_PI_2,
4794    )
4795}
4796
4797fn marker_path(center: Point) -> Path {
4798    const RADIUS: f64 = 2.5;
4799    const KAPPA: f64 = 0.552_284_749_830_793_6;
4800    let control = RADIUS * KAPPA;
4801    Path {
4802        commands: vec![
4803            PathCommand::MoveTo(Point {
4804                x: center.x + RADIUS,
4805                y: center.y,
4806            }),
4807            PathCommand::CurveTo {
4808                c1: Point {
4809                    x: center.x + RADIUS,
4810                    y: center.y + control,
4811                },
4812                c2: Point {
4813                    x: center.x + control,
4814                    y: center.y + RADIUS,
4815                },
4816                to: Point {
4817                    x: center.x,
4818                    y: center.y + RADIUS,
4819                },
4820            },
4821            PathCommand::CurveTo {
4822                c1: Point {
4823                    x: center.x - control,
4824                    y: center.y + RADIUS,
4825                },
4826                c2: Point {
4827                    x: center.x - RADIUS,
4828                    y: center.y + control,
4829                },
4830                to: Point {
4831                    x: center.x - RADIUS,
4832                    y: center.y,
4833                },
4834            },
4835            PathCommand::CurveTo {
4836                c1: Point {
4837                    x: center.x - RADIUS,
4838                    y: center.y - control,
4839                },
4840                c2: Point {
4841                    x: center.x - control,
4842                    y: center.y - RADIUS,
4843                },
4844                to: Point {
4845                    x: center.x,
4846                    y: center.y - RADIUS,
4847                },
4848            },
4849            PathCommand::CurveTo {
4850                c1: Point {
4851                    x: center.x + control,
4852                    y: center.y - RADIUS,
4853                },
4854                c2: Point {
4855                    x: center.x + RADIUS,
4856                    y: center.y - control,
4857                },
4858                to: Point {
4859                    x: center.x + RADIUS,
4860                    y: center.y,
4861                },
4862            },
4863            PathCommand::Close,
4864        ],
4865        fill_rule: FillRule::NonZero,
4866    }
4867}
4868
4869fn push_markers(elements: &mut Vec<PositionedElement>, points: &[Point], colour: Color) {
4870    for point in points {
4871        elements.push(filled_path(marker_path(*point), colour));
4872    }
4873}
4874
4875fn series_colour(series_colours: &[Color], index: usize) -> Result<Color> {
4876    series_colours
4877        .get(index)
4878        .copied()
4879        .ok_or_else(|| ChartError::InvalidValue {
4880            element: "chart series palette".to_owned(),
4881            value: format!("missing resolved colour for series {index}"),
4882        })
4883}
4884
4885fn filled_path(path: Path, color: Color) -> PositionedElement {
4886    PositionedElement::Path(PathElement {
4887        path,
4888        fill: Some(Paint::Solid(color)),
4889        stroke: None,
4890    })
4891}
4892
4893fn stroked_path(path: Path, color: Color) -> PositionedElement {
4894    PositionedElement::Path(PathElement {
4895        path,
4896        fill: None,
4897        stroke: Some(Stroke::new(Paint::Solid(color), 1.5)),
4898    })
4899}
4900
4901fn filled_and_stroked_path(path: Path, color: Color) -> PositionedElement {
4902    PositionedElement::Path(PathElement {
4903        path,
4904        fill: Some(Paint::Solid(Color {
4905            a: color.a * 0.55,
4906            ..color
4907        })),
4908        stroke: Some(Stroke::new(Paint::Solid(color), 1.0)),
4909    })
4910}
4911
4912type NamespaceBindings = Vec<(Vec<u8>, String)>;
4913type XmlAttributes = Vec<(String, String)>;
4914type RootAttributes = (XmlAttributes, XmlAttributes);
4915
4916#[derive(Clone, Debug, Default, Eq, PartialEq)]
4917struct ScalarMarkup {
4918    raw_attributes: XmlAttributes,
4919    raw_content: Vec<Vec<u8>>,
4920}
4921
4922#[derive(Clone, Debug, Default, Eq, PartialEq)]
4923struct TextMarkup {
4924    raw_attributes: XmlAttributes,
4925    raw_children: OrderedRawChildren,
4926}
4927
4928#[derive(Clone, Debug, Default, Eq, PartialEq)]
4929struct PointMarkup {
4930    raw_attributes: XmlAttributes,
4931    raw_children: OrderedRawChildren,
4932    value: TextMarkup,
4933}
4934
4935#[derive(Clone, Debug, Default, Eq, PartialEq)]
4936struct ReferenceMarkup {
4937    raw_attributes: XmlAttributes,
4938    raw_children: OrderedRawChildren,
4939    formula: TextMarkup,
4940    cache_attributes: XmlAttributes,
4941    cache_children: OrderedRawChildren,
4942    format_code: Option<TextMarkup>,
4943    format_code_omitted: bool,
4944    point_count: ScalarMarkup,
4945    declared_point_count: Option<u32>,
4946    point_indexes: Vec<u32>,
4947    points: Vec<PointMarkup>,
4948}
4949
4950#[derive(Clone, Debug, Default, Eq, PartialEq)]
4951struct WrapperMarkup {
4952    raw_attributes: XmlAttributes,
4953    raw_children: OrderedRawChildren,
4954}
4955
4956/// A formula-backed string cache whose count and point indexes are derived.
4957#[derive(Clone, Debug, PartialEq)]
4958pub struct StringRef {
4959    pub formula: String,
4960    pub values: Vec<String>,
4961    markup: ReferenceMarkup,
4962}
4963
4964impl StringRef {
4965    pub fn new(formula: String, values: Vec<String>) -> Result<Self> {
4966        let reference = Self {
4967            formula,
4968            values,
4969            markup: ReferenceMarkup::default(),
4970        };
4971        reference.validate()?;
4972        Ok(reference)
4973    }
4974
4975    pub fn from_xml(xml: &[u8]) -> Result<Self> {
4976        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
4977    }
4978
4979    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
4980        let parsed = parse_reference(xml, b"strRef", b"strCache", false, inherited)?;
4981        let values = parsed.values;
4982        let reference = Self {
4983            formula: parsed.formula,
4984            values,
4985            markup: parsed.markup,
4986        };
4987        reference.validate()?;
4988        Ok(reference)
4989    }
4990
4991    pub fn to_xml(&self) -> Result<Vec<u8>> {
4992        self.validate()?;
4993        let mut writer = Writer::new(Vec::new());
4994        self.write_xml(&mut writer, true)?;
4995        Ok(writer.into_inner())
4996    }
4997
4998    fn validate(&self) -> Result<()> {
4999        validate_formula(&self.formula, "c:strRef/c:f")?;
5000        for value in &self.values {
5001            validate_xml_text(value, "c:strCache/c:pt/c:v")?;
5002        }
5003        validate_point_count(self.values.len(), "c:strCache")
5004    }
5005
5006    fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
5007        self.validate()?;
5008        let mut start = BytesStart::new("c:strRef");
5009        if standalone {
5010            start.push_attribute(("xmlns:c", C_NS));
5011        }
5012        push_attributes(&mut start, &self.markup.raw_attributes);
5013        writer
5014            .write_event(Event::Start(start))
5015            .map_err(OxmlError::from)?;
5016        emit_raw(writer, self.markup.raw_children.at(0))?;
5017        write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
5018        emit_raw(writer, self.markup.raw_children.at(1))?;
5019        write_string_cache(writer, &self.values, &self.markup)?;
5020        emit_raw(writer, self.markup.raw_children.at(2))?;
5021        writer
5022            .write_event(Event::End(BytesEnd::new("c:strRef")))
5023            .map_err(OxmlError::from)?;
5024        Ok(())
5025    }
5026}
5027
5028/// A formula-backed numeric cache whose metadata and points are derived.
5029#[derive(Clone, Debug, PartialEq)]
5030pub struct NumericData {
5031    pub formula: String,
5032    pub format_code: String,
5033    pub values: Vec<f64>,
5034    markup: ReferenceMarkup,
5035}
5036
5037impl NumericData {
5038    pub fn new(formula: String, format_code: String, values: Vec<f64>) -> Result<Self> {
5039        validate_formula(&formula, "c:numRef/c:f")?;
5040        validate_format_code(&format_code)?;
5041        validate_numeric_values(&values)?;
5042        validate_point_count(values.len(), "c:numCache")?;
5043        Ok(Self {
5044            formula,
5045            format_code,
5046            values,
5047            markup: ReferenceMarkup::default(),
5048        })
5049    }
5050
5051    pub fn from_xml(xml: &[u8]) -> Result<Self> {
5052        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
5053    }
5054
5055    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
5056        let parsed = parse_reference(xml, b"numRef", b"numCache", true, inherited)?;
5057        let format_code = parsed.format_code.unwrap_or_else(|| "General".to_owned());
5058        let mut values = Vec::with_capacity(parsed.values.len());
5059        for value in parsed.values {
5060            let number = value.parse::<f64>().map_err(|_| ChartError::InvalidValue {
5061                element: "c:v".to_owned(),
5062                value: value.clone(),
5063            })?;
5064            if !number.is_finite() {
5065                return Err(ChartError::InvalidValue {
5066                    element: "c:v".to_owned(),
5067                    value,
5068                });
5069            }
5070            values.push(number);
5071        }
5072        let data = Self {
5073            formula: parsed.formula,
5074            format_code,
5075            values,
5076            markup: parsed.markup,
5077        };
5078        data.validate()?;
5079        Ok(data)
5080    }
5081
5082    pub fn to_xml(&self) -> Result<Vec<u8>> {
5083        self.validate()?;
5084        let mut writer = Writer::new(Vec::new());
5085        self.write_xml(&mut writer, true)?;
5086        Ok(writer.into_inner())
5087    }
5088
5089    fn validate(&self) -> Result<()> {
5090        validate_formula(&self.formula, "c:numRef/c:f")?;
5091        validate_format_code(&self.format_code)?;
5092        validate_numeric_values(&self.values)?;
5093        validate_point_count(self.values.len(), "c:numCache")
5094    }
5095
5096    fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
5097        self.validate()?;
5098        let mut start = BytesStart::new("c:numRef");
5099        if standalone {
5100            start.push_attribute(("xmlns:c", C_NS));
5101        }
5102        push_attributes(&mut start, &self.markup.raw_attributes);
5103        writer
5104            .write_event(Event::Start(start))
5105            .map_err(OxmlError::from)?;
5106        emit_raw(writer, self.markup.raw_children.at(0))?;
5107        write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
5108        emit_raw(writer, self.markup.raw_children.at(1))?;
5109        write_numeric_cache(writer, self, &self.markup)?;
5110        emit_raw(writer, self.markup.raw_children.at(2))?;
5111        writer
5112            .write_event(Event::End(BytesEnd::new("c:numRef")))
5113            .map_err(OxmlError::from)?;
5114        Ok(())
5115    }
5116}
5117
5118#[derive(Clone, Debug, PartialEq)]
5119pub enum AxisData {
5120    String(StringRef),
5121    Numeric(NumericData),
5122}
5123
5124#[derive(Clone, Debug, PartialEq)]
5125struct DataPoint {
5126    index: u32,
5127    sp_pr: CT_ShapeProperties,
5128}
5129
5130impl DataPoint {
5131    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
5132        writer
5133            .write_event(Event::Start(BytesStart::new("c:dPt")))
5134            .map_err(OxmlError::from)?;
5135        write_scalar(writer, "c:idx", &self.index.to_string(), None)?;
5136        self.sp_pr.write_xml_as(writer, "c:spPr")?;
5137        writer
5138            .write_event(Event::End(BytesEnd::new("c:dPt")))
5139            .map_err(OxmlError::from)?;
5140        Ok(())
5141    }
5142}
5143
5144/// The common formula-backed payload of one ChartML series.
5145#[derive(Clone, Debug, PartialEq)]
5146pub struct Series {
5147    pub index: u32,
5148    pub order: u32,
5149    pub name: Option<StringRef>,
5150    pub categories: Option<AxisData>,
5151    pub values: NumericData,
5152    pub bubble_size: Option<NumericData>,
5153    pub sp_pr: Option<CT_ShapeProperties>,
5154    pub data_labels: Option<CT_DLbls>,
5155    authored_data_points: Vec<DataPoint>,
5156    index_markup: ScalarMarkup,
5157    order_markup: ScalarMarkup,
5158    name_markup: Option<WrapperMarkup>,
5159    categories_markup: Option<WrapperMarkup>,
5160    values_markup: WrapperMarkup,
5161    bubble_size_markup: Option<WrapperMarkup>,
5162    opaque_name: bool,
5163    opaque_categories: bool,
5164    opaque_bubble_size: bool,
5165    uses_scatter_wrappers: bool,
5166    parsed_from_xml: bool,
5167    raw_attributes: XmlAttributes,
5168    namespace_declarations: XmlAttributes,
5169    raw_children: OrderedRawChildren,
5170}
5171
5172impl Series {
5173    pub fn new(index: u32, order: u32, values: NumericData) -> Self {
5174        Self {
5175            index,
5176            order,
5177            name: None,
5178            categories: None,
5179            values,
5180            bubble_size: None,
5181            sp_pr: None,
5182            data_labels: None,
5183            authored_data_points: Vec::new(),
5184            index_markup: ScalarMarkup::default(),
5185            order_markup: ScalarMarkup::default(),
5186            name_markup: None,
5187            categories_markup: None,
5188            values_markup: WrapperMarkup::default(),
5189            bubble_size_markup: None,
5190            opaque_name: false,
5191            opaque_categories: false,
5192            opaque_bubble_size: false,
5193            uses_scatter_wrappers: false,
5194            parsed_from_xml: false,
5195            raw_attributes: Vec::new(),
5196            namespace_declarations: Vec::new(),
5197            raw_children: OrderedRawChildren::default(),
5198        }
5199    }
5200
5201    pub fn from_xml(xml: &[u8]) -> Result<Self> {
5202        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
5203    }
5204
5205    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
5206        let mut reader = Reader::from_reader(xml);
5207        let mut buffer = Vec::new();
5208        loop {
5209            match reader
5210                .read_event_into(&mut buffer)
5211                .map_err(OxmlError::from)?
5212            {
5213                Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
5214                    chart_root_prefix(&element)?;
5215                    if !element_is_in_namespace(&element, C_NS, inherited)? {
5216                        return Err(ChartError::UnexpectedElement(element_name(&element)));
5217                    }
5218                    return Self::from_element(&mut reader, &element, inherited);
5219                }
5220                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
5221                    return Err(ChartError::MissingElement("c:idx".to_owned()));
5222                }
5223                Event::Start(element) | Event::Empty(element) => {
5224                    return Err(ChartError::UnexpectedElement(element_name(&element)));
5225                }
5226                Event::Eof => return Err(ChartError::MissingElement("c:ser".to_owned())),
5227                _ => {}
5228            }
5229            buffer.clear();
5230        }
5231    }
5232
5233    fn from_element(
5234        reader: &mut Reader<&[u8]>,
5235        start: &BytesStart<'_>,
5236        inherited: &NamespaceBindings,
5237    ) -> Result<Self> {
5238        reject_conflicting_prefix(start, b"a", A_NS)?;
5239        let namespaces = chart_bindings(inherited, start)?;
5240        require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
5241        require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
5242        require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
5243        let (raw_attributes, _) =
5244            capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
5245        let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
5246        let mut state = SeriesParseState::default();
5247        let mut buffer = Vec::new();
5248        loop {
5249            match reader
5250                .read_event_into(&mut buffer)
5251                .map_err(OxmlError::from)?
5252            {
5253                Event::Start(element) => {
5254                    let name = chart_child_local(&element, &namespaces)?;
5255                    let raw = capture_element(reader, &element)?;
5256                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
5257                }
5258                Event::Empty(element) => {
5259                    let name = chart_child_local(&element, &namespaces)?;
5260                    let raw = capture_empty_element(&element)?;
5261                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
5262                }
5263                event @ (Event::Text(_)
5264                | Event::CData(_)
5265                | Event::Comment(_)
5266                | Event::PI(_)
5267                | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
5268                Event::End(element) if matches_local_name(element.name().as_ref(), b"ser") => {
5269                    break;
5270                }
5271                Event::Eof => return Err(missing_end("c:ser")),
5272                _ => {}
5273            }
5274            buffer.clear();
5275        }
5276        state.finish(raw_attributes, namespace_declarations)
5277    }
5278
5279    pub fn to_xml(&self) -> Result<Vec<u8>> {
5280        self.to_xml_for_plot(self.uses_scatter_wrappers)
5281    }
5282
5283    fn to_xml_for_plot(&self, scatter: bool) -> Result<Vec<u8>> {
5284        if self.opaque_name && self.name.is_some() {
5285            return Err(ChartError::DuplicateElement("c:tx".to_owned()));
5286        }
5287        if self.opaque_categories && self.categories.is_some() {
5288            return Err(ChartError::DuplicateElement("c:cat".to_owned()));
5289        }
5290        if self.opaque_bubble_size && self.bubble_size.is_some() {
5291            return Err(ChartError::DuplicateElement("c:bubbleSize".to_owned()));
5292        }
5293        self.values.validate()?;
5294        if let Some(name) = &self.name {
5295            name.validate()?;
5296        }
5297        if let Some(categories) = &self.categories {
5298            if scatter && !matches!(categories, AxisData::Numeric(_)) {
5299                return Err(ChartError::InvalidValue {
5300                    element: "c:xVal".to_owned(),
5301                    value: "scatter x values must use a numeric cache".to_owned(),
5302                });
5303            }
5304            match categories {
5305                AxisData::String(reference) => reference.validate()?,
5306                AxisData::Numeric(reference) => reference.validate()?,
5307            }
5308        }
5309        if scatter && self.categories.is_none() {
5310            return Err(ChartError::MissingElement("c:xVal".to_owned()));
5311        }
5312        if let Some(size) = &self.bubble_size {
5313            size.validate()?;
5314        }
5315        if let Some(labels) = &self.data_labels {
5316            labels.validate()?;
5317        }
5318        let mut writer = Writer::new(Vec::new());
5319        let mut start = BytesStart::new("c:ser");
5320        start.push_attribute(("xmlns:c", C_NS));
5321        start.push_attribute(("xmlns:a", A_NS));
5322        start.push_attribute(("xmlns:r", R_NS));
5323        push_attributes(&mut start, &self.namespace_declarations);
5324        push_attributes(&mut start, &self.raw_attributes);
5325        writer
5326            .write_event(Event::Start(start))
5327            .map_err(OxmlError::from)?;
5328        emit_raw(&mut writer, self.raw_children.at(0))?;
5329        write_scalar(
5330            &mut writer,
5331            "c:idx",
5332            &self.index.to_string(),
5333            Some(&self.index_markup),
5334        )?;
5335        emit_raw(&mut writer, self.raw_children.at(1))?;
5336        write_scalar(
5337            &mut writer,
5338            "c:order",
5339            &self.order.to_string(),
5340            Some(&self.order_markup),
5341        )?;
5342        emit_raw(&mut writer, self.raw_children.at(2))?;
5343        if let Some(name) = &self.name {
5344            write_wrapper_start(
5345                &mut writer,
5346                "c:tx",
5347                self.name_markup
5348                    .as_ref()
5349                    .unwrap_or(&WrapperMarkup::default()),
5350            )?;
5351            name.write_xml(&mut writer, false)?;
5352            write_wrapper_end(
5353                &mut writer,
5354                "c:tx",
5355                self.name_markup
5356                    .as_ref()
5357                    .unwrap_or(&WrapperMarkup::default()),
5358            )?;
5359        }
5360        emit_raw(&mut writer, self.raw_children.at(3))?;
5361        if let Some(properties) = &self.sp_pr {
5362            properties.write_xml_as(&mut writer, "c:spPr")?;
5363        }
5364        emit_raw(&mut writer, self.raw_children.at(4))?;
5365        for point in &self.authored_data_points {
5366            point.write_xml(&mut writer)?;
5367        }
5368        if let Some(labels) = &self.data_labels {
5369            labels.write_xml(&mut writer, false)?;
5370        }
5371        emit_raw(&mut writer, self.raw_children.at(5))?;
5372        if let Some(categories) = &self.categories {
5373            let default_markup = WrapperMarkup::default();
5374            let markup = self.categories_markup.as_ref().unwrap_or(&default_markup);
5375            write_wrapper_start(
5376                &mut writer,
5377                if scatter { "c:xVal" } else { "c:cat" },
5378                markup,
5379            )?;
5380            match categories {
5381                AxisData::String(reference) => reference.write_xml(&mut writer, false)?,
5382                AxisData::Numeric(reference) => reference.write_xml(&mut writer, false)?,
5383            }
5384            write_wrapper_end(
5385                &mut writer,
5386                if scatter { "c:xVal" } else { "c:cat" },
5387                markup,
5388            )?;
5389        }
5390        emit_raw(&mut writer, self.raw_children.at(6))?;
5391        write_wrapper_start(
5392            &mut writer,
5393            if scatter { "c:yVal" } else { "c:val" },
5394            &self.values_markup,
5395        )?;
5396        self.values.write_xml(&mut writer, false)?;
5397        write_wrapper_end(
5398            &mut writer,
5399            if scatter { "c:yVal" } else { "c:val" },
5400            &self.values_markup,
5401        )?;
5402        emit_raw(&mut writer, self.raw_children.at(7))?;
5403        if let Some(size) = &self.bubble_size {
5404            let default_markup = WrapperMarkup::default();
5405            let markup = self.bubble_size_markup.as_ref().unwrap_or(&default_markup);
5406            write_wrapper_start(&mut writer, "c:bubbleSize", markup)?;
5407            size.write_xml(&mut writer, false)?;
5408            write_wrapper_end(&mut writer, "c:bubbleSize", markup)?;
5409        }
5410        emit_raw(&mut writer, self.raw_children.at(8))?;
5411        writer
5412            .write_event(Event::End(BytesEnd::new("c:ser")))
5413            .map_err(OxmlError::from)?;
5414        Ok(writer.into_inner())
5415    }
5416
5417    pub fn raw_children(&self) -> &OrderedRawChildren {
5418        &self.raw_children
5419    }
5420}
5421
5422#[derive(Default)]
5423struct SeriesParseState {
5424    index: Option<(u32, ScalarMarkup)>,
5425    order: Option<(u32, ScalarMarkup)>,
5426    name: Option<(StringRef, WrapperMarkup)>,
5427    categories: Option<(AxisData, WrapperMarkup)>,
5428    values: Option<(NumericData, WrapperMarkup)>,
5429    bubble_size: Option<(NumericData, WrapperMarkup)>,
5430    data_labels: Option<CT_DLbls>,
5431    name_seen: bool,
5432    categories_seen: bool,
5433    bubble_size_seen: bool,
5434    opaque_name: bool,
5435    opaque_categories: bool,
5436    opaque_bubble_size: bool,
5437    saw_standard_wrapper: bool,
5438    saw_scatter_wrapper: bool,
5439    sp_pr: Option<CT_ShapeProperties>,
5440    raw_children: OrderedRawChildren,
5441    boundary: usize,
5442}
5443
5444impl SeriesParseState {
5445    fn capture_event(&mut self, raw: Vec<u8>) {
5446        self.raw_children.push(self.boundary, raw);
5447    }
5448
5449    fn parse_child(
5450        &mut self,
5451        name: &[u8],
5452        raw: Vec<u8>,
5453        namespaces: &NamespaceBindings,
5454    ) -> Result<()> {
5455        match name {
5456            b"idx" => {
5457                set_once(&mut self.index, parse_u32_scalar(&raw, "idx")?, "c:idx")?;
5458                self.boundary = self.boundary.max(1);
5459            }
5460            b"order" => {
5461                set_once(&mut self.order, parse_u32_scalar(&raw, "order")?, "c:order")?;
5462                self.boundary = self.boundary.max(2);
5463            }
5464            b"tx" => {
5465                mark_once(&mut self.name_seen, "c:tx")?;
5466                let parsed = parse_wrapper(&raw, b"tx", &[b"strRef"], namespaces)?;
5467                if let Some((_, reference)) = parsed.choice {
5468                    self.name = Some((
5469                        StringRef::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
5470                        parsed.markup,
5471                    ));
5472                } else {
5473                    self.opaque_name = true;
5474                    self.raw_children.push(2, raw);
5475                }
5476                self.boundary = self.boundary.max(3);
5477            }
5478            b"spPr" => {
5479                parse_chart_shape_properties(&raw, namespaces, &mut self.sp_pr)?;
5480                self.boundary = self.boundary.max(4);
5481            }
5482            b"dLbls" => {
5483                set_once(
5484                    &mut self.data_labels,
5485                    CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
5486                    "c:dLbls",
5487                )?;
5488                self.boundary = self.boundary.max(5);
5489            }
5490            b"cat" => {
5491                self.saw_standard_wrapper = true;
5492                mark_once(&mut self.categories_seen, "c:cat")?;
5493                let parsed = parse_wrapper(&raw, b"cat", &[b"strRef", b"numRef"], namespaces)?;
5494                if let Some((choice, reference)) = parsed.choice {
5495                    let categories = if choice == b"strRef" {
5496                        AxisData::String(StringRef::from_xml_with_namespaces(
5497                            &reference,
5498                            &parsed.namespaces,
5499                        )?)
5500                    } else {
5501                        AxisData::Numeric(NumericData::from_xml_with_namespaces(
5502                            &reference,
5503                            &parsed.namespaces,
5504                        )?)
5505                    };
5506                    self.categories = Some((categories, parsed.markup));
5507                } else {
5508                    self.opaque_categories = true;
5509                    self.raw_children.push(5, raw);
5510                }
5511                self.boundary = self.boundary.max(6);
5512            }
5513            b"xVal" => {
5514                self.saw_scatter_wrapper = true;
5515                mark_once(&mut self.categories_seen, "c:xVal")?;
5516                let parsed = parse_wrapper(&raw, b"xVal", &[b"numRef"], namespaces)?;
5517                let (_, reference) = parsed
5518                    .choice
5519                    .ok_or_else(|| ChartError::MissingElement("c:xVal/c:numRef".to_owned()))?;
5520                self.categories = Some((
5521                    AxisData::Numeric(NumericData::from_xml_with_namespaces(
5522                        &reference,
5523                        &parsed.namespaces,
5524                    )?),
5525                    parsed.markup,
5526                ));
5527                self.boundary = self.boundary.max(6);
5528            }
5529            b"val" | b"yVal" => {
5530                if name == b"yVal" {
5531                    self.saw_scatter_wrapper = true;
5532                } else {
5533                    self.saw_standard_wrapper = true;
5534                }
5535                let wrapper = if name == b"yVal" {
5536                    b"yVal".as_slice()
5537                } else {
5538                    b"val".as_slice()
5539                };
5540                let parsed = parse_wrapper(&raw, wrapper, &[b"numRef"], namespaces)?;
5541                let (_, reference) = parsed.choice.ok_or_else(|| {
5542                    ChartError::MissingElement(format!(
5543                        "c:{}/c:numRef",
5544                        String::from_utf8_lossy(wrapper)
5545                    ))
5546                })?;
5547                set_once(
5548                    &mut self.values,
5549                    (
5550                        NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
5551                        parsed.markup,
5552                    ),
5553                    "c:val or c:yVal",
5554                )?;
5555                self.boundary = self.boundary.max(7);
5556            }
5557            b"bubbleSize" => {
5558                mark_once(&mut self.bubble_size_seen, "c:bubbleSize")?;
5559                let parsed = parse_wrapper(&raw, b"bubbleSize", &[b"numRef"], namespaces)?;
5560                if let Some((_, reference)) = parsed.choice {
5561                    self.bubble_size = Some((
5562                        NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
5563                        parsed.markup,
5564                    ));
5565                } else {
5566                    self.opaque_bubble_size = true;
5567                    self.raw_children.push(7, raw);
5568                }
5569                self.boundary = self.boundary.max(8);
5570            }
5571            _ => {
5572                let boundary = series_raw_boundary(name, self.boundary);
5573                self.raw_children.push(boundary, raw);
5574                self.boundary = self.boundary.max(boundary);
5575            }
5576        }
5577        Ok(())
5578    }
5579
5580    fn finish(
5581        self,
5582        raw_attributes: XmlAttributes,
5583        namespace_declarations: XmlAttributes,
5584    ) -> Result<Series> {
5585        if self.saw_standard_wrapper && self.saw_scatter_wrapper {
5586            return Err(ChartError::InvalidValue {
5587                element: "c:ser".to_owned(),
5588                value: "category/value and x/y wrappers cannot be combined".to_owned(),
5589            });
5590        }
5591        let (index, index_markup) = self
5592            .index
5593            .ok_or_else(|| ChartError::MissingElement("c:idx".to_owned()))?;
5594        let (order, order_markup) = self
5595            .order
5596            .ok_or_else(|| ChartError::MissingElement("c:order".to_owned()))?;
5597        let (values, values_markup) = self
5598            .values
5599            .ok_or_else(|| ChartError::MissingElement("c:val".to_owned()))?;
5600        let (name, name_markup) = self
5601            .name
5602            .map(|(value, markup)| (Some(value), Some(markup)))
5603            .unwrap_or((None, None));
5604        let (categories, categories_markup) = self
5605            .categories
5606            .map(|(value, markup)| (Some(value), Some(markup)))
5607            .unwrap_or((None, None));
5608        let (bubble_size, bubble_size_markup) = self
5609            .bubble_size
5610            .map(|(value, markup)| (Some(value), Some(markup)))
5611            .unwrap_or((None, None));
5612        Ok(Series {
5613            index,
5614            order,
5615            name,
5616            categories,
5617            values,
5618            bubble_size,
5619            sp_pr: self.sp_pr,
5620            data_labels: self.data_labels,
5621            authored_data_points: Vec::new(),
5622            index_markup,
5623            order_markup,
5624            name_markup,
5625            categories_markup,
5626            values_markup,
5627            bubble_size_markup,
5628            opaque_name: self.opaque_name,
5629            opaque_categories: self.opaque_categories,
5630            opaque_bubble_size: self.opaque_bubble_size,
5631            uses_scatter_wrappers: self.saw_scatter_wrapper,
5632            parsed_from_xml: true,
5633            raw_attributes,
5634            namespace_declarations,
5635            raw_children: raw_children_in_schema_order(&self.raw_children, 8),
5636        })
5637    }
5638}
5639
5640fn series_raw_boundary(name: &[u8], current: usize) -> usize {
5641    match name {
5642        b"marker" | b"invertIfNegative" | b"pictureOptions" | b"explosion" | b"dPt" => 4,
5643        b"trendline" | b"errBars" => 5,
5644        b"shape" | b"smooth" => 7,
5645        b"extLst" => 8,
5646        _ => current,
5647    }
5648}
5649
5650struct ParsedReference {
5651    formula: String,
5652    format_code: Option<String>,
5653    values: Vec<String>,
5654    markup: ReferenceMarkup,
5655}
5656
5657fn parse_reference(
5658    xml: &[u8],
5659    reference_local: &[u8],
5660    cache_local: &[u8],
5661    numeric: bool,
5662    inherited: &NamespaceBindings,
5663) -> Result<ParsedReference> {
5664    let mut reader = Reader::from_reader(xml);
5665    let mut buffer = Vec::new();
5666    loop {
5667        match reader
5668            .read_event_into(&mut buffer)
5669            .map_err(OxmlError::from)?
5670        {
5671            Event::Start(element)
5672                if matches_local_name(element.name().as_ref(), reference_local) =>
5673            {
5674                let namespaces = typed_rewrite_bindings(&element, inherited)?;
5675                let raw_attributes = capture_fixed_attributes(&element, &["xmlns:c"])?;
5676                let mut formula: Option<(String, TextMarkup)> = None;
5677                let mut cache: Option<(Option<String>, Vec<String>, CacheMarkup)> = None;
5678                let mut raw_children = OrderedRawChildren::default();
5679                let mut boundary = 0usize;
5680                let mut inner = Vec::new();
5681                loop {
5682                    match reader
5683                        .read_event_into(&mut inner)
5684                        .map_err(OxmlError::from)?
5685                    {
5686                        Event::Start(child) => {
5687                            let name = chart_child_local(&child, &namespaces)?;
5688                            let raw = capture_element(&mut reader, &child)?;
5689                            match name.as_deref().unwrap_or_default() {
5690                                b"f" => {
5691                                    set_once(
5692                                        &mut formula,
5693                                        parse_text_element(&raw, b"f", &namespaces)?,
5694                                        "c:f",
5695                                    )?;
5696                                    boundary = boundary.max(1);
5697                                }
5698                                name if name == cache_local => {
5699                                    set_once(
5700                                        &mut cache,
5701                                        parse_cache(&raw, cache_local, numeric, &namespaces)?,
5702                                        &format!("c:{}", String::from_utf8_lossy(cache_local)),
5703                                    )?;
5704                                    boundary = boundary.max(2);
5705                                }
5706                                _ => raw_children.push(boundary, raw),
5707                            }
5708                        }
5709                        Event::Empty(child) => {
5710                            let name = chart_child_local(&child, &namespaces)?;
5711                            let raw = capture_empty_element(&child)?;
5712                            match name.as_deref().unwrap_or_default() {
5713                                b"f" => {
5714                                    set_once(
5715                                        &mut formula,
5716                                        parse_text_element(&raw, b"f", &namespaces)?,
5717                                        "c:f",
5718                                    )?;
5719                                    boundary = boundary.max(1);
5720                                }
5721                                name if name == cache_local => {
5722                                    set_once(
5723                                        &mut cache,
5724                                        parse_cache(&raw, cache_local, numeric, &namespaces)?,
5725                                        &format!("c:{}", String::from_utf8_lossy(cache_local)),
5726                                    )?;
5727                                    boundary = boundary.max(2);
5728                                }
5729                                _ => raw_children.push(boundary, raw),
5730                            }
5731                        }
5732                        event @ (Event::Text(_)
5733                        | Event::CData(_)
5734                        | Event::Comment(_)
5735                        | Event::PI(_)
5736                        | Event::GeneralRef(_)) => {
5737                            raw_children.push(boundary, capture_event(event)?);
5738                        }
5739                        Event::End(end)
5740                            if matches_local_name(end.name().as_ref(), reference_local) =>
5741                        {
5742                            let (formula, formula_markup) = formula
5743                                .ok_or_else(|| ChartError::MissingElement("c:f".to_owned()))?;
5744                            validate_formula(&formula, "c:f")?;
5745                            let (format_code, values, cache_markup) = cache.ok_or_else(|| {
5746                                ChartError::MissingElement(format!(
5747                                    "c:{}",
5748                                    String::from_utf8_lossy(cache_local)
5749                                ))
5750                            })?;
5751                            let markup = ReferenceMarkup {
5752                                raw_attributes,
5753                                raw_children: raw_children_in_schema_order(&raw_children, 2),
5754                                formula: formula_markup,
5755                                cache_attributes: cache_markup.raw_attributes,
5756                                cache_children: cache_markup.raw_children,
5757                                format_code: cache_markup.format_code,
5758                                format_code_omitted: cache_markup.format_code_omitted,
5759                                point_count: cache_markup.point_count,
5760                                declared_point_count: cache_markup.declared_point_count,
5761                                point_indexes: cache_markup.point_indexes,
5762                                points: cache_markup.points,
5763                            };
5764                            return Ok(ParsedReference {
5765                                formula,
5766                                format_code,
5767                                values,
5768                                markup,
5769                            });
5770                        }
5771                        Event::Eof => {
5772                            return Err(missing_end(&format!(
5773                                "c:{}",
5774                                String::from_utf8_lossy(reference_local)
5775                            )));
5776                        }
5777                        _ => {}
5778                    }
5779                    inner.clear();
5780                }
5781            }
5782            Event::Empty(element)
5783                if matches_local_name(element.name().as_ref(), reference_local) =>
5784            {
5785                return Err(ChartError::MissingElement("c:f".to_owned()));
5786            }
5787            Event::Start(element) | Event::Empty(element) => {
5788                return Err(ChartError::UnexpectedElement(element_name(&element)));
5789            }
5790            Event::Eof => {
5791                return Err(ChartError::MissingElement(format!(
5792                    "c:{}",
5793                    String::from_utf8_lossy(reference_local)
5794                )));
5795            }
5796            _ => {}
5797        }
5798        buffer.clear();
5799    }
5800}
5801
5802struct CacheMarkup {
5803    raw_attributes: XmlAttributes,
5804    raw_children: OrderedRawChildren,
5805    format_code: Option<TextMarkup>,
5806    format_code_omitted: bool,
5807    point_count: ScalarMarkup,
5808    declared_point_count: Option<u32>,
5809    point_indexes: Vec<u32>,
5810    points: Vec<PointMarkup>,
5811}
5812
5813fn parse_cache(
5814    xml: &[u8],
5815    cache_local: &[u8],
5816    numeric: bool,
5817    inherited: &NamespaceBindings,
5818) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
5819    let mut reader = Reader::from_reader(xml);
5820    let mut buffer = Vec::new();
5821    loop {
5822        match reader
5823            .read_event_into(&mut buffer)
5824            .map_err(OxmlError::from)?
5825        {
5826            Event::Start(element) if matches_local_name(element.name().as_ref(), cache_local) => {
5827                let namespaces = typed_rewrite_bindings(&element, inherited)?;
5828                let raw_attributes = capture_attributes(&element)?;
5829                let mut state = CacheParseState::new(numeric, namespaces);
5830                let mut inner = Vec::new();
5831                loop {
5832                    match reader
5833                        .read_event_into(&mut inner)
5834                        .map_err(OxmlError::from)?
5835                    {
5836                        Event::Start(child) => {
5837                            let name = chart_child_local(&child, &state.namespaces)?;
5838                            let raw = capture_element(&mut reader, &child)?;
5839                            state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
5840                        }
5841                        Event::Empty(child) => {
5842                            let name = chart_child_local(&child, &state.namespaces)?;
5843                            let raw = capture_empty_element(&child)?;
5844                            state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
5845                        }
5846                        event @ (Event::Text(_)
5847                        | Event::CData(_)
5848                        | Event::Comment(_)
5849                        | Event::PI(_)
5850                        | Event::GeneralRef(_)) => {
5851                            state.capture_event(capture_event(event)?);
5852                        }
5853                        Event::End(end) if matches_local_name(end.name().as_ref(), cache_local) => {
5854                            return state.finish(raw_attributes);
5855                        }
5856                        Event::Eof => {
5857                            return Err(missing_end(&format!(
5858                                "c:{}",
5859                                String::from_utf8_lossy(cache_local)
5860                            )));
5861                        }
5862                        _ => {}
5863                    }
5864                    inner.clear();
5865                }
5866            }
5867            Event::Empty(element) if matches_local_name(element.name().as_ref(), cache_local) => {
5868                return Err(ChartError::MissingElement("c:ptCount".to_owned()));
5869            }
5870            Event::Start(element) | Event::Empty(element) => {
5871                return Err(ChartError::UnexpectedElement(element_name(&element)));
5872            }
5873            Event::Eof => {
5874                return Err(ChartError::MissingElement(format!(
5875                    "c:{}",
5876                    String::from_utf8_lossy(cache_local)
5877                )));
5878            }
5879            _ => {}
5880        }
5881        buffer.clear();
5882    }
5883}
5884
5885struct CacheParseState {
5886    numeric: bool,
5887    namespaces: NamespaceBindings,
5888    point_base: usize,
5889    boundary: usize,
5890    format_code: Option<(String, TextMarkup)>,
5891    point_count: Option<(u32, ScalarMarkup)>,
5892    points: Vec<(u32, String, PointMarkup)>,
5893    raw_children: OrderedRawChildren,
5894}
5895
5896impl CacheParseState {
5897    fn new(numeric: bool, namespaces: NamespaceBindings) -> Self {
5898        Self {
5899            numeric,
5900            namespaces,
5901            point_base: usize::from(numeric) + 1,
5902            boundary: 0,
5903            format_code: None,
5904            point_count: None,
5905            points: Vec::new(),
5906            raw_children: OrderedRawChildren::default(),
5907        }
5908    }
5909
5910    fn capture_event(&mut self, raw: Vec<u8>) {
5911        self.raw_children.push(self.boundary, raw);
5912    }
5913
5914    fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
5915        match name {
5916            b"formatCode" if self.numeric => {
5917                set_once(
5918                    &mut self.format_code,
5919                    parse_text_element(&raw, b"formatCode", &self.namespaces)?,
5920                    "c:formatCode",
5921                )?;
5922                self.boundary = self.boundary.max(1);
5923            }
5924            b"ptCount" => {
5925                set_once(
5926                    &mut self.point_count,
5927                    parse_u32_scalar(&raw, "ptCount")?,
5928                    "c:ptCount",
5929                )?;
5930                self.boundary = self.boundary.max(self.point_base);
5931            }
5932            b"pt" => {
5933                self.points.push(parse_point(&raw, &self.namespaces)?);
5934                self.boundary = self.boundary.max(self.point_base + self.points.len());
5935            }
5936            _ => self.raw_children.push(self.boundary, raw),
5937        }
5938        Ok(())
5939    }
5940
5941    fn finish(
5942        self,
5943        raw_attributes: XmlAttributes,
5944    ) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
5945        let (declared, point_count_markup) = self
5946            .point_count
5947            .ok_or_else(|| ChartError::MissingElement("c:ptCount".to_owned()))?;
5948        let actual = u32::try_from(self.points.len()).map_err(|_| ChartError::InvalidValue {
5949            element: "c:ptCount".to_owned(),
5950            value: self.points.len().to_string(),
5951        })?;
5952        if declared < actual {
5953            return Err(invalid_attribute("ptCount", "val", declared.to_string()));
5954        }
5955        let mut values = Vec::with_capacity(self.points.len());
5956        let mut point_indexes = Vec::with_capacity(self.points.len());
5957        let mut point_markup = Vec::with_capacity(self.points.len());
5958        let mut previous = None;
5959        for (index, value, markup) in self.points {
5960            if index >= declared || previous.is_some_and(|last| index <= last) {
5961                return Err(invalid_attribute("pt", "idx", index.to_string()));
5962            }
5963            previous = Some(index);
5964            values.push(value);
5965            point_indexes.push(index);
5966            point_markup.push(markup);
5967        }
5968        let (format_code, format_markup) = self
5969            .format_code
5970            .map(|(value, markup)| (Some(value), Some(markup)))
5971            .unwrap_or((None, None));
5972        let format_code_omitted = self.numeric && format_code.is_none();
5973        Ok((
5974            format_code,
5975            values,
5976            CacheMarkup {
5977                raw_attributes,
5978                raw_children: raw_children_in_schema_order(
5979                    &self.raw_children,
5980                    self.point_base + point_markup.len(),
5981                ),
5982                format_code: format_markup,
5983                format_code_omitted,
5984                point_count: point_count_markup,
5985                declared_point_count: Some(declared),
5986                point_indexes,
5987                points: point_markup,
5988            },
5989        ))
5990    }
5991}
5992
5993fn parse_point(xml: &[u8], inherited: &NamespaceBindings) -> Result<(u32, String, PointMarkup)> {
5994    let mut reader = Reader::from_reader(xml);
5995    let mut buffer = Vec::new();
5996    loop {
5997        match reader
5998            .read_event_into(&mut buffer)
5999            .map_err(OxmlError::from)?
6000        {
6001            Event::Start(element) if matches_local_name(element.name().as_ref(), b"pt") => {
6002                let namespaces = typed_rewrite_bindings(&element, inherited)?;
6003                let index = required_u32_attribute(&element, "pt", b"idx")?;
6004                let raw_attributes = capture_attributes_except(&element, b"idx")?;
6005                let mut value: Option<(String, TextMarkup)> = None;
6006                let mut raw_children = OrderedRawChildren::default();
6007                let mut boundary = 0usize;
6008                let mut inner = Vec::new();
6009                loop {
6010                    match reader
6011                        .read_event_into(&mut inner)
6012                        .map_err(OxmlError::from)?
6013                    {
6014                        Event::Start(child) => {
6015                            let name = chart_child_local(&child, &namespaces)?;
6016                            let raw = capture_element(&mut reader, &child)?;
6017                            if name.as_deref() == Some(b"v") {
6018                                set_once(
6019                                    &mut value,
6020                                    parse_text_element(&raw, b"v", &namespaces)?,
6021                                    "c:v",
6022                                )?;
6023                                boundary = 1;
6024                            } else {
6025                                raw_children.push(boundary, raw);
6026                            }
6027                        }
6028                        Event::Empty(child) => {
6029                            let name = chart_child_local(&child, &namespaces)?;
6030                            let raw = capture_empty_element(&child)?;
6031                            if name.as_deref() == Some(b"v") {
6032                                set_once(
6033                                    &mut value,
6034                                    parse_text_element(&raw, b"v", &namespaces)?,
6035                                    "c:v",
6036                                )?;
6037                                boundary = 1;
6038                            } else {
6039                                raw_children.push(boundary, raw);
6040                            }
6041                        }
6042                        event @ (Event::Text(_)
6043                        | Event::CData(_)
6044                        | Event::Comment(_)
6045                        | Event::PI(_)
6046                        | Event::GeneralRef(_)) => {
6047                            raw_children.push(boundary, capture_event(event)?);
6048                        }
6049                        Event::End(end) if matches_local_name(end.name().as_ref(), b"pt") => {
6050                            let (value, value_markup) = value
6051                                .ok_or_else(|| ChartError::MissingElement("c:v".to_owned()))?;
6052                            return Ok((
6053                                index,
6054                                value,
6055                                PointMarkup {
6056                                    raw_attributes,
6057                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
6058                                    value: value_markup,
6059                                },
6060                            ));
6061                        }
6062                        Event::Eof => return Err(missing_end("c:pt")),
6063                        _ => {}
6064                    }
6065                    inner.clear();
6066                }
6067            }
6068            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pt") => {
6069                return Err(ChartError::MissingElement("c:v".to_owned()));
6070            }
6071            Event::Start(element) | Event::Empty(element) => {
6072                return Err(ChartError::UnexpectedElement(element_name(&element)));
6073            }
6074            Event::Eof => return Err(ChartError::MissingElement("c:pt".to_owned())),
6075            _ => {}
6076        }
6077        buffer.clear();
6078    }
6079}
6080
6081fn parse_text_element(
6082    xml: &[u8],
6083    local: &[u8],
6084    inherited: &NamespaceBindings,
6085) -> Result<(String, TextMarkup)> {
6086    let mut reader = Reader::from_reader(xml);
6087    let mut buffer = Vec::new();
6088    loop {
6089        match reader
6090            .read_event_into(&mut buffer)
6091            .map_err(OxmlError::from)?
6092        {
6093            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
6094                typed_rewrite_bindings(&element, inherited)?;
6095                let raw_attributes = capture_attributes(&element)?;
6096                let mut value = String::new();
6097                let mut raw_children = OrderedRawChildren::default();
6098                let mut seen_text = false;
6099                let mut inner = Vec::new();
6100                loop {
6101                    match reader
6102                        .read_event_into(&mut inner)
6103                        .map_err(OxmlError::from)?
6104                    {
6105                        Event::Text(text) => {
6106                            value.push_str(&decode_plain(&text));
6107                            seen_text = true;
6108                        }
6109                        Event::CData(text) => {
6110                            let decoded =
6111                                text.decode().map_err(|error| ChartError::InvalidValue {
6112                                    element: format!("c:{}", String::from_utf8_lossy(local)),
6113                                    value: error.to_string(),
6114                                })?;
6115                            value.push_str(&decoded);
6116                            seen_text = true;
6117                        }
6118                        Event::GeneralRef(reference) => {
6119                            value.push_str(&resolve_entity(&reference));
6120                            seen_text = true;
6121                        }
6122                        event @ (Event::Comment(_) | Event::PI(_)) => {
6123                            raw_children.push(usize::from(seen_text), capture_event(event)?);
6124                        }
6125                        Event::Start(child) | Event::Empty(child) => {
6126                            return Err(ChartError::UnexpectedElement(element_name(&child)));
6127                        }
6128                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
6129                            return Ok((
6130                                value,
6131                                TextMarkup {
6132                                    raw_attributes,
6133                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
6134                                },
6135                            ));
6136                        }
6137                        Event::Eof => {
6138                            return Err(missing_end(&format!(
6139                                "c:{}",
6140                                String::from_utf8_lossy(local)
6141                            )));
6142                        }
6143                        _ => {}
6144                    }
6145                    inner.clear();
6146                }
6147            }
6148            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
6149                typed_rewrite_bindings(&element, inherited)?;
6150                return Ok((
6151                    String::new(),
6152                    TextMarkup {
6153                        raw_attributes: capture_attributes(&element)?,
6154                        raw_children: OrderedRawChildren::default(),
6155                    },
6156                ));
6157            }
6158            Event::Start(element) | Event::Empty(element) => {
6159                return Err(ChartError::UnexpectedElement(element_name(&element)));
6160            }
6161            Event::Eof => {
6162                return Err(ChartError::MissingElement(format!(
6163                    "c:{}",
6164                    String::from_utf8_lossy(local)
6165                )));
6166            }
6167            _ => {}
6168        }
6169        buffer.clear();
6170    }
6171}
6172
6173struct ParsedWrapper {
6174    choice: Option<(Vec<u8>, Vec<u8>)>,
6175    markup: WrapperMarkup,
6176    namespaces: NamespaceBindings,
6177}
6178
6179fn parse_wrapper(
6180    xml: &[u8],
6181    local: &[u8],
6182    choices: &[&[u8]],
6183    inherited: &NamespaceBindings,
6184) -> Result<ParsedWrapper> {
6185    let mut reader = Reader::from_reader(xml);
6186    let mut buffer = Vec::new();
6187    loop {
6188        match reader
6189            .read_event_into(&mut buffer)
6190            .map_err(OxmlError::from)?
6191        {
6192            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
6193                let namespaces = typed_rewrite_bindings(&element, inherited)?;
6194                let raw_attributes = capture_attributes(&element)?;
6195                let mut choice = None;
6196                let mut raw_children = OrderedRawChildren::default();
6197                let mut boundary = 0usize;
6198                let mut inner = Vec::new();
6199                loop {
6200                    match reader
6201                        .read_event_into(&mut inner)
6202                        .map_err(OxmlError::from)?
6203                    {
6204                        Event::Start(child) => {
6205                            let name = chart_child_local(&child, &namespaces)?;
6206                            let raw = capture_element(&mut reader, &child)?;
6207                            if let Some(name) =
6208                                name.filter(|name| choices.contains(&name.as_slice()))
6209                            {
6210                                if choice.is_some() {
6211                                    return Err(ChartError::DuplicateElement(format!(
6212                                        "c:{} reference",
6213                                        String::from_utf8_lossy(local)
6214                                    )));
6215                                }
6216                                choice = Some((name, raw));
6217                                boundary = 1;
6218                            } else {
6219                                raw_children.push(boundary, raw);
6220                            }
6221                        }
6222                        Event::Empty(child) => {
6223                            let name = chart_child_local(&child, &namespaces)?;
6224                            let raw = capture_empty_element(&child)?;
6225                            if let Some(name) =
6226                                name.filter(|name| choices.contains(&name.as_slice()))
6227                            {
6228                                if choice.is_some() {
6229                                    return Err(ChartError::DuplicateElement(format!(
6230                                        "c:{} reference",
6231                                        String::from_utf8_lossy(local)
6232                                    )));
6233                                }
6234                                choice = Some((name, raw));
6235                                boundary = 1;
6236                            } else {
6237                                raw_children.push(boundary, raw);
6238                            }
6239                        }
6240                        event @ (Event::Text(_)
6241                        | Event::CData(_)
6242                        | Event::Comment(_)
6243                        | Event::PI(_)
6244                        | Event::GeneralRef(_)) => {
6245                            raw_children.push(boundary, capture_event(event)?);
6246                        }
6247                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
6248                            return Ok(ParsedWrapper {
6249                                choice,
6250                                markup: WrapperMarkup {
6251                                    raw_attributes,
6252                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
6253                                },
6254                                namespaces,
6255                            });
6256                        }
6257                        Event::Eof => {
6258                            return Err(missing_end(&format!(
6259                                "c:{}",
6260                                String::from_utf8_lossy(local)
6261                            )));
6262                        }
6263                        _ => {}
6264                    }
6265                    inner.clear();
6266                }
6267            }
6268            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
6269                let namespaces = chart_bindings(inherited, &element)?;
6270                return Ok(ParsedWrapper {
6271                    choice: None,
6272                    markup: WrapperMarkup {
6273                        raw_attributes: capture_attributes(&element)?,
6274                        raw_children: OrderedRawChildren::default(),
6275                    },
6276                    namespaces,
6277                });
6278            }
6279            Event::Start(element) | Event::Empty(element) => {
6280                return Err(ChartError::UnexpectedElement(element_name(&element)));
6281            }
6282            Event::Eof => {
6283                return Err(ChartError::MissingElement(format!(
6284                    "c:{}",
6285                    String::from_utf8_lossy(local)
6286                )));
6287            }
6288            _ => {}
6289        }
6290        buffer.clear();
6291    }
6292}
6293
6294fn write_string_cache(
6295    writer: &mut Writer<Vec<u8>>,
6296    values: &[String],
6297    markup: &ReferenceMarkup,
6298) -> Result<()> {
6299    let (declared_count, indexes) = cache_layout(markup, values.len())?;
6300    let mut start = BytesStart::new("c:strCache");
6301    push_attributes(&mut start, &markup.cache_attributes);
6302    writer
6303        .write_event(Event::Start(start))
6304        .map_err(OxmlError::from)?;
6305    emit_raw(writer, markup.cache_children.at(0))?;
6306    write_scalar(
6307        writer,
6308        "c:ptCount",
6309        &declared_count.to_string(),
6310        Some(&markup.point_count),
6311    )?;
6312    emit_cache_point_raw(writer, markup, 1, values.len(), 0)?;
6313    for (position, value) in values.iter().enumerate() {
6314        let default_markup = PointMarkup::default();
6315        write_point(
6316            writer,
6317            indexes[position],
6318            value,
6319            markup.points.get(position).unwrap_or(&default_markup),
6320        )?;
6321        emit_cache_point_raw(writer, markup, 1, values.len(), position + 1)?;
6322    }
6323    writer
6324        .write_event(Event::End(BytesEnd::new("c:strCache")))
6325        .map_err(OxmlError::from)?;
6326    Ok(())
6327}
6328
6329fn write_numeric_cache(
6330    writer: &mut Writer<Vec<u8>>,
6331    data: &NumericData,
6332    markup: &ReferenceMarkup,
6333) -> Result<()> {
6334    let (declared_count, indexes) = cache_layout(markup, data.values.len())?;
6335    let mut start = BytesStart::new("c:numCache");
6336    push_attributes(&mut start, &markup.cache_attributes);
6337    writer
6338        .write_event(Event::Start(start))
6339        .map_err(OxmlError::from)?;
6340    emit_raw(writer, markup.cache_children.at(0))?;
6341    if !markup.format_code_omitted || data.format_code != "General" {
6342        let default_format_markup = TextMarkup::default();
6343        write_text(
6344            writer,
6345            "c:formatCode",
6346            &data.format_code,
6347            markup
6348                .format_code
6349                .as_ref()
6350                .unwrap_or(&default_format_markup),
6351        )?;
6352    }
6353    emit_raw(writer, markup.cache_children.at(1))?;
6354    write_scalar(
6355        writer,
6356        "c:ptCount",
6357        &declared_count.to_string(),
6358        Some(&markup.point_count),
6359    )?;
6360    emit_cache_point_raw(writer, markup, 2, data.values.len(), 0)?;
6361    for (position, value) in data.values.iter().enumerate() {
6362        let default_markup = PointMarkup::default();
6363        write_point(
6364            writer,
6365            indexes[position],
6366            &value.to_string(),
6367            markup.points.get(position).unwrap_or(&default_markup),
6368        )?;
6369        emit_cache_point_raw(writer, markup, 2, data.values.len(), position + 1)?;
6370    }
6371    writer
6372        .write_event(Event::End(BytesEnd::new("c:numCache")))
6373        .map_err(OxmlError::from)?;
6374    Ok(())
6375}
6376
6377fn emit_cache_point_raw(
6378    writer: &mut Writer<Vec<u8>>,
6379    markup: &ReferenceMarkup,
6380    point_base: usize,
6381    current_count: usize,
6382    completed_points: usize,
6383) -> Result<()> {
6384    let original_count = markup.points.len();
6385    if current_count == 0 {
6386        for boundary in point_base..=point_base + original_count {
6387            emit_raw(writer, markup.cache_children.at(boundary))?;
6388        }
6389    } else if completed_points == 0 {
6390        if original_count > 0 {
6391            emit_raw(writer, markup.cache_children.at(point_base))?;
6392        }
6393    } else if completed_points < current_count {
6394        if completed_points < original_count {
6395            emit_raw(
6396                writer,
6397                markup.cache_children.at(point_base + completed_points),
6398            )?;
6399        }
6400    } else {
6401        let first_tail = if original_count == 0 {
6402            point_base
6403        } else {
6404            point_base + current_count.min(original_count)
6405        };
6406        for boundary in first_tail..=point_base + original_count {
6407            emit_raw(writer, markup.cache_children.at(boundary))?;
6408        }
6409    }
6410    Ok(())
6411}
6412
6413fn write_point(
6414    writer: &mut Writer<Vec<u8>>,
6415    index: u32,
6416    value: &str,
6417    markup: &PointMarkup,
6418) -> Result<()> {
6419    let index = index.to_string();
6420    let mut start = BytesStart::new("c:pt");
6421    start.push_attribute(("idx", index.as_str()));
6422    push_attributes(&mut start, &markup.raw_attributes);
6423    writer
6424        .write_event(Event::Start(start))
6425        .map_err(OxmlError::from)?;
6426    emit_raw(writer, markup.raw_children.at(0))?;
6427    write_text(writer, "c:v", value, &markup.value)?;
6428    emit_raw(writer, markup.raw_children.at(1))?;
6429    writer
6430        .write_event(Event::End(BytesEnd::new("c:pt")))
6431        .map_err(OxmlError::from)?;
6432    Ok(())
6433}
6434
6435fn cache_layout(markup: &ReferenceMarkup, value_count: usize) -> Result<(u32, Vec<u32>)> {
6436    if markup.point_indexes.len() == value_count
6437        && let Some(declared) = markup.declared_point_count
6438    {
6439        return Ok((declared, markup.point_indexes.clone()));
6440    }
6441    let declared = u32::try_from(value_count).map_err(|_| ChartError::InvalidValue {
6442        element: "c:ptCount".to_owned(),
6443        value: value_count.to_string(),
6444    })?;
6445    Ok((declared, (0..declared).collect()))
6446}
6447
6448fn write_text(
6449    writer: &mut Writer<Vec<u8>>,
6450    tag: &str,
6451    value: &str,
6452    markup: &TextMarkup,
6453) -> Result<()> {
6454    let mut start = BytesStart::new(tag);
6455    push_attributes(&mut start, &markup.raw_attributes);
6456    writer
6457        .write_event(Event::Start(start))
6458        .map_err(OxmlError::from)?;
6459    emit_raw(writer, markup.raw_children.at(0))?;
6460    writer
6461        .write_event(Event::Text(BytesText::new(value)))
6462        .map_err(OxmlError::from)?;
6463    emit_raw(writer, markup.raw_children.at(1))?;
6464    writer
6465        .write_event(Event::End(BytesEnd::new(tag)))
6466        .map_err(OxmlError::from)?;
6467    Ok(())
6468}
6469
6470fn write_wrapper_start(
6471    writer: &mut Writer<Vec<u8>>,
6472    tag: &str,
6473    markup: &WrapperMarkup,
6474) -> Result<()> {
6475    let mut start = BytesStart::new(tag);
6476    push_attributes(&mut start, &markup.raw_attributes);
6477    writer
6478        .write_event(Event::Start(start))
6479        .map_err(OxmlError::from)?;
6480    emit_raw(writer, markup.raw_children.at(0))
6481}
6482
6483fn write_wrapper_end(
6484    writer: &mut Writer<Vec<u8>>,
6485    tag: &str,
6486    markup: &WrapperMarkup,
6487) -> Result<()> {
6488    emit_raw(writer, markup.raw_children.at(1))?;
6489    writer
6490        .write_event(Event::End(BytesEnd::new(tag)))
6491        .map_err(OxmlError::from)?;
6492    Ok(())
6493}
6494
6495fn validate_formula(formula: &str, element: &str) -> Result<()> {
6496    if formula.trim().is_empty() {
6497        return Err(ChartError::InvalidValue {
6498            element: element.to_owned(),
6499            value: formula.to_owned(),
6500        });
6501    }
6502    validate_xml_text(formula, element)
6503}
6504
6505fn validate_format_code(format_code: &str) -> Result<()> {
6506    if format_code.is_empty() {
6507        return Err(ChartError::InvalidValue {
6508            element: "c:formatCode".to_owned(),
6509            value: format_code.to_owned(),
6510        });
6511    }
6512    validate_xml_text(format_code, "c:formatCode")
6513}
6514
6515fn validate_xml_text(value: &str, element: &str) -> Result<()> {
6516    if value.chars().all(|character| {
6517        matches!(
6518            character,
6519            '\u{9}' | '\u{A}' | '\u{D}' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}'
6520        )
6521    }) {
6522        return Ok(());
6523    }
6524    Err(ChartError::InvalidValue {
6525        element: element.to_owned(),
6526        value: value.to_owned(),
6527    })
6528}
6529
6530fn validate_numeric_values(values: &[f64]) -> Result<()> {
6531    if let Some(value) = values.iter().find(|value| !value.is_finite()) {
6532        return Err(ChartError::InvalidValue {
6533            element: "c:v".to_owned(),
6534            value: value.to_string(),
6535        });
6536    }
6537    Ok(())
6538}
6539
6540fn validate_point_count(count: usize, element: &str) -> Result<()> {
6541    u32::try_from(count)
6542        .map(|_| ())
6543        .map_err(|_| ChartError::InvalidValue {
6544            element: element.to_owned(),
6545            value: count.to_string(),
6546        })
6547}
6548
6549fn parse_u32_scalar(xml: &[u8], local: &str) -> Result<(u32, ScalarMarkup)> {
6550    let (value, markup) = scalar_value(xml, local)?;
6551    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
6552    let parsed = value
6553        .parse::<u32>()
6554        .map_err(|_| invalid_attribute(local, "val", value))?;
6555    Ok((parsed, markup))
6556}
6557
6558fn required_u32_attribute(element: &BytesStart<'_>, local: &str, attribute: &[u8]) -> Result<u32> {
6559    let value = attribute_value(element, attribute)?.ok_or_else(|| {
6560        invalid_attribute(
6561            local,
6562            &String::from_utf8_lossy(attribute),
6563            "<missing>".to_owned(),
6564        )
6565    })?;
6566    value
6567        .parse::<u32>()
6568        .map_err(|_| invalid_attribute(local, &String::from_utf8_lossy(attribute), value))
6569}
6570
6571fn chart_root_prefix(element: &BytesStart<'_>) -> Result<Vec<u8>> {
6572    let name = element.name();
6573    let qualified = name.as_ref();
6574    let prefix = qualified
6575        .iter()
6576        .position(|byte| *byte == b':')
6577        .map(|position| qualified[..position].to_vec())
6578        .unwrap_or_default();
6579    let declaration = if prefix.is_empty() {
6580        b"xmlns".to_vec()
6581    } else {
6582        let mut declaration = b"xmlns:".to_vec();
6583        declaration.extend_from_slice(&prefix);
6584        declaration
6585    };
6586    if let Some(namespace) = attribute_value(element, &declaration)?
6587        && namespace != C_NS
6588    {
6589        return Err(invalid_attribute(
6590            &element_name(element),
6591            "namespace",
6592            namespace,
6593        ));
6594    }
6595    reject_conflicting_prefix(element, b"c", C_NS)?;
6596    Ok(prefix)
6597}
6598
6599fn typed_rewrite_bindings(
6600    element: &BytesStart<'_>,
6601    inherited: &NamespaceBindings,
6602) -> Result<NamespaceBindings> {
6603    chart_root_prefix(element)?;
6604    if !element_is_in_namespace(element, C_NS, inherited)? {
6605        return Err(ChartError::UnexpectedElement(element_name(element)));
6606    }
6607    let bindings = chart_bindings(inherited, element)?;
6608    require_fixed_namespace(&bindings, b"c", C_NS, element)?;
6609    Ok(bindings)
6610}
6611
6612fn chart_namespace_defaults() -> NamespaceBindings {
6613    vec![
6614        (b"c".to_vec(), C_NS.to_owned()),
6615        (b"a".to_vec(), A_NS.to_owned()),
6616        (b"r".to_vec(), R_NS.to_owned()),
6617    ]
6618}
6619
6620fn chart_bindings(
6621    inherited: &NamespaceBindings,
6622    element: &BytesStart<'_>,
6623) -> Result<NamespaceBindings> {
6624    let mut bindings = chart_namespace_defaults();
6625    for (prefix, namespace) in inherited
6626        .iter()
6627        .cloned()
6628        .chain(namespace_bindings(element)?)
6629    {
6630        upsert_namespace_binding(&mut bindings, prefix, namespace);
6631    }
6632    Ok(bindings)
6633}
6634
6635fn root_chart_bindings(
6636    xml: &[u8],
6637    local: &[u8],
6638    inherited: &NamespaceBindings,
6639) -> Result<NamespaceBindings> {
6640    let mut reader = Reader::from_reader(xml);
6641    let mut buffer = Vec::new();
6642    loop {
6643        match reader
6644            .read_event_into(&mut buffer)
6645            .map_err(OxmlError::from)?
6646        {
6647            Event::Start(element) | Event::Empty(element)
6648                if matches_local_name(element.name().as_ref(), local) =>
6649            {
6650                if !element_is_in_namespace(&element, C_NS, inherited)? {
6651                    return Err(ChartError::UnexpectedElement(element_name(&element)));
6652                }
6653                return chart_bindings(inherited, &element);
6654            }
6655            Event::Start(element) | Event::Empty(element) => {
6656                return Err(ChartError::UnexpectedElement(element_name(&element)));
6657            }
6658            Event::Eof => {
6659                return Err(ChartError::MissingElement(format!(
6660                    "c:{}",
6661                    String::from_utf8_lossy(local)
6662                )));
6663            }
6664            _ => {}
6665        }
6666        buffer.clear();
6667    }
6668}
6669
6670fn chart_child_local(
6671    element: &BytesStart<'_>,
6672    inherited: &NamespaceBindings,
6673) -> Result<Option<Vec<u8>>> {
6674    Ok(element_is_in_namespace(element, C_NS, inherited)?
6675        .then(|| local_name(element.name().as_ref()).to_vec()))
6676}
6677
6678fn capture_fixed_root_attributes(
6679    start: &BytesStart<'_>,
6680    fixed: &[&str],
6681) -> Result<(XmlAttributes, XmlAttributes)> {
6682    let mut attributes = Vec::new();
6683    let mut namespaces = Vec::new();
6684    for attribute in start.attributes() {
6685        let attribute = attribute.map_err(OxmlError::from)?;
6686        let name = std::str::from_utf8(attribute.key.as_ref())
6687            .map_err(OxmlError::from)?
6688            .to_owned();
6689        let value = attribute
6690            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
6691            .map_err(OxmlError::from)?
6692            .into_owned();
6693        if fixed.contains(&name.as_str()) {
6694            continue;
6695        }
6696        if name == "xmlns" || name.starts_with("xmlns:") {
6697            namespaces.push((name, value));
6698        } else {
6699            attributes.push((name, value));
6700        }
6701    }
6702    Ok((attributes, namespaces))
6703}
6704
6705fn require_fixed_namespace(
6706    bindings: &NamespaceBindings,
6707    prefix: &[u8],
6708    expected: &str,
6709    element: &BytesStart<'_>,
6710) -> Result<()> {
6711    let actual = bindings
6712        .iter()
6713        .find(|(candidate, _)| candidate.as_slice() == prefix)
6714        .map(|(_, namespace)| namespace.as_str());
6715    if actual == Some(expected) {
6716        return Ok(());
6717    }
6718    Err(invalid_attribute(
6719        &element_name(element),
6720        &format!("xmlns:{}", String::from_utf8_lossy(prefix)),
6721        actual.unwrap_or("<missing>").to_owned(),
6722    ))
6723}
6724
6725fn standalone_namespace_declarations(bindings: &NamespaceBindings) -> Result<XmlAttributes> {
6726    bindings
6727        .iter()
6728        .filter(|(prefix, _)| prefix != b"c" && prefix != b"a" && prefix != b"r")
6729        .map(|(prefix, namespace)| {
6730            let prefix = std::str::from_utf8(prefix).map_err(OxmlError::from)?;
6731            let name = if prefix.is_empty() {
6732                "xmlns".to_owned()
6733            } else {
6734                format!("xmlns:{prefix}")
6735            };
6736            Ok((name, namespace.clone()))
6737        })
6738        .collect()
6739}
6740
6741fn capture_fixed_attributes(start: &BytesStart<'_>, fixed: &[&str]) -> Result<XmlAttributes> {
6742    let mut attributes = Vec::new();
6743    for attribute in start.attributes() {
6744        let attribute = attribute.map_err(OxmlError::from)?;
6745        let name = std::str::from_utf8(attribute.key.as_ref())
6746            .map_err(OxmlError::from)?
6747            .to_owned();
6748        if fixed.contains(&name.as_str()) {
6749            continue;
6750        }
6751        let value = attribute
6752            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
6753            .map_err(OxmlError::from)?
6754            .into_owned();
6755        attributes.push((name, value));
6756    }
6757    Ok(attributes)
6758}
6759
6760/// A producer-compatible ChartML axis identifier.
6761///
6762/// ECMA-376 names an unsigned value, but PowerPoint emits signed 32-bit
6763/// lexical values in real chart parts. The accepted domain covers both forms.
6764#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
6765pub struct AxisId(i64);
6766
6767impl AxisId {
6768    pub fn new(value: i64) -> Result<Self> {
6769        if (i64::from(i32::MIN)..=i64::from(u32::MAX)).contains(&value) {
6770            Ok(Self(value))
6771        } else {
6772            Err(ChartError::InvalidValue {
6773                element: "c:axId".to_owned(),
6774                value: value.to_string(),
6775            })
6776        }
6777    }
6778
6779    pub const fn value(self) -> i64 {
6780        self.0
6781    }
6782}
6783
6784/// Whether bars extend along the category or value axis.
6785#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6786pub enum BarDirection {
6787    Bar,
6788    Column,
6789}
6790
6791impl BarDirection {
6792    fn parse(value: &str) -> Option<Self> {
6793        match value {
6794            "bar" => Some(Self::Bar),
6795            "col" => Some(Self::Column),
6796            _ => None,
6797        }
6798    }
6799
6800    const fn as_str(self) -> &'static str {
6801        match self {
6802            Self::Bar => "bar",
6803            Self::Column => "col",
6804        }
6805    }
6806}
6807
6808/// Grouping modes supported by a two-dimensional bar plot.
6809#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6810pub enum BarGrouping {
6811    Clustered,
6812    PercentStacked,
6813    Stacked,
6814    Standard,
6815}
6816
6817impl BarGrouping {
6818    fn parse(value: &str) -> Option<Self> {
6819        match value {
6820            "clustered" => Some(Self::Clustered),
6821            "percentStacked" => Some(Self::PercentStacked),
6822            "stacked" => Some(Self::Stacked),
6823            "standard" => Some(Self::Standard),
6824            _ => None,
6825        }
6826    }
6827
6828    const fn as_str(self) -> &'static str {
6829        match self {
6830            Self::Clustered => "clustered",
6831            Self::PercentStacked => "percentStacked",
6832            Self::Stacked => "stacked",
6833            Self::Standard => "standard",
6834        }
6835    }
6836}
6837
6838/// Grouping modes shared by line and later area plots.
6839#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6840pub enum Grouping {
6841    PercentStacked,
6842    Stacked,
6843    Standard,
6844}
6845
6846impl Grouping {
6847    fn parse(value: &str) -> Option<Self> {
6848        match value {
6849            "percentStacked" => Some(Self::PercentStacked),
6850            "stacked" => Some(Self::Stacked),
6851            "standard" => Some(Self::Standard),
6852            _ => None,
6853        }
6854    }
6855
6856    const fn as_str(self) -> &'static str {
6857        match self {
6858            Self::PercentStacked => "percentStacked",
6859            Self::Stacked => "stacked",
6860            Self::Standard => "standard",
6861        }
6862    }
6863}
6864
6865/// Display style for a two-dimensional scatter plot.
6866#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6867pub enum ScatterStyle {
6868    Line,
6869    LineMarker,
6870    Marker,
6871    None,
6872    Smooth,
6873    SmoothMarker,
6874}
6875
6876impl ScatterStyle {
6877    fn parse(value: &str) -> Option<Self> {
6878        match value {
6879            "line" => Some(Self::Line),
6880            "lineMarker" => Some(Self::LineMarker),
6881            "marker" => Some(Self::Marker),
6882            "none" => Some(Self::None),
6883            "smooth" => Some(Self::Smooth),
6884            "smoothMarker" => Some(Self::SmoothMarker),
6885            _ => None,
6886        }
6887    }
6888
6889    const fn as_str(self) -> &'static str {
6890        match self {
6891            Self::Line => "line",
6892            Self::LineMarker => "lineMarker",
6893            Self::Marker => "marker",
6894            Self::None => "none",
6895            Self::Smooth => "smooth",
6896            Self::SmoothMarker => "smoothMarker",
6897        }
6898    }
6899}
6900
6901/// Display style for a two-dimensional radar plot.
6902#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6903pub enum RadarStyle {
6904    Filled,
6905    Marker,
6906    Standard,
6907}
6908
6909impl RadarStyle {
6910    fn parse(value: &str) -> Option<Self> {
6911        match value {
6912            "filled" => Some(Self::Filled),
6913            "marker" => Some(Self::Marker),
6914            "standard" => Some(Self::Standard),
6915            _ => None,
6916        }
6917    }
6918
6919    const fn as_str(self) -> &'static str {
6920        match self {
6921            Self::Filled => "filled",
6922            Self::Marker => "marker",
6923            Self::Standard => "standard",
6924        }
6925    }
6926}
6927
6928/// A supported two-dimensional plot owned by one plot area.
6929#[derive(Clone, Debug, PartialEq)]
6930pub enum Plot {
6931    Bar {
6932        direction: BarDirection,
6933        grouping: BarGrouping,
6934        gap_width: u16,
6935        overlap: i8,
6936        series: Vec<Series>,
6937        data_labels: Option<CT_DLbls>,
6938        axis_ids: [AxisId; 2],
6939    },
6940    Line {
6941        grouping: Grouping,
6942        marker: bool,
6943        smooth: bool,
6944        series: Vec<Series>,
6945        data_labels: Option<CT_DLbls>,
6946        axis_ids: [AxisId; 2],
6947    },
6948    Pie {
6949        first_slice_angle: u16,
6950        series: Vec<Series>,
6951        data_labels: Option<CT_DLbls>,
6952    },
6953    Doughnut {
6954        first_slice_angle: u16,
6955        hole_size: u8,
6956        series: Vec<Series>,
6957        data_labels: Option<CT_DLbls>,
6958    },
6959    Area {
6960        grouping: Grouping,
6961        series: Vec<Series>,
6962        data_labels: Option<CT_DLbls>,
6963        axis_ids: [AxisId; 2],
6964    },
6965    Scatter {
6966        style: ScatterStyle,
6967        series: Vec<Series>,
6968        data_labels: Option<CT_DLbls>,
6969        axis_ids: [AxisId; 2],
6970    },
6971    Radar {
6972        style: RadarStyle,
6973        series: Vec<Series>,
6974        data_labels: Option<CT_DLbls>,
6975        axis_ids: [AxisId; 2],
6976    },
6977}
6978
6979impl Plot {
6980    pub fn bar(
6981        direction: BarDirection,
6982        grouping: BarGrouping,
6983        series: Vec<Series>,
6984        axis_ids: [AxisId; 2],
6985    ) -> Result<Self> {
6986        let plot = Self::Bar {
6987            direction,
6988            grouping,
6989            gap_width: 150,
6990            overlap: 0,
6991            series,
6992            data_labels: None,
6993            axis_ids,
6994        };
6995        plot.validate()?;
6996        Ok(plot)
6997    }
6998
6999    pub fn line(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
7000        let plot = Self::Line {
7001            grouping,
7002            marker: false,
7003            smooth: false,
7004            series,
7005            data_labels: None,
7006            axis_ids,
7007        };
7008        plot.validate()?;
7009        Ok(plot)
7010    }
7011
7012    pub fn pie(series: Vec<Series>) -> Result<Self> {
7013        let plot = Self::Pie {
7014            first_slice_angle: 0,
7015            series,
7016            data_labels: None,
7017        };
7018        plot.validate()?;
7019        Ok(plot)
7020    }
7021
7022    pub fn doughnut(series: Vec<Series>) -> Result<Self> {
7023        let plot = Self::Doughnut {
7024            first_slice_angle: 0,
7025            hole_size: 50,
7026            series,
7027            data_labels: None,
7028        };
7029        plot.validate()?;
7030        Ok(plot)
7031    }
7032
7033    pub fn area(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
7034        let plot = Self::Area {
7035            grouping,
7036            series,
7037            data_labels: None,
7038            axis_ids,
7039        };
7040        plot.validate()?;
7041        Ok(plot)
7042    }
7043
7044    pub fn scatter(
7045        style: ScatterStyle,
7046        series: Vec<Series>,
7047        axis_ids: [AxisId; 2],
7048    ) -> Result<Self> {
7049        let plot = Self::Scatter {
7050            style,
7051            series,
7052            data_labels: None,
7053            axis_ids,
7054        };
7055        plot.validate()?;
7056        Ok(plot)
7057    }
7058
7059    pub fn radar(style: RadarStyle, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
7060        let plot = Self::Radar {
7061            style,
7062            series,
7063            data_labels: None,
7064            axis_ids,
7065        };
7066        plot.validate()?;
7067        Ok(plot)
7068    }
7069
7070    fn axis_ids(&self) -> Option<[AxisId; 2]> {
7071        match self {
7072            Self::Bar { axis_ids, .. }
7073            | Self::Line { axis_ids, .. }
7074            | Self::Area { axis_ids, .. }
7075            | Self::Scatter { axis_ids, .. }
7076            | Self::Radar { axis_ids, .. } => Some(*axis_ids),
7077            Self::Pie { .. } | Self::Doughnut { .. } => None,
7078        }
7079    }
7080
7081    fn validate(&self) -> Result<()> {
7082        let (series, labels, axis_ids) = match self {
7083            Self::Bar {
7084                gap_width,
7085                overlap,
7086                series,
7087                data_labels,
7088                axis_ids,
7089                ..
7090            } => {
7091                if *gap_width > 500 {
7092                    return Err(ChartError::InvalidValue {
7093                        element: "c:gapWidth".to_owned(),
7094                        value: gap_width.to_string(),
7095                    });
7096                }
7097                if !(-100..=100).contains(overlap) {
7098                    return Err(ChartError::InvalidValue {
7099                        element: "c:overlap".to_owned(),
7100                        value: overlap.to_string(),
7101                    });
7102                }
7103                (series, data_labels, Some(axis_ids))
7104            }
7105            Self::Line {
7106                series,
7107                data_labels,
7108                axis_ids,
7109                ..
7110            } => (series, data_labels, Some(axis_ids)),
7111            Self::Pie {
7112                first_slice_angle,
7113                series,
7114                data_labels,
7115            } => {
7116                if *first_slice_angle > 360 {
7117                    return Err(ChartError::InvalidValue {
7118                        element: "c:firstSliceAng".to_owned(),
7119                        value: first_slice_angle.to_string(),
7120                    });
7121                }
7122                (series, data_labels, None)
7123            }
7124            Self::Doughnut {
7125                first_slice_angle,
7126                hole_size,
7127                series,
7128                data_labels,
7129            } => {
7130                if *first_slice_angle > 360 {
7131                    return Err(ChartError::InvalidValue {
7132                        element: "c:firstSliceAng".to_owned(),
7133                        value: first_slice_angle.to_string(),
7134                    });
7135                }
7136                if !(10..=90).contains(hole_size) {
7137                    return Err(ChartError::InvalidValue {
7138                        element: "c:holeSize".to_owned(),
7139                        value: hole_size.to_string(),
7140                    });
7141                }
7142                (series, data_labels, None)
7143            }
7144            Self::Area {
7145                series,
7146                data_labels,
7147                axis_ids,
7148                ..
7149            }
7150            | Self::Radar {
7151                series,
7152                data_labels,
7153                axis_ids,
7154                ..
7155            } => (series, data_labels, Some(axis_ids)),
7156            Self::Scatter {
7157                series,
7158                data_labels,
7159                axis_ids,
7160                ..
7161            } => {
7162                for item in series {
7163                    if !matches!(item.categories, Some(AxisData::Numeric(_))) {
7164                        return Err(ChartError::InvalidValue {
7165                            element: "c:xVal".to_owned(),
7166                            value: "scatter series requires numeric x values".to_owned(),
7167                        });
7168                    }
7169                }
7170                (series, data_labels, Some(axis_ids))
7171            }
7172        };
7173        if series.is_empty() {
7174            return Err(ChartError::MissingElement("c:ser".to_owned()));
7175        }
7176        let scatter_plot = matches!(self, Self::Scatter { .. });
7177        for item in series {
7178            if item.bubble_size.is_some() || item.opaque_bubble_size {
7179                return Err(ChartError::InvalidValue {
7180                    element: "c:bubbleSize".to_owned(),
7181                    value: "bubble size is only valid in an opaque bubble plot".to_owned(),
7182                });
7183            }
7184            if item.uses_scatter_wrappers && !scatter_plot {
7185                return Err(ChartError::InvalidValue {
7186                    element: "c:ser".to_owned(),
7187                    value: "x/y wrappers are only valid in scatter plots".to_owned(),
7188                });
7189            }
7190            if scatter_plot && item.parsed_from_xml && !item.uses_scatter_wrappers {
7191                return Err(ChartError::InvalidValue {
7192                    element: "c:ser".to_owned(),
7193                    value: "scatter plots require xVal/yVal wrappers".to_owned(),
7194                });
7195            }
7196        }
7197        if let Some(axis_ids) = axis_ids {
7198            if axis_ids[0] == axis_ids[1] {
7199                return Err(ChartError::DuplicateElement(format!(
7200                    "c:axId {}",
7201                    axis_ids[0].value()
7202                )));
7203            }
7204            for id in axis_ids {
7205                AxisId::new(id.value())?;
7206            }
7207        }
7208        if let Some(labels) = labels {
7209            labels.validate()?;
7210        }
7211        for item in series {
7212            item.values.validate()?;
7213        }
7214        Ok(())
7215    }
7216}
7217
7218/// The concrete ChartML root used by an axis.
7219#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
7220pub enum AxisKind {
7221    Category,
7222    Value,
7223    Date,
7224    Series,
7225}
7226
7227impl AxisKind {
7228    fn parse(local: &[u8]) -> Option<Self> {
7229        match local {
7230            b"catAx" => Some(Self::Category),
7231            b"valAx" => Some(Self::Value),
7232            b"dateAx" => Some(Self::Date),
7233            b"serAx" => Some(Self::Series),
7234            _ => None,
7235        }
7236    }
7237
7238    const fn local(self) -> &'static str {
7239        match self {
7240            Self::Category => "catAx",
7241            Self::Value => "valAx",
7242            Self::Date => "dateAx",
7243            Self::Series => "serAx",
7244        }
7245    }
7246}
7247
7248/// Direction of values along an axis scale.
7249#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
7250pub enum Orientation {
7251    #[default]
7252    MinMax,
7253    MaxMin,
7254}
7255
7256impl Orientation {
7257    fn parse(value: &str) -> Option<Self> {
7258        match value {
7259            "minMax" => Some(Self::MinMax),
7260            "maxMin" => Some(Self::MaxMin),
7261            _ => None,
7262        }
7263    }
7264
7265    const fn as_str(self) -> &'static str {
7266        match self {
7267            Self::MinMax => "minMax",
7268            Self::MaxMin => "maxMin",
7269        }
7270    }
7271}
7272
7273/// Which side of the plot area carries an axis.
7274#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7275pub enum AxisPosition {
7276    Bottom,
7277    Left,
7278    Right,
7279    Top,
7280}
7281
7282impl AxisPosition {
7283    fn parse(value: &str) -> Option<Self> {
7284        match value {
7285            "b" => Some(Self::Bottom),
7286            "l" => Some(Self::Left),
7287            "r" => Some(Self::Right),
7288            "t" => Some(Self::Top),
7289            _ => None,
7290        }
7291    }
7292
7293    const fn as_str(self) -> &'static str {
7294        match self {
7295            Self::Bottom => "b",
7296            Self::Left => "l",
7297            Self::Right => "r",
7298            Self::Top => "t",
7299        }
7300    }
7301}
7302
7303/// Placement of a major or minor tick relative to its axis line.
7304#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
7305pub enum TickMark {
7306    Cross,
7307    Inside,
7308    #[default]
7309    None,
7310    Outside,
7311}
7312
7313impl TickMark {
7314    fn parse(value: &str) -> Option<Self> {
7315        match value {
7316            "cross" => Some(Self::Cross),
7317            "in" => Some(Self::Inside),
7318            "none" => Some(Self::None),
7319            "out" => Some(Self::Outside),
7320            _ => None,
7321        }
7322    }
7323
7324    const fn as_str(self) -> &'static str {
7325        match self {
7326            Self::Cross => "cross",
7327            Self::Inside => "in",
7328            Self::None => "none",
7329            Self::Outside => "out",
7330        }
7331    }
7332}
7333
7334/// Placement of tick labels relative to an axis.
7335#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
7336pub enum TickLabelPosition {
7337    High,
7338    Low,
7339    #[default]
7340    NextTo,
7341    None,
7342}
7343
7344impl TickLabelPosition {
7345    fn parse(value: &str) -> Option<Self> {
7346        match value {
7347            "high" => Some(Self::High),
7348            "low" => Some(Self::Low),
7349            "nextTo" => Some(Self::NextTo),
7350            "none" => Some(Self::None),
7351            _ => None,
7352        }
7353    }
7354
7355    const fn as_str(self) -> &'static str {
7356        match self {
7357            Self::High => "high",
7358            Self::Low => "low",
7359            Self::NextTo => "nextTo",
7360            Self::None => "none",
7361        }
7362    }
7363}
7364
7365/// The modelled values of `c:scaling`.
7366#[derive(Clone, Debug)]
7367pub struct Scaling {
7368    pub log_base: Option<f64>,
7369    pub orientation: Orientation,
7370    pub maximum: Option<f64>,
7371    pub minimum: Option<f64>,
7372    log_base_markup: Option<ScalarMarkup>,
7373    orientation_markup: Option<ScalarMarkup>,
7374    maximum_markup: Option<ScalarMarkup>,
7375    minimum_markup: Option<ScalarMarkup>,
7376    raw_attributes: XmlAttributes,
7377    raw_children: OrderedRawChildren,
7378}
7379
7380impl PartialEq for Scaling {
7381    fn eq(&self, other: &Self) -> bool {
7382        self.log_base == other.log_base
7383            && self.orientation == other.orientation
7384            && self.maximum == other.maximum
7385            && self.minimum == other.minimum
7386            && optional_scalar_markup_eq(&self.log_base_markup, &other.log_base_markup)
7387            && optional_scalar_markup_eq(&self.orientation_markup, &other.orientation_markup)
7388            && optional_scalar_markup_eq(&self.maximum_markup, &other.maximum_markup)
7389            && optional_scalar_markup_eq(&self.minimum_markup, &other.minimum_markup)
7390            && self.raw_attributes == other.raw_attributes
7391            && self.raw_children == other.raw_children
7392    }
7393}
7394
7395impl Default for Scaling {
7396    fn default() -> Self {
7397        Self {
7398            log_base: None,
7399            orientation: Orientation::MinMax,
7400            maximum: None,
7401            minimum: None,
7402            log_base_markup: None,
7403            orientation_markup: None,
7404            maximum_markup: None,
7405            minimum_markup: None,
7406            raw_attributes: Vec::new(),
7407            raw_children: OrderedRawChildren::default(),
7408        }
7409    }
7410}
7411
7412/// Shape properties on a major or minor gridline container.
7413#[derive(Clone, Debug, Default, PartialEq)]
7414pub struct ChartLines {
7415    pub sp_pr: Option<CT_ShapeProperties>,
7416    raw_attributes: XmlAttributes,
7417    raw_children: OrderedRawChildren,
7418}
7419
7420/// Number format applied to axis values and labels.
7421#[derive(Clone, Debug, Eq, PartialEq)]
7422pub struct NumberFormat {
7423    pub format_code: String,
7424    pub source_linked: bool,
7425    raw_attributes: XmlAttributes,
7426    raw_content: Vec<Vec<u8>>,
7427}
7428
7429/// Placement of a chart data label relative to its data point.
7430#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7431pub enum DataLabelPosition {
7432    BestFit,
7433    Bottom,
7434    Center,
7435    InsideBase,
7436    InsideEnd,
7437    Left,
7438    OutsideEnd,
7439    Right,
7440    Top,
7441}
7442
7443impl DataLabelPosition {
7444    fn parse(value: &str) -> Option<Self> {
7445        match value {
7446            "bestFit" => Some(Self::BestFit),
7447            "b" => Some(Self::Bottom),
7448            "ctr" => Some(Self::Center),
7449            "inBase" => Some(Self::InsideBase),
7450            "inEnd" => Some(Self::InsideEnd),
7451            "l" => Some(Self::Left),
7452            "outEnd" => Some(Self::OutsideEnd),
7453            "r" => Some(Self::Right),
7454            "t" => Some(Self::Top),
7455            _ => None,
7456        }
7457    }
7458
7459    const fn as_str(self) -> &'static str {
7460        match self {
7461            Self::BestFit => "bestFit",
7462            Self::Bottom => "b",
7463            Self::Center => "ctr",
7464            Self::InsideBase => "inBase",
7465            Self::InsideEnd => "inEnd",
7466            Self::Left => "l",
7467            Self::OutsideEnd => "outEnd",
7468            Self::Right => "r",
7469            Self::Top => "t",
7470        }
7471    }
7472}
7473
7474/// Collection-level data-label defaults for one chart series or plot.
7475#[derive(Clone, Debug, Default, PartialEq)]
7476pub struct CT_DLbls {
7477    pub number_format: Option<NumberFormat>,
7478    pub position: Option<DataLabelPosition>,
7479    pub separator: Option<String>,
7480    pub show_legend_key: bool,
7481    pub show_value: bool,
7482    pub show_category_name: bool,
7483    pub show_series_name: bool,
7484    pub show_percent: bool,
7485    pub show_bubble_size: bool,
7486    position_markup: Option<ScalarMarkup>,
7487    show_legend_key_markup: Option<ScalarMarkup>,
7488    show_value_markup: Option<ScalarMarkup>,
7489    show_category_name_markup: Option<ScalarMarkup>,
7490    show_series_name_markup: Option<ScalarMarkup>,
7491    show_percent_markup: Option<ScalarMarkup>,
7492    show_bubble_size_markup: Option<ScalarMarkup>,
7493    separator_markup: Option<TextMarkup>,
7494    raw_attributes: XmlAttributes,
7495    namespace_declarations: XmlAttributes,
7496    raw_children: OrderedRawChildren,
7497}
7498
7499/// How a chart displays cells whose cached values are blank.
7500#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
7501pub enum DispBlanksAs {
7502    #[default]
7503    Gap,
7504    Zero,
7505    Span,
7506}
7507
7508impl DispBlanksAs {
7509    fn parse(value: &str) -> Option<Self> {
7510        match value {
7511            "gap" => Some(Self::Gap),
7512            "zero" => Some(Self::Zero),
7513            "span" => Some(Self::Span),
7514            _ => None,
7515        }
7516    }
7517
7518    const fn as_str(self) -> &'static str {
7519        match self {
7520            Self::Gap => "gap",
7521            Self::Zero => "zero",
7522            Self::Span => "span",
7523        }
7524    }
7525}
7526
7527/// A chart title with plain-text authoring and preserved unsupported children.
7528#[derive(Clone, Debug, Default, Eq, PartialEq)]
7529pub struct CT_Title {
7530    text: Option<CT_TextBody>,
7531    raw_attributes: Vec<(String, String)>,
7532    raw_children: OrderedRawChildren,
7533}
7534
7535/// Preserved lexical and ordered-raw state for one typed plot.
7536#[derive(Clone, Debug, Default, PartialEq)]
7537struct PlotMarkup {
7538    raw_attributes: XmlAttributes,
7539    raw_children: OrderedRawChildren,
7540    direction: Option<ScalarMarkup>,
7541    grouping: Option<ScalarMarkup>,
7542    gap_width: Option<ScalarMarkup>,
7543    overlap: Option<ScalarMarkup>,
7544    marker: Option<ScalarMarkup>,
7545    smooth: Option<ScalarMarkup>,
7546    first_slice_angle: Option<ScalarMarkup>,
7547    hole_size: Option<ScalarMarkup>,
7548    style: Option<ScalarMarkup>,
7549    axis_ids: Vec<AxisIdMarkup>,
7550    original_series_keys: Vec<(u32, u32)>,
7551    original_axis_ids: Vec<AxisId>,
7552    parsed_bar: Option<bool>,
7553    parsed_remaining: Option<&'static str>,
7554}
7555
7556/// A plot-area shell. F-119 through F-122 replace selected raw slots with types.
7557#[derive(Clone, Debug, PartialEq)]
7558pub struct CT_PlotArea {
7559    raw_attributes: Vec<(String, String)>,
7560    raw_children: OrderedRawChildren,
7561    namespace_bindings: NamespaceBindings,
7562    plots: Option<Vec<Plot>>,
7563    plot_markup: Vec<PlotMarkup>,
7564    axes: Vec<Axis>,
7565}
7566
7567/// The five ChartML legend positions.
7568#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
7569pub enum LegendPosition {
7570    #[default]
7571    Right,
7572    Left,
7573    Top,
7574    Bottom,
7575    TopRight,
7576}
7577
7578impl LegendPosition {
7579    /// The ChartML lexical value.
7580    pub fn as_str(self) -> &'static str {
7581        match self {
7582            Self::Right => "r",
7583            Self::Left => "l",
7584            Self::Top => "t",
7585            Self::Bottom => "b",
7586            Self::TopRight => "tr",
7587        }
7588    }
7589    /// Parse one of the five supported lexical values.
7590    pub fn parse(value: &str) -> Result<Self> {
7591        match value {
7592            "r" => Ok(Self::Right),
7593            "l" => Ok(Self::Left),
7594            "t" => Ok(Self::Top),
7595            "b" => Ok(Self::Bottom),
7596            "tr" => Ok(Self::TopRight),
7597            _ => Err(invalid_authoring_value(format!(
7598                "unknown legend position {value}"
7599            ))),
7600        }
7601    }
7602}
7603
7604/// Effective legend settings. An absent legend has right/false defaults.
7605#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
7606pub struct LegendSettings {
7607    pub visible: bool,
7608    pub position: LegendPosition,
7609    pub overlay: bool,
7610}
7611
7612fn legend_scalar_value(xml: &[u8], node: &ChartSourceNode) -> Result<String> {
7613    let mut reader = Reader::from_reader(&xml[node.start..node.end]);
7614    let value = match reader.read_event().map_err(OxmlError::from)? {
7615        Event::Start(element) | Event::Empty(element) => attribute_value(&element, b"val")?
7616            .unwrap_or_else(|| {
7617                if node.local == b"legendPos" {
7618                    "r".into()
7619                } else {
7620                    "1".into()
7621                }
7622            }),
7623        _ => return Err(invalid_authoring_value("legend scalar root")),
7624    };
7625    loop {
7626        match reader.read_event().map_err(OxmlError::from)? {
7627            Event::End(_) | Event::Comment(_) | Event::PI(_) => {}
7628            Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
7629            Event::Eof => return Ok(value),
7630            _ => {
7631                return Err(invalid_authoring_value(
7632                    "legend scalar has non-scalar content",
7633                ));
7634            }
7635        }
7636    }
7637}
7638
7639fn legend_settings_at(
7640    xml: &[u8],
7641    nodes: &[ChartSourceNode],
7642    legend: usize,
7643) -> Result<LegendSettings> {
7644    let mut settings = LegendSettings {
7645        visible: true,
7646        ..LegendSettings::default()
7647    };
7648    let mut position = None;
7649    let mut overlay = None;
7650    for (index, node) in nodes
7651        .iter()
7652        .enumerate()
7653        .filter(|(_, n)| n.parent == Some(legend) && n.qualified)
7654    {
7655        if node.local != b"legendPos" && node.local != b"overlay" {
7656            continue;
7657        }
7658        if nodes.iter().any(|child| child.parent == Some(index)) {
7659            return Err(invalid_authoring_value("legend scalar has child elements"));
7660        }
7661        let value = legend_scalar_value(xml, node)?;
7662        if node.local == b"legendPos" {
7663            set_once(&mut position, LegendPosition::parse(&value)?, "c:legendPos")?;
7664        } else {
7665            set_once(
7666                &mut overlay,
7667                parse_bool_lexical("c:overlay", "val", &value)?,
7668                "c:overlay",
7669            )?;
7670        }
7671    }
7672    settings.position = position.unwrap_or_default();
7673    settings.overlay = overlay.unwrap_or(false);
7674    Ok(settings)
7675}
7676
7677/// Read only the namespace-owned legend without rewriting producer bytes.
7678pub fn chart_legend_settings(xml: &[u8]) -> Result<LegendSettings> {
7679    oxml_core::xml::validate_strict_xml_1_0(xml)
7680        .map_err(|error| invalid_authoring_value(format!("invalid chart XML: {error:?}")))?;
7681    let nodes = chart_source_nodes(xml)?;
7682    if !nodes
7683        .first()
7684        .is_some_and(|n| n.qualified && n.local == b"chartSpace")
7685        || nodes.iter().filter(|n| n.parent.is_none()).count() != 1
7686    {
7687        return Err(invalid_authoring_value("owned chartSpace required"));
7688    }
7689    let chart = chart_child_node(&nodes, 0, b"chart")?;
7690    let legends = nodes
7691        .iter()
7692        .enumerate()
7693        .filter(|(_, n)| n.parent == Some(chart) && n.qualified && n.local == b"legend")
7694        .map(|(i, _)| i)
7695        .collect::<Vec<_>>();
7696    match legends.as_slice() {
7697        [] => Ok(LegendSettings::default()),
7698        [index] => legend_settings_at(xml, &nodes, *index),
7699        _ => Err(ChartError::DuplicateElement("c:legend".into())),
7700    }
7701}
7702
7703fn legend_child_rank(local: &[u8]) -> Option<u8> {
7704    match local {
7705        b"legendPos" => Some(0),
7706        b"legendEntry" => Some(1),
7707        b"layout" => Some(2),
7708        b"overlay" => Some(3),
7709        b"spPr" => Some(4),
7710        b"txPr" => Some(5),
7711        b"extLst" => Some(6),
7712        _ => None,
7713    }
7714}
7715
7716fn insert_chart_child(
7717    xml: &[u8],
7718    node: &ChartSourceNode,
7719    position: usize,
7720    bytes: &[u8],
7721) -> Result<Vec<u8>> {
7722    let mut reader = Reader::from_reader(&xml[node.start..node.end]);
7723    let event = reader.read_event().map_err(OxmlError::from)?;
7724    let mut result = xml.to_vec();
7725    if let Event::Empty(element) = event {
7726        let end = node.start + reader.buffer_position() as usize;
7727        let name = element_name(&element);
7728        let mut replacement = xml[node.start..end - 2].to_vec();
7729        replacement.push(b'>');
7730        replacement.extend_from_slice(bytes);
7731        replacement.extend_from_slice(format!("</{name}>").as_bytes());
7732        result.splice(node.start..node.end, replacement);
7733    } else {
7734        result.splice(position..position, bytes.iter().copied());
7735    }
7736    Ok(result)
7737}
7738
7739/// Stage a legend edit, preserving all other source bytes. None leaves a property unchanged.
7740/// Position and overlay edits of an absent legend are no-ops. Hiding drops its settings.
7741pub fn set_chart_legend(
7742    xml: &[u8],
7743    visible: Option<bool>,
7744    position: Option<LegendPosition>,
7745    overlay: Option<bool>,
7746) -> Result<Vec<u8>> {
7747    let current = chart_legend_settings(xml)?;
7748    if visible == Some(false) {
7749        if !current.visible {
7750            return Ok(xml.to_vec());
7751        }
7752        let nodes = chart_source_nodes(xml)?;
7753        let chart = chart_child_node(&nodes, 0, b"chart")?;
7754        let legend = chart_child_node(&nodes, chart, b"legend")?;
7755        let mut result = xml.to_vec();
7756        result.drain(nodes[legend].start..nodes[legend].end);
7757        return Ok(result);
7758    }
7759    if !current.visible && visible != Some(true) {
7760        return Ok(xml.to_vec());
7761    }
7762    if !current.visible {
7763        let nodes = chart_source_nodes(xml)?;
7764        let chart = chart_child_node(&nodes, 0, b"chart")?;
7765        let after = chart_child_node(&nodes, chart, b"plotArea")?;
7766        let bytes = format!(
7767            "<c:legend xmlns:c=\"{C_NS}\"><c:legendPos val=\"{}\"/><c:overlay val=\"{}\"/></c:legend>",
7768            position.unwrap_or_default().as_str(),
7769            u8::from(overlay.unwrap_or(false))
7770        );
7771        return insert_chart_child(xml, &nodes[chart], nodes[after].end, bytes.as_bytes());
7772    }
7773    let mut result = xml.to_vec();
7774    for (local, value, changed) in [
7775        (
7776            "legendPos",
7777            position.unwrap_or(current.position).as_str().to_owned(),
7778            position.is_some_and(|v| v != current.position),
7779        ),
7780        (
7781            "overlay",
7782            u8::from(overlay.unwrap_or(current.overlay)).to_string(),
7783            overlay.is_some_and(|v| v != current.overlay),
7784        ),
7785    ] {
7786        if !changed {
7787            continue;
7788        }
7789        let nodes = chart_source_nodes(&result)?;
7790        let chart = chart_child_node(&nodes, 0, b"chart")?;
7791        let legend = chart_child_node(&nodes, chart, b"legend")?;
7792        if let Some(node) = nodes
7793            .iter()
7794            .find(|n| n.parent == Some(legend) && n.qualified && n.local == local.as_bytes())
7795        {
7796            let mut reader = Reader::from_reader(&result[node.start..node.end]);
7797            let event = reader.read_event().map_err(OxmlError::from)?;
7798            let end = node.start + reader.buffer_position() as usize;
7799            let mut writer = Writer::new(Vec::new());
7800            match event {
7801                Event::Start(element) | Event::Empty(element) => {
7802                    let mut start = BytesStart::new(element_name(&element));
7803                    push_attributes(&mut start, &capture_attributes_except(&element, b"val")?);
7804                    start.push_attribute(("val", value.as_str()));
7805                    writer
7806                        .write_event(if result[end - 2..end] == *b"/>" {
7807                            Event::Empty(start)
7808                        } else {
7809                            Event::Start(start)
7810                        })
7811                        .map_err(OxmlError::from)?;
7812                }
7813                _ => return Err(invalid_authoring_value("legend scalar root")),
7814            }
7815            result.splice(node.start..end, writer.into_inner());
7816        } else {
7817            let rank = legend_child_rank(local.as_bytes()).expect("owned legend scalar");
7818            let insert = nodes
7819                .iter()
7820                .find(|n| {
7821                    n.parent == Some(legend)
7822                        && n.qualified
7823                        && legend_child_rank(&n.local).is_some_and(|r| r > rank)
7824                })
7825                .map(|n| n.start)
7826                .unwrap_or_else(|| {
7827                    result[..nodes[legend].end]
7828                        .iter()
7829                        .rposition(|v| *v == b'<')
7830                        .unwrap_or(nodes[legend].start)
7831                });
7832            let bytes = format!("<c:{local} xmlns:c=\"{C_NS}\" val=\"{value}\"/>");
7833            result = insert_chart_child(&result, &nodes[legend], insert, bytes.as_bytes())?;
7834        }
7835    }
7836    let expected = LegendSettings {
7837        visible: true,
7838        position: position.unwrap_or(current.position),
7839        overlay: overlay.unwrap_or(current.overlay),
7840    };
7841    if chart_legend_settings(&result)? != expected {
7842        return Err(invalid_authoring_value("legend edit projection mismatch"));
7843    }
7844    Ok(result)
7845}
7846
7847/// A legend retaining opaque children and their effective projection.
7848#[derive(Clone, Debug, Eq, PartialEq)]
7849pub struct CT_Legend {
7850    root_name: String,
7851    settings: LegendSettings,
7852    raw_attributes: Vec<(String, String)>,
7853    raw_children: OrderedRawChildren,
7854}
7855
7856#[derive(Clone, Debug, PartialEq)]
7857struct AxisIdMarkup {
7858    scalar: ScalarMarkup,
7859    parsed: AxisId,
7860    lexical: String,
7861}
7862
7863/// The common modelled sequence of one ChartML axis.
7864#[derive(Clone, Debug)]
7865pub struct Axis {
7866    pub kind: AxisKind,
7867    pub id: AxisId,
7868    pub scaling: Scaling,
7869    pub deleted: bool,
7870    pub position: AxisPosition,
7871    pub major_gridlines: Option<ChartLines>,
7872    pub minor_gridlines: Option<ChartLines>,
7873    pub title: Option<CT_Title>,
7874    pub number_format: Option<NumberFormat>,
7875    pub major_tick_mark: TickMark,
7876    pub minor_tick_mark: TickMark,
7877    pub tick_label_position: TickLabelPosition,
7878    pub sp_pr: Option<CT_ShapeProperties>,
7879    pub tx_pr: Option<CT_TextBody>,
7880    pub cross_axis: AxisId,
7881    parsed_kind: Option<AxisKind>,
7882    id_markup: AxisIdMarkup,
7883    delete_markup: Option<ScalarMarkup>,
7884    position_markup: ScalarMarkup,
7885    major_tick_mark_markup: Option<ScalarMarkup>,
7886    minor_tick_mark_markup: Option<ScalarMarkup>,
7887    tick_label_position_markup: Option<ScalarMarkup>,
7888    cross_axis_markup: AxisIdMarkup,
7889    raw_attributes: XmlAttributes,
7890    namespace_declarations: XmlAttributes,
7891    raw_children: OrderedRawChildren,
7892}
7893
7894impl PartialEq for Axis {
7895    fn eq(&self, other: &Self) -> bool {
7896        self.kind == other.kind
7897            && self.id == other.id
7898            && axis_id_markup_eq(&self.id_markup, &other.id_markup)
7899            && self.scaling == other.scaling
7900            && self.deleted == other.deleted
7901            && optional_scalar_markup_eq(&self.delete_markup, &other.delete_markup)
7902            && self.position == other.position
7903            && self.position_markup == other.position_markup
7904            && self.major_gridlines == other.major_gridlines
7905            && self.minor_gridlines == other.minor_gridlines
7906            && self.title == other.title
7907            && self.number_format == other.number_format
7908            && self.major_tick_mark == other.major_tick_mark
7909            && optional_scalar_markup_eq(
7910                &self.major_tick_mark_markup,
7911                &other.major_tick_mark_markup,
7912            )
7913            && self.minor_tick_mark == other.minor_tick_mark
7914            && optional_scalar_markup_eq(
7915                &self.minor_tick_mark_markup,
7916                &other.minor_tick_mark_markup,
7917            )
7918            && self.tick_label_position == other.tick_label_position
7919            && optional_scalar_markup_eq(
7920                &self.tick_label_position_markup,
7921                &other.tick_label_position_markup,
7922            )
7923            && self.sp_pr == other.sp_pr
7924            && self.tx_pr == other.tx_pr
7925            && self.cross_axis == other.cross_axis
7926            && axis_id_markup_eq(&self.cross_axis_markup, &other.cross_axis_markup)
7927            && self.raw_attributes == other.raw_attributes
7928            && self.namespace_declarations == other.namespace_declarations
7929            && self.raw_children == other.raw_children
7930    }
7931}
7932
7933impl Axis {
7934    pub fn new(kind: AxisKind, id: AxisId, position: AxisPosition, cross_axis: AxisId) -> Self {
7935        Self {
7936            kind,
7937            id,
7938            scaling: Scaling::default(),
7939            deleted: false,
7940            position,
7941            major_gridlines: None,
7942            minor_gridlines: None,
7943            title: None,
7944            number_format: None,
7945            major_tick_mark: TickMark::None,
7946            minor_tick_mark: TickMark::None,
7947            tick_label_position: TickLabelPosition::NextTo,
7948            sp_pr: None,
7949            tx_pr: None,
7950            cross_axis,
7951            parsed_kind: None,
7952            id_markup: default_axis_id_markup(id),
7953            delete_markup: None,
7954            position_markup: ScalarMarkup::default(),
7955            major_tick_mark_markup: None,
7956            minor_tick_mark_markup: None,
7957            tick_label_position_markup: None,
7958            cross_axis_markup: default_axis_id_markup(cross_axis),
7959            raw_attributes: Vec::new(),
7960            namespace_declarations: Vec::new(),
7961            raw_children: OrderedRawChildren::default(),
7962        }
7963    }
7964
7965    /// Sets axis visibility and writes the explicit ChartML deletion flag.
7966    pub fn set_deleted(&mut self, deleted: bool) {
7967        self.deleted = deleted;
7968        self.delete_markup.get_or_insert_with(ScalarMarkup::default);
7969    }
7970
7971    pub fn from_xml(xml: &[u8]) -> Result<Self> {
7972        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
7973    }
7974
7975    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7976        let mut reader = Reader::from_reader(xml);
7977        let mut buffer = Vec::new();
7978        loop {
7979            match reader
7980                .read_event_into(&mut buffer)
7981                .map_err(OxmlError::from)?
7982            {
7983                Event::Start(element) => {
7984                    let name = element.name();
7985                    let local = local_name(name.as_ref());
7986                    let Some(kind) = AxisKind::parse(local) else {
7987                        return Err(ChartError::UnexpectedElement(element_name(&element)));
7988                    };
7989                    chart_root_prefix(&element)?;
7990                    if !element_is_in_namespace(&element, C_NS, inherited)? {
7991                        return Err(ChartError::UnexpectedElement(element_name(&element)));
7992                    }
7993                    return Self::from_element(&mut reader, &element, inherited, kind);
7994                }
7995                Event::Empty(element) => {
7996                    let name = element.name();
7997                    let local = local_name(name.as_ref());
7998                    if AxisKind::parse(local).is_some() {
7999                        return Err(ChartError::MissingElement("c:axId".to_owned()));
8000                    }
8001                    return Err(ChartError::UnexpectedElement(element_name(&element)));
8002                }
8003                Event::Eof => return Err(ChartError::MissingElement("ChartML axis".to_owned())),
8004                _ => {}
8005            }
8006            buffer.clear();
8007        }
8008    }
8009
8010    fn from_element(
8011        reader: &mut Reader<&[u8]>,
8012        start: &BytesStart<'_>,
8013        inherited: &NamespaceBindings,
8014        kind: AxisKind,
8015    ) -> Result<Self> {
8016        reject_conflicting_prefix(start, b"a", A_NS)?;
8017        reject_conflicting_prefix(start, b"r", R_NS)?;
8018        let namespaces = chart_bindings(inherited, start)?;
8019        require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
8020        require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
8021        require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
8022        let (raw_attributes, _) =
8023            capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8024        let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
8025        let mut state = AxisParseState::default();
8026        let mut buffer = Vec::new();
8027        loop {
8028            match reader
8029                .read_event_into(&mut buffer)
8030                .map_err(OxmlError::from)?
8031            {
8032                Event::Start(element) => {
8033                    let name = chart_child_local(&element, &namespaces)?;
8034                    let raw = capture_element(reader, &element)?;
8035                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
8036                }
8037                Event::Empty(element) => {
8038                    let name = chart_child_local(&element, &namespaces)?;
8039                    let raw = capture_empty_element(&element)?;
8040                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
8041                }
8042                event @ (Event::Text(_)
8043                | Event::CData(_)
8044                | Event::Comment(_)
8045                | Event::PI(_)
8046                | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
8047                Event::End(element)
8048                    if matches_local_name(element.name().as_ref(), kind.local().as_bytes()) =>
8049                {
8050                    break;
8051                }
8052                Event::Eof => return Err(missing_end(&format!("c:{}", kind.local()))),
8053                _ => {}
8054            }
8055            buffer.clear();
8056        }
8057        state.finish(kind, raw_attributes, namespace_declarations)
8058    }
8059
8060    pub fn to_xml(&self) -> Result<Vec<u8>> {
8061        self.validate()?;
8062        let mut writer = Writer::new(Vec::new());
8063        let root = format!("c:{}", self.kind.local());
8064        let mut start = BytesStart::new(&root);
8065        start.push_attribute(("xmlns:c", C_NS));
8066        start.push_attribute(("xmlns:a", A_NS));
8067        start.push_attribute(("xmlns:r", R_NS));
8068        push_attributes(&mut start, &self.namespace_declarations);
8069        push_attributes(&mut start, &self.raw_attributes);
8070        writer
8071            .write_event(Event::Start(start))
8072            .map_err(OxmlError::from)?;
8073        emit_raw(&mut writer, self.raw_children.at(0))?;
8074        write_axis_id(&mut writer, "c:axId", self.id, &self.id_markup)?;
8075        emit_raw(&mut writer, self.raw_children.at(1))?;
8076        self.scaling.write_xml(&mut writer)?;
8077        emit_raw(&mut writer, self.raw_children.at(2))?;
8078        if self.deleted || self.delete_markup.is_some() {
8079            write_scalar(
8080                &mut writer,
8081                "c:delete",
8082                bool_lexical(self.deleted),
8083                self.delete_markup.as_ref(),
8084            )?;
8085        }
8086        emit_raw(&mut writer, self.raw_children.at(3))?;
8087        write_scalar(
8088            &mut writer,
8089            "c:axPos",
8090            self.position.as_str(),
8091            Some(&self.position_markup),
8092        )?;
8093        emit_raw(&mut writer, self.raw_children.at(4))?;
8094        if let Some(lines) = &self.major_gridlines {
8095            lines.write_xml(&mut writer, "c:majorGridlines")?;
8096        }
8097        emit_raw(&mut writer, self.raw_children.at(5))?;
8098        if let Some(lines) = &self.minor_gridlines {
8099            lines.write_xml(&mut writer, "c:minorGridlines")?;
8100        }
8101        emit_raw(&mut writer, self.raw_children.at(6))?;
8102        if let Some(title) = &self.title {
8103            title.write_xml(&mut writer)?;
8104        }
8105        emit_raw(&mut writer, self.raw_children.at(7))?;
8106        if let Some(number_format) = &self.number_format {
8107            number_format.write_xml(&mut writer)?;
8108        }
8109        emit_raw(&mut writer, self.raw_children.at(8))?;
8110        if self.major_tick_mark != TickMark::None || self.major_tick_mark_markup.is_some() {
8111            write_scalar(
8112                &mut writer,
8113                "c:majorTickMark",
8114                self.major_tick_mark.as_str(),
8115                self.major_tick_mark_markup.as_ref(),
8116            )?;
8117        }
8118        emit_raw(&mut writer, self.raw_children.at(9))?;
8119        if self.minor_tick_mark != TickMark::None || self.minor_tick_mark_markup.is_some() {
8120            write_scalar(
8121                &mut writer,
8122                "c:minorTickMark",
8123                self.minor_tick_mark.as_str(),
8124                self.minor_tick_mark_markup.as_ref(),
8125            )?;
8126        }
8127        emit_raw(&mut writer, self.raw_children.at(10))?;
8128        if self.tick_label_position != TickLabelPosition::NextTo
8129            || self.tick_label_position_markup.is_some()
8130        {
8131            write_scalar(
8132                &mut writer,
8133                "c:tickLblPos",
8134                self.tick_label_position.as_str(),
8135                self.tick_label_position_markup.as_ref(),
8136            )?;
8137        }
8138        emit_raw(&mut writer, self.raw_children.at(11))?;
8139        if let Some(properties) = &self.sp_pr {
8140            properties.write_xml_as(&mut writer, "c:spPr")?;
8141        }
8142        emit_raw(&mut writer, self.raw_children.at(12))?;
8143        if let Some(text) = &self.tx_pr {
8144            text.write_xml_as(&mut writer, "c:txPr")?;
8145        }
8146        emit_raw(&mut writer, self.raw_children.at(13))?;
8147        write_axis_id(
8148            &mut writer,
8149            "c:crossAx",
8150            self.cross_axis,
8151            &self.cross_axis_markup,
8152        )?;
8153        emit_raw(&mut writer, self.raw_children.at(14))?;
8154        writer
8155            .write_event(Event::End(BytesEnd::new(&root)))
8156            .map_err(OxmlError::from)?;
8157        Ok(writer.into_inner())
8158    }
8159
8160    pub fn raw_children(&self) -> &OrderedRawChildren {
8161        &self.raw_children
8162    }
8163
8164    fn validate(&self) -> Result<()> {
8165        if let Some(parsed_kind) = self.parsed_kind
8166            && self.kind != parsed_kind
8167        {
8168            return Err(ChartError::InvalidValue {
8169                element: "ChartML axis root".to_owned(),
8170                value: format!(
8171                    "parsed c:{} cannot be relabelled c:{}",
8172                    parsed_kind.local(),
8173                    self.kind.local()
8174                ),
8175            });
8176        }
8177        AxisId::new(self.id.value())?;
8178        AxisId::new(self.cross_axis.value())?;
8179        self.scaling.validate()?;
8180        if let Some(number_format) = &self.number_format {
8181            number_format.validate()?;
8182        }
8183        Ok(())
8184    }
8185}
8186
8187#[derive(Default)]
8188struct AxisParseState {
8189    id: Option<(AxisId, AxisIdMarkup)>,
8190    scaling: Option<Scaling>,
8191    deleted: Option<(bool, ScalarMarkup)>,
8192    position: Option<(AxisPosition, ScalarMarkup)>,
8193    major_gridlines: Option<ChartLines>,
8194    minor_gridlines: Option<ChartLines>,
8195    title: Option<CT_Title>,
8196    number_format: Option<NumberFormat>,
8197    major_tick_mark: Option<(TickMark, ScalarMarkup)>,
8198    minor_tick_mark: Option<(TickMark, ScalarMarkup)>,
8199    tick_label_position: Option<(TickLabelPosition, ScalarMarkup)>,
8200    sp_pr: Option<CT_ShapeProperties>,
8201    tx_pr: Option<CT_TextBody>,
8202    cross_axis: Option<(AxisId, AxisIdMarkup)>,
8203    raw_children: OrderedRawChildren,
8204    boundary: usize,
8205}
8206
8207impl AxisParseState {
8208    fn capture_event(&mut self, raw: Vec<u8>) {
8209        self.raw_children.push(self.boundary, raw);
8210    }
8211
8212    fn parse_child(
8213        &mut self,
8214        name: &[u8],
8215        raw: Vec<u8>,
8216        namespaces: &NamespaceBindings,
8217    ) -> Result<()> {
8218        match name {
8219            b"axId" => {
8220                set_once(&mut self.id, parse_axis_id_scalar(&raw, "axId")?, "c:axId")?;
8221                self.boundary = self.boundary.max(1);
8222            }
8223            b"scaling" => {
8224                set_once(
8225                    &mut self.scaling,
8226                    Scaling::from_xml(&raw, namespaces)?,
8227                    "c:scaling",
8228                )?;
8229                self.boundary = self.boundary.max(2);
8230            }
8231            b"delete" => {
8232                set_once(
8233                    &mut self.deleted,
8234                    parse_bool_value(&raw, "delete")?,
8235                    "c:delete",
8236                )?;
8237                self.boundary = self.boundary.max(3);
8238            }
8239            b"axPos" => {
8240                set_once(&mut self.position, parse_axis_position(&raw)?, "c:axPos")?;
8241                self.boundary = self.boundary.max(4);
8242            }
8243            b"majorGridlines" => {
8244                set_once(
8245                    &mut self.major_gridlines,
8246                    ChartLines::from_xml(&raw, b"majorGridlines", namespaces)?,
8247                    "c:majorGridlines",
8248                )?;
8249                self.boundary = self.boundary.max(5);
8250            }
8251            b"minorGridlines" => {
8252                set_once(
8253                    &mut self.minor_gridlines,
8254                    ChartLines::from_xml(&raw, b"minorGridlines", namespaces)?,
8255                    "c:minorGridlines",
8256                )?;
8257                self.boundary = self.boundary.max(6);
8258            }
8259            b"title" => {
8260                set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
8261                self.boundary = self.boundary.max(7);
8262            }
8263            b"numFmt" => {
8264                set_once(
8265                    &mut self.number_format,
8266                    NumberFormat::from_xml(&raw)?,
8267                    "c:numFmt",
8268                )?;
8269                self.boundary = self.boundary.max(8);
8270            }
8271            b"majorTickMark" => {
8272                set_once(
8273                    &mut self.major_tick_mark,
8274                    parse_tick_mark(&raw, "majorTickMark")?,
8275                    "c:majorTickMark",
8276                )?;
8277                self.boundary = self.boundary.max(9);
8278            }
8279            b"minorTickMark" => {
8280                set_once(
8281                    &mut self.minor_tick_mark,
8282                    parse_tick_mark(&raw, "minorTickMark")?,
8283                    "c:minorTickMark",
8284                )?;
8285                self.boundary = self.boundary.max(10);
8286            }
8287            b"tickLblPos" => {
8288                set_once(
8289                    &mut self.tick_label_position,
8290                    parse_tick_label_position(&raw)?,
8291                    "c:tickLblPos",
8292                )?;
8293                self.boundary = self.boundary.max(11);
8294            }
8295            b"spPr" => {
8296                parse_chart_shape_properties(&raw, namespaces, &mut self.sp_pr)?;
8297                self.boundary = self.boundary.max(12);
8298            }
8299            b"txPr" => {
8300                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
8301                let inherited = namespaces
8302                    .iter()
8303                    .map(|(prefix, uri)| {
8304                        Ok((
8305                            std::str::from_utf8(prefix)
8306                                .map_err(OxmlError::from)?
8307                                .to_owned(),
8308                            uri.clone(),
8309                        ))
8310                    })
8311                    .collect::<Result<Vec<_>>>()?;
8312                let text = CT_TextBody::from_xml_as_with_namespaces(&raw, b"txPr", &inherited)?;
8313                let mut writer = Writer::new(Vec::new());
8314                text.write_xml_as(&mut writer, "c:txPr")?;
8315                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
8316                set_once(&mut self.tx_pr, text, "c:txPr")?;
8317                self.boundary = self.boundary.max(13);
8318            }
8319            b"crossAx" => {
8320                set_once(
8321                    &mut self.cross_axis,
8322                    parse_axis_id_scalar(&raw, "crossAx")?,
8323                    "c:crossAx",
8324                )?;
8325                self.boundary = self.boundary.max(14);
8326            }
8327            _ => self.raw_children.push(self.boundary, raw),
8328        }
8329        Ok(())
8330    }
8331
8332    fn finish(
8333        self,
8334        kind: AxisKind,
8335        raw_attributes: XmlAttributes,
8336        namespace_declarations: XmlAttributes,
8337    ) -> Result<Axis> {
8338        let (id, id_markup) = self
8339            .id
8340            .ok_or_else(|| ChartError::MissingElement("c:axId".to_owned()))?;
8341        let scaling = self
8342            .scaling
8343            .ok_or_else(|| ChartError::MissingElement("c:scaling".to_owned()))?;
8344        let (position, position_markup) = self
8345            .position
8346            .ok_or_else(|| ChartError::MissingElement("c:axPos".to_owned()))?;
8347        let (cross_axis, cross_axis_markup) = self
8348            .cross_axis
8349            .ok_or_else(|| ChartError::MissingElement("c:crossAx".to_owned()))?;
8350        let (deleted, delete_markup) = self
8351            .deleted
8352            .map(|(value, markup)| (value, Some(markup)))
8353            .unwrap_or((false, None));
8354        let (major_tick_mark, major_tick_mark_markup) = self
8355            .major_tick_mark
8356            .map(|(value, markup)| (value, Some(markup)))
8357            .unwrap_or((TickMark::None, None));
8358        let (minor_tick_mark, minor_tick_mark_markup) = self
8359            .minor_tick_mark
8360            .map(|(value, markup)| (value, Some(markup)))
8361            .unwrap_or((TickMark::None, None));
8362        let (tick_label_position, tick_label_position_markup) = self
8363            .tick_label_position
8364            .map(|(value, markup)| (value, Some(markup)))
8365            .unwrap_or((TickLabelPosition::NextTo, None));
8366        let axis = Axis {
8367            kind,
8368            id,
8369            scaling,
8370            deleted,
8371            position,
8372            major_gridlines: self.major_gridlines,
8373            minor_gridlines: self.minor_gridlines,
8374            title: self.title,
8375            number_format: self.number_format,
8376            major_tick_mark,
8377            minor_tick_mark,
8378            tick_label_position,
8379            sp_pr: self.sp_pr,
8380            tx_pr: self.tx_pr,
8381            cross_axis,
8382            parsed_kind: Some(kind),
8383            id_markup,
8384            delete_markup,
8385            position_markup,
8386            major_tick_mark_markup,
8387            minor_tick_mark_markup,
8388            tick_label_position_markup,
8389            cross_axis_markup,
8390            raw_attributes,
8391            namespace_declarations,
8392            raw_children: raw_children_in_schema_order(&self.raw_children, 14),
8393        };
8394        axis.validate()?;
8395        Ok(axis)
8396    }
8397}
8398
8399impl Scaling {
8400    fn from_xml(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
8401        let mut reader = Reader::from_reader(xml);
8402        let mut buffer = Vec::new();
8403        loop {
8404            match reader
8405                .read_event_into(&mut buffer)
8406                .map_err(OxmlError::from)?
8407            {
8408                Event::Start(element)
8409                    if matches_local_name(element.name().as_ref(), b"scaling") =>
8410                {
8411                    let namespaces = typed_rewrite_bindings(&element, inherited)?;
8412                    let raw_attributes = capture_attributes(&element)?;
8413                    let mut log_base = None;
8414                    let mut orientation = None;
8415                    let mut maximum = None;
8416                    let mut minimum = None;
8417                    let mut raw_children = OrderedRawChildren::default();
8418                    let mut boundary = 0usize;
8419                    let mut inner = Vec::new();
8420                    loop {
8421                        match reader
8422                            .read_event_into(&mut inner)
8423                            .map_err(OxmlError::from)?
8424                        {
8425                            Event::Start(child) => {
8426                                let name = chart_child_local(&child, &namespaces)?;
8427                                let raw = capture_element(&mut reader, &child)?;
8428                                parse_scaling_child(
8429                                    name.as_deref().unwrap_or_default(),
8430                                    raw,
8431                                    &mut log_base,
8432                                    &mut orientation,
8433                                    &mut maximum,
8434                                    &mut minimum,
8435                                    &mut raw_children,
8436                                    &mut boundary,
8437                                )?;
8438                            }
8439                            Event::Empty(child) => {
8440                                let name = chart_child_local(&child, &namespaces)?;
8441                                let raw = capture_empty_element(&child)?;
8442                                parse_scaling_child(
8443                                    name.as_deref().unwrap_or_default(),
8444                                    raw,
8445                                    &mut log_base,
8446                                    &mut orientation,
8447                                    &mut maximum,
8448                                    &mut minimum,
8449                                    &mut raw_children,
8450                                    &mut boundary,
8451                                )?;
8452                            }
8453                            event @ (Event::Text(_)
8454                            | Event::CData(_)
8455                            | Event::Comment(_)
8456                            | Event::PI(_)
8457                            | Event::GeneralRef(_)) => {
8458                                raw_children.push(boundary, capture_event(event)?);
8459                            }
8460                            Event::End(end)
8461                                if matches_local_name(end.name().as_ref(), b"scaling") =>
8462                            {
8463                                let (log_base, log_base_markup) = optional_markup(log_base);
8464                                let (orientation, orientation_markup) = orientation
8465                                    .map(|(value, markup)| (value, Some(markup)))
8466                                    .unwrap_or((Orientation::MinMax, None));
8467                                let (maximum, maximum_markup) = optional_markup(maximum);
8468                                let (minimum, minimum_markup) = optional_markup(minimum);
8469                                let scaling = Self {
8470                                    log_base,
8471                                    orientation,
8472                                    maximum,
8473                                    minimum,
8474                                    log_base_markup,
8475                                    orientation_markup,
8476                                    maximum_markup,
8477                                    minimum_markup,
8478                                    raw_attributes,
8479                                    raw_children: raw_children_in_schema_order(&raw_children, 4),
8480                                };
8481                                scaling.validate()?;
8482                                return Ok(scaling);
8483                            }
8484                            Event::Eof => return Err(missing_end("c:scaling")),
8485                            _ => {}
8486                        }
8487                        inner.clear();
8488                    }
8489                }
8490                Event::Empty(element)
8491                    if matches_local_name(element.name().as_ref(), b"scaling") =>
8492                {
8493                    typed_rewrite_bindings(&element, inherited)?;
8494                    return Ok(Self {
8495                        raw_attributes: capture_attributes(&element)?,
8496                        ..Self::default()
8497                    });
8498                }
8499                Event::Start(element) | Event::Empty(element) => {
8500                    return Err(ChartError::UnexpectedElement(element_name(&element)));
8501                }
8502                Event::Eof => return Err(ChartError::MissingElement("c:scaling".to_owned())),
8503                _ => {}
8504            }
8505            buffer.clear();
8506        }
8507    }
8508
8509    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8510        self.validate()?;
8511        let mut start = BytesStart::new("c:scaling");
8512        push_attributes(&mut start, &self.raw_attributes);
8513        writer
8514            .write_event(Event::Start(start))
8515            .map_err(OxmlError::from)?;
8516        emit_raw(writer, self.raw_children.at(0))?;
8517        if let Some(value) = self.log_base {
8518            write_scalar(
8519                writer,
8520                "c:logBase",
8521                &value.to_string(),
8522                self.log_base_markup.as_ref(),
8523            )?;
8524        }
8525        emit_raw(writer, self.raw_children.at(1))?;
8526        if self.orientation != Orientation::MinMax || self.orientation_markup.is_some() {
8527            write_scalar(
8528                writer,
8529                "c:orientation",
8530                self.orientation.as_str(),
8531                self.orientation_markup.as_ref(),
8532            )?;
8533        }
8534        emit_raw(writer, self.raw_children.at(2))?;
8535        if let Some(value) = self.maximum {
8536            write_scalar(
8537                writer,
8538                "c:max",
8539                &value.to_string(),
8540                self.maximum_markup.as_ref(),
8541            )?;
8542        }
8543        emit_raw(writer, self.raw_children.at(3))?;
8544        if let Some(value) = self.minimum {
8545            write_scalar(
8546                writer,
8547                "c:min",
8548                &value.to_string(),
8549                self.minimum_markup.as_ref(),
8550            )?;
8551        }
8552        emit_raw(writer, self.raw_children.at(4))?;
8553        writer
8554            .write_event(Event::End(BytesEnd::new("c:scaling")))
8555            .map_err(OxmlError::from)?;
8556        Ok(())
8557    }
8558
8559    fn validate(&self) -> Result<()> {
8560        if let Some(value) = self.log_base
8561            && (!value.is_finite() || !(2.0..=1000.0).contains(&value))
8562        {
8563            return Err(ChartError::InvalidValue {
8564                element: "c:logBase".to_owned(),
8565                value: value.to_string(),
8566            });
8567        }
8568        for (element, value) in [("c:max", self.maximum), ("c:min", self.minimum)] {
8569            if let Some(value) = value
8570                && !value.is_finite()
8571            {
8572                return Err(ChartError::InvalidValue {
8573                    element: element.to_owned(),
8574                    value: value.to_string(),
8575                });
8576            }
8577        }
8578        if let (Some(minimum), Some(maximum)) = (self.minimum, self.maximum)
8579            && minimum > maximum
8580        {
8581            return Err(ChartError::InvalidValue {
8582                element: "c:scaling".to_owned(),
8583                value: format!("minimum {minimum} exceeds maximum {maximum}"),
8584            });
8585        }
8586        Ok(())
8587    }
8588}
8589
8590#[allow(clippy::too_many_arguments)]
8591fn parse_scaling_child(
8592    name: &[u8],
8593    raw: Vec<u8>,
8594    log_base: &mut Option<(f64, ScalarMarkup)>,
8595    orientation: &mut Option<(Orientation, ScalarMarkup)>,
8596    maximum: &mut Option<(f64, ScalarMarkup)>,
8597    minimum: &mut Option<(f64, ScalarMarkup)>,
8598    raw_children: &mut OrderedRawChildren,
8599    boundary: &mut usize,
8600) -> Result<()> {
8601    match name {
8602        b"logBase" => {
8603            set_once(log_base, parse_f64_scalar(&raw, "logBase")?, "c:logBase")?;
8604            *boundary = (*boundary).max(1);
8605        }
8606        b"orientation" => {
8607            let (value, markup) = scalar_value(&raw, "orientation")?;
8608            let value = value
8609                .ok_or_else(|| invalid_attribute("orientation", "val", "<missing>".to_owned()))?;
8610            let parsed = Orientation::parse(&value)
8611                .ok_or_else(|| invalid_attribute("orientation", "val", value))?;
8612            set_once(orientation, (parsed, markup), "c:orientation")?;
8613            *boundary = (*boundary).max(2);
8614        }
8615        b"max" => {
8616            set_once(maximum, parse_f64_scalar(&raw, "max")?, "c:max")?;
8617            *boundary = (*boundary).max(3);
8618        }
8619        b"min" => {
8620            set_once(minimum, parse_f64_scalar(&raw, "min")?, "c:min")?;
8621            *boundary = (*boundary).max(4);
8622        }
8623        _ => raw_children.push(*boundary, raw),
8624    }
8625    Ok(())
8626}
8627
8628impl ChartLines {
8629    fn from_xml(xml: &[u8], local: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
8630        let mut reader = Reader::from_reader(xml);
8631        let mut buffer = Vec::new();
8632        loop {
8633            match reader
8634                .read_event_into(&mut buffer)
8635                .map_err(OxmlError::from)?
8636            {
8637                Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
8638                    let namespaces = typed_rewrite_bindings(&element, inherited)?;
8639                    let raw_attributes = capture_attributes(&element)?;
8640                    let mut sp_pr = None;
8641                    let mut raw_children = OrderedRawChildren::default();
8642                    let mut boundary = 0usize;
8643                    let mut inner = Vec::new();
8644                    loop {
8645                        match reader
8646                            .read_event_into(&mut inner)
8647                            .map_err(OxmlError::from)?
8648                        {
8649                            Event::Start(child) => {
8650                                let name = chart_child_local(&child, &namespaces)?;
8651                                let raw = capture_element(&mut reader, &child)?;
8652                                if name.as_deref() == Some(b"spPr") {
8653                                    parse_chart_shape_properties(&raw, &namespaces, &mut sp_pr)?;
8654                                    boundary = 1;
8655                                } else {
8656                                    raw_children.push(boundary, raw);
8657                                }
8658                            }
8659                            Event::Empty(child) => {
8660                                let name = chart_child_local(&child, &namespaces)?;
8661                                let raw = capture_empty_element(&child)?;
8662                                if name.as_deref() == Some(b"spPr") {
8663                                    parse_chart_shape_properties(&raw, &namespaces, &mut sp_pr)?;
8664                                    boundary = 1;
8665                                } else {
8666                                    raw_children.push(boundary, raw);
8667                                }
8668                            }
8669                            event @ (Event::Text(_)
8670                            | Event::CData(_)
8671                            | Event::Comment(_)
8672                            | Event::PI(_)
8673                            | Event::GeneralRef(_)) => {
8674                                raw_children.push(boundary, capture_event(event)?);
8675                            }
8676                            Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
8677                                return Ok(Self {
8678                                    sp_pr,
8679                                    raw_attributes,
8680                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
8681                                });
8682                            }
8683                            Event::Eof => {
8684                                return Err(missing_end(&format!(
8685                                    "c:{}",
8686                                    String::from_utf8_lossy(local)
8687                                )));
8688                            }
8689                            _ => {}
8690                        }
8691                        inner.clear();
8692                    }
8693                }
8694                Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
8695                    typed_rewrite_bindings(&element, inherited)?;
8696                    return Ok(Self {
8697                        sp_pr: None,
8698                        raw_attributes: capture_attributes(&element)?,
8699                        raw_children: OrderedRawChildren::default(),
8700                    });
8701                }
8702                Event::Start(element) | Event::Empty(element) => {
8703                    return Err(ChartError::UnexpectedElement(element_name(&element)));
8704                }
8705                Event::Eof => {
8706                    return Err(ChartError::MissingElement(format!(
8707                        "c:{}",
8708                        String::from_utf8_lossy(local)
8709                    )));
8710                }
8711                _ => {}
8712            }
8713            buffer.clear();
8714        }
8715    }
8716
8717    fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
8718        let mut start = BytesStart::new(tag);
8719        push_attributes(&mut start, &self.raw_attributes);
8720        if self.sp_pr.is_none() && self.raw_children.is_empty() {
8721            writer
8722                .write_event(Event::Empty(start))
8723                .map_err(OxmlError::from)?;
8724            return Ok(());
8725        }
8726        writer
8727            .write_event(Event::Start(start))
8728            .map_err(OxmlError::from)?;
8729        emit_raw(writer, self.raw_children.at(0))?;
8730        if let Some(properties) = &self.sp_pr {
8731            properties.write_xml_as(writer, "c:spPr")?;
8732        }
8733        emit_raw(writer, self.raw_children.at(1))?;
8734        writer
8735            .write_event(Event::End(BytesEnd::new(tag)))
8736            .map_err(OxmlError::from)?;
8737        Ok(())
8738    }
8739}
8740
8741fn parse_chart_shape_properties(
8742    raw: &[u8],
8743    namespaces: &NamespaceBindings,
8744    slot: &mut Option<CT_ShapeProperties>,
8745) -> Result<()> {
8746    reject_conflicting_prefix_in_xml(raw, b"a", A_NS)?;
8747    let inherited = namespaces
8748        .iter()
8749        .map(|(prefix, uri)| {
8750            Ok((
8751                std::str::from_utf8(prefix)
8752                    .map_err(OxmlError::from)?
8753                    .to_owned(),
8754                uri.clone(),
8755            ))
8756        })
8757        .collect::<Result<Vec<_>>>()?;
8758    let properties = CT_ShapeProperties::from_xml_with_inherited_namespaces(raw, &inherited)?;
8759    let mut writer = Writer::new(Vec::new());
8760    properties.write_xml_as(&mut writer, "c:spPr")?;
8761    reject_rewritten_foreign_elements(raw, &writer.into_inner(), namespaces, b"spPr")?;
8762    set_once(slot, properties, "c:spPr")
8763}
8764
8765impl NumberFormat {
8766    pub fn new(format_code: String, source_linked: bool) -> Result<Self> {
8767        let number_format = Self {
8768            format_code,
8769            source_linked,
8770            raw_attributes: Vec::new(),
8771            raw_content: Vec::new(),
8772        };
8773        number_format.validate()?;
8774        Ok(number_format)
8775    }
8776
8777    fn from_xml(xml: &[u8]) -> Result<Self> {
8778        let mut reader = Reader::from_reader(xml);
8779        let mut buffer = Vec::new();
8780        loop {
8781            match reader
8782                .read_event_into(&mut buffer)
8783                .map_err(OxmlError::from)?
8784            {
8785                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
8786                    return Self::from_element(&element, Vec::new());
8787                }
8788                Event::Start(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
8789                    let mut raw_content = Vec::new();
8790                    let mut inner = Vec::new();
8791                    loop {
8792                        match reader
8793                            .read_event_into(&mut inner)
8794                            .map_err(OxmlError::from)?
8795                        {
8796                            Event::End(end)
8797                                if matches_local_name(end.name().as_ref(), b"numFmt") =>
8798                            {
8799                                return Self::from_element(&element, raw_content);
8800                            }
8801                            event @ (Event::Text(_)
8802                            | Event::CData(_)
8803                            | Event::Comment(_)
8804                            | Event::PI(_)
8805                            | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
8806                            Event::Eof => return Err(missing_end("c:numFmt")),
8807                            _ => return Err(ChartError::UnexpectedElement("c:numFmt".to_owned())),
8808                        }
8809                        inner.clear();
8810                    }
8811                }
8812                Event::Start(element) | Event::Empty(element) => {
8813                    return Err(ChartError::UnexpectedElement(element_name(&element)));
8814                }
8815                Event::Eof => return Err(ChartError::MissingElement("c:numFmt".to_owned())),
8816                _ => {}
8817            }
8818            buffer.clear();
8819        }
8820    }
8821
8822    fn from_element(element: &BytesStart<'_>, raw_content: Vec<Vec<u8>>) -> Result<Self> {
8823        reject_conflicting_prefix(element, b"c", C_NS)?;
8824        let format_code = attribute_value(element, b"formatCode")?
8825            .ok_or_else(|| invalid_attribute("numFmt", "formatCode", "<missing>".to_owned()))?;
8826        let source_linked = attribute_value(element, b"sourceLinked")?
8827            .ok_or_else(|| invalid_attribute("numFmt", "sourceLinked", "<missing>".to_owned()))?;
8828        let source_linked = parse_bool_lexical("numFmt", "sourceLinked", &source_linked)?;
8829        let number_format = Self {
8830            format_code,
8831            source_linked,
8832            raw_attributes: capture_attributes_excluding(
8833                element,
8834                &[b"formatCode", b"sourceLinked"],
8835            )?,
8836            raw_content,
8837        };
8838        number_format.validate()?;
8839        Ok(number_format)
8840    }
8841
8842    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8843        self.validate()?;
8844        let mut start = BytesStart::new("c:numFmt");
8845        start.push_attribute(("formatCode", self.format_code.as_str()));
8846        start.push_attribute(("sourceLinked", bool_lexical(self.source_linked)));
8847        push_attributes(&mut start, &self.raw_attributes);
8848        if self.raw_content.is_empty() {
8849            writer
8850                .write_event(Event::Empty(start))
8851                .map_err(OxmlError::from)?;
8852            return Ok(());
8853        }
8854        writer
8855            .write_event(Event::Start(start))
8856            .map_err(OxmlError::from)?;
8857        for raw in &self.raw_content {
8858            writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
8859        }
8860        writer
8861            .write_event(Event::End(BytesEnd::new("c:numFmt")))
8862            .map_err(OxmlError::from)?;
8863        Ok(())
8864    }
8865
8866    /// Projects one cached value through the supported deterministic subset.
8867    pub fn format_value(&self, value: f64) -> Result<String> {
8868        if !value.is_finite() {
8869            return Err(ChartError::InvalidValue {
8870                element: "c:numFmt value".to_owned(),
8871                value: value.to_string(),
8872            });
8873        }
8874        if self.format_code == "General" {
8875            return Ok(value.to_string());
8876        }
8877
8878        let (numeric_code, percentage) = self
8879            .format_code
8880            .strip_suffix('%')
8881            .map(|code| (code, true))
8882            .unwrap_or((&self.format_code, false));
8883        let precision = if numeric_code == "0" {
8884            Some(0)
8885        } else if let Some(decimals) = numeric_code.strip_prefix("0.")
8886            && !decimals.is_empty()
8887            && decimals.bytes().all(|byte| byte == b'0')
8888        {
8889            Some(decimals.len())
8890        } else {
8891            None
8892        };
8893        let Some(precision) = precision else {
8894            return Err(ChartError::InvalidValue {
8895                element: "c:numFmt/@formatCode projection".to_owned(),
8896                value: self.format_code.clone(),
8897            });
8898        };
8899        let projected = if percentage { value * 100.0 } else { value };
8900        if !projected.is_finite() {
8901            return Err(ChartError::InvalidValue {
8902                element: "c:numFmt value".to_owned(),
8903                value: projected.to_string(),
8904            });
8905        }
8906        let mut formatted = format!("{projected:.precision$}");
8907        if percentage {
8908            formatted.push('%');
8909        }
8910        Ok(formatted)
8911    }
8912
8913    fn validate(&self) -> Result<()> {
8914        if self.format_code.is_empty() {
8915            return Err(ChartError::InvalidValue {
8916                element: "c:numFmt/@formatCode".to_owned(),
8917                value: self.format_code.clone(),
8918            });
8919        }
8920        validate_xml_text(&self.format_code, "c:numFmt/@formatCode")
8921    }
8922}
8923
8924impl CT_DLbls {
8925    pub fn from_xml(xml: &[u8]) -> Result<Self> {
8926        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
8927    }
8928
8929    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
8930        let mut reader = Reader::from_reader(xml);
8931        let mut buffer = Vec::new();
8932        loop {
8933            match reader
8934                .read_event_into(&mut buffer)
8935                .map_err(OxmlError::from)?
8936            {
8937                Event::Start(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
8938                    chart_root_prefix(&element)?;
8939                    if !element_is_in_namespace(&element, C_NS, inherited)? {
8940                        return Err(ChartError::UnexpectedElement(element_name(&element)));
8941                    }
8942                    return Self::from_element(&mut reader, &element, inherited);
8943                }
8944                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
8945                    chart_root_prefix(&element)?;
8946                    let namespaces = chart_bindings(inherited, &element)?;
8947                    if !element_is_in_namespace(&element, C_NS, inherited)? {
8948                        return Err(ChartError::UnexpectedElement(element_name(&element)));
8949                    }
8950                    let (raw_attributes, _) = capture_fixed_root_attributes(
8951                        &element,
8952                        &["xmlns:c", "xmlns:a", "xmlns:r"],
8953                    )?;
8954                    return Ok(Self {
8955                        raw_attributes,
8956                        namespace_declarations: standalone_namespace_declarations(&namespaces)?,
8957                        ..Self::default()
8958                    });
8959                }
8960                Event::Start(element) | Event::Empty(element) => {
8961                    return Err(ChartError::UnexpectedElement(element_name(&element)));
8962                }
8963                Event::Eof => return Err(ChartError::MissingElement("c:dLbls".to_owned())),
8964                _ => {}
8965            }
8966            buffer.clear();
8967        }
8968    }
8969
8970    fn from_element(
8971        reader: &mut Reader<&[u8]>,
8972        start: &BytesStart<'_>,
8973        inherited: &NamespaceBindings,
8974    ) -> Result<Self> {
8975        reject_conflicting_prefix(start, b"a", A_NS)?;
8976        let namespaces = chart_bindings(inherited, start)?;
8977        require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
8978        let (raw_attributes, _) =
8979            capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8980        let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
8981        let mut state = DataLabelsParseState::new(namespaces);
8982        let mut buffer = Vec::new();
8983        loop {
8984            match reader
8985                .read_event_into(&mut buffer)
8986                .map_err(OxmlError::from)?
8987            {
8988                Event::Start(element) => {
8989                    let name = chart_child_local(&element, &state.namespaces)?;
8990                    let raw = capture_element(reader, &element)?;
8991                    state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
8992                }
8993                Event::Empty(element) => {
8994                    let name = chart_child_local(&element, &state.namespaces)?;
8995                    let raw = capture_empty_element(&element)?;
8996                    state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
8997                }
8998                event @ (Event::Text(_)
8999                | Event::CData(_)
9000                | Event::Comment(_)
9001                | Event::PI(_)
9002                | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
9003                Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
9004                    return state.finish(raw_attributes, namespace_declarations);
9005                }
9006                Event::Eof => return Err(missing_end("c:dLbls")),
9007                _ => {}
9008            }
9009            buffer.clear();
9010        }
9011    }
9012
9013    pub fn to_xml(&self) -> Result<Vec<u8>> {
9014        let mut writer = Writer::new(Vec::new());
9015        self.write_xml(&mut writer, true)?;
9016        Ok(writer.into_inner())
9017    }
9018
9019    fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
9020        self.validate()?;
9021        let mut start = BytesStart::new("c:dLbls");
9022        if standalone {
9023            start.push_attribute(("xmlns:c", C_NS));
9024            start.push_attribute(("xmlns:a", A_NS));
9025            start.push_attribute(("xmlns:r", R_NS));
9026        }
9027        push_attributes(&mut start, &self.namespace_declarations);
9028        push_attributes(&mut start, &self.raw_attributes);
9029        writer
9030            .write_event(Event::Start(start))
9031            .map_err(OxmlError::from)?;
9032        emit_raw(writer, self.raw_children.at(0))?;
9033        if let Some(number_format) = &self.number_format {
9034            number_format.write_xml(writer)?;
9035        }
9036        emit_raw(writer, self.raw_children.at(1))?;
9037        if let Some(position) = self.position {
9038            write_scalar(
9039                writer,
9040                "c:dLblPos",
9041                position.as_str(),
9042                self.position_markup.as_ref(),
9043            )?;
9044        }
9045        emit_raw(writer, self.raw_children.at(2))?;
9046        write_optional_bool(
9047            writer,
9048            "c:showLegendKey",
9049            self.show_legend_key,
9050            self.show_legend_key_markup.as_ref(),
9051        )?;
9052        emit_raw(writer, self.raw_children.at(3))?;
9053        write_optional_bool(
9054            writer,
9055            "c:showVal",
9056            self.show_value,
9057            self.show_value_markup.as_ref(),
9058        )?;
9059        emit_raw(writer, self.raw_children.at(4))?;
9060        write_optional_bool(
9061            writer,
9062            "c:showCatName",
9063            self.show_category_name,
9064            self.show_category_name_markup.as_ref(),
9065        )?;
9066        emit_raw(writer, self.raw_children.at(5))?;
9067        write_optional_bool(
9068            writer,
9069            "c:showSerName",
9070            self.show_series_name,
9071            self.show_series_name_markup.as_ref(),
9072        )?;
9073        emit_raw(writer, self.raw_children.at(6))?;
9074        write_optional_bool(
9075            writer,
9076            "c:showPercent",
9077            self.show_percent,
9078            self.show_percent_markup.as_ref(),
9079        )?;
9080        emit_raw(writer, self.raw_children.at(7))?;
9081        write_optional_bool(
9082            writer,
9083            "c:showBubbleSize",
9084            self.show_bubble_size,
9085            self.show_bubble_size_markup.as_ref(),
9086        )?;
9087        emit_raw(writer, self.raw_children.at(8))?;
9088        if let Some(separator) = &self.separator {
9089            write_text(
9090                writer,
9091                "c:separator",
9092                separator,
9093                self.separator_markup
9094                    .as_ref()
9095                    .unwrap_or(&TextMarkup::default()),
9096            )?;
9097        }
9098        emit_raw(writer, self.raw_children.at(9))?;
9099        writer
9100            .write_event(Event::End(BytesEnd::new("c:dLbls")))
9101            .map_err(OxmlError::from)?;
9102        Ok(())
9103    }
9104
9105    fn validate(&self) -> Result<()> {
9106        if let Some(number_format) = &self.number_format {
9107            number_format.validate()?;
9108        }
9109        if let Some(separator) = &self.separator {
9110            validate_xml_text(separator, "c:separator")?;
9111        }
9112        Ok(())
9113    }
9114
9115    pub fn raw_children(&self) -> &OrderedRawChildren {
9116        &self.raw_children
9117    }
9118}
9119
9120struct DataLabelsParseState {
9121    namespaces: NamespaceBindings,
9122    number_format: Option<NumberFormat>,
9123    position: Option<(DataLabelPosition, ScalarMarkup)>,
9124    show_legend_key: Option<(bool, ScalarMarkup)>,
9125    show_value: Option<(bool, ScalarMarkup)>,
9126    show_category_name: Option<(bool, ScalarMarkup)>,
9127    show_series_name: Option<(bool, ScalarMarkup)>,
9128    show_percent: Option<(bool, ScalarMarkup)>,
9129    show_bubble_size: Option<(bool, ScalarMarkup)>,
9130    separator: Option<(String, TextMarkup)>,
9131    raw_children: OrderedRawChildren,
9132    boundary: usize,
9133}
9134
9135impl DataLabelsParseState {
9136    fn new(namespaces: NamespaceBindings) -> Self {
9137        Self {
9138            namespaces,
9139            number_format: None,
9140            position: None,
9141            show_legend_key: None,
9142            show_value: None,
9143            show_category_name: None,
9144            show_series_name: None,
9145            show_percent: None,
9146            show_bubble_size: None,
9147            separator: None,
9148            raw_children: OrderedRawChildren::default(),
9149            boundary: 0,
9150        }
9151    }
9152
9153    fn capture_event(&mut self, raw: Vec<u8>) {
9154        self.raw_children.push(self.boundary, raw);
9155    }
9156
9157    fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
9158        match name {
9159            b"numFmt" => {
9160                set_once(
9161                    &mut self.number_format,
9162                    NumberFormat::from_xml(&raw)?,
9163                    "c:numFmt",
9164                )?;
9165                self.boundary = self.boundary.max(1);
9166            }
9167            b"dLblPos" => {
9168                let (value, markup) = scalar_value(&raw, "dLblPos")?;
9169                let lexical = value
9170                    .ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
9171                let position = DataLabelPosition::parse(&lexical)
9172                    .ok_or_else(|| invalid_attribute("dLblPos", "val", lexical))?;
9173                set_once(&mut self.position, (position, markup), "c:dLblPos")?;
9174                self.boundary = self.boundary.max(2);
9175            }
9176            b"showLegendKey" => {
9177                set_once(
9178                    &mut self.show_legend_key,
9179                    parse_bool_value(&raw, "showLegendKey")?,
9180                    "c:showLegendKey",
9181                )?;
9182                self.boundary = self.boundary.max(3);
9183            }
9184            b"showVal" => {
9185                set_once(
9186                    &mut self.show_value,
9187                    parse_bool_value(&raw, "showVal")?,
9188                    "c:showVal",
9189                )?;
9190                self.boundary = self.boundary.max(4);
9191            }
9192            b"showCatName" => {
9193                set_once(
9194                    &mut self.show_category_name,
9195                    parse_bool_value(&raw, "showCatName")?,
9196                    "c:showCatName",
9197                )?;
9198                self.boundary = self.boundary.max(5);
9199            }
9200            b"showSerName" => {
9201                set_once(
9202                    &mut self.show_series_name,
9203                    parse_bool_value(&raw, "showSerName")?,
9204                    "c:showSerName",
9205                )?;
9206                self.boundary = self.boundary.max(6);
9207            }
9208            b"showPercent" => {
9209                set_once(
9210                    &mut self.show_percent,
9211                    parse_bool_value(&raw, "showPercent")?,
9212                    "c:showPercent",
9213                )?;
9214                self.boundary = self.boundary.max(7);
9215            }
9216            b"showBubbleSize" => {
9217                set_once(
9218                    &mut self.show_bubble_size,
9219                    parse_bool_value(&raw, "showBubbleSize")?,
9220                    "c:showBubbleSize",
9221                )?;
9222                self.boundary = self.boundary.max(8);
9223            }
9224            b"separator" => {
9225                set_once(
9226                    &mut self.separator,
9227                    parse_text_element(&raw, b"separator", &self.namespaces)?,
9228                    "c:separator",
9229                )?;
9230                self.boundary = self.boundary.max(9);
9231            }
9232            _ => {
9233                let boundary = data_labels_raw_boundary(name, self.boundary);
9234                self.raw_children.push(boundary, raw);
9235                self.boundary = self.boundary.max(boundary);
9236            }
9237        }
9238        Ok(())
9239    }
9240
9241    fn finish(
9242        self,
9243        raw_attributes: XmlAttributes,
9244        namespace_declarations: XmlAttributes,
9245    ) -> Result<CT_DLbls> {
9246        let (position, position_markup) = optional_markup(self.position);
9247        let (show_legend_key, show_legend_key_markup) = optional_bool_markup(self.show_legend_key);
9248        let (show_value, show_value_markup) = optional_bool_markup(self.show_value);
9249        let (show_category_name, show_category_name_markup) =
9250            optional_bool_markup(self.show_category_name);
9251        let (show_series_name, show_series_name_markup) =
9252            optional_bool_markup(self.show_series_name);
9253        let (show_percent, show_percent_markup) = optional_bool_markup(self.show_percent);
9254        let (show_bubble_size, show_bubble_size_markup) =
9255            optional_bool_markup(self.show_bubble_size);
9256        let (separator, separator_markup) = self
9257            .separator
9258            .map(|(value, markup)| (Some(value), Some(markup)))
9259            .unwrap_or((None, None));
9260        let labels = CT_DLbls {
9261            number_format: self.number_format,
9262            position,
9263            separator,
9264            show_legend_key,
9265            show_value,
9266            show_category_name,
9267            show_series_name,
9268            show_percent,
9269            show_bubble_size,
9270            position_markup,
9271            show_legend_key_markup,
9272            show_value_markup,
9273            show_category_name_markup,
9274            show_series_name_markup,
9275            show_percent_markup,
9276            show_bubble_size_markup,
9277            separator_markup,
9278            raw_attributes,
9279            namespace_declarations,
9280            raw_children: raw_children_in_schema_order(&self.raw_children, 9),
9281        };
9282        labels.validate()?;
9283        Ok(labels)
9284    }
9285}
9286
9287fn data_labels_raw_boundary(name: &[u8], current: usize) -> usize {
9288    match name {
9289        b"dLbl" | b"delete" => 0,
9290        b"spPr" | b"txPr" => 1,
9291        b"showLeaderLines" | b"leaderLines" | b"extLst" => 9,
9292        _ => current,
9293    }
9294}
9295
9296fn optional_bool_markup(value: Option<(bool, ScalarMarkup)>) -> (bool, Option<ScalarMarkup>) {
9297    value
9298        .map(|(value, markup)| (value, Some(markup)))
9299        .unwrap_or((false, None))
9300}
9301
9302fn write_optional_bool(
9303    writer: &mut Writer<Vec<u8>>,
9304    tag: &str,
9305    value: bool,
9306    markup: Option<&ScalarMarkup>,
9307) -> Result<()> {
9308    if value || markup.is_some() {
9309        write_scalar(writer, tag, bool_lexical(value), markup)?;
9310    }
9311    Ok(())
9312}
9313
9314fn default_axis_id_markup(id: AxisId) -> AxisIdMarkup {
9315    AxisIdMarkup {
9316        scalar: ScalarMarkup::default(),
9317        parsed: id,
9318        lexical: id.value().to_string(),
9319    }
9320}
9321
9322fn parse_axis_id_scalar(xml: &[u8], local: &str) -> Result<(AxisId, AxisIdMarkup)> {
9323    let (value, scalar) = scalar_value(xml, local)?;
9324    let lexical = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
9325    let parsed = lexical
9326        .parse::<i64>()
9327        .map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
9328    let id = AxisId::new(parsed).map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
9329    Ok((
9330        id,
9331        AxisIdMarkup {
9332            scalar,
9333            parsed: id,
9334            lexical,
9335        },
9336    ))
9337}
9338
9339fn write_axis_id<W: Write>(
9340    writer: &mut Writer<W>,
9341    tag: &str,
9342    id: AxisId,
9343    markup: &AxisIdMarkup,
9344) -> Result<()> {
9345    AxisId::new(id.value())?;
9346    let normalized;
9347    let value = if id == markup.parsed {
9348        markup.lexical.as_str()
9349    } else {
9350        normalized = id.value().to_string();
9351        normalized.as_str()
9352    };
9353    write_scalar(writer, tag, value, Some(&markup.scalar))
9354}
9355
9356fn parse_f64_scalar(xml: &[u8], local: &str) -> Result<(f64, ScalarMarkup)> {
9357    let (value, markup) = scalar_value(xml, local)?;
9358    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
9359    let parsed = value
9360        .parse::<f64>()
9361        .map_err(|_| invalid_attribute(local, "val", value.clone()))?;
9362    if !parsed.is_finite() {
9363        return Err(invalid_attribute(local, "val", value));
9364    }
9365    Ok((parsed, markup))
9366}
9367
9368fn parse_axis_position(xml: &[u8]) -> Result<(AxisPosition, ScalarMarkup)> {
9369    let (value, markup) = scalar_value(xml, "axPos")?;
9370    let value = value.ok_or_else(|| invalid_attribute("axPos", "val", "<missing>".to_owned()))?;
9371    let parsed =
9372        AxisPosition::parse(&value).ok_or_else(|| invalid_attribute("axPos", "val", value))?;
9373    Ok((parsed, markup))
9374}
9375
9376fn parse_tick_mark(xml: &[u8], local: &str) -> Result<(TickMark, ScalarMarkup)> {
9377    let (value, markup) = scalar_value(xml, local)?;
9378    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
9379    let parsed = TickMark::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
9380    Ok((parsed, markup))
9381}
9382
9383fn parse_tick_label_position(xml: &[u8]) -> Result<(TickLabelPosition, ScalarMarkup)> {
9384    let (value, markup) = scalar_value(xml, "tickLblPos")?;
9385    let value =
9386        value.ok_or_else(|| invalid_attribute("tickLblPos", "val", "<missing>".to_owned()))?;
9387    let parsed = TickLabelPosition::parse(&value)
9388        .ok_or_else(|| invalid_attribute("tickLblPos", "val", value))?;
9389    Ok((parsed, markup))
9390}
9391
9392fn optional_markup<T>(value: Option<(T, ScalarMarkup)>) -> (Option<T>, Option<ScalarMarkup>) {
9393    value
9394        .map(|(value, markup)| (Some(value), Some(markup)))
9395        .unwrap_or((None, None))
9396}
9397
9398fn optional_scalar_markup_eq(left: &Option<ScalarMarkup>, right: &Option<ScalarMarkup>) -> bool {
9399    let empty = ScalarMarkup::default();
9400    left.as_ref().unwrap_or(&empty) == right.as_ref().unwrap_or(&empty)
9401}
9402
9403fn axis_id_markup_eq(left: &AxisIdMarkup, right: &AxisIdMarkup) -> bool {
9404    left.scalar == right.scalar
9405}
9406
9407const fn bool_lexical(value: bool) -> &'static str {
9408    if value { "1" } else { "0" }
9409}
9410
9411fn parse_bool_lexical(element: &str, attribute: &str, value: &str) -> Result<bool> {
9412    match value {
9413        "1" | "true" => Ok(true),
9414        "0" | "false" => Ok(false),
9415        _ => Err(invalid_attribute(element, attribute, value.to_owned())),
9416    }
9417}
9418
9419impl CT_Title {
9420    fn plain_text(text: &str) -> Self {
9421        let mut body = CT_TextBody::new();
9422        body.set_text(text);
9423        let mut raw_children = OrderedRawChildren::default();
9424        raw_children.push(1, br#"<c:layout/><c:overlay val="0"/>"#.to_vec());
9425        Self {
9426            text: Some(body),
9427            raw_children,
9428            ..Self::default()
9429        }
9430    }
9431
9432    fn from_xml(xml: &[u8]) -> Result<Self> {
9433        let (raw_attributes, raw_children) = parse_raw_shell(xml, b"title", "c:title")?;
9434        Ok(Self {
9435            text: None,
9436            raw_attributes,
9437            raw_children,
9438        })
9439    }
9440
9441    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
9442        if self.text.is_none() {
9443            return write_raw_shell(writer, "c:title", &self.raw_attributes, &self.raw_children);
9444        }
9445        let mut start = BytesStart::new("c:title");
9446        push_attributes(&mut start, &self.raw_attributes);
9447        writer
9448            .write_event(Event::Start(start))
9449            .map_err(OxmlError::from)?;
9450        emit_raw(writer, self.raw_children.at(0))?;
9451        writer
9452            .write_event(Event::Start(BytesStart::new("c:tx")))
9453            .map_err(OxmlError::from)?;
9454        if let Some(text) = &self.text {
9455            text.write_xml_as(writer, "c:rich")?;
9456        }
9457        writer
9458            .write_event(Event::End(BytesEnd::new("c:tx")))
9459            .map_err(OxmlError::from)?;
9460        emit_raw(writer, self.raw_children.at(1))?;
9461        writer
9462            .write_event(Event::End(BytesEnd::new("c:title")))
9463            .map_err(OxmlError::from)?;
9464        Ok(())
9465    }
9466
9467    pub fn raw_children(&self) -> &OrderedRawChildren {
9468        &self.raw_children
9469    }
9470}
9471
9472impl CT_PlotArea {
9473    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
9474        let (raw_attributes, captured_children) = parse_raw_shell(xml, b"plotArea", "c:plotArea")?;
9475        let namespace_bindings = root_chart_bindings(xml, b"plotArea", inherited)?;
9476        let direct_roots: Vec<_> = captured_children
9477            .at(0)
9478            .filter_map(|raw| chart_root_local(raw, &namespace_bindings).transpose())
9479            .collect::<Result<_>>()?;
9480        let plot_roots: Vec<_> = direct_roots
9481            .iter()
9482            .filter(|local| is_plot_container(local))
9483            .cloned()
9484            .collect();
9485        let typed_local = (plot_roots.len() == 1
9486            && matches!(
9487                plot_roots[0].as_slice(),
9488                b"barChart"
9489                    | b"lineChart"
9490                    | b"pieChart"
9491                    | b"doughnutChart"
9492                    | b"areaChart"
9493                    | b"scatterChart"
9494                    | b"radarChart"
9495            ))
9496        .then(|| plot_roots[0].clone());
9497
9498        let Some(typed_local) = typed_local else {
9499            return Ok(Self {
9500                raw_attributes,
9501                raw_children: captured_children,
9502                namespace_bindings,
9503                plots: None,
9504                plot_markup: Vec::new(),
9505                axes: Vec::new(),
9506            });
9507        };
9508
9509        let mut raw_children = OrderedRawChildren::default();
9510        let mut plots = Vec::new();
9511        let mut plot_markup = Vec::new();
9512        let mut axes = Vec::new();
9513        let mut boundary = 0usize;
9514        for raw in captured_children.at(0) {
9515            let local = chart_root_local(raw, &namespace_bindings)?;
9516            if local.as_deref() == Some(typed_local.as_slice()) {
9517                let (plot, markup) = parse_plot(raw, &namespace_bindings)?;
9518                plots.push(plot);
9519                plot_markup.push(markup);
9520                boundary = 1;
9521            } else if local.as_deref().and_then(AxisKind::parse).is_some() {
9522                axes.push(Axis::from_xml_with_namespaces(raw, &namespace_bindings)?);
9523                boundary = axes.len() + 1;
9524            } else {
9525                raw_children.push(boundary, raw.to_vec());
9526            }
9527        }
9528        let plot_area = Self {
9529            raw_attributes,
9530            raw_children,
9531            namespace_bindings,
9532            plots: Some(plots),
9533            plot_markup,
9534            axes,
9535        };
9536        plot_area.validate_typed()?;
9537        Ok(plot_area)
9538    }
9539
9540    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
9541        let Some(plots) = &self.plots else {
9542            return write_raw_shell(
9543                writer,
9544                "c:plotArea",
9545                &self.raw_attributes,
9546                &self.raw_children,
9547            );
9548        };
9549        self.validate_typed()?;
9550        let mut start = BytesStart::new("c:plotArea");
9551        push_attributes(&mut start, &self.raw_attributes);
9552        writer
9553            .write_event(Event::Start(start))
9554            .map_err(OxmlError::from)?;
9555        emit_raw(writer, self.raw_children.at(0))?;
9556        for (index, plot) in plots.iter().enumerate() {
9557            write_plot(
9558                writer,
9559                plot,
9560                self.plot_markup
9561                    .get(index)
9562                    .unwrap_or(&PlotMarkup::default()),
9563            )?;
9564        }
9565        emit_raw(writer, self.raw_children.at(1))?;
9566        for (index, axis) in self.axes.iter().enumerate() {
9567            writer
9568                .get_mut()
9569                .write_all(&axis.to_xml()?)
9570                .map_err(OxmlError::from)?;
9571            emit_raw(writer, self.raw_children.at(index + 2))?;
9572        }
9573        writer
9574            .write_event(Event::End(BytesEnd::new("c:plotArea")))
9575            .map_err(OxmlError::from)?;
9576        Ok(())
9577    }
9578
9579    pub fn raw_children(&self) -> &OrderedRawChildren {
9580        &self.raw_children
9581    }
9582
9583    /// Creates a supported single-family plot area with owned axes.
9584    pub fn new(plots: Vec<Plot>, axes: Vec<Axis>) -> Result<Self> {
9585        let plot_markup = plots
9586            .iter()
9587            .map(|plot| {
9588                let mut markup = PlotMarkup::default();
9589                if matches!(plot, Plot::Line { .. }) {
9590                    markup.marker = Some(ScalarMarkup::default());
9591                    markup.smooth = Some(ScalarMarkup::default());
9592                }
9593                if matches!(plot, Plot::Doughnut { .. }) {
9594                    markup.hole_size = Some(ScalarMarkup::default());
9595                }
9596                markup
9597            })
9598            .collect();
9599        let plot_area = Self {
9600            raw_attributes: Vec::new(),
9601            raw_children: OrderedRawChildren::default(),
9602            namespace_bindings: chart_namespace_defaults(),
9603            plot_markup,
9604            plots: Some(plots),
9605            axes,
9606        };
9607        plot_area.validate_typed()?;
9608        Ok(plot_area)
9609    }
9610
9611    /// Returns the owned supported plots, or an error for an opaque choice.
9612    pub fn plots(&self) -> Result<&[Plot]> {
9613        self.plots
9614            .as_deref()
9615            .ok_or_else(|| ChartError::InvalidValue {
9616                element: "c:plotArea".to_owned(),
9617                value: "unsupported or combination plot area is opaque".to_owned(),
9618            })
9619    }
9620
9621    /// Returns mutable supported plots without exposing opaque content to edits.
9622    pub fn plots_mut(&mut self) -> Result<&mut [Plot]> {
9623        self.plots
9624            .as_deref_mut()
9625            .ok_or_else(|| ChartError::InvalidValue {
9626                element: "c:plotArea".to_owned(),
9627                value: "cannot combine typed plots with an opaque plot choice".to_owned(),
9628            })
9629    }
9630
9631    /// Parses the common series payloads nested in category-based plot shells.
9632    pub fn series(&self) -> Result<Vec<Series>> {
9633        if let Some(plots) = &self.plots {
9634            return Ok(plots
9635                .iter()
9636                .flat_map(|plot| match plot {
9637                    Plot::Bar { series, .. }
9638                    | Plot::Line { series, .. }
9639                    | Plot::Pie { series, .. }
9640                    | Plot::Doughnut { series, .. }
9641                    | Plot::Area { series, .. }
9642                    | Plot::Scatter { series, .. }
9643                    | Plot::Radar { series, .. } => series.clone(),
9644                })
9645                .collect());
9646        }
9647        let mut series = Vec::new();
9648        for raw in self.raw_children.at(0) {
9649            parse_plot_series(raw, &self.namespace_bindings, &mut series)?;
9650        }
9651        Ok(series)
9652    }
9653
9654    /// Parses and validates the direct axis children of this plot area.
9655    pub fn axes(&self) -> Result<Vec<Axis>> {
9656        if self.plots.is_some() {
9657            self.validate_typed()?;
9658            return Ok(self.axes.clone());
9659        }
9660        let mut axes = Vec::new();
9661        for raw in self.raw_children.at(0) {
9662            if let Some(axis) = parse_plot_axis(raw, &self.namespace_bindings)? {
9663                axes.push(axis);
9664            }
9665        }
9666        validate_axis_pairs(&axes)?;
9667        Ok(axes)
9668    }
9669
9670    fn validate_typed(&self) -> Result<()> {
9671        let plots = self
9672            .plots
9673            .as_ref()
9674            .ok_or_else(|| ChartError::InvalidValue {
9675                element: "c:plotArea".to_owned(),
9676                value: "opaque plot area has no typed validation view".to_owned(),
9677            })?;
9678        if plots.len() != 1 {
9679            return Err(ChartError::InvalidValue {
9680                element: "c:plotArea".to_owned(),
9681                value: "typed plot area requires exactly one plot family".to_owned(),
9682            });
9683        }
9684        validate_axis_pairs(&self.axes)?;
9685        for plot in plots {
9686            plot.validate()?;
9687            if let Some(axis_ids) = plot.axis_ids() {
9688                for id in axis_ids {
9689                    if !self.axes.iter().any(|axis| axis.id == id) {
9690                        return Err(ChartError::InvalidValue {
9691                            element: "c:axId".to_owned(),
9692                            value: format!("plot references missing axis {}", id.value()),
9693                        });
9694                    }
9695                }
9696            } else if !self.axes.is_empty() {
9697                return Err(ChartError::InvalidValue {
9698                    element: "c:plotArea".to_owned(),
9699                    value: "pie and doughnut plot areas must not own axes".to_owned(),
9700                });
9701            }
9702            if plot.axis_ids().is_some() && self.axes.len() != 2 {
9703                return Err(ChartError::InvalidValue {
9704                    element: "c:plotArea".to_owned(),
9705                    value: format!(
9706                        "axis-owned plot requires exactly 2 axes, found {}",
9707                        self.axes.len()
9708                    ),
9709                });
9710            }
9711        }
9712        Ok(())
9713    }
9714}
9715
9716impl Default for CT_PlotArea {
9717    fn default() -> Self {
9718        Self {
9719            raw_attributes: Vec::new(),
9720            raw_children: OrderedRawChildren::default(),
9721            namespace_bindings: chart_namespace_defaults(),
9722            plots: Some(Vec::new()),
9723            plot_markup: Vec::new(),
9724            axes: Vec::new(),
9725        }
9726    }
9727}
9728
9729fn chart_root_local(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Vec<u8>>> {
9730    let mut reader = Reader::from_reader(xml);
9731    let mut buffer = Vec::new();
9732    loop {
9733        match reader
9734            .read_event_into(&mut buffer)
9735            .map_err(OxmlError::from)?
9736        {
9737            Event::Start(element) | Event::Empty(element) => {
9738                return Ok(element_is_in_namespace(&element, C_NS, inherited)?
9739                    .then(|| local_name(element.name().as_ref()).to_vec()));
9740            }
9741            Event::Eof => return Ok(None),
9742            _ => {}
9743        }
9744        buffer.clear();
9745    }
9746}
9747
9748fn is_plot_container(local: &[u8]) -> bool {
9749    matches!(
9750        local,
9751        b"areaChart"
9752            | b"area3DChart"
9753            | b"barChart"
9754            | b"bar3DChart"
9755            | b"bubbleChart"
9756            | b"doughnutChart"
9757            | b"lineChart"
9758            | b"line3DChart"
9759            | b"ofPieChart"
9760            | b"pieChart"
9761            | b"pie3DChart"
9762            | b"radarChart"
9763            | b"scatterChart"
9764            | b"stockChart"
9765            | b"surfaceChart"
9766            | b"surface3DChart"
9767    )
9768}
9769
9770fn parse_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
9771    let local = chart_root_local(xml, inherited)?
9772        .ok_or_else(|| ChartError::MissingElement("c:barChart or c:lineChart".to_owned()))?;
9773    match local.as_slice() {
9774        b"barChart" => parse_bar_plot(xml, inherited),
9775        b"lineChart" => parse_line_plot(xml, inherited),
9776        b"pieChart" | b"doughnutChart" | b"areaChart" | b"scatterChart" | b"radarChart" => {
9777            parse_remaining_plot(xml, inherited, &local)
9778        }
9779        _ => Err(ChartError::UnexpectedElement(format!(
9780            "c:{}",
9781            String::from_utf8_lossy(&local)
9782        ))),
9783    }
9784}
9785
9786fn parse_remaining_plot(
9787    xml: &[u8],
9788    inherited: &NamespaceBindings,
9789    local: &[u8],
9790) -> Result<(Plot, PlotMarkup)> {
9791    let root = String::from_utf8_lossy(local).into_owned();
9792    let mut reader = Reader::from_reader(xml);
9793    let mut buffer = Vec::new();
9794    loop {
9795        match reader
9796            .read_event_into(&mut buffer)
9797            .map_err(OxmlError::from)?
9798        {
9799            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
9800                let namespaces = typed_rewrite_bindings(&element, inherited)?;
9801                let (mut raw_attributes, namespace_declarations) =
9802                    capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
9803                raw_attributes.splice(0..0, namespace_declarations);
9804                let mut grouping = None;
9805                let mut style = None;
9806                let mut first_slice_angle = None;
9807                let mut hole_size = None;
9808                let mut series = Vec::new();
9809                let mut data_labels = None;
9810                let mut axis_ids = Vec::new();
9811                let mut raw_children = OrderedRawChildren::default();
9812                let mut boundary = 0usize;
9813                let mut inner = Vec::new();
9814                loop {
9815                    match reader
9816                        .read_event_into(&mut inner)
9817                        .map_err(OxmlError::from)?
9818                    {
9819                        Event::Start(child) => {
9820                            let name = chart_child_local(&child, &namespaces)?;
9821                            let raw = capture_element(&mut reader, &child)?;
9822                            parse_remaining_plot_child(
9823                                local,
9824                                name.as_deref().unwrap_or_default(),
9825                                raw,
9826                                &namespaces,
9827                                &mut grouping,
9828                                &mut style,
9829                                &mut first_slice_angle,
9830                                &mut hole_size,
9831                                &mut series,
9832                                &mut data_labels,
9833                                &mut axis_ids,
9834                                &mut raw_children,
9835                                &mut boundary,
9836                            )?;
9837                        }
9838                        Event::Empty(child) => {
9839                            let name = chart_child_local(&child, &namespaces)?;
9840                            let raw = capture_empty_element(&child)?;
9841                            parse_remaining_plot_child(
9842                                local,
9843                                name.as_deref().unwrap_or_default(),
9844                                raw,
9845                                &namespaces,
9846                                &mut grouping,
9847                                &mut style,
9848                                &mut first_slice_angle,
9849                                &mut hole_size,
9850                                &mut series,
9851                                &mut data_labels,
9852                                &mut axis_ids,
9853                                &mut raw_children,
9854                                &mut boundary,
9855                            )?;
9856                        }
9857                        event @ (Event::Text(_)
9858                        | Event::CData(_)
9859                        | Event::Comment(_)
9860                        | Event::PI(_)
9861                        | Event::GeneralRef(_)) => {
9862                            raw_children.push(boundary, capture_event(event)?);
9863                        }
9864                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
9865                            let needs_axes =
9866                                matches!(local, b"areaChart" | b"scatterChart" | b"radarChart");
9867                            if needs_axes && axis_ids.len() != 2 {
9868                                return Err(ChartError::InvalidValue {
9869                                    element: format!("c:{root}/c:axId"),
9870                                    value: format!("expected 2, found {}", axis_ids.len()),
9871                                });
9872                            }
9873                            if !needs_axes && !axis_ids.is_empty() {
9874                                return Err(ChartError::InvalidValue {
9875                                    element: format!("c:{root}/c:axId"),
9876                                    value: "axis-free plot contains axis references".to_owned(),
9877                                });
9878                            }
9879                            let parsed_ids = needs_axes.then(|| [axis_ids[0].0, axis_ids[1].0]);
9880                            let original_series_keys = series
9881                                .iter()
9882                                .map(|item| (item.index, item.order))
9883                                .collect::<Vec<_>>();
9884                            let plot = match local {
9885                                b"pieChart" => Plot::Pie {
9886                                    first_slice_angle: first_slice_angle
9887                                        .as_ref()
9888                                        .map_or(0, |item| item.0),
9889                                    series,
9890                                    data_labels,
9891                                },
9892                                b"doughnutChart" => Plot::Doughnut {
9893                                    first_slice_angle: first_slice_angle
9894                                        .as_ref()
9895                                        .map_or(0, |item| item.0),
9896                                    hole_size: hole_size.as_ref().map_or(50, |item| item.0),
9897                                    series,
9898                                    data_labels,
9899                                },
9900                                b"areaChart" => Plot::Area {
9901                                    grouping: grouping
9902                                        .as_ref()
9903                                        .ok_or_else(|| {
9904                                            ChartError::MissingElement("c:grouping".to_owned())
9905                                        })?
9906                                        .0,
9907                                    series,
9908                                    data_labels,
9909                                    axis_ids: parsed_ids.ok_or_else(|| {
9910                                        ChartError::MissingElement("c:areaChart/c:axId".to_owned())
9911                                    })?,
9912                                },
9913                                b"scatterChart" => {
9914                                    let lexical = style
9915                                        .as_ref()
9916                                        .ok_or_else(|| {
9917                                            ChartError::MissingElement("c:scatterStyle".to_owned())
9918                                        })?
9919                                        .0
9920                                        .as_str();
9921                                    Plot::Scatter {
9922                                        style: ScatterStyle::parse(lexical).ok_or_else(|| {
9923                                            invalid_attribute(
9924                                                "scatterStyle",
9925                                                "val",
9926                                                lexical.to_owned(),
9927                                            )
9928                                        })?,
9929                                        series,
9930                                        data_labels,
9931                                        axis_ids: parsed_ids.ok_or_else(|| {
9932                                            ChartError::MissingElement(
9933                                                "c:scatterChart/c:axId".to_owned(),
9934                                            )
9935                                        })?,
9936                                    }
9937                                }
9938                                b"radarChart" => {
9939                                    let lexical = style
9940                                        .as_ref()
9941                                        .ok_or_else(|| {
9942                                            ChartError::MissingElement("c:radarStyle".to_owned())
9943                                        })?
9944                                        .0
9945                                        .as_str();
9946                                    Plot::Radar {
9947                                        style: RadarStyle::parse(lexical).ok_or_else(|| {
9948                                            invalid_attribute(
9949                                                "radarStyle",
9950                                                "val",
9951                                                lexical.to_owned(),
9952                                            )
9953                                        })?,
9954                                        series,
9955                                        data_labels,
9956                                        axis_ids: parsed_ids.ok_or_else(|| {
9957                                            ChartError::MissingElement(
9958                                                "c:radarChart/c:axId".to_owned(),
9959                                            )
9960                                        })?,
9961                                    }
9962                                }
9963                                _ => {
9964                                    return Err(ChartError::UnexpectedElement(format!(
9965                                        "c:{}",
9966                                        String::from_utf8_lossy(local)
9967                                    )));
9968                                }
9969                            };
9970                            plot.validate()?;
9971                            let final_boundary = boundary.max(original_series_keys.len() + 6);
9972                            return Ok((
9973                                plot,
9974                                PlotMarkup {
9975                                    raw_attributes,
9976                                    raw_children: raw_children_in_schema_order(
9977                                        &raw_children,
9978                                        final_boundary,
9979                                    ),
9980                                    grouping: grouping.map(|item| item.1),
9981                                    first_slice_angle: first_slice_angle.map(|item| item.1),
9982                                    hole_size: hole_size.map(|item| item.1),
9983                                    style: style.map(|item| item.1),
9984                                    axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
9985                                    original_series_keys,
9986                                    original_axis_ids: parsed_ids
9987                                        .map_or_else(Vec::new, |ids| ids.to_vec()),
9988                                    parsed_remaining: match local {
9989                                        b"pieChart" => Some("pieChart"),
9990                                        b"doughnutChart" => Some("doughnutChart"),
9991                                        b"areaChart" => Some("areaChart"),
9992                                        b"scatterChart" => Some("scatterChart"),
9993                                        b"radarChart" => Some("radarChart"),
9994                                        _ => None,
9995                                    },
9996                                    ..PlotMarkup::default()
9997                                },
9998                            ));
9999                        }
10000                        Event::Eof => return Err(missing_end(&format!("c:{root}"))),
10001                        _ => {}
10002                    }
10003                    inner.clear();
10004                }
10005            }
10006            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
10007                return Err(ChartError::MissingElement("c:ser".to_owned()));
10008            }
10009            Event::Start(element) | Event::Empty(element) => {
10010                return Err(ChartError::UnexpectedElement(element_name(&element)));
10011            }
10012            Event::Eof => return Err(ChartError::MissingElement(format!("c:{root}"))),
10013            _ => {}
10014        }
10015        buffer.clear();
10016    }
10017}
10018
10019#[allow(clippy::too_many_arguments)]
10020fn parse_remaining_plot_child(
10021    root: &[u8],
10022    name: &[u8],
10023    raw: Vec<u8>,
10024    namespaces: &NamespaceBindings,
10025    grouping: &mut Option<(Grouping, ScalarMarkup)>,
10026    style: &mut Option<(String, ScalarMarkup)>,
10027    first_slice_angle: &mut Option<(u16, ScalarMarkup)>,
10028    hole_size: &mut Option<(u8, ScalarMarkup)>,
10029    series: &mut Vec<Series>,
10030    data_labels: &mut Option<CT_DLbls>,
10031    axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
10032    raw_children: &mut OrderedRawChildren,
10033    boundary: &mut usize,
10034) -> Result<()> {
10035    let property_first = matches!(root, b"areaChart" | b"scatterChart" | b"radarChart");
10036    let base = usize::from(property_first);
10037    match name {
10038        b"grouping" if root == b"areaChart" => {
10039            let (value, markup) = required_scalar(&raw, "grouping")?;
10040            let parsed = Grouping::parse(&value)
10041                .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
10042            set_once(grouping, (parsed, markup), "c:grouping")?;
10043            *boundary = (*boundary).max(1);
10044        }
10045        b"scatterStyle" if root == b"scatterChart" => {
10046            let (value, markup) = required_scalar(&raw, "scatterStyle")?;
10047            if ScatterStyle::parse(&value).is_none() {
10048                return Err(invalid_attribute("scatterStyle", "val", value));
10049            }
10050            set_once(style, (value, markup), "c:scatterStyle")?;
10051            *boundary = (*boundary).max(1);
10052        }
10053        b"radarStyle" if root == b"radarChart" => {
10054            let (value, markup) = required_scalar(&raw, "radarStyle")?;
10055            if RadarStyle::parse(&value).is_none() {
10056                return Err(invalid_attribute("radarStyle", "val", value));
10057            }
10058            set_once(style, (value, markup), "c:radarStyle")?;
10059            *boundary = (*boundary).max(1);
10060        }
10061        b"ser" => {
10062            series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
10063            *boundary = (*boundary).max(base + series.len());
10064        }
10065        b"dLbls" => {
10066            set_once(
10067                data_labels,
10068                CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
10069                "c:dLbls",
10070            )?;
10071            *boundary = (*boundary).max(base + series.len() + 1);
10072        }
10073        b"firstSliceAng" if matches!(root, b"pieChart" | b"doughnutChart") => {
10074            let (value, markup) = required_scalar(&raw, "firstSliceAng")?;
10075            let parsed = value
10076                .parse::<u16>()
10077                .map_err(|_| invalid_attribute("firstSliceAng", "val", value.clone()))?;
10078            if parsed > 360 {
10079                return Err(invalid_attribute("firstSliceAng", "val", value));
10080            }
10081            set_once(first_slice_angle, (parsed, markup), "c:firstSliceAng")?;
10082            *boundary = (*boundary).max(series.len() + 2);
10083        }
10084        b"holeSize" if root == b"doughnutChart" => {
10085            let (value, markup) = required_scalar(&raw, "holeSize")?;
10086            let parsed = value
10087                .parse::<u8>()
10088                .map_err(|_| invalid_attribute("holeSize", "val", value.clone()))?;
10089            if !(10..=90).contains(&parsed) {
10090                return Err(invalid_attribute("holeSize", "val", value));
10091            }
10092            set_once(hole_size, (parsed, markup), "c:holeSize")?;
10093            *boundary = (*boundary).max(series.len() + 3);
10094        }
10095        b"axId" if property_first => {
10096            if axis_ids.len() == 2 {
10097                return Err(ChartError::DuplicateElement("c:axId".to_owned()));
10098            }
10099            axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
10100            *boundary = (*boundary).max(base + series.len() + 1 + axis_ids.len());
10101        }
10102        b"axId" => {
10103            return Err(ChartError::InvalidValue {
10104                element: "c:axId".to_owned(),
10105                value: "axis-free plot contains axis reference".to_owned(),
10106            });
10107        }
10108        _ => {
10109            let raw_boundary = match (root, name) {
10110                (_, b"varyColors") => base,
10111                (b"pieChart", b"extLst") => series.len() + 2,
10112                (b"doughnutChart", b"extLst") => series.len() + 3,
10113                (b"areaChart" | b"scatterChart" | b"radarChart", b"dropLines") => {
10114                    base + series.len() + 1
10115                }
10116                (b"areaChart" | b"scatterChart" | b"radarChart", b"extLst") => {
10117                    base + series.len() + 3
10118                }
10119                _ => *boundary,
10120            };
10121            raw_children.push(raw_boundary, raw);
10122            *boundary = (*boundary).max(raw_boundary);
10123        }
10124    }
10125    Ok(())
10126}
10127
10128fn parse_bar_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
10129    let mut reader = Reader::from_reader(xml);
10130    let mut buffer = Vec::new();
10131    loop {
10132        match reader
10133            .read_event_into(&mut buffer)
10134            .map_err(OxmlError::from)?
10135        {
10136            Event::Start(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
10137                let namespaces = typed_rewrite_bindings(&element, inherited)?;
10138                let (mut raw_attributes, namespace_declarations) =
10139                    capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
10140                raw_attributes.splice(0..0, namespace_declarations);
10141                let mut direction = None;
10142                let mut grouping = None;
10143                let mut gap_width = None;
10144                let mut overlap = None;
10145                let mut series = Vec::new();
10146                let mut data_labels = None;
10147                let mut axis_ids = Vec::new();
10148                let mut raw_children = OrderedRawChildren::default();
10149                let mut boundary = 0usize;
10150                let mut inner = Vec::new();
10151                loop {
10152                    match reader
10153                        .read_event_into(&mut inner)
10154                        .map_err(OxmlError::from)?
10155                    {
10156                        Event::Start(child) => {
10157                            let name = chart_child_local(&child, &namespaces)?;
10158                            let raw = capture_element(&mut reader, &child)?;
10159                            parse_bar_child(
10160                                name.as_deref().unwrap_or_default(),
10161                                raw,
10162                                &namespaces,
10163                                &mut direction,
10164                                &mut grouping,
10165                                &mut gap_width,
10166                                &mut overlap,
10167                                &mut series,
10168                                &mut data_labels,
10169                                &mut axis_ids,
10170                                &mut raw_children,
10171                                &mut boundary,
10172                            )?;
10173                        }
10174                        Event::Empty(child) => {
10175                            let name = chart_child_local(&child, &namespaces)?;
10176                            let raw = capture_empty_element(&child)?;
10177                            parse_bar_child(
10178                                name.as_deref().unwrap_or_default(),
10179                                raw,
10180                                &namespaces,
10181                                &mut direction,
10182                                &mut grouping,
10183                                &mut gap_width,
10184                                &mut overlap,
10185                                &mut series,
10186                                &mut data_labels,
10187                                &mut axis_ids,
10188                                &mut raw_children,
10189                                &mut boundary,
10190                            )?;
10191                        }
10192                        event @ (Event::Text(_)
10193                        | Event::CData(_)
10194                        | Event::Comment(_)
10195                        | Event::PI(_)
10196                        | Event::GeneralRef(_)) => {
10197                            raw_children.push(boundary, capture_event(event)?);
10198                        }
10199                        Event::End(end) if matches_local_name(end.name().as_ref(), b"barChart") => {
10200                            let (direction, direction_markup) = direction
10201                                .ok_or_else(|| ChartError::MissingElement("c:barDir".to_owned()))?;
10202                            let (grouping, grouping_markup) = grouping.ok_or_else(|| {
10203                                ChartError::MissingElement("c:grouping".to_owned())
10204                            })?;
10205                            if axis_ids.len() != 2 {
10206                                return Err(ChartError::InvalidValue {
10207                                    element: "c:barChart/c:axId".to_owned(),
10208                                    value: format!("expected 2, found {}", axis_ids.len()),
10209                                });
10210                            }
10211                            let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
10212                            let original_series_keys: Vec<(u32, u32)> =
10213                                series.iter().map(|item| (item.index, item.order)).collect();
10214                            let plot = Plot::Bar {
10215                                direction,
10216                                grouping,
10217                                gap_width: gap_width.as_ref().map_or(150, |item| item.0),
10218                                overlap: overlap.as_ref().map_or(0, |item| item.0),
10219                                series,
10220                                data_labels,
10221                                axis_ids: parsed_ids,
10222                            };
10223                            plot.validate()?;
10224                            return Ok((
10225                                plot,
10226                                PlotMarkup {
10227                                    raw_attributes,
10228                                    raw_children: raw_children_in_schema_order(
10229                                        &raw_children,
10230                                        original_series_keys.len() + 7,
10231                                    ),
10232                                    direction: Some(direction_markup),
10233                                    grouping: Some(grouping_markup),
10234                                    gap_width: gap_width.map(|item| item.1),
10235                                    overlap: overlap.map(|item| item.1),
10236                                    marker: None,
10237                                    smooth: None,
10238                                    first_slice_angle: None,
10239                                    hole_size: None,
10240                                    style: None,
10241                                    axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
10242                                    original_series_keys,
10243                                    original_axis_ids: parsed_ids.to_vec(),
10244                                    parsed_bar: Some(true),
10245                                    parsed_remaining: None,
10246                                },
10247                            ));
10248                        }
10249                        Event::Eof => return Err(missing_end("c:barChart")),
10250                        _ => {}
10251                    }
10252                    inner.clear();
10253                }
10254            }
10255            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
10256                return Err(ChartError::MissingElement("c:barDir".to_owned()));
10257            }
10258            Event::Start(element) | Event::Empty(element) => {
10259                return Err(ChartError::UnexpectedElement(element_name(&element)));
10260            }
10261            Event::Eof => return Err(ChartError::MissingElement("c:barChart".to_owned())),
10262            _ => {}
10263        }
10264        buffer.clear();
10265    }
10266}
10267
10268#[allow(clippy::too_many_arguments)]
10269fn parse_bar_child(
10270    name: &[u8],
10271    raw: Vec<u8>,
10272    namespaces: &NamespaceBindings,
10273    direction: &mut Option<(BarDirection, ScalarMarkup)>,
10274    grouping: &mut Option<(BarGrouping, ScalarMarkup)>,
10275    gap_width: &mut Option<(u16, ScalarMarkup)>,
10276    overlap: &mut Option<(i8, ScalarMarkup)>,
10277    series: &mut Vec<Series>,
10278    data_labels: &mut Option<CT_DLbls>,
10279    axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
10280    raw_children: &mut OrderedRawChildren,
10281    boundary: &mut usize,
10282) -> Result<()> {
10283    match name {
10284        b"barDir" => {
10285            let (value, markup) = required_scalar(&raw, "barDir")?;
10286            let value = BarDirection::parse(&value)
10287                .ok_or_else(|| invalid_attribute("barDir", "val", value))?;
10288            set_once(direction, (value, markup), "c:barDir")?;
10289            *boundary = (*boundary).max(1);
10290        }
10291        b"grouping" => {
10292            let (value, markup) = required_scalar(&raw, "grouping")?;
10293            let value = BarGrouping::parse(&value)
10294                .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
10295            set_once(grouping, (value, markup), "c:grouping")?;
10296            *boundary = (*boundary).max(2);
10297        }
10298        b"ser" => {
10299            series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
10300            *boundary = (*boundary).max(series.len() + 2);
10301        }
10302        b"dLbls" => {
10303            set_once(
10304                data_labels,
10305                CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
10306                "c:dLbls",
10307            )?;
10308            *boundary = (*boundary).max(series.len() + 3);
10309        }
10310        b"gapWidth" => {
10311            let (value, markup) = required_scalar(&raw, "gapWidth")?;
10312            let parsed = value
10313                .parse::<u16>()
10314                .map_err(|_| invalid_attribute("gapWidth", "val", value.clone()))?;
10315            if parsed > 500 {
10316                return Err(invalid_attribute("gapWidth", "val", value));
10317            }
10318            set_once(gap_width, (parsed, markup), "c:gapWidth")?;
10319            *boundary = (*boundary).max(series.len() + 4);
10320        }
10321        b"overlap" => {
10322            let (value, markup) = required_scalar(&raw, "overlap")?;
10323            let parsed = value
10324                .parse::<i8>()
10325                .map_err(|_| invalid_attribute("overlap", "val", value.clone()))?;
10326            if !(-100..=100).contains(&parsed) {
10327                return Err(invalid_attribute("overlap", "val", value));
10328            }
10329            set_once(overlap, (parsed, markup), "c:overlap")?;
10330            *boundary = (*boundary).max(series.len() + 5);
10331        }
10332        b"axId" => {
10333            if axis_ids.len() == 2 {
10334                return Err(ChartError::DuplicateElement("c:axId".to_owned()));
10335            }
10336            axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
10337            *boundary = (*boundary).max(series.len() + 5 + axis_ids.len());
10338        }
10339        _ => raw_children.push(*boundary, raw),
10340    }
10341    Ok(())
10342}
10343
10344fn parse_line_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
10345    let mut reader = Reader::from_reader(xml);
10346    let mut buffer = Vec::new();
10347    loop {
10348        match reader
10349            .read_event_into(&mut buffer)
10350            .map_err(OxmlError::from)?
10351        {
10352            Event::Start(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
10353                let namespaces = typed_rewrite_bindings(&element, inherited)?;
10354                let (mut raw_attributes, namespace_declarations) =
10355                    capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
10356                raw_attributes.splice(0..0, namespace_declarations);
10357                let mut grouping = None;
10358                let mut marker = None;
10359                let mut smooth = None;
10360                let mut series = Vec::new();
10361                let mut data_labels = None;
10362                let mut axis_ids = Vec::new();
10363                let mut raw_children = OrderedRawChildren::default();
10364                let mut boundary = 0usize;
10365                let mut inner = Vec::new();
10366                loop {
10367                    match reader
10368                        .read_event_into(&mut inner)
10369                        .map_err(OxmlError::from)?
10370                    {
10371                        Event::Start(child) => {
10372                            let name = chart_child_local(&child, &namespaces)?;
10373                            let raw = capture_element(&mut reader, &child)?;
10374                            parse_line_child(
10375                                name.as_deref().unwrap_or_default(),
10376                                raw,
10377                                &namespaces,
10378                                &mut grouping,
10379                                &mut marker,
10380                                &mut smooth,
10381                                &mut series,
10382                                &mut data_labels,
10383                                &mut axis_ids,
10384                                &mut raw_children,
10385                                &mut boundary,
10386                            )?;
10387                        }
10388                        Event::Empty(child) => {
10389                            let name = chart_child_local(&child, &namespaces)?;
10390                            let raw = capture_empty_element(&child)?;
10391                            parse_line_child(
10392                                name.as_deref().unwrap_or_default(),
10393                                raw,
10394                                &namespaces,
10395                                &mut grouping,
10396                                &mut marker,
10397                                &mut smooth,
10398                                &mut series,
10399                                &mut data_labels,
10400                                &mut axis_ids,
10401                                &mut raw_children,
10402                                &mut boundary,
10403                            )?;
10404                        }
10405                        event @ (Event::Text(_)
10406                        | Event::CData(_)
10407                        | Event::Comment(_)
10408                        | Event::PI(_)
10409                        | Event::GeneralRef(_)) => {
10410                            raw_children.push(boundary, capture_event(event)?);
10411                        }
10412                        Event::End(end)
10413                            if matches_local_name(end.name().as_ref(), b"lineChart") =>
10414                        {
10415                            let (grouping, grouping_markup) = grouping.ok_or_else(|| {
10416                                ChartError::MissingElement("c:grouping".to_owned())
10417                            })?;
10418                            if axis_ids.len() != 2 {
10419                                return Err(ChartError::InvalidValue {
10420                                    element: "c:lineChart/c:axId".to_owned(),
10421                                    value: format!("expected 2, found {}", axis_ids.len()),
10422                                });
10423                            }
10424                            let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
10425                            let original_series_keys: Vec<(u32, u32)> =
10426                                series.iter().map(|item| (item.index, item.order)).collect();
10427                            let plot = Plot::Line {
10428                                grouping,
10429                                marker: marker.as_ref().is_some_and(|item| item.0),
10430                                smooth: smooth.as_ref().is_some_and(|item| item.0),
10431                                series,
10432                                data_labels,
10433                                axis_ids: parsed_ids,
10434                            };
10435                            plot.validate()?;
10436                            return Ok((
10437                                plot,
10438                                PlotMarkup {
10439                                    raw_attributes,
10440                                    raw_children: raw_children_in_schema_order(
10441                                        &raw_children,
10442                                        original_series_keys.len() + 6,
10443                                    ),
10444                                    direction: None,
10445                                    grouping: Some(grouping_markup),
10446                                    gap_width: None,
10447                                    overlap: None,
10448                                    marker: marker.map(|item| item.1),
10449                                    smooth: smooth.map(|item| item.1),
10450                                    first_slice_angle: None,
10451                                    hole_size: None,
10452                                    style: None,
10453                                    axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
10454                                    original_series_keys,
10455                                    original_axis_ids: parsed_ids.to_vec(),
10456                                    parsed_bar: Some(false),
10457                                    parsed_remaining: None,
10458                                },
10459                            ));
10460                        }
10461                        Event::Eof => return Err(missing_end("c:lineChart")),
10462                        _ => {}
10463                    }
10464                    inner.clear();
10465                }
10466            }
10467            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
10468                return Err(ChartError::MissingElement("c:grouping".to_owned()));
10469            }
10470            Event::Start(element) | Event::Empty(element) => {
10471                return Err(ChartError::UnexpectedElement(element_name(&element)));
10472            }
10473            Event::Eof => return Err(ChartError::MissingElement("c:lineChart".to_owned())),
10474            _ => {}
10475        }
10476        buffer.clear();
10477    }
10478}
10479
10480#[allow(clippy::too_many_arguments)]
10481fn parse_line_child(
10482    name: &[u8],
10483    raw: Vec<u8>,
10484    namespaces: &NamespaceBindings,
10485    grouping: &mut Option<(Grouping, ScalarMarkup)>,
10486    marker: &mut Option<(bool, ScalarMarkup)>,
10487    smooth: &mut Option<(bool, ScalarMarkup)>,
10488    series: &mut Vec<Series>,
10489    data_labels: &mut Option<CT_DLbls>,
10490    axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
10491    raw_children: &mut OrderedRawChildren,
10492    boundary: &mut usize,
10493) -> Result<()> {
10494    match name {
10495        b"grouping" => {
10496            let (value, markup) = required_scalar(&raw, "grouping")?;
10497            let value = Grouping::parse(&value)
10498                .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
10499            set_once(grouping, (value, markup), "c:grouping")?;
10500            *boundary = (*boundary).max(1);
10501        }
10502        b"ser" => {
10503            series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
10504            *boundary = (*boundary).max(series.len() + 1);
10505        }
10506        b"dLbls" => {
10507            set_once(
10508                data_labels,
10509                CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
10510                "c:dLbls",
10511            )?;
10512            *boundary = (*boundary).max(series.len() + 2);
10513        }
10514        b"marker" => {
10515            set_once(marker, parse_bool_value(&raw, "marker")?, "c:marker")?;
10516            *boundary = (*boundary).max(series.len() + 3);
10517        }
10518        b"smooth" => {
10519            set_once(smooth, parse_bool_value(&raw, "smooth")?, "c:smooth")?;
10520            *boundary = (*boundary).max(series.len() + 4);
10521        }
10522        b"axId" => {
10523            if axis_ids.len() == 2 {
10524                return Err(ChartError::DuplicateElement("c:axId".to_owned()));
10525            }
10526            axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
10527            *boundary = (*boundary).max(series.len() + 4 + axis_ids.len());
10528        }
10529        _ => raw_children.push(*boundary, raw),
10530    }
10531    Ok(())
10532}
10533
10534fn required_scalar(xml: &[u8], local: &str) -> Result<(String, ScalarMarkup)> {
10535    let (value, markup) = scalar_value(xml, local)?;
10536    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
10537    Ok((value, markup))
10538}
10539
10540fn write_plot<W: Write>(writer: &mut Writer<W>, plot: &Plot, markup: &PlotMarkup) -> Result<()> {
10541    plot.validate()?;
10542    if let Some(parsed_bar) = markup.parsed_bar
10543        && parsed_bar != matches!(plot, Plot::Bar { .. })
10544    {
10545        return Err(ChartError::InvalidValue {
10546            element: "c:plotArea".to_owned(),
10547            value: "a parsed plot family cannot be replaced while preserved payload remains"
10548                .to_owned(),
10549        });
10550    }
10551    let current_remaining = match plot {
10552        Plot::Pie { .. } => Some("pieChart"),
10553        Plot::Doughnut { .. } => Some("doughnutChart"),
10554        Plot::Area { .. } => Some("areaChart"),
10555        Plot::Scatter { .. } => Some("scatterChart"),
10556        Plot::Radar { .. } => Some("radarChart"),
10557        Plot::Bar { .. } | Plot::Line { .. } => None,
10558    };
10559    if let Some(parsed) = markup.parsed_remaining
10560        && current_remaining != Some(parsed)
10561    {
10562        return Err(ChartError::InvalidValue {
10563            element: "c:plotArea".to_owned(),
10564            value: "a parsed plot family cannot be replaced while preserved payload remains"
10565                .to_owned(),
10566        });
10567    }
10568    match plot {
10569        Plot::Bar {
10570            direction,
10571            grouping,
10572            gap_width,
10573            overlap,
10574            series,
10575            data_labels,
10576            axis_ids,
10577        } => {
10578            let mut start = BytesStart::new("c:barChart");
10579            push_attributes(&mut start, &markup.raw_attributes);
10580            writer
10581                .write_event(Event::Start(start))
10582                .map_err(OxmlError::from)?;
10583            emit_raw(writer, markup.raw_children.at(0))?;
10584            write_scalar(
10585                writer,
10586                "c:barDir",
10587                direction.as_str(),
10588                markup.direction.as_ref(),
10589            )?;
10590            emit_raw(writer, markup.raw_children.at(1))?;
10591            write_scalar(
10592                writer,
10593                "c:grouping",
10594                grouping.as_str(),
10595                markup.grouping.as_ref(),
10596            )?;
10597            let original_to_current =
10598                series_original_to_current(&markup.original_series_keys, series);
10599            emit_repeated_raw(
10600                writer,
10601                &markup.raw_children,
10602                2,
10603                &original_to_current,
10604                series.len(),
10605                0,
10606            )?;
10607            for (index, item) in series.iter().enumerate() {
10608                writer
10609                    .get_mut()
10610                    .write_all(&item.to_xml()?)
10611                    .map_err(OxmlError::from)?;
10612                emit_repeated_raw(
10613                    writer,
10614                    &markup.raw_children,
10615                    2,
10616                    &original_to_current,
10617                    series.len(),
10618                    index + 1,
10619                )?;
10620            }
10621            if let Some(labels) = data_labels {
10622                writer
10623                    .get_mut()
10624                    .write_all(&labels.to_xml()?)
10625                    .map_err(OxmlError::from)?;
10626            }
10627            let trailing = markup.original_series_keys.len();
10628            emit_raw(writer, markup.raw_children.at(trailing + 3))?;
10629            if *gap_width != 150 || markup.gap_width.is_some() {
10630                write_scalar(
10631                    writer,
10632                    "c:gapWidth",
10633                    &gap_width.to_string(),
10634                    markup.gap_width.as_ref(),
10635                )?;
10636            }
10637            emit_raw(writer, markup.raw_children.at(trailing + 4))?;
10638            if *overlap != 0 || markup.overlap.is_some() {
10639                write_scalar(
10640                    writer,
10641                    "c:overlap",
10642                    &overlap.to_string(),
10643                    markup.overlap.as_ref(),
10644                )?;
10645            }
10646            let axis_original_to_current =
10647                axis_original_to_current(&markup.original_axis_ids, axis_ids);
10648            let axis_current_to_original =
10649                invert_original_to_current(&axis_original_to_current, axis_ids.len());
10650            emit_repeated_raw(
10651                writer,
10652                &markup.raw_children,
10653                trailing + 5,
10654                &axis_original_to_current,
10655                axis_ids.len(),
10656                0,
10657            )?;
10658            for (index, id) in axis_ids.iter().enumerate() {
10659                let default_markup = default_axis_id_markup(*id);
10660                let original_markup = axis_current_to_original[index]
10661                    .and_then(|original| markup.axis_ids.get(original));
10662                write_axis_id(
10663                    writer,
10664                    "c:axId",
10665                    *id,
10666                    original_markup.unwrap_or(&default_markup),
10667                )?;
10668                emit_repeated_raw(
10669                    writer,
10670                    &markup.raw_children,
10671                    trailing + 5,
10672                    &axis_original_to_current,
10673                    axis_ids.len(),
10674                    index + 1,
10675                )?;
10676            }
10677            writer
10678                .write_event(Event::End(BytesEnd::new("c:barChart")))
10679                .map_err(OxmlError::from)?;
10680        }
10681        Plot::Line {
10682            grouping,
10683            marker,
10684            smooth,
10685            series,
10686            data_labels,
10687            axis_ids,
10688        } => {
10689            let mut start = BytesStart::new("c:lineChart");
10690            push_attributes(&mut start, &markup.raw_attributes);
10691            writer
10692                .write_event(Event::Start(start))
10693                .map_err(OxmlError::from)?;
10694            emit_raw(writer, markup.raw_children.at(0))?;
10695            write_scalar(
10696                writer,
10697                "c:grouping",
10698                grouping.as_str(),
10699                markup.grouping.as_ref(),
10700            )?;
10701            let original_to_current =
10702                series_original_to_current(&markup.original_series_keys, series);
10703            emit_repeated_raw(
10704                writer,
10705                &markup.raw_children,
10706                1,
10707                &original_to_current,
10708                series.len(),
10709                0,
10710            )?;
10711            for (index, item) in series.iter().enumerate() {
10712                writer
10713                    .get_mut()
10714                    .write_all(&item.to_xml()?)
10715                    .map_err(OxmlError::from)?;
10716                emit_repeated_raw(
10717                    writer,
10718                    &markup.raw_children,
10719                    1,
10720                    &original_to_current,
10721                    series.len(),
10722                    index + 1,
10723                )?;
10724            }
10725            if let Some(labels) = data_labels {
10726                writer
10727                    .get_mut()
10728                    .write_all(&labels.to_xml()?)
10729                    .map_err(OxmlError::from)?;
10730            }
10731            let trailing = markup.original_series_keys.len();
10732            emit_raw(writer, markup.raw_children.at(trailing + 2))?;
10733            if *marker || markup.marker.is_some() {
10734                write_scalar(
10735                    writer,
10736                    "c:marker",
10737                    bool_lexical(*marker),
10738                    markup.marker.as_ref(),
10739                )?;
10740            }
10741            emit_raw(writer, markup.raw_children.at(trailing + 3))?;
10742            if *smooth || markup.smooth.is_some() {
10743                write_scalar(
10744                    writer,
10745                    "c:smooth",
10746                    bool_lexical(*smooth),
10747                    markup.smooth.as_ref(),
10748                )?;
10749            }
10750            let axis_original_to_current =
10751                axis_original_to_current(&markup.original_axis_ids, axis_ids);
10752            let axis_current_to_original =
10753                invert_original_to_current(&axis_original_to_current, axis_ids.len());
10754            emit_repeated_raw(
10755                writer,
10756                &markup.raw_children,
10757                trailing + 4,
10758                &axis_original_to_current,
10759                axis_ids.len(),
10760                0,
10761            )?;
10762            for (index, id) in axis_ids.iter().enumerate() {
10763                let default_markup = default_axis_id_markup(*id);
10764                let original_markup = axis_current_to_original[index]
10765                    .and_then(|original| markup.axis_ids.get(original));
10766                write_axis_id(
10767                    writer,
10768                    "c:axId",
10769                    *id,
10770                    original_markup.unwrap_or(&default_markup),
10771                )?;
10772                emit_repeated_raw(
10773                    writer,
10774                    &markup.raw_children,
10775                    trailing + 4,
10776                    &axis_original_to_current,
10777                    axis_ids.len(),
10778                    index + 1,
10779                )?;
10780            }
10781            writer
10782                .write_event(Event::End(BytesEnd::new("c:lineChart")))
10783                .map_err(OxmlError::from)?;
10784        }
10785        remaining => {
10786            let (
10787                root,
10788                property,
10789                series,
10790                data_labels,
10791                axis_ids,
10792                first_slice_angle,
10793                hole_size,
10794                scatter,
10795            ) = match remaining {
10796                Plot::Pie {
10797                    first_slice_angle,
10798                    series,
10799                    data_labels,
10800                } => (
10801                    "pieChart",
10802                    None,
10803                    series,
10804                    data_labels,
10805                    None,
10806                    Some(*first_slice_angle),
10807                    None,
10808                    false,
10809                ),
10810                Plot::Doughnut {
10811                    first_slice_angle,
10812                    hole_size,
10813                    series,
10814                    data_labels,
10815                } => (
10816                    "doughnutChart",
10817                    None,
10818                    series,
10819                    data_labels,
10820                    None,
10821                    Some(*first_slice_angle),
10822                    Some(*hole_size),
10823                    false,
10824                ),
10825                Plot::Area {
10826                    grouping,
10827                    series,
10828                    data_labels,
10829                    axis_ids,
10830                } => (
10831                    "areaChart",
10832                    Some(("c:grouping", grouping.as_str(), markup.grouping.as_ref())),
10833                    series,
10834                    data_labels,
10835                    Some(axis_ids),
10836                    None,
10837                    None,
10838                    false,
10839                ),
10840                Plot::Scatter {
10841                    style,
10842                    series,
10843                    data_labels,
10844                    axis_ids,
10845                } => (
10846                    "scatterChart",
10847                    Some(("c:scatterStyle", style.as_str(), markup.style.as_ref())),
10848                    series,
10849                    data_labels,
10850                    Some(axis_ids),
10851                    None,
10852                    None,
10853                    true,
10854                ),
10855                Plot::Radar {
10856                    style,
10857                    series,
10858                    data_labels,
10859                    axis_ids,
10860                } => (
10861                    "radarChart",
10862                    Some(("c:radarStyle", style.as_str(), markup.style.as_ref())),
10863                    series,
10864                    data_labels,
10865                    Some(axis_ids),
10866                    None,
10867                    None,
10868                    false,
10869                ),
10870                Plot::Bar { .. } | Plot::Line { .. } => unreachable!("handled above"),
10871            };
10872            let tag = format!("c:{root}");
10873            let mut start = BytesStart::new(&tag);
10874            push_attributes(&mut start, &markup.raw_attributes);
10875            writer
10876                .write_event(Event::Start(start))
10877                .map_err(OxmlError::from)?;
10878            let base = usize::from(property.is_some());
10879            if let Some((name, value, scalar_markup)) = property {
10880                emit_raw(writer, markup.raw_children.at(0))?;
10881                write_scalar(writer, name, value, scalar_markup)?;
10882            }
10883            let original_to_current =
10884                series_original_to_current(&markup.original_series_keys, series);
10885            emit_repeated_raw(
10886                writer,
10887                &markup.raw_children,
10888                base,
10889                &original_to_current,
10890                series.len(),
10891                0,
10892            )?;
10893            for (index, item) in series.iter().enumerate() {
10894                writer
10895                    .get_mut()
10896                    .write_all(&item.to_xml_for_plot(scatter)?)
10897                    .map_err(OxmlError::from)?;
10898                emit_repeated_raw(
10899                    writer,
10900                    &markup.raw_children,
10901                    base,
10902                    &original_to_current,
10903                    series.len(),
10904                    index + 1,
10905                )?;
10906            }
10907            if let Some(labels) = data_labels {
10908                writer
10909                    .get_mut()
10910                    .write_all(&labels.to_xml()?)
10911                    .map_err(OxmlError::from)?;
10912            }
10913            let trailing = base + markup.original_series_keys.len();
10914            if let Some(axis_ids) = axis_ids {
10915                let axis_original_to_current =
10916                    axis_original_to_current(&markup.original_axis_ids, axis_ids);
10917                let axis_current_to_original =
10918                    invert_original_to_current(&axis_original_to_current, axis_ids.len());
10919                emit_repeated_raw(
10920                    writer,
10921                    &markup.raw_children,
10922                    trailing + 1,
10923                    &axis_original_to_current,
10924                    axis_ids.len(),
10925                    0,
10926                )?;
10927                for (index, id) in axis_ids.iter().enumerate() {
10928                    let default_markup = default_axis_id_markup(*id);
10929                    let original_markup = axis_current_to_original[index]
10930                        .and_then(|original| markup.axis_ids.get(original));
10931                    write_axis_id(
10932                        writer,
10933                        "c:axId",
10934                        *id,
10935                        original_markup.unwrap_or(&default_markup),
10936                    )?;
10937                    emit_repeated_raw(
10938                        writer,
10939                        &markup.raw_children,
10940                        trailing + 1,
10941                        &axis_original_to_current,
10942                        axis_ids.len(),
10943                        index + 1,
10944                    )?;
10945                }
10946            } else {
10947                emit_raw(writer, markup.raw_children.at(trailing + 1))?;
10948                if let Some(angle) = first_slice_angle {
10949                    if angle != 0 || markup.first_slice_angle.is_some() {
10950                        write_scalar(
10951                            writer,
10952                            "c:firstSliceAng",
10953                            &angle.to_string(),
10954                            markup.first_slice_angle.as_ref(),
10955                        )?;
10956                    }
10957                    emit_raw(writer, markup.raw_children.at(trailing + 2))?;
10958                }
10959                if let Some(size) = hole_size {
10960                    if size != 50 || markup.hole_size.is_some() {
10961                        write_scalar(
10962                            writer,
10963                            "c:holeSize",
10964                            &size.to_string(),
10965                            markup.hole_size.as_ref(),
10966                        )?;
10967                    }
10968                    emit_raw(writer, markup.raw_children.at(trailing + 3))?;
10969                }
10970            }
10971            writer
10972                .write_event(Event::End(BytesEnd::new(&tag)))
10973                .map_err(OxmlError::from)?;
10974        }
10975    }
10976    Ok(())
10977}
10978
10979fn series_original_to_current(original: &[(u32, u32)], current: &[Series]) -> Vec<Option<usize>> {
10980    let mut used = vec![false; current.len()];
10981    let mut matches = vec![None; original.len()];
10982    for original_index in 0..original.len().min(current.len()) {
10983        if original[original_index]
10984            == (current[original_index].index, current[original_index].order)
10985        {
10986            matches[original_index] = Some(original_index);
10987            used[original_index] = true;
10988        }
10989    }
10990    for (original_index, key) in original.iter().enumerate() {
10991        if matches[original_index].is_some() {
10992            continue;
10993        }
10994        if let Some(current_index) = current
10995            .iter()
10996            .enumerate()
10997            .position(|(index, item)| !used[index] && (item.index, item.order) == *key)
10998        {
10999            matches[original_index] = Some(current_index);
11000            used[current_index] = true;
11001        }
11002    }
11003    match_unidentified_by_position(&mut matches, &mut used);
11004    matches
11005}
11006
11007fn axis_original_to_current(original: &[AxisId], current: &[AxisId]) -> Vec<Option<usize>> {
11008    let mut used = vec![false; current.len()];
11009    let mut matches = vec![None; original.len()];
11010    for (original_index, id) in original.iter().enumerate() {
11011        if let Some(current_index) = current
11012            .iter()
11013            .enumerate()
11014            .position(|(index, current_id)| !used[index] && current_id == id)
11015        {
11016            matches[original_index] = Some(current_index);
11017            used[current_index] = true;
11018        }
11019    }
11020    match_unidentified_by_position(&mut matches, &mut used);
11021    matches
11022}
11023
11024fn match_unidentified_by_position(matches: &mut [Option<usize>], used: &mut [bool]) {
11025    for original_index in 0..matches.len() {
11026        if matches[original_index].is_none() && original_index < used.len() && !used[original_index]
11027        {
11028            matches[original_index] = Some(original_index);
11029            used[original_index] = true;
11030        }
11031    }
11032    for matched in matches.iter_mut().filter(|matched| matched.is_none()) {
11033        if let Some(current_index) = used.iter().position(|used| !used) {
11034            *matched = Some(current_index);
11035            used[current_index] = true;
11036        }
11037    }
11038}
11039
11040fn invert_original_to_current(
11041    original_to_current: &[Option<usize>],
11042    current_len: usize,
11043) -> Vec<Option<usize>> {
11044    let mut current_to_original = vec![None; current_len];
11045    for (original, current) in original_to_current.iter().enumerate() {
11046        if let Some(current) = current {
11047            current_to_original[*current] = Some(original);
11048        }
11049    }
11050    current_to_original
11051}
11052
11053fn emit_repeated_raw<W: Write>(
11054    writer: &mut Writer<W>,
11055    raw_children: &OrderedRawChildren,
11056    offset: usize,
11057    original_to_current: &[Option<usize>],
11058    current_len: usize,
11059    current_boundary: usize,
11060) -> Result<()> {
11061    if current_boundary == 0 {
11062        emit_raw(writer, raw_children.at(offset))?;
11063    }
11064    for original_boundary in 1..=original_to_current.len() {
11065        let effective = original_to_current
11066            .iter()
11067            .skip(original_boundary)
11068            .flatten()
11069            .copied()
11070            .next()
11071            .unwrap_or(current_len);
11072        if effective == current_boundary {
11073            emit_raw(writer, raw_children.at(offset + original_boundary))?;
11074        }
11075    }
11076    Ok(())
11077}
11078
11079fn parse_plot_axis(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Axis>> {
11080    let mut reader = Reader::from_reader(xml);
11081    let mut buffer = Vec::new();
11082    loop {
11083        match reader
11084            .read_event_into(&mut buffer)
11085            .map_err(OxmlError::from)?
11086        {
11087            Event::Start(element) | Event::Empty(element) => {
11088                if !element_is_in_namespace(&element, C_NS, inherited)? {
11089                    return Ok(None);
11090                }
11091                let name = element.name();
11092                if AxisKind::parse(local_name(name.as_ref())).is_none() {
11093                    return Ok(None);
11094                }
11095                return Axis::from_xml_with_namespaces(xml, inherited).map(Some);
11096            }
11097            Event::Eof => return Ok(None),
11098            _ => {}
11099        }
11100        buffer.clear();
11101    }
11102}
11103
11104fn validate_axis_pairs(axes: &[Axis]) -> Result<()> {
11105    for (index, axis) in axes.iter().enumerate() {
11106        if axes[..index].iter().any(|other| other.id == axis.id) {
11107            return Err(ChartError::DuplicateElement(format!(
11108                "c:axId {}",
11109                axis.id.value()
11110            )));
11111        }
11112    }
11113    for axis in axes {
11114        if axis.id == axis.cross_axis {
11115            return Err(ChartError::InvalidValue {
11116                element: "c:crossAx".to_owned(),
11117                value: format!("axis {} crosses itself", axis.id.value()),
11118            });
11119        }
11120        let crossed = axes
11121            .iter()
11122            .find(|candidate| candidate.id == axis.cross_axis)
11123            .ok_or_else(|| ChartError::InvalidValue {
11124                element: "c:crossAx".to_owned(),
11125                value: format!(
11126                    "axis {} references missing axis {}",
11127                    axis.id.value(),
11128                    axis.cross_axis.value()
11129                ),
11130            })?;
11131        if crossed.cross_axis != axis.id {
11132            return Err(ChartError::InvalidValue {
11133                element: "c:crossAx".to_owned(),
11134                value: format!(
11135                    "axis {} references {}, which references {}",
11136                    axis.id.value(),
11137                    crossed.id.value(),
11138                    crossed.cross_axis.value()
11139                ),
11140            });
11141        }
11142    }
11143    Ok(())
11144}
11145
11146fn parse_plot_series(
11147    xml: &[u8],
11148    inherited: &NamespaceBindings,
11149    series: &mut Vec<Series>,
11150) -> Result<()> {
11151    let mut reader = Reader::from_reader(xml);
11152    let mut buffer = Vec::new();
11153    loop {
11154        match reader
11155            .read_event_into(&mut buffer)
11156            .map_err(OxmlError::from)?
11157        {
11158            Event::Start(element) => {
11159                if !element_is_in_namespace(&element, C_NS, inherited)? {
11160                    return Ok(());
11161                }
11162                let local = local_name(element.name().as_ref()).to_vec();
11163                if !is_supported_series_plot(&local) {
11164                    return Ok(());
11165                }
11166                let namespaces = chart_bindings(inherited, &element)?;
11167                let mut inner = Vec::new();
11168                loop {
11169                    match reader
11170                        .read_event_into(&mut inner)
11171                        .map_err(OxmlError::from)?
11172                    {
11173                        Event::Start(child) => {
11174                            let name = chart_child_local(&child, &namespaces)?;
11175                            let raw = capture_element(&mut reader, &child)?;
11176                            if name.as_deref() == Some(b"ser") {
11177                                series.push(Series::from_xml_with_namespaces(&raw, &namespaces)?);
11178                            }
11179                        }
11180                        Event::Empty(child) => {
11181                            let name = chart_child_local(&child, &namespaces)?;
11182                            if name.as_deref() == Some(b"ser") {
11183                                return Err(ChartError::MissingElement("c:ser/c:idx".to_owned()));
11184                            }
11185                        }
11186                        Event::End(end) if matches_local_name(end.name().as_ref(), &local) => {
11187                            return Ok(());
11188                        }
11189                        Event::Eof => {
11190                            return Err(missing_end(&format!(
11191                                "c:{}",
11192                                String::from_utf8_lossy(&local)
11193                            )));
11194                        }
11195                        _ => {}
11196                    }
11197                    inner.clear();
11198                }
11199            }
11200            Event::Empty(_) | Event::Eof => return Ok(()),
11201            _ => {}
11202        }
11203        buffer.clear();
11204    }
11205}
11206
11207fn is_supported_series_plot(local: &[u8]) -> bool {
11208    matches!(
11209        local,
11210        b"areaChart"
11211            | b"area3DChart"
11212            | b"barChart"
11213            | b"bar3DChart"
11214            | b"doughnutChart"
11215            | b"lineChart"
11216            | b"line3DChart"
11217            | b"ofPieChart"
11218            | b"pieChart"
11219            | b"pie3DChart"
11220            | b"radarChart"
11221            | b"stockChart"
11222            | b"surfaceChart"
11223            | b"surface3DChart"
11224    )
11225}
11226
11227impl Default for CT_Legend {
11228    fn default() -> Self {
11229        Self {
11230            root_name: "c:legend".into(),
11231            settings: LegendSettings {
11232                visible: true,
11233                ..LegendSettings::default()
11234            },
11235            raw_attributes: Vec::new(),
11236            raw_children: OrderedRawChildren::default(),
11237        }
11238    }
11239}
11240
11241impl CT_Legend {
11242    fn from_xml_with_namespaces(xml: &[u8], namespaces: &NamespaceBindings) -> Result<Self> {
11243        let mut wrapper = BytesStart::new("scope");
11244        for (prefix, uri) in namespaces {
11245            let name = if prefix.is_empty() {
11246                "xmlns".to_owned()
11247            } else {
11248                format!("xmlns:{}", String::from_utf8_lossy(prefix))
11249            };
11250            wrapper.push_attribute((name.as_str(), uri.as_str()));
11251        }
11252        let mut writer = Writer::new(Vec::new());
11253        writer
11254            .write_event(Event::Start(wrapper))
11255            .map_err(OxmlError::from)?;
11256        writer.get_mut().extend_from_slice(xml);
11257        writer
11258            .write_event(Event::End(BytesEnd::new("scope")))
11259            .map_err(OxmlError::from)?;
11260        let wrapped = writer.into_inner();
11261        let nodes = chart_source_nodes(&wrapped)?;
11262        let legend = chart_child_node(&nodes, 0, b"legend")?;
11263        let settings = legend_settings_at(&wrapped, &nodes, legend)?;
11264        let (raw_attributes, raw_children) = parse_raw_shell(xml, b"legend", "c:legend")?;
11265        let mut reader = Reader::from_reader(xml);
11266        let root_name = loop {
11267            match reader.read_event().map_err(OxmlError::from)? {
11268                Event::Start(e) | Event::Empty(e) => break element_name(&e),
11269                Event::Eof => return Err(missing_end("legend")),
11270                _ => {}
11271            }
11272        };
11273        // Preserve a producer root alias only when a foreign c binding must
11274        // remain in scope for opaque descendants. Otherwise retain fixed c output.
11275        let root_name = if raw_attributes
11276            .iter()
11277            .any(|(name, uri)| name == "xmlns:c" && uri != C_NS)
11278        {
11279            root_name
11280        } else {
11281            "c:legend".to_owned()
11282        };
11283        Ok(Self {
11284            root_name,
11285            settings,
11286            raw_attributes,
11287            raw_children,
11288        })
11289    }
11290
11291    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
11292        write_raw_shell(
11293            writer,
11294            &self.root_name,
11295            &self.raw_attributes,
11296            &self.raw_children,
11297        )
11298    }
11299
11300    pub fn raw_children(&self) -> &OrderedRawChildren {
11301        &self.raw_children
11302    }
11303}
11304
11305fn parse_raw_shell(
11306    xml: &[u8],
11307    local: &[u8],
11308    tag: &str,
11309) -> Result<(XmlAttributes, OrderedRawChildren)> {
11310    let mut reader = Reader::from_reader(xml);
11311    let mut buffer = Vec::new();
11312    loop {
11313        match reader
11314            .read_event_into(&mut buffer)
11315            .map_err(OxmlError::from)?
11316        {
11317            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
11318                if local != b"legend" {
11319                    reject_conflicting_prefix(&element, b"c", C_NS)?;
11320                }
11321                let raw_attributes = capture_attributes(&element)?;
11322                let mut raw_children = OrderedRawChildren::default();
11323                let mut child_buffer = Vec::new();
11324                loop {
11325                    match reader
11326                        .read_event_into(&mut child_buffer)
11327                        .map_err(OxmlError::from)?
11328                    {
11329                        Event::Start(child) => {
11330                            raw_children.push(0, capture_element(&mut reader, &child)?)
11331                        }
11332                        Event::Empty(child) => raw_children.push(0, capture_empty_element(&child)?),
11333                        event @ (Event::Text(_)
11334                        | Event::CData(_)
11335                        | Event::Comment(_)
11336                        | Event::PI(_)
11337                        | Event::GeneralRef(_)) => {
11338                            raw_children.push(0, capture_event(event)?);
11339                        }
11340                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => break,
11341                        Event::Eof => return Err(missing_end(tag)),
11342                        _ => {}
11343                    }
11344                    child_buffer.clear();
11345                }
11346                return Ok((raw_attributes, raw_children));
11347            }
11348            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
11349                if local != b"legend" {
11350                    reject_conflicting_prefix(&element, b"c", C_NS)?;
11351                }
11352                return Ok((capture_attributes(&element)?, OrderedRawChildren::default()));
11353            }
11354            Event::Start(element) | Event::Empty(element) => {
11355                return Err(ChartError::UnexpectedElement(element_name(&element)));
11356            }
11357            Event::Eof => return Err(ChartError::MissingElement(tag.to_owned())),
11358            _ => {}
11359        }
11360        buffer.clear();
11361    }
11362}
11363
11364fn write_raw_shell<W: Write>(
11365    writer: &mut Writer<W>,
11366    tag: &str,
11367    raw_attributes: &XmlAttributes,
11368    raw_children: &OrderedRawChildren,
11369) -> Result<()> {
11370    let mut start = BytesStart::new(tag);
11371    push_attributes(&mut start, raw_attributes);
11372    if raw_children.is_empty() {
11373        writer
11374            .write_event(Event::Empty(start))
11375            .map_err(OxmlError::from)?;
11376        return Ok(());
11377    }
11378    writer
11379        .write_event(Event::Start(start))
11380        .map_err(OxmlError::from)?;
11381    emit_raw(writer, raw_children.at(0))?;
11382    writer
11383        .write_event(Event::End(BytesEnd::new(tag)))
11384        .map_err(OxmlError::from)?;
11385    Ok(())
11386}
11387
11388/// The implemented behavior-bearing core of one `c:chart`.
11389#[derive(Clone, Debug, PartialEq)]
11390pub struct CT_Chart {
11391    pub title: Option<CT_Title>,
11392    pub auto_title_deleted: bool,
11393    pub plot_area: CT_PlotArea,
11394    pub legend: Option<CT_Legend>,
11395    pub plot_vis_only: bool,
11396    pub disp_blanks_as: DispBlanksAs,
11397    auto_title_deleted_markup: Option<ScalarMarkup>,
11398    plot_vis_only_markup: Option<ScalarMarkup>,
11399    disp_blanks_as_markup: Option<ScalarMarkup>,
11400    raw_attributes: Vec<(String, String)>,
11401    raw_children: OrderedRawChildren,
11402}
11403
11404impl CT_Chart {
11405    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
11406        let mut reader = Reader::from_reader(xml);
11407        let mut buffer = Vec::new();
11408        loop {
11409            match reader
11410                .read_event_into(&mut buffer)
11411                .map_err(OxmlError::from)?
11412            {
11413                Event::Start(element) if matches_local_name(element.name().as_ref(), b"chart") => {
11414                    reject_conflicting_prefix(&element, b"c", C_NS)?;
11415                    return Self::from_element(&mut reader, &element, inherited);
11416                }
11417                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"chart") => {
11418                    return Err(ChartError::MissingElement("c:plotArea".to_owned()));
11419                }
11420                Event::Start(element) | Event::Empty(element) => {
11421                    return Err(ChartError::UnexpectedElement(element_name(&element)));
11422                }
11423                Event::Eof => return Err(ChartError::MissingElement("c:chart".to_owned())),
11424                _ => {}
11425            }
11426            buffer.clear();
11427        }
11428    }
11429
11430    fn from_element(
11431        reader: &mut Reader<&[u8]>,
11432        start: &BytesStart<'_>,
11433        inherited: &NamespaceBindings,
11434    ) -> Result<Self> {
11435        let namespaces = bindings_with_local(inherited, start)?;
11436        let raw_attributes = capture_attributes(start)?;
11437        let mut state = ChartParseState::default();
11438        let mut buffer = Vec::new();
11439
11440        loop {
11441            match reader
11442                .read_event_into(&mut buffer)
11443                .map_err(OxmlError::from)?
11444            {
11445                Event::Start(element) => {
11446                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
11447                        local_name(element.name().as_ref()).to_vec()
11448                    } else {
11449                        Vec::new()
11450                    };
11451                    let raw = capture_element(reader, &element)?;
11452                    state.parse_child(&name, raw, &namespaces)?;
11453                }
11454                Event::Empty(element) => {
11455                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
11456                        local_name(element.name().as_ref()).to_vec()
11457                    } else {
11458                        Vec::new()
11459                    };
11460                    let raw = capture_empty_element(&element)?;
11461                    state.parse_child(&name, raw, &namespaces)?;
11462                }
11463                event @ (Event::Text(_)
11464                | Event::CData(_)
11465                | Event::Comment(_)
11466                | Event::PI(_)
11467                | Event::GeneralRef(_)) => {
11468                    state.capture_event(capture_event(event)?);
11469                }
11470                Event::End(element) if matches_local_name(element.name().as_ref(), b"chart") => {
11471                    break;
11472                }
11473                Event::Eof => return Err(missing_end("c:chart")),
11474                _ => {}
11475            }
11476            buffer.clear();
11477        }
11478
11479        state.finish(raw_attributes)
11480    }
11481
11482    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
11483        let mut start = BytesStart::new("c:chart");
11484        push_attributes(&mut start, &self.raw_attributes);
11485        writer
11486            .write_event(Event::Start(start))
11487            .map_err(OxmlError::from)?;
11488        emit_raw(writer, self.raw_children.at(0))?;
11489        if let Some(title) = &self.title {
11490            title.write_xml(writer)?;
11491        }
11492        emit_raw(writer, self.raw_children.at(1))?;
11493        if self.auto_title_deleted_markup.is_some() || self.auto_title_deleted {
11494            write_scalar(
11495                writer,
11496                "c:autoTitleDeleted",
11497                if self.auto_title_deleted { "1" } else { "0" },
11498                self.auto_title_deleted_markup.as_ref(),
11499            )?;
11500        }
11501        for raw_boundary in 2..8 {
11502            emit_raw(writer, self.raw_children.at(raw_boundary))?;
11503        }
11504        self.plot_area.write_xml(writer)?;
11505        emit_raw(writer, self.raw_children.at(8))?;
11506        if let Some(legend) = &self.legend {
11507            legend.write_xml(writer)?;
11508        }
11509        emit_raw(writer, self.raw_children.at(9))?;
11510        if self.plot_vis_only_markup.is_some() || !self.plot_vis_only {
11511            write_scalar(
11512                writer,
11513                "c:plotVisOnly",
11514                if self.plot_vis_only { "1" } else { "0" },
11515                self.plot_vis_only_markup.as_ref(),
11516            )?;
11517        }
11518        emit_raw(writer, self.raw_children.at(10))?;
11519        if self.disp_blanks_as_markup.is_some() || self.disp_blanks_as != DispBlanksAs::Gap {
11520            write_scalar(
11521                writer,
11522                "c:dispBlanksAs",
11523                self.disp_blanks_as.as_str(),
11524                self.disp_blanks_as_markup.as_ref(),
11525            )?;
11526        }
11527        for raw_boundary in 11..=13 {
11528            emit_raw(writer, self.raw_children.at(raw_boundary))?;
11529        }
11530        writer
11531            .write_event(Event::End(BytesEnd::new("c:chart")))
11532            .map_err(OxmlError::from)?;
11533        Ok(())
11534    }
11535
11536    pub fn raw_children(&self) -> &OrderedRawChildren {
11537        &self.raw_children
11538    }
11539}
11540
11541#[derive(Default)]
11542struct ChartParseState {
11543    title: Option<CT_Title>,
11544    auto_title_deleted: Option<(bool, ScalarMarkup)>,
11545    plot_area: Option<CT_PlotArea>,
11546    legend: Option<CT_Legend>,
11547    plot_vis_only: Option<(bool, ScalarMarkup)>,
11548    disp_blanks_as: Option<(DispBlanksAs, ScalarMarkup)>,
11549    raw_children: OrderedRawChildren,
11550    boundary: usize,
11551}
11552
11553impl ChartParseState {
11554    fn capture_event(&mut self, raw: Vec<u8>) {
11555        self.raw_children.push(self.boundary, raw);
11556    }
11557
11558    fn parse_child(
11559        &mut self,
11560        name: &[u8],
11561        raw: Vec<u8>,
11562        namespaces: &NamespaceBindings,
11563    ) -> Result<()> {
11564        match name {
11565            b"title" => {
11566                set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
11567                self.boundary = self.boundary.max(1);
11568            }
11569            b"autoTitleDeleted" => {
11570                set_once(
11571                    &mut self.auto_title_deleted,
11572                    parse_bool_value(&raw, "autoTitleDeleted")?,
11573                    "c:autoTitleDeleted",
11574                )?;
11575                self.boundary = self.boundary.max(2);
11576            }
11577            b"plotArea" => {
11578                set_once(
11579                    &mut self.plot_area,
11580                    CT_PlotArea::from_xml_with_namespaces(&raw, namespaces)?,
11581                    "c:plotArea",
11582                )?;
11583                self.boundary = self.boundary.max(8);
11584            }
11585            b"legend" => {
11586                set_once(
11587                    &mut self.legend,
11588                    CT_Legend::from_xml_with_namespaces(&raw, namespaces)?,
11589                    "c:legend",
11590                )?;
11591                self.boundary = self.boundary.max(9);
11592            }
11593            b"plotVisOnly" => {
11594                set_once(
11595                    &mut self.plot_vis_only,
11596                    parse_bool_value(&raw, "plotVisOnly")?,
11597                    "c:plotVisOnly",
11598                )?;
11599                self.boundary = self.boundary.max(10);
11600            }
11601            b"dispBlanksAs" => {
11602                set_once(
11603                    &mut self.disp_blanks_as,
11604                    parse_enum_value(&raw, "dispBlanksAs")?,
11605                    "c:dispBlanksAs",
11606                )?;
11607                self.boundary = self.boundary.max(11);
11608            }
11609            _ => {
11610                self.raw_children
11611                    .push(chart_raw_slot(name).unwrap_or(self.boundary), raw);
11612                self.boundary = self.boundary.max(chart_raw_boundary_after(name));
11613            }
11614        }
11615        Ok(())
11616    }
11617
11618    fn finish(self, raw_attributes: XmlAttributes) -> Result<CT_Chart> {
11619        let (auto_title_deleted, auto_title_deleted_markup) = self
11620            .auto_title_deleted
11621            .map(|(value, markup)| (value, Some(markup)))
11622            .unwrap_or((false, None));
11623        let (plot_vis_only, plot_vis_only_markup) = self
11624            .plot_vis_only
11625            .map(|(value, markup)| (value, Some(markup)))
11626            .unwrap_or((true, None));
11627        let (disp_blanks_as, disp_blanks_as_markup) = self
11628            .disp_blanks_as
11629            .map(|(value, markup)| (value, Some(markup)))
11630            .unwrap_or((DispBlanksAs::Gap, None));
11631        Ok(CT_Chart {
11632            title: self.title,
11633            auto_title_deleted,
11634            plot_area: self
11635                .plot_area
11636                .ok_or_else(|| ChartError::MissingElement("c:plotArea".to_owned()))?,
11637            legend: self.legend,
11638            plot_vis_only,
11639            disp_blanks_as,
11640            auto_title_deleted_markup,
11641            plot_vis_only_markup,
11642            disp_blanks_as_markup,
11643            raw_attributes,
11644            raw_children: raw_children_in_schema_order(&self.raw_children, 13),
11645        })
11646    }
11647}
11648
11649fn chart_raw_slot(name: &[u8]) -> Option<usize> {
11650    match name {
11651        b"pivotFmts" => Some(2),
11652        b"view3D" => Some(3),
11653        b"floor" => Some(4),
11654        b"sideWall" => Some(5),
11655        b"backWall" => Some(6),
11656        // Both unmodelled tail children share a slot so producer sibling order
11657        // remains byte-stable until a later story types either element.
11658        b"showDLblsOverMax" | b"extLst" => Some(11),
11659        _ => None,
11660    }
11661}
11662
11663fn chart_raw_boundary_after(name: &[u8]) -> usize {
11664    match name {
11665        b"title" => 1,
11666        b"autoTitleDeleted" => 2,
11667        b"pivotFmts" => 3,
11668        b"view3D" => 4,
11669        b"floor" => 5,
11670        b"sideWall" => 6,
11671        b"backWall" => 7,
11672        b"plotArea" => 8,
11673        b"legend" => 9,
11674        b"plotVisOnly" => 10,
11675        b"dispBlanksAs" => 11,
11676        b"showDLblsOverMax" | b"extLst" => 11,
11677        _ => 0,
11678    }
11679}
11680
11681/// One complete `c:chartSpace` part with the F-118 core typed.
11682#[derive(Clone, Debug, PartialEq)]
11683pub struct CT_ChartSpace {
11684    pub chart: CT_Chart,
11685    pub sp_pr: Option<CT_ShapeProperties>,
11686    pub tx_pr: Option<CT_TextBody>,
11687    raw_attributes: Vec<(String, String)>,
11688    namespace_declarations: Vec<(String, String)>,
11689    raw_children: OrderedRawChildren,
11690}
11691
11692impl CT_ChartSpace {
11693    /// Parses one complete ChartML chart-space part.
11694    pub fn from_xml(xml: &[u8]) -> Result<Self> {
11695        let mut reader = Reader::from_reader(xml);
11696        let mut buffer = Vec::new();
11697        loop {
11698            match reader
11699                .read_event_into(&mut buffer)
11700                .map_err(OxmlError::from)?
11701            {
11702                Event::Start(element)
11703                    if matches_local_name(element.name().as_ref(), b"chartSpace") =>
11704                {
11705                    validate_chart_space_namespace(&element)?;
11706                    return Self::from_element(&mut reader, &element);
11707                }
11708                Event::Empty(element)
11709                    if matches_local_name(element.name().as_ref(), b"chartSpace") =>
11710                {
11711                    validate_chart_space_namespace(&element)?;
11712                    return Err(ChartError::MissingElement("c:chart".to_owned()));
11713                }
11714                Event::Start(element) | Event::Empty(element) => {
11715                    return Err(ChartError::UnexpectedElement(element_name(&element)));
11716                }
11717                Event::Eof => return Err(ChartError::MissingElement("c:chartSpace".to_owned())),
11718                _ => {}
11719            }
11720            buffer.clear();
11721        }
11722    }
11723
11724    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
11725        let namespaces = namespace_bindings(start)?;
11726        let (raw_attributes, namespace_declarations) = capture_root_attributes(start)?;
11727        let mut state = ChartSpaceParseState::default();
11728        let mut buffer = Vec::new();
11729
11730        loop {
11731            match reader
11732                .read_event_into(&mut buffer)
11733                .map_err(OxmlError::from)?
11734            {
11735                Event::Start(element) => {
11736                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
11737                        local_name(element.name().as_ref()).to_vec()
11738                    } else {
11739                        Vec::new()
11740                    };
11741                    let raw = capture_element(reader, &element)?;
11742                    state.parse_child(&name, raw, &namespaces)?;
11743                }
11744                Event::Empty(element) => {
11745                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
11746                        local_name(element.name().as_ref()).to_vec()
11747                    } else {
11748                        Vec::new()
11749                    };
11750                    let raw = capture_empty_element(&element)?;
11751                    state.parse_child(&name, raw, &namespaces)?;
11752                }
11753                event @ (Event::Text(_)
11754                | Event::CData(_)
11755                | Event::Comment(_)
11756                | Event::PI(_)
11757                | Event::GeneralRef(_)) => {
11758                    state.capture_event(capture_event(event)?);
11759                }
11760                Event::End(element)
11761                    if matches_local_name(element.name().as_ref(), b"chartSpace") =>
11762                {
11763                    break;
11764                }
11765                Event::Eof => return Err(missing_end("c:chartSpace")),
11766                _ => {}
11767            }
11768            buffer.clear();
11769        }
11770
11771        state.finish(raw_attributes, namespace_declarations)
11772    }
11773
11774    /// Writes one chart-space part using fixed OOXML prefixes and schema order.
11775    pub fn to_xml(&self) -> Result<Vec<u8>> {
11776        let mut writer = Writer::new(Vec::new());
11777        let mut start = BytesStart::new("c:chartSpace");
11778        start.push_attribute(("xmlns:c", C_NS));
11779        start.push_attribute(("xmlns:a", A_NS));
11780        start.push_attribute(("xmlns:r", R_NS));
11781        push_attributes(&mut start, &self.namespace_declarations);
11782        push_attributes(&mut start, &self.raw_attributes);
11783        writer
11784            .write_event(Event::Start(start))
11785            .map_err(OxmlError::from)?;
11786        for raw_boundary in 0..8 {
11787            emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
11788        }
11789        self.chart.write_xml(&mut writer)?;
11790        emit_raw(&mut writer, self.raw_children.at(8))?;
11791        if let Some(properties) = &self.sp_pr {
11792            properties.write_xml_as(&mut writer, "c:spPr")?;
11793        }
11794        emit_raw(&mut writer, self.raw_children.at(9))?;
11795        if let Some(text) = &self.tx_pr {
11796            text.write_xml_as(&mut writer, "c:txPr")?;
11797        }
11798        for raw_boundary in 10..=14 {
11799            emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
11800        }
11801        writer
11802            .write_event(Event::End(BytesEnd::new("c:chartSpace")))
11803            .map_err(OxmlError::from)?;
11804        Ok(writer.into_inner())
11805    }
11806
11807    pub fn raw_children(&self) -> &OrderedRawChildren {
11808        &self.raw_children
11809    }
11810}
11811
11812#[derive(Default)]
11813struct ChartSpaceParseState {
11814    chart: Option<CT_Chart>,
11815    sp_pr: Option<CT_ShapeProperties>,
11816    tx_pr: Option<CT_TextBody>,
11817    raw_children: OrderedRawChildren,
11818    boundary: usize,
11819}
11820
11821impl ChartSpaceParseState {
11822    fn capture_event(&mut self, raw: Vec<u8>) {
11823        self.raw_children.push(self.boundary, raw);
11824    }
11825
11826    fn parse_child(
11827        &mut self,
11828        name: &[u8],
11829        raw: Vec<u8>,
11830        namespaces: &NamespaceBindings,
11831    ) -> Result<()> {
11832        match name {
11833            b"chart" => {
11834                set_once(
11835                    &mut self.chart,
11836                    CT_Chart::from_xml_with_namespaces(&raw, namespaces)?,
11837                    "c:chart",
11838                )?;
11839                self.boundary = self.boundary.max(8);
11840            }
11841            b"spPr" => {
11842                parse_chart_shape_properties(&raw, namespaces, &mut self.sp_pr)?;
11843                self.boundary = self.boundary.max(9);
11844            }
11845            b"txPr" => {
11846                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
11847                let inherited = namespaces
11848                    .iter()
11849                    .map(|(prefix, uri)| {
11850                        Ok((
11851                            std::str::from_utf8(prefix)
11852                                .map_err(OxmlError::from)?
11853                                .to_owned(),
11854                            uri.clone(),
11855                        ))
11856                    })
11857                    .collect::<Result<Vec<_>>>()?;
11858                let text = CT_TextBody::from_xml_as_with_namespaces(&raw, b"txPr", &inherited)?;
11859                let mut writer = Writer::new(Vec::new());
11860                text.write_xml_as(&mut writer, "c:txPr")?;
11861                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
11862                set_once(&mut self.tx_pr, text, "c:txPr")?;
11863                self.boundary = self.boundary.max(10);
11864            }
11865            _ => {
11866                self.raw_children
11867                    .push(chart_space_raw_slot(name).unwrap_or(self.boundary), raw);
11868                self.boundary = self.boundary.max(chart_space_raw_boundary_after(name));
11869            }
11870        }
11871        Ok(())
11872    }
11873
11874    fn finish(
11875        self,
11876        raw_attributes: XmlAttributes,
11877        namespace_declarations: XmlAttributes,
11878    ) -> Result<CT_ChartSpace> {
11879        Ok(CT_ChartSpace {
11880            chart: self
11881                .chart
11882                .ok_or_else(|| ChartError::MissingElement("c:chart".to_owned()))?,
11883            sp_pr: self.sp_pr,
11884            tx_pr: self.tx_pr,
11885            raw_attributes,
11886            namespace_declarations,
11887            raw_children: raw_children_in_schema_order(&self.raw_children, 14),
11888        })
11889    }
11890}
11891
11892fn raw_children_in_schema_order(
11893    raw_children: &OrderedRawChildren,
11894    final_boundary: usize,
11895) -> OrderedRawChildren {
11896    let mut ordered = OrderedRawChildren::default();
11897    for boundary in 0..=final_boundary {
11898        for raw in raw_children.at(boundary) {
11899            ordered.push(boundary, raw.to_vec());
11900        }
11901    }
11902    ordered
11903}
11904
11905fn chart_space_raw_slot(name: &[u8]) -> Option<usize> {
11906    match name {
11907        b"date1904" => Some(0),
11908        b"lang" => Some(1),
11909        b"roundedCorners" => Some(2),
11910        b"style" => Some(3),
11911        b"clrMapOvr" => Some(4),
11912        b"pivotSource" => Some(5),
11913        b"protection" => Some(6),
11914        b"externalData" => Some(10),
11915        b"printSettings" => Some(11),
11916        b"userShapes" => Some(12),
11917        b"extLst" => Some(13),
11918        _ => None,
11919    }
11920}
11921
11922fn chart_space_raw_boundary_after(name: &[u8]) -> usize {
11923    match name {
11924        b"date1904" => 1,
11925        b"lang" => 2,
11926        b"roundedCorners" => 3,
11927        b"style" => 4,
11928        b"clrMapOvr" => 5,
11929        b"pivotSource" => 6,
11930        b"protection" => 7,
11931        b"chart" => 8,
11932        b"spPr" => 9,
11933        b"txPr" => 10,
11934        b"externalData" => 11,
11935        b"printSettings" => 12,
11936        b"userShapes" => 13,
11937        b"extLst" => 14,
11938        _ => 0,
11939    }
11940}
11941
11942fn parse_bool_value(xml: &[u8], local: &str) -> Result<(bool, ScalarMarkup)> {
11943    let (value, markup) = scalar_value(xml, local)?;
11944    let value = match value.as_deref().unwrap_or("1") {
11945        "1" | "true" => true,
11946        "0" | "false" => false,
11947        invalid => return Err(invalid_attribute(local, "val", invalid.to_owned())),
11948    };
11949    Ok((value, markup))
11950}
11951
11952fn parse_enum_value(xml: &[u8], local: &str) -> Result<(DispBlanksAs, ScalarMarkup)> {
11953    let (value, markup) = scalar_value(xml, local)?;
11954    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
11955    let parsed =
11956        DispBlanksAs::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
11957    Ok((parsed, markup))
11958}
11959
11960fn scalar_value(xml: &[u8], expected: &str) -> Result<(Option<String>, ScalarMarkup)> {
11961    let mut reader = Reader::from_reader(xml);
11962    let mut buffer = Vec::new();
11963    loop {
11964        match reader
11965            .read_event_into(&mut buffer)
11966            .map_err(OxmlError::from)?
11967        {
11968            Event::Empty(element)
11969                if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
11970            {
11971                reject_conflicting_prefix(&element, b"c", C_NS)?;
11972                return Ok((
11973                    attribute_value(&element, b"val")?,
11974                    ScalarMarkup {
11975                        raw_attributes: capture_attributes_except(&element, b"val")?,
11976                        raw_content: Vec::new(),
11977                    },
11978                ));
11979            }
11980            Event::Start(element)
11981                if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
11982            {
11983                reject_conflicting_prefix(&element, b"c", C_NS)?;
11984                let value = attribute_value(&element, b"val")?;
11985                let raw_attributes = capture_attributes_except(&element, b"val")?;
11986                let mut raw_content = Vec::new();
11987                let mut inner = Vec::new();
11988                loop {
11989                    match reader
11990                        .read_event_into(&mut inner)
11991                        .map_err(OxmlError::from)?
11992                    {
11993                        Event::End(end)
11994                            if matches_local_name(end.name().as_ref(), expected.as_bytes()) =>
11995                        {
11996                            return Ok((
11997                                value,
11998                                ScalarMarkup {
11999                                    raw_attributes,
12000                                    raw_content,
12001                                },
12002                            ));
12003                        }
12004                        event @ (Event::Text(_)
12005                        | Event::CData(_)
12006                        | Event::Comment(_)
12007                        | Event::PI(_)
12008                        | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
12009                        Event::Eof => return Err(missing_end(expected)),
12010                        _ => return Err(ChartError::UnexpectedElement(expected.to_owned())),
12011                    }
12012                    inner.clear();
12013                }
12014            }
12015            Event::Start(element) | Event::Empty(element) => {
12016                return Err(ChartError::UnexpectedElement(element_name(&element)));
12017            }
12018            Event::Eof => return Err(ChartError::MissingElement(expected.to_owned())),
12019            _ => {}
12020        }
12021        buffer.clear();
12022    }
12023}
12024
12025fn set_once<T>(slot: &mut Option<T>, value: T, name: &str) -> Result<()> {
12026    if slot.is_some() {
12027        return Err(ChartError::DuplicateElement(name.to_owned()));
12028    }
12029    *slot = Some(value);
12030    Ok(())
12031}
12032
12033fn mark_once(seen: &mut bool, name: &str) -> Result<()> {
12034    if *seen {
12035        return Err(ChartError::DuplicateElement(name.to_owned()));
12036    }
12037    *seen = true;
12038    Ok(())
12039}
12040
12041fn validate_chart_space_namespace(element: &BytesStart<'_>) -> Result<()> {
12042    let name = element.name();
12043    let qualified = name.as_ref();
12044    let declaration = match qualified.iter().position(|byte| *byte == b':') {
12045        Some(position) => {
12046            let mut declaration = b"xmlns:".to_vec();
12047            declaration.extend_from_slice(&qualified[..position]);
12048            declaration
12049        }
12050        None => b"xmlns".to_vec(),
12051    };
12052    let namespace = attribute_value(element, &declaration)?
12053        .ok_or_else(|| invalid_attribute("chartSpace", "namespace", "<missing>".to_owned()))?;
12054    if namespace != C_NS {
12055        return Err(invalid_attribute("chartSpace", "namespace", namespace));
12056    }
12057    reject_conflicting_prefix(element, b"c", C_NS)?;
12058    reject_conflicting_prefix(element, b"a", A_NS)?;
12059    reject_conflicting_prefix(element, b"r", R_NS)
12060}
12061
12062fn reject_conflicting_prefix_in_xml(xml: &[u8], prefix: &[u8], namespace: &str) -> Result<()> {
12063    let mut reader = Reader::from_reader(xml);
12064    let mut buffer = Vec::new();
12065    loop {
12066        match reader
12067            .read_event_into(&mut buffer)
12068            .map_err(OxmlError::from)?
12069        {
12070            Event::Start(element) | Event::Empty(element) => {
12071                return reject_conflicting_prefix(&element, prefix, namespace);
12072            }
12073            Event::Eof => return Err(ChartError::MissingElement("XML root".to_owned())),
12074            _ => {}
12075        }
12076        buffer.clear();
12077    }
12078}
12079
12080fn reject_rewritten_foreign_elements(
12081    original: &[u8],
12082    written: &[u8],
12083    inherited: &NamespaceBindings,
12084    root_local: &[u8],
12085) -> Result<()> {
12086    let original = foreign_element_regions(original, inherited, root_local)?;
12087    let mut written_namespaces = inherited.clone();
12088    written_namespaces.retain(|(prefix, _)| prefix != b"a");
12089    written_namespaces.push((b"a".to_vec(), A_NS.to_owned()));
12090    let written = foreign_element_regions(written, &written_namespaces, root_local)?;
12091    if original != written {
12092        return Err(invalid_attribute(
12093            &String::from_utf8_lossy(root_local),
12094            "namespace",
12095            "foreign DrawingML content would be converted to typed content".to_owned(),
12096        ));
12097    }
12098    Ok(())
12099}
12100
12101fn foreign_element_regions(
12102    xml: &[u8],
12103    inherited: &NamespaceBindings,
12104    root_local: &[u8],
12105) -> Result<Vec<Vec<u8>>> {
12106    let mut reader = Reader::from_reader(xml);
12107    let mut buffer = Vec::new();
12108    let root_namespaces = loop {
12109        match reader
12110            .read_event_into(&mut buffer)
12111            .map_err(OxmlError::from)?
12112        {
12113            Event::Start(element) if matches_local_name(element.name().as_ref(), root_local) => {
12114                break bindings_with_local(inherited, &element)?;
12115            }
12116            Event::Empty(element) if matches_local_name(element.name().as_ref(), root_local) => {
12117                return Ok(Vec::new());
12118            }
12119            Event::Start(element) | Event::Empty(element) => {
12120                return Err(ChartError::UnexpectedElement(element_name(&element)));
12121            }
12122            Event::Eof => {
12123                return Err(ChartError::MissingElement(
12124                    String::from_utf8_lossy(root_local).into_owned(),
12125                ));
12126            }
12127            _ => {}
12128        }
12129        buffer.clear();
12130    };
12131
12132    let mut namespace_stack = vec![root_namespaces];
12133    let mut foreign = Vec::new();
12134    let mut child_buffer = Vec::new();
12135    loop {
12136        match reader
12137            .read_event_into(&mut child_buffer)
12138            .map_err(OxmlError::from)?
12139        {
12140            Event::Start(element) => {
12141                let namespaces = namespace_stack.last().expect("root namespace binding");
12142                if element_is_in_namespace(&element, A_NS, namespaces)? {
12143                    namespace_stack.push(bindings_with_local(namespaces, &element)?);
12144                } else {
12145                    foreign.push(capture_element(&mut reader, &element)?);
12146                }
12147            }
12148            Event::Empty(element) => {
12149                let namespaces = namespace_stack.last().expect("root namespace binding");
12150                if !element_is_in_namespace(&element, A_NS, namespaces)? {
12151                    foreign.push(capture_empty_element(&element)?);
12152                }
12153            }
12154            Event::End(element) if matches_local_name(element.name().as_ref(), root_local) => {
12155                return Ok(foreign);
12156            }
12157            Event::End(_) => {
12158                namespace_stack.pop();
12159            }
12160            Event::Eof => {
12161                return Err(missing_end(&String::from_utf8_lossy(root_local)));
12162            }
12163            _ => {}
12164        }
12165        child_buffer.clear();
12166    }
12167}
12168
12169fn reject_conflicting_prefix(
12170    element: &BytesStart<'_>,
12171    prefix: &[u8],
12172    namespace: &str,
12173) -> Result<()> {
12174    let mut key = b"xmlns:".to_vec();
12175    key.extend_from_slice(prefix);
12176    if let Some(value) = attribute_value(element, &key)?
12177        && value != namespace
12178    {
12179        return Err(invalid_attribute(
12180            &element_name(element),
12181            &String::from_utf8_lossy(&key),
12182            value,
12183        ));
12184    }
12185    Ok(())
12186}
12187
12188fn namespace_bindings(element: &BytesStart<'_>) -> Result<NamespaceBindings> {
12189    let mut bindings = Vec::new();
12190    for attribute in element.attributes() {
12191        let attribute = attribute.map_err(OxmlError::from)?;
12192        let key = attribute.key.as_ref();
12193        let prefix = if key == b"xmlns" {
12194            Vec::new()
12195        } else if let Some(prefix) = key.strip_prefix(b"xmlns:") {
12196            prefix.to_vec()
12197        } else {
12198            continue;
12199        };
12200        let namespace = attribute
12201            .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
12202            .map_err(OxmlError::from)?
12203            .into_owned();
12204        bindings.push((prefix, namespace));
12205    }
12206    Ok(bindings)
12207}
12208
12209fn bindings_with_local(
12210    inherited: &NamespaceBindings,
12211    element: &BytesStart<'_>,
12212) -> Result<NamespaceBindings> {
12213    let mut bindings = inherited.clone();
12214    for (prefix, namespace) in namespace_bindings(element)? {
12215        upsert_namespace_binding(&mut bindings, prefix, namespace);
12216    }
12217    Ok(bindings)
12218}
12219
12220fn upsert_namespace_binding(bindings: &mut NamespaceBindings, prefix: Vec<u8>, namespace: String) {
12221    if let Some((_, current)) = bindings.iter_mut().find(|(current, _)| current == &prefix) {
12222        *current = namespace;
12223    } else {
12224        bindings.push((prefix, namespace));
12225    }
12226}
12227
12228fn element_is_in_namespace(
12229    element: &BytesStart<'_>,
12230    expected: &str,
12231    inherited: &NamespaceBindings,
12232) -> Result<bool> {
12233    let name = element.name();
12234    let qualified = name.as_ref();
12235    let prefix = qualified
12236        .iter()
12237        .position(|byte| *byte == b':')
12238        .map(|position| &qualified[..position])
12239        .unwrap_or_default();
12240    let local = namespace_bindings(element)?;
12241    let namespace = local
12242        .iter()
12243        .rev()
12244        .chain(inherited.iter().rev())
12245        .find(|(candidate, _)| candidate.as_slice() == prefix)
12246        .map(|(_, namespace)| namespace.as_str());
12247    Ok(namespace == Some(expected))
12248}
12249
12250fn attribute_value(element: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
12251    for attribute in element.attributes() {
12252        let attribute = attribute.map_err(OxmlError::from)?;
12253        if attribute.key.as_ref() == name {
12254            return Ok(Some(
12255                attribute
12256                    .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
12257                    .map_err(OxmlError::from)?
12258                    .into_owned(),
12259            ));
12260        }
12261    }
12262    Ok(None)
12263}
12264
12265fn capture_root_attributes(start: &BytesStart<'_>) -> Result<RootAttributes> {
12266    let mut attributes = Vec::new();
12267    let mut namespaces = Vec::new();
12268    for attribute in start.attributes() {
12269        let attribute = attribute.map_err(OxmlError::from)?;
12270        let name = std::str::from_utf8(attribute.key.as_ref())
12271            .map_err(OxmlError::from)?
12272            .to_owned();
12273        let value = attribute
12274            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
12275            .map_err(OxmlError::from)?
12276            .into_owned();
12277        if name == "xmlns:c" || name == "xmlns:a" || name == "xmlns:r" {
12278            continue;
12279        }
12280        if name == "xmlns" || name.starts_with("xmlns:") {
12281            namespaces.push((name, value));
12282        } else {
12283            attributes.push((name, value));
12284        }
12285    }
12286    Ok((attributes, namespaces))
12287}
12288
12289fn capture_attributes(start: &BytesStart<'_>) -> Result<XmlAttributes> {
12290    let mut attributes = Vec::new();
12291    for attribute in start.attributes() {
12292        let attribute = attribute.map_err(OxmlError::from)?;
12293        attributes.push((
12294            std::str::from_utf8(attribute.key.as_ref())
12295                .map_err(OxmlError::from)?
12296                .to_owned(),
12297            attribute
12298                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
12299                .map_err(OxmlError::from)?
12300                .into_owned(),
12301        ));
12302    }
12303    Ok(attributes)
12304}
12305
12306fn capture_attributes_except(start: &BytesStart<'_>, excluded: &[u8]) -> Result<XmlAttributes> {
12307    let mut attributes = Vec::new();
12308    for attribute in start.attributes() {
12309        let attribute = attribute.map_err(OxmlError::from)?;
12310        if attribute.key.as_ref() == excluded {
12311            continue;
12312        }
12313        attributes.push((
12314            std::str::from_utf8(attribute.key.as_ref())
12315                .map_err(OxmlError::from)?
12316                .to_owned(),
12317            attribute
12318                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
12319                .map_err(OxmlError::from)?
12320                .into_owned(),
12321        ));
12322    }
12323    Ok(attributes)
12324}
12325
12326fn capture_attributes_excluding(
12327    start: &BytesStart<'_>,
12328    excluded: &[&[u8]],
12329) -> Result<XmlAttributes> {
12330    let mut attributes = Vec::new();
12331    for attribute in start.attributes() {
12332        let attribute = attribute.map_err(OxmlError::from)?;
12333        if excluded.contains(&attribute.key.as_ref()) {
12334            continue;
12335        }
12336        attributes.push((
12337            std::str::from_utf8(attribute.key.as_ref())
12338                .map_err(OxmlError::from)?
12339                .to_owned(),
12340            attribute
12341                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
12342                .map_err(OxmlError::from)?
12343                .into_owned(),
12344        ));
12345    }
12346    Ok(attributes)
12347}
12348
12349fn push_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
12350    for (name, value) in attributes {
12351        start.push_attribute((name.as_str(), value.as_str()));
12352    }
12353}
12354
12355fn write_scalar<W: Write>(
12356    writer: &mut Writer<W>,
12357    name: &str,
12358    value: &str,
12359    markup: Option<&ScalarMarkup>,
12360) -> Result<()> {
12361    let mut start = BytesStart::new(name);
12362    start.push_attribute(("val", value));
12363    if let Some(markup) = markup {
12364        push_attributes(&mut start, &markup.raw_attributes);
12365    }
12366    let has_content = markup.is_some_and(|markup| !markup.raw_content.is_empty());
12367    if !has_content {
12368        writer
12369            .write_event(Event::Empty(start))
12370            .map_err(OxmlError::from)?;
12371        return Ok(());
12372    }
12373    writer
12374        .write_event(Event::Start(start))
12375        .map_err(OxmlError::from)?;
12376    if let Some(markup) = markup {
12377        for raw in &markup.raw_content {
12378            writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
12379        }
12380    }
12381    writer
12382        .write_event(Event::End(BytesEnd::new(name)))
12383        .map_err(OxmlError::from)?;
12384    Ok(())
12385}
12386
12387fn capture_event(event: Event<'_>) -> Result<Vec<u8>> {
12388    let mut writer = Writer::new(Vec::new());
12389    writer
12390        .write_event(event.into_owned())
12391        .map_err(OxmlError::from)?;
12392    Ok(writer.into_inner())
12393}
12394
12395fn emit_raw<'a, W: Write>(
12396    writer: &mut Writer<W>,
12397    children: impl Iterator<Item = &'a [u8]>,
12398) -> Result<()> {
12399    for child in children {
12400        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
12401    }
12402    Ok(())
12403}
12404
12405fn invalid_attribute(element: &str, attribute: &str, value: String) -> ChartError {
12406    ChartError::InvalidAttribute {
12407        element: element.to_owned(),
12408        attribute: attribute.to_owned(),
12409        value,
12410    }
12411}
12412
12413fn element_name(element: &BytesStart<'_>) -> String {
12414    String::from_utf8_lossy(element.name().as_ref()).into_owned()
12415}
12416
12417fn missing_end(element: &str) -> ChartError {
12418    ChartError::Xml(OxmlError::MissingElement(format!("closing {element}")))
12419}
12420
12421#[cfg(test)]
12422mod tests {
12423    use std::collections::HashSet;
12424    use std::fs;
12425    use std::path::{Path, PathBuf};
12426    use std::process::Command;
12427
12428    use oxml_core::raw_xml::{capture_element, capture_empty_element};
12429    use oxml_drawing::color::{ColorMap, ColorMapSlot, RgbColor, ThemeColorSlot};
12430    use oxml_drawing::theme::CT_OfficeStyleSheet;
12431    use oxml_layout::{
12432        Color, FontManager, LayoutResult, PageFrame, Paint, PathCommand, Point, PositionedElement,
12433        Rect,
12434    };
12435    use oxml_opc::OpcPackage;
12436    use quick_xml::Reader;
12437    use quick_xml::events::Event;
12438
12439    use super::{
12440        A_NS, Axis, AxisData, AxisId, AxisKind, AxisPosition, BarDirection, BarGrouping, C_NS,
12441        CT_ChartSpace, CT_DLbls, CT_ShapeProperties, CT_TextBody, ChartData, ChartGeometry,
12442        ChartKind, DataLabelPosition, DispBlanksAs, Domain, Grouping, NumberFormat, NumericData,
12443        Orientation, Plot, R_NS, ScatterStyle, Series, StringRef, TickLabelPosition, TickMark,
12444        authored_chart_parts, capture_event, chart_workbook_relationship_id, local_name,
12445        matches_local_name, nice_number_scale, read_chart_data,
12446        render_chart as render_chart_with_theme, render_geometry as render_geometry_with_theme,
12447        replace_chart_data,
12448    };
12449
12450    const MANIFEST: &str = include_str!("../../../scripts/pptx-corpus-manifest.tsv");
12451    const EXPECTED_DECKS: usize = 50;
12452    const LIBREOFFICE_VERSION: &str =
12453        "LibreOffice 26.2.5.2 cd7284b4cbbfeb507e630c1aac019f4157393acb";
12454    const PDFTOTEXT_VERSION: &str = "pdftotext version 26.01.0";
12455    const PDFTOPPM_VERSION: &str = "pdftoppm version 26.01.0";
12456    const PLOT_RENDER_NORMALIZED_MAE_THRESHOLD: f64 = 0.0;
12457
12458    #[test]
12459    fn authored_charts_emit_portable_viewer_defaults() {
12460        let data = ChartData {
12461            categories: vec!["August".to_owned(), "September".to_owned()],
12462            series: vec![("Price".to_owned(), vec![12.5, 19.2])],
12463            number_format: Some(r"0.##\%".to_owned()),
12464            category_axis_title: Some("Month".to_owned()),
12465            value_axis_title: Some("Change".to_owned()),
12466            palette: vec![
12467                RgbColor::parse("2B6FE3").unwrap(),
12468                RgbColor::parse("F0761F").unwrap(),
12469            ],
12470        };
12471        let (line, line_workbook) = authored_chart_parts(ChartKind::Line, &data, "rId1").unwrap();
12472        let line = std::str::from_utf8(&line).unwrap();
12473        assert_eq!(line.matches(r#"<c:delete val="0"/>"#).count(), 2);
12474        assert!(line.contains(r#"<c:numFmt formatCode="0.##\%" sourceLinked="0"/>"#));
12475        assert!(line.contains("<a:t>Month</a:t>"));
12476        assert!(line.contains("<a:t>Change</a:t>"));
12477        assert!(line.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
12478        let assert_portable_line = |xml: &str| {
12479            let titles = xml
12480                .split("<c:title>")
12481                .skip(1)
12482                .map(|tail| tail.split_once("</c:title>").unwrap().0)
12483                .collect::<Vec<_>>();
12484            assert_eq!(titles.len(), 2);
12485            for title in titles {
12486                let text = title.find("<c:tx>").unwrap();
12487                let layout = title.find("<c:layout/>").unwrap();
12488                let overlay = title.find(r#"<c:overlay val="0"/>"#).unwrap();
12489                assert!(text < layout && layout < overlay);
12490            }
12491
12492            let line_plot = xml
12493                .split_once("<c:lineChart>")
12494                .unwrap()
12495                .1
12496                .split_once("</c:lineChart>")
12497                .unwrap()
12498                .0;
12499            assert_eq!(line_plot.matches(r#"<c:marker val="0"/>"#).count(), 1);
12500            assert_eq!(line_plot.matches(r#"<c:smooth val="0"/>"#).count(), 1);
12501            let marker = line_plot.find(r#"<c:marker val="0"/>"#).unwrap();
12502            let smooth = line_plot.find(r#"<c:smooth val="0"/>"#).unwrap();
12503            let axis_id = line_plot.find("<c:axId").unwrap();
12504            assert!(marker < smooth && smooth < axis_id);
12505        };
12506        assert_portable_line(line);
12507        for axis in line.split("<c:crossAx").take(2) {
12508            let scaling = axis.rfind("<c:scaling").unwrap();
12509            let deleted = axis.rfind("<c:delete").unwrap();
12510            let position = axis.rfind("<c:axPos").unwrap();
12511            assert!(scaling < deleted && deleted < position);
12512        }
12513
12514        let parsed = CT_ChartSpace::from_xml(line.as_bytes()).unwrap();
12515        let rewritten = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
12516        assert_portable_line(&rewritten);
12517        assert!(rewritten.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
12518        assert!(rewritten.contains(r#"<c:externalData r:id="rId1">"#));
12519
12520        for kind in [
12521            ChartKind::Bar,
12522            ChartKind::Pie,
12523            ChartKind::Doughnut,
12524            ChartKind::Area,
12525            ChartKind::Scatter,
12526            ChartKind::Radar,
12527        ] {
12528            let mut kind_data = data.clone();
12529            if kind == ChartKind::Scatter {
12530                kind_data.categories = vec!["1".to_owned(), "2".to_owned()];
12531            }
12532            let (chart, workbook) = authored_chart_parts(kind, &kind_data, "rId1").unwrap();
12533            let chart = std::str::from_utf8(&chart).unwrap();
12534            assert!(!chart.contains(r#"<c:marker val="0"/>"#));
12535            assert!(!chart.contains(r#"<c:smooth val="0"/>"#));
12536            if kind != ChartKind::Scatter {
12537                assert_eq!(workbook, line_workbook);
12538            }
12539            if matches!(kind, ChartKind::Pie | ChartKind::Doughnut) {
12540                assert!(chart.contains("<c:legend"));
12541                assert!(chart.contains(r#"<c:showPercent val="1"/>"#));
12542            }
12543            if kind == ChartKind::Doughnut {
12544                assert!(chart.contains(r#"<c:holeSize val="50"/>"#));
12545            }
12546            if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) {
12547                assert_eq!(chart.matches("<c:dPt>").count(), 2);
12548                assert!(chart.contains(
12549                    r#"<c:dPt><c:idx val="0"/><c:spPr><a:solidFill><a:srgbClr val="2B6FE3"/>"#
12550                ));
12551                assert!(chart.contains(
12552                    r#"<c:dPt><c:idx val="1"/><c:spPr><a:solidFill><a:srgbClr val="F0761F"/>"#
12553                ));
12554            }
12555
12556            let parsed = CT_ChartSpace::from_xml(chart.as_bytes()).unwrap();
12557            let rewritten = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
12558            assert_eq!(
12559                rewritten.matches("<c:dPt>").count(),
12560                usize::from(matches!(
12561                    kind,
12562                    ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut
12563                )) * 2
12564            );
12565            assert!(rewritten.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
12566            if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) {
12567                assert!(rewritten.contains(r#"<a:srgbClr val="F0761F"/>"#));
12568            }
12569        }
12570    }
12571
12572    #[test]
12573    fn optional_numeric_cache_format_remains_omitted() {
12574        let xml = br#"<c:numRef xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:f>Sheet1!$B$2:$B$3</c:f><c:numCache><c:ptCount val="2"/><c:pt idx="0"><c:v>12.5</c:v></c:pt><c:pt idx="1"><c:v>19.2</c:v></c:pt></c:numCache></c:numRef>"#;
12575        let mut parsed = NumericData::from_xml(xml).unwrap();
12576        assert_eq!(parsed.format_code, "General");
12577        let written = parsed.to_xml().unwrap();
12578        assert!(!String::from_utf8_lossy(&written).contains("formatCode"));
12579        assert_eq!(NumericData::from_xml(&written).unwrap(), parsed);
12580
12581        parsed.format_code = "0.00".to_owned();
12582        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
12583        assert!(written.contains("<c:formatCode>0.00</c:formatCode>"));
12584    }
12585
12586    fn render_geometry(chart: &super::CT_Chart, bounds: Rect) -> super::Result<ChartGeometry> {
12587        render_geometry_with_theme(
12588            chart,
12589            bounds,
12590            &CT_OfficeStyleSheet::office_default(),
12591            &ColorMap::default(),
12592        )
12593    }
12594
12595    fn render_chart(
12596        chart: &super::CT_Chart,
12597        bounds: Rect,
12598        fonts: &mut FontManager,
12599    ) -> super::Result<oxml_layout::GroupElement> {
12600        render_chart_with_theme(
12601            chart,
12602            bounds,
12603            &CT_OfficeStyleSheet::office_default(),
12604            &ColorMap::default(),
12605            fonts,
12606        )
12607    }
12608
12609    #[test]
12610    fn bar_chart_rasterises_at_computed_positions() {
12611        let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12612        let geometry = render_geometry(
12613            &chart.chart,
12614            Rect {
12615                x: 0.0,
12616                y: 0.0,
12617                width: 200.0,
12618                height: 140.0,
12619            },
12620        )
12621        .unwrap();
12622        assert_eq!(
12623            geometry.plot_bounds,
12624            Rect {
12625                x: 36.0,
12626                y: 12.0,
12627                width: 152.0,
12628                height: 100.0,
12629            }
12630        );
12631        let bounds = path_bounds(&geometry);
12632        assert_eq!(bounds.len(), 2);
12633        assert_rect_near(
12634            bounds[0],
12635            Rect {
12636                x: 58.8,
12637                y: 62.0,
12638                width: 30.4,
12639                height: 50.0,
12640            },
12641        );
12642        assert_rect_near(
12643            bounds[1],
12644            Rect {
12645                x: 134.8,
12646                y: 12.0,
12647                width: 30.4,
12648                height: 100.0,
12649            },
12650        );
12651
12652        let layout = LayoutResult::new(
12653            vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
12654            Vec::new(),
12655            None,
12656            Vec::new(),
12657        );
12658        let png = oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap();
12659        let pixmap = tiny_skia::Pixmap::decode_png(&png).unwrap();
12660        for (x, y) in [(74, 87), (150, 37)] {
12661            let pixel = pixmap.pixel(x, y).unwrap();
12662            assert_ne!((pixel.red(), pixel.green(), pixel.blue()), (255, 255, 255));
12663        }
12664    }
12665
12666    #[test]
12667    fn unstyled_four_series_use_accent_one_through_four() {
12668        let chart = four_series_bar_chart();
12669        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12670        let colours = path_colours(&geometry);
12671
12672        assert_eq!(
12673            &colours[..4],
12674            &[
12675                Color::from_hex("156082"),
12676                Color::from_hex("E97132"),
12677                Color::from_hex("196B24"),
12678                Color::from_hex("0F9ED5"),
12679            ]
12680        );
12681    }
12682
12683    #[test]
12684    fn direct_series_solid_colour_overrides_theme_accent() {
12685        for (paint, expected) in [
12686            (
12687                "<a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill>",
12688                Color::from_hex("123456"),
12689            ),
12690            (
12691                "<a:ln><a:solidFill><a:srgbClr val=\"654321\"/></a:solidFill></a:ln>",
12692                Color::from_hex("654321"),
12693            ),
12694            (
12695                "<a:noFill/>",
12696                Color {
12697                    r: 0.0,
12698                    g: 0.0,
12699                    b: 0.0,
12700                    a: 0.0,
12701                },
12702            ),
12703        ] {
12704            let styled =
12705                plot_series(0).replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
12706            let plot = bar_plot("").replace(&plot_series(0), &styled);
12707            let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
12708            let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12709
12710            assert_eq!(path_colours(&geometry)[0], expected);
12711        }
12712
12713        let styled = plot_series(0).replace(
12714            "<q:cat>",
12715            "<q:spPr><a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill></q:spPr><q:cat>",
12716        );
12717        let plot = bar_plot("").replace(&plot_series(0), &styled);
12718        let xml = chart_with_plot(&plot).replace(
12719            "</q:plotArea></q:chart>",
12720            "</q:plotArea><q:legend/></q:chart>",
12721        );
12722        let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
12723        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12724        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12725        assert!(group.children.iter().any(|element| matches!(
12726            element,
12727            PositionedElement::Path(path)
12728                if path.fill == Some(Paint::Solid(Color::from_hex("123456")))
12729        )));
12730
12731        let mut theme = CT_OfficeStyleSheet::office_default();
12732        let preset = oxml_drawing::fill::Fill::from_xml(
12733            br#"<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:prstClr val="red"/></a:solidFill>"#,
12734        )
12735        .unwrap();
12736        let oxml_drawing::fill::Fill::Solid(preset) = preset else {
12737            panic!("expected solid fill");
12738        };
12739        theme.theme_elements.color_scheme.hyperlink = preset.color.unwrap();
12740        let geometry =
12741            render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &ColorMap::default())
12742                .unwrap();
12743        assert_eq!(path_colours(&geometry)[0], Color::from_hex("123456"));
12744
12745        for (paint, expected_alpha) in [
12746            ("<a:noFill/>", 0.0),
12747            (
12748                "<a:solidFill><a:srgbClr val=\"123456\"><a:alpha val=\"50000\"/></a:srgbClr></a:solidFill>",
12749                (128.0 / 255.0) * 0.55,
12750            ),
12751        ] {
12752            let (_, area, _) = remaining_plot_fixtures()
12753                .into_iter()
12754                .find(|(kind, _, _)| *kind == "areaChart")
12755                .unwrap();
12756            let styled_area = area.replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
12757            let chart =
12758                CT_ChartSpace::from_xml(chart_with_optional_axes(&styled_area, true).as_bytes())
12759                    .unwrap();
12760            let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12761            let actual_alpha = path_colours(&geometry)[0].a;
12762
12763            assert!((actual_alpha - expected_alpha).abs() < 1.0e-12);
12764        }
12765    }
12766
12767    #[test]
12768    fn series_accent_cycle_repeats_after_six() {
12769        let chart = seven_series_bar_chart();
12770        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12771        let colours = path_colours(&geometry);
12772
12773        assert_eq!(colours[0], Color::from_hex("156082"));
12774        assert_eq!(colours[6], colours[0]);
12775    }
12776
12777    #[test]
12778    fn series_colours_honor_colour_map_and_transform_order() {
12779        let styled = plot_series(0).replace(
12780            "<q:cat>",
12781            "<q:spPr><a:solidFill><a:schemeClr val=\"accent1\"><a:shade val=\"80000\"/><a:lumOff val=\"5000\"/><a:alpha val=\"50000\"/></a:schemeClr></a:solidFill></q:spPr><q:cat>",
12782        );
12783        let plot = bar_plot("").replace(&plot_series(0), &styled);
12784        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
12785        let color_map =
12786            ColorMap::default().with_overrides(&[(ColorMapSlot::Accent1, ThemeColorSlot::Accent4)]);
12787        let mut theme = CT_OfficeStyleSheet::office_default();
12788        let theme_fill = oxml_drawing::fill::Fill::from_xml(
12789            br#"<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:srgbClr val="336699"><a:tint val="20000"/><a:lumMod val="90000"/></a:srgbClr></a:solidFill>"#,
12790        )
12791        .unwrap();
12792        let oxml_drawing::fill::Fill::Solid(theme_fill) = theme_fill else {
12793            panic!("expected solid fill");
12794        };
12795        let theme_colour = theme_fill.color.unwrap();
12796        theme.theme_elements.color_scheme.accent4 = theme_colour.clone();
12797        let geometry =
12798            render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &color_map).unwrap();
12799        let series_fill = oxml_drawing::fill::Fill::from_xml(
12800            br#"<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:schemeClr val="accent1"><a:shade val="80000"/><a:lumOff val="5000"/><a:alpha val="50000"/></a:schemeClr></a:solidFill>"#,
12801        )
12802        .unwrap();
12803        let oxml_drawing::fill::Fill::Solid(series_fill) = series_fill else {
12804            panic!("expected solid fill");
12805        };
12806        let mut transforms = theme_colour.transforms().to_vec();
12807        transforms.extend_from_slice(series_fill.color.as_ref().unwrap().transforms());
12808        let expected = oxml_drawing::color::apply_color_transforms(
12809            RgbColor::parse("336699").unwrap(),
12810            &transforms,
12811        );
12812
12813        assert_eq!(path_colours(&geometry)[0], resolved_layout_colour(expected));
12814    }
12815
12816    #[test]
12817    fn chart_colours_do_not_use_word_tint_shade() {
12818        let styled = plot_series(0).replace(
12819            "<q:cat>",
12820            "<q:spPr><a:solidFill><a:srgbClr val=\"808080\"><a:tint val=\"50000\"/></a:srgbClr></a:solidFill></q:spPr><q:cat>",
12821        );
12822        let plot = bar_plot("").replace(&plot_series(0), &styled);
12823        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
12824        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12825
12826        assert_eq!(
12827            path_colours(&geometry)[0],
12828            Color {
12829                r: 205.0 / 255.0,
12830                g: 205.0 / 255.0,
12831                b: 205.0 / 255.0,
12832                a: 1.0,
12833            }
12834        );
12835    }
12836
12837    #[test]
12838    fn unsupported_direct_series_paint_is_contextual() {
12839        let styled = plot_series(0).replace(
12840            "<q:cat>",
12841            "<q:spPr><a:gradFill><a:gsLst><a:gs pos=\"0\"><a:srgbClr val=\"FF0000\"/></a:gs><a:gs pos=\"100000\"><a:srgbClr val=\"0000FF\"/></a:gs></a:gsLst></a:gradFill></q:spPr><q:cat>",
12842        );
12843        let plot = bar_plot("").replace(&plot_series(0), &styled);
12844        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
12845        let error = render_geometry(&chart.chart, chart_bounds()).unwrap_err();
12846
12847        assert!(error.to_string().contains("c:ser[0] direct fill paint"));
12848        assert!(error.to_string().contains("gradient"));
12849    }
12850
12851    #[test]
12852    fn resolved_chart_palette_raster_is_deterministic() {
12853        let raster = || {
12854            let chart = four_series_bar_chart();
12855            let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12856            let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12857            let layout = LayoutResult::new(
12858                vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
12859                fonts.all_font_data(),
12860                None,
12861                Vec::new(),
12862            );
12863            oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
12864        };
12865        let chart = four_series_bar_chart();
12866        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12867
12868        assert_eq!(path_colours(&geometry)[0], Color::from_hex("156082"));
12869        assert_eq!(raster(), raster());
12870    }
12871
12872    #[test]
12873    fn bar_geometry_handles_direction_grouping_gap_and_overlap() {
12874        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12875        let Plot::Bar {
12876            overlap, series, ..
12877        } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
12878        else {
12879            panic!("expected bar plot");
12880        };
12881        let mut second = series[0].clone();
12882        second.index = 1;
12883        second.order = 1;
12884        second.values.values = vec![2.0, 1.0];
12885        series.push(second);
12886
12887        *overlap = 25;
12888        let clustered = render_geometry(&chart.chart, chart_bounds()).unwrap();
12889        let clustered_bounds = path_bounds(&clustered);
12890        let clustered_width = 30.4 / 1.75;
12891        let clustered_advance = clustered_width * 0.75;
12892        assert_rects_near(
12893            &clustered_bounds,
12894            &[
12895                Rect {
12896                    x: 58.8,
12897                    y: 62.0,
12898                    width: clustered_width,
12899                    height: 50.0,
12900                },
12901                Rect {
12902                    x: 134.8,
12903                    y: 12.0,
12904                    width: clustered_width,
12905                    height: 100.0,
12906                },
12907                Rect {
12908                    x: 58.8 + clustered_advance,
12909                    y: 12.0,
12910                    width: clustered_width,
12911                    height: 100.0,
12912                },
12913                Rect {
12914                    x: 134.8 + clustered_advance,
12915                    y: 62.0,
12916                    width: clustered_width,
12917                    height: 50.0,
12918                },
12919            ],
12920        );
12921
12922        let Plot::Bar { direction, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12923            unreachable!()
12924        };
12925        *direction = BarDirection::Bar;
12926        let horizontal = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
12927        let horizontal_width = 20.0 / 1.75;
12928        let horizontal_advance = horizontal_width * 0.75;
12929        assert_rects_near(
12930            &horizontal,
12931            &[
12932                Rect {
12933                    x: 36.0,
12934                    y: 27.0,
12935                    width: 76.0,
12936                    height: horizontal_width,
12937                },
12938                Rect {
12939                    x: 36.0,
12940                    y: 77.0,
12941                    width: 152.0,
12942                    height: horizontal_width,
12943                },
12944                Rect {
12945                    x: 36.0,
12946                    y: 27.0 + horizontal_advance,
12947                    width: 152.0,
12948                    height: horizontal_width,
12949                },
12950                Rect {
12951                    x: 36.0,
12952                    y: 77.0 + horizontal_advance,
12953                    width: 76.0,
12954                    height: horizontal_width,
12955                },
12956            ],
12957        );
12958
12959        let Plot::Bar {
12960            direction,
12961            grouping,
12962            ..
12963        } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
12964        else {
12965            unreachable!()
12966        };
12967        *direction = BarDirection::Column;
12968        *grouping = BarGrouping::Stacked;
12969        let stacked = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
12970        assert_rects_near(
12971            &stacked,
12972            &[
12973                Rect {
12974                    x: 58.8,
12975                    y: 112.0 - 100.0 / 3.0,
12976                    width: 30.4,
12977                    height: 100.0 / 3.0,
12978                },
12979                Rect {
12980                    x: 134.8,
12981                    y: 112.0 - 200.0 / 3.0,
12982                    width: 30.4,
12983                    height: 200.0 / 3.0,
12984                },
12985                Rect {
12986                    x: 58.8,
12987                    y: 12.0,
12988                    width: 30.4,
12989                    height: 200.0 / 3.0,
12990                },
12991                Rect {
12992                    x: 134.8,
12993                    y: 12.0,
12994                    width: 30.4,
12995                    height: 100.0 / 3.0,
12996                },
12997            ],
12998        );
12999
13000        let Plot::Bar {
13001            grouping, series, ..
13002        } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
13003        else {
13004            unreachable!()
13005        };
13006        *grouping = BarGrouping::PercentStacked;
13007        series[1].values.values = vec![2.0, 2.0];
13008        let percent = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
13009        assert_rects_near(
13010            &percent,
13011            &[
13012                Rect {
13013                    x: 58.8,
13014                    y: 112.0 - 100.0 / 3.0,
13015                    width: 30.4,
13016                    height: 100.0 / 3.0,
13017                },
13018                Rect {
13019                    x: 134.8,
13020                    y: 62.0,
13021                    width: 30.4,
13022                    height: 50.0,
13023                },
13024                Rect {
13025                    x: 58.8,
13026                    y: 12.0,
13027                    width: 30.4,
13028                    height: 200.0 / 3.0,
13029                },
13030                Rect {
13031                    x: 134.8,
13032                    y: 12.0,
13033                    width: 30.4,
13034                    height: 50.0,
13035                },
13036            ],
13037        );
13038    }
13039
13040    #[test]
13041    fn line_scatter_and_radar_emit_paths_and_markers() {
13042        let line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
13043        let line_geometry = render_geometry(&line.chart, chart_bounds()).unwrap();
13044        assert_path_points(
13045            path_commands(&line_geometry)[0],
13046            &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
13047            false,
13048        );
13049        assert_rects_near(
13050            &path_bounds(&line_geometry)[1..],
13051            &[
13052                Rect {
13053                    x: 71.5,
13054                    y: 109.5,
13055                    width: 5.0,
13056                    height: 5.0,
13057                },
13058                Rect {
13059                    x: 147.5,
13060                    y: 9.5,
13061                    width: 5.0,
13062                    height: 5.0,
13063                },
13064            ],
13065        );
13066
13067        let (scatter_plot, _) = remaining_plot_fixtures()
13068            .into_iter()
13069            .find(|(kind, _, _)| *kind == "scatterChart")
13070            .map(|(_, plot, axes)| (plot, axes))
13071            .unwrap();
13072        let mut scatter =
13073            CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
13074                .unwrap();
13075        let Plot::Scatter { style, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
13076        else {
13077            unreachable!()
13078        };
13079        *style = ScatterStyle::LineMarker;
13080        let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
13081        assert_path_points(
13082            path_commands(&scatter_geometry)[0],
13083            &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
13084            false,
13085        );
13086        assert_rects_near(
13087            &path_bounds(&scatter_geometry)[1..],
13088            &[
13089                Rect {
13090                    x: 33.5,
13091                    y: 109.5,
13092                    width: 5.0,
13093                    height: 5.0,
13094                },
13095                Rect {
13096                    x: 185.5,
13097                    y: 9.5,
13098                    width: 5.0,
13099                    height: 5.0,
13100                },
13101            ],
13102        );
13103
13104        let (radar_plot, _) = remaining_plot_fixtures()
13105            .into_iter()
13106            .find(|(kind, _, _)| *kind == "radarChart")
13107            .map(|(_, plot, axes)| (plot, axes))
13108            .unwrap();
13109        let radar = CT_ChartSpace::from_xml(chart_with_optional_axes(&radar_plot, true).as_bytes())
13110            .unwrap();
13111        let radar_geometry = render_geometry(&radar.chart, chart_bounds()).unwrap();
13112        assert_path_points(
13113            path_commands(&radar_geometry)[0],
13114            &[Point { x: 112.0, y: 37.0 }, Point { x: 112.0, y: 112.0 }],
13115            true,
13116        );
13117        assert_rects_near(
13118            &path_bounds(&radar_geometry)[1..],
13119            &[
13120                Rect {
13121                    x: 109.5,
13122                    y: 34.5,
13123                    width: 5.0,
13124                    height: 5.0,
13125                },
13126                Rect {
13127                    x: 109.5,
13128                    y: 109.5,
13129                    width: 5.0,
13130                    height: 5.0,
13131                },
13132            ],
13133        );
13134    }
13135
13136    #[test]
13137    fn pie_doughnut_and_area_emit_closed_paths() {
13138        let fixtures = remaining_plot_fixtures();
13139        let pie_plot = fixtures
13140            .iter()
13141            .find(|(kind, _, _)| *kind == "pieChart")
13142            .unwrap();
13143        let pie =
13144            CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot.1, pie_plot.2).as_bytes())
13145                .unwrap();
13146        let pie_geometry = render_geometry(&pie.chart, chart_bounds()).unwrap();
13147        let pie_commands = path_commands(&pie_geometry);
13148        assert_eq!(pie_commands.len(), 2);
13149        let [
13150            PathCommand::MoveTo(center),
13151            PathCommand::LineTo(start),
13152            ..,
13153            PathCommand::CurveTo { to: end, .. },
13154            PathCommand::Close,
13155        ] = pie_commands[0]
13156        else {
13157            panic!("expected closed cubic pie wedge");
13158        };
13159        assert_point_near(*center, Point { x: 112.0, y: 62.0 });
13160        assert_point_near(
13161            *start,
13162            Point {
13163                x: 137.0,
13164                y: 62.0 - 25.0 * 3.0_f64.sqrt(),
13165            },
13166        );
13167        assert_point_near(
13168            *end,
13169            Point {
13170                x: 137.0,
13171                y: 62.0 + 25.0 * 3.0_f64.sqrt(),
13172            },
13173        );
13174
13175        let doughnut_plot = fixtures
13176            .iter()
13177            .find(|(kind, _, _)| *kind == "doughnutChart")
13178            .unwrap();
13179        let doughnut = CT_ChartSpace::from_xml(
13180            chart_with_optional_axes(&doughnut_plot.1, doughnut_plot.2).as_bytes(),
13181        )
13182        .unwrap();
13183        let doughnut_geometry = render_geometry(&doughnut.chart, chart_bounds()).unwrap();
13184        let doughnut_commands = path_commands(&doughnut_geometry);
13185        assert_eq!(doughnut_commands.len(), 2);
13186        let commands = doughnut_commands[0];
13187        assert_eq!(commands.len(), 7);
13188        let PathCommand::MoveTo(outer_start) = commands[0] else {
13189            panic!("expected doughnut outer start");
13190        };
13191        let PathCommand::LineTo(inner_end) = commands[3] else {
13192            panic!("expected doughnut inner-radius join");
13193        };
13194        let PathCommand::CurveTo {
13195            to: inner_start, ..
13196        } = commands[5]
13197        else {
13198            panic!("expected reverse inner arc");
13199        };
13200        let diagonal = 25.0 * 2.0_f64.sqrt();
13201        assert_point_near(
13202            outer_start,
13203            Point {
13204                x: 112.0 + diagonal,
13205                y: 62.0 - diagonal,
13206            },
13207        );
13208        assert_point_near(
13209            inner_end,
13210            Point {
13211                x: 112.0 + 30.0 * 75.0_f64.to_radians().cos(),
13212                y: 62.0 + 30.0 * 75.0_f64.to_radians().sin(),
13213            },
13214        );
13215        let inner_diagonal = 15.0 * 2.0_f64.sqrt();
13216        assert_point_near(
13217            inner_start,
13218            Point {
13219                x: 112.0 + inner_diagonal,
13220                y: 62.0 - inner_diagonal,
13221            },
13222        );
13223        assert_eq!(commands.last(), Some(&PathCommand::Close));
13224
13225        let area_plot = fixtures
13226            .iter()
13227            .find(|(kind, _, _)| *kind == "areaChart")
13228            .unwrap();
13229        let mut area =
13230            CT_ChartSpace::from_xml(chart_with_optional_axes(&area_plot.1, area_plot.2).as_bytes())
13231                .unwrap();
13232        let area_geometry = render_geometry(&area.chart, chart_bounds()).unwrap();
13233        assert_path_points(
13234            path_commands(&area_geometry)[0],
13235            &[
13236                Point { x: 74.0, y: 62.0 },
13237                Point { x: 150.0, y: 12.0 },
13238                Point { x: 150.0, y: 112.0 },
13239                Point { x: 74.0, y: 112.0 },
13240            ],
13241            true,
13242        );
13243
13244        let Plot::Area {
13245            grouping, series, ..
13246        } = &mut area.chart.plot_area.plots_mut().unwrap()[0]
13247        else {
13248            unreachable!()
13249        };
13250        *grouping = Grouping::Stacked;
13251        let mut second = series[0].clone();
13252        second.index = 1;
13253        second.order = 1;
13254        second.values.values = vec![2.0, 1.0];
13255        series.push(second);
13256        let stacked_area = render_geometry(&area.chart, chart_bounds()).unwrap();
13257        assert_path_points(
13258            path_commands(&stacked_area)[1],
13259            &[
13260                Point { x: 74.0, y: 12.0 },
13261                Point { x: 150.0, y: 12.0 },
13262                Point {
13263                    x: 150.0,
13264                    y: 112.0 - 200.0 / 3.0,
13265                },
13266                Point {
13267                    x: 74.0,
13268                    y: 112.0 - 100.0 / 3.0,
13269                },
13270            ],
13271            true,
13272        );
13273    }
13274
13275    #[test]
13276    fn sparse_cache_indexes_preserve_slots_and_scatter_pairing() {
13277        let series = sparse_category_series();
13278        let bar = format!(
13279            r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{series}<q:gapWidth val="150"/><q:overlap val="0"/><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:barChart>"#
13280        );
13281        let bar = CT_ChartSpace::from_xml(chart_with_plot(&bar).as_bytes()).unwrap();
13282        assert_rects_near(
13283            &path_bounds(&render_geometry(&bar.chart, chart_bounds()).unwrap()),
13284            &[
13285                Rect {
13286                    x: 51.2,
13287                    y: 62.0,
13288                    width: 304.0 / 15.0,
13289                    height: 50.0,
13290                },
13291                Rect {
13292                    x: 36.0 + 304.0 / 3.0 + 15.2,
13293                    y: 12.0,
13294                    width: 304.0 / 15.0,
13295                    height: 100.0,
13296                },
13297            ],
13298        );
13299
13300        let line = format!(
13301            r#"<q:lineChart><q:grouping val="standard"/>{series}<q:marker val="0"/><q:smooth val="0"/><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:lineChart>"#
13302        );
13303        let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line).as_bytes()).unwrap();
13304        let first = Point {
13305            x: 36.0 + 76.0 / 3.0,
13306            y: 62.0,
13307        };
13308        let missing = Point { x: 112.0, y: 112.0 };
13309        let last = Point {
13310            x: 36.0 + 380.0 / 3.0,
13311            y: 12.0,
13312        };
13313        let gap = render_geometry(&line.chart, chart_bounds()).unwrap();
13314        let gap_commands = path_commands(&gap);
13315        assert_eq!(gap_commands.len(), 2);
13316        assert_path_points(
13317            gap_commands[0],
13318            &[Point {
13319                x: first.x,
13320                y: 112.0,
13321            }],
13322            false,
13323        );
13324        assert_path_points(gap_commands[1], &[last], false);
13325        line.chart.disp_blanks_as = DispBlanksAs::Zero;
13326        assert_path_points(
13327            path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
13328            &[first, missing, last],
13329            false,
13330        );
13331        line.chart.disp_blanks_as = DispBlanksAs::Span;
13332        assert_path_points(
13333            path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
13334            &[
13335                Point {
13336                    x: first.x,
13337                    y: 112.0,
13338                },
13339                last,
13340            ],
13341            false,
13342        );
13343
13344        let area = format!(
13345            r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:areaChart>"#
13346        );
13347        let mut area = CT_ChartSpace::from_xml(chart_with_plot(&area).as_bytes()).unwrap();
13348        let area_gap = render_geometry(&area.chart, chart_bounds()).unwrap();
13349        let area_gap_commands = path_commands(&area_gap);
13350        assert_eq!(area_gap_commands.len(), 2);
13351        assert_path_points(
13352            area_gap_commands[0],
13353            &[
13354                first,
13355                Point {
13356                    x: first.x,
13357                    y: 112.0,
13358                },
13359            ],
13360            true,
13361        );
13362        assert_path_points(
13363            area_gap_commands[1],
13364            &[
13365                last,
13366                Point {
13367                    x: last.x,
13368                    y: 112.0,
13369                },
13370            ],
13371            true,
13372        );
13373        area.chart.disp_blanks_as = DispBlanksAs::Zero;
13374        assert_path_points(
13375            path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
13376            &[
13377                first,
13378                missing,
13379                last,
13380                Point {
13381                    x: last.x,
13382                    y: 112.0,
13383                },
13384                missing,
13385                Point {
13386                    x: first.x,
13387                    y: 112.0,
13388                },
13389            ],
13390            true,
13391        );
13392        area.chart.disp_blanks_as = DispBlanksAs::Span;
13393        assert_path_points(
13394            path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
13395            &[
13396                first,
13397                last,
13398                Point {
13399                    x: last.x,
13400                    y: 112.0,
13401                },
13402                Point {
13403                    x: first.x,
13404                    y: 112.0,
13405                },
13406            ],
13407            true,
13408        );
13409
13410        let radar = format!(
13411            r#"<q:radarChart><q:radarStyle val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#
13412        );
13413        let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
13414        assert_path_points(
13415            path_commands(&render_geometry(&radar.chart, chart_bounds()).unwrap())[0],
13416            &[
13417                Point { x: 112.0, y: 37.0 },
13418                Point {
13419                    x: 112.0 - 25.0 * 3.0_f64.sqrt(),
13420                    y: 87.0,
13421                },
13422            ],
13423            true,
13424        );
13425
13426        let scatter = format!(
13427            r#"<q:scatterChart><q:scatterStyle val="lineMarker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:scatterChart>"#,
13428            sparse_scatter_series()
13429        );
13430        let mut scatter = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
13431        let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
13432        assert_eq!(geometry_children(&scatter_geometry).len(), 4);
13433        let gap_commands = path_commands(&scatter_geometry);
13434        assert_path_points(gap_commands[0], &[Point { x: 36.0, y: 112.0 }], false);
13435        assert_path_points(gap_commands[1], &[Point { x: 188.0, y: 12.0 }], false);
13436
13437        scatter.chart.disp_blanks_as = DispBlanksAs::Zero;
13438        let zero = render_geometry(&scatter.chart, chart_bounds()).unwrap();
13439        assert_path_points(
13440            path_commands(&zero)[0],
13441            &[
13442                Point { x: 74.0, y: 87.0 },
13443                Point { x: 36.0, y: 62.0 },
13444                Point { x: 150.0, y: 112.0 },
13445                Point { x: 188.0, y: 12.0 },
13446            ],
13447            false,
13448        );
13449
13450        scatter.chart.disp_blanks_as = DispBlanksAs::Span;
13451        assert_path_points(
13452            path_commands(&render_geometry(&scatter.chart, chart_bounds()).unwrap())[0],
13453            &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
13454            false,
13455        );
13456    }
13457
13458    #[test]
13459    fn sparse_scatter_domains_and_labels_use_only_matched_points() {
13460        let scatter = format!(
13461            r#"<q:scatterChart><q:scatterStyle val="lineMarker"/>{}<q:dLbls><q:showVal val="1"/></q:dLbls><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:scatterChart>"#,
13462            sparse_scatter_series()
13463        );
13464        let chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
13465        let plot = &chart.chart.plot_area.plots().unwrap()[0];
13466        assert_eq!(
13467            super::scatter_domains(plot, DispBlanksAs::Gap).unwrap(),
13468            (
13469                Domain {
13470                    min: 10.0,
13471                    max: 40.0,
13472                },
13473                Domain {
13474                    min: 100.0,
13475                    max: 400.0,
13476                },
13477            )
13478        );
13479        let axes = chart.chart.plot_area.axes().unwrap();
13480        let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
13481        let anchors =
13482            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
13483        assert_eq!(
13484            anchors.keys().copied().collect::<Vec<_>>(),
13485            vec![(0, 0), (0, 3)]
13486        );
13487        assert_eq!(anchors[&(0, 0)].center, Point { x: 0.0, y: 140.0 });
13488        assert_eq!(anchors[&(0, 3)].center, Point { x: 200.0, y: 0.0 });
13489    }
13490
13491    #[test]
13492    fn sparse_bubble_size_labels_resolve_logical_indexes() {
13493        let scatter = format!(
13494            r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:dLbls><q:showBubbleSize val="1"/></q:dLbls><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:scatterChart>"#,
13495            sparse_scatter_series()
13496        );
13497        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
13498        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13499        let Plot::Scatter { series, .. } = plot else {
13500            panic!("expected scatter plot");
13501        };
13502        series[0].bubble_size = Some(
13503            NumericData::from_xml(
13504                format!(
13505                    r#"<q:numRef xmlns:q="{C_NS}"><q:f>Sheet1!$C$2:$C$5</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="4"/><q:pt idx="0"><q:v>5</q:v></q:pt><q:pt idx="3"><q:v>9</q:v></q:pt></q:numCache></q:numRef>"#
13506                )
13507                .as_bytes(),
13508            )
13509            .unwrap(),
13510        );
13511        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13512        let mut rendered = Vec::new();
13513        super::render_data_labels(
13514            plot,
13515            chart_bounds(),
13516            DispBlanksAs::Gap,
13517            super::GeometryOptions::default(),
13518            &mut fonts,
13519            &mut rendered,
13520        )
13521        .unwrap();
13522        let text: Vec<_> = rendered
13523            .iter()
13524            .filter_map(|element| match element {
13525                PositionedElement::Text(run) => Some(run.text.as_str()),
13526                _ => None,
13527            })
13528            .collect();
13529        assert_eq!(text, vec!["5", "9"]);
13530    }
13531
13532    #[test]
13533    fn geometry_rejects_invalid_bounds_and_opaque_plots() {
13534        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13535        for bounds in [
13536            Rect {
13537                x: 0.0,
13538                y: 0.0,
13539                width: f64::NAN,
13540                height: 100.0,
13541            },
13542            Rect {
13543                x: 0.0,
13544                y: 0.0,
13545                width: 0.0,
13546                height: 100.0,
13547            },
13548            Rect {
13549                x: 0.0,
13550                y: 0.0,
13551                width: 40.0,
13552                height: 30.0,
13553            },
13554        ] {
13555            assert!(render_geometry(&chart.chart, bounds).is_err());
13556        }
13557
13558        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
13559            unreachable!()
13560        };
13561        series[0].values.values.clear();
13562        assert!(render_geometry(&chart.chart, chart_bounds()).is_err());
13563
13564        let opaque = CT_ChartSpace::from_xml(
13565            chart_with_plot(r#"<q:bar3DChart><q:barDir val="col"/></q:bar3DChart>"#).as_bytes(),
13566        )
13567        .unwrap();
13568        let error = render_geometry(&opaque.chart, chart_bounds()).unwrap_err();
13569        assert!(error.to_string().contains("unsupported or combination"));
13570
13571        let (scatter_plot, _) = remaining_plot_fixtures()
13572            .into_iter()
13573            .find(|(kind, _, _)| *kind == "scatterChart")
13574            .map(|(_, plot, axes)| (plot, axes))
13575            .unwrap();
13576        let mut scatter =
13577            CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
13578                .unwrap();
13579        for invalid in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
13580            let Plot::Scatter { series, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
13581            else {
13582                unreachable!()
13583            };
13584            let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
13585                unreachable!()
13586            };
13587            x_values.values[0] = invalid;
13588            let error = render_geometry(&scatter.chart, chart_bounds()).unwrap_err();
13589            assert!(error.to_string().contains("xVal"));
13590        }
13591    }
13592
13593    #[test]
13594    fn finite_extremes_never_produce_nonfinite_geometry() {
13595        let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
13596        let Plot::Line { series, .. } = &mut line.chart.plot_area.plots_mut().unwrap()[0] else {
13597            unreachable!()
13598        };
13599        series[0].values.values = vec![-f64::MAX, f64::MAX];
13600        let extreme_line = render_geometry(&line.chart, chart_bounds()).unwrap();
13601        assert_path_points(
13602            path_commands(&extreme_line)[0],
13603            &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
13604            false,
13605        );
13606
13607        let mut stacked =
13608            CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13609        let Plot::Bar {
13610            grouping, series, ..
13611        } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
13612        else {
13613            unreachable!()
13614        };
13615        *grouping = BarGrouping::Stacked;
13616        series[0].values.values = vec![f64::MAX, 1.0];
13617        let mut second = series[0].clone();
13618        second.index = 1;
13619        second.order = 1;
13620        series.push(second);
13621        let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
13622        assert!(error.to_string().contains("stacked value total"));
13623
13624        let Plot::Bar { grouping, .. } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
13625        else {
13626            unreachable!()
13627        };
13628        *grouping = BarGrouping::PercentStacked;
13629        let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
13630        assert!(error.to_string().contains("stacked percentage total"));
13631
13632        let (pie_plot, _) = remaining_plot_fixtures()
13633            .into_iter()
13634            .find(|(kind, _, _)| *kind == "pieChart")
13635            .map(|(_, plot, axes)| (plot, axes))
13636            .unwrap();
13637        let mut pie =
13638            CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot, false).as_bytes()).unwrap();
13639        let Plot::Pie { series, .. } = &mut pie.chart.plot_area.plots_mut().unwrap()[0] else {
13640            unreachable!()
13641        };
13642        series[0].values.values = vec![f64::MAX, f64::MAX];
13643        let error = render_geometry(&pie.chart, chart_bounds()).unwrap_err();
13644        assert!(error.to_string().contains("pie value total"));
13645
13646        let (scatter_plot, _) = remaining_plot_fixtures()
13647            .into_iter()
13648            .find(|(kind, _, _)| *kind == "scatterChart")
13649            .map(|(_, plot, axes)| (plot, axes))
13650            .unwrap();
13651        let mut scatter =
13652            CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
13653                .unwrap();
13654        let Plot::Scatter { style, series, .. } =
13655            &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
13656        else {
13657            unreachable!()
13658        };
13659        *style = ScatterStyle::Line;
13660        let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
13661            unreachable!()
13662        };
13663        x_values.values = vec![-f64::MAX, f64::MAX];
13664        series[0].values.values = vec![-f64::MAX, f64::MAX];
13665        let extreme_scatter = render_geometry(&scatter.chart, chart_bounds()).unwrap();
13666        assert_path_points(
13667            path_commands(&extreme_scatter)[0],
13668            &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
13669            false,
13670        );
13671    }
13672
13673    #[test]
13674    fn geometry_is_backend_neutral_and_deterministic() {
13675        let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13676        let first = render_geometry(&chart.chart, chart_bounds()).unwrap();
13677        let second = render_geometry(&chart.chart, chart_bounds()).unwrap();
13678        assert_eq!(
13679            format!("{:?}", first.elements),
13680            format!("{:?}", second.elements)
13681        );
13682
13683        let raster = |geometry: ChartGeometry| {
13684            let layout = LayoutResult::new(
13685                vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
13686                Vec::new(),
13687                None,
13688                Vec::new(),
13689            );
13690            oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
13691        };
13692        assert_eq!(raster(first), raster(second));
13693    }
13694
13695    #[test]
13696    fn zero_to_one_hundred_axis_uses_expected_ticks() {
13697        let scale = nice_number_scale(
13698            Domain {
13699                min: 0.0,
13700                max: 100.0,
13701            },
13702            None,
13703            None,
13704            6,
13705        )
13706        .unwrap();
13707        assert_eq!(scale.ticks, vec![0.0, 20.0, 40.0, 60.0, 80.0, 100.0]);
13708    }
13709
13710    #[test]
13711    fn nice_number_ticks_cover_unpinned_extents() {
13712        for domain in [
13713            Domain {
13714                min: 3.0,
13715                max: 97.0,
13716            },
13717            Domain {
13718                min: -97.0,
13719                max: -3.0,
13720            },
13721            Domain {
13722                min: -3.5,
13723                max: 8.25,
13724            },
13725            Domain {
13726                min: 0.125,
13727                max: 0.875,
13728            },
13729            Domain {
13730                min: 1.0e12,
13731                max: 7.0e12,
13732            },
13733            Domain {
13734                min: -f64::MAX,
13735                max: f64::MAX,
13736            },
13737            Domain {
13738                min: f64::from_bits(1),
13739                max: f64::from_bits(5),
13740            },
13741        ] {
13742            let scale = nice_number_scale(domain, None, None, 6).unwrap();
13743            assert!(scale.domain.min <= domain.min);
13744            assert!(scale.domain.max >= domain.max);
13745            assert!(scale.ticks.iter().all(|tick| tick.is_finite()));
13746            assert!(scale.ticks.windows(2).all(|pair| pair[0] < pair[1]));
13747        }
13748
13749        let constant = nice_number_scale(Domain { min: 4.0, max: 4.0 }, None, None, 6).unwrap();
13750        assert!(constant.domain.min < 4.0 && constant.domain.max > 4.0);
13751    }
13752
13753    #[test]
13754    fn one_sided_explicit_scale_beyond_data_derives_a_valid_opposite_bound() {
13755        let minimum = nice_number_scale(
13756            Domain {
13757                min: 0.0,
13758                max: 10.0,
13759            },
13760            Some(20.0),
13761            None,
13762            6,
13763        )
13764        .unwrap();
13765        assert_eq!(minimum.domain.min, 20.0);
13766        assert!(minimum.domain.max > 20.0);
13767        assert!(minimum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
13768
13769        let maximum = nice_number_scale(
13770            Domain {
13771                min: 0.0,
13772                max: 10.0,
13773            },
13774            None,
13775            Some(-20.0),
13776            6,
13777        )
13778        .unwrap();
13779        assert_eq!(maximum.domain.max, -20.0);
13780        assert!(maximum.domain.min < -20.0);
13781        assert!(maximum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
13782    }
13783
13784    #[test]
13785    fn standard_line_domain_does_not_force_zero() {
13786        let mut chart =
13787            CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
13788        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13789        let Plot::Line { series, .. } = plot else {
13790            panic!("expected line plot");
13791        };
13792        series[0].values.values = vec![1_000.0, 1_010.0];
13793        assert_eq!(
13794            super::plot_value_domain(plot, DispBlanksAs::Gap).unwrap(),
13795            Some(Domain {
13796                min: 1_000.0,
13797                max: 1_010.0,
13798            })
13799        );
13800        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
13801        assert_path_points(
13802            path_commands(&geometry)[0],
13803            &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
13804            false,
13805        );
13806    }
13807
13808    #[test]
13809    fn explicit_scaling_bounds_and_orientation_are_honored() {
13810        let scale = nice_number_scale(
13811            Domain {
13812                min: 25.0,
13813                max: 75.0,
13814            },
13815            Some(0.0),
13816            Some(100.0),
13817            6,
13818        )
13819        .unwrap();
13820        assert_eq!(
13821            scale.domain,
13822            Domain {
13823                min: 0.0,
13824                max: 100.0
13825            }
13826        );
13827        assert_eq!(
13828            super::map_y_oriented(0.0, scale.domain, chart_bounds(), Orientation::MaxMin).unwrap(),
13829            0.0
13830        );
13831        assert_eq!(
13832            super::map_y_oriented(100.0, scale.domain, chart_bounds(), Orientation::MaxMin)
13833                .unwrap(),
13834            140.0
13835        );
13836
13837        let mut chart = labelled_bar_chart();
13838        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
13839            panic!("expected bar plot");
13840        };
13841        series[0].values.values = vec![25.0, 75.0];
13842        let value_axis = chart
13843            .chart
13844            .plot_area
13845            .axes
13846            .iter_mut()
13847            .find(|axis| axis.kind == AxisKind::Value)
13848            .unwrap();
13849        value_axis.scaling.orientation = Orientation::MaxMin;
13850        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13851        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13852        let plot = group
13853            .children
13854            .iter()
13855            .find_map(|element| match element {
13856                PositionedElement::Group(group) => Some(group),
13857                _ => None,
13858            })
13859            .unwrap();
13860        let bars: Vec<_> = plot
13861            .children
13862            .iter()
13863            .filter_map(|element| match element {
13864                PositionedElement::Path(path) => path.path.bounds(),
13865                _ => None,
13866            })
13867            .collect();
13868        assert_eq!(bars.len(), 2);
13869        assert!((bars[0].y - 12.0).abs() < 1.0e-9);
13870        assert!((bars[0].height - 25.0).abs() < 1.0e-9);
13871        assert!((bars[1].height - 75.0).abs() < 1.0e-9);
13872    }
13873
13874    #[test]
13875    fn family_label_anchors_share_rendered_geometry() {
13876        let mut bar = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13877        let bar_plot = &mut bar.chart.plot_area.plots_mut().unwrap()[0];
13878        let Plot::Bar { series, .. } = bar_plot else {
13879            panic!("expected bar plot");
13880        };
13881        let mut second = series[0].clone();
13882        second.index = 1;
13883        second.order = 1;
13884        series.push(second);
13885        let anchors = super::plot_label_anchors(
13886            bar_plot,
13887            chart_bounds(),
13888            DispBlanksAs::Gap,
13889            super::GeometryOptions::default(),
13890        )
13891        .unwrap();
13892        assert_point_near(anchors[&(0, 0)].center, Point { x: 40.0, y: 105.0 });
13893        assert_point_near(anchors[&(1, 0)].center, Point { x: 60.0, y: 105.0 });
13894
13895        let pie = CT_ChartSpace::from_xml(
13896            chart_with_optional_axes(
13897                &remaining_plot_fixtures()
13898                    .into_iter()
13899                    .find(|(kind, _, _)| *kind == "pieChart")
13900                    .unwrap()
13901                    .1,
13902                false,
13903            )
13904            .as_bytes(),
13905        )
13906        .unwrap();
13907        let Plot::Pie {
13908            first_slice_angle,
13909            series,
13910            ..
13911        } = &pie.chart.plot_area.plots().unwrap()[0]
13912        else {
13913            panic!("expected pie plot");
13914        };
13915        let slices = super::pie_slices(*first_slice_angle, None, series, chart_bounds()).unwrap();
13916        assert_eq!(slices.len(), 2);
13917        let first_span = std::f64::consts::TAU / 3.0;
13918        let first_midpoint = f64::from(*first_slice_angle).to_radians()
13919            - std::f64::consts::FRAC_PI_2
13920            + first_span / 2.0;
13921        assert_point_near(
13922            slices[0].anchor,
13923            super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, first_midpoint),
13924        );
13925        let second_midpoint = first_midpoint + std::f64::consts::PI;
13926        assert_point_near(
13927            slices[1].anchor,
13928            super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, second_midpoint),
13929        );
13930
13931        let radar = format!(
13932            r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
13933            plot_series(0)
13934        );
13935        let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
13936        let Plot::Radar { series, .. } = &radar.chart.plot_area.plots().unwrap()[0] else {
13937            panic!("expected radar plot");
13938        };
13939        let radar_points =
13940            super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
13941                .unwrap();
13942        assert_eq!(radar_points[0][0], (0, Point { x: 100.0, y: 35.0 }));
13943        assert_eq!(radar_points[0][1], (1, Point { x: 100.0, y: 140.0 }));
13944    }
13945
13946    #[test]
13947    fn radar_annotations_use_spokes_perimeter_labels_and_radial_gridlines() {
13948        let radar_plot = format!(
13949            r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
13950            sparse_category_series()
13951        );
13952        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
13953        for axis in &mut chart.chart.plot_area.axes {
13954            axis.major_tick_mark = TickMark::Cross;
13955            if axis.kind == AxisKind::Value {
13956                axis.major_gridlines = Some(super::ChartLines::default());
13957            }
13958        }
13959        let plot = &chart.chart.plot_area.plots().unwrap()[0];
13960        let axes = chart.chart.plot_area.axes().unwrap();
13961        let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
13962        let center = Point { x: 112.0, y: 62.0 };
13963        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13964        let annotations =
13965            super::render_axes_and_labels(plot, &axes, bounds, DispBlanksAs::Gap, &mut fonts)
13966                .unwrap();
13967        assert_eq!(annotations.axis_lines.len(), 4);
13968        for spoke in annotations.axis_lines.iter().take(3) {
13969            let PositionedElement::Path(spoke) = spoke else {
13970                panic!("radar spoke must be a path");
13971            };
13972            assert_eq!(
13973                spoke.path.commands.first(),
13974                Some(&PathCommand::MoveTo(center))
13975            );
13976        }
13977        assert!(!annotations.gridlines.is_empty());
13978        for gridline in &annotations.gridlines {
13979            let PositionedElement::Path(gridline) = gridline else {
13980                panic!("radar gridline must be a path");
13981            };
13982            assert_eq!(gridline.path.commands.last(), Some(&PathCommand::Close));
13983            let radii: Vec<_> = gridline
13984                .path
13985                .commands
13986                .iter()
13987                .filter_map(|command| match command {
13988                    PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
13989                        Some((point.x - center.x).hypot(point.y - center.y))
13990                    }
13991                    _ => None,
13992                })
13993                .collect();
13994            assert_eq!(radii.len(), 3);
13995            assert!(
13996                radii
13997                    .windows(2)
13998                    .all(|pair| (pair[0] - pair[1]).abs() < 1.0e-9)
13999            );
14000        }
14001        let text: Vec<_> = annotations
14002            .labels
14003            .iter()
14004            .filter_map(|element| match element {
14005                PositionedElement::Text(run) => Some(run.text.as_str()),
14006                _ => None,
14007            })
14008            .collect();
14009        assert!(text.contains(&"North"));
14010        assert!(text.contains(&"West"));
14011    }
14012
14013    #[test]
14014    fn radar_tick_label_positions_are_distinct() {
14015        let radar_plot = format!(
14016            r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
14017            plot_series(0)
14018        );
14019        let chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
14020        let plot = &chart.chart.plot_area.plots().unwrap()[0];
14021        let axes = chart.chart.plot_area.axes().unwrap();
14022        let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
14023        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14024
14025        let category_axis = axes
14026            .iter()
14027            .find(|axis| axis.kind == AxisKind::Category)
14028            .unwrap();
14029        for (position, expected) in [
14030            (TickLabelPosition::High, Point { x: 108.0, y: 0.0 }),
14031            (TickLabelPosition::Low, Point { x: 108.0, y: 25.0 }),
14032            (TickLabelPosition::NextTo, Point { x: 108.0, y: 5.0 }),
14033        ] {
14034            let mut axis = category_axis.clone();
14035            axis.tick_label_position = position;
14036            let annotations =
14037                super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
14038                    .unwrap();
14039            let north = annotations
14040                .labels
14041                .iter()
14042                .find_map(|element| match element {
14043                    PositionedElement::Text(run) if run.text == "North" => Some(run),
14044                    _ => None,
14045                })
14046                .unwrap();
14047            assert_point_near(north.origin, expected);
14048        }
14049        let mut hidden_category = category_axis.clone();
14050        hidden_category.tick_label_position = TickLabelPosition::None;
14051        let annotations = super::render_axes_and_labels(
14052            plot,
14053            &[hidden_category],
14054            bounds,
14055            DispBlanksAs::Gap,
14056            &mut fonts,
14057        )
14058        .unwrap();
14059        assert!(annotations.labels.is_empty());
14060
14061        let narrow_bounds = Rect {
14062            x: 36.0,
14063            y: 12.0,
14064            width: 24.0,
14065            height: 100.0,
14066        };
14067        let narrow_center = Point { x: 48.0, y: 62.0 };
14068        assert_eq!(
14069            super::radar_category_label_origin(
14070                narrow_bounds,
14071                narrow_center,
14072                12.0,
14073                1,
14074                4,
14075                TickLabelPosition::High,
14076            ),
14077            Some(Point { x: 72.0, y: 65.0 })
14078        );
14079        assert_eq!(
14080            super::radar_category_label_origin(
14081                narrow_bounds,
14082                narrow_center,
14083                12.0,
14084                1,
14085                4,
14086                TickLabelPosition::NextTo,
14087            ),
14088            Some(Point { x: 66.0, y: 65.0 })
14089        );
14090
14091        let value_axis = axes
14092            .iter()
14093            .find(|axis| axis.kind == AxisKind::Value)
14094            .unwrap();
14095        for (position, expected) in [
14096            (TickLabelPosition::High, Point { x: 166.0, y: 65.0 }),
14097            (TickLabelPosition::Low, Point { x: 32.0, y: 65.0 }),
14098            (TickLabelPosition::NextTo, Point { x: 116.0, y: 65.0 }),
14099        ] {
14100            let mut axis = value_axis.clone();
14101            axis.tick_label_position = position;
14102            let annotations =
14103                super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
14104                    .unwrap();
14105            let zero = annotations
14106                .labels
14107                .iter()
14108                .find_map(|element| match element {
14109                    PositionedElement::Text(run) if run.text == "0" => Some(run),
14110                    _ => None,
14111                })
14112                .unwrap();
14113            assert_point_near(zero.origin, expected);
14114        }
14115        let mut hidden_value = value_axis.clone();
14116        hidden_value.tick_label_position = TickLabelPosition::None;
14117        let annotations = super::render_axes_and_labels(
14118            plot,
14119            &[hidden_value],
14120            bounds,
14121            DispBlanksAs::Gap,
14122            &mut fonts,
14123        )
14124        .unwrap();
14125        assert!(annotations.labels.is_empty());
14126    }
14127
14128    #[test]
14129    fn radar_negative_and_mixed_sign_domains_preserve_values() {
14130        let radar_plot = format!(
14131            r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
14132            plot_series(0)
14133        );
14134        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
14135        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14136        let Plot::Radar { series, .. } = plot else {
14137            panic!("expected radar plot");
14138        };
14139        series[0].values.values = vec![-10.0, -5.0];
14140        let points =
14141            super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
14142                .unwrap();
14143        assert_eq!(points[0][0], (0, Point { x: 100.0, y: 70.0 }));
14144        assert_eq!(points[0][1], (1, Point { x: 100.0, y: 105.0 }));
14145
14146        series[0].values.values = vec![-5.0, 10.0];
14147        let options = super::GeometryOptions {
14148            value: Some(super::AxisMapping {
14149                domain: Domain {
14150                    min: -10.0,
14151                    max: 10.0,
14152                },
14153                orientation: Orientation::MinMax,
14154            }),
14155            ..super::GeometryOptions::default()
14156        };
14157        let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
14158        assert_eq!(points[0][0], (0, Point { x: 100.0, y: 52.5 }));
14159        assert_eq!(points[0][1], (1, Point { x: 100.0, y: 140.0 }));
14160    }
14161
14162    #[test]
14163    fn radar_explicit_bounds_suppress_out_of_range_points_and_labels() {
14164        let radar_plot = format!(
14165            r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:dLbls><q:showVal val="1"/></q:dLbls><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
14166            plot_series(0)
14167        );
14168        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
14169        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14170        let Plot::Radar { series, .. } = plot else {
14171            panic!("expected radar plot");
14172        };
14173        series[0].values.values = vec![25.0, 75.0];
14174        let options = super::GeometryOptions {
14175            value: Some(super::AxisMapping {
14176                domain: Domain {
14177                    min: 50.0,
14178                    max: 100.0,
14179                },
14180                orientation: Orientation::MinMax,
14181            }),
14182            ..super::GeometryOptions::default()
14183        };
14184        let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
14185        assert_eq!(points[0], vec![(1, Point { x: 100.0, y: 105.0 })]);
14186        let anchors =
14187            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
14188        assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 1)]);
14189
14190        let Plot::Radar { series, .. } = plot else {
14191            panic!("expected radar plot");
14192        };
14193        series[0].values.values = vec![75.0, 125.0];
14194        let reversed = super::GeometryOptions {
14195            value: Some(super::AxisMapping {
14196                domain: Domain {
14197                    min: 50.0,
14198                    max: 100.0,
14199                },
14200                orientation: Orientation::MaxMin,
14201            }),
14202            ..super::GeometryOptions::default()
14203        };
14204        let points = super::radar_series_points(series, chart_bounds(), reversed).unwrap();
14205        assert_eq!(points[0], vec![(0, Point { x: 100.0, y: 35.0 })]);
14206        let anchors =
14207            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
14208        assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
14209
14210        let value_axis = chart
14211            .chart
14212            .plot_area
14213            .axes
14214            .iter_mut()
14215            .find(|axis| axis.kind == AxisKind::Value)
14216            .unwrap();
14217        value_axis.scaling.minimum = Some(50.0);
14218        value_axis.scaling.maximum = Some(100.0);
14219        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14220        let Plot::Radar { series, .. } = plot else {
14221            panic!("expected radar plot");
14222        };
14223        series[0].values.values = vec![25.0, 25.0];
14224        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14225        let rendered = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
14226        assert!(rendered.children.iter().any(|element| matches!(
14227            element,
14228            PositionedElement::Group(group) if group.clip.is_some() && group.children.is_empty()
14229        )));
14230        assert!(
14231            rendered
14232                .children
14233                .iter()
14234                .any(|element| matches!(element, PositionedElement::Path(_)))
14235        );
14236        assert!(!rendered.children.iter().any(|element| matches!(
14237            element,
14238            PositionedElement::Text(run) if run.text == "25"
14239        )));
14240    }
14241
14242    #[test]
14243    fn inside_and_outside_label_positions_follow_family_geometry() {
14244        let mut horizontal =
14245            CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14246        let plot = &mut horizontal.chart.plot_area.plots_mut().unwrap()[0];
14247        let Plot::Bar { direction, .. } = plot else {
14248            panic!("expected bar plot");
14249        };
14250        *direction = BarDirection::Bar;
14251        let anchors = super::plot_label_anchors(
14252            plot,
14253            chart_bounds(),
14254            DispBlanksAs::Gap,
14255            super::GeometryOptions::default(),
14256        )
14257        .unwrap();
14258        let anchor = anchors[&(0, 0)];
14259        assert_point_near(
14260            anchor.origin(Some(DataLabelPosition::InsideBase)),
14261            Point { x: 5.0, y: 35.0 },
14262        );
14263        assert_point_near(
14264            anchor.origin(Some(DataLabelPosition::InsideEnd)),
14265            Point { x: 95.0, y: 35.0 },
14266        );
14267        assert_point_near(
14268            anchor.origin(Some(DataLabelPosition::OutsideEnd)),
14269            Point { x: 105.0, y: 35.0 },
14270        );
14271
14272        let mut negative =
14273            CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14274        let plot = &mut negative.chart.plot_area.plots_mut().unwrap()[0];
14275        let Plot::Bar { series, .. } = plot else {
14276            panic!("expected bar plot");
14277        };
14278        series[0].values.values = vec![-1.0, -2.0];
14279        let anchors = super::plot_label_anchors(
14280            plot,
14281            chart_bounds(),
14282            DispBlanksAs::Gap,
14283            super::GeometryOptions::default(),
14284        )
14285        .unwrap();
14286        assert_point_near(
14287            anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
14288            Point { x: 50.0, y: 75.0 },
14289        );
14290        let reversed = super::GeometryOptions {
14291            value: Some(super::AxisMapping {
14292                domain: Domain {
14293                    min: -2.0,
14294                    max: 0.0,
14295                },
14296                orientation: Orientation::MaxMin,
14297            }),
14298            ..super::GeometryOptions::default()
14299        };
14300        let anchors =
14301            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
14302        assert_point_near(
14303            anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
14304            Point { x: 50.0, y: 65.0 },
14305        );
14306
14307        let pie = CT_ChartSpace::from_xml(
14308            chart_with_optional_axes(
14309                &remaining_plot_fixtures()
14310                    .into_iter()
14311                    .find(|(kind, _, _)| *kind == "pieChart")
14312                    .unwrap()
14313                    .1,
14314                false,
14315            )
14316            .as_bytes(),
14317        )
14318        .unwrap();
14319        let Plot::Pie { series, .. } = &pie.chart.plot_area.plots().unwrap()[0] else {
14320            panic!("expected pie plot");
14321        };
14322        let pie_slice = &super::pie_slices(30, None, series, chart_bounds()).unwrap()[0];
14323        assert_point_near(
14324            super::LabelAnchor::segment(pie_slice.anchor, pie_slice.base, pie_slice.end)
14325                .origin(Some(DataLabelPosition::OutsideEnd)),
14326            Point { x: 175.0, y: 70.0 },
14327        );
14328        let doughnut_slice = &super::pie_slices(30, Some(60), series, chart_bounds()).unwrap()[0];
14329        assert_point_near(
14330            super::LabelAnchor::segment(
14331                doughnut_slice.anchor,
14332                doughnut_slice.base,
14333                doughnut_slice.end,
14334            )
14335            .origin(Some(DataLabelPosition::InsideBase)),
14336            Point { x: 147.0, y: 70.0 },
14337        );
14338
14339        let short = super::LabelAnchor::segment(
14340            Point { x: 2.0, y: 0.0 },
14341            Point { x: 0.0, y: 0.0 },
14342            Point { x: 4.0, y: 0.0 },
14343        );
14344        assert_eq!(
14345            short.origin(Some(DataLabelPosition::InsideBase)),
14346            Point { x: 2.0, y: 0.0 }
14347        );
14348        assert_eq!(
14349            short.origin(Some(DataLabelPosition::InsideEnd)),
14350            Point { x: 2.0, y: 0.0 }
14351        );
14352        let thin_bounds = Rect {
14353            x: 0.0,
14354            y: 0.0,
14355            width: 80.0,
14356            height: 80.0,
14357        };
14358        let thin = &super::pie_slices(30, Some(90), series, thin_bounds).unwrap()[0];
14359        let thin = super::LabelAnchor::segment(thin.anchor, thin.base, thin.end);
14360        assert_point_near(
14361            thin.origin(Some(DataLabelPosition::InsideBase)),
14362            Point { x: 78.0, y: 40.0 },
14363        );
14364        assert_point_near(
14365            thin.origin(Some(DataLabelPosition::InsideEnd)),
14366            Point { x: 78.0, y: 40.0 },
14367        );
14368    }
14369
14370    #[test]
14371    fn degenerate_bar_and_radar_anchors_preserve_family_direction() {
14372        let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14373        let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
14374        let Plot::Bar { series, .. } = plot else {
14375            panic!("expected bar plot");
14376        };
14377        series[0].values.values = vec![80.0, 125.0];
14378        for (direction, orientation, boundary, outside) in [
14379            (
14380                BarDirection::Column,
14381                Orientation::MinMax,
14382                Point { x: 50.0, y: 140.0 },
14383                Point { x: 50.0, y: 135.0 },
14384            ),
14385            (
14386                BarDirection::Column,
14387                Orientation::MaxMin,
14388                Point { x: 50.0, y: 0.0 },
14389                Point { x: 50.0, y: 5.0 },
14390            ),
14391            (
14392                BarDirection::Bar,
14393                Orientation::MinMax,
14394                Point { x: 0.0, y: 35.0 },
14395                Point { x: 5.0, y: 35.0 },
14396            ),
14397            (
14398                BarDirection::Bar,
14399                Orientation::MaxMin,
14400                Point { x: 200.0, y: 35.0 },
14401                Point { x: 195.0, y: 35.0 },
14402            ),
14403        ] {
14404            let Plot::Bar {
14405                direction: plot_direction,
14406                ..
14407            } = plot
14408            else {
14409                panic!("expected bar plot");
14410            };
14411            *plot_direction = direction;
14412            let options = super::GeometryOptions {
14413                value: Some(super::AxisMapping {
14414                    domain: Domain {
14415                        min: 80.0,
14416                        max: 100.0,
14417                    },
14418                    orientation,
14419                }),
14420                ..super::GeometryOptions::default()
14421            };
14422            let anchors =
14423                super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options)
14424                    .unwrap();
14425            let anchor = anchors[&(0, 0)];
14426            assert_point_near(anchor.center, boundary);
14427            assert_point_near(anchor.base, boundary);
14428            assert_point_near(anchor.end, boundary);
14429            assert_point_near(anchor.origin(Some(DataLabelPosition::InsideBase)), boundary);
14430            assert_point_near(anchor.origin(Some(DataLabelPosition::InsideEnd)), boundary);
14431            assert_point_near(anchor.origin(Some(DataLabelPosition::OutsideEnd)), outside);
14432        }
14433
14434        let radar_plot = format!(
14435            r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
14436            plot_series(0)
14437        );
14438        let mut radar = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
14439        let plot = &mut radar.chart.plot_area.plots_mut().unwrap()[0];
14440        let Plot::Radar { series, .. } = plot else {
14441            panic!("expected radar plot");
14442        };
14443        series[0].values.values = vec![0.0, 0.0, 0.0, 0.0];
14444        let anchors = super::plot_label_anchors(
14445            plot,
14446            chart_bounds(),
14447            DispBlanksAs::Gap,
14448            super::GeometryOptions::default(),
14449        )
14450        .unwrap();
14451        let center = Point { x: 100.0, y: 70.0 };
14452        for logical_index in 0..4 {
14453            assert_point_near(
14454                anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideBase)),
14455                center,
14456            );
14457            assert_point_near(
14458                anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideEnd)),
14459                center,
14460            );
14461        }
14462        assert_point_near(
14463            anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
14464            Point { x: 100.0, y: 65.0 },
14465        );
14466        assert_point_near(
14467            anchors[&(0, 1)].origin(Some(DataLabelPosition::OutsideEnd)),
14468            Point { x: 105.0, y: 70.0 },
14469        );
14470        assert_point_near(
14471            anchors[&(0, 2)].origin(Some(DataLabelPosition::OutsideEnd)),
14472            Point { x: 100.0, y: 75.0 },
14473        );
14474        assert_point_near(
14475            anchors[&(0, 3)].origin(Some(DataLabelPosition::OutsideEnd)),
14476            Point { x: 95.0, y: 70.0 },
14477        );
14478    }
14479
14480    #[test]
14481    fn labels_outside_explicit_axis_bounds_are_suppressed() {
14482        let mut chart =
14483            CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
14484        {
14485            let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14486            let Plot::Line { series, .. } = plot else {
14487                panic!("expected line plot");
14488            };
14489            series[0].values.values = vec![25.0, 75.0];
14490        }
14491        let value_axis = chart
14492            .chart
14493            .plot_area
14494            .axes
14495            .iter_mut()
14496            .find(|axis| axis.kind == AxisKind::Value)
14497            .unwrap();
14498        value_axis.scaling.minimum = Some(50.0);
14499        value_axis.scaling.maximum = Some(100.0);
14500        value_axis.scaling.orientation = Orientation::MaxMin;
14501        let plot = &chart.chart.plot_area.plots().unwrap()[0];
14502        let axes = chart.chart.plot_area.axes().unwrap();
14503        let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
14504        let anchors =
14505            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
14506        assert!(!anchors.contains_key(&(0, 0)));
14507        assert_point_near(anchors[&(0, 1)].center, Point { x: 150.0, y: 70.0 });
14508
14509        let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14510        let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
14511        let Plot::Bar { series, .. } = plot else {
14512            panic!("expected bar plot");
14513        };
14514        series[0].values.values = vec![90.0, 125.0];
14515        let options = super::GeometryOptions {
14516            value: Some(super::AxisMapping {
14517                domain: Domain {
14518                    min: 80.0,
14519                    max: 100.0,
14520                },
14521                orientation: Orientation::MinMax,
14522            }),
14523            ..super::GeometryOptions::default()
14524        };
14525        let anchors =
14526            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
14527        assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
14528        assert_point_near(anchors[&(0, 0)].base, Point { x: 50.0, y: 140.0 });
14529        assert_point_near(anchors[&(0, 0)].end, Point { x: 50.0, y: 70.0 });
14530        assert_point_near(anchors[&(0, 0)].center, Point { x: 50.0, y: 105.0 });
14531        assert_point_near(
14532            anchors[&(0, 0)].origin(Some(DataLabelPosition::InsideBase)),
14533            Point { x: 50.0, y: 135.0 },
14534        );
14535    }
14536
14537    #[test]
14538    fn zero_bars_render_and_emit_requested_value_labels() {
14539        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14540        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14541        let Plot::Bar { series, .. } = plot else {
14542            panic!("expected bar plot");
14543        };
14544        series[0].values.values = vec![0.0, 0.0];
14545        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
14546        assert_eq!(path_commands(&geometry).len(), 2);
14547        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14548        render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
14549        let plot = &chart.chart.plot_area.plots().unwrap()[0];
14550        let mut labels = Vec::new();
14551        super::render_data_labels(
14552            plot,
14553            chart_bounds(),
14554            DispBlanksAs::Gap,
14555            super::GeometryOptions::default(),
14556            &mut fonts,
14557            &mut labels,
14558        )
14559        .unwrap();
14560        let text: Vec<_> = labels
14561            .iter()
14562            .filter_map(|element| match element {
14563                PositionedElement::Text(run) => Some(run.text.as_str()),
14564                _ => None,
14565            })
14566            .collect();
14567        assert_eq!(text, vec!["0", "0"]);
14568    }
14569
14570    #[test]
14571    fn axes_gridlines_and_tick_marks_follow_model_state() {
14572        let chart = labelled_bar_chart();
14573        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14574        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
14575        let ranks: Vec<_> = group
14576            .children
14577            .iter()
14578            .map(|element| match element {
14579                PositionedElement::Group(group) => {
14580                    assert!(group.clip.is_some());
14581                    1
14582                }
14583                PositionedElement::Path(path)
14584                    if path
14585                        .stroke
14586                        .as_ref()
14587                        .is_some_and(|stroke| stroke.width == 0.5) =>
14588                {
14589                    0
14590                }
14591                PositionedElement::Path(path) if path.fill.is_some() => 4,
14592                PositionedElement::Path(path) => {
14593                    let bounds = path.path.bounds().expect("axis or tick bounds");
14594                    assert_eq!(path.stroke.as_ref().map(|stroke| stroke.width), Some(1.0));
14595                    if bounds.width > 4.0 || bounds.height > 4.0 {
14596                        2
14597                    } else {
14598                        3
14599                    }
14600                }
14601                PositionedElement::Text(_) => 5,
14602                _ => panic!("unexpected labelled chart element"),
14603            })
14604            .collect();
14605        assert!(ranks.windows(2).all(|pair| pair[0] <= pair[1]), "{ranks:?}");
14606        for required in 0..=5 {
14607            assert!(
14608                ranks.contains(&required),
14609                "missing z-order rank {required}: {ranks:?}"
14610            );
14611        }
14612        assert_eq!(ranks.iter().filter(|rank| **rank == 1).count(), 1);
14613        assert_eq!(ranks.iter().filter(|rank| **rank == 2).count(), 2);
14614
14615        let mut deleted = chart.clone();
14616        for axis in &mut deleted.chart.plot_area.axes {
14617            axis.deleted = true;
14618        }
14619        let deleted_group = render_chart(&deleted.chart, chart_bounds(), &mut fonts).unwrap();
14620        assert_eq!(
14621            deleted_group
14622                .children
14623                .iter()
14624                .filter(|element| {
14625                    matches!(
14626                        element,
14627                        PositionedElement::Path(path) if path.stroke.is_some()
14628                    )
14629                })
14630                .count(),
14631            0
14632        );
14633    }
14634
14635    #[test]
14636    fn category_ticks_cover_unlabelled_and_sparse_logical_slots() {
14637        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14638        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
14639            panic!("expected bar plot");
14640        };
14641        series[0].categories = None;
14642        let plot = &chart.chart.plot_area.plots().unwrap()[0];
14643        let mut category_axis = chart
14644            .chart
14645            .plot_area
14646            .axes()
14647            .unwrap()
14648            .into_iter()
14649            .find(|axis| axis.kind == AxisKind::Category)
14650            .unwrap();
14651        category_axis.major_gridlines = Some(super::ChartLines::default());
14652        category_axis.major_tick_mark = TickMark::Cross;
14653        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14654        let annotations = super::render_axes_and_labels(
14655            plot,
14656            &[category_axis],
14657            chart_bounds(),
14658            DispBlanksAs::Gap,
14659            &mut fonts,
14660        )
14661        .unwrap();
14662        assert_eq!(annotations.gridlines.len(), 2);
14663        assert_eq!(annotations.ticks.len(), 2);
14664        assert!(annotations.labels.is_empty());
14665
14666        let sparse_plot = format!(
14667            r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{}<q:gapWidth val="150"/><q:overlap val="0"/><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:barChart>"#,
14668            sparse_category_series()
14669        );
14670        let sparse = CT_ChartSpace::from_xml(chart_with_plot(&sparse_plot).as_bytes()).unwrap();
14671        let plot = &sparse.chart.plot_area.plots().unwrap()[0];
14672        let mut category_axis = sparse
14673            .chart
14674            .plot_area
14675            .axes()
14676            .unwrap()
14677            .into_iter()
14678            .find(|axis| axis.kind == AxisKind::Category)
14679            .unwrap();
14680        category_axis.major_gridlines = Some(super::ChartLines::default());
14681        category_axis.major_tick_mark = TickMark::Cross;
14682        let annotations = super::render_axes_and_labels(
14683            plot,
14684            &[category_axis],
14685            chart_bounds(),
14686            DispBlanksAs::Gap,
14687            &mut fonts,
14688        )
14689        .unwrap();
14690        assert_eq!(annotations.gridlines.len(), 3);
14691        assert_eq!(annotations.ticks.len(), 3);
14692        assert_eq!(annotations.labels.len(), 2);
14693
14694        let oversized_plot = sparse_plot.replacen(
14695            r#"<q:ptCount val="3"/>"#,
14696            r#"<q:ptCount val="4294967295"/>"#,
14697            1,
14698        );
14699        let oversized =
14700            CT_ChartSpace::from_xml(chart_with_plot(&oversized_plot).as_bytes()).unwrap();
14701        let plot = &oversized.chart.plot_area.plots().unwrap()[0];
14702        let category_axis = oversized
14703            .chart
14704            .plot_area
14705            .axes()
14706            .unwrap()
14707            .into_iter()
14708            .find(|axis| axis.kind == AxisKind::Category)
14709            .unwrap();
14710        let result = super::render_axes_and_labels(
14711            plot,
14712            &[category_axis],
14713            chart_bounds(),
14714            DispBlanksAs::Gap,
14715            &mut fonts,
14716        );
14717        let error = match result {
14718            Err(error) => error,
14719            Ok(_) => panic!("oversized category annotations rendered"),
14720        };
14721        assert!(
14722            error
14723                .to_string()
14724                .contains("logical category count 4294967295")
14725        );
14726        assert!(error.to_string().contains("exceeds 16384"));
14727    }
14728
14729    #[test]
14730    fn unsupported_numeric_category_formats_return_projection_errors() {
14731        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14732        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
14733            panic!("expected bar plot");
14734        };
14735        series[0].categories = Some(AxisData::Numeric(
14736            NumericData::new(
14737                "Sheet1!$A$2:$A$3".to_owned(),
14738                "General".to_owned(),
14739                vec![1.0, 2.0],
14740            )
14741            .unwrap(),
14742        ));
14743        let plot = &chart.chart.plot_area.plots().unwrap()[0];
14744        let mut category_axis = chart
14745            .chart
14746            .plot_area
14747            .axes()
14748            .unwrap()
14749            .into_iter()
14750            .find(|axis| axis.kind == AxisKind::Category)
14751            .unwrap();
14752        category_axis.number_format = Some(NumberFormat::new("0.00".to_owned(), false).unwrap());
14753        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14754        let annotations = super::render_axes_and_labels(
14755            plot,
14756            &[category_axis.clone()],
14757            chart_bounds(),
14758            DispBlanksAs::Gap,
14759            &mut fonts,
14760        )
14761        .unwrap();
14762        let text: Vec<_> = annotations
14763            .labels
14764            .iter()
14765            .filter_map(|element| match element {
14766                PositionedElement::Text(run) => Some(run.text.as_str()),
14767                _ => None,
14768            })
14769            .collect();
14770        assert_eq!(text, vec!["1.00", "2.00"]);
14771
14772        category_axis.number_format = Some(NumberFormat::new("#,##0".to_owned(), false).unwrap());
14773        let result = super::render_axes_and_labels(
14774            plot,
14775            &[category_axis],
14776            chart_bounds(),
14777            DispBlanksAs::Gap,
14778            &mut fonts,
14779        );
14780        let error = match result {
14781            Err(error) => error,
14782            Ok(_) => panic!("unsupported category-axis format rendered"),
14783        };
14784        assert!(
14785            error
14786                .to_string()
14787                .contains("c:numFmt/@formatCode projection")
14788        );
14789        assert!(error.to_string().contains("#,##0"));
14790    }
14791
14792    #[test]
14793    fn labels_and_legend_shape_with_deterministic_fonts() {
14794        let chart = labelled_bar_chart();
14795        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14796        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
14797        let labels: Vec<_> = group
14798            .children
14799            .iter()
14800            .filter_map(|element| match element {
14801                PositionedElement::Text(run) => Some(run),
14802                _ => None,
14803            })
14804            .collect();
14805        for text in ["North", "South", "20", "Sales", "100"] {
14806            assert!(labels.iter().any(|run| run.text == text), "missing {text}");
14807        }
14808        let styled_tick = labels.iter().find(|run| run.text == "20").unwrap();
14809        assert_eq!(styled_tick.font_size, 12.0);
14810        assert!(styled_tick.bold);
14811        assert!(
14812            labels
14813                .iter()
14814                .all(|run| !run.glyph_ids.is_empty() && run.glyph_ids.len() == run.advances.len())
14815        );
14816    }
14817
14818    #[test]
14819    fn labelled_chart_raster_is_deterministic() {
14820        let raster = || {
14821            let chart = labelled_bar_chart();
14822            let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14823            let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
14824            let layout = LayoutResult::new(
14825                vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
14826                fonts.all_font_data(),
14827                None,
14828                Vec::new(),
14829            );
14830            oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
14831        };
14832        assert_eq!(raster(), raster());
14833    }
14834
14835    fn labelled_bar_chart() -> CT_ChartSpace {
14836        let series = plot_series(0)
14837            .replace(
14838                "<q:cat>",
14839                "<q:tx><q:strRef><q:f>Sheet1!$B$1</q:f><q:strCache><q:ptCount val=\"1\"/><q:pt idx=\"0\"><q:v>Sales</q:v></q:pt></q:strCache></q:strRef></q:tx><q:cat>",
14840            )
14841            .replace("<q:v>1</q:v>", "<q:v>0</q:v>")
14842            .replace("<q:v>2</q:v>", "<q:v>100</q:v>");
14843        let plot = format!(
14844            r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{series}<q:dLbls><q:numFmt formatCode="0" sourceLinked="0"/><q:showVal val="1"/></q:dLbls><q:gapWidth val="150"/><q:overlap val="0"/><q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:barChart>"#
14845        );
14846        let xml = chart_with_plot(&plot)
14847            .replacen(
14848                "<q:scaling/>",
14849                "<q:scaling><q:orientation val=\"minMax\"/></q:scaling>",
14850                1,
14851            )
14852            .replacen(
14853                "<q:scaling/>",
14854                "<q:scaling><q:max val=\"100\"/><q:min val=\"0\"/></q:scaling>",
14855                1,
14856            )
14857            .replace(
14858                "<q:axPos val=\"b\"/>",
14859                "<q:axPos val=\"b\"/><q:majorTickMark val=\"cross\"/>",
14860            )
14861            .replace(
14862                "<q:axPos val=\"l\"/>",
14863                "<q:axPos val=\"l\"/><q:majorGridlines/><q:numFmt formatCode=\"0\" sourceLinked=\"0\"/><q:majorTickMark val=\"out\"/><q:txPr><a:bodyPr/><a:p><a:pPr><a:defRPr sz=\"1200\" b=\"1\"><a:latin typeface=\"Carlito\"/></a:defRPr></a:pPr></a:p></q:txPr>",
14864            )
14865            .replace("</q:plotArea></q:chart>", "</q:plotArea><q:legend/></q:chart>");
14866        CT_ChartSpace::from_xml(xml.as_bytes()).unwrap()
14867    }
14868
14869    fn chart_bounds() -> Rect {
14870        Rect {
14871            x: 0.0,
14872            y: 0.0,
14873            width: 200.0,
14874            height: 140.0,
14875        }
14876    }
14877
14878    fn four_series_bar_chart() -> CT_ChartSpace {
14879        let series = (0..4).map(plot_series).collect::<String>();
14880        let plot = bar_plot("").replace(&plot_series(0), &series);
14881        CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
14882    }
14883
14884    fn seven_series_bar_chart() -> CT_ChartSpace {
14885        let series = (0..7).map(plot_series).collect::<String>();
14886        let plot = bar_plot("").replace(&plot_series(0), &series);
14887        CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
14888    }
14889
14890    fn path_colours(geometry: &ChartGeometry) -> Vec<Color> {
14891        let mut colours = geometry_children(geometry)
14892            .iter()
14893            .filter_map(|element| match element {
14894                PositionedElement::Path(path) => match path
14895                    .fill
14896                    .as_ref()
14897                    .or_else(|| path.stroke.as_ref().map(|stroke| &stroke.paint))
14898                {
14899                    Some(Paint::Solid(colour)) => Some(*colour),
14900                    _ => None,
14901                },
14902                _ => None,
14903            })
14904            .collect::<Vec<_>>();
14905        colours.dedup_by(|left, right| left == right);
14906        colours
14907    }
14908
14909    fn resolved_layout_colour(colour: oxml_drawing::color::ResolvedColor) -> Color {
14910        Color {
14911            r: f64::from(colour.red) / 255.0,
14912            g: f64::from(colour.green) / 255.0,
14913            b: f64::from(colour.blue) / 255.0,
14914            a: f64::from(colour.alpha) / 255.0,
14915        }
14916    }
14917
14918    fn geometry_children(geometry: &ChartGeometry) -> &[PositionedElement] {
14919        let [PositionedElement::Group(group)] = geometry.elements.as_slice() else {
14920            panic!("chart geometry must contain one group");
14921        };
14922        &group.children
14923    }
14924
14925    fn path_commands(geometry: &ChartGeometry) -> Vec<&[PathCommand]> {
14926        geometry_children(geometry)
14927            .iter()
14928            .map(|element| {
14929                let PositionedElement::Path(path) = element else {
14930                    panic!("chart geometry child must be a path");
14931                };
14932                path.path.commands.as_slice()
14933            })
14934            .collect()
14935    }
14936
14937    fn path_bounds(geometry: &ChartGeometry) -> Vec<Rect> {
14938        geometry_children(geometry)
14939            .iter()
14940            .map(|element| {
14941                let PositionedElement::Path(path) = element else {
14942                    panic!("chart geometry child must be a path");
14943                };
14944                path.path.bounds().expect("chart path has bounds")
14945            })
14946            .collect()
14947    }
14948
14949    fn assert_rect_near(actual: Rect, expected: Rect) {
14950        assert_near(actual.x, expected.x);
14951        assert_near(actual.y, expected.y);
14952        assert_near(actual.width, expected.width);
14953        assert_near(actual.height, expected.height);
14954    }
14955
14956    fn assert_rects_near(actual: &[Rect], expected: &[Rect]) {
14957        assert_eq!(actual.len(), expected.len());
14958        for (actual, expected) in actual.iter().copied().zip(expected.iter().copied()) {
14959            assert_rect_near(actual, expected);
14960        }
14961    }
14962
14963    fn assert_path_points(commands: &[PathCommand], expected: &[Point], closed: bool) {
14964        let points: Vec<_> = commands
14965            .iter()
14966            .filter_map(|command| match command {
14967                PathCommand::MoveTo(point) | PathCommand::LineTo(point) => Some(*point),
14968                PathCommand::Close => None,
14969                PathCommand::CurveTo { .. } => panic!("expected polygonal path"),
14970            })
14971            .collect();
14972        assert_eq!(points.len(), expected.len());
14973        for (actual, expected) in points.into_iter().zip(expected.iter().copied()) {
14974            assert_point_near(actual, expected);
14975        }
14976        assert_eq!(commands.last() == Some(&PathCommand::Close), closed);
14977    }
14978
14979    fn assert_point_near(actual: Point, expected: Point) {
14980        assert_near(actual.x, expected.x);
14981        assert_near(actual.y, expected.y);
14982    }
14983
14984    fn assert_near(actual: f64, expected: f64) {
14985        assert!(
14986            (actual - expected).abs() < 1e-9,
14987            "expected {expected}, got {actual}"
14988        );
14989    }
14990
14991    #[test]
14992    fn remaining_v1_plots_round_trip_and_render() {
14993        for (kind, plot, axes) in remaining_plot_fixtures() {
14994            let xml = chart_with_optional_axes(&plot, axes);
14995            let parsed = CT_ChartSpace::from_xml(xml.as_bytes())
14996                .unwrap_or_else(|error| panic!("{kind}: parse failed: {error}"));
14997            assert_eq!(parsed.chart.plot_area.plots().unwrap().len(), 1);
14998            assert_eq!(
14999                parsed.chart.plot_area.axes().unwrap().len(),
15000                usize::from(axes) * 2
15001            );
15002            let written = parsed
15003                .to_xml()
15004                .unwrap_or_else(|error| panic!("{kind}: write failed: {error}"));
15005            assert_eq!(
15006                parsed,
15007                CT_ChartSpace::from_xml(&written)
15008                    .unwrap_or_else(|error| panic!("{kind}: reparse failed: {error}"))
15009            );
15010        }
15011        if let Some(corpus) = require_or_skip_corpus() {
15012            verify_remaining_plot_viewer_gate(&corpus);
15013        }
15014    }
15015
15016    #[test]
15017    fn remaining_plot_families_write_fixed_prefixes_in_schema_order() {
15018        for (kind, plot, axes) in remaining_plot_fixtures() {
15019            let parsed =
15020                CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
15021            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15022            assert!(written.contains(&format!("<c:{kind}")));
15023            assert!(!written.contains(&format!("<q:{kind}")));
15024            let series = written.find("<c:ser").unwrap();
15025            let labels = written.find("<c:dLbls").unwrap();
15026            assert!(series < labels, "{kind}: series must precede labels");
15027            if matches!(kind, "areaChart" | "scatterChart" | "radarChart") {
15028                let property = match kind {
15029                    "areaChart" => "<c:grouping",
15030                    "scatterChart" => "<c:scatterStyle",
15031                    "radarChart" => "<c:radarStyle",
15032                    _ => unreachable!(),
15033                };
15034                assert!(written.find(property).unwrap() < series);
15035                assert!(labels < written.find("<c:axId").unwrap());
15036            } else {
15037                assert!(labels < written.find("<c:firstSliceAng").unwrap());
15038            }
15039            if kind == "doughnutChart" {
15040                assert!(
15041                    written.find("<c:firstSliceAng").unwrap()
15042                        < written.find("<c:holeSize").unwrap()
15043                );
15044            }
15045        }
15046    }
15047
15048    #[test]
15049    fn scatter_series_map_numeric_categories_and_values_to_x_and_y() {
15050        let plot = remaining_plot_fixtures()
15051            .into_iter()
15052            .find(|(kind, _, _)| *kind == "scatterChart")
15053            .unwrap()
15054            .1;
15055        let mut parsed =
15056            CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, true).as_bytes()).unwrap();
15057        let standalone_series = {
15058            let Plot::Scatter { series, .. } = &mut parsed.chart.plot_area.plots_mut().unwrap()[0]
15059            else {
15060                panic!("expected scatter plot");
15061            };
15062            let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
15063                panic!("expected numeric x values");
15064            };
15065            x_values.values[0] = 3.5;
15066            series[0].values.values[0] = 7.5;
15067            series[0].clone()
15068        };
15069        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15070        assert!(written.contains("<c:xVal>"));
15071        assert!(written.contains("<c:yVal>"));
15072        assert!(!written.contains("<c:cat>"));
15073        assert!(!written.contains("<c:val>"));
15074        assert!(written.contains("<c:v>3.5</c:v>"));
15075        assert!(written.contains("<c:v>7.5</c:v>"));
15076        let standalone = String::from_utf8(standalone_series.to_xml().unwrap()).unwrap();
15077        assert!(standalone.contains("<c:xVal>"));
15078        assert!(standalone.contains("<c:yVal>"));
15079        assert!(!standalone.contains("<c:cat>"));
15080        assert!(!standalone.contains("<c:val>"));
15081    }
15082
15083    #[test]
15084    fn malformed_remaining_plots_return_errors_without_panicking() {
15085        let series = plot_series(0);
15086        let scatter = numeric_plot_series(0);
15087        let opaque_bubble = series.replace(
15088            "</q:val></q:ser>",
15089            "</q:val><q:bubbleSize><x:opaque/></q:bubbleSize></q:ser>",
15090        );
15091        let cases = [
15092            r#"<q:pieChart/>"#.to_owned(),
15093            format!(r#"<q:pieChart>{series}<q:firstSliceAng val="361"/></q:pieChart>"#),
15094            format!(
15095                r#"<q:pieChart>{series}<q:firstSliceAng val="1"/><q:firstSliceAng val="2"/></q:pieChart>"#
15096            ),
15097            format!(r#"<q:doughnutChart>{series}<q:holeSize val="9"/></q:doughnutChart>"#),
15098            format!(
15099                r#"<q:doughnutChart>{series}<q:holeSize val="50"/><q:holeSize val="60"/></q:doughnutChart>"#
15100            ),
15101            format!(
15102                r#"<q:areaChart><q:grouping val="clustered"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
15103            ),
15104            format!(
15105                r#"<q:areaChart><q:grouping val="standard"/><q:grouping val="stacked"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
15106            ),
15107            format!(
15108                r#"<q:scatterChart><q:scatterStyle val="dots"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
15109            ),
15110            format!(
15111                r#"<q:scatterChart><q:scatterStyle val="marker"/><q:scatterStyle val="line"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
15112            ),
15113            format!(
15114                r#"<q:radarChart><q:radarStyle val="smooth"/>{series}<q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
15115            ),
15116            format!(
15117                r#"<q:radarChart><q:radarStyle val="marker"/>{series}<q:dLbls/><q:dLbls/><q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
15118            ),
15119            format!(r#"<q:pieChart>{series}<q:axId val="1"/></q:pieChart>"#),
15120            format!(
15121                r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/></q:areaChart>"#
15122            ),
15123            r#"<q:areaChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:areaChart>"#.to_owned(),
15124            format!(
15125                r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/><q:axId val="2"/><q:axId val="3"/></q:areaChart>"#
15126            ),
15127            format!(
15128                r#"<q:scatterChart><q:scatterStyle val="marker"/>{series}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
15129            ),
15130            format!(
15131                r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
15132                scatter
15133                    .replace("<q:yVal>", "<q:val>")
15134                    .replace("</q:yVal>", "</q:val>")
15135            ),
15136            format!(
15137                r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
15138                scatter_without_x(0)
15139            ),
15140            format!(
15141                r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
15142                scatter_without_y(0)
15143            ),
15144            format!(r#"<q:pieChart>{opaque_bubble}</q:pieChart>"#),
15145        ];
15146        for plot in cases {
15147            let xml = chart_with_optional_axes(&plot, false);
15148            let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
15149            assert!(result.is_ok(), "parser panicked for {plot}");
15150            assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
15151        }
15152
15153        let missing_axis = format!(
15154            r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="7"/></q:areaChart>"#
15155        );
15156        let missing_axis =
15157            CT_ChartSpace::from_xml(chart_with_optional_axes(&missing_axis, true).as_bytes())
15158                .unwrap_err();
15159        assert!(missing_axis.to_string().contains("missing axis 7"));
15160
15161        let values =
15162            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
15163        let mut bubble_series = Series::new(0, 0, values.clone());
15164        bubble_series.bubble_size = Some(values);
15165        assert!(Plot::pie(vec![bubble_series]).is_err());
15166    }
15167
15168    #[test]
15169    fn unsupported_plot_families_and_children_remain_byte_preserved() {
15170        for raw in [
15171            r#"<q:pie3DChart x:keep="3d"><q:ser><x:opaque/></q:ser></q:pie3DChart>"#,
15172            r#"<q:ofPieChart x:keep="of"><x:opaque/></q:ofPieChart>"#,
15173            r#"<q:stockChart x:keep="stock"><x:opaque/></q:stockChart>"#,
15174            r#"<q:surfaceChart x:keep="surface"><x:opaque/></q:surfaceChart>"#,
15175            r#"<q:bubbleChart x:keep="bubble"><x:opaque/></q:bubbleChart>"#,
15176        ] {
15177            let parsed =
15178                CT_ChartSpace::from_xml(chart_with_optional_axes(raw, false).as_bytes()).unwrap();
15179            assert!(parsed.chart.plot_area.plots().is_err());
15180            let written = parsed.to_xml().unwrap();
15181            assert!(
15182                written
15183                    .windows(raw.len())
15184                    .any(|window| window == raw.as_bytes())
15185            );
15186        }
15187
15188        let pie = remaining_plot_fixtures()[0].1.clone();
15189        let preserved = r#"<!--point--><q:dPt x:id="1"><q:explosion val="7"/><x:tail/></q:dPt>"#;
15190        let plot = pie.replace("<q:dLbls>", &format!("{preserved}<q:dLbls>"));
15191        let parsed =
15192            CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, false).as_bytes()).unwrap();
15193        let written = parsed.to_xml().unwrap();
15194        assert!(
15195            written
15196                .windows(preserved.len())
15197                .any(|window| window == preserved.as_bytes())
15198        );
15199
15200        for (kind, mut plot, axes) in [
15201            ("pieChart", remaining_plot_fixtures()[0].1.clone(), false),
15202            ("areaChart", remaining_plot_fixtures()[2].1.clone(), true),
15203        ] {
15204            plot = plot.replace(r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#, "");
15205            if kind == "pieChart" {
15206                plot = plot.replace(
15207                    "</q:extLst></q:pieChart>",
15208                    "</q:extLst><!--after-ext--></q:pieChart>",
15209                );
15210            } else {
15211                plot = plot.replace(
15212                    "</q:dropLines><q:axId",
15213                    "</q:dropLines><!--after-drop-lines--><q:axId",
15214                );
15215            }
15216            let mut parsed =
15217                CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
15218            let labels =
15219                CT_DLbls::from_xml(format!(r#"<c:dLbls xmlns:c="{C_NS}"/>"#).as_bytes()).unwrap();
15220            match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
15221                Plot::Pie { data_labels, .. } | Plot::Area { data_labels, .. } => {
15222                    *data_labels = Some(labels)
15223                }
15224                _ => panic!("expected pie or area"),
15225            }
15226            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15227            assert!(
15228                written.find("<c:dLbls").unwrap() < written.find("<q:extLst").unwrap(),
15229                "{kind}: inserted labels must precede the preserved extension"
15230            );
15231            if kind == "areaChart" {
15232                assert!(written.find("<c:dLbls").unwrap() < written.find("<q:dropLines").unwrap());
15233                assert!(
15234                    written.find("<q:dropLines").unwrap()
15235                        < written.find("<!--after-drop-lines-->").unwrap()
15236                );
15237            } else {
15238                assert!(
15239                    written.find("<q:extLst").unwrap() < written.find("<!--after-ext-->").unwrap()
15240                );
15241            }
15242        }
15243    }
15244
15245    #[test]
15246    fn every_supported_corpus_plot_round_trips_structurally() {
15247        let Some(corpus) = require_or_skip_corpus() else {
15248            return;
15249        };
15250        verify_fetched_corpus(&corpus);
15251        let mut pie_count = 0usize;
15252        let mut bar_count = 0usize;
15253        let mut line_count = 0usize;
15254        for path in manifest_paths() {
15255            let package = OpcPackage::open(corpus.join(path)).unwrap();
15256            for (part, xml) in &package.parts {
15257                if !is_chart_part(part) {
15258                    continue;
15259                }
15260                let parsed = CT_ChartSpace::from_xml(xml).unwrap();
15261                if let Ok(plots) = parsed.chart.plot_area.plots() {
15262                    for plot in plots {
15263                        match plot {
15264                            Plot::Pie { .. } => pie_count += 1,
15265                            Plot::Bar { .. } => bar_count += 1,
15266                            Plot::Line { .. } => line_count += 1,
15267                            _ => {}
15268                        }
15269                    }
15270                }
15271                let written = parsed.to_xml().unwrap();
15272                assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
15273            }
15274        }
15275        assert_eq!(pie_count, 1, "typed corpus pie coverage changed");
15276        assert_eq!(bar_count, 11, "typed corpus bar coverage changed");
15277        assert_eq!(line_count, 2, "typed corpus line coverage changed");
15278    }
15279
15280    fn standalone_axis(local: &str, children: &str) -> String {
15281        format!(
15282            r#"<c:{local} xmlns:c="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}">{children}</c:{local}>"#
15283        )
15284    }
15285
15286    fn minimal_axis(local: &str, id: &str, position: &str, cross_axis: &str) -> String {
15287        format!(
15288            r#"<c:{local}><c:axId val="{id}"/><c:scaling/><c:axPos val="{position}"/><c:crossAx val="{cross_axis}"/></c:{local}>"#
15289        )
15290    }
15291
15292    fn chart_with_axes(axes: &[String]) -> String {
15293        format!(
15294            r#"<c:chartSpace xmlns:c="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}"><c:chart><c:plotArea>{}</c:plotArea></c:chart></c:chartSpace>"#,
15295            axes.join("")
15296        )
15297    }
15298
15299    #[test]
15300    fn data_labels_write_fixed_prefixes_in_schema_order() {
15301        let xml = format!(
15302            r#"<q:dLbls xmlns:q="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}" xmlns:x="urn:producer"><q:dLbl><q:idx val="0"/><x:point/></q:dLbl><q:numFmt formatCode="0.0%" sourceLinked="0"/><q:spPr><x:shape/></q:spPr><q:txPr><x:text/></q:txPr><q:dLblPos val="outEnd"/><q:showLegendKey val="1"/><q:showVal val="true"/><q:showCatName val="0"/><q:showSerName val="1"/><q:showPercent val="0"/><q:showBubbleSize val="1"/><q:separator> / </q:separator><q:leaderLines><x:line/></q:leaderLines><q:extLst><q:ext uri="labels"><x:tail/></q:ext></q:extLst></q:dLbls>"#
15303        );
15304        let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
15305        assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0%");
15306        assert_eq!(parsed.position, Some(DataLabelPosition::OutsideEnd));
15307        assert_eq!(parsed.separator.as_deref(), Some(" / "));
15308        assert!(parsed.show_legend_key);
15309        assert!(parsed.show_value);
15310        assert!(!parsed.show_category_name);
15311        assert!(parsed.show_series_name);
15312        assert!(!parsed.show_percent);
15313        assert!(parsed.show_bubble_size);
15314
15315        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15316        assert!(written.starts_with(&format!(
15317            "<c:dLbls xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
15318        )));
15319        let mut cursor = 0usize;
15320        for tag in [
15321            "<q:dLbl",
15322            "<c:numFmt",
15323            "<q:spPr",
15324            "<q:txPr",
15325            "<c:dLblPos",
15326            "<c:showLegendKey",
15327            "<c:showVal",
15328            "<c:showCatName",
15329            "<c:showSerName",
15330            "<c:showPercent",
15331            "<c:showBubbleSize",
15332            "<c:separator",
15333            "<q:leaderLines",
15334            "<q:extLst",
15335        ] {
15336            let position = written[cursor..]
15337                .find(tag)
15338                .unwrap_or_else(|| panic!("missing ordered data-label tag {tag}"));
15339            cursor += position + tag.len();
15340        }
15341        assert_eq!(parsed, CT_DLbls::from_xml(written.as_bytes()).unwrap());
15342    }
15343
15344    #[test]
15345    fn percentage_formatted_label_renders_with_correct_text() {
15346        let Some(corpus) = require_or_skip_corpus() else {
15347            return;
15348        };
15349        verify_fetched_corpus(&corpus);
15350        assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
15351        assert_command_version("pdftotext", &["-v"], PDFTOTEXT_VERSION);
15352
15353        let source = corpus.join("bar-chart.pptx");
15354        let mut package = OpcPackage::open(&source).expect("open percentage-label source deck");
15355        let chart_part = "/ppt/charts/chart1.xml";
15356        let original_parts = package.parts.clone();
15357        let chart_xml = package
15358            .get_part(chart_part)
15359            .expect("bar chart part")
15360            .to_vec();
15361        let original_series = first_series_xml(&chart_xml).expect("bar chart series");
15362        let mut series = Series::from_xml(&original_series).expect("parse bar chart series");
15363        series.values.values.fill(0.25);
15364        let labels = CT_DLbls {
15365            number_format: Some(NumberFormat::new("0%".to_owned(), false).unwrap()),
15366            position: Some(DataLabelPosition::OutsideEnd),
15367            show_value: true,
15368            ..CT_DLbls::default()
15369        };
15370        series.data_labels = Some(labels);
15371        let replacement = series.to_xml().expect("serialize typed percentage labels");
15372        let candidate_chart = replace_once(&chart_xml, &original_series, &replacement);
15373        package.set_part(chart_part, candidate_chart);
15374        for (part, bytes) in &original_parts {
15375            if part == chart_part {
15376                assert_ne!(package.parts[part], *bytes);
15377            } else {
15378                assert_eq!(
15379                    package.parts[part], *bytes,
15380                    "unexpected changed part {part}"
15381                );
15382            }
15383        }
15384
15385        let temp_root =
15386            std::env::temp_dir().join(format!("oxml-chart-f123-label-gate-{}", std::process::id()));
15387        let profile = std::env::temp_dir().join(format!(
15388            "oxml-chart-f123-label-profile-{}",
15389            std::process::id()
15390        ));
15391        if temp_root.exists() {
15392            fs::remove_dir_all(&temp_root).expect("remove stale F-123 evidence");
15393        }
15394        if profile.exists() {
15395            fs::remove_dir_all(&profile).expect("remove stale F-123 profile");
15396        }
15397        fs::create_dir_all(&temp_root).expect("create F-123 evidence root");
15398        let unbound_candidate = temp_root.join("f123-percentage-label.pptx");
15399        package
15400            .save(&unbound_candidate)
15401            .expect("save percentage-label candidate");
15402        let candidate_sha = sha256(&unbound_candidate);
15403        let evidence_dir = temp_root.join(&candidate_sha);
15404        fs::create_dir(&evidence_dir).expect("create SHA-bound evidence directory");
15405        let candidate = evidence_dir.join("f123-percentage-label.pptx");
15406        fs::rename(&unbound_candidate, &candidate).expect("bind candidate to evidence SHA");
15407        assert_eq!(sha256(&candidate), candidate_sha);
15408
15409        let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
15410        let conversion = Command::new("soffice")
15411            .args([
15412                "--headless",
15413                &profile_argument,
15414                "--convert-to",
15415                "pdf:impress_pdf_Export",
15416                "--outdir",
15417            ])
15418            .arg(&evidence_dir)
15419            .arg(&candidate)
15420            .output()
15421            .expect("run pinned LibreOffice percentage-label gate");
15422        assert!(
15423            conversion.status.success(),
15424            "LibreOffice conversion failed: {}",
15425            String::from_utf8_lossy(&conversion.stderr)
15426        );
15427        let pdf = evidence_dir.join("f123-percentage-label.pdf");
15428        assert!(
15429            pdf.is_file(),
15430            "LibreOffice did not create {}",
15431            pdf.display()
15432        );
15433        let text_path = evidence_dir.join("f123-percentage-label.txt");
15434        let extraction = Command::new("pdftotext")
15435            .arg(&pdf)
15436            .arg(&text_path)
15437            .output()
15438            .expect("run pinned Poppler percentage-label extraction");
15439        assert!(
15440            extraction.status.success(),
15441            "pdftotext failed: {}",
15442            String::from_utf8_lossy(&extraction.stderr)
15443        );
15444        let extracted = fs::read_to_string(&text_path).expect("read percentage-label text");
15445        assert!(
15446            extracted.contains("25%"),
15447            "SHA-bound viewer text lacks 25%: {extracted:?}"
15448        );
15449        for artifact in [&candidate, &pdf, &text_path] {
15450            assert_eq!(
15451                artifact.parent(),
15452                Some(evidence_dir.as_path()),
15453                "evidence escaped candidate SHA"
15454            );
15455        }
15456        eprintln!("F-123 candidate/render/text evidence SHA-256 {candidate_sha}, extracted 25%");
15457        fs::remove_dir_all(&temp_root).expect("remove F-123 evidence");
15458        if profile.exists() {
15459            fs::remove_dir_all(&profile).expect("remove F-123 profile");
15460        }
15461    }
15462
15463    #[test]
15464    fn common_number_formats_project_cached_values_deterministically() {
15465        for (code, value, expected) in [
15466            ("General", 0.25, "0.25"),
15467            ("0", 12.6, "13"),
15468            ("0.0", 12.64, "12.6"),
15469            ("0.00", -12.645, "-12.64"),
15470            ("0%", 0.25, "25%"),
15471            ("0.0%", 0.256, "25.6%"),
15472            ("0.00%", 0.256, "25.60%"),
15473        ] {
15474            let format = NumberFormat::new(code.to_owned(), false).unwrap();
15475            assert_eq!(format.format_value(value).unwrap(), expected, "{code}");
15476        }
15477    }
15478
15479    #[test]
15480    fn malformed_data_labels_and_number_formats_return_errors_without_panicking() {
15481        let cases = [
15482            format!(
15483                r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="1"/><c:showVal val="0"/></c:dLbls>"#
15484            ),
15485            format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="yes"/></c:dLbls>"#),
15486            format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:dLblPos val="middle"/></c:dLbls>"#),
15487            format!(
15488                r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="" sourceLinked="1"/></c:dLbls>"#
15489            ),
15490            format!(
15491                r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="0" sourceLinked="maybe"/></c:dLbls>"#
15492            ),
15493        ];
15494        for xml in cases {
15495            let result = std::panic::catch_unwind(|| CT_DLbls::from_xml(xml.as_bytes()));
15496            assert!(result.is_ok(), "data-label parser panicked for {xml}");
15497            assert!(result.unwrap().is_err(), "malformed labels parsed: {xml}");
15498        }
15499
15500        let invalid_code = NumberFormat::new("bad\u{0}code".to_owned(), false);
15501        assert!(invalid_code.is_err());
15502        let unsupported = NumberFormat::new("#,##0".to_owned(), false).unwrap();
15503        assert!(unsupported.format_value(12.0).is_err());
15504        let general = NumberFormat::new("General".to_owned(), false).unwrap();
15505        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
15506            assert!(general.format_value(value).is_err());
15507        }
15508
15509        let invalid_separator = CT_DLbls {
15510            separator: Some("bad\u{0}separator".to_owned()),
15511            ..CT_DLbls::default()
15512        };
15513        assert!(invalid_separator.to_xml().is_err());
15514    }
15515
15516    #[test]
15517    fn data_labels_preserve_point_overrides_and_extensions_byte_for_byte() {
15518        let xml = format!(
15519            r#"<q:dLbls xmlns:q="{C_NS}" xmlns:x="urn:producer" x:keep="labels"><!--before--><q:dLbl x:keep="point"><q:idx val="0"/><q:spPr><x:shape/></q:spPr><q:txPr><x:text/></q:txPr><q:extLst><q:ext uri="point"><x:point/></q:ext></q:extLst></q:dLbl><?after-point?><q:numFmt formatCode="0%" sourceLinked="0"/><q:spPr><x:collection-shape/></q:spPr><q:txPr><x:collection-text/></q:txPr><q:showVal val="1"/><q:separator><![CDATA[ | ]]></q:separator><q:showLeaderLines val="1"/><q:leaderLines><q:spPr><x:leader-shape/></q:spPr></q:leaderLines><q:extLst><q:ext uri="collection"><x:tail/></q:ext></q:extLst><!--after--></q:dLbls>"#
15520        );
15521        let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
15522        let written = parsed.to_xml().unwrap();
15523        for raw in [
15524            br#"<!--before-->"#.as_slice(),
15525            br#"<q:dLbl x:keep="point"><q:idx val="0"/><q:spPr><x:shape/></q:spPr><q:txPr><x:text/></q:txPr><q:extLst><q:ext uri="point"><x:point/></q:ext></q:extLst></q:dLbl>"#.as_slice(),
15526            br#"<?after-point?>"#.as_slice(),
15527            br#"<q:spPr><x:collection-shape/></q:spPr>"#.as_slice(),
15528            br#"<q:txPr><x:collection-text/></q:txPr>"#.as_slice(),
15529            br#"<q:showLeaderLines val="1"/>"#.as_slice(),
15530            br#"<q:leaderLines><q:spPr><x:leader-shape/></q:spPr></q:leaderLines>"#.as_slice(),
15531            br#"<q:extLst><q:ext uri="collection"><x:tail/></q:ext></q:extLst>"#.as_slice(),
15532            br#"<!--after-->"#.as_slice(),
15533        ] {
15534            assert!(
15535                written.windows(raw.len()).any(|window| window == raw),
15536                "preserved data-label bytes changed: {}",
15537                String::from_utf8_lossy(raw)
15538            );
15539        }
15540        assert_eq!(parsed, CT_DLbls::from_xml(&written).unwrap());
15541    }
15542
15543    #[test]
15544    fn every_corpus_data_label_collection_round_trips_structurally() {
15545        let Some(corpus) = require_or_skip_corpus() else {
15546            return;
15547        };
15548        verify_fetched_corpus(&corpus);
15549        let mut label_collections = 0usize;
15550        let mut axis_number_formats = 0usize;
15551        for path in manifest_paths() {
15552            let package = OpcPackage::open(corpus.join(path))
15553                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
15554            for (part, xml) in &package.parts {
15555                if !is_chart_part(part) {
15556                    continue;
15557                }
15558                let chart = CT_ChartSpace::from_xml(xml)
15559                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
15560                for series in chart.chart.plot_area.series().unwrap_or_else(|error| {
15561                    panic!("{path} {part}: series projection failed: {error}")
15562                }) {
15563                    if let Some(labels) = series.data_labels {
15564                        let written = labels.to_xml().unwrap_or_else(|error| {
15565                            panic!("{path} {part}: label write failed: {error}")
15566                        });
15567                        let reparsed = CT_DLbls::from_xml(&written).unwrap_or_else(|error| {
15568                            panic!("{path} {part}: written labels parse failed: {error}")
15569                        });
15570                        assert_eq!(labels, reparsed, "{path} {part}: labels changed");
15571                        label_collections += 1;
15572                    }
15573                }
15574                for axis in chart.chart.plot_area.axes().unwrap_or_else(|error| {
15575                    panic!("{path} {part}: axis projection failed: {error}")
15576                }) {
15577                    if axis.number_format.is_some() {
15578                        let written = axis.to_xml().unwrap_or_else(|error| {
15579                            panic!("{path} {part}: axis write failed: {error}")
15580                        });
15581                        let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
15582                            panic!("{path} {part}: written axis parse failed: {error}")
15583                        });
15584                        assert_eq!(axis, reparsed, "{path} {part}: axis changed");
15585                        axis_number_formats += 1;
15586                    }
15587                }
15588            }
15589        }
15590        assert!(label_collections > 0, "the pinned corpus has no c:dLbls");
15591        assert!(
15592            axis_number_formats > 0,
15593            "the pinned corpus has no axis c:numFmt"
15594        );
15595        eprintln!(
15596            "ChartML data-label corpus gate checked {label_collections} collections and {axis_number_formats} axis number formats"
15597        );
15598    }
15599
15600    fn full_axis(local: &str, position: &str) -> String {
15601        format!(
15602            r#"<q:{local} xmlns:q="{C_NS}" xmlns:d="{A_NS}" xmlns:r="{R_NS}" xmlns:x="urn:producer"><q:axId val="-1125255920"/><q:scaling><q:logBase val="10"/><q:orientation val="maxMin"/><q:max val="100"/><q:min val="-10"/></q:scaling><q:delete val="1"/><q:axPos val="{position}"/><q:majorGridlines><q:spPr><d:ln/></q:spPr></q:majorGridlines><q:minorGridlines/><q:title><x:title/></q:title><q:numFmt formatCode="0.0" sourceLinked="0"/><q:majorTickMark val="cross"/><q:minorTickMark val="in"/><q:tickLblPos val="high"/><q:spPr><d:solidFill><d:srgbClr val="112233"/></d:solidFill></q:spPr><q:txPr><d:bodyPr/><d:p/></q:txPr><q:crossAx val="-1004303696"/></q:{local}>"#
15603        )
15604    }
15605
15606    #[test]
15607    fn axis_id_pairs_are_reciprocal() {
15608        assert_eq!(
15609            AxisId::new(i64::from(i32::MIN)).unwrap().value(),
15610            i64::from(i32::MIN)
15611        );
15612        assert_eq!(
15613            AxisId::new(i64::from(u32::MAX)).unwrap().value(),
15614            i64::from(u32::MAX)
15615        );
15616        assert!(AxisId::new(i64::from(i32::MIN) - 1).is_err());
15617        assert!(AxisId::new(i64::from(u32::MAX) + 1).is_err());
15618
15619        let valid = chart_with_axes(&[
15620            minimal_axis("catAx", "-1884094432", "b", "-1884097184"),
15621            minimal_axis("valAx", "-1884097184", "l", "-1884094432"),
15622        ]);
15623        let axes = CT_ChartSpace::from_xml(valid.as_bytes())
15624            .unwrap()
15625            .chart
15626            .plot_area
15627            .axes()
15628            .unwrap();
15629        assert_eq!(axes.len(), 2);
15630        assert_eq!(axes[0].id.value(), -1_884_094_432);
15631        assert_eq!(axes[0].cross_axis, axes[1].id);
15632        assert_eq!(axes[1].cross_axis, axes[0].id);
15633
15634        let invalid_sets = [
15635            vec![
15636                minimal_axis("catAx", "1", "b", "2"),
15637                minimal_axis("valAx", "1", "l", "2"),
15638            ],
15639            vec![minimal_axis("catAx", "1", "b", "1")],
15640            vec![minimal_axis("catAx", "1", "b", "2")],
15641            vec![
15642                minimal_axis("catAx", "1", "b", "2"),
15643                minimal_axis("valAx", "2", "l", "3"),
15644                minimal_axis("serAx", "3", "r", "2"),
15645            ],
15646        ];
15647        for axes in invalid_sets {
15648            let chart = CT_ChartSpace::from_xml(chart_with_axes(&axes).as_bytes()).unwrap();
15649            assert!(chart.chart.plot_area.axes().is_err());
15650        }
15651
15652        let empty = CT_ChartSpace::from_xml(chart_with_axes(&[]).as_bytes()).unwrap();
15653        assert!(empty.chart.plot_area.axes().unwrap().is_empty());
15654    }
15655
15656    #[test]
15657    fn all_axis_forms_write_fixed_prefixes_in_schema_order() {
15658        for (local, kind, position) in [
15659            ("catAx", AxisKind::Category, AxisPosition::Bottom),
15660            ("valAx", AxisKind::Value, AxisPosition::Left),
15661            ("dateAx", AxisKind::Date, AxisPosition::Top),
15662            ("serAx", AxisKind::Series, AxisPosition::Right),
15663        ] {
15664            let xml = full_axis(local, position.as_str());
15665            let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
15666            assert_eq!(parsed.kind, kind);
15667            assert_eq!(parsed.id.value(), -1_125_255_920);
15668            assert_eq!(parsed.scaling.log_base, Some(10.0));
15669            assert_eq!(parsed.scaling.orientation, Orientation::MaxMin);
15670            assert_eq!(parsed.scaling.maximum, Some(100.0));
15671            assert_eq!(parsed.scaling.minimum, Some(-10.0));
15672            assert!(parsed.deleted);
15673            assert_eq!(parsed.position, position);
15674            assert!(parsed.major_gridlines.is_some());
15675            assert!(parsed.minor_gridlines.is_some());
15676            assert!(parsed.title.is_some());
15677            assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0");
15678            assert_eq!(parsed.major_tick_mark, TickMark::Cross);
15679            assert_eq!(parsed.minor_tick_mark, TickMark::Inside);
15680            assert_eq!(parsed.tick_label_position, TickLabelPosition::High);
15681            assert!(parsed.sp_pr.is_some());
15682            assert!(parsed.tx_pr.is_some());
15683            assert_eq!(parsed.cross_axis.value(), -1_004_303_696);
15684
15685            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15686            assert!(written.starts_with(&format!(
15687                "<c:{local} xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
15688            )));
15689            assert!(written.contains("<c:axId val=\"-1125255920\""));
15690            let mut cursor = 0usize;
15691            for tag in [
15692                "<c:axId",
15693                "<c:scaling",
15694                "<c:delete",
15695                "<c:axPos",
15696                "<c:majorGridlines",
15697                "<c:minorGridlines",
15698                "<c:title",
15699                "<c:numFmt",
15700                "<c:majorTickMark",
15701                "<c:minorTickMark",
15702                "<c:tickLblPos",
15703                "<c:spPr",
15704                "<c:txPr",
15705                "<c:crossAx",
15706            ] {
15707                let position = written[cursor..]
15708                    .find(tag)
15709                    .unwrap_or_else(|| panic!("missing ordered axis tag {tag}"));
15710                cursor += position + tag.len();
15711            }
15712            assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
15713        }
15714
15715        let mut constructed = Axis::new(
15716            AxisKind::Value,
15717            AxisId::new(1).unwrap(),
15718            AxisPosition::Left,
15719            AxisId::new(2).unwrap(),
15720        );
15721        assert_eq!(
15722            constructed,
15723            Axis::from_xml(&constructed.to_xml().unwrap()).unwrap()
15724        );
15725        constructed.kind = AxisKind::Category;
15726        constructed.id = AxisId::new(3).unwrap();
15727        constructed.cross_axis = AxisId::new(4).unwrap();
15728        constructed.scaling.minimum = Some(0.0);
15729        constructed.scaling.maximum = Some(10.0);
15730        let written = constructed.to_xml().unwrap();
15731        assert!(written.starts_with(b"<c:catAx"));
15732        assert_eq!(constructed, Axis::from_xml(&written).unwrap());
15733    }
15734
15735    #[test]
15736    fn axis_equality_normalizes_ids_without_losing_lexical_preservation() {
15737        let padded = Axis::from_xml(
15738            standalone_axis(
15739                "catAx",
15740                "<c:axId val=\"01\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"002\"/>",
15741            )
15742            .as_bytes(),
15743        )
15744        .unwrap();
15745        let normalized = Axis::from_xml(
15746            standalone_axis(
15747                "catAx",
15748                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15749            )
15750            .as_bytes(),
15751        )
15752        .unwrap();
15753        assert_eq!(padded, normalized);
15754
15755        let preserved = String::from_utf8(padded.to_xml().unwrap()).unwrap();
15756        assert!(preserved.contains("<c:axId val=\"01\""));
15757        assert!(preserved.contains("<c:crossAx val=\"002\""));
15758
15759        let mut mutated = padded;
15760        mutated.id = AxisId::new(3).unwrap();
15761        mutated.cross_axis = AxisId::new(4).unwrap();
15762        let rewritten = String::from_utf8(mutated.to_xml().unwrap()).unwrap();
15763        assert!(rewritten.contains("<c:axId val=\"3\""));
15764        assert!(rewritten.contains("<c:crossAx val=\"4\""));
15765        assert!(!rewritten.contains("val=\"01\""));
15766        assert!(!rewritten.contains("val=\"002\""));
15767    }
15768
15769    #[test]
15770    fn malformed_axis_values_return_errors_without_panicking() {
15771        let cases = [
15772            standalone_axis(
15773                "catAx",
15774                "<c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15775            ),
15776            standalone_axis(
15777                "catAx",
15778                "<c:axId val=\"1\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15779            ),
15780            standalone_axis(
15781                "catAx",
15782                "<c:axId val=\"1\"/><c:scaling/><c:crossAx val=\"2\"/>",
15783            ),
15784            standalone_axis(
15785                "catAx",
15786                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/>",
15787            ),
15788            standalone_axis(
15789                "catAx",
15790                "<c:axId val=\"-2147483649\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15791            ),
15792            standalone_axis(
15793                "catAx",
15794                "<c:axId val=\"4294967296\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15795            ),
15796            standalone_axis(
15797                "catAx",
15798                "<c:axId val=\"1\"/><c:scaling><c:logBase val=\"1\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15799            ),
15800            standalone_axis(
15801                "catAx",
15802                "<c:axId val=\"1\"/><c:scaling><c:max val=\"NaN\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15803            ),
15804            standalone_axis(
15805                "catAx",
15806                "<c:axId val=\"1\"/><c:scaling><c:max val=\"1\"/><c:min val=\"2\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15807            ),
15808            standalone_axis(
15809                "catAx",
15810                "<c:axId val=\"1\"/><c:scaling><c:orientation val=\"sideways\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15811            ),
15812            standalone_axis(
15813                "catAx",
15814                "<c:axId val=\"1\"/><c:scaling/><c:delete val=\"yes\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15815            ),
15816            standalone_axis(
15817                "catAx",
15818                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"middle\"/><c:crossAx val=\"2\"/>",
15819            ),
15820            standalone_axis(
15821                "catAx",
15822                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:majorTickMark val=\"near\"/><c:crossAx val=\"2\"/>",
15823            ),
15824            standalone_axis(
15825                "catAx",
15826                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:tickLblPos val=\"middle\"/><c:crossAx val=\"2\"/>",
15827            ),
15828            standalone_axis(
15829                "catAx",
15830                "<c:axId val=\"1\"/><c:axId val=\"2\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15831            ),
15832            standalone_axis(
15833                "catAx",
15834                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt sourceLinked=\"1\"/><c:crossAx val=\"2\"/>",
15835            ),
15836            standalone_axis(
15837                "catAx",
15838                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt formatCode=\"0\" sourceLinked=\"maybe\"/><c:crossAx val=\"2\"/>",
15839            ),
15840        ];
15841        for xml in cases {
15842            let result = std::panic::catch_unwind(|| Axis::from_xml(xml.as_bytes()));
15843            assert!(result.is_ok(), "axis parser panicked for {xml}");
15844            assert!(result.unwrap().is_err(), "malformed axis parsed: {xml}");
15845        }
15846
15847        let mut axis = Axis::new(
15848            AxisKind::Value,
15849            AxisId::new(1).unwrap(),
15850            AxisPosition::Left,
15851            AxisId::new(2).unwrap(),
15852        );
15853        axis.scaling.maximum = Some(f64::INFINITY);
15854        assert!(axis.to_xml().is_err());
15855    }
15856
15857    #[test]
15858    fn axes_preserve_unmodelled_children_byte_for_byte() {
15859        let xml = format!(
15860            r#"<q:catAx xmlns:q="{C_NS}" xmlns:d="{A_NS}" xmlns:x="urn:producer" x:keep="axis"><!--before--><q:axId val="-1884094432" x:keep="id"/><?after-id?><q:scaling x:keep="scaling"><!--scale--><q:orientation val="minMax"/><q:extLst><q:ext uri="scale"><x:data/></q:ext></q:extLst></q:scaling><q:axPos val="b"/><q:majorGridlines x:keep="grid"><x:grid/></q:majorGridlines><q:title><x:title/></q:title><q:crossAx val="-1884097184"/><q:crosses val="autoZero"/><q:auto val="1"><x:auto/></q:auto><q:extLst><q:ext uri="axis"><x:tail/></q:ext></q:extLst><!--after--></q:catAx>"#
15861        );
15862        let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
15863        let mut relabelled = parsed.clone();
15864        relabelled.kind = AxisKind::Value;
15865        assert!(
15866            relabelled.to_xml().is_err(),
15867            "a parsed category axis must not retain its tail under a value-axis root"
15868        );
15869        let written = parsed.to_xml().unwrap();
15870        for raw in [
15871            br#"<!--before-->"#.as_slice(),
15872            br#"<?after-id?>"#.as_slice(),
15873            br#"<!--scale-->"#.as_slice(),
15874            br#"<q:extLst><q:ext uri="scale"><x:data/></q:ext></q:extLst>"#.as_slice(),
15875            br#"<x:grid/>"#.as_slice(),
15876            br#"<x:title/>"#.as_slice(),
15877            br#"<q:crosses val="autoZero"/>"#.as_slice(),
15878            br#"<q:auto val="1"><x:auto/></q:auto>"#.as_slice(),
15879            br#"<q:extLst><q:ext uri="axis"><x:tail/></q:ext></q:extLst>"#.as_slice(),
15880            br#"<!--after-->"#.as_slice(),
15881        ] {
15882            assert!(
15883                written.windows(raw.len()).any(|window| window == raw),
15884                "preserved axis bytes changed: {}",
15885                String::from_utf8_lossy(raw)
15886            );
15887        }
15888        let written = String::from_utf8(written).unwrap();
15889        assert!(written.starts_with("<c:catAx"));
15890        assert!(written.contains("x:keep=\"axis\""));
15891        assert!(written.contains("x:keep=\"id\""));
15892        assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
15893    }
15894
15895    #[test]
15896    fn every_corpus_axis_round_trips_structurally() {
15897        let Some(corpus) = require_or_skip_corpus() else {
15898            return;
15899        };
15900        verify_fetched_corpus(&corpus);
15901        let mut axis_count = 0usize;
15902        let mut chart_parts = 0usize;
15903        let mut kinds = HashSet::new();
15904        for path in manifest_paths() {
15905            let package = OpcPackage::open(corpus.join(path))
15906                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
15907            for (part, xml) in &package.parts {
15908                if !is_chart_part(part) {
15909                    continue;
15910                }
15911                let chart = CT_ChartSpace::from_xml(xml)
15912                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
15913                let axes = chart.chart.plot_area.axes().unwrap_or_else(|error| {
15914                    panic!("{path} {part}: axis projection failed: {error}")
15915                });
15916                for axis in axes {
15917                    kinds.insert(axis.kind);
15918                    let written = axis.to_xml().unwrap_or_else(|error| {
15919                        panic!("{path} {part}: axis write failed: {error}")
15920                    });
15921                    let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
15922                        panic!("{path} {part}: written axis parse failed: {error}")
15923                    });
15924                    assert_eq!(axis, reparsed, "{path} {part}: axis model changed");
15925                    axis_count += 1;
15926                }
15927                chart_parts += 1;
15928            }
15929        }
15930        assert!(
15931            axis_count > 0,
15932            "the pinned corpus contained no ChartML axes"
15933        );
15934        assert!(kinds.contains(&AxisKind::Category));
15935        assert!(kinds.contains(&AxisKind::Value));
15936
15937        for kind in ["dateAx", "serAx"] {
15938            let parsed = Axis::from_xml(
15939                standalone_axis(
15940                    kind,
15941                    "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
15942                )
15943                .as_bytes(),
15944            )
15945            .unwrap();
15946            assert_eq!(parsed, Axis::from_xml(&parsed.to_xml().unwrap()).unwrap());
15947        }
15948        eprintln!(
15949            "ChartML axis corpus gate checked {axis_count} axes across {chart_parts} chart parts"
15950        );
15951    }
15952
15953    #[test]
15954    fn series_formula_and_cache_are_consistent_with_one_source() {
15955        let values = NumericData::new(
15956            "'Sales 24'!$B$2:$B$4".to_owned(),
15957            "0.0".to_owned(),
15958            vec![4.25, 8.5, 17.0],
15959        )
15960        .unwrap();
15961        let mut series = Series::new(3, 1, values);
15962        series.categories = Some(AxisData::String(
15963            StringRef::new(
15964                "'Sales 24'!$A$2:$A$4".to_owned(),
15965                vec!["North".to_owned(), "South".to_owned(), "West".to_owned()],
15966            )
15967            .unwrap(),
15968        ));
15969
15970        let written = String::from_utf8(series.to_xml().unwrap()).unwrap();
15971        assert!(written.contains("<c:f>&apos;Sales 24&apos;!$B$2:$B$4</c:f>"));
15972        assert!(written.contains("<c:formatCode>0.0</c:formatCode>"));
15973        assert_eq!(written.matches("<c:ptCount val=\"3\"").count(), 2);
15974        for (index, value) in ["4.25", "8.5", "17"].iter().enumerate() {
15975            assert!(written.contains(&format!("<c:pt idx=\"{index}\"><c:v>{value}</c:v></c:pt>")));
15976        }
15977        let reparsed = Series::from_xml(written.as_bytes()).unwrap();
15978        assert_eq!(reparsed.index, series.index);
15979        assert_eq!(reparsed.order, series.order);
15980        match (&reparsed.categories, &series.categories) {
15981            (Some(AxisData::String(left)), Some(AxisData::String(right))) => {
15982                assert_eq!(left.formula, right.formula);
15983                assert_eq!(left.values, right.values);
15984            }
15985            _ => panic!("expected string categories"),
15986        }
15987        assert_eq!(reparsed.values.formula, series.values.formula);
15988        assert_eq!(reparsed.values.format_code, series.values.format_code);
15989        assert_eq!(reparsed.values.values, series.values.values);
15990    }
15991
15992    #[test]
15993    fn string_and_numeric_references_write_fixed_prefixes_in_schema_order() {
15994        let string_xml = br#"<q:strRef xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:strCache><q:pt idx="0"><q:v>West</q:v></q:pt><q:ptCount val="1"/></q:strCache><q:f>Sheet1!$A$2</q:f></q:strRef>"#;
15995        let string_ref = StringRef::from_xml(string_xml).unwrap();
15996        let written = String::from_utf8(string_ref.to_xml().unwrap()).unwrap();
15997        assert!(written.starts_with("<c:strRef xmlns:c="));
15998        assert!(written.find("<c:f>").unwrap() < written.find("<c:strCache>").unwrap());
15999        assert!(written.find("<c:ptCount").unwrap() < written.find("<c:pt idx=").unwrap());
16000
16001        let numeric_xml = br#"<q:numRef xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:numCache><q:ptCount val="2"/><q:pt idx="0"><q:v>1.5</q:v></q:pt><q:pt idx="1"><q:v>2.5</q:v></q:pt><q:formatCode>0.00</q:formatCode></q:numCache><q:f>Sheet1!$B$2:$B$3</q:f></q:numRef>"#;
16002        let numeric = NumericData::from_xml(numeric_xml).unwrap();
16003        let written = String::from_utf8(numeric.to_xml().unwrap()).unwrap();
16004        let positions: Vec<_> = [
16005            "<c:f>",
16006            "<c:numCache>",
16007            "<c:formatCode>",
16008            "<c:ptCount",
16009            "<c:pt idx=",
16010        ]
16011        .iter()
16012        .map(|tag| written.find(tag).unwrap())
16013        .collect();
16014        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
16015        assert_eq!(numeric, NumericData::from_xml(written.as_bytes()).unwrap());
16016    }
16017
16018    #[test]
16019    fn mixed_chartml_aliases_resolve_by_namespace_uri() {
16020        let xml = br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="4"/><q:order val="2"/><c:cat><q:strRef><c:f>Sheet1!$A$2:$A$3</c:f><q:strCache><c:ptCount val="2"/><q:pt idx="0"><c:v>North</c:v></q:pt><c:pt idx="1"><q:v>West</q:v></c:pt></q:strCache></q:strRef></c:cat><q:val><c:numRef><q:f>Sheet1!$B$2:$B$3</q:f><c:numCache><q:formatCode>0.0</q:formatCode><c:ptCount val="2"/><q:pt idx="0"><c:v>1.5</c:v></q:pt><c:pt idx="1"><q:v>2.5</q:v></c:pt></c:numCache></c:numRef></q:val></q:ser>"#;
16021        let parsed = Series::from_xml(xml).unwrap();
16022        assert_eq!(parsed.index, 4);
16023        assert_eq!(parsed.order, 2);
16024        assert_eq!(parsed.values.values, vec![1.5, 2.5]);
16025        assert!(matches!(parsed.categories, Some(AxisData::String(_))));
16026        assert_eq!(parsed, Series::from_xml(&parsed.to_xml().unwrap()).unwrap());
16027
16028        let foreign = br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:producer"><c:idx val="0"/><c:order val="0"/><c:spPr><x:solidFill><x:srgbClr val="112233"/></x:solidFill></c:spPr><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#;
16029        let parsed = Series::from_xml(foreign).unwrap();
16030        assert!(parsed.sp_pr.as_ref().unwrap().fill.is_none());
16031        let written = parsed.to_xml().unwrap();
16032        assert!(
16033            std::str::from_utf8(&written)
16034                .unwrap()
16035                .contains(r#"<x:solidFill><x:srgbClr val="112233"/></x:solidFill>"#)
16036        );
16037        assert_eq!(parsed, Series::from_xml(&written).unwrap());
16038    }
16039
16040    #[test]
16041    fn plot_area_series_ignores_inherited_foreign_plot_aliases() {
16042        let xml = br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:producer"><c:chart><c:plotArea><x:barChart><x:ser><x:idx val="9"/><x:order val="9"/><x:val><x:numRef><x:f>foreign</x:f><x:numCache><x:formatCode>General</x:formatCode><x:ptCount val="0"/></x:numCache></x:numRef></x:val></x:ser></x:barChart><c:barChart><q:ser><q:idx val="1"/><q:order val="0"/><q:marker><x:data/></q:marker><q:val><q:numRef><q:f>Sheet1!$B$2</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="1"/><q:pt idx="0"><q:v>3</q:v></q:pt></q:numCache></q:numRef></q:val></q:ser></c:barChart><c:pieChart/></c:plotArea></c:chart></c:chartSpace>"#;
16043        let chart = CT_ChartSpace::from_xml(xml).unwrap();
16044        let series = chart.chart.plot_area.series().unwrap();
16045        assert_eq!(series.len(), 1);
16046        assert_eq!(series[0].index, 1);
16047        let written = String::from_utf8(series[0].to_xml().unwrap()).unwrap();
16048        assert!(
16049            written.contains(r#"xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart""#)
16050        );
16051        assert!(written.contains(r#"xmlns:x="urn:producer""#));
16052        assert!(written.contains("<q:marker><x:data/></q:marker>"));
16053        assert_eq!(series[0], Series::from_xml(written.as_bytes()).unwrap());
16054
16055        let conflicting = br#"<q:chartSpace xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:chart><q:plotArea><q:barChart xmlns:c="urn:foreign"><q:ser><q:idx val="0"/><q:order val="0"/><q:val><q:numRef><q:f>Sheet1!$A$1</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="0"/></q:numCache></q:numRef></q:val></q:ser></q:barChart><q:pieChart/></q:plotArea></q:chart></q:chartSpace>"#;
16056        let chart = CT_ChartSpace::from_xml(conflicting).unwrap();
16057        assert!(chart.chart.plot_area.series().is_err());
16058    }
16059
16060    #[test]
16061    fn malformed_series_and_cache_values_return_errors_without_panicking() {
16062        let cases: &[&[u8]] = &[
16063            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:order val="0"/><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#,
16064            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/></c:ser>"#,
16065            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:idx val="1"/><c:order val="0"/><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#,
16066            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:val><c:numRef><c:f></c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#,
16067            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/><c:pt idx="0"><c:v>1</c:v></c:pt></c:numCache></c:numRef></c:val></c:ser>"#,
16068            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:val><c:numRef><c:f>S!$A$1:$A$2</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="2"/><c:pt idx="0"><c:v>1</c:v></c:pt><c:pt idx="2"><c:v>2</c:v></c:pt></c:numCache></c:numRef></c:val></c:ser>"#,
16069            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="1"/><c:pt idx="0"><c:v>NaN</c:v></c:pt></c:numCache></c:numRef></c:val></c:ser>"#,
16070            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="urn:producer"><c:idx val="0"/><c:order val="0"/><a:marker/><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#,
16071            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:tx><c:v>opaque</c:v></c:tx><c:tx><c:strRef><c:f>S!$A$1</c:f><c:strCache><c:ptCount val="0"/></c:strCache></c:strRef></c:tx><c:val><c:numRef><c:f>S!$B$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#,
16072            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:cat><c:multiLvlStrRef/></c:cat><c:cat><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:cat><c:val><c:numRef><c:f>S!$B$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val></c:ser>"#,
16073            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:val><c:numRef><c:f>S!$B$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val><c:bubbleSize><c:numLit/></c:bubbleSize><c:bubbleSize><c:numRef><c:f>S!$C$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:bubbleSize></c:ser>"#,
16074            br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:idx val="0"/><q:order val="0"/><q:val xmlns:c="urn:producer"><q:numRef><q:f>S!$A$1</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="0"/></q:numCache></q:numRef></q:val></q:ser>"#,
16075            br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:idx val="0"/><q:order val="0"/><q:val><q:numRef><q:f>S!$A$1</q:f><q:numCache xmlns:c="urn:producer"><q:formatCode>General</q:formatCode><q:ptCount val="0"/></q:numCache></q:numRef></q:val></q:ser>"#,
16076            br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:idx val="0"/><q:order val="0"/><q:val><q:numRef><q:f>S!$A$1</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="1"/><q:pt xmlns:c="urn:producer" idx="0"><q:v>1</q:v></q:pt></q:numCache></q:numRef></q:val></q:ser>"#,
16077            br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:idx val="0"/><q:order val="0"/><q:val><q:numRef><q:f>S!$A$1</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount xmlns:c="urn:producer" val="0"/></q:numCache></q:numRef></q:val></q:ser>"#,
16078            br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:idx val="0"/><q:order val="0"/><q:val><q:numRef><q:f xmlns:c="urn:producer">S!$A$1</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="0"/></q:numCache></q:numRef></q:val></q:ser>"#,
16079        ];
16080        for xml in cases {
16081            let result = std::panic::catch_unwind(|| Series::from_xml(xml));
16082            assert!(result.is_ok(), "series parser panicked");
16083            assert!(
16084                result.unwrap().is_err(),
16085                "malformed series parsed: {}",
16086                String::from_utf8_lossy(xml)
16087            );
16088        }
16089        assert!(
16090            NumericData::new(
16091                "S!$A$1".to_owned(),
16092                "General".to_owned(),
16093                vec![f64::INFINITY]
16094            )
16095            .is_err()
16096        );
16097    }
16098
16099    #[test]
16100    fn chart_text_rejects_xml_forbidden_characters_and_escapes_metacharacters() {
16101        assert!(StringRef::new("S!$A$1\u{1}".to_owned(), Vec::new()).is_err());
16102        assert!(StringRef::new("S!$A$1".to_owned(), vec!["invalid\u{1}".to_owned()]).is_err());
16103        assert!(
16104            NumericData::new("S!$A$1\u{1}".to_owned(), "General".to_owned(), Vec::new()).is_err()
16105        );
16106        assert!(
16107            NumericData::new("S!$A$1".to_owned(), "General\u{1}".to_owned(), Vec::new()).is_err()
16108        );
16109
16110        let mut strings = StringRef::new("S!$A$1".to_owned(), vec!["valid".to_owned()]).unwrap();
16111        strings.formula = "invalid\u{1}".to_owned();
16112        assert!(strings.to_xml().is_err());
16113        strings.formula = "S!$A$1".to_owned();
16114        strings.values[0] = "invalid\u{1}".to_owned();
16115        assert!(strings.to_xml().is_err());
16116
16117        let mut numbers =
16118            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap();
16119        numbers.formula = "invalid\u{1}".to_owned();
16120        assert!(numbers.to_xml().is_err());
16121        numbers.formula = "S!$A$1".to_owned();
16122        numbers.format_code = "invalid\u{1}".to_owned();
16123        assert!(numbers.to_xml().is_err());
16124
16125        let strings =
16126            StringRef::new("S!$A$1&".to_owned(), vec!["North < West & East".to_owned()]).unwrap();
16127        let written = String::from_utf8(strings.to_xml().unwrap()).unwrap();
16128        assert!(written.contains("S!$A$1&amp;"));
16129        assert!(written.contains("North &lt; West &amp; East"));
16130    }
16131
16132    #[test]
16133    fn series_preserves_unmodelled_children_byte_for_byte() {
16134        let xml = br#"<q:ser xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:d="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer" x:keep="series"><!--before--><q:idx val="0" x:keep="idx"/><x:between/><q:order val="0"/><q:tx x:keep="tx"><q:strRef x:keep="ref"><q:f x:keep="formula">S!$B$1</q:f><q:strCache x:keep="cache"><q:ptCount val="1" x:keep="count"/><!--point--><q:pt idx="0" x:keep="point"><q:v x:keep="value">Revenue</q:v></q:pt><x:cacheExt/></q:strCache></q:strRef></q:tx><q:spPr><d:noFill/><x:shapeExt/></q:spPr><q:dPt x:id="one"><q:idx val="0"/></q:dPt><q:dLbls x:id="labels"/><q:cat><q:strRef><q:f>S!$A$2:$A$3</q:f><q:strCache><q:ptCount val="2"/><q:pt idx="0"><q:v>North</q:v></q:pt><q:pt idx="1"><q:v>West</q:v></q:pt></q:strCache></q:strRef></q:cat><q:trendline x:id="trend"/><q:val><q:numRef><q:f>S!$B$2:$B$3</q:f><q:numCache><q:formatCode>0.0</q:formatCode><q:ptCount val="2"/><q:pt idx="0"><q:v>1.5</q:v></q:pt><q:pt idx="1"><q:v>2.5</q:v></q:pt></q:numCache></q:numRef></q:val><q:extLst><q:ext uri="keep"><x:data/></q:ext></q:extLst></q:ser>"#;
16135        let parsed = Series::from_xml(xml).unwrap();
16136        let written = parsed.to_xml().unwrap();
16137        for raw in [
16138            br#"<!--before-->"#.as_slice(),
16139            br#"<x:between/>"#.as_slice(),
16140            br#"<q:dPt x:id="one"><q:idx val="0"/></q:dPt>"#.as_slice(),
16141            br#"<q:trendline x:id="trend"/>"#.as_slice(),
16142            br#"<q:extLst><q:ext uri="keep"><x:data/></q:ext></q:extLst>"#.as_slice(),
16143            br#"<!--point-->"#.as_slice(),
16144            br#"<x:cacheExt/>"#.as_slice(),
16145        ] {
16146            assert!(
16147                written.windows(raw.len()).any(|window| window == raw),
16148                "preserved series bytes changed: {}",
16149                String::from_utf8_lossy(raw)
16150            );
16151        }
16152        let written = String::from_utf8(written.clone()).unwrap();
16153        assert!(written.contains(r#"x:keep="series""#));
16154        assert!(written.contains(r#"x:keep="formula""#));
16155        assert!(written.contains("<c:dLbls"));
16156        assert!(written.contains(r#"x:id="labels""#));
16157        assert!(parsed.data_labels.is_some());
16158        assert_eq!(parsed, Series::from_xml(written.as_bytes()).unwrap());
16159    }
16160
16161    #[test]
16162    fn public_series_edits_do_not_duplicate_or_drop_preserved_payloads() {
16163        let opaque_xml = br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/><c:tx><c:v>opaque</c:v></c:tx><c:cat><c:multiLvlStrRef/></c:cat><c:val><c:numRef><c:f>S!$A$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val><c:bubbleSize><c:numLit/></c:bubbleSize></c:ser>"#;
16164        let parsed = Series::from_xml(opaque_xml).unwrap();
16165
16166        let mut edited = parsed.clone();
16167        edited.name = Some(StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap());
16168        assert!(edited.to_xml().is_err());
16169
16170        let mut edited = parsed.clone();
16171        edited.categories = Some(AxisData::Numeric(
16172            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap(),
16173        ));
16174        assert!(edited.to_xml().is_err());
16175
16176        let mut edited = parsed;
16177        edited.bubble_size =
16178            Some(NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
16179        assert!(edited.to_xml().is_err());
16180
16181        let cache_xml = br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:producer"><c:idx val="0"/><c:order val="0"/><c:val><c:numRef><c:f>S!$A$1:$A$2</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="2"/><c:pt idx="0"><c:v>1</c:v></c:pt><c:pt idx="1"><c:v>2</c:v></c:pt><c:extLst><c:ext uri="keep"><x:data/></c:ext></c:extLst></c:numCache></c:numRef></c:val></c:ser>"#;
16182        let parsed = Series::from_xml(cache_xml).unwrap();
16183        let tail = "<c:extLst><c:ext uri=\"keep\"><x:data/></c:ext></c:extLst>";
16184
16185        let mut shortened = parsed.clone();
16186        shortened.values.values.truncate(1);
16187        let written = String::from_utf8(shortened.to_xml().unwrap()).unwrap();
16188        assert!(!written.contains("<c:pt idx=\"1\""));
16189        assert!(written.find("<c:pt idx=\"0\"").unwrap() < written.find(tail).unwrap());
16190        assert!(Series::from_xml(written.as_bytes()).is_ok());
16191
16192        let mut grown = parsed;
16193        grown.values.values.push(3.0);
16194        let written = String::from_utf8(grown.to_xml().unwrap()).unwrap();
16195        assert!(written.find("<c:pt idx=\"2\"").unwrap() < written.find(tail).unwrap());
16196        assert!(Series::from_xml(written.as_bytes()).is_ok());
16197
16198        let ordered_xml = br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><c:idx val="0"/><c:order val="0"/><c:marker><c:symbol val="circle"/></c:marker><c:val><c:numRef><c:f>S!$B$1</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></c:numCache></c:numRef></c:val><c:extLst><c:ext uri="keep"/></c:extLst></c:ser>"#;
16199        let mut edited = Series::from_xml(ordered_xml).unwrap();
16200        edited.name = Some(StringRef::new("S!$B$1".to_owned(), Vec::new()).unwrap());
16201        edited.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
16202        edited.categories = Some(AxisData::String(
16203            StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap(),
16204        ));
16205        edited.bubble_size =
16206            Some(NumericData::new("S!$C$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
16207        let written = String::from_utf8(edited.to_xml().unwrap()).unwrap();
16208        let positions: Vec<_> = [
16209            "<c:tx>",
16210            "<c:spPr",
16211            "<c:marker>",
16212            "<c:cat>",
16213            "<c:val>",
16214            "<c:bubbleSize>",
16215            "<c:extLst>",
16216        ]
16217        .iter()
16218        .map(|tag| written.find(tag).unwrap())
16219        .collect();
16220        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
16221        assert!(Series::from_xml(written.as_bytes()).is_ok());
16222    }
16223
16224    #[test]
16225    fn every_corpus_series_round_trips_structurally() {
16226        let Some(corpus) = require_or_skip_corpus() else {
16227            return;
16228        };
16229        verify_fetched_corpus(&corpus);
16230        let mut series_count = 0usize;
16231        let mut chart_parts = 0usize;
16232        for path in manifest_paths() {
16233            let package = OpcPackage::open(corpus.join(path))
16234                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
16235            for (part, xml) in &package.parts {
16236                if !is_chart_part(part) {
16237                    continue;
16238                }
16239                let chart = CT_ChartSpace::from_xml(xml)
16240                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
16241                let parsed_series =
16242                    chart.chart.plot_area.series().unwrap_or_else(|error| {
16243                        panic!("{path} {part}: series parse failed: {error}")
16244                    });
16245                for series in parsed_series {
16246                    let written = series.to_xml().unwrap_or_else(|error| {
16247                        panic!("{path} {part}: series write failed: {error}")
16248                    });
16249                    let reparsed = Series::from_xml(&written).unwrap_or_else(|error| {
16250                        panic!("{path} {part}: written series parse failed: {error}")
16251                    });
16252                    assert_eq!(series, reparsed, "{path} {part}: series model changed");
16253                    series_count += 1;
16254                }
16255                chart_parts += 1;
16256            }
16257        }
16258        assert!(
16259            series_count > 0,
16260            "the pinned corpus contained no supported series"
16261        );
16262        eprintln!(
16263            "ChartML series corpus gate checked {series_count} series across {chart_parts} chart parts"
16264        );
16265    }
16266
16267    #[test]
16268    fn chart_space_reads_aliases_and_writes_fixed_prefixes_in_schema_order() {
16269        let xml = br#"<ch:chartSpace xmlns:ch="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:d="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:rel="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:x="urn:producer"><x:before/><ch:chart><ch:plotVisOnly val="0"/><ch:legend><x:legend/></ch:legend><ch:title><x:title/></ch:title><ch:plotArea><x:plot/></ch:plotArea><ch:dispBlanksAs val="span"/></ch:chart><ch:txPr><d:bodyPr/><d:p/></ch:txPr><ch:spPr><d:solidFill><d:srgbClr val="112233"/></d:solidFill></ch:spPr><x:after/></ch:chartSpace>"#;
16270        let parsed = CT_ChartSpace::from_xml(xml).unwrap();
16271        assert!(!parsed.chart.auto_title_deleted);
16272        assert!(!parsed.chart.plot_vis_only);
16273        assert_eq!(parsed.chart.disp_blanks_as, DispBlanksAs::Span);
16274        assert!(parsed.chart.title.is_some());
16275        assert!(parsed.chart.legend.is_some());
16276        assert!(parsed.sp_pr.as_ref().unwrap().fill.is_some());
16277        assert!(parsed.tx_pr.is_some());
16278
16279        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
16280        assert!(written.starts_with(
16281            r#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships""#
16282        ));
16283        for tag in [
16284            "<c:chart>",
16285            "<c:title>",
16286            "<c:plotArea>",
16287            "<c:legend>",
16288            "<c:plotVisOnly",
16289            "<c:dispBlanksAs",
16290            "<c:spPr>",
16291            "<c:txPr",
16292        ] {
16293            assert!(written.contains(tag), "missing fixed-prefix tag {tag}");
16294        }
16295        let positions: Vec<_> = [
16296            "<c:title>",
16297            "<c:plotArea>",
16298            "<c:legend>",
16299            "<c:plotVisOnly",
16300            "<c:dispBlanksAs",
16301            "<c:spPr>",
16302            "<c:txPr",
16303        ]
16304        .iter()
16305        .map(|tag| written.find(tag).unwrap())
16306        .collect();
16307        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
16308        assert!(written.contains("<a:solidFill><a:srgbClr"));
16309        assert!(written.contains("<d:bodyPr/><d:p"));
16310        assert!(written.contains(&format!("xmlns:d=\"{A_NS}\"")));
16311        assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
16312    }
16313
16314    #[test]
16315    fn public_core_edits_keep_preserved_children_in_their_schema_slots() {
16316        let xml = br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><c:chart><c:pivotFmts><c:pivotFmt/></c:pivotFmts><c:plotArea/></c:chart><c:externalData r:id="rId1"/></c:chartSpace>"#;
16317        let mut parsed = CT_ChartSpace::from_xml(xml).unwrap();
16318        parsed.chart.auto_title_deleted = true;
16319        parsed.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
16320        parsed.tx_pr =
16321            Some(CT_TextBody::from_xml(br#"<a:txBody><a:bodyPr/><a:p/></a:txBody>"#).unwrap());
16322        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
16323        let positions: Vec<_> = [
16324            "<c:autoTitleDeleted",
16325            "<c:pivotFmts>",
16326            "<c:plotArea",
16327            "<c:spPr",
16328            "<c:txPr",
16329            "<c:externalData",
16330        ]
16331        .iter()
16332        .map(|tag| written.find(tag).unwrap())
16333        .collect();
16334        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
16335        assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
16336    }
16337
16338    #[test]
16339    fn malformed_core_chart_values_return_errors_without_panicking() {
16340        let cases: &[&[u8]] = &[
16341            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"/>"#,
16342            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart/></c:chartSpace>"#,
16343            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:plotArea/></c:chart></c:chartSpace>"#,
16344            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:autoTitleDeleted val="yes"/><c:plotArea/></c:chart></c:chartSpace>"#,
16345            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:dispBlanksAs val="empty"/></c:chart></c:chartSpace>"#,
16346            br#"<c:chartSpace xmlns:c="urn:wrong"><c:chart><c:plotArea/></c:chart></c:chartSpace>"#,
16347            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:foreign"><x:chart><x:plotArea/></x:chart></c:chartSpace>"#,
16348            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/></c:chart><c:chart><c:plotArea/></c:chart></c:chartSpace>"#,
16349            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:title/><c:title/><c:plotArea/></c:chart></c:chartSpace>"#,
16350            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:plotVisOnly><c:child/></c:plotVisOnly></c:chart></c:chartSpace>"#,
16351            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:foreign"><c:chart><c:plotArea/></c:chart><c:txPr><x:bodyPr/><a:p/></c:txPr></c:chartSpace>"#,
16352            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:foreign"><c:chart><c:plotArea/></c:chart><c:spPr><a:solidFill><x:srgbClr val="112233"/></a:solidFill></c:spPr></c:chartSpace>"#,
16353        ];
16354        for xml in cases {
16355            let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml));
16356            assert!(result.is_ok(), "ChartML parser panicked");
16357            assert!(
16358                result.unwrap().is_err(),
16359                "malformed ChartML parsed: {}",
16360                String::from_utf8_lossy(xml)
16361            );
16362        }
16363    }
16364
16365    #[test]
16366    fn fx215_chart_text_adapter_preserves_qualified_and_foreign_owners() {
16367        for (prefix, declarations) in [
16368            ("d:", format!("xmlns:d=\"{A_NS}\"")),
16369            ("", format!("xmlns=\"{A_NS}\"")),
16370        ] {
16371            let opaque = "<x:r><x:t>foreign</x:t></x:r>";
16372            let xml = format!(
16373                "<c:chartSpace xmlns:c=\"{C_NS}\" {declarations} xmlns:x=\"urn:foreign\"><c:chart><c:plotArea/></c:chart><c:txPr><{prefix}bodyPr/><{prefix}p>{opaque}<{prefix}r><{prefix}t>owned</{prefix}t></{prefix}r></{prefix}p></c:txPr></c:chartSpace>"
16374            );
16375            let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16376            let text = chart.tx_pr.as_ref().unwrap();
16377            assert_eq!(text.paragraphs().len(), 1);
16378            assert_eq!(text.paragraphs()[0].runs.len(), 1);
16379            let written = chart.to_xml().unwrap();
16380            assert!(std::str::from_utf8(&written).unwrap().contains(opaque));
16381            let reopened = CT_ChartSpace::from_xml(&written).unwrap();
16382            assert_eq!(
16383                reopened.tx_pr.as_ref().unwrap().paragraphs()[0].runs.len(),
16384                1
16385            );
16386            assert_eq!(
16387                text.body_properties,
16388                reopened.tx_pr.as_ref().unwrap().body_properties
16389            );
16390            for body in [text, reopened.tx_pr.as_ref().unwrap()] {
16391                let oxml_drawing::text::TextRun::Run(run) = &body.paragraphs()[0].runs[0] else {
16392                    panic!("qualified text must remain a regular run");
16393                };
16394                assert_eq!(run.text.value, "owned");
16395            }
16396        }
16397        let xml = format!(
16398            "<c:chartSpace xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:x=\"urn:foreign\"><c:chart><c:plotArea/></c:chart><c:txPr><a:bodyPr/><a:p><x:r><x:t>foreign</x:t></x:r></a:p></c:txPr></c:chartSpace>"
16399        );
16400        let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16401        assert!(
16402            chart.tx_pr.as_ref().unwrap().paragraphs()[0]
16403                .runs
16404                .is_empty()
16405        );
16406        let saved = chart.to_xml().unwrap();
16407        assert!(
16408            std::str::from_utf8(&saved)
16409                .unwrap()
16410                .contains("<x:r><x:t>foreign</x:t></x:r>")
16411        );
16412        assert_eq!(chart, CT_ChartSpace::from_xml(&saved).unwrap());
16413    }
16414
16415    #[test]
16416    fn point_label_overrides_render_without_changing_preserved_xml() {
16417        let raw_overrides = r#"<q:dLbl><q:idx val="1"/><q:numFmt formatCode="0.0" sourceLinked="0"/><q:dLblPos val="r"/><q:showVal val="1"/><q:showCatName val="1"/><q:separator> / </q:separator><q:extLst><x:point/></q:extLst></q:dLbl>"#;
16418        let plot = bar_plot("").replace(
16419            r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
16420            &format!(
16421                r#"<q:dLbls>{raw_overrides}<q:numFmt formatCode="0" sourceLinked="0"/><q:showVal val="1"/><q:showCatName val="1"/><q:separator>, </q:separator></q:dLbls>"#
16422            ),
16423        );
16424        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
16425        let plot = &chart.chart.plot_area.plots().unwrap()[0];
16426        let Plot::Bar { data_labels, .. } = plot else {
16427            panic!("expected bar plot");
16428        };
16429        let labels = data_labels.as_ref().unwrap();
16430        let written = labels.to_xml().unwrap();
16431        assert!(
16432            written
16433                .windows(raw_overrides.len())
16434                .any(|window| window == raw_overrides.as_bytes())
16435        );
16436        let overrides = super::point_label_overrides(labels).unwrap();
16437        assert_eq!(overrides.keys().copied().collect::<Vec<_>>(), vec![1]);
16438
16439        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
16440        let mut rendered = Vec::new();
16441        super::render_data_labels(
16442            plot,
16443            chart_bounds(),
16444            DispBlanksAs::Gap,
16445            super::GeometryOptions::default(),
16446            &mut fonts,
16447            &mut rendered,
16448        )
16449        .unwrap();
16450        let labels: Vec<_> = rendered
16451            .iter()
16452            .filter_map(|element| match element {
16453                PositionedElement::Text(run) => Some(run),
16454                _ => None,
16455            })
16456            .collect();
16457        assert_eq!(labels.len(), 2);
16458        assert_eq!(labels[0].text, "North, 1");
16459        assert_eq!(labels[1].text, "South / 2.0");
16460        assert_point_near(labels[0].origin, Point { x: 50.0, y: 105.0 });
16461        assert_point_near(labels[1].origin, Point { x: 158.0, y: 70.0 });
16462        assert_eq!(
16463            CT_DLbls::from_xml(&written).unwrap().to_xml().unwrap(),
16464            written
16465        );
16466    }
16467
16468    #[test]
16469    fn malformed_point_label_overrides_return_contextual_errors() {
16470        let cases = [
16471            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:showVal val="1"/></q:dLbl></q:dLbls>"#,
16472            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="0"/><q:idx val="1"/></q:dLbl></q:dLbls>"#,
16473            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="point"/></q:dLbl></q:dLbls>"#,
16474            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="0"/><q:showVal val="maybe"/></q:dLbl></q:dLbls>"#,
16475            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="0"/><q:dLblPos val="middle"/></q:dLbl></q:dLbls>"#,
16476            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="0"/><q:showVal val="1"/><q:dLblPos val="r"/></q:dLbl></q:dLbls>"#,
16477            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="0"/></q:dLbl><q:dLbl><q:idx val="0"/></q:dLbl></q:dLbls>"#,
16478        ];
16479        for xml in cases {
16480            let labels = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
16481            let result = std::panic::catch_unwind(|| super::point_label_overrides(&labels));
16482            assert!(result.is_ok(), "point-label parser panicked for {xml}");
16483            assert!(
16484                result.unwrap().is_err(),
16485                "malformed point labels parsed: {xml}"
16486            );
16487        }
16488
16489        let foreign = format!(
16490            r#"<q:dLbls xmlns:q="{C_NS}" xmlns:x="urn:foreign"><x:dLbl><x:idx val="0"/></x:dLbl></q:dLbls>"#
16491        );
16492        let labels = CT_DLbls::from_xml(foreign.as_bytes()).unwrap();
16493        assert!(super::point_label_overrides(&labels).unwrap().is_empty());
16494    }
16495
16496    #[test]
16497    fn percentage_label_aggregation_rejects_nonfinite_totals() {
16498        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
16499        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
16500        let Plot::Bar { series, .. } = plot else {
16501            panic!("expected bar plot");
16502        };
16503        series[0].values.values = vec![f64::MAX, f64::MAX];
16504        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
16505        super::render_data_labels(
16506            plot,
16507            chart_bounds(),
16508            DispBlanksAs::Gap,
16509            super::GeometryOptions::default(),
16510            &mut fonts,
16511            &mut Vec::new(),
16512        )
16513        .expect("value-only labels do not need a percentage total");
16514        let Plot::Bar { data_labels, .. } = plot else {
16515            panic!("expected bar plot");
16516        };
16517        data_labels.as_mut().unwrap().show_percent = true;
16518        let error = super::render_data_labels(
16519            plot,
16520            chart_bounds(),
16521            DispBlanksAs::Gap,
16522            super::GeometryOptions::default(),
16523            &mut fonts,
16524            &mut Vec::new(),
16525        )
16526        .unwrap_err();
16527        assert!(error.to_string().contains("data-label percentage total"));
16528
16529        let overrides = r#"<q:dLbl><q:idx val="0"/><q:showPercent val="0"/></q:dLbl><q:dLbl><q:idx val="1"/><q:showPercent val="0"/></q:dLbl>"#;
16530        let plot_xml = bar_plot("").replace(
16531            r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
16532            &format!(
16533                r#"<q:dLbls>{overrides}<q:showVal val="1"/><q:showPercent val="1"/></q:dLbls>"#
16534            ),
16535        );
16536        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
16537        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
16538        let Plot::Bar { series, .. } = plot else {
16539            panic!("expected bar plot");
16540        };
16541        series[0].values.values = vec![f64::MAX, f64::MAX];
16542        super::render_data_labels(
16543            plot,
16544            chart_bounds(),
16545            DispBlanksAs::Gap,
16546            super::GeometryOptions::default(),
16547            &mut fonts,
16548            &mut Vec::new(),
16549        )
16550        .expect("effective point overrides disable percentage aggregation");
16551    }
16552
16553    #[test]
16554    fn percentage_labels_use_effective_number_format_precision() {
16555        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
16556        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
16557        let Plot::Bar {
16558            series,
16559            data_labels,
16560            ..
16561        } = plot
16562        else {
16563            panic!("expected bar plot");
16564        };
16565        series[0].values.values = vec![1.0, 2.0];
16566        let labels = data_labels.as_mut().unwrap();
16567        labels.show_value = false;
16568        labels.show_percent = true;
16569        labels.number_format = Some(NumberFormat::new("0.00%".to_owned(), false).unwrap());
16570        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
16571        let mut rendered = Vec::new();
16572        super::render_data_labels(
16573            plot,
16574            chart_bounds(),
16575            DispBlanksAs::Gap,
16576            super::GeometryOptions::default(),
16577            &mut fonts,
16578            &mut rendered,
16579        )
16580        .unwrap();
16581        let text: Vec<_> = rendered
16582            .iter()
16583            .filter_map(|element| match element {
16584                PositionedElement::Text(run) => Some(run.text.as_str()),
16585                _ => None,
16586            })
16587            .collect();
16588        assert_eq!(text, vec!["33.33%", "66.67%"]);
16589
16590        let point_override = r#"<q:dLbl><q:idx val="0"/><q:numFmt formatCode="0.00%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbl>"#;
16591        let plot_xml = bar_plot("").replace(
16592            r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
16593            &format!(
16594                r#"<q:dLbls>{point_override}<q:numFmt formatCode="0%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbls>"#
16595            ),
16596        );
16597        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
16598        let plot = &chart.chart.plot_area.plots().unwrap()[0];
16599        let mut rendered = Vec::new();
16600        super::render_data_labels(
16601            plot,
16602            chart_bounds(),
16603            DispBlanksAs::Gap,
16604            super::GeometryOptions::default(),
16605            &mut fonts,
16606            &mut rendered,
16607        )
16608        .unwrap();
16609        let text: Vec<_> = rendered
16610            .iter()
16611            .filter_map(|element| match element {
16612                PositionedElement::Text(run) => Some(run.text.as_str()),
16613                _ => None,
16614            })
16615            .collect();
16616        assert_eq!(text, vec!["33.33%", "67%"]);
16617    }
16618
16619    #[test]
16620    fn core_chart_shells_preserve_unmodelled_children_byte_for_byte() {
16621        let preserved = [
16622            br#"<x:root x:id="1"><x:nested>one &amp; two</x:nested><!--root--></x:root>"#
16623                .as_slice(),
16624            br#"<x:title x:id="2"><x:nested><![CDATA[title]]></x:nested></x:title>"#.as_slice(),
16625            br#"<x:plot x:id="3"><x:nested>plot</x:nested></x:plot>"#.as_slice(),
16626            br#"<x:legend x:id="4"><x:nested>legend</x:nested></x:legend>"#.as_slice(),
16627            br#"<x:extension x:id="5"><x:nested>extension</x:nested></x:extension>"#.as_slice(),
16628            br#"<!--space-comment-->"#.as_slice(),
16629            br#"<?space processing?>"#.as_slice(),
16630            br#"<!--chart-comment-->"#.as_slice(),
16631            br#"<?title processing?>"#.as_slice(),
16632            br#"<!--flag-comment-->"#.as_slice(),
16633            br#"<?flag processing?>"#.as_slice(),
16634            br#"<x:producerShape/>"#.as_slice(),
16635            br#"<x:producerText/>"#.as_slice(),
16636        ];
16637        let xml = br#"<q:chartSpace xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer"><!--space-comment--><?space processing?><x:root x:id="1"><x:nested>one &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>"#;
16638        let parsed = CT_ChartSpace::from_xml(xml).unwrap();
16639        let written = parsed.to_xml().unwrap();
16640        for raw in preserved {
16641            assert!(
16642                written.windows(raw.len()).any(|window| window == raw),
16643                "preserved subtree changed: {}",
16644                String::from_utf8_lossy(raw)
16645            );
16646        }
16647        let written_text = String::from_utf8(written.clone()).unwrap();
16648        assert!(written_text.contains(r#"x:keep="one &amp; two""#));
16649        assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
16650    }
16651
16652    #[test]
16653    fn every_corpus_chart_part_round_trips_structurally() {
16654        let Some(corpus) = require_or_skip_corpus() else {
16655            return;
16656        };
16657        verify_fetched_corpus(&corpus);
16658        let mut chart_parts = 0usize;
16659        let mut decks_with_charts = HashSet::new();
16660        for path in manifest_paths() {
16661            let package = OpcPackage::open(corpus.join(path))
16662                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
16663            for (part, xml) in &package.parts {
16664                if !is_chart_part(part) {
16665                    continue;
16666                }
16667                let parsed = CT_ChartSpace::from_xml(xml)
16668                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
16669                let written = parsed
16670                    .to_xml()
16671                    .unwrap_or_else(|error| panic!("{path} {part}: write failed: {error}"));
16672                let original_preserved = preserved_chartml_regions(xml)
16673                    .unwrap_or_else(|error| panic!("{path} {part}: original scan failed: {error}"));
16674                let written_preserved = preserved_chartml_regions(&written)
16675                    .unwrap_or_else(|error| panic!("{path} {part}: written scan failed: {error}"));
16676                assert_eq!(
16677                    original_preserved.len(),
16678                    written_preserved.len(),
16679                    "{path} {part}: preserved ChartML region count changed"
16680                );
16681                for (index, (original, written)) in original_preserved
16682                    .iter()
16683                    .zip(&written_preserved)
16684                    .enumerate()
16685                {
16686                    assert_eq!(
16687                        original.parent_path, written.parent_path,
16688                        "{path} {part}: preserved region {index} parent changed"
16689                    );
16690                    assert_eq!(
16691                        original.boundary, written.boundary,
16692                        "{path} {part}: preserved region {index} boundary changed"
16693                    );
16694                    assert_eq!(
16695                        original.sibling_order, written.sibling_order,
16696                        "{path} {part}: preserved region {index} order changed"
16697                    );
16698                    assert_eq!(
16699                        original.xml, written.xml,
16700                        "{path} {part}: preserved region {index} bytes changed"
16701                    );
16702                }
16703                let reparsed = CT_ChartSpace::from_xml(&written)
16704                    .unwrap_or_else(|error| panic!("{path} {part}: written parse failed: {error}"));
16705                assert_eq!(parsed, reparsed, "{path} {part}: core model changed");
16706                chart_parts += 1;
16707                decks_with_charts.insert(path);
16708            }
16709        }
16710        assert!(
16711            chart_parts > 0,
16712            "the pinned corpus contained no chart parts"
16713        );
16714        eprintln!(
16715            "ChartML corpus gate checked {chart_parts} chart parts across {} decks",
16716            decks_with_charts.len()
16717        );
16718    }
16719
16720    #[derive(Debug, Eq, PartialEq)]
16721    struct PreservedChartRegion {
16722        parent_path: Vec<Vec<u8>>,
16723        boundary: usize,
16724        sibling_order: usize,
16725        xml: Vec<u8>,
16726    }
16727
16728    struct PreservationFrame {
16729        local_name: Vec<u8>,
16730        boundary: usize,
16731        next_sibling: usize,
16732    }
16733
16734    fn preserved_chartml_regions(xml: &[u8]) -> Result<Vec<PreservedChartRegion>, String> {
16735        let mut reader = Reader::from_reader(xml);
16736        let mut buffer = Vec::new();
16737        let mut parents: Vec<PreservationFrame> = Vec::new();
16738        let mut preserved = Vec::new();
16739        loop {
16740            match reader.read_event_into(&mut buffer) {
16741                Ok(Event::Start(element)) => {
16742                    let child = local_name(element.name().as_ref()).to_vec();
16743                    if parents.is_empty() && child == b"chartSpace" {
16744                        parents.push(PreservationFrame {
16745                            local_name: child,
16746                            boundary: 0,
16747                            next_sibling: 0,
16748                        });
16749                    } else if let Some(parent) = parents.last()
16750                        && is_opaque_chartml_child(&parent.local_name, &child)
16751                    {
16752                        let boundary =
16753                            preserved_child_boundary(&parent.local_name, &child, parent.boundary);
16754                        let xml = capture_element(&mut reader, &element)
16755                            .map_err(|error| error.to_string())?;
16756                        record_preserved_region(&mut parents, &mut preserved, boundary, xml);
16757                        advance_preservation_boundary(&mut parents, &child);
16758                    } else {
16759                        advance_preservation_boundary(&mut parents, &child);
16760                        if is_chartml_preservation_parent(&child) {
16761                            parents.push(PreservationFrame {
16762                                local_name: child,
16763                                boundary: 0,
16764                                next_sibling: 0,
16765                            });
16766                        } else {
16767                            capture_element(&mut reader, &element)
16768                                .map_err(|error| error.to_string())?;
16769                        }
16770                    }
16771                }
16772                Ok(Event::Empty(element)) => {
16773                    let name = element.name();
16774                    let child = local_name(name.as_ref()).to_vec();
16775                    if let Some(parent) = parents.last()
16776                        && is_opaque_chartml_child(&parent.local_name, &child)
16777                    {
16778                        let boundary =
16779                            preserved_child_boundary(&parent.local_name, &child, parent.boundary);
16780                        let xml =
16781                            capture_empty_element(&element).map_err(|error| error.to_string())?;
16782                        record_preserved_region(&mut parents, &mut preserved, boundary, xml);
16783                    }
16784                    advance_preservation_boundary(&mut parents, &child);
16785                }
16786                Ok(
16787                    event @ (Event::Comment(_)
16788                    | Event::PI(_)
16789                    | Event::CData(_)
16790                    | Event::GeneralRef(_)),
16791                ) if !parents.is_empty() => {
16792                    let boundary = parents.last().expect("preservation parent").boundary;
16793                    let xml = capture_event(event).map_err(|error| error.to_string())?;
16794                    record_preserved_region(&mut parents, &mut preserved, boundary, xml);
16795                }
16796                Ok(Event::End(_)) => {
16797                    parents.pop();
16798                }
16799                Ok(Event::Eof) => break,
16800                Ok(_) => {}
16801                Err(error) => return Err(error.to_string()),
16802            }
16803            buffer.clear();
16804        }
16805        Ok(preserved)
16806    }
16807
16808    fn record_preserved_region(
16809        parents: &mut [PreservationFrame],
16810        preserved: &mut Vec<PreservedChartRegion>,
16811        boundary: usize,
16812        xml: Vec<u8>,
16813    ) {
16814        let sibling_order = parents.last().expect("preservation parent").next_sibling;
16815        parents
16816            .last_mut()
16817            .expect("preservation parent")
16818            .next_sibling += 1;
16819        preserved.push(PreservedChartRegion {
16820            parent_path: parents
16821                .iter()
16822                .map(|parent| parent.local_name.clone())
16823                .collect(),
16824            boundary,
16825            sibling_order,
16826            xml,
16827        });
16828    }
16829
16830    fn advance_preservation_boundary(parents: &mut [PreservationFrame], child: &[u8]) {
16831        let Some(parent) = parents.last_mut() else {
16832            return;
16833        };
16834        parent.boundary = parent
16835            .boundary
16836            .max(preserved_boundary_after(&parent.local_name, child));
16837    }
16838
16839    fn preserved_child_boundary(parent: &[u8], child: &[u8], current: usize) -> usize {
16840        match parent {
16841            b"chartSpace" => match child {
16842                b"date1904" => 0,
16843                b"lang" => 1,
16844                b"roundedCorners" => 2,
16845                b"style" => 3,
16846                b"clrMapOvr" => 4,
16847                b"pivotSource" => 5,
16848                b"protection" => 6,
16849                b"externalData" => 10,
16850                b"printSettings" => 11,
16851                b"userShapes" => 12,
16852                b"extLst" => 13,
16853                _ => current,
16854            },
16855            b"chart" => match child {
16856                b"pivotFmts" => 2,
16857                b"view3D" => 3,
16858                b"floor" => 4,
16859                b"sideWall" => 5,
16860                b"backWall" => 6,
16861                b"showDLblsOverMax" | b"extLst" => 11,
16862                _ => current,
16863            },
16864            _ => current,
16865        }
16866    }
16867
16868    fn preserved_boundary_after(parent: &[u8], child: &[u8]) -> usize {
16869        match parent {
16870            b"chartSpace" => match child {
16871                b"date1904" => 1,
16872                b"lang" => 2,
16873                b"roundedCorners" => 3,
16874                b"style" => 4,
16875                b"clrMapOvr" => 5,
16876                b"pivotSource" => 6,
16877                b"protection" => 7,
16878                b"chart" => 8,
16879                b"spPr" => 9,
16880                b"txPr" => 10,
16881                b"externalData" => 11,
16882                b"printSettings" => 12,
16883                b"userShapes" => 13,
16884                b"extLst" => 14,
16885                _ => 0,
16886            },
16887            b"chart" => match child {
16888                b"title" => 1,
16889                b"autoTitleDeleted" => 2,
16890                b"pivotFmts" => 3,
16891                b"view3D" => 4,
16892                b"floor" => 5,
16893                b"sideWall" => 6,
16894                b"backWall" => 7,
16895                b"plotArea" => 8,
16896                b"legend" => 9,
16897                b"plotVisOnly" => 10,
16898                b"dispBlanksAs" => 11,
16899                b"showDLblsOverMax" | b"extLst" => 11,
16900                _ => 0,
16901            },
16902            b"pieChart" => match child {
16903                b"ser" => 1,
16904                b"dLbls" => 2,
16905                b"firstSliceAng" => 3,
16906                _ => 0,
16907            },
16908            b"doughnutChart" => match child {
16909                b"ser" => 1,
16910                b"dLbls" => 2,
16911                b"firstSliceAng" => 3,
16912                b"holeSize" => 4,
16913                _ => 0,
16914            },
16915            b"areaChart" => match child {
16916                b"grouping" => 1,
16917                b"ser" => 2,
16918                b"dLbls" => 3,
16919                b"axId" => 4,
16920                _ => 0,
16921            },
16922            b"scatterChart" | b"radarChart" => match child {
16923                b"scatterStyle" | b"radarStyle" => 1,
16924                b"ser" => 2,
16925                b"dLbls" => 3,
16926                b"axId" => 4,
16927                _ => 0,
16928            },
16929            _ => 0,
16930        }
16931    }
16932
16933    fn is_opaque_chartml_child(parent: &[u8], child: &[u8]) -> bool {
16934        match parent {
16935            b"chartSpace" => !matches!(child, b"chart" | b"spPr" | b"txPr"),
16936            b"chart" => !matches!(
16937                child,
16938                b"title"
16939                    | b"autoTitleDeleted"
16940                    | b"plotArea"
16941                    | b"legend"
16942                    | b"plotVisOnly"
16943                    | b"dispBlanksAs"
16944            ),
16945            b"plotArea" => !matches!(
16946                child,
16947                b"barChart"
16948                    | b"lineChart"
16949                    | b"pieChart"
16950                    | b"doughnutChart"
16951                    | b"areaChart"
16952                    | b"scatterChart"
16953                    | b"radarChart"
16954                    | b"catAx"
16955                    | b"valAx"
16956                    | b"dateAx"
16957                    | b"serAx"
16958            ),
16959            b"pieChart" => !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng"),
16960            b"doughnutChart" => {
16961                !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng" | b"holeSize")
16962            }
16963            b"areaChart" => !matches!(child, b"grouping" | b"ser" | b"dLbls" | b"axId"),
16964            b"scatterChart" => !matches!(child, b"scatterStyle" | b"ser" | b"dLbls" | b"axId"),
16965            b"radarChart" => !matches!(child, b"radarStyle" | b"ser" | b"dLbls" | b"axId"),
16966            b"title" | b"legend" => true,
16967            _ => false,
16968        }
16969    }
16970
16971    fn is_chartml_preservation_parent(parent: &[u8]) -> bool {
16972        matches!(
16973            parent,
16974            b"chartSpace"
16975                | b"chart"
16976                | b"title"
16977                | b"plotArea"
16978                | b"pieChart"
16979                | b"doughnutChart"
16980                | b"areaChart"
16981                | b"scatterChart"
16982                | b"radarChart"
16983                | b"legend"
16984                | b"autoTitleDeleted"
16985                | b"plotVisOnly"
16986                | b"dispBlanksAs"
16987        )
16988    }
16989
16990    #[test]
16991    fn oxml_chart_is_an_explicit_publication_candidate() {
16992        let manifest = include_str!("../Cargo.toml");
16993        assert!(manifest.contains("name = \"oxml-chart\""));
16994        assert!(manifest.contains("version = \"0.14.2\""));
16995        assert!(manifest.contains("publish = true"));
16996        for dependency in [
16997            "oxml-core.workspace",
16998            "oxml-drawing.workspace",
16999            "quick-xml.workspace",
17000        ] {
17001            assert!(manifest.contains(dependency));
17002        }
17003        assert!(!manifest.contains("rpptx.workspace"));
17004        assert!(!manifest.contains("rdocx.workspace"));
17005    }
17006
17007    fn first_series_xml(xml: &[u8]) -> Option<Vec<u8>> {
17008        let mut reader = Reader::from_reader(xml);
17009        let mut buffer = Vec::new();
17010        loop {
17011            match reader.read_event_into(&mut buffer).ok()? {
17012                Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
17013                    return capture_element(&mut reader, &element).ok();
17014                }
17015                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
17016                    return capture_empty_element(&element).ok();
17017                }
17018                Event::Eof => return None,
17019                _ => {}
17020            }
17021            buffer.clear();
17022        }
17023    }
17024
17025    fn replace_once(haystack: &[u8], needle: &[u8], replacement: &[u8]) -> Vec<u8> {
17026        let offset = haystack
17027            .windows(needle.len())
17028            .position(|window| window == needle)
17029            .expect("serialized source series occurs in chart part");
17030        assert!(
17031            !haystack[offset + needle.len()..]
17032                .windows(needle.len())
17033                .any(|window| window == needle),
17034            "source series unexpectedly occurs more than once"
17035        );
17036        let mut result = Vec::with_capacity(haystack.len() - needle.len() + replacement.len());
17037        result.extend_from_slice(&haystack[..offset]);
17038        result.extend_from_slice(replacement);
17039        result.extend_from_slice(&haystack[offset + needle.len()..]);
17040        result
17041    }
17042
17043    fn assert_command_version(program: &str, arguments: &[&str], expected: &str) {
17044        let output = Command::new(program)
17045            .args(arguments)
17046            .output()
17047            .unwrap_or_else(|error| panic!("run {program} version check: {error}"));
17048        assert!(
17049            output.status.success(),
17050            "{program} version check failed: {}",
17051            String::from_utf8_lossy(&output.stderr)
17052        );
17053        let stdout = String::from_utf8(output.stdout).expect("version stdout is UTF-8");
17054        let stderr = String::from_utf8(output.stderr).expect("version stderr is UTF-8");
17055        let actual = if stdout.trim().is_empty() {
17056            stderr.trim()
17057        } else {
17058            stdout.trim()
17059        };
17060        assert_eq!(
17061            actual.lines().next(),
17062            Some(expected),
17063            "{program} version drift"
17064        );
17065    }
17066
17067    fn sha256(path: &Path) -> String {
17068        let output = Command::new("shasum")
17069            .args(["-a", "256"])
17070            .arg(path)
17071            .output()
17072            .unwrap_or_else(|error| panic!("{}: run shasum: {error}", path.display()));
17073        assert!(
17074            output.status.success(),
17075            "{}: shasum failed: {}",
17076            path.display(),
17077            String::from_utf8_lossy(&output.stderr)
17078        );
17079        String::from_utf8(output.stdout)
17080            .expect("shasum output is UTF-8")
17081            .split_whitespace()
17082            .next()
17083            .expect("shasum digest")
17084            .to_owned()
17085    }
17086
17087    fn workspace_root() -> PathBuf {
17088        Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
17089    }
17090
17091    fn corpus_dir() -> PathBuf {
17092        std::env::var_os("RDOCX_PPTX_CORPUS_DIR")
17093            .map(PathBuf::from)
17094            .unwrap_or_else(|| workspace_root().join("corpus/pptx"))
17095    }
17096
17097    fn require_or_skip_corpus() -> Option<PathBuf> {
17098        let corpus = corpus_dir();
17099        if corpus.is_dir() {
17100            return Some(corpus);
17101        }
17102        assert_ne!(
17103            std::env::var_os("RDOCX_PPTX_CORPUS_REQUIRED").as_deref(),
17104            Some(std::ffi::OsStr::new("1")),
17105            "the pinned corpus is required but {} does not exist",
17106            corpus.display()
17107        );
17108        eprintln!(
17109            "ChartML corpus gate skipped because {} is absent",
17110            corpus.display()
17111        );
17112        None
17113    }
17114
17115    fn verify_fetched_corpus(corpus: &Path) {
17116        let status = Command::new("python3")
17117            .arg(workspace_root().join("scripts/fetch_pptx_corpus.py"))
17118            .arg("--check")
17119            .env("RDOCX_PPTX_CORPUS_DIR", corpus)
17120            .status()
17121            .expect("run corpus verifier");
17122        assert!(status.success(), "pinned corpus verification failed");
17123    }
17124
17125    fn manifest_paths() -> Vec<&'static str> {
17126        let mut lines = MANIFEST.lines();
17127        assert_eq!(lines.next(), Some("path\tproducer\tsha256\turl"));
17128        let paths: Vec<_> = lines
17129            .map(|line| line.split('\t').next().expect("manifest path"))
17130            .collect();
17131        assert_eq!(paths.len(), EXPECTED_DECKS);
17132        paths
17133    }
17134
17135    fn is_chart_part(part: &str) -> bool {
17136        let Some(stem) = part
17137            .strip_prefix("/ppt/charts/chart")
17138            .and_then(|name| name.strip_suffix(".xml"))
17139        else {
17140            return false;
17141        };
17142        !stem.is_empty() && stem.bytes().all(|byte| byte.is_ascii_digit())
17143    }
17144
17145    fn plot_series(index: u32) -> String {
17146        format!(
17147            r#"<q:ser><q:idx val="{index}"/><q:order val="{index}"/><q:cat><q:strRef><q:f>Sheet1!$A$2:$A$3</q:f><q:strCache><q:ptCount val="2"/><q:pt idx="0"><q:v>North</q:v></q:pt><q:pt idx="1"><q:v>South</q:v></q:pt></q:strCache></q:strRef></q:cat><q:val><q:numRef><q:f>Sheet1!$B$2:$B$3</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="2"/><q:pt idx="0"><q:v>1</q:v></q:pt><q:pt idx="1"><q:v>2</q:v></q:pt></q:numCache></q:numRef></q:val></q:ser>"#
17148        )
17149    }
17150
17151    fn sparse_category_series() -> &'static str {
17152        r#"<q:ser><q:idx val="0"/><q:order val="0"/><q:cat><q:strRef><q:f>Sheet1!$A$2:$A$4</q:f><q:strCache><q:ptCount val="3"/><q:pt idx="0"><q:v>North</q:v></q:pt><q:pt idx="2"><q:v>West</q:v></q:pt></q:strCache></q:strRef></q:cat><q:val><q:numRef><q:f>Sheet1!$B$2:$B$4</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="3"/><q:pt idx="0"><q:v>1</q:v></q:pt><q:pt idx="2"><q:v>2</q:v></q:pt></q:numCache></q:numRef></q:val></q:ser>"#
17153    }
17154
17155    fn sparse_scatter_series() -> &'static str {
17156        r#"<q:ser><q:idx val="0"/><q:order val="0"/><q:xVal><q:numRef><q:f>Sheet1!$A$2:$A$5</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="4"/><q:pt idx="0"><q:v>10</q:v></q:pt><q:pt idx="2"><q:v>30</q:v></q:pt><q:pt idx="3"><q:v>40</q:v></q:pt></q:numCache></q:numRef></q:xVal><q:yVal><q:numRef><q:f>Sheet1!$B$2:$B$5</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="4"/><q:pt idx="0"><q:v>100</q:v></q:pt><q:pt idx="1"><q:v>200</q:v></q:pt><q:pt idx="3"><q:v>400</q:v></q:pt></q:numCache></q:numRef></q:yVal></q:ser>"#
17157    }
17158
17159    fn chart_with_plot(plot: &str) -> String {
17160        format!(
17161            r#"<q:chartSpace xmlns:q="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}" xmlns:x="urn:producer"><q:chart><q:plotArea>{plot}<q:catAx><q:axId val="-1884094432"/><q:scaling/><q:axPos val="b"/><q:crossAx val="-1884097184"/></q:catAx><q:valAx><q:axId val="-1884097184"/><q:scaling/><q:axPos val="l"/><q:crossAx val="-1884094432"/></q:valAx></q:plotArea></q:chart></q:chartSpace>"#
17162        )
17163    }
17164
17165    fn chart_with_optional_axes(plot: &str, axes: bool) -> String {
17166        let axis_xml = if axes {
17167            r#"<q:catAx><q:axId val="-1884094432"/><q:scaling/><q:axPos val="b"/><q:crossAx val="-1884097184"/></q:catAx><q:valAx><q:axId val="-1884097184"/><q:scaling/><q:axPos val="l"/><q:crossAx val="-1884094432"/></q:valAx>"#
17168        } else {
17169            ""
17170        };
17171        format!(
17172            r#"<q:chartSpace xmlns:q="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}" xmlns:x="urn:producer"><q:chart><q:plotArea>{plot}{axis_xml}</q:plotArea></q:chart></q:chartSpace>"#
17173        )
17174    }
17175
17176    fn numeric_plot_series(index: u32) -> String {
17177        format!(
17178            r#"<q:ser><q:idx val="{index}"/><q:order val="{index}"/><q:xVal><q:numRef><q:f>Sheet1!$A$2:$A$3</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="2"/><q:pt idx="0"><q:v>1</q:v></q:pt><q:pt idx="1"><q:v>2</q:v></q:pt></q:numCache></q:numRef></q:xVal><q:yVal><q:numRef><q:f>Sheet1!$B$2:$B$3</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="2"/><q:pt idx="0"><q:v>4</q:v></q:pt><q:pt idx="1"><q:v>8</q:v></q:pt></q:numCache></q:numRef></q:yVal></q:ser>"#
17179        )
17180    }
17181
17182    fn scatter_without_x(index: u32) -> String {
17183        format!(
17184            r#"<q:ser><q:idx val="{index}"/><q:order val="{index}"/><q:yVal><q:numRef><q:f>Sheet1!$B$2</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="1"/><q:pt idx="0"><q:v>4</q:v></q:pt></q:numCache></q:numRef></q:yVal></q:ser>"#
17185        )
17186    }
17187
17188    fn scatter_without_y(index: u32) -> String {
17189        format!(
17190            r#"<q:ser><q:idx val="{index}"/><q:order val="{index}"/><q:xVal><q:numRef><q:f>Sheet1!$A$2</q:f><q:numCache><q:formatCode>General</q:formatCode><q:ptCount val="1"/><q:pt idx="0"><q:v>1</q:v></q:pt></q:numCache></q:numRef></q:xVal></q:ser>"#
17191        )
17192    }
17193
17194    fn remaining_plot_fixtures() -> Vec<(&'static str, String, bool)> {
17195        let series = plot_series(0);
17196        let scatter = numeric_plot_series(0);
17197        vec![
17198            (
17199                "pieChart",
17200                format!(
17201                    r#"<q:pieChart x:keep="pie"><q:varyColors val="1"/>{series}<q:dLbls><q:showVal val="1"/></q:dLbls><q:firstSliceAng val="30"/><q:extLst><x:pie/></q:extLst></q:pieChart>"#
17202                ),
17203                false,
17204            ),
17205            (
17206                "doughnutChart",
17207                format!(
17208                    r#"<q:doughnutChart x:keep="doughnut"><q:varyColors val="1"/>{series}<q:dLbls><q:showPercent val="1"/></q:dLbls><q:firstSliceAng val="45"/><q:holeSize val="60"/><q:extLst><x:doughnut/></q:extLst></q:doughnutChart>"#
17209                ),
17210                false,
17211            ),
17212            (
17213                "areaChart",
17214                format!(
17215                    r#"<q:areaChart x:keep="area"><q:grouping val="standard"/><q:varyColors val="0"/>{series}<q:dLbls><q:showVal val="1"/></q:dLbls><q:dropLines><x:line/></q:dropLines><q:axId val="-1884094432"/><q:axId val="-1884097184"/><q:extLst><x:area/></q:extLst></q:areaChart>"#
17216                ),
17217                true,
17218            ),
17219            (
17220                "scatterChart",
17221                format!(
17222                    r#"<q:scatterChart x:keep="scatter"><q:scatterStyle val="marker"/><q:varyColors val="0"/>{scatter}<q:dLbls><q:showVal val="1"/></q:dLbls><q:axId val="-1884094432"/><q:axId val="-1884097184"/><q:extLst><x:scatter/></q:extLst></q:scatterChart>"#
17223                ),
17224                true,
17225            ),
17226            (
17227                "radarChart",
17228                format!(
17229                    r#"<q:radarChart x:keep="radar"><q:radarStyle val="marker"/><q:varyColors val="0"/>{series}<q:dLbls><q:showVal val="1"/></q:dLbls><q:axId val="-1884094432"/><q:axId val="-1884097184"/><q:extLst><x:radar/></q:extLst></q:radarChart>"#
17230                ),
17231                true,
17232            ),
17233        ]
17234    }
17235
17236    fn bar_plot(extra: &str) -> String {
17237        format!(
17238            r#"<q:barChart x:keep="bar"><q:barDir val="col"/><q:grouping val="clustered"/><q:varyColors val="1"/>{}<q:dLbls><q:showVal val="1"/></q:dLbls><q:gapWidth val="150"/><q:overlap val="0"/><q:serLines><x:line/></q:serLines><q:axId val="-1884094432"/><q:axId val="-1884097184"/>{extra}</q:barChart>"#,
17239            plot_series(0)
17240        )
17241    }
17242
17243    fn line_plot(extra: &str) -> String {
17244        format!(
17245            r#"<q:lineChart x:keep="line"><q:grouping val="standard"/><q:varyColors val="0"/>{}<q:dLbls><q:showVal val="1"/></q:dLbls><q:dropLines><x:line/></q:dropLines><q:marker val="1"/><q:smooth val="0"/><q:axId val="-1884094432"/><q:axId val="-1884097184"/>{extra}</q:lineChart>"#,
17246            plot_series(0)
17247        )
17248    }
17249
17250    #[test]
17251    fn bar_and_line_plots_round_trip_and_render() {
17252        for (xml, is_bar) in [
17253            (chart_with_plot(&bar_plot("")), true),
17254            (chart_with_plot(&line_plot("")), false),
17255        ] {
17256            let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
17257            let plots = parsed.chart.plot_area.plots().unwrap();
17258            assert_eq!(plots.len(), 1);
17259            assert_eq!(matches!(plots[0], Plot::Bar { .. }), is_bar);
17260            assert_eq!(matches!(plots[0], Plot::Line { .. }), !is_bar);
17261            assert_eq!(parsed.chart.plot_area.axes().unwrap().len(), 2);
17262            let written = parsed.to_xml().unwrap();
17263            let reparsed = CT_ChartSpace::from_xml(&written).unwrap();
17264            assert_eq!(parsed, reparsed);
17265        }
17266        if let Some(corpus) = require_or_skip_corpus() {
17267            verify_bar_and_line_viewer_gate(&corpus);
17268        }
17269    }
17270
17271    #[test]
17272    fn ppm_parser_preserves_whitespace_valued_first_pixels() {
17273        for first_pixel in *b" \n\r\t" {
17274            let mut ppm = b"P6\n1 1\n255\n".to_vec();
17275            ppm.extend_from_slice(&[first_pixel, 0x7f, 0xff]);
17276            let (width, height, pixels) = ppm_pixels(&ppm);
17277            assert_eq!((width, height), (1, 1));
17278            assert_eq!(pixels, [first_pixel, 0x7f, 0xff]);
17279        }
17280
17281        let ppm = b"P6\r\n1 1\r\n255\r\n\x7f\xff";
17282        assert_eq!(ppm_pixels(ppm).2, [b'\n', 0x7f, 0xff]);
17283    }
17284
17285    #[test]
17286    fn bar_and_line_plots_write_fixed_prefixes_in_schema_order() {
17287        for xml in [
17288            chart_with_plot(&bar_plot(r#"<q:extLst><x:bar-tail/></q:extLst>"#)),
17289            chart_with_plot(&line_plot(r#"<q:extLst><x:line-tail/></q:extLst>"#)),
17290        ] {
17291            let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
17292            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
17293            assert!(written.contains("<c:barChart") || written.contains("<c:lineChart"));
17294            assert!(!written.contains("<q:barChart"));
17295            assert!(!written.contains("<q:lineChart"));
17296            if written.contains("<c:barChart") {
17297                let positions: Vec<_> = [
17298                    "<c:barDir",
17299                    "<c:grouping",
17300                    "<q:varyColors",
17301                    "<c:ser",
17302                    "<c:dLbls",
17303                    "<c:gapWidth",
17304                    "<c:overlap",
17305                    "<q:serLines",
17306                    "<c:axId",
17307                    "<q:extLst",
17308                ]
17309                .iter()
17310                .map(|tag| written.find(tag).unwrap())
17311                .collect();
17312                assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
17313            } else {
17314                let positions: Vec<_> = [
17315                    "<c:grouping",
17316                    "<q:varyColors",
17317                    "<c:ser",
17318                    "<c:dLbls",
17319                    "<q:dropLines",
17320                    "<c:marker",
17321                    "<c:smooth",
17322                    "<c:axId",
17323                    "<q:extLst",
17324                ]
17325                .iter()
17326                .map(|tag| written.find(tag).unwrap())
17327                .collect();
17328                assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
17329            }
17330            assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
17331        }
17332
17333        for plot in [bar_plot(""), line_plot("")] {
17334            let first = plot_series(0);
17335            let with_series_boundary = plot.replacen(
17336                &first,
17337                &format!(
17338                    "{first}<!--between-series--><?series-boundary?>\n  {}",
17339                    plot_series(1)
17340                ),
17341                1,
17342            );
17343            let with_axis_boundary = with_series_boundary.replacen(
17344                r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
17345                r#"<q:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
17346  <q:axId val="-1884097184"/>"#,
17347                1,
17348            );
17349            let parsed =
17350                CT_ChartSpace::from_xml(chart_with_plot(&with_axis_boundary).as_bytes()).unwrap();
17351            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
17352            assert!(written.contains("</c:ser><!--between-series--><?series-boundary?>\n  <c:ser"));
17353            assert!(written.contains(
17354                r#"<c:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
17355  <c:axId val="-1884097184"/>"#
17356            ));
17357            assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
17358        }
17359    }
17360
17361    #[test]
17362    fn malformed_bar_and_line_plots_return_errors_without_panicking() {
17363        let valid_series = plot_series(0);
17364        let cases = [
17365            format!(r#"<q:barChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
17366            format!(r#"<q:barChart><q:barDir val="diagonal"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
17367            format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{valid_series}<q:gapWidth val="501"/><q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
17368            format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{valid_series}<q:overlap val="-101"/><q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
17369            format!(r#"<q:lineChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
17370            format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:marker val="maybe"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
17371            r#"<q:lineChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#.to_owned(),
17372            format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:axId val="1"/><q:axId val="1"/></q:lineChart>"#),
17373            format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/><q:grouping val="stacked"/>{valid_series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:barChart>"#),
17374            format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
17375        ];
17376        for plot in cases {
17377            let xml = chart_with_plot(&plot);
17378            let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
17379            assert!(result.is_ok(), "plot parser panicked for {plot}");
17380            assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
17381        }
17382
17383        let missing_axis = format!(
17384            r#"<q:chartSpace xmlns:q="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}"><q:chart><q:plotArea>{}<q:catAx><q:axId val="-1884094432"/><q:scaling/><q:axPos val="b"/><q:crossAx val="-1884097184"/></q:catAx></q:plotArea></q:chart></q:chartSpace>"#,
17385            bar_plot("")
17386        );
17387        assert!(CT_ChartSpace::from_xml(missing_axis.as_bytes()).is_err());
17388
17389        let values =
17390            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
17391        let series = vec![Series::new(0, 0, values)];
17392        let ids = [AxisId::new(1).unwrap(), AxisId::new(2).unwrap()];
17393        let mut invalid =
17394            Plot::bar(BarDirection::Column, BarGrouping::Clustered, series, ids).unwrap();
17395        if let Plot::Bar { gap_width, .. } = &mut invalid {
17396            *gap_width = 501;
17397        }
17398        assert!(invalid.validate().is_err());
17399    }
17400
17401    #[test]
17402    fn unsupported_and_combo_plots_remain_byte_preserved() {
17403        let bar = bar_plot("");
17404        let line = line_plot("");
17405        let combo = chart_with_plot(&format!("<!--before-->{bar}<?between?>{line}<!--after-->"));
17406        let parsed = CT_ChartSpace::from_xml(combo.as_bytes()).unwrap();
17407        assert!(parsed.chart.plot_area.plots().is_err());
17408        let written = parsed.to_xml().unwrap();
17409        for raw in [
17410            bar.as_bytes(),
17411            line.as_bytes(),
17412            br#"<!--before-->"#.as_slice(),
17413            br#"<?between?>"#.as_slice(),
17414            br#"<!--after-->"#.as_slice(),
17415        ] {
17416            assert!(written.windows(raw.len()).any(|window| window == raw));
17417        }
17418
17419        let unsupported = chart_with_plot(
17420            r#"<q:bar3DChart x:keep="three"><q:barDir val="col"/><q:ser><x:opaque/></q:ser></q:bar3DChart>"#,
17421        );
17422        let parsed = CT_ChartSpace::from_xml(unsupported.as_bytes()).unwrap();
17423        assert!(parsed.chart.plot_area.plots().is_err());
17424        assert!(
17425            String::from_utf8(parsed.to_xml().unwrap())
17426                .unwrap()
17427                .contains(r#"<q:bar3DChart x:keep="three">"#)
17428        );
17429    }
17430
17431    #[test]
17432    fn public_plot_edits_preserve_axes_and_unselected_payloads() {
17433        let xml = chart_with_plot(&bar_plot(
17434            r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#,
17435        ));
17436        let mut parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
17437        let axes_before = parsed.chart.plot_area.axes().unwrap();
17438        match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
17439            Plot::Bar {
17440                gap_width,
17441                overlap,
17442                series,
17443                ..
17444            } => {
17445                *gap_width = 225;
17446                *overlap = -25;
17447                series[0].index = 7;
17448                series[0].values.values[0] = 9.0;
17449            }
17450            _ => panic!("expected bar plot"),
17451        }
17452        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
17453        assert!(written.contains(r#"<c:gapWidth val="225"/>"#));
17454        assert!(written.contains(r#"<c:overlap val="-25"/>"#));
17455        assert!(written.contains("<c:v>9</c:v>"));
17456        assert!(written.contains("<q:varyColors val=\"1\"/>"));
17457        assert!(written.find("<q:varyColors").unwrap() < written.find("<c:ser").unwrap());
17458        assert!(written.contains("<q:serLines><x:line/></q:serLines>"));
17459        assert!(written.contains(r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#));
17460        let reparsed = CT_ChartSpace::from_xml(written.as_bytes()).unwrap();
17461        assert_eq!(axes_before, reparsed.chart.plot_area.axes().unwrap());
17462
17463        let first = plot_series(0);
17464        let colliding_series =
17465            bar_plot("").replacen(&first, &format!("{first}{}", plot_series(1)), 1);
17466        let mut collision =
17467            CT_ChartSpace::from_xml(chart_with_plot(&colliding_series).as_bytes()).unwrap();
17468        if let Plot::Bar { series, .. } = &mut collision.chart.plot_area.plots_mut().unwrap()[0] {
17469            series[0].index = series[1].index;
17470            series[0].order = series[1].order;
17471        }
17472        let collision = String::from_utf8(collision.to_xml().unwrap()).unwrap();
17473        assert!(collision.find("<q:varyColors").unwrap() < collision.find("<c:ser").unwrap());
17474
17475        let mut inserted = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
17476        if let Plot::Bar { series, .. } = &mut inserted.chart.plot_area.plots_mut().unwrap()[0] {
17477            let mut new_series = series[0].clone();
17478            new_series.index = 99;
17479            new_series.order = 99;
17480            series.insert(0, new_series);
17481        }
17482        let inserted = String::from_utf8(inserted.to_xml().unwrap()).unwrap();
17483        assert!(inserted.find("<q:varyColors").unwrap() < inserted.find("<c:ser").unwrap());
17484
17485        let axis_payload = bar_plot("").replacen(
17486            r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
17487            r#"<q:axId val="-1884094432" x:slot="category"/><!--axis-anchor--><q:axId val="-1884097184" x:slot="value"/>"#,
17488            1,
17489        );
17490        let mut swapped =
17491            CT_ChartSpace::from_xml(chart_with_plot(&axis_payload).as_bytes()).unwrap();
17492        if let Plot::Bar { axis_ids, .. } = &mut swapped.chart.plot_area.plots_mut().unwrap()[0] {
17493            axis_ids.swap(0, 1);
17494        }
17495        let swapped = String::from_utf8(swapped.to_xml().unwrap()).unwrap();
17496        let anchor = swapped.find("<!--axis-anchor-->").unwrap();
17497        let value_axis = swapped
17498            .find(r#"<c:axId val="-1884097184" x:slot="value"/>"#)
17499            .unwrap();
17500        let category_axis = swapped
17501            .find(r#"<c:axId val="-1884094432" x:slot="category"/>"#)
17502            .unwrap();
17503        let series_lines = swapped.find("<q:serLines").unwrap();
17504        assert!(series_lines < value_axis && series_lines < category_axis);
17505        assert!(anchor < value_axis && value_axis < category_axis);
17506
17507        let mut replaced = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
17508        let Plot::Bar {
17509            series, axis_ids, ..
17510        } = &replaced.chart.plot_area.plots().unwrap()[0]
17511        else {
17512            panic!("expected bar plot");
17513        };
17514        let line = Plot::line(Grouping::Standard, series.clone(), *axis_ids).unwrap();
17515        replaced.chart.plot_area.plots_mut().unwrap()[0] = line;
17516        assert!(replaced.to_xml().is_err());
17517    }
17518
17519    #[test]
17520    fn every_corpus_bar_and_line_plot_round_trips_structurally() {
17521        let Some(corpus) = require_or_skip_corpus() else {
17522            return;
17523        };
17524        verify_fetched_corpus(&corpus);
17525        let mut typed_bar_count = 0usize;
17526        let mut typed_line_count = 0usize;
17527        let mut opaque_bar_count = 0usize;
17528        let mut opaque_line_count = 0usize;
17529        let mut opaque_combo_count = 0usize;
17530        for path in manifest_paths() {
17531            let package = OpcPackage::open(corpus.join(path))
17532                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
17533            for (part, xml) in &package.parts {
17534                if !is_chart_part(part) {
17535                    continue;
17536                }
17537                let chart = CT_ChartSpace::from_xml(xml)
17538                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
17539                match chart.chart.plot_area.plots() {
17540                    Ok(plots) => {
17541                        for plot in plots {
17542                            match plot {
17543                                Plot::Bar { .. } => typed_bar_count += 1,
17544                                Plot::Line { .. } => typed_line_count += 1,
17545                                _ => {}
17546                            }
17547                        }
17548                    }
17549                    Err(_) => {
17550                        let mut preserved_plot_count = 0usize;
17551                        for raw in chart.chart.plot_area.raw_children.at(0) {
17552                            if let Ok(Some(local)) = super::chart_root_local(
17553                                raw,
17554                                &chart.chart.plot_area.namespace_bindings,
17555                            ) {
17556                                match local.as_slice() {
17557                                    b"barChart" => {
17558                                        opaque_bar_count += 1;
17559                                        preserved_plot_count += 1;
17560                                    }
17561                                    b"lineChart" => {
17562                                        opaque_line_count += 1;
17563                                        preserved_plot_count += 1;
17564                                    }
17565                                    _ => {}
17566                                }
17567                            }
17568                        }
17569                        if preserved_plot_count > 1 {
17570                            opaque_combo_count += 1;
17571                        }
17572                    }
17573                }
17574                let written = chart
17575                    .to_xml()
17576                    .unwrap_or_else(|error| panic!("{path} {part}: chart write failed: {error}"));
17577                let reparsed = CT_ChartSpace::from_xml(&written).unwrap_or_else(|error| {
17578                    panic!("{path} {part}: written chart parse failed: {error}")
17579                });
17580                assert_eq!(chart, reparsed, "{path} {part}: chart model changed");
17581            }
17582        }
17583        assert_eq!(typed_bar_count, 11, "typed corpus bar coverage changed");
17584        assert_eq!(typed_line_count, 2, "typed corpus line coverage changed");
17585        assert_eq!(opaque_bar_count, 1, "preserved combo bar coverage changed");
17586        assert_eq!(
17587            opaque_line_count, 1,
17588            "preserved combo line coverage changed"
17589        );
17590        assert_eq!(opaque_combo_count, 1, "preserved combo coverage changed");
17591        assert_eq!(typed_bar_count + opaque_bar_count, 12);
17592        assert_eq!(typed_line_count + opaque_line_count, 3);
17593        eprintln!(
17594            "ChartML plot corpus gate checked {typed_bar_count} typed bar, {typed_line_count} typed line, and one preserved bar-line combination"
17595        );
17596    }
17597
17598    fn verify_remaining_plot_viewer_gate(corpus: &Path) {
17599        verify_fetched_corpus(corpus);
17600        assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
17601        assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
17602        let source = corpus.join("bar-chart.pptx");
17603        let source_sha = sha256(&source);
17604        let temp_root = std::env::temp_dir().join(format!(
17605            "oxml-chart-f122-viewer-gate-{}",
17606            std::process::id()
17607        ));
17608        if temp_root.exists() {
17609            fs::remove_dir_all(&temp_root).expect("remove stale F-122 evidence");
17610        }
17611        fs::create_dir_all(&temp_root).expect("create F-122 evidence root");
17612
17613        for (kind, plot, axes) in remaining_plot_fixtures() {
17614            let mut package = OpcPackage::open(&source).expect("open F-122 viewer source");
17615            package.set_part(
17616                "/ppt/charts/chart1.xml",
17617                CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes())
17618                    .expect("parse F-122 viewer chart")
17619                    .to_xml()
17620                    .expect("serialize F-122 viewer chart"),
17621            );
17622            let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
17623            package.save(&unbound).expect("save F-122 viewer candidate");
17624            let candidate_sha = sha256(&unbound);
17625            let evidence = temp_root.join(format!("{source_sha}-{candidate_sha}"));
17626            fs::create_dir(&evidence).expect("create SHA-bound F-122 evidence directory");
17627            let candidate = evidence.join(format!("{kind}-candidate.pptx"));
17628            fs::rename(&unbound, &candidate).expect("bind F-122 candidate to SHA");
17629            assert_eq!(sha256(&candidate), candidate_sha);
17630            let render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
17631            let bytes = fs::read(&render).expect("read F-122 viewer PPM");
17632            let (width, height, pixels) = ppm_pixels(&bytes);
17633            let left = width / 5;
17634            let right = width * 4 / 5;
17635            let top = height * 3 / 20;
17636            let bottom = height * 17 / 20;
17637            let mut nonblank = 0usize;
17638            for y in top..bottom {
17639                for x in left..right {
17640                    let offset = (y * width + x) * 3;
17641                    if pixels[offset..offset + 3]
17642                        .iter()
17643                        .any(|channel| *channel < 245)
17644                    {
17645                        nonblank += 1;
17646                    }
17647                }
17648            }
17649            const CHART_RECTANGLE_NONBLANK_THRESHOLD: usize = 1_000;
17650            assert!(
17651                nonblank >= CHART_RECTANGLE_NONBLANK_THRESHOLD,
17652                "{kind}: chart rectangle [{left},{top}) to [{right},{bottom}) contains {nonblank} nonblank pixels, below {CHART_RECTANGLE_NONBLANK_THRESHOLD}"
17653            );
17654            eprintln!(
17655                "F-122 {kind} viewer gate source deck {source_sha}, candidate deck {candidate_sha}, render {}, chart rectangle [{left},{top}) to [{right},{bottom}), RGB<245 nonblank pixels {nonblank} >= {CHART_RECTANGLE_NONBLANK_THRESHOLD}",
17656                sha256(&render)
17657            );
17658        }
17659        fs::remove_dir_all(&temp_root).expect("remove F-122 viewer evidence");
17660    }
17661
17662    fn verify_bar_and_line_viewer_gate(corpus: &Path) {
17663        verify_fetched_corpus(corpus);
17664        assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
17665        assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
17666        let temp_root = std::env::temp_dir().join(format!(
17667            "oxml-chart-f121-viewer-gate-{}",
17668            std::process::id()
17669        ));
17670        if temp_root.exists() {
17671            fs::remove_dir_all(&temp_root).expect("remove stale F-121 evidence");
17672        }
17673        fs::create_dir_all(&temp_root).expect("create F-121 evidence root");
17674
17675        for (kind, deck) in [("bar", "bar-chart.pptx"), ("line", "line-chart.pptx")] {
17676            let source = corpus.join(deck);
17677            let original_sha = sha256(&source);
17678            let mut package = OpcPackage::open(&source)
17679                .unwrap_or_else(|error| panic!("{deck}: open viewer source: {error}"));
17680            let chart_part = "/ppt/charts/chart1.xml";
17681            let original_parts = package.parts.clone();
17682            let parsed = CT_ChartSpace::from_xml(
17683                package
17684                    .get_part(chart_part)
17685                    .unwrap_or_else(|| panic!("{deck}: missing {chart_part}")),
17686            )
17687            .unwrap_or_else(|error| panic!("{deck}: parse viewer chart: {error}"));
17688            assert!(
17689                parsed.chart.plot_area.plots().is_ok(),
17690                "{deck}: plot is opaque"
17691            );
17692            package.set_part(
17693                chart_part,
17694                parsed
17695                    .to_xml()
17696                    .unwrap_or_else(|error| panic!("{deck}: serialize viewer chart: {error}")),
17697            );
17698            for (part, bytes) in &original_parts {
17699                if part == chart_part {
17700                    assert_ne!(package.parts[part], *bytes, "{deck}: chart did not rewrite");
17701                } else {
17702                    assert_eq!(package.parts[part], *bytes, "{deck}: changed part {part}");
17703                }
17704            }
17705
17706            let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
17707            package
17708                .save(&unbound)
17709                .unwrap_or_else(|error| panic!("{deck}: save viewer candidate: {error}"));
17710            let candidate_sha = sha256(&unbound);
17711            let evidence = temp_root.join(format!("{original_sha}-{candidate_sha}"));
17712            fs::create_dir(&evidence).expect("create SHA-bound F-121 evidence directory");
17713            let original = evidence.join(format!("{kind}-original.pptx"));
17714            let candidate = evidence.join(format!("{kind}-candidate.pptx"));
17715            fs::copy(&source, &original).expect("copy SHA-bound viewer original");
17716            fs::rename(&unbound, &candidate).expect("bind viewer candidate to SHA");
17717            assert_eq!(sha256(&original), original_sha);
17718            assert_eq!(sha256(&candidate), candidate_sha);
17719
17720            let original_render = render_deck_to_ppm(&original, &evidence, kind, "original");
17721            let candidate_render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
17722            let original_bytes = fs::read(&original_render).expect("read original PPM");
17723            let candidate_bytes = fs::read(&candidate_render).expect("read candidate PPM");
17724            let normalized_mae = normalized_ppm_mae(&original_bytes, &candidate_bytes);
17725            assert!(
17726                normalized_mae <= PLOT_RENDER_NORMALIZED_MAE_THRESHOLD,
17727                "{deck}: normalized RGB MAE {normalized_mae:.8} exceeds {:.8}",
17728                PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
17729            );
17730            eprintln!(
17731                "F-121 {kind} viewer gate original deck {original_sha}, candidate deck {candidate_sha}, original render {}, candidate render {}, normalized RGB MAE {normalized_mae:.8} <= {:.8}",
17732                sha256(&original_render),
17733                sha256(&candidate_render),
17734                PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
17735            );
17736        }
17737        fs::remove_dir_all(&temp_root).expect("remove F-121 viewer evidence");
17738    }
17739
17740    fn render_deck_to_ppm(deck: &Path, evidence: &Path, kind: &str, side: &str) -> PathBuf {
17741        let profile = std::env::temp_dir().join(format!(
17742            "oxml-chart-f121-{kind}-{side}-profile-{}",
17743            std::process::id()
17744        ));
17745        if profile.exists() {
17746            fs::remove_dir_all(&profile).expect("remove stale F-121 viewer profile");
17747        }
17748        let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
17749        let conversion = Command::new("soffice")
17750            .args([
17751                "--headless",
17752                &profile_argument,
17753                "--convert-to",
17754                "pdf:impress_pdf_Export",
17755                "--outdir",
17756            ])
17757            .arg(evidence)
17758            .arg(deck)
17759            .output()
17760            .expect("run pinned LibreOffice F-121 viewer gate");
17761        assert!(
17762            conversion.status.success(),
17763            "{}: LibreOffice conversion failed: {}",
17764            deck.display(),
17765            String::from_utf8_lossy(&conversion.stderr)
17766        );
17767        let pdf = deck.with_extension("pdf");
17768        assert!(
17769            pdf.is_file(),
17770            "LibreOffice did not create {}",
17771            pdf.display()
17772        );
17773        let prefix = evidence.join(format!("{kind}-{side}"));
17774        let raster = Command::new("pdftoppm")
17775            .args(["-f", "1", "-singlefile", "-r", "150"])
17776            .arg(&pdf)
17777            .arg(&prefix)
17778            .output()
17779            .expect("run pinned Poppler F-121 raster gate");
17780        assert!(
17781            raster.status.success(),
17782            "{}: pdftoppm failed: {}",
17783            pdf.display(),
17784            String::from_utf8_lossy(&raster.stderr)
17785        );
17786        if profile.exists() {
17787            fs::remove_dir_all(&profile).expect("remove F-121 viewer profile");
17788        }
17789        prefix.with_extension("ppm")
17790    }
17791
17792    fn normalized_ppm_mae(left: &[u8], right: &[u8]) -> f64 {
17793        let (left_width, left_height, left_pixels) = ppm_pixels(left);
17794        let (right_width, right_height, right_pixels) = ppm_pixels(right);
17795        assert_eq!((left_width, left_height), (right_width, right_height));
17796        assert_eq!(left_pixels.len(), right_pixels.len());
17797        let difference: u64 = left_pixels
17798            .iter()
17799            .zip(right_pixels)
17800            .map(|(left, right)| u64::from(left.abs_diff(*right)))
17801            .sum();
17802        difference as f64 / (left_pixels.len() as f64 * 255.0)
17803    }
17804
17805    fn ppm_pixels(bytes: &[u8]) -> (usize, usize, &[u8]) {
17806        let mut cursor = 0usize;
17807        let mut tokens = Vec::new();
17808        while tokens.len() < 4 {
17809            while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
17810                cursor += 1;
17811            }
17812            if cursor < bytes.len() && bytes[cursor] == b'#' {
17813                while cursor < bytes.len() && bytes[cursor] != b'\n' {
17814                    cursor += 1;
17815                }
17816                continue;
17817            }
17818            let start = cursor;
17819            while cursor < bytes.len() && !bytes[cursor].is_ascii_whitespace() {
17820                cursor += 1;
17821            }
17822            tokens.push(std::str::from_utf8(&bytes[start..cursor]).expect("PPM header UTF-8"));
17823        }
17824        assert_eq!(tokens[0], "P6");
17825        let width = tokens[1].parse::<usize>().expect("PPM width");
17826        let height = tokens[2].parse::<usize>().expect("PPM height");
17827        assert_eq!(tokens[3], "255");
17828        assert!(
17829            cursor < bytes.len() && bytes[cursor].is_ascii_whitespace(),
17830            "PPM header must end with whitespace"
17831        );
17832        cursor += 1;
17833        let pixels = &bytes[cursor..];
17834        assert_eq!(pixels.len(), width * height * 3, "PPM pixel length");
17835        (width, height, pixels)
17836    }
17837    #[test]
17838    fn fx253_invalid_selected_metadata_is_distinct_from_valid_unsupported_styles() {
17839        for (plot, scalar, valid, expected) in [
17840            ("scatterChart", "scatterStyle", "smoothMarker", None),
17841            ("radarChart", "radarStyle", "filled", None),
17842            ("lineChart", "grouping", "standard", Some(65)),
17843        ] {
17844            let shell = format!(
17845                r#"<c:chartSpace xmlns:c="{C_NS}"><c:chart><c:plotArea><c:{plot}><c:{scalar} val="{valid}"/></c:{plot}></c:plotArea></c:chart></c:chartSpace>"#
17846            );
17847            assert_eq!(super::read_chart_type(shell.as_bytes()).unwrap(), expected);
17848            let invalid = shell.replace(&format!("val=\"{valid}\""), "val=\"garbage\"");
17849            assert!(super::read_chart_type(invalid.as_bytes()).is_err());
17850        }
17851        for symbol in ["none", "circle", "auto", "garbage"] {
17852            let shell = format!(
17853                r#"<c:chartSpace xmlns:c="{C_NS}"><c:chart><c:plotArea><c:lineChart><c:ser><c:marker><c:symbol val="{symbol}"/></c:marker></c:ser></c:lineChart></c:plotArea></c:chart></c:chartSpace>"#
17854            );
17855            let actual = super::read_chart_type(shell.as_bytes());
17856            if symbol == "garbage" {
17857                assert!(actual.is_err());
17858            } else {
17859                assert_eq!(actual.unwrap(), Some(if symbol == "none" { 4 } else { 65 }));
17860            }
17861        }
17862    }
17863
17864    #[test]
17865    fn fx253_many_sparse_series_fit_the_linear_metadata_work_budget() {
17866        let series = "<c:ser><c:idx val=\"0\"/><c:order val=\"0\"/><c:marker><c:symbol val=\"none\"/></c:marker></c:ser>".repeat(10_000);
17867        let xml = format!(
17868            r#"<c:chartSpace xmlns:c="{C_NS}"><c:chart><c:plotArea><c:lineChart><c:grouping val="standard"/>{series}</c:lineChart></c:plotArea></c:chart></c:chartSpace>"#
17869        );
17870        // The getter has an explicit four-visits-per-node metadata budget.
17871        // Global rescans for each series exhaust it instead of hiding quadratic work.
17872        assert_eq!(super::read_chart_type(xml.as_bytes()).unwrap(), Some(4));
17873        let nodes = super::chart_source_nodes(xml.as_bytes()).unwrap();
17874        let mut children = vec![Vec::new(); nodes.len()];
17875        for (index, node) in nodes.iter().enumerate() {
17876            if let Some(parent) = node.parent {
17877                children[parent].push(index);
17878            }
17879        }
17880        assert!(super::chart_optional_child_node(&nodes, &children, 0, b"chart", &mut 0).is_err());
17881    }
17882
17883    #[test]
17884    fn fx253_imported_chart_types_use_direct_qualified_metadata() {
17885        let data = ChartData {
17886            categories: vec!["1".into(), "2".into()],
17887            series: vec![("Series".into(), vec![1.0, 2.0])],
17888            ..ChartData::default()
17889        };
17890        for (kind, expected) in [
17891            (ChartKind::Bar, 51),
17892            (ChartKind::Line, 65),
17893            (ChartKind::Pie, 5),
17894            (ChartKind::Doughnut, -4120),
17895            (ChartKind::Area, 1),
17896            (ChartKind::Scatter, 74),
17897            (ChartKind::Radar, 81),
17898        ] {
17899            let (xml, _) = authored_chart_parts(kind, &data, "rIdWorkbook").unwrap();
17900            let source = String::from_utf8(xml).unwrap();
17901            assert_eq!(
17902                super::read_chart_type(source.as_bytes()).unwrap(),
17903                Some(expected)
17904            );
17905            let changed = match kind {
17906                ChartKind::Bar => source
17907                    .replace("val=\"col\"", "val=\"bar\"")
17908                    .replace("val=\"clustered\"", "val=\"stacked\""),
17909                ChartKind::Line => source.replace(
17910                    "</c:ser>",
17911                    "<c:marker><c:symbol val=\"none\"/></c:marker></c:ser>",
17912                ),
17913                ChartKind::Pie => source.replace("</c:ser>", "<c:explosion val=\"0\"/></c:ser>"),
17914                _ => continue,
17915            };
17916            let expected = match kind {
17917                ChartKind::Bar => 58,
17918                ChartKind::Line => 4,
17919                _ => 69,
17920            };
17921            assert_eq!(
17922                super::read_chart_type(changed.as_bytes()).unwrap(),
17923                Some(expected)
17924            );
17925            if kind == ChartKind::Bar {
17926                let column = changed.replace("val=\"bar\"", "val=\"col\"");
17927                assert_eq!(super::read_chart_type(column.as_bytes()).unwrap(), Some(52));
17928            }
17929            let alias = changed.replace("xmlns:c=", "xmlns:q=").replace("c:", "q:");
17930            assert_eq!(
17931                super::read_chart_type(alias.as_bytes()).unwrap(),
17932                Some(expected)
17933            );
17934            let foreign = changed
17935                .replace("<c:marker>", "<c:marker xmlns:c=\"urn:foreign\">")
17936                .replace("<c:explosion ", "<c:explosion xmlns:c=\"urn:foreign\" ");
17937            if kind != ChartKind::Bar {
17938                assert_eq!(
17939                    super::read_chart_type(foreign.as_bytes()).unwrap(),
17940                    Some(if kind == ChartKind::Line { 65 } else { 5 })
17941                );
17942                let nested = source.replace("</c:ser>", "<c:extLst><c:ext uri=\"control\"><c:marker><c:symbol val=\"none\"/></c:marker><c:explosion val=\"0\"/></c:ext></c:extLst></c:ser>");
17943                assert_eq!(
17944                    super::read_chart_type(nested.as_bytes()).unwrap(),
17945                    Some(if kind == ChartKind::Line { 65 } else { 5 })
17946                );
17947            }
17948        }
17949        let shell = format!(
17950            "<c:chartSpace xmlns:c=\"{C_NS}\"><c:chart><c:plotArea><c:bubbleChart/></c:plotArea></c:chart></c:chartSpace>"
17951        );
17952        assert_eq!(super::read_chart_type(shell.as_bytes()).unwrap(), None);
17953        assert!(super::read_chart_type(b"<c:chartSpace").is_err());
17954        let mixed = shell.replace("<c:bubbleChart/>", "<c:pieChart/><c:barChart/>");
17955        assert_eq!(super::read_chart_type(mixed.as_bytes()).unwrap(), None);
17956        let foreign = shell
17957            .replace("xmlns:c=", "xmlns:foreign=")
17958            .replace("c:", "foreign:")
17959            .replace(C_NS, "urn:foreign");
17960        assert!(super::read_chart_type(foreign.as_bytes()).is_err());
17961        let invalid = shell.replace(
17962            "<c:bubbleChart/>",
17963            "<c:barChart><c:barDir val=\"invalid\"/></c:barChart>",
17964        );
17965        assert!(super::read_chart_type(invalid.as_bytes()).is_err());
17966        let duplicate = invalid
17967            .replace("val=\"invalid\"", "val=\"col\"")
17968            .replace("</c:barChart>", "<c:barDir val=\"bar\"/></c:barChart>");
17969        assert!(super::read_chart_type(duplicate.as_bytes()).is_err());
17970    }
17971
17972    #[test]
17973    fn fx215_existing_chart_data_projection() {
17974        let data = ChartData {
17975            categories: vec!["1".into(), "2".into()],
17976            series: vec![("Revenue & margin".into(), vec![1.25, 2.5])],
17977            number_format: Some("0.00".into()),
17978            ..ChartData::default()
17979        };
17980        for kind in [
17981            ChartKind::Bar,
17982            ChartKind::Line,
17983            ChartKind::Pie,
17984            ChartKind::Doughnut,
17985            ChartKind::Area,
17986            ChartKind::Scatter,
17987            ChartKind::Radar,
17988        ] {
17989            let (xml, _) = authored_chart_parts(kind, &data, "rIdWorkbook").unwrap();
17990            let original = xml.clone();
17991            let (actual_kind, actual) = read_chart_data(&xml).unwrap();
17992            assert_eq!(actual_kind, kind);
17993            assert_eq!(actual.categories, data.categories);
17994            assert_eq!(actual.series, data.series);
17995            assert_eq!(actual.number_format, data.number_format);
17996            assert_eq!(xml, original);
17997            assert_eq!(
17998                chart_workbook_relationship_id(&xml).unwrap().as_deref(),
17999                Some("rIdWorkbook")
18000            );
18001        }
18002    }
18003
18004    #[test]
18005    fn fx215_workbook_relationship_ignores_foreign_id_and_accepts_alias() {
18006        let data = ChartData {
18007            categories: vec!["One".into()],
18008            series: vec![("Values".into(), vec![2.0])],
18009            ..ChartData::default()
18010        };
18011        let (xml, _) = authored_chart_parts(ChartKind::Bar, &data, "rIdWorkbook").unwrap();
18012        let xml = String::from_utf8(xml)
18013            .unwrap()
18014            .replace("r:id=", "rr:id=")
18015            .replace(
18016                "<c:externalData",
18017                &format!(
18018                    "<c:externalData xmlns:rr=\"{R_NS}\" xmlns:f=\"urn:foreign\" f:id=\"wrong\""
18019                ),
18020            );
18021        assert_eq!(
18022            chart_workbook_relationship_id(xml.as_bytes())
18023                .unwrap()
18024                .as_deref(),
18025            Some("rIdWorkbook")
18026        );
18027        let foreign = xml.replace(&format!("xmlns:rr=\"{R_NS}\""), "xmlns:rr=\"urn:foreign\"");
18028        assert!(chart_workbook_relationship_id(foreign.as_bytes()).is_err());
18029    }
18030
18031    #[test]
18032    fn fx215_replace_data_updates_cache_formula_and_workbook() {
18033        let original = ChartData {
18034            categories: vec!["North".into(), "South".into()],
18035            series: vec![
18036                ("Revenue".into(), vec![1.0, 2.0]),
18037                ("Cost".into(), vec![0.5, 1.0]),
18038            ],
18039            ..ChartData::default()
18040        };
18041        for kind in [ChartKind::Bar, ChartKind::Line] {
18042            let (xml, workbook) = authored_chart_parts(kind, &original, "rIdWorkbook").unwrap();
18043            for series in [
18044                vec![("New revenue".into(), vec![8.0, 9.0, 10.0])],
18045                vec![
18046                    ("New revenue".into(), vec![8.0, 9.0, 10.0]),
18047                    ("New cost".into(), vec![1.0, 2.0, 3.0]),
18048                    ("Profit".into(), vec![7.0, 7.0, 7.0]),
18049                ],
18050            ] {
18051                let data = ChartData {
18052                    categories: vec!["East".into(), "West".into(), "Central".into()],
18053                    series,
18054                    ..ChartData::default()
18055                };
18056                let (chart, book) = replace_chart_data(&xml, &workbook, &data).unwrap();
18057                assert_eq!(read_chart_data(&chart).unwrap().1.series, data.series);
18058                assert_eq!(
18059                    read_chart_data(&chart).unwrap().1.categories,
18060                    data.categories
18061                );
18062                let package = OpcPackage::from_reader(std::io::Cursor::new(book)).unwrap();
18063                let sheet = String::from_utf8(
18064                    package
18065                        .get_part("/xl/worksheets/sheet1.xml")
18066                        .unwrap()
18067                        .to_vec(),
18068                )
18069                .unwrap();
18070                assert!(sheet.contains("<v>8</v>"));
18071                assert!(sheet.contains("<v>10</v>"));
18072                assert!(sheet.contains("East"));
18073            }
18074        }
18075    }
18076
18077    #[test]
18078    fn fx215_surviving_series_and_workbook_opaque_payload() {
18079        let original = ChartData {
18080            categories: vec!["North".into(), "South".into()],
18081            series: vec![("Revenue".into(), vec![1.0, 2.0])],
18082            ..ChartData::default()
18083        };
18084        let (xml, workbook) =
18085            authored_chart_parts(ChartKind::Bar, &original, "rIdWorkbook").unwrap();
18086        let source=String::from_utf8(xml).unwrap().replace("<c:chartSpace ","<c:chartSpace xmlns:f=\"urn:foreign\" f:keep='root' ").replace("<c:ser ","<c:ser f:keep='series' ").replace("<c:idx", "<f:opaque data='exact'/><c:idx").replace("</c:ser>","<c:extLst><c:ext uri='producer'><f:payload value='retain'/></c:ext></c:extLst></c:ser>");
18087        assert!(source.contains("f:keep='series'"));
18088        assert!(source.contains("<f:opaque data='exact'/>"));
18089        let data = ChartData {
18090            categories: vec!["West".into()],
18091            series: vec![("Changed".into(), vec![9.0])],
18092            ..ChartData::default()
18093        };
18094        let (changed, _) = replace_chart_data(source.as_bytes(), &workbook, &data).unwrap();
18095        let changed = String::from_utf8(changed).unwrap();
18096        for exact in [
18097            "f:keep='root'",
18098            "f:keep='series'",
18099            "<f:opaque data='exact'/>",
18100            "<c:extLst><c:ext uri='producer'><f:payload value='retain'/></c:ext></c:extLst>",
18101        ] {
18102            assert!(changed.contains(exact), "{exact}");
18103        }
18104        let space = CT_ChartSpace::from_xml(changed.as_bytes()).unwrap();
18105        assert_eq!(space.chart.plot_area.series().unwrap()[0].index, 0);
18106        let no_op = replace_chart_data(source.as_bytes(), &workbook, &original).unwrap();
18107        assert_eq!(no_op.0, source.as_bytes());
18108        assert_eq!(no_op.1, workbook);
18109    }
18110
18111    #[test]
18112    fn fx215_all_seven_families_replace_data_without_changing_kind() {
18113        let original = ChartData {
18114            categories: vec!["1".into(), "2".into()],
18115            series: vec![("Old".into(), vec![1.0, 2.0])],
18116            ..ChartData::default()
18117        };
18118        let data = ChartData {
18119            categories: vec!["3".into(), "4".into(), "5".into()],
18120            series: vec![("New".into(), vec![0.25, 0.5, 0.75])],
18121            number_format: Some("0.0%".into()),
18122            ..ChartData::default()
18123        };
18124        for kind in [
18125            ChartKind::Bar,
18126            ChartKind::Line,
18127            ChartKind::Pie,
18128            ChartKind::Doughnut,
18129            ChartKind::Area,
18130            ChartKind::Scatter,
18131            ChartKind::Radar,
18132        ] {
18133            let (chart, book) = authored_chart_parts(kind, &original, "rIdWorkbook").unwrap();
18134            let (updated, workbook) = replace_chart_data(&chart, &book, &data).unwrap();
18135            let (actual, projected) = read_chart_data(&updated).unwrap();
18136            assert_eq!(actual, kind);
18137            assert_eq!(projected.categories, data.categories);
18138            assert_eq!(projected.series, data.series);
18139            assert_eq!(projected.number_format, data.number_format);
18140            let package = OpcPackage::from_reader(std::io::Cursor::new(workbook)).unwrap();
18141            let styles =
18142                String::from_utf8(package.get_part("/xl/styles.xml").unwrap().to_vec()).unwrap();
18143            assert!(styles.contains("formatCode=\"0.0%\""));
18144        }
18145    }
18146    #[test]
18147    fn fx215_qualified_corpus_replacement_cardinalities() {
18148        let corpus = corpus_dir();
18149        assert!(corpus.is_dir(), "F-X215 required corpus is missing");
18150        for deck in ["bar-chart.pptx", "line-chart.pptx", "chart-picture-bg.pptx"] {
18151            let path = corpus.join(deck);
18152            let expected = MANIFEST
18153                .lines()
18154                .find(|line| line.starts_with(&format!("{deck}\t")))
18155                .unwrap()
18156                .split('\t')
18157                .nth(2)
18158                .unwrap();
18159            assert_eq!(sha256(&path), expected);
18160            let package = OpcPackage::open(&path).unwrap();
18161            let chart_part = "/ppt/charts/chart1.xml";
18162            let chart = package.get_part(chart_part).unwrap();
18163            let (kind, original) = read_chart_data(chart).unwrap();
18164            let workbook_id = chart_workbook_relationship_id(chart).unwrap().unwrap();
18165            let rel = package
18166                .get_part_rels(chart_part)
18167                .unwrap()
18168                .items
18169                .iter()
18170                .find(|rel| rel.id == workbook_id)
18171                .unwrap();
18172            assert_eq!(rel.rel_type, oxml_opc::relationship::rel_types::PACKAGE);
18173            assert_ne!(rel.target_mode.as_deref(), Some("External"));
18174            let workbook_part = OpcPackage::resolve_rel_target(chart_part, &rel.target);
18175            let workbook = package.get_part(&workbook_part).unwrap();
18176            let mut variants = Vec::new();
18177            if original.series.len() > 1 {
18178                let mut fewer = original.clone();
18179                fewer.series.pop();
18180                variants.push(("fewer", fewer));
18181            }
18182            let mut more = original.clone();
18183            more.series.push((
18184                "Added exact values".into(),
18185                (0..more.categories.len())
18186                    .map(|index| 100.0 + index as f64)
18187                    .collect(),
18188            ));
18189            variants.push(("more", more));
18190            let mut categories = original.clone();
18191            categories.categories.push("Added category".into());
18192            for (_, values) in &mut categories.series {
18193                values.push(777.0);
18194            }
18195            variants.push(("categories", categories));
18196            for (label, data) in variants {
18197                let (changed, book) = replace_chart_data(chart, workbook, &data)
18198                    .unwrap_or_else(|error| panic!("{deck}/{label}: {error}"));
18199                let (actual, projection) = read_chart_data(&changed).unwrap();
18200                assert_eq!(actual, kind);
18201                assert_eq!(projection.categories, data.categories);
18202                assert_eq!(projection.series, data.series);
18203                let mut candidate = package.clone();
18204                candidate.set_part(chart_part, changed);
18205                candidate.set_part(&workbook_part, book);
18206                let mut bytes = std::io::Cursor::new(Vec::new());
18207                candidate.write_to(&mut bytes).unwrap();
18208                if let Some(directory) = std::env::var_os("RDOCX_FX215_CANDIDATES_DIR") {
18209                    let directory = PathBuf::from(directory);
18210                    fs::create_dir_all(&directory).unwrap();
18211                    fs::write(
18212                        directory.join(format!("{deck}-{label}.pptx")),
18213                        bytes.get_ref(),
18214                    )
18215                    .unwrap();
18216                }
18217                let reopened =
18218                    OpcPackage::from_reader(std::io::Cursor::new(bytes.into_inner())).unwrap();
18219                for (part, source) in &package.parts {
18220                    if part != chart_part && part != &workbook_part {
18221                        assert_eq!(
18222                            reopened.get_part(part).unwrap(),
18223                            source,
18224                            "{deck}/{label}: {part}"
18225                        );
18226                    }
18227                }
18228                for (owner, relationships) in &package.part_rels {
18229                    assert_eq!(
18230                        reopened.get_part_rels(owner).unwrap().to_xml().unwrap(),
18231                        relationships.to_xml().unwrap()
18232                    );
18233                }
18234                let old_book = OpcPackage::from_reader(std::io::Cursor::new(workbook)).unwrap();
18235                let new_book = OpcPackage::from_reader(std::io::Cursor::new(
18236                    reopened.get_part(&workbook_part).unwrap(),
18237                ))
18238                .unwrap();
18239                for (part, source) in &old_book.parts {
18240                    if part != "/xl/worksheets/sheet1.xml" && part != "/xl/styles.xml" {
18241                        assert_eq!(
18242                            new_book.get_part(part).unwrap(),
18243                            source,
18244                            "{deck}/{label}: workbook {part}"
18245                        );
18246                    }
18247                }
18248                eprintln!("F-X215 corpus {deck}/{label} qualified cache and workbook candidate");
18249            }
18250        }
18251    }
18252    #[test]
18253    fn fx215_sparse_cache_projection_retains_missing_values() {
18254        let plot = format!(
18255            r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:barChart>"#,
18256            sparse_category_series()
18257        );
18258        let xml = chart_with_plot(&plot);
18259        let (_, data) = read_chart_data(xml.as_bytes()).unwrap();
18260        assert_eq!(data.categories, vec!["North", "", "West"]);
18261        assert_eq!(data.series[0].1[0], 1.0);
18262        assert!(data.series[0].1[1].is_nan());
18263        assert_eq!(data.series[0].1[2], 2.0);
18264    }
18265
18266    #[test]
18267    fn fx215_invalid_data_and_external_formula_preserve_inputs() {
18268        let original = ChartData {
18269            categories: vec!["North".into(), "South".into()],
18270            series: vec![("Revenue".into(), vec![1.0, 2.0])],
18271            ..ChartData::default()
18272        };
18273        let (xml, book) = authored_chart_parts(ChartKind::Bar, &original, "rIdWorkbook").unwrap();
18274        let chart_source = xml.clone();
18275        let book_source = book.clone();
18276        let invalid = ChartData {
18277            categories: original.categories.clone(),
18278            series: vec![("Bad".into(), vec![f64::NAN, 1.0])],
18279            ..ChartData::default()
18280        };
18281        assert!(replace_chart_data(&xml, &book, &invalid).is_err());
18282        assert_eq!(xml, chart_source);
18283        assert_eq!(book, book_source);
18284        let external = String::from_utf8(xml.clone())
18285            .unwrap()
18286            .replace("Sheet1!", "'[1]Sheet1'!");
18287        let changed = ChartData {
18288            categories: original.categories.clone(),
18289            series: vec![("New".into(), vec![8.0, 9.0])],
18290            ..ChartData::default()
18291        };
18292        let error = replace_chart_data(external.as_bytes(), &book, &changed).unwrap_err();
18293        assert!(error.to_string().contains("external workbook formula"));
18294        assert_eq!(book, book_source);
18295    }
18296    #[test]
18297    fn fx246_legend_position_reserves_measured_plot_space() {
18298        let mut outputs = Vec::new();
18299        for position in ["r", "b", "l"] {
18300            let xml = chart_with_plot(&bar_plot("")).replace(
18301                "</q:plotArea></q:chart>",
18302                &format!("</q:plotArea><q:legend><q:legendPos val=\"{position}\"/><q:overlay val=\"0\"/></q:legend></q:chart>"),
18303            );
18304            let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
18305            let mut fonts = FontManager::new_deterministic().unwrap();
18306            outputs.push(format!(
18307                "{:?}",
18308                render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap()
18309            ));
18310        }
18311        assert_ne!(
18312            outputs[0], outputs[1],
18313            "right and bottom legends must differ"
18314        );
18315        assert_ne!(outputs[0], outputs[2], "right and left legends must differ");
18316    }
18317
18318    #[test]
18319    fn fx246_legend_mutation_preserves_opaque_source_and_order() {
18320        let base = chart_with_plot(&bar_plot(""));
18321        let payload = "<x:legendPos xmlns:x='urn:foreign' val='unknown'/><!--keep exact--><x:opaque xmlns:x='urn:foreign' literal='a &amp; b'/>";
18322        let xml=base.replace("</q:plotArea></q:chart>",&format!("</q:plotArea><q:legend xmlns:c='urn:foreign' producer='keep'>{payload}<q:layout/><q:overlay val='false' untouched='yes'/><q:extLst/></q:legend></q:chart>"));
18323        let settings = super::chart_legend_settings(xml.as_bytes()).unwrap();
18324        assert_eq!(
18325            settings,
18326            super::LegendSettings {
18327                visible: true,
18328                ..Default::default()
18329            }
18330        );
18331        let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
18332        assert_eq!(parsed.chart.legend.as_ref().unwrap().settings, settings);
18333        assert_eq!(
18334            super::set_chart_legend(
18335                xml.as_bytes(),
18336                None,
18337                Some(super::LegendPosition::Right),
18338                Some(false)
18339            )
18340            .unwrap(),
18341            xml.as_bytes()
18342        );
18343        let changed = super::set_chart_legend(
18344            xml.as_bytes(),
18345            None,
18346            Some(super::LegendPosition::Bottom),
18347            Some(true),
18348        )
18349        .unwrap();
18350        let text = String::from_utf8(changed.clone()).unwrap();
18351        assert!(text.contains(payload));
18352        assert!(text.contains("untouched=\"yes\""));
18353        assert!(text.find("<c:legendPos").unwrap() < text.find("<q:layout").unwrap());
18354        assert!(text.find("<q:overlay").unwrap() < text.find("<q:extLst").unwrap());
18355        assert_eq!(
18356            super::chart_legend_settings(&changed).unwrap(),
18357            super::LegendSettings {
18358                visible: true,
18359                position: super::LegendPosition::Bottom,
18360                overlay: true
18361            }
18362        );
18363        assert!(
18364            super::chart_legend_settings(
18365                xml.replace(
18366                    "<q:layout/>",
18367                    "<q:legendPos val='l'/><q:legendPos val='r'/>"
18368                )
18369                .as_bytes()
18370            )
18371            .is_err()
18372        );
18373        assert!(
18374            super::chart_legend_settings(xml.replace("val='false'", "val='maybe'").as_bytes())
18375                .is_err()
18376        );
18377        let hidden = super::set_chart_legend(&changed, Some(false), None, None).unwrap();
18378        assert!(!super::chart_legend_settings(&hidden).unwrap().visible);
18379        assert_eq!(
18380            super::set_chart_legend(&hidden, None, Some(super::LegendPosition::Left), Some(true))
18381                .unwrap(),
18382            hidden
18383        );
18384        let shown = super::set_chart_legend(
18385            &hidden,
18386            Some(true),
18387            Some(super::LegendPosition::Top),
18388            Some(true),
18389        )
18390        .unwrap();
18391        assert_eq!(
18392            super::chart_legend_settings(&shown).unwrap(),
18393            super::LegendSettings {
18394                visible: true,
18395                position: super::LegendPosition::Top,
18396                overlay: true
18397            }
18398        );
18399    }
18400
18401    #[test]
18402    fn fx246_overlay_and_absent_legends_keep_the_correct_plot() {
18403        use super::{CT_Legend, LegendPosition as P, LegendSettings};
18404        let mut chart = labelled_bar_chart();
18405        let bounds = chart_bounds();
18406        let full = super::geometry_plot_bounds(bounds).unwrap();
18407        let plot = chart.chart.plot_area.plots().unwrap()[0].clone();
18408        let mut fonts = FontManager::new_deterministic().unwrap();
18409        for position in [P::Right, P::Left, P::Top, P::Bottom, P::TopRight] {
18410            chart.chart.legend = Some(CT_Legend {
18411                settings: LegendSettings {
18412                    visible: true,
18413                    position,
18414                    overlay: false,
18415                },
18416                ..Default::default()
18417            });
18418            let layout = super::layout_legend(&chart.chart, &plot, bounds, &mut fonts).unwrap();
18419            for (key, label) in layout.keys.iter().zip(&layout.labels) {
18420                let right = label.origin.x + label.advances.iter().sum::<f64>();
18421                assert!(key.x >= bounds.x && right <= bounds.x + bounds.width);
18422                let outside = key.x + key.width <= layout.plot.x
18423                    || key.x >= layout.plot.x + layout.plot.width
18424                    || key.y + key.height <= layout.plot.y
18425                    || key.y >= layout.plot.y + layout.plot.height;
18426                assert!(outside, "non-overlay {position:?}");
18427                assert!(label.origin.y >= bounds.y && label.origin.y <= bounds.y + bounds.height);
18428            }
18429            chart.chart.legend.as_mut().unwrap().settings.overlay = true;
18430            let overlay = super::layout_legend(&chart.chart, &plot, bounds, &mut fonts).unwrap();
18431            assert_eq!(overlay.plot, full);
18432            assert!(
18433                overlay.keys.iter().any(|key| key.x < full.x + full.width
18434                    && key.x + key.width > full.x
18435                    && key.y < full.y + full.height
18436                    && key.y + key.height > full.y),
18437                "negative control moved inside the plot must be rejected by the non-overlap observer"
18438            );
18439        }
18440        chart.chart.legend = None;
18441        let absent = super::layout_legend(&chart.chart, &plot, bounds, &mut fonts).unwrap();
18442        assert_eq!(absent.plot, full);
18443        assert!(absent.keys.is_empty());
18444    }
18445
18446    #[test]
18447    fn fx246_legend_labels_fit_bounded_chart_geometry() {
18448        let mut chart = labelled_bar_chart();
18449        if let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] {
18450            series[0].name.as_mut().unwrap().values[0] =
18451                "A legend considerably wider than fifty eight points".into();
18452        }
18453        let mut fonts = FontManager::new_deterministic().unwrap();
18454        assert!(
18455            render_chart(&chart.chart, chart_bounds(), &mut fonts).is_err(),
18456            "a chart too narrow for its label refuses explicitly"
18457        );
18458        let wide = Rect {
18459            width: 600.0,
18460            ..chart_bounds()
18461        };
18462        let group = render_chart(&chart.chart, wide, &mut fonts).unwrap();
18463        assert!(format!("{group:?}").contains("considerably wider"));
18464    }
18465    #[test]
18466    fn fx246_legend_scalar_defaults_and_local_shadows_are_qualified() {
18467        let base = chart_with_plot(&bar_plot(""));
18468        for (children, overlay) in [
18469            ("<q:legendPos/><q:overlay/>", true),
18470            (
18471                "<q:legendPos xmlns:q='urn:foreign' val='not owned'/><q:overlay val='0'/>",
18472                false,
18473            ),
18474            (
18475                "<c:legendPos xmlns:c='urn:foreign' val='not owned'/>",
18476                false,
18477            ),
18478        ] {
18479            let xml = base.replace(
18480                "</q:plotArea></q:chart>",
18481                &format!("</q:plotArea><q:legend>{children}</q:legend></q:chart>"),
18482            );
18483            let settings = super::chart_legend_settings(xml.as_bytes()).unwrap();
18484            assert_eq!(settings.position, super::LegendPosition::Right);
18485            assert_eq!(settings.overlay, overlay);
18486            assert_eq!(
18487                CT_ChartSpace::from_xml(xml.as_bytes())
18488                    .unwrap()
18489                    .chart
18490                    .legend
18491                    .unwrap()
18492                    .settings,
18493                settings
18494            );
18495        }
18496    }
18497    #[test]
18498    fn fx246_legend_entrypoints_require_one_well_formed_xml_document() {
18499        let source = chart_with_plot(&bar_plot(""));
18500        for invalid in [
18501            format!("{source}NOT_XML_DOCUMENT_CONTENT"),
18502            format!("NOT_XML_DOCUMENT_CONTENT{source}"),
18503            format!("{source}<![CDATA[tail]]>"),
18504            format!("{source}&amp;"),
18505            format!("{source}{source}"),
18506            format!("<!--prefix--><?xml version='1.0'?>{source}"),
18507            source.replace("</q:chartSpace>", "<u:opaque/></q:chartSpace>"),
18508            source.replace(
18509                "</q:chartSpace>",
18510                "<x:opaque xmlns:z='urn:producer' x:a='1' z:a='2'/></q:chartSpace>",
18511            ),
18512        ] {
18513            assert!(
18514                super::chart_legend_settings(invalid.as_bytes()).is_err(),
18515                "getter admitted {invalid}"
18516            );
18517            assert!(
18518                super::set_chart_legend(
18519                    invalid.as_bytes(),
18520                    Some(true),
18521                    Some(super::LegendPosition::Left),
18522                    None
18523                )
18524                .is_err(),
18525                "setter admitted {invalid}"
18526            );
18527        }
18528        let valid = format!(
18529            "<?xml version='1.0'?><!--prolog-->{source}\n<!--tail--><?producer preserved?>"
18530        );
18531        assert_eq!(
18532            super::set_chart_legend(valid.as_bytes(), None, None, None).unwrap(),
18533            valid.as_bytes()
18534        );
18535    }
18536
18537    #[test]
18538    fn fx246_nominal_legend_metrics_match_independent_native_components() {
18539        let chart = labelled_bar_chart();
18540        let plot = &chart.chart.plot_area.plots().unwrap()[0];
18541        let mut fonts = FontManager::new_deterministic().unwrap();
18542        let layout = super::layout_legend(&chart.chart, plot, chart_bounds(), &mut fonts).unwrap();
18543        // Frozen Word16.113.2 and PowerPoint16.113.4, explicit Aptos9pt
18544        // legend controls. Absolute position and cropped paint are separately
18545        // qualified from actual PDFs with the same one-point bound.
18546        assert!((layout.keys[0].width - 4.9433).abs() <= 1.0);
18547        assert!((layout.labels[0].origin.x - layout.keys[0].x - 7.0399).abs() <= 1.0);
18548        assert!((layout.keys[0].height - 4.9433).abs() <= 1.0);
18549        let mut multiple = four_series_bar_chart();
18550        multiple.chart.legend = Some(super::CT_Legend::default());
18551        let plot = &multiple.chart.plot_area.plots().unwrap()[0];
18552        let layout =
18553            super::layout_legend(&multiple.chart, plot, chart_bounds(), &mut fonts).unwrap();
18554        for pair in layout.keys.windows(2) {
18555            assert!((pair[1].y - pair[0].y - 16.875).abs() <= 1.0);
18556        }
18557    }
18558}