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, RgbColor, ThemeColorSlot,
13    apply_color_transforms, resolve_color,
14};
15use oxml_drawing::fill::Fill;
16use oxml_drawing::order::OrderedRawChildren;
17use oxml_drawing::shape_props::{CT_ShapeProperties, ShapePropertiesError};
18use oxml_drawing::text::{CT_TextBody, CT_TextCharacterProperties, TextError};
19use oxml_drawing::theme::CT_OfficeStyleSheet;
20use oxml_layout::{
21    Color, FillRule, FontManager, GlyphRun, GroupElement, Paint, Path, PathCommand, PathElement,
22    Point, PositionedElement, Rect, Stroke, Transform,
23};
24use oxml_sml::{Column, Workbook};
25use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
26use quick_xml::{Reader, Writer, XmlVersion};
27
28/// The transitional ChartML namespace used by OOXML packages.
29pub const C_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/chart";
30/// The fixed ChartML prefix used by writers in this crate.
31pub const C_PREFIX: &str = "c";
32/// The DrawingML namespace written on a chart-space root.
33pub const A_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
34/// The office-document relationships namespace written on a chart-space root.
35pub const R_NS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
36const MAX_CATEGORY_ANNOTATIONS: usize = 16_384;
37const PLOT_MARGIN_LEFT: f64 = 36.0;
38const PLOT_MARGIN_RIGHT: f64 = 12.0;
39const PLOT_MARGIN_TOP: f64 = 12.0;
40const PLOT_MARGIN_BOTTOM: f64 = 28.0;
41
42/// Errors produced while reading or writing the implemented ChartML core.
43#[derive(Debug)]
44pub enum ChartError {
45    Xml(OxmlError),
46    ShapeProperties(ShapePropertiesError),
47    Text(TextError),
48    UnexpectedElement(String),
49    MissingElement(String),
50    DuplicateElement(String),
51    InvalidAttribute {
52        element: String,
53        attribute: String,
54        value: String,
55    },
56    InvalidValue {
57        element: String,
58        value: String,
59    },
60}
61
62impl fmt::Display for ChartError {
63    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
64        match self {
65            Self::Xml(error) => error.fmt(formatter),
66            Self::ShapeProperties(error) => error.fmt(formatter),
67            Self::Text(error) => error.fmt(formatter),
68            Self::UnexpectedElement(element) => {
69                write!(formatter, "unexpected ChartML element: {element}")
70            }
71            Self::MissingElement(element) => {
72                write!(formatter, "ChartML requires {element}")
73            }
74            Self::DuplicateElement(element) => {
75                write!(formatter, "ChartML contains duplicate {element}")
76            }
77            Self::InvalidAttribute {
78                element,
79                attribute,
80                value,
81            } => write!(
82                formatter,
83                "ChartML {element} has invalid @{attribute}: {value}"
84            ),
85            Self::InvalidValue { element, value } => {
86                write!(formatter, "ChartML {element} has invalid value: {value}")
87            }
88        }
89    }
90}
91
92impl std::error::Error for ChartError {
93    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
94        match self {
95            Self::Xml(error) => Some(error),
96            Self::ShapeProperties(error) => Some(error),
97            Self::Text(error) => Some(error),
98            _ => None,
99        }
100    }
101}
102
103impl From<OxmlError> for ChartError {
104    fn from(error: OxmlError) -> Self {
105        Self::Xml(error)
106    }
107}
108
109impl From<ShapePropertiesError> for ChartError {
110    fn from(error: ShapePropertiesError) -> Self {
111        Self::ShapeProperties(error)
112    }
113}
114
115impl From<TextError> for ChartError {
116    fn from(error: TextError) -> Self {
117        Self::Text(error)
118    }
119}
120
121pub type Result<T> = std::result::Result<T, ChartError>;
122
123/// One supported two-dimensional chart family.
124#[derive(Clone, Copy, Debug, Eq, PartialEq)]
125pub enum ChartKind {
126    Bar,
127    Line,
128    Pie,
129    Doughnut,
130    Area,
131    Scatter,
132    Radar,
133}
134
135/// Data used to author one chart and its editable workbook.
136#[derive(Clone, Debug, PartialEq)]
137pub struct ChartData {
138    pub categories: Vec<String>,
139    pub series: Vec<(String, Vec<f64>)>,
140    pub number_format: Option<String>,
141}
142
143/// Authors one ChartML part and its editable workbook from one validated source.
144pub fn authored_chart_parts(
145    kind: ChartKind,
146    data: &ChartData,
147    workbook_relationship_id: &str,
148) -> Result<(Vec<u8>, Vec<u8>)> {
149    validate_chart_data(kind, data)?;
150    let category_column = if kind == ChartKind::Scatter {
151        Column::Number {
152            header: "Category".to_owned(),
153            values: data
154                .categories
155                .iter()
156                .map(|value| value.parse::<f64>().expect("validated scatter category"))
157                .collect(),
158            number_format: None,
159        }
160    } else {
161        Column::Text {
162            header: "Category".to_owned(),
163            values: data.categories.clone(),
164        }
165    };
166    let mut columns = Vec::with_capacity(data.series.len() + 1);
167    columns.push(category_column);
168    columns.extend(data.series.iter().map(|(name, values)| Column::Number {
169        header: name.clone(),
170        values: values.clone(),
171        number_format: data.number_format.clone(),
172    }));
173    let workbook = Workbook::new("Sheet1", columns).map_err(invalid_authoring_value)?;
174    let category_formula = workbook
175        .formula_range(0)
176        .expect("validated categories produce a formula range");
177    let number_format = data.number_format.as_deref().unwrap_or("General");
178    let mut chart_series = Vec::with_capacity(data.series.len());
179    for (index, (name, values)) in data.series.iter().enumerate() {
180        let values_formula = workbook
181            .formula_range(index + 1)
182            .expect("validated series produces a formula range");
183        let values = NumericData::new(values_formula, number_format.to_owned(), values.clone())
184            .map_err(invalid_authoring_value)?;
185        let mut series = Series::new(index as u32, index as u32, values);
186        series.name = Some(
187            StringRef::new(
188                format!("Sheet1!${}$1", spreadsheet_column_name(index + 1)),
189                vec![name.clone()],
190            )
191            .map_err(invalid_authoring_value)?,
192        );
193        series.categories = Some(if kind == ChartKind::Scatter {
194            AxisData::Numeric(
195                NumericData::new(
196                    category_formula.clone(),
197                    "General".to_owned(),
198                    data.categories
199                        .iter()
200                        .map(|value| value.parse::<f64>().expect("validated scatter category"))
201                        .collect(),
202                )
203                .map_err(invalid_authoring_value)?,
204            )
205        } else {
206            AxisData::String(
207                StringRef::new(category_formula.clone(), data.categories.clone())
208                    .map_err(invalid_authoring_value)?,
209            )
210        });
211        chart_series.push(series);
212    }
213
214    let category_axis_id = AxisId::new(48_650_112).map_err(invalid_authoring_value)?;
215    let value_axis_id = AxisId::new(48_672_768).map_err(invalid_authoring_value)?;
216    let axis_ids = [category_axis_id, value_axis_id];
217    let (plot, axes) = match kind {
218        ChartKind::Bar => (
219            Plot::bar(
220                BarDirection::Column,
221                BarGrouping::Clustered,
222                chart_series,
223                axis_ids,
224            ),
225            category_value_axes(category_axis_id, value_axis_id),
226        ),
227        ChartKind::Line => (
228            Plot::line(Grouping::Standard, chart_series, axis_ids),
229            category_value_axes(category_axis_id, value_axis_id),
230        ),
231        ChartKind::Pie => (Plot::pie(chart_series), Vec::new()),
232        ChartKind::Doughnut => (Plot::doughnut(chart_series), Vec::new()),
233        ChartKind::Area => (
234            Plot::area(Grouping::Standard, chart_series, axis_ids),
235            category_value_axes(category_axis_id, value_axis_id),
236        ),
237        ChartKind::Scatter => (
238            Plot::scatter(ScatterStyle::LineMarker, chart_series, axis_ids),
239            vec![
240                Axis::new(
241                    AxisKind::Value,
242                    category_axis_id,
243                    AxisPosition::Bottom,
244                    value_axis_id,
245                ),
246                Axis::new(
247                    AxisKind::Value,
248                    value_axis_id,
249                    AxisPosition::Left,
250                    category_axis_id,
251                ),
252            ],
253        ),
254        ChartKind::Radar => (
255            Plot::radar(RadarStyle::Standard, chart_series, axis_ids),
256            category_value_axes(category_axis_id, value_axis_id),
257        ),
258    };
259    let plot = plot.map_err(invalid_authoring_value)?;
260    let plot_area = CT_PlotArea::new(vec![plot], axes).map_err(invalid_authoring_value)?;
261    let chart_shell = format!(
262        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>"#,
263    );
264    let mut chart_space = CT_ChartSpace::from_xml(chart_shell.as_bytes())?;
265    chart_space.chart.auto_title_deleted = true;
266    chart_space.chart.plot_area = plot_area;
267    chart_space.chart.legend = (data.series.len() > 1).then(CT_Legend::default);
268    let chart_xml = chart_space.to_xml()?;
269    let workbook_bytes = workbook.to_xlsx_bytes().map_err(invalid_authoring_value)?;
270    Ok((chart_xml, workbook_bytes))
271}
272
273fn validate_chart_data(kind: ChartKind, data: &ChartData) -> Result<()> {
274    if data.categories.is_empty() {
275        return Err(invalid_authoring_value("at least one category is required"));
276    }
277    if data.series.is_empty() {
278        return Err(invalid_authoring_value("at least one series is required"));
279    }
280    if matches!(kind, ChartKind::Pie | ChartKind::Doughnut) && data.series.len() != 1 {
281        return Err(invalid_authoring_value(
282            "pie and doughnut charts require exactly one series",
283        ));
284    }
285    for (name, values) in &data.series {
286        if values.len() != data.categories.len() {
287            return Err(invalid_authoring_value(format!(
288                "series {name:?} has {} values for {} categories",
289                values.len(),
290                data.categories.len()
291            )));
292        }
293        if values.iter().any(|value| !value.is_finite()) {
294            return Err(invalid_authoring_value(format!(
295                "series {name:?} contains a nonfinite value"
296            )));
297        }
298    }
299    if kind == ChartKind::Scatter {
300        for category in &data.categories {
301            if category
302                .parse::<f64>()
303                .ok()
304                .is_none_or(|value| !value.is_finite())
305            {
306                return Err(invalid_authoring_value(format!(
307                    "scatter category {category:?} is not a finite number"
308                )));
309            }
310        }
311    }
312    if let Some(format) = &data.number_format {
313        NumericData::new("Sheet1!$A$2".to_owned(), format.clone(), vec![0.0])?;
314    }
315    Ok(())
316}
317
318fn invalid_authoring_value(error: impl fmt::Display) -> ChartError {
319    ChartError::InvalidValue {
320        element: "chart authoring".to_owned(),
321        value: error.to_string(),
322    }
323}
324
325fn category_value_axes(category: AxisId, value: AxisId) -> Vec<Axis> {
326    vec![
327        Axis::new(AxisKind::Category, category, AxisPosition::Bottom, value),
328        Axis::new(AxisKind::Value, value, AxisPosition::Left, category),
329    ]
330}
331
332fn spreadsheet_column_name(mut index: usize) -> String {
333    let mut name = Vec::new();
334    index += 1;
335    while index != 0 {
336        let remainder = (index - 1) % 26;
337        name.push((b'A' + remainder as u8) as char);
338        index = (index - 1) / 26;
339    }
340    name.iter().rev().collect()
341}
342
343/// Backend-neutral plot geometry and the plot rectangle reserved inside a chart.
344#[derive(Clone, Debug)]
345pub struct ChartGeometry {
346    pub plot_bounds: Rect,
347    pub elements: Vec<PositionedElement>,
348}
349
350#[derive(Clone, Debug)]
351struct AxisScale {
352    domain: Domain,
353    ticks: Vec<f64>,
354}
355
356#[derive(Clone, Copy, Debug)]
357struct AxisMapping {
358    domain: Domain,
359    orientation: Orientation,
360}
361
362#[derive(Clone, Copy, Debug, Default)]
363struct GeometryOptions {
364    category_orientation: Orientation,
365    value: Option<AxisMapping>,
366    scatter_x: Option<AxisMapping>,
367    scatter_y: Option<AxisMapping>,
368}
369
370#[derive(Default)]
371struct ChartAnnotations {
372    gridlines: Vec<PositionedElement>,
373    axis_lines: Vec<PositionedElement>,
374    ticks: Vec<PositionedElement>,
375    labels: Vec<PositionedElement>,
376}
377
378/// Converts one supported typed chart into a labelled backend-neutral group.
379pub fn render_chart(
380    chart: &CT_Chart,
381    bounds: Rect,
382    theme: &CT_OfficeStyleSheet,
383    color_map: &ColorMap,
384    fonts: &mut FontManager,
385) -> Result<GroupElement> {
386    let plot_bounds = geometry_plot_bounds(bounds)?;
387    let plots = chart.plot_area.plots()?;
388    let plot = plots
389        .first()
390        .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
391    plot.validate()?;
392    let series_colours = resolve_series_colours(plot, theme, color_map)?;
393    let axes = chart.plot_area.axes()?;
394    let options = geometry_options(plot, &axes, chart.disp_blanks_as)?;
395    let plot_children = render_plot_geometry(
396        plot,
397        plot_bounds,
398        chart.disp_blanks_as,
399        options,
400        &series_colours,
401    )?;
402    if plot_children.is_empty() && !plot_can_render_without_marks(plot) {
403        return Err(ChartError::InvalidValue {
404            element: "c:plotArea".to_owned(),
405            value: "plot has no renderable cached data".to_owned(),
406        });
407    }
408
409    let mut chart_annotations =
410        render_axes_and_labels(plot, &axes, plot_bounds, chart.disp_blanks_as, fonts)?;
411    let mut annotations = Vec::new();
412    render_data_labels(
413        plot,
414        plot_bounds,
415        chart.disp_blanks_as,
416        options,
417        fonts,
418        &mut chart_annotations.labels,
419    )?;
420    render_legend(
421        chart,
422        plot,
423        plot_bounds,
424        fonts,
425        &series_colours,
426        &mut annotations,
427        &mut chart_annotations.labels,
428    )?;
429
430    validate_geometry_coordinates(&chart_annotations.gridlines)?;
431    validate_geometry_coordinates(&plot_children)?;
432    validate_geometry_coordinates(&chart_annotations.axis_lines)?;
433    validate_geometry_coordinates(&chart_annotations.ticks)?;
434    validate_geometry_coordinates(&annotations)?;
435    let mut children = chart_annotations.gridlines;
436    children.push(PositionedElement::Group(GroupElement {
437        transform: Transform::IDENTITY,
438        clip: Some(Path::rect(plot_bounds)),
439        opacity: 1.0,
440        effects: Vec::new(),
441        children: plot_children,
442    }));
443    children.extend(chart_annotations.axis_lines);
444    children.extend(chart_annotations.ticks);
445    children.extend(annotations);
446    children.extend(chart_annotations.labels);
447    Ok(GroupElement {
448        transform: Transform::IDENTITY,
449        clip: None,
450        opacity: 1.0,
451        effects: Vec::new(),
452        children,
453    })
454}
455
456/// Shapes the stable label used inside a visible unsupported-chart fallback.
457pub fn render_chart_placeholder(bounds: Rect, fonts: &mut FontManager) -> Result<GroupElement> {
458    if !bounds.width.is_finite()
459        || !bounds.height.is_finite()
460        || bounds.width <= 0.0
461        || bounds.height <= 0.0
462    {
463        return Err(ChartError::InvalidValue {
464            element: "chart fallback bounds".to_owned(),
465            value: "width and height must be finite and positive".to_owned(),
466        });
467    }
468    let label = shape_label(
469        fonts,
470        "Unsupported chart",
471        Point {
472            x: 4.0,
473            y: (bounds.height / 2.0).max(9.0),
474        },
475    )?;
476    Ok(GroupElement {
477        transform: Transform::IDENTITY,
478        clip: Some(Path::rect(Rect {
479            x: 0.0,
480            y: 0.0,
481            width: bounds.width,
482            height: bounds.height,
483        })),
484        opacity: 1.0,
485        effects: Vec::new(),
486        children: vec![PositionedElement::Text(label)],
487    })
488}
489
490/// Converts one supported typed chart into chart-local backend-neutral paths.
491pub fn render_geometry(
492    chart: &CT_Chart,
493    bounds: Rect,
494    theme: &CT_OfficeStyleSheet,
495    color_map: &ColorMap,
496) -> Result<ChartGeometry> {
497    let plot_bounds = geometry_plot_bounds(bounds)?;
498    let plots = chart.plot_area.plots()?;
499    let plot = plots
500        .first()
501        .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
502    plot.validate()?;
503    let series_colours = resolve_series_colours(plot, theme, color_map)?;
504    let children = render_plot_geometry(
505        plot,
506        plot_bounds,
507        chart.disp_blanks_as,
508        geometry_options(plot, &[], chart.disp_blanks_as)?,
509        &series_colours,
510    )?;
511    if children.is_empty() && !plot_can_render_without_marks(plot) {
512        return Err(ChartError::InvalidValue {
513            element: "c:plotArea".to_owned(),
514            value: "plot has no renderable cached data".to_owned(),
515        });
516    }
517    validate_geometry_coordinates(&children)?;
518    Ok(ChartGeometry {
519        plot_bounds,
520        elements: vec![PositionedElement::Group(GroupElement {
521            transform: Transform::IDENTITY,
522            clip: None,
523            opacity: 1.0,
524            effects: Vec::new(),
525            children,
526        })],
527    })
528}
529
530fn plot_can_render_without_marks(plot: &Plot) -> bool {
531    matches!(
532        plot,
533        Plot::Radar { series, .. }
534            if series
535                .iter()
536                .any(|series| !series.values.values.is_empty())
537    )
538}
539
540fn geometry_plot_bounds(bounds: Rect) -> Result<Rect> {
541    if ![bounds.x, bounds.y, bounds.width, bounds.height]
542        .into_iter()
543        .all(f64::is_finite)
544        || bounds.width <= 0.0
545        || bounds.height <= 0.0
546        || !(bounds.x + bounds.width).is_finite()
547        || !(bounds.y + bounds.height).is_finite()
548    {
549        return Err(ChartError::InvalidValue {
550            element: "chart geometry bounds".to_owned(),
551            value: format!(
552                "x={}, y={}, width={}, height={}",
553                bounds.x, bounds.y, bounds.width, bounds.height
554            ),
555        });
556    }
557    let plot_bounds = Rect {
558        x: bounds.x + PLOT_MARGIN_LEFT,
559        y: bounds.y + PLOT_MARGIN_TOP,
560        width: bounds.width - PLOT_MARGIN_LEFT - PLOT_MARGIN_RIGHT,
561        height: bounds.height - PLOT_MARGIN_TOP - PLOT_MARGIN_BOTTOM,
562    };
563    if ![
564        plot_bounds.x,
565        plot_bounds.y,
566        plot_bounds.width,
567        plot_bounds.height,
568        plot_bounds.x + plot_bounds.width,
569        plot_bounds.y + plot_bounds.height,
570    ]
571    .into_iter()
572    .all(f64::is_finite)
573        || plot_bounds.width <= 0.0
574        || plot_bounds.height <= 0.0
575    {
576        return Err(ChartError::InvalidValue {
577            element: "chart geometry bounds".to_owned(),
578            value: "chart is too small for the fixed plot margins".to_owned(),
579        });
580    }
581    Ok(plot_bounds)
582}
583
584fn geometry_options(plot: &Plot, axes: &[Axis], blanks: DispBlanksAs) -> Result<GeometryOptions> {
585    let mut options = GeometryOptions::default();
586    if let Some(category_axis) = axes.iter().find(|axis| axis.kind != AxisKind::Value) {
587        options.category_orientation = category_axis.scaling.orientation;
588    }
589
590    if let Plot::Scatter { axis_ids, .. } = plot {
591        let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
592        if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[0]) {
593            options.scatter_x = Some(axis_mapping(axis, x_domain)?);
594        }
595        if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[1]) {
596            options.scatter_y = Some(axis_mapping(axis, y_domain)?);
597        }
598    } else if let Some(domain) = plot_value_domain(plot, blanks)? {
599        options.value = Some(
600            axes.iter()
601                .find(|axis| axis.kind == AxisKind::Value)
602                .map(|axis| axis_mapping(axis, domain))
603                .transpose()?
604                .unwrap_or(AxisMapping {
605                    domain,
606                    orientation: Orientation::MinMax,
607                }),
608        );
609    }
610    Ok(options)
611}
612
613fn axis_mapping(axis: &Axis, extent: Domain) -> Result<AxisMapping> {
614    if axis.scaling.log_base.is_some() {
615        return Err(ChartError::InvalidValue {
616            element: "c:scaling/c:logBase".to_owned(),
617            value: "native chart rendering currently supports linear axes".to_owned(),
618        });
619    }
620    let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
621    Ok(AxisMapping {
622        domain: scale.domain,
623        orientation: axis.scaling.orientation,
624    })
625}
626
627fn plot_value_domain(plot: &Plot, blanks: DispBlanksAs) -> Result<Option<Domain>> {
628    let domain = match plot {
629        Plot::Bar {
630            grouping, series, ..
631        } => {
632            let logical = logical_series_values(series)?;
633            let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
634            let layers =
635                stacked_series_bounds(&logical, stacked, *grouping == BarGrouping::PercentStacked)?;
636            domain_from_layers(&layers)?
637        }
638        Plot::Line {
639            grouping, series, ..
640        } => {
641            let logical = logical_series_values(series)?;
642            if *grouping == Grouping::Standard {
643                let values = if blanks == DispBlanksAs::Zero {
644                    zero_filled_series(&logical, series_category_count(series)?)
645                        .into_iter()
646                        .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
647                        .collect::<Vec<_>>()
648                } else {
649                    logical
650                        .into_iter()
651                        .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
652                        .collect::<Vec<_>>()
653                };
654                data_domain(&values, false)?
655            } else {
656                let layers =
657                    stacked_series_bounds(&logical, true, *grouping == Grouping::PercentStacked)?;
658                domain_from_layers(&layers)?
659            }
660        }
661        Plot::Area {
662            grouping, series, ..
663        } => {
664            let logical = logical_series_values(series)?;
665            let layers = stacked_series_bounds(
666                &logical,
667                *grouping != Grouping::Standard,
668                *grouping == Grouping::PercentStacked,
669            )?;
670            domain_from_layers(&layers)?
671        }
672        Plot::Radar { series, .. } => {
673            let values = series
674                .iter()
675                .flat_map(|series| series.values.values.iter().copied())
676                .collect::<Vec<_>>();
677            data_domain(&values, true)?
678        }
679        Plot::Pie { .. } | Plot::Doughnut { .. } | Plot::Scatter { .. } => return Ok(None),
680    };
681    Ok(Some(domain))
682}
683
684fn scatter_domains(plot: &Plot, blanks: DispBlanksAs) -> Result<(Domain, Domain)> {
685    let Plot::Scatter { series, .. } = plot else {
686        return Err(ChartError::UnexpectedElement("non-scatter plot".to_owned()));
687    };
688    let mut x_values = Vec::new();
689    let mut y_values = Vec::new();
690    for series in series {
691        for (_, x, y) in scatter_render_points(series, blanks)? {
692            x_values.push(x);
693            y_values.push(y);
694        }
695    }
696    Ok((
697        data_domain(&x_values, false)?,
698        data_domain(&y_values, false)?,
699    ))
700}
701
702fn render_axes_and_labels(
703    plot: &Plot,
704    axes: &[Axis],
705    bounds: Rect,
706    blanks: DispBlanksAs,
707    fonts: &mut FontManager,
708) -> Result<ChartAnnotations> {
709    if matches!(plot, Plot::Radar { .. }) {
710        return render_radar_axes_and_labels(plot, axes, bounds, blanks, fonts);
711    }
712    let mut annotations = ChartAnnotations::default();
713    let series = plot_series(plot);
714    let category_count = if series.is_empty() {
715        1
716    } else {
717        series_category_count(series)?
718    };
719    for axis in axes {
720        if axis.deleted {
721            continue;
722        }
723        axis_line(&mut annotations.axis_lines, bounds, axis.position);
724        if axis.kind == AxisKind::Value {
725            let extent = axis_extent(plot, axis, blanks)?;
726            let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
727            for tick in &scale.ticks {
728                let coordinate = axis_tick_coordinate(
729                    *tick,
730                    scale.domain,
731                    bounds,
732                    axis.position,
733                    axis.scaling.orientation,
734                )?;
735                if axis.major_gridlines.is_some() {
736                    major_gridline(
737                        &mut annotations.gridlines,
738                        bounds,
739                        axis.position,
740                        coordinate,
741                    );
742                }
743                axis_tick(
744                    &mut annotations.ticks,
745                    bounds,
746                    axis.position,
747                    coordinate,
748                    axis.major_tick_mark,
749                );
750                if axis.tick_label_position != TickLabelPosition::None {
751                    let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
752                    let origin = tick_label_origin(
753                        bounds,
754                        axis.position,
755                        axis.tick_label_position,
756                        coordinate,
757                    );
758                    annotations
759                        .labels
760                        .push(PositionedElement::Text(shape_label_with_properties(
761                            fonts,
762                            &text,
763                            origin,
764                            axis.tx_pr.as_ref(),
765                        )?));
766                }
767            }
768        } else {
769            let count = category_count;
770            if count > MAX_CATEGORY_ANNOTATIONS {
771                return Err(ChartError::InvalidValue {
772                    element: "c:catAx annotations".to_owned(),
773                    value: format!(
774                        "logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"
775                    ),
776                });
777            }
778            let category_labels: BTreeMap<_, _> =
779                if axis.tick_label_position != TickLabelPosition::None {
780                    series
781                        .first()
782                        .map(|series| series_categories(series, axis.number_format.as_ref()))
783                        .transpose()?
784                        .unwrap_or_default()
785                        .into_iter()
786                        .collect()
787                } else {
788                    BTreeMap::new()
789                };
790            for logical_index in 0..count {
791                let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
792                let fraction = (index as f64 + 0.5) / count as f64;
793                let coordinate =
794                    if matches!(axis.position, AxisPosition::Bottom | AxisPosition::Top) {
795                        bounds.x + fraction * bounds.width
796                    } else {
797                        bounds.y + fraction * bounds.height
798                    };
799                if axis.major_gridlines.is_some() {
800                    major_gridline(
801                        &mut annotations.gridlines,
802                        bounds,
803                        axis.position,
804                        coordinate,
805                    );
806                }
807                axis_tick(
808                    &mut annotations.ticks,
809                    bounds,
810                    axis.position,
811                    coordinate,
812                    axis.major_tick_mark,
813                );
814                if axis.tick_label_position != TickLabelPosition::None
815                    && let Some(text) = category_labels.get(&logical_index)
816                {
817                    let origin = tick_label_origin(
818                        bounds,
819                        axis.position,
820                        axis.tick_label_position,
821                        coordinate,
822                    );
823                    annotations
824                        .labels
825                        .push(PositionedElement::Text(shape_label_with_properties(
826                            fonts,
827                            text,
828                            origin,
829                            axis.tx_pr.as_ref(),
830                        )?));
831                }
832            }
833        }
834    }
835    Ok(annotations)
836}
837
838fn render_radar_axes_and_labels(
839    plot: &Plot,
840    axes: &[Axis],
841    bounds: Rect,
842    blanks: DispBlanksAs,
843    fonts: &mut FontManager,
844) -> Result<ChartAnnotations> {
845    let mut annotations = ChartAnnotations::default();
846    let series = plot_series(plot);
847    let count = series_category_count(series)?;
848    if count > MAX_CATEGORY_ANNOTATIONS {
849        return Err(ChartError::InvalidValue {
850            element: "c:radarChart annotations".to_owned(),
851            value: format!("logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"),
852        });
853    }
854    let center = Point {
855        x: bounds.x + bounds.width / 2.0,
856        y: bounds.y + bounds.height / 2.0,
857    };
858    let radius = bounds.width.min(bounds.height) / 2.0;
859    for axis in axes {
860        if axis.deleted {
861            continue;
862        }
863        if axis.kind == AxisKind::Value {
864            let scale = nice_number_scale(
865                axis_extent(plot, axis, blanks)?,
866                axis.scaling.minimum,
867                axis.scaling.maximum,
868                6,
869            )?;
870            annotations.axis_lines.push(stroked_segment(
871                center,
872                radial_point(center, radius, 0, count),
873                Color::BLACK,
874                1.0,
875            ));
876            for tick in &scale.ticks {
877                let distance =
878                    normalized_value_oriented(*tick, scale.domain, axis.scaling.orientation)?
879                        * radius;
880                if axis.major_gridlines.is_some() && distance > 0.0 {
881                    let points: Vec<_> = (0..count)
882                        .map(|index| radial_point(center, distance, index, count))
883                        .collect();
884                    annotations
885                        .gridlines
886                        .push(PositionedElement::Path(PathElement {
887                            path: polyline_path(&points, true),
888                            fill: None,
889                            stroke: Some(Stroke::new(
890                                Paint::Solid(Color {
891                                    r: 0.82,
892                                    g: 0.82,
893                                    b: 0.82,
894                                    a: 1.0,
895                                }),
896                                0.5,
897                            )),
898                        }));
899                }
900                radar_value_tick(
901                    &mut annotations.ticks,
902                    center,
903                    distance,
904                    axis.major_tick_mark,
905                );
906                let origin = match axis.tick_label_position {
907                    TickLabelPosition::High => Some(Point {
908                        x: center.x + radius + 4.0,
909                        y: center.y - distance + 3.0,
910                    }),
911                    TickLabelPosition::Low => Some(Point {
912                        x: center.x - radius - 30.0,
913                        y: center.y - distance + 3.0,
914                    }),
915                    TickLabelPosition::NextTo => Some(Point {
916                        x: center.x + 4.0,
917                        y: center.y - distance + 3.0,
918                    }),
919                    TickLabelPosition::None => None,
920                };
921                if let Some(origin) = origin {
922                    let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
923                    annotations
924                        .labels
925                        .push(PositionedElement::Text(shape_label_with_properties(
926                            fonts,
927                            &text,
928                            origin,
929                            axis.tx_pr.as_ref(),
930                        )?));
931                }
932            }
933        } else {
934            let category_labels: BTreeMap<_, _> =
935                if axis.tick_label_position != TickLabelPosition::None {
936                    series
937                        .first()
938                        .map(|series| series_categories(series, axis.number_format.as_ref()))
939                        .transpose()?
940                        .unwrap_or_default()
941                        .into_iter()
942                        .collect()
943                } else {
944                    BTreeMap::new()
945                };
946            for logical_index in 0..count {
947                let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
948                let end = radial_point(center, radius, index, count);
949                annotations
950                    .axis_lines
951                    .push(stroked_segment(center, end, Color::BLACK, 1.0));
952                radar_category_tick(
953                    &mut annotations.ticks,
954                    center,
955                    radius,
956                    index,
957                    count,
958                    axis.major_tick_mark,
959                );
960                if let Some(text) = category_labels.get(&logical_index) {
961                    let Some(origin) = radar_category_label_origin(
962                        bounds,
963                        center,
964                        radius,
965                        index,
966                        count,
967                        axis.tick_label_position,
968                    ) else {
969                        continue;
970                    };
971                    annotations
972                        .labels
973                        .push(PositionedElement::Text(shape_label_with_properties(
974                            fonts,
975                            text,
976                            origin,
977                            axis.tx_pr.as_ref(),
978                        )?));
979                }
980            }
981        }
982    }
983    Ok(annotations)
984}
985
986fn radar_category_label_origin(
987    bounds: Rect,
988    center: Point,
989    radius: f64,
990    index: usize,
991    count: usize,
992    position: TickLabelPosition,
993) -> Option<Point> {
994    let label_radius = match position {
995        TickLabelPosition::High => radius + 18.0,
996        TickLabelPosition::Low => (radius - 10.0).max(0.0),
997        TickLabelPosition::NextTo => radius + 10.0,
998        TickLabelPosition::None => return None,
999    };
1000    let label = radial_point(center, label_radius, index, count);
1001    let mut origin = Point {
1002        x: label.x - 4.0,
1003        y: label.y + 3.0,
1004    };
1005    if position == TickLabelPosition::High {
1006        origin.x = origin.x.clamp(
1007            bounds.x - PLOT_MARGIN_LEFT,
1008            bounds.x + bounds.width + PLOT_MARGIN_RIGHT,
1009        );
1010        origin.y = origin.y.clamp(
1011            bounds.y - PLOT_MARGIN_TOP,
1012            bounds.y + bounds.height + PLOT_MARGIN_BOTTOM,
1013        );
1014    }
1015    Some(origin)
1016}
1017
1018fn radar_value_tick(
1019    elements: &mut Vec<PositionedElement>,
1020    center: Point,
1021    distance: f64,
1022    mark: TickMark,
1023) {
1024    let (left, right) = match mark {
1025        TickMark::None => return,
1026        TickMark::Inside => (4.0, 0.0),
1027        TickMark::Outside => (0.0, 4.0),
1028        TickMark::Cross => (2.0, 2.0),
1029    };
1030    let y = center.y - distance;
1031    elements.push(stroked_segment(
1032        Point {
1033            x: center.x - left,
1034            y,
1035        },
1036        Point {
1037            x: center.x + right,
1038            y,
1039        },
1040        Color::BLACK,
1041        1.0,
1042    ));
1043}
1044
1045fn radar_category_tick(
1046    elements: &mut Vec<PositionedElement>,
1047    center: Point,
1048    radius: f64,
1049    index: usize,
1050    count: usize,
1051    mark: TickMark,
1052) {
1053    let (inside, outside) = match mark {
1054        TickMark::None => return,
1055        TickMark::Inside => (4.0, 0.0),
1056        TickMark::Outside => (0.0, 4.0),
1057        TickMark::Cross => (2.0, 2.0),
1058    };
1059    elements.push(stroked_segment(
1060        radial_point(center, radius - inside, index, count),
1061        radial_point(center, radius + outside, index, count),
1062        Color::BLACK,
1063        1.0,
1064    ));
1065}
1066
1067fn axis_extent(plot: &Plot, axis: &Axis, blanks: DispBlanksAs) -> Result<Domain> {
1068    if let Plot::Scatter { axis_ids, .. } = plot {
1069        let (x, y) = scatter_domains(plot, blanks)?;
1070        return Ok(if axis.id == axis_ids[0] { x } else { y });
1071    }
1072    plot_value_domain(plot, blanks)?.ok_or_else(|| ChartError::InvalidValue {
1073        element: "chart axis".to_owned(),
1074        value: "axis-free plot supplied an axis".to_owned(),
1075    })
1076}
1077
1078fn axis_tick_coordinate(
1079    value: f64,
1080    domain: Domain,
1081    bounds: Rect,
1082    position: AxisPosition,
1083    orientation: Orientation,
1084) -> Result<f64> {
1085    if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
1086        map_x_oriented(value, domain, bounds, orientation)
1087    } else {
1088        map_y_oriented(value, domain, bounds, orientation)
1089    }
1090}
1091
1092fn axis_line(elements: &mut Vec<PositionedElement>, bounds: Rect, position: AxisPosition) {
1093    let (start, end) = match position {
1094        AxisPosition::Bottom => (
1095            Point {
1096                x: bounds.x,
1097                y: bounds.y + bounds.height,
1098            },
1099            Point {
1100                x: bounds.x + bounds.width,
1101                y: bounds.y + bounds.height,
1102            },
1103        ),
1104        AxisPosition::Top => (
1105            Point {
1106                x: bounds.x,
1107                y: bounds.y,
1108            },
1109            Point {
1110                x: bounds.x + bounds.width,
1111                y: bounds.y,
1112            },
1113        ),
1114        AxisPosition::Left => (
1115            Point {
1116                x: bounds.x,
1117                y: bounds.y,
1118            },
1119            Point {
1120                x: bounds.x,
1121                y: bounds.y + bounds.height,
1122            },
1123        ),
1124        AxisPosition::Right => (
1125            Point {
1126                x: bounds.x + bounds.width,
1127                y: bounds.y,
1128            },
1129            Point {
1130                x: bounds.x + bounds.width,
1131                y: bounds.y + bounds.height,
1132            },
1133        ),
1134    };
1135    elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
1136}
1137
1138fn major_gridline(
1139    elements: &mut Vec<PositionedElement>,
1140    bounds: Rect,
1141    position: AxisPosition,
1142    coordinate: f64,
1143) {
1144    let (start, end) = if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
1145        (
1146            Point {
1147                x: coordinate,
1148                y: bounds.y,
1149            },
1150            Point {
1151                x: coordinate,
1152                y: bounds.y + bounds.height,
1153            },
1154        )
1155    } else {
1156        (
1157            Point {
1158                x: bounds.x,
1159                y: coordinate,
1160            },
1161            Point {
1162                x: bounds.x + bounds.width,
1163                y: coordinate,
1164            },
1165        )
1166    };
1167    elements.push(stroked_segment(
1168        start,
1169        end,
1170        Color {
1171            r: 0.82,
1172            g: 0.82,
1173            b: 0.82,
1174            a: 1.0,
1175        },
1176        0.5,
1177    ));
1178}
1179
1180fn axis_tick(
1181    elements: &mut Vec<PositionedElement>,
1182    bounds: Rect,
1183    position: AxisPosition,
1184    coordinate: f64,
1185    mark: TickMark,
1186) {
1187    let (inside, outside) = match mark {
1188        TickMark::None => return,
1189        TickMark::Inside => (4.0, 0.0),
1190        TickMark::Outside => (0.0, 4.0),
1191        TickMark::Cross => (2.0, 2.0),
1192    };
1193    let (start, end) = match position {
1194        AxisPosition::Bottom => (
1195            Point {
1196                x: coordinate,
1197                y: bounds.y + bounds.height - inside,
1198            },
1199            Point {
1200                x: coordinate,
1201                y: bounds.y + bounds.height + outside,
1202            },
1203        ),
1204        AxisPosition::Top => (
1205            Point {
1206                x: coordinate,
1207                y: bounds.y + inside,
1208            },
1209            Point {
1210                x: coordinate,
1211                y: bounds.y - outside,
1212            },
1213        ),
1214        AxisPosition::Left => (
1215            Point {
1216                x: bounds.x + inside,
1217                y: coordinate,
1218            },
1219            Point {
1220                x: bounds.x - outside,
1221                y: coordinate,
1222            },
1223        ),
1224        AxisPosition::Right => (
1225            Point {
1226                x: bounds.x + bounds.width - inside,
1227                y: coordinate,
1228            },
1229            Point {
1230                x: bounds.x + bounds.width + outside,
1231                y: coordinate,
1232            },
1233        ),
1234    };
1235    elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
1236}
1237
1238fn tick_label_origin(
1239    bounds: Rect,
1240    axis_position: AxisPosition,
1241    label_position: TickLabelPosition,
1242    coordinate: f64,
1243) -> Point {
1244    let side = match label_position {
1245        TickLabelPosition::NextTo => axis_position,
1246        TickLabelPosition::High => {
1247            if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
1248                AxisPosition::Top
1249            } else {
1250                AxisPosition::Right
1251            }
1252        }
1253        TickLabelPosition::Low => {
1254            if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
1255                AxisPosition::Bottom
1256            } else {
1257                AxisPosition::Left
1258            }
1259        }
1260        TickLabelPosition::None => axis_position,
1261    };
1262    match side {
1263        AxisPosition::Bottom => Point {
1264            x: coordinate - 4.0,
1265            y: bounds.y + bounds.height + 12.0,
1266        },
1267        AxisPosition::Top => Point {
1268            x: coordinate - 4.0,
1269            y: bounds.y - 4.0,
1270        },
1271        AxisPosition::Left => Point {
1272            x: bounds.x - 30.0,
1273            y: coordinate + 3.0,
1274        },
1275        AxisPosition::Right => Point {
1276            x: bounds.x + bounds.width + 4.0,
1277            y: coordinate + 3.0,
1278        },
1279    }
1280}
1281
1282fn format_axis_value(format: Option<&NumberFormat>, value: f64) -> Result<String> {
1283    if let Some(format) = format {
1284        format.format_value(value)
1285    } else {
1286        NumberFormat::new("General".to_owned(), false)?.format_value(value)
1287    }
1288}
1289
1290fn shape_label(fonts: &mut FontManager, text: &str, origin: Point) -> Result<GlyphRun> {
1291    shape_label_with_properties(fonts, text, origin, None)
1292}
1293
1294fn shape_label_with_properties(
1295    fonts: &mut FontManager,
1296    text: &str,
1297    origin: Point,
1298    properties: Option<&CT_TextBody>,
1299) -> Result<GlyphRun> {
1300    const FAMILY: &str = "Carlito";
1301    const SIZE: f64 = 9.0;
1302    let properties = properties.and_then(chart_text_properties);
1303    let family = properties
1304        .and_then(|properties| properties.latin.as_ref())
1305        .map(|font| font.typeface.as_str())
1306        .unwrap_or(FAMILY);
1307    let size = properties
1308        .and_then(|properties| properties.font_size)
1309        .map(|size| f64::from(size) / 100.0)
1310        .unwrap_or(SIZE);
1311    let bold = properties
1312        .and_then(|properties| properties.bold)
1313        .unwrap_or(false);
1314    let italic = properties
1315        .and_then(|properties| properties.italic)
1316        .unwrap_or(false);
1317    let font_id = fonts
1318        .resolve_font_for_text(Some(family), bold, italic, text)
1319        .map_err(|error| ChartError::InvalidValue {
1320            element: "chart label font".to_owned(),
1321            value: error.to_string(),
1322        })?;
1323    let shaped =
1324        fonts
1325            .shape_text(font_id, text, size)
1326            .map_err(|error| ChartError::InvalidValue {
1327                element: "chart label shaping".to_owned(),
1328                value: error.to_string(),
1329            })?;
1330    Ok(GlyphRun {
1331        origin,
1332        font_id,
1333        font_size: size,
1334        glyph_ids: shaped.glyph_ids,
1335        advances: shaped.advances,
1336        text: text.to_owned(),
1337        source: None,
1338        color: Color::BLACK,
1339        bold,
1340        italic,
1341        field_kind: None,
1342        note: None,
1343    })
1344}
1345
1346fn chart_text_properties(body: &CT_TextBody) -> Option<&CT_TextCharacterProperties> {
1347    body.paragraphs()
1348        .first()
1349        .and_then(|paragraph| paragraph.properties.as_ref())
1350        .and_then(|properties| properties.default_run_properties.as_ref())
1351}
1352
1353fn stroked_segment(start: Point, end: Point, color: Color, width: f64) -> PositionedElement {
1354    PositionedElement::Path(PathElement {
1355        path: Path {
1356            commands: vec![PathCommand::MoveTo(start), PathCommand::LineTo(end)],
1357            fill_rule: FillRule::NonZero,
1358        },
1359        fill: None,
1360        stroke: Some(Stroke::new(Paint::Solid(color), width)),
1361    })
1362}
1363
1364fn plot_series(plot: &Plot) -> &[Series] {
1365    match plot {
1366        Plot::Bar { series, .. }
1367        | Plot::Line { series, .. }
1368        | Plot::Pie { series, .. }
1369        | Plot::Doughnut { series, .. }
1370        | Plot::Area { series, .. }
1371        | Plot::Scatter { series, .. }
1372        | Plot::Radar { series, .. } => series,
1373    }
1374}
1375
1376fn plot_data_labels(plot: &Plot) -> Option<&CT_DLbls> {
1377    match plot {
1378        Plot::Bar { data_labels, .. }
1379        | Plot::Line { data_labels, .. }
1380        | Plot::Pie { data_labels, .. }
1381        | Plot::Doughnut { data_labels, .. }
1382        | Plot::Area { data_labels, .. }
1383        | Plot::Scatter { data_labels, .. }
1384        | Plot::Radar { data_labels, .. } => data_labels.as_ref(),
1385    }
1386}
1387
1388fn series_categories(
1389    series: &Series,
1390    number_format: Option<&NumberFormat>,
1391) -> Result<Vec<(usize, String)>> {
1392    let Some(categories) = &series.categories else {
1393        return Ok(Vec::new());
1394    };
1395    match categories {
1396        AxisData::String(data) => {
1397            let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
1398            indexes
1399                .into_iter()
1400                .zip(&data.values)
1401                .map(|(index, value)| {
1402                    usize::try_from(index)
1403                        .map(|index| (index, value.clone()))
1404                        .map_err(|_| ChartError::InvalidValue {
1405                            element: "c:cat/c:pt/@idx".to_owned(),
1406                            value: index.to_string(),
1407                        })
1408                })
1409                .collect()
1410        }
1411        AxisData::Numeric(data) => {
1412            let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
1413            let cache_format;
1414            let format = if let Some(format) = number_format {
1415                format
1416            } else {
1417                cache_format = NumberFormat::new(data.format_code.clone(), false)?;
1418                &cache_format
1419            };
1420            indexes
1421                .into_iter()
1422                .zip(&data.values)
1423                .map(|(index, value)| {
1424                    let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
1425                        element: "c:cat/c:pt/@idx".to_owned(),
1426                        value: index.to_string(),
1427                    })?;
1428                    let text = format.format_value(*value)?;
1429                    Ok((index, text))
1430                })
1431                .collect()
1432        }
1433    }
1434}
1435
1436fn series_name(series: &Series, index: usize) -> String {
1437    series
1438        .name
1439        .as_ref()
1440        .and_then(|name| name.values.first())
1441        .cloned()
1442        .unwrap_or_else(|| format!("Series {}", index + 1))
1443}
1444
1445fn render_data_labels(
1446    plot: &Plot,
1447    bounds: Rect,
1448    blanks: DispBlanksAs,
1449    options: GeometryOptions,
1450    fonts: &mut FontManager,
1451    elements: &mut Vec<PositionedElement>,
1452) -> Result<()> {
1453    let plot_labels = plot_data_labels(plot);
1454    let anchors = plot_label_anchors(plot, bounds, blanks, options)?;
1455    for (series_index, series) in plot_series(plot).iter().enumerate() {
1456        let Some(labels) = series.data_labels.as_ref().or(plot_labels) else {
1457            continue;
1458        };
1459        let overrides = point_label_overrides(labels)?;
1460        let mut categories = None;
1461        let mut bubble_sizes = None;
1462        let (_, values) = logical_numeric_values(&series.values)?;
1463        let mut percentage_total = None;
1464        for (logical_index, value) in values {
1465            let point_override = overrides.get(&logical_index);
1466            if point_override.is_some_and(|point| point.deleted == Some(true)) {
1467                continue;
1468            }
1469            let effective_labels = point_override
1470                .map(|point| point.apply_to(labels))
1471                .unwrap_or_else(|| labels.clone());
1472            let labels = &effective_labels;
1473            let Some(anchor) = anchors.get(&(series_index, logical_index)).copied() else {
1474                continue;
1475            };
1476            let mut fields = Vec::new();
1477            if labels.show_series_name {
1478                fields.push(series_name(series, series_index));
1479            }
1480            if labels.show_category_name {
1481                if categories.is_none() {
1482                    categories = Some(
1483                        series_categories(series, None)?
1484                            .into_iter()
1485                            .collect::<BTreeMap<_, _>>(),
1486                    );
1487                }
1488                if let Some(category) = categories
1489                    .as_ref()
1490                    .and_then(|categories| categories.get(&logical_index))
1491                {
1492                    fields.push(category.clone());
1493                }
1494            }
1495            if labels.show_value {
1496                fields.push(format_axis_value(labels.number_format.as_ref(), value)?);
1497            }
1498            if labels.show_percent {
1499                let total = if let Some(total) = percentage_total {
1500                    total
1501                } else {
1502                    let total = series
1503                        .values
1504                        .values
1505                        .iter()
1506                        .copied()
1507                        .filter(|value| *value > 0.0)
1508                        .try_fold(0.0, |total, value| {
1509                            checked_geometry_sum(total, value, "data-label percentage total")
1510                        })?;
1511                    percentage_total = Some(total);
1512                    total
1513                };
1514                if total > 0.0 {
1515                    let text = if let Some(format) = &labels.number_format {
1516                        format.format_value(value / total)?
1517                    } else {
1518                        NumberFormat::new("0%".to_owned(), false)?.format_value(value / total)?
1519                    };
1520                    fields.push(text);
1521                }
1522            }
1523            if labels.show_bubble_size {
1524                if bubble_sizes.is_none() {
1525                    bubble_sizes = Some(
1526                        series
1527                            .bubble_size
1528                            .as_ref()
1529                            .map(logical_numeric_values)
1530                            .transpose()?
1531                            .map(|(_, values)| values.into_iter().collect::<BTreeMap<_, _>>())
1532                            .unwrap_or_default(),
1533                    );
1534                }
1535                if let Some(value) = bubble_sizes
1536                    .as_ref()
1537                    .and_then(|sizes| sizes.get(&logical_index))
1538                {
1539                    fields.push(format_axis_value(labels.number_format.as_ref(), *value)?);
1540                }
1541            }
1542            if fields.is_empty() {
1543                continue;
1544            }
1545            let separator = labels.separator.as_deref().unwrap_or(", ");
1546            let text = fields.join(separator);
1547            let origin = anchor.origin(labels.position);
1548            elements.push(PositionedElement::Text(shape_label(fonts, &text, origin)?));
1549        }
1550    }
1551    Ok(())
1552}
1553
1554#[derive(Clone, Debug, Default)]
1555struct PointLabelOverride {
1556    index: Option<usize>,
1557    deleted: Option<bool>,
1558    number_format: Option<NumberFormat>,
1559    position: Option<DataLabelPosition>,
1560    separator: Option<String>,
1561    show_legend_key: Option<bool>,
1562    show_value: Option<bool>,
1563    show_category_name: Option<bool>,
1564    show_series_name: Option<bool>,
1565    show_percent: Option<bool>,
1566    show_bubble_size: Option<bool>,
1567}
1568
1569impl PointLabelOverride {
1570    fn apply_to(&self, labels: &CT_DLbls) -> CT_DLbls {
1571        let mut effective = labels.clone();
1572        if let Some(value) = &self.number_format {
1573            effective.number_format = Some(value.clone());
1574        }
1575        if let Some(value) = self.position {
1576            effective.position = Some(value);
1577        }
1578        if let Some(value) = &self.separator {
1579            effective.separator = Some(value.clone());
1580        }
1581        if let Some(value) = self.show_legend_key {
1582            effective.show_legend_key = value;
1583        }
1584        if let Some(value) = self.show_value {
1585            effective.show_value = value;
1586        }
1587        if let Some(value) = self.show_category_name {
1588            effective.show_category_name = value;
1589        }
1590        if let Some(value) = self.show_series_name {
1591            effective.show_series_name = value;
1592        }
1593        if let Some(value) = self.show_percent {
1594            effective.show_percent = value;
1595        }
1596        if let Some(value) = self.show_bubble_size {
1597            effective.show_bubble_size = value;
1598        }
1599        effective
1600    }
1601}
1602
1603fn point_label_overrides(labels: &CT_DLbls) -> Result<BTreeMap<usize, PointLabelOverride>> {
1604    let mut namespaces = chart_namespace_defaults();
1605    for (name, namespace) in &labels.namespace_declarations {
1606        let prefix = if name == "xmlns" {
1607            Vec::new()
1608        } else if let Some(prefix) = name.strip_prefix("xmlns:") {
1609            prefix.as_bytes().to_vec()
1610        } else {
1611            continue;
1612        };
1613        upsert_namespace_binding(&mut namespaces, prefix, namespace.clone());
1614    }
1615    let mut overrides = BTreeMap::new();
1616    for raw in labels.raw_children.at(0) {
1617        if chart_root_local(raw, &namespaces)?.as_deref() != Some(b"dLbl") {
1618            continue;
1619        }
1620        let point = parse_point_label_override(raw, &namespaces)?;
1621        let index = point
1622            .index
1623            .ok_or_else(|| ChartError::MissingElement("c:dLbl/c:idx".to_owned()))?;
1624        if overrides.insert(index, point).is_some() {
1625            return Err(ChartError::DuplicateElement(format!(
1626                "c:dLbl[c:idx={index}]"
1627            )));
1628        }
1629    }
1630    Ok(overrides)
1631}
1632
1633fn parse_point_label_override(
1634    xml: &[u8],
1635    inherited: &NamespaceBindings,
1636) -> Result<PointLabelOverride> {
1637    let mut reader = Reader::from_reader(xml);
1638    let mut buffer = Vec::new();
1639    let mut namespaces = None;
1640    let mut state = PointLabelOverride::default();
1641    let mut order = 0;
1642    loop {
1643        match reader
1644            .read_event_into(&mut buffer)
1645            .map_err(OxmlError::from)?
1646        {
1647            Event::Start(element) if namespaces.is_none() => {
1648                if !matches_local_name(element.name().as_ref(), b"dLbl")
1649                    || !element_is_in_namespace(&element, C_NS, inherited)?
1650                {
1651                    return Err(ChartError::UnexpectedElement(element_name(&element)));
1652                }
1653                namespaces = Some(chart_bindings(inherited, &element)?);
1654            }
1655            Event::Start(element) => {
1656                let bindings = namespaces
1657                    .as_ref()
1658                    .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
1659                let name = chart_child_local(&element, bindings)?;
1660                let raw = capture_element(&mut reader, &element)?;
1661                parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
1662            }
1663            Event::Empty(element) => {
1664                let bindings = namespaces
1665                    .as_ref()
1666                    .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
1667                let name = chart_child_local(&element, bindings)?;
1668                let raw = capture_empty_element(&element)?;
1669                parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
1670            }
1671            Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbl") => {
1672                if state.index.is_none() {
1673                    return Err(ChartError::MissingElement("c:dLbl/c:idx".to_owned()));
1674                }
1675                return Ok(state);
1676            }
1677            Event::Eof => return Err(missing_end("c:dLbl")),
1678            _ => {}
1679        }
1680        buffer.clear();
1681    }
1682}
1683
1684fn parse_point_label_child(
1685    state: &mut PointLabelOverride,
1686    name: Option<&[u8]>,
1687    raw: &[u8],
1688    namespaces: &NamespaceBindings,
1689    order: &mut usize,
1690) -> Result<()> {
1691    let Some(name) = name else {
1692        return Ok(());
1693    };
1694    let rank = match name {
1695        b"idx" => 0,
1696        b"delete" => 1,
1697        b"layout" => 2,
1698        b"tx" => 3,
1699        b"numFmt" => 4,
1700        b"spPr" => 5,
1701        b"txPr" => 6,
1702        b"dLblPos" => 7,
1703        b"showLegendKey" => 8,
1704        b"showVal" => 9,
1705        b"showCatName" => 10,
1706        b"showSerName" => 11,
1707        b"showPercent" => 12,
1708        b"showBubbleSize" => 13,
1709        b"separator" => 14,
1710        b"showLeaderLines" => 15,
1711        b"leaderLines" => 16,
1712        b"extLst" => 17,
1713        _ => return Ok(()),
1714    };
1715    if rank < *order {
1716        return Err(ChartError::InvalidValue {
1717            element: "c:dLbl".to_owned(),
1718            value: format!("c:{} is out of schema order", String::from_utf8_lossy(name)),
1719        });
1720    }
1721    *order = rank;
1722    match name {
1723        b"idx" => {
1724            let (index, _) = parse_u32_scalar(raw, "idx")?;
1725            let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
1726                element: "c:dLbl/c:idx".to_owned(),
1727                value: index.to_string(),
1728            })?;
1729            set_once(&mut state.index, index, "c:dLbl/c:idx")?;
1730        }
1731        b"delete" => set_once(
1732            &mut state.deleted,
1733            parse_bool_value(raw, "delete")?.0,
1734            "c:dLbl/c:delete",
1735        )?,
1736        b"numFmt" => set_once(
1737            &mut state.number_format,
1738            NumberFormat::from_xml(raw)?,
1739            "c:dLbl/c:numFmt",
1740        )?,
1741        b"dLblPos" => {
1742            let (value, _) = scalar_value(raw, "dLblPos")?;
1743            let value =
1744                value.ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
1745            let position = DataLabelPosition::parse(&value)
1746                .ok_or_else(|| invalid_attribute("dLblPos", "val", value))?;
1747            set_once(&mut state.position, position, "c:dLbl/c:dLblPos")?;
1748        }
1749        b"showLegendKey" => set_once(
1750            &mut state.show_legend_key,
1751            parse_bool_value(raw, "showLegendKey")?.0,
1752            "c:dLbl/c:showLegendKey",
1753        )?,
1754        b"showVal" => set_once(
1755            &mut state.show_value,
1756            parse_bool_value(raw, "showVal")?.0,
1757            "c:dLbl/c:showVal",
1758        )?,
1759        b"showCatName" => set_once(
1760            &mut state.show_category_name,
1761            parse_bool_value(raw, "showCatName")?.0,
1762            "c:dLbl/c:showCatName",
1763        )?,
1764        b"showSerName" => set_once(
1765            &mut state.show_series_name,
1766            parse_bool_value(raw, "showSerName")?.0,
1767            "c:dLbl/c:showSerName",
1768        )?,
1769        b"showPercent" => set_once(
1770            &mut state.show_percent,
1771            parse_bool_value(raw, "showPercent")?.0,
1772            "c:dLbl/c:showPercent",
1773        )?,
1774        b"showBubbleSize" => set_once(
1775            &mut state.show_bubble_size,
1776            parse_bool_value(raw, "showBubbleSize")?.0,
1777            "c:dLbl/c:showBubbleSize",
1778        )?,
1779        b"separator" => set_once(
1780            &mut state.separator,
1781            parse_text_element(raw, b"separator", namespaces)?.0,
1782            "c:dLbl/c:separator",
1783        )?,
1784        _ => {}
1785    }
1786    Ok(())
1787}
1788
1789#[derive(Clone, Copy, Debug)]
1790struct LabelAnchor {
1791    center: Point,
1792    base: Point,
1793    end: Point,
1794    direction: Point,
1795}
1796
1797impl LabelAnchor {
1798    const fn point(center: Point) -> Self {
1799        Self {
1800            center,
1801            base: center,
1802            end: center,
1803            direction: Point { x: 0.0, y: 0.0 },
1804        }
1805    }
1806
1807    const fn segment(center: Point, base: Point, end: Point) -> Self {
1808        Self {
1809            center,
1810            base,
1811            end,
1812            direction: Point {
1813                x: end.x - base.x,
1814                y: end.y - base.y,
1815            },
1816        }
1817    }
1818
1819    const fn directed_segment(center: Point, base: Point, end: Point, direction: Point) -> Self {
1820        Self {
1821            center,
1822            base,
1823            end,
1824            direction,
1825        }
1826    }
1827
1828    fn origin(self, position: Option<DataLabelPosition>) -> Point {
1829        let mut origin = self.center;
1830        match position.unwrap_or(DataLabelPosition::BestFit) {
1831            DataLabelPosition::Bottom => origin.y += 8.0,
1832            DataLabelPosition::Top => origin.y -= 5.0,
1833            DataLabelPosition::Left => origin.x -= 8.0,
1834            DataLabelPosition::Right => origin.x += 8.0,
1835            DataLabelPosition::InsideBase => {
1836                origin = self.along_segment(self.base, 5.0, true).unwrap_or(Point {
1837                    x: origin.x,
1838                    y: origin.y + 8.0,
1839                });
1840            }
1841            DataLabelPosition::InsideEnd => {
1842                origin = self.along_segment(self.end, -5.0, true).unwrap_or(origin);
1843            }
1844            DataLabelPosition::OutsideEnd => {
1845                origin = self.along_segment(self.end, 5.0, false).unwrap_or(Point {
1846                    x: origin.x,
1847                    y: origin.y - 5.0,
1848                });
1849            }
1850            DataLabelPosition::BestFit | DataLabelPosition::Center => {}
1851        }
1852        origin
1853    }
1854
1855    fn along_segment(self, origin: Point, distance: f64, clamp_inside: bool) -> Option<Point> {
1856        let dx = self.end.x - self.base.x;
1857        let dy = self.end.y - self.base.y;
1858        let length = dx.hypot(dy);
1859        let (dx, dy, direction_length) = if length.is_finite() && length > 0.0 {
1860            (dx, dy, length)
1861        } else {
1862            let direction_length = self.direction.x.hypot(self.direction.y);
1863            if !direction_length.is_finite() || direction_length == 0.0 {
1864                return None;
1865            }
1866            (self.direction.x, self.direction.y, direction_length)
1867        };
1868        let distance = if clamp_inside {
1869            distance.clamp(-length / 2.0, length / 2.0)
1870        } else {
1871            distance
1872        };
1873        Some(Point {
1874            x: origin.x + dx / direction_length * distance,
1875            y: origin.y + dy / direction_length * distance,
1876        })
1877    }
1878}
1879
1880fn plot_label_anchors(
1881    plot: &Plot,
1882    bounds: Rect,
1883    blanks: DispBlanksAs,
1884    options: GeometryOptions,
1885) -> Result<BTreeMap<(usize, usize), LabelAnchor>> {
1886    let mut anchors = BTreeMap::new();
1887    match plot {
1888        Plot::Bar {
1889            direction,
1890            grouping,
1891            gap_width,
1892            overlap,
1893            series,
1894            ..
1895        } => {
1896            for bar in bar_rectangles(
1897                *direction, *grouping, *gap_width, *overlap, series, bounds, options,
1898            )? {
1899                if !point_in_rect(bar.end, bounds) {
1900                    continue;
1901                }
1902                let visible_base = Point {
1903                    x: bar.base.x.clamp(bounds.x, bounds.x + bounds.width),
1904                    y: bar.base.y.clamp(bounds.y, bounds.y + bounds.height),
1905                };
1906                let visible_end = Point {
1907                    x: bar.end.x.clamp(bounds.x, bounds.x + bounds.width),
1908                    y: bar.end.y.clamp(bounds.y, bounds.y + bounds.height),
1909                };
1910                anchors.insert(
1911                    (bar.series_index, bar.logical_index),
1912                    LabelAnchor::directed_segment(
1913                        Point {
1914                            x: (visible_base.x + visible_end.x) / 2.0,
1915                            y: (visible_base.y + visible_end.y) / 2.0,
1916                        },
1917                        visible_base,
1918                        visible_end,
1919                        Point {
1920                            x: bar.end.x - bar.base.x,
1921                            y: bar.end.y - bar.base.y,
1922                        },
1923                    ),
1924                );
1925            }
1926        }
1927        Plot::Line {
1928            grouping, series, ..
1929        }
1930        | Plot::Area {
1931            grouping, series, ..
1932        } => {
1933            for (series_index, points) in
1934                category_label_points(*grouping, series, bounds, blanks, options)?
1935                    .into_iter()
1936                    .enumerate()
1937            {
1938                for (logical_index, point) in points {
1939                    anchors.insert((series_index, logical_index), LabelAnchor::point(point));
1940                }
1941            }
1942        }
1943        Plot::Scatter { series, .. } => {
1944            let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
1945            let x_mapping = options.scatter_x.unwrap_or(AxisMapping {
1946                domain: x_domain,
1947                orientation: Orientation::MinMax,
1948            });
1949            let y_mapping = options.scatter_y.unwrap_or(AxisMapping {
1950                domain: y_domain,
1951                orientation: Orientation::MinMax,
1952            });
1953            for (series_index, series) in series.iter().enumerate() {
1954                let indexed = matched_scatter_points(series)?;
1955                let points = scatter_points(
1956                    &indexed,
1957                    x_mapping.domain,
1958                    y_mapping.domain,
1959                    bounds,
1960                    x_mapping.orientation,
1961                    y_mapping.orientation,
1962                )?;
1963                for ((logical_index, _, _), point) in indexed.into_iter().zip(points) {
1964                    anchors.insert((series_index, logical_index), LabelAnchor::point(point));
1965                }
1966            }
1967        }
1968        Plot::Radar { series, .. } => {
1969            let center = Point {
1970                x: bounds.x + bounds.width / 2.0,
1971                y: bounds.y + bounds.height / 2.0,
1972            };
1973            let category_count = series_category_count(series)?;
1974            for (series_index, points) in radar_series_points(series, bounds, options)?
1975                .into_iter()
1976                .enumerate()
1977            {
1978                for (logical_index, point) in points {
1979                    let spoke = radial_point(
1980                        center,
1981                        1.0,
1982                        oriented_category_index(
1983                            logical_index,
1984                            category_count,
1985                            options.category_orientation,
1986                        ),
1987                        category_count,
1988                    );
1989                    anchors.insert(
1990                        (series_index, logical_index),
1991                        LabelAnchor::directed_segment(
1992                            point,
1993                            center,
1994                            point,
1995                            Point {
1996                                x: spoke.x - center.x,
1997                                y: spoke.y - center.y,
1998                            },
1999                        ),
2000                    );
2001                }
2002            }
2003        }
2004        Plot::Pie {
2005            first_slice_angle,
2006            series,
2007            ..
2008        } => {
2009            for slice in pie_slices(*first_slice_angle, None, series, bounds)? {
2010                anchors.insert(
2011                    (slice.series_index, slice.logical_index),
2012                    LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2013                );
2014            }
2015        }
2016        Plot::Doughnut {
2017            first_slice_angle,
2018            hole_size,
2019            series,
2020            ..
2021        } => {
2022            for slice in pie_slices(*first_slice_angle, Some(*hole_size), series, bounds)? {
2023                anchors.insert(
2024                    (slice.series_index, slice.logical_index),
2025                    LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2026                );
2027            }
2028        }
2029    }
2030    anchors.retain(|_, anchor| point_in_rect(anchor.end, bounds));
2031    Ok(anchors)
2032}
2033
2034fn category_label_points(
2035    grouping: Grouping,
2036    series: &[Series],
2037    bounds: Rect,
2038    blanks: DispBlanksAs,
2039    options: GeometryOptions,
2040) -> Result<Vec<Vec<(usize, Point)>>> {
2041    validate_series_values(series)?;
2042    let logical_series = logical_series_values(series)?;
2043    let category_count = series_category_count(series)?;
2044    let calculated_series = if blanks == DispBlanksAs::Zero {
2045        zero_filled_series(&logical_series, category_count)
2046    } else {
2047        logical_series.clone()
2048    };
2049    let layers = stacked_series_bounds(
2050        &calculated_series,
2051        grouping != Grouping::Standard,
2052        grouping == Grouping::PercentStacked,
2053    )?;
2054    let domain = options
2055        .value
2056        .map(|mapping| mapping.domain)
2057        .unwrap_or(domain_from_layers(&layers)?);
2058    let value_orientation = options
2059        .value
2060        .map(|mapping| mapping.orientation)
2061        .unwrap_or(Orientation::MinMax);
2062    let mut point_series = Vec::with_capacity(layers.len());
2063    for (series_index, layer) in layers.iter().enumerate() {
2064        let present: BTreeSet<_> = logical_series[series_index]
2065            .1
2066            .iter()
2067            .map(|(index, _)| *index)
2068            .collect();
2069        let indexed: Vec<_> = layer
2070            .iter()
2071            .filter(|(index, _, _)| present.contains(index))
2072            .map(|(index, _, upper)| (*index, *upper))
2073            .collect();
2074        let points = category_points(
2075            &indexed,
2076            category_count,
2077            domain,
2078            bounds,
2079            options.category_orientation,
2080            value_orientation,
2081        )?;
2082        point_series.push(
2083            indexed
2084                .into_iter()
2085                .map(|(index, _)| index)
2086                .zip(points)
2087                .collect(),
2088        );
2089    }
2090    Ok(point_series)
2091}
2092
2093fn point_in_rect(point: Point, bounds: Rect) -> bool {
2094    const EPSILON: f64 = 1.0e-9;
2095    point.x >= bounds.x - EPSILON
2096        && point.x <= bounds.x + bounds.width + EPSILON
2097        && point.y >= bounds.y - EPSILON
2098        && point.y <= bounds.y + bounds.height + EPSILON
2099}
2100
2101fn resolve_series_colours(
2102    plot: &Plot,
2103    theme: &CT_OfficeStyleSheet,
2104    color_map: &ColorMap,
2105) -> Result<Vec<Color>> {
2106    let lookup = theme_colour_lookup(theme);
2107    plot_series(plot)
2108        .iter()
2109        .enumerate()
2110        .map(|(index, series)| {
2111            if let Some(properties) = series.sp_pr.as_ref() {
2112                if let Some(fill) = properties.fill.as_ref() {
2113                    return resolve_series_paint(fill, index, "fill", theme, color_map, &lookup);
2114                }
2115                if let Some(fill) = properties.line.as_ref().and_then(|line| line.fill.as_ref()) {
2116                    return resolve_series_paint(fill, index, "line", theme, color_map, &lookup);
2117                }
2118            }
2119            resolve_series_accent(index, theme, color_map, &lookup)
2120        })
2121        .collect()
2122}
2123
2124fn resolve_series_paint(
2125    fill: &Fill,
2126    series_index: usize,
2127    source: &str,
2128    theme: &CT_OfficeStyleSheet,
2129    color_map: &ColorMap,
2130    lookup: &[(&str, RgbColor)],
2131) -> Result<Color> {
2132    match fill {
2133        Fill::NoFill(_) => Ok(Color {
2134            r: 0.0,
2135            g: 0.0,
2136            b: 0.0,
2137            a: 0.0,
2138        }),
2139        Fill::Solid(solid) => {
2140            let choice = solid
2141                .color
2142                .as_ref()
2143                .ok_or_else(|| ChartError::InvalidValue {
2144                    element: format!("c:ser[{series_index}] direct {source} paint"),
2145                    value: "solid fill has no colour choice".to_owned(),
2146                })?;
2147            resolve_direct_chart_colour(choice, theme, color_map, lookup, || {
2148                format!("c:ser[{series_index}] direct {source} paint")
2149            })
2150        }
2151        Fill::Gradient(_) => unsupported_series_paint(series_index, source, "gradient"),
2152        Fill::Pattern(_) => unsupported_series_paint(series_index, source, "pattern"),
2153        Fill::Blip(_) => unsupported_series_paint(series_index, source, "picture"),
2154    }
2155}
2156
2157fn resolve_direct_chart_colour(
2158    choice: &ColorChoice,
2159    theme: &CT_OfficeStyleSheet,
2160    color_map: &ColorMap,
2161    lookup: &[(&str, RgbColor)],
2162    element: impl FnOnce() -> String,
2163) -> Result<Color> {
2164    let ColorChoice::Scheme {
2165        value, transforms, ..
2166    } = choice
2167    else {
2168        return resolve_chart_colour(choice, color_map, lookup, element);
2169    };
2170    let theme_slot =
2171        mapped_theme_slot(value, color_map).ok_or_else(|| ChartError::InvalidValue {
2172            element: element(),
2173            value: format!("no concrete theme slot is available for scheme colour {value}"),
2174        })?;
2175    let theme_choice = theme.theme_elements.color_scheme.color(theme_slot);
2176    let base = concrete_theme_colour(theme_choice).ok_or_else(|| ChartError::InvalidValue {
2177        element: format!("a:clrScheme/a:{}", theme_slot.as_str()),
2178        value: "selected theme slot has no concrete sRGB or system fallback colour".to_owned(),
2179    })?;
2180    let mut combined = theme_choice.transforms().to_vec();
2181    combined.extend_from_slice(transforms);
2182    Ok(resolved_layout_colour(apply_color_transforms(
2183        base, &combined,
2184    )))
2185}
2186
2187fn mapped_theme_slot(value: &str, color_map: &ColorMap) -> Option<ThemeColorSlot> {
2188    let mapped = match value {
2189        "bg1" => color_map.theme_slot(ColorMapSlot::Background1),
2190        "tx1" => color_map.theme_slot(ColorMapSlot::Text1),
2191        "bg2" => color_map.theme_slot(ColorMapSlot::Background2),
2192        "tx2" => color_map.theme_slot(ColorMapSlot::Text2),
2193        "accent1" => color_map.theme_slot(ColorMapSlot::Accent1),
2194        "accent2" => color_map.theme_slot(ColorMapSlot::Accent2),
2195        "accent3" => color_map.theme_slot(ColorMapSlot::Accent3),
2196        "accent4" => color_map.theme_slot(ColorMapSlot::Accent4),
2197        "accent5" => color_map.theme_slot(ColorMapSlot::Accent5),
2198        "accent6" => color_map.theme_slot(ColorMapSlot::Accent6),
2199        "hlink" => color_map.theme_slot(ColorMapSlot::Hyperlink),
2200        "folHlink" => color_map.theme_slot(ColorMapSlot::FollowedHyperlink),
2201        "dk1" => ThemeColorSlot::Dark1,
2202        "lt1" => ThemeColorSlot::Light1,
2203        "dk2" => ThemeColorSlot::Dark2,
2204        "lt2" => ThemeColorSlot::Light2,
2205        _ => return None,
2206    };
2207    Some(mapped)
2208}
2209
2210fn unsupported_series_paint(series_index: usize, source: &str, kind: &str) -> Result<Color> {
2211    Err(ChartError::InvalidValue {
2212        element: format!("c:ser[{series_index}] direct {source} paint"),
2213        value: format!("unsupported {kind} paint"),
2214    })
2215}
2216
2217fn resolve_series_accent(
2218    series_index: usize,
2219    theme: &CT_OfficeStyleSheet,
2220    color_map: &ColorMap,
2221    lookup: &[(&str, RgbColor)],
2222) -> Result<Color> {
2223    const ACCENTS: [ColorMapSlot; 6] = [
2224        ColorMapSlot::Accent1,
2225        ColorMapSlot::Accent2,
2226        ColorMapSlot::Accent3,
2227        ColorMapSlot::Accent4,
2228        ColorMapSlot::Accent5,
2229        ColorMapSlot::Accent6,
2230    ];
2231    let semantic_slot = ACCENTS[series_index % ACCENTS.len()];
2232    let theme_slot = color_map.theme_slot(semantic_slot);
2233    let choice = theme.theme_elements.color_scheme.color(theme_slot);
2234    resolve_chart_colour(choice, color_map, lookup, || {
2235        format!("c:ser[{series_index}] theme accent")
2236    })
2237}
2238
2239fn resolve_chart_colour(
2240    choice: &ColorChoice,
2241    color_map: &ColorMap,
2242    lookup: &[(&str, RgbColor)],
2243    element: impl FnOnce() -> String,
2244) -> Result<Color> {
2245    resolve_color(choice, color_map, lookup)
2246        .map(resolved_layout_colour)
2247        .map_err(|error| ChartError::InvalidValue {
2248            element: element(),
2249            value: error.to_string(),
2250        })
2251}
2252
2253fn theme_colour_lookup(theme: &CT_OfficeStyleSheet) -> Vec<(&'static str, RgbColor)> {
2254    theme
2255        .theme_elements
2256        .color_scheme
2257        .iter()
2258        .filter_map(|(slot, choice)| {
2259            concrete_theme_colour(choice).map(|colour| (slot.as_str(), colour))
2260        })
2261        .collect()
2262}
2263
2264fn concrete_theme_colour(choice: &ColorChoice) -> Option<RgbColor> {
2265    match choice {
2266        ColorChoice::Srgb { value, .. } => Some(*value),
2267        ColorChoice::System { last_color, .. } => *last_color,
2268        ColorChoice::Scheme { .. } | ColorChoice::Preset { .. } => None,
2269    }
2270}
2271
2272fn resolved_layout_colour(colour: ResolvedColor) -> Color {
2273    Color {
2274        r: f64::from(colour.red) / 255.0,
2275        g: f64::from(colour.green) / 255.0,
2276        b: f64::from(colour.blue) / 255.0,
2277        a: f64::from(colour.alpha) / 255.0,
2278    }
2279}
2280
2281fn render_legend(
2282    chart: &CT_Chart,
2283    plot: &Plot,
2284    bounds: Rect,
2285    fonts: &mut FontManager,
2286    series_colours: &[Color],
2287    paths: &mut Vec<PositionedElement>,
2288    labels: &mut Vec<PositionedElement>,
2289) -> Result<()> {
2290    if chart.legend.is_none() {
2291        return Ok(());
2292    }
2293    let x = bounds.x + bounds.width - 58.0;
2294    for (index, (series, colour)) in plot_series(plot).iter().zip(series_colours).enumerate() {
2295        let y = bounds.y + 6.0 + index as f64 * 13.0;
2296        paths.push(filled_path(
2297            Path::rect(Rect {
2298                x,
2299                y,
2300                width: 7.0,
2301                height: 7.0,
2302            }),
2303            *colour,
2304        ));
2305        labels.push(PositionedElement::Text(shape_label(
2306            fonts,
2307            &series_name(series, index),
2308            Point {
2309                x: x + 10.0,
2310                y: y + 7.0,
2311            },
2312        )?));
2313    }
2314    Ok(())
2315}
2316
2317fn validate_geometry_coordinates(elements: &[PositionedElement]) -> Result<()> {
2318    fn finite_point(point: Point) -> bool {
2319        point.x.is_finite() && point.y.is_finite()
2320    }
2321
2322    for element in elements {
2323        match element {
2324            PositionedElement::Path(path) => {
2325                for command in &path.path.commands {
2326                    let finite = match command {
2327                        PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
2328                            finite_point(*point)
2329                        }
2330                        PathCommand::CurveTo { c1, c2, to } => {
2331                            finite_point(*c1) && finite_point(*c2) && finite_point(*to)
2332                        }
2333                        PathCommand::Close => true,
2334                    };
2335                    if !finite {
2336                        return Err(ChartError::InvalidValue {
2337                            element: "chart geometry coordinate".to_owned(),
2338                            value: "generated path contains a nonfinite point".to_owned(),
2339                        });
2340                    }
2341                }
2342            }
2343            PositionedElement::Group(group) => validate_geometry_coordinates(&group.children)?,
2344            _ => {}
2345        }
2346    }
2347    Ok(())
2348}
2349
2350fn render_plot_geometry(
2351    plot: &Plot,
2352    bounds: Rect,
2353    blanks: DispBlanksAs,
2354    options: GeometryOptions,
2355    series_colours: &[Color],
2356) -> Result<Vec<PositionedElement>> {
2357    match plot {
2358        Plot::Bar {
2359            direction,
2360            grouping,
2361            gap_width,
2362            overlap,
2363            series,
2364            ..
2365        } => render_bar_geometry(
2366            *direction,
2367            *grouping,
2368            *gap_width,
2369            *overlap,
2370            series,
2371            bounds,
2372            options,
2373            series_colours,
2374        ),
2375        Plot::Line {
2376            grouping,
2377            marker,
2378            series,
2379            ..
2380        } => render_line_geometry(
2381            *grouping,
2382            *marker,
2383            series,
2384            bounds,
2385            blanks,
2386            options,
2387            series_colours,
2388        ),
2389        Plot::Pie {
2390            first_slice_angle,
2391            series,
2392            ..
2393        } => render_pie_geometry(*first_slice_angle, None, series, bounds, series_colours),
2394        Plot::Doughnut {
2395            first_slice_angle,
2396            hole_size,
2397            series,
2398            ..
2399        } => render_pie_geometry(
2400            *first_slice_angle,
2401            Some(*hole_size),
2402            series,
2403            bounds,
2404            series_colours,
2405        ),
2406        Plot::Area {
2407            grouping, series, ..
2408        } => render_area_geometry(*grouping, series, bounds, blanks, options, series_colours),
2409        Plot::Scatter { style, series, .. } => {
2410            render_scatter_geometry(*style, series, bounds, blanks, options, series_colours)
2411        }
2412        Plot::Radar { style, series, .. } => {
2413            render_radar_geometry(*style, series, bounds, options, series_colours)
2414        }
2415    }
2416}
2417
2418#[allow(clippy::too_many_arguments)]
2419fn render_bar_geometry(
2420    direction: BarDirection,
2421    grouping: BarGrouping,
2422    gap_width: u16,
2423    overlap: i8,
2424    series: &[Series],
2425    bounds: Rect,
2426    options: GeometryOptions,
2427    series_colours: &[Color],
2428) -> Result<Vec<PositionedElement>> {
2429    let bars = bar_rectangles(
2430        direction, grouping, gap_width, overlap, series, bounds, options,
2431    )?;
2432    bars.into_iter()
2433        .map(|bar| {
2434            Ok(filled_path(
2435                Path::rect(bar.rect),
2436                series_colour(series_colours, bar.series_index)?,
2437            ))
2438        })
2439        .collect()
2440}
2441
2442#[derive(Clone, Copy, Debug)]
2443struct BarRectangle {
2444    series_index: usize,
2445    logical_index: usize,
2446    rect: Rect,
2447    base: Point,
2448    end: Point,
2449}
2450
2451fn bar_rectangles(
2452    direction: BarDirection,
2453    grouping: BarGrouping,
2454    gap_width: u16,
2455    overlap: i8,
2456    series: &[Series],
2457    bounds: Rect,
2458    options: GeometryOptions,
2459) -> Result<Vec<BarRectangle>> {
2460    validate_series_values(series)?;
2461    let logical_series = logical_series_values(series)?;
2462    let category_count = series_category_count(series)?;
2463    let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
2464    let percent = grouping == BarGrouping::PercentStacked;
2465    let layers = stacked_series_bounds(&logical_series, stacked, percent)?;
2466    let domain = options
2467        .value
2468        .map(|mapping| mapping.domain)
2469        .unwrap_or(domain_from_layers(&layers)?);
2470    let value_orientation = options
2471        .value
2472        .map(|mapping| mapping.orientation)
2473        .unwrap_or(Orientation::MinMax);
2474    let category_extent = if direction == BarDirection::Column {
2475        bounds.width
2476    } else {
2477        bounds.height
2478    };
2479    let slot = category_extent / category_count as f64;
2480    let cluster_extent = slot * 100.0 / (100.0 + f64::from(gap_width));
2481    let bars_per_cluster = if stacked { 1 } else { series.len() };
2482    let overlap_fraction = if stacked {
2483        0.0
2484    } else {
2485        f64::from(overlap) / 100.0
2486    };
2487    let divisor =
2488        bars_per_cluster as f64 - (bars_per_cluster.saturating_sub(1)) as f64 * overlap_fraction;
2489    let bar_extent = cluster_extent / divisor;
2490    let advance = bar_extent * (1.0 - overlap_fraction);
2491    let mut bars = Vec::new();
2492    for (series_index, layer) in layers.iter().enumerate() {
2493        for &(logical_index, start, end) in layer {
2494            let category = oriented_category_index(
2495                logical_index,
2496                category_count,
2497                options.category_orientation,
2498            );
2499            let cluster_start = category as f64 / category_count as f64 * category_extent
2500                + (slot - cluster_extent) / 2.0;
2501            let category_start = if stacked {
2502                cluster_start
2503            } else {
2504                cluster_start + series_index as f64 * advance
2505            };
2506            let (rect, base, end) = if direction == BarDirection::Column {
2507                let y0 = map_y_oriented(start, domain, bounds, value_orientation)?;
2508                let y1 = map_y_oriented(end, domain, bounds, value_orientation)?;
2509                let center_x = bounds.x + category_start + bar_extent / 2.0;
2510                (
2511                    Rect {
2512                        x: bounds.x + category_start,
2513                        y: y0.min(y1),
2514                        width: bar_extent,
2515                        height: (y1 - y0).abs(),
2516                    },
2517                    Point { x: center_x, y: y0 },
2518                    Point { x: center_x, y: y1 },
2519                )
2520            } else {
2521                let x0 = map_x_oriented(start, domain, bounds, value_orientation)?;
2522                let x1 = map_x_oriented(end, domain, bounds, value_orientation)?;
2523                let center_y = bounds.y + category_start + bar_extent / 2.0;
2524                (
2525                    Rect {
2526                        x: x0.min(x1),
2527                        y: bounds.y + category_start,
2528                        width: (x1 - x0).abs(),
2529                        height: bar_extent,
2530                    },
2531                    Point { x: x0, y: center_y },
2532                    Point { x: x1, y: center_y },
2533                )
2534            };
2535            bars.push(BarRectangle {
2536                series_index,
2537                logical_index,
2538                rect,
2539                base,
2540                end,
2541            });
2542        }
2543    }
2544    Ok(bars)
2545}
2546
2547fn render_line_geometry(
2548    grouping: Grouping,
2549    marker: bool,
2550    series: &[Series],
2551    bounds: Rect,
2552    blanks: DispBlanksAs,
2553    options: GeometryOptions,
2554    series_colours: &[Color],
2555) -> Result<Vec<PositionedElement>> {
2556    validate_series_values(series)?;
2557    let logical_series = logical_series_values(series)?;
2558    let category_count = series_category_count(series)?;
2559    let calculated_series = if blanks == DispBlanksAs::Zero {
2560        zero_filled_series(&logical_series, category_count)
2561    } else {
2562        logical_series.clone()
2563    };
2564    let stacked = grouping != Grouping::Standard;
2565    let percent = grouping == Grouping::PercentStacked;
2566    let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
2567    let domain = options
2568        .value
2569        .map(|mapping| mapping.domain)
2570        .unwrap_or(domain_from_layers(&layers)?);
2571    let value_orientation = options
2572        .value
2573        .map(|mapping| mapping.orientation)
2574        .unwrap_or(Orientation::MinMax);
2575    let mut elements = Vec::new();
2576    for (series_index, layer) in layers.iter().enumerate() {
2577        let colour = series_colour(series_colours, series_index)?;
2578        let indexed: Vec<_> = layer
2579            .iter()
2580            .map(|(index, _, upper)| (*index, *upper))
2581            .collect();
2582        let indexes: Vec<_> = indexed.iter().map(|(index, _)| *index).collect();
2583        for (start, end) in contiguous_ranges(&indexes, blanks) {
2584            let points = category_points(
2585                &indexed[start..end],
2586                category_count,
2587                domain,
2588                bounds,
2589                options.category_orientation,
2590                value_orientation,
2591            )?;
2592            elements.push(stroked_path(polyline_path(&points, false), colour));
2593        }
2594        if marker {
2595            let present: BTreeSet<_> = logical_series[series_index]
2596                .1
2597                .iter()
2598                .map(|(index, _)| *index)
2599                .collect();
2600            let marker_values: Vec<_> = indexed
2601                .iter()
2602                .copied()
2603                .filter(|(index, _)| present.contains(index))
2604                .collect();
2605            let marker_points = category_points(
2606                &marker_values,
2607                category_count,
2608                domain,
2609                bounds,
2610                options.category_orientation,
2611                value_orientation,
2612            )?;
2613            push_markers(&mut elements, &marker_points, colour);
2614        }
2615    }
2616    Ok(elements)
2617}
2618
2619fn render_area_geometry(
2620    grouping: Grouping,
2621    series: &[Series],
2622    bounds: Rect,
2623    blanks: DispBlanksAs,
2624    options: GeometryOptions,
2625    series_colours: &[Color],
2626) -> Result<Vec<PositionedElement>> {
2627    validate_series_values(series)?;
2628    let logical_series = logical_series_values(series)?;
2629    let category_count = series_category_count(series)?;
2630    let calculated_series = if blanks == DispBlanksAs::Zero {
2631        zero_filled_series(&logical_series, category_count)
2632    } else {
2633        logical_series
2634    };
2635    let stacked = grouping != Grouping::Standard;
2636    let percent = grouping == Grouping::PercentStacked;
2637    let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
2638    let domain = options
2639        .value
2640        .map(|mapping| mapping.domain)
2641        .unwrap_or(domain_from_layers(&layers)?);
2642    let value_orientation = options
2643        .value
2644        .map(|mapping| mapping.orientation)
2645        .unwrap_or(Orientation::MinMax);
2646    let mut elements = Vec::new();
2647    for (series_index, layer) in layers.iter().enumerate() {
2648        let colour = series_colour(series_colours, series_index)?;
2649        let indexes: Vec<_> = layer.iter().map(|(index, _, _)| *index).collect();
2650        for (start, end) in contiguous_ranges(&indexes, blanks) {
2651            let segment = &layer[start..end];
2652            let top_values: Vec<_> = segment
2653                .iter()
2654                .map(|(index, _, upper)| (*index, *upper))
2655                .collect();
2656            let lower_values: Vec<_> = segment
2657                .iter()
2658                .map(|(index, lower, _)| (*index, *lower))
2659                .collect();
2660            let top = category_points(
2661                &top_values,
2662                category_count,
2663                domain,
2664                bounds,
2665                options.category_orientation,
2666                value_orientation,
2667            )?;
2668            let mut bottom = category_points(
2669                &lower_values,
2670                category_count,
2671                domain,
2672                bounds,
2673                options.category_orientation,
2674                value_orientation,
2675            )?;
2676            bottom.reverse();
2677            let mut commands = Vec::with_capacity(top.len() + bottom.len() + 2);
2678            commands.push(PathCommand::MoveTo(top[0]));
2679            commands.extend(top.iter().skip(1).copied().map(PathCommand::LineTo));
2680            commands.extend(bottom.into_iter().map(PathCommand::LineTo));
2681            commands.push(PathCommand::Close);
2682            elements.push(filled_and_stroked_path(
2683                Path {
2684                    commands,
2685                    fill_rule: FillRule::NonZero,
2686                },
2687                colour,
2688            ));
2689        }
2690    }
2691    Ok(elements)
2692}
2693
2694fn render_scatter_geometry(
2695    style: ScatterStyle,
2696    series: &[Series],
2697    bounds: Rect,
2698    blanks: DispBlanksAs,
2699    options: GeometryOptions,
2700    series_colours: &[Color],
2701) -> Result<Vec<PositionedElement>> {
2702    validate_series_values(series)?;
2703    let mut paired_series = Vec::with_capacity(series.len());
2704    let mut marker_series = Vec::with_capacity(series.len());
2705    let mut all_x = Vec::new();
2706    let mut all_y = Vec::new();
2707    for item in series {
2708        let markers = matched_scatter_points(item)?;
2709        let paired = scatter_render_points(item, blanks)?;
2710        all_x.extend(paired.iter().map(|(_, x, _)| *x));
2711        all_y.extend(paired.iter().map(|(_, _, y)| *y));
2712        paired_series.push(paired);
2713        marker_series.push(markers);
2714    }
2715    let x_domain = options
2716        .scatter_x
2717        .map(|mapping| mapping.domain)
2718        .unwrap_or(data_domain(&all_x, false)?);
2719    let y_domain = options
2720        .scatter_y
2721        .map(|mapping| mapping.domain)
2722        .unwrap_or(data_domain(&all_y, false)?);
2723    let x_orientation = options
2724        .scatter_x
2725        .map(|mapping| mapping.orientation)
2726        .unwrap_or(Orientation::MinMax);
2727    let y_orientation = options
2728        .scatter_y
2729        .map(|mapping| mapping.orientation)
2730        .unwrap_or(Orientation::MinMax);
2731    let draw_line = matches!(
2732        style,
2733        ScatterStyle::Line
2734            | ScatterStyle::LineMarker
2735            | ScatterStyle::Smooth
2736            | ScatterStyle::SmoothMarker
2737    );
2738    let draw_marker = matches!(
2739        style,
2740        ScatterStyle::Marker | ScatterStyle::LineMarker | ScatterStyle::SmoothMarker
2741    );
2742    let mut elements = Vec::new();
2743    for (series_index, paired) in paired_series.iter().enumerate() {
2744        let colour = series_colour(series_colours, series_index)?;
2745        if draw_line {
2746            let indexes: Vec<_> = paired.iter().map(|(index, _, _)| *index).collect();
2747            for (start, end) in contiguous_ranges(&indexes, blanks) {
2748                let points = scatter_points(
2749                    &paired[start..end],
2750                    x_domain,
2751                    y_domain,
2752                    bounds,
2753                    x_orientation,
2754                    y_orientation,
2755                )?;
2756                elements.push(stroked_path(polyline_path(&points, false), colour));
2757            }
2758        }
2759        if draw_marker {
2760            let marker_points = scatter_points(
2761                &marker_series[series_index],
2762                x_domain,
2763                y_domain,
2764                bounds,
2765                x_orientation,
2766                y_orientation,
2767            )?;
2768            push_markers(&mut elements, &marker_points, colour);
2769        }
2770    }
2771    Ok(elements)
2772}
2773
2774type ScatterPoint = (usize, f64, f64);
2775
2776struct ScatterPointMaps {
2777    x_count: usize,
2778    y_count: usize,
2779    x_by_index: BTreeMap<usize, f64>,
2780    y_by_index: BTreeMap<usize, f64>,
2781}
2782
2783fn scatter_point_maps(series: &Series) -> Result<ScatterPointMaps> {
2784    let Some(AxisData::Numeric(x_values)) = &series.categories else {
2785        return Err(ChartError::InvalidValue {
2786            element: "c:xVal".to_owned(),
2787            value: "scatter series requires numeric cached x values".to_owned(),
2788        });
2789    };
2790    validate_finite(&x_values.values, "c:xVal/c:numCache")?;
2791    let (x_count, x_points) = logical_numeric_values(x_values)?;
2792    let (y_count, y_points) = logical_numeric_values(&series.values)?;
2793    Ok(ScatterPointMaps {
2794        x_count,
2795        y_count,
2796        x_by_index: x_points.into_iter().collect(),
2797        y_by_index: y_points.into_iter().collect(),
2798    })
2799}
2800
2801fn matched_scatter_points(series: &Series) -> Result<Vec<ScatterPoint>> {
2802    let points = scatter_point_maps(series)?;
2803    Ok(points
2804        .y_by_index
2805        .iter()
2806        .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
2807        .collect())
2808}
2809
2810fn scatter_render_points(series: &Series, blanks: DispBlanksAs) -> Result<Vec<ScatterPoint>> {
2811    let points = scatter_point_maps(series)?;
2812    if blanks != DispBlanksAs::Zero {
2813        return Ok(points
2814            .y_by_index
2815            .iter()
2816            .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
2817            .collect());
2818    }
2819    let count = points.x_count.max(points.y_count);
2820    let present = points
2821        .x_by_index
2822        .keys()
2823        .chain(points.y_by_index.keys())
2824        .copied()
2825        .collect();
2826    Ok(zero_control_indexes(count, &present)
2827        .into_iter()
2828        .map(|index| {
2829            (
2830                index,
2831                points.x_by_index.get(&index).copied().unwrap_or(0.0),
2832                points.y_by_index.get(&index).copied().unwrap_or(0.0),
2833            )
2834        })
2835        .collect())
2836}
2837
2838fn scatter_points(
2839    values: &[(usize, f64, f64)],
2840    x_domain: Domain,
2841    y_domain: Domain,
2842    bounds: Rect,
2843    x_orientation: Orientation,
2844    y_orientation: Orientation,
2845) -> Result<Vec<Point>> {
2846    values
2847        .iter()
2848        .map(|(_, x, y)| {
2849            Ok(Point {
2850                x: map_x_oriented(*x, x_domain, bounds, x_orientation)?,
2851                y: map_y_oriented(*y, y_domain, bounds, y_orientation)?,
2852            })
2853        })
2854        .collect()
2855}
2856
2857fn render_radar_geometry(
2858    style: RadarStyle,
2859    series: &[Series],
2860    bounds: Rect,
2861    options: GeometryOptions,
2862    series_colours: &[Color],
2863) -> Result<Vec<PositionedElement>> {
2864    let point_series = radar_series_points(series, bounds, options)?;
2865    let mut elements = Vec::new();
2866    for (series_index, points) in point_series.iter().enumerate() {
2867        let colour = series_colour(series_colours, series_index)?;
2868        let plain_points: Vec<_> = points.iter().map(|(_, point)| *point).collect();
2869        if plain_points.is_empty() {
2870            continue;
2871        }
2872        let path = polyline_path(&plain_points, true);
2873        if style == RadarStyle::Filled {
2874            elements.push(filled_and_stroked_path(path, colour));
2875        } else {
2876            elements.push(stroked_path(path, colour));
2877        }
2878        if style == RadarStyle::Marker {
2879            push_markers(&mut elements, &plain_points, colour);
2880        }
2881    }
2882    Ok(elements)
2883}
2884
2885fn radar_series_points(
2886    series: &[Series],
2887    bounds: Rect,
2888    options: GeometryOptions,
2889) -> Result<Vec<Vec<(usize, Point)>>> {
2890    validate_series_values(series)?;
2891    let logical_series = logical_series_values(series)?;
2892    let category_count = series_category_count(series)?;
2893    let values = logical_series
2894        .iter()
2895        .flat_map(|(_, points)| points.iter().map(|(_, value)| *value))
2896        .collect::<Vec<_>>();
2897    let mapping = options.value.unwrap_or(AxisMapping {
2898        domain: data_domain(&values, true)?,
2899        orientation: Orientation::MinMax,
2900    });
2901    let center = Point {
2902        x: bounds.x + bounds.width / 2.0,
2903        y: bounds.y + bounds.height / 2.0,
2904    };
2905    let radius = bounds.width.min(bounds.height) / 2.0;
2906    let mut point_series = Vec::with_capacity(logical_series.len());
2907    for (_, logical) in &logical_series {
2908        let mut points = Vec::with_capacity(logical.len());
2909        for (category, value) in logical {
2910            if *value < mapping.domain.min || *value > mapping.domain.max {
2911                continue;
2912            }
2913            points.push((
2914                *category,
2915                radial_point(
2916                    center,
2917                    normalized_value_oriented(*value, mapping.domain, mapping.orientation)?
2918                        * radius,
2919                    oriented_category_index(
2920                        *category,
2921                        category_count,
2922                        options.category_orientation,
2923                    ),
2924                    category_count,
2925                ),
2926            ));
2927        }
2928        point_series.push(points);
2929    }
2930    Ok(point_series)
2931}
2932
2933fn render_pie_geometry(
2934    first_slice_angle: u16,
2935    hole_size: Option<u8>,
2936    series: &[Series],
2937    bounds: Rect,
2938    series_colours: &[Color],
2939) -> Result<Vec<PositionedElement>> {
2940    pie_slices(first_slice_angle, hole_size, series, bounds)?
2941        .into_iter()
2942        .map(|slice| {
2943            Ok(filled_path(
2944                slice.path,
2945                series_colour(series_colours, slice.series_index)?,
2946            ))
2947        })
2948        .collect()
2949}
2950
2951struct PieSliceGeometry {
2952    series_index: usize,
2953    logical_index: usize,
2954    path: Path,
2955    anchor: Point,
2956    base: Point,
2957    end: Point,
2958}
2959
2960fn pie_slices(
2961    first_slice_angle: u16,
2962    hole_size: Option<u8>,
2963    series: &[Series],
2964    bounds: Rect,
2965) -> Result<Vec<PieSliceGeometry>> {
2966    validate_series_values(series)?;
2967    let center = Point {
2968        x: bounds.x + bounds.width / 2.0,
2969        y: bounds.y + bounds.height / 2.0,
2970    };
2971    let radius = bounds.width.min(bounds.height) / 2.0;
2972    let mut elements = Vec::new();
2973    for (series_index, item) in series.iter().enumerate() {
2974        let (_, logical) = logical_numeric_values(&item.values)?;
2975        let total = logical
2976            .iter()
2977            .map(|(_, value)| *value)
2978            .filter(|value| *value > 0.0)
2979            .try_fold(0.0, |total, value| {
2980                checked_geometry_sum(total, value, "pie value total")
2981            })?;
2982        if total <= 0.0 {
2983            continue;
2984        }
2985        let mut angle = f64::from(first_slice_angle).to_radians() - std::f64::consts::FRAC_PI_2;
2986        for (logical_index, value) in logical {
2987            if value <= 0.0 {
2988                continue;
2989            }
2990            let next = angle + value / total * std::f64::consts::TAU;
2991            let path = if let Some(hole_size) = hole_size {
2992                doughnut_wedge(
2993                    center,
2994                    radius,
2995                    radius * f64::from(hole_size) / 100.0,
2996                    angle,
2997                    next,
2998                )
2999            } else {
3000                pie_wedge(center, radius, angle, next)
3001            };
3002            let inner_radius = hole_size
3003                .map(|hole| radius * f64::from(hole) / 100.0)
3004                .unwrap_or(0.0);
3005            let anchor_radius = hole_size
3006                .map(|_| (radius + inner_radius) / 2.0)
3007                .unwrap_or(radius * 2.0 / 3.0);
3008            let midpoint = (angle + next) / 2.0;
3009            elements.push(PieSliceGeometry {
3010                series_index,
3011                logical_index,
3012                path,
3013                anchor: circle_point(center, anchor_radius, midpoint),
3014                base: circle_point(center, inner_radius, midpoint),
3015                end: circle_point(center, radius, midpoint),
3016            });
3017            angle = next;
3018        }
3019    }
3020    Ok(elements)
3021}
3022
3023fn validate_series_values(series: &[Series]) -> Result<()> {
3024    if series.is_empty() {
3025        return Err(ChartError::MissingElement("c:ser".to_owned()));
3026    }
3027    for item in series {
3028        validate_finite(&item.values.values, "c:val/c:numCache")?;
3029    }
3030    Ok(())
3031}
3032
3033fn validate_finite(values: &[f64], element: &str) -> Result<()> {
3034    if let Some(value) = values.iter().find(|value| !value.is_finite()) {
3035        return Err(ChartError::InvalidValue {
3036            element: element.to_owned(),
3037            value: value.to_string(),
3038        });
3039    }
3040    Ok(())
3041}
3042
3043type IndexedValues = Vec<(usize, f64)>;
3044type LogicalSeries = (usize, IndexedValues);
3045type GeometryLayer = Vec<(usize, f64, f64)>;
3046
3047fn zero_control_indexes(count: usize, present: &BTreeSet<usize>) -> BTreeSet<usize> {
3048    let mut controls = present.clone();
3049    if count == 0 {
3050        return controls;
3051    }
3052    controls.insert(0);
3053    controls.insert(count - 1);
3054    for index in present {
3055        if *index > 0 {
3056            controls.insert(*index - 1);
3057        }
3058        if index + 1 < count {
3059            controls.insert(index + 1);
3060        }
3061    }
3062    controls
3063}
3064
3065fn zero_filled_series(series: &[LogicalSeries], count: usize) -> Vec<LogicalSeries> {
3066    let present = series
3067        .iter()
3068        .flat_map(|(_, points)| points.iter().map(|(index, _)| *index))
3069        .collect();
3070    let controls = zero_control_indexes(count, &present);
3071    series
3072        .iter()
3073        .map(|(declared, points)| {
3074            let values: BTreeMap<_, _> = points.iter().copied().collect();
3075            let filled = controls
3076                .iter()
3077                .map(|index| (*index, values.get(index).copied().unwrap_or(0.0)))
3078                .collect();
3079            (*declared, filled)
3080        })
3081        .collect()
3082}
3083
3084fn contiguous_ranges(indexes: &[usize], blanks: DispBlanksAs) -> Vec<(usize, usize)> {
3085    if indexes.is_empty() {
3086        return Vec::new();
3087    }
3088    if blanks != DispBlanksAs::Gap {
3089        return vec![(0, indexes.len())];
3090    }
3091    let mut ranges = Vec::new();
3092    let mut start = 0usize;
3093    for position in 1..indexes.len() {
3094        if indexes[position - 1].checked_add(1) != Some(indexes[position]) {
3095            ranges.push((start, position));
3096            start = position;
3097        }
3098    }
3099    ranges.push((start, indexes.len()));
3100    ranges
3101}
3102
3103fn logical_numeric_values(data: &NumericData) -> Result<LogicalSeries> {
3104    validate_finite(&data.values, "c:numCache")?;
3105    let (declared, indexes) = cache_layout(&data.markup, data.values.len())?;
3106    let declared = usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
3107        element: "c:ptCount".to_owned(),
3108        value: declared.to_string(),
3109    })?;
3110    let points = indexes
3111        .into_iter()
3112        .zip(data.values.iter().copied())
3113        .map(|(index, value)| {
3114            usize::try_from(index)
3115                .map(|index| (index, value))
3116                .map_err(|_| ChartError::InvalidValue {
3117                    element: "c:pt/@idx".to_owned(),
3118                    value: index.to_string(),
3119                })
3120        })
3121        .collect::<Result<Vec<_>>>()?;
3122    Ok((declared, points))
3123}
3124
3125fn logical_series_values(series: &[Series]) -> Result<Vec<LogicalSeries>> {
3126    series
3127        .iter()
3128        .map(|item| logical_numeric_values(&item.values))
3129        .collect()
3130}
3131
3132fn reference_logical_count(markup: &ReferenceMarkup, value_count: usize) -> Result<usize> {
3133    let (declared, _) = cache_layout(markup, value_count)?;
3134    usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
3135        element: "c:ptCount".to_owned(),
3136        value: declared.to_string(),
3137    })
3138}
3139
3140fn series_category_count(series: &[Series]) -> Result<usize> {
3141    let mut count = 0usize;
3142    for item in series {
3143        count = count.max(reference_logical_count(
3144            &item.values.markup,
3145            item.values.values.len(),
3146        )?);
3147        if let Some(categories) = &item.categories {
3148            let category_count = match categories {
3149                AxisData::String(data) => reference_logical_count(&data.markup, data.values.len())?,
3150                AxisData::Numeric(data) => {
3151                    reference_logical_count(&data.markup, data.values.len())?
3152                }
3153            };
3154            count = count.max(category_count);
3155        }
3156    }
3157    if count == 0 {
3158        return Err(ChartError::InvalidValue {
3159            element: "c:ser".to_owned(),
3160            value: "cached series contain no points".to_owned(),
3161        });
3162    }
3163    Ok(count)
3164}
3165
3166fn stacked_series_bounds(
3167    series: &[LogicalSeries],
3168    stacked: bool,
3169    percent: bool,
3170) -> Result<Vec<GeometryLayer>> {
3171    let mut positive_totals = BTreeMap::<usize, f64>::new();
3172    let mut negative_totals = BTreeMap::<usize, f64>::new();
3173    if percent {
3174        for (_, points) in series {
3175            for &(index, value) in points {
3176                let totals = if value >= 0.0 {
3177                    &mut positive_totals
3178                } else {
3179                    &mut negative_totals
3180                };
3181                let total = totals.entry(index).or_default();
3182                *total = checked_geometry_sum(*total, value.abs(), "stacked percentage total")?;
3183            }
3184        }
3185    }
3186    let mut positive = BTreeMap::<usize, f64>::new();
3187    let mut negative = BTreeMap::<usize, f64>::new();
3188    let mut layers = Vec::with_capacity(series.len());
3189    for (_, points) in series {
3190        let mut layer = Vec::with_capacity(points.len());
3191        for &(index, original) in points {
3192            let total = if original >= 0.0 {
3193                positive_totals.get(&index).copied().unwrap_or(0.0)
3194            } else {
3195                negative_totals.get(&index).copied().unwrap_or(0.0)
3196            };
3197            let value = if percent && total != 0.0 {
3198                original / total
3199            } else {
3200                original
3201            };
3202            if stacked {
3203                let accumulators = if value >= 0.0 {
3204                    &mut positive
3205                } else {
3206                    &mut negative
3207                };
3208                let accumulator = accumulators.entry(index).or_default();
3209                let lower = *accumulator;
3210                *accumulator = checked_geometry_sum(*accumulator, value, "stacked value total")?;
3211                layer.push((index, lower, *accumulator));
3212            } else {
3213                layer.push((index, 0.0, value));
3214            }
3215        }
3216        layers.push(layer);
3217    }
3218    Ok(layers)
3219}
3220
3221fn checked_geometry_sum(current: f64, value: f64, context: &str) -> Result<f64> {
3222    let sum = current + value;
3223    if sum.is_finite() {
3224        Ok(sum)
3225    } else {
3226        Err(ChartError::InvalidValue {
3227            element: "chart geometry aggregate".to_owned(),
3228            value: format!("{context} is nonfinite"),
3229        })
3230    }
3231}
3232
3233#[derive(Clone, Copy, Debug, PartialEq)]
3234struct Domain {
3235    min: f64,
3236    max: f64,
3237}
3238
3239fn nice_number_scale(
3240    extent: Domain,
3241    pinned_minimum: Option<f64>,
3242    pinned_maximum: Option<f64>,
3243    target_tick_count: usize,
3244) -> Result<AxisScale> {
3245    if target_tick_count < 2
3246        || !extent.min.is_finite()
3247        || !extent.max.is_finite()
3248        || pinned_minimum.is_some_and(|value| !value.is_finite())
3249        || pinned_maximum.is_some_and(|value| !value.is_finite())
3250    {
3251        return Err(ChartError::InvalidValue {
3252            element: "chart axis scale".to_owned(),
3253            value: "scale bounds and target tick count must be finite and valid".to_owned(),
3254        });
3255    }
3256
3257    let mut data_min = extent.min.min(extent.max);
3258    let mut data_max = extent.min.max(extent.max);
3259    if data_min == data_max {
3260        let expanded = expand_constant_domain(data_min)?;
3261        data_min = expanded.min;
3262        data_max = expanded.max;
3263    }
3264    let requested_min = match (pinned_minimum, pinned_maximum) {
3265        (Some(minimum), _) => minimum,
3266        (None, Some(maximum)) if data_min >= maximum => {
3267            extend_bound(maximum, stable_positive_step(data_min, data_max, 3)?, false)?
3268        }
3269        (None, _) => data_min,
3270    };
3271    let requested_max = match (pinned_minimum, pinned_maximum) {
3272        (_, Some(maximum)) => maximum,
3273        (Some(minimum), None) if data_max <= minimum => {
3274            extend_bound(minimum, stable_positive_step(data_min, data_max, 3)?, true)?
3275        }
3276        (_, None) => data_max,
3277    };
3278    if requested_min >= requested_max {
3279        return Err(ChartError::InvalidValue {
3280            element: "c:scaling".to_owned(),
3281            value: format!("minimum {requested_min} is not less than maximum {requested_max}"),
3282        });
3283    }
3284
3285    let raw_step = stable_positive_step(requested_min, requested_max, target_tick_count)?;
3286    let step = nice_step(raw_step)?;
3287    let rounded_minimum = (requested_min / step).floor() * step;
3288    let rounded_maximum = (requested_max / step).ceil() * step;
3289    let lower_fallback = pinned_minimum.is_none() && !rounded_minimum.is_finite();
3290    let upper_fallback = pinned_maximum.is_none() && !rounded_maximum.is_finite();
3291    let minimum = pinned_minimum.unwrap_or(if rounded_minimum.is_finite() {
3292        rounded_minimum
3293    } else {
3294        requested_min
3295    });
3296    let maximum = pinned_maximum.unwrap_or(if rounded_maximum.is_finite() {
3297        rounded_maximum
3298    } else {
3299        requested_max
3300    });
3301    if !minimum.is_finite() || !maximum.is_finite() || minimum >= maximum {
3302        return Err(ChartError::InvalidValue {
3303            element: "chart axis scale".to_owned(),
3304            value: "nice-number bounds are nonfinite or empty".to_owned(),
3305        });
3306    }
3307
3308    let first = (minimum / step).ceil();
3309    let last = (maximum / step).floor();
3310    let tick_count = if last >= first {
3311        (last - first).min(1024.0) as usize + 1
3312    } else {
3313        0
3314    };
3315    let mut ticks = Vec::with_capacity(tick_count);
3316    for index in 0..tick_count {
3317        let tick = (first + index as f64) * step;
3318        if tick.is_finite() && tick >= minimum && tick <= maximum {
3319            ticks.push(clean_tick(tick, step));
3320        }
3321    }
3322    if lower_fallback && ticks.first().is_none_or(|tick| *tick > minimum) {
3323        ticks.insert(0, minimum);
3324    }
3325    if upper_fallback && ticks.last().is_none_or(|tick| *tick < maximum) {
3326        ticks.push(maximum);
3327    }
3328    if ticks.len() < 2 {
3329        ticks.extend([minimum, maximum]);
3330        ticks.sort_by(f64::total_cmp);
3331    }
3332    ticks.dedup_by(|left, right| *left == *right);
3333    if ticks.len() < 2 || !ticks.windows(2).all(|pair| pair[0] < pair[1]) {
3334        return Err(ChartError::InvalidValue {
3335            element: "chart axis scale".to_owned(),
3336            value: "scale cannot produce two increasing finite ticks".to_owned(),
3337        });
3338    }
3339    Ok(AxisScale {
3340        domain: Domain {
3341            min: minimum,
3342            max: maximum,
3343        },
3344        ticks,
3345    })
3346}
3347
3348const MIN_POSITIVE_SUBNORMAL: f64 = f64::from_bits(1);
3349
3350fn stable_positive_step(minimum: f64, maximum: f64, target_tick_count: usize) -> Result<f64> {
3351    let scale = minimum.abs().max(maximum.abs());
3352    let scale = if scale == 0.0 {
3353        MIN_POSITIVE_SUBNORMAL
3354    } else {
3355        scale
3356    };
3357    let normalized =
3358        (maximum / scale - minimum / scale) / target_tick_count.saturating_sub(1) as f64;
3359    let step = normalized * scale;
3360    if normalized.is_finite() && normalized > 0.0 && step.is_finite() {
3361        Ok(step.max(MIN_POSITIVE_SUBNORMAL))
3362    } else {
3363        Err(ChartError::InvalidValue {
3364            element: "chart axis scale".to_owned(),
3365            value: "scale step is nonfinite or empty".to_owned(),
3366        })
3367    }
3368}
3369
3370fn nice_step(raw_step: f64) -> Result<f64> {
3371    if !raw_step.is_finite() || raw_step <= 0.0 {
3372        return Err(ChartError::InvalidValue {
3373            element: "chart axis scale".to_owned(),
3374            value: format!("invalid raw tick step {raw_step}"),
3375        });
3376    }
3377    let magnitude = 10.0_f64
3378        .powf(raw_step.log10().floor())
3379        .max(MIN_POSITIVE_SUBNORMAL);
3380    let fraction = raw_step / magnitude;
3381    let multiplier = if fraction <= 1.0 {
3382        1.0
3383    } else if fraction <= 2.0 {
3384        2.0
3385    } else if fraction <= 5.0 {
3386        5.0
3387    } else {
3388        10.0
3389    };
3390    let step = (multiplier * magnitude).max(MIN_POSITIVE_SUBNORMAL);
3391    if step.is_finite() && step > 0.0 {
3392        Ok(step)
3393    } else {
3394        Err(ChartError::InvalidValue {
3395            element: "chart axis scale".to_owned(),
3396            value: "nice-number step is nonfinite".to_owned(),
3397        })
3398    }
3399}
3400
3401fn expand_constant_domain(value: f64) -> Result<Domain> {
3402    if value == 0.0 {
3403        return Ok(Domain {
3404            min: -1.0,
3405            max: 1.0,
3406        });
3407    }
3408    let padding = (value.abs() * 0.5).max(MIN_POSITIVE_SUBNORMAL);
3409    let minimum = value - padding;
3410    let maximum = value + padding;
3411    if minimum.is_finite() && maximum.is_finite() && minimum < maximum {
3412        return Ok(Domain {
3413            min: minimum,
3414            max: maximum,
3415        });
3416    }
3417    let domain = if value.is_sign_positive() {
3418        Domain {
3419            min: 0.0,
3420            max: value,
3421        }
3422    } else {
3423        Domain {
3424            min: value,
3425            max: 0.0,
3426        }
3427    };
3428    if domain.min < domain.max {
3429        Ok(domain)
3430    } else {
3431        Err(ChartError::InvalidValue {
3432            element: "chart axis scale".to_owned(),
3433            value: format!("constant value {value} has no finite renderable extent"),
3434        })
3435    }
3436}
3437
3438fn extend_bound(bound: f64, span: f64, positive: bool) -> Result<f64> {
3439    let candidate = if positive { bound + span } else { bound - span };
3440    let extended = if candidate.is_finite()
3441        && if positive {
3442            candidate > bound
3443        } else {
3444            candidate < bound
3445        } {
3446        candidate
3447    } else if positive {
3448        bound.next_up()
3449    } else {
3450        bound.next_down()
3451    };
3452    if extended.is_finite()
3453        && if positive {
3454            extended > bound
3455        } else {
3456            extended < bound
3457        }
3458    {
3459        Ok(extended)
3460    } else {
3461        Err(ChartError::InvalidValue {
3462            element: "c:scaling".to_owned(),
3463            value: format!("one-sided bound {bound} leaves no finite automatic extent"),
3464        })
3465    }
3466}
3467
3468fn clean_tick(value: f64, step: f64) -> f64 {
3469    if value.abs() <= step.abs() * 1.0e-12 {
3470        0.0
3471    } else {
3472        value
3473    }
3474}
3475
3476fn domain_from_layers(layers: &[GeometryLayer]) -> Result<Domain> {
3477    let values = layers
3478        .iter()
3479        .flatten()
3480        .flat_map(|(_, lower, upper)| [*lower, *upper]);
3481    let mut min = 0.0_f64;
3482    let mut max = 0.0_f64;
3483    for value in values {
3484        if !value.is_finite() {
3485            return Err(ChartError::InvalidValue {
3486                element: "chart geometry domain".to_owned(),
3487                value: "aggregate produced a nonfinite value".to_owned(),
3488            });
3489        }
3490        min = min.min(value);
3491        max = max.max(value);
3492    }
3493    if min == max {
3494        max = 1.0;
3495    }
3496    Ok(Domain { min, max })
3497}
3498
3499fn data_domain(values: &[f64], include_zero: bool) -> Result<Domain> {
3500    validate_finite(values, "chart geometry cache")?;
3501    let Some(first) = values.first().copied() else {
3502        return Err(ChartError::InvalidValue {
3503            element: "c:numCache".to_owned(),
3504            value: "cached series contain no points".to_owned(),
3505        });
3506    };
3507    let mut min = first;
3508    let mut max = first;
3509    for value in values.iter().copied() {
3510        min = min.min(value);
3511        max = max.max(value);
3512    }
3513    if include_zero {
3514        min = min.min(0.0);
3515        max = max.max(0.0);
3516    }
3517    if min == max {
3518        if !include_zero {
3519            return expand_constant_domain(min);
3520        }
3521        if min > 0.0 {
3522            min = 0.0;
3523        } else if max < 0.0 {
3524            max = 0.0;
3525        } else {
3526            max = 1.0;
3527        }
3528    }
3529    Ok(Domain { min, max })
3530}
3531
3532fn normalized_value(value: f64, domain: Domain) -> Result<f64> {
3533    let scale = value
3534        .abs()
3535        .max(domain.min.abs())
3536        .max(domain.max.abs())
3537        .max(1.0);
3538    let value = value / scale;
3539    let min = domain.min / scale;
3540    let max = domain.max / scale;
3541    let range = max - min;
3542    let normalized = (value - min) / range;
3543    if !range.is_finite() || range <= 0.0 || !normalized.is_finite() {
3544        return Err(ChartError::InvalidValue {
3545            element: "chart geometry domain".to_owned(),
3546            value: format!("cannot map finite domain {} to {}", domain.min, domain.max),
3547        });
3548    }
3549    Ok(normalized)
3550}
3551
3552fn normalized_value_oriented(value: f64, domain: Domain, orientation: Orientation) -> Result<f64> {
3553    let normalized = normalized_value(value, domain)?;
3554    Ok(if orientation == Orientation::MaxMin {
3555        1.0 - normalized
3556    } else {
3557        normalized
3558    })
3559}
3560
3561fn map_x_oriented(
3562    value: f64,
3563    domain: Domain,
3564    bounds: Rect,
3565    orientation: Orientation,
3566) -> Result<f64> {
3567    let coordinate =
3568        bounds.x + normalized_value_oriented(value, domain, orientation)? * bounds.width;
3569    if coordinate.is_finite() {
3570        Ok(coordinate)
3571    } else {
3572        Err(ChartError::InvalidValue {
3573            element: "chart geometry coordinate".to_owned(),
3574            value: "x coordinate is nonfinite".to_owned(),
3575        })
3576    }
3577}
3578
3579fn map_y_oriented(
3580    value: f64,
3581    domain: Domain,
3582    bounds: Rect,
3583    orientation: Orientation,
3584) -> Result<f64> {
3585    let coordinate = bounds.y + bounds.height
3586        - normalized_value_oriented(value, domain, orientation)? * bounds.height;
3587    if coordinate.is_finite() {
3588        Ok(coordinate)
3589    } else {
3590        Err(ChartError::InvalidValue {
3591            element: "chart geometry coordinate".to_owned(),
3592            value: "y coordinate is nonfinite".to_owned(),
3593        })
3594    }
3595}
3596
3597fn oriented_category_index(index: usize, count: usize, orientation: Orientation) -> usize {
3598    if orientation == Orientation::MaxMin && index < count {
3599        count - index - 1
3600    } else {
3601        index
3602    }
3603}
3604
3605fn category_points(
3606    values: &[(usize, f64)],
3607    count: usize,
3608    domain: Domain,
3609    bounds: Rect,
3610    category_orientation: Orientation,
3611    value_orientation: Orientation,
3612) -> Result<Vec<Point>> {
3613    values
3614        .iter()
3615        .map(|(index, value)| {
3616            let index = oriented_category_index(*index, count, category_orientation);
3617            let x = bounds.x + (index as f64 + 0.5) / count as f64 * bounds.width;
3618            let y = map_y_oriented(*value, domain, bounds, value_orientation)?;
3619            if !x.is_finite() {
3620                return Err(ChartError::InvalidValue {
3621                    element: "chart geometry coordinate".to_owned(),
3622                    value: "category x coordinate is nonfinite".to_owned(),
3623                });
3624            }
3625            Ok(Point { x, y })
3626        })
3627        .collect()
3628}
3629
3630fn polyline_path(points: &[Point], close: bool) -> Path {
3631    let mut commands = Vec::with_capacity(points.len() + usize::from(close));
3632    if let Some(first) = points.first() {
3633        commands.push(PathCommand::MoveTo(*first));
3634        commands.extend(points.iter().skip(1).copied().map(PathCommand::LineTo));
3635        if close {
3636            commands.push(PathCommand::Close);
3637        }
3638    }
3639    Path {
3640        commands,
3641        fill_rule: FillRule::NonZero,
3642    }
3643}
3644
3645fn pie_wedge(center: Point, radius: f64, start: f64, end: f64) -> Path {
3646    let first = circle_point(center, radius, start);
3647    let mut commands = vec![PathCommand::MoveTo(center), PathCommand::LineTo(first)];
3648    append_arc(&mut commands, center, radius, start, end);
3649    commands.push(PathCommand::Close);
3650    Path {
3651        commands,
3652        fill_rule: FillRule::NonZero,
3653    }
3654}
3655
3656fn doughnut_wedge(
3657    center: Point,
3658    outer_radius: f64,
3659    inner_radius: f64,
3660    start: f64,
3661    end: f64,
3662) -> Path {
3663    let outer_start = circle_point(center, outer_radius, start);
3664    let inner_end = circle_point(center, inner_radius, end);
3665    let mut commands = vec![PathCommand::MoveTo(outer_start)];
3666    append_arc(&mut commands, center, outer_radius, start, end);
3667    commands.push(PathCommand::LineTo(inner_end));
3668    append_arc(&mut commands, center, inner_radius, end, start);
3669    commands.push(PathCommand::Close);
3670    Path {
3671        commands,
3672        fill_rule: FillRule::NonZero,
3673    }
3674}
3675
3676fn append_arc(commands: &mut Vec<PathCommand>, center: Point, radius: f64, start: f64, end: f64) {
3677    let segments = ((end - start).abs() / std::f64::consts::FRAC_PI_2)
3678        .ceil()
3679        .max(1.0) as usize;
3680    let sweep = (end - start) / segments as f64;
3681    for segment in 0..segments {
3682        let angle = start + segment as f64 * sweep;
3683        let next = angle + sweep;
3684        let factor = 4.0 / 3.0 * (sweep / 4.0).tan();
3685        let from = circle_point(center, radius, angle);
3686        let to = circle_point(center, radius, next);
3687        commands.push(PathCommand::CurveTo {
3688            c1: Point {
3689                x: from.x - factor * radius * angle.sin(),
3690                y: from.y + factor * radius * angle.cos(),
3691            },
3692            c2: Point {
3693                x: to.x + factor * radius * next.sin(),
3694                y: to.y - factor * radius * next.cos(),
3695            },
3696            to,
3697        });
3698    }
3699}
3700
3701fn circle_point(center: Point, radius: f64, angle: f64) -> Point {
3702    Point {
3703        x: center.x + radius * angle.cos(),
3704        y: center.y + radius * angle.sin(),
3705    }
3706}
3707
3708fn radial_point(center: Point, radius: f64, index: usize, count: usize) -> Point {
3709    circle_point(
3710        center,
3711        radius,
3712        index as f64 * std::f64::consts::TAU / count as f64 - std::f64::consts::FRAC_PI_2,
3713    )
3714}
3715
3716fn marker_path(center: Point) -> Path {
3717    const RADIUS: f64 = 2.5;
3718    const KAPPA: f64 = 0.552_284_749_830_793_6;
3719    let control = RADIUS * KAPPA;
3720    Path {
3721        commands: vec![
3722            PathCommand::MoveTo(Point {
3723                x: center.x + RADIUS,
3724                y: center.y,
3725            }),
3726            PathCommand::CurveTo {
3727                c1: Point {
3728                    x: center.x + RADIUS,
3729                    y: center.y + control,
3730                },
3731                c2: Point {
3732                    x: center.x + control,
3733                    y: center.y + RADIUS,
3734                },
3735                to: Point {
3736                    x: center.x,
3737                    y: center.y + RADIUS,
3738                },
3739            },
3740            PathCommand::CurveTo {
3741                c1: Point {
3742                    x: center.x - control,
3743                    y: center.y + RADIUS,
3744                },
3745                c2: Point {
3746                    x: center.x - RADIUS,
3747                    y: center.y + control,
3748                },
3749                to: Point {
3750                    x: center.x - RADIUS,
3751                    y: center.y,
3752                },
3753            },
3754            PathCommand::CurveTo {
3755                c1: Point {
3756                    x: center.x - RADIUS,
3757                    y: center.y - control,
3758                },
3759                c2: Point {
3760                    x: center.x - control,
3761                    y: center.y - RADIUS,
3762                },
3763                to: Point {
3764                    x: center.x,
3765                    y: center.y - RADIUS,
3766                },
3767            },
3768            PathCommand::CurveTo {
3769                c1: Point {
3770                    x: center.x + control,
3771                    y: center.y - RADIUS,
3772                },
3773                c2: Point {
3774                    x: center.x + RADIUS,
3775                    y: center.y - control,
3776                },
3777                to: Point {
3778                    x: center.x + RADIUS,
3779                    y: center.y,
3780                },
3781            },
3782            PathCommand::Close,
3783        ],
3784        fill_rule: FillRule::NonZero,
3785    }
3786}
3787
3788fn push_markers(elements: &mut Vec<PositionedElement>, points: &[Point], colour: Color) {
3789    for point in points {
3790        elements.push(filled_path(marker_path(*point), colour));
3791    }
3792}
3793
3794fn series_colour(series_colours: &[Color], index: usize) -> Result<Color> {
3795    series_colours
3796        .get(index)
3797        .copied()
3798        .ok_or_else(|| ChartError::InvalidValue {
3799            element: "chart series palette".to_owned(),
3800            value: format!("missing resolved colour for series {index}"),
3801        })
3802}
3803
3804fn filled_path(path: Path, color: Color) -> PositionedElement {
3805    PositionedElement::Path(PathElement {
3806        path,
3807        fill: Some(Paint::Solid(color)),
3808        stroke: None,
3809    })
3810}
3811
3812fn stroked_path(path: Path, color: Color) -> PositionedElement {
3813    PositionedElement::Path(PathElement {
3814        path,
3815        fill: None,
3816        stroke: Some(Stroke::new(Paint::Solid(color), 1.5)),
3817    })
3818}
3819
3820fn filled_and_stroked_path(path: Path, color: Color) -> PositionedElement {
3821    PositionedElement::Path(PathElement {
3822        path,
3823        fill: Some(Paint::Solid(Color {
3824            a: color.a * 0.55,
3825            ..color
3826        })),
3827        stroke: Some(Stroke::new(Paint::Solid(color), 1.0)),
3828    })
3829}
3830
3831type NamespaceBindings = Vec<(Vec<u8>, String)>;
3832type XmlAttributes = Vec<(String, String)>;
3833type RootAttributes = (XmlAttributes, XmlAttributes);
3834
3835#[derive(Clone, Debug, Default, Eq, PartialEq)]
3836struct ScalarMarkup {
3837    raw_attributes: XmlAttributes,
3838    raw_content: Vec<Vec<u8>>,
3839}
3840
3841#[derive(Clone, Debug, Default, Eq, PartialEq)]
3842struct TextMarkup {
3843    raw_attributes: XmlAttributes,
3844    raw_children: OrderedRawChildren,
3845}
3846
3847#[derive(Clone, Debug, Default, Eq, PartialEq)]
3848struct PointMarkup {
3849    raw_attributes: XmlAttributes,
3850    raw_children: OrderedRawChildren,
3851    value: TextMarkup,
3852}
3853
3854#[derive(Clone, Debug, Default, Eq, PartialEq)]
3855struct ReferenceMarkup {
3856    raw_attributes: XmlAttributes,
3857    raw_children: OrderedRawChildren,
3858    formula: TextMarkup,
3859    cache_attributes: XmlAttributes,
3860    cache_children: OrderedRawChildren,
3861    format_code: Option<TextMarkup>,
3862    point_count: ScalarMarkup,
3863    declared_point_count: Option<u32>,
3864    point_indexes: Vec<u32>,
3865    points: Vec<PointMarkup>,
3866}
3867
3868#[derive(Clone, Debug, Default, Eq, PartialEq)]
3869struct WrapperMarkup {
3870    raw_attributes: XmlAttributes,
3871    raw_children: OrderedRawChildren,
3872}
3873
3874/// A formula-backed string cache whose count and point indexes are derived.
3875#[derive(Clone, Debug, PartialEq)]
3876pub struct StringRef {
3877    pub formula: String,
3878    pub values: Vec<String>,
3879    markup: ReferenceMarkup,
3880}
3881
3882impl StringRef {
3883    pub fn new(formula: String, values: Vec<String>) -> Result<Self> {
3884        let reference = Self {
3885            formula,
3886            values,
3887            markup: ReferenceMarkup::default(),
3888        };
3889        reference.validate()?;
3890        Ok(reference)
3891    }
3892
3893    pub fn from_xml(xml: &[u8]) -> Result<Self> {
3894        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
3895    }
3896
3897    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
3898        let parsed = parse_reference(xml, b"strRef", b"strCache", false, inherited)?;
3899        let values = parsed.values;
3900        let reference = Self {
3901            formula: parsed.formula,
3902            values,
3903            markup: parsed.markup,
3904        };
3905        reference.validate()?;
3906        Ok(reference)
3907    }
3908
3909    pub fn to_xml(&self) -> Result<Vec<u8>> {
3910        self.validate()?;
3911        let mut writer = Writer::new(Vec::new());
3912        self.write_xml(&mut writer, true)?;
3913        Ok(writer.into_inner())
3914    }
3915
3916    fn validate(&self) -> Result<()> {
3917        validate_formula(&self.formula, "c:strRef/c:f")?;
3918        for value in &self.values {
3919            validate_xml_text(value, "c:strCache/c:pt/c:v")?;
3920        }
3921        validate_point_count(self.values.len(), "c:strCache")
3922    }
3923
3924    fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
3925        self.validate()?;
3926        let mut start = BytesStart::new("c:strRef");
3927        if standalone {
3928            start.push_attribute(("xmlns:c", C_NS));
3929        }
3930        push_attributes(&mut start, &self.markup.raw_attributes);
3931        writer
3932            .write_event(Event::Start(start))
3933            .map_err(OxmlError::from)?;
3934        emit_raw(writer, self.markup.raw_children.at(0))?;
3935        write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
3936        emit_raw(writer, self.markup.raw_children.at(1))?;
3937        write_string_cache(writer, &self.values, &self.markup)?;
3938        emit_raw(writer, self.markup.raw_children.at(2))?;
3939        writer
3940            .write_event(Event::End(BytesEnd::new("c:strRef")))
3941            .map_err(OxmlError::from)?;
3942        Ok(())
3943    }
3944}
3945
3946/// A formula-backed numeric cache whose metadata and points are derived.
3947#[derive(Clone, Debug, PartialEq)]
3948pub struct NumericData {
3949    pub formula: String,
3950    pub format_code: String,
3951    pub values: Vec<f64>,
3952    markup: ReferenceMarkup,
3953}
3954
3955impl NumericData {
3956    pub fn new(formula: String, format_code: String, values: Vec<f64>) -> Result<Self> {
3957        validate_formula(&formula, "c:numRef/c:f")?;
3958        validate_format_code(&format_code)?;
3959        validate_numeric_values(&values)?;
3960        validate_point_count(values.len(), "c:numCache")?;
3961        Ok(Self {
3962            formula,
3963            format_code,
3964            values,
3965            markup: ReferenceMarkup::default(),
3966        })
3967    }
3968
3969    pub fn from_xml(xml: &[u8]) -> Result<Self> {
3970        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
3971    }
3972
3973    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
3974        let parsed = parse_reference(xml, b"numRef", b"numCache", true, inherited)?;
3975        let format_code = parsed
3976            .format_code
3977            .ok_or_else(|| ChartError::MissingElement("c:formatCode".to_owned()))?;
3978        let mut values = Vec::with_capacity(parsed.values.len());
3979        for value in parsed.values {
3980            let number = value.parse::<f64>().map_err(|_| ChartError::InvalidValue {
3981                element: "c:v".to_owned(),
3982                value: value.clone(),
3983            })?;
3984            if !number.is_finite() {
3985                return Err(ChartError::InvalidValue {
3986                    element: "c:v".to_owned(),
3987                    value,
3988                });
3989            }
3990            values.push(number);
3991        }
3992        let data = Self {
3993            formula: parsed.formula,
3994            format_code,
3995            values,
3996            markup: parsed.markup,
3997        };
3998        data.validate()?;
3999        Ok(data)
4000    }
4001
4002    pub fn to_xml(&self) -> Result<Vec<u8>> {
4003        self.validate()?;
4004        let mut writer = Writer::new(Vec::new());
4005        self.write_xml(&mut writer, true)?;
4006        Ok(writer.into_inner())
4007    }
4008
4009    fn validate(&self) -> Result<()> {
4010        validate_formula(&self.formula, "c:numRef/c:f")?;
4011        validate_format_code(&self.format_code)?;
4012        validate_numeric_values(&self.values)?;
4013        validate_point_count(self.values.len(), "c:numCache")
4014    }
4015
4016    fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
4017        self.validate()?;
4018        let mut start = BytesStart::new("c:numRef");
4019        if standalone {
4020            start.push_attribute(("xmlns:c", C_NS));
4021        }
4022        push_attributes(&mut start, &self.markup.raw_attributes);
4023        writer
4024            .write_event(Event::Start(start))
4025            .map_err(OxmlError::from)?;
4026        emit_raw(writer, self.markup.raw_children.at(0))?;
4027        write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
4028        emit_raw(writer, self.markup.raw_children.at(1))?;
4029        write_numeric_cache(writer, self, &self.markup)?;
4030        emit_raw(writer, self.markup.raw_children.at(2))?;
4031        writer
4032            .write_event(Event::End(BytesEnd::new("c:numRef")))
4033            .map_err(OxmlError::from)?;
4034        Ok(())
4035    }
4036}
4037
4038#[derive(Clone, Debug, PartialEq)]
4039pub enum AxisData {
4040    String(StringRef),
4041    Numeric(NumericData),
4042}
4043
4044/// The common formula-backed payload of one ChartML series.
4045#[derive(Clone, Debug, PartialEq)]
4046pub struct Series {
4047    pub index: u32,
4048    pub order: u32,
4049    pub name: Option<StringRef>,
4050    pub categories: Option<AxisData>,
4051    pub values: NumericData,
4052    pub bubble_size: Option<NumericData>,
4053    pub sp_pr: Option<CT_ShapeProperties>,
4054    pub data_labels: Option<CT_DLbls>,
4055    index_markup: ScalarMarkup,
4056    order_markup: ScalarMarkup,
4057    name_markup: Option<WrapperMarkup>,
4058    categories_markup: Option<WrapperMarkup>,
4059    values_markup: WrapperMarkup,
4060    bubble_size_markup: Option<WrapperMarkup>,
4061    opaque_name: bool,
4062    opaque_categories: bool,
4063    opaque_bubble_size: bool,
4064    uses_scatter_wrappers: bool,
4065    parsed_from_xml: bool,
4066    raw_attributes: XmlAttributes,
4067    namespace_declarations: XmlAttributes,
4068    raw_children: OrderedRawChildren,
4069}
4070
4071impl Series {
4072    pub fn new(index: u32, order: u32, values: NumericData) -> Self {
4073        Self {
4074            index,
4075            order,
4076            name: None,
4077            categories: None,
4078            values,
4079            bubble_size: None,
4080            sp_pr: None,
4081            data_labels: None,
4082            index_markup: ScalarMarkup::default(),
4083            order_markup: ScalarMarkup::default(),
4084            name_markup: None,
4085            categories_markup: None,
4086            values_markup: WrapperMarkup::default(),
4087            bubble_size_markup: None,
4088            opaque_name: false,
4089            opaque_categories: false,
4090            opaque_bubble_size: false,
4091            uses_scatter_wrappers: false,
4092            parsed_from_xml: false,
4093            raw_attributes: Vec::new(),
4094            namespace_declarations: Vec::new(),
4095            raw_children: OrderedRawChildren::default(),
4096        }
4097    }
4098
4099    pub fn from_xml(xml: &[u8]) -> Result<Self> {
4100        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
4101    }
4102
4103    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
4104        let mut reader = Reader::from_reader(xml);
4105        let mut buffer = Vec::new();
4106        loop {
4107            match reader
4108                .read_event_into(&mut buffer)
4109                .map_err(OxmlError::from)?
4110            {
4111                Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4112                    chart_root_prefix(&element)?;
4113                    if !element_is_in_namespace(&element, C_NS, inherited)? {
4114                        return Err(ChartError::UnexpectedElement(element_name(&element)));
4115                    }
4116                    return Self::from_element(&mut reader, &element, inherited);
4117                }
4118                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4119                    return Err(ChartError::MissingElement("c:idx".to_owned()));
4120                }
4121                Event::Start(element) | Event::Empty(element) => {
4122                    return Err(ChartError::UnexpectedElement(element_name(&element)));
4123                }
4124                Event::Eof => return Err(ChartError::MissingElement("c:ser".to_owned())),
4125                _ => {}
4126            }
4127            buffer.clear();
4128        }
4129    }
4130
4131    fn from_element(
4132        reader: &mut Reader<&[u8]>,
4133        start: &BytesStart<'_>,
4134        inherited: &NamespaceBindings,
4135    ) -> Result<Self> {
4136        reject_conflicting_prefix(start, b"a", A_NS)?;
4137        let namespaces = chart_bindings(inherited, start)?;
4138        require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
4139        require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
4140        require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
4141        let (raw_attributes, _) =
4142            capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
4143        let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
4144        let mut state = SeriesParseState::default();
4145        let mut buffer = Vec::new();
4146        loop {
4147            match reader
4148                .read_event_into(&mut buffer)
4149                .map_err(OxmlError::from)?
4150            {
4151                Event::Start(element) => {
4152                    let name = chart_child_local(&element, &namespaces)?;
4153                    let raw = capture_element(reader, &element)?;
4154                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
4155                }
4156                Event::Empty(element) => {
4157                    let name = chart_child_local(&element, &namespaces)?;
4158                    let raw = capture_empty_element(&element)?;
4159                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
4160                }
4161                event @ (Event::Text(_)
4162                | Event::CData(_)
4163                | Event::Comment(_)
4164                | Event::PI(_)
4165                | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
4166                Event::End(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4167                    break;
4168                }
4169                Event::Eof => return Err(missing_end("c:ser")),
4170                _ => {}
4171            }
4172            buffer.clear();
4173        }
4174        state.finish(raw_attributes, namespace_declarations)
4175    }
4176
4177    pub fn to_xml(&self) -> Result<Vec<u8>> {
4178        self.to_xml_for_plot(self.uses_scatter_wrappers)
4179    }
4180
4181    fn to_xml_for_plot(&self, scatter: bool) -> Result<Vec<u8>> {
4182        if self.opaque_name && self.name.is_some() {
4183            return Err(ChartError::DuplicateElement("c:tx".to_owned()));
4184        }
4185        if self.opaque_categories && self.categories.is_some() {
4186            return Err(ChartError::DuplicateElement("c:cat".to_owned()));
4187        }
4188        if self.opaque_bubble_size && self.bubble_size.is_some() {
4189            return Err(ChartError::DuplicateElement("c:bubbleSize".to_owned()));
4190        }
4191        self.values.validate()?;
4192        if let Some(name) = &self.name {
4193            name.validate()?;
4194        }
4195        if let Some(categories) = &self.categories {
4196            if scatter && !matches!(categories, AxisData::Numeric(_)) {
4197                return Err(ChartError::InvalidValue {
4198                    element: "c:xVal".to_owned(),
4199                    value: "scatter x values must use a numeric cache".to_owned(),
4200                });
4201            }
4202            match categories {
4203                AxisData::String(reference) => reference.validate()?,
4204                AxisData::Numeric(reference) => reference.validate()?,
4205            }
4206        }
4207        if scatter && self.categories.is_none() {
4208            return Err(ChartError::MissingElement("c:xVal".to_owned()));
4209        }
4210        if let Some(size) = &self.bubble_size {
4211            size.validate()?;
4212        }
4213        if let Some(labels) = &self.data_labels {
4214            labels.validate()?;
4215        }
4216        let mut writer = Writer::new(Vec::new());
4217        let mut start = BytesStart::new("c:ser");
4218        start.push_attribute(("xmlns:c", C_NS));
4219        start.push_attribute(("xmlns:a", A_NS));
4220        start.push_attribute(("xmlns:r", R_NS));
4221        push_attributes(&mut start, &self.namespace_declarations);
4222        push_attributes(&mut start, &self.raw_attributes);
4223        writer
4224            .write_event(Event::Start(start))
4225            .map_err(OxmlError::from)?;
4226        emit_raw(&mut writer, self.raw_children.at(0))?;
4227        write_scalar(
4228            &mut writer,
4229            "c:idx",
4230            &self.index.to_string(),
4231            Some(&self.index_markup),
4232        )?;
4233        emit_raw(&mut writer, self.raw_children.at(1))?;
4234        write_scalar(
4235            &mut writer,
4236            "c:order",
4237            &self.order.to_string(),
4238            Some(&self.order_markup),
4239        )?;
4240        emit_raw(&mut writer, self.raw_children.at(2))?;
4241        if let Some(name) = &self.name {
4242            write_wrapper_start(
4243                &mut writer,
4244                "c:tx",
4245                self.name_markup
4246                    .as_ref()
4247                    .unwrap_or(&WrapperMarkup::default()),
4248            )?;
4249            name.write_xml(&mut writer, false)?;
4250            write_wrapper_end(
4251                &mut writer,
4252                "c:tx",
4253                self.name_markup
4254                    .as_ref()
4255                    .unwrap_or(&WrapperMarkup::default()),
4256            )?;
4257        }
4258        emit_raw(&mut writer, self.raw_children.at(3))?;
4259        if let Some(properties) = &self.sp_pr {
4260            properties.write_xml_as(&mut writer, "c:spPr")?;
4261        }
4262        emit_raw(&mut writer, self.raw_children.at(4))?;
4263        if let Some(labels) = &self.data_labels {
4264            labels.write_xml(&mut writer, false)?;
4265        }
4266        emit_raw(&mut writer, self.raw_children.at(5))?;
4267        if let Some(categories) = &self.categories {
4268            let default_markup = WrapperMarkup::default();
4269            let markup = self.categories_markup.as_ref().unwrap_or(&default_markup);
4270            write_wrapper_start(
4271                &mut writer,
4272                if scatter { "c:xVal" } else { "c:cat" },
4273                markup,
4274            )?;
4275            match categories {
4276                AxisData::String(reference) => reference.write_xml(&mut writer, false)?,
4277                AxisData::Numeric(reference) => reference.write_xml(&mut writer, false)?,
4278            }
4279            write_wrapper_end(
4280                &mut writer,
4281                if scatter { "c:xVal" } else { "c:cat" },
4282                markup,
4283            )?;
4284        }
4285        emit_raw(&mut writer, self.raw_children.at(6))?;
4286        write_wrapper_start(
4287            &mut writer,
4288            if scatter { "c:yVal" } else { "c:val" },
4289            &self.values_markup,
4290        )?;
4291        self.values.write_xml(&mut writer, false)?;
4292        write_wrapper_end(
4293            &mut writer,
4294            if scatter { "c:yVal" } else { "c:val" },
4295            &self.values_markup,
4296        )?;
4297        emit_raw(&mut writer, self.raw_children.at(7))?;
4298        if let Some(size) = &self.bubble_size {
4299            let default_markup = WrapperMarkup::default();
4300            let markup = self.bubble_size_markup.as_ref().unwrap_or(&default_markup);
4301            write_wrapper_start(&mut writer, "c:bubbleSize", markup)?;
4302            size.write_xml(&mut writer, false)?;
4303            write_wrapper_end(&mut writer, "c:bubbleSize", markup)?;
4304        }
4305        emit_raw(&mut writer, self.raw_children.at(8))?;
4306        writer
4307            .write_event(Event::End(BytesEnd::new("c:ser")))
4308            .map_err(OxmlError::from)?;
4309        Ok(writer.into_inner())
4310    }
4311
4312    pub fn raw_children(&self) -> &OrderedRawChildren {
4313        &self.raw_children
4314    }
4315}
4316
4317#[derive(Default)]
4318struct SeriesParseState {
4319    index: Option<(u32, ScalarMarkup)>,
4320    order: Option<(u32, ScalarMarkup)>,
4321    name: Option<(StringRef, WrapperMarkup)>,
4322    categories: Option<(AxisData, WrapperMarkup)>,
4323    values: Option<(NumericData, WrapperMarkup)>,
4324    bubble_size: Option<(NumericData, WrapperMarkup)>,
4325    data_labels: Option<CT_DLbls>,
4326    name_seen: bool,
4327    categories_seen: bool,
4328    bubble_size_seen: bool,
4329    opaque_name: bool,
4330    opaque_categories: bool,
4331    opaque_bubble_size: bool,
4332    saw_standard_wrapper: bool,
4333    saw_scatter_wrapper: bool,
4334    sp_pr: Option<CT_ShapeProperties>,
4335    raw_children: OrderedRawChildren,
4336    boundary: usize,
4337}
4338
4339impl SeriesParseState {
4340    fn capture_event(&mut self, raw: Vec<u8>) {
4341        self.raw_children.push(self.boundary, raw);
4342    }
4343
4344    fn parse_child(
4345        &mut self,
4346        name: &[u8],
4347        raw: Vec<u8>,
4348        namespaces: &NamespaceBindings,
4349    ) -> Result<()> {
4350        match name {
4351            b"idx" => {
4352                set_once(&mut self.index, parse_u32_scalar(&raw, "idx")?, "c:idx")?;
4353                self.boundary = self.boundary.max(1);
4354            }
4355            b"order" => {
4356                set_once(&mut self.order, parse_u32_scalar(&raw, "order")?, "c:order")?;
4357                self.boundary = self.boundary.max(2);
4358            }
4359            b"tx" => {
4360                mark_once(&mut self.name_seen, "c:tx")?;
4361                let parsed = parse_wrapper(&raw, b"tx", &[b"strRef"], namespaces)?;
4362                if let Some((_, reference)) = parsed.choice {
4363                    self.name = Some((
4364                        StringRef::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4365                        parsed.markup,
4366                    ));
4367                } else {
4368                    self.opaque_name = true;
4369                    self.raw_children.push(2, raw);
4370                }
4371                self.boundary = self.boundary.max(3);
4372            }
4373            b"spPr" => {
4374                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
4375                let properties = CT_ShapeProperties::from_xml(&raw)?;
4376                let mut writer = Writer::new(Vec::new());
4377                properties.write_xml_as(&mut writer, "c:spPr")?;
4378                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
4379                set_once(&mut self.sp_pr, properties, "c:spPr")?;
4380                self.boundary = self.boundary.max(4);
4381            }
4382            b"dLbls" => {
4383                set_once(
4384                    &mut self.data_labels,
4385                    CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
4386                    "c:dLbls",
4387                )?;
4388                self.boundary = self.boundary.max(5);
4389            }
4390            b"cat" => {
4391                self.saw_standard_wrapper = true;
4392                mark_once(&mut self.categories_seen, "c:cat")?;
4393                let parsed = parse_wrapper(&raw, b"cat", &[b"strRef", b"numRef"], namespaces)?;
4394                if let Some((choice, reference)) = parsed.choice {
4395                    let categories = if choice == b"strRef" {
4396                        AxisData::String(StringRef::from_xml_with_namespaces(
4397                            &reference,
4398                            &parsed.namespaces,
4399                        )?)
4400                    } else {
4401                        AxisData::Numeric(NumericData::from_xml_with_namespaces(
4402                            &reference,
4403                            &parsed.namespaces,
4404                        )?)
4405                    };
4406                    self.categories = Some((categories, parsed.markup));
4407                } else {
4408                    self.opaque_categories = true;
4409                    self.raw_children.push(5, raw);
4410                }
4411                self.boundary = self.boundary.max(6);
4412            }
4413            b"xVal" => {
4414                self.saw_scatter_wrapper = true;
4415                mark_once(&mut self.categories_seen, "c:xVal")?;
4416                let parsed = parse_wrapper(&raw, b"xVal", &[b"numRef"], namespaces)?;
4417                let (_, reference) = parsed
4418                    .choice
4419                    .ok_or_else(|| ChartError::MissingElement("c:xVal/c:numRef".to_owned()))?;
4420                self.categories = Some((
4421                    AxisData::Numeric(NumericData::from_xml_with_namespaces(
4422                        &reference,
4423                        &parsed.namespaces,
4424                    )?),
4425                    parsed.markup,
4426                ));
4427                self.boundary = self.boundary.max(6);
4428            }
4429            b"val" | b"yVal" => {
4430                if name == b"yVal" {
4431                    self.saw_scatter_wrapper = true;
4432                } else {
4433                    self.saw_standard_wrapper = true;
4434                }
4435                let wrapper = if name == b"yVal" {
4436                    b"yVal".as_slice()
4437                } else {
4438                    b"val".as_slice()
4439                };
4440                let parsed = parse_wrapper(&raw, wrapper, &[b"numRef"], namespaces)?;
4441                let (_, reference) = parsed.choice.ok_or_else(|| {
4442                    ChartError::MissingElement(format!(
4443                        "c:{}/c:numRef",
4444                        String::from_utf8_lossy(wrapper)
4445                    ))
4446                })?;
4447                set_once(
4448                    &mut self.values,
4449                    (
4450                        NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4451                        parsed.markup,
4452                    ),
4453                    "c:val or c:yVal",
4454                )?;
4455                self.boundary = self.boundary.max(7);
4456            }
4457            b"bubbleSize" => {
4458                mark_once(&mut self.bubble_size_seen, "c:bubbleSize")?;
4459                let parsed = parse_wrapper(&raw, b"bubbleSize", &[b"numRef"], namespaces)?;
4460                if let Some((_, reference)) = parsed.choice {
4461                    self.bubble_size = Some((
4462                        NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4463                        parsed.markup,
4464                    ));
4465                } else {
4466                    self.opaque_bubble_size = true;
4467                    self.raw_children.push(7, raw);
4468                }
4469                self.boundary = self.boundary.max(8);
4470            }
4471            _ => {
4472                let boundary = series_raw_boundary(name, self.boundary);
4473                self.raw_children.push(boundary, raw);
4474                self.boundary = self.boundary.max(boundary);
4475            }
4476        }
4477        Ok(())
4478    }
4479
4480    fn finish(
4481        self,
4482        raw_attributes: XmlAttributes,
4483        namespace_declarations: XmlAttributes,
4484    ) -> Result<Series> {
4485        if self.saw_standard_wrapper && self.saw_scatter_wrapper {
4486            return Err(ChartError::InvalidValue {
4487                element: "c:ser".to_owned(),
4488                value: "category/value and x/y wrappers cannot be combined".to_owned(),
4489            });
4490        }
4491        let (index, index_markup) = self
4492            .index
4493            .ok_or_else(|| ChartError::MissingElement("c:idx".to_owned()))?;
4494        let (order, order_markup) = self
4495            .order
4496            .ok_or_else(|| ChartError::MissingElement("c:order".to_owned()))?;
4497        let (values, values_markup) = self
4498            .values
4499            .ok_or_else(|| ChartError::MissingElement("c:val".to_owned()))?;
4500        let (name, name_markup) = self
4501            .name
4502            .map(|(value, markup)| (Some(value), Some(markup)))
4503            .unwrap_or((None, None));
4504        let (categories, categories_markup) = self
4505            .categories
4506            .map(|(value, markup)| (Some(value), Some(markup)))
4507            .unwrap_or((None, None));
4508        let (bubble_size, bubble_size_markup) = self
4509            .bubble_size
4510            .map(|(value, markup)| (Some(value), Some(markup)))
4511            .unwrap_or((None, None));
4512        Ok(Series {
4513            index,
4514            order,
4515            name,
4516            categories,
4517            values,
4518            bubble_size,
4519            sp_pr: self.sp_pr,
4520            data_labels: self.data_labels,
4521            index_markup,
4522            order_markup,
4523            name_markup,
4524            categories_markup,
4525            values_markup,
4526            bubble_size_markup,
4527            opaque_name: self.opaque_name,
4528            opaque_categories: self.opaque_categories,
4529            opaque_bubble_size: self.opaque_bubble_size,
4530            uses_scatter_wrappers: self.saw_scatter_wrapper,
4531            parsed_from_xml: true,
4532            raw_attributes,
4533            namespace_declarations,
4534            raw_children: raw_children_in_schema_order(&self.raw_children, 8),
4535        })
4536    }
4537}
4538
4539fn series_raw_boundary(name: &[u8], current: usize) -> usize {
4540    match name {
4541        b"marker" | b"invertIfNegative" | b"pictureOptions" | b"explosion" | b"dPt" => 4,
4542        b"trendline" | b"errBars" => 5,
4543        b"shape" | b"smooth" => 7,
4544        b"extLst" => 8,
4545        _ => current,
4546    }
4547}
4548
4549struct ParsedReference {
4550    formula: String,
4551    format_code: Option<String>,
4552    values: Vec<String>,
4553    markup: ReferenceMarkup,
4554}
4555
4556fn parse_reference(
4557    xml: &[u8],
4558    reference_local: &[u8],
4559    cache_local: &[u8],
4560    numeric: bool,
4561    inherited: &NamespaceBindings,
4562) -> Result<ParsedReference> {
4563    let mut reader = Reader::from_reader(xml);
4564    let mut buffer = Vec::new();
4565    loop {
4566        match reader
4567            .read_event_into(&mut buffer)
4568            .map_err(OxmlError::from)?
4569        {
4570            Event::Start(element)
4571                if matches_local_name(element.name().as_ref(), reference_local) =>
4572            {
4573                let namespaces = typed_rewrite_bindings(&element, inherited)?;
4574                let raw_attributes = capture_fixed_attributes(&element, &["xmlns:c"])?;
4575                let mut formula: Option<(String, TextMarkup)> = None;
4576                let mut cache: Option<(Option<String>, Vec<String>, CacheMarkup)> = None;
4577                let mut raw_children = OrderedRawChildren::default();
4578                let mut boundary = 0usize;
4579                let mut inner = Vec::new();
4580                loop {
4581                    match reader
4582                        .read_event_into(&mut inner)
4583                        .map_err(OxmlError::from)?
4584                    {
4585                        Event::Start(child) => {
4586                            let name = chart_child_local(&child, &namespaces)?;
4587                            let raw = capture_element(&mut reader, &child)?;
4588                            match name.as_deref().unwrap_or_default() {
4589                                b"f" => {
4590                                    set_once(
4591                                        &mut formula,
4592                                        parse_text_element(&raw, b"f", &namespaces)?,
4593                                        "c:f",
4594                                    )?;
4595                                    boundary = boundary.max(1);
4596                                }
4597                                name if name == cache_local => {
4598                                    set_once(
4599                                        &mut cache,
4600                                        parse_cache(&raw, cache_local, numeric, &namespaces)?,
4601                                        &format!("c:{}", String::from_utf8_lossy(cache_local)),
4602                                    )?;
4603                                    boundary = boundary.max(2);
4604                                }
4605                                _ => raw_children.push(boundary, raw),
4606                            }
4607                        }
4608                        Event::Empty(child) => {
4609                            let name = chart_child_local(&child, &namespaces)?;
4610                            let raw = capture_empty_element(&child)?;
4611                            match name.as_deref().unwrap_or_default() {
4612                                b"f" => {
4613                                    set_once(
4614                                        &mut formula,
4615                                        parse_text_element(&raw, b"f", &namespaces)?,
4616                                        "c:f",
4617                                    )?;
4618                                    boundary = boundary.max(1);
4619                                }
4620                                name if name == cache_local => {
4621                                    set_once(
4622                                        &mut cache,
4623                                        parse_cache(&raw, cache_local, numeric, &namespaces)?,
4624                                        &format!("c:{}", String::from_utf8_lossy(cache_local)),
4625                                    )?;
4626                                    boundary = boundary.max(2);
4627                                }
4628                                _ => raw_children.push(boundary, raw),
4629                            }
4630                        }
4631                        event @ (Event::Text(_)
4632                        | Event::CData(_)
4633                        | Event::Comment(_)
4634                        | Event::PI(_)
4635                        | Event::GeneralRef(_)) => {
4636                            raw_children.push(boundary, capture_event(event)?);
4637                        }
4638                        Event::End(end)
4639                            if matches_local_name(end.name().as_ref(), reference_local) =>
4640                        {
4641                            let (formula, formula_markup) = formula
4642                                .ok_or_else(|| ChartError::MissingElement("c:f".to_owned()))?;
4643                            validate_formula(&formula, "c:f")?;
4644                            let (format_code, values, cache_markup) = cache.ok_or_else(|| {
4645                                ChartError::MissingElement(format!(
4646                                    "c:{}",
4647                                    String::from_utf8_lossy(cache_local)
4648                                ))
4649                            })?;
4650                            let markup = ReferenceMarkup {
4651                                raw_attributes,
4652                                raw_children: raw_children_in_schema_order(&raw_children, 2),
4653                                formula: formula_markup,
4654                                cache_attributes: cache_markup.raw_attributes,
4655                                cache_children: cache_markup.raw_children,
4656                                format_code: cache_markup.format_code,
4657                                point_count: cache_markup.point_count,
4658                                declared_point_count: cache_markup.declared_point_count,
4659                                point_indexes: cache_markup.point_indexes,
4660                                points: cache_markup.points,
4661                            };
4662                            return Ok(ParsedReference {
4663                                formula,
4664                                format_code,
4665                                values,
4666                                markup,
4667                            });
4668                        }
4669                        Event::Eof => {
4670                            return Err(missing_end(&format!(
4671                                "c:{}",
4672                                String::from_utf8_lossy(reference_local)
4673                            )));
4674                        }
4675                        _ => {}
4676                    }
4677                    inner.clear();
4678                }
4679            }
4680            Event::Empty(element)
4681                if matches_local_name(element.name().as_ref(), reference_local) =>
4682            {
4683                return Err(ChartError::MissingElement("c:f".to_owned()));
4684            }
4685            Event::Start(element) | Event::Empty(element) => {
4686                return Err(ChartError::UnexpectedElement(element_name(&element)));
4687            }
4688            Event::Eof => {
4689                return Err(ChartError::MissingElement(format!(
4690                    "c:{}",
4691                    String::from_utf8_lossy(reference_local)
4692                )));
4693            }
4694            _ => {}
4695        }
4696        buffer.clear();
4697    }
4698}
4699
4700struct CacheMarkup {
4701    raw_attributes: XmlAttributes,
4702    raw_children: OrderedRawChildren,
4703    format_code: Option<TextMarkup>,
4704    point_count: ScalarMarkup,
4705    declared_point_count: Option<u32>,
4706    point_indexes: Vec<u32>,
4707    points: Vec<PointMarkup>,
4708}
4709
4710fn parse_cache(
4711    xml: &[u8],
4712    cache_local: &[u8],
4713    numeric: bool,
4714    inherited: &NamespaceBindings,
4715) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
4716    let mut reader = Reader::from_reader(xml);
4717    let mut buffer = Vec::new();
4718    loop {
4719        match reader
4720            .read_event_into(&mut buffer)
4721            .map_err(OxmlError::from)?
4722        {
4723            Event::Start(element) if matches_local_name(element.name().as_ref(), cache_local) => {
4724                let namespaces = typed_rewrite_bindings(&element, inherited)?;
4725                let raw_attributes = capture_attributes(&element)?;
4726                let mut state = CacheParseState::new(numeric, namespaces);
4727                let mut inner = Vec::new();
4728                loop {
4729                    match reader
4730                        .read_event_into(&mut inner)
4731                        .map_err(OxmlError::from)?
4732                    {
4733                        Event::Start(child) => {
4734                            let name = chart_child_local(&child, &state.namespaces)?;
4735                            let raw = capture_element(&mut reader, &child)?;
4736                            state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
4737                        }
4738                        Event::Empty(child) => {
4739                            let name = chart_child_local(&child, &state.namespaces)?;
4740                            let raw = capture_empty_element(&child)?;
4741                            state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
4742                        }
4743                        event @ (Event::Text(_)
4744                        | Event::CData(_)
4745                        | Event::Comment(_)
4746                        | Event::PI(_)
4747                        | Event::GeneralRef(_)) => {
4748                            state.capture_event(capture_event(event)?);
4749                        }
4750                        Event::End(end) if matches_local_name(end.name().as_ref(), cache_local) => {
4751                            return state.finish(raw_attributes);
4752                        }
4753                        Event::Eof => {
4754                            return Err(missing_end(&format!(
4755                                "c:{}",
4756                                String::from_utf8_lossy(cache_local)
4757                            )));
4758                        }
4759                        _ => {}
4760                    }
4761                    inner.clear();
4762                }
4763            }
4764            Event::Empty(element) if matches_local_name(element.name().as_ref(), cache_local) => {
4765                return Err(ChartError::MissingElement("c:ptCount".to_owned()));
4766            }
4767            Event::Start(element) | Event::Empty(element) => {
4768                return Err(ChartError::UnexpectedElement(element_name(&element)));
4769            }
4770            Event::Eof => {
4771                return Err(ChartError::MissingElement(format!(
4772                    "c:{}",
4773                    String::from_utf8_lossy(cache_local)
4774                )));
4775            }
4776            _ => {}
4777        }
4778        buffer.clear();
4779    }
4780}
4781
4782struct CacheParseState {
4783    numeric: bool,
4784    namespaces: NamespaceBindings,
4785    point_base: usize,
4786    boundary: usize,
4787    format_code: Option<(String, TextMarkup)>,
4788    point_count: Option<(u32, ScalarMarkup)>,
4789    points: Vec<(u32, String, PointMarkup)>,
4790    raw_children: OrderedRawChildren,
4791}
4792
4793impl CacheParseState {
4794    fn new(numeric: bool, namespaces: NamespaceBindings) -> Self {
4795        Self {
4796            numeric,
4797            namespaces,
4798            point_base: usize::from(numeric) + 1,
4799            boundary: 0,
4800            format_code: None,
4801            point_count: None,
4802            points: Vec::new(),
4803            raw_children: OrderedRawChildren::default(),
4804        }
4805    }
4806
4807    fn capture_event(&mut self, raw: Vec<u8>) {
4808        self.raw_children.push(self.boundary, raw);
4809    }
4810
4811    fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
4812        match name {
4813            b"formatCode" if self.numeric => {
4814                set_once(
4815                    &mut self.format_code,
4816                    parse_text_element(&raw, b"formatCode", &self.namespaces)?,
4817                    "c:formatCode",
4818                )?;
4819                self.boundary = self.boundary.max(1);
4820            }
4821            b"ptCount" => {
4822                set_once(
4823                    &mut self.point_count,
4824                    parse_u32_scalar(&raw, "ptCount")?,
4825                    "c:ptCount",
4826                )?;
4827                self.boundary = self.boundary.max(self.point_base);
4828            }
4829            b"pt" => {
4830                self.points.push(parse_point(&raw, &self.namespaces)?);
4831                self.boundary = self.boundary.max(self.point_base + self.points.len());
4832            }
4833            _ => self.raw_children.push(self.boundary, raw),
4834        }
4835        Ok(())
4836    }
4837
4838    fn finish(
4839        self,
4840        raw_attributes: XmlAttributes,
4841    ) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
4842        let (declared, point_count_markup) = self
4843            .point_count
4844            .ok_or_else(|| ChartError::MissingElement("c:ptCount".to_owned()))?;
4845        let actual = u32::try_from(self.points.len()).map_err(|_| ChartError::InvalidValue {
4846            element: "c:ptCount".to_owned(),
4847            value: self.points.len().to_string(),
4848        })?;
4849        if declared < actual {
4850            return Err(invalid_attribute("ptCount", "val", declared.to_string()));
4851        }
4852        let mut values = Vec::with_capacity(self.points.len());
4853        let mut point_indexes = Vec::with_capacity(self.points.len());
4854        let mut point_markup = Vec::with_capacity(self.points.len());
4855        let mut previous = None;
4856        for (index, value, markup) in self.points {
4857            if index >= declared || previous.is_some_and(|last| index <= last) {
4858                return Err(invalid_attribute("pt", "idx", index.to_string()));
4859            }
4860            previous = Some(index);
4861            values.push(value);
4862            point_indexes.push(index);
4863            point_markup.push(markup);
4864        }
4865        let (format_code, format_markup) = self
4866            .format_code
4867            .map(|(value, markup)| (Some(value), Some(markup)))
4868            .unwrap_or((None, None));
4869        if self.numeric && format_code.is_none() {
4870            return Err(ChartError::MissingElement("c:formatCode".to_owned()));
4871        }
4872        Ok((
4873            format_code,
4874            values,
4875            CacheMarkup {
4876                raw_attributes,
4877                raw_children: raw_children_in_schema_order(
4878                    &self.raw_children,
4879                    self.point_base + point_markup.len(),
4880                ),
4881                format_code: format_markup,
4882                point_count: point_count_markup,
4883                declared_point_count: Some(declared),
4884                point_indexes,
4885                points: point_markup,
4886            },
4887        ))
4888    }
4889}
4890
4891fn parse_point(xml: &[u8], inherited: &NamespaceBindings) -> Result<(u32, String, PointMarkup)> {
4892    let mut reader = Reader::from_reader(xml);
4893    let mut buffer = Vec::new();
4894    loop {
4895        match reader
4896            .read_event_into(&mut buffer)
4897            .map_err(OxmlError::from)?
4898        {
4899            Event::Start(element) if matches_local_name(element.name().as_ref(), b"pt") => {
4900                let namespaces = typed_rewrite_bindings(&element, inherited)?;
4901                let index = required_u32_attribute(&element, "pt", b"idx")?;
4902                let raw_attributes = capture_attributes_except(&element, b"idx")?;
4903                let mut value: Option<(String, TextMarkup)> = None;
4904                let mut raw_children = OrderedRawChildren::default();
4905                let mut boundary = 0usize;
4906                let mut inner = Vec::new();
4907                loop {
4908                    match reader
4909                        .read_event_into(&mut inner)
4910                        .map_err(OxmlError::from)?
4911                    {
4912                        Event::Start(child) => {
4913                            let name = chart_child_local(&child, &namespaces)?;
4914                            let raw = capture_element(&mut reader, &child)?;
4915                            if name.as_deref() == Some(b"v") {
4916                                set_once(
4917                                    &mut value,
4918                                    parse_text_element(&raw, b"v", &namespaces)?,
4919                                    "c:v",
4920                                )?;
4921                                boundary = 1;
4922                            } else {
4923                                raw_children.push(boundary, raw);
4924                            }
4925                        }
4926                        Event::Empty(child) => {
4927                            let name = chart_child_local(&child, &namespaces)?;
4928                            let raw = capture_empty_element(&child)?;
4929                            if name.as_deref() == Some(b"v") {
4930                                set_once(
4931                                    &mut value,
4932                                    parse_text_element(&raw, b"v", &namespaces)?,
4933                                    "c:v",
4934                                )?;
4935                                boundary = 1;
4936                            } else {
4937                                raw_children.push(boundary, raw);
4938                            }
4939                        }
4940                        event @ (Event::Text(_)
4941                        | Event::CData(_)
4942                        | Event::Comment(_)
4943                        | Event::PI(_)
4944                        | Event::GeneralRef(_)) => {
4945                            raw_children.push(boundary, capture_event(event)?);
4946                        }
4947                        Event::End(end) if matches_local_name(end.name().as_ref(), b"pt") => {
4948                            let (value, value_markup) = value
4949                                .ok_or_else(|| ChartError::MissingElement("c:v".to_owned()))?;
4950                            return Ok((
4951                                index,
4952                                value,
4953                                PointMarkup {
4954                                    raw_attributes,
4955                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
4956                                    value: value_markup,
4957                                },
4958                            ));
4959                        }
4960                        Event::Eof => return Err(missing_end("c:pt")),
4961                        _ => {}
4962                    }
4963                    inner.clear();
4964                }
4965            }
4966            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pt") => {
4967                return Err(ChartError::MissingElement("c:v".to_owned()));
4968            }
4969            Event::Start(element) | Event::Empty(element) => {
4970                return Err(ChartError::UnexpectedElement(element_name(&element)));
4971            }
4972            Event::Eof => return Err(ChartError::MissingElement("c:pt".to_owned())),
4973            _ => {}
4974        }
4975        buffer.clear();
4976    }
4977}
4978
4979fn parse_text_element(
4980    xml: &[u8],
4981    local: &[u8],
4982    inherited: &NamespaceBindings,
4983) -> Result<(String, TextMarkup)> {
4984    let mut reader = Reader::from_reader(xml);
4985    let mut buffer = Vec::new();
4986    loop {
4987        match reader
4988            .read_event_into(&mut buffer)
4989            .map_err(OxmlError::from)?
4990        {
4991            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
4992                typed_rewrite_bindings(&element, inherited)?;
4993                let raw_attributes = capture_attributes(&element)?;
4994                let mut value = String::new();
4995                let mut raw_children = OrderedRawChildren::default();
4996                let mut seen_text = false;
4997                let mut inner = Vec::new();
4998                loop {
4999                    match reader
5000                        .read_event_into(&mut inner)
5001                        .map_err(OxmlError::from)?
5002                    {
5003                        Event::Text(text) => {
5004                            value.push_str(&decode_plain(&text));
5005                            seen_text = true;
5006                        }
5007                        Event::CData(text) => {
5008                            let decoded =
5009                                text.decode().map_err(|error| ChartError::InvalidValue {
5010                                    element: format!("c:{}", String::from_utf8_lossy(local)),
5011                                    value: error.to_string(),
5012                                })?;
5013                            value.push_str(&decoded);
5014                            seen_text = true;
5015                        }
5016                        Event::GeneralRef(reference) => {
5017                            value.push_str(&resolve_entity(&reference));
5018                            seen_text = true;
5019                        }
5020                        event @ (Event::Comment(_) | Event::PI(_)) => {
5021                            raw_children.push(usize::from(seen_text), capture_event(event)?);
5022                        }
5023                        Event::Start(child) | Event::Empty(child) => {
5024                            return Err(ChartError::UnexpectedElement(element_name(&child)));
5025                        }
5026                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
5027                            return Ok((
5028                                value,
5029                                TextMarkup {
5030                                    raw_attributes,
5031                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
5032                                },
5033                            ));
5034                        }
5035                        Event::Eof => {
5036                            return Err(missing_end(&format!(
5037                                "c:{}",
5038                                String::from_utf8_lossy(local)
5039                            )));
5040                        }
5041                        _ => {}
5042                    }
5043                    inner.clear();
5044                }
5045            }
5046            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
5047                typed_rewrite_bindings(&element, inherited)?;
5048                return Ok((
5049                    String::new(),
5050                    TextMarkup {
5051                        raw_attributes: capture_attributes(&element)?,
5052                        raw_children: OrderedRawChildren::default(),
5053                    },
5054                ));
5055            }
5056            Event::Start(element) | Event::Empty(element) => {
5057                return Err(ChartError::UnexpectedElement(element_name(&element)));
5058            }
5059            Event::Eof => {
5060                return Err(ChartError::MissingElement(format!(
5061                    "c:{}",
5062                    String::from_utf8_lossy(local)
5063                )));
5064            }
5065            _ => {}
5066        }
5067        buffer.clear();
5068    }
5069}
5070
5071struct ParsedWrapper {
5072    choice: Option<(Vec<u8>, Vec<u8>)>,
5073    markup: WrapperMarkup,
5074    namespaces: NamespaceBindings,
5075}
5076
5077fn parse_wrapper(
5078    xml: &[u8],
5079    local: &[u8],
5080    choices: &[&[u8]],
5081    inherited: &NamespaceBindings,
5082) -> Result<ParsedWrapper> {
5083    let mut reader = Reader::from_reader(xml);
5084    let mut buffer = Vec::new();
5085    loop {
5086        match reader
5087            .read_event_into(&mut buffer)
5088            .map_err(OxmlError::from)?
5089        {
5090            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
5091                let namespaces = typed_rewrite_bindings(&element, inherited)?;
5092                let raw_attributes = capture_attributes(&element)?;
5093                let mut choice = None;
5094                let mut raw_children = OrderedRawChildren::default();
5095                let mut boundary = 0usize;
5096                let mut inner = Vec::new();
5097                loop {
5098                    match reader
5099                        .read_event_into(&mut inner)
5100                        .map_err(OxmlError::from)?
5101                    {
5102                        Event::Start(child) => {
5103                            let name = chart_child_local(&child, &namespaces)?;
5104                            let raw = capture_element(&mut reader, &child)?;
5105                            if let Some(name) =
5106                                name.filter(|name| choices.contains(&name.as_slice()))
5107                            {
5108                                if choice.is_some() {
5109                                    return Err(ChartError::DuplicateElement(format!(
5110                                        "c:{} reference",
5111                                        String::from_utf8_lossy(local)
5112                                    )));
5113                                }
5114                                choice = Some((name, raw));
5115                                boundary = 1;
5116                            } else {
5117                                raw_children.push(boundary, raw);
5118                            }
5119                        }
5120                        Event::Empty(child) => {
5121                            let name = chart_child_local(&child, &namespaces)?;
5122                            let raw = capture_empty_element(&child)?;
5123                            if let Some(name) =
5124                                name.filter(|name| choices.contains(&name.as_slice()))
5125                            {
5126                                if choice.is_some() {
5127                                    return Err(ChartError::DuplicateElement(format!(
5128                                        "c:{} reference",
5129                                        String::from_utf8_lossy(local)
5130                                    )));
5131                                }
5132                                choice = Some((name, raw));
5133                                boundary = 1;
5134                            } else {
5135                                raw_children.push(boundary, raw);
5136                            }
5137                        }
5138                        event @ (Event::Text(_)
5139                        | Event::CData(_)
5140                        | Event::Comment(_)
5141                        | Event::PI(_)
5142                        | Event::GeneralRef(_)) => {
5143                            raw_children.push(boundary, capture_event(event)?);
5144                        }
5145                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
5146                            return Ok(ParsedWrapper {
5147                                choice,
5148                                markup: WrapperMarkup {
5149                                    raw_attributes,
5150                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
5151                                },
5152                                namespaces,
5153                            });
5154                        }
5155                        Event::Eof => {
5156                            return Err(missing_end(&format!(
5157                                "c:{}",
5158                                String::from_utf8_lossy(local)
5159                            )));
5160                        }
5161                        _ => {}
5162                    }
5163                    inner.clear();
5164                }
5165            }
5166            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
5167                let namespaces = chart_bindings(inherited, &element)?;
5168                return Ok(ParsedWrapper {
5169                    choice: None,
5170                    markup: WrapperMarkup {
5171                        raw_attributes: capture_attributes(&element)?,
5172                        raw_children: OrderedRawChildren::default(),
5173                    },
5174                    namespaces,
5175                });
5176            }
5177            Event::Start(element) | Event::Empty(element) => {
5178                return Err(ChartError::UnexpectedElement(element_name(&element)));
5179            }
5180            Event::Eof => {
5181                return Err(ChartError::MissingElement(format!(
5182                    "c:{}",
5183                    String::from_utf8_lossy(local)
5184                )));
5185            }
5186            _ => {}
5187        }
5188        buffer.clear();
5189    }
5190}
5191
5192fn write_string_cache(
5193    writer: &mut Writer<Vec<u8>>,
5194    values: &[String],
5195    markup: &ReferenceMarkup,
5196) -> Result<()> {
5197    let (declared_count, indexes) = cache_layout(markup, values.len())?;
5198    let mut start = BytesStart::new("c:strCache");
5199    push_attributes(&mut start, &markup.cache_attributes);
5200    writer
5201        .write_event(Event::Start(start))
5202        .map_err(OxmlError::from)?;
5203    emit_raw(writer, markup.cache_children.at(0))?;
5204    write_scalar(
5205        writer,
5206        "c:ptCount",
5207        &declared_count.to_string(),
5208        Some(&markup.point_count),
5209    )?;
5210    emit_cache_point_raw(writer, markup, 1, values.len(), 0)?;
5211    for (position, value) in values.iter().enumerate() {
5212        let default_markup = PointMarkup::default();
5213        write_point(
5214            writer,
5215            indexes[position],
5216            value,
5217            markup.points.get(position).unwrap_or(&default_markup),
5218        )?;
5219        emit_cache_point_raw(writer, markup, 1, values.len(), position + 1)?;
5220    }
5221    writer
5222        .write_event(Event::End(BytesEnd::new("c:strCache")))
5223        .map_err(OxmlError::from)?;
5224    Ok(())
5225}
5226
5227fn write_numeric_cache(
5228    writer: &mut Writer<Vec<u8>>,
5229    data: &NumericData,
5230    markup: &ReferenceMarkup,
5231) -> Result<()> {
5232    let (declared_count, indexes) = cache_layout(markup, data.values.len())?;
5233    let mut start = BytesStart::new("c:numCache");
5234    push_attributes(&mut start, &markup.cache_attributes);
5235    writer
5236        .write_event(Event::Start(start))
5237        .map_err(OxmlError::from)?;
5238    emit_raw(writer, markup.cache_children.at(0))?;
5239    let default_format_markup = TextMarkup::default();
5240    write_text(
5241        writer,
5242        "c:formatCode",
5243        &data.format_code,
5244        markup
5245            .format_code
5246            .as_ref()
5247            .unwrap_or(&default_format_markup),
5248    )?;
5249    emit_raw(writer, markup.cache_children.at(1))?;
5250    write_scalar(
5251        writer,
5252        "c:ptCount",
5253        &declared_count.to_string(),
5254        Some(&markup.point_count),
5255    )?;
5256    emit_cache_point_raw(writer, markup, 2, data.values.len(), 0)?;
5257    for (position, value) in data.values.iter().enumerate() {
5258        let default_markup = PointMarkup::default();
5259        write_point(
5260            writer,
5261            indexes[position],
5262            &value.to_string(),
5263            markup.points.get(position).unwrap_or(&default_markup),
5264        )?;
5265        emit_cache_point_raw(writer, markup, 2, data.values.len(), position + 1)?;
5266    }
5267    writer
5268        .write_event(Event::End(BytesEnd::new("c:numCache")))
5269        .map_err(OxmlError::from)?;
5270    Ok(())
5271}
5272
5273fn emit_cache_point_raw(
5274    writer: &mut Writer<Vec<u8>>,
5275    markup: &ReferenceMarkup,
5276    point_base: usize,
5277    current_count: usize,
5278    completed_points: usize,
5279) -> Result<()> {
5280    let original_count = markup.points.len();
5281    if current_count == 0 {
5282        for boundary in point_base..=point_base + original_count {
5283            emit_raw(writer, markup.cache_children.at(boundary))?;
5284        }
5285    } else if completed_points == 0 {
5286        if original_count > 0 {
5287            emit_raw(writer, markup.cache_children.at(point_base))?;
5288        }
5289    } else if completed_points < current_count {
5290        if completed_points < original_count {
5291            emit_raw(
5292                writer,
5293                markup.cache_children.at(point_base + completed_points),
5294            )?;
5295        }
5296    } else {
5297        let first_tail = if original_count == 0 {
5298            point_base
5299        } else {
5300            point_base + current_count.min(original_count)
5301        };
5302        for boundary in first_tail..=point_base + original_count {
5303            emit_raw(writer, markup.cache_children.at(boundary))?;
5304        }
5305    }
5306    Ok(())
5307}
5308
5309fn write_point(
5310    writer: &mut Writer<Vec<u8>>,
5311    index: u32,
5312    value: &str,
5313    markup: &PointMarkup,
5314) -> Result<()> {
5315    let index = index.to_string();
5316    let mut start = BytesStart::new("c:pt");
5317    start.push_attribute(("idx", index.as_str()));
5318    push_attributes(&mut start, &markup.raw_attributes);
5319    writer
5320        .write_event(Event::Start(start))
5321        .map_err(OxmlError::from)?;
5322    emit_raw(writer, markup.raw_children.at(0))?;
5323    write_text(writer, "c:v", value, &markup.value)?;
5324    emit_raw(writer, markup.raw_children.at(1))?;
5325    writer
5326        .write_event(Event::End(BytesEnd::new("c:pt")))
5327        .map_err(OxmlError::from)?;
5328    Ok(())
5329}
5330
5331fn cache_layout(markup: &ReferenceMarkup, value_count: usize) -> Result<(u32, Vec<u32>)> {
5332    if markup.point_indexes.len() == value_count
5333        && let Some(declared) = markup.declared_point_count
5334    {
5335        return Ok((declared, markup.point_indexes.clone()));
5336    }
5337    let declared = u32::try_from(value_count).map_err(|_| ChartError::InvalidValue {
5338        element: "c:ptCount".to_owned(),
5339        value: value_count.to_string(),
5340    })?;
5341    Ok((declared, (0..declared).collect()))
5342}
5343
5344fn write_text(
5345    writer: &mut Writer<Vec<u8>>,
5346    tag: &str,
5347    value: &str,
5348    markup: &TextMarkup,
5349) -> Result<()> {
5350    let mut start = BytesStart::new(tag);
5351    push_attributes(&mut start, &markup.raw_attributes);
5352    writer
5353        .write_event(Event::Start(start))
5354        .map_err(OxmlError::from)?;
5355    emit_raw(writer, markup.raw_children.at(0))?;
5356    writer
5357        .write_event(Event::Text(BytesText::new(value)))
5358        .map_err(OxmlError::from)?;
5359    emit_raw(writer, markup.raw_children.at(1))?;
5360    writer
5361        .write_event(Event::End(BytesEnd::new(tag)))
5362        .map_err(OxmlError::from)?;
5363    Ok(())
5364}
5365
5366fn write_wrapper_start(
5367    writer: &mut Writer<Vec<u8>>,
5368    tag: &str,
5369    markup: &WrapperMarkup,
5370) -> Result<()> {
5371    let mut start = BytesStart::new(tag);
5372    push_attributes(&mut start, &markup.raw_attributes);
5373    writer
5374        .write_event(Event::Start(start))
5375        .map_err(OxmlError::from)?;
5376    emit_raw(writer, markup.raw_children.at(0))
5377}
5378
5379fn write_wrapper_end(
5380    writer: &mut Writer<Vec<u8>>,
5381    tag: &str,
5382    markup: &WrapperMarkup,
5383) -> Result<()> {
5384    emit_raw(writer, markup.raw_children.at(1))?;
5385    writer
5386        .write_event(Event::End(BytesEnd::new(tag)))
5387        .map_err(OxmlError::from)?;
5388    Ok(())
5389}
5390
5391fn validate_formula(formula: &str, element: &str) -> Result<()> {
5392    if formula.trim().is_empty() {
5393        return Err(ChartError::InvalidValue {
5394            element: element.to_owned(),
5395            value: formula.to_owned(),
5396        });
5397    }
5398    validate_xml_text(formula, element)
5399}
5400
5401fn validate_format_code(format_code: &str) -> Result<()> {
5402    if format_code.is_empty() {
5403        return Err(ChartError::InvalidValue {
5404            element: "c:formatCode".to_owned(),
5405            value: format_code.to_owned(),
5406        });
5407    }
5408    validate_xml_text(format_code, "c:formatCode")
5409}
5410
5411fn validate_xml_text(value: &str, element: &str) -> Result<()> {
5412    if value.chars().all(|character| {
5413        matches!(
5414            character,
5415            '\u{9}' | '\u{A}' | '\u{D}' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}'
5416        )
5417    }) {
5418        return Ok(());
5419    }
5420    Err(ChartError::InvalidValue {
5421        element: element.to_owned(),
5422        value: value.to_owned(),
5423    })
5424}
5425
5426fn validate_numeric_values(values: &[f64]) -> Result<()> {
5427    if let Some(value) = values.iter().find(|value| !value.is_finite()) {
5428        return Err(ChartError::InvalidValue {
5429            element: "c:v".to_owned(),
5430            value: value.to_string(),
5431        });
5432    }
5433    Ok(())
5434}
5435
5436fn validate_point_count(count: usize, element: &str) -> Result<()> {
5437    u32::try_from(count)
5438        .map(|_| ())
5439        .map_err(|_| ChartError::InvalidValue {
5440            element: element.to_owned(),
5441            value: count.to_string(),
5442        })
5443}
5444
5445fn parse_u32_scalar(xml: &[u8], local: &str) -> Result<(u32, ScalarMarkup)> {
5446    let (value, markup) = scalar_value(xml, local)?;
5447    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
5448    let parsed = value
5449        .parse::<u32>()
5450        .map_err(|_| invalid_attribute(local, "val", value))?;
5451    Ok((parsed, markup))
5452}
5453
5454fn required_u32_attribute(element: &BytesStart<'_>, local: &str, attribute: &[u8]) -> Result<u32> {
5455    let value = attribute_value(element, attribute)?.ok_or_else(|| {
5456        invalid_attribute(
5457            local,
5458            &String::from_utf8_lossy(attribute),
5459            "<missing>".to_owned(),
5460        )
5461    })?;
5462    value
5463        .parse::<u32>()
5464        .map_err(|_| invalid_attribute(local, &String::from_utf8_lossy(attribute), value))
5465}
5466
5467fn chart_root_prefix(element: &BytesStart<'_>) -> Result<Vec<u8>> {
5468    let name = element.name();
5469    let qualified = name.as_ref();
5470    let prefix = qualified
5471        .iter()
5472        .position(|byte| *byte == b':')
5473        .map(|position| qualified[..position].to_vec())
5474        .unwrap_or_default();
5475    let declaration = if prefix.is_empty() {
5476        b"xmlns".to_vec()
5477    } else {
5478        let mut declaration = b"xmlns:".to_vec();
5479        declaration.extend_from_slice(&prefix);
5480        declaration
5481    };
5482    if let Some(namespace) = attribute_value(element, &declaration)?
5483        && namespace != C_NS
5484    {
5485        return Err(invalid_attribute(
5486            &element_name(element),
5487            "namespace",
5488            namespace,
5489        ));
5490    }
5491    reject_conflicting_prefix(element, b"c", C_NS)?;
5492    Ok(prefix)
5493}
5494
5495fn typed_rewrite_bindings(
5496    element: &BytesStart<'_>,
5497    inherited: &NamespaceBindings,
5498) -> Result<NamespaceBindings> {
5499    chart_root_prefix(element)?;
5500    if !element_is_in_namespace(element, C_NS, inherited)? {
5501        return Err(ChartError::UnexpectedElement(element_name(element)));
5502    }
5503    let bindings = chart_bindings(inherited, element)?;
5504    require_fixed_namespace(&bindings, b"c", C_NS, element)?;
5505    Ok(bindings)
5506}
5507
5508fn chart_namespace_defaults() -> NamespaceBindings {
5509    vec![
5510        (b"c".to_vec(), C_NS.to_owned()),
5511        (b"a".to_vec(), A_NS.to_owned()),
5512        (b"r".to_vec(), R_NS.to_owned()),
5513    ]
5514}
5515
5516fn chart_bindings(
5517    inherited: &NamespaceBindings,
5518    element: &BytesStart<'_>,
5519) -> Result<NamespaceBindings> {
5520    let mut bindings = chart_namespace_defaults();
5521    for (prefix, namespace) in inherited
5522        .iter()
5523        .cloned()
5524        .chain(namespace_bindings(element)?)
5525    {
5526        upsert_namespace_binding(&mut bindings, prefix, namespace);
5527    }
5528    Ok(bindings)
5529}
5530
5531fn root_chart_bindings(
5532    xml: &[u8],
5533    local: &[u8],
5534    inherited: &NamespaceBindings,
5535) -> Result<NamespaceBindings> {
5536    let mut reader = Reader::from_reader(xml);
5537    let mut buffer = Vec::new();
5538    loop {
5539        match reader
5540            .read_event_into(&mut buffer)
5541            .map_err(OxmlError::from)?
5542        {
5543            Event::Start(element) | Event::Empty(element)
5544                if matches_local_name(element.name().as_ref(), local) =>
5545            {
5546                if !element_is_in_namespace(&element, C_NS, inherited)? {
5547                    return Err(ChartError::UnexpectedElement(element_name(&element)));
5548                }
5549                return chart_bindings(inherited, &element);
5550            }
5551            Event::Start(element) | Event::Empty(element) => {
5552                return Err(ChartError::UnexpectedElement(element_name(&element)));
5553            }
5554            Event::Eof => {
5555                return Err(ChartError::MissingElement(format!(
5556                    "c:{}",
5557                    String::from_utf8_lossy(local)
5558                )));
5559            }
5560            _ => {}
5561        }
5562        buffer.clear();
5563    }
5564}
5565
5566fn chart_child_local(
5567    element: &BytesStart<'_>,
5568    inherited: &NamespaceBindings,
5569) -> Result<Option<Vec<u8>>> {
5570    Ok(element_is_in_namespace(element, C_NS, inherited)?
5571        .then(|| local_name(element.name().as_ref()).to_vec()))
5572}
5573
5574fn capture_fixed_root_attributes(
5575    start: &BytesStart<'_>,
5576    fixed: &[&str],
5577) -> Result<(XmlAttributes, XmlAttributes)> {
5578    let mut attributes = Vec::new();
5579    let mut namespaces = Vec::new();
5580    for attribute in start.attributes() {
5581        let attribute = attribute.map_err(OxmlError::from)?;
5582        let name = std::str::from_utf8(attribute.key.as_ref())
5583            .map_err(OxmlError::from)?
5584            .to_owned();
5585        let value = attribute
5586            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
5587            .map_err(OxmlError::from)?
5588            .into_owned();
5589        if fixed.contains(&name.as_str()) {
5590            continue;
5591        }
5592        if name == "xmlns" || name.starts_with("xmlns:") {
5593            namespaces.push((name, value));
5594        } else {
5595            attributes.push((name, value));
5596        }
5597    }
5598    Ok((attributes, namespaces))
5599}
5600
5601fn require_fixed_namespace(
5602    bindings: &NamespaceBindings,
5603    prefix: &[u8],
5604    expected: &str,
5605    element: &BytesStart<'_>,
5606) -> Result<()> {
5607    let actual = bindings
5608        .iter()
5609        .find(|(candidate, _)| candidate.as_slice() == prefix)
5610        .map(|(_, namespace)| namespace.as_str());
5611    if actual == Some(expected) {
5612        return Ok(());
5613    }
5614    Err(invalid_attribute(
5615        &element_name(element),
5616        &format!("xmlns:{}", String::from_utf8_lossy(prefix)),
5617        actual.unwrap_or("<missing>").to_owned(),
5618    ))
5619}
5620
5621fn standalone_namespace_declarations(bindings: &NamespaceBindings) -> Result<XmlAttributes> {
5622    bindings
5623        .iter()
5624        .filter(|(prefix, _)| prefix != b"c" && prefix != b"a" && prefix != b"r")
5625        .map(|(prefix, namespace)| {
5626            let prefix = std::str::from_utf8(prefix).map_err(OxmlError::from)?;
5627            let name = if prefix.is_empty() {
5628                "xmlns".to_owned()
5629            } else {
5630                format!("xmlns:{prefix}")
5631            };
5632            Ok((name, namespace.clone()))
5633        })
5634        .collect()
5635}
5636
5637fn capture_fixed_attributes(start: &BytesStart<'_>, fixed: &[&str]) -> Result<XmlAttributes> {
5638    let mut attributes = Vec::new();
5639    for attribute in start.attributes() {
5640        let attribute = attribute.map_err(OxmlError::from)?;
5641        let name = std::str::from_utf8(attribute.key.as_ref())
5642            .map_err(OxmlError::from)?
5643            .to_owned();
5644        if fixed.contains(&name.as_str()) {
5645            continue;
5646        }
5647        let value = attribute
5648            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
5649            .map_err(OxmlError::from)?
5650            .into_owned();
5651        attributes.push((name, value));
5652    }
5653    Ok(attributes)
5654}
5655
5656/// A producer-compatible ChartML axis identifier.
5657///
5658/// ECMA-376 names an unsigned value, but PowerPoint emits signed 32-bit
5659/// lexical values in real chart parts. The accepted domain covers both forms.
5660#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
5661pub struct AxisId(i64);
5662
5663impl AxisId {
5664    pub fn new(value: i64) -> Result<Self> {
5665        if (i64::from(i32::MIN)..=i64::from(u32::MAX)).contains(&value) {
5666            Ok(Self(value))
5667        } else {
5668            Err(ChartError::InvalidValue {
5669                element: "c:axId".to_owned(),
5670                value: value.to_string(),
5671            })
5672        }
5673    }
5674
5675    pub const fn value(self) -> i64 {
5676        self.0
5677    }
5678}
5679
5680/// Whether bars extend along the category or value axis.
5681#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5682pub enum BarDirection {
5683    Bar,
5684    Column,
5685}
5686
5687impl BarDirection {
5688    fn parse(value: &str) -> Option<Self> {
5689        match value {
5690            "bar" => Some(Self::Bar),
5691            "col" => Some(Self::Column),
5692            _ => None,
5693        }
5694    }
5695
5696    const fn as_str(self) -> &'static str {
5697        match self {
5698            Self::Bar => "bar",
5699            Self::Column => "col",
5700        }
5701    }
5702}
5703
5704/// Grouping modes supported by a two-dimensional bar plot.
5705#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5706pub enum BarGrouping {
5707    Clustered,
5708    PercentStacked,
5709    Stacked,
5710    Standard,
5711}
5712
5713impl BarGrouping {
5714    fn parse(value: &str) -> Option<Self> {
5715        match value {
5716            "clustered" => Some(Self::Clustered),
5717            "percentStacked" => Some(Self::PercentStacked),
5718            "stacked" => Some(Self::Stacked),
5719            "standard" => Some(Self::Standard),
5720            _ => None,
5721        }
5722    }
5723
5724    const fn as_str(self) -> &'static str {
5725        match self {
5726            Self::Clustered => "clustered",
5727            Self::PercentStacked => "percentStacked",
5728            Self::Stacked => "stacked",
5729            Self::Standard => "standard",
5730        }
5731    }
5732}
5733
5734/// Grouping modes shared by line and later area plots.
5735#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5736pub enum Grouping {
5737    PercentStacked,
5738    Stacked,
5739    Standard,
5740}
5741
5742impl Grouping {
5743    fn parse(value: &str) -> Option<Self> {
5744        match value {
5745            "percentStacked" => Some(Self::PercentStacked),
5746            "stacked" => Some(Self::Stacked),
5747            "standard" => Some(Self::Standard),
5748            _ => None,
5749        }
5750    }
5751
5752    const fn as_str(self) -> &'static str {
5753        match self {
5754            Self::PercentStacked => "percentStacked",
5755            Self::Stacked => "stacked",
5756            Self::Standard => "standard",
5757        }
5758    }
5759}
5760
5761/// Display style for a two-dimensional scatter plot.
5762#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5763pub enum ScatterStyle {
5764    Line,
5765    LineMarker,
5766    Marker,
5767    None,
5768    Smooth,
5769    SmoothMarker,
5770}
5771
5772impl ScatterStyle {
5773    fn parse(value: &str) -> Option<Self> {
5774        match value {
5775            "line" => Some(Self::Line),
5776            "lineMarker" => Some(Self::LineMarker),
5777            "marker" => Some(Self::Marker),
5778            "none" => Some(Self::None),
5779            "smooth" => Some(Self::Smooth),
5780            "smoothMarker" => Some(Self::SmoothMarker),
5781            _ => None,
5782        }
5783    }
5784
5785    const fn as_str(self) -> &'static str {
5786        match self {
5787            Self::Line => "line",
5788            Self::LineMarker => "lineMarker",
5789            Self::Marker => "marker",
5790            Self::None => "none",
5791            Self::Smooth => "smooth",
5792            Self::SmoothMarker => "smoothMarker",
5793        }
5794    }
5795}
5796
5797/// Display style for a two-dimensional radar plot.
5798#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5799pub enum RadarStyle {
5800    Filled,
5801    Marker,
5802    Standard,
5803}
5804
5805impl RadarStyle {
5806    fn parse(value: &str) -> Option<Self> {
5807        match value {
5808            "filled" => Some(Self::Filled),
5809            "marker" => Some(Self::Marker),
5810            "standard" => Some(Self::Standard),
5811            _ => None,
5812        }
5813    }
5814
5815    const fn as_str(self) -> &'static str {
5816        match self {
5817            Self::Filled => "filled",
5818            Self::Marker => "marker",
5819            Self::Standard => "standard",
5820        }
5821    }
5822}
5823
5824/// A supported two-dimensional plot owned by one plot area.
5825#[derive(Clone, Debug, PartialEq)]
5826pub enum Plot {
5827    Bar {
5828        direction: BarDirection,
5829        grouping: BarGrouping,
5830        gap_width: u16,
5831        overlap: i8,
5832        series: Vec<Series>,
5833        data_labels: Option<CT_DLbls>,
5834        axis_ids: [AxisId; 2],
5835    },
5836    Line {
5837        grouping: Grouping,
5838        marker: bool,
5839        smooth: bool,
5840        series: Vec<Series>,
5841        data_labels: Option<CT_DLbls>,
5842        axis_ids: [AxisId; 2],
5843    },
5844    Pie {
5845        first_slice_angle: u16,
5846        series: Vec<Series>,
5847        data_labels: Option<CT_DLbls>,
5848    },
5849    Doughnut {
5850        first_slice_angle: u16,
5851        hole_size: u8,
5852        series: Vec<Series>,
5853        data_labels: Option<CT_DLbls>,
5854    },
5855    Area {
5856        grouping: Grouping,
5857        series: Vec<Series>,
5858        data_labels: Option<CT_DLbls>,
5859        axis_ids: [AxisId; 2],
5860    },
5861    Scatter {
5862        style: ScatterStyle,
5863        series: Vec<Series>,
5864        data_labels: Option<CT_DLbls>,
5865        axis_ids: [AxisId; 2],
5866    },
5867    Radar {
5868        style: RadarStyle,
5869        series: Vec<Series>,
5870        data_labels: Option<CT_DLbls>,
5871        axis_ids: [AxisId; 2],
5872    },
5873}
5874
5875impl Plot {
5876    pub fn bar(
5877        direction: BarDirection,
5878        grouping: BarGrouping,
5879        series: Vec<Series>,
5880        axis_ids: [AxisId; 2],
5881    ) -> Result<Self> {
5882        let plot = Self::Bar {
5883            direction,
5884            grouping,
5885            gap_width: 150,
5886            overlap: 0,
5887            series,
5888            data_labels: None,
5889            axis_ids,
5890        };
5891        plot.validate()?;
5892        Ok(plot)
5893    }
5894
5895    pub fn line(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
5896        let plot = Self::Line {
5897            grouping,
5898            marker: false,
5899            smooth: false,
5900            series,
5901            data_labels: None,
5902            axis_ids,
5903        };
5904        plot.validate()?;
5905        Ok(plot)
5906    }
5907
5908    pub fn pie(series: Vec<Series>) -> Result<Self> {
5909        let plot = Self::Pie {
5910            first_slice_angle: 0,
5911            series,
5912            data_labels: None,
5913        };
5914        plot.validate()?;
5915        Ok(plot)
5916    }
5917
5918    pub fn doughnut(series: Vec<Series>) -> Result<Self> {
5919        let plot = Self::Doughnut {
5920            first_slice_angle: 0,
5921            hole_size: 50,
5922            series,
5923            data_labels: None,
5924        };
5925        plot.validate()?;
5926        Ok(plot)
5927    }
5928
5929    pub fn area(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
5930        let plot = Self::Area {
5931            grouping,
5932            series,
5933            data_labels: None,
5934            axis_ids,
5935        };
5936        plot.validate()?;
5937        Ok(plot)
5938    }
5939
5940    pub fn scatter(
5941        style: ScatterStyle,
5942        series: Vec<Series>,
5943        axis_ids: [AxisId; 2],
5944    ) -> Result<Self> {
5945        let plot = Self::Scatter {
5946            style,
5947            series,
5948            data_labels: None,
5949            axis_ids,
5950        };
5951        plot.validate()?;
5952        Ok(plot)
5953    }
5954
5955    pub fn radar(style: RadarStyle, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
5956        let plot = Self::Radar {
5957            style,
5958            series,
5959            data_labels: None,
5960            axis_ids,
5961        };
5962        plot.validate()?;
5963        Ok(plot)
5964    }
5965
5966    fn axis_ids(&self) -> Option<[AxisId; 2]> {
5967        match self {
5968            Self::Bar { axis_ids, .. }
5969            | Self::Line { axis_ids, .. }
5970            | Self::Area { axis_ids, .. }
5971            | Self::Scatter { axis_ids, .. }
5972            | Self::Radar { axis_ids, .. } => Some(*axis_ids),
5973            Self::Pie { .. } | Self::Doughnut { .. } => None,
5974        }
5975    }
5976
5977    fn validate(&self) -> Result<()> {
5978        let (series, labels, axis_ids) = match self {
5979            Self::Bar {
5980                gap_width,
5981                overlap,
5982                series,
5983                data_labels,
5984                axis_ids,
5985                ..
5986            } => {
5987                if *gap_width > 500 {
5988                    return Err(ChartError::InvalidValue {
5989                        element: "c:gapWidth".to_owned(),
5990                        value: gap_width.to_string(),
5991                    });
5992                }
5993                if !(-100..=100).contains(overlap) {
5994                    return Err(ChartError::InvalidValue {
5995                        element: "c:overlap".to_owned(),
5996                        value: overlap.to_string(),
5997                    });
5998                }
5999                (series, data_labels, Some(axis_ids))
6000            }
6001            Self::Line {
6002                series,
6003                data_labels,
6004                axis_ids,
6005                ..
6006            } => (series, data_labels, Some(axis_ids)),
6007            Self::Pie {
6008                first_slice_angle,
6009                series,
6010                data_labels,
6011            } => {
6012                if *first_slice_angle > 360 {
6013                    return Err(ChartError::InvalidValue {
6014                        element: "c:firstSliceAng".to_owned(),
6015                        value: first_slice_angle.to_string(),
6016                    });
6017                }
6018                (series, data_labels, None)
6019            }
6020            Self::Doughnut {
6021                first_slice_angle,
6022                hole_size,
6023                series,
6024                data_labels,
6025            } => {
6026                if *first_slice_angle > 360 {
6027                    return Err(ChartError::InvalidValue {
6028                        element: "c:firstSliceAng".to_owned(),
6029                        value: first_slice_angle.to_string(),
6030                    });
6031                }
6032                if !(10..=90).contains(hole_size) {
6033                    return Err(ChartError::InvalidValue {
6034                        element: "c:holeSize".to_owned(),
6035                        value: hole_size.to_string(),
6036                    });
6037                }
6038                (series, data_labels, None)
6039            }
6040            Self::Area {
6041                series,
6042                data_labels,
6043                axis_ids,
6044                ..
6045            }
6046            | Self::Radar {
6047                series,
6048                data_labels,
6049                axis_ids,
6050                ..
6051            } => (series, data_labels, Some(axis_ids)),
6052            Self::Scatter {
6053                series,
6054                data_labels,
6055                axis_ids,
6056                ..
6057            } => {
6058                for item in series {
6059                    if !matches!(item.categories, Some(AxisData::Numeric(_))) {
6060                        return Err(ChartError::InvalidValue {
6061                            element: "c:xVal".to_owned(),
6062                            value: "scatter series requires numeric x values".to_owned(),
6063                        });
6064                    }
6065                }
6066                (series, data_labels, Some(axis_ids))
6067            }
6068        };
6069        if series.is_empty() {
6070            return Err(ChartError::MissingElement("c:ser".to_owned()));
6071        }
6072        let scatter_plot = matches!(self, Self::Scatter { .. });
6073        for item in series {
6074            if item.bubble_size.is_some() || item.opaque_bubble_size {
6075                return Err(ChartError::InvalidValue {
6076                    element: "c:bubbleSize".to_owned(),
6077                    value: "bubble size is only valid in an opaque bubble plot".to_owned(),
6078                });
6079            }
6080            if item.uses_scatter_wrappers && !scatter_plot {
6081                return Err(ChartError::InvalidValue {
6082                    element: "c:ser".to_owned(),
6083                    value: "x/y wrappers are only valid in scatter plots".to_owned(),
6084                });
6085            }
6086            if scatter_plot && item.parsed_from_xml && !item.uses_scatter_wrappers {
6087                return Err(ChartError::InvalidValue {
6088                    element: "c:ser".to_owned(),
6089                    value: "scatter plots require xVal/yVal wrappers".to_owned(),
6090                });
6091            }
6092        }
6093        if let Some(axis_ids) = axis_ids {
6094            if axis_ids[0] == axis_ids[1] {
6095                return Err(ChartError::DuplicateElement(format!(
6096                    "c:axId {}",
6097                    axis_ids[0].value()
6098                )));
6099            }
6100            for id in axis_ids {
6101                AxisId::new(id.value())?;
6102            }
6103        }
6104        if let Some(labels) = labels {
6105            labels.validate()?;
6106        }
6107        for item in series {
6108            item.values.validate()?;
6109        }
6110        Ok(())
6111    }
6112}
6113
6114/// The concrete ChartML root used by an axis.
6115#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
6116pub enum AxisKind {
6117    Category,
6118    Value,
6119    Date,
6120    Series,
6121}
6122
6123impl AxisKind {
6124    fn parse(local: &[u8]) -> Option<Self> {
6125        match local {
6126            b"catAx" => Some(Self::Category),
6127            b"valAx" => Some(Self::Value),
6128            b"dateAx" => Some(Self::Date),
6129            b"serAx" => Some(Self::Series),
6130            _ => None,
6131        }
6132    }
6133
6134    const fn local(self) -> &'static str {
6135        match self {
6136            Self::Category => "catAx",
6137            Self::Value => "valAx",
6138            Self::Date => "dateAx",
6139            Self::Series => "serAx",
6140        }
6141    }
6142}
6143
6144/// Direction of values along an axis scale.
6145#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6146pub enum Orientation {
6147    #[default]
6148    MinMax,
6149    MaxMin,
6150}
6151
6152impl Orientation {
6153    fn parse(value: &str) -> Option<Self> {
6154        match value {
6155            "minMax" => Some(Self::MinMax),
6156            "maxMin" => Some(Self::MaxMin),
6157            _ => None,
6158        }
6159    }
6160
6161    const fn as_str(self) -> &'static str {
6162        match self {
6163            Self::MinMax => "minMax",
6164            Self::MaxMin => "maxMin",
6165        }
6166    }
6167}
6168
6169/// Which side of the plot area carries an axis.
6170#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6171pub enum AxisPosition {
6172    Bottom,
6173    Left,
6174    Right,
6175    Top,
6176}
6177
6178impl AxisPosition {
6179    fn parse(value: &str) -> Option<Self> {
6180        match value {
6181            "b" => Some(Self::Bottom),
6182            "l" => Some(Self::Left),
6183            "r" => Some(Self::Right),
6184            "t" => Some(Self::Top),
6185            _ => None,
6186        }
6187    }
6188
6189    const fn as_str(self) -> &'static str {
6190        match self {
6191            Self::Bottom => "b",
6192            Self::Left => "l",
6193            Self::Right => "r",
6194            Self::Top => "t",
6195        }
6196    }
6197}
6198
6199/// Placement of a major or minor tick relative to its axis line.
6200#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6201pub enum TickMark {
6202    Cross,
6203    Inside,
6204    #[default]
6205    None,
6206    Outside,
6207}
6208
6209impl TickMark {
6210    fn parse(value: &str) -> Option<Self> {
6211        match value {
6212            "cross" => Some(Self::Cross),
6213            "in" => Some(Self::Inside),
6214            "none" => Some(Self::None),
6215            "out" => Some(Self::Outside),
6216            _ => None,
6217        }
6218    }
6219
6220    const fn as_str(self) -> &'static str {
6221        match self {
6222            Self::Cross => "cross",
6223            Self::Inside => "in",
6224            Self::None => "none",
6225            Self::Outside => "out",
6226        }
6227    }
6228}
6229
6230/// Placement of tick labels relative to an axis.
6231#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6232pub enum TickLabelPosition {
6233    High,
6234    Low,
6235    #[default]
6236    NextTo,
6237    None,
6238}
6239
6240impl TickLabelPosition {
6241    fn parse(value: &str) -> Option<Self> {
6242        match value {
6243            "high" => Some(Self::High),
6244            "low" => Some(Self::Low),
6245            "nextTo" => Some(Self::NextTo),
6246            "none" => Some(Self::None),
6247            _ => None,
6248        }
6249    }
6250
6251    const fn as_str(self) -> &'static str {
6252        match self {
6253            Self::High => "high",
6254            Self::Low => "low",
6255            Self::NextTo => "nextTo",
6256            Self::None => "none",
6257        }
6258    }
6259}
6260
6261/// The modelled values of `c:scaling`.
6262#[derive(Clone, Debug)]
6263pub struct Scaling {
6264    pub log_base: Option<f64>,
6265    pub orientation: Orientation,
6266    pub maximum: Option<f64>,
6267    pub minimum: Option<f64>,
6268    log_base_markup: Option<ScalarMarkup>,
6269    orientation_markup: Option<ScalarMarkup>,
6270    maximum_markup: Option<ScalarMarkup>,
6271    minimum_markup: Option<ScalarMarkup>,
6272    raw_attributes: XmlAttributes,
6273    raw_children: OrderedRawChildren,
6274}
6275
6276impl PartialEq for Scaling {
6277    fn eq(&self, other: &Self) -> bool {
6278        self.log_base == other.log_base
6279            && self.orientation == other.orientation
6280            && self.maximum == other.maximum
6281            && self.minimum == other.minimum
6282            && optional_scalar_markup_eq(&self.log_base_markup, &other.log_base_markup)
6283            && optional_scalar_markup_eq(&self.orientation_markup, &other.orientation_markup)
6284            && optional_scalar_markup_eq(&self.maximum_markup, &other.maximum_markup)
6285            && optional_scalar_markup_eq(&self.minimum_markup, &other.minimum_markup)
6286            && self.raw_attributes == other.raw_attributes
6287            && self.raw_children == other.raw_children
6288    }
6289}
6290
6291impl Default for Scaling {
6292    fn default() -> Self {
6293        Self {
6294            log_base: None,
6295            orientation: Orientation::MinMax,
6296            maximum: None,
6297            minimum: None,
6298            log_base_markup: None,
6299            orientation_markup: None,
6300            maximum_markup: None,
6301            minimum_markup: None,
6302            raw_attributes: Vec::new(),
6303            raw_children: OrderedRawChildren::default(),
6304        }
6305    }
6306}
6307
6308/// Shape properties on a major or minor gridline container.
6309#[derive(Clone, Debug, Default, PartialEq)]
6310pub struct ChartLines {
6311    pub sp_pr: Option<CT_ShapeProperties>,
6312    raw_attributes: XmlAttributes,
6313    raw_children: OrderedRawChildren,
6314}
6315
6316/// Number format applied to axis values and labels.
6317#[derive(Clone, Debug, Eq, PartialEq)]
6318pub struct NumberFormat {
6319    pub format_code: String,
6320    pub source_linked: bool,
6321    raw_attributes: XmlAttributes,
6322    raw_content: Vec<Vec<u8>>,
6323}
6324
6325/// Placement of a chart data label relative to its data point.
6326#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6327pub enum DataLabelPosition {
6328    BestFit,
6329    Bottom,
6330    Center,
6331    InsideBase,
6332    InsideEnd,
6333    Left,
6334    OutsideEnd,
6335    Right,
6336    Top,
6337}
6338
6339impl DataLabelPosition {
6340    fn parse(value: &str) -> Option<Self> {
6341        match value {
6342            "bestFit" => Some(Self::BestFit),
6343            "b" => Some(Self::Bottom),
6344            "ctr" => Some(Self::Center),
6345            "inBase" => Some(Self::InsideBase),
6346            "inEnd" => Some(Self::InsideEnd),
6347            "l" => Some(Self::Left),
6348            "outEnd" => Some(Self::OutsideEnd),
6349            "r" => Some(Self::Right),
6350            "t" => Some(Self::Top),
6351            _ => None,
6352        }
6353    }
6354
6355    const fn as_str(self) -> &'static str {
6356        match self {
6357            Self::BestFit => "bestFit",
6358            Self::Bottom => "b",
6359            Self::Center => "ctr",
6360            Self::InsideBase => "inBase",
6361            Self::InsideEnd => "inEnd",
6362            Self::Left => "l",
6363            Self::OutsideEnd => "outEnd",
6364            Self::Right => "r",
6365            Self::Top => "t",
6366        }
6367    }
6368}
6369
6370/// Collection-level data-label defaults for one chart series or plot.
6371#[derive(Clone, Debug, Default, PartialEq)]
6372pub struct CT_DLbls {
6373    pub number_format: Option<NumberFormat>,
6374    pub position: Option<DataLabelPosition>,
6375    pub separator: Option<String>,
6376    pub show_legend_key: bool,
6377    pub show_value: bool,
6378    pub show_category_name: bool,
6379    pub show_series_name: bool,
6380    pub show_percent: bool,
6381    pub show_bubble_size: bool,
6382    position_markup: Option<ScalarMarkup>,
6383    show_legend_key_markup: Option<ScalarMarkup>,
6384    show_value_markup: Option<ScalarMarkup>,
6385    show_category_name_markup: Option<ScalarMarkup>,
6386    show_series_name_markup: Option<ScalarMarkup>,
6387    show_percent_markup: Option<ScalarMarkup>,
6388    show_bubble_size_markup: Option<ScalarMarkup>,
6389    separator_markup: Option<TextMarkup>,
6390    raw_attributes: XmlAttributes,
6391    namespace_declarations: XmlAttributes,
6392    raw_children: OrderedRawChildren,
6393}
6394
6395/// How a chart displays cells whose cached values are blank.
6396#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6397pub enum DispBlanksAs {
6398    #[default]
6399    Gap,
6400    Zero,
6401    Span,
6402}
6403
6404impl DispBlanksAs {
6405    fn parse(value: &str) -> Option<Self> {
6406        match value {
6407            "gap" => Some(Self::Gap),
6408            "zero" => Some(Self::Zero),
6409            "span" => Some(Self::Span),
6410            _ => None,
6411        }
6412    }
6413
6414    const fn as_str(self) -> &'static str {
6415        match self {
6416            Self::Gap => "gap",
6417            Self::Zero => "zero",
6418            Self::Span => "span",
6419        }
6420    }
6421}
6422
6423/// A chart title shell whose current children remain opaque until later stories.
6424#[derive(Clone, Debug, Default, Eq, PartialEq)]
6425pub struct CT_Title {
6426    raw_attributes: Vec<(String, String)>,
6427    raw_children: OrderedRawChildren,
6428}
6429
6430/// Preserved lexical and ordered-raw state for one typed plot.
6431#[derive(Clone, Debug, Default, PartialEq)]
6432struct PlotMarkup {
6433    raw_attributes: XmlAttributes,
6434    raw_children: OrderedRawChildren,
6435    direction: Option<ScalarMarkup>,
6436    grouping: Option<ScalarMarkup>,
6437    gap_width: Option<ScalarMarkup>,
6438    overlap: Option<ScalarMarkup>,
6439    marker: Option<ScalarMarkup>,
6440    smooth: Option<ScalarMarkup>,
6441    first_slice_angle: Option<ScalarMarkup>,
6442    hole_size: Option<ScalarMarkup>,
6443    style: Option<ScalarMarkup>,
6444    axis_ids: Vec<AxisIdMarkup>,
6445    original_series_keys: Vec<(u32, u32)>,
6446    original_axis_ids: Vec<AxisId>,
6447    parsed_bar: Option<bool>,
6448    parsed_remaining: Option<&'static str>,
6449}
6450
6451/// A plot-area shell. F-119 through F-122 replace selected raw slots with types.
6452#[derive(Clone, Debug, PartialEq)]
6453pub struct CT_PlotArea {
6454    raw_attributes: Vec<(String, String)>,
6455    raw_children: OrderedRawChildren,
6456    namespace_bindings: NamespaceBindings,
6457    plots: Option<Vec<Plot>>,
6458    plot_markup: Vec<PlotMarkup>,
6459    axes: Vec<Axis>,
6460}
6461
6462/// A chart legend shell whose current children remain opaque.
6463#[derive(Clone, Debug, Default, Eq, PartialEq)]
6464pub struct CT_Legend {
6465    raw_attributes: Vec<(String, String)>,
6466    raw_children: OrderedRawChildren,
6467}
6468
6469#[derive(Clone, Debug, PartialEq)]
6470struct AxisIdMarkup {
6471    scalar: ScalarMarkup,
6472    parsed: AxisId,
6473    lexical: String,
6474}
6475
6476/// The common modelled sequence of one ChartML axis.
6477#[derive(Clone, Debug)]
6478pub struct Axis {
6479    pub kind: AxisKind,
6480    pub id: AxisId,
6481    pub scaling: Scaling,
6482    pub deleted: bool,
6483    pub position: AxisPosition,
6484    pub major_gridlines: Option<ChartLines>,
6485    pub minor_gridlines: Option<ChartLines>,
6486    pub title: Option<CT_Title>,
6487    pub number_format: Option<NumberFormat>,
6488    pub major_tick_mark: TickMark,
6489    pub minor_tick_mark: TickMark,
6490    pub tick_label_position: TickLabelPosition,
6491    pub sp_pr: Option<CT_ShapeProperties>,
6492    pub tx_pr: Option<CT_TextBody>,
6493    pub cross_axis: AxisId,
6494    parsed_kind: Option<AxisKind>,
6495    id_markup: AxisIdMarkup,
6496    delete_markup: Option<ScalarMarkup>,
6497    position_markup: ScalarMarkup,
6498    major_tick_mark_markup: Option<ScalarMarkup>,
6499    minor_tick_mark_markup: Option<ScalarMarkup>,
6500    tick_label_position_markup: Option<ScalarMarkup>,
6501    cross_axis_markup: AxisIdMarkup,
6502    raw_attributes: XmlAttributes,
6503    namespace_declarations: XmlAttributes,
6504    raw_children: OrderedRawChildren,
6505}
6506
6507impl PartialEq for Axis {
6508    fn eq(&self, other: &Self) -> bool {
6509        self.kind == other.kind
6510            && self.id == other.id
6511            && axis_id_markup_eq(&self.id_markup, &other.id_markup)
6512            && self.scaling == other.scaling
6513            && self.deleted == other.deleted
6514            && optional_scalar_markup_eq(&self.delete_markup, &other.delete_markup)
6515            && self.position == other.position
6516            && self.position_markup == other.position_markup
6517            && self.major_gridlines == other.major_gridlines
6518            && self.minor_gridlines == other.minor_gridlines
6519            && self.title == other.title
6520            && self.number_format == other.number_format
6521            && self.major_tick_mark == other.major_tick_mark
6522            && optional_scalar_markup_eq(
6523                &self.major_tick_mark_markup,
6524                &other.major_tick_mark_markup,
6525            )
6526            && self.minor_tick_mark == other.minor_tick_mark
6527            && optional_scalar_markup_eq(
6528                &self.minor_tick_mark_markup,
6529                &other.minor_tick_mark_markup,
6530            )
6531            && self.tick_label_position == other.tick_label_position
6532            && optional_scalar_markup_eq(
6533                &self.tick_label_position_markup,
6534                &other.tick_label_position_markup,
6535            )
6536            && self.sp_pr == other.sp_pr
6537            && self.tx_pr == other.tx_pr
6538            && self.cross_axis == other.cross_axis
6539            && axis_id_markup_eq(&self.cross_axis_markup, &other.cross_axis_markup)
6540            && self.raw_attributes == other.raw_attributes
6541            && self.namespace_declarations == other.namespace_declarations
6542            && self.raw_children == other.raw_children
6543    }
6544}
6545
6546impl Axis {
6547    pub fn new(kind: AxisKind, id: AxisId, position: AxisPosition, cross_axis: AxisId) -> Self {
6548        Self {
6549            kind,
6550            id,
6551            scaling: Scaling::default(),
6552            deleted: false,
6553            position,
6554            major_gridlines: None,
6555            minor_gridlines: None,
6556            title: None,
6557            number_format: None,
6558            major_tick_mark: TickMark::None,
6559            minor_tick_mark: TickMark::None,
6560            tick_label_position: TickLabelPosition::NextTo,
6561            sp_pr: None,
6562            tx_pr: None,
6563            cross_axis,
6564            parsed_kind: None,
6565            id_markup: default_axis_id_markup(id),
6566            delete_markup: None,
6567            position_markup: ScalarMarkup::default(),
6568            major_tick_mark_markup: None,
6569            minor_tick_mark_markup: None,
6570            tick_label_position_markup: None,
6571            cross_axis_markup: default_axis_id_markup(cross_axis),
6572            raw_attributes: Vec::new(),
6573            namespace_declarations: Vec::new(),
6574            raw_children: OrderedRawChildren::default(),
6575        }
6576    }
6577
6578    pub fn from_xml(xml: &[u8]) -> Result<Self> {
6579        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
6580    }
6581
6582    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
6583        let mut reader = Reader::from_reader(xml);
6584        let mut buffer = Vec::new();
6585        loop {
6586            match reader
6587                .read_event_into(&mut buffer)
6588                .map_err(OxmlError::from)?
6589            {
6590                Event::Start(element) => {
6591                    let name = element.name();
6592                    let local = local_name(name.as_ref());
6593                    let Some(kind) = AxisKind::parse(local) else {
6594                        return Err(ChartError::UnexpectedElement(element_name(&element)));
6595                    };
6596                    chart_root_prefix(&element)?;
6597                    if !element_is_in_namespace(&element, C_NS, inherited)? {
6598                        return Err(ChartError::UnexpectedElement(element_name(&element)));
6599                    }
6600                    return Self::from_element(&mut reader, &element, inherited, kind);
6601                }
6602                Event::Empty(element) => {
6603                    let name = element.name();
6604                    let local = local_name(name.as_ref());
6605                    if AxisKind::parse(local).is_some() {
6606                        return Err(ChartError::MissingElement("c:axId".to_owned()));
6607                    }
6608                    return Err(ChartError::UnexpectedElement(element_name(&element)));
6609                }
6610                Event::Eof => return Err(ChartError::MissingElement("ChartML axis".to_owned())),
6611                _ => {}
6612            }
6613            buffer.clear();
6614        }
6615    }
6616
6617    fn from_element(
6618        reader: &mut Reader<&[u8]>,
6619        start: &BytesStart<'_>,
6620        inherited: &NamespaceBindings,
6621        kind: AxisKind,
6622    ) -> Result<Self> {
6623        reject_conflicting_prefix(start, b"a", A_NS)?;
6624        reject_conflicting_prefix(start, b"r", R_NS)?;
6625        let namespaces = chart_bindings(inherited, start)?;
6626        require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
6627        require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
6628        require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
6629        let (raw_attributes, _) =
6630            capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
6631        let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
6632        let mut state = AxisParseState::default();
6633        let mut buffer = Vec::new();
6634        loop {
6635            match reader
6636                .read_event_into(&mut buffer)
6637                .map_err(OxmlError::from)?
6638            {
6639                Event::Start(element) => {
6640                    let name = chart_child_local(&element, &namespaces)?;
6641                    let raw = capture_element(reader, &element)?;
6642                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
6643                }
6644                Event::Empty(element) => {
6645                    let name = chart_child_local(&element, &namespaces)?;
6646                    let raw = capture_empty_element(&element)?;
6647                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
6648                }
6649                event @ (Event::Text(_)
6650                | Event::CData(_)
6651                | Event::Comment(_)
6652                | Event::PI(_)
6653                | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
6654                Event::End(element)
6655                    if matches_local_name(element.name().as_ref(), kind.local().as_bytes()) =>
6656                {
6657                    break;
6658                }
6659                Event::Eof => return Err(missing_end(&format!("c:{}", kind.local()))),
6660                _ => {}
6661            }
6662            buffer.clear();
6663        }
6664        state.finish(kind, raw_attributes, namespace_declarations)
6665    }
6666
6667    pub fn to_xml(&self) -> Result<Vec<u8>> {
6668        self.validate()?;
6669        let mut writer = Writer::new(Vec::new());
6670        let root = format!("c:{}", self.kind.local());
6671        let mut start = BytesStart::new(&root);
6672        start.push_attribute(("xmlns:c", C_NS));
6673        start.push_attribute(("xmlns:a", A_NS));
6674        start.push_attribute(("xmlns:r", R_NS));
6675        push_attributes(&mut start, &self.namespace_declarations);
6676        push_attributes(&mut start, &self.raw_attributes);
6677        writer
6678            .write_event(Event::Start(start))
6679            .map_err(OxmlError::from)?;
6680        emit_raw(&mut writer, self.raw_children.at(0))?;
6681        write_axis_id(&mut writer, "c:axId", self.id, &self.id_markup)?;
6682        emit_raw(&mut writer, self.raw_children.at(1))?;
6683        self.scaling.write_xml(&mut writer)?;
6684        emit_raw(&mut writer, self.raw_children.at(2))?;
6685        if self.deleted || self.delete_markup.is_some() {
6686            write_scalar(
6687                &mut writer,
6688                "c:delete",
6689                bool_lexical(self.deleted),
6690                self.delete_markup.as_ref(),
6691            )?;
6692        }
6693        emit_raw(&mut writer, self.raw_children.at(3))?;
6694        write_scalar(
6695            &mut writer,
6696            "c:axPos",
6697            self.position.as_str(),
6698            Some(&self.position_markup),
6699        )?;
6700        emit_raw(&mut writer, self.raw_children.at(4))?;
6701        if let Some(lines) = &self.major_gridlines {
6702            lines.write_xml(&mut writer, "c:majorGridlines")?;
6703        }
6704        emit_raw(&mut writer, self.raw_children.at(5))?;
6705        if let Some(lines) = &self.minor_gridlines {
6706            lines.write_xml(&mut writer, "c:minorGridlines")?;
6707        }
6708        emit_raw(&mut writer, self.raw_children.at(6))?;
6709        if let Some(title) = &self.title {
6710            title.write_xml(&mut writer)?;
6711        }
6712        emit_raw(&mut writer, self.raw_children.at(7))?;
6713        if let Some(number_format) = &self.number_format {
6714            number_format.write_xml(&mut writer)?;
6715        }
6716        emit_raw(&mut writer, self.raw_children.at(8))?;
6717        if self.major_tick_mark != TickMark::None || self.major_tick_mark_markup.is_some() {
6718            write_scalar(
6719                &mut writer,
6720                "c:majorTickMark",
6721                self.major_tick_mark.as_str(),
6722                self.major_tick_mark_markup.as_ref(),
6723            )?;
6724        }
6725        emit_raw(&mut writer, self.raw_children.at(9))?;
6726        if self.minor_tick_mark != TickMark::None || self.minor_tick_mark_markup.is_some() {
6727            write_scalar(
6728                &mut writer,
6729                "c:minorTickMark",
6730                self.minor_tick_mark.as_str(),
6731                self.minor_tick_mark_markup.as_ref(),
6732            )?;
6733        }
6734        emit_raw(&mut writer, self.raw_children.at(10))?;
6735        if self.tick_label_position != TickLabelPosition::NextTo
6736            || self.tick_label_position_markup.is_some()
6737        {
6738            write_scalar(
6739                &mut writer,
6740                "c:tickLblPos",
6741                self.tick_label_position.as_str(),
6742                self.tick_label_position_markup.as_ref(),
6743            )?;
6744        }
6745        emit_raw(&mut writer, self.raw_children.at(11))?;
6746        if let Some(properties) = &self.sp_pr {
6747            properties.write_xml_as(&mut writer, "c:spPr")?;
6748        }
6749        emit_raw(&mut writer, self.raw_children.at(12))?;
6750        if let Some(text) = &self.tx_pr {
6751            text.write_xml_as(&mut writer, "c:txPr")?;
6752        }
6753        emit_raw(&mut writer, self.raw_children.at(13))?;
6754        write_axis_id(
6755            &mut writer,
6756            "c:crossAx",
6757            self.cross_axis,
6758            &self.cross_axis_markup,
6759        )?;
6760        emit_raw(&mut writer, self.raw_children.at(14))?;
6761        writer
6762            .write_event(Event::End(BytesEnd::new(&root)))
6763            .map_err(OxmlError::from)?;
6764        Ok(writer.into_inner())
6765    }
6766
6767    pub fn raw_children(&self) -> &OrderedRawChildren {
6768        &self.raw_children
6769    }
6770
6771    fn validate(&self) -> Result<()> {
6772        if let Some(parsed_kind) = self.parsed_kind
6773            && self.kind != parsed_kind
6774        {
6775            return Err(ChartError::InvalidValue {
6776                element: "ChartML axis root".to_owned(),
6777                value: format!(
6778                    "parsed c:{} cannot be relabelled c:{}",
6779                    parsed_kind.local(),
6780                    self.kind.local()
6781                ),
6782            });
6783        }
6784        AxisId::new(self.id.value())?;
6785        AxisId::new(self.cross_axis.value())?;
6786        self.scaling.validate()?;
6787        if let Some(number_format) = &self.number_format {
6788            number_format.validate()?;
6789        }
6790        Ok(())
6791    }
6792}
6793
6794#[derive(Default)]
6795struct AxisParseState {
6796    id: Option<(AxisId, AxisIdMarkup)>,
6797    scaling: Option<Scaling>,
6798    deleted: Option<(bool, ScalarMarkup)>,
6799    position: Option<(AxisPosition, ScalarMarkup)>,
6800    major_gridlines: Option<ChartLines>,
6801    minor_gridlines: Option<ChartLines>,
6802    title: Option<CT_Title>,
6803    number_format: Option<NumberFormat>,
6804    major_tick_mark: Option<(TickMark, ScalarMarkup)>,
6805    minor_tick_mark: Option<(TickMark, ScalarMarkup)>,
6806    tick_label_position: Option<(TickLabelPosition, ScalarMarkup)>,
6807    sp_pr: Option<CT_ShapeProperties>,
6808    tx_pr: Option<CT_TextBody>,
6809    cross_axis: Option<(AxisId, AxisIdMarkup)>,
6810    raw_children: OrderedRawChildren,
6811    boundary: usize,
6812}
6813
6814impl AxisParseState {
6815    fn capture_event(&mut self, raw: Vec<u8>) {
6816        self.raw_children.push(self.boundary, raw);
6817    }
6818
6819    fn parse_child(
6820        &mut self,
6821        name: &[u8],
6822        raw: Vec<u8>,
6823        namespaces: &NamespaceBindings,
6824    ) -> Result<()> {
6825        match name {
6826            b"axId" => {
6827                set_once(&mut self.id, parse_axis_id_scalar(&raw, "axId")?, "c:axId")?;
6828                self.boundary = self.boundary.max(1);
6829            }
6830            b"scaling" => {
6831                set_once(
6832                    &mut self.scaling,
6833                    Scaling::from_xml(&raw, namespaces)?,
6834                    "c:scaling",
6835                )?;
6836                self.boundary = self.boundary.max(2);
6837            }
6838            b"delete" => {
6839                set_once(
6840                    &mut self.deleted,
6841                    parse_bool_value(&raw, "delete")?,
6842                    "c:delete",
6843                )?;
6844                self.boundary = self.boundary.max(3);
6845            }
6846            b"axPos" => {
6847                set_once(&mut self.position, parse_axis_position(&raw)?, "c:axPos")?;
6848                self.boundary = self.boundary.max(4);
6849            }
6850            b"majorGridlines" => {
6851                set_once(
6852                    &mut self.major_gridlines,
6853                    ChartLines::from_xml(&raw, b"majorGridlines", namespaces)?,
6854                    "c:majorGridlines",
6855                )?;
6856                self.boundary = self.boundary.max(5);
6857            }
6858            b"minorGridlines" => {
6859                set_once(
6860                    &mut self.minor_gridlines,
6861                    ChartLines::from_xml(&raw, b"minorGridlines", namespaces)?,
6862                    "c:minorGridlines",
6863                )?;
6864                self.boundary = self.boundary.max(6);
6865            }
6866            b"title" => {
6867                set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
6868                self.boundary = self.boundary.max(7);
6869            }
6870            b"numFmt" => {
6871                set_once(
6872                    &mut self.number_format,
6873                    NumberFormat::from_xml(&raw)?,
6874                    "c:numFmt",
6875                )?;
6876                self.boundary = self.boundary.max(8);
6877            }
6878            b"majorTickMark" => {
6879                set_once(
6880                    &mut self.major_tick_mark,
6881                    parse_tick_mark(&raw, "majorTickMark")?,
6882                    "c:majorTickMark",
6883                )?;
6884                self.boundary = self.boundary.max(9);
6885            }
6886            b"minorTickMark" => {
6887                set_once(
6888                    &mut self.minor_tick_mark,
6889                    parse_tick_mark(&raw, "minorTickMark")?,
6890                    "c:minorTickMark",
6891                )?;
6892                self.boundary = self.boundary.max(10);
6893            }
6894            b"tickLblPos" => {
6895                set_once(
6896                    &mut self.tick_label_position,
6897                    parse_tick_label_position(&raw)?,
6898                    "c:tickLblPos",
6899                )?;
6900                self.boundary = self.boundary.max(11);
6901            }
6902            b"spPr" => {
6903                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
6904                let properties = CT_ShapeProperties::from_xml(&raw)?;
6905                let mut writer = Writer::new(Vec::new());
6906                properties.write_xml_as(&mut writer, "c:spPr")?;
6907                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
6908                set_once(&mut self.sp_pr, properties, "c:spPr")?;
6909                self.boundary = self.boundary.max(12);
6910            }
6911            b"txPr" => {
6912                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
6913                let text = CT_TextBody::from_xml_as(&raw, b"txPr")?;
6914                let mut writer = Writer::new(Vec::new());
6915                text.write_xml_as(&mut writer, "c:txPr")?;
6916                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
6917                set_once(&mut self.tx_pr, text, "c:txPr")?;
6918                self.boundary = self.boundary.max(13);
6919            }
6920            b"crossAx" => {
6921                set_once(
6922                    &mut self.cross_axis,
6923                    parse_axis_id_scalar(&raw, "crossAx")?,
6924                    "c:crossAx",
6925                )?;
6926                self.boundary = self.boundary.max(14);
6927            }
6928            _ => self.raw_children.push(self.boundary, raw),
6929        }
6930        Ok(())
6931    }
6932
6933    fn finish(
6934        self,
6935        kind: AxisKind,
6936        raw_attributes: XmlAttributes,
6937        namespace_declarations: XmlAttributes,
6938    ) -> Result<Axis> {
6939        let (id, id_markup) = self
6940            .id
6941            .ok_or_else(|| ChartError::MissingElement("c:axId".to_owned()))?;
6942        let scaling = self
6943            .scaling
6944            .ok_or_else(|| ChartError::MissingElement("c:scaling".to_owned()))?;
6945        let (position, position_markup) = self
6946            .position
6947            .ok_or_else(|| ChartError::MissingElement("c:axPos".to_owned()))?;
6948        let (cross_axis, cross_axis_markup) = self
6949            .cross_axis
6950            .ok_or_else(|| ChartError::MissingElement("c:crossAx".to_owned()))?;
6951        let (deleted, delete_markup) = self
6952            .deleted
6953            .map(|(value, markup)| (value, Some(markup)))
6954            .unwrap_or((false, None));
6955        let (major_tick_mark, major_tick_mark_markup) = self
6956            .major_tick_mark
6957            .map(|(value, markup)| (value, Some(markup)))
6958            .unwrap_or((TickMark::None, None));
6959        let (minor_tick_mark, minor_tick_mark_markup) = self
6960            .minor_tick_mark
6961            .map(|(value, markup)| (value, Some(markup)))
6962            .unwrap_or((TickMark::None, None));
6963        let (tick_label_position, tick_label_position_markup) = self
6964            .tick_label_position
6965            .map(|(value, markup)| (value, Some(markup)))
6966            .unwrap_or((TickLabelPosition::NextTo, None));
6967        let axis = Axis {
6968            kind,
6969            id,
6970            scaling,
6971            deleted,
6972            position,
6973            major_gridlines: self.major_gridlines,
6974            minor_gridlines: self.minor_gridlines,
6975            title: self.title,
6976            number_format: self.number_format,
6977            major_tick_mark,
6978            minor_tick_mark,
6979            tick_label_position,
6980            sp_pr: self.sp_pr,
6981            tx_pr: self.tx_pr,
6982            cross_axis,
6983            parsed_kind: Some(kind),
6984            id_markup,
6985            delete_markup,
6986            position_markup,
6987            major_tick_mark_markup,
6988            minor_tick_mark_markup,
6989            tick_label_position_markup,
6990            cross_axis_markup,
6991            raw_attributes,
6992            namespace_declarations,
6993            raw_children: raw_children_in_schema_order(&self.raw_children, 14),
6994        };
6995        axis.validate()?;
6996        Ok(axis)
6997    }
6998}
6999
7000impl Scaling {
7001    fn from_xml(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7002        let mut reader = Reader::from_reader(xml);
7003        let mut buffer = Vec::new();
7004        loop {
7005            match reader
7006                .read_event_into(&mut buffer)
7007                .map_err(OxmlError::from)?
7008            {
7009                Event::Start(element)
7010                    if matches_local_name(element.name().as_ref(), b"scaling") =>
7011                {
7012                    let namespaces = typed_rewrite_bindings(&element, inherited)?;
7013                    let raw_attributes = capture_attributes(&element)?;
7014                    let mut log_base = None;
7015                    let mut orientation = None;
7016                    let mut maximum = None;
7017                    let mut minimum = None;
7018                    let mut raw_children = OrderedRawChildren::default();
7019                    let mut boundary = 0usize;
7020                    let mut inner = Vec::new();
7021                    loop {
7022                        match reader
7023                            .read_event_into(&mut inner)
7024                            .map_err(OxmlError::from)?
7025                        {
7026                            Event::Start(child) => {
7027                                let name = chart_child_local(&child, &namespaces)?;
7028                                let raw = capture_element(&mut reader, &child)?;
7029                                parse_scaling_child(
7030                                    name.as_deref().unwrap_or_default(),
7031                                    raw,
7032                                    &mut log_base,
7033                                    &mut orientation,
7034                                    &mut maximum,
7035                                    &mut minimum,
7036                                    &mut raw_children,
7037                                    &mut boundary,
7038                                )?;
7039                            }
7040                            Event::Empty(child) => {
7041                                let name = chart_child_local(&child, &namespaces)?;
7042                                let raw = capture_empty_element(&child)?;
7043                                parse_scaling_child(
7044                                    name.as_deref().unwrap_or_default(),
7045                                    raw,
7046                                    &mut log_base,
7047                                    &mut orientation,
7048                                    &mut maximum,
7049                                    &mut minimum,
7050                                    &mut raw_children,
7051                                    &mut boundary,
7052                                )?;
7053                            }
7054                            event @ (Event::Text(_)
7055                            | Event::CData(_)
7056                            | Event::Comment(_)
7057                            | Event::PI(_)
7058                            | Event::GeneralRef(_)) => {
7059                                raw_children.push(boundary, capture_event(event)?);
7060                            }
7061                            Event::End(end)
7062                                if matches_local_name(end.name().as_ref(), b"scaling") =>
7063                            {
7064                                let (log_base, log_base_markup) = optional_markup(log_base);
7065                                let (orientation, orientation_markup) = orientation
7066                                    .map(|(value, markup)| (value, Some(markup)))
7067                                    .unwrap_or((Orientation::MinMax, None));
7068                                let (maximum, maximum_markup) = optional_markup(maximum);
7069                                let (minimum, minimum_markup) = optional_markup(minimum);
7070                                let scaling = Self {
7071                                    log_base,
7072                                    orientation,
7073                                    maximum,
7074                                    minimum,
7075                                    log_base_markup,
7076                                    orientation_markup,
7077                                    maximum_markup,
7078                                    minimum_markup,
7079                                    raw_attributes,
7080                                    raw_children: raw_children_in_schema_order(&raw_children, 4),
7081                                };
7082                                scaling.validate()?;
7083                                return Ok(scaling);
7084                            }
7085                            Event::Eof => return Err(missing_end("c:scaling")),
7086                            _ => {}
7087                        }
7088                        inner.clear();
7089                    }
7090                }
7091                Event::Empty(element)
7092                    if matches_local_name(element.name().as_ref(), b"scaling") =>
7093                {
7094                    typed_rewrite_bindings(&element, inherited)?;
7095                    return Ok(Self {
7096                        raw_attributes: capture_attributes(&element)?,
7097                        ..Self::default()
7098                    });
7099                }
7100                Event::Start(element) | Event::Empty(element) => {
7101                    return Err(ChartError::UnexpectedElement(element_name(&element)));
7102                }
7103                Event::Eof => return Err(ChartError::MissingElement("c:scaling".to_owned())),
7104                _ => {}
7105            }
7106            buffer.clear();
7107        }
7108    }
7109
7110    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7111        self.validate()?;
7112        let mut start = BytesStart::new("c:scaling");
7113        push_attributes(&mut start, &self.raw_attributes);
7114        writer
7115            .write_event(Event::Start(start))
7116            .map_err(OxmlError::from)?;
7117        emit_raw(writer, self.raw_children.at(0))?;
7118        if let Some(value) = self.log_base {
7119            write_scalar(
7120                writer,
7121                "c:logBase",
7122                &value.to_string(),
7123                self.log_base_markup.as_ref(),
7124            )?;
7125        }
7126        emit_raw(writer, self.raw_children.at(1))?;
7127        if self.orientation != Orientation::MinMax || self.orientation_markup.is_some() {
7128            write_scalar(
7129                writer,
7130                "c:orientation",
7131                self.orientation.as_str(),
7132                self.orientation_markup.as_ref(),
7133            )?;
7134        }
7135        emit_raw(writer, self.raw_children.at(2))?;
7136        if let Some(value) = self.maximum {
7137            write_scalar(
7138                writer,
7139                "c:max",
7140                &value.to_string(),
7141                self.maximum_markup.as_ref(),
7142            )?;
7143        }
7144        emit_raw(writer, self.raw_children.at(3))?;
7145        if let Some(value) = self.minimum {
7146            write_scalar(
7147                writer,
7148                "c:min",
7149                &value.to_string(),
7150                self.minimum_markup.as_ref(),
7151            )?;
7152        }
7153        emit_raw(writer, self.raw_children.at(4))?;
7154        writer
7155            .write_event(Event::End(BytesEnd::new("c:scaling")))
7156            .map_err(OxmlError::from)?;
7157        Ok(())
7158    }
7159
7160    fn validate(&self) -> Result<()> {
7161        if let Some(value) = self.log_base
7162            && (!value.is_finite() || !(2.0..=1000.0).contains(&value))
7163        {
7164            return Err(ChartError::InvalidValue {
7165                element: "c:logBase".to_owned(),
7166                value: value.to_string(),
7167            });
7168        }
7169        for (element, value) in [("c:max", self.maximum), ("c:min", self.minimum)] {
7170            if let Some(value) = value
7171                && !value.is_finite()
7172            {
7173                return Err(ChartError::InvalidValue {
7174                    element: element.to_owned(),
7175                    value: value.to_string(),
7176                });
7177            }
7178        }
7179        if let (Some(minimum), Some(maximum)) = (self.minimum, self.maximum)
7180            && minimum > maximum
7181        {
7182            return Err(ChartError::InvalidValue {
7183                element: "c:scaling".to_owned(),
7184                value: format!("minimum {minimum} exceeds maximum {maximum}"),
7185            });
7186        }
7187        Ok(())
7188    }
7189}
7190
7191#[allow(clippy::too_many_arguments)]
7192fn parse_scaling_child(
7193    name: &[u8],
7194    raw: Vec<u8>,
7195    log_base: &mut Option<(f64, ScalarMarkup)>,
7196    orientation: &mut Option<(Orientation, ScalarMarkup)>,
7197    maximum: &mut Option<(f64, ScalarMarkup)>,
7198    minimum: &mut Option<(f64, ScalarMarkup)>,
7199    raw_children: &mut OrderedRawChildren,
7200    boundary: &mut usize,
7201) -> Result<()> {
7202    match name {
7203        b"logBase" => {
7204            set_once(log_base, parse_f64_scalar(&raw, "logBase")?, "c:logBase")?;
7205            *boundary = (*boundary).max(1);
7206        }
7207        b"orientation" => {
7208            let (value, markup) = scalar_value(&raw, "orientation")?;
7209            let value = value
7210                .ok_or_else(|| invalid_attribute("orientation", "val", "<missing>".to_owned()))?;
7211            let parsed = Orientation::parse(&value)
7212                .ok_or_else(|| invalid_attribute("orientation", "val", value))?;
7213            set_once(orientation, (parsed, markup), "c:orientation")?;
7214            *boundary = (*boundary).max(2);
7215        }
7216        b"max" => {
7217            set_once(maximum, parse_f64_scalar(&raw, "max")?, "c:max")?;
7218            *boundary = (*boundary).max(3);
7219        }
7220        b"min" => {
7221            set_once(minimum, parse_f64_scalar(&raw, "min")?, "c:min")?;
7222            *boundary = (*boundary).max(4);
7223        }
7224        _ => raw_children.push(*boundary, raw),
7225    }
7226    Ok(())
7227}
7228
7229impl ChartLines {
7230    fn from_xml(xml: &[u8], local: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7231        let mut reader = Reader::from_reader(xml);
7232        let mut buffer = Vec::new();
7233        loop {
7234            match reader
7235                .read_event_into(&mut buffer)
7236                .map_err(OxmlError::from)?
7237            {
7238                Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
7239                    let namespaces = typed_rewrite_bindings(&element, inherited)?;
7240                    let raw_attributes = capture_attributes(&element)?;
7241                    let mut sp_pr = None;
7242                    let mut raw_children = OrderedRawChildren::default();
7243                    let mut boundary = 0usize;
7244                    let mut inner = Vec::new();
7245                    loop {
7246                        match reader
7247                            .read_event_into(&mut inner)
7248                            .map_err(OxmlError::from)?
7249                        {
7250                            Event::Start(child) => {
7251                                let name = chart_child_local(&child, &namespaces)?;
7252                                let raw = capture_element(&mut reader, &child)?;
7253                                if name.as_deref() == Some(b"spPr") {
7254                                    parse_chart_lines_shape(&raw, &namespaces, &mut sp_pr)?;
7255                                    boundary = 1;
7256                                } else {
7257                                    raw_children.push(boundary, raw);
7258                                }
7259                            }
7260                            Event::Empty(child) => {
7261                                let name = chart_child_local(&child, &namespaces)?;
7262                                let raw = capture_empty_element(&child)?;
7263                                if name.as_deref() == Some(b"spPr") {
7264                                    parse_chart_lines_shape(&raw, &namespaces, &mut sp_pr)?;
7265                                    boundary = 1;
7266                                } else {
7267                                    raw_children.push(boundary, raw);
7268                                }
7269                            }
7270                            event @ (Event::Text(_)
7271                            | Event::CData(_)
7272                            | Event::Comment(_)
7273                            | Event::PI(_)
7274                            | Event::GeneralRef(_)) => {
7275                                raw_children.push(boundary, capture_event(event)?);
7276                            }
7277                            Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
7278                                return Ok(Self {
7279                                    sp_pr,
7280                                    raw_attributes,
7281                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
7282                                });
7283                            }
7284                            Event::Eof => {
7285                                return Err(missing_end(&format!(
7286                                    "c:{}",
7287                                    String::from_utf8_lossy(local)
7288                                )));
7289                            }
7290                            _ => {}
7291                        }
7292                        inner.clear();
7293                    }
7294                }
7295                Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
7296                    typed_rewrite_bindings(&element, inherited)?;
7297                    return Ok(Self {
7298                        sp_pr: None,
7299                        raw_attributes: capture_attributes(&element)?,
7300                        raw_children: OrderedRawChildren::default(),
7301                    });
7302                }
7303                Event::Start(element) | Event::Empty(element) => {
7304                    return Err(ChartError::UnexpectedElement(element_name(&element)));
7305                }
7306                Event::Eof => {
7307                    return Err(ChartError::MissingElement(format!(
7308                        "c:{}",
7309                        String::from_utf8_lossy(local)
7310                    )));
7311                }
7312                _ => {}
7313            }
7314            buffer.clear();
7315        }
7316    }
7317
7318    fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
7319        let mut start = BytesStart::new(tag);
7320        push_attributes(&mut start, &self.raw_attributes);
7321        if self.sp_pr.is_none() && self.raw_children.is_empty() {
7322            writer
7323                .write_event(Event::Empty(start))
7324                .map_err(OxmlError::from)?;
7325            return Ok(());
7326        }
7327        writer
7328            .write_event(Event::Start(start))
7329            .map_err(OxmlError::from)?;
7330        emit_raw(writer, self.raw_children.at(0))?;
7331        if let Some(properties) = &self.sp_pr {
7332            properties.write_xml_as(writer, "c:spPr")?;
7333        }
7334        emit_raw(writer, self.raw_children.at(1))?;
7335        writer
7336            .write_event(Event::End(BytesEnd::new(tag)))
7337            .map_err(OxmlError::from)?;
7338        Ok(())
7339    }
7340}
7341
7342fn parse_chart_lines_shape(
7343    raw: &[u8],
7344    namespaces: &NamespaceBindings,
7345    slot: &mut Option<CT_ShapeProperties>,
7346) -> Result<()> {
7347    reject_conflicting_prefix_in_xml(raw, b"a", A_NS)?;
7348    let properties = CT_ShapeProperties::from_xml(raw)?;
7349    let mut writer = Writer::new(Vec::new());
7350    properties.write_xml_as(&mut writer, "c:spPr")?;
7351    reject_rewritten_foreign_elements(raw, &writer.into_inner(), namespaces, b"spPr")?;
7352    set_once(slot, properties, "c:spPr")
7353}
7354
7355impl NumberFormat {
7356    pub fn new(format_code: String, source_linked: bool) -> Result<Self> {
7357        let number_format = Self {
7358            format_code,
7359            source_linked,
7360            raw_attributes: Vec::new(),
7361            raw_content: Vec::new(),
7362        };
7363        number_format.validate()?;
7364        Ok(number_format)
7365    }
7366
7367    fn from_xml(xml: &[u8]) -> Result<Self> {
7368        let mut reader = Reader::from_reader(xml);
7369        let mut buffer = Vec::new();
7370        loop {
7371            match reader
7372                .read_event_into(&mut buffer)
7373                .map_err(OxmlError::from)?
7374            {
7375                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
7376                    return Self::from_element(&element, Vec::new());
7377                }
7378                Event::Start(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
7379                    let mut raw_content = Vec::new();
7380                    let mut inner = Vec::new();
7381                    loop {
7382                        match reader
7383                            .read_event_into(&mut inner)
7384                            .map_err(OxmlError::from)?
7385                        {
7386                            Event::End(end)
7387                                if matches_local_name(end.name().as_ref(), b"numFmt") =>
7388                            {
7389                                return Self::from_element(&element, raw_content);
7390                            }
7391                            event @ (Event::Text(_)
7392                            | Event::CData(_)
7393                            | Event::Comment(_)
7394                            | Event::PI(_)
7395                            | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
7396                            Event::Eof => return Err(missing_end("c:numFmt")),
7397                            _ => return Err(ChartError::UnexpectedElement("c:numFmt".to_owned())),
7398                        }
7399                        inner.clear();
7400                    }
7401                }
7402                Event::Start(element) | Event::Empty(element) => {
7403                    return Err(ChartError::UnexpectedElement(element_name(&element)));
7404                }
7405                Event::Eof => return Err(ChartError::MissingElement("c:numFmt".to_owned())),
7406                _ => {}
7407            }
7408            buffer.clear();
7409        }
7410    }
7411
7412    fn from_element(element: &BytesStart<'_>, raw_content: Vec<Vec<u8>>) -> Result<Self> {
7413        reject_conflicting_prefix(element, b"c", C_NS)?;
7414        let format_code = attribute_value(element, b"formatCode")?
7415            .ok_or_else(|| invalid_attribute("numFmt", "formatCode", "<missing>".to_owned()))?;
7416        let source_linked = attribute_value(element, b"sourceLinked")?
7417            .ok_or_else(|| invalid_attribute("numFmt", "sourceLinked", "<missing>".to_owned()))?;
7418        let source_linked = parse_bool_lexical("numFmt", "sourceLinked", &source_linked)?;
7419        let number_format = Self {
7420            format_code,
7421            source_linked,
7422            raw_attributes: capture_attributes_excluding(
7423                element,
7424                &[b"formatCode", b"sourceLinked"],
7425            )?,
7426            raw_content,
7427        };
7428        number_format.validate()?;
7429        Ok(number_format)
7430    }
7431
7432    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7433        self.validate()?;
7434        let mut start = BytesStart::new("c:numFmt");
7435        start.push_attribute(("formatCode", self.format_code.as_str()));
7436        start.push_attribute(("sourceLinked", bool_lexical(self.source_linked)));
7437        push_attributes(&mut start, &self.raw_attributes);
7438        if self.raw_content.is_empty() {
7439            writer
7440                .write_event(Event::Empty(start))
7441                .map_err(OxmlError::from)?;
7442            return Ok(());
7443        }
7444        writer
7445            .write_event(Event::Start(start))
7446            .map_err(OxmlError::from)?;
7447        for raw in &self.raw_content {
7448            writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
7449        }
7450        writer
7451            .write_event(Event::End(BytesEnd::new("c:numFmt")))
7452            .map_err(OxmlError::from)?;
7453        Ok(())
7454    }
7455
7456    /// Projects one cached value through the supported deterministic subset.
7457    pub fn format_value(&self, value: f64) -> Result<String> {
7458        if !value.is_finite() {
7459            return Err(ChartError::InvalidValue {
7460                element: "c:numFmt value".to_owned(),
7461                value: value.to_string(),
7462            });
7463        }
7464        if self.format_code == "General" {
7465            return Ok(value.to_string());
7466        }
7467
7468        let (numeric_code, percentage) = self
7469            .format_code
7470            .strip_suffix('%')
7471            .map(|code| (code, true))
7472            .unwrap_or((&self.format_code, false));
7473        let precision = if numeric_code == "0" {
7474            Some(0)
7475        } else if let Some(decimals) = numeric_code.strip_prefix("0.")
7476            && !decimals.is_empty()
7477            && decimals.bytes().all(|byte| byte == b'0')
7478        {
7479            Some(decimals.len())
7480        } else {
7481            None
7482        };
7483        let Some(precision) = precision else {
7484            return Err(ChartError::InvalidValue {
7485                element: "c:numFmt/@formatCode projection".to_owned(),
7486                value: self.format_code.clone(),
7487            });
7488        };
7489        let projected = if percentage { value * 100.0 } else { value };
7490        if !projected.is_finite() {
7491            return Err(ChartError::InvalidValue {
7492                element: "c:numFmt value".to_owned(),
7493                value: projected.to_string(),
7494            });
7495        }
7496        let mut formatted = format!("{projected:.precision$}");
7497        if percentage {
7498            formatted.push('%');
7499        }
7500        Ok(formatted)
7501    }
7502
7503    fn validate(&self) -> Result<()> {
7504        if self.format_code.is_empty() {
7505            return Err(ChartError::InvalidValue {
7506                element: "c:numFmt/@formatCode".to_owned(),
7507                value: self.format_code.clone(),
7508            });
7509        }
7510        validate_xml_text(&self.format_code, "c:numFmt/@formatCode")
7511    }
7512}
7513
7514impl CT_DLbls {
7515    pub fn from_xml(xml: &[u8]) -> Result<Self> {
7516        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
7517    }
7518
7519    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7520        let mut reader = Reader::from_reader(xml);
7521        let mut buffer = Vec::new();
7522        loop {
7523            match reader
7524                .read_event_into(&mut buffer)
7525                .map_err(OxmlError::from)?
7526            {
7527                Event::Start(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7528                    chart_root_prefix(&element)?;
7529                    if !element_is_in_namespace(&element, C_NS, inherited)? {
7530                        return Err(ChartError::UnexpectedElement(element_name(&element)));
7531                    }
7532                    return Self::from_element(&mut reader, &element, inherited);
7533                }
7534                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7535                    chart_root_prefix(&element)?;
7536                    let namespaces = chart_bindings(inherited, &element)?;
7537                    if !element_is_in_namespace(&element, C_NS, inherited)? {
7538                        return Err(ChartError::UnexpectedElement(element_name(&element)));
7539                    }
7540                    let (raw_attributes, _) = capture_fixed_root_attributes(
7541                        &element,
7542                        &["xmlns:c", "xmlns:a", "xmlns:r"],
7543                    )?;
7544                    return Ok(Self {
7545                        raw_attributes,
7546                        namespace_declarations: standalone_namespace_declarations(&namespaces)?,
7547                        ..Self::default()
7548                    });
7549                }
7550                Event::Start(element) | Event::Empty(element) => {
7551                    return Err(ChartError::UnexpectedElement(element_name(&element)));
7552                }
7553                Event::Eof => return Err(ChartError::MissingElement("c:dLbls".to_owned())),
7554                _ => {}
7555            }
7556            buffer.clear();
7557        }
7558    }
7559
7560    fn from_element(
7561        reader: &mut Reader<&[u8]>,
7562        start: &BytesStart<'_>,
7563        inherited: &NamespaceBindings,
7564    ) -> Result<Self> {
7565        reject_conflicting_prefix(start, b"a", A_NS)?;
7566        let namespaces = chart_bindings(inherited, start)?;
7567        require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
7568        let (raw_attributes, _) =
7569            capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
7570        let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
7571        let mut state = DataLabelsParseState::new(namespaces);
7572        let mut buffer = Vec::new();
7573        loop {
7574            match reader
7575                .read_event_into(&mut buffer)
7576                .map_err(OxmlError::from)?
7577            {
7578                Event::Start(element) => {
7579                    let name = chart_child_local(&element, &state.namespaces)?;
7580                    let raw = capture_element(reader, &element)?;
7581                    state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
7582                }
7583                Event::Empty(element) => {
7584                    let name = chart_child_local(&element, &state.namespaces)?;
7585                    let raw = capture_empty_element(&element)?;
7586                    state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
7587                }
7588                event @ (Event::Text(_)
7589                | Event::CData(_)
7590                | Event::Comment(_)
7591                | Event::PI(_)
7592                | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
7593                Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7594                    return state.finish(raw_attributes, namespace_declarations);
7595                }
7596                Event::Eof => return Err(missing_end("c:dLbls")),
7597                _ => {}
7598            }
7599            buffer.clear();
7600        }
7601    }
7602
7603    pub fn to_xml(&self) -> Result<Vec<u8>> {
7604        let mut writer = Writer::new(Vec::new());
7605        self.write_xml(&mut writer, true)?;
7606        Ok(writer.into_inner())
7607    }
7608
7609    fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
7610        self.validate()?;
7611        let mut start = BytesStart::new("c:dLbls");
7612        if standalone {
7613            start.push_attribute(("xmlns:c", C_NS));
7614            start.push_attribute(("xmlns:a", A_NS));
7615            start.push_attribute(("xmlns:r", R_NS));
7616        }
7617        push_attributes(&mut start, &self.namespace_declarations);
7618        push_attributes(&mut start, &self.raw_attributes);
7619        writer
7620            .write_event(Event::Start(start))
7621            .map_err(OxmlError::from)?;
7622        emit_raw(writer, self.raw_children.at(0))?;
7623        if let Some(number_format) = &self.number_format {
7624            number_format.write_xml(writer)?;
7625        }
7626        emit_raw(writer, self.raw_children.at(1))?;
7627        if let Some(position) = self.position {
7628            write_scalar(
7629                writer,
7630                "c:dLblPos",
7631                position.as_str(),
7632                self.position_markup.as_ref(),
7633            )?;
7634        }
7635        emit_raw(writer, self.raw_children.at(2))?;
7636        write_optional_bool(
7637            writer,
7638            "c:showLegendKey",
7639            self.show_legend_key,
7640            self.show_legend_key_markup.as_ref(),
7641        )?;
7642        emit_raw(writer, self.raw_children.at(3))?;
7643        write_optional_bool(
7644            writer,
7645            "c:showVal",
7646            self.show_value,
7647            self.show_value_markup.as_ref(),
7648        )?;
7649        emit_raw(writer, self.raw_children.at(4))?;
7650        write_optional_bool(
7651            writer,
7652            "c:showCatName",
7653            self.show_category_name,
7654            self.show_category_name_markup.as_ref(),
7655        )?;
7656        emit_raw(writer, self.raw_children.at(5))?;
7657        write_optional_bool(
7658            writer,
7659            "c:showSerName",
7660            self.show_series_name,
7661            self.show_series_name_markup.as_ref(),
7662        )?;
7663        emit_raw(writer, self.raw_children.at(6))?;
7664        write_optional_bool(
7665            writer,
7666            "c:showPercent",
7667            self.show_percent,
7668            self.show_percent_markup.as_ref(),
7669        )?;
7670        emit_raw(writer, self.raw_children.at(7))?;
7671        write_optional_bool(
7672            writer,
7673            "c:showBubbleSize",
7674            self.show_bubble_size,
7675            self.show_bubble_size_markup.as_ref(),
7676        )?;
7677        emit_raw(writer, self.raw_children.at(8))?;
7678        if let Some(separator) = &self.separator {
7679            write_text(
7680                writer,
7681                "c:separator",
7682                separator,
7683                self.separator_markup
7684                    .as_ref()
7685                    .unwrap_or(&TextMarkup::default()),
7686            )?;
7687        }
7688        emit_raw(writer, self.raw_children.at(9))?;
7689        writer
7690            .write_event(Event::End(BytesEnd::new("c:dLbls")))
7691            .map_err(OxmlError::from)?;
7692        Ok(())
7693    }
7694
7695    fn validate(&self) -> Result<()> {
7696        if let Some(number_format) = &self.number_format {
7697            number_format.validate()?;
7698        }
7699        if let Some(separator) = &self.separator {
7700            validate_xml_text(separator, "c:separator")?;
7701        }
7702        Ok(())
7703    }
7704
7705    pub fn raw_children(&self) -> &OrderedRawChildren {
7706        &self.raw_children
7707    }
7708}
7709
7710struct DataLabelsParseState {
7711    namespaces: NamespaceBindings,
7712    number_format: Option<NumberFormat>,
7713    position: Option<(DataLabelPosition, ScalarMarkup)>,
7714    show_legend_key: Option<(bool, ScalarMarkup)>,
7715    show_value: Option<(bool, ScalarMarkup)>,
7716    show_category_name: Option<(bool, ScalarMarkup)>,
7717    show_series_name: Option<(bool, ScalarMarkup)>,
7718    show_percent: Option<(bool, ScalarMarkup)>,
7719    show_bubble_size: Option<(bool, ScalarMarkup)>,
7720    separator: Option<(String, TextMarkup)>,
7721    raw_children: OrderedRawChildren,
7722    boundary: usize,
7723}
7724
7725impl DataLabelsParseState {
7726    fn new(namespaces: NamespaceBindings) -> Self {
7727        Self {
7728            namespaces,
7729            number_format: None,
7730            position: None,
7731            show_legend_key: None,
7732            show_value: None,
7733            show_category_name: None,
7734            show_series_name: None,
7735            show_percent: None,
7736            show_bubble_size: None,
7737            separator: None,
7738            raw_children: OrderedRawChildren::default(),
7739            boundary: 0,
7740        }
7741    }
7742
7743    fn capture_event(&mut self, raw: Vec<u8>) {
7744        self.raw_children.push(self.boundary, raw);
7745    }
7746
7747    fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
7748        match name {
7749            b"numFmt" => {
7750                set_once(
7751                    &mut self.number_format,
7752                    NumberFormat::from_xml(&raw)?,
7753                    "c:numFmt",
7754                )?;
7755                self.boundary = self.boundary.max(1);
7756            }
7757            b"dLblPos" => {
7758                let (value, markup) = scalar_value(&raw, "dLblPos")?;
7759                let lexical = value
7760                    .ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
7761                let position = DataLabelPosition::parse(&lexical)
7762                    .ok_or_else(|| invalid_attribute("dLblPos", "val", lexical))?;
7763                set_once(&mut self.position, (position, markup), "c:dLblPos")?;
7764                self.boundary = self.boundary.max(2);
7765            }
7766            b"showLegendKey" => {
7767                set_once(
7768                    &mut self.show_legend_key,
7769                    parse_bool_value(&raw, "showLegendKey")?,
7770                    "c:showLegendKey",
7771                )?;
7772                self.boundary = self.boundary.max(3);
7773            }
7774            b"showVal" => {
7775                set_once(
7776                    &mut self.show_value,
7777                    parse_bool_value(&raw, "showVal")?,
7778                    "c:showVal",
7779                )?;
7780                self.boundary = self.boundary.max(4);
7781            }
7782            b"showCatName" => {
7783                set_once(
7784                    &mut self.show_category_name,
7785                    parse_bool_value(&raw, "showCatName")?,
7786                    "c:showCatName",
7787                )?;
7788                self.boundary = self.boundary.max(5);
7789            }
7790            b"showSerName" => {
7791                set_once(
7792                    &mut self.show_series_name,
7793                    parse_bool_value(&raw, "showSerName")?,
7794                    "c:showSerName",
7795                )?;
7796                self.boundary = self.boundary.max(6);
7797            }
7798            b"showPercent" => {
7799                set_once(
7800                    &mut self.show_percent,
7801                    parse_bool_value(&raw, "showPercent")?,
7802                    "c:showPercent",
7803                )?;
7804                self.boundary = self.boundary.max(7);
7805            }
7806            b"showBubbleSize" => {
7807                set_once(
7808                    &mut self.show_bubble_size,
7809                    parse_bool_value(&raw, "showBubbleSize")?,
7810                    "c:showBubbleSize",
7811                )?;
7812                self.boundary = self.boundary.max(8);
7813            }
7814            b"separator" => {
7815                set_once(
7816                    &mut self.separator,
7817                    parse_text_element(&raw, b"separator", &self.namespaces)?,
7818                    "c:separator",
7819                )?;
7820                self.boundary = self.boundary.max(9);
7821            }
7822            _ => {
7823                let boundary = data_labels_raw_boundary(name, self.boundary);
7824                self.raw_children.push(boundary, raw);
7825                self.boundary = self.boundary.max(boundary);
7826            }
7827        }
7828        Ok(())
7829    }
7830
7831    fn finish(
7832        self,
7833        raw_attributes: XmlAttributes,
7834        namespace_declarations: XmlAttributes,
7835    ) -> Result<CT_DLbls> {
7836        let (position, position_markup) = optional_markup(self.position);
7837        let (show_legend_key, show_legend_key_markup) = optional_bool_markup(self.show_legend_key);
7838        let (show_value, show_value_markup) = optional_bool_markup(self.show_value);
7839        let (show_category_name, show_category_name_markup) =
7840            optional_bool_markup(self.show_category_name);
7841        let (show_series_name, show_series_name_markup) =
7842            optional_bool_markup(self.show_series_name);
7843        let (show_percent, show_percent_markup) = optional_bool_markup(self.show_percent);
7844        let (show_bubble_size, show_bubble_size_markup) =
7845            optional_bool_markup(self.show_bubble_size);
7846        let (separator, separator_markup) = self
7847            .separator
7848            .map(|(value, markup)| (Some(value), Some(markup)))
7849            .unwrap_or((None, None));
7850        let labels = CT_DLbls {
7851            number_format: self.number_format,
7852            position,
7853            separator,
7854            show_legend_key,
7855            show_value,
7856            show_category_name,
7857            show_series_name,
7858            show_percent,
7859            show_bubble_size,
7860            position_markup,
7861            show_legend_key_markup,
7862            show_value_markup,
7863            show_category_name_markup,
7864            show_series_name_markup,
7865            show_percent_markup,
7866            show_bubble_size_markup,
7867            separator_markup,
7868            raw_attributes,
7869            namespace_declarations,
7870            raw_children: raw_children_in_schema_order(&self.raw_children, 9),
7871        };
7872        labels.validate()?;
7873        Ok(labels)
7874    }
7875}
7876
7877fn data_labels_raw_boundary(name: &[u8], current: usize) -> usize {
7878    match name {
7879        b"dLbl" | b"delete" => 0,
7880        b"spPr" | b"txPr" => 1,
7881        b"showLeaderLines" | b"leaderLines" | b"extLst" => 9,
7882        _ => current,
7883    }
7884}
7885
7886fn optional_bool_markup(value: Option<(bool, ScalarMarkup)>) -> (bool, Option<ScalarMarkup>) {
7887    value
7888        .map(|(value, markup)| (value, Some(markup)))
7889        .unwrap_or((false, None))
7890}
7891
7892fn write_optional_bool(
7893    writer: &mut Writer<Vec<u8>>,
7894    tag: &str,
7895    value: bool,
7896    markup: Option<&ScalarMarkup>,
7897) -> Result<()> {
7898    if value || markup.is_some() {
7899        write_scalar(writer, tag, bool_lexical(value), markup)?;
7900    }
7901    Ok(())
7902}
7903
7904fn default_axis_id_markup(id: AxisId) -> AxisIdMarkup {
7905    AxisIdMarkup {
7906        scalar: ScalarMarkup::default(),
7907        parsed: id,
7908        lexical: id.value().to_string(),
7909    }
7910}
7911
7912fn parse_axis_id_scalar(xml: &[u8], local: &str) -> Result<(AxisId, AxisIdMarkup)> {
7913    let (value, scalar) = scalar_value(xml, local)?;
7914    let lexical = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
7915    let parsed = lexical
7916        .parse::<i64>()
7917        .map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
7918    let id = AxisId::new(parsed).map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
7919    Ok((
7920        id,
7921        AxisIdMarkup {
7922            scalar,
7923            parsed: id,
7924            lexical,
7925        },
7926    ))
7927}
7928
7929fn write_axis_id<W: Write>(
7930    writer: &mut Writer<W>,
7931    tag: &str,
7932    id: AxisId,
7933    markup: &AxisIdMarkup,
7934) -> Result<()> {
7935    AxisId::new(id.value())?;
7936    let normalized;
7937    let value = if id == markup.parsed {
7938        markup.lexical.as_str()
7939    } else {
7940        normalized = id.value().to_string();
7941        normalized.as_str()
7942    };
7943    write_scalar(writer, tag, value, Some(&markup.scalar))
7944}
7945
7946fn parse_f64_scalar(xml: &[u8], local: &str) -> Result<(f64, ScalarMarkup)> {
7947    let (value, markup) = scalar_value(xml, local)?;
7948    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
7949    let parsed = value
7950        .parse::<f64>()
7951        .map_err(|_| invalid_attribute(local, "val", value.clone()))?;
7952    if !parsed.is_finite() {
7953        return Err(invalid_attribute(local, "val", value));
7954    }
7955    Ok((parsed, markup))
7956}
7957
7958fn parse_axis_position(xml: &[u8]) -> Result<(AxisPosition, ScalarMarkup)> {
7959    let (value, markup) = scalar_value(xml, "axPos")?;
7960    let value = value.ok_or_else(|| invalid_attribute("axPos", "val", "<missing>".to_owned()))?;
7961    let parsed =
7962        AxisPosition::parse(&value).ok_or_else(|| invalid_attribute("axPos", "val", value))?;
7963    Ok((parsed, markup))
7964}
7965
7966fn parse_tick_mark(xml: &[u8], local: &str) -> Result<(TickMark, ScalarMarkup)> {
7967    let (value, markup) = scalar_value(xml, local)?;
7968    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
7969    let parsed = TickMark::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
7970    Ok((parsed, markup))
7971}
7972
7973fn parse_tick_label_position(xml: &[u8]) -> Result<(TickLabelPosition, ScalarMarkup)> {
7974    let (value, markup) = scalar_value(xml, "tickLblPos")?;
7975    let value =
7976        value.ok_or_else(|| invalid_attribute("tickLblPos", "val", "<missing>".to_owned()))?;
7977    let parsed = TickLabelPosition::parse(&value)
7978        .ok_or_else(|| invalid_attribute("tickLblPos", "val", value))?;
7979    Ok((parsed, markup))
7980}
7981
7982fn optional_markup<T>(value: Option<(T, ScalarMarkup)>) -> (Option<T>, Option<ScalarMarkup>) {
7983    value
7984        .map(|(value, markup)| (Some(value), Some(markup)))
7985        .unwrap_or((None, None))
7986}
7987
7988fn optional_scalar_markup_eq(left: &Option<ScalarMarkup>, right: &Option<ScalarMarkup>) -> bool {
7989    let empty = ScalarMarkup::default();
7990    left.as_ref().unwrap_or(&empty) == right.as_ref().unwrap_or(&empty)
7991}
7992
7993fn axis_id_markup_eq(left: &AxisIdMarkup, right: &AxisIdMarkup) -> bool {
7994    left.scalar == right.scalar
7995}
7996
7997const fn bool_lexical(value: bool) -> &'static str {
7998    if value { "1" } else { "0" }
7999}
8000
8001fn parse_bool_lexical(element: &str, attribute: &str, value: &str) -> Result<bool> {
8002    match value {
8003        "1" | "true" => Ok(true),
8004        "0" | "false" => Ok(false),
8005        _ => Err(invalid_attribute(element, attribute, value.to_owned())),
8006    }
8007}
8008
8009impl CT_Title {
8010    fn from_xml(xml: &[u8]) -> Result<Self> {
8011        let (raw_attributes, raw_children) = parse_raw_shell(xml, b"title", "c:title")?;
8012        Ok(Self {
8013            raw_attributes,
8014            raw_children,
8015        })
8016    }
8017
8018    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8019        write_raw_shell(writer, "c:title", &self.raw_attributes, &self.raw_children)
8020    }
8021
8022    pub fn raw_children(&self) -> &OrderedRawChildren {
8023        &self.raw_children
8024    }
8025}
8026
8027impl CT_PlotArea {
8028    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
8029        let (raw_attributes, captured_children) = parse_raw_shell(xml, b"plotArea", "c:plotArea")?;
8030        let namespace_bindings = root_chart_bindings(xml, b"plotArea", inherited)?;
8031        let direct_roots: Vec<_> = captured_children
8032            .at(0)
8033            .filter_map(|raw| chart_root_local(raw, &namespace_bindings).transpose())
8034            .collect::<Result<_>>()?;
8035        let plot_roots: Vec<_> = direct_roots
8036            .iter()
8037            .filter(|local| is_plot_container(local))
8038            .cloned()
8039            .collect();
8040        let typed_local = (plot_roots.len() == 1
8041            && matches!(
8042                plot_roots[0].as_slice(),
8043                b"barChart"
8044                    | b"lineChart"
8045                    | b"pieChart"
8046                    | b"doughnutChart"
8047                    | b"areaChart"
8048                    | b"scatterChart"
8049                    | b"radarChart"
8050            ))
8051        .then(|| plot_roots[0].clone());
8052
8053        let Some(typed_local) = typed_local else {
8054            return Ok(Self {
8055                raw_attributes,
8056                raw_children: captured_children,
8057                namespace_bindings,
8058                plots: None,
8059                plot_markup: Vec::new(),
8060                axes: Vec::new(),
8061            });
8062        };
8063
8064        let mut raw_children = OrderedRawChildren::default();
8065        let mut plots = Vec::new();
8066        let mut plot_markup = Vec::new();
8067        let mut axes = Vec::new();
8068        let mut boundary = 0usize;
8069        for raw in captured_children.at(0) {
8070            let local = chart_root_local(raw, &namespace_bindings)?;
8071            if local.as_deref() == Some(typed_local.as_slice()) {
8072                let (plot, markup) = parse_plot(raw, &namespace_bindings)?;
8073                plots.push(plot);
8074                plot_markup.push(markup);
8075                boundary = 1;
8076            } else if local.as_deref().and_then(AxisKind::parse).is_some() {
8077                axes.push(Axis::from_xml_with_namespaces(raw, &namespace_bindings)?);
8078                boundary = axes.len() + 1;
8079            } else {
8080                raw_children.push(boundary, raw.to_vec());
8081            }
8082        }
8083        let plot_area = Self {
8084            raw_attributes,
8085            raw_children,
8086            namespace_bindings,
8087            plots: Some(plots),
8088            plot_markup,
8089            axes,
8090        };
8091        plot_area.validate_typed()?;
8092        Ok(plot_area)
8093    }
8094
8095    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8096        let Some(plots) = &self.plots else {
8097            return write_raw_shell(
8098                writer,
8099                "c:plotArea",
8100                &self.raw_attributes,
8101                &self.raw_children,
8102            );
8103        };
8104        self.validate_typed()?;
8105        let mut start = BytesStart::new("c:plotArea");
8106        push_attributes(&mut start, &self.raw_attributes);
8107        writer
8108            .write_event(Event::Start(start))
8109            .map_err(OxmlError::from)?;
8110        emit_raw(writer, self.raw_children.at(0))?;
8111        for (index, plot) in plots.iter().enumerate() {
8112            write_plot(
8113                writer,
8114                plot,
8115                self.plot_markup
8116                    .get(index)
8117                    .unwrap_or(&PlotMarkup::default()),
8118            )?;
8119        }
8120        emit_raw(writer, self.raw_children.at(1))?;
8121        for (index, axis) in self.axes.iter().enumerate() {
8122            writer
8123                .get_mut()
8124                .write_all(&axis.to_xml()?)
8125                .map_err(OxmlError::from)?;
8126            emit_raw(writer, self.raw_children.at(index + 2))?;
8127        }
8128        writer
8129            .write_event(Event::End(BytesEnd::new("c:plotArea")))
8130            .map_err(OxmlError::from)?;
8131        Ok(())
8132    }
8133
8134    pub fn raw_children(&self) -> &OrderedRawChildren {
8135        &self.raw_children
8136    }
8137
8138    /// Creates a supported single-family plot area with owned axes.
8139    pub fn new(plots: Vec<Plot>, axes: Vec<Axis>) -> Result<Self> {
8140        let plot_area = Self {
8141            raw_attributes: Vec::new(),
8142            raw_children: OrderedRawChildren::default(),
8143            namespace_bindings: chart_namespace_defaults(),
8144            plot_markup: vec![PlotMarkup::default(); plots.len()],
8145            plots: Some(plots),
8146            axes,
8147        };
8148        plot_area.validate_typed()?;
8149        Ok(plot_area)
8150    }
8151
8152    /// Returns the owned supported plots, or an error for an opaque choice.
8153    pub fn plots(&self) -> Result<&[Plot]> {
8154        self.plots
8155            .as_deref()
8156            .ok_or_else(|| ChartError::InvalidValue {
8157                element: "c:plotArea".to_owned(),
8158                value: "unsupported or combination plot area is opaque".to_owned(),
8159            })
8160    }
8161
8162    /// Returns mutable supported plots without exposing opaque content to edits.
8163    pub fn plots_mut(&mut self) -> Result<&mut [Plot]> {
8164        self.plots
8165            .as_deref_mut()
8166            .ok_or_else(|| ChartError::InvalidValue {
8167                element: "c:plotArea".to_owned(),
8168                value: "cannot combine typed plots with an opaque plot choice".to_owned(),
8169            })
8170    }
8171
8172    /// Parses the common series payloads nested in category-based plot shells.
8173    pub fn series(&self) -> Result<Vec<Series>> {
8174        if let Some(plots) = &self.plots {
8175            return Ok(plots
8176                .iter()
8177                .flat_map(|plot| match plot {
8178                    Plot::Bar { series, .. }
8179                    | Plot::Line { series, .. }
8180                    | Plot::Pie { series, .. }
8181                    | Plot::Doughnut { series, .. }
8182                    | Plot::Area { series, .. }
8183                    | Plot::Scatter { series, .. }
8184                    | Plot::Radar { series, .. } => series.clone(),
8185                })
8186                .collect());
8187        }
8188        let mut series = Vec::new();
8189        for raw in self.raw_children.at(0) {
8190            parse_plot_series(raw, &self.namespace_bindings, &mut series)?;
8191        }
8192        Ok(series)
8193    }
8194
8195    /// Parses and validates the direct axis children of this plot area.
8196    pub fn axes(&self) -> Result<Vec<Axis>> {
8197        if self.plots.is_some() {
8198            self.validate_typed()?;
8199            return Ok(self.axes.clone());
8200        }
8201        let mut axes = Vec::new();
8202        for raw in self.raw_children.at(0) {
8203            if let Some(axis) = parse_plot_axis(raw, &self.namespace_bindings)? {
8204                axes.push(axis);
8205            }
8206        }
8207        validate_axis_pairs(&axes)?;
8208        Ok(axes)
8209    }
8210
8211    fn validate_typed(&self) -> Result<()> {
8212        let plots = self
8213            .plots
8214            .as_ref()
8215            .ok_or_else(|| ChartError::InvalidValue {
8216                element: "c:plotArea".to_owned(),
8217                value: "opaque plot area has no typed validation view".to_owned(),
8218            })?;
8219        if plots.len() != 1 {
8220            return Err(ChartError::InvalidValue {
8221                element: "c:plotArea".to_owned(),
8222                value: "typed plot area requires exactly one plot family".to_owned(),
8223            });
8224        }
8225        validate_axis_pairs(&self.axes)?;
8226        for plot in plots {
8227            plot.validate()?;
8228            if let Some(axis_ids) = plot.axis_ids() {
8229                for id in axis_ids {
8230                    if !self.axes.iter().any(|axis| axis.id == id) {
8231                        return Err(ChartError::InvalidValue {
8232                            element: "c:axId".to_owned(),
8233                            value: format!("plot references missing axis {}", id.value()),
8234                        });
8235                    }
8236                }
8237            } else if !self.axes.is_empty() {
8238                return Err(ChartError::InvalidValue {
8239                    element: "c:plotArea".to_owned(),
8240                    value: "pie and doughnut plot areas must not own axes".to_owned(),
8241                });
8242            }
8243            if plot.axis_ids().is_some() && self.axes.len() != 2 {
8244                return Err(ChartError::InvalidValue {
8245                    element: "c:plotArea".to_owned(),
8246                    value: format!(
8247                        "axis-owned plot requires exactly 2 axes, found {}",
8248                        self.axes.len()
8249                    ),
8250                });
8251            }
8252        }
8253        Ok(())
8254    }
8255}
8256
8257impl Default for CT_PlotArea {
8258    fn default() -> Self {
8259        Self {
8260            raw_attributes: Vec::new(),
8261            raw_children: OrderedRawChildren::default(),
8262            namespace_bindings: chart_namespace_defaults(),
8263            plots: Some(Vec::new()),
8264            plot_markup: Vec::new(),
8265            axes: Vec::new(),
8266        }
8267    }
8268}
8269
8270fn chart_root_local(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Vec<u8>>> {
8271    let mut reader = Reader::from_reader(xml);
8272    let mut buffer = Vec::new();
8273    loop {
8274        match reader
8275            .read_event_into(&mut buffer)
8276            .map_err(OxmlError::from)?
8277        {
8278            Event::Start(element) | Event::Empty(element) => {
8279                return Ok(element_is_in_namespace(&element, C_NS, inherited)?
8280                    .then(|| local_name(element.name().as_ref()).to_vec()));
8281            }
8282            Event::Eof => return Ok(None),
8283            _ => {}
8284        }
8285        buffer.clear();
8286    }
8287}
8288
8289fn is_plot_container(local: &[u8]) -> bool {
8290    matches!(
8291        local,
8292        b"areaChart"
8293            | b"area3DChart"
8294            | b"barChart"
8295            | b"bar3DChart"
8296            | b"bubbleChart"
8297            | b"doughnutChart"
8298            | b"lineChart"
8299            | b"line3DChart"
8300            | b"ofPieChart"
8301            | b"pieChart"
8302            | b"pie3DChart"
8303            | b"radarChart"
8304            | b"scatterChart"
8305            | b"stockChart"
8306            | b"surfaceChart"
8307            | b"surface3DChart"
8308    )
8309}
8310
8311fn parse_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8312    let local = chart_root_local(xml, inherited)?
8313        .ok_or_else(|| ChartError::MissingElement("c:barChart or c:lineChart".to_owned()))?;
8314    match local.as_slice() {
8315        b"barChart" => parse_bar_plot(xml, inherited),
8316        b"lineChart" => parse_line_plot(xml, inherited),
8317        b"pieChart" | b"doughnutChart" | b"areaChart" | b"scatterChart" | b"radarChart" => {
8318            parse_remaining_plot(xml, inherited, &local)
8319        }
8320        _ => Err(ChartError::UnexpectedElement(format!(
8321            "c:{}",
8322            String::from_utf8_lossy(&local)
8323        ))),
8324    }
8325}
8326
8327fn parse_remaining_plot(
8328    xml: &[u8],
8329    inherited: &NamespaceBindings,
8330    local: &[u8],
8331) -> Result<(Plot, PlotMarkup)> {
8332    let root = String::from_utf8_lossy(local).into_owned();
8333    let mut reader = Reader::from_reader(xml);
8334    let mut buffer = Vec::new();
8335    loop {
8336        match reader
8337            .read_event_into(&mut buffer)
8338            .map_err(OxmlError::from)?
8339        {
8340            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
8341                let namespaces = typed_rewrite_bindings(&element, inherited)?;
8342                let (mut raw_attributes, namespace_declarations) =
8343                    capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8344                raw_attributes.splice(0..0, namespace_declarations);
8345                let mut grouping = None;
8346                let mut style = None;
8347                let mut first_slice_angle = None;
8348                let mut hole_size = None;
8349                let mut series = Vec::new();
8350                let mut data_labels = None;
8351                let mut axis_ids = Vec::new();
8352                let mut raw_children = OrderedRawChildren::default();
8353                let mut boundary = 0usize;
8354                let mut inner = Vec::new();
8355                loop {
8356                    match reader
8357                        .read_event_into(&mut inner)
8358                        .map_err(OxmlError::from)?
8359                    {
8360                        Event::Start(child) => {
8361                            let name = chart_child_local(&child, &namespaces)?;
8362                            let raw = capture_element(&mut reader, &child)?;
8363                            parse_remaining_plot_child(
8364                                local,
8365                                name.as_deref().unwrap_or_default(),
8366                                raw,
8367                                &namespaces,
8368                                &mut grouping,
8369                                &mut style,
8370                                &mut first_slice_angle,
8371                                &mut hole_size,
8372                                &mut series,
8373                                &mut data_labels,
8374                                &mut axis_ids,
8375                                &mut raw_children,
8376                                &mut boundary,
8377                            )?;
8378                        }
8379                        Event::Empty(child) => {
8380                            let name = chart_child_local(&child, &namespaces)?;
8381                            let raw = capture_empty_element(&child)?;
8382                            parse_remaining_plot_child(
8383                                local,
8384                                name.as_deref().unwrap_or_default(),
8385                                raw,
8386                                &namespaces,
8387                                &mut grouping,
8388                                &mut style,
8389                                &mut first_slice_angle,
8390                                &mut hole_size,
8391                                &mut series,
8392                                &mut data_labels,
8393                                &mut axis_ids,
8394                                &mut raw_children,
8395                                &mut boundary,
8396                            )?;
8397                        }
8398                        event @ (Event::Text(_)
8399                        | Event::CData(_)
8400                        | Event::Comment(_)
8401                        | Event::PI(_)
8402                        | Event::GeneralRef(_)) => {
8403                            raw_children.push(boundary, capture_event(event)?);
8404                        }
8405                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
8406                            let needs_axes =
8407                                matches!(local, b"areaChart" | b"scatterChart" | b"radarChart");
8408                            if needs_axes && axis_ids.len() != 2 {
8409                                return Err(ChartError::InvalidValue {
8410                                    element: format!("c:{root}/c:axId"),
8411                                    value: format!("expected 2, found {}", axis_ids.len()),
8412                                });
8413                            }
8414                            if !needs_axes && !axis_ids.is_empty() {
8415                                return Err(ChartError::InvalidValue {
8416                                    element: format!("c:{root}/c:axId"),
8417                                    value: "axis-free plot contains axis references".to_owned(),
8418                                });
8419                            }
8420                            let parsed_ids = needs_axes.then(|| [axis_ids[0].0, axis_ids[1].0]);
8421                            let original_series_keys = series
8422                                .iter()
8423                                .map(|item| (item.index, item.order))
8424                                .collect::<Vec<_>>();
8425                            let plot = match local {
8426                                b"pieChart" => Plot::Pie {
8427                                    first_slice_angle: first_slice_angle
8428                                        .as_ref()
8429                                        .map_or(0, |item| item.0),
8430                                    series,
8431                                    data_labels,
8432                                },
8433                                b"doughnutChart" => Plot::Doughnut {
8434                                    first_slice_angle: first_slice_angle
8435                                        .as_ref()
8436                                        .map_or(0, |item| item.0),
8437                                    hole_size: hole_size.as_ref().map_or(50, |item| item.0),
8438                                    series,
8439                                    data_labels,
8440                                },
8441                                b"areaChart" => Plot::Area {
8442                                    grouping: grouping
8443                                        .as_ref()
8444                                        .ok_or_else(|| {
8445                                            ChartError::MissingElement("c:grouping".to_owned())
8446                                        })?
8447                                        .0,
8448                                    series,
8449                                    data_labels,
8450                                    axis_ids: parsed_ids.ok_or_else(|| {
8451                                        ChartError::MissingElement("c:areaChart/c:axId".to_owned())
8452                                    })?,
8453                                },
8454                                b"scatterChart" => {
8455                                    let lexical = style
8456                                        .as_ref()
8457                                        .ok_or_else(|| {
8458                                            ChartError::MissingElement("c:scatterStyle".to_owned())
8459                                        })?
8460                                        .0
8461                                        .as_str();
8462                                    Plot::Scatter {
8463                                        style: ScatterStyle::parse(lexical).ok_or_else(|| {
8464                                            invalid_attribute(
8465                                                "scatterStyle",
8466                                                "val",
8467                                                lexical.to_owned(),
8468                                            )
8469                                        })?,
8470                                        series,
8471                                        data_labels,
8472                                        axis_ids: parsed_ids.ok_or_else(|| {
8473                                            ChartError::MissingElement(
8474                                                "c:scatterChart/c:axId".to_owned(),
8475                                            )
8476                                        })?,
8477                                    }
8478                                }
8479                                b"radarChart" => {
8480                                    let lexical = style
8481                                        .as_ref()
8482                                        .ok_or_else(|| {
8483                                            ChartError::MissingElement("c:radarStyle".to_owned())
8484                                        })?
8485                                        .0
8486                                        .as_str();
8487                                    Plot::Radar {
8488                                        style: RadarStyle::parse(lexical).ok_or_else(|| {
8489                                            invalid_attribute(
8490                                                "radarStyle",
8491                                                "val",
8492                                                lexical.to_owned(),
8493                                            )
8494                                        })?,
8495                                        series,
8496                                        data_labels,
8497                                        axis_ids: parsed_ids.ok_or_else(|| {
8498                                            ChartError::MissingElement(
8499                                                "c:radarChart/c:axId".to_owned(),
8500                                            )
8501                                        })?,
8502                                    }
8503                                }
8504                                _ => {
8505                                    return Err(ChartError::UnexpectedElement(format!(
8506                                        "c:{}",
8507                                        String::from_utf8_lossy(local)
8508                                    )));
8509                                }
8510                            };
8511                            plot.validate()?;
8512                            let final_boundary = boundary.max(original_series_keys.len() + 6);
8513                            return Ok((
8514                                plot,
8515                                PlotMarkup {
8516                                    raw_attributes,
8517                                    raw_children: raw_children_in_schema_order(
8518                                        &raw_children,
8519                                        final_boundary,
8520                                    ),
8521                                    grouping: grouping.map(|item| item.1),
8522                                    first_slice_angle: first_slice_angle.map(|item| item.1),
8523                                    hole_size: hole_size.map(|item| item.1),
8524                                    style: style.map(|item| item.1),
8525                                    axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8526                                    original_series_keys,
8527                                    original_axis_ids: parsed_ids
8528                                        .map_or_else(Vec::new, |ids| ids.to_vec()),
8529                                    parsed_remaining: match local {
8530                                        b"pieChart" => Some("pieChart"),
8531                                        b"doughnutChart" => Some("doughnutChart"),
8532                                        b"areaChart" => Some("areaChart"),
8533                                        b"scatterChart" => Some("scatterChart"),
8534                                        b"radarChart" => Some("radarChart"),
8535                                        _ => None,
8536                                    },
8537                                    ..PlotMarkup::default()
8538                                },
8539                            ));
8540                        }
8541                        Event::Eof => return Err(missing_end(&format!("c:{root}"))),
8542                        _ => {}
8543                    }
8544                    inner.clear();
8545                }
8546            }
8547            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
8548                return Err(ChartError::MissingElement("c:ser".to_owned()));
8549            }
8550            Event::Start(element) | Event::Empty(element) => {
8551                return Err(ChartError::UnexpectedElement(element_name(&element)));
8552            }
8553            Event::Eof => return Err(ChartError::MissingElement(format!("c:{root}"))),
8554            _ => {}
8555        }
8556        buffer.clear();
8557    }
8558}
8559
8560#[allow(clippy::too_many_arguments)]
8561fn parse_remaining_plot_child(
8562    root: &[u8],
8563    name: &[u8],
8564    raw: Vec<u8>,
8565    namespaces: &NamespaceBindings,
8566    grouping: &mut Option<(Grouping, ScalarMarkup)>,
8567    style: &mut Option<(String, ScalarMarkup)>,
8568    first_slice_angle: &mut Option<(u16, ScalarMarkup)>,
8569    hole_size: &mut Option<(u8, ScalarMarkup)>,
8570    series: &mut Vec<Series>,
8571    data_labels: &mut Option<CT_DLbls>,
8572    axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
8573    raw_children: &mut OrderedRawChildren,
8574    boundary: &mut usize,
8575) -> Result<()> {
8576    let property_first = matches!(root, b"areaChart" | b"scatterChart" | b"radarChart");
8577    let base = usize::from(property_first);
8578    match name {
8579        b"grouping" if root == b"areaChart" => {
8580            let (value, markup) = required_scalar(&raw, "grouping")?;
8581            let parsed = Grouping::parse(&value)
8582                .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
8583            set_once(grouping, (parsed, markup), "c:grouping")?;
8584            *boundary = (*boundary).max(1);
8585        }
8586        b"scatterStyle" if root == b"scatterChart" => {
8587            let (value, markup) = required_scalar(&raw, "scatterStyle")?;
8588            if ScatterStyle::parse(&value).is_none() {
8589                return Err(invalid_attribute("scatterStyle", "val", value));
8590            }
8591            set_once(style, (value, markup), "c:scatterStyle")?;
8592            *boundary = (*boundary).max(1);
8593        }
8594        b"radarStyle" if root == b"radarChart" => {
8595            let (value, markup) = required_scalar(&raw, "radarStyle")?;
8596            if RadarStyle::parse(&value).is_none() {
8597                return Err(invalid_attribute("radarStyle", "val", value));
8598            }
8599            set_once(style, (value, markup), "c:radarStyle")?;
8600            *boundary = (*boundary).max(1);
8601        }
8602        b"ser" => {
8603            series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
8604            *boundary = (*boundary).max(base + series.len());
8605        }
8606        b"dLbls" => {
8607            set_once(
8608                data_labels,
8609                CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
8610                "c:dLbls",
8611            )?;
8612            *boundary = (*boundary).max(base + series.len() + 1);
8613        }
8614        b"firstSliceAng" if matches!(root, b"pieChart" | b"doughnutChart") => {
8615            let (value, markup) = required_scalar(&raw, "firstSliceAng")?;
8616            let parsed = value
8617                .parse::<u16>()
8618                .map_err(|_| invalid_attribute("firstSliceAng", "val", value.clone()))?;
8619            if parsed > 360 {
8620                return Err(invalid_attribute("firstSliceAng", "val", value));
8621            }
8622            set_once(first_slice_angle, (parsed, markup), "c:firstSliceAng")?;
8623            *boundary = (*boundary).max(series.len() + 2);
8624        }
8625        b"holeSize" if root == b"doughnutChart" => {
8626            let (value, markup) = required_scalar(&raw, "holeSize")?;
8627            let parsed = value
8628                .parse::<u8>()
8629                .map_err(|_| invalid_attribute("holeSize", "val", value.clone()))?;
8630            if !(10..=90).contains(&parsed) {
8631                return Err(invalid_attribute("holeSize", "val", value));
8632            }
8633            set_once(hole_size, (parsed, markup), "c:holeSize")?;
8634            *boundary = (*boundary).max(series.len() + 3);
8635        }
8636        b"axId" if property_first => {
8637            if axis_ids.len() == 2 {
8638                return Err(ChartError::DuplicateElement("c:axId".to_owned()));
8639            }
8640            axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
8641            *boundary = (*boundary).max(base + series.len() + 1 + axis_ids.len());
8642        }
8643        b"axId" => {
8644            return Err(ChartError::InvalidValue {
8645                element: "c:axId".to_owned(),
8646                value: "axis-free plot contains axis reference".to_owned(),
8647            });
8648        }
8649        _ => {
8650            let raw_boundary = match (root, name) {
8651                (_, b"varyColors") => base,
8652                (b"pieChart", b"extLst") => series.len() + 2,
8653                (b"doughnutChart", b"extLst") => series.len() + 3,
8654                (b"areaChart" | b"scatterChart" | b"radarChart", b"dropLines") => {
8655                    base + series.len() + 1
8656                }
8657                (b"areaChart" | b"scatterChart" | b"radarChart", b"extLst") => {
8658                    base + series.len() + 3
8659                }
8660                _ => *boundary,
8661            };
8662            raw_children.push(raw_boundary, raw);
8663            *boundary = (*boundary).max(raw_boundary);
8664        }
8665    }
8666    Ok(())
8667}
8668
8669fn parse_bar_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8670    let mut reader = Reader::from_reader(xml);
8671    let mut buffer = Vec::new();
8672    loop {
8673        match reader
8674            .read_event_into(&mut buffer)
8675            .map_err(OxmlError::from)?
8676        {
8677            Event::Start(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
8678                let namespaces = typed_rewrite_bindings(&element, inherited)?;
8679                let (mut raw_attributes, namespace_declarations) =
8680                    capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8681                raw_attributes.splice(0..0, namespace_declarations);
8682                let mut direction = None;
8683                let mut grouping = None;
8684                let mut gap_width = None;
8685                let mut overlap = None;
8686                let mut series = Vec::new();
8687                let mut data_labels = None;
8688                let mut axis_ids = Vec::new();
8689                let mut raw_children = OrderedRawChildren::default();
8690                let mut boundary = 0usize;
8691                let mut inner = Vec::new();
8692                loop {
8693                    match reader
8694                        .read_event_into(&mut inner)
8695                        .map_err(OxmlError::from)?
8696                    {
8697                        Event::Start(child) => {
8698                            let name = chart_child_local(&child, &namespaces)?;
8699                            let raw = capture_element(&mut reader, &child)?;
8700                            parse_bar_child(
8701                                name.as_deref().unwrap_or_default(),
8702                                raw,
8703                                &namespaces,
8704                                &mut direction,
8705                                &mut grouping,
8706                                &mut gap_width,
8707                                &mut overlap,
8708                                &mut series,
8709                                &mut data_labels,
8710                                &mut axis_ids,
8711                                &mut raw_children,
8712                                &mut boundary,
8713                            )?;
8714                        }
8715                        Event::Empty(child) => {
8716                            let name = chart_child_local(&child, &namespaces)?;
8717                            let raw = capture_empty_element(&child)?;
8718                            parse_bar_child(
8719                                name.as_deref().unwrap_or_default(),
8720                                raw,
8721                                &namespaces,
8722                                &mut direction,
8723                                &mut grouping,
8724                                &mut gap_width,
8725                                &mut overlap,
8726                                &mut series,
8727                                &mut data_labels,
8728                                &mut axis_ids,
8729                                &mut raw_children,
8730                                &mut boundary,
8731                            )?;
8732                        }
8733                        event @ (Event::Text(_)
8734                        | Event::CData(_)
8735                        | Event::Comment(_)
8736                        | Event::PI(_)
8737                        | Event::GeneralRef(_)) => {
8738                            raw_children.push(boundary, capture_event(event)?);
8739                        }
8740                        Event::End(end) if matches_local_name(end.name().as_ref(), b"barChart") => {
8741                            let (direction, direction_markup) = direction
8742                                .ok_or_else(|| ChartError::MissingElement("c:barDir".to_owned()))?;
8743                            let (grouping, grouping_markup) = grouping.ok_or_else(|| {
8744                                ChartError::MissingElement("c:grouping".to_owned())
8745                            })?;
8746                            if axis_ids.len() != 2 {
8747                                return Err(ChartError::InvalidValue {
8748                                    element: "c:barChart/c:axId".to_owned(),
8749                                    value: format!("expected 2, found {}", axis_ids.len()),
8750                                });
8751                            }
8752                            let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
8753                            let original_series_keys: Vec<(u32, u32)> =
8754                                series.iter().map(|item| (item.index, item.order)).collect();
8755                            let plot = Plot::Bar {
8756                                direction,
8757                                grouping,
8758                                gap_width: gap_width.as_ref().map_or(150, |item| item.0),
8759                                overlap: overlap.as_ref().map_or(0, |item| item.0),
8760                                series,
8761                                data_labels,
8762                                axis_ids: parsed_ids,
8763                            };
8764                            plot.validate()?;
8765                            return Ok((
8766                                plot,
8767                                PlotMarkup {
8768                                    raw_attributes,
8769                                    raw_children: raw_children_in_schema_order(
8770                                        &raw_children,
8771                                        original_series_keys.len() + 7,
8772                                    ),
8773                                    direction: Some(direction_markup),
8774                                    grouping: Some(grouping_markup),
8775                                    gap_width: gap_width.map(|item| item.1),
8776                                    overlap: overlap.map(|item| item.1),
8777                                    marker: None,
8778                                    smooth: None,
8779                                    first_slice_angle: None,
8780                                    hole_size: None,
8781                                    style: None,
8782                                    axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8783                                    original_series_keys,
8784                                    original_axis_ids: parsed_ids.to_vec(),
8785                                    parsed_bar: Some(true),
8786                                    parsed_remaining: None,
8787                                },
8788                            ));
8789                        }
8790                        Event::Eof => return Err(missing_end("c:barChart")),
8791                        _ => {}
8792                    }
8793                    inner.clear();
8794                }
8795            }
8796            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
8797                return Err(ChartError::MissingElement("c:barDir".to_owned()));
8798            }
8799            Event::Start(element) | Event::Empty(element) => {
8800                return Err(ChartError::UnexpectedElement(element_name(&element)));
8801            }
8802            Event::Eof => return Err(ChartError::MissingElement("c:barChart".to_owned())),
8803            _ => {}
8804        }
8805        buffer.clear();
8806    }
8807}
8808
8809#[allow(clippy::too_many_arguments)]
8810fn parse_bar_child(
8811    name: &[u8],
8812    raw: Vec<u8>,
8813    namespaces: &NamespaceBindings,
8814    direction: &mut Option<(BarDirection, ScalarMarkup)>,
8815    grouping: &mut Option<(BarGrouping, ScalarMarkup)>,
8816    gap_width: &mut Option<(u16, ScalarMarkup)>,
8817    overlap: &mut Option<(i8, ScalarMarkup)>,
8818    series: &mut Vec<Series>,
8819    data_labels: &mut Option<CT_DLbls>,
8820    axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
8821    raw_children: &mut OrderedRawChildren,
8822    boundary: &mut usize,
8823) -> Result<()> {
8824    match name {
8825        b"barDir" => {
8826            let (value, markup) = required_scalar(&raw, "barDir")?;
8827            let value = BarDirection::parse(&value)
8828                .ok_or_else(|| invalid_attribute("barDir", "val", value))?;
8829            set_once(direction, (value, markup), "c:barDir")?;
8830            *boundary = (*boundary).max(1);
8831        }
8832        b"grouping" => {
8833            let (value, markup) = required_scalar(&raw, "grouping")?;
8834            let value = BarGrouping::parse(&value)
8835                .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
8836            set_once(grouping, (value, markup), "c:grouping")?;
8837            *boundary = (*boundary).max(2);
8838        }
8839        b"ser" => {
8840            series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
8841            *boundary = (*boundary).max(series.len() + 2);
8842        }
8843        b"dLbls" => {
8844            set_once(
8845                data_labels,
8846                CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
8847                "c:dLbls",
8848            )?;
8849            *boundary = (*boundary).max(series.len() + 3);
8850        }
8851        b"gapWidth" => {
8852            let (value, markup) = required_scalar(&raw, "gapWidth")?;
8853            let parsed = value
8854                .parse::<u16>()
8855                .map_err(|_| invalid_attribute("gapWidth", "val", value.clone()))?;
8856            if parsed > 500 {
8857                return Err(invalid_attribute("gapWidth", "val", value));
8858            }
8859            set_once(gap_width, (parsed, markup), "c:gapWidth")?;
8860            *boundary = (*boundary).max(series.len() + 4);
8861        }
8862        b"overlap" => {
8863            let (value, markup) = required_scalar(&raw, "overlap")?;
8864            let parsed = value
8865                .parse::<i8>()
8866                .map_err(|_| invalid_attribute("overlap", "val", value.clone()))?;
8867            if !(-100..=100).contains(&parsed) {
8868                return Err(invalid_attribute("overlap", "val", value));
8869            }
8870            set_once(overlap, (parsed, markup), "c:overlap")?;
8871            *boundary = (*boundary).max(series.len() + 5);
8872        }
8873        b"axId" => {
8874            if axis_ids.len() == 2 {
8875                return Err(ChartError::DuplicateElement("c:axId".to_owned()));
8876            }
8877            axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
8878            *boundary = (*boundary).max(series.len() + 5 + axis_ids.len());
8879        }
8880        _ => raw_children.push(*boundary, raw),
8881    }
8882    Ok(())
8883}
8884
8885fn parse_line_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8886    let mut reader = Reader::from_reader(xml);
8887    let mut buffer = Vec::new();
8888    loop {
8889        match reader
8890            .read_event_into(&mut buffer)
8891            .map_err(OxmlError::from)?
8892        {
8893            Event::Start(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
8894                let namespaces = typed_rewrite_bindings(&element, inherited)?;
8895                let (mut raw_attributes, namespace_declarations) =
8896                    capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8897                raw_attributes.splice(0..0, namespace_declarations);
8898                let mut grouping = None;
8899                let mut marker = None;
8900                let mut smooth = None;
8901                let mut series = Vec::new();
8902                let mut data_labels = None;
8903                let mut axis_ids = Vec::new();
8904                let mut raw_children = OrderedRawChildren::default();
8905                let mut boundary = 0usize;
8906                let mut inner = Vec::new();
8907                loop {
8908                    match reader
8909                        .read_event_into(&mut inner)
8910                        .map_err(OxmlError::from)?
8911                    {
8912                        Event::Start(child) => {
8913                            let name = chart_child_local(&child, &namespaces)?;
8914                            let raw = capture_element(&mut reader, &child)?;
8915                            parse_line_child(
8916                                name.as_deref().unwrap_or_default(),
8917                                raw,
8918                                &namespaces,
8919                                &mut grouping,
8920                                &mut marker,
8921                                &mut smooth,
8922                                &mut series,
8923                                &mut data_labels,
8924                                &mut axis_ids,
8925                                &mut raw_children,
8926                                &mut boundary,
8927                            )?;
8928                        }
8929                        Event::Empty(child) => {
8930                            let name = chart_child_local(&child, &namespaces)?;
8931                            let raw = capture_empty_element(&child)?;
8932                            parse_line_child(
8933                                name.as_deref().unwrap_or_default(),
8934                                raw,
8935                                &namespaces,
8936                                &mut grouping,
8937                                &mut marker,
8938                                &mut smooth,
8939                                &mut series,
8940                                &mut data_labels,
8941                                &mut axis_ids,
8942                                &mut raw_children,
8943                                &mut boundary,
8944                            )?;
8945                        }
8946                        event @ (Event::Text(_)
8947                        | Event::CData(_)
8948                        | Event::Comment(_)
8949                        | Event::PI(_)
8950                        | Event::GeneralRef(_)) => {
8951                            raw_children.push(boundary, capture_event(event)?);
8952                        }
8953                        Event::End(end)
8954                            if matches_local_name(end.name().as_ref(), b"lineChart") =>
8955                        {
8956                            let (grouping, grouping_markup) = grouping.ok_or_else(|| {
8957                                ChartError::MissingElement("c:grouping".to_owned())
8958                            })?;
8959                            if axis_ids.len() != 2 {
8960                                return Err(ChartError::InvalidValue {
8961                                    element: "c:lineChart/c:axId".to_owned(),
8962                                    value: format!("expected 2, found {}", axis_ids.len()),
8963                                });
8964                            }
8965                            let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
8966                            let original_series_keys: Vec<(u32, u32)> =
8967                                series.iter().map(|item| (item.index, item.order)).collect();
8968                            let plot = Plot::Line {
8969                                grouping,
8970                                marker: marker.as_ref().is_some_and(|item| item.0),
8971                                smooth: smooth.as_ref().is_some_and(|item| item.0),
8972                                series,
8973                                data_labels,
8974                                axis_ids: parsed_ids,
8975                            };
8976                            plot.validate()?;
8977                            return Ok((
8978                                plot,
8979                                PlotMarkup {
8980                                    raw_attributes,
8981                                    raw_children: raw_children_in_schema_order(
8982                                        &raw_children,
8983                                        original_series_keys.len() + 6,
8984                                    ),
8985                                    direction: None,
8986                                    grouping: Some(grouping_markup),
8987                                    gap_width: None,
8988                                    overlap: None,
8989                                    marker: marker.map(|item| item.1),
8990                                    smooth: smooth.map(|item| item.1),
8991                                    first_slice_angle: None,
8992                                    hole_size: None,
8993                                    style: None,
8994                                    axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8995                                    original_series_keys,
8996                                    original_axis_ids: parsed_ids.to_vec(),
8997                                    parsed_bar: Some(false),
8998                                    parsed_remaining: None,
8999                                },
9000                            ));
9001                        }
9002                        Event::Eof => return Err(missing_end("c:lineChart")),
9003                        _ => {}
9004                    }
9005                    inner.clear();
9006                }
9007            }
9008            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
9009                return Err(ChartError::MissingElement("c:grouping".to_owned()));
9010            }
9011            Event::Start(element) | Event::Empty(element) => {
9012                return Err(ChartError::UnexpectedElement(element_name(&element)));
9013            }
9014            Event::Eof => return Err(ChartError::MissingElement("c:lineChart".to_owned())),
9015            _ => {}
9016        }
9017        buffer.clear();
9018    }
9019}
9020
9021#[allow(clippy::too_many_arguments)]
9022fn parse_line_child(
9023    name: &[u8],
9024    raw: Vec<u8>,
9025    namespaces: &NamespaceBindings,
9026    grouping: &mut Option<(Grouping, ScalarMarkup)>,
9027    marker: &mut Option<(bool, ScalarMarkup)>,
9028    smooth: &mut Option<(bool, ScalarMarkup)>,
9029    series: &mut Vec<Series>,
9030    data_labels: &mut Option<CT_DLbls>,
9031    axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
9032    raw_children: &mut OrderedRawChildren,
9033    boundary: &mut usize,
9034) -> Result<()> {
9035    match name {
9036        b"grouping" => {
9037            let (value, markup) = required_scalar(&raw, "grouping")?;
9038            let value = Grouping::parse(&value)
9039                .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
9040            set_once(grouping, (value, markup), "c:grouping")?;
9041            *boundary = (*boundary).max(1);
9042        }
9043        b"ser" => {
9044            series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
9045            *boundary = (*boundary).max(series.len() + 1);
9046        }
9047        b"dLbls" => {
9048            set_once(
9049                data_labels,
9050                CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
9051                "c:dLbls",
9052            )?;
9053            *boundary = (*boundary).max(series.len() + 2);
9054        }
9055        b"marker" => {
9056            set_once(marker, parse_bool_value(&raw, "marker")?, "c:marker")?;
9057            *boundary = (*boundary).max(series.len() + 3);
9058        }
9059        b"smooth" => {
9060            set_once(smooth, parse_bool_value(&raw, "smooth")?, "c:smooth")?;
9061            *boundary = (*boundary).max(series.len() + 4);
9062        }
9063        b"axId" => {
9064            if axis_ids.len() == 2 {
9065                return Err(ChartError::DuplicateElement("c:axId".to_owned()));
9066            }
9067            axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
9068            *boundary = (*boundary).max(series.len() + 4 + axis_ids.len());
9069        }
9070        _ => raw_children.push(*boundary, raw),
9071    }
9072    Ok(())
9073}
9074
9075fn required_scalar(xml: &[u8], local: &str) -> Result<(String, ScalarMarkup)> {
9076    let (value, markup) = scalar_value(xml, local)?;
9077    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
9078    Ok((value, markup))
9079}
9080
9081fn write_plot<W: Write>(writer: &mut Writer<W>, plot: &Plot, markup: &PlotMarkup) -> Result<()> {
9082    plot.validate()?;
9083    if let Some(parsed_bar) = markup.parsed_bar
9084        && parsed_bar != matches!(plot, Plot::Bar { .. })
9085    {
9086        return Err(ChartError::InvalidValue {
9087            element: "c:plotArea".to_owned(),
9088            value: "a parsed plot family cannot be replaced while preserved payload remains"
9089                .to_owned(),
9090        });
9091    }
9092    let current_remaining = match plot {
9093        Plot::Pie { .. } => Some("pieChart"),
9094        Plot::Doughnut { .. } => Some("doughnutChart"),
9095        Plot::Area { .. } => Some("areaChart"),
9096        Plot::Scatter { .. } => Some("scatterChart"),
9097        Plot::Radar { .. } => Some("radarChart"),
9098        Plot::Bar { .. } | Plot::Line { .. } => None,
9099    };
9100    if let Some(parsed) = markup.parsed_remaining
9101        && current_remaining != Some(parsed)
9102    {
9103        return Err(ChartError::InvalidValue {
9104            element: "c:plotArea".to_owned(),
9105            value: "a parsed plot family cannot be replaced while preserved payload remains"
9106                .to_owned(),
9107        });
9108    }
9109    match plot {
9110        Plot::Bar {
9111            direction,
9112            grouping,
9113            gap_width,
9114            overlap,
9115            series,
9116            data_labels,
9117            axis_ids,
9118        } => {
9119            let mut start = BytesStart::new("c:barChart");
9120            push_attributes(&mut start, &markup.raw_attributes);
9121            writer
9122                .write_event(Event::Start(start))
9123                .map_err(OxmlError::from)?;
9124            emit_raw(writer, markup.raw_children.at(0))?;
9125            write_scalar(
9126                writer,
9127                "c:barDir",
9128                direction.as_str(),
9129                markup.direction.as_ref(),
9130            )?;
9131            emit_raw(writer, markup.raw_children.at(1))?;
9132            write_scalar(
9133                writer,
9134                "c:grouping",
9135                grouping.as_str(),
9136                markup.grouping.as_ref(),
9137            )?;
9138            let original_to_current =
9139                series_original_to_current(&markup.original_series_keys, series);
9140            emit_repeated_raw(
9141                writer,
9142                &markup.raw_children,
9143                2,
9144                &original_to_current,
9145                series.len(),
9146                0,
9147            )?;
9148            for (index, item) in series.iter().enumerate() {
9149                writer
9150                    .get_mut()
9151                    .write_all(&item.to_xml()?)
9152                    .map_err(OxmlError::from)?;
9153                emit_repeated_raw(
9154                    writer,
9155                    &markup.raw_children,
9156                    2,
9157                    &original_to_current,
9158                    series.len(),
9159                    index + 1,
9160                )?;
9161            }
9162            if let Some(labels) = data_labels {
9163                writer
9164                    .get_mut()
9165                    .write_all(&labels.to_xml()?)
9166                    .map_err(OxmlError::from)?;
9167            }
9168            let trailing = markup.original_series_keys.len();
9169            emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9170            if *gap_width != 150 || markup.gap_width.is_some() {
9171                write_scalar(
9172                    writer,
9173                    "c:gapWidth",
9174                    &gap_width.to_string(),
9175                    markup.gap_width.as_ref(),
9176                )?;
9177            }
9178            emit_raw(writer, markup.raw_children.at(trailing + 4))?;
9179            if *overlap != 0 || markup.overlap.is_some() {
9180                write_scalar(
9181                    writer,
9182                    "c:overlap",
9183                    &overlap.to_string(),
9184                    markup.overlap.as_ref(),
9185                )?;
9186            }
9187            let axis_original_to_current =
9188                axis_original_to_current(&markup.original_axis_ids, axis_ids);
9189            let axis_current_to_original =
9190                invert_original_to_current(&axis_original_to_current, axis_ids.len());
9191            emit_repeated_raw(
9192                writer,
9193                &markup.raw_children,
9194                trailing + 5,
9195                &axis_original_to_current,
9196                axis_ids.len(),
9197                0,
9198            )?;
9199            for (index, id) in axis_ids.iter().enumerate() {
9200                let default_markup = default_axis_id_markup(*id);
9201                let original_markup = axis_current_to_original[index]
9202                    .and_then(|original| markup.axis_ids.get(original));
9203                write_axis_id(
9204                    writer,
9205                    "c:axId",
9206                    *id,
9207                    original_markup.unwrap_or(&default_markup),
9208                )?;
9209                emit_repeated_raw(
9210                    writer,
9211                    &markup.raw_children,
9212                    trailing + 5,
9213                    &axis_original_to_current,
9214                    axis_ids.len(),
9215                    index + 1,
9216                )?;
9217            }
9218            writer
9219                .write_event(Event::End(BytesEnd::new("c:barChart")))
9220                .map_err(OxmlError::from)?;
9221        }
9222        Plot::Line {
9223            grouping,
9224            marker,
9225            smooth,
9226            series,
9227            data_labels,
9228            axis_ids,
9229        } => {
9230            let mut start = BytesStart::new("c:lineChart");
9231            push_attributes(&mut start, &markup.raw_attributes);
9232            writer
9233                .write_event(Event::Start(start))
9234                .map_err(OxmlError::from)?;
9235            emit_raw(writer, markup.raw_children.at(0))?;
9236            write_scalar(
9237                writer,
9238                "c:grouping",
9239                grouping.as_str(),
9240                markup.grouping.as_ref(),
9241            )?;
9242            let original_to_current =
9243                series_original_to_current(&markup.original_series_keys, series);
9244            emit_repeated_raw(
9245                writer,
9246                &markup.raw_children,
9247                1,
9248                &original_to_current,
9249                series.len(),
9250                0,
9251            )?;
9252            for (index, item) in series.iter().enumerate() {
9253                writer
9254                    .get_mut()
9255                    .write_all(&item.to_xml()?)
9256                    .map_err(OxmlError::from)?;
9257                emit_repeated_raw(
9258                    writer,
9259                    &markup.raw_children,
9260                    1,
9261                    &original_to_current,
9262                    series.len(),
9263                    index + 1,
9264                )?;
9265            }
9266            if let Some(labels) = data_labels {
9267                writer
9268                    .get_mut()
9269                    .write_all(&labels.to_xml()?)
9270                    .map_err(OxmlError::from)?;
9271            }
9272            let trailing = markup.original_series_keys.len();
9273            emit_raw(writer, markup.raw_children.at(trailing + 2))?;
9274            if *marker || markup.marker.is_some() {
9275                write_scalar(
9276                    writer,
9277                    "c:marker",
9278                    bool_lexical(*marker),
9279                    markup.marker.as_ref(),
9280                )?;
9281            }
9282            emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9283            if *smooth || markup.smooth.is_some() {
9284                write_scalar(
9285                    writer,
9286                    "c:smooth",
9287                    bool_lexical(*smooth),
9288                    markup.smooth.as_ref(),
9289                )?;
9290            }
9291            let axis_original_to_current =
9292                axis_original_to_current(&markup.original_axis_ids, axis_ids);
9293            let axis_current_to_original =
9294                invert_original_to_current(&axis_original_to_current, axis_ids.len());
9295            emit_repeated_raw(
9296                writer,
9297                &markup.raw_children,
9298                trailing + 4,
9299                &axis_original_to_current,
9300                axis_ids.len(),
9301                0,
9302            )?;
9303            for (index, id) in axis_ids.iter().enumerate() {
9304                let default_markup = default_axis_id_markup(*id);
9305                let original_markup = axis_current_to_original[index]
9306                    .and_then(|original| markup.axis_ids.get(original));
9307                write_axis_id(
9308                    writer,
9309                    "c:axId",
9310                    *id,
9311                    original_markup.unwrap_or(&default_markup),
9312                )?;
9313                emit_repeated_raw(
9314                    writer,
9315                    &markup.raw_children,
9316                    trailing + 4,
9317                    &axis_original_to_current,
9318                    axis_ids.len(),
9319                    index + 1,
9320                )?;
9321            }
9322            writer
9323                .write_event(Event::End(BytesEnd::new("c:lineChart")))
9324                .map_err(OxmlError::from)?;
9325        }
9326        remaining => {
9327            let (
9328                root,
9329                property,
9330                series,
9331                data_labels,
9332                axis_ids,
9333                first_slice_angle,
9334                hole_size,
9335                scatter,
9336            ) = match remaining {
9337                Plot::Pie {
9338                    first_slice_angle,
9339                    series,
9340                    data_labels,
9341                } => (
9342                    "pieChart",
9343                    None,
9344                    series,
9345                    data_labels,
9346                    None,
9347                    Some(*first_slice_angle),
9348                    None,
9349                    false,
9350                ),
9351                Plot::Doughnut {
9352                    first_slice_angle,
9353                    hole_size,
9354                    series,
9355                    data_labels,
9356                } => (
9357                    "doughnutChart",
9358                    None,
9359                    series,
9360                    data_labels,
9361                    None,
9362                    Some(*first_slice_angle),
9363                    Some(*hole_size),
9364                    false,
9365                ),
9366                Plot::Area {
9367                    grouping,
9368                    series,
9369                    data_labels,
9370                    axis_ids,
9371                } => (
9372                    "areaChart",
9373                    Some(("c:grouping", grouping.as_str(), markup.grouping.as_ref())),
9374                    series,
9375                    data_labels,
9376                    Some(axis_ids),
9377                    None,
9378                    None,
9379                    false,
9380                ),
9381                Plot::Scatter {
9382                    style,
9383                    series,
9384                    data_labels,
9385                    axis_ids,
9386                } => (
9387                    "scatterChart",
9388                    Some(("c:scatterStyle", style.as_str(), markup.style.as_ref())),
9389                    series,
9390                    data_labels,
9391                    Some(axis_ids),
9392                    None,
9393                    None,
9394                    true,
9395                ),
9396                Plot::Radar {
9397                    style,
9398                    series,
9399                    data_labels,
9400                    axis_ids,
9401                } => (
9402                    "radarChart",
9403                    Some(("c:radarStyle", style.as_str(), markup.style.as_ref())),
9404                    series,
9405                    data_labels,
9406                    Some(axis_ids),
9407                    None,
9408                    None,
9409                    false,
9410                ),
9411                Plot::Bar { .. } | Plot::Line { .. } => unreachable!("handled above"),
9412            };
9413            let tag = format!("c:{root}");
9414            let mut start = BytesStart::new(&tag);
9415            push_attributes(&mut start, &markup.raw_attributes);
9416            writer
9417                .write_event(Event::Start(start))
9418                .map_err(OxmlError::from)?;
9419            let base = usize::from(property.is_some());
9420            if let Some((name, value, scalar_markup)) = property {
9421                emit_raw(writer, markup.raw_children.at(0))?;
9422                write_scalar(writer, name, value, scalar_markup)?;
9423            }
9424            let original_to_current =
9425                series_original_to_current(&markup.original_series_keys, series);
9426            emit_repeated_raw(
9427                writer,
9428                &markup.raw_children,
9429                base,
9430                &original_to_current,
9431                series.len(),
9432                0,
9433            )?;
9434            for (index, item) in series.iter().enumerate() {
9435                writer
9436                    .get_mut()
9437                    .write_all(&item.to_xml_for_plot(scatter)?)
9438                    .map_err(OxmlError::from)?;
9439                emit_repeated_raw(
9440                    writer,
9441                    &markup.raw_children,
9442                    base,
9443                    &original_to_current,
9444                    series.len(),
9445                    index + 1,
9446                )?;
9447            }
9448            if let Some(labels) = data_labels {
9449                writer
9450                    .get_mut()
9451                    .write_all(&labels.to_xml()?)
9452                    .map_err(OxmlError::from)?;
9453            }
9454            let trailing = base + markup.original_series_keys.len();
9455            if let Some(axis_ids) = axis_ids {
9456                let axis_original_to_current =
9457                    axis_original_to_current(&markup.original_axis_ids, axis_ids);
9458                let axis_current_to_original =
9459                    invert_original_to_current(&axis_original_to_current, axis_ids.len());
9460                emit_repeated_raw(
9461                    writer,
9462                    &markup.raw_children,
9463                    trailing + 1,
9464                    &axis_original_to_current,
9465                    axis_ids.len(),
9466                    0,
9467                )?;
9468                for (index, id) in axis_ids.iter().enumerate() {
9469                    let default_markup = default_axis_id_markup(*id);
9470                    let original_markup = axis_current_to_original[index]
9471                        .and_then(|original| markup.axis_ids.get(original));
9472                    write_axis_id(
9473                        writer,
9474                        "c:axId",
9475                        *id,
9476                        original_markup.unwrap_or(&default_markup),
9477                    )?;
9478                    emit_repeated_raw(
9479                        writer,
9480                        &markup.raw_children,
9481                        trailing + 1,
9482                        &axis_original_to_current,
9483                        axis_ids.len(),
9484                        index + 1,
9485                    )?;
9486                }
9487            } else {
9488                emit_raw(writer, markup.raw_children.at(trailing + 1))?;
9489                if let Some(angle) = first_slice_angle {
9490                    if angle != 0 || markup.first_slice_angle.is_some() {
9491                        write_scalar(
9492                            writer,
9493                            "c:firstSliceAng",
9494                            &angle.to_string(),
9495                            markup.first_slice_angle.as_ref(),
9496                        )?;
9497                    }
9498                    emit_raw(writer, markup.raw_children.at(trailing + 2))?;
9499                }
9500                if let Some(size) = hole_size {
9501                    if size != 50 || markup.hole_size.is_some() {
9502                        write_scalar(
9503                            writer,
9504                            "c:holeSize",
9505                            &size.to_string(),
9506                            markup.hole_size.as_ref(),
9507                        )?;
9508                    }
9509                    emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9510                }
9511            }
9512            writer
9513                .write_event(Event::End(BytesEnd::new(&tag)))
9514                .map_err(OxmlError::from)?;
9515        }
9516    }
9517    Ok(())
9518}
9519
9520fn series_original_to_current(original: &[(u32, u32)], current: &[Series]) -> Vec<Option<usize>> {
9521    let mut used = vec![false; current.len()];
9522    let mut matches = vec![None; original.len()];
9523    for original_index in 0..original.len().min(current.len()) {
9524        if original[original_index]
9525            == (current[original_index].index, current[original_index].order)
9526        {
9527            matches[original_index] = Some(original_index);
9528            used[original_index] = true;
9529        }
9530    }
9531    for (original_index, key) in original.iter().enumerate() {
9532        if matches[original_index].is_some() {
9533            continue;
9534        }
9535        if let Some(current_index) = current
9536            .iter()
9537            .enumerate()
9538            .position(|(index, item)| !used[index] && (item.index, item.order) == *key)
9539        {
9540            matches[original_index] = Some(current_index);
9541            used[current_index] = true;
9542        }
9543    }
9544    match_unidentified_by_position(&mut matches, &mut used);
9545    matches
9546}
9547
9548fn axis_original_to_current(original: &[AxisId], current: &[AxisId]) -> Vec<Option<usize>> {
9549    let mut used = vec![false; current.len()];
9550    let mut matches = vec![None; original.len()];
9551    for (original_index, id) in original.iter().enumerate() {
9552        if let Some(current_index) = current
9553            .iter()
9554            .enumerate()
9555            .position(|(index, current_id)| !used[index] && current_id == id)
9556        {
9557            matches[original_index] = Some(current_index);
9558            used[current_index] = true;
9559        }
9560    }
9561    match_unidentified_by_position(&mut matches, &mut used);
9562    matches
9563}
9564
9565fn match_unidentified_by_position(matches: &mut [Option<usize>], used: &mut [bool]) {
9566    for original_index in 0..matches.len() {
9567        if matches[original_index].is_none() && original_index < used.len() && !used[original_index]
9568        {
9569            matches[original_index] = Some(original_index);
9570            used[original_index] = true;
9571        }
9572    }
9573    for matched in matches.iter_mut().filter(|matched| matched.is_none()) {
9574        if let Some(current_index) = used.iter().position(|used| !used) {
9575            *matched = Some(current_index);
9576            used[current_index] = true;
9577        }
9578    }
9579}
9580
9581fn invert_original_to_current(
9582    original_to_current: &[Option<usize>],
9583    current_len: usize,
9584) -> Vec<Option<usize>> {
9585    let mut current_to_original = vec![None; current_len];
9586    for (original, current) in original_to_current.iter().enumerate() {
9587        if let Some(current) = current {
9588            current_to_original[*current] = Some(original);
9589        }
9590    }
9591    current_to_original
9592}
9593
9594fn emit_repeated_raw<W: Write>(
9595    writer: &mut Writer<W>,
9596    raw_children: &OrderedRawChildren,
9597    offset: usize,
9598    original_to_current: &[Option<usize>],
9599    current_len: usize,
9600    current_boundary: usize,
9601) -> Result<()> {
9602    if current_boundary == 0 {
9603        emit_raw(writer, raw_children.at(offset))?;
9604    }
9605    for original_boundary in 1..=original_to_current.len() {
9606        let effective = original_to_current
9607            .iter()
9608            .skip(original_boundary)
9609            .flatten()
9610            .copied()
9611            .next()
9612            .unwrap_or(current_len);
9613        if effective == current_boundary {
9614            emit_raw(writer, raw_children.at(offset + original_boundary))?;
9615        }
9616    }
9617    Ok(())
9618}
9619
9620fn parse_plot_axis(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Axis>> {
9621    let mut reader = Reader::from_reader(xml);
9622    let mut buffer = Vec::new();
9623    loop {
9624        match reader
9625            .read_event_into(&mut buffer)
9626            .map_err(OxmlError::from)?
9627        {
9628            Event::Start(element) | Event::Empty(element) => {
9629                if !element_is_in_namespace(&element, C_NS, inherited)? {
9630                    return Ok(None);
9631                }
9632                let name = element.name();
9633                if AxisKind::parse(local_name(name.as_ref())).is_none() {
9634                    return Ok(None);
9635                }
9636                return Axis::from_xml_with_namespaces(xml, inherited).map(Some);
9637            }
9638            Event::Eof => return Ok(None),
9639            _ => {}
9640        }
9641        buffer.clear();
9642    }
9643}
9644
9645fn validate_axis_pairs(axes: &[Axis]) -> Result<()> {
9646    for (index, axis) in axes.iter().enumerate() {
9647        if axes[..index].iter().any(|other| other.id == axis.id) {
9648            return Err(ChartError::DuplicateElement(format!(
9649                "c:axId {}",
9650                axis.id.value()
9651            )));
9652        }
9653    }
9654    for axis in axes {
9655        if axis.id == axis.cross_axis {
9656            return Err(ChartError::InvalidValue {
9657                element: "c:crossAx".to_owned(),
9658                value: format!("axis {} crosses itself", axis.id.value()),
9659            });
9660        }
9661        let crossed = axes
9662            .iter()
9663            .find(|candidate| candidate.id == axis.cross_axis)
9664            .ok_or_else(|| ChartError::InvalidValue {
9665                element: "c:crossAx".to_owned(),
9666                value: format!(
9667                    "axis {} references missing axis {}",
9668                    axis.id.value(),
9669                    axis.cross_axis.value()
9670                ),
9671            })?;
9672        if crossed.cross_axis != axis.id {
9673            return Err(ChartError::InvalidValue {
9674                element: "c:crossAx".to_owned(),
9675                value: format!(
9676                    "axis {} references {}, which references {}",
9677                    axis.id.value(),
9678                    crossed.id.value(),
9679                    crossed.cross_axis.value()
9680                ),
9681            });
9682        }
9683    }
9684    Ok(())
9685}
9686
9687fn parse_plot_series(
9688    xml: &[u8],
9689    inherited: &NamespaceBindings,
9690    series: &mut Vec<Series>,
9691) -> Result<()> {
9692    let mut reader = Reader::from_reader(xml);
9693    let mut buffer = Vec::new();
9694    loop {
9695        match reader
9696            .read_event_into(&mut buffer)
9697            .map_err(OxmlError::from)?
9698        {
9699            Event::Start(element) => {
9700                if !element_is_in_namespace(&element, C_NS, inherited)? {
9701                    return Ok(());
9702                }
9703                let local = local_name(element.name().as_ref()).to_vec();
9704                if !is_supported_series_plot(&local) {
9705                    return Ok(());
9706                }
9707                let namespaces = chart_bindings(inherited, &element)?;
9708                let mut inner = Vec::new();
9709                loop {
9710                    match reader
9711                        .read_event_into(&mut inner)
9712                        .map_err(OxmlError::from)?
9713                    {
9714                        Event::Start(child) => {
9715                            let name = chart_child_local(&child, &namespaces)?;
9716                            let raw = capture_element(&mut reader, &child)?;
9717                            if name.as_deref() == Some(b"ser") {
9718                                series.push(Series::from_xml_with_namespaces(&raw, &namespaces)?);
9719                            }
9720                        }
9721                        Event::Empty(child) => {
9722                            let name = chart_child_local(&child, &namespaces)?;
9723                            if name.as_deref() == Some(b"ser") {
9724                                return Err(ChartError::MissingElement("c:ser/c:idx".to_owned()));
9725                            }
9726                        }
9727                        Event::End(end) if matches_local_name(end.name().as_ref(), &local) => {
9728                            return Ok(());
9729                        }
9730                        Event::Eof => {
9731                            return Err(missing_end(&format!(
9732                                "c:{}",
9733                                String::from_utf8_lossy(&local)
9734                            )));
9735                        }
9736                        _ => {}
9737                    }
9738                    inner.clear();
9739                }
9740            }
9741            Event::Empty(_) | Event::Eof => return Ok(()),
9742            _ => {}
9743        }
9744        buffer.clear();
9745    }
9746}
9747
9748fn is_supported_series_plot(local: &[u8]) -> bool {
9749    matches!(
9750        local,
9751        b"areaChart"
9752            | b"area3DChart"
9753            | b"barChart"
9754            | b"bar3DChart"
9755            | b"doughnutChart"
9756            | b"lineChart"
9757            | b"line3DChart"
9758            | b"ofPieChart"
9759            | b"pieChart"
9760            | b"pie3DChart"
9761            | b"radarChart"
9762            | b"stockChart"
9763            | b"surfaceChart"
9764            | b"surface3DChart"
9765    )
9766}
9767
9768impl CT_Legend {
9769    fn from_xml(xml: &[u8]) -> Result<Self> {
9770        let (raw_attributes, raw_children) = parse_raw_shell(xml, b"legend", "c:legend")?;
9771        Ok(Self {
9772            raw_attributes,
9773            raw_children,
9774        })
9775    }
9776
9777    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
9778        write_raw_shell(writer, "c:legend", &self.raw_attributes, &self.raw_children)
9779    }
9780
9781    pub fn raw_children(&self) -> &OrderedRawChildren {
9782        &self.raw_children
9783    }
9784}
9785
9786fn parse_raw_shell(
9787    xml: &[u8],
9788    local: &[u8],
9789    tag: &str,
9790) -> Result<(XmlAttributes, OrderedRawChildren)> {
9791    let mut reader = Reader::from_reader(xml);
9792    let mut buffer = Vec::new();
9793    loop {
9794        match reader
9795            .read_event_into(&mut buffer)
9796            .map_err(OxmlError::from)?
9797        {
9798            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
9799                reject_conflicting_prefix(&element, b"c", C_NS)?;
9800                let raw_attributes = capture_attributes(&element)?;
9801                let mut raw_children = OrderedRawChildren::default();
9802                let mut child_buffer = Vec::new();
9803                loop {
9804                    match reader
9805                        .read_event_into(&mut child_buffer)
9806                        .map_err(OxmlError::from)?
9807                    {
9808                        Event::Start(child) => {
9809                            raw_children.push(0, capture_element(&mut reader, &child)?)
9810                        }
9811                        Event::Empty(child) => raw_children.push(0, capture_empty_element(&child)?),
9812                        event @ (Event::Text(_)
9813                        | Event::CData(_)
9814                        | Event::Comment(_)
9815                        | Event::PI(_)
9816                        | Event::GeneralRef(_)) => {
9817                            raw_children.push(0, capture_event(event)?);
9818                        }
9819                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => break,
9820                        Event::Eof => return Err(missing_end(tag)),
9821                        _ => {}
9822                    }
9823                    child_buffer.clear();
9824                }
9825                return Ok((raw_attributes, raw_children));
9826            }
9827            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
9828                reject_conflicting_prefix(&element, b"c", C_NS)?;
9829                return Ok((capture_attributes(&element)?, OrderedRawChildren::default()));
9830            }
9831            Event::Start(element) | Event::Empty(element) => {
9832                return Err(ChartError::UnexpectedElement(element_name(&element)));
9833            }
9834            Event::Eof => return Err(ChartError::MissingElement(tag.to_owned())),
9835            _ => {}
9836        }
9837        buffer.clear();
9838    }
9839}
9840
9841fn write_raw_shell<W: Write>(
9842    writer: &mut Writer<W>,
9843    tag: &str,
9844    raw_attributes: &XmlAttributes,
9845    raw_children: &OrderedRawChildren,
9846) -> Result<()> {
9847    let mut start = BytesStart::new(tag);
9848    push_attributes(&mut start, raw_attributes);
9849    if raw_children.is_empty() {
9850        writer
9851            .write_event(Event::Empty(start))
9852            .map_err(OxmlError::from)?;
9853        return Ok(());
9854    }
9855    writer
9856        .write_event(Event::Start(start))
9857        .map_err(OxmlError::from)?;
9858    emit_raw(writer, raw_children.at(0))?;
9859    writer
9860        .write_event(Event::End(BytesEnd::new(tag)))
9861        .map_err(OxmlError::from)?;
9862    Ok(())
9863}
9864
9865/// The implemented behavior-bearing core of one `c:chart`.
9866#[derive(Clone, Debug, PartialEq)]
9867pub struct CT_Chart {
9868    pub title: Option<CT_Title>,
9869    pub auto_title_deleted: bool,
9870    pub plot_area: CT_PlotArea,
9871    pub legend: Option<CT_Legend>,
9872    pub plot_vis_only: bool,
9873    pub disp_blanks_as: DispBlanksAs,
9874    auto_title_deleted_markup: Option<ScalarMarkup>,
9875    plot_vis_only_markup: Option<ScalarMarkup>,
9876    disp_blanks_as_markup: Option<ScalarMarkup>,
9877    raw_attributes: Vec<(String, String)>,
9878    raw_children: OrderedRawChildren,
9879}
9880
9881impl CT_Chart {
9882    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
9883        let mut reader = Reader::from_reader(xml);
9884        let mut buffer = Vec::new();
9885        loop {
9886            match reader
9887                .read_event_into(&mut buffer)
9888                .map_err(OxmlError::from)?
9889            {
9890                Event::Start(element) if matches_local_name(element.name().as_ref(), b"chart") => {
9891                    reject_conflicting_prefix(&element, b"c", C_NS)?;
9892                    return Self::from_element(&mut reader, &element, inherited);
9893                }
9894                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"chart") => {
9895                    return Err(ChartError::MissingElement("c:plotArea".to_owned()));
9896                }
9897                Event::Start(element) | Event::Empty(element) => {
9898                    return Err(ChartError::UnexpectedElement(element_name(&element)));
9899                }
9900                Event::Eof => return Err(ChartError::MissingElement("c:chart".to_owned())),
9901                _ => {}
9902            }
9903            buffer.clear();
9904        }
9905    }
9906
9907    fn from_element(
9908        reader: &mut Reader<&[u8]>,
9909        start: &BytesStart<'_>,
9910        inherited: &NamespaceBindings,
9911    ) -> Result<Self> {
9912        let namespaces = bindings_with_local(inherited, start)?;
9913        let raw_attributes = capture_attributes(start)?;
9914        let mut state = ChartParseState::default();
9915        let mut buffer = Vec::new();
9916
9917        loop {
9918            match reader
9919                .read_event_into(&mut buffer)
9920                .map_err(OxmlError::from)?
9921            {
9922                Event::Start(element) => {
9923                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
9924                        local_name(element.name().as_ref()).to_vec()
9925                    } else {
9926                        Vec::new()
9927                    };
9928                    let raw = capture_element(reader, &element)?;
9929                    state.parse_child(&name, raw, &namespaces)?;
9930                }
9931                Event::Empty(element) => {
9932                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
9933                        local_name(element.name().as_ref()).to_vec()
9934                    } else {
9935                        Vec::new()
9936                    };
9937                    let raw = capture_empty_element(&element)?;
9938                    state.parse_child(&name, raw, &namespaces)?;
9939                }
9940                event @ (Event::Text(_)
9941                | Event::CData(_)
9942                | Event::Comment(_)
9943                | Event::PI(_)
9944                | Event::GeneralRef(_)) => {
9945                    state.capture_event(capture_event(event)?);
9946                }
9947                Event::End(element) if matches_local_name(element.name().as_ref(), b"chart") => {
9948                    break;
9949                }
9950                Event::Eof => return Err(missing_end("c:chart")),
9951                _ => {}
9952            }
9953            buffer.clear();
9954        }
9955
9956        state.finish(raw_attributes)
9957    }
9958
9959    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
9960        let mut start = BytesStart::new("c:chart");
9961        push_attributes(&mut start, &self.raw_attributes);
9962        writer
9963            .write_event(Event::Start(start))
9964            .map_err(OxmlError::from)?;
9965        emit_raw(writer, self.raw_children.at(0))?;
9966        if let Some(title) = &self.title {
9967            title.write_xml(writer)?;
9968        }
9969        emit_raw(writer, self.raw_children.at(1))?;
9970        if self.auto_title_deleted_markup.is_some() || self.auto_title_deleted {
9971            write_scalar(
9972                writer,
9973                "c:autoTitleDeleted",
9974                if self.auto_title_deleted { "1" } else { "0" },
9975                self.auto_title_deleted_markup.as_ref(),
9976            )?;
9977        }
9978        for raw_boundary in 2..8 {
9979            emit_raw(writer, self.raw_children.at(raw_boundary))?;
9980        }
9981        self.plot_area.write_xml(writer)?;
9982        emit_raw(writer, self.raw_children.at(8))?;
9983        if let Some(legend) = &self.legend {
9984            legend.write_xml(writer)?;
9985        }
9986        emit_raw(writer, self.raw_children.at(9))?;
9987        if self.plot_vis_only_markup.is_some() || !self.plot_vis_only {
9988            write_scalar(
9989                writer,
9990                "c:plotVisOnly",
9991                if self.plot_vis_only { "1" } else { "0" },
9992                self.plot_vis_only_markup.as_ref(),
9993            )?;
9994        }
9995        emit_raw(writer, self.raw_children.at(10))?;
9996        if self.disp_blanks_as_markup.is_some() || self.disp_blanks_as != DispBlanksAs::Gap {
9997            write_scalar(
9998                writer,
9999                "c:dispBlanksAs",
10000                self.disp_blanks_as.as_str(),
10001                self.disp_blanks_as_markup.as_ref(),
10002            )?;
10003        }
10004        for raw_boundary in 11..=13 {
10005            emit_raw(writer, self.raw_children.at(raw_boundary))?;
10006        }
10007        writer
10008            .write_event(Event::End(BytesEnd::new("c:chart")))
10009            .map_err(OxmlError::from)?;
10010        Ok(())
10011    }
10012
10013    pub fn raw_children(&self) -> &OrderedRawChildren {
10014        &self.raw_children
10015    }
10016}
10017
10018#[derive(Default)]
10019struct ChartParseState {
10020    title: Option<CT_Title>,
10021    auto_title_deleted: Option<(bool, ScalarMarkup)>,
10022    plot_area: Option<CT_PlotArea>,
10023    legend: Option<CT_Legend>,
10024    plot_vis_only: Option<(bool, ScalarMarkup)>,
10025    disp_blanks_as: Option<(DispBlanksAs, ScalarMarkup)>,
10026    raw_children: OrderedRawChildren,
10027    boundary: usize,
10028}
10029
10030impl ChartParseState {
10031    fn capture_event(&mut self, raw: Vec<u8>) {
10032        self.raw_children.push(self.boundary, raw);
10033    }
10034
10035    fn parse_child(
10036        &mut self,
10037        name: &[u8],
10038        raw: Vec<u8>,
10039        namespaces: &NamespaceBindings,
10040    ) -> Result<()> {
10041        match name {
10042            b"title" => {
10043                set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
10044                self.boundary = self.boundary.max(1);
10045            }
10046            b"autoTitleDeleted" => {
10047                set_once(
10048                    &mut self.auto_title_deleted,
10049                    parse_bool_value(&raw, "autoTitleDeleted")?,
10050                    "c:autoTitleDeleted",
10051                )?;
10052                self.boundary = self.boundary.max(2);
10053            }
10054            b"plotArea" => {
10055                set_once(
10056                    &mut self.plot_area,
10057                    CT_PlotArea::from_xml_with_namespaces(&raw, namespaces)?,
10058                    "c:plotArea",
10059                )?;
10060                self.boundary = self.boundary.max(8);
10061            }
10062            b"legend" => {
10063                set_once(&mut self.legend, CT_Legend::from_xml(&raw)?, "c:legend")?;
10064                self.boundary = self.boundary.max(9);
10065            }
10066            b"plotVisOnly" => {
10067                set_once(
10068                    &mut self.plot_vis_only,
10069                    parse_bool_value(&raw, "plotVisOnly")?,
10070                    "c:plotVisOnly",
10071                )?;
10072                self.boundary = self.boundary.max(10);
10073            }
10074            b"dispBlanksAs" => {
10075                set_once(
10076                    &mut self.disp_blanks_as,
10077                    parse_enum_value(&raw, "dispBlanksAs")?,
10078                    "c:dispBlanksAs",
10079                )?;
10080                self.boundary = self.boundary.max(11);
10081            }
10082            _ => {
10083                self.raw_children
10084                    .push(chart_raw_slot(name).unwrap_or(self.boundary), raw);
10085                self.boundary = self.boundary.max(chart_raw_boundary_after(name));
10086            }
10087        }
10088        Ok(())
10089    }
10090
10091    fn finish(self, raw_attributes: XmlAttributes) -> Result<CT_Chart> {
10092        let (auto_title_deleted, auto_title_deleted_markup) = self
10093            .auto_title_deleted
10094            .map(|(value, markup)| (value, Some(markup)))
10095            .unwrap_or((false, None));
10096        let (plot_vis_only, plot_vis_only_markup) = self
10097            .plot_vis_only
10098            .map(|(value, markup)| (value, Some(markup)))
10099            .unwrap_or((true, None));
10100        let (disp_blanks_as, disp_blanks_as_markup) = self
10101            .disp_blanks_as
10102            .map(|(value, markup)| (value, Some(markup)))
10103            .unwrap_or((DispBlanksAs::Gap, None));
10104        Ok(CT_Chart {
10105            title: self.title,
10106            auto_title_deleted,
10107            plot_area: self
10108                .plot_area
10109                .ok_or_else(|| ChartError::MissingElement("c:plotArea".to_owned()))?,
10110            legend: self.legend,
10111            plot_vis_only,
10112            disp_blanks_as,
10113            auto_title_deleted_markup,
10114            plot_vis_only_markup,
10115            disp_blanks_as_markup,
10116            raw_attributes,
10117            raw_children: raw_children_in_schema_order(&self.raw_children, 13),
10118        })
10119    }
10120}
10121
10122fn chart_raw_slot(name: &[u8]) -> Option<usize> {
10123    match name {
10124        b"pivotFmts" => Some(2),
10125        b"view3D" => Some(3),
10126        b"floor" => Some(4),
10127        b"sideWall" => Some(5),
10128        b"backWall" => Some(6),
10129        // Both unmodelled tail children share a slot so producer sibling order
10130        // remains byte-stable until a later story types either element.
10131        b"showDLblsOverMax" | b"extLst" => Some(11),
10132        _ => None,
10133    }
10134}
10135
10136fn chart_raw_boundary_after(name: &[u8]) -> usize {
10137    match name {
10138        b"title" => 1,
10139        b"autoTitleDeleted" => 2,
10140        b"pivotFmts" => 3,
10141        b"view3D" => 4,
10142        b"floor" => 5,
10143        b"sideWall" => 6,
10144        b"backWall" => 7,
10145        b"plotArea" => 8,
10146        b"legend" => 9,
10147        b"plotVisOnly" => 10,
10148        b"dispBlanksAs" => 11,
10149        b"showDLblsOverMax" | b"extLst" => 11,
10150        _ => 0,
10151    }
10152}
10153
10154/// One complete `c:chartSpace` part with the F-118 core typed.
10155#[derive(Clone, Debug, PartialEq)]
10156pub struct CT_ChartSpace {
10157    pub chart: CT_Chart,
10158    pub sp_pr: Option<CT_ShapeProperties>,
10159    pub tx_pr: Option<CT_TextBody>,
10160    raw_attributes: Vec<(String, String)>,
10161    namespace_declarations: Vec<(String, String)>,
10162    raw_children: OrderedRawChildren,
10163}
10164
10165impl CT_ChartSpace {
10166    /// Parses one complete ChartML chart-space part.
10167    pub fn from_xml(xml: &[u8]) -> Result<Self> {
10168        let mut reader = Reader::from_reader(xml);
10169        let mut buffer = Vec::new();
10170        loop {
10171            match reader
10172                .read_event_into(&mut buffer)
10173                .map_err(OxmlError::from)?
10174            {
10175                Event::Start(element)
10176                    if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10177                {
10178                    validate_chart_space_namespace(&element)?;
10179                    return Self::from_element(&mut reader, &element);
10180                }
10181                Event::Empty(element)
10182                    if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10183                {
10184                    validate_chart_space_namespace(&element)?;
10185                    return Err(ChartError::MissingElement("c:chart".to_owned()));
10186                }
10187                Event::Start(element) | Event::Empty(element) => {
10188                    return Err(ChartError::UnexpectedElement(element_name(&element)));
10189                }
10190                Event::Eof => return Err(ChartError::MissingElement("c:chartSpace".to_owned())),
10191                _ => {}
10192            }
10193            buffer.clear();
10194        }
10195    }
10196
10197    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
10198        let namespaces = namespace_bindings(start)?;
10199        let (raw_attributes, namespace_declarations) = capture_root_attributes(start)?;
10200        let mut state = ChartSpaceParseState::default();
10201        let mut buffer = Vec::new();
10202
10203        loop {
10204            match reader
10205                .read_event_into(&mut buffer)
10206                .map_err(OxmlError::from)?
10207            {
10208                Event::Start(element) => {
10209                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10210                        local_name(element.name().as_ref()).to_vec()
10211                    } else {
10212                        Vec::new()
10213                    };
10214                    let raw = capture_element(reader, &element)?;
10215                    state.parse_child(&name, raw, &namespaces)?;
10216                }
10217                Event::Empty(element) => {
10218                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10219                        local_name(element.name().as_ref()).to_vec()
10220                    } else {
10221                        Vec::new()
10222                    };
10223                    let raw = capture_empty_element(&element)?;
10224                    state.parse_child(&name, raw, &namespaces)?;
10225                }
10226                event @ (Event::Text(_)
10227                | Event::CData(_)
10228                | Event::Comment(_)
10229                | Event::PI(_)
10230                | Event::GeneralRef(_)) => {
10231                    state.capture_event(capture_event(event)?);
10232                }
10233                Event::End(element)
10234                    if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10235                {
10236                    break;
10237                }
10238                Event::Eof => return Err(missing_end("c:chartSpace")),
10239                _ => {}
10240            }
10241            buffer.clear();
10242        }
10243
10244        state.finish(raw_attributes, namespace_declarations)
10245    }
10246
10247    /// Writes one chart-space part using fixed OOXML prefixes and schema order.
10248    pub fn to_xml(&self) -> Result<Vec<u8>> {
10249        let mut writer = Writer::new(Vec::new());
10250        let mut start = BytesStart::new("c:chartSpace");
10251        start.push_attribute(("xmlns:c", C_NS));
10252        start.push_attribute(("xmlns:a", A_NS));
10253        start.push_attribute(("xmlns:r", R_NS));
10254        push_attributes(&mut start, &self.namespace_declarations);
10255        push_attributes(&mut start, &self.raw_attributes);
10256        writer
10257            .write_event(Event::Start(start))
10258            .map_err(OxmlError::from)?;
10259        for raw_boundary in 0..8 {
10260            emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
10261        }
10262        self.chart.write_xml(&mut writer)?;
10263        emit_raw(&mut writer, self.raw_children.at(8))?;
10264        if let Some(properties) = &self.sp_pr {
10265            properties.write_xml_as(&mut writer, "c:spPr")?;
10266        }
10267        emit_raw(&mut writer, self.raw_children.at(9))?;
10268        if let Some(text) = &self.tx_pr {
10269            text.write_xml_as(&mut writer, "c:txPr")?;
10270        }
10271        for raw_boundary in 10..=14 {
10272            emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
10273        }
10274        writer
10275            .write_event(Event::End(BytesEnd::new("c:chartSpace")))
10276            .map_err(OxmlError::from)?;
10277        Ok(writer.into_inner())
10278    }
10279
10280    pub fn raw_children(&self) -> &OrderedRawChildren {
10281        &self.raw_children
10282    }
10283}
10284
10285#[derive(Default)]
10286struct ChartSpaceParseState {
10287    chart: Option<CT_Chart>,
10288    sp_pr: Option<CT_ShapeProperties>,
10289    tx_pr: Option<CT_TextBody>,
10290    raw_children: OrderedRawChildren,
10291    boundary: usize,
10292}
10293
10294impl ChartSpaceParseState {
10295    fn capture_event(&mut self, raw: Vec<u8>) {
10296        self.raw_children.push(self.boundary, raw);
10297    }
10298
10299    fn parse_child(
10300        &mut self,
10301        name: &[u8],
10302        raw: Vec<u8>,
10303        namespaces: &NamespaceBindings,
10304    ) -> Result<()> {
10305        match name {
10306            b"chart" => {
10307                set_once(
10308                    &mut self.chart,
10309                    CT_Chart::from_xml_with_namespaces(&raw, namespaces)?,
10310                    "c:chart",
10311                )?;
10312                self.boundary = self.boundary.max(8);
10313            }
10314            b"spPr" => {
10315                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
10316                let properties = CT_ShapeProperties::from_xml(&raw)?;
10317                let mut writer = Writer::new(Vec::new());
10318                properties.write_xml_as(&mut writer, "c:spPr")?;
10319                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
10320                set_once(&mut self.sp_pr, properties, "c:spPr")?;
10321                self.boundary = self.boundary.max(9);
10322            }
10323            b"txPr" => {
10324                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
10325                let text = CT_TextBody::from_xml_as(&raw, b"txPr")?;
10326                let mut writer = Writer::new(Vec::new());
10327                text.write_xml_as(&mut writer, "c:txPr")?;
10328                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
10329                set_once(&mut self.tx_pr, text, "c:txPr")?;
10330                self.boundary = self.boundary.max(10);
10331            }
10332            _ => {
10333                self.raw_children
10334                    .push(chart_space_raw_slot(name).unwrap_or(self.boundary), raw);
10335                self.boundary = self.boundary.max(chart_space_raw_boundary_after(name));
10336            }
10337        }
10338        Ok(())
10339    }
10340
10341    fn finish(
10342        self,
10343        raw_attributes: XmlAttributes,
10344        namespace_declarations: XmlAttributes,
10345    ) -> Result<CT_ChartSpace> {
10346        Ok(CT_ChartSpace {
10347            chart: self
10348                .chart
10349                .ok_or_else(|| ChartError::MissingElement("c:chart".to_owned()))?,
10350            sp_pr: self.sp_pr,
10351            tx_pr: self.tx_pr,
10352            raw_attributes,
10353            namespace_declarations,
10354            raw_children: raw_children_in_schema_order(&self.raw_children, 14),
10355        })
10356    }
10357}
10358
10359fn raw_children_in_schema_order(
10360    raw_children: &OrderedRawChildren,
10361    final_boundary: usize,
10362) -> OrderedRawChildren {
10363    let mut ordered = OrderedRawChildren::default();
10364    for boundary in 0..=final_boundary {
10365        for raw in raw_children.at(boundary) {
10366            ordered.push(boundary, raw.to_vec());
10367        }
10368    }
10369    ordered
10370}
10371
10372fn chart_space_raw_slot(name: &[u8]) -> Option<usize> {
10373    match name {
10374        b"date1904" => Some(0),
10375        b"lang" => Some(1),
10376        b"roundedCorners" => Some(2),
10377        b"style" => Some(3),
10378        b"clrMapOvr" => Some(4),
10379        b"pivotSource" => Some(5),
10380        b"protection" => Some(6),
10381        b"externalData" => Some(10),
10382        b"printSettings" => Some(11),
10383        b"userShapes" => Some(12),
10384        b"extLst" => Some(13),
10385        _ => None,
10386    }
10387}
10388
10389fn chart_space_raw_boundary_after(name: &[u8]) -> usize {
10390    match name {
10391        b"date1904" => 1,
10392        b"lang" => 2,
10393        b"roundedCorners" => 3,
10394        b"style" => 4,
10395        b"clrMapOvr" => 5,
10396        b"pivotSource" => 6,
10397        b"protection" => 7,
10398        b"chart" => 8,
10399        b"spPr" => 9,
10400        b"txPr" => 10,
10401        b"externalData" => 11,
10402        b"printSettings" => 12,
10403        b"userShapes" => 13,
10404        b"extLst" => 14,
10405        _ => 0,
10406    }
10407}
10408
10409fn parse_bool_value(xml: &[u8], local: &str) -> Result<(bool, ScalarMarkup)> {
10410    let (value, markup) = scalar_value(xml, local)?;
10411    let value = match value.as_deref().unwrap_or("1") {
10412        "1" | "true" => true,
10413        "0" | "false" => false,
10414        invalid => return Err(invalid_attribute(local, "val", invalid.to_owned())),
10415    };
10416    Ok((value, markup))
10417}
10418
10419fn parse_enum_value(xml: &[u8], local: &str) -> Result<(DispBlanksAs, ScalarMarkup)> {
10420    let (value, markup) = scalar_value(xml, local)?;
10421    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
10422    let parsed =
10423        DispBlanksAs::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
10424    Ok((parsed, markup))
10425}
10426
10427fn scalar_value(xml: &[u8], expected: &str) -> Result<(Option<String>, ScalarMarkup)> {
10428    let mut reader = Reader::from_reader(xml);
10429    let mut buffer = Vec::new();
10430    loop {
10431        match reader
10432            .read_event_into(&mut buffer)
10433            .map_err(OxmlError::from)?
10434        {
10435            Event::Empty(element)
10436                if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
10437            {
10438                reject_conflicting_prefix(&element, b"c", C_NS)?;
10439                return Ok((
10440                    attribute_value(&element, b"val")?,
10441                    ScalarMarkup {
10442                        raw_attributes: capture_attributes_except(&element, b"val")?,
10443                        raw_content: Vec::new(),
10444                    },
10445                ));
10446            }
10447            Event::Start(element)
10448                if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
10449            {
10450                reject_conflicting_prefix(&element, b"c", C_NS)?;
10451                let value = attribute_value(&element, b"val")?;
10452                let raw_attributes = capture_attributes_except(&element, b"val")?;
10453                let mut raw_content = Vec::new();
10454                let mut inner = Vec::new();
10455                loop {
10456                    match reader
10457                        .read_event_into(&mut inner)
10458                        .map_err(OxmlError::from)?
10459                    {
10460                        Event::End(end)
10461                            if matches_local_name(end.name().as_ref(), expected.as_bytes()) =>
10462                        {
10463                            return Ok((
10464                                value,
10465                                ScalarMarkup {
10466                                    raw_attributes,
10467                                    raw_content,
10468                                },
10469                            ));
10470                        }
10471                        event @ (Event::Text(_)
10472                        | Event::CData(_)
10473                        | Event::Comment(_)
10474                        | Event::PI(_)
10475                        | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
10476                        Event::Eof => return Err(missing_end(expected)),
10477                        _ => return Err(ChartError::UnexpectedElement(expected.to_owned())),
10478                    }
10479                    inner.clear();
10480                }
10481            }
10482            Event::Start(element) | Event::Empty(element) => {
10483                return Err(ChartError::UnexpectedElement(element_name(&element)));
10484            }
10485            Event::Eof => return Err(ChartError::MissingElement(expected.to_owned())),
10486            _ => {}
10487        }
10488        buffer.clear();
10489    }
10490}
10491
10492fn set_once<T>(slot: &mut Option<T>, value: T, name: &str) -> Result<()> {
10493    if slot.is_some() {
10494        return Err(ChartError::DuplicateElement(name.to_owned()));
10495    }
10496    *slot = Some(value);
10497    Ok(())
10498}
10499
10500fn mark_once(seen: &mut bool, name: &str) -> Result<()> {
10501    if *seen {
10502        return Err(ChartError::DuplicateElement(name.to_owned()));
10503    }
10504    *seen = true;
10505    Ok(())
10506}
10507
10508fn validate_chart_space_namespace(element: &BytesStart<'_>) -> Result<()> {
10509    let name = element.name();
10510    let qualified = name.as_ref();
10511    let declaration = match qualified.iter().position(|byte| *byte == b':') {
10512        Some(position) => {
10513            let mut declaration = b"xmlns:".to_vec();
10514            declaration.extend_from_slice(&qualified[..position]);
10515            declaration
10516        }
10517        None => b"xmlns".to_vec(),
10518    };
10519    let namespace = attribute_value(element, &declaration)?
10520        .ok_or_else(|| invalid_attribute("chartSpace", "namespace", "<missing>".to_owned()))?;
10521    if namespace != C_NS {
10522        return Err(invalid_attribute("chartSpace", "namespace", namespace));
10523    }
10524    reject_conflicting_prefix(element, b"c", C_NS)?;
10525    reject_conflicting_prefix(element, b"a", A_NS)?;
10526    reject_conflicting_prefix(element, b"r", R_NS)
10527}
10528
10529fn reject_conflicting_prefix_in_xml(xml: &[u8], prefix: &[u8], namespace: &str) -> Result<()> {
10530    let mut reader = Reader::from_reader(xml);
10531    let mut buffer = Vec::new();
10532    loop {
10533        match reader
10534            .read_event_into(&mut buffer)
10535            .map_err(OxmlError::from)?
10536        {
10537            Event::Start(element) | Event::Empty(element) => {
10538                return reject_conflicting_prefix(&element, prefix, namespace);
10539            }
10540            Event::Eof => return Err(ChartError::MissingElement("XML root".to_owned())),
10541            _ => {}
10542        }
10543        buffer.clear();
10544    }
10545}
10546
10547fn reject_rewritten_foreign_elements(
10548    original: &[u8],
10549    written: &[u8],
10550    inherited: &NamespaceBindings,
10551    root_local: &[u8],
10552) -> Result<()> {
10553    let original = foreign_element_regions(original, inherited, root_local)?;
10554    let mut written_namespaces = inherited.clone();
10555    written_namespaces.retain(|(prefix, _)| prefix != b"a");
10556    written_namespaces.push((b"a".to_vec(), A_NS.to_owned()));
10557    let written = foreign_element_regions(written, &written_namespaces, root_local)?;
10558    if original != written {
10559        return Err(invalid_attribute(
10560            &String::from_utf8_lossy(root_local),
10561            "namespace",
10562            "foreign DrawingML content would be converted to typed content".to_owned(),
10563        ));
10564    }
10565    Ok(())
10566}
10567
10568fn foreign_element_regions(
10569    xml: &[u8],
10570    inherited: &NamespaceBindings,
10571    root_local: &[u8],
10572) -> Result<Vec<Vec<u8>>> {
10573    let mut reader = Reader::from_reader(xml);
10574    let mut buffer = Vec::new();
10575    let root_namespaces = loop {
10576        match reader
10577            .read_event_into(&mut buffer)
10578            .map_err(OxmlError::from)?
10579        {
10580            Event::Start(element) if matches_local_name(element.name().as_ref(), root_local) => {
10581                break bindings_with_local(inherited, &element)?;
10582            }
10583            Event::Empty(element) if matches_local_name(element.name().as_ref(), root_local) => {
10584                return Ok(Vec::new());
10585            }
10586            Event::Start(element) | Event::Empty(element) => {
10587                return Err(ChartError::UnexpectedElement(element_name(&element)));
10588            }
10589            Event::Eof => {
10590                return Err(ChartError::MissingElement(
10591                    String::from_utf8_lossy(root_local).into_owned(),
10592                ));
10593            }
10594            _ => {}
10595        }
10596        buffer.clear();
10597    };
10598
10599    let mut namespace_stack = vec![root_namespaces];
10600    let mut foreign = Vec::new();
10601    let mut child_buffer = Vec::new();
10602    loop {
10603        match reader
10604            .read_event_into(&mut child_buffer)
10605            .map_err(OxmlError::from)?
10606        {
10607            Event::Start(element) => {
10608                let namespaces = namespace_stack.last().expect("root namespace binding");
10609                if element_is_in_namespace(&element, A_NS, namespaces)? {
10610                    namespace_stack.push(bindings_with_local(namespaces, &element)?);
10611                } else {
10612                    foreign.push(capture_element(&mut reader, &element)?);
10613                }
10614            }
10615            Event::Empty(element) => {
10616                let namespaces = namespace_stack.last().expect("root namespace binding");
10617                if !element_is_in_namespace(&element, A_NS, namespaces)? {
10618                    foreign.push(capture_empty_element(&element)?);
10619                }
10620            }
10621            Event::End(element) if matches_local_name(element.name().as_ref(), root_local) => {
10622                return Ok(foreign);
10623            }
10624            Event::End(_) => {
10625                namespace_stack.pop();
10626            }
10627            Event::Eof => {
10628                return Err(missing_end(&String::from_utf8_lossy(root_local)));
10629            }
10630            _ => {}
10631        }
10632        child_buffer.clear();
10633    }
10634}
10635
10636fn reject_conflicting_prefix(
10637    element: &BytesStart<'_>,
10638    prefix: &[u8],
10639    namespace: &str,
10640) -> Result<()> {
10641    let mut key = b"xmlns:".to_vec();
10642    key.extend_from_slice(prefix);
10643    if let Some(value) = attribute_value(element, &key)?
10644        && value != namespace
10645    {
10646        return Err(invalid_attribute(
10647            &element_name(element),
10648            &String::from_utf8_lossy(&key),
10649            value,
10650        ));
10651    }
10652    Ok(())
10653}
10654
10655fn namespace_bindings(element: &BytesStart<'_>) -> Result<NamespaceBindings> {
10656    let mut bindings = Vec::new();
10657    for attribute in element.attributes() {
10658        let attribute = attribute.map_err(OxmlError::from)?;
10659        let key = attribute.key.as_ref();
10660        let prefix = if key == b"xmlns" {
10661            Vec::new()
10662        } else if let Some(prefix) = key.strip_prefix(b"xmlns:") {
10663            prefix.to_vec()
10664        } else {
10665            continue;
10666        };
10667        let namespace = attribute
10668            .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
10669            .map_err(OxmlError::from)?
10670            .into_owned();
10671        bindings.push((prefix, namespace));
10672    }
10673    Ok(bindings)
10674}
10675
10676fn bindings_with_local(
10677    inherited: &NamespaceBindings,
10678    element: &BytesStart<'_>,
10679) -> Result<NamespaceBindings> {
10680    let mut bindings = inherited.clone();
10681    for (prefix, namespace) in namespace_bindings(element)? {
10682        upsert_namespace_binding(&mut bindings, prefix, namespace);
10683    }
10684    Ok(bindings)
10685}
10686
10687fn upsert_namespace_binding(bindings: &mut NamespaceBindings, prefix: Vec<u8>, namespace: String) {
10688    if let Some((_, current)) = bindings.iter_mut().find(|(current, _)| current == &prefix) {
10689        *current = namespace;
10690    } else {
10691        bindings.push((prefix, namespace));
10692    }
10693}
10694
10695fn element_is_in_namespace(
10696    element: &BytesStart<'_>,
10697    expected: &str,
10698    inherited: &NamespaceBindings,
10699) -> Result<bool> {
10700    let name = element.name();
10701    let qualified = name.as_ref();
10702    let prefix = qualified
10703        .iter()
10704        .position(|byte| *byte == b':')
10705        .map(|position| &qualified[..position])
10706        .unwrap_or_default();
10707    let local = namespace_bindings(element)?;
10708    let namespace = local
10709        .iter()
10710        .rev()
10711        .chain(inherited.iter().rev())
10712        .find(|(candidate, _)| candidate.as_slice() == prefix)
10713        .map(|(_, namespace)| namespace.as_str());
10714    Ok(namespace == Some(expected))
10715}
10716
10717fn attribute_value(element: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
10718    for attribute in element.attributes() {
10719        let attribute = attribute.map_err(OxmlError::from)?;
10720        if attribute.key.as_ref() == name {
10721            return Ok(Some(
10722                attribute
10723                    .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
10724                    .map_err(OxmlError::from)?
10725                    .into_owned(),
10726            ));
10727        }
10728    }
10729    Ok(None)
10730}
10731
10732fn capture_root_attributes(start: &BytesStart<'_>) -> Result<RootAttributes> {
10733    let mut attributes = Vec::new();
10734    let mut namespaces = Vec::new();
10735    for attribute in start.attributes() {
10736        let attribute = attribute.map_err(OxmlError::from)?;
10737        let name = std::str::from_utf8(attribute.key.as_ref())
10738            .map_err(OxmlError::from)?
10739            .to_owned();
10740        let value = attribute
10741            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10742            .map_err(OxmlError::from)?
10743            .into_owned();
10744        if name == "xmlns:c" || name == "xmlns:a" || name == "xmlns:r" {
10745            continue;
10746        }
10747        if name == "xmlns" || name.starts_with("xmlns:") {
10748            namespaces.push((name, value));
10749        } else {
10750            attributes.push((name, value));
10751        }
10752    }
10753    Ok((attributes, namespaces))
10754}
10755
10756fn capture_attributes(start: &BytesStart<'_>) -> Result<XmlAttributes> {
10757    let mut attributes = Vec::new();
10758    for attribute in start.attributes() {
10759        let attribute = attribute.map_err(OxmlError::from)?;
10760        attributes.push((
10761            std::str::from_utf8(attribute.key.as_ref())
10762                .map_err(OxmlError::from)?
10763                .to_owned(),
10764            attribute
10765                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10766                .map_err(OxmlError::from)?
10767                .into_owned(),
10768        ));
10769    }
10770    Ok(attributes)
10771}
10772
10773fn capture_attributes_except(start: &BytesStart<'_>, excluded: &[u8]) -> Result<XmlAttributes> {
10774    let mut attributes = Vec::new();
10775    for attribute in start.attributes() {
10776        let attribute = attribute.map_err(OxmlError::from)?;
10777        if attribute.key.as_ref() == excluded {
10778            continue;
10779        }
10780        attributes.push((
10781            std::str::from_utf8(attribute.key.as_ref())
10782                .map_err(OxmlError::from)?
10783                .to_owned(),
10784            attribute
10785                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10786                .map_err(OxmlError::from)?
10787                .into_owned(),
10788        ));
10789    }
10790    Ok(attributes)
10791}
10792
10793fn capture_attributes_excluding(
10794    start: &BytesStart<'_>,
10795    excluded: &[&[u8]],
10796) -> Result<XmlAttributes> {
10797    let mut attributes = Vec::new();
10798    for attribute in start.attributes() {
10799        let attribute = attribute.map_err(OxmlError::from)?;
10800        if excluded.contains(&attribute.key.as_ref()) {
10801            continue;
10802        }
10803        attributes.push((
10804            std::str::from_utf8(attribute.key.as_ref())
10805                .map_err(OxmlError::from)?
10806                .to_owned(),
10807            attribute
10808                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10809                .map_err(OxmlError::from)?
10810                .into_owned(),
10811        ));
10812    }
10813    Ok(attributes)
10814}
10815
10816fn push_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
10817    for (name, value) in attributes {
10818        start.push_attribute((name.as_str(), value.as_str()));
10819    }
10820}
10821
10822fn write_scalar<W: Write>(
10823    writer: &mut Writer<W>,
10824    name: &str,
10825    value: &str,
10826    markup: Option<&ScalarMarkup>,
10827) -> Result<()> {
10828    let mut start = BytesStart::new(name);
10829    start.push_attribute(("val", value));
10830    if let Some(markup) = markup {
10831        push_attributes(&mut start, &markup.raw_attributes);
10832    }
10833    let has_content = markup.is_some_and(|markup| !markup.raw_content.is_empty());
10834    if !has_content {
10835        writer
10836            .write_event(Event::Empty(start))
10837            .map_err(OxmlError::from)?;
10838        return Ok(());
10839    }
10840    writer
10841        .write_event(Event::Start(start))
10842        .map_err(OxmlError::from)?;
10843    if let Some(markup) = markup {
10844        for raw in &markup.raw_content {
10845            writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
10846        }
10847    }
10848    writer
10849        .write_event(Event::End(BytesEnd::new(name)))
10850        .map_err(OxmlError::from)?;
10851    Ok(())
10852}
10853
10854fn capture_event(event: Event<'_>) -> Result<Vec<u8>> {
10855    let mut writer = Writer::new(Vec::new());
10856    writer
10857        .write_event(event.into_owned())
10858        .map_err(OxmlError::from)?;
10859    Ok(writer.into_inner())
10860}
10861
10862fn emit_raw<'a, W: Write>(
10863    writer: &mut Writer<W>,
10864    children: impl Iterator<Item = &'a [u8]>,
10865) -> Result<()> {
10866    for child in children {
10867        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
10868    }
10869    Ok(())
10870}
10871
10872fn invalid_attribute(element: &str, attribute: &str, value: String) -> ChartError {
10873    ChartError::InvalidAttribute {
10874        element: element.to_owned(),
10875        attribute: attribute.to_owned(),
10876        value,
10877    }
10878}
10879
10880fn element_name(element: &BytesStart<'_>) -> String {
10881    String::from_utf8_lossy(element.name().as_ref()).into_owned()
10882}
10883
10884fn missing_end(element: &str) -> ChartError {
10885    ChartError::Xml(OxmlError::MissingElement(format!("closing {element}")))
10886}
10887
10888#[cfg(test)]
10889mod tests {
10890    use std::collections::HashSet;
10891    use std::fs;
10892    use std::path::{Path, PathBuf};
10893    use std::process::Command;
10894
10895    use oxml_core::raw_xml::{capture_element, capture_empty_element};
10896    use oxml_drawing::color::{ColorMap, ColorMapSlot, RgbColor, ThemeColorSlot};
10897    use oxml_drawing::theme::CT_OfficeStyleSheet;
10898    use oxml_layout::{
10899        Color, FontManager, LayoutResult, PageFrame, Paint, PathCommand, Point, PositionedElement,
10900        Rect,
10901    };
10902    use oxml_opc::OpcPackage;
10903    use quick_xml::Reader;
10904    use quick_xml::events::Event;
10905
10906    use super::{
10907        A_NS, Axis, AxisData, AxisId, AxisKind, AxisPosition, BarDirection, BarGrouping, C_NS,
10908        CT_ChartSpace, CT_DLbls, CT_ShapeProperties, CT_TextBody, ChartGeometry, DataLabelPosition,
10909        DispBlanksAs, Domain, Grouping, NumberFormat, NumericData, Orientation, Plot, R_NS,
10910        ScatterStyle, Series, StringRef, TickLabelPosition, TickMark, capture_event, local_name,
10911        matches_local_name, nice_number_scale, render_chart as render_chart_with_theme,
10912        render_geometry as render_geometry_with_theme,
10913    };
10914
10915    const MANIFEST: &str = include_str!("../../../scripts/pptx-corpus-manifest.tsv");
10916    const EXPECTED_DECKS: usize = 50;
10917    const LIBREOFFICE_VERSION: &str =
10918        "LibreOffice 26.2.5.2 cd7284b4cbbfeb507e630c1aac019f4157393acb";
10919    const PDFTOTEXT_VERSION: &str = "pdftotext version 26.01.0";
10920    const PDFTOPPM_VERSION: &str = "pdftoppm version 26.01.0";
10921    const PLOT_RENDER_NORMALIZED_MAE_THRESHOLD: f64 = 0.0;
10922
10923    fn render_geometry(chart: &super::CT_Chart, bounds: Rect) -> super::Result<ChartGeometry> {
10924        render_geometry_with_theme(
10925            chart,
10926            bounds,
10927            &CT_OfficeStyleSheet::office_default(),
10928            &ColorMap::default(),
10929        )
10930    }
10931
10932    fn render_chart(
10933        chart: &super::CT_Chart,
10934        bounds: Rect,
10935        fonts: &mut FontManager,
10936    ) -> super::Result<oxml_layout::GroupElement> {
10937        render_chart_with_theme(
10938            chart,
10939            bounds,
10940            &CT_OfficeStyleSheet::office_default(),
10941            &ColorMap::default(),
10942            fonts,
10943        )
10944    }
10945
10946    #[test]
10947    fn bar_chart_rasterises_at_computed_positions() {
10948        let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
10949        let geometry = render_geometry(
10950            &chart.chart,
10951            Rect {
10952                x: 0.0,
10953                y: 0.0,
10954                width: 200.0,
10955                height: 140.0,
10956            },
10957        )
10958        .unwrap();
10959        assert_eq!(
10960            geometry.plot_bounds,
10961            Rect {
10962                x: 36.0,
10963                y: 12.0,
10964                width: 152.0,
10965                height: 100.0,
10966            }
10967        );
10968        let bounds = path_bounds(&geometry);
10969        assert_eq!(bounds.len(), 2);
10970        assert_rect_near(
10971            bounds[0],
10972            Rect {
10973                x: 58.8,
10974                y: 62.0,
10975                width: 30.4,
10976                height: 50.0,
10977            },
10978        );
10979        assert_rect_near(
10980            bounds[1],
10981            Rect {
10982                x: 134.8,
10983                y: 12.0,
10984                width: 30.4,
10985                height: 100.0,
10986            },
10987        );
10988
10989        let layout = LayoutResult::new(
10990            vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
10991            Vec::new(),
10992            None,
10993            Vec::new(),
10994        );
10995        let png = oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap();
10996        let pixmap = tiny_skia::Pixmap::decode_png(&png).unwrap();
10997        for (x, y) in [(74, 87), (150, 37)] {
10998            let pixel = pixmap.pixel(x, y).unwrap();
10999            assert_ne!((pixel.red(), pixel.green(), pixel.blue()), (255, 255, 255));
11000        }
11001    }
11002
11003    #[test]
11004    fn unstyled_four_series_use_accent_one_through_four() {
11005        let chart = four_series_bar_chart();
11006        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11007        let colours = path_colours(&geometry);
11008
11009        assert_eq!(
11010            &colours[..4],
11011            &[
11012                Color::from_hex("156082"),
11013                Color::from_hex("E97132"),
11014                Color::from_hex("196B24"),
11015                Color::from_hex("0F9ED5"),
11016            ]
11017        );
11018    }
11019
11020    #[test]
11021    fn direct_series_solid_colour_overrides_theme_accent() {
11022        for (paint, expected) in [
11023            (
11024                "<a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill>",
11025                Color::from_hex("123456"),
11026            ),
11027            (
11028                "<a:ln><a:solidFill><a:srgbClr val=\"654321\"/></a:solidFill></a:ln>",
11029                Color::from_hex("654321"),
11030            ),
11031            (
11032                "<a:noFill/>",
11033                Color {
11034                    r: 0.0,
11035                    g: 0.0,
11036                    b: 0.0,
11037                    a: 0.0,
11038                },
11039            ),
11040        ] {
11041            let styled =
11042                plot_series(0).replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
11043            let plot = bar_plot("").replace(&plot_series(0), &styled);
11044            let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11045            let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11046
11047            assert_eq!(path_colours(&geometry)[0], expected);
11048        }
11049
11050        let styled = plot_series(0).replace(
11051            "<q:cat>",
11052            "<q:spPr><a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill></q:spPr><q:cat>",
11053        );
11054        let plot = bar_plot("").replace(&plot_series(0), &styled);
11055        let xml = chart_with_plot(&plot).replace(
11056            "</q:plotArea></q:chart>",
11057            "</q:plotArea><q:legend/></q:chart>",
11058        );
11059        let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
11060        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
11061        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
11062        assert!(group.children.iter().any(|element| matches!(
11063            element,
11064            PositionedElement::Path(path)
11065                if path.fill == Some(Paint::Solid(Color::from_hex("123456")))
11066        )));
11067
11068        let mut theme = CT_OfficeStyleSheet::office_default();
11069        let preset = oxml_drawing::fill::Fill::from_xml(
11070            br#"<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:prstClr val="red"/></a:solidFill>"#,
11071        )
11072        .unwrap();
11073        let oxml_drawing::fill::Fill::Solid(preset) = preset else {
11074            panic!("expected solid fill");
11075        };
11076        theme.theme_elements.color_scheme.hyperlink = preset.color.unwrap();
11077        let geometry =
11078            render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &ColorMap::default())
11079                .unwrap();
11080        assert_eq!(path_colours(&geometry)[0], Color::from_hex("123456"));
11081
11082        for (paint, expected_alpha) in [
11083            ("<a:noFill/>", 0.0),
11084            (
11085                "<a:solidFill><a:srgbClr val=\"123456\"><a:alpha val=\"50000\"/></a:srgbClr></a:solidFill>",
11086                (128.0 / 255.0) * 0.55,
11087            ),
11088        ] {
11089            let (_, area, _) = remaining_plot_fixtures()
11090                .into_iter()
11091                .find(|(kind, _, _)| *kind == "areaChart")
11092                .unwrap();
11093            let styled_area = area.replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
11094            let chart =
11095                CT_ChartSpace::from_xml(chart_with_optional_axes(&styled_area, true).as_bytes())
11096                    .unwrap();
11097            let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11098            let actual_alpha = path_colours(&geometry)[0].a;
11099
11100            assert!((actual_alpha - expected_alpha).abs() < 1.0e-12);
11101        }
11102    }
11103
11104    #[test]
11105    fn series_accent_cycle_repeats_after_six() {
11106        let chart = seven_series_bar_chart();
11107        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11108        let colours = path_colours(&geometry);
11109
11110        assert_eq!(colours[0], Color::from_hex("156082"));
11111        assert_eq!(colours[6], colours[0]);
11112    }
11113
11114    #[test]
11115    fn series_colours_honor_colour_map_and_transform_order() {
11116        let styled = plot_series(0).replace(
11117            "<q:cat>",
11118            "<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>",
11119        );
11120        let plot = bar_plot("").replace(&plot_series(0), &styled);
11121        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11122        let color_map =
11123            ColorMap::default().with_overrides(&[(ColorMapSlot::Accent1, ThemeColorSlot::Accent4)]);
11124        let mut theme = CT_OfficeStyleSheet::office_default();
11125        let theme_fill = oxml_drawing::fill::Fill::from_xml(
11126            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>"#,
11127        )
11128        .unwrap();
11129        let oxml_drawing::fill::Fill::Solid(theme_fill) = theme_fill else {
11130            panic!("expected solid fill");
11131        };
11132        let theme_colour = theme_fill.color.unwrap();
11133        theme.theme_elements.color_scheme.accent4 = theme_colour.clone();
11134        let geometry =
11135            render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &color_map).unwrap();
11136        let series_fill = oxml_drawing::fill::Fill::from_xml(
11137            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>"#,
11138        )
11139        .unwrap();
11140        let oxml_drawing::fill::Fill::Solid(series_fill) = series_fill else {
11141            panic!("expected solid fill");
11142        };
11143        let mut transforms = theme_colour.transforms().to_vec();
11144        transforms.extend_from_slice(series_fill.color.as_ref().unwrap().transforms());
11145        let expected = oxml_drawing::color::apply_color_transforms(
11146            RgbColor::parse("336699").unwrap(),
11147            &transforms,
11148        );
11149
11150        assert_eq!(path_colours(&geometry)[0], resolved_layout_colour(expected));
11151    }
11152
11153    #[test]
11154    fn chart_colours_do_not_use_word_tint_shade() {
11155        let styled = plot_series(0).replace(
11156            "<q:cat>",
11157            "<q:spPr><a:solidFill><a:srgbClr val=\"808080\"><a:tint val=\"50000\"/></a:srgbClr></a:solidFill></q:spPr><q:cat>",
11158        );
11159        let plot = bar_plot("").replace(&plot_series(0), &styled);
11160        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11161        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11162
11163        assert_eq!(
11164            path_colours(&geometry)[0],
11165            Color {
11166                r: 205.0 / 255.0,
11167                g: 205.0 / 255.0,
11168                b: 205.0 / 255.0,
11169                a: 1.0,
11170            }
11171        );
11172    }
11173
11174    #[test]
11175    fn unsupported_direct_series_paint_is_contextual() {
11176        let styled = plot_series(0).replace(
11177            "<q:cat>",
11178            "<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>",
11179        );
11180        let plot = bar_plot("").replace(&plot_series(0), &styled);
11181        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11182        let error = render_geometry(&chart.chart, chart_bounds()).unwrap_err();
11183
11184        assert!(error.to_string().contains("c:ser[0] direct fill paint"));
11185        assert!(error.to_string().contains("gradient"));
11186    }
11187
11188    #[test]
11189    fn resolved_chart_palette_raster_is_deterministic() {
11190        let raster = || {
11191            let chart = four_series_bar_chart();
11192            let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
11193            let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
11194            let layout = LayoutResult::new(
11195                vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
11196                fonts.all_font_data(),
11197                None,
11198                Vec::new(),
11199            );
11200            oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
11201        };
11202        let chart = four_series_bar_chart();
11203        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11204
11205        assert_eq!(path_colours(&geometry)[0], Color::from_hex("156082"));
11206        assert_eq!(raster(), raster());
11207    }
11208
11209    #[test]
11210    fn bar_geometry_handles_direction_grouping_gap_and_overlap() {
11211        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11212        let Plot::Bar {
11213            overlap, series, ..
11214        } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11215        else {
11216            panic!("expected bar plot");
11217        };
11218        let mut second = series[0].clone();
11219        second.index = 1;
11220        second.order = 1;
11221        second.values.values = vec![2.0, 1.0];
11222        series.push(second);
11223
11224        *overlap = 25;
11225        let clustered = render_geometry(&chart.chart, chart_bounds()).unwrap();
11226        let clustered_bounds = path_bounds(&clustered);
11227        let clustered_width = 30.4 / 1.75;
11228        let clustered_advance = clustered_width * 0.75;
11229        assert_rects_near(
11230            &clustered_bounds,
11231            &[
11232                Rect {
11233                    x: 58.8,
11234                    y: 62.0,
11235                    width: clustered_width,
11236                    height: 50.0,
11237                },
11238                Rect {
11239                    x: 134.8,
11240                    y: 12.0,
11241                    width: clustered_width,
11242                    height: 100.0,
11243                },
11244                Rect {
11245                    x: 58.8 + clustered_advance,
11246                    y: 12.0,
11247                    width: clustered_width,
11248                    height: 100.0,
11249                },
11250                Rect {
11251                    x: 134.8 + clustered_advance,
11252                    y: 62.0,
11253                    width: clustered_width,
11254                    height: 50.0,
11255                },
11256            ],
11257        );
11258
11259        let Plot::Bar { direction, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
11260            unreachable!()
11261        };
11262        *direction = BarDirection::Bar;
11263        let horizontal = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11264        let horizontal_width = 20.0 / 1.75;
11265        let horizontal_advance = horizontal_width * 0.75;
11266        assert_rects_near(
11267            &horizontal,
11268            &[
11269                Rect {
11270                    x: 36.0,
11271                    y: 27.0,
11272                    width: 76.0,
11273                    height: horizontal_width,
11274                },
11275                Rect {
11276                    x: 36.0,
11277                    y: 77.0,
11278                    width: 152.0,
11279                    height: horizontal_width,
11280                },
11281                Rect {
11282                    x: 36.0,
11283                    y: 27.0 + horizontal_advance,
11284                    width: 152.0,
11285                    height: horizontal_width,
11286                },
11287                Rect {
11288                    x: 36.0,
11289                    y: 77.0 + horizontal_advance,
11290                    width: 76.0,
11291                    height: horizontal_width,
11292                },
11293            ],
11294        );
11295
11296        let Plot::Bar {
11297            direction,
11298            grouping,
11299            ..
11300        } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11301        else {
11302            unreachable!()
11303        };
11304        *direction = BarDirection::Column;
11305        *grouping = BarGrouping::Stacked;
11306        let stacked = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11307        assert_rects_near(
11308            &stacked,
11309            &[
11310                Rect {
11311                    x: 58.8,
11312                    y: 112.0 - 100.0 / 3.0,
11313                    width: 30.4,
11314                    height: 100.0 / 3.0,
11315                },
11316                Rect {
11317                    x: 134.8,
11318                    y: 112.0 - 200.0 / 3.0,
11319                    width: 30.4,
11320                    height: 200.0 / 3.0,
11321                },
11322                Rect {
11323                    x: 58.8,
11324                    y: 12.0,
11325                    width: 30.4,
11326                    height: 200.0 / 3.0,
11327                },
11328                Rect {
11329                    x: 134.8,
11330                    y: 12.0,
11331                    width: 30.4,
11332                    height: 100.0 / 3.0,
11333                },
11334            ],
11335        );
11336
11337        let Plot::Bar {
11338            grouping, series, ..
11339        } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11340        else {
11341            unreachable!()
11342        };
11343        *grouping = BarGrouping::PercentStacked;
11344        series[1].values.values = vec![2.0, 2.0];
11345        let percent = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11346        assert_rects_near(
11347            &percent,
11348            &[
11349                Rect {
11350                    x: 58.8,
11351                    y: 112.0 - 100.0 / 3.0,
11352                    width: 30.4,
11353                    height: 100.0 / 3.0,
11354                },
11355                Rect {
11356                    x: 134.8,
11357                    y: 62.0,
11358                    width: 30.4,
11359                    height: 50.0,
11360                },
11361                Rect {
11362                    x: 58.8,
11363                    y: 12.0,
11364                    width: 30.4,
11365                    height: 200.0 / 3.0,
11366                },
11367                Rect {
11368                    x: 134.8,
11369                    y: 12.0,
11370                    width: 30.4,
11371                    height: 50.0,
11372                },
11373            ],
11374        );
11375    }
11376
11377    #[test]
11378    fn line_scatter_and_radar_emit_paths_and_markers() {
11379        let line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
11380        let line_geometry = render_geometry(&line.chart, chart_bounds()).unwrap();
11381        assert_path_points(
11382            path_commands(&line_geometry)[0],
11383            &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
11384            false,
11385        );
11386        assert_rects_near(
11387            &path_bounds(&line_geometry)[1..],
11388            &[
11389                Rect {
11390                    x: 71.5,
11391                    y: 109.5,
11392                    width: 5.0,
11393                    height: 5.0,
11394                },
11395                Rect {
11396                    x: 147.5,
11397                    y: 9.5,
11398                    width: 5.0,
11399                    height: 5.0,
11400                },
11401            ],
11402        );
11403
11404        let (scatter_plot, _) = remaining_plot_fixtures()
11405            .into_iter()
11406            .find(|(kind, _, _)| *kind == "scatterChart")
11407            .map(|(_, plot, axes)| (plot, axes))
11408            .unwrap();
11409        let mut scatter =
11410            CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
11411                .unwrap();
11412        let Plot::Scatter { style, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
11413        else {
11414            unreachable!()
11415        };
11416        *style = ScatterStyle::LineMarker;
11417        let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
11418        assert_path_points(
11419            path_commands(&scatter_geometry)[0],
11420            &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
11421            false,
11422        );
11423        assert_rects_near(
11424            &path_bounds(&scatter_geometry)[1..],
11425            &[
11426                Rect {
11427                    x: 33.5,
11428                    y: 109.5,
11429                    width: 5.0,
11430                    height: 5.0,
11431                },
11432                Rect {
11433                    x: 185.5,
11434                    y: 9.5,
11435                    width: 5.0,
11436                    height: 5.0,
11437                },
11438            ],
11439        );
11440
11441        let (radar_plot, _) = remaining_plot_fixtures()
11442            .into_iter()
11443            .find(|(kind, _, _)| *kind == "radarChart")
11444            .map(|(_, plot, axes)| (plot, axes))
11445            .unwrap();
11446        let radar = CT_ChartSpace::from_xml(chart_with_optional_axes(&radar_plot, true).as_bytes())
11447            .unwrap();
11448        let radar_geometry = render_geometry(&radar.chart, chart_bounds()).unwrap();
11449        assert_path_points(
11450            path_commands(&radar_geometry)[0],
11451            &[Point { x: 112.0, y: 37.0 }, Point { x: 112.0, y: 112.0 }],
11452            true,
11453        );
11454        assert_rects_near(
11455            &path_bounds(&radar_geometry)[1..],
11456            &[
11457                Rect {
11458                    x: 109.5,
11459                    y: 34.5,
11460                    width: 5.0,
11461                    height: 5.0,
11462                },
11463                Rect {
11464                    x: 109.5,
11465                    y: 109.5,
11466                    width: 5.0,
11467                    height: 5.0,
11468                },
11469            ],
11470        );
11471    }
11472
11473    #[test]
11474    fn pie_doughnut_and_area_emit_closed_paths() {
11475        let fixtures = remaining_plot_fixtures();
11476        let pie_plot = fixtures
11477            .iter()
11478            .find(|(kind, _, _)| *kind == "pieChart")
11479            .unwrap();
11480        let pie =
11481            CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot.1, pie_plot.2).as_bytes())
11482                .unwrap();
11483        let pie_geometry = render_geometry(&pie.chart, chart_bounds()).unwrap();
11484        let pie_commands = path_commands(&pie_geometry);
11485        assert_eq!(pie_commands.len(), 2);
11486        let [
11487            PathCommand::MoveTo(center),
11488            PathCommand::LineTo(start),
11489            ..,
11490            PathCommand::CurveTo { to: end, .. },
11491            PathCommand::Close,
11492        ] = pie_commands[0]
11493        else {
11494            panic!("expected closed cubic pie wedge");
11495        };
11496        assert_point_near(*center, Point { x: 112.0, y: 62.0 });
11497        assert_point_near(
11498            *start,
11499            Point {
11500                x: 137.0,
11501                y: 62.0 - 25.0 * 3.0_f64.sqrt(),
11502            },
11503        );
11504        assert_point_near(
11505            *end,
11506            Point {
11507                x: 137.0,
11508                y: 62.0 + 25.0 * 3.0_f64.sqrt(),
11509            },
11510        );
11511
11512        let doughnut_plot = fixtures
11513            .iter()
11514            .find(|(kind, _, _)| *kind == "doughnutChart")
11515            .unwrap();
11516        let doughnut = CT_ChartSpace::from_xml(
11517            chart_with_optional_axes(&doughnut_plot.1, doughnut_plot.2).as_bytes(),
11518        )
11519        .unwrap();
11520        let doughnut_geometry = render_geometry(&doughnut.chart, chart_bounds()).unwrap();
11521        let doughnut_commands = path_commands(&doughnut_geometry);
11522        assert_eq!(doughnut_commands.len(), 2);
11523        let commands = doughnut_commands[0];
11524        assert_eq!(commands.len(), 7);
11525        let PathCommand::MoveTo(outer_start) = commands[0] else {
11526            panic!("expected doughnut outer start");
11527        };
11528        let PathCommand::LineTo(inner_end) = commands[3] else {
11529            panic!("expected doughnut inner-radius join");
11530        };
11531        let PathCommand::CurveTo {
11532            to: inner_start, ..
11533        } = commands[5]
11534        else {
11535            panic!("expected reverse inner arc");
11536        };
11537        let diagonal = 25.0 * 2.0_f64.sqrt();
11538        assert_point_near(
11539            outer_start,
11540            Point {
11541                x: 112.0 + diagonal,
11542                y: 62.0 - diagonal,
11543            },
11544        );
11545        assert_point_near(
11546            inner_end,
11547            Point {
11548                x: 112.0 + 30.0 * 75.0_f64.to_radians().cos(),
11549                y: 62.0 + 30.0 * 75.0_f64.to_radians().sin(),
11550            },
11551        );
11552        let inner_diagonal = 15.0 * 2.0_f64.sqrt();
11553        assert_point_near(
11554            inner_start,
11555            Point {
11556                x: 112.0 + inner_diagonal,
11557                y: 62.0 - inner_diagonal,
11558            },
11559        );
11560        assert_eq!(commands.last(), Some(&PathCommand::Close));
11561
11562        let area_plot = fixtures
11563            .iter()
11564            .find(|(kind, _, _)| *kind == "areaChart")
11565            .unwrap();
11566        let mut area =
11567            CT_ChartSpace::from_xml(chart_with_optional_axes(&area_plot.1, area_plot.2).as_bytes())
11568                .unwrap();
11569        let area_geometry = render_geometry(&area.chart, chart_bounds()).unwrap();
11570        assert_path_points(
11571            path_commands(&area_geometry)[0],
11572            &[
11573                Point { x: 74.0, y: 62.0 },
11574                Point { x: 150.0, y: 12.0 },
11575                Point { x: 150.0, y: 112.0 },
11576                Point { x: 74.0, y: 112.0 },
11577            ],
11578            true,
11579        );
11580
11581        let Plot::Area {
11582            grouping, series, ..
11583        } = &mut area.chart.plot_area.plots_mut().unwrap()[0]
11584        else {
11585            unreachable!()
11586        };
11587        *grouping = Grouping::Stacked;
11588        let mut second = series[0].clone();
11589        second.index = 1;
11590        second.order = 1;
11591        second.values.values = vec![2.0, 1.0];
11592        series.push(second);
11593        let stacked_area = render_geometry(&area.chart, chart_bounds()).unwrap();
11594        assert_path_points(
11595            path_commands(&stacked_area)[1],
11596            &[
11597                Point { x: 74.0, y: 12.0 },
11598                Point { x: 150.0, y: 12.0 },
11599                Point {
11600                    x: 150.0,
11601                    y: 112.0 - 200.0 / 3.0,
11602                },
11603                Point {
11604                    x: 74.0,
11605                    y: 112.0 - 100.0 / 3.0,
11606                },
11607            ],
11608            true,
11609        );
11610    }
11611
11612    #[test]
11613    fn sparse_cache_indexes_preserve_slots_and_scatter_pairing() {
11614        let series = sparse_category_series();
11615        let bar = format!(
11616            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>"#
11617        );
11618        let bar = CT_ChartSpace::from_xml(chart_with_plot(&bar).as_bytes()).unwrap();
11619        assert_rects_near(
11620            &path_bounds(&render_geometry(&bar.chart, chart_bounds()).unwrap()),
11621            &[
11622                Rect {
11623                    x: 51.2,
11624                    y: 62.0,
11625                    width: 304.0 / 15.0,
11626                    height: 50.0,
11627                },
11628                Rect {
11629                    x: 36.0 + 304.0 / 3.0 + 15.2,
11630                    y: 12.0,
11631                    width: 304.0 / 15.0,
11632                    height: 100.0,
11633                },
11634            ],
11635        );
11636
11637        let line = format!(
11638            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>"#
11639        );
11640        let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line).as_bytes()).unwrap();
11641        let first = Point {
11642            x: 36.0 + 76.0 / 3.0,
11643            y: 62.0,
11644        };
11645        let missing = Point { x: 112.0, y: 112.0 };
11646        let last = Point {
11647            x: 36.0 + 380.0 / 3.0,
11648            y: 12.0,
11649        };
11650        let gap = render_geometry(&line.chart, chart_bounds()).unwrap();
11651        let gap_commands = path_commands(&gap);
11652        assert_eq!(gap_commands.len(), 2);
11653        assert_path_points(
11654            gap_commands[0],
11655            &[Point {
11656                x: first.x,
11657                y: 112.0,
11658            }],
11659            false,
11660        );
11661        assert_path_points(gap_commands[1], &[last], false);
11662        line.chart.disp_blanks_as = DispBlanksAs::Zero;
11663        assert_path_points(
11664            path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
11665            &[first, missing, last],
11666            false,
11667        );
11668        line.chart.disp_blanks_as = DispBlanksAs::Span;
11669        assert_path_points(
11670            path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
11671            &[
11672                Point {
11673                    x: first.x,
11674                    y: 112.0,
11675                },
11676                last,
11677            ],
11678            false,
11679        );
11680
11681        let area = format!(
11682            r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:areaChart>"#
11683        );
11684        let mut area = CT_ChartSpace::from_xml(chart_with_plot(&area).as_bytes()).unwrap();
11685        let area_gap = render_geometry(&area.chart, chart_bounds()).unwrap();
11686        let area_gap_commands = path_commands(&area_gap);
11687        assert_eq!(area_gap_commands.len(), 2);
11688        assert_path_points(
11689            area_gap_commands[0],
11690            &[
11691                first,
11692                Point {
11693                    x: first.x,
11694                    y: 112.0,
11695                },
11696            ],
11697            true,
11698        );
11699        assert_path_points(
11700            area_gap_commands[1],
11701            &[
11702                last,
11703                Point {
11704                    x: last.x,
11705                    y: 112.0,
11706                },
11707            ],
11708            true,
11709        );
11710        area.chart.disp_blanks_as = DispBlanksAs::Zero;
11711        assert_path_points(
11712            path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
11713            &[
11714                first,
11715                missing,
11716                last,
11717                Point {
11718                    x: last.x,
11719                    y: 112.0,
11720                },
11721                missing,
11722                Point {
11723                    x: first.x,
11724                    y: 112.0,
11725                },
11726            ],
11727            true,
11728        );
11729        area.chart.disp_blanks_as = DispBlanksAs::Span;
11730        assert_path_points(
11731            path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
11732            &[
11733                first,
11734                last,
11735                Point {
11736                    x: last.x,
11737                    y: 112.0,
11738                },
11739                Point {
11740                    x: first.x,
11741                    y: 112.0,
11742                },
11743            ],
11744            true,
11745        );
11746
11747        let radar = format!(
11748            r#"<q:radarChart><q:radarStyle val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#
11749        );
11750        let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
11751        assert_path_points(
11752            path_commands(&render_geometry(&radar.chart, chart_bounds()).unwrap())[0],
11753            &[
11754                Point { x: 112.0, y: 37.0 },
11755                Point {
11756                    x: 112.0 - 25.0 * 3.0_f64.sqrt(),
11757                    y: 87.0,
11758                },
11759            ],
11760            true,
11761        );
11762
11763        let scatter = format!(
11764            r#"<q:scatterChart><q:scatterStyle val="lineMarker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:scatterChart>"#,
11765            sparse_scatter_series()
11766        );
11767        let mut scatter = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
11768        let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
11769        assert_eq!(geometry_children(&scatter_geometry).len(), 4);
11770        let gap_commands = path_commands(&scatter_geometry);
11771        assert_path_points(gap_commands[0], &[Point { x: 36.0, y: 112.0 }], false);
11772        assert_path_points(gap_commands[1], &[Point { x: 188.0, y: 12.0 }], false);
11773
11774        scatter.chart.disp_blanks_as = DispBlanksAs::Zero;
11775        let zero = render_geometry(&scatter.chart, chart_bounds()).unwrap();
11776        assert_path_points(
11777            path_commands(&zero)[0],
11778            &[
11779                Point { x: 74.0, y: 87.0 },
11780                Point { x: 36.0, y: 62.0 },
11781                Point { x: 150.0, y: 112.0 },
11782                Point { x: 188.0, y: 12.0 },
11783            ],
11784            false,
11785        );
11786
11787        scatter.chart.disp_blanks_as = DispBlanksAs::Span;
11788        assert_path_points(
11789            path_commands(&render_geometry(&scatter.chart, chart_bounds()).unwrap())[0],
11790            &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
11791            false,
11792        );
11793    }
11794
11795    #[test]
11796    fn sparse_scatter_domains_and_labels_use_only_matched_points() {
11797        let scatter = format!(
11798            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>"#,
11799            sparse_scatter_series()
11800        );
11801        let chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
11802        let plot = &chart.chart.plot_area.plots().unwrap()[0];
11803        assert_eq!(
11804            super::scatter_domains(plot, DispBlanksAs::Gap).unwrap(),
11805            (
11806                Domain {
11807                    min: 10.0,
11808                    max: 40.0,
11809                },
11810                Domain {
11811                    min: 100.0,
11812                    max: 400.0,
11813                },
11814            )
11815        );
11816        let axes = chart.chart.plot_area.axes().unwrap();
11817        let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
11818        let anchors =
11819            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
11820        assert_eq!(
11821            anchors.keys().copied().collect::<Vec<_>>(),
11822            vec![(0, 0), (0, 3)]
11823        );
11824        assert_eq!(anchors[&(0, 0)].center, Point { x: 0.0, y: 140.0 });
11825        assert_eq!(anchors[&(0, 3)].center, Point { x: 200.0, y: 0.0 });
11826    }
11827
11828    #[test]
11829    fn sparse_bubble_size_labels_resolve_logical_indexes() {
11830        let scatter = format!(
11831            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>"#,
11832            sparse_scatter_series()
11833        );
11834        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
11835        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
11836        let Plot::Scatter { series, .. } = plot else {
11837            panic!("expected scatter plot");
11838        };
11839        series[0].bubble_size = Some(
11840            NumericData::from_xml(
11841                format!(
11842                    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>"#
11843                )
11844                .as_bytes(),
11845            )
11846            .unwrap(),
11847        );
11848        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
11849        let mut rendered = Vec::new();
11850        super::render_data_labels(
11851            plot,
11852            chart_bounds(),
11853            DispBlanksAs::Gap,
11854            super::GeometryOptions::default(),
11855            &mut fonts,
11856            &mut rendered,
11857        )
11858        .unwrap();
11859        let text: Vec<_> = rendered
11860            .iter()
11861            .filter_map(|element| match element {
11862                PositionedElement::Text(run) => Some(run.text.as_str()),
11863                _ => None,
11864            })
11865            .collect();
11866        assert_eq!(text, vec!["5", "9"]);
11867    }
11868
11869    #[test]
11870    fn geometry_rejects_invalid_bounds_and_opaque_plots() {
11871        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11872        for bounds in [
11873            Rect {
11874                x: 0.0,
11875                y: 0.0,
11876                width: f64::NAN,
11877                height: 100.0,
11878            },
11879            Rect {
11880                x: 0.0,
11881                y: 0.0,
11882                width: 0.0,
11883                height: 100.0,
11884            },
11885            Rect {
11886                x: 0.0,
11887                y: 0.0,
11888                width: 40.0,
11889                height: 30.0,
11890            },
11891        ] {
11892            assert!(render_geometry(&chart.chart, bounds).is_err());
11893        }
11894
11895        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
11896            unreachable!()
11897        };
11898        series[0].values.values.clear();
11899        assert!(render_geometry(&chart.chart, chart_bounds()).is_err());
11900
11901        let opaque = CT_ChartSpace::from_xml(
11902            chart_with_plot(r#"<q:bar3DChart><q:barDir val="col"/></q:bar3DChart>"#).as_bytes(),
11903        )
11904        .unwrap();
11905        let error = render_geometry(&opaque.chart, chart_bounds()).unwrap_err();
11906        assert!(error.to_string().contains("unsupported or combination"));
11907
11908        let (scatter_plot, _) = remaining_plot_fixtures()
11909            .into_iter()
11910            .find(|(kind, _, _)| *kind == "scatterChart")
11911            .map(|(_, plot, axes)| (plot, axes))
11912            .unwrap();
11913        let mut scatter =
11914            CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
11915                .unwrap();
11916        for invalid in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
11917            let Plot::Scatter { series, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
11918            else {
11919                unreachable!()
11920            };
11921            let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
11922                unreachable!()
11923            };
11924            x_values.values[0] = invalid;
11925            let error = render_geometry(&scatter.chart, chart_bounds()).unwrap_err();
11926            assert!(error.to_string().contains("xVal"));
11927        }
11928    }
11929
11930    #[test]
11931    fn finite_extremes_never_produce_nonfinite_geometry() {
11932        let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
11933        let Plot::Line { series, .. } = &mut line.chart.plot_area.plots_mut().unwrap()[0] else {
11934            unreachable!()
11935        };
11936        series[0].values.values = vec![-f64::MAX, f64::MAX];
11937        let extreme_line = render_geometry(&line.chart, chart_bounds()).unwrap();
11938        assert_path_points(
11939            path_commands(&extreme_line)[0],
11940            &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
11941            false,
11942        );
11943
11944        let mut stacked =
11945            CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11946        let Plot::Bar {
11947            grouping, series, ..
11948        } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
11949        else {
11950            unreachable!()
11951        };
11952        *grouping = BarGrouping::Stacked;
11953        series[0].values.values = vec![f64::MAX, 1.0];
11954        let mut second = series[0].clone();
11955        second.index = 1;
11956        second.order = 1;
11957        series.push(second);
11958        let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
11959        assert!(error.to_string().contains("stacked value total"));
11960
11961        let Plot::Bar { grouping, .. } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
11962        else {
11963            unreachable!()
11964        };
11965        *grouping = BarGrouping::PercentStacked;
11966        let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
11967        assert!(error.to_string().contains("stacked percentage total"));
11968
11969        let (pie_plot, _) = remaining_plot_fixtures()
11970            .into_iter()
11971            .find(|(kind, _, _)| *kind == "pieChart")
11972            .map(|(_, plot, axes)| (plot, axes))
11973            .unwrap();
11974        let mut pie =
11975            CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot, false).as_bytes()).unwrap();
11976        let Plot::Pie { series, .. } = &mut pie.chart.plot_area.plots_mut().unwrap()[0] else {
11977            unreachable!()
11978        };
11979        series[0].values.values = vec![f64::MAX, f64::MAX];
11980        let error = render_geometry(&pie.chart, chart_bounds()).unwrap_err();
11981        assert!(error.to_string().contains("pie value total"));
11982
11983        let (scatter_plot, _) = remaining_plot_fixtures()
11984            .into_iter()
11985            .find(|(kind, _, _)| *kind == "scatterChart")
11986            .map(|(_, plot, axes)| (plot, axes))
11987            .unwrap();
11988        let mut scatter =
11989            CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
11990                .unwrap();
11991        let Plot::Scatter { style, series, .. } =
11992            &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
11993        else {
11994            unreachable!()
11995        };
11996        *style = ScatterStyle::Line;
11997        let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
11998            unreachable!()
11999        };
12000        x_values.values = vec![-f64::MAX, f64::MAX];
12001        series[0].values.values = vec![-f64::MAX, f64::MAX];
12002        let extreme_scatter = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12003        assert_path_points(
12004            path_commands(&extreme_scatter)[0],
12005            &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
12006            false,
12007        );
12008    }
12009
12010    #[test]
12011    fn geometry_is_backend_neutral_and_deterministic() {
12012        let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12013        let first = render_geometry(&chart.chart, chart_bounds()).unwrap();
12014        let second = render_geometry(&chart.chart, chart_bounds()).unwrap();
12015        assert_eq!(
12016            format!("{:?}", first.elements),
12017            format!("{:?}", second.elements)
12018        );
12019
12020        let raster = |geometry: ChartGeometry| {
12021            let layout = LayoutResult::new(
12022                vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
12023                Vec::new(),
12024                None,
12025                Vec::new(),
12026            );
12027            oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
12028        };
12029        assert_eq!(raster(first), raster(second));
12030    }
12031
12032    #[test]
12033    fn zero_to_one_hundred_axis_uses_expected_ticks() {
12034        let scale = nice_number_scale(
12035            Domain {
12036                min: 0.0,
12037                max: 100.0,
12038            },
12039            None,
12040            None,
12041            6,
12042        )
12043        .unwrap();
12044        assert_eq!(scale.ticks, vec![0.0, 20.0, 40.0, 60.0, 80.0, 100.0]);
12045    }
12046
12047    #[test]
12048    fn nice_number_ticks_cover_unpinned_extents() {
12049        for domain in [
12050            Domain {
12051                min: 3.0,
12052                max: 97.0,
12053            },
12054            Domain {
12055                min: -97.0,
12056                max: -3.0,
12057            },
12058            Domain {
12059                min: -3.5,
12060                max: 8.25,
12061            },
12062            Domain {
12063                min: 0.125,
12064                max: 0.875,
12065            },
12066            Domain {
12067                min: 1.0e12,
12068                max: 7.0e12,
12069            },
12070            Domain {
12071                min: -f64::MAX,
12072                max: f64::MAX,
12073            },
12074            Domain {
12075                min: f64::from_bits(1),
12076                max: f64::from_bits(5),
12077            },
12078        ] {
12079            let scale = nice_number_scale(domain, None, None, 6).unwrap();
12080            assert!(scale.domain.min <= domain.min);
12081            assert!(scale.domain.max >= domain.max);
12082            assert!(scale.ticks.iter().all(|tick| tick.is_finite()));
12083            assert!(scale.ticks.windows(2).all(|pair| pair[0] < pair[1]));
12084        }
12085
12086        let constant = nice_number_scale(Domain { min: 4.0, max: 4.0 }, None, None, 6).unwrap();
12087        assert!(constant.domain.min < 4.0 && constant.domain.max > 4.0);
12088    }
12089
12090    #[test]
12091    fn one_sided_explicit_scale_beyond_data_derives_a_valid_opposite_bound() {
12092        let minimum = nice_number_scale(
12093            Domain {
12094                min: 0.0,
12095                max: 10.0,
12096            },
12097            Some(20.0),
12098            None,
12099            6,
12100        )
12101        .unwrap();
12102        assert_eq!(minimum.domain.min, 20.0);
12103        assert!(minimum.domain.max > 20.0);
12104        assert!(minimum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
12105
12106        let maximum = nice_number_scale(
12107            Domain {
12108                min: 0.0,
12109                max: 10.0,
12110            },
12111            None,
12112            Some(-20.0),
12113            6,
12114        )
12115        .unwrap();
12116        assert_eq!(maximum.domain.max, -20.0);
12117        assert!(maximum.domain.min < -20.0);
12118        assert!(maximum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
12119    }
12120
12121    #[test]
12122    fn standard_line_domain_does_not_force_zero() {
12123        let mut chart =
12124            CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
12125        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12126        let Plot::Line { series, .. } = plot else {
12127            panic!("expected line plot");
12128        };
12129        series[0].values.values = vec![1_000.0, 1_010.0];
12130        assert_eq!(
12131            super::plot_value_domain(plot, DispBlanksAs::Gap).unwrap(),
12132            Some(Domain {
12133                min: 1_000.0,
12134                max: 1_010.0,
12135            })
12136        );
12137        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12138        assert_path_points(
12139            path_commands(&geometry)[0],
12140            &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
12141            false,
12142        );
12143    }
12144
12145    #[test]
12146    fn explicit_scaling_bounds_and_orientation_are_honored() {
12147        let scale = nice_number_scale(
12148            Domain {
12149                min: 25.0,
12150                max: 75.0,
12151            },
12152            Some(0.0),
12153            Some(100.0),
12154            6,
12155        )
12156        .unwrap();
12157        assert_eq!(
12158            scale.domain,
12159            Domain {
12160                min: 0.0,
12161                max: 100.0
12162            }
12163        );
12164        assert_eq!(
12165            super::map_y_oriented(0.0, scale.domain, chart_bounds(), Orientation::MaxMin).unwrap(),
12166            0.0
12167        );
12168        assert_eq!(
12169            super::map_y_oriented(100.0, scale.domain, chart_bounds(), Orientation::MaxMin)
12170                .unwrap(),
12171            140.0
12172        );
12173
12174        let mut chart = labelled_bar_chart();
12175        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12176            panic!("expected bar plot");
12177        };
12178        series[0].values.values = vec![25.0, 75.0];
12179        let value_axis = chart
12180            .chart
12181            .plot_area
12182            .axes
12183            .iter_mut()
12184            .find(|axis| axis.kind == AxisKind::Value)
12185            .unwrap();
12186        value_axis.scaling.orientation = Orientation::MaxMin;
12187        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12188        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12189        let plot = group
12190            .children
12191            .iter()
12192            .find_map(|element| match element {
12193                PositionedElement::Group(group) => Some(group),
12194                _ => None,
12195            })
12196            .unwrap();
12197        let bars: Vec<_> = plot
12198            .children
12199            .iter()
12200            .filter_map(|element| match element {
12201                PositionedElement::Path(path) => path.path.bounds(),
12202                _ => None,
12203            })
12204            .collect();
12205        assert_eq!(bars.len(), 2);
12206        assert!((bars[0].y - 12.0).abs() < 1.0e-9);
12207        assert!((bars[0].height - 25.0).abs() < 1.0e-9);
12208        assert!((bars[1].height - 75.0).abs() < 1.0e-9);
12209    }
12210
12211    #[test]
12212    fn family_label_anchors_share_rendered_geometry() {
12213        let mut bar = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12214        let bar_plot = &mut bar.chart.plot_area.plots_mut().unwrap()[0];
12215        let Plot::Bar { series, .. } = bar_plot else {
12216            panic!("expected bar plot");
12217        };
12218        let mut second = series[0].clone();
12219        second.index = 1;
12220        second.order = 1;
12221        series.push(second);
12222        let anchors = super::plot_label_anchors(
12223            bar_plot,
12224            chart_bounds(),
12225            DispBlanksAs::Gap,
12226            super::GeometryOptions::default(),
12227        )
12228        .unwrap();
12229        assert_point_near(anchors[&(0, 0)].center, Point { x: 40.0, y: 105.0 });
12230        assert_point_near(anchors[&(1, 0)].center, Point { x: 60.0, y: 105.0 });
12231
12232        let pie = CT_ChartSpace::from_xml(
12233            chart_with_optional_axes(
12234                &remaining_plot_fixtures()
12235                    .into_iter()
12236                    .find(|(kind, _, _)| *kind == "pieChart")
12237                    .unwrap()
12238                    .1,
12239                false,
12240            )
12241            .as_bytes(),
12242        )
12243        .unwrap();
12244        let Plot::Pie {
12245            first_slice_angle,
12246            series,
12247            ..
12248        } = &pie.chart.plot_area.plots().unwrap()[0]
12249        else {
12250            panic!("expected pie plot");
12251        };
12252        let slices = super::pie_slices(*first_slice_angle, None, series, chart_bounds()).unwrap();
12253        assert_eq!(slices.len(), 2);
12254        let first_span = std::f64::consts::TAU / 3.0;
12255        let first_midpoint = f64::from(*first_slice_angle).to_radians()
12256            - std::f64::consts::FRAC_PI_2
12257            + first_span / 2.0;
12258        assert_point_near(
12259            slices[0].anchor,
12260            super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, first_midpoint),
12261        );
12262        let second_midpoint = first_midpoint + std::f64::consts::PI;
12263        assert_point_near(
12264            slices[1].anchor,
12265            super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, second_midpoint),
12266        );
12267
12268        let radar = format!(
12269            r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12270            plot_series(0)
12271        );
12272        let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
12273        let Plot::Radar { series, .. } = &radar.chart.plot_area.plots().unwrap()[0] else {
12274            panic!("expected radar plot");
12275        };
12276        let radar_points =
12277            super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
12278                .unwrap();
12279        assert_eq!(radar_points[0][0], (0, Point { x: 100.0, y: 35.0 }));
12280        assert_eq!(radar_points[0][1], (1, Point { x: 100.0, y: 140.0 }));
12281    }
12282
12283    #[test]
12284    fn radar_annotations_use_spokes_perimeter_labels_and_radial_gridlines() {
12285        let radar_plot = format!(
12286            r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12287            sparse_category_series()
12288        );
12289        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12290        for axis in &mut chart.chart.plot_area.axes {
12291            axis.major_tick_mark = TickMark::Cross;
12292            if axis.kind == AxisKind::Value {
12293                axis.major_gridlines = Some(super::ChartLines::default());
12294            }
12295        }
12296        let plot = &chart.chart.plot_area.plots().unwrap()[0];
12297        let axes = chart.chart.plot_area.axes().unwrap();
12298        let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
12299        let center = Point { x: 112.0, y: 62.0 };
12300        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12301        let annotations =
12302            super::render_axes_and_labels(plot, &axes, bounds, DispBlanksAs::Gap, &mut fonts)
12303                .unwrap();
12304        assert_eq!(annotations.axis_lines.len(), 4);
12305        for spoke in annotations.axis_lines.iter().take(3) {
12306            let PositionedElement::Path(spoke) = spoke else {
12307                panic!("radar spoke must be a path");
12308            };
12309            assert_eq!(
12310                spoke.path.commands.first(),
12311                Some(&PathCommand::MoveTo(center))
12312            );
12313        }
12314        assert!(!annotations.gridlines.is_empty());
12315        for gridline in &annotations.gridlines {
12316            let PositionedElement::Path(gridline) = gridline else {
12317                panic!("radar gridline must be a path");
12318            };
12319            assert_eq!(gridline.path.commands.last(), Some(&PathCommand::Close));
12320            let radii: Vec<_> = gridline
12321                .path
12322                .commands
12323                .iter()
12324                .filter_map(|command| match command {
12325                    PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
12326                        Some((point.x - center.x).hypot(point.y - center.y))
12327                    }
12328                    _ => None,
12329                })
12330                .collect();
12331            assert_eq!(radii.len(), 3);
12332            assert!(
12333                radii
12334                    .windows(2)
12335                    .all(|pair| (pair[0] - pair[1]).abs() < 1.0e-9)
12336            );
12337        }
12338        let text: Vec<_> = annotations
12339            .labels
12340            .iter()
12341            .filter_map(|element| match element {
12342                PositionedElement::Text(run) => Some(run.text.as_str()),
12343                _ => None,
12344            })
12345            .collect();
12346        assert!(text.contains(&"North"));
12347        assert!(text.contains(&"West"));
12348    }
12349
12350    #[test]
12351    fn radar_tick_label_positions_are_distinct() {
12352        let radar_plot = format!(
12353            r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12354            plot_series(0)
12355        );
12356        let chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12357        let plot = &chart.chart.plot_area.plots().unwrap()[0];
12358        let axes = chart.chart.plot_area.axes().unwrap();
12359        let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
12360        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12361
12362        let category_axis = axes
12363            .iter()
12364            .find(|axis| axis.kind == AxisKind::Category)
12365            .unwrap();
12366        for (position, expected) in [
12367            (TickLabelPosition::High, Point { x: 108.0, y: 0.0 }),
12368            (TickLabelPosition::Low, Point { x: 108.0, y: 25.0 }),
12369            (TickLabelPosition::NextTo, Point { x: 108.0, y: 5.0 }),
12370        ] {
12371            let mut axis = category_axis.clone();
12372            axis.tick_label_position = position;
12373            let annotations =
12374                super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
12375                    .unwrap();
12376            let north = annotations
12377                .labels
12378                .iter()
12379                .find_map(|element| match element {
12380                    PositionedElement::Text(run) if run.text == "North" => Some(run),
12381                    _ => None,
12382                })
12383                .unwrap();
12384            assert_point_near(north.origin, expected);
12385        }
12386        let mut hidden_category = category_axis.clone();
12387        hidden_category.tick_label_position = TickLabelPosition::None;
12388        let annotations = super::render_axes_and_labels(
12389            plot,
12390            &[hidden_category],
12391            bounds,
12392            DispBlanksAs::Gap,
12393            &mut fonts,
12394        )
12395        .unwrap();
12396        assert!(annotations.labels.is_empty());
12397
12398        let narrow_bounds = Rect {
12399            x: 36.0,
12400            y: 12.0,
12401            width: 24.0,
12402            height: 100.0,
12403        };
12404        let narrow_center = Point { x: 48.0, y: 62.0 };
12405        assert_eq!(
12406            super::radar_category_label_origin(
12407                narrow_bounds,
12408                narrow_center,
12409                12.0,
12410                1,
12411                4,
12412                TickLabelPosition::High,
12413            ),
12414            Some(Point { x: 72.0, y: 65.0 })
12415        );
12416        assert_eq!(
12417            super::radar_category_label_origin(
12418                narrow_bounds,
12419                narrow_center,
12420                12.0,
12421                1,
12422                4,
12423                TickLabelPosition::NextTo,
12424            ),
12425            Some(Point { x: 66.0, y: 65.0 })
12426        );
12427
12428        let value_axis = axes
12429            .iter()
12430            .find(|axis| axis.kind == AxisKind::Value)
12431            .unwrap();
12432        for (position, expected) in [
12433            (TickLabelPosition::High, Point { x: 166.0, y: 65.0 }),
12434            (TickLabelPosition::Low, Point { x: 32.0, y: 65.0 }),
12435            (TickLabelPosition::NextTo, Point { x: 116.0, y: 65.0 }),
12436        ] {
12437            let mut axis = value_axis.clone();
12438            axis.tick_label_position = position;
12439            let annotations =
12440                super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
12441                    .unwrap();
12442            let zero = annotations
12443                .labels
12444                .iter()
12445                .find_map(|element| match element {
12446                    PositionedElement::Text(run) if run.text == "0" => Some(run),
12447                    _ => None,
12448                })
12449                .unwrap();
12450            assert_point_near(zero.origin, expected);
12451        }
12452        let mut hidden_value = value_axis.clone();
12453        hidden_value.tick_label_position = TickLabelPosition::None;
12454        let annotations = super::render_axes_and_labels(
12455            plot,
12456            &[hidden_value],
12457            bounds,
12458            DispBlanksAs::Gap,
12459            &mut fonts,
12460        )
12461        .unwrap();
12462        assert!(annotations.labels.is_empty());
12463    }
12464
12465    #[test]
12466    fn radar_negative_and_mixed_sign_domains_preserve_values() {
12467        let radar_plot = format!(
12468            r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12469            plot_series(0)
12470        );
12471        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12472        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12473        let Plot::Radar { series, .. } = plot else {
12474            panic!("expected radar plot");
12475        };
12476        series[0].values.values = vec![-10.0, -5.0];
12477        let points =
12478            super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
12479                .unwrap();
12480        assert_eq!(points[0][0], (0, Point { x: 100.0, y: 70.0 }));
12481        assert_eq!(points[0][1], (1, Point { x: 100.0, y: 105.0 }));
12482
12483        series[0].values.values = vec![-5.0, 10.0];
12484        let options = super::GeometryOptions {
12485            value: Some(super::AxisMapping {
12486                domain: Domain {
12487                    min: -10.0,
12488                    max: 10.0,
12489                },
12490                orientation: Orientation::MinMax,
12491            }),
12492            ..super::GeometryOptions::default()
12493        };
12494        let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
12495        assert_eq!(points[0][0], (0, Point { x: 100.0, y: 52.5 }));
12496        assert_eq!(points[0][1], (1, Point { x: 100.0, y: 140.0 }));
12497    }
12498
12499    #[test]
12500    fn radar_explicit_bounds_suppress_out_of_range_points_and_labels() {
12501        let radar_plot = format!(
12502            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>"#,
12503            plot_series(0)
12504        );
12505        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12506        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12507        let Plot::Radar { series, .. } = plot else {
12508            panic!("expected radar plot");
12509        };
12510        series[0].values.values = vec![25.0, 75.0];
12511        let options = super::GeometryOptions {
12512            value: Some(super::AxisMapping {
12513                domain: Domain {
12514                    min: 50.0,
12515                    max: 100.0,
12516                },
12517                orientation: Orientation::MinMax,
12518            }),
12519            ..super::GeometryOptions::default()
12520        };
12521        let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
12522        assert_eq!(points[0], vec![(1, Point { x: 100.0, y: 105.0 })]);
12523        let anchors =
12524            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12525        assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 1)]);
12526
12527        let Plot::Radar { series, .. } = plot else {
12528            panic!("expected radar plot");
12529        };
12530        series[0].values.values = vec![75.0, 125.0];
12531        let reversed = super::GeometryOptions {
12532            value: Some(super::AxisMapping {
12533                domain: Domain {
12534                    min: 50.0,
12535                    max: 100.0,
12536                },
12537                orientation: Orientation::MaxMin,
12538            }),
12539            ..super::GeometryOptions::default()
12540        };
12541        let points = super::radar_series_points(series, chart_bounds(), reversed).unwrap();
12542        assert_eq!(points[0], vec![(0, Point { x: 100.0, y: 35.0 })]);
12543        let anchors =
12544            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
12545        assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
12546
12547        let value_axis = chart
12548            .chart
12549            .plot_area
12550            .axes
12551            .iter_mut()
12552            .find(|axis| axis.kind == AxisKind::Value)
12553            .unwrap();
12554        value_axis.scaling.minimum = Some(50.0);
12555        value_axis.scaling.maximum = Some(100.0);
12556        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12557        let Plot::Radar { series, .. } = plot else {
12558            panic!("expected radar plot");
12559        };
12560        series[0].values.values = vec![25.0, 25.0];
12561        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12562        let rendered = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12563        assert!(rendered.children.iter().any(|element| matches!(
12564            element,
12565            PositionedElement::Group(group) if group.clip.is_some() && group.children.is_empty()
12566        )));
12567        assert!(
12568            rendered
12569                .children
12570                .iter()
12571                .any(|element| matches!(element, PositionedElement::Path(_)))
12572        );
12573        assert!(!rendered.children.iter().any(|element| matches!(
12574            element,
12575            PositionedElement::Text(run) if run.text == "25"
12576        )));
12577    }
12578
12579    #[test]
12580    fn inside_and_outside_label_positions_follow_family_geometry() {
12581        let mut horizontal =
12582            CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12583        let plot = &mut horizontal.chart.plot_area.plots_mut().unwrap()[0];
12584        let Plot::Bar { direction, .. } = plot else {
12585            panic!("expected bar plot");
12586        };
12587        *direction = BarDirection::Bar;
12588        let anchors = super::plot_label_anchors(
12589            plot,
12590            chart_bounds(),
12591            DispBlanksAs::Gap,
12592            super::GeometryOptions::default(),
12593        )
12594        .unwrap();
12595        let anchor = anchors[&(0, 0)];
12596        assert_point_near(
12597            anchor.origin(Some(DataLabelPosition::InsideBase)),
12598            Point { x: 5.0, y: 35.0 },
12599        );
12600        assert_point_near(
12601            anchor.origin(Some(DataLabelPosition::InsideEnd)),
12602            Point { x: 95.0, y: 35.0 },
12603        );
12604        assert_point_near(
12605            anchor.origin(Some(DataLabelPosition::OutsideEnd)),
12606            Point { x: 105.0, y: 35.0 },
12607        );
12608
12609        let mut negative =
12610            CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12611        let plot = &mut negative.chart.plot_area.plots_mut().unwrap()[0];
12612        let Plot::Bar { series, .. } = plot else {
12613            panic!("expected bar plot");
12614        };
12615        series[0].values.values = vec![-1.0, -2.0];
12616        let anchors = super::plot_label_anchors(
12617            plot,
12618            chart_bounds(),
12619            DispBlanksAs::Gap,
12620            super::GeometryOptions::default(),
12621        )
12622        .unwrap();
12623        assert_point_near(
12624            anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12625            Point { x: 50.0, y: 75.0 },
12626        );
12627        let reversed = super::GeometryOptions {
12628            value: Some(super::AxisMapping {
12629                domain: Domain {
12630                    min: -2.0,
12631                    max: 0.0,
12632                },
12633                orientation: Orientation::MaxMin,
12634            }),
12635            ..super::GeometryOptions::default()
12636        };
12637        let anchors =
12638            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
12639        assert_point_near(
12640            anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12641            Point { x: 50.0, y: 65.0 },
12642        );
12643
12644        let pie = CT_ChartSpace::from_xml(
12645            chart_with_optional_axes(
12646                &remaining_plot_fixtures()
12647                    .into_iter()
12648                    .find(|(kind, _, _)| *kind == "pieChart")
12649                    .unwrap()
12650                    .1,
12651                false,
12652            )
12653            .as_bytes(),
12654        )
12655        .unwrap();
12656        let Plot::Pie { series, .. } = &pie.chart.plot_area.plots().unwrap()[0] else {
12657            panic!("expected pie plot");
12658        };
12659        let pie_slice = &super::pie_slices(30, None, series, chart_bounds()).unwrap()[0];
12660        assert_point_near(
12661            super::LabelAnchor::segment(pie_slice.anchor, pie_slice.base, pie_slice.end)
12662                .origin(Some(DataLabelPosition::OutsideEnd)),
12663            Point { x: 175.0, y: 70.0 },
12664        );
12665        let doughnut_slice = &super::pie_slices(30, Some(60), series, chart_bounds()).unwrap()[0];
12666        assert_point_near(
12667            super::LabelAnchor::segment(
12668                doughnut_slice.anchor,
12669                doughnut_slice.base,
12670                doughnut_slice.end,
12671            )
12672            .origin(Some(DataLabelPosition::InsideBase)),
12673            Point { x: 147.0, y: 70.0 },
12674        );
12675
12676        let short = super::LabelAnchor::segment(
12677            Point { x: 2.0, y: 0.0 },
12678            Point { x: 0.0, y: 0.0 },
12679            Point { x: 4.0, y: 0.0 },
12680        );
12681        assert_eq!(
12682            short.origin(Some(DataLabelPosition::InsideBase)),
12683            Point { x: 2.0, y: 0.0 }
12684        );
12685        assert_eq!(
12686            short.origin(Some(DataLabelPosition::InsideEnd)),
12687            Point { x: 2.0, y: 0.0 }
12688        );
12689        let thin_bounds = Rect {
12690            x: 0.0,
12691            y: 0.0,
12692            width: 80.0,
12693            height: 80.0,
12694        };
12695        let thin = &super::pie_slices(30, Some(90), series, thin_bounds).unwrap()[0];
12696        let thin = super::LabelAnchor::segment(thin.anchor, thin.base, thin.end);
12697        assert_point_near(
12698            thin.origin(Some(DataLabelPosition::InsideBase)),
12699            Point { x: 78.0, y: 40.0 },
12700        );
12701        assert_point_near(
12702            thin.origin(Some(DataLabelPosition::InsideEnd)),
12703            Point { x: 78.0, y: 40.0 },
12704        );
12705    }
12706
12707    #[test]
12708    fn degenerate_bar_and_radar_anchors_preserve_family_direction() {
12709        let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12710        let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
12711        let Plot::Bar { series, .. } = plot else {
12712            panic!("expected bar plot");
12713        };
12714        series[0].values.values = vec![80.0, 125.0];
12715        for (direction, orientation, boundary, outside) in [
12716            (
12717                BarDirection::Column,
12718                Orientation::MinMax,
12719                Point { x: 50.0, y: 140.0 },
12720                Point { x: 50.0, y: 135.0 },
12721            ),
12722            (
12723                BarDirection::Column,
12724                Orientation::MaxMin,
12725                Point { x: 50.0, y: 0.0 },
12726                Point { x: 50.0, y: 5.0 },
12727            ),
12728            (
12729                BarDirection::Bar,
12730                Orientation::MinMax,
12731                Point { x: 0.0, y: 35.0 },
12732                Point { x: 5.0, y: 35.0 },
12733            ),
12734            (
12735                BarDirection::Bar,
12736                Orientation::MaxMin,
12737                Point { x: 200.0, y: 35.0 },
12738                Point { x: 195.0, y: 35.0 },
12739            ),
12740        ] {
12741            let Plot::Bar {
12742                direction: plot_direction,
12743                ..
12744            } = plot
12745            else {
12746                panic!("expected bar plot");
12747            };
12748            *plot_direction = direction;
12749            let options = super::GeometryOptions {
12750                value: Some(super::AxisMapping {
12751                    domain: Domain {
12752                        min: 80.0,
12753                        max: 100.0,
12754                    },
12755                    orientation,
12756                }),
12757                ..super::GeometryOptions::default()
12758            };
12759            let anchors =
12760                super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options)
12761                    .unwrap();
12762            let anchor = anchors[&(0, 0)];
12763            assert_point_near(anchor.center, boundary);
12764            assert_point_near(anchor.base, boundary);
12765            assert_point_near(anchor.end, boundary);
12766            assert_point_near(anchor.origin(Some(DataLabelPosition::InsideBase)), boundary);
12767            assert_point_near(anchor.origin(Some(DataLabelPosition::InsideEnd)), boundary);
12768            assert_point_near(anchor.origin(Some(DataLabelPosition::OutsideEnd)), outside);
12769        }
12770
12771        let radar_plot = format!(
12772            r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12773            plot_series(0)
12774        );
12775        let mut radar = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12776        let plot = &mut radar.chart.plot_area.plots_mut().unwrap()[0];
12777        let Plot::Radar { series, .. } = plot else {
12778            panic!("expected radar plot");
12779        };
12780        series[0].values.values = vec![0.0, 0.0, 0.0, 0.0];
12781        let anchors = super::plot_label_anchors(
12782            plot,
12783            chart_bounds(),
12784            DispBlanksAs::Gap,
12785            super::GeometryOptions::default(),
12786        )
12787        .unwrap();
12788        let center = Point { x: 100.0, y: 70.0 };
12789        for logical_index in 0..4 {
12790            assert_point_near(
12791                anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideBase)),
12792                center,
12793            );
12794            assert_point_near(
12795                anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideEnd)),
12796                center,
12797            );
12798        }
12799        assert_point_near(
12800            anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12801            Point { x: 100.0, y: 65.0 },
12802        );
12803        assert_point_near(
12804            anchors[&(0, 1)].origin(Some(DataLabelPosition::OutsideEnd)),
12805            Point { x: 105.0, y: 70.0 },
12806        );
12807        assert_point_near(
12808            anchors[&(0, 2)].origin(Some(DataLabelPosition::OutsideEnd)),
12809            Point { x: 100.0, y: 75.0 },
12810        );
12811        assert_point_near(
12812            anchors[&(0, 3)].origin(Some(DataLabelPosition::OutsideEnd)),
12813            Point { x: 95.0, y: 70.0 },
12814        );
12815    }
12816
12817    #[test]
12818    fn labels_outside_explicit_axis_bounds_are_suppressed() {
12819        let mut chart =
12820            CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
12821        {
12822            let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12823            let Plot::Line { series, .. } = plot else {
12824                panic!("expected line plot");
12825            };
12826            series[0].values.values = vec![25.0, 75.0];
12827        }
12828        let value_axis = chart
12829            .chart
12830            .plot_area
12831            .axes
12832            .iter_mut()
12833            .find(|axis| axis.kind == AxisKind::Value)
12834            .unwrap();
12835        value_axis.scaling.minimum = Some(50.0);
12836        value_axis.scaling.maximum = Some(100.0);
12837        value_axis.scaling.orientation = Orientation::MaxMin;
12838        let plot = &chart.chart.plot_area.plots().unwrap()[0];
12839        let axes = chart.chart.plot_area.axes().unwrap();
12840        let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
12841        let anchors =
12842            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12843        assert!(!anchors.contains_key(&(0, 0)));
12844        assert_point_near(anchors[&(0, 1)].center, Point { x: 150.0, y: 70.0 });
12845
12846        let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12847        let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
12848        let Plot::Bar { series, .. } = plot else {
12849            panic!("expected bar plot");
12850        };
12851        series[0].values.values = vec![90.0, 125.0];
12852        let options = super::GeometryOptions {
12853            value: Some(super::AxisMapping {
12854                domain: Domain {
12855                    min: 80.0,
12856                    max: 100.0,
12857                },
12858                orientation: Orientation::MinMax,
12859            }),
12860            ..super::GeometryOptions::default()
12861        };
12862        let anchors =
12863            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12864        assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
12865        assert_point_near(anchors[&(0, 0)].base, Point { x: 50.0, y: 140.0 });
12866        assert_point_near(anchors[&(0, 0)].end, Point { x: 50.0, y: 70.0 });
12867        assert_point_near(anchors[&(0, 0)].center, Point { x: 50.0, y: 105.0 });
12868        assert_point_near(
12869            anchors[&(0, 0)].origin(Some(DataLabelPosition::InsideBase)),
12870            Point { x: 50.0, y: 135.0 },
12871        );
12872    }
12873
12874    #[test]
12875    fn zero_bars_render_and_emit_requested_value_labels() {
12876        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12877        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12878        let Plot::Bar { series, .. } = plot else {
12879            panic!("expected bar plot");
12880        };
12881        series[0].values.values = vec![0.0, 0.0];
12882        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12883        assert_eq!(path_commands(&geometry).len(), 2);
12884        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12885        render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12886        let plot = &chart.chart.plot_area.plots().unwrap()[0];
12887        let mut labels = Vec::new();
12888        super::render_data_labels(
12889            plot,
12890            chart_bounds(),
12891            DispBlanksAs::Gap,
12892            super::GeometryOptions::default(),
12893            &mut fonts,
12894            &mut labels,
12895        )
12896        .unwrap();
12897        let text: Vec<_> = labels
12898            .iter()
12899            .filter_map(|element| match element {
12900                PositionedElement::Text(run) => Some(run.text.as_str()),
12901                _ => None,
12902            })
12903            .collect();
12904        assert_eq!(text, vec!["0", "0"]);
12905    }
12906
12907    #[test]
12908    fn axes_gridlines_and_tick_marks_follow_model_state() {
12909        let chart = labelled_bar_chart();
12910        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12911        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12912        let ranks: Vec<_> = group
12913            .children
12914            .iter()
12915            .map(|element| match element {
12916                PositionedElement::Group(group) => {
12917                    assert!(group.clip.is_some());
12918                    1
12919                }
12920                PositionedElement::Path(path)
12921                    if path
12922                        .stroke
12923                        .as_ref()
12924                        .is_some_and(|stroke| stroke.width == 0.5) =>
12925                {
12926                    0
12927                }
12928                PositionedElement::Path(path) if path.fill.is_some() => 4,
12929                PositionedElement::Path(path) => {
12930                    let bounds = path.path.bounds().expect("axis or tick bounds");
12931                    assert_eq!(path.stroke.as_ref().map(|stroke| stroke.width), Some(1.0));
12932                    if bounds.width > 4.0 || bounds.height > 4.0 {
12933                        2
12934                    } else {
12935                        3
12936                    }
12937                }
12938                PositionedElement::Text(_) => 5,
12939                _ => panic!("unexpected labelled chart element"),
12940            })
12941            .collect();
12942        assert!(ranks.windows(2).all(|pair| pair[0] <= pair[1]), "{ranks:?}");
12943        for required in 0..=5 {
12944            assert!(
12945                ranks.contains(&required),
12946                "missing z-order rank {required}: {ranks:?}"
12947            );
12948        }
12949        assert_eq!(ranks.iter().filter(|rank| **rank == 1).count(), 1);
12950        assert_eq!(ranks.iter().filter(|rank| **rank == 2).count(), 2);
12951
12952        let mut deleted = chart.clone();
12953        for axis in &mut deleted.chart.plot_area.axes {
12954            axis.deleted = true;
12955        }
12956        let deleted_group = render_chart(&deleted.chart, chart_bounds(), &mut fonts).unwrap();
12957        assert_eq!(
12958            deleted_group
12959                .children
12960                .iter()
12961                .filter(|element| {
12962                    matches!(
12963                        element,
12964                        PositionedElement::Path(path) if path.stroke.is_some()
12965                    )
12966                })
12967                .count(),
12968            0
12969        );
12970    }
12971
12972    #[test]
12973    fn category_ticks_cover_unlabelled_and_sparse_logical_slots() {
12974        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12975        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12976            panic!("expected bar plot");
12977        };
12978        series[0].categories = None;
12979        let plot = &chart.chart.plot_area.plots().unwrap()[0];
12980        let mut category_axis = chart
12981            .chart
12982            .plot_area
12983            .axes()
12984            .unwrap()
12985            .into_iter()
12986            .find(|axis| axis.kind == AxisKind::Category)
12987            .unwrap();
12988        category_axis.major_gridlines = Some(super::ChartLines::default());
12989        category_axis.major_tick_mark = TickMark::Cross;
12990        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12991        let annotations = super::render_axes_and_labels(
12992            plot,
12993            &[category_axis],
12994            chart_bounds(),
12995            DispBlanksAs::Gap,
12996            &mut fonts,
12997        )
12998        .unwrap();
12999        assert_eq!(annotations.gridlines.len(), 2);
13000        assert_eq!(annotations.ticks.len(), 2);
13001        assert!(annotations.labels.is_empty());
13002
13003        let sparse_plot = format!(
13004            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>"#,
13005            sparse_category_series()
13006        );
13007        let sparse = CT_ChartSpace::from_xml(chart_with_plot(&sparse_plot).as_bytes()).unwrap();
13008        let plot = &sparse.chart.plot_area.plots().unwrap()[0];
13009        let mut category_axis = sparse
13010            .chart
13011            .plot_area
13012            .axes()
13013            .unwrap()
13014            .into_iter()
13015            .find(|axis| axis.kind == AxisKind::Category)
13016            .unwrap();
13017        category_axis.major_gridlines = Some(super::ChartLines::default());
13018        category_axis.major_tick_mark = TickMark::Cross;
13019        let annotations = super::render_axes_and_labels(
13020            plot,
13021            &[category_axis],
13022            chart_bounds(),
13023            DispBlanksAs::Gap,
13024            &mut fonts,
13025        )
13026        .unwrap();
13027        assert_eq!(annotations.gridlines.len(), 3);
13028        assert_eq!(annotations.ticks.len(), 3);
13029        assert_eq!(annotations.labels.len(), 2);
13030
13031        let oversized_plot = sparse_plot.replacen(
13032            r#"<q:ptCount val="3"/>"#,
13033            r#"<q:ptCount val="4294967295"/>"#,
13034            1,
13035        );
13036        let oversized =
13037            CT_ChartSpace::from_xml(chart_with_plot(&oversized_plot).as_bytes()).unwrap();
13038        let plot = &oversized.chart.plot_area.plots().unwrap()[0];
13039        let category_axis = oversized
13040            .chart
13041            .plot_area
13042            .axes()
13043            .unwrap()
13044            .into_iter()
13045            .find(|axis| axis.kind == AxisKind::Category)
13046            .unwrap();
13047        let result = super::render_axes_and_labels(
13048            plot,
13049            &[category_axis],
13050            chart_bounds(),
13051            DispBlanksAs::Gap,
13052            &mut fonts,
13053        );
13054        let error = match result {
13055            Err(error) => error,
13056            Ok(_) => panic!("oversized category annotations rendered"),
13057        };
13058        assert!(
13059            error
13060                .to_string()
13061                .contains("logical category count 4294967295")
13062        );
13063        assert!(error.to_string().contains("exceeds 16384"));
13064    }
13065
13066    #[test]
13067    fn unsupported_numeric_category_formats_return_projection_errors() {
13068        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13069        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
13070            panic!("expected bar plot");
13071        };
13072        series[0].categories = Some(AxisData::Numeric(
13073            NumericData::new(
13074                "Sheet1!$A$2:$A$3".to_owned(),
13075                "General".to_owned(),
13076                vec![1.0, 2.0],
13077            )
13078            .unwrap(),
13079        ));
13080        let plot = &chart.chart.plot_area.plots().unwrap()[0];
13081        let mut category_axis = chart
13082            .chart
13083            .plot_area
13084            .axes()
13085            .unwrap()
13086            .into_iter()
13087            .find(|axis| axis.kind == AxisKind::Category)
13088            .unwrap();
13089        category_axis.number_format = Some(NumberFormat::new("0.00".to_owned(), false).unwrap());
13090        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13091        let annotations = super::render_axes_and_labels(
13092            plot,
13093            &[category_axis.clone()],
13094            chart_bounds(),
13095            DispBlanksAs::Gap,
13096            &mut fonts,
13097        )
13098        .unwrap();
13099        let text: Vec<_> = annotations
13100            .labels
13101            .iter()
13102            .filter_map(|element| match element {
13103                PositionedElement::Text(run) => Some(run.text.as_str()),
13104                _ => None,
13105            })
13106            .collect();
13107        assert_eq!(text, vec!["1.00", "2.00"]);
13108
13109        category_axis.number_format = Some(NumberFormat::new("#,##0".to_owned(), false).unwrap());
13110        let result = super::render_axes_and_labels(
13111            plot,
13112            &[category_axis],
13113            chart_bounds(),
13114            DispBlanksAs::Gap,
13115            &mut fonts,
13116        );
13117        let error = match result {
13118            Err(error) => error,
13119            Ok(_) => panic!("unsupported category-axis format rendered"),
13120        };
13121        assert!(
13122            error
13123                .to_string()
13124                .contains("c:numFmt/@formatCode projection")
13125        );
13126        assert!(error.to_string().contains("#,##0"));
13127    }
13128
13129    #[test]
13130    fn labels_and_legend_shape_with_deterministic_fonts() {
13131        let chart = labelled_bar_chart();
13132        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13133        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13134        let labels: Vec<_> = group
13135            .children
13136            .iter()
13137            .filter_map(|element| match element {
13138                PositionedElement::Text(run) => Some(run),
13139                _ => None,
13140            })
13141            .collect();
13142        for text in ["North", "South", "20", "Sales", "100"] {
13143            assert!(labels.iter().any(|run| run.text == text), "missing {text}");
13144        }
13145        let styled_tick = labels.iter().find(|run| run.text == "20").unwrap();
13146        assert_eq!(styled_tick.font_size, 12.0);
13147        assert!(styled_tick.bold);
13148        assert!(
13149            labels
13150                .iter()
13151                .all(|run| !run.glyph_ids.is_empty() && run.glyph_ids.len() == run.advances.len())
13152        );
13153    }
13154
13155    #[test]
13156    fn labelled_chart_raster_is_deterministic() {
13157        let raster = || {
13158            let chart = labelled_bar_chart();
13159            let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13160            let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13161            let layout = LayoutResult::new(
13162                vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
13163                fonts.all_font_data(),
13164                None,
13165                Vec::new(),
13166            );
13167            oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
13168        };
13169        assert_eq!(raster(), raster());
13170    }
13171
13172    fn labelled_bar_chart() -> CT_ChartSpace {
13173        let series = plot_series(0)
13174            .replace(
13175                "<q:cat>",
13176                "<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>",
13177            )
13178            .replace("<q:v>1</q:v>", "<q:v>0</q:v>")
13179            .replace("<q:v>2</q:v>", "<q:v>100</q:v>");
13180        let plot = format!(
13181            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>"#
13182        );
13183        let xml = chart_with_plot(&plot)
13184            .replacen(
13185                "<q:scaling/>",
13186                "<q:scaling><q:orientation val=\"minMax\"/></q:scaling>",
13187                1,
13188            )
13189            .replacen(
13190                "<q:scaling/>",
13191                "<q:scaling><q:max val=\"100\"/><q:min val=\"0\"/></q:scaling>",
13192                1,
13193            )
13194            .replace(
13195                "<q:axPos val=\"b\"/>",
13196                "<q:axPos val=\"b\"/><q:majorTickMark val=\"cross\"/>",
13197            )
13198            .replace(
13199                "<q:axPos val=\"l\"/>",
13200                "<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>",
13201            )
13202            .replace("</q:plotArea></q:chart>", "</q:plotArea><q:legend/></q:chart>");
13203        CT_ChartSpace::from_xml(xml.as_bytes()).unwrap()
13204    }
13205
13206    fn chart_bounds() -> Rect {
13207        Rect {
13208            x: 0.0,
13209            y: 0.0,
13210            width: 200.0,
13211            height: 140.0,
13212        }
13213    }
13214
13215    fn four_series_bar_chart() -> CT_ChartSpace {
13216        let series = (0..4).map(plot_series).collect::<String>();
13217        let plot = bar_plot("").replace(&plot_series(0), &series);
13218        CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
13219    }
13220
13221    fn seven_series_bar_chart() -> CT_ChartSpace {
13222        let series = (0..7).map(plot_series).collect::<String>();
13223        let plot = bar_plot("").replace(&plot_series(0), &series);
13224        CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
13225    }
13226
13227    fn path_colours(geometry: &ChartGeometry) -> Vec<Color> {
13228        let mut colours = geometry_children(geometry)
13229            .iter()
13230            .filter_map(|element| match element {
13231                PositionedElement::Path(path) => match path
13232                    .fill
13233                    .as_ref()
13234                    .or_else(|| path.stroke.as_ref().map(|stroke| &stroke.paint))
13235                {
13236                    Some(Paint::Solid(colour)) => Some(*colour),
13237                    _ => None,
13238                },
13239                _ => None,
13240            })
13241            .collect::<Vec<_>>();
13242        colours.dedup_by(|left, right| left == right);
13243        colours
13244    }
13245
13246    fn resolved_layout_colour(colour: oxml_drawing::color::ResolvedColor) -> Color {
13247        Color {
13248            r: f64::from(colour.red) / 255.0,
13249            g: f64::from(colour.green) / 255.0,
13250            b: f64::from(colour.blue) / 255.0,
13251            a: f64::from(colour.alpha) / 255.0,
13252        }
13253    }
13254
13255    fn geometry_children(geometry: &ChartGeometry) -> &[PositionedElement] {
13256        let [PositionedElement::Group(group)] = geometry.elements.as_slice() else {
13257            panic!("chart geometry must contain one group");
13258        };
13259        &group.children
13260    }
13261
13262    fn path_commands(geometry: &ChartGeometry) -> Vec<&[PathCommand]> {
13263        geometry_children(geometry)
13264            .iter()
13265            .map(|element| {
13266                let PositionedElement::Path(path) = element else {
13267                    panic!("chart geometry child must be a path");
13268                };
13269                path.path.commands.as_slice()
13270            })
13271            .collect()
13272    }
13273
13274    fn path_bounds(geometry: &ChartGeometry) -> Vec<Rect> {
13275        geometry_children(geometry)
13276            .iter()
13277            .map(|element| {
13278                let PositionedElement::Path(path) = element else {
13279                    panic!("chart geometry child must be a path");
13280                };
13281                path.path.bounds().expect("chart path has bounds")
13282            })
13283            .collect()
13284    }
13285
13286    fn assert_rect_near(actual: Rect, expected: Rect) {
13287        assert_near(actual.x, expected.x);
13288        assert_near(actual.y, expected.y);
13289        assert_near(actual.width, expected.width);
13290        assert_near(actual.height, expected.height);
13291    }
13292
13293    fn assert_rects_near(actual: &[Rect], expected: &[Rect]) {
13294        assert_eq!(actual.len(), expected.len());
13295        for (actual, expected) in actual.iter().copied().zip(expected.iter().copied()) {
13296            assert_rect_near(actual, expected);
13297        }
13298    }
13299
13300    fn assert_path_points(commands: &[PathCommand], expected: &[Point], closed: bool) {
13301        let points: Vec<_> = commands
13302            .iter()
13303            .filter_map(|command| match command {
13304                PathCommand::MoveTo(point) | PathCommand::LineTo(point) => Some(*point),
13305                PathCommand::Close => None,
13306                PathCommand::CurveTo { .. } => panic!("expected polygonal path"),
13307            })
13308            .collect();
13309        assert_eq!(points.len(), expected.len());
13310        for (actual, expected) in points.into_iter().zip(expected.iter().copied()) {
13311            assert_point_near(actual, expected);
13312        }
13313        assert_eq!(commands.last() == Some(&PathCommand::Close), closed);
13314    }
13315
13316    fn assert_point_near(actual: Point, expected: Point) {
13317        assert_near(actual.x, expected.x);
13318        assert_near(actual.y, expected.y);
13319    }
13320
13321    fn assert_near(actual: f64, expected: f64) {
13322        assert!(
13323            (actual - expected).abs() < 1e-9,
13324            "expected {expected}, got {actual}"
13325        );
13326    }
13327
13328    #[test]
13329    fn remaining_v1_plots_round_trip_and_render() {
13330        for (kind, plot, axes) in remaining_plot_fixtures() {
13331            let xml = chart_with_optional_axes(&plot, axes);
13332            let parsed = CT_ChartSpace::from_xml(xml.as_bytes())
13333                .unwrap_or_else(|error| panic!("{kind}: parse failed: {error}"));
13334            assert_eq!(parsed.chart.plot_area.plots().unwrap().len(), 1);
13335            assert_eq!(
13336                parsed.chart.plot_area.axes().unwrap().len(),
13337                usize::from(axes) * 2
13338            );
13339            let written = parsed
13340                .to_xml()
13341                .unwrap_or_else(|error| panic!("{kind}: write failed: {error}"));
13342            assert_eq!(
13343                parsed,
13344                CT_ChartSpace::from_xml(&written)
13345                    .unwrap_or_else(|error| panic!("{kind}: reparse failed: {error}"))
13346            );
13347        }
13348        if let Some(corpus) = require_or_skip_corpus() {
13349            verify_remaining_plot_viewer_gate(&corpus);
13350        }
13351    }
13352
13353    #[test]
13354    fn remaining_plot_families_write_fixed_prefixes_in_schema_order() {
13355        for (kind, plot, axes) in remaining_plot_fixtures() {
13356            let parsed =
13357                CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
13358            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13359            assert!(written.contains(&format!("<c:{kind}")));
13360            assert!(!written.contains(&format!("<q:{kind}")));
13361            let series = written.find("<c:ser").unwrap();
13362            let labels = written.find("<c:dLbls").unwrap();
13363            assert!(series < labels, "{kind}: series must precede labels");
13364            if matches!(kind, "areaChart" | "scatterChart" | "radarChart") {
13365                let property = match kind {
13366                    "areaChart" => "<c:grouping",
13367                    "scatterChart" => "<c:scatterStyle",
13368                    "radarChart" => "<c:radarStyle",
13369                    _ => unreachable!(),
13370                };
13371                assert!(written.find(property).unwrap() < series);
13372                assert!(labels < written.find("<c:axId").unwrap());
13373            } else {
13374                assert!(labels < written.find("<c:firstSliceAng").unwrap());
13375            }
13376            if kind == "doughnutChart" {
13377                assert!(
13378                    written.find("<c:firstSliceAng").unwrap()
13379                        < written.find("<c:holeSize").unwrap()
13380                );
13381            }
13382        }
13383    }
13384
13385    #[test]
13386    fn scatter_series_map_numeric_categories_and_values_to_x_and_y() {
13387        let plot = remaining_plot_fixtures()
13388            .into_iter()
13389            .find(|(kind, _, _)| *kind == "scatterChart")
13390            .unwrap()
13391            .1;
13392        let mut parsed =
13393            CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, true).as_bytes()).unwrap();
13394        let standalone_series = {
13395            let Plot::Scatter { series, .. } = &mut parsed.chart.plot_area.plots_mut().unwrap()[0]
13396            else {
13397                panic!("expected scatter plot");
13398            };
13399            let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
13400                panic!("expected numeric x values");
13401            };
13402            x_values.values[0] = 3.5;
13403            series[0].values.values[0] = 7.5;
13404            series[0].clone()
13405        };
13406        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13407        assert!(written.contains("<c:xVal>"));
13408        assert!(written.contains("<c:yVal>"));
13409        assert!(!written.contains("<c:cat>"));
13410        assert!(!written.contains("<c:val>"));
13411        assert!(written.contains("<c:v>3.5</c:v>"));
13412        assert!(written.contains("<c:v>7.5</c:v>"));
13413        let standalone = String::from_utf8(standalone_series.to_xml().unwrap()).unwrap();
13414        assert!(standalone.contains("<c:xVal>"));
13415        assert!(standalone.contains("<c:yVal>"));
13416        assert!(!standalone.contains("<c:cat>"));
13417        assert!(!standalone.contains("<c:val>"));
13418    }
13419
13420    #[test]
13421    fn malformed_remaining_plots_return_errors_without_panicking() {
13422        let series = plot_series(0);
13423        let scatter = numeric_plot_series(0);
13424        let opaque_bubble = series.replace(
13425            "</q:val></q:ser>",
13426            "</q:val><q:bubbleSize><x:opaque/></q:bubbleSize></q:ser>",
13427        );
13428        let cases = [
13429            r#"<q:pieChart/>"#.to_owned(),
13430            format!(r#"<q:pieChart>{series}<q:firstSliceAng val="361"/></q:pieChart>"#),
13431            format!(
13432                r#"<q:pieChart>{series}<q:firstSliceAng val="1"/><q:firstSliceAng val="2"/></q:pieChart>"#
13433            ),
13434            format!(r#"<q:doughnutChart>{series}<q:holeSize val="9"/></q:doughnutChart>"#),
13435            format!(
13436                r#"<q:doughnutChart>{series}<q:holeSize val="50"/><q:holeSize val="60"/></q:doughnutChart>"#
13437            ),
13438            format!(
13439                r#"<q:areaChart><q:grouping val="clustered"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
13440            ),
13441            format!(
13442                r#"<q:areaChart><q:grouping val="standard"/><q:grouping val="stacked"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
13443            ),
13444            format!(
13445                r#"<q:scatterChart><q:scatterStyle val="dots"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13446            ),
13447            format!(
13448                r#"<q:scatterChart><q:scatterStyle val="marker"/><q:scatterStyle val="line"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13449            ),
13450            format!(
13451                r#"<q:radarChart><q:radarStyle val="smooth"/>{series}<q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
13452            ),
13453            format!(
13454                r#"<q:radarChart><q:radarStyle val="marker"/>{series}<q:dLbls/><q:dLbls/><q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
13455            ),
13456            format!(r#"<q:pieChart>{series}<q:axId val="1"/></q:pieChart>"#),
13457            format!(
13458                r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/></q:areaChart>"#
13459            ),
13460            r#"<q:areaChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:areaChart>"#.to_owned(),
13461            format!(
13462                r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/><q:axId val="2"/><q:axId val="3"/></q:areaChart>"#
13463            ),
13464            format!(
13465                r#"<q:scatterChart><q:scatterStyle val="marker"/>{series}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13466            ),
13467            format!(
13468                r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13469                scatter
13470                    .replace("<q:yVal>", "<q:val>")
13471                    .replace("</q:yVal>", "</q:val>")
13472            ),
13473            format!(
13474                r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13475                scatter_without_x(0)
13476            ),
13477            format!(
13478                r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13479                scatter_without_y(0)
13480            ),
13481            format!(r#"<q:pieChart>{opaque_bubble}</q:pieChart>"#),
13482        ];
13483        for plot in cases {
13484            let xml = chart_with_optional_axes(&plot, false);
13485            let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
13486            assert!(result.is_ok(), "parser panicked for {plot}");
13487            assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
13488        }
13489
13490        let missing_axis = format!(
13491            r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="7"/></q:areaChart>"#
13492        );
13493        let missing_axis =
13494            CT_ChartSpace::from_xml(chart_with_optional_axes(&missing_axis, true).as_bytes())
13495                .unwrap_err();
13496        assert!(missing_axis.to_string().contains("missing axis 7"));
13497
13498        let values =
13499            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
13500        let mut bubble_series = Series::new(0, 0, values.clone());
13501        bubble_series.bubble_size = Some(values);
13502        assert!(Plot::pie(vec![bubble_series]).is_err());
13503    }
13504
13505    #[test]
13506    fn unsupported_plot_families_and_children_remain_byte_preserved() {
13507        for raw in [
13508            r#"<q:pie3DChart x:keep="3d"><q:ser><x:opaque/></q:ser></q:pie3DChart>"#,
13509            r#"<q:ofPieChart x:keep="of"><x:opaque/></q:ofPieChart>"#,
13510            r#"<q:stockChart x:keep="stock"><x:opaque/></q:stockChart>"#,
13511            r#"<q:surfaceChart x:keep="surface"><x:opaque/></q:surfaceChart>"#,
13512            r#"<q:bubbleChart x:keep="bubble"><x:opaque/></q:bubbleChart>"#,
13513        ] {
13514            let parsed =
13515                CT_ChartSpace::from_xml(chart_with_optional_axes(raw, false).as_bytes()).unwrap();
13516            assert!(parsed.chart.plot_area.plots().is_err());
13517            let written = parsed.to_xml().unwrap();
13518            assert!(
13519                written
13520                    .windows(raw.len())
13521                    .any(|window| window == raw.as_bytes())
13522            );
13523        }
13524
13525        let pie = remaining_plot_fixtures()[0].1.clone();
13526        let preserved = r#"<!--point--><q:dPt x:id="1"><q:explosion val="7"/><x:tail/></q:dPt>"#;
13527        let plot = pie.replace("<q:dLbls>", &format!("{preserved}<q:dLbls>"));
13528        let parsed =
13529            CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, false).as_bytes()).unwrap();
13530        let written = parsed.to_xml().unwrap();
13531        assert!(
13532            written
13533                .windows(preserved.len())
13534                .any(|window| window == preserved.as_bytes())
13535        );
13536
13537        for (kind, mut plot, axes) in [
13538            ("pieChart", remaining_plot_fixtures()[0].1.clone(), false),
13539            ("areaChart", remaining_plot_fixtures()[2].1.clone(), true),
13540        ] {
13541            plot = plot.replace(r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#, "");
13542            if kind == "pieChart" {
13543                plot = plot.replace(
13544                    "</q:extLst></q:pieChart>",
13545                    "</q:extLst><!--after-ext--></q:pieChart>",
13546                );
13547            } else {
13548                plot = plot.replace(
13549                    "</q:dropLines><q:axId",
13550                    "</q:dropLines><!--after-drop-lines--><q:axId",
13551                );
13552            }
13553            let mut parsed =
13554                CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
13555            let labels =
13556                CT_DLbls::from_xml(format!(r#"<c:dLbls xmlns:c="{C_NS}"/>"#).as_bytes()).unwrap();
13557            match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
13558                Plot::Pie { data_labels, .. } | Plot::Area { data_labels, .. } => {
13559                    *data_labels = Some(labels)
13560                }
13561                _ => panic!("expected pie or area"),
13562            }
13563            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13564            assert!(
13565                written.find("<c:dLbls").unwrap() < written.find("<q:extLst").unwrap(),
13566                "{kind}: inserted labels must precede the preserved extension"
13567            );
13568            if kind == "areaChart" {
13569                assert!(written.find("<c:dLbls").unwrap() < written.find("<q:dropLines").unwrap());
13570                assert!(
13571                    written.find("<q:dropLines").unwrap()
13572                        < written.find("<!--after-drop-lines-->").unwrap()
13573                );
13574            } else {
13575                assert!(
13576                    written.find("<q:extLst").unwrap() < written.find("<!--after-ext-->").unwrap()
13577                );
13578            }
13579        }
13580    }
13581
13582    #[test]
13583    fn every_supported_corpus_plot_round_trips_structurally() {
13584        let Some(corpus) = require_or_skip_corpus() else {
13585            return;
13586        };
13587        verify_fetched_corpus(&corpus);
13588        let mut pie_count = 0usize;
13589        let mut bar_count = 0usize;
13590        let mut line_count = 0usize;
13591        for path in manifest_paths() {
13592            let package = OpcPackage::open(corpus.join(path)).unwrap();
13593            for (part, xml) in &package.parts {
13594                if !is_chart_part(part) {
13595                    continue;
13596                }
13597                let parsed = CT_ChartSpace::from_xml(xml).unwrap();
13598                if let Ok(plots) = parsed.chart.plot_area.plots() {
13599                    for plot in plots {
13600                        match plot {
13601                            Plot::Pie { .. } => pie_count += 1,
13602                            Plot::Bar { .. } => bar_count += 1,
13603                            Plot::Line { .. } => line_count += 1,
13604                            _ => {}
13605                        }
13606                    }
13607                }
13608                let written = parsed.to_xml().unwrap();
13609                assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
13610            }
13611        }
13612        assert_eq!(pie_count, 1, "typed corpus pie coverage changed");
13613        assert_eq!(bar_count, 11, "typed corpus bar coverage changed");
13614        assert_eq!(line_count, 2, "typed corpus line coverage changed");
13615    }
13616
13617    fn standalone_axis(local: &str, children: &str) -> String {
13618        format!(
13619            r#"<c:{local} xmlns:c="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}">{children}</c:{local}>"#
13620        )
13621    }
13622
13623    fn minimal_axis(local: &str, id: &str, position: &str, cross_axis: &str) -> String {
13624        format!(
13625            r#"<c:{local}><c:axId val="{id}"/><c:scaling/><c:axPos val="{position}"/><c:crossAx val="{cross_axis}"/></c:{local}>"#
13626        )
13627    }
13628
13629    fn chart_with_axes(axes: &[String]) -> String {
13630        format!(
13631            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>"#,
13632            axes.join("")
13633        )
13634    }
13635
13636    #[test]
13637    fn data_labels_write_fixed_prefixes_in_schema_order() {
13638        let xml = format!(
13639            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>"#
13640        );
13641        let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
13642        assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0%");
13643        assert_eq!(parsed.position, Some(DataLabelPosition::OutsideEnd));
13644        assert_eq!(parsed.separator.as_deref(), Some(" / "));
13645        assert!(parsed.show_legend_key);
13646        assert!(parsed.show_value);
13647        assert!(!parsed.show_category_name);
13648        assert!(parsed.show_series_name);
13649        assert!(!parsed.show_percent);
13650        assert!(parsed.show_bubble_size);
13651
13652        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13653        assert!(written.starts_with(&format!(
13654            "<c:dLbls xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
13655        )));
13656        let mut cursor = 0usize;
13657        for tag in [
13658            "<q:dLbl",
13659            "<c:numFmt",
13660            "<q:spPr",
13661            "<q:txPr",
13662            "<c:dLblPos",
13663            "<c:showLegendKey",
13664            "<c:showVal",
13665            "<c:showCatName",
13666            "<c:showSerName",
13667            "<c:showPercent",
13668            "<c:showBubbleSize",
13669            "<c:separator",
13670            "<q:leaderLines",
13671            "<q:extLst",
13672        ] {
13673            let position = written[cursor..]
13674                .find(tag)
13675                .unwrap_or_else(|| panic!("missing ordered data-label tag {tag}"));
13676            cursor += position + tag.len();
13677        }
13678        assert_eq!(parsed, CT_DLbls::from_xml(written.as_bytes()).unwrap());
13679    }
13680
13681    #[test]
13682    fn percentage_formatted_label_renders_with_correct_text() {
13683        let Some(corpus) = require_or_skip_corpus() else {
13684            return;
13685        };
13686        verify_fetched_corpus(&corpus);
13687        assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
13688        assert_command_version("pdftotext", &["-v"], PDFTOTEXT_VERSION);
13689
13690        let source = corpus.join("bar-chart.pptx");
13691        let mut package = OpcPackage::open(&source).expect("open percentage-label source deck");
13692        let chart_part = "/ppt/charts/chart1.xml";
13693        let original_parts = package.parts.clone();
13694        let chart_xml = package
13695            .get_part(chart_part)
13696            .expect("bar chart part")
13697            .to_vec();
13698        let original_series = first_series_xml(&chart_xml).expect("bar chart series");
13699        let mut series = Series::from_xml(&original_series).expect("parse bar chart series");
13700        series.values.values.fill(0.25);
13701        let labels = CT_DLbls {
13702            number_format: Some(NumberFormat::new("0%".to_owned(), false).unwrap()),
13703            position: Some(DataLabelPosition::OutsideEnd),
13704            show_value: true,
13705            ..CT_DLbls::default()
13706        };
13707        series.data_labels = Some(labels);
13708        let replacement = series.to_xml().expect("serialize typed percentage labels");
13709        let candidate_chart = replace_once(&chart_xml, &original_series, &replacement);
13710        package.set_part(chart_part, candidate_chart);
13711        for (part, bytes) in &original_parts {
13712            if part == chart_part {
13713                assert_ne!(package.parts[part], *bytes);
13714            } else {
13715                assert_eq!(
13716                    package.parts[part], *bytes,
13717                    "unexpected changed part {part}"
13718                );
13719            }
13720        }
13721
13722        let temp_root =
13723            std::env::temp_dir().join(format!("oxml-chart-f123-label-gate-{}", std::process::id()));
13724        let profile = std::env::temp_dir().join(format!(
13725            "oxml-chart-f123-label-profile-{}",
13726            std::process::id()
13727        ));
13728        if temp_root.exists() {
13729            fs::remove_dir_all(&temp_root).expect("remove stale F-123 evidence");
13730        }
13731        if profile.exists() {
13732            fs::remove_dir_all(&profile).expect("remove stale F-123 profile");
13733        }
13734        fs::create_dir_all(&temp_root).expect("create F-123 evidence root");
13735        let unbound_candidate = temp_root.join("f123-percentage-label.pptx");
13736        package
13737            .save(&unbound_candidate)
13738            .expect("save percentage-label candidate");
13739        let candidate_sha = sha256(&unbound_candidate);
13740        let evidence_dir = temp_root.join(&candidate_sha);
13741        fs::create_dir(&evidence_dir).expect("create SHA-bound evidence directory");
13742        let candidate = evidence_dir.join("f123-percentage-label.pptx");
13743        fs::rename(&unbound_candidate, &candidate).expect("bind candidate to evidence SHA");
13744        assert_eq!(sha256(&candidate), candidate_sha);
13745
13746        let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
13747        let conversion = Command::new("soffice")
13748            .args([
13749                "--headless",
13750                &profile_argument,
13751                "--convert-to",
13752                "pdf:impress_pdf_Export",
13753                "--outdir",
13754            ])
13755            .arg(&evidence_dir)
13756            .arg(&candidate)
13757            .output()
13758            .expect("run pinned LibreOffice percentage-label gate");
13759        assert!(
13760            conversion.status.success(),
13761            "LibreOffice conversion failed: {}",
13762            String::from_utf8_lossy(&conversion.stderr)
13763        );
13764        let pdf = evidence_dir.join("f123-percentage-label.pdf");
13765        assert!(
13766            pdf.is_file(),
13767            "LibreOffice did not create {}",
13768            pdf.display()
13769        );
13770        let text_path = evidence_dir.join("f123-percentage-label.txt");
13771        let extraction = Command::new("pdftotext")
13772            .arg(&pdf)
13773            .arg(&text_path)
13774            .output()
13775            .expect("run pinned Poppler percentage-label extraction");
13776        assert!(
13777            extraction.status.success(),
13778            "pdftotext failed: {}",
13779            String::from_utf8_lossy(&extraction.stderr)
13780        );
13781        let extracted = fs::read_to_string(&text_path).expect("read percentage-label text");
13782        assert!(
13783            extracted.contains("25%"),
13784            "SHA-bound viewer text lacks 25%: {extracted:?}"
13785        );
13786        for artifact in [&candidate, &pdf, &text_path] {
13787            assert_eq!(
13788                artifact.parent(),
13789                Some(evidence_dir.as_path()),
13790                "evidence escaped candidate SHA"
13791            );
13792        }
13793        eprintln!("F-123 candidate/render/text evidence SHA-256 {candidate_sha}, extracted 25%");
13794        fs::remove_dir_all(&temp_root).expect("remove F-123 evidence");
13795        if profile.exists() {
13796            fs::remove_dir_all(&profile).expect("remove F-123 profile");
13797        }
13798    }
13799
13800    #[test]
13801    fn common_number_formats_project_cached_values_deterministically() {
13802        for (code, value, expected) in [
13803            ("General", 0.25, "0.25"),
13804            ("0", 12.6, "13"),
13805            ("0.0", 12.64, "12.6"),
13806            ("0.00", -12.645, "-12.64"),
13807            ("0%", 0.25, "25%"),
13808            ("0.0%", 0.256, "25.6%"),
13809            ("0.00%", 0.256, "25.60%"),
13810        ] {
13811            let format = NumberFormat::new(code.to_owned(), false).unwrap();
13812            assert_eq!(format.format_value(value).unwrap(), expected, "{code}");
13813        }
13814    }
13815
13816    #[test]
13817    fn malformed_data_labels_and_number_formats_return_errors_without_panicking() {
13818        let cases = [
13819            format!(
13820                r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="1"/><c:showVal val="0"/></c:dLbls>"#
13821            ),
13822            format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="yes"/></c:dLbls>"#),
13823            format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:dLblPos val="middle"/></c:dLbls>"#),
13824            format!(
13825                r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="" sourceLinked="1"/></c:dLbls>"#
13826            ),
13827            format!(
13828                r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="0" sourceLinked="maybe"/></c:dLbls>"#
13829            ),
13830        ];
13831        for xml in cases {
13832            let result = std::panic::catch_unwind(|| CT_DLbls::from_xml(xml.as_bytes()));
13833            assert!(result.is_ok(), "data-label parser panicked for {xml}");
13834            assert!(result.unwrap().is_err(), "malformed labels parsed: {xml}");
13835        }
13836
13837        let invalid_code = NumberFormat::new("bad\u{0}code".to_owned(), false);
13838        assert!(invalid_code.is_err());
13839        let unsupported = NumberFormat::new("#,##0".to_owned(), false).unwrap();
13840        assert!(unsupported.format_value(12.0).is_err());
13841        let general = NumberFormat::new("General".to_owned(), false).unwrap();
13842        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
13843            assert!(general.format_value(value).is_err());
13844        }
13845
13846        let invalid_separator = CT_DLbls {
13847            separator: Some("bad\u{0}separator".to_owned()),
13848            ..CT_DLbls::default()
13849        };
13850        assert!(invalid_separator.to_xml().is_err());
13851    }
13852
13853    #[test]
13854    fn data_labels_preserve_point_overrides_and_extensions_byte_for_byte() {
13855        let xml = format!(
13856            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>"#
13857        );
13858        let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
13859        let written = parsed.to_xml().unwrap();
13860        for raw in [
13861            br#"<!--before-->"#.as_slice(),
13862            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(),
13863            br#"<?after-point?>"#.as_slice(),
13864            br#"<q:spPr><x:collection-shape/></q:spPr>"#.as_slice(),
13865            br#"<q:txPr><x:collection-text/></q:txPr>"#.as_slice(),
13866            br#"<q:showLeaderLines val="1"/>"#.as_slice(),
13867            br#"<q:leaderLines><q:spPr><x:leader-shape/></q:spPr></q:leaderLines>"#.as_slice(),
13868            br#"<q:extLst><q:ext uri="collection"><x:tail/></q:ext></q:extLst>"#.as_slice(),
13869            br#"<!--after-->"#.as_slice(),
13870        ] {
13871            assert!(
13872                written.windows(raw.len()).any(|window| window == raw),
13873                "preserved data-label bytes changed: {}",
13874                String::from_utf8_lossy(raw)
13875            );
13876        }
13877        assert_eq!(parsed, CT_DLbls::from_xml(&written).unwrap());
13878    }
13879
13880    #[test]
13881    fn every_corpus_data_label_collection_round_trips_structurally() {
13882        let Some(corpus) = require_or_skip_corpus() else {
13883            return;
13884        };
13885        verify_fetched_corpus(&corpus);
13886        let mut label_collections = 0usize;
13887        let mut axis_number_formats = 0usize;
13888        for path in manifest_paths() {
13889            let package = OpcPackage::open(corpus.join(path))
13890                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
13891            for (part, xml) in &package.parts {
13892                if !is_chart_part(part) {
13893                    continue;
13894                }
13895                let chart = CT_ChartSpace::from_xml(xml)
13896                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
13897                for series in chart.chart.plot_area.series().unwrap_or_else(|error| {
13898                    panic!("{path} {part}: series projection failed: {error}")
13899                }) {
13900                    if let Some(labels) = series.data_labels {
13901                        let written = labels.to_xml().unwrap_or_else(|error| {
13902                            panic!("{path} {part}: label write failed: {error}")
13903                        });
13904                        let reparsed = CT_DLbls::from_xml(&written).unwrap_or_else(|error| {
13905                            panic!("{path} {part}: written labels parse failed: {error}")
13906                        });
13907                        assert_eq!(labels, reparsed, "{path} {part}: labels changed");
13908                        label_collections += 1;
13909                    }
13910                }
13911                for axis in chart.chart.plot_area.axes().unwrap_or_else(|error| {
13912                    panic!("{path} {part}: axis projection failed: {error}")
13913                }) {
13914                    if axis.number_format.is_some() {
13915                        let written = axis.to_xml().unwrap_or_else(|error| {
13916                            panic!("{path} {part}: axis write failed: {error}")
13917                        });
13918                        let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
13919                            panic!("{path} {part}: written axis parse failed: {error}")
13920                        });
13921                        assert_eq!(axis, reparsed, "{path} {part}: axis changed");
13922                        axis_number_formats += 1;
13923                    }
13924                }
13925            }
13926        }
13927        assert!(label_collections > 0, "the pinned corpus has no c:dLbls");
13928        assert!(
13929            axis_number_formats > 0,
13930            "the pinned corpus has no axis c:numFmt"
13931        );
13932        eprintln!(
13933            "ChartML data-label corpus gate checked {label_collections} collections and {axis_number_formats} axis number formats"
13934        );
13935    }
13936
13937    fn full_axis(local: &str, position: &str) -> String {
13938        format!(
13939            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}>"#
13940        )
13941    }
13942
13943    #[test]
13944    fn axis_id_pairs_are_reciprocal() {
13945        assert_eq!(
13946            AxisId::new(i64::from(i32::MIN)).unwrap().value(),
13947            i64::from(i32::MIN)
13948        );
13949        assert_eq!(
13950            AxisId::new(i64::from(u32::MAX)).unwrap().value(),
13951            i64::from(u32::MAX)
13952        );
13953        assert!(AxisId::new(i64::from(i32::MIN) - 1).is_err());
13954        assert!(AxisId::new(i64::from(u32::MAX) + 1).is_err());
13955
13956        let valid = chart_with_axes(&[
13957            minimal_axis("catAx", "-1884094432", "b", "-1884097184"),
13958            minimal_axis("valAx", "-1884097184", "l", "-1884094432"),
13959        ]);
13960        let axes = CT_ChartSpace::from_xml(valid.as_bytes())
13961            .unwrap()
13962            .chart
13963            .plot_area
13964            .axes()
13965            .unwrap();
13966        assert_eq!(axes.len(), 2);
13967        assert_eq!(axes[0].id.value(), -1_884_094_432);
13968        assert_eq!(axes[0].cross_axis, axes[1].id);
13969        assert_eq!(axes[1].cross_axis, axes[0].id);
13970
13971        let invalid_sets = [
13972            vec![
13973                minimal_axis("catAx", "1", "b", "2"),
13974                minimal_axis("valAx", "1", "l", "2"),
13975            ],
13976            vec![minimal_axis("catAx", "1", "b", "1")],
13977            vec![minimal_axis("catAx", "1", "b", "2")],
13978            vec![
13979                minimal_axis("catAx", "1", "b", "2"),
13980                minimal_axis("valAx", "2", "l", "3"),
13981                minimal_axis("serAx", "3", "r", "2"),
13982            ],
13983        ];
13984        for axes in invalid_sets {
13985            let chart = CT_ChartSpace::from_xml(chart_with_axes(&axes).as_bytes()).unwrap();
13986            assert!(chart.chart.plot_area.axes().is_err());
13987        }
13988
13989        let empty = CT_ChartSpace::from_xml(chart_with_axes(&[]).as_bytes()).unwrap();
13990        assert!(empty.chart.plot_area.axes().unwrap().is_empty());
13991    }
13992
13993    #[test]
13994    fn all_axis_forms_write_fixed_prefixes_in_schema_order() {
13995        for (local, kind, position) in [
13996            ("catAx", AxisKind::Category, AxisPosition::Bottom),
13997            ("valAx", AxisKind::Value, AxisPosition::Left),
13998            ("dateAx", AxisKind::Date, AxisPosition::Top),
13999            ("serAx", AxisKind::Series, AxisPosition::Right),
14000        ] {
14001            let xml = full_axis(local, position.as_str());
14002            let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
14003            assert_eq!(parsed.kind, kind);
14004            assert_eq!(parsed.id.value(), -1_125_255_920);
14005            assert_eq!(parsed.scaling.log_base, Some(10.0));
14006            assert_eq!(parsed.scaling.orientation, Orientation::MaxMin);
14007            assert_eq!(parsed.scaling.maximum, Some(100.0));
14008            assert_eq!(parsed.scaling.minimum, Some(-10.0));
14009            assert!(parsed.deleted);
14010            assert_eq!(parsed.position, position);
14011            assert!(parsed.major_gridlines.is_some());
14012            assert!(parsed.minor_gridlines.is_some());
14013            assert!(parsed.title.is_some());
14014            assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0");
14015            assert_eq!(parsed.major_tick_mark, TickMark::Cross);
14016            assert_eq!(parsed.minor_tick_mark, TickMark::Inside);
14017            assert_eq!(parsed.tick_label_position, TickLabelPosition::High);
14018            assert!(parsed.sp_pr.is_some());
14019            assert!(parsed.tx_pr.is_some());
14020            assert_eq!(parsed.cross_axis.value(), -1_004_303_696);
14021
14022            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14023            assert!(written.starts_with(&format!(
14024                "<c:{local} xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
14025            )));
14026            assert!(written.contains("<c:axId val=\"-1125255920\""));
14027            let mut cursor = 0usize;
14028            for tag in [
14029                "<c:axId",
14030                "<c:scaling",
14031                "<c:delete",
14032                "<c:axPos",
14033                "<c:majorGridlines",
14034                "<c:minorGridlines",
14035                "<c:title",
14036                "<c:numFmt",
14037                "<c:majorTickMark",
14038                "<c:minorTickMark",
14039                "<c:tickLblPos",
14040                "<c:spPr",
14041                "<c:txPr",
14042                "<c:crossAx",
14043            ] {
14044                let position = written[cursor..]
14045                    .find(tag)
14046                    .unwrap_or_else(|| panic!("missing ordered axis tag {tag}"));
14047                cursor += position + tag.len();
14048            }
14049            assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
14050        }
14051
14052        let mut constructed = Axis::new(
14053            AxisKind::Value,
14054            AxisId::new(1).unwrap(),
14055            AxisPosition::Left,
14056            AxisId::new(2).unwrap(),
14057        );
14058        assert_eq!(
14059            constructed,
14060            Axis::from_xml(&constructed.to_xml().unwrap()).unwrap()
14061        );
14062        constructed.kind = AxisKind::Category;
14063        constructed.id = AxisId::new(3).unwrap();
14064        constructed.cross_axis = AxisId::new(4).unwrap();
14065        constructed.scaling.minimum = Some(0.0);
14066        constructed.scaling.maximum = Some(10.0);
14067        let written = constructed.to_xml().unwrap();
14068        assert!(written.starts_with(b"<c:catAx"));
14069        assert_eq!(constructed, Axis::from_xml(&written).unwrap());
14070    }
14071
14072    #[test]
14073    fn axis_equality_normalizes_ids_without_losing_lexical_preservation() {
14074        let padded = Axis::from_xml(
14075            standalone_axis(
14076                "catAx",
14077                "<c:axId val=\"01\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"002\"/>",
14078            )
14079            .as_bytes(),
14080        )
14081        .unwrap();
14082        let normalized = Axis::from_xml(
14083            standalone_axis(
14084                "catAx",
14085                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14086            )
14087            .as_bytes(),
14088        )
14089        .unwrap();
14090        assert_eq!(padded, normalized);
14091
14092        let preserved = String::from_utf8(padded.to_xml().unwrap()).unwrap();
14093        assert!(preserved.contains("<c:axId val=\"01\""));
14094        assert!(preserved.contains("<c:crossAx val=\"002\""));
14095
14096        let mut mutated = padded;
14097        mutated.id = AxisId::new(3).unwrap();
14098        mutated.cross_axis = AxisId::new(4).unwrap();
14099        let rewritten = String::from_utf8(mutated.to_xml().unwrap()).unwrap();
14100        assert!(rewritten.contains("<c:axId val=\"3\""));
14101        assert!(rewritten.contains("<c:crossAx val=\"4\""));
14102        assert!(!rewritten.contains("val=\"01\""));
14103        assert!(!rewritten.contains("val=\"002\""));
14104    }
14105
14106    #[test]
14107    fn malformed_axis_values_return_errors_without_panicking() {
14108        let cases = [
14109            standalone_axis(
14110                "catAx",
14111                "<c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14112            ),
14113            standalone_axis(
14114                "catAx",
14115                "<c:axId val=\"1\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14116            ),
14117            standalone_axis(
14118                "catAx",
14119                "<c:axId val=\"1\"/><c:scaling/><c:crossAx val=\"2\"/>",
14120            ),
14121            standalone_axis(
14122                "catAx",
14123                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/>",
14124            ),
14125            standalone_axis(
14126                "catAx",
14127                "<c:axId val=\"-2147483649\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14128            ),
14129            standalone_axis(
14130                "catAx",
14131                "<c:axId val=\"4294967296\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14132            ),
14133            standalone_axis(
14134                "catAx",
14135                "<c:axId val=\"1\"/><c:scaling><c:logBase val=\"1\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14136            ),
14137            standalone_axis(
14138                "catAx",
14139                "<c:axId val=\"1\"/><c:scaling><c:max val=\"NaN\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14140            ),
14141            standalone_axis(
14142                "catAx",
14143                "<c:axId val=\"1\"/><c:scaling><c:max val=\"1\"/><c:min val=\"2\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14144            ),
14145            standalone_axis(
14146                "catAx",
14147                "<c:axId val=\"1\"/><c:scaling><c:orientation val=\"sideways\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14148            ),
14149            standalone_axis(
14150                "catAx",
14151                "<c:axId val=\"1\"/><c:scaling/><c:delete val=\"yes\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14152            ),
14153            standalone_axis(
14154                "catAx",
14155                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"middle\"/><c:crossAx val=\"2\"/>",
14156            ),
14157            standalone_axis(
14158                "catAx",
14159                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:majorTickMark val=\"near\"/><c:crossAx val=\"2\"/>",
14160            ),
14161            standalone_axis(
14162                "catAx",
14163                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:tickLblPos val=\"middle\"/><c:crossAx val=\"2\"/>",
14164            ),
14165            standalone_axis(
14166                "catAx",
14167                "<c:axId val=\"1\"/><c:axId val=\"2\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14168            ),
14169            standalone_axis(
14170                "catAx",
14171                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt sourceLinked=\"1\"/><c:crossAx val=\"2\"/>",
14172            ),
14173            standalone_axis(
14174                "catAx",
14175                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt formatCode=\"0\" sourceLinked=\"maybe\"/><c:crossAx val=\"2\"/>",
14176            ),
14177        ];
14178        for xml in cases {
14179            let result = std::panic::catch_unwind(|| Axis::from_xml(xml.as_bytes()));
14180            assert!(result.is_ok(), "axis parser panicked for {xml}");
14181            assert!(result.unwrap().is_err(), "malformed axis parsed: {xml}");
14182        }
14183
14184        let mut axis = Axis::new(
14185            AxisKind::Value,
14186            AxisId::new(1).unwrap(),
14187            AxisPosition::Left,
14188            AxisId::new(2).unwrap(),
14189        );
14190        axis.scaling.maximum = Some(f64::INFINITY);
14191        assert!(axis.to_xml().is_err());
14192    }
14193
14194    #[test]
14195    fn axes_preserve_unmodelled_children_byte_for_byte() {
14196        let xml = format!(
14197            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>"#
14198        );
14199        let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
14200        let mut relabelled = parsed.clone();
14201        relabelled.kind = AxisKind::Value;
14202        assert!(
14203            relabelled.to_xml().is_err(),
14204            "a parsed category axis must not retain its tail under a value-axis root"
14205        );
14206        let written = parsed.to_xml().unwrap();
14207        for raw in [
14208            br#"<!--before-->"#.as_slice(),
14209            br#"<?after-id?>"#.as_slice(),
14210            br#"<!--scale-->"#.as_slice(),
14211            br#"<q:extLst><q:ext uri="scale"><x:data/></q:ext></q:extLst>"#.as_slice(),
14212            br#"<x:grid/>"#.as_slice(),
14213            br#"<x:title/>"#.as_slice(),
14214            br#"<q:crosses val="autoZero"/>"#.as_slice(),
14215            br#"<q:auto val="1"><x:auto/></q:auto>"#.as_slice(),
14216            br#"<q:extLst><q:ext uri="axis"><x:tail/></q:ext></q:extLst>"#.as_slice(),
14217            br#"<!--after-->"#.as_slice(),
14218        ] {
14219            assert!(
14220                written.windows(raw.len()).any(|window| window == raw),
14221                "preserved axis bytes changed: {}",
14222                String::from_utf8_lossy(raw)
14223            );
14224        }
14225        let written = String::from_utf8(written).unwrap();
14226        assert!(written.starts_with("<c:catAx"));
14227        assert!(written.contains("x:keep=\"axis\""));
14228        assert!(written.contains("x:keep=\"id\""));
14229        assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
14230    }
14231
14232    #[test]
14233    fn every_corpus_axis_round_trips_structurally() {
14234        let Some(corpus) = require_or_skip_corpus() else {
14235            return;
14236        };
14237        verify_fetched_corpus(&corpus);
14238        let mut axis_count = 0usize;
14239        let mut chart_parts = 0usize;
14240        let mut kinds = HashSet::new();
14241        for path in manifest_paths() {
14242            let package = OpcPackage::open(corpus.join(path))
14243                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14244            for (part, xml) in &package.parts {
14245                if !is_chart_part(part) {
14246                    continue;
14247                }
14248                let chart = CT_ChartSpace::from_xml(xml)
14249                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14250                let axes = chart.chart.plot_area.axes().unwrap_or_else(|error| {
14251                    panic!("{path} {part}: axis projection failed: {error}")
14252                });
14253                for axis in axes {
14254                    kinds.insert(axis.kind);
14255                    let written = axis.to_xml().unwrap_or_else(|error| {
14256                        panic!("{path} {part}: axis write failed: {error}")
14257                    });
14258                    let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
14259                        panic!("{path} {part}: written axis parse failed: {error}")
14260                    });
14261                    assert_eq!(axis, reparsed, "{path} {part}: axis model changed");
14262                    axis_count += 1;
14263                }
14264                chart_parts += 1;
14265            }
14266        }
14267        assert!(
14268            axis_count > 0,
14269            "the pinned corpus contained no ChartML axes"
14270        );
14271        assert!(kinds.contains(&AxisKind::Category));
14272        assert!(kinds.contains(&AxisKind::Value));
14273
14274        for kind in ["dateAx", "serAx"] {
14275            let parsed = Axis::from_xml(
14276                standalone_axis(
14277                    kind,
14278                    "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14279                )
14280                .as_bytes(),
14281            )
14282            .unwrap();
14283            assert_eq!(parsed, Axis::from_xml(&parsed.to_xml().unwrap()).unwrap());
14284        }
14285        eprintln!(
14286            "ChartML axis corpus gate checked {axis_count} axes across {chart_parts} chart parts"
14287        );
14288    }
14289
14290    #[test]
14291    fn series_formula_and_cache_are_consistent_with_one_source() {
14292        let values = NumericData::new(
14293            "'Sales 24'!$B$2:$B$4".to_owned(),
14294            "0.0".to_owned(),
14295            vec![4.25, 8.5, 17.0],
14296        )
14297        .unwrap();
14298        let mut series = Series::new(3, 1, values);
14299        series.categories = Some(AxisData::String(
14300            StringRef::new(
14301                "'Sales 24'!$A$2:$A$4".to_owned(),
14302                vec!["North".to_owned(), "South".to_owned(), "West".to_owned()],
14303            )
14304            .unwrap(),
14305        ));
14306
14307        let written = String::from_utf8(series.to_xml().unwrap()).unwrap();
14308        assert!(written.contains("<c:f>&apos;Sales 24&apos;!$B$2:$B$4</c:f>"));
14309        assert!(written.contains("<c:formatCode>0.0</c:formatCode>"));
14310        assert_eq!(written.matches("<c:ptCount val=\"3\"").count(), 2);
14311        for (index, value) in ["4.25", "8.5", "17"].iter().enumerate() {
14312            assert!(written.contains(&format!("<c:pt idx=\"{index}\"><c:v>{value}</c:v></c:pt>")));
14313        }
14314        let reparsed = Series::from_xml(written.as_bytes()).unwrap();
14315        assert_eq!(reparsed.index, series.index);
14316        assert_eq!(reparsed.order, series.order);
14317        match (&reparsed.categories, &series.categories) {
14318            (Some(AxisData::String(left)), Some(AxisData::String(right))) => {
14319                assert_eq!(left.formula, right.formula);
14320                assert_eq!(left.values, right.values);
14321            }
14322            _ => panic!("expected string categories"),
14323        }
14324        assert_eq!(reparsed.values.formula, series.values.formula);
14325        assert_eq!(reparsed.values.format_code, series.values.format_code);
14326        assert_eq!(reparsed.values.values, series.values.values);
14327    }
14328
14329    #[test]
14330    fn string_and_numeric_references_write_fixed_prefixes_in_schema_order() {
14331        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>"#;
14332        let string_ref = StringRef::from_xml(string_xml).unwrap();
14333        let written = String::from_utf8(string_ref.to_xml().unwrap()).unwrap();
14334        assert!(written.starts_with("<c:strRef xmlns:c="));
14335        assert!(written.find("<c:f>").unwrap() < written.find("<c:strCache>").unwrap());
14336        assert!(written.find("<c:ptCount").unwrap() < written.find("<c:pt idx=").unwrap());
14337
14338        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>"#;
14339        let numeric = NumericData::from_xml(numeric_xml).unwrap();
14340        let written = String::from_utf8(numeric.to_xml().unwrap()).unwrap();
14341        let positions: Vec<_> = [
14342            "<c:f>",
14343            "<c:numCache>",
14344            "<c:formatCode>",
14345            "<c:ptCount",
14346            "<c:pt idx=",
14347        ]
14348        .iter()
14349        .map(|tag| written.find(tag).unwrap())
14350        .collect();
14351        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14352        assert_eq!(numeric, NumericData::from_xml(written.as_bytes()).unwrap());
14353    }
14354
14355    #[test]
14356    fn mixed_chartml_aliases_resolve_by_namespace_uri() {
14357        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>"#;
14358        let parsed = Series::from_xml(xml).unwrap();
14359        assert_eq!(parsed.index, 4);
14360        assert_eq!(parsed.order, 2);
14361        assert_eq!(parsed.values.values, vec![1.5, 2.5]);
14362        assert!(matches!(parsed.categories, Some(AxisData::String(_))));
14363        assert_eq!(parsed, Series::from_xml(&parsed.to_xml().unwrap()).unwrap());
14364    }
14365
14366    #[test]
14367    fn plot_area_series_ignores_inherited_foreign_plot_aliases() {
14368        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>"#;
14369        let chart = CT_ChartSpace::from_xml(xml).unwrap();
14370        let series = chart.chart.plot_area.series().unwrap();
14371        assert_eq!(series.len(), 1);
14372        assert_eq!(series[0].index, 1);
14373        let written = String::from_utf8(series[0].to_xml().unwrap()).unwrap();
14374        assert!(
14375            written.contains(r#"xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart""#)
14376        );
14377        assert!(written.contains(r#"xmlns:x="urn:producer""#));
14378        assert!(written.contains("<q:marker><x:data/></q:marker>"));
14379        assert_eq!(series[0], Series::from_xml(written.as_bytes()).unwrap());
14380
14381        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>"#;
14382        let chart = CT_ChartSpace::from_xml(conflicting).unwrap();
14383        assert!(chart.chart.plot_area.series().is_err());
14384    }
14385
14386    #[test]
14387    fn malformed_series_and_cache_values_return_errors_without_panicking() {
14388        let cases: &[&[u8]] = &[
14389            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>"#,
14390            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/></c:ser>"#,
14391            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>"#,
14392            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>"#,
14393            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>"#,
14394            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>"#,
14395            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>"#,
14396            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>"#,
14397            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>"#,
14398            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>"#,
14399            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>"#,
14400            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>"#,
14401            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>"#,
14402            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>"#,
14403            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>"#,
14404            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>"#,
14405            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>"#,
14406        ];
14407        for xml in cases {
14408            let result = std::panic::catch_unwind(|| Series::from_xml(xml));
14409            assert!(result.is_ok(), "series parser panicked");
14410            assert!(
14411                result.unwrap().is_err(),
14412                "malformed series parsed: {}",
14413                String::from_utf8_lossy(xml)
14414            );
14415        }
14416        assert!(
14417            NumericData::new(
14418                "S!$A$1".to_owned(),
14419                "General".to_owned(),
14420                vec![f64::INFINITY]
14421            )
14422            .is_err()
14423        );
14424    }
14425
14426    #[test]
14427    fn chart_text_rejects_xml_forbidden_characters_and_escapes_metacharacters() {
14428        assert!(StringRef::new("S!$A$1\u{1}".to_owned(), Vec::new()).is_err());
14429        assert!(StringRef::new("S!$A$1".to_owned(), vec!["invalid\u{1}".to_owned()]).is_err());
14430        assert!(
14431            NumericData::new("S!$A$1\u{1}".to_owned(), "General".to_owned(), Vec::new()).is_err()
14432        );
14433        assert!(
14434            NumericData::new("S!$A$1".to_owned(), "General\u{1}".to_owned(), Vec::new()).is_err()
14435        );
14436
14437        let mut strings = StringRef::new("S!$A$1".to_owned(), vec!["valid".to_owned()]).unwrap();
14438        strings.formula = "invalid\u{1}".to_owned();
14439        assert!(strings.to_xml().is_err());
14440        strings.formula = "S!$A$1".to_owned();
14441        strings.values[0] = "invalid\u{1}".to_owned();
14442        assert!(strings.to_xml().is_err());
14443
14444        let mut numbers =
14445            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap();
14446        numbers.formula = "invalid\u{1}".to_owned();
14447        assert!(numbers.to_xml().is_err());
14448        numbers.formula = "S!$A$1".to_owned();
14449        numbers.format_code = "invalid\u{1}".to_owned();
14450        assert!(numbers.to_xml().is_err());
14451
14452        let strings =
14453            StringRef::new("S!$A$1&".to_owned(), vec!["North < West & East".to_owned()]).unwrap();
14454        let written = String::from_utf8(strings.to_xml().unwrap()).unwrap();
14455        assert!(written.contains("S!$A$1&amp;"));
14456        assert!(written.contains("North &lt; West &amp; East"));
14457    }
14458
14459    #[test]
14460    fn series_preserves_unmodelled_children_byte_for_byte() {
14461        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>"#;
14462        let parsed = Series::from_xml(xml).unwrap();
14463        let written = parsed.to_xml().unwrap();
14464        for raw in [
14465            br#"<!--before-->"#.as_slice(),
14466            br#"<x:between/>"#.as_slice(),
14467            br#"<q:dPt x:id="one"><q:idx val="0"/></q:dPt>"#.as_slice(),
14468            br#"<q:trendline x:id="trend"/>"#.as_slice(),
14469            br#"<q:extLst><q:ext uri="keep"><x:data/></q:ext></q:extLst>"#.as_slice(),
14470            br#"<!--point-->"#.as_slice(),
14471            br#"<x:cacheExt/>"#.as_slice(),
14472        ] {
14473            assert!(
14474                written.windows(raw.len()).any(|window| window == raw),
14475                "preserved series bytes changed: {}",
14476                String::from_utf8_lossy(raw)
14477            );
14478        }
14479        let written = String::from_utf8(written.clone()).unwrap();
14480        assert!(written.contains(r#"x:keep="series""#));
14481        assert!(written.contains(r#"x:keep="formula""#));
14482        assert!(written.contains("<c:dLbls"));
14483        assert!(written.contains(r#"x:id="labels""#));
14484        assert!(parsed.data_labels.is_some());
14485        assert_eq!(parsed, Series::from_xml(written.as_bytes()).unwrap());
14486    }
14487
14488    #[test]
14489    fn public_series_edits_do_not_duplicate_or_drop_preserved_payloads() {
14490        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>"#;
14491        let parsed = Series::from_xml(opaque_xml).unwrap();
14492
14493        let mut edited = parsed.clone();
14494        edited.name = Some(StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap());
14495        assert!(edited.to_xml().is_err());
14496
14497        let mut edited = parsed.clone();
14498        edited.categories = Some(AxisData::Numeric(
14499            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap(),
14500        ));
14501        assert!(edited.to_xml().is_err());
14502
14503        let mut edited = parsed;
14504        edited.bubble_size =
14505            Some(NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
14506        assert!(edited.to_xml().is_err());
14507
14508        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>"#;
14509        let parsed = Series::from_xml(cache_xml).unwrap();
14510        let tail = "<c:extLst><c:ext uri=\"keep\"><x:data/></c:ext></c:extLst>";
14511
14512        let mut shortened = parsed.clone();
14513        shortened.values.values.truncate(1);
14514        let written = String::from_utf8(shortened.to_xml().unwrap()).unwrap();
14515        assert!(!written.contains("<c:pt idx=\"1\""));
14516        assert!(written.find("<c:pt idx=\"0\"").unwrap() < written.find(tail).unwrap());
14517        assert!(Series::from_xml(written.as_bytes()).is_ok());
14518
14519        let mut grown = parsed;
14520        grown.values.values.push(3.0);
14521        let written = String::from_utf8(grown.to_xml().unwrap()).unwrap();
14522        assert!(written.find("<c:pt idx=\"2\"").unwrap() < written.find(tail).unwrap());
14523        assert!(Series::from_xml(written.as_bytes()).is_ok());
14524
14525        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>"#;
14526        let mut edited = Series::from_xml(ordered_xml).unwrap();
14527        edited.name = Some(StringRef::new("S!$B$1".to_owned(), Vec::new()).unwrap());
14528        edited.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
14529        edited.categories = Some(AxisData::String(
14530            StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap(),
14531        ));
14532        edited.bubble_size =
14533            Some(NumericData::new("S!$C$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
14534        let written = String::from_utf8(edited.to_xml().unwrap()).unwrap();
14535        let positions: Vec<_> = [
14536            "<c:tx>",
14537            "<c:spPr",
14538            "<c:marker>",
14539            "<c:cat>",
14540            "<c:val>",
14541            "<c:bubbleSize>",
14542            "<c:extLst>",
14543        ]
14544        .iter()
14545        .map(|tag| written.find(tag).unwrap())
14546        .collect();
14547        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14548        assert!(Series::from_xml(written.as_bytes()).is_ok());
14549    }
14550
14551    #[test]
14552    fn every_corpus_series_round_trips_structurally() {
14553        let Some(corpus) = require_or_skip_corpus() else {
14554            return;
14555        };
14556        verify_fetched_corpus(&corpus);
14557        let mut series_count = 0usize;
14558        let mut chart_parts = 0usize;
14559        for path in manifest_paths() {
14560            let package = OpcPackage::open(corpus.join(path))
14561                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14562            for (part, xml) in &package.parts {
14563                if !is_chart_part(part) {
14564                    continue;
14565                }
14566                let chart = CT_ChartSpace::from_xml(xml)
14567                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14568                let parsed_series =
14569                    chart.chart.plot_area.series().unwrap_or_else(|error| {
14570                        panic!("{path} {part}: series parse failed: {error}")
14571                    });
14572                for series in parsed_series {
14573                    let written = series.to_xml().unwrap_or_else(|error| {
14574                        panic!("{path} {part}: series write failed: {error}")
14575                    });
14576                    let reparsed = Series::from_xml(&written).unwrap_or_else(|error| {
14577                        panic!("{path} {part}: written series parse failed: {error}")
14578                    });
14579                    assert_eq!(series, reparsed, "{path} {part}: series model changed");
14580                    series_count += 1;
14581                }
14582                chart_parts += 1;
14583            }
14584        }
14585        assert!(
14586            series_count > 0,
14587            "the pinned corpus contained no supported series"
14588        );
14589        eprintln!(
14590            "ChartML series corpus gate checked {series_count} series across {chart_parts} chart parts"
14591        );
14592    }
14593
14594    #[test]
14595    fn chart_space_reads_aliases_and_writes_fixed_prefixes_in_schema_order() {
14596        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>"#;
14597        let parsed = CT_ChartSpace::from_xml(xml).unwrap();
14598        assert!(!parsed.chart.auto_title_deleted);
14599        assert!(!parsed.chart.plot_vis_only);
14600        assert_eq!(parsed.chart.disp_blanks_as, DispBlanksAs::Span);
14601        assert!(parsed.chart.title.is_some());
14602        assert!(parsed.chart.legend.is_some());
14603        assert!(parsed.sp_pr.is_some());
14604        assert!(parsed.tx_pr.is_some());
14605
14606        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14607        assert!(written.starts_with(
14608            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""#
14609        ));
14610        for tag in [
14611            "<c:chart>",
14612            "<c:title>",
14613            "<c:plotArea>",
14614            "<c:legend>",
14615            "<c:plotVisOnly",
14616            "<c:dispBlanksAs",
14617            "<c:spPr>",
14618            "<c:txPr>",
14619        ] {
14620            assert!(written.contains(tag), "missing fixed-prefix tag {tag}");
14621        }
14622        let positions: Vec<_> = [
14623            "<c:title>",
14624            "<c:plotArea>",
14625            "<c:legend>",
14626            "<c:plotVisOnly",
14627            "<c:dispBlanksAs",
14628            "<c:spPr>",
14629            "<c:txPr>",
14630        ]
14631        .iter()
14632        .map(|tag| written.find(tag).unwrap())
14633        .collect();
14634        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14635        assert!(written.contains("<a:solidFill><a:srgbClr"));
14636        assert!(written.contains("<a:bodyPr/><a:p/>"));
14637        assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
14638    }
14639
14640    #[test]
14641    fn public_core_edits_keep_preserved_children_in_their_schema_slots() {
14642        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>"#;
14643        let mut parsed = CT_ChartSpace::from_xml(xml).unwrap();
14644        parsed.chart.auto_title_deleted = true;
14645        parsed.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
14646        parsed.tx_pr =
14647            Some(CT_TextBody::from_xml(br#"<a:txBody><a:bodyPr/><a:p/></a:txBody>"#).unwrap());
14648        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14649        let positions: Vec<_> = [
14650            "<c:autoTitleDeleted",
14651            "<c:pivotFmts>",
14652            "<c:plotArea",
14653            "<c:spPr",
14654            "<c:txPr",
14655            "<c:externalData",
14656        ]
14657        .iter()
14658        .map(|tag| written.find(tag).unwrap())
14659        .collect();
14660        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14661        assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
14662    }
14663
14664    #[test]
14665    fn malformed_core_chart_values_return_errors_without_panicking() {
14666        let cases: &[&[u8]] = &[
14667            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"/>"#,
14668            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart/></c:chartSpace>"#,
14669            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:plotArea/></c:chart></c:chartSpace>"#,
14670            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:autoTitleDeleted val="yes"/><c:plotArea/></c:chart></c:chartSpace>"#,
14671            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:dispBlanksAs val="empty"/></c:chart></c:chartSpace>"#,
14672            br#"<c:chartSpace xmlns:c="urn:wrong"><c:chart><c:plotArea/></c:chart></c:chartSpace>"#,
14673            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:foreign"><x:chart><x:plotArea/></x:chart></c:chartSpace>"#,
14674            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>"#,
14675            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:title/><c:title/><c:plotArea/></c:chart></c:chartSpace>"#,
14676            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>"#,
14677            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:foreign"><c:chart><c:plotArea/></c:chart><c:txPr><a:bodyPr/><a:p><x:r><x:t>foreign</x:t></x:r></a:p></c:txPr></c:chartSpace>"#,
14678            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>"#,
14679        ];
14680        for xml in cases {
14681            let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml));
14682            assert!(result.is_ok(), "ChartML parser panicked");
14683            assert!(
14684                result.unwrap().is_err(),
14685                "malformed ChartML parsed: {}",
14686                String::from_utf8_lossy(xml)
14687            );
14688        }
14689    }
14690
14691    #[test]
14692    fn point_label_overrides_render_without_changing_preserved_xml() {
14693        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>"#;
14694        let plot = bar_plot("").replace(
14695            r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
14696            &format!(
14697                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>"#
14698            ),
14699        );
14700        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
14701        let plot = &chart.chart.plot_area.plots().unwrap()[0];
14702        let Plot::Bar { data_labels, .. } = plot else {
14703            panic!("expected bar plot");
14704        };
14705        let labels = data_labels.as_ref().unwrap();
14706        let written = labels.to_xml().unwrap();
14707        assert!(
14708            written
14709                .windows(raw_overrides.len())
14710                .any(|window| window == raw_overrides.as_bytes())
14711        );
14712        let overrides = super::point_label_overrides(labels).unwrap();
14713        assert_eq!(overrides.keys().copied().collect::<Vec<_>>(), vec![1]);
14714
14715        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14716        let mut rendered = Vec::new();
14717        super::render_data_labels(
14718            plot,
14719            chart_bounds(),
14720            DispBlanksAs::Gap,
14721            super::GeometryOptions::default(),
14722            &mut fonts,
14723            &mut rendered,
14724        )
14725        .unwrap();
14726        let labels: Vec<_> = rendered
14727            .iter()
14728            .filter_map(|element| match element {
14729                PositionedElement::Text(run) => Some(run),
14730                _ => None,
14731            })
14732            .collect();
14733        assert_eq!(labels.len(), 2);
14734        assert_eq!(labels[0].text, "North, 1");
14735        assert_eq!(labels[1].text, "South / 2.0");
14736        assert_point_near(labels[0].origin, Point { x: 50.0, y: 105.0 });
14737        assert_point_near(labels[1].origin, Point { x: 158.0, y: 70.0 });
14738        assert_eq!(
14739            CT_DLbls::from_xml(&written).unwrap().to_xml().unwrap(),
14740            written
14741        );
14742    }
14743
14744    #[test]
14745    fn malformed_point_label_overrides_return_contextual_errors() {
14746        let cases = [
14747            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:showVal val="1"/></q:dLbl></q:dLbls>"#,
14748            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>"#,
14749            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="point"/></q:dLbl></q:dLbls>"#,
14750            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>"#,
14751            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>"#,
14752            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>"#,
14753            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>"#,
14754        ];
14755        for xml in cases {
14756            let labels = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
14757            let result = std::panic::catch_unwind(|| super::point_label_overrides(&labels));
14758            assert!(result.is_ok(), "point-label parser panicked for {xml}");
14759            assert!(
14760                result.unwrap().is_err(),
14761                "malformed point labels parsed: {xml}"
14762            );
14763        }
14764
14765        let foreign = format!(
14766            r#"<q:dLbls xmlns:q="{C_NS}" xmlns:x="urn:foreign"><x:dLbl><x:idx val="0"/></x:dLbl></q:dLbls>"#
14767        );
14768        let labels = CT_DLbls::from_xml(foreign.as_bytes()).unwrap();
14769        assert!(super::point_label_overrides(&labels).unwrap().is_empty());
14770    }
14771
14772    #[test]
14773    fn percentage_label_aggregation_rejects_nonfinite_totals() {
14774        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14775        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14776        let Plot::Bar { series, .. } = plot else {
14777            panic!("expected bar plot");
14778        };
14779        series[0].values.values = vec![f64::MAX, f64::MAX];
14780        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14781        super::render_data_labels(
14782            plot,
14783            chart_bounds(),
14784            DispBlanksAs::Gap,
14785            super::GeometryOptions::default(),
14786            &mut fonts,
14787            &mut Vec::new(),
14788        )
14789        .expect("value-only labels do not need a percentage total");
14790        let Plot::Bar { data_labels, .. } = plot else {
14791            panic!("expected bar plot");
14792        };
14793        data_labels.as_mut().unwrap().show_percent = true;
14794        let error = super::render_data_labels(
14795            plot,
14796            chart_bounds(),
14797            DispBlanksAs::Gap,
14798            super::GeometryOptions::default(),
14799            &mut fonts,
14800            &mut Vec::new(),
14801        )
14802        .unwrap_err();
14803        assert!(error.to_string().contains("data-label percentage total"));
14804
14805        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>"#;
14806        let plot_xml = bar_plot("").replace(
14807            r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
14808            &format!(
14809                r#"<q:dLbls>{overrides}<q:showVal val="1"/><q:showPercent val="1"/></q:dLbls>"#
14810            ),
14811        );
14812        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
14813        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14814        let Plot::Bar { series, .. } = plot else {
14815            panic!("expected bar plot");
14816        };
14817        series[0].values.values = vec![f64::MAX, f64::MAX];
14818        super::render_data_labels(
14819            plot,
14820            chart_bounds(),
14821            DispBlanksAs::Gap,
14822            super::GeometryOptions::default(),
14823            &mut fonts,
14824            &mut Vec::new(),
14825        )
14826        .expect("effective point overrides disable percentage aggregation");
14827    }
14828
14829    #[test]
14830    fn percentage_labels_use_effective_number_format_precision() {
14831        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14832        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14833        let Plot::Bar {
14834            series,
14835            data_labels,
14836            ..
14837        } = plot
14838        else {
14839            panic!("expected bar plot");
14840        };
14841        series[0].values.values = vec![1.0, 2.0];
14842        let labels = data_labels.as_mut().unwrap();
14843        labels.show_value = false;
14844        labels.show_percent = true;
14845        labels.number_format = Some(NumberFormat::new("0.00%".to_owned(), false).unwrap());
14846        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14847        let mut rendered = Vec::new();
14848        super::render_data_labels(
14849            plot,
14850            chart_bounds(),
14851            DispBlanksAs::Gap,
14852            super::GeometryOptions::default(),
14853            &mut fonts,
14854            &mut rendered,
14855        )
14856        .unwrap();
14857        let text: Vec<_> = rendered
14858            .iter()
14859            .filter_map(|element| match element {
14860                PositionedElement::Text(run) => Some(run.text.as_str()),
14861                _ => None,
14862            })
14863            .collect();
14864        assert_eq!(text, vec!["33.33%", "66.67%"]);
14865
14866        let point_override = r#"<q:dLbl><q:idx val="0"/><q:numFmt formatCode="0.00%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbl>"#;
14867        let plot_xml = bar_plot("").replace(
14868            r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
14869            &format!(
14870                r#"<q:dLbls>{point_override}<q:numFmt formatCode="0%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbls>"#
14871            ),
14872        );
14873        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
14874        let plot = &chart.chart.plot_area.plots().unwrap()[0];
14875        let mut rendered = Vec::new();
14876        super::render_data_labels(
14877            plot,
14878            chart_bounds(),
14879            DispBlanksAs::Gap,
14880            super::GeometryOptions::default(),
14881            &mut fonts,
14882            &mut rendered,
14883        )
14884        .unwrap();
14885        let text: Vec<_> = rendered
14886            .iter()
14887            .filter_map(|element| match element {
14888                PositionedElement::Text(run) => Some(run.text.as_str()),
14889                _ => None,
14890            })
14891            .collect();
14892        assert_eq!(text, vec!["33.33%", "67%"]);
14893    }
14894
14895    #[test]
14896    fn core_chart_shells_preserve_unmodelled_children_byte_for_byte() {
14897        let preserved = [
14898            br#"<x:root x:id="1"><x:nested>one &amp; two</x:nested><!--root--></x:root>"#
14899                .as_slice(),
14900            br#"<x:title x:id="2"><x:nested><![CDATA[title]]></x:nested></x:title>"#.as_slice(),
14901            br#"<x:plot x:id="3"><x:nested>plot</x:nested></x:plot>"#.as_slice(),
14902            br#"<x:legend x:id="4"><x:nested>legend</x:nested></x:legend>"#.as_slice(),
14903            br#"<x:extension x:id="5"><x:nested>extension</x:nested></x:extension>"#.as_slice(),
14904            br#"<!--space-comment-->"#.as_slice(),
14905            br#"<?space processing?>"#.as_slice(),
14906            br#"<!--chart-comment-->"#.as_slice(),
14907            br#"<?title processing?>"#.as_slice(),
14908            br#"<!--flag-comment-->"#.as_slice(),
14909            br#"<?flag processing?>"#.as_slice(),
14910            br#"<x:producerShape/>"#.as_slice(),
14911            br#"<x:producerText/>"#.as_slice(),
14912        ];
14913        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>"#;
14914        let parsed = CT_ChartSpace::from_xml(xml).unwrap();
14915        let written = parsed.to_xml().unwrap();
14916        for raw in preserved {
14917            assert!(
14918                written.windows(raw.len()).any(|window| window == raw),
14919                "preserved subtree changed: {}",
14920                String::from_utf8_lossy(raw)
14921            );
14922        }
14923        let written_text = String::from_utf8(written.clone()).unwrap();
14924        assert!(written_text.contains(r#"x:keep="one &amp; two""#));
14925        assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
14926    }
14927
14928    #[test]
14929    fn every_corpus_chart_part_round_trips_structurally() {
14930        let Some(corpus) = require_or_skip_corpus() else {
14931            return;
14932        };
14933        verify_fetched_corpus(&corpus);
14934        let mut chart_parts = 0usize;
14935        let mut decks_with_charts = HashSet::new();
14936        for path in manifest_paths() {
14937            let package = OpcPackage::open(corpus.join(path))
14938                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14939            for (part, xml) in &package.parts {
14940                if !is_chart_part(part) {
14941                    continue;
14942                }
14943                let parsed = CT_ChartSpace::from_xml(xml)
14944                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14945                let written = parsed
14946                    .to_xml()
14947                    .unwrap_or_else(|error| panic!("{path} {part}: write failed: {error}"));
14948                let original_preserved = preserved_chartml_regions(xml)
14949                    .unwrap_or_else(|error| panic!("{path} {part}: original scan failed: {error}"));
14950                let written_preserved = preserved_chartml_regions(&written)
14951                    .unwrap_or_else(|error| panic!("{path} {part}: written scan failed: {error}"));
14952                assert_eq!(
14953                    original_preserved.len(),
14954                    written_preserved.len(),
14955                    "{path} {part}: preserved ChartML region count changed"
14956                );
14957                for (index, (original, written)) in original_preserved
14958                    .iter()
14959                    .zip(&written_preserved)
14960                    .enumerate()
14961                {
14962                    assert_eq!(
14963                        original.parent_path, written.parent_path,
14964                        "{path} {part}: preserved region {index} parent changed"
14965                    );
14966                    assert_eq!(
14967                        original.boundary, written.boundary,
14968                        "{path} {part}: preserved region {index} boundary changed"
14969                    );
14970                    assert_eq!(
14971                        original.sibling_order, written.sibling_order,
14972                        "{path} {part}: preserved region {index} order changed"
14973                    );
14974                    assert_eq!(
14975                        original.xml, written.xml,
14976                        "{path} {part}: preserved region {index} bytes changed"
14977                    );
14978                }
14979                let reparsed = CT_ChartSpace::from_xml(&written)
14980                    .unwrap_or_else(|error| panic!("{path} {part}: written parse failed: {error}"));
14981                assert_eq!(parsed, reparsed, "{path} {part}: core model changed");
14982                chart_parts += 1;
14983                decks_with_charts.insert(path);
14984            }
14985        }
14986        assert!(
14987            chart_parts > 0,
14988            "the pinned corpus contained no chart parts"
14989        );
14990        eprintln!(
14991            "ChartML corpus gate checked {chart_parts} chart parts across {} decks",
14992            decks_with_charts.len()
14993        );
14994    }
14995
14996    #[derive(Debug, Eq, PartialEq)]
14997    struct PreservedChartRegion {
14998        parent_path: Vec<Vec<u8>>,
14999        boundary: usize,
15000        sibling_order: usize,
15001        xml: Vec<u8>,
15002    }
15003
15004    struct PreservationFrame {
15005        local_name: Vec<u8>,
15006        boundary: usize,
15007        next_sibling: usize,
15008    }
15009
15010    fn preserved_chartml_regions(xml: &[u8]) -> Result<Vec<PreservedChartRegion>, String> {
15011        let mut reader = Reader::from_reader(xml);
15012        let mut buffer = Vec::new();
15013        let mut parents: Vec<PreservationFrame> = Vec::new();
15014        let mut preserved = Vec::new();
15015        loop {
15016            match reader.read_event_into(&mut buffer) {
15017                Ok(Event::Start(element)) => {
15018                    let child = local_name(element.name().as_ref()).to_vec();
15019                    if parents.is_empty() && child == b"chartSpace" {
15020                        parents.push(PreservationFrame {
15021                            local_name: child,
15022                            boundary: 0,
15023                            next_sibling: 0,
15024                        });
15025                    } else if let Some(parent) = parents.last()
15026                        && is_opaque_chartml_child(&parent.local_name, &child)
15027                    {
15028                        let boundary =
15029                            preserved_child_boundary(&parent.local_name, &child, parent.boundary);
15030                        let xml = capture_element(&mut reader, &element)
15031                            .map_err(|error| error.to_string())?;
15032                        record_preserved_region(&mut parents, &mut preserved, boundary, xml);
15033                        advance_preservation_boundary(&mut parents, &child);
15034                    } else {
15035                        advance_preservation_boundary(&mut parents, &child);
15036                        if is_chartml_preservation_parent(&child) {
15037                            parents.push(PreservationFrame {
15038                                local_name: child,
15039                                boundary: 0,
15040                                next_sibling: 0,
15041                            });
15042                        } else {
15043                            capture_element(&mut reader, &element)
15044                                .map_err(|error| error.to_string())?;
15045                        }
15046                    }
15047                }
15048                Ok(Event::Empty(element)) => {
15049                    let name = element.name();
15050                    let child = local_name(name.as_ref()).to_vec();
15051                    if let Some(parent) = parents.last()
15052                        && is_opaque_chartml_child(&parent.local_name, &child)
15053                    {
15054                        let boundary =
15055                            preserved_child_boundary(&parent.local_name, &child, parent.boundary);
15056                        let xml =
15057                            capture_empty_element(&element).map_err(|error| error.to_string())?;
15058                        record_preserved_region(&mut parents, &mut preserved, boundary, xml);
15059                    }
15060                    advance_preservation_boundary(&mut parents, &child);
15061                }
15062                Ok(
15063                    event @ (Event::Comment(_)
15064                    | Event::PI(_)
15065                    | Event::CData(_)
15066                    | Event::GeneralRef(_)),
15067                ) if !parents.is_empty() => {
15068                    let boundary = parents.last().expect("preservation parent").boundary;
15069                    let xml = capture_event(event).map_err(|error| error.to_string())?;
15070                    record_preserved_region(&mut parents, &mut preserved, boundary, xml);
15071                }
15072                Ok(Event::End(_)) => {
15073                    parents.pop();
15074                }
15075                Ok(Event::Eof) => break,
15076                Ok(_) => {}
15077                Err(error) => return Err(error.to_string()),
15078            }
15079            buffer.clear();
15080        }
15081        Ok(preserved)
15082    }
15083
15084    fn record_preserved_region(
15085        parents: &mut [PreservationFrame],
15086        preserved: &mut Vec<PreservedChartRegion>,
15087        boundary: usize,
15088        xml: Vec<u8>,
15089    ) {
15090        let sibling_order = parents.last().expect("preservation parent").next_sibling;
15091        parents
15092            .last_mut()
15093            .expect("preservation parent")
15094            .next_sibling += 1;
15095        preserved.push(PreservedChartRegion {
15096            parent_path: parents
15097                .iter()
15098                .map(|parent| parent.local_name.clone())
15099                .collect(),
15100            boundary,
15101            sibling_order,
15102            xml,
15103        });
15104    }
15105
15106    fn advance_preservation_boundary(parents: &mut [PreservationFrame], child: &[u8]) {
15107        let Some(parent) = parents.last_mut() else {
15108            return;
15109        };
15110        parent.boundary = parent
15111            .boundary
15112            .max(preserved_boundary_after(&parent.local_name, child));
15113    }
15114
15115    fn preserved_child_boundary(parent: &[u8], child: &[u8], current: usize) -> usize {
15116        match parent {
15117            b"chartSpace" => match child {
15118                b"date1904" => 0,
15119                b"lang" => 1,
15120                b"roundedCorners" => 2,
15121                b"style" => 3,
15122                b"clrMapOvr" => 4,
15123                b"pivotSource" => 5,
15124                b"protection" => 6,
15125                b"externalData" => 10,
15126                b"printSettings" => 11,
15127                b"userShapes" => 12,
15128                b"extLst" => 13,
15129                _ => current,
15130            },
15131            b"chart" => match child {
15132                b"pivotFmts" => 2,
15133                b"view3D" => 3,
15134                b"floor" => 4,
15135                b"sideWall" => 5,
15136                b"backWall" => 6,
15137                b"showDLblsOverMax" | b"extLst" => 11,
15138                _ => current,
15139            },
15140            _ => current,
15141        }
15142    }
15143
15144    fn preserved_boundary_after(parent: &[u8], child: &[u8]) -> usize {
15145        match parent {
15146            b"chartSpace" => match child {
15147                b"date1904" => 1,
15148                b"lang" => 2,
15149                b"roundedCorners" => 3,
15150                b"style" => 4,
15151                b"clrMapOvr" => 5,
15152                b"pivotSource" => 6,
15153                b"protection" => 7,
15154                b"chart" => 8,
15155                b"spPr" => 9,
15156                b"txPr" => 10,
15157                b"externalData" => 11,
15158                b"printSettings" => 12,
15159                b"userShapes" => 13,
15160                b"extLst" => 14,
15161                _ => 0,
15162            },
15163            b"chart" => match child {
15164                b"title" => 1,
15165                b"autoTitleDeleted" => 2,
15166                b"pivotFmts" => 3,
15167                b"view3D" => 4,
15168                b"floor" => 5,
15169                b"sideWall" => 6,
15170                b"backWall" => 7,
15171                b"plotArea" => 8,
15172                b"legend" => 9,
15173                b"plotVisOnly" => 10,
15174                b"dispBlanksAs" => 11,
15175                b"showDLblsOverMax" | b"extLst" => 11,
15176                _ => 0,
15177            },
15178            b"pieChart" => match child {
15179                b"ser" => 1,
15180                b"dLbls" => 2,
15181                b"firstSliceAng" => 3,
15182                _ => 0,
15183            },
15184            b"doughnutChart" => match child {
15185                b"ser" => 1,
15186                b"dLbls" => 2,
15187                b"firstSliceAng" => 3,
15188                b"holeSize" => 4,
15189                _ => 0,
15190            },
15191            b"areaChart" => match child {
15192                b"grouping" => 1,
15193                b"ser" => 2,
15194                b"dLbls" => 3,
15195                b"axId" => 4,
15196                _ => 0,
15197            },
15198            b"scatterChart" | b"radarChart" => match child {
15199                b"scatterStyle" | b"radarStyle" => 1,
15200                b"ser" => 2,
15201                b"dLbls" => 3,
15202                b"axId" => 4,
15203                _ => 0,
15204            },
15205            _ => 0,
15206        }
15207    }
15208
15209    fn is_opaque_chartml_child(parent: &[u8], child: &[u8]) -> bool {
15210        match parent {
15211            b"chartSpace" => !matches!(child, b"chart" | b"spPr" | b"txPr"),
15212            b"chart" => !matches!(
15213                child,
15214                b"title"
15215                    | b"autoTitleDeleted"
15216                    | b"plotArea"
15217                    | b"legend"
15218                    | b"plotVisOnly"
15219                    | b"dispBlanksAs"
15220            ),
15221            b"plotArea" => !matches!(
15222                child,
15223                b"barChart"
15224                    | b"lineChart"
15225                    | b"pieChart"
15226                    | b"doughnutChart"
15227                    | b"areaChart"
15228                    | b"scatterChart"
15229                    | b"radarChart"
15230                    | b"catAx"
15231                    | b"valAx"
15232                    | b"dateAx"
15233                    | b"serAx"
15234            ),
15235            b"pieChart" => !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng"),
15236            b"doughnutChart" => {
15237                !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng" | b"holeSize")
15238            }
15239            b"areaChart" => !matches!(child, b"grouping" | b"ser" | b"dLbls" | b"axId"),
15240            b"scatterChart" => !matches!(child, b"scatterStyle" | b"ser" | b"dLbls" | b"axId"),
15241            b"radarChart" => !matches!(child, b"radarStyle" | b"ser" | b"dLbls" | b"axId"),
15242            b"title" | b"legend" => true,
15243            _ => false,
15244        }
15245    }
15246
15247    fn is_chartml_preservation_parent(parent: &[u8]) -> bool {
15248        matches!(
15249            parent,
15250            b"chartSpace"
15251                | b"chart"
15252                | b"title"
15253                | b"plotArea"
15254                | b"pieChart"
15255                | b"doughnutChart"
15256                | b"areaChart"
15257                | b"scatterChart"
15258                | b"radarChart"
15259                | b"legend"
15260                | b"autoTitleDeleted"
15261                | b"plotVisOnly"
15262                | b"dispBlanksAs"
15263        )
15264    }
15265
15266    #[test]
15267    fn oxml_chart_is_an_explicit_publication_candidate() {
15268        let manifest = include_str!("../Cargo.toml");
15269        assert!(manifest.contains("name = \"oxml-chart\""));
15270        assert!(manifest.contains("version = \"0.9.0\""));
15271        assert!(manifest.contains("publish = true"));
15272        for dependency in [
15273            "oxml-core.workspace",
15274            "oxml-drawing.workspace",
15275            "quick-xml.workspace",
15276        ] {
15277            assert!(manifest.contains(dependency));
15278        }
15279        assert!(!manifest.contains("rpptx.workspace"));
15280        assert!(!manifest.contains("rdocx.workspace"));
15281    }
15282
15283    fn first_series_xml(xml: &[u8]) -> Option<Vec<u8>> {
15284        let mut reader = Reader::from_reader(xml);
15285        let mut buffer = Vec::new();
15286        loop {
15287            match reader.read_event_into(&mut buffer).ok()? {
15288                Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
15289                    return capture_element(&mut reader, &element).ok();
15290                }
15291                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
15292                    return capture_empty_element(&element).ok();
15293                }
15294                Event::Eof => return None,
15295                _ => {}
15296            }
15297            buffer.clear();
15298        }
15299    }
15300
15301    fn replace_once(haystack: &[u8], needle: &[u8], replacement: &[u8]) -> Vec<u8> {
15302        let offset = haystack
15303            .windows(needle.len())
15304            .position(|window| window == needle)
15305            .expect("serialized source series occurs in chart part");
15306        assert!(
15307            !haystack[offset + needle.len()..]
15308                .windows(needle.len())
15309                .any(|window| window == needle),
15310            "source series unexpectedly occurs more than once"
15311        );
15312        let mut result = Vec::with_capacity(haystack.len() - needle.len() + replacement.len());
15313        result.extend_from_slice(&haystack[..offset]);
15314        result.extend_from_slice(replacement);
15315        result.extend_from_slice(&haystack[offset + needle.len()..]);
15316        result
15317    }
15318
15319    fn assert_command_version(program: &str, arguments: &[&str], expected: &str) {
15320        let output = Command::new(program)
15321            .args(arguments)
15322            .output()
15323            .unwrap_or_else(|error| panic!("run {program} version check: {error}"));
15324        assert!(
15325            output.status.success(),
15326            "{program} version check failed: {}",
15327            String::from_utf8_lossy(&output.stderr)
15328        );
15329        let stdout = String::from_utf8(output.stdout).expect("version stdout is UTF-8");
15330        let stderr = String::from_utf8(output.stderr).expect("version stderr is UTF-8");
15331        let actual = if stdout.trim().is_empty() {
15332            stderr.trim()
15333        } else {
15334            stdout.trim()
15335        };
15336        assert_eq!(
15337            actual.lines().next(),
15338            Some(expected),
15339            "{program} version drift"
15340        );
15341    }
15342
15343    fn sha256(path: &Path) -> String {
15344        let output = Command::new("shasum")
15345            .args(["-a", "256"])
15346            .arg(path)
15347            .output()
15348            .unwrap_or_else(|error| panic!("{}: run shasum: {error}", path.display()));
15349        assert!(
15350            output.status.success(),
15351            "{}: shasum failed: {}",
15352            path.display(),
15353            String::from_utf8_lossy(&output.stderr)
15354        );
15355        String::from_utf8(output.stdout)
15356            .expect("shasum output is UTF-8")
15357            .split_whitespace()
15358            .next()
15359            .expect("shasum digest")
15360            .to_owned()
15361    }
15362
15363    fn workspace_root() -> PathBuf {
15364        Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
15365    }
15366
15367    fn corpus_dir() -> PathBuf {
15368        std::env::var_os("RDOCX_PPTX_CORPUS_DIR")
15369            .map(PathBuf::from)
15370            .unwrap_or_else(|| workspace_root().join("corpus/pptx"))
15371    }
15372
15373    fn require_or_skip_corpus() -> Option<PathBuf> {
15374        let corpus = corpus_dir();
15375        if corpus.is_dir() {
15376            return Some(corpus);
15377        }
15378        assert_ne!(
15379            std::env::var_os("RDOCX_PPTX_CORPUS_REQUIRED").as_deref(),
15380            Some(std::ffi::OsStr::new("1")),
15381            "the pinned corpus is required but {} does not exist",
15382            corpus.display()
15383        );
15384        eprintln!(
15385            "ChartML corpus gate skipped because {} is absent",
15386            corpus.display()
15387        );
15388        None
15389    }
15390
15391    fn verify_fetched_corpus(corpus: &Path) {
15392        let status = Command::new("python3")
15393            .arg(workspace_root().join("scripts/fetch_pptx_corpus.py"))
15394            .arg("--check")
15395            .env("RDOCX_PPTX_CORPUS_DIR", corpus)
15396            .status()
15397            .expect("run corpus verifier");
15398        assert!(status.success(), "pinned corpus verification failed");
15399    }
15400
15401    fn manifest_paths() -> Vec<&'static str> {
15402        let mut lines = MANIFEST.lines();
15403        assert_eq!(lines.next(), Some("path\tproducer\tsha256\turl"));
15404        let paths: Vec<_> = lines
15405            .map(|line| line.split('\t').next().expect("manifest path"))
15406            .collect();
15407        assert_eq!(paths.len(), EXPECTED_DECKS);
15408        paths
15409    }
15410
15411    fn is_chart_part(part: &str) -> bool {
15412        let Some(stem) = part
15413            .strip_prefix("/ppt/charts/chart")
15414            .and_then(|name| name.strip_suffix(".xml"))
15415        else {
15416            return false;
15417        };
15418        !stem.is_empty() && stem.bytes().all(|byte| byte.is_ascii_digit())
15419    }
15420
15421    fn plot_series(index: u32) -> String {
15422        format!(
15423            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>"#
15424        )
15425    }
15426
15427    fn sparse_category_series() -> &'static str {
15428        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>"#
15429    }
15430
15431    fn sparse_scatter_series() -> &'static str {
15432        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>"#
15433    }
15434
15435    fn chart_with_plot(plot: &str) -> String {
15436        format!(
15437            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>"#
15438        )
15439    }
15440
15441    fn chart_with_optional_axes(plot: &str, axes: bool) -> String {
15442        let axis_xml = if axes {
15443            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>"#
15444        } else {
15445            ""
15446        };
15447        format!(
15448            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>"#
15449        )
15450    }
15451
15452    fn numeric_plot_series(index: u32) -> String {
15453        format!(
15454            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>"#
15455        )
15456    }
15457
15458    fn scatter_without_x(index: u32) -> String {
15459        format!(
15460            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>"#
15461        )
15462    }
15463
15464    fn scatter_without_y(index: u32) -> String {
15465        format!(
15466            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>"#
15467        )
15468    }
15469
15470    fn remaining_plot_fixtures() -> Vec<(&'static str, String, bool)> {
15471        let series = plot_series(0);
15472        let scatter = numeric_plot_series(0);
15473        vec![
15474            (
15475                "pieChart",
15476                format!(
15477                    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>"#
15478                ),
15479                false,
15480            ),
15481            (
15482                "doughnutChart",
15483                format!(
15484                    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>"#
15485                ),
15486                false,
15487            ),
15488            (
15489                "areaChart",
15490                format!(
15491                    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>"#
15492                ),
15493                true,
15494            ),
15495            (
15496                "scatterChart",
15497                format!(
15498                    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>"#
15499                ),
15500                true,
15501            ),
15502            (
15503                "radarChart",
15504                format!(
15505                    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>"#
15506                ),
15507                true,
15508            ),
15509        ]
15510    }
15511
15512    fn bar_plot(extra: &str) -> String {
15513        format!(
15514            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>"#,
15515            plot_series(0)
15516        )
15517    }
15518
15519    fn line_plot(extra: &str) -> String {
15520        format!(
15521            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>"#,
15522            plot_series(0)
15523        )
15524    }
15525
15526    #[test]
15527    fn bar_and_line_plots_round_trip_and_render() {
15528        for (xml, is_bar) in [
15529            (chart_with_plot(&bar_plot("")), true),
15530            (chart_with_plot(&line_plot("")), false),
15531        ] {
15532            let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15533            let plots = parsed.chart.plot_area.plots().unwrap();
15534            assert_eq!(plots.len(), 1);
15535            assert_eq!(matches!(plots[0], Plot::Bar { .. }), is_bar);
15536            assert_eq!(matches!(plots[0], Plot::Line { .. }), !is_bar);
15537            assert_eq!(parsed.chart.plot_area.axes().unwrap().len(), 2);
15538            let written = parsed.to_xml().unwrap();
15539            let reparsed = CT_ChartSpace::from_xml(&written).unwrap();
15540            assert_eq!(parsed, reparsed);
15541        }
15542        if let Some(corpus) = require_or_skip_corpus() {
15543            verify_bar_and_line_viewer_gate(&corpus);
15544        }
15545    }
15546
15547    #[test]
15548    fn ppm_parser_preserves_whitespace_valued_first_pixels() {
15549        for first_pixel in *b" \n\r\t" {
15550            let mut ppm = b"P6\n1 1\n255\n".to_vec();
15551            ppm.extend_from_slice(&[first_pixel, 0x7f, 0xff]);
15552            let (width, height, pixels) = ppm_pixels(&ppm);
15553            assert_eq!((width, height), (1, 1));
15554            assert_eq!(pixels, [first_pixel, 0x7f, 0xff]);
15555        }
15556
15557        let ppm = b"P6\r\n1 1\r\n255\r\n\x7f\xff";
15558        assert_eq!(ppm_pixels(ppm).2, [b'\n', 0x7f, 0xff]);
15559    }
15560
15561    #[test]
15562    fn bar_and_line_plots_write_fixed_prefixes_in_schema_order() {
15563        for xml in [
15564            chart_with_plot(&bar_plot(r#"<q:extLst><x:bar-tail/></q:extLst>"#)),
15565            chart_with_plot(&line_plot(r#"<q:extLst><x:line-tail/></q:extLst>"#)),
15566        ] {
15567            let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15568            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15569            assert!(written.contains("<c:barChart") || written.contains("<c:lineChart"));
15570            assert!(!written.contains("<q:barChart"));
15571            assert!(!written.contains("<q:lineChart"));
15572            if written.contains("<c:barChart") {
15573                let positions: Vec<_> = [
15574                    "<c:barDir",
15575                    "<c:grouping",
15576                    "<q:varyColors",
15577                    "<c:ser",
15578                    "<c:dLbls",
15579                    "<c:gapWidth",
15580                    "<c:overlap",
15581                    "<q:serLines",
15582                    "<c:axId",
15583                    "<q:extLst",
15584                ]
15585                .iter()
15586                .map(|tag| written.find(tag).unwrap())
15587                .collect();
15588                assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15589            } else {
15590                let positions: Vec<_> = [
15591                    "<c:grouping",
15592                    "<q:varyColors",
15593                    "<c:ser",
15594                    "<c:dLbls",
15595                    "<q:dropLines",
15596                    "<c:marker",
15597                    "<c:smooth",
15598                    "<c:axId",
15599                    "<q:extLst",
15600                ]
15601                .iter()
15602                .map(|tag| written.find(tag).unwrap())
15603                .collect();
15604                assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15605            }
15606            assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
15607        }
15608
15609        for plot in [bar_plot(""), line_plot("")] {
15610            let first = plot_series(0);
15611            let with_series_boundary = plot.replacen(
15612                &first,
15613                &format!(
15614                    "{first}<!--between-series--><?series-boundary?>\n  {}",
15615                    plot_series(1)
15616                ),
15617                1,
15618            );
15619            let with_axis_boundary = with_series_boundary.replacen(
15620                r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
15621                r#"<q:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
15622  <q:axId val="-1884097184"/>"#,
15623                1,
15624            );
15625            let parsed =
15626                CT_ChartSpace::from_xml(chart_with_plot(&with_axis_boundary).as_bytes()).unwrap();
15627            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15628            assert!(written.contains("</c:ser><!--between-series--><?series-boundary?>\n  <c:ser"));
15629            assert!(written.contains(
15630                r#"<c:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
15631  <c:axId val="-1884097184"/>"#
15632            ));
15633            assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
15634        }
15635    }
15636
15637    #[test]
15638    fn malformed_bar_and_line_plots_return_errors_without_panicking() {
15639        let valid_series = plot_series(0);
15640        let cases = [
15641            format!(r#"<q:barChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15642            format!(r#"<q:barChart><q:barDir val="diagonal"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15643            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>"#),
15644            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>"#),
15645            format!(r#"<q:lineChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
15646            format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:marker val="maybe"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
15647            r#"<q:lineChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#.to_owned(),
15648            format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:axId val="1"/><q:axId val="1"/></q:lineChart>"#),
15649            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>"#),
15650            format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15651        ];
15652        for plot in cases {
15653            let xml = chart_with_plot(&plot);
15654            let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
15655            assert!(result.is_ok(), "plot parser panicked for {plot}");
15656            assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
15657        }
15658
15659        let missing_axis = format!(
15660            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>"#,
15661            bar_plot("")
15662        );
15663        assert!(CT_ChartSpace::from_xml(missing_axis.as_bytes()).is_err());
15664
15665        let values =
15666            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
15667        let series = vec![Series::new(0, 0, values)];
15668        let ids = [AxisId::new(1).unwrap(), AxisId::new(2).unwrap()];
15669        let mut invalid =
15670            Plot::bar(BarDirection::Column, BarGrouping::Clustered, series, ids).unwrap();
15671        if let Plot::Bar { gap_width, .. } = &mut invalid {
15672            *gap_width = 501;
15673        }
15674        assert!(invalid.validate().is_err());
15675    }
15676
15677    #[test]
15678    fn unsupported_and_combo_plots_remain_byte_preserved() {
15679        let bar = bar_plot("");
15680        let line = line_plot("");
15681        let combo = chart_with_plot(&format!("<!--before-->{bar}<?between?>{line}<!--after-->"));
15682        let parsed = CT_ChartSpace::from_xml(combo.as_bytes()).unwrap();
15683        assert!(parsed.chart.plot_area.plots().is_err());
15684        let written = parsed.to_xml().unwrap();
15685        for raw in [
15686            bar.as_bytes(),
15687            line.as_bytes(),
15688            br#"<!--before-->"#.as_slice(),
15689            br#"<?between?>"#.as_slice(),
15690            br#"<!--after-->"#.as_slice(),
15691        ] {
15692            assert!(written.windows(raw.len()).any(|window| window == raw));
15693        }
15694
15695        let unsupported = chart_with_plot(
15696            r#"<q:bar3DChart x:keep="three"><q:barDir val="col"/><q:ser><x:opaque/></q:ser></q:bar3DChart>"#,
15697        );
15698        let parsed = CT_ChartSpace::from_xml(unsupported.as_bytes()).unwrap();
15699        assert!(parsed.chart.plot_area.plots().is_err());
15700        assert!(
15701            String::from_utf8(parsed.to_xml().unwrap())
15702                .unwrap()
15703                .contains(r#"<q:bar3DChart x:keep="three">"#)
15704        );
15705    }
15706
15707    #[test]
15708    fn public_plot_edits_preserve_axes_and_unselected_payloads() {
15709        let xml = chart_with_plot(&bar_plot(
15710            r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#,
15711        ));
15712        let mut parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15713        let axes_before = parsed.chart.plot_area.axes().unwrap();
15714        match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
15715            Plot::Bar {
15716                gap_width,
15717                overlap,
15718                series,
15719                ..
15720            } => {
15721                *gap_width = 225;
15722                *overlap = -25;
15723                series[0].index = 7;
15724                series[0].values.values[0] = 9.0;
15725            }
15726            _ => panic!("expected bar plot"),
15727        }
15728        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15729        assert!(written.contains(r#"<c:gapWidth val="225"/>"#));
15730        assert!(written.contains(r#"<c:overlap val="-25"/>"#));
15731        assert!(written.contains("<c:v>9</c:v>"));
15732        assert!(written.contains("<q:varyColors val=\"1\"/>"));
15733        assert!(written.find("<q:varyColors").unwrap() < written.find("<c:ser").unwrap());
15734        assert!(written.contains("<q:serLines><x:line/></q:serLines>"));
15735        assert!(written.contains(r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#));
15736        let reparsed = CT_ChartSpace::from_xml(written.as_bytes()).unwrap();
15737        assert_eq!(axes_before, reparsed.chart.plot_area.axes().unwrap());
15738
15739        let first = plot_series(0);
15740        let colliding_series =
15741            bar_plot("").replacen(&first, &format!("{first}{}", plot_series(1)), 1);
15742        let mut collision =
15743            CT_ChartSpace::from_xml(chart_with_plot(&colliding_series).as_bytes()).unwrap();
15744        if let Plot::Bar { series, .. } = &mut collision.chart.plot_area.plots_mut().unwrap()[0] {
15745            series[0].index = series[1].index;
15746            series[0].order = series[1].order;
15747        }
15748        let collision = String::from_utf8(collision.to_xml().unwrap()).unwrap();
15749        assert!(collision.find("<q:varyColors").unwrap() < collision.find("<c:ser").unwrap());
15750
15751        let mut inserted = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15752        if let Plot::Bar { series, .. } = &mut inserted.chart.plot_area.plots_mut().unwrap()[0] {
15753            let mut new_series = series[0].clone();
15754            new_series.index = 99;
15755            new_series.order = 99;
15756            series.insert(0, new_series);
15757        }
15758        let inserted = String::from_utf8(inserted.to_xml().unwrap()).unwrap();
15759        assert!(inserted.find("<q:varyColors").unwrap() < inserted.find("<c:ser").unwrap());
15760
15761        let axis_payload = bar_plot("").replacen(
15762            r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
15763            r#"<q:axId val="-1884094432" x:slot="category"/><!--axis-anchor--><q:axId val="-1884097184" x:slot="value"/>"#,
15764            1,
15765        );
15766        let mut swapped =
15767            CT_ChartSpace::from_xml(chart_with_plot(&axis_payload).as_bytes()).unwrap();
15768        if let Plot::Bar { axis_ids, .. } = &mut swapped.chart.plot_area.plots_mut().unwrap()[0] {
15769            axis_ids.swap(0, 1);
15770        }
15771        let swapped = String::from_utf8(swapped.to_xml().unwrap()).unwrap();
15772        let anchor = swapped.find("<!--axis-anchor-->").unwrap();
15773        let value_axis = swapped
15774            .find(r#"<c:axId val="-1884097184" x:slot="value"/>"#)
15775            .unwrap();
15776        let category_axis = swapped
15777            .find(r#"<c:axId val="-1884094432" x:slot="category"/>"#)
15778            .unwrap();
15779        let series_lines = swapped.find("<q:serLines").unwrap();
15780        assert!(series_lines < value_axis && series_lines < category_axis);
15781        assert!(anchor < value_axis && value_axis < category_axis);
15782
15783        let mut replaced = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15784        let Plot::Bar {
15785            series, axis_ids, ..
15786        } = &replaced.chart.plot_area.plots().unwrap()[0]
15787        else {
15788            panic!("expected bar plot");
15789        };
15790        let line = Plot::line(Grouping::Standard, series.clone(), *axis_ids).unwrap();
15791        replaced.chart.plot_area.plots_mut().unwrap()[0] = line;
15792        assert!(replaced.to_xml().is_err());
15793    }
15794
15795    #[test]
15796    fn every_corpus_bar_and_line_plot_round_trips_structurally() {
15797        let Some(corpus) = require_or_skip_corpus() else {
15798            return;
15799        };
15800        verify_fetched_corpus(&corpus);
15801        let mut typed_bar_count = 0usize;
15802        let mut typed_line_count = 0usize;
15803        let mut opaque_bar_count = 0usize;
15804        let mut opaque_line_count = 0usize;
15805        let mut opaque_combo_count = 0usize;
15806        for path in manifest_paths() {
15807            let package = OpcPackage::open(corpus.join(path))
15808                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
15809            for (part, xml) in &package.parts {
15810                if !is_chart_part(part) {
15811                    continue;
15812                }
15813                let chart = CT_ChartSpace::from_xml(xml)
15814                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
15815                match chart.chart.plot_area.plots() {
15816                    Ok(plots) => {
15817                        for plot in plots {
15818                            match plot {
15819                                Plot::Bar { .. } => typed_bar_count += 1,
15820                                Plot::Line { .. } => typed_line_count += 1,
15821                                _ => {}
15822                            }
15823                        }
15824                    }
15825                    Err(_) => {
15826                        let mut preserved_plot_count = 0usize;
15827                        for raw in chart.chart.plot_area.raw_children.at(0) {
15828                            if let Ok(Some(local)) = super::chart_root_local(
15829                                raw,
15830                                &chart.chart.plot_area.namespace_bindings,
15831                            ) {
15832                                match local.as_slice() {
15833                                    b"barChart" => {
15834                                        opaque_bar_count += 1;
15835                                        preserved_plot_count += 1;
15836                                    }
15837                                    b"lineChart" => {
15838                                        opaque_line_count += 1;
15839                                        preserved_plot_count += 1;
15840                                    }
15841                                    _ => {}
15842                                }
15843                            }
15844                        }
15845                        if preserved_plot_count > 1 {
15846                            opaque_combo_count += 1;
15847                        }
15848                    }
15849                }
15850                let written = chart
15851                    .to_xml()
15852                    .unwrap_or_else(|error| panic!("{path} {part}: chart write failed: {error}"));
15853                let reparsed = CT_ChartSpace::from_xml(&written).unwrap_or_else(|error| {
15854                    panic!("{path} {part}: written chart parse failed: {error}")
15855                });
15856                assert_eq!(chart, reparsed, "{path} {part}: chart model changed");
15857            }
15858        }
15859        assert_eq!(typed_bar_count, 11, "typed corpus bar coverage changed");
15860        assert_eq!(typed_line_count, 2, "typed corpus line coverage changed");
15861        assert_eq!(opaque_bar_count, 1, "preserved combo bar coverage changed");
15862        assert_eq!(
15863            opaque_line_count, 1,
15864            "preserved combo line coverage changed"
15865        );
15866        assert_eq!(opaque_combo_count, 1, "preserved combo coverage changed");
15867        assert_eq!(typed_bar_count + opaque_bar_count, 12);
15868        assert_eq!(typed_line_count + opaque_line_count, 3);
15869        eprintln!(
15870            "ChartML plot corpus gate checked {typed_bar_count} typed bar, {typed_line_count} typed line, and one preserved bar-line combination"
15871        );
15872    }
15873
15874    fn verify_remaining_plot_viewer_gate(corpus: &Path) {
15875        verify_fetched_corpus(corpus);
15876        assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
15877        assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
15878        let source = corpus.join("bar-chart.pptx");
15879        let source_sha = sha256(&source);
15880        let temp_root = std::env::temp_dir().join(format!(
15881            "oxml-chart-f122-viewer-gate-{}",
15882            std::process::id()
15883        ));
15884        if temp_root.exists() {
15885            fs::remove_dir_all(&temp_root).expect("remove stale F-122 evidence");
15886        }
15887        fs::create_dir_all(&temp_root).expect("create F-122 evidence root");
15888
15889        for (kind, plot, axes) in remaining_plot_fixtures() {
15890            let mut package = OpcPackage::open(&source).expect("open F-122 viewer source");
15891            package.set_part(
15892                "/ppt/charts/chart1.xml",
15893                CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes())
15894                    .expect("parse F-122 viewer chart")
15895                    .to_xml()
15896                    .expect("serialize F-122 viewer chart"),
15897            );
15898            let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
15899            package.save(&unbound).expect("save F-122 viewer candidate");
15900            let candidate_sha = sha256(&unbound);
15901            let evidence = temp_root.join(format!("{source_sha}-{candidate_sha}"));
15902            fs::create_dir(&evidence).expect("create SHA-bound F-122 evidence directory");
15903            let candidate = evidence.join(format!("{kind}-candidate.pptx"));
15904            fs::rename(&unbound, &candidate).expect("bind F-122 candidate to SHA");
15905            assert_eq!(sha256(&candidate), candidate_sha);
15906            let render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
15907            let bytes = fs::read(&render).expect("read F-122 viewer PPM");
15908            let (width, height, pixels) = ppm_pixels(&bytes);
15909            let left = width / 5;
15910            let right = width * 4 / 5;
15911            let top = height * 3 / 20;
15912            let bottom = height * 17 / 20;
15913            let mut nonblank = 0usize;
15914            for y in top..bottom {
15915                for x in left..right {
15916                    let offset = (y * width + x) * 3;
15917                    if pixels[offset..offset + 3]
15918                        .iter()
15919                        .any(|channel| *channel < 245)
15920                    {
15921                        nonblank += 1;
15922                    }
15923                }
15924            }
15925            const CHART_RECTANGLE_NONBLANK_THRESHOLD: usize = 1_000;
15926            assert!(
15927                nonblank >= CHART_RECTANGLE_NONBLANK_THRESHOLD,
15928                "{kind}: chart rectangle [{left},{top}) to [{right},{bottom}) contains {nonblank} nonblank pixels, below {CHART_RECTANGLE_NONBLANK_THRESHOLD}"
15929            );
15930            eprintln!(
15931                "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}",
15932                sha256(&render)
15933            );
15934        }
15935        fs::remove_dir_all(&temp_root).expect("remove F-122 viewer evidence");
15936    }
15937
15938    fn verify_bar_and_line_viewer_gate(corpus: &Path) {
15939        verify_fetched_corpus(corpus);
15940        assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
15941        assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
15942        let temp_root = std::env::temp_dir().join(format!(
15943            "oxml-chart-f121-viewer-gate-{}",
15944            std::process::id()
15945        ));
15946        if temp_root.exists() {
15947            fs::remove_dir_all(&temp_root).expect("remove stale F-121 evidence");
15948        }
15949        fs::create_dir_all(&temp_root).expect("create F-121 evidence root");
15950
15951        for (kind, deck) in [("bar", "bar-chart.pptx"), ("line", "line-chart.pptx")] {
15952            let source = corpus.join(deck);
15953            let original_sha = sha256(&source);
15954            let mut package = OpcPackage::open(&source)
15955                .unwrap_or_else(|error| panic!("{deck}: open viewer source: {error}"));
15956            let chart_part = "/ppt/charts/chart1.xml";
15957            let original_parts = package.parts.clone();
15958            let parsed = CT_ChartSpace::from_xml(
15959                package
15960                    .get_part(chart_part)
15961                    .unwrap_or_else(|| panic!("{deck}: missing {chart_part}")),
15962            )
15963            .unwrap_or_else(|error| panic!("{deck}: parse viewer chart: {error}"));
15964            assert!(
15965                parsed.chart.plot_area.plots().is_ok(),
15966                "{deck}: plot is opaque"
15967            );
15968            package.set_part(
15969                chart_part,
15970                parsed
15971                    .to_xml()
15972                    .unwrap_or_else(|error| panic!("{deck}: serialize viewer chart: {error}")),
15973            );
15974            for (part, bytes) in &original_parts {
15975                if part == chart_part {
15976                    assert_ne!(package.parts[part], *bytes, "{deck}: chart did not rewrite");
15977                } else {
15978                    assert_eq!(package.parts[part], *bytes, "{deck}: changed part {part}");
15979                }
15980            }
15981
15982            let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
15983            package
15984                .save(&unbound)
15985                .unwrap_or_else(|error| panic!("{deck}: save viewer candidate: {error}"));
15986            let candidate_sha = sha256(&unbound);
15987            let evidence = temp_root.join(format!("{original_sha}-{candidate_sha}"));
15988            fs::create_dir(&evidence).expect("create SHA-bound F-121 evidence directory");
15989            let original = evidence.join(format!("{kind}-original.pptx"));
15990            let candidate = evidence.join(format!("{kind}-candidate.pptx"));
15991            fs::copy(&source, &original).expect("copy SHA-bound viewer original");
15992            fs::rename(&unbound, &candidate).expect("bind viewer candidate to SHA");
15993            assert_eq!(sha256(&original), original_sha);
15994            assert_eq!(sha256(&candidate), candidate_sha);
15995
15996            let original_render = render_deck_to_ppm(&original, &evidence, kind, "original");
15997            let candidate_render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
15998            let original_bytes = fs::read(&original_render).expect("read original PPM");
15999            let candidate_bytes = fs::read(&candidate_render).expect("read candidate PPM");
16000            let normalized_mae = normalized_ppm_mae(&original_bytes, &candidate_bytes);
16001            assert!(
16002                normalized_mae <= PLOT_RENDER_NORMALIZED_MAE_THRESHOLD,
16003                "{deck}: normalized RGB MAE {normalized_mae:.8} exceeds {:.8}",
16004                PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
16005            );
16006            eprintln!(
16007                "F-121 {kind} viewer gate original deck {original_sha}, candidate deck {candidate_sha}, original render {}, candidate render {}, normalized RGB MAE {normalized_mae:.8} <= {:.8}",
16008                sha256(&original_render),
16009                sha256(&candidate_render),
16010                PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
16011            );
16012        }
16013        fs::remove_dir_all(&temp_root).expect("remove F-121 viewer evidence");
16014    }
16015
16016    fn render_deck_to_ppm(deck: &Path, evidence: &Path, kind: &str, side: &str) -> PathBuf {
16017        let profile = std::env::temp_dir().join(format!(
16018            "oxml-chart-f121-{kind}-{side}-profile-{}",
16019            std::process::id()
16020        ));
16021        if profile.exists() {
16022            fs::remove_dir_all(&profile).expect("remove stale F-121 viewer profile");
16023        }
16024        let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
16025        let conversion = Command::new("soffice")
16026            .args([
16027                "--headless",
16028                &profile_argument,
16029                "--convert-to",
16030                "pdf:impress_pdf_Export",
16031                "--outdir",
16032            ])
16033            .arg(evidence)
16034            .arg(deck)
16035            .output()
16036            .expect("run pinned LibreOffice F-121 viewer gate");
16037        assert!(
16038            conversion.status.success(),
16039            "{}: LibreOffice conversion failed: {}",
16040            deck.display(),
16041            String::from_utf8_lossy(&conversion.stderr)
16042        );
16043        let pdf = deck.with_extension("pdf");
16044        assert!(
16045            pdf.is_file(),
16046            "LibreOffice did not create {}",
16047            pdf.display()
16048        );
16049        let prefix = evidence.join(format!("{kind}-{side}"));
16050        let raster = Command::new("pdftoppm")
16051            .args(["-f", "1", "-singlefile", "-r", "150"])
16052            .arg(&pdf)
16053            .arg(&prefix)
16054            .output()
16055            .expect("run pinned Poppler F-121 raster gate");
16056        assert!(
16057            raster.status.success(),
16058            "{}: pdftoppm failed: {}",
16059            pdf.display(),
16060            String::from_utf8_lossy(&raster.stderr)
16061        );
16062        if profile.exists() {
16063            fs::remove_dir_all(&profile).expect("remove F-121 viewer profile");
16064        }
16065        prefix.with_extension("ppm")
16066    }
16067
16068    fn normalized_ppm_mae(left: &[u8], right: &[u8]) -> f64 {
16069        let (left_width, left_height, left_pixels) = ppm_pixels(left);
16070        let (right_width, right_height, right_pixels) = ppm_pixels(right);
16071        assert_eq!((left_width, left_height), (right_width, right_height));
16072        assert_eq!(left_pixels.len(), right_pixels.len());
16073        let difference: u64 = left_pixels
16074            .iter()
16075            .zip(right_pixels)
16076            .map(|(left, right)| u64::from(left.abs_diff(*right)))
16077            .sum();
16078        difference as f64 / (left_pixels.len() as f64 * 255.0)
16079    }
16080
16081    fn ppm_pixels(bytes: &[u8]) -> (usize, usize, &[u8]) {
16082        let mut cursor = 0usize;
16083        let mut tokens = Vec::new();
16084        while tokens.len() < 4 {
16085            while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
16086                cursor += 1;
16087            }
16088            if cursor < bytes.len() && bytes[cursor] == b'#' {
16089                while cursor < bytes.len() && bytes[cursor] != b'\n' {
16090                    cursor += 1;
16091                }
16092                continue;
16093            }
16094            let start = cursor;
16095            while cursor < bytes.len() && !bytes[cursor].is_ascii_whitespace() {
16096                cursor += 1;
16097            }
16098            tokens.push(std::str::from_utf8(&bytes[start..cursor]).expect("PPM header UTF-8"));
16099        }
16100        assert_eq!(tokens[0], "P6");
16101        let width = tokens[1].parse::<usize>().expect("PPM width");
16102        let height = tokens[2].parse::<usize>().expect("PPM height");
16103        assert_eq!(tokens[3], "255");
16104        assert!(
16105            cursor < bytes.len() && bytes[cursor].is_ascii_whitespace(),
16106            "PPM header must end with whitespace"
16107        );
16108        cursor += 1;
16109        let pixels = &bytes[cursor..];
16110        assert_eq!(pixels.len(), width * height * 3, "PPM pixel length");
16111        (width, height, pixels)
16112    }
16113}