Skip to main content

oxml_chart/
lib.rs

1#![allow(non_camel_case_types)]
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::fmt;
5use std::io::Write;
6
7use oxml_core::OxmlError;
8use oxml_core::raw_xml::{capture_element, capture_empty_element};
9use oxml_core::xml::{local_name, matches_local_name};
10use oxml_core::xml_text::{decode_plain, resolve_entity};
11use oxml_drawing::color::{
12    ColorChoice, ColorMap, ColorMapSlot, ResolvedColor, ThemeColorSlot, apply_color_transforms,
13    resolve_color,
14};
15use oxml_drawing::fill::{Fill, SolidFill};
16use oxml_drawing::line::CT_LineProperties;
17use oxml_drawing::order::OrderedRawChildren;
18use oxml_drawing::shape_props::{CT_ShapeProperties, ShapePropertiesError};
19use oxml_drawing::text::{CT_TextBody, CT_TextCharacterProperties, TextError};
20use oxml_drawing::theme::CT_OfficeStyleSheet;
21use oxml_layout::{
22    Color, FillRule, FontManager, GlyphRun, GroupElement, Paint, Path, PathCommand, PathElement,
23    Point, PositionedElement, Rect, Stroke, Transform,
24};
25use oxml_sml::{Column, Workbook};
26use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
27use quick_xml::{Reader, Writer, XmlVersion};
28
29pub use oxml_drawing::color::RgbColor;
30
31/// The transitional ChartML namespace used by OOXML packages.
32pub const C_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/chart";
33/// The fixed ChartML prefix used by writers in this crate.
34pub const C_PREFIX: &str = "c";
35/// The DrawingML namespace written on a chart-space root.
36pub const A_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
37/// The office-document relationships namespace written on a chart-space root.
38pub const R_NS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
39const MAX_CATEGORY_ANNOTATIONS: usize = 16_384;
40const PLOT_MARGIN_LEFT: f64 = 36.0;
41const PLOT_MARGIN_RIGHT: f64 = 12.0;
42const PLOT_MARGIN_TOP: f64 = 12.0;
43const PLOT_MARGIN_BOTTOM: f64 = 28.0;
44
45/// Errors produced while reading or writing the implemented ChartML core.
46#[derive(Debug)]
47pub enum ChartError {
48    Xml(OxmlError),
49    ShapeProperties(ShapePropertiesError),
50    Text(TextError),
51    UnexpectedElement(String),
52    MissingElement(String),
53    DuplicateElement(String),
54    InvalidAttribute {
55        element: String,
56        attribute: String,
57        value: String,
58    },
59    InvalidValue {
60        element: String,
61        value: String,
62    },
63}
64
65impl fmt::Display for ChartError {
66    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67        match self {
68            Self::Xml(error) => error.fmt(formatter),
69            Self::ShapeProperties(error) => error.fmt(formatter),
70            Self::Text(error) => error.fmt(formatter),
71            Self::UnexpectedElement(element) => {
72                write!(formatter, "unexpected ChartML element: {element}")
73            }
74            Self::MissingElement(element) => {
75                write!(formatter, "ChartML requires {element}")
76            }
77            Self::DuplicateElement(element) => {
78                write!(formatter, "ChartML contains duplicate {element}")
79            }
80            Self::InvalidAttribute {
81                element,
82                attribute,
83                value,
84            } => write!(
85                formatter,
86                "ChartML {element} has invalid @{attribute}: {value}"
87            ),
88            Self::InvalidValue { element, value } => {
89                write!(formatter, "ChartML {element} has invalid value: {value}")
90            }
91        }
92    }
93}
94
95impl std::error::Error for ChartError {
96    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
97        match self {
98            Self::Xml(error) => Some(error),
99            Self::ShapeProperties(error) => Some(error),
100            Self::Text(error) => Some(error),
101            _ => None,
102        }
103    }
104}
105
106impl From<OxmlError> for ChartError {
107    fn from(error: OxmlError) -> Self {
108        Self::Xml(error)
109    }
110}
111
112impl From<ShapePropertiesError> for ChartError {
113    fn from(error: ShapePropertiesError) -> Self {
114        Self::ShapeProperties(error)
115    }
116}
117
118impl From<TextError> for ChartError {
119    fn from(error: TextError) -> Self {
120        Self::Text(error)
121    }
122}
123
124pub type Result<T> = std::result::Result<T, ChartError>;
125
126/// One supported two-dimensional chart family.
127#[derive(Clone, Copy, Debug, Eq, PartialEq)]
128pub enum ChartKind {
129    Bar,
130    Line,
131    Pie,
132    Doughnut,
133    Area,
134    Scatter,
135    Radar,
136}
137
138/// Data used to author one chart and its editable workbook.
139#[derive(Clone, Debug, Default, PartialEq)]
140pub struct ChartData {
141    pub categories: Vec<String>,
142    pub series: Vec<(String, Vec<f64>)>,
143    pub number_format: Option<String>,
144    pub category_axis_title: Option<String>,
145    pub value_axis_title: Option<String>,
146    pub palette: Vec<RgbColor>,
147}
148
149/// Authors one ChartML part and its editable workbook from one validated source.
150pub fn authored_chart_parts(
151    kind: ChartKind,
152    data: &ChartData,
153    workbook_relationship_id: &str,
154) -> Result<(Vec<u8>, Vec<u8>)> {
155    validate_chart_data(kind, data)?;
156    let category_column = if kind == ChartKind::Scatter {
157        Column::Number {
158            header: "Category".to_owned(),
159            values: data
160                .categories
161                .iter()
162                .map(|value| value.parse::<f64>().expect("validated scatter category"))
163                .collect(),
164            number_format: None,
165        }
166    } else {
167        Column::Text {
168            header: "Category".to_owned(),
169            values: data.categories.clone(),
170        }
171    };
172    let mut columns = Vec::with_capacity(data.series.len() + 1);
173    columns.push(category_column);
174    columns.extend(data.series.iter().map(|(name, values)| Column::Number {
175        header: name.clone(),
176        values: values.clone(),
177        number_format: data.number_format.clone(),
178    }));
179    let workbook = Workbook::new("Sheet1", columns).map_err(invalid_authoring_value)?;
180    let category_formula = workbook
181        .formula_range(0)
182        .expect("validated categories produce a formula range");
183    let number_format = data.number_format.as_deref().unwrap_or("General");
184    let mut chart_series = Vec::with_capacity(data.series.len());
185    for (index, (name, values)) in data.series.iter().enumerate() {
186        let values_formula = workbook
187            .formula_range(index + 1)
188            .expect("validated series produces a formula range");
189        let values = NumericData::new(values_formula, number_format.to_owned(), values.clone())
190            .map_err(invalid_authoring_value)?;
191        let mut series = Series::new(index as u32, index as u32, values);
192        series.name = Some(
193            StringRef::new(
194                format!("Sheet1!${}$1", spreadsheet_column_name(index + 1)),
195                vec![name.clone()],
196            )
197            .map_err(invalid_authoring_value)?,
198        );
199        series.categories = Some(if kind == ChartKind::Scatter {
200            AxisData::Numeric(
201                NumericData::new(
202                    category_formula.clone(),
203                    "General".to_owned(),
204                    data.categories
205                        .iter()
206                        .map(|value| value.parse::<f64>().expect("validated scatter category"))
207                        .collect(),
208                )
209                .map_err(invalid_authoring_value)?,
210            )
211        } else {
212            AxisData::String(
213                StringRef::new(category_formula.clone(), data.categories.clone())
214                    .map_err(invalid_authoring_value)?,
215            )
216        });
217        chart_series.push(series);
218    }
219    apply_palette(kind, data, &mut chart_series);
220
221    let category_axis_id = AxisId::new(48_650_112).map_err(invalid_authoring_value)?;
222    let value_axis_id = AxisId::new(48_672_768).map_err(invalid_authoring_value)?;
223    let axis_ids = [category_axis_id, value_axis_id];
224    let (plot, axes) = match kind {
225        ChartKind::Bar => (
226            Plot::bar(
227                BarDirection::Column,
228                BarGrouping::Clustered,
229                chart_series,
230                axis_ids,
231            ),
232            category_value_axes(category_axis_id, value_axis_id, data)?,
233        ),
234        ChartKind::Line => (
235            Plot::line(Grouping::Standard, chart_series, axis_ids),
236            category_value_axes(category_axis_id, value_axis_id, data)?,
237        ),
238        ChartKind::Pie => (Plot::pie(chart_series), Vec::new()),
239        ChartKind::Doughnut => (Plot::doughnut(chart_series), Vec::new()),
240        ChartKind::Area => (
241            Plot::area(Grouping::Standard, chart_series, axis_ids),
242            category_value_axes(category_axis_id, value_axis_id, data)?,
243        ),
244        ChartKind::Scatter => (
245            Plot::scatter(ScatterStyle::LineMarker, chart_series, axis_ids),
246            authored_axes(
247                [
248                    Axis::new(
249                        AxisKind::Value,
250                        category_axis_id,
251                        AxisPosition::Bottom,
252                        value_axis_id,
253                    ),
254                    Axis::new(
255                        AxisKind::Value,
256                        value_axis_id,
257                        AxisPosition::Left,
258                        category_axis_id,
259                    ),
260                ],
261                data,
262            )?,
263        ),
264        ChartKind::Radar => (
265            Plot::radar(RadarStyle::Standard, chart_series, axis_ids),
266            category_value_axes(category_axis_id, value_axis_id, data)?,
267        ),
268    };
269    let mut plot = plot.map_err(invalid_authoring_value)?;
270    if let Plot::Pie { data_labels, .. } | Plot::Doughnut { data_labels, .. } = &mut plot {
271        *data_labels = Some(CT_DLbls {
272            show_percent: true,
273            ..CT_DLbls::default()
274        });
275    }
276    let plot_area = CT_PlotArea::new(vec![plot], axes).map_err(invalid_authoring_value)?;
277    let chart_shell = format!(
278        r#"<c:chartSpace xmlns:c="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}"><c:chart><c:plotArea/></c:chart><c:externalData r:id="{workbook_relationship_id}"><c:autoUpdate val="0"/></c:externalData></c:chartSpace>"#,
279    );
280    let mut chart_space = CT_ChartSpace::from_xml(chart_shell.as_bytes())?;
281    chart_space.chart.auto_title_deleted = true;
282    chart_space.chart.plot_area = plot_area;
283    chart_space.chart.legend = (data.series.len() > 1
284        || matches!(kind, ChartKind::Pie | ChartKind::Doughnut))
285    .then(CT_Legend::default);
286    let chart_xml = chart_space.to_xml()?;
287    let workbook_bytes = workbook.to_xlsx_bytes().map_err(invalid_authoring_value)?;
288    Ok((chart_xml, workbook_bytes))
289}
290
291fn validate_chart_data(kind: ChartKind, data: &ChartData) -> Result<()> {
292    if data.categories.is_empty() {
293        return Err(invalid_authoring_value("at least one category is required"));
294    }
295    if data.series.is_empty() {
296        return Err(invalid_authoring_value("at least one series is required"));
297    }
298    if matches!(kind, ChartKind::Pie | ChartKind::Doughnut) && data.series.len() != 1 {
299        return Err(invalid_authoring_value(
300            "pie and doughnut charts require exactly one series",
301        ));
302    }
303    for (name, values) in &data.series {
304        if values.len() != data.categories.len() {
305            return Err(invalid_authoring_value(format!(
306                "series {name:?} has {} values for {} categories",
307                values.len(),
308                data.categories.len()
309            )));
310        }
311        if values.iter().any(|value| !value.is_finite()) {
312            return Err(invalid_authoring_value(format!(
313                "series {name:?} contains a nonfinite value"
314            )));
315        }
316    }
317    if kind == ChartKind::Scatter {
318        for category in &data.categories {
319            if category
320                .parse::<f64>()
321                .ok()
322                .is_none_or(|value| !value.is_finite())
323            {
324                return Err(invalid_authoring_value(format!(
325                    "scatter category {category:?} is not a finite number"
326                )));
327            }
328        }
329    }
330    if let Some(format) = &data.number_format {
331        NumericData::new("Sheet1!$A$2".to_owned(), format.clone(), vec![0.0])?;
332    }
333    Ok(())
334}
335
336fn invalid_authoring_value(error: impl fmt::Display) -> ChartError {
337    ChartError::InvalidValue {
338        element: "chart authoring".to_owned(),
339        value: error.to_string(),
340    }
341}
342
343fn category_value_axes(category: AxisId, value: AxisId, data: &ChartData) -> Result<Vec<Axis>> {
344    authored_axes(
345        [
346            Axis::new(AxisKind::Category, category, AxisPosition::Bottom, value),
347            Axis::new(AxisKind::Value, value, AxisPosition::Left, category),
348        ],
349        data,
350    )
351}
352
353fn authored_axes(mut axes: [Axis; 2], data: &ChartData) -> Result<Vec<Axis>> {
354    for axis in &mut axes {
355        axis.set_deleted(false);
356    }
357    axes[0].title = data
358        .category_axis_title
359        .as_deref()
360        .map(CT_Title::plain_text);
361    axes[1].title = data.value_axis_title.as_deref().map(CT_Title::plain_text);
362    axes[1].number_format = data
363        .number_format
364        .as_ref()
365        .map(|format| NumberFormat::new(format.clone(), false))
366        .transpose()?;
367    Ok(axes.into())
368}
369
370fn apply_palette(kind: ChartKind, data: &ChartData, series: &mut [Series]) {
371    if data.palette.is_empty() {
372        return;
373    }
374    for (index, item) in series.iter_mut().enumerate() {
375        item.sp_pr = Some(series_shape(kind, data.palette[index % data.palette.len()]));
376    }
377    if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) && series.len() == 1 {
378        series[0].authored_data_points = data
379            .categories
380            .iter()
381            .enumerate()
382            .map(|(index, _)| DataPoint {
383                index: index as u32,
384                sp_pr: series_shape(kind, data.palette[index % data.palette.len()]),
385            })
386            .collect();
387    }
388}
389
390fn series_shape(kind: ChartKind, color: RgbColor) -> CT_ShapeProperties {
391    let mut solid = SolidFill::default();
392    solid.color = Some(ColorChoice::srgb(color));
393    let fill = Fill::Solid(solid);
394    let mut properties = CT_ShapeProperties::default();
395    if matches!(
396        kind,
397        ChartKind::Line | ChartKind::Scatter | ChartKind::Radar
398    ) {
399        let mut line = CT_LineProperties::default();
400        line.fill = Some(fill);
401        properties.line = Some(line);
402    } else {
403        properties.fill = Some(fill);
404    }
405    properties
406}
407
408fn spreadsheet_column_name(mut index: usize) -> String {
409    let mut name = Vec::new();
410    index += 1;
411    while index != 0 {
412        let remainder = (index - 1) % 26;
413        name.push((b'A' + remainder as u8) as char);
414        index = (index - 1) / 26;
415    }
416    name.iter().rev().collect()
417}
418
419/// Backend-neutral plot geometry and the plot rectangle reserved inside a chart.
420#[derive(Clone, Debug)]
421pub struct ChartGeometry {
422    pub plot_bounds: Rect,
423    pub elements: Vec<PositionedElement>,
424}
425
426#[derive(Clone, Debug)]
427struct AxisScale {
428    domain: Domain,
429    ticks: Vec<f64>,
430}
431
432#[derive(Clone, Copy, Debug)]
433struct AxisMapping {
434    domain: Domain,
435    orientation: Orientation,
436}
437
438#[derive(Clone, Copy, Debug, Default)]
439struct GeometryOptions {
440    category_orientation: Orientation,
441    value: Option<AxisMapping>,
442    scatter_x: Option<AxisMapping>,
443    scatter_y: Option<AxisMapping>,
444}
445
446#[derive(Default)]
447struct ChartAnnotations {
448    gridlines: Vec<PositionedElement>,
449    axis_lines: Vec<PositionedElement>,
450    ticks: Vec<PositionedElement>,
451    labels: Vec<PositionedElement>,
452}
453
454/// Converts one supported typed chart into a labelled backend-neutral group.
455pub fn render_chart(
456    chart: &CT_Chart,
457    bounds: Rect,
458    theme: &CT_OfficeStyleSheet,
459    color_map: &ColorMap,
460    fonts: &mut FontManager,
461) -> Result<GroupElement> {
462    let plot_bounds = geometry_plot_bounds(bounds)?;
463    let plots = chart.plot_area.plots()?;
464    let plot = plots
465        .first()
466        .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
467    plot.validate()?;
468    let series_colours = resolve_series_colours(plot, theme, color_map)?;
469    let axes = chart.plot_area.axes()?;
470    let options = geometry_options(plot, &axes, chart.disp_blanks_as)?;
471    let plot_children = render_plot_geometry(
472        plot,
473        plot_bounds,
474        chart.disp_blanks_as,
475        options,
476        &series_colours,
477    )?;
478    if plot_children.is_empty() && !plot_can_render_without_marks(plot) {
479        return Err(ChartError::InvalidValue {
480            element: "c:plotArea".to_owned(),
481            value: "plot has no renderable cached data".to_owned(),
482        });
483    }
484
485    let mut chart_annotations =
486        render_axes_and_labels(plot, &axes, plot_bounds, chart.disp_blanks_as, fonts)?;
487    let mut annotations = Vec::new();
488    render_data_labels(
489        plot,
490        plot_bounds,
491        chart.disp_blanks_as,
492        options,
493        fonts,
494        &mut chart_annotations.labels,
495    )?;
496    render_legend(
497        chart,
498        plot,
499        plot_bounds,
500        fonts,
501        &series_colours,
502        &mut annotations,
503        &mut chart_annotations.labels,
504    )?;
505
506    validate_geometry_coordinates(&chart_annotations.gridlines)?;
507    validate_geometry_coordinates(&plot_children)?;
508    validate_geometry_coordinates(&chart_annotations.axis_lines)?;
509    validate_geometry_coordinates(&chart_annotations.ticks)?;
510    validate_geometry_coordinates(&annotations)?;
511    let mut children = chart_annotations.gridlines;
512    children.push(PositionedElement::Group(GroupElement {
513        transform: Transform::IDENTITY,
514        clip: Some(Path::rect(plot_bounds)),
515        opacity: 1.0,
516        effects: Vec::new(),
517        children: plot_children,
518    }));
519    children.extend(chart_annotations.axis_lines);
520    children.extend(chart_annotations.ticks);
521    children.extend(annotations);
522    children.extend(chart_annotations.labels);
523    Ok(GroupElement {
524        transform: Transform::IDENTITY,
525        clip: None,
526        opacity: 1.0,
527        effects: Vec::new(),
528        children,
529    })
530}
531
532/// Shapes the stable label used inside a visible unsupported-chart fallback.
533pub fn render_chart_placeholder(bounds: Rect, fonts: &mut FontManager) -> Result<GroupElement> {
534    if !bounds.width.is_finite()
535        || !bounds.height.is_finite()
536        || bounds.width <= 0.0
537        || bounds.height <= 0.0
538    {
539        return Err(ChartError::InvalidValue {
540            element: "chart fallback bounds".to_owned(),
541            value: "width and height must be finite and positive".to_owned(),
542        });
543    }
544    let label = shape_label(
545        fonts,
546        "Unsupported chart",
547        Point {
548            x: 4.0,
549            y: (bounds.height / 2.0).max(9.0),
550        },
551    )?;
552    Ok(GroupElement {
553        transform: Transform::IDENTITY,
554        clip: Some(Path::rect(Rect {
555            x: 0.0,
556            y: 0.0,
557            width: bounds.width,
558            height: bounds.height,
559        })),
560        opacity: 1.0,
561        effects: Vec::new(),
562        children: vec![PositionedElement::Text(label)],
563    })
564}
565
566/// Converts one supported typed chart into chart-local backend-neutral paths.
567pub fn render_geometry(
568    chart: &CT_Chart,
569    bounds: Rect,
570    theme: &CT_OfficeStyleSheet,
571    color_map: &ColorMap,
572) -> Result<ChartGeometry> {
573    let plot_bounds = geometry_plot_bounds(bounds)?;
574    let plots = chart.plot_area.plots()?;
575    let plot = plots
576        .first()
577        .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
578    plot.validate()?;
579    let series_colours = resolve_series_colours(plot, theme, color_map)?;
580    let children = render_plot_geometry(
581        plot,
582        plot_bounds,
583        chart.disp_blanks_as,
584        geometry_options(plot, &[], chart.disp_blanks_as)?,
585        &series_colours,
586    )?;
587    if children.is_empty() && !plot_can_render_without_marks(plot) {
588        return Err(ChartError::InvalidValue {
589            element: "c:plotArea".to_owned(),
590            value: "plot has no renderable cached data".to_owned(),
591        });
592    }
593    validate_geometry_coordinates(&children)?;
594    Ok(ChartGeometry {
595        plot_bounds,
596        elements: vec![PositionedElement::Group(GroupElement {
597            transform: Transform::IDENTITY,
598            clip: None,
599            opacity: 1.0,
600            effects: Vec::new(),
601            children,
602        })],
603    })
604}
605
606fn plot_can_render_without_marks(plot: &Plot) -> bool {
607    matches!(
608        plot,
609        Plot::Radar { series, .. }
610            if series
611                .iter()
612                .any(|series| !series.values.values.is_empty())
613    )
614}
615
616fn geometry_plot_bounds(bounds: Rect) -> Result<Rect> {
617    if ![bounds.x, bounds.y, bounds.width, bounds.height]
618        .into_iter()
619        .all(f64::is_finite)
620        || bounds.width <= 0.0
621        || bounds.height <= 0.0
622        || !(bounds.x + bounds.width).is_finite()
623        || !(bounds.y + bounds.height).is_finite()
624    {
625        return Err(ChartError::InvalidValue {
626            element: "chart geometry bounds".to_owned(),
627            value: format!(
628                "x={}, y={}, width={}, height={}",
629                bounds.x, bounds.y, bounds.width, bounds.height
630            ),
631        });
632    }
633    let plot_bounds = Rect {
634        x: bounds.x + PLOT_MARGIN_LEFT,
635        y: bounds.y + PLOT_MARGIN_TOP,
636        width: bounds.width - PLOT_MARGIN_LEFT - PLOT_MARGIN_RIGHT,
637        height: bounds.height - PLOT_MARGIN_TOP - PLOT_MARGIN_BOTTOM,
638    };
639    if ![
640        plot_bounds.x,
641        plot_bounds.y,
642        plot_bounds.width,
643        plot_bounds.height,
644        plot_bounds.x + plot_bounds.width,
645        plot_bounds.y + plot_bounds.height,
646    ]
647    .into_iter()
648    .all(f64::is_finite)
649        || plot_bounds.width <= 0.0
650        || plot_bounds.height <= 0.0
651    {
652        return Err(ChartError::InvalidValue {
653            element: "chart geometry bounds".to_owned(),
654            value: "chart is too small for the fixed plot margins".to_owned(),
655        });
656    }
657    Ok(plot_bounds)
658}
659
660fn geometry_options(plot: &Plot, axes: &[Axis], blanks: DispBlanksAs) -> Result<GeometryOptions> {
661    let mut options = GeometryOptions::default();
662    if let Some(category_axis) = axes.iter().find(|axis| axis.kind != AxisKind::Value) {
663        options.category_orientation = category_axis.scaling.orientation;
664    }
665
666    if let Plot::Scatter { axis_ids, .. } = plot {
667        let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
668        if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[0]) {
669            options.scatter_x = Some(axis_mapping(axis, x_domain)?);
670        }
671        if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[1]) {
672            options.scatter_y = Some(axis_mapping(axis, y_domain)?);
673        }
674    } else if let Some(domain) = plot_value_domain(plot, blanks)? {
675        options.value = Some(
676            axes.iter()
677                .find(|axis| axis.kind == AxisKind::Value)
678                .map(|axis| axis_mapping(axis, domain))
679                .transpose()?
680                .unwrap_or(AxisMapping {
681                    domain,
682                    orientation: Orientation::MinMax,
683                }),
684        );
685    }
686    Ok(options)
687}
688
689fn axis_mapping(axis: &Axis, extent: Domain) -> Result<AxisMapping> {
690    if axis.scaling.log_base.is_some() {
691        return Err(ChartError::InvalidValue {
692            element: "c:scaling/c:logBase".to_owned(),
693            value: "native chart rendering currently supports linear axes".to_owned(),
694        });
695    }
696    let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
697    Ok(AxisMapping {
698        domain: scale.domain,
699        orientation: axis.scaling.orientation,
700    })
701}
702
703fn plot_value_domain(plot: &Plot, blanks: DispBlanksAs) -> Result<Option<Domain>> {
704    let domain = match plot {
705        Plot::Bar {
706            grouping, series, ..
707        } => {
708            let logical = logical_series_values(series)?;
709            let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
710            let layers =
711                stacked_series_bounds(&logical, stacked, *grouping == BarGrouping::PercentStacked)?;
712            domain_from_layers(&layers)?
713        }
714        Plot::Line {
715            grouping, series, ..
716        } => {
717            let logical = logical_series_values(series)?;
718            if *grouping == Grouping::Standard {
719                let values = if blanks == DispBlanksAs::Zero {
720                    zero_filled_series(&logical, series_category_count(series)?)
721                        .into_iter()
722                        .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
723                        .collect::<Vec<_>>()
724                } else {
725                    logical
726                        .into_iter()
727                        .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
728                        .collect::<Vec<_>>()
729                };
730                data_domain(&values, false)?
731            } else {
732                let layers =
733                    stacked_series_bounds(&logical, true, *grouping == Grouping::PercentStacked)?;
734                domain_from_layers(&layers)?
735            }
736        }
737        Plot::Area {
738            grouping, series, ..
739        } => {
740            let logical = logical_series_values(series)?;
741            let layers = stacked_series_bounds(
742                &logical,
743                *grouping != Grouping::Standard,
744                *grouping == Grouping::PercentStacked,
745            )?;
746            domain_from_layers(&layers)?
747        }
748        Plot::Radar { series, .. } => {
749            let values = series
750                .iter()
751                .flat_map(|series| series.values.values.iter().copied())
752                .collect::<Vec<_>>();
753            data_domain(&values, true)?
754        }
755        Plot::Pie { .. } | Plot::Doughnut { .. } | Plot::Scatter { .. } => return Ok(None),
756    };
757    Ok(Some(domain))
758}
759
760fn scatter_domains(plot: &Plot, blanks: DispBlanksAs) -> Result<(Domain, Domain)> {
761    let Plot::Scatter { series, .. } = plot else {
762        return Err(ChartError::UnexpectedElement("non-scatter plot".to_owned()));
763    };
764    let mut x_values = Vec::new();
765    let mut y_values = Vec::new();
766    for series in series {
767        for (_, x, y) in scatter_render_points(series, blanks)? {
768            x_values.push(x);
769            y_values.push(y);
770        }
771    }
772    Ok((
773        data_domain(&x_values, false)?,
774        data_domain(&y_values, false)?,
775    ))
776}
777
778fn render_axes_and_labels(
779    plot: &Plot,
780    axes: &[Axis],
781    bounds: Rect,
782    blanks: DispBlanksAs,
783    fonts: &mut FontManager,
784) -> Result<ChartAnnotations> {
785    if matches!(plot, Plot::Radar { .. }) {
786        return render_radar_axes_and_labels(plot, axes, bounds, blanks, fonts);
787    }
788    let mut annotations = ChartAnnotations::default();
789    let series = plot_series(plot);
790    let category_count = if series.is_empty() {
791        1
792    } else {
793        series_category_count(series)?
794    };
795    for axis in axes {
796        if axis.deleted {
797            continue;
798        }
799        axis_line(&mut annotations.axis_lines, bounds, axis.position);
800        if axis.kind == AxisKind::Value {
801            let extent = axis_extent(plot, axis, blanks)?;
802            let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
803            for tick in &scale.ticks {
804                let coordinate = axis_tick_coordinate(
805                    *tick,
806                    scale.domain,
807                    bounds,
808                    axis.position,
809                    axis.scaling.orientation,
810                )?;
811                if axis.major_gridlines.is_some() {
812                    major_gridline(
813                        &mut annotations.gridlines,
814                        bounds,
815                        axis.position,
816                        coordinate,
817                    );
818                }
819                axis_tick(
820                    &mut annotations.ticks,
821                    bounds,
822                    axis.position,
823                    coordinate,
824                    axis.major_tick_mark,
825                );
826                if axis.tick_label_position != TickLabelPosition::None {
827                    let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
828                    let origin = tick_label_origin(
829                        bounds,
830                        axis.position,
831                        axis.tick_label_position,
832                        coordinate,
833                    );
834                    annotations
835                        .labels
836                        .push(PositionedElement::Text(shape_label_with_properties(
837                            fonts,
838                            &text,
839                            origin,
840                            axis.tx_pr.as_ref(),
841                        )?));
842                }
843            }
844        } else {
845            let count = category_count;
846            if count > MAX_CATEGORY_ANNOTATIONS {
847                return Err(ChartError::InvalidValue {
848                    element: "c:catAx annotations".to_owned(),
849                    value: format!(
850                        "logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"
851                    ),
852                });
853            }
854            let category_labels: BTreeMap<_, _> =
855                if axis.tick_label_position != TickLabelPosition::None {
856                    series
857                        .first()
858                        .map(|series| series_categories(series, axis.number_format.as_ref()))
859                        .transpose()?
860                        .unwrap_or_default()
861                        .into_iter()
862                        .collect()
863                } else {
864                    BTreeMap::new()
865                };
866            for logical_index in 0..count {
867                let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
868                let fraction = (index as f64 + 0.5) / count as f64;
869                let coordinate =
870                    if matches!(axis.position, AxisPosition::Bottom | AxisPosition::Top) {
871                        bounds.x + fraction * bounds.width
872                    } else {
873                        bounds.y + fraction * bounds.height
874                    };
875                if axis.major_gridlines.is_some() {
876                    major_gridline(
877                        &mut annotations.gridlines,
878                        bounds,
879                        axis.position,
880                        coordinate,
881                    );
882                }
883                axis_tick(
884                    &mut annotations.ticks,
885                    bounds,
886                    axis.position,
887                    coordinate,
888                    axis.major_tick_mark,
889                );
890                if axis.tick_label_position != TickLabelPosition::None
891                    && let Some(text) = category_labels.get(&logical_index)
892                {
893                    let origin = tick_label_origin(
894                        bounds,
895                        axis.position,
896                        axis.tick_label_position,
897                        coordinate,
898                    );
899                    annotations
900                        .labels
901                        .push(PositionedElement::Text(shape_label_with_properties(
902                            fonts,
903                            text,
904                            origin,
905                            axis.tx_pr.as_ref(),
906                        )?));
907                }
908            }
909        }
910    }
911    Ok(annotations)
912}
913
914fn render_radar_axes_and_labels(
915    plot: &Plot,
916    axes: &[Axis],
917    bounds: Rect,
918    blanks: DispBlanksAs,
919    fonts: &mut FontManager,
920) -> Result<ChartAnnotations> {
921    let mut annotations = ChartAnnotations::default();
922    let series = plot_series(plot);
923    let count = series_category_count(series)?;
924    if count > MAX_CATEGORY_ANNOTATIONS {
925        return Err(ChartError::InvalidValue {
926            element: "c:radarChart annotations".to_owned(),
927            value: format!("logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"),
928        });
929    }
930    let center = Point {
931        x: bounds.x + bounds.width / 2.0,
932        y: bounds.y + bounds.height / 2.0,
933    };
934    let radius = bounds.width.min(bounds.height) / 2.0;
935    for axis in axes {
936        if axis.deleted {
937            continue;
938        }
939        if axis.kind == AxisKind::Value {
940            let scale = nice_number_scale(
941                axis_extent(plot, axis, blanks)?,
942                axis.scaling.minimum,
943                axis.scaling.maximum,
944                6,
945            )?;
946            annotations.axis_lines.push(stroked_segment(
947                center,
948                radial_point(center, radius, 0, count),
949                Color::BLACK,
950                1.0,
951            ));
952            for tick in &scale.ticks {
953                let distance =
954                    normalized_value_oriented(*tick, scale.domain, axis.scaling.orientation)?
955                        * radius;
956                if axis.major_gridlines.is_some() && distance > 0.0 {
957                    let points: Vec<_> = (0..count)
958                        .map(|index| radial_point(center, distance, index, count))
959                        .collect();
960                    annotations
961                        .gridlines
962                        .push(PositionedElement::Path(PathElement {
963                            path: polyline_path(&points, true),
964                            fill: None,
965                            stroke: Some(Stroke::new(
966                                Paint::Solid(Color {
967                                    r: 0.82,
968                                    g: 0.82,
969                                    b: 0.82,
970                                    a: 1.0,
971                                }),
972                                0.5,
973                            )),
974                        }));
975                }
976                radar_value_tick(
977                    &mut annotations.ticks,
978                    center,
979                    distance,
980                    axis.major_tick_mark,
981                );
982                let origin = match axis.tick_label_position {
983                    TickLabelPosition::High => Some(Point {
984                        x: center.x + radius + 4.0,
985                        y: center.y - distance + 3.0,
986                    }),
987                    TickLabelPosition::Low => Some(Point {
988                        x: center.x - radius - 30.0,
989                        y: center.y - distance + 3.0,
990                    }),
991                    TickLabelPosition::NextTo => Some(Point {
992                        x: center.x + 4.0,
993                        y: center.y - distance + 3.0,
994                    }),
995                    TickLabelPosition::None => None,
996                };
997                if let Some(origin) = origin {
998                    let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
999                    annotations
1000                        .labels
1001                        .push(PositionedElement::Text(shape_label_with_properties(
1002                            fonts,
1003                            &text,
1004                            origin,
1005                            axis.tx_pr.as_ref(),
1006                        )?));
1007                }
1008            }
1009        } else {
1010            let category_labels: BTreeMap<_, _> =
1011                if axis.tick_label_position != TickLabelPosition::None {
1012                    series
1013                        .first()
1014                        .map(|series| series_categories(series, axis.number_format.as_ref()))
1015                        .transpose()?
1016                        .unwrap_or_default()
1017                        .into_iter()
1018                        .collect()
1019                } else {
1020                    BTreeMap::new()
1021                };
1022            for logical_index in 0..count {
1023                let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
1024                let end = radial_point(center, radius, index, count);
1025                annotations
1026                    .axis_lines
1027                    .push(stroked_segment(center, end, Color::BLACK, 1.0));
1028                radar_category_tick(
1029                    &mut annotations.ticks,
1030                    center,
1031                    radius,
1032                    index,
1033                    count,
1034                    axis.major_tick_mark,
1035                );
1036                if let Some(text) = category_labels.get(&logical_index) {
1037                    let Some(origin) = radar_category_label_origin(
1038                        bounds,
1039                        center,
1040                        radius,
1041                        index,
1042                        count,
1043                        axis.tick_label_position,
1044                    ) else {
1045                        continue;
1046                    };
1047                    annotations
1048                        .labels
1049                        .push(PositionedElement::Text(shape_label_with_properties(
1050                            fonts,
1051                            text,
1052                            origin,
1053                            axis.tx_pr.as_ref(),
1054                        )?));
1055                }
1056            }
1057        }
1058    }
1059    Ok(annotations)
1060}
1061
1062fn radar_category_label_origin(
1063    bounds: Rect,
1064    center: Point,
1065    radius: f64,
1066    index: usize,
1067    count: usize,
1068    position: TickLabelPosition,
1069) -> Option<Point> {
1070    let label_radius = match position {
1071        TickLabelPosition::High => radius + 18.0,
1072        TickLabelPosition::Low => (radius - 10.0).max(0.0),
1073        TickLabelPosition::NextTo => radius + 10.0,
1074        TickLabelPosition::None => return None,
1075    };
1076    let label = radial_point(center, label_radius, index, count);
1077    let mut origin = Point {
1078        x: label.x - 4.0,
1079        y: label.y + 3.0,
1080    };
1081    if position == TickLabelPosition::High {
1082        origin.x = origin.x.clamp(
1083            bounds.x - PLOT_MARGIN_LEFT,
1084            bounds.x + bounds.width + PLOT_MARGIN_RIGHT,
1085        );
1086        origin.y = origin.y.clamp(
1087            bounds.y - PLOT_MARGIN_TOP,
1088            bounds.y + bounds.height + PLOT_MARGIN_BOTTOM,
1089        );
1090    }
1091    Some(origin)
1092}
1093
1094fn radar_value_tick(
1095    elements: &mut Vec<PositionedElement>,
1096    center: Point,
1097    distance: f64,
1098    mark: TickMark,
1099) {
1100    let (left, right) = match mark {
1101        TickMark::None => return,
1102        TickMark::Inside => (4.0, 0.0),
1103        TickMark::Outside => (0.0, 4.0),
1104        TickMark::Cross => (2.0, 2.0),
1105    };
1106    let y = center.y - distance;
1107    elements.push(stroked_segment(
1108        Point {
1109            x: center.x - left,
1110            y,
1111        },
1112        Point {
1113            x: center.x + right,
1114            y,
1115        },
1116        Color::BLACK,
1117        1.0,
1118    ));
1119}
1120
1121fn radar_category_tick(
1122    elements: &mut Vec<PositionedElement>,
1123    center: Point,
1124    radius: f64,
1125    index: usize,
1126    count: usize,
1127    mark: TickMark,
1128) {
1129    let (inside, outside) = match mark {
1130        TickMark::None => return,
1131        TickMark::Inside => (4.0, 0.0),
1132        TickMark::Outside => (0.0, 4.0),
1133        TickMark::Cross => (2.0, 2.0),
1134    };
1135    elements.push(stroked_segment(
1136        radial_point(center, radius - inside, index, count),
1137        radial_point(center, radius + outside, index, count),
1138        Color::BLACK,
1139        1.0,
1140    ));
1141}
1142
1143fn axis_extent(plot: &Plot, axis: &Axis, blanks: DispBlanksAs) -> Result<Domain> {
1144    if let Plot::Scatter { axis_ids, .. } = plot {
1145        let (x, y) = scatter_domains(plot, blanks)?;
1146        return Ok(if axis.id == axis_ids[0] { x } else { y });
1147    }
1148    plot_value_domain(plot, blanks)?.ok_or_else(|| ChartError::InvalidValue {
1149        element: "chart axis".to_owned(),
1150        value: "axis-free plot supplied an axis".to_owned(),
1151    })
1152}
1153
1154fn axis_tick_coordinate(
1155    value: f64,
1156    domain: Domain,
1157    bounds: Rect,
1158    position: AxisPosition,
1159    orientation: Orientation,
1160) -> Result<f64> {
1161    if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
1162        map_x_oriented(value, domain, bounds, orientation)
1163    } else {
1164        map_y_oriented(value, domain, bounds, orientation)
1165    }
1166}
1167
1168fn axis_line(elements: &mut Vec<PositionedElement>, bounds: Rect, position: AxisPosition) {
1169    let (start, end) = match position {
1170        AxisPosition::Bottom => (
1171            Point {
1172                x: bounds.x,
1173                y: bounds.y + bounds.height,
1174            },
1175            Point {
1176                x: bounds.x + bounds.width,
1177                y: bounds.y + bounds.height,
1178            },
1179        ),
1180        AxisPosition::Top => (
1181            Point {
1182                x: bounds.x,
1183                y: bounds.y,
1184            },
1185            Point {
1186                x: bounds.x + bounds.width,
1187                y: bounds.y,
1188            },
1189        ),
1190        AxisPosition::Left => (
1191            Point {
1192                x: bounds.x,
1193                y: bounds.y,
1194            },
1195            Point {
1196                x: bounds.x,
1197                y: bounds.y + bounds.height,
1198            },
1199        ),
1200        AxisPosition::Right => (
1201            Point {
1202                x: bounds.x + bounds.width,
1203                y: bounds.y,
1204            },
1205            Point {
1206                x: bounds.x + bounds.width,
1207                y: bounds.y + bounds.height,
1208            },
1209        ),
1210    };
1211    elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
1212}
1213
1214fn major_gridline(
1215    elements: &mut Vec<PositionedElement>,
1216    bounds: Rect,
1217    position: AxisPosition,
1218    coordinate: f64,
1219) {
1220    let (start, end) = if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
1221        (
1222            Point {
1223                x: coordinate,
1224                y: bounds.y,
1225            },
1226            Point {
1227                x: coordinate,
1228                y: bounds.y + bounds.height,
1229            },
1230        )
1231    } else {
1232        (
1233            Point {
1234                x: bounds.x,
1235                y: coordinate,
1236            },
1237            Point {
1238                x: bounds.x + bounds.width,
1239                y: coordinate,
1240            },
1241        )
1242    };
1243    elements.push(stroked_segment(
1244        start,
1245        end,
1246        Color {
1247            r: 0.82,
1248            g: 0.82,
1249            b: 0.82,
1250            a: 1.0,
1251        },
1252        0.5,
1253    ));
1254}
1255
1256fn axis_tick(
1257    elements: &mut Vec<PositionedElement>,
1258    bounds: Rect,
1259    position: AxisPosition,
1260    coordinate: f64,
1261    mark: TickMark,
1262) {
1263    let (inside, outside) = match mark {
1264        TickMark::None => return,
1265        TickMark::Inside => (4.0, 0.0),
1266        TickMark::Outside => (0.0, 4.0),
1267        TickMark::Cross => (2.0, 2.0),
1268    };
1269    let (start, end) = match position {
1270        AxisPosition::Bottom => (
1271            Point {
1272                x: coordinate,
1273                y: bounds.y + bounds.height - inside,
1274            },
1275            Point {
1276                x: coordinate,
1277                y: bounds.y + bounds.height + outside,
1278            },
1279        ),
1280        AxisPosition::Top => (
1281            Point {
1282                x: coordinate,
1283                y: bounds.y + inside,
1284            },
1285            Point {
1286                x: coordinate,
1287                y: bounds.y - outside,
1288            },
1289        ),
1290        AxisPosition::Left => (
1291            Point {
1292                x: bounds.x + inside,
1293                y: coordinate,
1294            },
1295            Point {
1296                x: bounds.x - outside,
1297                y: coordinate,
1298            },
1299        ),
1300        AxisPosition::Right => (
1301            Point {
1302                x: bounds.x + bounds.width - inside,
1303                y: coordinate,
1304            },
1305            Point {
1306                x: bounds.x + bounds.width + outside,
1307                y: coordinate,
1308            },
1309        ),
1310    };
1311    elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
1312}
1313
1314fn tick_label_origin(
1315    bounds: Rect,
1316    axis_position: AxisPosition,
1317    label_position: TickLabelPosition,
1318    coordinate: f64,
1319) -> Point {
1320    let side = match label_position {
1321        TickLabelPosition::NextTo => axis_position,
1322        TickLabelPosition::High => {
1323            if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
1324                AxisPosition::Top
1325            } else {
1326                AxisPosition::Right
1327            }
1328        }
1329        TickLabelPosition::Low => {
1330            if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
1331                AxisPosition::Bottom
1332            } else {
1333                AxisPosition::Left
1334            }
1335        }
1336        TickLabelPosition::None => axis_position,
1337    };
1338    match side {
1339        AxisPosition::Bottom => Point {
1340            x: coordinate - 4.0,
1341            y: bounds.y + bounds.height + 12.0,
1342        },
1343        AxisPosition::Top => Point {
1344            x: coordinate - 4.0,
1345            y: bounds.y - 4.0,
1346        },
1347        AxisPosition::Left => Point {
1348            x: bounds.x - 30.0,
1349            y: coordinate + 3.0,
1350        },
1351        AxisPosition::Right => Point {
1352            x: bounds.x + bounds.width + 4.0,
1353            y: coordinate + 3.0,
1354        },
1355    }
1356}
1357
1358fn format_axis_value(format: Option<&NumberFormat>, value: f64) -> Result<String> {
1359    if let Some(format) = format {
1360        format.format_value(value)
1361    } else {
1362        NumberFormat::new("General".to_owned(), false)?.format_value(value)
1363    }
1364}
1365
1366fn shape_label(fonts: &mut FontManager, text: &str, origin: Point) -> Result<GlyphRun> {
1367    shape_label_with_properties(fonts, text, origin, None)
1368}
1369
1370fn shape_label_with_properties(
1371    fonts: &mut FontManager,
1372    text: &str,
1373    origin: Point,
1374    properties: Option<&CT_TextBody>,
1375) -> Result<GlyphRun> {
1376    const FAMILY: &str = "Carlito";
1377    const SIZE: f64 = 9.0;
1378    let properties = properties.and_then(chart_text_properties);
1379    let family = properties
1380        .and_then(|properties| properties.latin.as_ref())
1381        .map(|font| font.typeface.as_str())
1382        .unwrap_or(FAMILY);
1383    let size = properties
1384        .and_then(|properties| properties.font_size)
1385        .map(|size| f64::from(size) / 100.0)
1386        .unwrap_or(SIZE);
1387    let bold = properties
1388        .and_then(|properties| properties.bold)
1389        .unwrap_or(false);
1390    let italic = properties
1391        .and_then(|properties| properties.italic)
1392        .unwrap_or(false);
1393    let font_id = fonts
1394        .resolve_font_for_text(Some(family), bold, italic, text)
1395        .map_err(|error| ChartError::InvalidValue {
1396            element: "chart label font".to_owned(),
1397            value: error.to_string(),
1398        })?;
1399    let shaped =
1400        fonts
1401            .shape_text(font_id, text, size)
1402            .map_err(|error| ChartError::InvalidValue {
1403                element: "chart label shaping".to_owned(),
1404                value: error.to_string(),
1405            })?;
1406    Ok(GlyphRun {
1407        origin,
1408        font_id,
1409        font_size: size,
1410        glyph_ids: shaped.glyph_ids,
1411        advances: shaped.advances,
1412        text: text.to_owned(),
1413        source: None,
1414        color: Color::BLACK,
1415        bold,
1416        italic,
1417        field_kind: None,
1418        field_source: None,
1419        note: None,
1420        tab_aligned: None,
1421    })
1422}
1423
1424fn chart_text_properties(body: &CT_TextBody) -> Option<&CT_TextCharacterProperties> {
1425    body.paragraphs()
1426        .first()
1427        .and_then(|paragraph| paragraph.properties.as_ref())
1428        .and_then(|properties| properties.default_run_properties.as_ref())
1429}
1430
1431fn stroked_segment(start: Point, end: Point, color: Color, width: f64) -> PositionedElement {
1432    PositionedElement::Path(PathElement {
1433        path: Path {
1434            commands: vec![PathCommand::MoveTo(start), PathCommand::LineTo(end)],
1435            fill_rule: FillRule::NonZero,
1436        },
1437        fill: None,
1438        stroke: Some(Stroke::new(Paint::Solid(color), width)),
1439    })
1440}
1441
1442fn plot_series(plot: &Plot) -> &[Series] {
1443    match plot {
1444        Plot::Bar { series, .. }
1445        | Plot::Line { series, .. }
1446        | Plot::Pie { series, .. }
1447        | Plot::Doughnut { series, .. }
1448        | Plot::Area { series, .. }
1449        | Plot::Scatter { series, .. }
1450        | Plot::Radar { series, .. } => series,
1451    }
1452}
1453
1454fn plot_data_labels(plot: &Plot) -> Option<&CT_DLbls> {
1455    match plot {
1456        Plot::Bar { data_labels, .. }
1457        | Plot::Line { data_labels, .. }
1458        | Plot::Pie { data_labels, .. }
1459        | Plot::Doughnut { data_labels, .. }
1460        | Plot::Area { data_labels, .. }
1461        | Plot::Scatter { data_labels, .. }
1462        | Plot::Radar { data_labels, .. } => data_labels.as_ref(),
1463    }
1464}
1465
1466fn series_categories(
1467    series: &Series,
1468    number_format: Option<&NumberFormat>,
1469) -> Result<Vec<(usize, String)>> {
1470    let Some(categories) = &series.categories else {
1471        return Ok(Vec::new());
1472    };
1473    match categories {
1474        AxisData::String(data) => {
1475            let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
1476            indexes
1477                .into_iter()
1478                .zip(&data.values)
1479                .map(|(index, value)| {
1480                    usize::try_from(index)
1481                        .map(|index| (index, value.clone()))
1482                        .map_err(|_| ChartError::InvalidValue {
1483                            element: "c:cat/c:pt/@idx".to_owned(),
1484                            value: index.to_string(),
1485                        })
1486                })
1487                .collect()
1488        }
1489        AxisData::Numeric(data) => {
1490            let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
1491            let cache_format;
1492            let format = if let Some(format) = number_format {
1493                format
1494            } else {
1495                cache_format = NumberFormat::new(data.format_code.clone(), false)?;
1496                &cache_format
1497            };
1498            indexes
1499                .into_iter()
1500                .zip(&data.values)
1501                .map(|(index, value)| {
1502                    let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
1503                        element: "c:cat/c:pt/@idx".to_owned(),
1504                        value: index.to_string(),
1505                    })?;
1506                    let text = format.format_value(*value)?;
1507                    Ok((index, text))
1508                })
1509                .collect()
1510        }
1511    }
1512}
1513
1514fn series_name(series: &Series, index: usize) -> String {
1515    series
1516        .name
1517        .as_ref()
1518        .and_then(|name| name.values.first())
1519        .cloned()
1520        .unwrap_or_else(|| format!("Series {}", index + 1))
1521}
1522
1523fn render_data_labels(
1524    plot: &Plot,
1525    bounds: Rect,
1526    blanks: DispBlanksAs,
1527    options: GeometryOptions,
1528    fonts: &mut FontManager,
1529    elements: &mut Vec<PositionedElement>,
1530) -> Result<()> {
1531    let plot_labels = plot_data_labels(plot);
1532    let anchors = plot_label_anchors(plot, bounds, blanks, options)?;
1533    for (series_index, series) in plot_series(plot).iter().enumerate() {
1534        let Some(labels) = series.data_labels.as_ref().or(plot_labels) else {
1535            continue;
1536        };
1537        let overrides = point_label_overrides(labels)?;
1538        let mut categories = None;
1539        let mut bubble_sizes = None;
1540        let (_, values) = logical_numeric_values(&series.values)?;
1541        let mut percentage_total = None;
1542        for (logical_index, value) in values {
1543            let point_override = overrides.get(&logical_index);
1544            if point_override.is_some_and(|point| point.deleted == Some(true)) {
1545                continue;
1546            }
1547            let effective_labels = point_override
1548                .map(|point| point.apply_to(labels))
1549                .unwrap_or_else(|| labels.clone());
1550            let labels = &effective_labels;
1551            let Some(anchor) = anchors.get(&(series_index, logical_index)).copied() else {
1552                continue;
1553            };
1554            let mut fields = Vec::new();
1555            if labels.show_series_name {
1556                fields.push(series_name(series, series_index));
1557            }
1558            if labels.show_category_name {
1559                if categories.is_none() {
1560                    categories = Some(
1561                        series_categories(series, None)?
1562                            .into_iter()
1563                            .collect::<BTreeMap<_, _>>(),
1564                    );
1565                }
1566                if let Some(category) = categories
1567                    .as_ref()
1568                    .and_then(|categories| categories.get(&logical_index))
1569                {
1570                    fields.push(category.clone());
1571                }
1572            }
1573            if labels.show_value {
1574                fields.push(format_axis_value(labels.number_format.as_ref(), value)?);
1575            }
1576            if labels.show_percent {
1577                let total = if let Some(total) = percentage_total {
1578                    total
1579                } else {
1580                    let total = series
1581                        .values
1582                        .values
1583                        .iter()
1584                        .copied()
1585                        .filter(|value| *value > 0.0)
1586                        .try_fold(0.0, |total, value| {
1587                            checked_geometry_sum(total, value, "data-label percentage total")
1588                        })?;
1589                    percentage_total = Some(total);
1590                    total
1591                };
1592                if total > 0.0 {
1593                    let text = if let Some(format) = &labels.number_format {
1594                        format.format_value(value / total)?
1595                    } else {
1596                        NumberFormat::new("0%".to_owned(), false)?.format_value(value / total)?
1597                    };
1598                    fields.push(text);
1599                }
1600            }
1601            if labels.show_bubble_size {
1602                if bubble_sizes.is_none() {
1603                    bubble_sizes = Some(
1604                        series
1605                            .bubble_size
1606                            .as_ref()
1607                            .map(logical_numeric_values)
1608                            .transpose()?
1609                            .map(|(_, values)| values.into_iter().collect::<BTreeMap<_, _>>())
1610                            .unwrap_or_default(),
1611                    );
1612                }
1613                if let Some(value) = bubble_sizes
1614                    .as_ref()
1615                    .and_then(|sizes| sizes.get(&logical_index))
1616                {
1617                    fields.push(format_axis_value(labels.number_format.as_ref(), *value)?);
1618                }
1619            }
1620            if fields.is_empty() {
1621                continue;
1622            }
1623            let separator = labels.separator.as_deref().unwrap_or(", ");
1624            let text = fields.join(separator);
1625            let origin = anchor.origin(labels.position);
1626            elements.push(PositionedElement::Text(shape_label(fonts, &text, origin)?));
1627        }
1628    }
1629    Ok(())
1630}
1631
1632#[derive(Clone, Debug, Default)]
1633struct PointLabelOverride {
1634    index: Option<usize>,
1635    deleted: Option<bool>,
1636    number_format: Option<NumberFormat>,
1637    position: Option<DataLabelPosition>,
1638    separator: Option<String>,
1639    show_legend_key: Option<bool>,
1640    show_value: Option<bool>,
1641    show_category_name: Option<bool>,
1642    show_series_name: Option<bool>,
1643    show_percent: Option<bool>,
1644    show_bubble_size: Option<bool>,
1645}
1646
1647impl PointLabelOverride {
1648    fn apply_to(&self, labels: &CT_DLbls) -> CT_DLbls {
1649        let mut effective = labels.clone();
1650        if let Some(value) = &self.number_format {
1651            effective.number_format = Some(value.clone());
1652        }
1653        if let Some(value) = self.position {
1654            effective.position = Some(value);
1655        }
1656        if let Some(value) = &self.separator {
1657            effective.separator = Some(value.clone());
1658        }
1659        if let Some(value) = self.show_legend_key {
1660            effective.show_legend_key = value;
1661        }
1662        if let Some(value) = self.show_value {
1663            effective.show_value = value;
1664        }
1665        if let Some(value) = self.show_category_name {
1666            effective.show_category_name = value;
1667        }
1668        if let Some(value) = self.show_series_name {
1669            effective.show_series_name = value;
1670        }
1671        if let Some(value) = self.show_percent {
1672            effective.show_percent = value;
1673        }
1674        if let Some(value) = self.show_bubble_size {
1675            effective.show_bubble_size = value;
1676        }
1677        effective
1678    }
1679}
1680
1681fn point_label_overrides(labels: &CT_DLbls) -> Result<BTreeMap<usize, PointLabelOverride>> {
1682    let mut namespaces = chart_namespace_defaults();
1683    for (name, namespace) in &labels.namespace_declarations {
1684        let prefix = if name == "xmlns" {
1685            Vec::new()
1686        } else if let Some(prefix) = name.strip_prefix("xmlns:") {
1687            prefix.as_bytes().to_vec()
1688        } else {
1689            continue;
1690        };
1691        upsert_namespace_binding(&mut namespaces, prefix, namespace.clone());
1692    }
1693    let mut overrides = BTreeMap::new();
1694    for raw in labels.raw_children.at(0) {
1695        if chart_root_local(raw, &namespaces)?.as_deref() != Some(b"dLbl") {
1696            continue;
1697        }
1698        let point = parse_point_label_override(raw, &namespaces)?;
1699        let index = point
1700            .index
1701            .ok_or_else(|| ChartError::MissingElement("c:dLbl/c:idx".to_owned()))?;
1702        if overrides.insert(index, point).is_some() {
1703            return Err(ChartError::DuplicateElement(format!(
1704                "c:dLbl[c:idx={index}]"
1705            )));
1706        }
1707    }
1708    Ok(overrides)
1709}
1710
1711fn parse_point_label_override(
1712    xml: &[u8],
1713    inherited: &NamespaceBindings,
1714) -> Result<PointLabelOverride> {
1715    let mut reader = Reader::from_reader(xml);
1716    let mut buffer = Vec::new();
1717    let mut namespaces = None;
1718    let mut state = PointLabelOverride::default();
1719    let mut order = 0;
1720    loop {
1721        match reader
1722            .read_event_into(&mut buffer)
1723            .map_err(OxmlError::from)?
1724        {
1725            Event::Start(element) if namespaces.is_none() => {
1726                if !matches_local_name(element.name().as_ref(), b"dLbl")
1727                    || !element_is_in_namespace(&element, C_NS, inherited)?
1728                {
1729                    return Err(ChartError::UnexpectedElement(element_name(&element)));
1730                }
1731                namespaces = Some(chart_bindings(inherited, &element)?);
1732            }
1733            Event::Start(element) => {
1734                let bindings = namespaces
1735                    .as_ref()
1736                    .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
1737                let name = chart_child_local(&element, bindings)?;
1738                let raw = capture_element(&mut reader, &element)?;
1739                parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
1740            }
1741            Event::Empty(element) => {
1742                let bindings = namespaces
1743                    .as_ref()
1744                    .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
1745                let name = chart_child_local(&element, bindings)?;
1746                let raw = capture_empty_element(&element)?;
1747                parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
1748            }
1749            Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbl") => {
1750                if state.index.is_none() {
1751                    return Err(ChartError::MissingElement("c:dLbl/c:idx".to_owned()));
1752                }
1753                return Ok(state);
1754            }
1755            Event::Eof => return Err(missing_end("c:dLbl")),
1756            _ => {}
1757        }
1758        buffer.clear();
1759    }
1760}
1761
1762fn parse_point_label_child(
1763    state: &mut PointLabelOverride,
1764    name: Option<&[u8]>,
1765    raw: &[u8],
1766    namespaces: &NamespaceBindings,
1767    order: &mut usize,
1768) -> Result<()> {
1769    let Some(name) = name else {
1770        return Ok(());
1771    };
1772    let rank = match name {
1773        b"idx" => 0,
1774        b"delete" => 1,
1775        b"layout" => 2,
1776        b"tx" => 3,
1777        b"numFmt" => 4,
1778        b"spPr" => 5,
1779        b"txPr" => 6,
1780        b"dLblPos" => 7,
1781        b"showLegendKey" => 8,
1782        b"showVal" => 9,
1783        b"showCatName" => 10,
1784        b"showSerName" => 11,
1785        b"showPercent" => 12,
1786        b"showBubbleSize" => 13,
1787        b"separator" => 14,
1788        b"showLeaderLines" => 15,
1789        b"leaderLines" => 16,
1790        b"extLst" => 17,
1791        _ => return Ok(()),
1792    };
1793    if rank < *order {
1794        return Err(ChartError::InvalidValue {
1795            element: "c:dLbl".to_owned(),
1796            value: format!("c:{} is out of schema order", String::from_utf8_lossy(name)),
1797        });
1798    }
1799    *order = rank;
1800    match name {
1801        b"idx" => {
1802            let (index, _) = parse_u32_scalar(raw, "idx")?;
1803            let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
1804                element: "c:dLbl/c:idx".to_owned(),
1805                value: index.to_string(),
1806            })?;
1807            set_once(&mut state.index, index, "c:dLbl/c:idx")?;
1808        }
1809        b"delete" => set_once(
1810            &mut state.deleted,
1811            parse_bool_value(raw, "delete")?.0,
1812            "c:dLbl/c:delete",
1813        )?,
1814        b"numFmt" => set_once(
1815            &mut state.number_format,
1816            NumberFormat::from_xml(raw)?,
1817            "c:dLbl/c:numFmt",
1818        )?,
1819        b"dLblPos" => {
1820            let (value, _) = scalar_value(raw, "dLblPos")?;
1821            let value =
1822                value.ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
1823            let position = DataLabelPosition::parse(&value)
1824                .ok_or_else(|| invalid_attribute("dLblPos", "val", value))?;
1825            set_once(&mut state.position, position, "c:dLbl/c:dLblPos")?;
1826        }
1827        b"showLegendKey" => set_once(
1828            &mut state.show_legend_key,
1829            parse_bool_value(raw, "showLegendKey")?.0,
1830            "c:dLbl/c:showLegendKey",
1831        )?,
1832        b"showVal" => set_once(
1833            &mut state.show_value,
1834            parse_bool_value(raw, "showVal")?.0,
1835            "c:dLbl/c:showVal",
1836        )?,
1837        b"showCatName" => set_once(
1838            &mut state.show_category_name,
1839            parse_bool_value(raw, "showCatName")?.0,
1840            "c:dLbl/c:showCatName",
1841        )?,
1842        b"showSerName" => set_once(
1843            &mut state.show_series_name,
1844            parse_bool_value(raw, "showSerName")?.0,
1845            "c:dLbl/c:showSerName",
1846        )?,
1847        b"showPercent" => set_once(
1848            &mut state.show_percent,
1849            parse_bool_value(raw, "showPercent")?.0,
1850            "c:dLbl/c:showPercent",
1851        )?,
1852        b"showBubbleSize" => set_once(
1853            &mut state.show_bubble_size,
1854            parse_bool_value(raw, "showBubbleSize")?.0,
1855            "c:dLbl/c:showBubbleSize",
1856        )?,
1857        b"separator" => set_once(
1858            &mut state.separator,
1859            parse_text_element(raw, b"separator", namespaces)?.0,
1860            "c:dLbl/c:separator",
1861        )?,
1862        _ => {}
1863    }
1864    Ok(())
1865}
1866
1867#[derive(Clone, Copy, Debug)]
1868struct LabelAnchor {
1869    center: Point,
1870    base: Point,
1871    end: Point,
1872    direction: Point,
1873}
1874
1875impl LabelAnchor {
1876    const fn point(center: Point) -> Self {
1877        Self {
1878            center,
1879            base: center,
1880            end: center,
1881            direction: Point { x: 0.0, y: 0.0 },
1882        }
1883    }
1884
1885    const fn segment(center: Point, base: Point, end: Point) -> Self {
1886        Self {
1887            center,
1888            base,
1889            end,
1890            direction: Point {
1891                x: end.x - base.x,
1892                y: end.y - base.y,
1893            },
1894        }
1895    }
1896
1897    const fn directed_segment(center: Point, base: Point, end: Point, direction: Point) -> Self {
1898        Self {
1899            center,
1900            base,
1901            end,
1902            direction,
1903        }
1904    }
1905
1906    fn origin(self, position: Option<DataLabelPosition>) -> Point {
1907        let mut origin = self.center;
1908        match position.unwrap_or(DataLabelPosition::BestFit) {
1909            DataLabelPosition::Bottom => origin.y += 8.0,
1910            DataLabelPosition::Top => origin.y -= 5.0,
1911            DataLabelPosition::Left => origin.x -= 8.0,
1912            DataLabelPosition::Right => origin.x += 8.0,
1913            DataLabelPosition::InsideBase => {
1914                origin = self.along_segment(self.base, 5.0, true).unwrap_or(Point {
1915                    x: origin.x,
1916                    y: origin.y + 8.0,
1917                });
1918            }
1919            DataLabelPosition::InsideEnd => {
1920                origin = self.along_segment(self.end, -5.0, true).unwrap_or(origin);
1921            }
1922            DataLabelPosition::OutsideEnd => {
1923                origin = self.along_segment(self.end, 5.0, false).unwrap_or(Point {
1924                    x: origin.x,
1925                    y: origin.y - 5.0,
1926                });
1927            }
1928            DataLabelPosition::BestFit | DataLabelPosition::Center => {}
1929        }
1930        origin
1931    }
1932
1933    fn along_segment(self, origin: Point, distance: f64, clamp_inside: bool) -> Option<Point> {
1934        let dx = self.end.x - self.base.x;
1935        let dy = self.end.y - self.base.y;
1936        let length = dx.hypot(dy);
1937        let (dx, dy, direction_length) = if length.is_finite() && length > 0.0 {
1938            (dx, dy, length)
1939        } else {
1940            let direction_length = self.direction.x.hypot(self.direction.y);
1941            if !direction_length.is_finite() || direction_length == 0.0 {
1942                return None;
1943            }
1944            (self.direction.x, self.direction.y, direction_length)
1945        };
1946        let distance = if clamp_inside {
1947            distance.clamp(-length / 2.0, length / 2.0)
1948        } else {
1949            distance
1950        };
1951        Some(Point {
1952            x: origin.x + dx / direction_length * distance,
1953            y: origin.y + dy / direction_length * distance,
1954        })
1955    }
1956}
1957
1958fn plot_label_anchors(
1959    plot: &Plot,
1960    bounds: Rect,
1961    blanks: DispBlanksAs,
1962    options: GeometryOptions,
1963) -> Result<BTreeMap<(usize, usize), LabelAnchor>> {
1964    let mut anchors = BTreeMap::new();
1965    match plot {
1966        Plot::Bar {
1967            direction,
1968            grouping,
1969            gap_width,
1970            overlap,
1971            series,
1972            ..
1973        } => {
1974            for bar in bar_rectangles(
1975                *direction, *grouping, *gap_width, *overlap, series, bounds, options,
1976            )? {
1977                if !point_in_rect(bar.end, bounds) {
1978                    continue;
1979                }
1980                let visible_base = Point {
1981                    x: bar.base.x.clamp(bounds.x, bounds.x + bounds.width),
1982                    y: bar.base.y.clamp(bounds.y, bounds.y + bounds.height),
1983                };
1984                let visible_end = Point {
1985                    x: bar.end.x.clamp(bounds.x, bounds.x + bounds.width),
1986                    y: bar.end.y.clamp(bounds.y, bounds.y + bounds.height),
1987                };
1988                anchors.insert(
1989                    (bar.series_index, bar.logical_index),
1990                    LabelAnchor::directed_segment(
1991                        Point {
1992                            x: (visible_base.x + visible_end.x) / 2.0,
1993                            y: (visible_base.y + visible_end.y) / 2.0,
1994                        },
1995                        visible_base,
1996                        visible_end,
1997                        Point {
1998                            x: bar.end.x - bar.base.x,
1999                            y: bar.end.y - bar.base.y,
2000                        },
2001                    ),
2002                );
2003            }
2004        }
2005        Plot::Line {
2006            grouping, series, ..
2007        }
2008        | Plot::Area {
2009            grouping, series, ..
2010        } => {
2011            for (series_index, points) in
2012                category_label_points(*grouping, series, bounds, blanks, options)?
2013                    .into_iter()
2014                    .enumerate()
2015            {
2016                for (logical_index, point) in points {
2017                    anchors.insert((series_index, logical_index), LabelAnchor::point(point));
2018                }
2019            }
2020        }
2021        Plot::Scatter { series, .. } => {
2022            let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
2023            let x_mapping = options.scatter_x.unwrap_or(AxisMapping {
2024                domain: x_domain,
2025                orientation: Orientation::MinMax,
2026            });
2027            let y_mapping = options.scatter_y.unwrap_or(AxisMapping {
2028                domain: y_domain,
2029                orientation: Orientation::MinMax,
2030            });
2031            for (series_index, series) in series.iter().enumerate() {
2032                let indexed = matched_scatter_points(series)?;
2033                let points = scatter_points(
2034                    &indexed,
2035                    x_mapping.domain,
2036                    y_mapping.domain,
2037                    bounds,
2038                    x_mapping.orientation,
2039                    y_mapping.orientation,
2040                )?;
2041                for ((logical_index, _, _), point) in indexed.into_iter().zip(points) {
2042                    anchors.insert((series_index, logical_index), LabelAnchor::point(point));
2043                }
2044            }
2045        }
2046        Plot::Radar { series, .. } => {
2047            let center = Point {
2048                x: bounds.x + bounds.width / 2.0,
2049                y: bounds.y + bounds.height / 2.0,
2050            };
2051            let category_count = series_category_count(series)?;
2052            for (series_index, points) in radar_series_points(series, bounds, options)?
2053                .into_iter()
2054                .enumerate()
2055            {
2056                for (logical_index, point) in points {
2057                    let spoke = radial_point(
2058                        center,
2059                        1.0,
2060                        oriented_category_index(
2061                            logical_index,
2062                            category_count,
2063                            options.category_orientation,
2064                        ),
2065                        category_count,
2066                    );
2067                    anchors.insert(
2068                        (series_index, logical_index),
2069                        LabelAnchor::directed_segment(
2070                            point,
2071                            center,
2072                            point,
2073                            Point {
2074                                x: spoke.x - center.x,
2075                                y: spoke.y - center.y,
2076                            },
2077                        ),
2078                    );
2079                }
2080            }
2081        }
2082        Plot::Pie {
2083            first_slice_angle,
2084            series,
2085            ..
2086        } => {
2087            for slice in pie_slices(*first_slice_angle, None, series, bounds)? {
2088                anchors.insert(
2089                    (slice.series_index, slice.logical_index),
2090                    LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2091                );
2092            }
2093        }
2094        Plot::Doughnut {
2095            first_slice_angle,
2096            hole_size,
2097            series,
2098            ..
2099        } => {
2100            for slice in pie_slices(*first_slice_angle, Some(*hole_size), series, bounds)? {
2101                anchors.insert(
2102                    (slice.series_index, slice.logical_index),
2103                    LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2104                );
2105            }
2106        }
2107    }
2108    anchors.retain(|_, anchor| point_in_rect(anchor.end, bounds));
2109    Ok(anchors)
2110}
2111
2112fn category_label_points(
2113    grouping: Grouping,
2114    series: &[Series],
2115    bounds: Rect,
2116    blanks: DispBlanksAs,
2117    options: GeometryOptions,
2118) -> Result<Vec<Vec<(usize, Point)>>> {
2119    validate_series_values(series)?;
2120    let logical_series = logical_series_values(series)?;
2121    let category_count = series_category_count(series)?;
2122    let calculated_series = if blanks == DispBlanksAs::Zero {
2123        zero_filled_series(&logical_series, category_count)
2124    } else {
2125        logical_series.clone()
2126    };
2127    let layers = stacked_series_bounds(
2128        &calculated_series,
2129        grouping != Grouping::Standard,
2130        grouping == Grouping::PercentStacked,
2131    )?;
2132    let domain = options
2133        .value
2134        .map(|mapping| mapping.domain)
2135        .unwrap_or(domain_from_layers(&layers)?);
2136    let value_orientation = options
2137        .value
2138        .map(|mapping| mapping.orientation)
2139        .unwrap_or(Orientation::MinMax);
2140    let mut point_series = Vec::with_capacity(layers.len());
2141    for (series_index, layer) in layers.iter().enumerate() {
2142        let present: BTreeSet<_> = logical_series[series_index]
2143            .1
2144            .iter()
2145            .map(|(index, _)| *index)
2146            .collect();
2147        let indexed: Vec<_> = layer
2148            .iter()
2149            .filter(|(index, _, _)| present.contains(index))
2150            .map(|(index, _, upper)| (*index, *upper))
2151            .collect();
2152        let points = category_points(
2153            &indexed,
2154            category_count,
2155            domain,
2156            bounds,
2157            options.category_orientation,
2158            value_orientation,
2159        )?;
2160        point_series.push(
2161            indexed
2162                .into_iter()
2163                .map(|(index, _)| index)
2164                .zip(points)
2165                .collect(),
2166        );
2167    }
2168    Ok(point_series)
2169}
2170
2171fn point_in_rect(point: Point, bounds: Rect) -> bool {
2172    const EPSILON: f64 = 1.0e-9;
2173    point.x >= bounds.x - EPSILON
2174        && point.x <= bounds.x + bounds.width + EPSILON
2175        && point.y >= bounds.y - EPSILON
2176        && point.y <= bounds.y + bounds.height + EPSILON
2177}
2178
2179fn resolve_series_colours(
2180    plot: &Plot,
2181    theme: &CT_OfficeStyleSheet,
2182    color_map: &ColorMap,
2183) -> Result<Vec<Color>> {
2184    let lookup = theme_colour_lookup(theme);
2185    plot_series(plot)
2186        .iter()
2187        .enumerate()
2188        .map(|(index, series)| {
2189            if let Some(properties) = series.sp_pr.as_ref() {
2190                if let Some(fill) = properties.fill.as_ref() {
2191                    return resolve_series_paint(fill, index, "fill", theme, color_map, &lookup);
2192                }
2193                if let Some(fill) = properties.line.as_ref().and_then(|line| line.fill.as_ref()) {
2194                    return resolve_series_paint(fill, index, "line", theme, color_map, &lookup);
2195                }
2196            }
2197            resolve_series_accent(index, theme, color_map, &lookup)
2198        })
2199        .collect()
2200}
2201
2202fn resolve_series_paint(
2203    fill: &Fill,
2204    series_index: usize,
2205    source: &str,
2206    theme: &CT_OfficeStyleSheet,
2207    color_map: &ColorMap,
2208    lookup: &[(&str, RgbColor)],
2209) -> Result<Color> {
2210    match fill {
2211        Fill::NoFill(_) => Ok(Color {
2212            r: 0.0,
2213            g: 0.0,
2214            b: 0.0,
2215            a: 0.0,
2216        }),
2217        Fill::Solid(solid) => {
2218            let choice = solid
2219                .color
2220                .as_ref()
2221                .ok_or_else(|| ChartError::InvalidValue {
2222                    element: format!("c:ser[{series_index}] direct {source} paint"),
2223                    value: "solid fill has no colour choice".to_owned(),
2224                })?;
2225            resolve_direct_chart_colour(choice, theme, color_map, lookup, || {
2226                format!("c:ser[{series_index}] direct {source} paint")
2227            })
2228        }
2229        Fill::Gradient(_) => unsupported_series_paint(series_index, source, "gradient"),
2230        Fill::Pattern(_) => unsupported_series_paint(series_index, source, "pattern"),
2231        Fill::Blip(_) => unsupported_series_paint(series_index, source, "picture"),
2232    }
2233}
2234
2235fn resolve_direct_chart_colour(
2236    choice: &ColorChoice,
2237    theme: &CT_OfficeStyleSheet,
2238    color_map: &ColorMap,
2239    lookup: &[(&str, RgbColor)],
2240    element: impl FnOnce() -> String,
2241) -> Result<Color> {
2242    let ColorChoice::Scheme {
2243        value, transforms, ..
2244    } = choice
2245    else {
2246        return resolve_chart_colour(choice, color_map, lookup, element);
2247    };
2248    let theme_slot =
2249        mapped_theme_slot(value, color_map).ok_or_else(|| ChartError::InvalidValue {
2250            element: element(),
2251            value: format!("no concrete theme slot is available for scheme colour {value}"),
2252        })?;
2253    let theme_choice = theme.theme_elements.color_scheme.color(theme_slot);
2254    let base = concrete_theme_colour(theme_choice).ok_or_else(|| ChartError::InvalidValue {
2255        element: format!("a:clrScheme/a:{}", theme_slot.as_str()),
2256        value: "selected theme slot has no concrete sRGB or system fallback colour".to_owned(),
2257    })?;
2258    let mut combined = theme_choice.transforms().to_vec();
2259    combined.extend_from_slice(transforms);
2260    Ok(resolved_layout_colour(apply_color_transforms(
2261        base, &combined,
2262    )))
2263}
2264
2265fn mapped_theme_slot(value: &str, color_map: &ColorMap) -> Option<ThemeColorSlot> {
2266    let mapped = match value {
2267        "bg1" => color_map.theme_slot(ColorMapSlot::Background1),
2268        "tx1" => color_map.theme_slot(ColorMapSlot::Text1),
2269        "bg2" => color_map.theme_slot(ColorMapSlot::Background2),
2270        "tx2" => color_map.theme_slot(ColorMapSlot::Text2),
2271        "accent1" => color_map.theme_slot(ColorMapSlot::Accent1),
2272        "accent2" => color_map.theme_slot(ColorMapSlot::Accent2),
2273        "accent3" => color_map.theme_slot(ColorMapSlot::Accent3),
2274        "accent4" => color_map.theme_slot(ColorMapSlot::Accent4),
2275        "accent5" => color_map.theme_slot(ColorMapSlot::Accent5),
2276        "accent6" => color_map.theme_slot(ColorMapSlot::Accent6),
2277        "hlink" => color_map.theme_slot(ColorMapSlot::Hyperlink),
2278        "folHlink" => color_map.theme_slot(ColorMapSlot::FollowedHyperlink),
2279        "dk1" => ThemeColorSlot::Dark1,
2280        "lt1" => ThemeColorSlot::Light1,
2281        "dk2" => ThemeColorSlot::Dark2,
2282        "lt2" => ThemeColorSlot::Light2,
2283        _ => return None,
2284    };
2285    Some(mapped)
2286}
2287
2288fn unsupported_series_paint(series_index: usize, source: &str, kind: &str) -> Result<Color> {
2289    Err(ChartError::InvalidValue {
2290        element: format!("c:ser[{series_index}] direct {source} paint"),
2291        value: format!("unsupported {kind} paint"),
2292    })
2293}
2294
2295fn resolve_series_accent(
2296    series_index: usize,
2297    theme: &CT_OfficeStyleSheet,
2298    color_map: &ColorMap,
2299    lookup: &[(&str, RgbColor)],
2300) -> Result<Color> {
2301    const ACCENTS: [ColorMapSlot; 6] = [
2302        ColorMapSlot::Accent1,
2303        ColorMapSlot::Accent2,
2304        ColorMapSlot::Accent3,
2305        ColorMapSlot::Accent4,
2306        ColorMapSlot::Accent5,
2307        ColorMapSlot::Accent6,
2308    ];
2309    let semantic_slot = ACCENTS[series_index % ACCENTS.len()];
2310    let theme_slot = color_map.theme_slot(semantic_slot);
2311    let choice = theme.theme_elements.color_scheme.color(theme_slot);
2312    resolve_chart_colour(choice, color_map, lookup, || {
2313        format!("c:ser[{series_index}] theme accent")
2314    })
2315}
2316
2317fn resolve_chart_colour(
2318    choice: &ColorChoice,
2319    color_map: &ColorMap,
2320    lookup: &[(&str, RgbColor)],
2321    element: impl FnOnce() -> String,
2322) -> Result<Color> {
2323    resolve_color(choice, color_map, lookup)
2324        .map(resolved_layout_colour)
2325        .map_err(|error| ChartError::InvalidValue {
2326            element: element(),
2327            value: error.to_string(),
2328        })
2329}
2330
2331fn theme_colour_lookup(theme: &CT_OfficeStyleSheet) -> Vec<(&'static str, RgbColor)> {
2332    theme
2333        .theme_elements
2334        .color_scheme
2335        .iter()
2336        .filter_map(|(slot, choice)| {
2337            concrete_theme_colour(choice).map(|colour| (slot.as_str(), colour))
2338        })
2339        .collect()
2340}
2341
2342fn concrete_theme_colour(choice: &ColorChoice) -> Option<RgbColor> {
2343    match choice {
2344        ColorChoice::Srgb { value, .. } => Some(*value),
2345        ColorChoice::System { last_color, .. } => *last_color,
2346        ColorChoice::Scheme { .. } | ColorChoice::Preset { .. } => None,
2347    }
2348}
2349
2350fn resolved_layout_colour(colour: ResolvedColor) -> Color {
2351    Color {
2352        r: f64::from(colour.red) / 255.0,
2353        g: f64::from(colour.green) / 255.0,
2354        b: f64::from(colour.blue) / 255.0,
2355        a: f64::from(colour.alpha) / 255.0,
2356    }
2357}
2358
2359fn render_legend(
2360    chart: &CT_Chart,
2361    plot: &Plot,
2362    bounds: Rect,
2363    fonts: &mut FontManager,
2364    series_colours: &[Color],
2365    paths: &mut Vec<PositionedElement>,
2366    labels: &mut Vec<PositionedElement>,
2367) -> Result<()> {
2368    if chart.legend.is_none() {
2369        return Ok(());
2370    }
2371    let x = bounds.x + bounds.width - 58.0;
2372    for (index, (series, colour)) in plot_series(plot).iter().zip(series_colours).enumerate() {
2373        let y = bounds.y + 6.0 + index as f64 * 13.0;
2374        paths.push(filled_path(
2375            Path::rect(Rect {
2376                x,
2377                y,
2378                width: 7.0,
2379                height: 7.0,
2380            }),
2381            *colour,
2382        ));
2383        labels.push(PositionedElement::Text(shape_label(
2384            fonts,
2385            &series_name(series, index),
2386            Point {
2387                x: x + 10.0,
2388                y: y + 7.0,
2389            },
2390        )?));
2391    }
2392    Ok(())
2393}
2394
2395fn validate_geometry_coordinates(elements: &[PositionedElement]) -> Result<()> {
2396    fn finite_point(point: Point) -> bool {
2397        point.x.is_finite() && point.y.is_finite()
2398    }
2399
2400    for element in elements {
2401        match element {
2402            PositionedElement::Path(path) => {
2403                for command in &path.path.commands {
2404                    let finite = match command {
2405                        PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
2406                            finite_point(*point)
2407                        }
2408                        PathCommand::CurveTo { c1, c2, to } => {
2409                            finite_point(*c1) && finite_point(*c2) && finite_point(*to)
2410                        }
2411                        PathCommand::Close => true,
2412                    };
2413                    if !finite {
2414                        return Err(ChartError::InvalidValue {
2415                            element: "chart geometry coordinate".to_owned(),
2416                            value: "generated path contains a nonfinite point".to_owned(),
2417                        });
2418                    }
2419                }
2420            }
2421            PositionedElement::Group(group) => validate_geometry_coordinates(&group.children)?,
2422            _ => {}
2423        }
2424    }
2425    Ok(())
2426}
2427
2428fn render_plot_geometry(
2429    plot: &Plot,
2430    bounds: Rect,
2431    blanks: DispBlanksAs,
2432    options: GeometryOptions,
2433    series_colours: &[Color],
2434) -> Result<Vec<PositionedElement>> {
2435    match plot {
2436        Plot::Bar {
2437            direction,
2438            grouping,
2439            gap_width,
2440            overlap,
2441            series,
2442            ..
2443        } => render_bar_geometry(
2444            *direction,
2445            *grouping,
2446            *gap_width,
2447            *overlap,
2448            series,
2449            bounds,
2450            options,
2451            series_colours,
2452        ),
2453        Plot::Line {
2454            grouping,
2455            marker,
2456            series,
2457            ..
2458        } => render_line_geometry(
2459            *grouping,
2460            *marker,
2461            series,
2462            bounds,
2463            blanks,
2464            options,
2465            series_colours,
2466        ),
2467        Plot::Pie {
2468            first_slice_angle,
2469            series,
2470            ..
2471        } => render_pie_geometry(*first_slice_angle, None, series, bounds, series_colours),
2472        Plot::Doughnut {
2473            first_slice_angle,
2474            hole_size,
2475            series,
2476            ..
2477        } => render_pie_geometry(
2478            *first_slice_angle,
2479            Some(*hole_size),
2480            series,
2481            bounds,
2482            series_colours,
2483        ),
2484        Plot::Area {
2485            grouping, series, ..
2486        } => render_area_geometry(*grouping, series, bounds, blanks, options, series_colours),
2487        Plot::Scatter { style, series, .. } => {
2488            render_scatter_geometry(*style, series, bounds, blanks, options, series_colours)
2489        }
2490        Plot::Radar { style, series, .. } => {
2491            render_radar_geometry(*style, series, bounds, options, series_colours)
2492        }
2493    }
2494}
2495
2496#[allow(clippy::too_many_arguments)]
2497fn render_bar_geometry(
2498    direction: BarDirection,
2499    grouping: BarGrouping,
2500    gap_width: u16,
2501    overlap: i8,
2502    series: &[Series],
2503    bounds: Rect,
2504    options: GeometryOptions,
2505    series_colours: &[Color],
2506) -> Result<Vec<PositionedElement>> {
2507    let bars = bar_rectangles(
2508        direction, grouping, gap_width, overlap, series, bounds, options,
2509    )?;
2510    bars.into_iter()
2511        .map(|bar| {
2512            Ok(filled_path(
2513                Path::rect(bar.rect),
2514                series_colour(series_colours, bar.series_index)?,
2515            ))
2516        })
2517        .collect()
2518}
2519
2520#[derive(Clone, Copy, Debug)]
2521struct BarRectangle {
2522    series_index: usize,
2523    logical_index: usize,
2524    rect: Rect,
2525    base: Point,
2526    end: Point,
2527}
2528
2529fn bar_rectangles(
2530    direction: BarDirection,
2531    grouping: BarGrouping,
2532    gap_width: u16,
2533    overlap: i8,
2534    series: &[Series],
2535    bounds: Rect,
2536    options: GeometryOptions,
2537) -> Result<Vec<BarRectangle>> {
2538    validate_series_values(series)?;
2539    let logical_series = logical_series_values(series)?;
2540    let category_count = series_category_count(series)?;
2541    let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
2542    let percent = grouping == BarGrouping::PercentStacked;
2543    let layers = stacked_series_bounds(&logical_series, stacked, percent)?;
2544    let domain = options
2545        .value
2546        .map(|mapping| mapping.domain)
2547        .unwrap_or(domain_from_layers(&layers)?);
2548    let value_orientation = options
2549        .value
2550        .map(|mapping| mapping.orientation)
2551        .unwrap_or(Orientation::MinMax);
2552    let category_extent = if direction == BarDirection::Column {
2553        bounds.width
2554    } else {
2555        bounds.height
2556    };
2557    let slot = category_extent / category_count as f64;
2558    let cluster_extent = slot * 100.0 / (100.0 + f64::from(gap_width));
2559    let bars_per_cluster = if stacked { 1 } else { series.len() };
2560    let overlap_fraction = if stacked {
2561        0.0
2562    } else {
2563        f64::from(overlap) / 100.0
2564    };
2565    let divisor =
2566        bars_per_cluster as f64 - (bars_per_cluster.saturating_sub(1)) as f64 * overlap_fraction;
2567    let bar_extent = cluster_extent / divisor;
2568    let advance = bar_extent * (1.0 - overlap_fraction);
2569    let mut bars = Vec::new();
2570    for (series_index, layer) in layers.iter().enumerate() {
2571        for &(logical_index, start, end) in layer {
2572            let category = oriented_category_index(
2573                logical_index,
2574                category_count,
2575                options.category_orientation,
2576            );
2577            let cluster_start = category as f64 / category_count as f64 * category_extent
2578                + (slot - cluster_extent) / 2.0;
2579            let category_start = if stacked {
2580                cluster_start
2581            } else {
2582                cluster_start + series_index as f64 * advance
2583            };
2584            let (rect, base, end) = if direction == BarDirection::Column {
2585                let y0 = map_y_oriented(start, domain, bounds, value_orientation)?;
2586                let y1 = map_y_oriented(end, domain, bounds, value_orientation)?;
2587                let center_x = bounds.x + category_start + bar_extent / 2.0;
2588                (
2589                    Rect {
2590                        x: bounds.x + category_start,
2591                        y: y0.min(y1),
2592                        width: bar_extent,
2593                        height: (y1 - y0).abs(),
2594                    },
2595                    Point { x: center_x, y: y0 },
2596                    Point { x: center_x, y: y1 },
2597                )
2598            } else {
2599                let x0 = map_x_oriented(start, domain, bounds, value_orientation)?;
2600                let x1 = map_x_oriented(end, domain, bounds, value_orientation)?;
2601                let center_y = bounds.y + category_start + bar_extent / 2.0;
2602                (
2603                    Rect {
2604                        x: x0.min(x1),
2605                        y: bounds.y + category_start,
2606                        width: (x1 - x0).abs(),
2607                        height: bar_extent,
2608                    },
2609                    Point { x: x0, y: center_y },
2610                    Point { x: x1, y: center_y },
2611                )
2612            };
2613            bars.push(BarRectangle {
2614                series_index,
2615                logical_index,
2616                rect,
2617                base,
2618                end,
2619            });
2620        }
2621    }
2622    Ok(bars)
2623}
2624
2625fn render_line_geometry(
2626    grouping: Grouping,
2627    marker: bool,
2628    series: &[Series],
2629    bounds: Rect,
2630    blanks: DispBlanksAs,
2631    options: GeometryOptions,
2632    series_colours: &[Color],
2633) -> Result<Vec<PositionedElement>> {
2634    validate_series_values(series)?;
2635    let logical_series = logical_series_values(series)?;
2636    let category_count = series_category_count(series)?;
2637    let calculated_series = if blanks == DispBlanksAs::Zero {
2638        zero_filled_series(&logical_series, category_count)
2639    } else {
2640        logical_series.clone()
2641    };
2642    let stacked = grouping != Grouping::Standard;
2643    let percent = grouping == Grouping::PercentStacked;
2644    let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
2645    let domain = options
2646        .value
2647        .map(|mapping| mapping.domain)
2648        .unwrap_or(domain_from_layers(&layers)?);
2649    let value_orientation = options
2650        .value
2651        .map(|mapping| mapping.orientation)
2652        .unwrap_or(Orientation::MinMax);
2653    let mut elements = Vec::new();
2654    for (series_index, layer) in layers.iter().enumerate() {
2655        let colour = series_colour(series_colours, series_index)?;
2656        let indexed: Vec<_> = layer
2657            .iter()
2658            .map(|(index, _, upper)| (*index, *upper))
2659            .collect();
2660        let indexes: Vec<_> = indexed.iter().map(|(index, _)| *index).collect();
2661        for (start, end) in contiguous_ranges(&indexes, blanks) {
2662            let points = category_points(
2663                &indexed[start..end],
2664                category_count,
2665                domain,
2666                bounds,
2667                options.category_orientation,
2668                value_orientation,
2669            )?;
2670            elements.push(stroked_path(polyline_path(&points, false), colour));
2671        }
2672        if marker {
2673            let present: BTreeSet<_> = logical_series[series_index]
2674                .1
2675                .iter()
2676                .map(|(index, _)| *index)
2677                .collect();
2678            let marker_values: Vec<_> = indexed
2679                .iter()
2680                .copied()
2681                .filter(|(index, _)| present.contains(index))
2682                .collect();
2683            let marker_points = category_points(
2684                &marker_values,
2685                category_count,
2686                domain,
2687                bounds,
2688                options.category_orientation,
2689                value_orientation,
2690            )?;
2691            push_markers(&mut elements, &marker_points, colour);
2692        }
2693    }
2694    Ok(elements)
2695}
2696
2697fn render_area_geometry(
2698    grouping: Grouping,
2699    series: &[Series],
2700    bounds: Rect,
2701    blanks: DispBlanksAs,
2702    options: GeometryOptions,
2703    series_colours: &[Color],
2704) -> Result<Vec<PositionedElement>> {
2705    validate_series_values(series)?;
2706    let logical_series = logical_series_values(series)?;
2707    let category_count = series_category_count(series)?;
2708    let calculated_series = if blanks == DispBlanksAs::Zero {
2709        zero_filled_series(&logical_series, category_count)
2710    } else {
2711        logical_series
2712    };
2713    let stacked = grouping != Grouping::Standard;
2714    let percent = grouping == Grouping::PercentStacked;
2715    let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
2716    let domain = options
2717        .value
2718        .map(|mapping| mapping.domain)
2719        .unwrap_or(domain_from_layers(&layers)?);
2720    let value_orientation = options
2721        .value
2722        .map(|mapping| mapping.orientation)
2723        .unwrap_or(Orientation::MinMax);
2724    let mut elements = Vec::new();
2725    for (series_index, layer) in layers.iter().enumerate() {
2726        let colour = series_colour(series_colours, series_index)?;
2727        let indexes: Vec<_> = layer.iter().map(|(index, _, _)| *index).collect();
2728        for (start, end) in contiguous_ranges(&indexes, blanks) {
2729            let segment = &layer[start..end];
2730            let top_values: Vec<_> = segment
2731                .iter()
2732                .map(|(index, _, upper)| (*index, *upper))
2733                .collect();
2734            let lower_values: Vec<_> = segment
2735                .iter()
2736                .map(|(index, lower, _)| (*index, *lower))
2737                .collect();
2738            let top = category_points(
2739                &top_values,
2740                category_count,
2741                domain,
2742                bounds,
2743                options.category_orientation,
2744                value_orientation,
2745            )?;
2746            let mut bottom = category_points(
2747                &lower_values,
2748                category_count,
2749                domain,
2750                bounds,
2751                options.category_orientation,
2752                value_orientation,
2753            )?;
2754            bottom.reverse();
2755            let mut commands = Vec::with_capacity(top.len() + bottom.len() + 2);
2756            commands.push(PathCommand::MoveTo(top[0]));
2757            commands.extend(top.iter().skip(1).copied().map(PathCommand::LineTo));
2758            commands.extend(bottom.into_iter().map(PathCommand::LineTo));
2759            commands.push(PathCommand::Close);
2760            elements.push(filled_and_stroked_path(
2761                Path {
2762                    commands,
2763                    fill_rule: FillRule::NonZero,
2764                },
2765                colour,
2766            ));
2767        }
2768    }
2769    Ok(elements)
2770}
2771
2772fn render_scatter_geometry(
2773    style: ScatterStyle,
2774    series: &[Series],
2775    bounds: Rect,
2776    blanks: DispBlanksAs,
2777    options: GeometryOptions,
2778    series_colours: &[Color],
2779) -> Result<Vec<PositionedElement>> {
2780    validate_series_values(series)?;
2781    let mut paired_series = Vec::with_capacity(series.len());
2782    let mut marker_series = Vec::with_capacity(series.len());
2783    let mut all_x = Vec::new();
2784    let mut all_y = Vec::new();
2785    for item in series {
2786        let markers = matched_scatter_points(item)?;
2787        let paired = scatter_render_points(item, blanks)?;
2788        all_x.extend(paired.iter().map(|(_, x, _)| *x));
2789        all_y.extend(paired.iter().map(|(_, _, y)| *y));
2790        paired_series.push(paired);
2791        marker_series.push(markers);
2792    }
2793    let x_domain = options
2794        .scatter_x
2795        .map(|mapping| mapping.domain)
2796        .unwrap_or(data_domain(&all_x, false)?);
2797    let y_domain = options
2798        .scatter_y
2799        .map(|mapping| mapping.domain)
2800        .unwrap_or(data_domain(&all_y, false)?);
2801    let x_orientation = options
2802        .scatter_x
2803        .map(|mapping| mapping.orientation)
2804        .unwrap_or(Orientation::MinMax);
2805    let y_orientation = options
2806        .scatter_y
2807        .map(|mapping| mapping.orientation)
2808        .unwrap_or(Orientation::MinMax);
2809    let draw_line = matches!(
2810        style,
2811        ScatterStyle::Line
2812            | ScatterStyle::LineMarker
2813            | ScatterStyle::Smooth
2814            | ScatterStyle::SmoothMarker
2815    );
2816    let draw_marker = matches!(
2817        style,
2818        ScatterStyle::Marker | ScatterStyle::LineMarker | ScatterStyle::SmoothMarker
2819    );
2820    let mut elements = Vec::new();
2821    for (series_index, paired) in paired_series.iter().enumerate() {
2822        let colour = series_colour(series_colours, series_index)?;
2823        if draw_line {
2824            let indexes: Vec<_> = paired.iter().map(|(index, _, _)| *index).collect();
2825            for (start, end) in contiguous_ranges(&indexes, blanks) {
2826                let points = scatter_points(
2827                    &paired[start..end],
2828                    x_domain,
2829                    y_domain,
2830                    bounds,
2831                    x_orientation,
2832                    y_orientation,
2833                )?;
2834                elements.push(stroked_path(polyline_path(&points, false), colour));
2835            }
2836        }
2837        if draw_marker {
2838            let marker_points = scatter_points(
2839                &marker_series[series_index],
2840                x_domain,
2841                y_domain,
2842                bounds,
2843                x_orientation,
2844                y_orientation,
2845            )?;
2846            push_markers(&mut elements, &marker_points, colour);
2847        }
2848    }
2849    Ok(elements)
2850}
2851
2852type ScatterPoint = (usize, f64, f64);
2853
2854struct ScatterPointMaps {
2855    x_count: usize,
2856    y_count: usize,
2857    x_by_index: BTreeMap<usize, f64>,
2858    y_by_index: BTreeMap<usize, f64>,
2859}
2860
2861fn scatter_point_maps(series: &Series) -> Result<ScatterPointMaps> {
2862    let Some(AxisData::Numeric(x_values)) = &series.categories else {
2863        return Err(ChartError::InvalidValue {
2864            element: "c:xVal".to_owned(),
2865            value: "scatter series requires numeric cached x values".to_owned(),
2866        });
2867    };
2868    validate_finite(&x_values.values, "c:xVal/c:numCache")?;
2869    let (x_count, x_points) = logical_numeric_values(x_values)?;
2870    let (y_count, y_points) = logical_numeric_values(&series.values)?;
2871    Ok(ScatterPointMaps {
2872        x_count,
2873        y_count,
2874        x_by_index: x_points.into_iter().collect(),
2875        y_by_index: y_points.into_iter().collect(),
2876    })
2877}
2878
2879fn matched_scatter_points(series: &Series) -> Result<Vec<ScatterPoint>> {
2880    let points = scatter_point_maps(series)?;
2881    Ok(points
2882        .y_by_index
2883        .iter()
2884        .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
2885        .collect())
2886}
2887
2888fn scatter_render_points(series: &Series, blanks: DispBlanksAs) -> Result<Vec<ScatterPoint>> {
2889    let points = scatter_point_maps(series)?;
2890    if blanks != DispBlanksAs::Zero {
2891        return Ok(points
2892            .y_by_index
2893            .iter()
2894            .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
2895            .collect());
2896    }
2897    let count = points.x_count.max(points.y_count);
2898    let present = points
2899        .x_by_index
2900        .keys()
2901        .chain(points.y_by_index.keys())
2902        .copied()
2903        .collect();
2904    Ok(zero_control_indexes(count, &present)
2905        .into_iter()
2906        .map(|index| {
2907            (
2908                index,
2909                points.x_by_index.get(&index).copied().unwrap_or(0.0),
2910                points.y_by_index.get(&index).copied().unwrap_or(0.0),
2911            )
2912        })
2913        .collect())
2914}
2915
2916fn scatter_points(
2917    values: &[(usize, f64, f64)],
2918    x_domain: Domain,
2919    y_domain: Domain,
2920    bounds: Rect,
2921    x_orientation: Orientation,
2922    y_orientation: Orientation,
2923) -> Result<Vec<Point>> {
2924    values
2925        .iter()
2926        .map(|(_, x, y)| {
2927            Ok(Point {
2928                x: map_x_oriented(*x, x_domain, bounds, x_orientation)?,
2929                y: map_y_oriented(*y, y_domain, bounds, y_orientation)?,
2930            })
2931        })
2932        .collect()
2933}
2934
2935fn render_radar_geometry(
2936    style: RadarStyle,
2937    series: &[Series],
2938    bounds: Rect,
2939    options: GeometryOptions,
2940    series_colours: &[Color],
2941) -> Result<Vec<PositionedElement>> {
2942    let point_series = radar_series_points(series, bounds, options)?;
2943    let mut elements = Vec::new();
2944    for (series_index, points) in point_series.iter().enumerate() {
2945        let colour = series_colour(series_colours, series_index)?;
2946        let plain_points: Vec<_> = points.iter().map(|(_, point)| *point).collect();
2947        if plain_points.is_empty() {
2948            continue;
2949        }
2950        let path = polyline_path(&plain_points, true);
2951        if style == RadarStyle::Filled {
2952            elements.push(filled_and_stroked_path(path, colour));
2953        } else {
2954            elements.push(stroked_path(path, colour));
2955        }
2956        if style == RadarStyle::Marker {
2957            push_markers(&mut elements, &plain_points, colour);
2958        }
2959    }
2960    Ok(elements)
2961}
2962
2963fn radar_series_points(
2964    series: &[Series],
2965    bounds: Rect,
2966    options: GeometryOptions,
2967) -> Result<Vec<Vec<(usize, Point)>>> {
2968    validate_series_values(series)?;
2969    let logical_series = logical_series_values(series)?;
2970    let category_count = series_category_count(series)?;
2971    let values = logical_series
2972        .iter()
2973        .flat_map(|(_, points)| points.iter().map(|(_, value)| *value))
2974        .collect::<Vec<_>>();
2975    let mapping = options.value.unwrap_or(AxisMapping {
2976        domain: data_domain(&values, true)?,
2977        orientation: Orientation::MinMax,
2978    });
2979    let center = Point {
2980        x: bounds.x + bounds.width / 2.0,
2981        y: bounds.y + bounds.height / 2.0,
2982    };
2983    let radius = bounds.width.min(bounds.height) / 2.0;
2984    let mut point_series = Vec::with_capacity(logical_series.len());
2985    for (_, logical) in &logical_series {
2986        let mut points = Vec::with_capacity(logical.len());
2987        for (category, value) in logical {
2988            if *value < mapping.domain.min || *value > mapping.domain.max {
2989                continue;
2990            }
2991            points.push((
2992                *category,
2993                radial_point(
2994                    center,
2995                    normalized_value_oriented(*value, mapping.domain, mapping.orientation)?
2996                        * radius,
2997                    oriented_category_index(
2998                        *category,
2999                        category_count,
3000                        options.category_orientation,
3001                    ),
3002                    category_count,
3003                ),
3004            ));
3005        }
3006        point_series.push(points);
3007    }
3008    Ok(point_series)
3009}
3010
3011fn render_pie_geometry(
3012    first_slice_angle: u16,
3013    hole_size: Option<u8>,
3014    series: &[Series],
3015    bounds: Rect,
3016    series_colours: &[Color],
3017) -> Result<Vec<PositionedElement>> {
3018    pie_slices(first_slice_angle, hole_size, series, bounds)?
3019        .into_iter()
3020        .map(|slice| {
3021            Ok(filled_path(
3022                slice.path,
3023                series_colour(series_colours, slice.series_index)?,
3024            ))
3025        })
3026        .collect()
3027}
3028
3029struct PieSliceGeometry {
3030    series_index: usize,
3031    logical_index: usize,
3032    path: Path,
3033    anchor: Point,
3034    base: Point,
3035    end: Point,
3036}
3037
3038fn pie_slices(
3039    first_slice_angle: u16,
3040    hole_size: Option<u8>,
3041    series: &[Series],
3042    bounds: Rect,
3043) -> Result<Vec<PieSliceGeometry>> {
3044    validate_series_values(series)?;
3045    let center = Point {
3046        x: bounds.x + bounds.width / 2.0,
3047        y: bounds.y + bounds.height / 2.0,
3048    };
3049    let radius = bounds.width.min(bounds.height) / 2.0;
3050    let mut elements = Vec::new();
3051    for (series_index, item) in series.iter().enumerate() {
3052        let (_, logical) = logical_numeric_values(&item.values)?;
3053        let total = logical
3054            .iter()
3055            .map(|(_, value)| *value)
3056            .filter(|value| *value > 0.0)
3057            .try_fold(0.0, |total, value| {
3058                checked_geometry_sum(total, value, "pie value total")
3059            })?;
3060        if total <= 0.0 {
3061            continue;
3062        }
3063        let mut angle = f64::from(first_slice_angle).to_radians() - std::f64::consts::FRAC_PI_2;
3064        for (logical_index, value) in logical {
3065            if value <= 0.0 {
3066                continue;
3067            }
3068            let next = angle + value / total * std::f64::consts::TAU;
3069            let path = if let Some(hole_size) = hole_size {
3070                doughnut_wedge(
3071                    center,
3072                    radius,
3073                    radius * f64::from(hole_size) / 100.0,
3074                    angle,
3075                    next,
3076                )
3077            } else {
3078                pie_wedge(center, radius, angle, next)
3079            };
3080            let inner_radius = hole_size
3081                .map(|hole| radius * f64::from(hole) / 100.0)
3082                .unwrap_or(0.0);
3083            let anchor_radius = hole_size
3084                .map(|_| (radius + inner_radius) / 2.0)
3085                .unwrap_or(radius * 2.0 / 3.0);
3086            let midpoint = (angle + next) / 2.0;
3087            elements.push(PieSliceGeometry {
3088                series_index,
3089                logical_index,
3090                path,
3091                anchor: circle_point(center, anchor_radius, midpoint),
3092                base: circle_point(center, inner_radius, midpoint),
3093                end: circle_point(center, radius, midpoint),
3094            });
3095            angle = next;
3096        }
3097    }
3098    Ok(elements)
3099}
3100
3101fn validate_series_values(series: &[Series]) -> Result<()> {
3102    if series.is_empty() {
3103        return Err(ChartError::MissingElement("c:ser".to_owned()));
3104    }
3105    for item in series {
3106        validate_finite(&item.values.values, "c:val/c:numCache")?;
3107    }
3108    Ok(())
3109}
3110
3111fn validate_finite(values: &[f64], element: &str) -> Result<()> {
3112    if let Some(value) = values.iter().find(|value| !value.is_finite()) {
3113        return Err(ChartError::InvalidValue {
3114            element: element.to_owned(),
3115            value: value.to_string(),
3116        });
3117    }
3118    Ok(())
3119}
3120
3121type IndexedValues = Vec<(usize, f64)>;
3122type LogicalSeries = (usize, IndexedValues);
3123type GeometryLayer = Vec<(usize, f64, f64)>;
3124
3125fn zero_control_indexes(count: usize, present: &BTreeSet<usize>) -> BTreeSet<usize> {
3126    let mut controls = present.clone();
3127    if count == 0 {
3128        return controls;
3129    }
3130    controls.insert(0);
3131    controls.insert(count - 1);
3132    for index in present {
3133        if *index > 0 {
3134            controls.insert(*index - 1);
3135        }
3136        if index + 1 < count {
3137            controls.insert(index + 1);
3138        }
3139    }
3140    controls
3141}
3142
3143fn zero_filled_series(series: &[LogicalSeries], count: usize) -> Vec<LogicalSeries> {
3144    let present = series
3145        .iter()
3146        .flat_map(|(_, points)| points.iter().map(|(index, _)| *index))
3147        .collect();
3148    let controls = zero_control_indexes(count, &present);
3149    series
3150        .iter()
3151        .map(|(declared, points)| {
3152            let values: BTreeMap<_, _> = points.iter().copied().collect();
3153            let filled = controls
3154                .iter()
3155                .map(|index| (*index, values.get(index).copied().unwrap_or(0.0)))
3156                .collect();
3157            (*declared, filled)
3158        })
3159        .collect()
3160}
3161
3162fn contiguous_ranges(indexes: &[usize], blanks: DispBlanksAs) -> Vec<(usize, usize)> {
3163    if indexes.is_empty() {
3164        return Vec::new();
3165    }
3166    if blanks != DispBlanksAs::Gap {
3167        return vec![(0, indexes.len())];
3168    }
3169    let mut ranges = Vec::new();
3170    let mut start = 0usize;
3171    for position in 1..indexes.len() {
3172        if indexes[position - 1].checked_add(1) != Some(indexes[position]) {
3173            ranges.push((start, position));
3174            start = position;
3175        }
3176    }
3177    ranges.push((start, indexes.len()));
3178    ranges
3179}
3180
3181fn logical_numeric_values(data: &NumericData) -> Result<LogicalSeries> {
3182    validate_finite(&data.values, "c:numCache")?;
3183    let (declared, indexes) = cache_layout(&data.markup, data.values.len())?;
3184    let declared = usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
3185        element: "c:ptCount".to_owned(),
3186        value: declared.to_string(),
3187    })?;
3188    let points = indexes
3189        .into_iter()
3190        .zip(data.values.iter().copied())
3191        .map(|(index, value)| {
3192            usize::try_from(index)
3193                .map(|index| (index, value))
3194                .map_err(|_| ChartError::InvalidValue {
3195                    element: "c:pt/@idx".to_owned(),
3196                    value: index.to_string(),
3197                })
3198        })
3199        .collect::<Result<Vec<_>>>()?;
3200    Ok((declared, points))
3201}
3202
3203fn logical_series_values(series: &[Series]) -> Result<Vec<LogicalSeries>> {
3204    series
3205        .iter()
3206        .map(|item| logical_numeric_values(&item.values))
3207        .collect()
3208}
3209
3210fn reference_logical_count(markup: &ReferenceMarkup, value_count: usize) -> Result<usize> {
3211    let (declared, _) = cache_layout(markup, value_count)?;
3212    usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
3213        element: "c:ptCount".to_owned(),
3214        value: declared.to_string(),
3215    })
3216}
3217
3218fn series_category_count(series: &[Series]) -> Result<usize> {
3219    let mut count = 0usize;
3220    for item in series {
3221        count = count.max(reference_logical_count(
3222            &item.values.markup,
3223            item.values.values.len(),
3224        )?);
3225        if let Some(categories) = &item.categories {
3226            let category_count = match categories {
3227                AxisData::String(data) => reference_logical_count(&data.markup, data.values.len())?,
3228                AxisData::Numeric(data) => {
3229                    reference_logical_count(&data.markup, data.values.len())?
3230                }
3231            };
3232            count = count.max(category_count);
3233        }
3234    }
3235    if count == 0 {
3236        return Err(ChartError::InvalidValue {
3237            element: "c:ser".to_owned(),
3238            value: "cached series contain no points".to_owned(),
3239        });
3240    }
3241    Ok(count)
3242}
3243
3244fn stacked_series_bounds(
3245    series: &[LogicalSeries],
3246    stacked: bool,
3247    percent: bool,
3248) -> Result<Vec<GeometryLayer>> {
3249    let mut positive_totals = BTreeMap::<usize, f64>::new();
3250    let mut negative_totals = BTreeMap::<usize, f64>::new();
3251    if percent {
3252        for (_, points) in series {
3253            for &(index, value) in points {
3254                let totals = if value >= 0.0 {
3255                    &mut positive_totals
3256                } else {
3257                    &mut negative_totals
3258                };
3259                let total = totals.entry(index).or_default();
3260                *total = checked_geometry_sum(*total, value.abs(), "stacked percentage total")?;
3261            }
3262        }
3263    }
3264    let mut positive = BTreeMap::<usize, f64>::new();
3265    let mut negative = BTreeMap::<usize, f64>::new();
3266    let mut layers = Vec::with_capacity(series.len());
3267    for (_, points) in series {
3268        let mut layer = Vec::with_capacity(points.len());
3269        for &(index, original) in points {
3270            let total = if original >= 0.0 {
3271                positive_totals.get(&index).copied().unwrap_or(0.0)
3272            } else {
3273                negative_totals.get(&index).copied().unwrap_or(0.0)
3274            };
3275            let value = if percent && total != 0.0 {
3276                original / total
3277            } else {
3278                original
3279            };
3280            if stacked {
3281                let accumulators = if value >= 0.0 {
3282                    &mut positive
3283                } else {
3284                    &mut negative
3285                };
3286                let accumulator = accumulators.entry(index).or_default();
3287                let lower = *accumulator;
3288                *accumulator = checked_geometry_sum(*accumulator, value, "stacked value total")?;
3289                layer.push((index, lower, *accumulator));
3290            } else {
3291                layer.push((index, 0.0, value));
3292            }
3293        }
3294        layers.push(layer);
3295    }
3296    Ok(layers)
3297}
3298
3299fn checked_geometry_sum(current: f64, value: f64, context: &str) -> Result<f64> {
3300    let sum = current + value;
3301    if sum.is_finite() {
3302        Ok(sum)
3303    } else {
3304        Err(ChartError::InvalidValue {
3305            element: "chart geometry aggregate".to_owned(),
3306            value: format!("{context} is nonfinite"),
3307        })
3308    }
3309}
3310
3311#[derive(Clone, Copy, Debug, PartialEq)]
3312struct Domain {
3313    min: f64,
3314    max: f64,
3315}
3316
3317fn nice_number_scale(
3318    extent: Domain,
3319    pinned_minimum: Option<f64>,
3320    pinned_maximum: Option<f64>,
3321    target_tick_count: usize,
3322) -> Result<AxisScale> {
3323    if target_tick_count < 2
3324        || !extent.min.is_finite()
3325        || !extent.max.is_finite()
3326        || pinned_minimum.is_some_and(|value| !value.is_finite())
3327        || pinned_maximum.is_some_and(|value| !value.is_finite())
3328    {
3329        return Err(ChartError::InvalidValue {
3330            element: "chart axis scale".to_owned(),
3331            value: "scale bounds and target tick count must be finite and valid".to_owned(),
3332        });
3333    }
3334
3335    let mut data_min = extent.min.min(extent.max);
3336    let mut data_max = extent.min.max(extent.max);
3337    if data_min == data_max {
3338        let expanded = expand_constant_domain(data_min)?;
3339        data_min = expanded.min;
3340        data_max = expanded.max;
3341    }
3342    let requested_min = match (pinned_minimum, pinned_maximum) {
3343        (Some(minimum), _) => minimum,
3344        (None, Some(maximum)) if data_min >= maximum => {
3345            extend_bound(maximum, stable_positive_step(data_min, data_max, 3)?, false)?
3346        }
3347        (None, _) => data_min,
3348    };
3349    let requested_max = match (pinned_minimum, pinned_maximum) {
3350        (_, Some(maximum)) => maximum,
3351        (Some(minimum), None) if data_max <= minimum => {
3352            extend_bound(minimum, stable_positive_step(data_min, data_max, 3)?, true)?
3353        }
3354        (_, None) => data_max,
3355    };
3356    if requested_min >= requested_max {
3357        return Err(ChartError::InvalidValue {
3358            element: "c:scaling".to_owned(),
3359            value: format!("minimum {requested_min} is not less than maximum {requested_max}"),
3360        });
3361    }
3362
3363    let raw_step = stable_positive_step(requested_min, requested_max, target_tick_count)?;
3364    let step = nice_step(raw_step)?;
3365    let rounded_minimum = (requested_min / step).floor() * step;
3366    let rounded_maximum = (requested_max / step).ceil() * step;
3367    let lower_fallback = pinned_minimum.is_none() && !rounded_minimum.is_finite();
3368    let upper_fallback = pinned_maximum.is_none() && !rounded_maximum.is_finite();
3369    let minimum = pinned_minimum.unwrap_or(if rounded_minimum.is_finite() {
3370        rounded_minimum
3371    } else {
3372        requested_min
3373    });
3374    let maximum = pinned_maximum.unwrap_or(if rounded_maximum.is_finite() {
3375        rounded_maximum
3376    } else {
3377        requested_max
3378    });
3379    if !minimum.is_finite() || !maximum.is_finite() || minimum >= maximum {
3380        return Err(ChartError::InvalidValue {
3381            element: "chart axis scale".to_owned(),
3382            value: "nice-number bounds are nonfinite or empty".to_owned(),
3383        });
3384    }
3385
3386    let first = (minimum / step).ceil();
3387    let last = (maximum / step).floor();
3388    let tick_count = if last >= first {
3389        (last - first).min(1024.0) as usize + 1
3390    } else {
3391        0
3392    };
3393    let mut ticks = Vec::with_capacity(tick_count);
3394    for index in 0..tick_count {
3395        let tick = (first + index as f64) * step;
3396        if tick.is_finite() && tick >= minimum && tick <= maximum {
3397            ticks.push(clean_tick(tick, step));
3398        }
3399    }
3400    if lower_fallback && ticks.first().is_none_or(|tick| *tick > minimum) {
3401        ticks.insert(0, minimum);
3402    }
3403    if upper_fallback && ticks.last().is_none_or(|tick| *tick < maximum) {
3404        ticks.push(maximum);
3405    }
3406    if ticks.len() < 2 {
3407        ticks.extend([minimum, maximum]);
3408        ticks.sort_by(f64::total_cmp);
3409    }
3410    ticks.dedup_by(|left, right| *left == *right);
3411    if ticks.len() < 2 || !ticks.windows(2).all(|pair| pair[0] < pair[1]) {
3412        return Err(ChartError::InvalidValue {
3413            element: "chart axis scale".to_owned(),
3414            value: "scale cannot produce two increasing finite ticks".to_owned(),
3415        });
3416    }
3417    Ok(AxisScale {
3418        domain: Domain {
3419            min: minimum,
3420            max: maximum,
3421        },
3422        ticks,
3423    })
3424}
3425
3426const MIN_POSITIVE_SUBNORMAL: f64 = f64::from_bits(1);
3427
3428fn stable_positive_step(minimum: f64, maximum: f64, target_tick_count: usize) -> Result<f64> {
3429    let scale = minimum.abs().max(maximum.abs());
3430    let scale = if scale == 0.0 {
3431        MIN_POSITIVE_SUBNORMAL
3432    } else {
3433        scale
3434    };
3435    let normalized =
3436        (maximum / scale - minimum / scale) / target_tick_count.saturating_sub(1) as f64;
3437    let step = normalized * scale;
3438    if normalized.is_finite() && normalized > 0.0 && step.is_finite() {
3439        Ok(step.max(MIN_POSITIVE_SUBNORMAL))
3440    } else {
3441        Err(ChartError::InvalidValue {
3442            element: "chart axis scale".to_owned(),
3443            value: "scale step is nonfinite or empty".to_owned(),
3444        })
3445    }
3446}
3447
3448fn nice_step(raw_step: f64) -> Result<f64> {
3449    if !raw_step.is_finite() || raw_step <= 0.0 {
3450        return Err(ChartError::InvalidValue {
3451            element: "chart axis scale".to_owned(),
3452            value: format!("invalid raw tick step {raw_step}"),
3453        });
3454    }
3455    let magnitude = 10.0_f64
3456        .powf(raw_step.log10().floor())
3457        .max(MIN_POSITIVE_SUBNORMAL);
3458    let fraction = raw_step / magnitude;
3459    let multiplier = if fraction <= 1.0 {
3460        1.0
3461    } else if fraction <= 2.0 {
3462        2.0
3463    } else if fraction <= 5.0 {
3464        5.0
3465    } else {
3466        10.0
3467    };
3468    let step = (multiplier * magnitude).max(MIN_POSITIVE_SUBNORMAL);
3469    if step.is_finite() && step > 0.0 {
3470        Ok(step)
3471    } else {
3472        Err(ChartError::InvalidValue {
3473            element: "chart axis scale".to_owned(),
3474            value: "nice-number step is nonfinite".to_owned(),
3475        })
3476    }
3477}
3478
3479fn expand_constant_domain(value: f64) -> Result<Domain> {
3480    if value == 0.0 {
3481        return Ok(Domain {
3482            min: -1.0,
3483            max: 1.0,
3484        });
3485    }
3486    let padding = (value.abs() * 0.5).max(MIN_POSITIVE_SUBNORMAL);
3487    let minimum = value - padding;
3488    let maximum = value + padding;
3489    if minimum.is_finite() && maximum.is_finite() && minimum < maximum {
3490        return Ok(Domain {
3491            min: minimum,
3492            max: maximum,
3493        });
3494    }
3495    let domain = if value.is_sign_positive() {
3496        Domain {
3497            min: 0.0,
3498            max: value,
3499        }
3500    } else {
3501        Domain {
3502            min: value,
3503            max: 0.0,
3504        }
3505    };
3506    if domain.min < domain.max {
3507        Ok(domain)
3508    } else {
3509        Err(ChartError::InvalidValue {
3510            element: "chart axis scale".to_owned(),
3511            value: format!("constant value {value} has no finite renderable extent"),
3512        })
3513    }
3514}
3515
3516fn extend_bound(bound: f64, span: f64, positive: bool) -> Result<f64> {
3517    let candidate = if positive { bound + span } else { bound - span };
3518    let extended = if candidate.is_finite()
3519        && if positive {
3520            candidate > bound
3521        } else {
3522            candidate < bound
3523        } {
3524        candidate
3525    } else if positive {
3526        bound.next_up()
3527    } else {
3528        bound.next_down()
3529    };
3530    if extended.is_finite()
3531        && if positive {
3532            extended > bound
3533        } else {
3534            extended < bound
3535        }
3536    {
3537        Ok(extended)
3538    } else {
3539        Err(ChartError::InvalidValue {
3540            element: "c:scaling".to_owned(),
3541            value: format!("one-sided bound {bound} leaves no finite automatic extent"),
3542        })
3543    }
3544}
3545
3546fn clean_tick(value: f64, step: f64) -> f64 {
3547    if value.abs() <= step.abs() * 1.0e-12 {
3548        0.0
3549    } else {
3550        value
3551    }
3552}
3553
3554fn domain_from_layers(layers: &[GeometryLayer]) -> Result<Domain> {
3555    let values = layers
3556        .iter()
3557        .flatten()
3558        .flat_map(|(_, lower, upper)| [*lower, *upper]);
3559    let mut min = 0.0_f64;
3560    let mut max = 0.0_f64;
3561    for value in values {
3562        if !value.is_finite() {
3563            return Err(ChartError::InvalidValue {
3564                element: "chart geometry domain".to_owned(),
3565                value: "aggregate produced a nonfinite value".to_owned(),
3566            });
3567        }
3568        min = min.min(value);
3569        max = max.max(value);
3570    }
3571    if min == max {
3572        max = 1.0;
3573    }
3574    Ok(Domain { min, max })
3575}
3576
3577fn data_domain(values: &[f64], include_zero: bool) -> Result<Domain> {
3578    validate_finite(values, "chart geometry cache")?;
3579    let Some(first) = values.first().copied() else {
3580        return Err(ChartError::InvalidValue {
3581            element: "c:numCache".to_owned(),
3582            value: "cached series contain no points".to_owned(),
3583        });
3584    };
3585    let mut min = first;
3586    let mut max = first;
3587    for value in values.iter().copied() {
3588        min = min.min(value);
3589        max = max.max(value);
3590    }
3591    if include_zero {
3592        min = min.min(0.0);
3593        max = max.max(0.0);
3594    }
3595    if min == max {
3596        if !include_zero {
3597            return expand_constant_domain(min);
3598        }
3599        if min > 0.0 {
3600            min = 0.0;
3601        } else if max < 0.0 {
3602            max = 0.0;
3603        } else {
3604            max = 1.0;
3605        }
3606    }
3607    Ok(Domain { min, max })
3608}
3609
3610fn normalized_value(value: f64, domain: Domain) -> Result<f64> {
3611    let scale = value
3612        .abs()
3613        .max(domain.min.abs())
3614        .max(domain.max.abs())
3615        .max(1.0);
3616    let value = value / scale;
3617    let min = domain.min / scale;
3618    let max = domain.max / scale;
3619    let range = max - min;
3620    let normalized = (value - min) / range;
3621    if !range.is_finite() || range <= 0.0 || !normalized.is_finite() {
3622        return Err(ChartError::InvalidValue {
3623            element: "chart geometry domain".to_owned(),
3624            value: format!("cannot map finite domain {} to {}", domain.min, domain.max),
3625        });
3626    }
3627    Ok(normalized)
3628}
3629
3630fn normalized_value_oriented(value: f64, domain: Domain, orientation: Orientation) -> Result<f64> {
3631    let normalized = normalized_value(value, domain)?;
3632    Ok(if orientation == Orientation::MaxMin {
3633        1.0 - normalized
3634    } else {
3635        normalized
3636    })
3637}
3638
3639fn map_x_oriented(
3640    value: f64,
3641    domain: Domain,
3642    bounds: Rect,
3643    orientation: Orientation,
3644) -> Result<f64> {
3645    let coordinate =
3646        bounds.x + normalized_value_oriented(value, domain, orientation)? * bounds.width;
3647    if coordinate.is_finite() {
3648        Ok(coordinate)
3649    } else {
3650        Err(ChartError::InvalidValue {
3651            element: "chart geometry coordinate".to_owned(),
3652            value: "x coordinate is nonfinite".to_owned(),
3653        })
3654    }
3655}
3656
3657fn map_y_oriented(
3658    value: f64,
3659    domain: Domain,
3660    bounds: Rect,
3661    orientation: Orientation,
3662) -> Result<f64> {
3663    let coordinate = bounds.y + bounds.height
3664        - normalized_value_oriented(value, domain, orientation)? * bounds.height;
3665    if coordinate.is_finite() {
3666        Ok(coordinate)
3667    } else {
3668        Err(ChartError::InvalidValue {
3669            element: "chart geometry coordinate".to_owned(),
3670            value: "y coordinate is nonfinite".to_owned(),
3671        })
3672    }
3673}
3674
3675fn oriented_category_index(index: usize, count: usize, orientation: Orientation) -> usize {
3676    if orientation == Orientation::MaxMin && index < count {
3677        count - index - 1
3678    } else {
3679        index
3680    }
3681}
3682
3683fn category_points(
3684    values: &[(usize, f64)],
3685    count: usize,
3686    domain: Domain,
3687    bounds: Rect,
3688    category_orientation: Orientation,
3689    value_orientation: Orientation,
3690) -> Result<Vec<Point>> {
3691    values
3692        .iter()
3693        .map(|(index, value)| {
3694            let index = oriented_category_index(*index, count, category_orientation);
3695            let x = bounds.x + (index as f64 + 0.5) / count as f64 * bounds.width;
3696            let y = map_y_oriented(*value, domain, bounds, value_orientation)?;
3697            if !x.is_finite() {
3698                return Err(ChartError::InvalidValue {
3699                    element: "chart geometry coordinate".to_owned(),
3700                    value: "category x coordinate is nonfinite".to_owned(),
3701                });
3702            }
3703            Ok(Point { x, y })
3704        })
3705        .collect()
3706}
3707
3708fn polyline_path(points: &[Point], close: bool) -> Path {
3709    let mut commands = Vec::with_capacity(points.len() + usize::from(close));
3710    if let Some(first) = points.first() {
3711        commands.push(PathCommand::MoveTo(*first));
3712        commands.extend(points.iter().skip(1).copied().map(PathCommand::LineTo));
3713        if close {
3714            commands.push(PathCommand::Close);
3715        }
3716    }
3717    Path {
3718        commands,
3719        fill_rule: FillRule::NonZero,
3720    }
3721}
3722
3723fn pie_wedge(center: Point, radius: f64, start: f64, end: f64) -> Path {
3724    let first = circle_point(center, radius, start);
3725    let mut commands = vec![PathCommand::MoveTo(center), PathCommand::LineTo(first)];
3726    append_arc(&mut commands, center, radius, start, end);
3727    commands.push(PathCommand::Close);
3728    Path {
3729        commands,
3730        fill_rule: FillRule::NonZero,
3731    }
3732}
3733
3734fn doughnut_wedge(
3735    center: Point,
3736    outer_radius: f64,
3737    inner_radius: f64,
3738    start: f64,
3739    end: f64,
3740) -> Path {
3741    let outer_start = circle_point(center, outer_radius, start);
3742    let inner_end = circle_point(center, inner_radius, end);
3743    let mut commands = vec![PathCommand::MoveTo(outer_start)];
3744    append_arc(&mut commands, center, outer_radius, start, end);
3745    commands.push(PathCommand::LineTo(inner_end));
3746    append_arc(&mut commands, center, inner_radius, end, start);
3747    commands.push(PathCommand::Close);
3748    Path {
3749        commands,
3750        fill_rule: FillRule::NonZero,
3751    }
3752}
3753
3754fn append_arc(commands: &mut Vec<PathCommand>, center: Point, radius: f64, start: f64, end: f64) {
3755    let segments = ((end - start).abs() / std::f64::consts::FRAC_PI_2)
3756        .ceil()
3757        .max(1.0) as usize;
3758    let sweep = (end - start) / segments as f64;
3759    for segment in 0..segments {
3760        let angle = start + segment as f64 * sweep;
3761        let next = angle + sweep;
3762        let factor = 4.0 / 3.0 * (sweep / 4.0).tan();
3763        let from = circle_point(center, radius, angle);
3764        let to = circle_point(center, radius, next);
3765        commands.push(PathCommand::CurveTo {
3766            c1: Point {
3767                x: from.x - factor * radius * angle.sin(),
3768                y: from.y + factor * radius * angle.cos(),
3769            },
3770            c2: Point {
3771                x: to.x + factor * radius * next.sin(),
3772                y: to.y - factor * radius * next.cos(),
3773            },
3774            to,
3775        });
3776    }
3777}
3778
3779fn circle_point(center: Point, radius: f64, angle: f64) -> Point {
3780    Point {
3781        x: center.x + radius * angle.cos(),
3782        y: center.y + radius * angle.sin(),
3783    }
3784}
3785
3786fn radial_point(center: Point, radius: f64, index: usize, count: usize) -> Point {
3787    circle_point(
3788        center,
3789        radius,
3790        index as f64 * std::f64::consts::TAU / count as f64 - std::f64::consts::FRAC_PI_2,
3791    )
3792}
3793
3794fn marker_path(center: Point) -> Path {
3795    const RADIUS: f64 = 2.5;
3796    const KAPPA: f64 = 0.552_284_749_830_793_6;
3797    let control = RADIUS * KAPPA;
3798    Path {
3799        commands: vec![
3800            PathCommand::MoveTo(Point {
3801                x: center.x + RADIUS,
3802                y: center.y,
3803            }),
3804            PathCommand::CurveTo {
3805                c1: Point {
3806                    x: center.x + RADIUS,
3807                    y: center.y + control,
3808                },
3809                c2: Point {
3810                    x: center.x + control,
3811                    y: center.y + RADIUS,
3812                },
3813                to: Point {
3814                    x: center.x,
3815                    y: center.y + RADIUS,
3816                },
3817            },
3818            PathCommand::CurveTo {
3819                c1: Point {
3820                    x: center.x - control,
3821                    y: center.y + RADIUS,
3822                },
3823                c2: Point {
3824                    x: center.x - RADIUS,
3825                    y: center.y + control,
3826                },
3827                to: Point {
3828                    x: center.x - RADIUS,
3829                    y: center.y,
3830                },
3831            },
3832            PathCommand::CurveTo {
3833                c1: Point {
3834                    x: center.x - RADIUS,
3835                    y: center.y - control,
3836                },
3837                c2: Point {
3838                    x: center.x - control,
3839                    y: center.y - RADIUS,
3840                },
3841                to: Point {
3842                    x: center.x,
3843                    y: center.y - RADIUS,
3844                },
3845            },
3846            PathCommand::CurveTo {
3847                c1: Point {
3848                    x: center.x + control,
3849                    y: center.y - RADIUS,
3850                },
3851                c2: Point {
3852                    x: center.x + RADIUS,
3853                    y: center.y - control,
3854                },
3855                to: Point {
3856                    x: center.x + RADIUS,
3857                    y: center.y,
3858                },
3859            },
3860            PathCommand::Close,
3861        ],
3862        fill_rule: FillRule::NonZero,
3863    }
3864}
3865
3866fn push_markers(elements: &mut Vec<PositionedElement>, points: &[Point], colour: Color) {
3867    for point in points {
3868        elements.push(filled_path(marker_path(*point), colour));
3869    }
3870}
3871
3872fn series_colour(series_colours: &[Color], index: usize) -> Result<Color> {
3873    series_colours
3874        .get(index)
3875        .copied()
3876        .ok_or_else(|| ChartError::InvalidValue {
3877            element: "chart series palette".to_owned(),
3878            value: format!("missing resolved colour for series {index}"),
3879        })
3880}
3881
3882fn filled_path(path: Path, color: Color) -> PositionedElement {
3883    PositionedElement::Path(PathElement {
3884        path,
3885        fill: Some(Paint::Solid(color)),
3886        stroke: None,
3887    })
3888}
3889
3890fn stroked_path(path: Path, color: Color) -> PositionedElement {
3891    PositionedElement::Path(PathElement {
3892        path,
3893        fill: None,
3894        stroke: Some(Stroke::new(Paint::Solid(color), 1.5)),
3895    })
3896}
3897
3898fn filled_and_stroked_path(path: Path, color: Color) -> PositionedElement {
3899    PositionedElement::Path(PathElement {
3900        path,
3901        fill: Some(Paint::Solid(Color {
3902            a: color.a * 0.55,
3903            ..color
3904        })),
3905        stroke: Some(Stroke::new(Paint::Solid(color), 1.0)),
3906    })
3907}
3908
3909type NamespaceBindings = Vec<(Vec<u8>, String)>;
3910type XmlAttributes = Vec<(String, String)>;
3911type RootAttributes = (XmlAttributes, XmlAttributes);
3912
3913#[derive(Clone, Debug, Default, Eq, PartialEq)]
3914struct ScalarMarkup {
3915    raw_attributes: XmlAttributes,
3916    raw_content: Vec<Vec<u8>>,
3917}
3918
3919#[derive(Clone, Debug, Default, Eq, PartialEq)]
3920struct TextMarkup {
3921    raw_attributes: XmlAttributes,
3922    raw_children: OrderedRawChildren,
3923}
3924
3925#[derive(Clone, Debug, Default, Eq, PartialEq)]
3926struct PointMarkup {
3927    raw_attributes: XmlAttributes,
3928    raw_children: OrderedRawChildren,
3929    value: TextMarkup,
3930}
3931
3932#[derive(Clone, Debug, Default, Eq, PartialEq)]
3933struct ReferenceMarkup {
3934    raw_attributes: XmlAttributes,
3935    raw_children: OrderedRawChildren,
3936    formula: TextMarkup,
3937    cache_attributes: XmlAttributes,
3938    cache_children: OrderedRawChildren,
3939    format_code: Option<TextMarkup>,
3940    format_code_omitted: bool,
3941    point_count: ScalarMarkup,
3942    declared_point_count: Option<u32>,
3943    point_indexes: Vec<u32>,
3944    points: Vec<PointMarkup>,
3945}
3946
3947#[derive(Clone, Debug, Default, Eq, PartialEq)]
3948struct WrapperMarkup {
3949    raw_attributes: XmlAttributes,
3950    raw_children: OrderedRawChildren,
3951}
3952
3953/// A formula-backed string cache whose count and point indexes are derived.
3954#[derive(Clone, Debug, PartialEq)]
3955pub struct StringRef {
3956    pub formula: String,
3957    pub values: Vec<String>,
3958    markup: ReferenceMarkup,
3959}
3960
3961impl StringRef {
3962    pub fn new(formula: String, values: Vec<String>) -> Result<Self> {
3963        let reference = Self {
3964            formula,
3965            values,
3966            markup: ReferenceMarkup::default(),
3967        };
3968        reference.validate()?;
3969        Ok(reference)
3970    }
3971
3972    pub fn from_xml(xml: &[u8]) -> Result<Self> {
3973        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
3974    }
3975
3976    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
3977        let parsed = parse_reference(xml, b"strRef", b"strCache", false, inherited)?;
3978        let values = parsed.values;
3979        let reference = Self {
3980            formula: parsed.formula,
3981            values,
3982            markup: parsed.markup,
3983        };
3984        reference.validate()?;
3985        Ok(reference)
3986    }
3987
3988    pub fn to_xml(&self) -> Result<Vec<u8>> {
3989        self.validate()?;
3990        let mut writer = Writer::new(Vec::new());
3991        self.write_xml(&mut writer, true)?;
3992        Ok(writer.into_inner())
3993    }
3994
3995    fn validate(&self) -> Result<()> {
3996        validate_formula(&self.formula, "c:strRef/c:f")?;
3997        for value in &self.values {
3998            validate_xml_text(value, "c:strCache/c:pt/c:v")?;
3999        }
4000        validate_point_count(self.values.len(), "c:strCache")
4001    }
4002
4003    fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
4004        self.validate()?;
4005        let mut start = BytesStart::new("c:strRef");
4006        if standalone {
4007            start.push_attribute(("xmlns:c", C_NS));
4008        }
4009        push_attributes(&mut start, &self.markup.raw_attributes);
4010        writer
4011            .write_event(Event::Start(start))
4012            .map_err(OxmlError::from)?;
4013        emit_raw(writer, self.markup.raw_children.at(0))?;
4014        write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
4015        emit_raw(writer, self.markup.raw_children.at(1))?;
4016        write_string_cache(writer, &self.values, &self.markup)?;
4017        emit_raw(writer, self.markup.raw_children.at(2))?;
4018        writer
4019            .write_event(Event::End(BytesEnd::new("c:strRef")))
4020            .map_err(OxmlError::from)?;
4021        Ok(())
4022    }
4023}
4024
4025/// A formula-backed numeric cache whose metadata and points are derived.
4026#[derive(Clone, Debug, PartialEq)]
4027pub struct NumericData {
4028    pub formula: String,
4029    pub format_code: String,
4030    pub values: Vec<f64>,
4031    markup: ReferenceMarkup,
4032}
4033
4034impl NumericData {
4035    pub fn new(formula: String, format_code: String, values: Vec<f64>) -> Result<Self> {
4036        validate_formula(&formula, "c:numRef/c:f")?;
4037        validate_format_code(&format_code)?;
4038        validate_numeric_values(&values)?;
4039        validate_point_count(values.len(), "c:numCache")?;
4040        Ok(Self {
4041            formula,
4042            format_code,
4043            values,
4044            markup: ReferenceMarkup::default(),
4045        })
4046    }
4047
4048    pub fn from_xml(xml: &[u8]) -> Result<Self> {
4049        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
4050    }
4051
4052    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
4053        let parsed = parse_reference(xml, b"numRef", b"numCache", true, inherited)?;
4054        let format_code = parsed.format_code.unwrap_or_else(|| "General".to_owned());
4055        let mut values = Vec::with_capacity(parsed.values.len());
4056        for value in parsed.values {
4057            let number = value.parse::<f64>().map_err(|_| ChartError::InvalidValue {
4058                element: "c:v".to_owned(),
4059                value: value.clone(),
4060            })?;
4061            if !number.is_finite() {
4062                return Err(ChartError::InvalidValue {
4063                    element: "c:v".to_owned(),
4064                    value,
4065                });
4066            }
4067            values.push(number);
4068        }
4069        let data = Self {
4070            formula: parsed.formula,
4071            format_code,
4072            values,
4073            markup: parsed.markup,
4074        };
4075        data.validate()?;
4076        Ok(data)
4077    }
4078
4079    pub fn to_xml(&self) -> Result<Vec<u8>> {
4080        self.validate()?;
4081        let mut writer = Writer::new(Vec::new());
4082        self.write_xml(&mut writer, true)?;
4083        Ok(writer.into_inner())
4084    }
4085
4086    fn validate(&self) -> Result<()> {
4087        validate_formula(&self.formula, "c:numRef/c:f")?;
4088        validate_format_code(&self.format_code)?;
4089        validate_numeric_values(&self.values)?;
4090        validate_point_count(self.values.len(), "c:numCache")
4091    }
4092
4093    fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
4094        self.validate()?;
4095        let mut start = BytesStart::new("c:numRef");
4096        if standalone {
4097            start.push_attribute(("xmlns:c", C_NS));
4098        }
4099        push_attributes(&mut start, &self.markup.raw_attributes);
4100        writer
4101            .write_event(Event::Start(start))
4102            .map_err(OxmlError::from)?;
4103        emit_raw(writer, self.markup.raw_children.at(0))?;
4104        write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
4105        emit_raw(writer, self.markup.raw_children.at(1))?;
4106        write_numeric_cache(writer, self, &self.markup)?;
4107        emit_raw(writer, self.markup.raw_children.at(2))?;
4108        writer
4109            .write_event(Event::End(BytesEnd::new("c:numRef")))
4110            .map_err(OxmlError::from)?;
4111        Ok(())
4112    }
4113}
4114
4115#[derive(Clone, Debug, PartialEq)]
4116pub enum AxisData {
4117    String(StringRef),
4118    Numeric(NumericData),
4119}
4120
4121#[derive(Clone, Debug, PartialEq)]
4122struct DataPoint {
4123    index: u32,
4124    sp_pr: CT_ShapeProperties,
4125}
4126
4127impl DataPoint {
4128    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
4129        writer
4130            .write_event(Event::Start(BytesStart::new("c:dPt")))
4131            .map_err(OxmlError::from)?;
4132        write_scalar(writer, "c:idx", &self.index.to_string(), None)?;
4133        self.sp_pr.write_xml_as(writer, "c:spPr")?;
4134        writer
4135            .write_event(Event::End(BytesEnd::new("c:dPt")))
4136            .map_err(OxmlError::from)?;
4137        Ok(())
4138    }
4139}
4140
4141/// The common formula-backed payload of one ChartML series.
4142#[derive(Clone, Debug, PartialEq)]
4143pub struct Series {
4144    pub index: u32,
4145    pub order: u32,
4146    pub name: Option<StringRef>,
4147    pub categories: Option<AxisData>,
4148    pub values: NumericData,
4149    pub bubble_size: Option<NumericData>,
4150    pub sp_pr: Option<CT_ShapeProperties>,
4151    pub data_labels: Option<CT_DLbls>,
4152    authored_data_points: Vec<DataPoint>,
4153    index_markup: ScalarMarkup,
4154    order_markup: ScalarMarkup,
4155    name_markup: Option<WrapperMarkup>,
4156    categories_markup: Option<WrapperMarkup>,
4157    values_markup: WrapperMarkup,
4158    bubble_size_markup: Option<WrapperMarkup>,
4159    opaque_name: bool,
4160    opaque_categories: bool,
4161    opaque_bubble_size: bool,
4162    uses_scatter_wrappers: bool,
4163    parsed_from_xml: bool,
4164    raw_attributes: XmlAttributes,
4165    namespace_declarations: XmlAttributes,
4166    raw_children: OrderedRawChildren,
4167}
4168
4169impl Series {
4170    pub fn new(index: u32, order: u32, values: NumericData) -> Self {
4171        Self {
4172            index,
4173            order,
4174            name: None,
4175            categories: None,
4176            values,
4177            bubble_size: None,
4178            sp_pr: None,
4179            data_labels: None,
4180            authored_data_points: Vec::new(),
4181            index_markup: ScalarMarkup::default(),
4182            order_markup: ScalarMarkup::default(),
4183            name_markup: None,
4184            categories_markup: None,
4185            values_markup: WrapperMarkup::default(),
4186            bubble_size_markup: None,
4187            opaque_name: false,
4188            opaque_categories: false,
4189            opaque_bubble_size: false,
4190            uses_scatter_wrappers: false,
4191            parsed_from_xml: false,
4192            raw_attributes: Vec::new(),
4193            namespace_declarations: Vec::new(),
4194            raw_children: OrderedRawChildren::default(),
4195        }
4196    }
4197
4198    pub fn from_xml(xml: &[u8]) -> Result<Self> {
4199        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
4200    }
4201
4202    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
4203        let mut reader = Reader::from_reader(xml);
4204        let mut buffer = Vec::new();
4205        loop {
4206            match reader
4207                .read_event_into(&mut buffer)
4208                .map_err(OxmlError::from)?
4209            {
4210                Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4211                    chart_root_prefix(&element)?;
4212                    if !element_is_in_namespace(&element, C_NS, inherited)? {
4213                        return Err(ChartError::UnexpectedElement(element_name(&element)));
4214                    }
4215                    return Self::from_element(&mut reader, &element, inherited);
4216                }
4217                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4218                    return Err(ChartError::MissingElement("c:idx".to_owned()));
4219                }
4220                Event::Start(element) | Event::Empty(element) => {
4221                    return Err(ChartError::UnexpectedElement(element_name(&element)));
4222                }
4223                Event::Eof => return Err(ChartError::MissingElement("c:ser".to_owned())),
4224                _ => {}
4225            }
4226            buffer.clear();
4227        }
4228    }
4229
4230    fn from_element(
4231        reader: &mut Reader<&[u8]>,
4232        start: &BytesStart<'_>,
4233        inherited: &NamespaceBindings,
4234    ) -> Result<Self> {
4235        reject_conflicting_prefix(start, b"a", A_NS)?;
4236        let namespaces = chart_bindings(inherited, start)?;
4237        require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
4238        require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
4239        require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
4240        let (raw_attributes, _) =
4241            capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
4242        let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
4243        let mut state = SeriesParseState::default();
4244        let mut buffer = Vec::new();
4245        loop {
4246            match reader
4247                .read_event_into(&mut buffer)
4248                .map_err(OxmlError::from)?
4249            {
4250                Event::Start(element) => {
4251                    let name = chart_child_local(&element, &namespaces)?;
4252                    let raw = capture_element(reader, &element)?;
4253                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
4254                }
4255                Event::Empty(element) => {
4256                    let name = chart_child_local(&element, &namespaces)?;
4257                    let raw = capture_empty_element(&element)?;
4258                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
4259                }
4260                event @ (Event::Text(_)
4261                | Event::CData(_)
4262                | Event::Comment(_)
4263                | Event::PI(_)
4264                | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
4265                Event::End(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4266                    break;
4267                }
4268                Event::Eof => return Err(missing_end("c:ser")),
4269                _ => {}
4270            }
4271            buffer.clear();
4272        }
4273        state.finish(raw_attributes, namespace_declarations)
4274    }
4275
4276    pub fn to_xml(&self) -> Result<Vec<u8>> {
4277        self.to_xml_for_plot(self.uses_scatter_wrappers)
4278    }
4279
4280    fn to_xml_for_plot(&self, scatter: bool) -> Result<Vec<u8>> {
4281        if self.opaque_name && self.name.is_some() {
4282            return Err(ChartError::DuplicateElement("c:tx".to_owned()));
4283        }
4284        if self.opaque_categories && self.categories.is_some() {
4285            return Err(ChartError::DuplicateElement("c:cat".to_owned()));
4286        }
4287        if self.opaque_bubble_size && self.bubble_size.is_some() {
4288            return Err(ChartError::DuplicateElement("c:bubbleSize".to_owned()));
4289        }
4290        self.values.validate()?;
4291        if let Some(name) = &self.name {
4292            name.validate()?;
4293        }
4294        if let Some(categories) = &self.categories {
4295            if scatter && !matches!(categories, AxisData::Numeric(_)) {
4296                return Err(ChartError::InvalidValue {
4297                    element: "c:xVal".to_owned(),
4298                    value: "scatter x values must use a numeric cache".to_owned(),
4299                });
4300            }
4301            match categories {
4302                AxisData::String(reference) => reference.validate()?,
4303                AxisData::Numeric(reference) => reference.validate()?,
4304            }
4305        }
4306        if scatter && self.categories.is_none() {
4307            return Err(ChartError::MissingElement("c:xVal".to_owned()));
4308        }
4309        if let Some(size) = &self.bubble_size {
4310            size.validate()?;
4311        }
4312        if let Some(labels) = &self.data_labels {
4313            labels.validate()?;
4314        }
4315        let mut writer = Writer::new(Vec::new());
4316        let mut start = BytesStart::new("c:ser");
4317        start.push_attribute(("xmlns:c", C_NS));
4318        start.push_attribute(("xmlns:a", A_NS));
4319        start.push_attribute(("xmlns:r", R_NS));
4320        push_attributes(&mut start, &self.namespace_declarations);
4321        push_attributes(&mut start, &self.raw_attributes);
4322        writer
4323            .write_event(Event::Start(start))
4324            .map_err(OxmlError::from)?;
4325        emit_raw(&mut writer, self.raw_children.at(0))?;
4326        write_scalar(
4327            &mut writer,
4328            "c:idx",
4329            &self.index.to_string(),
4330            Some(&self.index_markup),
4331        )?;
4332        emit_raw(&mut writer, self.raw_children.at(1))?;
4333        write_scalar(
4334            &mut writer,
4335            "c:order",
4336            &self.order.to_string(),
4337            Some(&self.order_markup),
4338        )?;
4339        emit_raw(&mut writer, self.raw_children.at(2))?;
4340        if let Some(name) = &self.name {
4341            write_wrapper_start(
4342                &mut writer,
4343                "c:tx",
4344                self.name_markup
4345                    .as_ref()
4346                    .unwrap_or(&WrapperMarkup::default()),
4347            )?;
4348            name.write_xml(&mut writer, false)?;
4349            write_wrapper_end(
4350                &mut writer,
4351                "c:tx",
4352                self.name_markup
4353                    .as_ref()
4354                    .unwrap_or(&WrapperMarkup::default()),
4355            )?;
4356        }
4357        emit_raw(&mut writer, self.raw_children.at(3))?;
4358        if let Some(properties) = &self.sp_pr {
4359            properties.write_xml_as(&mut writer, "c:spPr")?;
4360        }
4361        emit_raw(&mut writer, self.raw_children.at(4))?;
4362        for point in &self.authored_data_points {
4363            point.write_xml(&mut writer)?;
4364        }
4365        if let Some(labels) = &self.data_labels {
4366            labels.write_xml(&mut writer, false)?;
4367        }
4368        emit_raw(&mut writer, self.raw_children.at(5))?;
4369        if let Some(categories) = &self.categories {
4370            let default_markup = WrapperMarkup::default();
4371            let markup = self.categories_markup.as_ref().unwrap_or(&default_markup);
4372            write_wrapper_start(
4373                &mut writer,
4374                if scatter { "c:xVal" } else { "c:cat" },
4375                markup,
4376            )?;
4377            match categories {
4378                AxisData::String(reference) => reference.write_xml(&mut writer, false)?,
4379                AxisData::Numeric(reference) => reference.write_xml(&mut writer, false)?,
4380            }
4381            write_wrapper_end(
4382                &mut writer,
4383                if scatter { "c:xVal" } else { "c:cat" },
4384                markup,
4385            )?;
4386        }
4387        emit_raw(&mut writer, self.raw_children.at(6))?;
4388        write_wrapper_start(
4389            &mut writer,
4390            if scatter { "c:yVal" } else { "c:val" },
4391            &self.values_markup,
4392        )?;
4393        self.values.write_xml(&mut writer, false)?;
4394        write_wrapper_end(
4395            &mut writer,
4396            if scatter { "c:yVal" } else { "c:val" },
4397            &self.values_markup,
4398        )?;
4399        emit_raw(&mut writer, self.raw_children.at(7))?;
4400        if let Some(size) = &self.bubble_size {
4401            let default_markup = WrapperMarkup::default();
4402            let markup = self.bubble_size_markup.as_ref().unwrap_or(&default_markup);
4403            write_wrapper_start(&mut writer, "c:bubbleSize", markup)?;
4404            size.write_xml(&mut writer, false)?;
4405            write_wrapper_end(&mut writer, "c:bubbleSize", markup)?;
4406        }
4407        emit_raw(&mut writer, self.raw_children.at(8))?;
4408        writer
4409            .write_event(Event::End(BytesEnd::new("c:ser")))
4410            .map_err(OxmlError::from)?;
4411        Ok(writer.into_inner())
4412    }
4413
4414    pub fn raw_children(&self) -> &OrderedRawChildren {
4415        &self.raw_children
4416    }
4417}
4418
4419#[derive(Default)]
4420struct SeriesParseState {
4421    index: Option<(u32, ScalarMarkup)>,
4422    order: Option<(u32, ScalarMarkup)>,
4423    name: Option<(StringRef, WrapperMarkup)>,
4424    categories: Option<(AxisData, WrapperMarkup)>,
4425    values: Option<(NumericData, WrapperMarkup)>,
4426    bubble_size: Option<(NumericData, WrapperMarkup)>,
4427    data_labels: Option<CT_DLbls>,
4428    name_seen: bool,
4429    categories_seen: bool,
4430    bubble_size_seen: bool,
4431    opaque_name: bool,
4432    opaque_categories: bool,
4433    opaque_bubble_size: bool,
4434    saw_standard_wrapper: bool,
4435    saw_scatter_wrapper: bool,
4436    sp_pr: Option<CT_ShapeProperties>,
4437    raw_children: OrderedRawChildren,
4438    boundary: usize,
4439}
4440
4441impl SeriesParseState {
4442    fn capture_event(&mut self, raw: Vec<u8>) {
4443        self.raw_children.push(self.boundary, raw);
4444    }
4445
4446    fn parse_child(
4447        &mut self,
4448        name: &[u8],
4449        raw: Vec<u8>,
4450        namespaces: &NamespaceBindings,
4451    ) -> Result<()> {
4452        match name {
4453            b"idx" => {
4454                set_once(&mut self.index, parse_u32_scalar(&raw, "idx")?, "c:idx")?;
4455                self.boundary = self.boundary.max(1);
4456            }
4457            b"order" => {
4458                set_once(&mut self.order, parse_u32_scalar(&raw, "order")?, "c:order")?;
4459                self.boundary = self.boundary.max(2);
4460            }
4461            b"tx" => {
4462                mark_once(&mut self.name_seen, "c:tx")?;
4463                let parsed = parse_wrapper(&raw, b"tx", &[b"strRef"], namespaces)?;
4464                if let Some((_, reference)) = parsed.choice {
4465                    self.name = Some((
4466                        StringRef::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4467                        parsed.markup,
4468                    ));
4469                } else {
4470                    self.opaque_name = true;
4471                    self.raw_children.push(2, raw);
4472                }
4473                self.boundary = self.boundary.max(3);
4474            }
4475            b"spPr" => {
4476                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
4477                let properties = CT_ShapeProperties::from_xml(&raw)?;
4478                let mut writer = Writer::new(Vec::new());
4479                properties.write_xml_as(&mut writer, "c:spPr")?;
4480                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
4481                set_once(&mut self.sp_pr, properties, "c:spPr")?;
4482                self.boundary = self.boundary.max(4);
4483            }
4484            b"dLbls" => {
4485                set_once(
4486                    &mut self.data_labels,
4487                    CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
4488                    "c:dLbls",
4489                )?;
4490                self.boundary = self.boundary.max(5);
4491            }
4492            b"cat" => {
4493                self.saw_standard_wrapper = true;
4494                mark_once(&mut self.categories_seen, "c:cat")?;
4495                let parsed = parse_wrapper(&raw, b"cat", &[b"strRef", b"numRef"], namespaces)?;
4496                if let Some((choice, reference)) = parsed.choice {
4497                    let categories = if choice == b"strRef" {
4498                        AxisData::String(StringRef::from_xml_with_namespaces(
4499                            &reference,
4500                            &parsed.namespaces,
4501                        )?)
4502                    } else {
4503                        AxisData::Numeric(NumericData::from_xml_with_namespaces(
4504                            &reference,
4505                            &parsed.namespaces,
4506                        )?)
4507                    };
4508                    self.categories = Some((categories, parsed.markup));
4509                } else {
4510                    self.opaque_categories = true;
4511                    self.raw_children.push(5, raw);
4512                }
4513                self.boundary = self.boundary.max(6);
4514            }
4515            b"xVal" => {
4516                self.saw_scatter_wrapper = true;
4517                mark_once(&mut self.categories_seen, "c:xVal")?;
4518                let parsed = parse_wrapper(&raw, b"xVal", &[b"numRef"], namespaces)?;
4519                let (_, reference) = parsed
4520                    .choice
4521                    .ok_or_else(|| ChartError::MissingElement("c:xVal/c:numRef".to_owned()))?;
4522                self.categories = Some((
4523                    AxisData::Numeric(NumericData::from_xml_with_namespaces(
4524                        &reference,
4525                        &parsed.namespaces,
4526                    )?),
4527                    parsed.markup,
4528                ));
4529                self.boundary = self.boundary.max(6);
4530            }
4531            b"val" | b"yVal" => {
4532                if name == b"yVal" {
4533                    self.saw_scatter_wrapper = true;
4534                } else {
4535                    self.saw_standard_wrapper = true;
4536                }
4537                let wrapper = if name == b"yVal" {
4538                    b"yVal".as_slice()
4539                } else {
4540                    b"val".as_slice()
4541                };
4542                let parsed = parse_wrapper(&raw, wrapper, &[b"numRef"], namespaces)?;
4543                let (_, reference) = parsed.choice.ok_or_else(|| {
4544                    ChartError::MissingElement(format!(
4545                        "c:{}/c:numRef",
4546                        String::from_utf8_lossy(wrapper)
4547                    ))
4548                })?;
4549                set_once(
4550                    &mut self.values,
4551                    (
4552                        NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4553                        parsed.markup,
4554                    ),
4555                    "c:val or c:yVal",
4556                )?;
4557                self.boundary = self.boundary.max(7);
4558            }
4559            b"bubbleSize" => {
4560                mark_once(&mut self.bubble_size_seen, "c:bubbleSize")?;
4561                let parsed = parse_wrapper(&raw, b"bubbleSize", &[b"numRef"], namespaces)?;
4562                if let Some((_, reference)) = parsed.choice {
4563                    self.bubble_size = Some((
4564                        NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4565                        parsed.markup,
4566                    ));
4567                } else {
4568                    self.opaque_bubble_size = true;
4569                    self.raw_children.push(7, raw);
4570                }
4571                self.boundary = self.boundary.max(8);
4572            }
4573            _ => {
4574                let boundary = series_raw_boundary(name, self.boundary);
4575                self.raw_children.push(boundary, raw);
4576                self.boundary = self.boundary.max(boundary);
4577            }
4578        }
4579        Ok(())
4580    }
4581
4582    fn finish(
4583        self,
4584        raw_attributes: XmlAttributes,
4585        namespace_declarations: XmlAttributes,
4586    ) -> Result<Series> {
4587        if self.saw_standard_wrapper && self.saw_scatter_wrapper {
4588            return Err(ChartError::InvalidValue {
4589                element: "c:ser".to_owned(),
4590                value: "category/value and x/y wrappers cannot be combined".to_owned(),
4591            });
4592        }
4593        let (index, index_markup) = self
4594            .index
4595            .ok_or_else(|| ChartError::MissingElement("c:idx".to_owned()))?;
4596        let (order, order_markup) = self
4597            .order
4598            .ok_or_else(|| ChartError::MissingElement("c:order".to_owned()))?;
4599        let (values, values_markup) = self
4600            .values
4601            .ok_or_else(|| ChartError::MissingElement("c:val".to_owned()))?;
4602        let (name, name_markup) = self
4603            .name
4604            .map(|(value, markup)| (Some(value), Some(markup)))
4605            .unwrap_or((None, None));
4606        let (categories, categories_markup) = self
4607            .categories
4608            .map(|(value, markup)| (Some(value), Some(markup)))
4609            .unwrap_or((None, None));
4610        let (bubble_size, bubble_size_markup) = self
4611            .bubble_size
4612            .map(|(value, markup)| (Some(value), Some(markup)))
4613            .unwrap_or((None, None));
4614        Ok(Series {
4615            index,
4616            order,
4617            name,
4618            categories,
4619            values,
4620            bubble_size,
4621            sp_pr: self.sp_pr,
4622            data_labels: self.data_labels,
4623            authored_data_points: Vec::new(),
4624            index_markup,
4625            order_markup,
4626            name_markup,
4627            categories_markup,
4628            values_markup,
4629            bubble_size_markup,
4630            opaque_name: self.opaque_name,
4631            opaque_categories: self.opaque_categories,
4632            opaque_bubble_size: self.opaque_bubble_size,
4633            uses_scatter_wrappers: self.saw_scatter_wrapper,
4634            parsed_from_xml: true,
4635            raw_attributes,
4636            namespace_declarations,
4637            raw_children: raw_children_in_schema_order(&self.raw_children, 8),
4638        })
4639    }
4640}
4641
4642fn series_raw_boundary(name: &[u8], current: usize) -> usize {
4643    match name {
4644        b"marker" | b"invertIfNegative" | b"pictureOptions" | b"explosion" | b"dPt" => 4,
4645        b"trendline" | b"errBars" => 5,
4646        b"shape" | b"smooth" => 7,
4647        b"extLst" => 8,
4648        _ => current,
4649    }
4650}
4651
4652struct ParsedReference {
4653    formula: String,
4654    format_code: Option<String>,
4655    values: Vec<String>,
4656    markup: ReferenceMarkup,
4657}
4658
4659fn parse_reference(
4660    xml: &[u8],
4661    reference_local: &[u8],
4662    cache_local: &[u8],
4663    numeric: bool,
4664    inherited: &NamespaceBindings,
4665) -> Result<ParsedReference> {
4666    let mut reader = Reader::from_reader(xml);
4667    let mut buffer = Vec::new();
4668    loop {
4669        match reader
4670            .read_event_into(&mut buffer)
4671            .map_err(OxmlError::from)?
4672        {
4673            Event::Start(element)
4674                if matches_local_name(element.name().as_ref(), reference_local) =>
4675            {
4676                let namespaces = typed_rewrite_bindings(&element, inherited)?;
4677                let raw_attributes = capture_fixed_attributes(&element, &["xmlns:c"])?;
4678                let mut formula: Option<(String, TextMarkup)> = None;
4679                let mut cache: Option<(Option<String>, Vec<String>, CacheMarkup)> = None;
4680                let mut raw_children = OrderedRawChildren::default();
4681                let mut boundary = 0usize;
4682                let mut inner = Vec::new();
4683                loop {
4684                    match reader
4685                        .read_event_into(&mut inner)
4686                        .map_err(OxmlError::from)?
4687                    {
4688                        Event::Start(child) => {
4689                            let name = chart_child_local(&child, &namespaces)?;
4690                            let raw = capture_element(&mut reader, &child)?;
4691                            match name.as_deref().unwrap_or_default() {
4692                                b"f" => {
4693                                    set_once(
4694                                        &mut formula,
4695                                        parse_text_element(&raw, b"f", &namespaces)?,
4696                                        "c:f",
4697                                    )?;
4698                                    boundary = boundary.max(1);
4699                                }
4700                                name if name == cache_local => {
4701                                    set_once(
4702                                        &mut cache,
4703                                        parse_cache(&raw, cache_local, numeric, &namespaces)?,
4704                                        &format!("c:{}", String::from_utf8_lossy(cache_local)),
4705                                    )?;
4706                                    boundary = boundary.max(2);
4707                                }
4708                                _ => raw_children.push(boundary, raw),
4709                            }
4710                        }
4711                        Event::Empty(child) => {
4712                            let name = chart_child_local(&child, &namespaces)?;
4713                            let raw = capture_empty_element(&child)?;
4714                            match name.as_deref().unwrap_or_default() {
4715                                b"f" => {
4716                                    set_once(
4717                                        &mut formula,
4718                                        parse_text_element(&raw, b"f", &namespaces)?,
4719                                        "c:f",
4720                                    )?;
4721                                    boundary = boundary.max(1);
4722                                }
4723                                name if name == cache_local => {
4724                                    set_once(
4725                                        &mut cache,
4726                                        parse_cache(&raw, cache_local, numeric, &namespaces)?,
4727                                        &format!("c:{}", String::from_utf8_lossy(cache_local)),
4728                                    )?;
4729                                    boundary = boundary.max(2);
4730                                }
4731                                _ => raw_children.push(boundary, raw),
4732                            }
4733                        }
4734                        event @ (Event::Text(_)
4735                        | Event::CData(_)
4736                        | Event::Comment(_)
4737                        | Event::PI(_)
4738                        | Event::GeneralRef(_)) => {
4739                            raw_children.push(boundary, capture_event(event)?);
4740                        }
4741                        Event::End(end)
4742                            if matches_local_name(end.name().as_ref(), reference_local) =>
4743                        {
4744                            let (formula, formula_markup) = formula
4745                                .ok_or_else(|| ChartError::MissingElement("c:f".to_owned()))?;
4746                            validate_formula(&formula, "c:f")?;
4747                            let (format_code, values, cache_markup) = cache.ok_or_else(|| {
4748                                ChartError::MissingElement(format!(
4749                                    "c:{}",
4750                                    String::from_utf8_lossy(cache_local)
4751                                ))
4752                            })?;
4753                            let markup = ReferenceMarkup {
4754                                raw_attributes,
4755                                raw_children: raw_children_in_schema_order(&raw_children, 2),
4756                                formula: formula_markup,
4757                                cache_attributes: cache_markup.raw_attributes,
4758                                cache_children: cache_markup.raw_children,
4759                                format_code: cache_markup.format_code,
4760                                format_code_omitted: cache_markup.format_code_omitted,
4761                                point_count: cache_markup.point_count,
4762                                declared_point_count: cache_markup.declared_point_count,
4763                                point_indexes: cache_markup.point_indexes,
4764                                points: cache_markup.points,
4765                            };
4766                            return Ok(ParsedReference {
4767                                formula,
4768                                format_code,
4769                                values,
4770                                markup,
4771                            });
4772                        }
4773                        Event::Eof => {
4774                            return Err(missing_end(&format!(
4775                                "c:{}",
4776                                String::from_utf8_lossy(reference_local)
4777                            )));
4778                        }
4779                        _ => {}
4780                    }
4781                    inner.clear();
4782                }
4783            }
4784            Event::Empty(element)
4785                if matches_local_name(element.name().as_ref(), reference_local) =>
4786            {
4787                return Err(ChartError::MissingElement("c:f".to_owned()));
4788            }
4789            Event::Start(element) | Event::Empty(element) => {
4790                return Err(ChartError::UnexpectedElement(element_name(&element)));
4791            }
4792            Event::Eof => {
4793                return Err(ChartError::MissingElement(format!(
4794                    "c:{}",
4795                    String::from_utf8_lossy(reference_local)
4796                )));
4797            }
4798            _ => {}
4799        }
4800        buffer.clear();
4801    }
4802}
4803
4804struct CacheMarkup {
4805    raw_attributes: XmlAttributes,
4806    raw_children: OrderedRawChildren,
4807    format_code: Option<TextMarkup>,
4808    format_code_omitted: bool,
4809    point_count: ScalarMarkup,
4810    declared_point_count: Option<u32>,
4811    point_indexes: Vec<u32>,
4812    points: Vec<PointMarkup>,
4813}
4814
4815fn parse_cache(
4816    xml: &[u8],
4817    cache_local: &[u8],
4818    numeric: bool,
4819    inherited: &NamespaceBindings,
4820) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
4821    let mut reader = Reader::from_reader(xml);
4822    let mut buffer = Vec::new();
4823    loop {
4824        match reader
4825            .read_event_into(&mut buffer)
4826            .map_err(OxmlError::from)?
4827        {
4828            Event::Start(element) if matches_local_name(element.name().as_ref(), cache_local) => {
4829                let namespaces = typed_rewrite_bindings(&element, inherited)?;
4830                let raw_attributes = capture_attributes(&element)?;
4831                let mut state = CacheParseState::new(numeric, namespaces);
4832                let mut inner = Vec::new();
4833                loop {
4834                    match reader
4835                        .read_event_into(&mut inner)
4836                        .map_err(OxmlError::from)?
4837                    {
4838                        Event::Start(child) => {
4839                            let name = chart_child_local(&child, &state.namespaces)?;
4840                            let raw = capture_element(&mut reader, &child)?;
4841                            state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
4842                        }
4843                        Event::Empty(child) => {
4844                            let name = chart_child_local(&child, &state.namespaces)?;
4845                            let raw = capture_empty_element(&child)?;
4846                            state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
4847                        }
4848                        event @ (Event::Text(_)
4849                        | Event::CData(_)
4850                        | Event::Comment(_)
4851                        | Event::PI(_)
4852                        | Event::GeneralRef(_)) => {
4853                            state.capture_event(capture_event(event)?);
4854                        }
4855                        Event::End(end) if matches_local_name(end.name().as_ref(), cache_local) => {
4856                            return state.finish(raw_attributes);
4857                        }
4858                        Event::Eof => {
4859                            return Err(missing_end(&format!(
4860                                "c:{}",
4861                                String::from_utf8_lossy(cache_local)
4862                            )));
4863                        }
4864                        _ => {}
4865                    }
4866                    inner.clear();
4867                }
4868            }
4869            Event::Empty(element) if matches_local_name(element.name().as_ref(), cache_local) => {
4870                return Err(ChartError::MissingElement("c:ptCount".to_owned()));
4871            }
4872            Event::Start(element) | Event::Empty(element) => {
4873                return Err(ChartError::UnexpectedElement(element_name(&element)));
4874            }
4875            Event::Eof => {
4876                return Err(ChartError::MissingElement(format!(
4877                    "c:{}",
4878                    String::from_utf8_lossy(cache_local)
4879                )));
4880            }
4881            _ => {}
4882        }
4883        buffer.clear();
4884    }
4885}
4886
4887struct CacheParseState {
4888    numeric: bool,
4889    namespaces: NamespaceBindings,
4890    point_base: usize,
4891    boundary: usize,
4892    format_code: Option<(String, TextMarkup)>,
4893    point_count: Option<(u32, ScalarMarkup)>,
4894    points: Vec<(u32, String, PointMarkup)>,
4895    raw_children: OrderedRawChildren,
4896}
4897
4898impl CacheParseState {
4899    fn new(numeric: bool, namespaces: NamespaceBindings) -> Self {
4900        Self {
4901            numeric,
4902            namespaces,
4903            point_base: usize::from(numeric) + 1,
4904            boundary: 0,
4905            format_code: None,
4906            point_count: None,
4907            points: Vec::new(),
4908            raw_children: OrderedRawChildren::default(),
4909        }
4910    }
4911
4912    fn capture_event(&mut self, raw: Vec<u8>) {
4913        self.raw_children.push(self.boundary, raw);
4914    }
4915
4916    fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
4917        match name {
4918            b"formatCode" if self.numeric => {
4919                set_once(
4920                    &mut self.format_code,
4921                    parse_text_element(&raw, b"formatCode", &self.namespaces)?,
4922                    "c:formatCode",
4923                )?;
4924                self.boundary = self.boundary.max(1);
4925            }
4926            b"ptCount" => {
4927                set_once(
4928                    &mut self.point_count,
4929                    parse_u32_scalar(&raw, "ptCount")?,
4930                    "c:ptCount",
4931                )?;
4932                self.boundary = self.boundary.max(self.point_base);
4933            }
4934            b"pt" => {
4935                self.points.push(parse_point(&raw, &self.namespaces)?);
4936                self.boundary = self.boundary.max(self.point_base + self.points.len());
4937            }
4938            _ => self.raw_children.push(self.boundary, raw),
4939        }
4940        Ok(())
4941    }
4942
4943    fn finish(
4944        self,
4945        raw_attributes: XmlAttributes,
4946    ) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
4947        let (declared, point_count_markup) = self
4948            .point_count
4949            .ok_or_else(|| ChartError::MissingElement("c:ptCount".to_owned()))?;
4950        let actual = u32::try_from(self.points.len()).map_err(|_| ChartError::InvalidValue {
4951            element: "c:ptCount".to_owned(),
4952            value: self.points.len().to_string(),
4953        })?;
4954        if declared < actual {
4955            return Err(invalid_attribute("ptCount", "val", declared.to_string()));
4956        }
4957        let mut values = Vec::with_capacity(self.points.len());
4958        let mut point_indexes = Vec::with_capacity(self.points.len());
4959        let mut point_markup = Vec::with_capacity(self.points.len());
4960        let mut previous = None;
4961        for (index, value, markup) in self.points {
4962            if index >= declared || previous.is_some_and(|last| index <= last) {
4963                return Err(invalid_attribute("pt", "idx", index.to_string()));
4964            }
4965            previous = Some(index);
4966            values.push(value);
4967            point_indexes.push(index);
4968            point_markup.push(markup);
4969        }
4970        let (format_code, format_markup) = self
4971            .format_code
4972            .map(|(value, markup)| (Some(value), Some(markup)))
4973            .unwrap_or((None, None));
4974        let format_code_omitted = self.numeric && format_code.is_none();
4975        Ok((
4976            format_code,
4977            values,
4978            CacheMarkup {
4979                raw_attributes,
4980                raw_children: raw_children_in_schema_order(
4981                    &self.raw_children,
4982                    self.point_base + point_markup.len(),
4983                ),
4984                format_code: format_markup,
4985                format_code_omitted,
4986                point_count: point_count_markup,
4987                declared_point_count: Some(declared),
4988                point_indexes,
4989                points: point_markup,
4990            },
4991        ))
4992    }
4993}
4994
4995fn parse_point(xml: &[u8], inherited: &NamespaceBindings) -> Result<(u32, String, PointMarkup)> {
4996    let mut reader = Reader::from_reader(xml);
4997    let mut buffer = Vec::new();
4998    loop {
4999        match reader
5000            .read_event_into(&mut buffer)
5001            .map_err(OxmlError::from)?
5002        {
5003            Event::Start(element) if matches_local_name(element.name().as_ref(), b"pt") => {
5004                let namespaces = typed_rewrite_bindings(&element, inherited)?;
5005                let index = required_u32_attribute(&element, "pt", b"idx")?;
5006                let raw_attributes = capture_attributes_except(&element, b"idx")?;
5007                let mut value: Option<(String, TextMarkup)> = None;
5008                let mut raw_children = OrderedRawChildren::default();
5009                let mut boundary = 0usize;
5010                let mut inner = Vec::new();
5011                loop {
5012                    match reader
5013                        .read_event_into(&mut inner)
5014                        .map_err(OxmlError::from)?
5015                    {
5016                        Event::Start(child) => {
5017                            let name = chart_child_local(&child, &namespaces)?;
5018                            let raw = capture_element(&mut reader, &child)?;
5019                            if name.as_deref() == Some(b"v") {
5020                                set_once(
5021                                    &mut value,
5022                                    parse_text_element(&raw, b"v", &namespaces)?,
5023                                    "c:v",
5024                                )?;
5025                                boundary = 1;
5026                            } else {
5027                                raw_children.push(boundary, raw);
5028                            }
5029                        }
5030                        Event::Empty(child) => {
5031                            let name = chart_child_local(&child, &namespaces)?;
5032                            let raw = capture_empty_element(&child)?;
5033                            if name.as_deref() == Some(b"v") {
5034                                set_once(
5035                                    &mut value,
5036                                    parse_text_element(&raw, b"v", &namespaces)?,
5037                                    "c:v",
5038                                )?;
5039                                boundary = 1;
5040                            } else {
5041                                raw_children.push(boundary, raw);
5042                            }
5043                        }
5044                        event @ (Event::Text(_)
5045                        | Event::CData(_)
5046                        | Event::Comment(_)
5047                        | Event::PI(_)
5048                        | Event::GeneralRef(_)) => {
5049                            raw_children.push(boundary, capture_event(event)?);
5050                        }
5051                        Event::End(end) if matches_local_name(end.name().as_ref(), b"pt") => {
5052                            let (value, value_markup) = value
5053                                .ok_or_else(|| ChartError::MissingElement("c:v".to_owned()))?;
5054                            return Ok((
5055                                index,
5056                                value,
5057                                PointMarkup {
5058                                    raw_attributes,
5059                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
5060                                    value: value_markup,
5061                                },
5062                            ));
5063                        }
5064                        Event::Eof => return Err(missing_end("c:pt")),
5065                        _ => {}
5066                    }
5067                    inner.clear();
5068                }
5069            }
5070            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pt") => {
5071                return Err(ChartError::MissingElement("c:v".to_owned()));
5072            }
5073            Event::Start(element) | Event::Empty(element) => {
5074                return Err(ChartError::UnexpectedElement(element_name(&element)));
5075            }
5076            Event::Eof => return Err(ChartError::MissingElement("c:pt".to_owned())),
5077            _ => {}
5078        }
5079        buffer.clear();
5080    }
5081}
5082
5083fn parse_text_element(
5084    xml: &[u8],
5085    local: &[u8],
5086    inherited: &NamespaceBindings,
5087) -> Result<(String, TextMarkup)> {
5088    let mut reader = Reader::from_reader(xml);
5089    let mut buffer = Vec::new();
5090    loop {
5091        match reader
5092            .read_event_into(&mut buffer)
5093            .map_err(OxmlError::from)?
5094        {
5095            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
5096                typed_rewrite_bindings(&element, inherited)?;
5097                let raw_attributes = capture_attributes(&element)?;
5098                let mut value = String::new();
5099                let mut raw_children = OrderedRawChildren::default();
5100                let mut seen_text = false;
5101                let mut inner = Vec::new();
5102                loop {
5103                    match reader
5104                        .read_event_into(&mut inner)
5105                        .map_err(OxmlError::from)?
5106                    {
5107                        Event::Text(text) => {
5108                            value.push_str(&decode_plain(&text));
5109                            seen_text = true;
5110                        }
5111                        Event::CData(text) => {
5112                            let decoded =
5113                                text.decode().map_err(|error| ChartError::InvalidValue {
5114                                    element: format!("c:{}", String::from_utf8_lossy(local)),
5115                                    value: error.to_string(),
5116                                })?;
5117                            value.push_str(&decoded);
5118                            seen_text = true;
5119                        }
5120                        Event::GeneralRef(reference) => {
5121                            value.push_str(&resolve_entity(&reference));
5122                            seen_text = true;
5123                        }
5124                        event @ (Event::Comment(_) | Event::PI(_)) => {
5125                            raw_children.push(usize::from(seen_text), capture_event(event)?);
5126                        }
5127                        Event::Start(child) | Event::Empty(child) => {
5128                            return Err(ChartError::UnexpectedElement(element_name(&child)));
5129                        }
5130                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
5131                            return Ok((
5132                                value,
5133                                TextMarkup {
5134                                    raw_attributes,
5135                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
5136                                },
5137                            ));
5138                        }
5139                        Event::Eof => {
5140                            return Err(missing_end(&format!(
5141                                "c:{}",
5142                                String::from_utf8_lossy(local)
5143                            )));
5144                        }
5145                        _ => {}
5146                    }
5147                    inner.clear();
5148                }
5149            }
5150            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
5151                typed_rewrite_bindings(&element, inherited)?;
5152                return Ok((
5153                    String::new(),
5154                    TextMarkup {
5155                        raw_attributes: capture_attributes(&element)?,
5156                        raw_children: OrderedRawChildren::default(),
5157                    },
5158                ));
5159            }
5160            Event::Start(element) | Event::Empty(element) => {
5161                return Err(ChartError::UnexpectedElement(element_name(&element)));
5162            }
5163            Event::Eof => {
5164                return Err(ChartError::MissingElement(format!(
5165                    "c:{}",
5166                    String::from_utf8_lossy(local)
5167                )));
5168            }
5169            _ => {}
5170        }
5171        buffer.clear();
5172    }
5173}
5174
5175struct ParsedWrapper {
5176    choice: Option<(Vec<u8>, Vec<u8>)>,
5177    markup: WrapperMarkup,
5178    namespaces: NamespaceBindings,
5179}
5180
5181fn parse_wrapper(
5182    xml: &[u8],
5183    local: &[u8],
5184    choices: &[&[u8]],
5185    inherited: &NamespaceBindings,
5186) -> Result<ParsedWrapper> {
5187    let mut reader = Reader::from_reader(xml);
5188    let mut buffer = Vec::new();
5189    loop {
5190        match reader
5191            .read_event_into(&mut buffer)
5192            .map_err(OxmlError::from)?
5193        {
5194            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
5195                let namespaces = typed_rewrite_bindings(&element, inherited)?;
5196                let raw_attributes = capture_attributes(&element)?;
5197                let mut choice = None;
5198                let mut raw_children = OrderedRawChildren::default();
5199                let mut boundary = 0usize;
5200                let mut inner = Vec::new();
5201                loop {
5202                    match reader
5203                        .read_event_into(&mut inner)
5204                        .map_err(OxmlError::from)?
5205                    {
5206                        Event::Start(child) => {
5207                            let name = chart_child_local(&child, &namespaces)?;
5208                            let raw = capture_element(&mut reader, &child)?;
5209                            if let Some(name) =
5210                                name.filter(|name| choices.contains(&name.as_slice()))
5211                            {
5212                                if choice.is_some() {
5213                                    return Err(ChartError::DuplicateElement(format!(
5214                                        "c:{} reference",
5215                                        String::from_utf8_lossy(local)
5216                                    )));
5217                                }
5218                                choice = Some((name, raw));
5219                                boundary = 1;
5220                            } else {
5221                                raw_children.push(boundary, raw);
5222                            }
5223                        }
5224                        Event::Empty(child) => {
5225                            let name = chart_child_local(&child, &namespaces)?;
5226                            let raw = capture_empty_element(&child)?;
5227                            if let Some(name) =
5228                                name.filter(|name| choices.contains(&name.as_slice()))
5229                            {
5230                                if choice.is_some() {
5231                                    return Err(ChartError::DuplicateElement(format!(
5232                                        "c:{} reference",
5233                                        String::from_utf8_lossy(local)
5234                                    )));
5235                                }
5236                                choice = Some((name, raw));
5237                                boundary = 1;
5238                            } else {
5239                                raw_children.push(boundary, raw);
5240                            }
5241                        }
5242                        event @ (Event::Text(_)
5243                        | Event::CData(_)
5244                        | Event::Comment(_)
5245                        | Event::PI(_)
5246                        | Event::GeneralRef(_)) => {
5247                            raw_children.push(boundary, capture_event(event)?);
5248                        }
5249                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
5250                            return Ok(ParsedWrapper {
5251                                choice,
5252                                markup: WrapperMarkup {
5253                                    raw_attributes,
5254                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
5255                                },
5256                                namespaces,
5257                            });
5258                        }
5259                        Event::Eof => {
5260                            return Err(missing_end(&format!(
5261                                "c:{}",
5262                                String::from_utf8_lossy(local)
5263                            )));
5264                        }
5265                        _ => {}
5266                    }
5267                    inner.clear();
5268                }
5269            }
5270            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
5271                let namespaces = chart_bindings(inherited, &element)?;
5272                return Ok(ParsedWrapper {
5273                    choice: None,
5274                    markup: WrapperMarkup {
5275                        raw_attributes: capture_attributes(&element)?,
5276                        raw_children: OrderedRawChildren::default(),
5277                    },
5278                    namespaces,
5279                });
5280            }
5281            Event::Start(element) | Event::Empty(element) => {
5282                return Err(ChartError::UnexpectedElement(element_name(&element)));
5283            }
5284            Event::Eof => {
5285                return Err(ChartError::MissingElement(format!(
5286                    "c:{}",
5287                    String::from_utf8_lossy(local)
5288                )));
5289            }
5290            _ => {}
5291        }
5292        buffer.clear();
5293    }
5294}
5295
5296fn write_string_cache(
5297    writer: &mut Writer<Vec<u8>>,
5298    values: &[String],
5299    markup: &ReferenceMarkup,
5300) -> Result<()> {
5301    let (declared_count, indexes) = cache_layout(markup, values.len())?;
5302    let mut start = BytesStart::new("c:strCache");
5303    push_attributes(&mut start, &markup.cache_attributes);
5304    writer
5305        .write_event(Event::Start(start))
5306        .map_err(OxmlError::from)?;
5307    emit_raw(writer, markup.cache_children.at(0))?;
5308    write_scalar(
5309        writer,
5310        "c:ptCount",
5311        &declared_count.to_string(),
5312        Some(&markup.point_count),
5313    )?;
5314    emit_cache_point_raw(writer, markup, 1, values.len(), 0)?;
5315    for (position, value) in values.iter().enumerate() {
5316        let default_markup = PointMarkup::default();
5317        write_point(
5318            writer,
5319            indexes[position],
5320            value,
5321            markup.points.get(position).unwrap_or(&default_markup),
5322        )?;
5323        emit_cache_point_raw(writer, markup, 1, values.len(), position + 1)?;
5324    }
5325    writer
5326        .write_event(Event::End(BytesEnd::new("c:strCache")))
5327        .map_err(OxmlError::from)?;
5328    Ok(())
5329}
5330
5331fn write_numeric_cache(
5332    writer: &mut Writer<Vec<u8>>,
5333    data: &NumericData,
5334    markup: &ReferenceMarkup,
5335) -> Result<()> {
5336    let (declared_count, indexes) = cache_layout(markup, data.values.len())?;
5337    let mut start = BytesStart::new("c:numCache");
5338    push_attributes(&mut start, &markup.cache_attributes);
5339    writer
5340        .write_event(Event::Start(start))
5341        .map_err(OxmlError::from)?;
5342    emit_raw(writer, markup.cache_children.at(0))?;
5343    if !markup.format_code_omitted || data.format_code != "General" {
5344        let default_format_markup = TextMarkup::default();
5345        write_text(
5346            writer,
5347            "c:formatCode",
5348            &data.format_code,
5349            markup
5350                .format_code
5351                .as_ref()
5352                .unwrap_or(&default_format_markup),
5353        )?;
5354    }
5355    emit_raw(writer, markup.cache_children.at(1))?;
5356    write_scalar(
5357        writer,
5358        "c:ptCount",
5359        &declared_count.to_string(),
5360        Some(&markup.point_count),
5361    )?;
5362    emit_cache_point_raw(writer, markup, 2, data.values.len(), 0)?;
5363    for (position, value) in data.values.iter().enumerate() {
5364        let default_markup = PointMarkup::default();
5365        write_point(
5366            writer,
5367            indexes[position],
5368            &value.to_string(),
5369            markup.points.get(position).unwrap_or(&default_markup),
5370        )?;
5371        emit_cache_point_raw(writer, markup, 2, data.values.len(), position + 1)?;
5372    }
5373    writer
5374        .write_event(Event::End(BytesEnd::new("c:numCache")))
5375        .map_err(OxmlError::from)?;
5376    Ok(())
5377}
5378
5379fn emit_cache_point_raw(
5380    writer: &mut Writer<Vec<u8>>,
5381    markup: &ReferenceMarkup,
5382    point_base: usize,
5383    current_count: usize,
5384    completed_points: usize,
5385) -> Result<()> {
5386    let original_count = markup.points.len();
5387    if current_count == 0 {
5388        for boundary in point_base..=point_base + original_count {
5389            emit_raw(writer, markup.cache_children.at(boundary))?;
5390        }
5391    } else if completed_points == 0 {
5392        if original_count > 0 {
5393            emit_raw(writer, markup.cache_children.at(point_base))?;
5394        }
5395    } else if completed_points < current_count {
5396        if completed_points < original_count {
5397            emit_raw(
5398                writer,
5399                markup.cache_children.at(point_base + completed_points),
5400            )?;
5401        }
5402    } else {
5403        let first_tail = if original_count == 0 {
5404            point_base
5405        } else {
5406            point_base + current_count.min(original_count)
5407        };
5408        for boundary in first_tail..=point_base + original_count {
5409            emit_raw(writer, markup.cache_children.at(boundary))?;
5410        }
5411    }
5412    Ok(())
5413}
5414
5415fn write_point(
5416    writer: &mut Writer<Vec<u8>>,
5417    index: u32,
5418    value: &str,
5419    markup: &PointMarkup,
5420) -> Result<()> {
5421    let index = index.to_string();
5422    let mut start = BytesStart::new("c:pt");
5423    start.push_attribute(("idx", index.as_str()));
5424    push_attributes(&mut start, &markup.raw_attributes);
5425    writer
5426        .write_event(Event::Start(start))
5427        .map_err(OxmlError::from)?;
5428    emit_raw(writer, markup.raw_children.at(0))?;
5429    write_text(writer, "c:v", value, &markup.value)?;
5430    emit_raw(writer, markup.raw_children.at(1))?;
5431    writer
5432        .write_event(Event::End(BytesEnd::new("c:pt")))
5433        .map_err(OxmlError::from)?;
5434    Ok(())
5435}
5436
5437fn cache_layout(markup: &ReferenceMarkup, value_count: usize) -> Result<(u32, Vec<u32>)> {
5438    if markup.point_indexes.len() == value_count
5439        && let Some(declared) = markup.declared_point_count
5440    {
5441        return Ok((declared, markup.point_indexes.clone()));
5442    }
5443    let declared = u32::try_from(value_count).map_err(|_| ChartError::InvalidValue {
5444        element: "c:ptCount".to_owned(),
5445        value: value_count.to_string(),
5446    })?;
5447    Ok((declared, (0..declared).collect()))
5448}
5449
5450fn write_text(
5451    writer: &mut Writer<Vec<u8>>,
5452    tag: &str,
5453    value: &str,
5454    markup: &TextMarkup,
5455) -> Result<()> {
5456    let mut start = BytesStart::new(tag);
5457    push_attributes(&mut start, &markup.raw_attributes);
5458    writer
5459        .write_event(Event::Start(start))
5460        .map_err(OxmlError::from)?;
5461    emit_raw(writer, markup.raw_children.at(0))?;
5462    writer
5463        .write_event(Event::Text(BytesText::new(value)))
5464        .map_err(OxmlError::from)?;
5465    emit_raw(writer, markup.raw_children.at(1))?;
5466    writer
5467        .write_event(Event::End(BytesEnd::new(tag)))
5468        .map_err(OxmlError::from)?;
5469    Ok(())
5470}
5471
5472fn write_wrapper_start(
5473    writer: &mut Writer<Vec<u8>>,
5474    tag: &str,
5475    markup: &WrapperMarkup,
5476) -> Result<()> {
5477    let mut start = BytesStart::new(tag);
5478    push_attributes(&mut start, &markup.raw_attributes);
5479    writer
5480        .write_event(Event::Start(start))
5481        .map_err(OxmlError::from)?;
5482    emit_raw(writer, markup.raw_children.at(0))
5483}
5484
5485fn write_wrapper_end(
5486    writer: &mut Writer<Vec<u8>>,
5487    tag: &str,
5488    markup: &WrapperMarkup,
5489) -> Result<()> {
5490    emit_raw(writer, markup.raw_children.at(1))?;
5491    writer
5492        .write_event(Event::End(BytesEnd::new(tag)))
5493        .map_err(OxmlError::from)?;
5494    Ok(())
5495}
5496
5497fn validate_formula(formula: &str, element: &str) -> Result<()> {
5498    if formula.trim().is_empty() {
5499        return Err(ChartError::InvalidValue {
5500            element: element.to_owned(),
5501            value: formula.to_owned(),
5502        });
5503    }
5504    validate_xml_text(formula, element)
5505}
5506
5507fn validate_format_code(format_code: &str) -> Result<()> {
5508    if format_code.is_empty() {
5509        return Err(ChartError::InvalidValue {
5510            element: "c:formatCode".to_owned(),
5511            value: format_code.to_owned(),
5512        });
5513    }
5514    validate_xml_text(format_code, "c:formatCode")
5515}
5516
5517fn validate_xml_text(value: &str, element: &str) -> Result<()> {
5518    if value.chars().all(|character| {
5519        matches!(
5520            character,
5521            '\u{9}' | '\u{A}' | '\u{D}' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}'
5522        )
5523    }) {
5524        return Ok(());
5525    }
5526    Err(ChartError::InvalidValue {
5527        element: element.to_owned(),
5528        value: value.to_owned(),
5529    })
5530}
5531
5532fn validate_numeric_values(values: &[f64]) -> Result<()> {
5533    if let Some(value) = values.iter().find(|value| !value.is_finite()) {
5534        return Err(ChartError::InvalidValue {
5535            element: "c:v".to_owned(),
5536            value: value.to_string(),
5537        });
5538    }
5539    Ok(())
5540}
5541
5542fn validate_point_count(count: usize, element: &str) -> Result<()> {
5543    u32::try_from(count)
5544        .map(|_| ())
5545        .map_err(|_| ChartError::InvalidValue {
5546            element: element.to_owned(),
5547            value: count.to_string(),
5548        })
5549}
5550
5551fn parse_u32_scalar(xml: &[u8], local: &str) -> Result<(u32, ScalarMarkup)> {
5552    let (value, markup) = scalar_value(xml, local)?;
5553    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
5554    let parsed = value
5555        .parse::<u32>()
5556        .map_err(|_| invalid_attribute(local, "val", value))?;
5557    Ok((parsed, markup))
5558}
5559
5560fn required_u32_attribute(element: &BytesStart<'_>, local: &str, attribute: &[u8]) -> Result<u32> {
5561    let value = attribute_value(element, attribute)?.ok_or_else(|| {
5562        invalid_attribute(
5563            local,
5564            &String::from_utf8_lossy(attribute),
5565            "<missing>".to_owned(),
5566        )
5567    })?;
5568    value
5569        .parse::<u32>()
5570        .map_err(|_| invalid_attribute(local, &String::from_utf8_lossy(attribute), value))
5571}
5572
5573fn chart_root_prefix(element: &BytesStart<'_>) -> Result<Vec<u8>> {
5574    let name = element.name();
5575    let qualified = name.as_ref();
5576    let prefix = qualified
5577        .iter()
5578        .position(|byte| *byte == b':')
5579        .map(|position| qualified[..position].to_vec())
5580        .unwrap_or_default();
5581    let declaration = if prefix.is_empty() {
5582        b"xmlns".to_vec()
5583    } else {
5584        let mut declaration = b"xmlns:".to_vec();
5585        declaration.extend_from_slice(&prefix);
5586        declaration
5587    };
5588    if let Some(namespace) = attribute_value(element, &declaration)?
5589        && namespace != C_NS
5590    {
5591        return Err(invalid_attribute(
5592            &element_name(element),
5593            "namespace",
5594            namespace,
5595        ));
5596    }
5597    reject_conflicting_prefix(element, b"c", C_NS)?;
5598    Ok(prefix)
5599}
5600
5601fn typed_rewrite_bindings(
5602    element: &BytesStart<'_>,
5603    inherited: &NamespaceBindings,
5604) -> Result<NamespaceBindings> {
5605    chart_root_prefix(element)?;
5606    if !element_is_in_namespace(element, C_NS, inherited)? {
5607        return Err(ChartError::UnexpectedElement(element_name(element)));
5608    }
5609    let bindings = chart_bindings(inherited, element)?;
5610    require_fixed_namespace(&bindings, b"c", C_NS, element)?;
5611    Ok(bindings)
5612}
5613
5614fn chart_namespace_defaults() -> NamespaceBindings {
5615    vec![
5616        (b"c".to_vec(), C_NS.to_owned()),
5617        (b"a".to_vec(), A_NS.to_owned()),
5618        (b"r".to_vec(), R_NS.to_owned()),
5619    ]
5620}
5621
5622fn chart_bindings(
5623    inherited: &NamespaceBindings,
5624    element: &BytesStart<'_>,
5625) -> Result<NamespaceBindings> {
5626    let mut bindings = chart_namespace_defaults();
5627    for (prefix, namespace) in inherited
5628        .iter()
5629        .cloned()
5630        .chain(namespace_bindings(element)?)
5631    {
5632        upsert_namespace_binding(&mut bindings, prefix, namespace);
5633    }
5634    Ok(bindings)
5635}
5636
5637fn root_chart_bindings(
5638    xml: &[u8],
5639    local: &[u8],
5640    inherited: &NamespaceBindings,
5641) -> Result<NamespaceBindings> {
5642    let mut reader = Reader::from_reader(xml);
5643    let mut buffer = Vec::new();
5644    loop {
5645        match reader
5646            .read_event_into(&mut buffer)
5647            .map_err(OxmlError::from)?
5648        {
5649            Event::Start(element) | Event::Empty(element)
5650                if matches_local_name(element.name().as_ref(), local) =>
5651            {
5652                if !element_is_in_namespace(&element, C_NS, inherited)? {
5653                    return Err(ChartError::UnexpectedElement(element_name(&element)));
5654                }
5655                return chart_bindings(inherited, &element);
5656            }
5657            Event::Start(element) | Event::Empty(element) => {
5658                return Err(ChartError::UnexpectedElement(element_name(&element)));
5659            }
5660            Event::Eof => {
5661                return Err(ChartError::MissingElement(format!(
5662                    "c:{}",
5663                    String::from_utf8_lossy(local)
5664                )));
5665            }
5666            _ => {}
5667        }
5668        buffer.clear();
5669    }
5670}
5671
5672fn chart_child_local(
5673    element: &BytesStart<'_>,
5674    inherited: &NamespaceBindings,
5675) -> Result<Option<Vec<u8>>> {
5676    Ok(element_is_in_namespace(element, C_NS, inherited)?
5677        .then(|| local_name(element.name().as_ref()).to_vec()))
5678}
5679
5680fn capture_fixed_root_attributes(
5681    start: &BytesStart<'_>,
5682    fixed: &[&str],
5683) -> Result<(XmlAttributes, XmlAttributes)> {
5684    let mut attributes = Vec::new();
5685    let mut namespaces = Vec::new();
5686    for attribute in start.attributes() {
5687        let attribute = attribute.map_err(OxmlError::from)?;
5688        let name = std::str::from_utf8(attribute.key.as_ref())
5689            .map_err(OxmlError::from)?
5690            .to_owned();
5691        let value = attribute
5692            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
5693            .map_err(OxmlError::from)?
5694            .into_owned();
5695        if fixed.contains(&name.as_str()) {
5696            continue;
5697        }
5698        if name == "xmlns" || name.starts_with("xmlns:") {
5699            namespaces.push((name, value));
5700        } else {
5701            attributes.push((name, value));
5702        }
5703    }
5704    Ok((attributes, namespaces))
5705}
5706
5707fn require_fixed_namespace(
5708    bindings: &NamespaceBindings,
5709    prefix: &[u8],
5710    expected: &str,
5711    element: &BytesStart<'_>,
5712) -> Result<()> {
5713    let actual = bindings
5714        .iter()
5715        .find(|(candidate, _)| candidate.as_slice() == prefix)
5716        .map(|(_, namespace)| namespace.as_str());
5717    if actual == Some(expected) {
5718        return Ok(());
5719    }
5720    Err(invalid_attribute(
5721        &element_name(element),
5722        &format!("xmlns:{}", String::from_utf8_lossy(prefix)),
5723        actual.unwrap_or("<missing>").to_owned(),
5724    ))
5725}
5726
5727fn standalone_namespace_declarations(bindings: &NamespaceBindings) -> Result<XmlAttributes> {
5728    bindings
5729        .iter()
5730        .filter(|(prefix, _)| prefix != b"c" && prefix != b"a" && prefix != b"r")
5731        .map(|(prefix, namespace)| {
5732            let prefix = std::str::from_utf8(prefix).map_err(OxmlError::from)?;
5733            let name = if prefix.is_empty() {
5734                "xmlns".to_owned()
5735            } else {
5736                format!("xmlns:{prefix}")
5737            };
5738            Ok((name, namespace.clone()))
5739        })
5740        .collect()
5741}
5742
5743fn capture_fixed_attributes(start: &BytesStart<'_>, fixed: &[&str]) -> Result<XmlAttributes> {
5744    let mut attributes = Vec::new();
5745    for attribute in start.attributes() {
5746        let attribute = attribute.map_err(OxmlError::from)?;
5747        let name = std::str::from_utf8(attribute.key.as_ref())
5748            .map_err(OxmlError::from)?
5749            .to_owned();
5750        if fixed.contains(&name.as_str()) {
5751            continue;
5752        }
5753        let value = attribute
5754            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
5755            .map_err(OxmlError::from)?
5756            .into_owned();
5757        attributes.push((name, value));
5758    }
5759    Ok(attributes)
5760}
5761
5762/// A producer-compatible ChartML axis identifier.
5763///
5764/// ECMA-376 names an unsigned value, but PowerPoint emits signed 32-bit
5765/// lexical values in real chart parts. The accepted domain covers both forms.
5766#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
5767pub struct AxisId(i64);
5768
5769impl AxisId {
5770    pub fn new(value: i64) -> Result<Self> {
5771        if (i64::from(i32::MIN)..=i64::from(u32::MAX)).contains(&value) {
5772            Ok(Self(value))
5773        } else {
5774            Err(ChartError::InvalidValue {
5775                element: "c:axId".to_owned(),
5776                value: value.to_string(),
5777            })
5778        }
5779    }
5780
5781    pub const fn value(self) -> i64 {
5782        self.0
5783    }
5784}
5785
5786/// Whether bars extend along the category or value axis.
5787#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5788pub enum BarDirection {
5789    Bar,
5790    Column,
5791}
5792
5793impl BarDirection {
5794    fn parse(value: &str) -> Option<Self> {
5795        match value {
5796            "bar" => Some(Self::Bar),
5797            "col" => Some(Self::Column),
5798            _ => None,
5799        }
5800    }
5801
5802    const fn as_str(self) -> &'static str {
5803        match self {
5804            Self::Bar => "bar",
5805            Self::Column => "col",
5806        }
5807    }
5808}
5809
5810/// Grouping modes supported by a two-dimensional bar plot.
5811#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5812pub enum BarGrouping {
5813    Clustered,
5814    PercentStacked,
5815    Stacked,
5816    Standard,
5817}
5818
5819impl BarGrouping {
5820    fn parse(value: &str) -> Option<Self> {
5821        match value {
5822            "clustered" => Some(Self::Clustered),
5823            "percentStacked" => Some(Self::PercentStacked),
5824            "stacked" => Some(Self::Stacked),
5825            "standard" => Some(Self::Standard),
5826            _ => None,
5827        }
5828    }
5829
5830    const fn as_str(self) -> &'static str {
5831        match self {
5832            Self::Clustered => "clustered",
5833            Self::PercentStacked => "percentStacked",
5834            Self::Stacked => "stacked",
5835            Self::Standard => "standard",
5836        }
5837    }
5838}
5839
5840/// Grouping modes shared by line and later area plots.
5841#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5842pub enum Grouping {
5843    PercentStacked,
5844    Stacked,
5845    Standard,
5846}
5847
5848impl Grouping {
5849    fn parse(value: &str) -> Option<Self> {
5850        match value {
5851            "percentStacked" => Some(Self::PercentStacked),
5852            "stacked" => Some(Self::Stacked),
5853            "standard" => Some(Self::Standard),
5854            _ => None,
5855        }
5856    }
5857
5858    const fn as_str(self) -> &'static str {
5859        match self {
5860            Self::PercentStacked => "percentStacked",
5861            Self::Stacked => "stacked",
5862            Self::Standard => "standard",
5863        }
5864    }
5865}
5866
5867/// Display style for a two-dimensional scatter plot.
5868#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5869pub enum ScatterStyle {
5870    Line,
5871    LineMarker,
5872    Marker,
5873    None,
5874    Smooth,
5875    SmoothMarker,
5876}
5877
5878impl ScatterStyle {
5879    fn parse(value: &str) -> Option<Self> {
5880        match value {
5881            "line" => Some(Self::Line),
5882            "lineMarker" => Some(Self::LineMarker),
5883            "marker" => Some(Self::Marker),
5884            "none" => Some(Self::None),
5885            "smooth" => Some(Self::Smooth),
5886            "smoothMarker" => Some(Self::SmoothMarker),
5887            _ => None,
5888        }
5889    }
5890
5891    const fn as_str(self) -> &'static str {
5892        match self {
5893            Self::Line => "line",
5894            Self::LineMarker => "lineMarker",
5895            Self::Marker => "marker",
5896            Self::None => "none",
5897            Self::Smooth => "smooth",
5898            Self::SmoothMarker => "smoothMarker",
5899        }
5900    }
5901}
5902
5903/// Display style for a two-dimensional radar plot.
5904#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5905pub enum RadarStyle {
5906    Filled,
5907    Marker,
5908    Standard,
5909}
5910
5911impl RadarStyle {
5912    fn parse(value: &str) -> Option<Self> {
5913        match value {
5914            "filled" => Some(Self::Filled),
5915            "marker" => Some(Self::Marker),
5916            "standard" => Some(Self::Standard),
5917            _ => None,
5918        }
5919    }
5920
5921    const fn as_str(self) -> &'static str {
5922        match self {
5923            Self::Filled => "filled",
5924            Self::Marker => "marker",
5925            Self::Standard => "standard",
5926        }
5927    }
5928}
5929
5930/// A supported two-dimensional plot owned by one plot area.
5931#[derive(Clone, Debug, PartialEq)]
5932pub enum Plot {
5933    Bar {
5934        direction: BarDirection,
5935        grouping: BarGrouping,
5936        gap_width: u16,
5937        overlap: i8,
5938        series: Vec<Series>,
5939        data_labels: Option<CT_DLbls>,
5940        axis_ids: [AxisId; 2],
5941    },
5942    Line {
5943        grouping: Grouping,
5944        marker: bool,
5945        smooth: bool,
5946        series: Vec<Series>,
5947        data_labels: Option<CT_DLbls>,
5948        axis_ids: [AxisId; 2],
5949    },
5950    Pie {
5951        first_slice_angle: u16,
5952        series: Vec<Series>,
5953        data_labels: Option<CT_DLbls>,
5954    },
5955    Doughnut {
5956        first_slice_angle: u16,
5957        hole_size: u8,
5958        series: Vec<Series>,
5959        data_labels: Option<CT_DLbls>,
5960    },
5961    Area {
5962        grouping: Grouping,
5963        series: Vec<Series>,
5964        data_labels: Option<CT_DLbls>,
5965        axis_ids: [AxisId; 2],
5966    },
5967    Scatter {
5968        style: ScatterStyle,
5969        series: Vec<Series>,
5970        data_labels: Option<CT_DLbls>,
5971        axis_ids: [AxisId; 2],
5972    },
5973    Radar {
5974        style: RadarStyle,
5975        series: Vec<Series>,
5976        data_labels: Option<CT_DLbls>,
5977        axis_ids: [AxisId; 2],
5978    },
5979}
5980
5981impl Plot {
5982    pub fn bar(
5983        direction: BarDirection,
5984        grouping: BarGrouping,
5985        series: Vec<Series>,
5986        axis_ids: [AxisId; 2],
5987    ) -> Result<Self> {
5988        let plot = Self::Bar {
5989            direction,
5990            grouping,
5991            gap_width: 150,
5992            overlap: 0,
5993            series,
5994            data_labels: None,
5995            axis_ids,
5996        };
5997        plot.validate()?;
5998        Ok(plot)
5999    }
6000
6001    pub fn line(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
6002        let plot = Self::Line {
6003            grouping,
6004            marker: false,
6005            smooth: false,
6006            series,
6007            data_labels: None,
6008            axis_ids,
6009        };
6010        plot.validate()?;
6011        Ok(plot)
6012    }
6013
6014    pub fn pie(series: Vec<Series>) -> Result<Self> {
6015        let plot = Self::Pie {
6016            first_slice_angle: 0,
6017            series,
6018            data_labels: None,
6019        };
6020        plot.validate()?;
6021        Ok(plot)
6022    }
6023
6024    pub fn doughnut(series: Vec<Series>) -> Result<Self> {
6025        let plot = Self::Doughnut {
6026            first_slice_angle: 0,
6027            hole_size: 50,
6028            series,
6029            data_labels: None,
6030        };
6031        plot.validate()?;
6032        Ok(plot)
6033    }
6034
6035    pub fn area(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
6036        let plot = Self::Area {
6037            grouping,
6038            series,
6039            data_labels: None,
6040            axis_ids,
6041        };
6042        plot.validate()?;
6043        Ok(plot)
6044    }
6045
6046    pub fn scatter(
6047        style: ScatterStyle,
6048        series: Vec<Series>,
6049        axis_ids: [AxisId; 2],
6050    ) -> Result<Self> {
6051        let plot = Self::Scatter {
6052            style,
6053            series,
6054            data_labels: None,
6055            axis_ids,
6056        };
6057        plot.validate()?;
6058        Ok(plot)
6059    }
6060
6061    pub fn radar(style: RadarStyle, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
6062        let plot = Self::Radar {
6063            style,
6064            series,
6065            data_labels: None,
6066            axis_ids,
6067        };
6068        plot.validate()?;
6069        Ok(plot)
6070    }
6071
6072    fn axis_ids(&self) -> Option<[AxisId; 2]> {
6073        match self {
6074            Self::Bar { axis_ids, .. }
6075            | Self::Line { axis_ids, .. }
6076            | Self::Area { axis_ids, .. }
6077            | Self::Scatter { axis_ids, .. }
6078            | Self::Radar { axis_ids, .. } => Some(*axis_ids),
6079            Self::Pie { .. } | Self::Doughnut { .. } => None,
6080        }
6081    }
6082
6083    fn validate(&self) -> Result<()> {
6084        let (series, labels, axis_ids) = match self {
6085            Self::Bar {
6086                gap_width,
6087                overlap,
6088                series,
6089                data_labels,
6090                axis_ids,
6091                ..
6092            } => {
6093                if *gap_width > 500 {
6094                    return Err(ChartError::InvalidValue {
6095                        element: "c:gapWidth".to_owned(),
6096                        value: gap_width.to_string(),
6097                    });
6098                }
6099                if !(-100..=100).contains(overlap) {
6100                    return Err(ChartError::InvalidValue {
6101                        element: "c:overlap".to_owned(),
6102                        value: overlap.to_string(),
6103                    });
6104                }
6105                (series, data_labels, Some(axis_ids))
6106            }
6107            Self::Line {
6108                series,
6109                data_labels,
6110                axis_ids,
6111                ..
6112            } => (series, data_labels, Some(axis_ids)),
6113            Self::Pie {
6114                first_slice_angle,
6115                series,
6116                data_labels,
6117            } => {
6118                if *first_slice_angle > 360 {
6119                    return Err(ChartError::InvalidValue {
6120                        element: "c:firstSliceAng".to_owned(),
6121                        value: first_slice_angle.to_string(),
6122                    });
6123                }
6124                (series, data_labels, None)
6125            }
6126            Self::Doughnut {
6127                first_slice_angle,
6128                hole_size,
6129                series,
6130                data_labels,
6131            } => {
6132                if *first_slice_angle > 360 {
6133                    return Err(ChartError::InvalidValue {
6134                        element: "c:firstSliceAng".to_owned(),
6135                        value: first_slice_angle.to_string(),
6136                    });
6137                }
6138                if !(10..=90).contains(hole_size) {
6139                    return Err(ChartError::InvalidValue {
6140                        element: "c:holeSize".to_owned(),
6141                        value: hole_size.to_string(),
6142                    });
6143                }
6144                (series, data_labels, None)
6145            }
6146            Self::Area {
6147                series,
6148                data_labels,
6149                axis_ids,
6150                ..
6151            }
6152            | Self::Radar {
6153                series,
6154                data_labels,
6155                axis_ids,
6156                ..
6157            } => (series, data_labels, Some(axis_ids)),
6158            Self::Scatter {
6159                series,
6160                data_labels,
6161                axis_ids,
6162                ..
6163            } => {
6164                for item in series {
6165                    if !matches!(item.categories, Some(AxisData::Numeric(_))) {
6166                        return Err(ChartError::InvalidValue {
6167                            element: "c:xVal".to_owned(),
6168                            value: "scatter series requires numeric x values".to_owned(),
6169                        });
6170                    }
6171                }
6172                (series, data_labels, Some(axis_ids))
6173            }
6174        };
6175        if series.is_empty() {
6176            return Err(ChartError::MissingElement("c:ser".to_owned()));
6177        }
6178        let scatter_plot = matches!(self, Self::Scatter { .. });
6179        for item in series {
6180            if item.bubble_size.is_some() || item.opaque_bubble_size {
6181                return Err(ChartError::InvalidValue {
6182                    element: "c:bubbleSize".to_owned(),
6183                    value: "bubble size is only valid in an opaque bubble plot".to_owned(),
6184                });
6185            }
6186            if item.uses_scatter_wrappers && !scatter_plot {
6187                return Err(ChartError::InvalidValue {
6188                    element: "c:ser".to_owned(),
6189                    value: "x/y wrappers are only valid in scatter plots".to_owned(),
6190                });
6191            }
6192            if scatter_plot && item.parsed_from_xml && !item.uses_scatter_wrappers {
6193                return Err(ChartError::InvalidValue {
6194                    element: "c:ser".to_owned(),
6195                    value: "scatter plots require xVal/yVal wrappers".to_owned(),
6196                });
6197            }
6198        }
6199        if let Some(axis_ids) = axis_ids {
6200            if axis_ids[0] == axis_ids[1] {
6201                return Err(ChartError::DuplicateElement(format!(
6202                    "c:axId {}",
6203                    axis_ids[0].value()
6204                )));
6205            }
6206            for id in axis_ids {
6207                AxisId::new(id.value())?;
6208            }
6209        }
6210        if let Some(labels) = labels {
6211            labels.validate()?;
6212        }
6213        for item in series {
6214            item.values.validate()?;
6215        }
6216        Ok(())
6217    }
6218}
6219
6220/// The concrete ChartML root used by an axis.
6221#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
6222pub enum AxisKind {
6223    Category,
6224    Value,
6225    Date,
6226    Series,
6227}
6228
6229impl AxisKind {
6230    fn parse(local: &[u8]) -> Option<Self> {
6231        match local {
6232            b"catAx" => Some(Self::Category),
6233            b"valAx" => Some(Self::Value),
6234            b"dateAx" => Some(Self::Date),
6235            b"serAx" => Some(Self::Series),
6236            _ => None,
6237        }
6238    }
6239
6240    const fn local(self) -> &'static str {
6241        match self {
6242            Self::Category => "catAx",
6243            Self::Value => "valAx",
6244            Self::Date => "dateAx",
6245            Self::Series => "serAx",
6246        }
6247    }
6248}
6249
6250/// Direction of values along an axis scale.
6251#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6252pub enum Orientation {
6253    #[default]
6254    MinMax,
6255    MaxMin,
6256}
6257
6258impl Orientation {
6259    fn parse(value: &str) -> Option<Self> {
6260        match value {
6261            "minMax" => Some(Self::MinMax),
6262            "maxMin" => Some(Self::MaxMin),
6263            _ => None,
6264        }
6265    }
6266
6267    const fn as_str(self) -> &'static str {
6268        match self {
6269            Self::MinMax => "minMax",
6270            Self::MaxMin => "maxMin",
6271        }
6272    }
6273}
6274
6275/// Which side of the plot area carries an axis.
6276#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6277pub enum AxisPosition {
6278    Bottom,
6279    Left,
6280    Right,
6281    Top,
6282}
6283
6284impl AxisPosition {
6285    fn parse(value: &str) -> Option<Self> {
6286        match value {
6287            "b" => Some(Self::Bottom),
6288            "l" => Some(Self::Left),
6289            "r" => Some(Self::Right),
6290            "t" => Some(Self::Top),
6291            _ => None,
6292        }
6293    }
6294
6295    const fn as_str(self) -> &'static str {
6296        match self {
6297            Self::Bottom => "b",
6298            Self::Left => "l",
6299            Self::Right => "r",
6300            Self::Top => "t",
6301        }
6302    }
6303}
6304
6305/// Placement of a major or minor tick relative to its axis line.
6306#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6307pub enum TickMark {
6308    Cross,
6309    Inside,
6310    #[default]
6311    None,
6312    Outside,
6313}
6314
6315impl TickMark {
6316    fn parse(value: &str) -> Option<Self> {
6317        match value {
6318            "cross" => Some(Self::Cross),
6319            "in" => Some(Self::Inside),
6320            "none" => Some(Self::None),
6321            "out" => Some(Self::Outside),
6322            _ => None,
6323        }
6324    }
6325
6326    const fn as_str(self) -> &'static str {
6327        match self {
6328            Self::Cross => "cross",
6329            Self::Inside => "in",
6330            Self::None => "none",
6331            Self::Outside => "out",
6332        }
6333    }
6334}
6335
6336/// Placement of tick labels relative to an axis.
6337#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6338pub enum TickLabelPosition {
6339    High,
6340    Low,
6341    #[default]
6342    NextTo,
6343    None,
6344}
6345
6346impl TickLabelPosition {
6347    fn parse(value: &str) -> Option<Self> {
6348        match value {
6349            "high" => Some(Self::High),
6350            "low" => Some(Self::Low),
6351            "nextTo" => Some(Self::NextTo),
6352            "none" => Some(Self::None),
6353            _ => None,
6354        }
6355    }
6356
6357    const fn as_str(self) -> &'static str {
6358        match self {
6359            Self::High => "high",
6360            Self::Low => "low",
6361            Self::NextTo => "nextTo",
6362            Self::None => "none",
6363        }
6364    }
6365}
6366
6367/// The modelled values of `c:scaling`.
6368#[derive(Clone, Debug)]
6369pub struct Scaling {
6370    pub log_base: Option<f64>,
6371    pub orientation: Orientation,
6372    pub maximum: Option<f64>,
6373    pub minimum: Option<f64>,
6374    log_base_markup: Option<ScalarMarkup>,
6375    orientation_markup: Option<ScalarMarkup>,
6376    maximum_markup: Option<ScalarMarkup>,
6377    minimum_markup: Option<ScalarMarkup>,
6378    raw_attributes: XmlAttributes,
6379    raw_children: OrderedRawChildren,
6380}
6381
6382impl PartialEq for Scaling {
6383    fn eq(&self, other: &Self) -> bool {
6384        self.log_base == other.log_base
6385            && self.orientation == other.orientation
6386            && self.maximum == other.maximum
6387            && self.minimum == other.minimum
6388            && optional_scalar_markup_eq(&self.log_base_markup, &other.log_base_markup)
6389            && optional_scalar_markup_eq(&self.orientation_markup, &other.orientation_markup)
6390            && optional_scalar_markup_eq(&self.maximum_markup, &other.maximum_markup)
6391            && optional_scalar_markup_eq(&self.minimum_markup, &other.minimum_markup)
6392            && self.raw_attributes == other.raw_attributes
6393            && self.raw_children == other.raw_children
6394    }
6395}
6396
6397impl Default for Scaling {
6398    fn default() -> Self {
6399        Self {
6400            log_base: None,
6401            orientation: Orientation::MinMax,
6402            maximum: None,
6403            minimum: None,
6404            log_base_markup: None,
6405            orientation_markup: None,
6406            maximum_markup: None,
6407            minimum_markup: None,
6408            raw_attributes: Vec::new(),
6409            raw_children: OrderedRawChildren::default(),
6410        }
6411    }
6412}
6413
6414/// Shape properties on a major or minor gridline container.
6415#[derive(Clone, Debug, Default, PartialEq)]
6416pub struct ChartLines {
6417    pub sp_pr: Option<CT_ShapeProperties>,
6418    raw_attributes: XmlAttributes,
6419    raw_children: OrderedRawChildren,
6420}
6421
6422/// Number format applied to axis values and labels.
6423#[derive(Clone, Debug, Eq, PartialEq)]
6424pub struct NumberFormat {
6425    pub format_code: String,
6426    pub source_linked: bool,
6427    raw_attributes: XmlAttributes,
6428    raw_content: Vec<Vec<u8>>,
6429}
6430
6431/// Placement of a chart data label relative to its data point.
6432#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6433pub enum DataLabelPosition {
6434    BestFit,
6435    Bottom,
6436    Center,
6437    InsideBase,
6438    InsideEnd,
6439    Left,
6440    OutsideEnd,
6441    Right,
6442    Top,
6443}
6444
6445impl DataLabelPosition {
6446    fn parse(value: &str) -> Option<Self> {
6447        match value {
6448            "bestFit" => Some(Self::BestFit),
6449            "b" => Some(Self::Bottom),
6450            "ctr" => Some(Self::Center),
6451            "inBase" => Some(Self::InsideBase),
6452            "inEnd" => Some(Self::InsideEnd),
6453            "l" => Some(Self::Left),
6454            "outEnd" => Some(Self::OutsideEnd),
6455            "r" => Some(Self::Right),
6456            "t" => Some(Self::Top),
6457            _ => None,
6458        }
6459    }
6460
6461    const fn as_str(self) -> &'static str {
6462        match self {
6463            Self::BestFit => "bestFit",
6464            Self::Bottom => "b",
6465            Self::Center => "ctr",
6466            Self::InsideBase => "inBase",
6467            Self::InsideEnd => "inEnd",
6468            Self::Left => "l",
6469            Self::OutsideEnd => "outEnd",
6470            Self::Right => "r",
6471            Self::Top => "t",
6472        }
6473    }
6474}
6475
6476/// Collection-level data-label defaults for one chart series or plot.
6477#[derive(Clone, Debug, Default, PartialEq)]
6478pub struct CT_DLbls {
6479    pub number_format: Option<NumberFormat>,
6480    pub position: Option<DataLabelPosition>,
6481    pub separator: Option<String>,
6482    pub show_legend_key: bool,
6483    pub show_value: bool,
6484    pub show_category_name: bool,
6485    pub show_series_name: bool,
6486    pub show_percent: bool,
6487    pub show_bubble_size: bool,
6488    position_markup: Option<ScalarMarkup>,
6489    show_legend_key_markup: Option<ScalarMarkup>,
6490    show_value_markup: Option<ScalarMarkup>,
6491    show_category_name_markup: Option<ScalarMarkup>,
6492    show_series_name_markup: Option<ScalarMarkup>,
6493    show_percent_markup: Option<ScalarMarkup>,
6494    show_bubble_size_markup: Option<ScalarMarkup>,
6495    separator_markup: Option<TextMarkup>,
6496    raw_attributes: XmlAttributes,
6497    namespace_declarations: XmlAttributes,
6498    raw_children: OrderedRawChildren,
6499}
6500
6501/// How a chart displays cells whose cached values are blank.
6502#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6503pub enum DispBlanksAs {
6504    #[default]
6505    Gap,
6506    Zero,
6507    Span,
6508}
6509
6510impl DispBlanksAs {
6511    fn parse(value: &str) -> Option<Self> {
6512        match value {
6513            "gap" => Some(Self::Gap),
6514            "zero" => Some(Self::Zero),
6515            "span" => Some(Self::Span),
6516            _ => None,
6517        }
6518    }
6519
6520    const fn as_str(self) -> &'static str {
6521        match self {
6522            Self::Gap => "gap",
6523            Self::Zero => "zero",
6524            Self::Span => "span",
6525        }
6526    }
6527}
6528
6529/// A chart title with plain-text authoring and preserved unsupported children.
6530#[derive(Clone, Debug, Default, Eq, PartialEq)]
6531pub struct CT_Title {
6532    text: Option<CT_TextBody>,
6533    raw_attributes: Vec<(String, String)>,
6534    raw_children: OrderedRawChildren,
6535}
6536
6537/// Preserved lexical and ordered-raw state for one typed plot.
6538#[derive(Clone, Debug, Default, PartialEq)]
6539struct PlotMarkup {
6540    raw_attributes: XmlAttributes,
6541    raw_children: OrderedRawChildren,
6542    direction: Option<ScalarMarkup>,
6543    grouping: Option<ScalarMarkup>,
6544    gap_width: Option<ScalarMarkup>,
6545    overlap: Option<ScalarMarkup>,
6546    marker: Option<ScalarMarkup>,
6547    smooth: Option<ScalarMarkup>,
6548    first_slice_angle: Option<ScalarMarkup>,
6549    hole_size: Option<ScalarMarkup>,
6550    style: Option<ScalarMarkup>,
6551    axis_ids: Vec<AxisIdMarkup>,
6552    original_series_keys: Vec<(u32, u32)>,
6553    original_axis_ids: Vec<AxisId>,
6554    parsed_bar: Option<bool>,
6555    parsed_remaining: Option<&'static str>,
6556}
6557
6558/// A plot-area shell. F-119 through F-122 replace selected raw slots with types.
6559#[derive(Clone, Debug, PartialEq)]
6560pub struct CT_PlotArea {
6561    raw_attributes: Vec<(String, String)>,
6562    raw_children: OrderedRawChildren,
6563    namespace_bindings: NamespaceBindings,
6564    plots: Option<Vec<Plot>>,
6565    plot_markup: Vec<PlotMarkup>,
6566    axes: Vec<Axis>,
6567}
6568
6569/// A chart legend shell whose current children remain opaque.
6570#[derive(Clone, Debug, Default, Eq, PartialEq)]
6571pub struct CT_Legend {
6572    raw_attributes: Vec<(String, String)>,
6573    raw_children: OrderedRawChildren,
6574}
6575
6576#[derive(Clone, Debug, PartialEq)]
6577struct AxisIdMarkup {
6578    scalar: ScalarMarkup,
6579    parsed: AxisId,
6580    lexical: String,
6581}
6582
6583/// The common modelled sequence of one ChartML axis.
6584#[derive(Clone, Debug)]
6585pub struct Axis {
6586    pub kind: AxisKind,
6587    pub id: AxisId,
6588    pub scaling: Scaling,
6589    pub deleted: bool,
6590    pub position: AxisPosition,
6591    pub major_gridlines: Option<ChartLines>,
6592    pub minor_gridlines: Option<ChartLines>,
6593    pub title: Option<CT_Title>,
6594    pub number_format: Option<NumberFormat>,
6595    pub major_tick_mark: TickMark,
6596    pub minor_tick_mark: TickMark,
6597    pub tick_label_position: TickLabelPosition,
6598    pub sp_pr: Option<CT_ShapeProperties>,
6599    pub tx_pr: Option<CT_TextBody>,
6600    pub cross_axis: AxisId,
6601    parsed_kind: Option<AxisKind>,
6602    id_markup: AxisIdMarkup,
6603    delete_markup: Option<ScalarMarkup>,
6604    position_markup: ScalarMarkup,
6605    major_tick_mark_markup: Option<ScalarMarkup>,
6606    minor_tick_mark_markup: Option<ScalarMarkup>,
6607    tick_label_position_markup: Option<ScalarMarkup>,
6608    cross_axis_markup: AxisIdMarkup,
6609    raw_attributes: XmlAttributes,
6610    namespace_declarations: XmlAttributes,
6611    raw_children: OrderedRawChildren,
6612}
6613
6614impl PartialEq for Axis {
6615    fn eq(&self, other: &Self) -> bool {
6616        self.kind == other.kind
6617            && self.id == other.id
6618            && axis_id_markup_eq(&self.id_markup, &other.id_markup)
6619            && self.scaling == other.scaling
6620            && self.deleted == other.deleted
6621            && optional_scalar_markup_eq(&self.delete_markup, &other.delete_markup)
6622            && self.position == other.position
6623            && self.position_markup == other.position_markup
6624            && self.major_gridlines == other.major_gridlines
6625            && self.minor_gridlines == other.minor_gridlines
6626            && self.title == other.title
6627            && self.number_format == other.number_format
6628            && self.major_tick_mark == other.major_tick_mark
6629            && optional_scalar_markup_eq(
6630                &self.major_tick_mark_markup,
6631                &other.major_tick_mark_markup,
6632            )
6633            && self.minor_tick_mark == other.minor_tick_mark
6634            && optional_scalar_markup_eq(
6635                &self.minor_tick_mark_markup,
6636                &other.minor_tick_mark_markup,
6637            )
6638            && self.tick_label_position == other.tick_label_position
6639            && optional_scalar_markup_eq(
6640                &self.tick_label_position_markup,
6641                &other.tick_label_position_markup,
6642            )
6643            && self.sp_pr == other.sp_pr
6644            && self.tx_pr == other.tx_pr
6645            && self.cross_axis == other.cross_axis
6646            && axis_id_markup_eq(&self.cross_axis_markup, &other.cross_axis_markup)
6647            && self.raw_attributes == other.raw_attributes
6648            && self.namespace_declarations == other.namespace_declarations
6649            && self.raw_children == other.raw_children
6650    }
6651}
6652
6653impl Axis {
6654    pub fn new(kind: AxisKind, id: AxisId, position: AxisPosition, cross_axis: AxisId) -> Self {
6655        Self {
6656            kind,
6657            id,
6658            scaling: Scaling::default(),
6659            deleted: false,
6660            position,
6661            major_gridlines: None,
6662            minor_gridlines: None,
6663            title: None,
6664            number_format: None,
6665            major_tick_mark: TickMark::None,
6666            minor_tick_mark: TickMark::None,
6667            tick_label_position: TickLabelPosition::NextTo,
6668            sp_pr: None,
6669            tx_pr: None,
6670            cross_axis,
6671            parsed_kind: None,
6672            id_markup: default_axis_id_markup(id),
6673            delete_markup: None,
6674            position_markup: ScalarMarkup::default(),
6675            major_tick_mark_markup: None,
6676            minor_tick_mark_markup: None,
6677            tick_label_position_markup: None,
6678            cross_axis_markup: default_axis_id_markup(cross_axis),
6679            raw_attributes: Vec::new(),
6680            namespace_declarations: Vec::new(),
6681            raw_children: OrderedRawChildren::default(),
6682        }
6683    }
6684
6685    /// Sets axis visibility and writes the explicit ChartML deletion flag.
6686    pub fn set_deleted(&mut self, deleted: bool) {
6687        self.deleted = deleted;
6688        self.delete_markup.get_or_insert_with(ScalarMarkup::default);
6689    }
6690
6691    pub fn from_xml(xml: &[u8]) -> Result<Self> {
6692        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
6693    }
6694
6695    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
6696        let mut reader = Reader::from_reader(xml);
6697        let mut buffer = Vec::new();
6698        loop {
6699            match reader
6700                .read_event_into(&mut buffer)
6701                .map_err(OxmlError::from)?
6702            {
6703                Event::Start(element) => {
6704                    let name = element.name();
6705                    let local = local_name(name.as_ref());
6706                    let Some(kind) = AxisKind::parse(local) else {
6707                        return Err(ChartError::UnexpectedElement(element_name(&element)));
6708                    };
6709                    chart_root_prefix(&element)?;
6710                    if !element_is_in_namespace(&element, C_NS, inherited)? {
6711                        return Err(ChartError::UnexpectedElement(element_name(&element)));
6712                    }
6713                    return Self::from_element(&mut reader, &element, inherited, kind);
6714                }
6715                Event::Empty(element) => {
6716                    let name = element.name();
6717                    let local = local_name(name.as_ref());
6718                    if AxisKind::parse(local).is_some() {
6719                        return Err(ChartError::MissingElement("c:axId".to_owned()));
6720                    }
6721                    return Err(ChartError::UnexpectedElement(element_name(&element)));
6722                }
6723                Event::Eof => return Err(ChartError::MissingElement("ChartML axis".to_owned())),
6724                _ => {}
6725            }
6726            buffer.clear();
6727        }
6728    }
6729
6730    fn from_element(
6731        reader: &mut Reader<&[u8]>,
6732        start: &BytesStart<'_>,
6733        inherited: &NamespaceBindings,
6734        kind: AxisKind,
6735    ) -> Result<Self> {
6736        reject_conflicting_prefix(start, b"a", A_NS)?;
6737        reject_conflicting_prefix(start, b"r", R_NS)?;
6738        let namespaces = chart_bindings(inherited, start)?;
6739        require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
6740        require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
6741        require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
6742        let (raw_attributes, _) =
6743            capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
6744        let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
6745        let mut state = AxisParseState::default();
6746        let mut buffer = Vec::new();
6747        loop {
6748            match reader
6749                .read_event_into(&mut buffer)
6750                .map_err(OxmlError::from)?
6751            {
6752                Event::Start(element) => {
6753                    let name = chart_child_local(&element, &namespaces)?;
6754                    let raw = capture_element(reader, &element)?;
6755                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
6756                }
6757                Event::Empty(element) => {
6758                    let name = chart_child_local(&element, &namespaces)?;
6759                    let raw = capture_empty_element(&element)?;
6760                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
6761                }
6762                event @ (Event::Text(_)
6763                | Event::CData(_)
6764                | Event::Comment(_)
6765                | Event::PI(_)
6766                | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
6767                Event::End(element)
6768                    if matches_local_name(element.name().as_ref(), kind.local().as_bytes()) =>
6769                {
6770                    break;
6771                }
6772                Event::Eof => return Err(missing_end(&format!("c:{}", kind.local()))),
6773                _ => {}
6774            }
6775            buffer.clear();
6776        }
6777        state.finish(kind, raw_attributes, namespace_declarations)
6778    }
6779
6780    pub fn to_xml(&self) -> Result<Vec<u8>> {
6781        self.validate()?;
6782        let mut writer = Writer::new(Vec::new());
6783        let root = format!("c:{}", self.kind.local());
6784        let mut start = BytesStart::new(&root);
6785        start.push_attribute(("xmlns:c", C_NS));
6786        start.push_attribute(("xmlns:a", A_NS));
6787        start.push_attribute(("xmlns:r", R_NS));
6788        push_attributes(&mut start, &self.namespace_declarations);
6789        push_attributes(&mut start, &self.raw_attributes);
6790        writer
6791            .write_event(Event::Start(start))
6792            .map_err(OxmlError::from)?;
6793        emit_raw(&mut writer, self.raw_children.at(0))?;
6794        write_axis_id(&mut writer, "c:axId", self.id, &self.id_markup)?;
6795        emit_raw(&mut writer, self.raw_children.at(1))?;
6796        self.scaling.write_xml(&mut writer)?;
6797        emit_raw(&mut writer, self.raw_children.at(2))?;
6798        if self.deleted || self.delete_markup.is_some() {
6799            write_scalar(
6800                &mut writer,
6801                "c:delete",
6802                bool_lexical(self.deleted),
6803                self.delete_markup.as_ref(),
6804            )?;
6805        }
6806        emit_raw(&mut writer, self.raw_children.at(3))?;
6807        write_scalar(
6808            &mut writer,
6809            "c:axPos",
6810            self.position.as_str(),
6811            Some(&self.position_markup),
6812        )?;
6813        emit_raw(&mut writer, self.raw_children.at(4))?;
6814        if let Some(lines) = &self.major_gridlines {
6815            lines.write_xml(&mut writer, "c:majorGridlines")?;
6816        }
6817        emit_raw(&mut writer, self.raw_children.at(5))?;
6818        if let Some(lines) = &self.minor_gridlines {
6819            lines.write_xml(&mut writer, "c:minorGridlines")?;
6820        }
6821        emit_raw(&mut writer, self.raw_children.at(6))?;
6822        if let Some(title) = &self.title {
6823            title.write_xml(&mut writer)?;
6824        }
6825        emit_raw(&mut writer, self.raw_children.at(7))?;
6826        if let Some(number_format) = &self.number_format {
6827            number_format.write_xml(&mut writer)?;
6828        }
6829        emit_raw(&mut writer, self.raw_children.at(8))?;
6830        if self.major_tick_mark != TickMark::None || self.major_tick_mark_markup.is_some() {
6831            write_scalar(
6832                &mut writer,
6833                "c:majorTickMark",
6834                self.major_tick_mark.as_str(),
6835                self.major_tick_mark_markup.as_ref(),
6836            )?;
6837        }
6838        emit_raw(&mut writer, self.raw_children.at(9))?;
6839        if self.minor_tick_mark != TickMark::None || self.minor_tick_mark_markup.is_some() {
6840            write_scalar(
6841                &mut writer,
6842                "c:minorTickMark",
6843                self.minor_tick_mark.as_str(),
6844                self.minor_tick_mark_markup.as_ref(),
6845            )?;
6846        }
6847        emit_raw(&mut writer, self.raw_children.at(10))?;
6848        if self.tick_label_position != TickLabelPosition::NextTo
6849            || self.tick_label_position_markup.is_some()
6850        {
6851            write_scalar(
6852                &mut writer,
6853                "c:tickLblPos",
6854                self.tick_label_position.as_str(),
6855                self.tick_label_position_markup.as_ref(),
6856            )?;
6857        }
6858        emit_raw(&mut writer, self.raw_children.at(11))?;
6859        if let Some(properties) = &self.sp_pr {
6860            properties.write_xml_as(&mut writer, "c:spPr")?;
6861        }
6862        emit_raw(&mut writer, self.raw_children.at(12))?;
6863        if let Some(text) = &self.tx_pr {
6864            text.write_xml_as(&mut writer, "c:txPr")?;
6865        }
6866        emit_raw(&mut writer, self.raw_children.at(13))?;
6867        write_axis_id(
6868            &mut writer,
6869            "c:crossAx",
6870            self.cross_axis,
6871            &self.cross_axis_markup,
6872        )?;
6873        emit_raw(&mut writer, self.raw_children.at(14))?;
6874        writer
6875            .write_event(Event::End(BytesEnd::new(&root)))
6876            .map_err(OxmlError::from)?;
6877        Ok(writer.into_inner())
6878    }
6879
6880    pub fn raw_children(&self) -> &OrderedRawChildren {
6881        &self.raw_children
6882    }
6883
6884    fn validate(&self) -> Result<()> {
6885        if let Some(parsed_kind) = self.parsed_kind
6886            && self.kind != parsed_kind
6887        {
6888            return Err(ChartError::InvalidValue {
6889                element: "ChartML axis root".to_owned(),
6890                value: format!(
6891                    "parsed c:{} cannot be relabelled c:{}",
6892                    parsed_kind.local(),
6893                    self.kind.local()
6894                ),
6895            });
6896        }
6897        AxisId::new(self.id.value())?;
6898        AxisId::new(self.cross_axis.value())?;
6899        self.scaling.validate()?;
6900        if let Some(number_format) = &self.number_format {
6901            number_format.validate()?;
6902        }
6903        Ok(())
6904    }
6905}
6906
6907#[derive(Default)]
6908struct AxisParseState {
6909    id: Option<(AxisId, AxisIdMarkup)>,
6910    scaling: Option<Scaling>,
6911    deleted: Option<(bool, ScalarMarkup)>,
6912    position: Option<(AxisPosition, ScalarMarkup)>,
6913    major_gridlines: Option<ChartLines>,
6914    minor_gridlines: Option<ChartLines>,
6915    title: Option<CT_Title>,
6916    number_format: Option<NumberFormat>,
6917    major_tick_mark: Option<(TickMark, ScalarMarkup)>,
6918    minor_tick_mark: Option<(TickMark, ScalarMarkup)>,
6919    tick_label_position: Option<(TickLabelPosition, ScalarMarkup)>,
6920    sp_pr: Option<CT_ShapeProperties>,
6921    tx_pr: Option<CT_TextBody>,
6922    cross_axis: Option<(AxisId, AxisIdMarkup)>,
6923    raw_children: OrderedRawChildren,
6924    boundary: usize,
6925}
6926
6927impl AxisParseState {
6928    fn capture_event(&mut self, raw: Vec<u8>) {
6929        self.raw_children.push(self.boundary, raw);
6930    }
6931
6932    fn parse_child(
6933        &mut self,
6934        name: &[u8],
6935        raw: Vec<u8>,
6936        namespaces: &NamespaceBindings,
6937    ) -> Result<()> {
6938        match name {
6939            b"axId" => {
6940                set_once(&mut self.id, parse_axis_id_scalar(&raw, "axId")?, "c:axId")?;
6941                self.boundary = self.boundary.max(1);
6942            }
6943            b"scaling" => {
6944                set_once(
6945                    &mut self.scaling,
6946                    Scaling::from_xml(&raw, namespaces)?,
6947                    "c:scaling",
6948                )?;
6949                self.boundary = self.boundary.max(2);
6950            }
6951            b"delete" => {
6952                set_once(
6953                    &mut self.deleted,
6954                    parse_bool_value(&raw, "delete")?,
6955                    "c:delete",
6956                )?;
6957                self.boundary = self.boundary.max(3);
6958            }
6959            b"axPos" => {
6960                set_once(&mut self.position, parse_axis_position(&raw)?, "c:axPos")?;
6961                self.boundary = self.boundary.max(4);
6962            }
6963            b"majorGridlines" => {
6964                set_once(
6965                    &mut self.major_gridlines,
6966                    ChartLines::from_xml(&raw, b"majorGridlines", namespaces)?,
6967                    "c:majorGridlines",
6968                )?;
6969                self.boundary = self.boundary.max(5);
6970            }
6971            b"minorGridlines" => {
6972                set_once(
6973                    &mut self.minor_gridlines,
6974                    ChartLines::from_xml(&raw, b"minorGridlines", namespaces)?,
6975                    "c:minorGridlines",
6976                )?;
6977                self.boundary = self.boundary.max(6);
6978            }
6979            b"title" => {
6980                set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
6981                self.boundary = self.boundary.max(7);
6982            }
6983            b"numFmt" => {
6984                set_once(
6985                    &mut self.number_format,
6986                    NumberFormat::from_xml(&raw)?,
6987                    "c:numFmt",
6988                )?;
6989                self.boundary = self.boundary.max(8);
6990            }
6991            b"majorTickMark" => {
6992                set_once(
6993                    &mut self.major_tick_mark,
6994                    parse_tick_mark(&raw, "majorTickMark")?,
6995                    "c:majorTickMark",
6996                )?;
6997                self.boundary = self.boundary.max(9);
6998            }
6999            b"minorTickMark" => {
7000                set_once(
7001                    &mut self.minor_tick_mark,
7002                    parse_tick_mark(&raw, "minorTickMark")?,
7003                    "c:minorTickMark",
7004                )?;
7005                self.boundary = self.boundary.max(10);
7006            }
7007            b"tickLblPos" => {
7008                set_once(
7009                    &mut self.tick_label_position,
7010                    parse_tick_label_position(&raw)?,
7011                    "c:tickLblPos",
7012                )?;
7013                self.boundary = self.boundary.max(11);
7014            }
7015            b"spPr" => {
7016                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
7017                let properties = CT_ShapeProperties::from_xml(&raw)?;
7018                let mut writer = Writer::new(Vec::new());
7019                properties.write_xml_as(&mut writer, "c:spPr")?;
7020                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
7021                set_once(&mut self.sp_pr, properties, "c:spPr")?;
7022                self.boundary = self.boundary.max(12);
7023            }
7024            b"txPr" => {
7025                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
7026                let text = CT_TextBody::from_xml_as(&raw, b"txPr")?;
7027                let mut writer = Writer::new(Vec::new());
7028                text.write_xml_as(&mut writer, "c:txPr")?;
7029                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
7030                set_once(&mut self.tx_pr, text, "c:txPr")?;
7031                self.boundary = self.boundary.max(13);
7032            }
7033            b"crossAx" => {
7034                set_once(
7035                    &mut self.cross_axis,
7036                    parse_axis_id_scalar(&raw, "crossAx")?,
7037                    "c:crossAx",
7038                )?;
7039                self.boundary = self.boundary.max(14);
7040            }
7041            _ => self.raw_children.push(self.boundary, raw),
7042        }
7043        Ok(())
7044    }
7045
7046    fn finish(
7047        self,
7048        kind: AxisKind,
7049        raw_attributes: XmlAttributes,
7050        namespace_declarations: XmlAttributes,
7051    ) -> Result<Axis> {
7052        let (id, id_markup) = self
7053            .id
7054            .ok_or_else(|| ChartError::MissingElement("c:axId".to_owned()))?;
7055        let scaling = self
7056            .scaling
7057            .ok_or_else(|| ChartError::MissingElement("c:scaling".to_owned()))?;
7058        let (position, position_markup) = self
7059            .position
7060            .ok_or_else(|| ChartError::MissingElement("c:axPos".to_owned()))?;
7061        let (cross_axis, cross_axis_markup) = self
7062            .cross_axis
7063            .ok_or_else(|| ChartError::MissingElement("c:crossAx".to_owned()))?;
7064        let (deleted, delete_markup) = self
7065            .deleted
7066            .map(|(value, markup)| (value, Some(markup)))
7067            .unwrap_or((false, None));
7068        let (major_tick_mark, major_tick_mark_markup) = self
7069            .major_tick_mark
7070            .map(|(value, markup)| (value, Some(markup)))
7071            .unwrap_or((TickMark::None, None));
7072        let (minor_tick_mark, minor_tick_mark_markup) = self
7073            .minor_tick_mark
7074            .map(|(value, markup)| (value, Some(markup)))
7075            .unwrap_or((TickMark::None, None));
7076        let (tick_label_position, tick_label_position_markup) = self
7077            .tick_label_position
7078            .map(|(value, markup)| (value, Some(markup)))
7079            .unwrap_or((TickLabelPosition::NextTo, None));
7080        let axis = Axis {
7081            kind,
7082            id,
7083            scaling,
7084            deleted,
7085            position,
7086            major_gridlines: self.major_gridlines,
7087            minor_gridlines: self.minor_gridlines,
7088            title: self.title,
7089            number_format: self.number_format,
7090            major_tick_mark,
7091            minor_tick_mark,
7092            tick_label_position,
7093            sp_pr: self.sp_pr,
7094            tx_pr: self.tx_pr,
7095            cross_axis,
7096            parsed_kind: Some(kind),
7097            id_markup,
7098            delete_markup,
7099            position_markup,
7100            major_tick_mark_markup,
7101            minor_tick_mark_markup,
7102            tick_label_position_markup,
7103            cross_axis_markup,
7104            raw_attributes,
7105            namespace_declarations,
7106            raw_children: raw_children_in_schema_order(&self.raw_children, 14),
7107        };
7108        axis.validate()?;
7109        Ok(axis)
7110    }
7111}
7112
7113impl Scaling {
7114    fn from_xml(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7115        let mut reader = Reader::from_reader(xml);
7116        let mut buffer = Vec::new();
7117        loop {
7118            match reader
7119                .read_event_into(&mut buffer)
7120                .map_err(OxmlError::from)?
7121            {
7122                Event::Start(element)
7123                    if matches_local_name(element.name().as_ref(), b"scaling") =>
7124                {
7125                    let namespaces = typed_rewrite_bindings(&element, inherited)?;
7126                    let raw_attributes = capture_attributes(&element)?;
7127                    let mut log_base = None;
7128                    let mut orientation = None;
7129                    let mut maximum = None;
7130                    let mut minimum = None;
7131                    let mut raw_children = OrderedRawChildren::default();
7132                    let mut boundary = 0usize;
7133                    let mut inner = Vec::new();
7134                    loop {
7135                        match reader
7136                            .read_event_into(&mut inner)
7137                            .map_err(OxmlError::from)?
7138                        {
7139                            Event::Start(child) => {
7140                                let name = chart_child_local(&child, &namespaces)?;
7141                                let raw = capture_element(&mut reader, &child)?;
7142                                parse_scaling_child(
7143                                    name.as_deref().unwrap_or_default(),
7144                                    raw,
7145                                    &mut log_base,
7146                                    &mut orientation,
7147                                    &mut maximum,
7148                                    &mut minimum,
7149                                    &mut raw_children,
7150                                    &mut boundary,
7151                                )?;
7152                            }
7153                            Event::Empty(child) => {
7154                                let name = chart_child_local(&child, &namespaces)?;
7155                                let raw = capture_empty_element(&child)?;
7156                                parse_scaling_child(
7157                                    name.as_deref().unwrap_or_default(),
7158                                    raw,
7159                                    &mut log_base,
7160                                    &mut orientation,
7161                                    &mut maximum,
7162                                    &mut minimum,
7163                                    &mut raw_children,
7164                                    &mut boundary,
7165                                )?;
7166                            }
7167                            event @ (Event::Text(_)
7168                            | Event::CData(_)
7169                            | Event::Comment(_)
7170                            | Event::PI(_)
7171                            | Event::GeneralRef(_)) => {
7172                                raw_children.push(boundary, capture_event(event)?);
7173                            }
7174                            Event::End(end)
7175                                if matches_local_name(end.name().as_ref(), b"scaling") =>
7176                            {
7177                                let (log_base, log_base_markup) = optional_markup(log_base);
7178                                let (orientation, orientation_markup) = orientation
7179                                    .map(|(value, markup)| (value, Some(markup)))
7180                                    .unwrap_or((Orientation::MinMax, None));
7181                                let (maximum, maximum_markup) = optional_markup(maximum);
7182                                let (minimum, minimum_markup) = optional_markup(minimum);
7183                                let scaling = Self {
7184                                    log_base,
7185                                    orientation,
7186                                    maximum,
7187                                    minimum,
7188                                    log_base_markup,
7189                                    orientation_markup,
7190                                    maximum_markup,
7191                                    minimum_markup,
7192                                    raw_attributes,
7193                                    raw_children: raw_children_in_schema_order(&raw_children, 4),
7194                                };
7195                                scaling.validate()?;
7196                                return Ok(scaling);
7197                            }
7198                            Event::Eof => return Err(missing_end("c:scaling")),
7199                            _ => {}
7200                        }
7201                        inner.clear();
7202                    }
7203                }
7204                Event::Empty(element)
7205                    if matches_local_name(element.name().as_ref(), b"scaling") =>
7206                {
7207                    typed_rewrite_bindings(&element, inherited)?;
7208                    return Ok(Self {
7209                        raw_attributes: capture_attributes(&element)?,
7210                        ..Self::default()
7211                    });
7212                }
7213                Event::Start(element) | Event::Empty(element) => {
7214                    return Err(ChartError::UnexpectedElement(element_name(&element)));
7215                }
7216                Event::Eof => return Err(ChartError::MissingElement("c:scaling".to_owned())),
7217                _ => {}
7218            }
7219            buffer.clear();
7220        }
7221    }
7222
7223    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7224        self.validate()?;
7225        let mut start = BytesStart::new("c:scaling");
7226        push_attributes(&mut start, &self.raw_attributes);
7227        writer
7228            .write_event(Event::Start(start))
7229            .map_err(OxmlError::from)?;
7230        emit_raw(writer, self.raw_children.at(0))?;
7231        if let Some(value) = self.log_base {
7232            write_scalar(
7233                writer,
7234                "c:logBase",
7235                &value.to_string(),
7236                self.log_base_markup.as_ref(),
7237            )?;
7238        }
7239        emit_raw(writer, self.raw_children.at(1))?;
7240        if self.orientation != Orientation::MinMax || self.orientation_markup.is_some() {
7241            write_scalar(
7242                writer,
7243                "c:orientation",
7244                self.orientation.as_str(),
7245                self.orientation_markup.as_ref(),
7246            )?;
7247        }
7248        emit_raw(writer, self.raw_children.at(2))?;
7249        if let Some(value) = self.maximum {
7250            write_scalar(
7251                writer,
7252                "c:max",
7253                &value.to_string(),
7254                self.maximum_markup.as_ref(),
7255            )?;
7256        }
7257        emit_raw(writer, self.raw_children.at(3))?;
7258        if let Some(value) = self.minimum {
7259            write_scalar(
7260                writer,
7261                "c:min",
7262                &value.to_string(),
7263                self.minimum_markup.as_ref(),
7264            )?;
7265        }
7266        emit_raw(writer, self.raw_children.at(4))?;
7267        writer
7268            .write_event(Event::End(BytesEnd::new("c:scaling")))
7269            .map_err(OxmlError::from)?;
7270        Ok(())
7271    }
7272
7273    fn validate(&self) -> Result<()> {
7274        if let Some(value) = self.log_base
7275            && (!value.is_finite() || !(2.0..=1000.0).contains(&value))
7276        {
7277            return Err(ChartError::InvalidValue {
7278                element: "c:logBase".to_owned(),
7279                value: value.to_string(),
7280            });
7281        }
7282        for (element, value) in [("c:max", self.maximum), ("c:min", self.minimum)] {
7283            if let Some(value) = value
7284                && !value.is_finite()
7285            {
7286                return Err(ChartError::InvalidValue {
7287                    element: element.to_owned(),
7288                    value: value.to_string(),
7289                });
7290            }
7291        }
7292        if let (Some(minimum), Some(maximum)) = (self.minimum, self.maximum)
7293            && minimum > maximum
7294        {
7295            return Err(ChartError::InvalidValue {
7296                element: "c:scaling".to_owned(),
7297                value: format!("minimum {minimum} exceeds maximum {maximum}"),
7298            });
7299        }
7300        Ok(())
7301    }
7302}
7303
7304#[allow(clippy::too_many_arguments)]
7305fn parse_scaling_child(
7306    name: &[u8],
7307    raw: Vec<u8>,
7308    log_base: &mut Option<(f64, ScalarMarkup)>,
7309    orientation: &mut Option<(Orientation, ScalarMarkup)>,
7310    maximum: &mut Option<(f64, ScalarMarkup)>,
7311    minimum: &mut Option<(f64, ScalarMarkup)>,
7312    raw_children: &mut OrderedRawChildren,
7313    boundary: &mut usize,
7314) -> Result<()> {
7315    match name {
7316        b"logBase" => {
7317            set_once(log_base, parse_f64_scalar(&raw, "logBase")?, "c:logBase")?;
7318            *boundary = (*boundary).max(1);
7319        }
7320        b"orientation" => {
7321            let (value, markup) = scalar_value(&raw, "orientation")?;
7322            let value = value
7323                .ok_or_else(|| invalid_attribute("orientation", "val", "<missing>".to_owned()))?;
7324            let parsed = Orientation::parse(&value)
7325                .ok_or_else(|| invalid_attribute("orientation", "val", value))?;
7326            set_once(orientation, (parsed, markup), "c:orientation")?;
7327            *boundary = (*boundary).max(2);
7328        }
7329        b"max" => {
7330            set_once(maximum, parse_f64_scalar(&raw, "max")?, "c:max")?;
7331            *boundary = (*boundary).max(3);
7332        }
7333        b"min" => {
7334            set_once(minimum, parse_f64_scalar(&raw, "min")?, "c:min")?;
7335            *boundary = (*boundary).max(4);
7336        }
7337        _ => raw_children.push(*boundary, raw),
7338    }
7339    Ok(())
7340}
7341
7342impl ChartLines {
7343    fn from_xml(xml: &[u8], local: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7344        let mut reader = Reader::from_reader(xml);
7345        let mut buffer = Vec::new();
7346        loop {
7347            match reader
7348                .read_event_into(&mut buffer)
7349                .map_err(OxmlError::from)?
7350            {
7351                Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
7352                    let namespaces = typed_rewrite_bindings(&element, inherited)?;
7353                    let raw_attributes = capture_attributes(&element)?;
7354                    let mut sp_pr = None;
7355                    let mut raw_children = OrderedRawChildren::default();
7356                    let mut boundary = 0usize;
7357                    let mut inner = Vec::new();
7358                    loop {
7359                        match reader
7360                            .read_event_into(&mut inner)
7361                            .map_err(OxmlError::from)?
7362                        {
7363                            Event::Start(child) => {
7364                                let name = chart_child_local(&child, &namespaces)?;
7365                                let raw = capture_element(&mut reader, &child)?;
7366                                if name.as_deref() == Some(b"spPr") {
7367                                    parse_chart_lines_shape(&raw, &namespaces, &mut sp_pr)?;
7368                                    boundary = 1;
7369                                } else {
7370                                    raw_children.push(boundary, raw);
7371                                }
7372                            }
7373                            Event::Empty(child) => {
7374                                let name = chart_child_local(&child, &namespaces)?;
7375                                let raw = capture_empty_element(&child)?;
7376                                if name.as_deref() == Some(b"spPr") {
7377                                    parse_chart_lines_shape(&raw, &namespaces, &mut sp_pr)?;
7378                                    boundary = 1;
7379                                } else {
7380                                    raw_children.push(boundary, raw);
7381                                }
7382                            }
7383                            event @ (Event::Text(_)
7384                            | Event::CData(_)
7385                            | Event::Comment(_)
7386                            | Event::PI(_)
7387                            | Event::GeneralRef(_)) => {
7388                                raw_children.push(boundary, capture_event(event)?);
7389                            }
7390                            Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
7391                                return Ok(Self {
7392                                    sp_pr,
7393                                    raw_attributes,
7394                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
7395                                });
7396                            }
7397                            Event::Eof => {
7398                                return Err(missing_end(&format!(
7399                                    "c:{}",
7400                                    String::from_utf8_lossy(local)
7401                                )));
7402                            }
7403                            _ => {}
7404                        }
7405                        inner.clear();
7406                    }
7407                }
7408                Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
7409                    typed_rewrite_bindings(&element, inherited)?;
7410                    return Ok(Self {
7411                        sp_pr: None,
7412                        raw_attributes: capture_attributes(&element)?,
7413                        raw_children: OrderedRawChildren::default(),
7414                    });
7415                }
7416                Event::Start(element) | Event::Empty(element) => {
7417                    return Err(ChartError::UnexpectedElement(element_name(&element)));
7418                }
7419                Event::Eof => {
7420                    return Err(ChartError::MissingElement(format!(
7421                        "c:{}",
7422                        String::from_utf8_lossy(local)
7423                    )));
7424                }
7425                _ => {}
7426            }
7427            buffer.clear();
7428        }
7429    }
7430
7431    fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
7432        let mut start = BytesStart::new(tag);
7433        push_attributes(&mut start, &self.raw_attributes);
7434        if self.sp_pr.is_none() && self.raw_children.is_empty() {
7435            writer
7436                .write_event(Event::Empty(start))
7437                .map_err(OxmlError::from)?;
7438            return Ok(());
7439        }
7440        writer
7441            .write_event(Event::Start(start))
7442            .map_err(OxmlError::from)?;
7443        emit_raw(writer, self.raw_children.at(0))?;
7444        if let Some(properties) = &self.sp_pr {
7445            properties.write_xml_as(writer, "c:spPr")?;
7446        }
7447        emit_raw(writer, self.raw_children.at(1))?;
7448        writer
7449            .write_event(Event::End(BytesEnd::new(tag)))
7450            .map_err(OxmlError::from)?;
7451        Ok(())
7452    }
7453}
7454
7455fn parse_chart_lines_shape(
7456    raw: &[u8],
7457    namespaces: &NamespaceBindings,
7458    slot: &mut Option<CT_ShapeProperties>,
7459) -> Result<()> {
7460    reject_conflicting_prefix_in_xml(raw, b"a", A_NS)?;
7461    let properties = CT_ShapeProperties::from_xml(raw)?;
7462    let mut writer = Writer::new(Vec::new());
7463    properties.write_xml_as(&mut writer, "c:spPr")?;
7464    reject_rewritten_foreign_elements(raw, &writer.into_inner(), namespaces, b"spPr")?;
7465    set_once(slot, properties, "c:spPr")
7466}
7467
7468impl NumberFormat {
7469    pub fn new(format_code: String, source_linked: bool) -> Result<Self> {
7470        let number_format = Self {
7471            format_code,
7472            source_linked,
7473            raw_attributes: Vec::new(),
7474            raw_content: Vec::new(),
7475        };
7476        number_format.validate()?;
7477        Ok(number_format)
7478    }
7479
7480    fn from_xml(xml: &[u8]) -> Result<Self> {
7481        let mut reader = Reader::from_reader(xml);
7482        let mut buffer = Vec::new();
7483        loop {
7484            match reader
7485                .read_event_into(&mut buffer)
7486                .map_err(OxmlError::from)?
7487            {
7488                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
7489                    return Self::from_element(&element, Vec::new());
7490                }
7491                Event::Start(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
7492                    let mut raw_content = Vec::new();
7493                    let mut inner = Vec::new();
7494                    loop {
7495                        match reader
7496                            .read_event_into(&mut inner)
7497                            .map_err(OxmlError::from)?
7498                        {
7499                            Event::End(end)
7500                                if matches_local_name(end.name().as_ref(), b"numFmt") =>
7501                            {
7502                                return Self::from_element(&element, raw_content);
7503                            }
7504                            event @ (Event::Text(_)
7505                            | Event::CData(_)
7506                            | Event::Comment(_)
7507                            | Event::PI(_)
7508                            | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
7509                            Event::Eof => return Err(missing_end("c:numFmt")),
7510                            _ => return Err(ChartError::UnexpectedElement("c:numFmt".to_owned())),
7511                        }
7512                        inner.clear();
7513                    }
7514                }
7515                Event::Start(element) | Event::Empty(element) => {
7516                    return Err(ChartError::UnexpectedElement(element_name(&element)));
7517                }
7518                Event::Eof => return Err(ChartError::MissingElement("c:numFmt".to_owned())),
7519                _ => {}
7520            }
7521            buffer.clear();
7522        }
7523    }
7524
7525    fn from_element(element: &BytesStart<'_>, raw_content: Vec<Vec<u8>>) -> Result<Self> {
7526        reject_conflicting_prefix(element, b"c", C_NS)?;
7527        let format_code = attribute_value(element, b"formatCode")?
7528            .ok_or_else(|| invalid_attribute("numFmt", "formatCode", "<missing>".to_owned()))?;
7529        let source_linked = attribute_value(element, b"sourceLinked")?
7530            .ok_or_else(|| invalid_attribute("numFmt", "sourceLinked", "<missing>".to_owned()))?;
7531        let source_linked = parse_bool_lexical("numFmt", "sourceLinked", &source_linked)?;
7532        let number_format = Self {
7533            format_code,
7534            source_linked,
7535            raw_attributes: capture_attributes_excluding(
7536                element,
7537                &[b"formatCode", b"sourceLinked"],
7538            )?,
7539            raw_content,
7540        };
7541        number_format.validate()?;
7542        Ok(number_format)
7543    }
7544
7545    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7546        self.validate()?;
7547        let mut start = BytesStart::new("c:numFmt");
7548        start.push_attribute(("formatCode", self.format_code.as_str()));
7549        start.push_attribute(("sourceLinked", bool_lexical(self.source_linked)));
7550        push_attributes(&mut start, &self.raw_attributes);
7551        if self.raw_content.is_empty() {
7552            writer
7553                .write_event(Event::Empty(start))
7554                .map_err(OxmlError::from)?;
7555            return Ok(());
7556        }
7557        writer
7558            .write_event(Event::Start(start))
7559            .map_err(OxmlError::from)?;
7560        for raw in &self.raw_content {
7561            writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
7562        }
7563        writer
7564            .write_event(Event::End(BytesEnd::new("c:numFmt")))
7565            .map_err(OxmlError::from)?;
7566        Ok(())
7567    }
7568
7569    /// Projects one cached value through the supported deterministic subset.
7570    pub fn format_value(&self, value: f64) -> Result<String> {
7571        if !value.is_finite() {
7572            return Err(ChartError::InvalidValue {
7573                element: "c:numFmt value".to_owned(),
7574                value: value.to_string(),
7575            });
7576        }
7577        if self.format_code == "General" {
7578            return Ok(value.to_string());
7579        }
7580
7581        let (numeric_code, percentage) = self
7582            .format_code
7583            .strip_suffix('%')
7584            .map(|code| (code, true))
7585            .unwrap_or((&self.format_code, false));
7586        let precision = if numeric_code == "0" {
7587            Some(0)
7588        } else if let Some(decimals) = numeric_code.strip_prefix("0.")
7589            && !decimals.is_empty()
7590            && decimals.bytes().all(|byte| byte == b'0')
7591        {
7592            Some(decimals.len())
7593        } else {
7594            None
7595        };
7596        let Some(precision) = precision else {
7597            return Err(ChartError::InvalidValue {
7598                element: "c:numFmt/@formatCode projection".to_owned(),
7599                value: self.format_code.clone(),
7600            });
7601        };
7602        let projected = if percentage { value * 100.0 } else { value };
7603        if !projected.is_finite() {
7604            return Err(ChartError::InvalidValue {
7605                element: "c:numFmt value".to_owned(),
7606                value: projected.to_string(),
7607            });
7608        }
7609        let mut formatted = format!("{projected:.precision$}");
7610        if percentage {
7611            formatted.push('%');
7612        }
7613        Ok(formatted)
7614    }
7615
7616    fn validate(&self) -> Result<()> {
7617        if self.format_code.is_empty() {
7618            return Err(ChartError::InvalidValue {
7619                element: "c:numFmt/@formatCode".to_owned(),
7620                value: self.format_code.clone(),
7621            });
7622        }
7623        validate_xml_text(&self.format_code, "c:numFmt/@formatCode")
7624    }
7625}
7626
7627impl CT_DLbls {
7628    pub fn from_xml(xml: &[u8]) -> Result<Self> {
7629        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
7630    }
7631
7632    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7633        let mut reader = Reader::from_reader(xml);
7634        let mut buffer = Vec::new();
7635        loop {
7636            match reader
7637                .read_event_into(&mut buffer)
7638                .map_err(OxmlError::from)?
7639            {
7640                Event::Start(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7641                    chart_root_prefix(&element)?;
7642                    if !element_is_in_namespace(&element, C_NS, inherited)? {
7643                        return Err(ChartError::UnexpectedElement(element_name(&element)));
7644                    }
7645                    return Self::from_element(&mut reader, &element, inherited);
7646                }
7647                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7648                    chart_root_prefix(&element)?;
7649                    let namespaces = chart_bindings(inherited, &element)?;
7650                    if !element_is_in_namespace(&element, C_NS, inherited)? {
7651                        return Err(ChartError::UnexpectedElement(element_name(&element)));
7652                    }
7653                    let (raw_attributes, _) = capture_fixed_root_attributes(
7654                        &element,
7655                        &["xmlns:c", "xmlns:a", "xmlns:r"],
7656                    )?;
7657                    return Ok(Self {
7658                        raw_attributes,
7659                        namespace_declarations: standalone_namespace_declarations(&namespaces)?,
7660                        ..Self::default()
7661                    });
7662                }
7663                Event::Start(element) | Event::Empty(element) => {
7664                    return Err(ChartError::UnexpectedElement(element_name(&element)));
7665                }
7666                Event::Eof => return Err(ChartError::MissingElement("c:dLbls".to_owned())),
7667                _ => {}
7668            }
7669            buffer.clear();
7670        }
7671    }
7672
7673    fn from_element(
7674        reader: &mut Reader<&[u8]>,
7675        start: &BytesStart<'_>,
7676        inherited: &NamespaceBindings,
7677    ) -> Result<Self> {
7678        reject_conflicting_prefix(start, b"a", A_NS)?;
7679        let namespaces = chart_bindings(inherited, start)?;
7680        require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
7681        let (raw_attributes, _) =
7682            capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
7683        let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
7684        let mut state = DataLabelsParseState::new(namespaces);
7685        let mut buffer = Vec::new();
7686        loop {
7687            match reader
7688                .read_event_into(&mut buffer)
7689                .map_err(OxmlError::from)?
7690            {
7691                Event::Start(element) => {
7692                    let name = chart_child_local(&element, &state.namespaces)?;
7693                    let raw = capture_element(reader, &element)?;
7694                    state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
7695                }
7696                Event::Empty(element) => {
7697                    let name = chart_child_local(&element, &state.namespaces)?;
7698                    let raw = capture_empty_element(&element)?;
7699                    state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
7700                }
7701                event @ (Event::Text(_)
7702                | Event::CData(_)
7703                | Event::Comment(_)
7704                | Event::PI(_)
7705                | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
7706                Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7707                    return state.finish(raw_attributes, namespace_declarations);
7708                }
7709                Event::Eof => return Err(missing_end("c:dLbls")),
7710                _ => {}
7711            }
7712            buffer.clear();
7713        }
7714    }
7715
7716    pub fn to_xml(&self) -> Result<Vec<u8>> {
7717        let mut writer = Writer::new(Vec::new());
7718        self.write_xml(&mut writer, true)?;
7719        Ok(writer.into_inner())
7720    }
7721
7722    fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
7723        self.validate()?;
7724        let mut start = BytesStart::new("c:dLbls");
7725        if standalone {
7726            start.push_attribute(("xmlns:c", C_NS));
7727            start.push_attribute(("xmlns:a", A_NS));
7728            start.push_attribute(("xmlns:r", R_NS));
7729        }
7730        push_attributes(&mut start, &self.namespace_declarations);
7731        push_attributes(&mut start, &self.raw_attributes);
7732        writer
7733            .write_event(Event::Start(start))
7734            .map_err(OxmlError::from)?;
7735        emit_raw(writer, self.raw_children.at(0))?;
7736        if let Some(number_format) = &self.number_format {
7737            number_format.write_xml(writer)?;
7738        }
7739        emit_raw(writer, self.raw_children.at(1))?;
7740        if let Some(position) = self.position {
7741            write_scalar(
7742                writer,
7743                "c:dLblPos",
7744                position.as_str(),
7745                self.position_markup.as_ref(),
7746            )?;
7747        }
7748        emit_raw(writer, self.raw_children.at(2))?;
7749        write_optional_bool(
7750            writer,
7751            "c:showLegendKey",
7752            self.show_legend_key,
7753            self.show_legend_key_markup.as_ref(),
7754        )?;
7755        emit_raw(writer, self.raw_children.at(3))?;
7756        write_optional_bool(
7757            writer,
7758            "c:showVal",
7759            self.show_value,
7760            self.show_value_markup.as_ref(),
7761        )?;
7762        emit_raw(writer, self.raw_children.at(4))?;
7763        write_optional_bool(
7764            writer,
7765            "c:showCatName",
7766            self.show_category_name,
7767            self.show_category_name_markup.as_ref(),
7768        )?;
7769        emit_raw(writer, self.raw_children.at(5))?;
7770        write_optional_bool(
7771            writer,
7772            "c:showSerName",
7773            self.show_series_name,
7774            self.show_series_name_markup.as_ref(),
7775        )?;
7776        emit_raw(writer, self.raw_children.at(6))?;
7777        write_optional_bool(
7778            writer,
7779            "c:showPercent",
7780            self.show_percent,
7781            self.show_percent_markup.as_ref(),
7782        )?;
7783        emit_raw(writer, self.raw_children.at(7))?;
7784        write_optional_bool(
7785            writer,
7786            "c:showBubbleSize",
7787            self.show_bubble_size,
7788            self.show_bubble_size_markup.as_ref(),
7789        )?;
7790        emit_raw(writer, self.raw_children.at(8))?;
7791        if let Some(separator) = &self.separator {
7792            write_text(
7793                writer,
7794                "c:separator",
7795                separator,
7796                self.separator_markup
7797                    .as_ref()
7798                    .unwrap_or(&TextMarkup::default()),
7799            )?;
7800        }
7801        emit_raw(writer, self.raw_children.at(9))?;
7802        writer
7803            .write_event(Event::End(BytesEnd::new("c:dLbls")))
7804            .map_err(OxmlError::from)?;
7805        Ok(())
7806    }
7807
7808    fn validate(&self) -> Result<()> {
7809        if let Some(number_format) = &self.number_format {
7810            number_format.validate()?;
7811        }
7812        if let Some(separator) = &self.separator {
7813            validate_xml_text(separator, "c:separator")?;
7814        }
7815        Ok(())
7816    }
7817
7818    pub fn raw_children(&self) -> &OrderedRawChildren {
7819        &self.raw_children
7820    }
7821}
7822
7823struct DataLabelsParseState {
7824    namespaces: NamespaceBindings,
7825    number_format: Option<NumberFormat>,
7826    position: Option<(DataLabelPosition, ScalarMarkup)>,
7827    show_legend_key: Option<(bool, ScalarMarkup)>,
7828    show_value: Option<(bool, ScalarMarkup)>,
7829    show_category_name: Option<(bool, ScalarMarkup)>,
7830    show_series_name: Option<(bool, ScalarMarkup)>,
7831    show_percent: Option<(bool, ScalarMarkup)>,
7832    show_bubble_size: Option<(bool, ScalarMarkup)>,
7833    separator: Option<(String, TextMarkup)>,
7834    raw_children: OrderedRawChildren,
7835    boundary: usize,
7836}
7837
7838impl DataLabelsParseState {
7839    fn new(namespaces: NamespaceBindings) -> Self {
7840        Self {
7841            namespaces,
7842            number_format: None,
7843            position: None,
7844            show_legend_key: None,
7845            show_value: None,
7846            show_category_name: None,
7847            show_series_name: None,
7848            show_percent: None,
7849            show_bubble_size: None,
7850            separator: None,
7851            raw_children: OrderedRawChildren::default(),
7852            boundary: 0,
7853        }
7854    }
7855
7856    fn capture_event(&mut self, raw: Vec<u8>) {
7857        self.raw_children.push(self.boundary, raw);
7858    }
7859
7860    fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
7861        match name {
7862            b"numFmt" => {
7863                set_once(
7864                    &mut self.number_format,
7865                    NumberFormat::from_xml(&raw)?,
7866                    "c:numFmt",
7867                )?;
7868                self.boundary = self.boundary.max(1);
7869            }
7870            b"dLblPos" => {
7871                let (value, markup) = scalar_value(&raw, "dLblPos")?;
7872                let lexical = value
7873                    .ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
7874                let position = DataLabelPosition::parse(&lexical)
7875                    .ok_or_else(|| invalid_attribute("dLblPos", "val", lexical))?;
7876                set_once(&mut self.position, (position, markup), "c:dLblPos")?;
7877                self.boundary = self.boundary.max(2);
7878            }
7879            b"showLegendKey" => {
7880                set_once(
7881                    &mut self.show_legend_key,
7882                    parse_bool_value(&raw, "showLegendKey")?,
7883                    "c:showLegendKey",
7884                )?;
7885                self.boundary = self.boundary.max(3);
7886            }
7887            b"showVal" => {
7888                set_once(
7889                    &mut self.show_value,
7890                    parse_bool_value(&raw, "showVal")?,
7891                    "c:showVal",
7892                )?;
7893                self.boundary = self.boundary.max(4);
7894            }
7895            b"showCatName" => {
7896                set_once(
7897                    &mut self.show_category_name,
7898                    parse_bool_value(&raw, "showCatName")?,
7899                    "c:showCatName",
7900                )?;
7901                self.boundary = self.boundary.max(5);
7902            }
7903            b"showSerName" => {
7904                set_once(
7905                    &mut self.show_series_name,
7906                    parse_bool_value(&raw, "showSerName")?,
7907                    "c:showSerName",
7908                )?;
7909                self.boundary = self.boundary.max(6);
7910            }
7911            b"showPercent" => {
7912                set_once(
7913                    &mut self.show_percent,
7914                    parse_bool_value(&raw, "showPercent")?,
7915                    "c:showPercent",
7916                )?;
7917                self.boundary = self.boundary.max(7);
7918            }
7919            b"showBubbleSize" => {
7920                set_once(
7921                    &mut self.show_bubble_size,
7922                    parse_bool_value(&raw, "showBubbleSize")?,
7923                    "c:showBubbleSize",
7924                )?;
7925                self.boundary = self.boundary.max(8);
7926            }
7927            b"separator" => {
7928                set_once(
7929                    &mut self.separator,
7930                    parse_text_element(&raw, b"separator", &self.namespaces)?,
7931                    "c:separator",
7932                )?;
7933                self.boundary = self.boundary.max(9);
7934            }
7935            _ => {
7936                let boundary = data_labels_raw_boundary(name, self.boundary);
7937                self.raw_children.push(boundary, raw);
7938                self.boundary = self.boundary.max(boundary);
7939            }
7940        }
7941        Ok(())
7942    }
7943
7944    fn finish(
7945        self,
7946        raw_attributes: XmlAttributes,
7947        namespace_declarations: XmlAttributes,
7948    ) -> Result<CT_DLbls> {
7949        let (position, position_markup) = optional_markup(self.position);
7950        let (show_legend_key, show_legend_key_markup) = optional_bool_markup(self.show_legend_key);
7951        let (show_value, show_value_markup) = optional_bool_markup(self.show_value);
7952        let (show_category_name, show_category_name_markup) =
7953            optional_bool_markup(self.show_category_name);
7954        let (show_series_name, show_series_name_markup) =
7955            optional_bool_markup(self.show_series_name);
7956        let (show_percent, show_percent_markup) = optional_bool_markup(self.show_percent);
7957        let (show_bubble_size, show_bubble_size_markup) =
7958            optional_bool_markup(self.show_bubble_size);
7959        let (separator, separator_markup) = self
7960            .separator
7961            .map(|(value, markup)| (Some(value), Some(markup)))
7962            .unwrap_or((None, None));
7963        let labels = CT_DLbls {
7964            number_format: self.number_format,
7965            position,
7966            separator,
7967            show_legend_key,
7968            show_value,
7969            show_category_name,
7970            show_series_name,
7971            show_percent,
7972            show_bubble_size,
7973            position_markup,
7974            show_legend_key_markup,
7975            show_value_markup,
7976            show_category_name_markup,
7977            show_series_name_markup,
7978            show_percent_markup,
7979            show_bubble_size_markup,
7980            separator_markup,
7981            raw_attributes,
7982            namespace_declarations,
7983            raw_children: raw_children_in_schema_order(&self.raw_children, 9),
7984        };
7985        labels.validate()?;
7986        Ok(labels)
7987    }
7988}
7989
7990fn data_labels_raw_boundary(name: &[u8], current: usize) -> usize {
7991    match name {
7992        b"dLbl" | b"delete" => 0,
7993        b"spPr" | b"txPr" => 1,
7994        b"showLeaderLines" | b"leaderLines" | b"extLst" => 9,
7995        _ => current,
7996    }
7997}
7998
7999fn optional_bool_markup(value: Option<(bool, ScalarMarkup)>) -> (bool, Option<ScalarMarkup>) {
8000    value
8001        .map(|(value, markup)| (value, Some(markup)))
8002        .unwrap_or((false, None))
8003}
8004
8005fn write_optional_bool(
8006    writer: &mut Writer<Vec<u8>>,
8007    tag: &str,
8008    value: bool,
8009    markup: Option<&ScalarMarkup>,
8010) -> Result<()> {
8011    if value || markup.is_some() {
8012        write_scalar(writer, tag, bool_lexical(value), markup)?;
8013    }
8014    Ok(())
8015}
8016
8017fn default_axis_id_markup(id: AxisId) -> AxisIdMarkup {
8018    AxisIdMarkup {
8019        scalar: ScalarMarkup::default(),
8020        parsed: id,
8021        lexical: id.value().to_string(),
8022    }
8023}
8024
8025fn parse_axis_id_scalar(xml: &[u8], local: &str) -> Result<(AxisId, AxisIdMarkup)> {
8026    let (value, scalar) = scalar_value(xml, local)?;
8027    let lexical = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8028    let parsed = lexical
8029        .parse::<i64>()
8030        .map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
8031    let id = AxisId::new(parsed).map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
8032    Ok((
8033        id,
8034        AxisIdMarkup {
8035            scalar,
8036            parsed: id,
8037            lexical,
8038        },
8039    ))
8040}
8041
8042fn write_axis_id<W: Write>(
8043    writer: &mut Writer<W>,
8044    tag: &str,
8045    id: AxisId,
8046    markup: &AxisIdMarkup,
8047) -> Result<()> {
8048    AxisId::new(id.value())?;
8049    let normalized;
8050    let value = if id == markup.parsed {
8051        markup.lexical.as_str()
8052    } else {
8053        normalized = id.value().to_string();
8054        normalized.as_str()
8055    };
8056    write_scalar(writer, tag, value, Some(&markup.scalar))
8057}
8058
8059fn parse_f64_scalar(xml: &[u8], local: &str) -> Result<(f64, ScalarMarkup)> {
8060    let (value, markup) = scalar_value(xml, local)?;
8061    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8062    let parsed = value
8063        .parse::<f64>()
8064        .map_err(|_| invalid_attribute(local, "val", value.clone()))?;
8065    if !parsed.is_finite() {
8066        return Err(invalid_attribute(local, "val", value));
8067    }
8068    Ok((parsed, markup))
8069}
8070
8071fn parse_axis_position(xml: &[u8]) -> Result<(AxisPosition, ScalarMarkup)> {
8072    let (value, markup) = scalar_value(xml, "axPos")?;
8073    let value = value.ok_or_else(|| invalid_attribute("axPos", "val", "<missing>".to_owned()))?;
8074    let parsed =
8075        AxisPosition::parse(&value).ok_or_else(|| invalid_attribute("axPos", "val", value))?;
8076    Ok((parsed, markup))
8077}
8078
8079fn parse_tick_mark(xml: &[u8], local: &str) -> Result<(TickMark, ScalarMarkup)> {
8080    let (value, markup) = scalar_value(xml, local)?;
8081    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8082    let parsed = TickMark::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
8083    Ok((parsed, markup))
8084}
8085
8086fn parse_tick_label_position(xml: &[u8]) -> Result<(TickLabelPosition, ScalarMarkup)> {
8087    let (value, markup) = scalar_value(xml, "tickLblPos")?;
8088    let value =
8089        value.ok_or_else(|| invalid_attribute("tickLblPos", "val", "<missing>".to_owned()))?;
8090    let parsed = TickLabelPosition::parse(&value)
8091        .ok_or_else(|| invalid_attribute("tickLblPos", "val", value))?;
8092    Ok((parsed, markup))
8093}
8094
8095fn optional_markup<T>(value: Option<(T, ScalarMarkup)>) -> (Option<T>, Option<ScalarMarkup>) {
8096    value
8097        .map(|(value, markup)| (Some(value), Some(markup)))
8098        .unwrap_or((None, None))
8099}
8100
8101fn optional_scalar_markup_eq(left: &Option<ScalarMarkup>, right: &Option<ScalarMarkup>) -> bool {
8102    let empty = ScalarMarkup::default();
8103    left.as_ref().unwrap_or(&empty) == right.as_ref().unwrap_or(&empty)
8104}
8105
8106fn axis_id_markup_eq(left: &AxisIdMarkup, right: &AxisIdMarkup) -> bool {
8107    left.scalar == right.scalar
8108}
8109
8110const fn bool_lexical(value: bool) -> &'static str {
8111    if value { "1" } else { "0" }
8112}
8113
8114fn parse_bool_lexical(element: &str, attribute: &str, value: &str) -> Result<bool> {
8115    match value {
8116        "1" | "true" => Ok(true),
8117        "0" | "false" => Ok(false),
8118        _ => Err(invalid_attribute(element, attribute, value.to_owned())),
8119    }
8120}
8121
8122impl CT_Title {
8123    fn plain_text(text: &str) -> Self {
8124        let mut body = CT_TextBody::new();
8125        body.set_text(text);
8126        let mut raw_children = OrderedRawChildren::default();
8127        raw_children.push(1, br#"<c:layout/><c:overlay val="0"/>"#.to_vec());
8128        Self {
8129            text: Some(body),
8130            raw_children,
8131            ..Self::default()
8132        }
8133    }
8134
8135    fn from_xml(xml: &[u8]) -> Result<Self> {
8136        let (raw_attributes, raw_children) = parse_raw_shell(xml, b"title", "c:title")?;
8137        Ok(Self {
8138            text: None,
8139            raw_attributes,
8140            raw_children,
8141        })
8142    }
8143
8144    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8145        if self.text.is_none() {
8146            return write_raw_shell(writer, "c:title", &self.raw_attributes, &self.raw_children);
8147        }
8148        let mut start = BytesStart::new("c:title");
8149        push_attributes(&mut start, &self.raw_attributes);
8150        writer
8151            .write_event(Event::Start(start))
8152            .map_err(OxmlError::from)?;
8153        emit_raw(writer, self.raw_children.at(0))?;
8154        writer
8155            .write_event(Event::Start(BytesStart::new("c:tx")))
8156            .map_err(OxmlError::from)?;
8157        if let Some(text) = &self.text {
8158            text.write_xml_as(writer, "c:rich")?;
8159        }
8160        writer
8161            .write_event(Event::End(BytesEnd::new("c:tx")))
8162            .map_err(OxmlError::from)?;
8163        emit_raw(writer, self.raw_children.at(1))?;
8164        writer
8165            .write_event(Event::End(BytesEnd::new("c:title")))
8166            .map_err(OxmlError::from)?;
8167        Ok(())
8168    }
8169
8170    pub fn raw_children(&self) -> &OrderedRawChildren {
8171        &self.raw_children
8172    }
8173}
8174
8175impl CT_PlotArea {
8176    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
8177        let (raw_attributes, captured_children) = parse_raw_shell(xml, b"plotArea", "c:plotArea")?;
8178        let namespace_bindings = root_chart_bindings(xml, b"plotArea", inherited)?;
8179        let direct_roots: Vec<_> = captured_children
8180            .at(0)
8181            .filter_map(|raw| chart_root_local(raw, &namespace_bindings).transpose())
8182            .collect::<Result<_>>()?;
8183        let plot_roots: Vec<_> = direct_roots
8184            .iter()
8185            .filter(|local| is_plot_container(local))
8186            .cloned()
8187            .collect();
8188        let typed_local = (plot_roots.len() == 1
8189            && matches!(
8190                plot_roots[0].as_slice(),
8191                b"barChart"
8192                    | b"lineChart"
8193                    | b"pieChart"
8194                    | b"doughnutChart"
8195                    | b"areaChart"
8196                    | b"scatterChart"
8197                    | b"radarChart"
8198            ))
8199        .then(|| plot_roots[0].clone());
8200
8201        let Some(typed_local) = typed_local else {
8202            return Ok(Self {
8203                raw_attributes,
8204                raw_children: captured_children,
8205                namespace_bindings,
8206                plots: None,
8207                plot_markup: Vec::new(),
8208                axes: Vec::new(),
8209            });
8210        };
8211
8212        let mut raw_children = OrderedRawChildren::default();
8213        let mut plots = Vec::new();
8214        let mut plot_markup = Vec::new();
8215        let mut axes = Vec::new();
8216        let mut boundary = 0usize;
8217        for raw in captured_children.at(0) {
8218            let local = chart_root_local(raw, &namespace_bindings)?;
8219            if local.as_deref() == Some(typed_local.as_slice()) {
8220                let (plot, markup) = parse_plot(raw, &namespace_bindings)?;
8221                plots.push(plot);
8222                plot_markup.push(markup);
8223                boundary = 1;
8224            } else if local.as_deref().and_then(AxisKind::parse).is_some() {
8225                axes.push(Axis::from_xml_with_namespaces(raw, &namespace_bindings)?);
8226                boundary = axes.len() + 1;
8227            } else {
8228                raw_children.push(boundary, raw.to_vec());
8229            }
8230        }
8231        let plot_area = Self {
8232            raw_attributes,
8233            raw_children,
8234            namespace_bindings,
8235            plots: Some(plots),
8236            plot_markup,
8237            axes,
8238        };
8239        plot_area.validate_typed()?;
8240        Ok(plot_area)
8241    }
8242
8243    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8244        let Some(plots) = &self.plots else {
8245            return write_raw_shell(
8246                writer,
8247                "c:plotArea",
8248                &self.raw_attributes,
8249                &self.raw_children,
8250            );
8251        };
8252        self.validate_typed()?;
8253        let mut start = BytesStart::new("c:plotArea");
8254        push_attributes(&mut start, &self.raw_attributes);
8255        writer
8256            .write_event(Event::Start(start))
8257            .map_err(OxmlError::from)?;
8258        emit_raw(writer, self.raw_children.at(0))?;
8259        for (index, plot) in plots.iter().enumerate() {
8260            write_plot(
8261                writer,
8262                plot,
8263                self.plot_markup
8264                    .get(index)
8265                    .unwrap_or(&PlotMarkup::default()),
8266            )?;
8267        }
8268        emit_raw(writer, self.raw_children.at(1))?;
8269        for (index, axis) in self.axes.iter().enumerate() {
8270            writer
8271                .get_mut()
8272                .write_all(&axis.to_xml()?)
8273                .map_err(OxmlError::from)?;
8274            emit_raw(writer, self.raw_children.at(index + 2))?;
8275        }
8276        writer
8277            .write_event(Event::End(BytesEnd::new("c:plotArea")))
8278            .map_err(OxmlError::from)?;
8279        Ok(())
8280    }
8281
8282    pub fn raw_children(&self) -> &OrderedRawChildren {
8283        &self.raw_children
8284    }
8285
8286    /// Creates a supported single-family plot area with owned axes.
8287    pub fn new(plots: Vec<Plot>, axes: Vec<Axis>) -> Result<Self> {
8288        let plot_markup = plots
8289            .iter()
8290            .map(|plot| {
8291                let mut markup = PlotMarkup::default();
8292                if matches!(plot, Plot::Line { .. }) {
8293                    markup.marker = Some(ScalarMarkup::default());
8294                    markup.smooth = Some(ScalarMarkup::default());
8295                }
8296                if matches!(plot, Plot::Doughnut { .. }) {
8297                    markup.hole_size = Some(ScalarMarkup::default());
8298                }
8299                markup
8300            })
8301            .collect();
8302        let plot_area = Self {
8303            raw_attributes: Vec::new(),
8304            raw_children: OrderedRawChildren::default(),
8305            namespace_bindings: chart_namespace_defaults(),
8306            plot_markup,
8307            plots: Some(plots),
8308            axes,
8309        };
8310        plot_area.validate_typed()?;
8311        Ok(plot_area)
8312    }
8313
8314    /// Returns the owned supported plots, or an error for an opaque choice.
8315    pub fn plots(&self) -> Result<&[Plot]> {
8316        self.plots
8317            .as_deref()
8318            .ok_or_else(|| ChartError::InvalidValue {
8319                element: "c:plotArea".to_owned(),
8320                value: "unsupported or combination plot area is opaque".to_owned(),
8321            })
8322    }
8323
8324    /// Returns mutable supported plots without exposing opaque content to edits.
8325    pub fn plots_mut(&mut self) -> Result<&mut [Plot]> {
8326        self.plots
8327            .as_deref_mut()
8328            .ok_or_else(|| ChartError::InvalidValue {
8329                element: "c:plotArea".to_owned(),
8330                value: "cannot combine typed plots with an opaque plot choice".to_owned(),
8331            })
8332    }
8333
8334    /// Parses the common series payloads nested in category-based plot shells.
8335    pub fn series(&self) -> Result<Vec<Series>> {
8336        if let Some(plots) = &self.plots {
8337            return Ok(plots
8338                .iter()
8339                .flat_map(|plot| match plot {
8340                    Plot::Bar { series, .. }
8341                    | Plot::Line { series, .. }
8342                    | Plot::Pie { series, .. }
8343                    | Plot::Doughnut { series, .. }
8344                    | Plot::Area { series, .. }
8345                    | Plot::Scatter { series, .. }
8346                    | Plot::Radar { series, .. } => series.clone(),
8347                })
8348                .collect());
8349        }
8350        let mut series = Vec::new();
8351        for raw in self.raw_children.at(0) {
8352            parse_plot_series(raw, &self.namespace_bindings, &mut series)?;
8353        }
8354        Ok(series)
8355    }
8356
8357    /// Parses and validates the direct axis children of this plot area.
8358    pub fn axes(&self) -> Result<Vec<Axis>> {
8359        if self.plots.is_some() {
8360            self.validate_typed()?;
8361            return Ok(self.axes.clone());
8362        }
8363        let mut axes = Vec::new();
8364        for raw in self.raw_children.at(0) {
8365            if let Some(axis) = parse_plot_axis(raw, &self.namespace_bindings)? {
8366                axes.push(axis);
8367            }
8368        }
8369        validate_axis_pairs(&axes)?;
8370        Ok(axes)
8371    }
8372
8373    fn validate_typed(&self) -> Result<()> {
8374        let plots = self
8375            .plots
8376            .as_ref()
8377            .ok_or_else(|| ChartError::InvalidValue {
8378                element: "c:plotArea".to_owned(),
8379                value: "opaque plot area has no typed validation view".to_owned(),
8380            })?;
8381        if plots.len() != 1 {
8382            return Err(ChartError::InvalidValue {
8383                element: "c:plotArea".to_owned(),
8384                value: "typed plot area requires exactly one plot family".to_owned(),
8385            });
8386        }
8387        validate_axis_pairs(&self.axes)?;
8388        for plot in plots {
8389            plot.validate()?;
8390            if let Some(axis_ids) = plot.axis_ids() {
8391                for id in axis_ids {
8392                    if !self.axes.iter().any(|axis| axis.id == id) {
8393                        return Err(ChartError::InvalidValue {
8394                            element: "c:axId".to_owned(),
8395                            value: format!("plot references missing axis {}", id.value()),
8396                        });
8397                    }
8398                }
8399            } else if !self.axes.is_empty() {
8400                return Err(ChartError::InvalidValue {
8401                    element: "c:plotArea".to_owned(),
8402                    value: "pie and doughnut plot areas must not own axes".to_owned(),
8403                });
8404            }
8405            if plot.axis_ids().is_some() && self.axes.len() != 2 {
8406                return Err(ChartError::InvalidValue {
8407                    element: "c:plotArea".to_owned(),
8408                    value: format!(
8409                        "axis-owned plot requires exactly 2 axes, found {}",
8410                        self.axes.len()
8411                    ),
8412                });
8413            }
8414        }
8415        Ok(())
8416    }
8417}
8418
8419impl Default for CT_PlotArea {
8420    fn default() -> Self {
8421        Self {
8422            raw_attributes: Vec::new(),
8423            raw_children: OrderedRawChildren::default(),
8424            namespace_bindings: chart_namespace_defaults(),
8425            plots: Some(Vec::new()),
8426            plot_markup: Vec::new(),
8427            axes: Vec::new(),
8428        }
8429    }
8430}
8431
8432fn chart_root_local(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Vec<u8>>> {
8433    let mut reader = Reader::from_reader(xml);
8434    let mut buffer = Vec::new();
8435    loop {
8436        match reader
8437            .read_event_into(&mut buffer)
8438            .map_err(OxmlError::from)?
8439        {
8440            Event::Start(element) | Event::Empty(element) => {
8441                return Ok(element_is_in_namespace(&element, C_NS, inherited)?
8442                    .then(|| local_name(element.name().as_ref()).to_vec()));
8443            }
8444            Event::Eof => return Ok(None),
8445            _ => {}
8446        }
8447        buffer.clear();
8448    }
8449}
8450
8451fn is_plot_container(local: &[u8]) -> bool {
8452    matches!(
8453        local,
8454        b"areaChart"
8455            | b"area3DChart"
8456            | b"barChart"
8457            | b"bar3DChart"
8458            | b"bubbleChart"
8459            | b"doughnutChart"
8460            | b"lineChart"
8461            | b"line3DChart"
8462            | b"ofPieChart"
8463            | b"pieChart"
8464            | b"pie3DChart"
8465            | b"radarChart"
8466            | b"scatterChart"
8467            | b"stockChart"
8468            | b"surfaceChart"
8469            | b"surface3DChart"
8470    )
8471}
8472
8473fn parse_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8474    let local = chart_root_local(xml, inherited)?
8475        .ok_or_else(|| ChartError::MissingElement("c:barChart or c:lineChart".to_owned()))?;
8476    match local.as_slice() {
8477        b"barChart" => parse_bar_plot(xml, inherited),
8478        b"lineChart" => parse_line_plot(xml, inherited),
8479        b"pieChart" | b"doughnutChart" | b"areaChart" | b"scatterChart" | b"radarChart" => {
8480            parse_remaining_plot(xml, inherited, &local)
8481        }
8482        _ => Err(ChartError::UnexpectedElement(format!(
8483            "c:{}",
8484            String::from_utf8_lossy(&local)
8485        ))),
8486    }
8487}
8488
8489fn parse_remaining_plot(
8490    xml: &[u8],
8491    inherited: &NamespaceBindings,
8492    local: &[u8],
8493) -> Result<(Plot, PlotMarkup)> {
8494    let root = String::from_utf8_lossy(local).into_owned();
8495    let mut reader = Reader::from_reader(xml);
8496    let mut buffer = Vec::new();
8497    loop {
8498        match reader
8499            .read_event_into(&mut buffer)
8500            .map_err(OxmlError::from)?
8501        {
8502            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
8503                let namespaces = typed_rewrite_bindings(&element, inherited)?;
8504                let (mut raw_attributes, namespace_declarations) =
8505                    capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8506                raw_attributes.splice(0..0, namespace_declarations);
8507                let mut grouping = None;
8508                let mut style = None;
8509                let mut first_slice_angle = None;
8510                let mut hole_size = None;
8511                let mut series = Vec::new();
8512                let mut data_labels = None;
8513                let mut axis_ids = Vec::new();
8514                let mut raw_children = OrderedRawChildren::default();
8515                let mut boundary = 0usize;
8516                let mut inner = Vec::new();
8517                loop {
8518                    match reader
8519                        .read_event_into(&mut inner)
8520                        .map_err(OxmlError::from)?
8521                    {
8522                        Event::Start(child) => {
8523                            let name = chart_child_local(&child, &namespaces)?;
8524                            let raw = capture_element(&mut reader, &child)?;
8525                            parse_remaining_plot_child(
8526                                local,
8527                                name.as_deref().unwrap_or_default(),
8528                                raw,
8529                                &namespaces,
8530                                &mut grouping,
8531                                &mut style,
8532                                &mut first_slice_angle,
8533                                &mut hole_size,
8534                                &mut series,
8535                                &mut data_labels,
8536                                &mut axis_ids,
8537                                &mut raw_children,
8538                                &mut boundary,
8539                            )?;
8540                        }
8541                        Event::Empty(child) => {
8542                            let name = chart_child_local(&child, &namespaces)?;
8543                            let raw = capture_empty_element(&child)?;
8544                            parse_remaining_plot_child(
8545                                local,
8546                                name.as_deref().unwrap_or_default(),
8547                                raw,
8548                                &namespaces,
8549                                &mut grouping,
8550                                &mut style,
8551                                &mut first_slice_angle,
8552                                &mut hole_size,
8553                                &mut series,
8554                                &mut data_labels,
8555                                &mut axis_ids,
8556                                &mut raw_children,
8557                                &mut boundary,
8558                            )?;
8559                        }
8560                        event @ (Event::Text(_)
8561                        | Event::CData(_)
8562                        | Event::Comment(_)
8563                        | Event::PI(_)
8564                        | Event::GeneralRef(_)) => {
8565                            raw_children.push(boundary, capture_event(event)?);
8566                        }
8567                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
8568                            let needs_axes =
8569                                matches!(local, b"areaChart" | b"scatterChart" | b"radarChart");
8570                            if needs_axes && axis_ids.len() != 2 {
8571                                return Err(ChartError::InvalidValue {
8572                                    element: format!("c:{root}/c:axId"),
8573                                    value: format!("expected 2, found {}", axis_ids.len()),
8574                                });
8575                            }
8576                            if !needs_axes && !axis_ids.is_empty() {
8577                                return Err(ChartError::InvalidValue {
8578                                    element: format!("c:{root}/c:axId"),
8579                                    value: "axis-free plot contains axis references".to_owned(),
8580                                });
8581                            }
8582                            let parsed_ids = needs_axes.then(|| [axis_ids[0].0, axis_ids[1].0]);
8583                            let original_series_keys = series
8584                                .iter()
8585                                .map(|item| (item.index, item.order))
8586                                .collect::<Vec<_>>();
8587                            let plot = match local {
8588                                b"pieChart" => Plot::Pie {
8589                                    first_slice_angle: first_slice_angle
8590                                        .as_ref()
8591                                        .map_or(0, |item| item.0),
8592                                    series,
8593                                    data_labels,
8594                                },
8595                                b"doughnutChart" => Plot::Doughnut {
8596                                    first_slice_angle: first_slice_angle
8597                                        .as_ref()
8598                                        .map_or(0, |item| item.0),
8599                                    hole_size: hole_size.as_ref().map_or(50, |item| item.0),
8600                                    series,
8601                                    data_labels,
8602                                },
8603                                b"areaChart" => Plot::Area {
8604                                    grouping: grouping
8605                                        .as_ref()
8606                                        .ok_or_else(|| {
8607                                            ChartError::MissingElement("c:grouping".to_owned())
8608                                        })?
8609                                        .0,
8610                                    series,
8611                                    data_labels,
8612                                    axis_ids: parsed_ids.ok_or_else(|| {
8613                                        ChartError::MissingElement("c:areaChart/c:axId".to_owned())
8614                                    })?,
8615                                },
8616                                b"scatterChart" => {
8617                                    let lexical = style
8618                                        .as_ref()
8619                                        .ok_or_else(|| {
8620                                            ChartError::MissingElement("c:scatterStyle".to_owned())
8621                                        })?
8622                                        .0
8623                                        .as_str();
8624                                    Plot::Scatter {
8625                                        style: ScatterStyle::parse(lexical).ok_or_else(|| {
8626                                            invalid_attribute(
8627                                                "scatterStyle",
8628                                                "val",
8629                                                lexical.to_owned(),
8630                                            )
8631                                        })?,
8632                                        series,
8633                                        data_labels,
8634                                        axis_ids: parsed_ids.ok_or_else(|| {
8635                                            ChartError::MissingElement(
8636                                                "c:scatterChart/c:axId".to_owned(),
8637                                            )
8638                                        })?,
8639                                    }
8640                                }
8641                                b"radarChart" => {
8642                                    let lexical = style
8643                                        .as_ref()
8644                                        .ok_or_else(|| {
8645                                            ChartError::MissingElement("c:radarStyle".to_owned())
8646                                        })?
8647                                        .0
8648                                        .as_str();
8649                                    Plot::Radar {
8650                                        style: RadarStyle::parse(lexical).ok_or_else(|| {
8651                                            invalid_attribute(
8652                                                "radarStyle",
8653                                                "val",
8654                                                lexical.to_owned(),
8655                                            )
8656                                        })?,
8657                                        series,
8658                                        data_labels,
8659                                        axis_ids: parsed_ids.ok_or_else(|| {
8660                                            ChartError::MissingElement(
8661                                                "c:radarChart/c:axId".to_owned(),
8662                                            )
8663                                        })?,
8664                                    }
8665                                }
8666                                _ => {
8667                                    return Err(ChartError::UnexpectedElement(format!(
8668                                        "c:{}",
8669                                        String::from_utf8_lossy(local)
8670                                    )));
8671                                }
8672                            };
8673                            plot.validate()?;
8674                            let final_boundary = boundary.max(original_series_keys.len() + 6);
8675                            return Ok((
8676                                plot,
8677                                PlotMarkup {
8678                                    raw_attributes,
8679                                    raw_children: raw_children_in_schema_order(
8680                                        &raw_children,
8681                                        final_boundary,
8682                                    ),
8683                                    grouping: grouping.map(|item| item.1),
8684                                    first_slice_angle: first_slice_angle.map(|item| item.1),
8685                                    hole_size: hole_size.map(|item| item.1),
8686                                    style: style.map(|item| item.1),
8687                                    axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8688                                    original_series_keys,
8689                                    original_axis_ids: parsed_ids
8690                                        .map_or_else(Vec::new, |ids| ids.to_vec()),
8691                                    parsed_remaining: match local {
8692                                        b"pieChart" => Some("pieChart"),
8693                                        b"doughnutChart" => Some("doughnutChart"),
8694                                        b"areaChart" => Some("areaChart"),
8695                                        b"scatterChart" => Some("scatterChart"),
8696                                        b"radarChart" => Some("radarChart"),
8697                                        _ => None,
8698                                    },
8699                                    ..PlotMarkup::default()
8700                                },
8701                            ));
8702                        }
8703                        Event::Eof => return Err(missing_end(&format!("c:{root}"))),
8704                        _ => {}
8705                    }
8706                    inner.clear();
8707                }
8708            }
8709            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
8710                return Err(ChartError::MissingElement("c:ser".to_owned()));
8711            }
8712            Event::Start(element) | Event::Empty(element) => {
8713                return Err(ChartError::UnexpectedElement(element_name(&element)));
8714            }
8715            Event::Eof => return Err(ChartError::MissingElement(format!("c:{root}"))),
8716            _ => {}
8717        }
8718        buffer.clear();
8719    }
8720}
8721
8722#[allow(clippy::too_many_arguments)]
8723fn parse_remaining_plot_child(
8724    root: &[u8],
8725    name: &[u8],
8726    raw: Vec<u8>,
8727    namespaces: &NamespaceBindings,
8728    grouping: &mut Option<(Grouping, ScalarMarkup)>,
8729    style: &mut Option<(String, ScalarMarkup)>,
8730    first_slice_angle: &mut Option<(u16, ScalarMarkup)>,
8731    hole_size: &mut Option<(u8, ScalarMarkup)>,
8732    series: &mut Vec<Series>,
8733    data_labels: &mut Option<CT_DLbls>,
8734    axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
8735    raw_children: &mut OrderedRawChildren,
8736    boundary: &mut usize,
8737) -> Result<()> {
8738    let property_first = matches!(root, b"areaChart" | b"scatterChart" | b"radarChart");
8739    let base = usize::from(property_first);
8740    match name {
8741        b"grouping" if root == b"areaChart" => {
8742            let (value, markup) = required_scalar(&raw, "grouping")?;
8743            let parsed = Grouping::parse(&value)
8744                .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
8745            set_once(grouping, (parsed, markup), "c:grouping")?;
8746            *boundary = (*boundary).max(1);
8747        }
8748        b"scatterStyle" if root == b"scatterChart" => {
8749            let (value, markup) = required_scalar(&raw, "scatterStyle")?;
8750            if ScatterStyle::parse(&value).is_none() {
8751                return Err(invalid_attribute("scatterStyle", "val", value));
8752            }
8753            set_once(style, (value, markup), "c:scatterStyle")?;
8754            *boundary = (*boundary).max(1);
8755        }
8756        b"radarStyle" if root == b"radarChart" => {
8757            let (value, markup) = required_scalar(&raw, "radarStyle")?;
8758            if RadarStyle::parse(&value).is_none() {
8759                return Err(invalid_attribute("radarStyle", "val", value));
8760            }
8761            set_once(style, (value, markup), "c:radarStyle")?;
8762            *boundary = (*boundary).max(1);
8763        }
8764        b"ser" => {
8765            series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
8766            *boundary = (*boundary).max(base + series.len());
8767        }
8768        b"dLbls" => {
8769            set_once(
8770                data_labels,
8771                CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
8772                "c:dLbls",
8773            )?;
8774            *boundary = (*boundary).max(base + series.len() + 1);
8775        }
8776        b"firstSliceAng" if matches!(root, b"pieChart" | b"doughnutChart") => {
8777            let (value, markup) = required_scalar(&raw, "firstSliceAng")?;
8778            let parsed = value
8779                .parse::<u16>()
8780                .map_err(|_| invalid_attribute("firstSliceAng", "val", value.clone()))?;
8781            if parsed > 360 {
8782                return Err(invalid_attribute("firstSliceAng", "val", value));
8783            }
8784            set_once(first_slice_angle, (parsed, markup), "c:firstSliceAng")?;
8785            *boundary = (*boundary).max(series.len() + 2);
8786        }
8787        b"holeSize" if root == b"doughnutChart" => {
8788            let (value, markup) = required_scalar(&raw, "holeSize")?;
8789            let parsed = value
8790                .parse::<u8>()
8791                .map_err(|_| invalid_attribute("holeSize", "val", value.clone()))?;
8792            if !(10..=90).contains(&parsed) {
8793                return Err(invalid_attribute("holeSize", "val", value));
8794            }
8795            set_once(hole_size, (parsed, markup), "c:holeSize")?;
8796            *boundary = (*boundary).max(series.len() + 3);
8797        }
8798        b"axId" if property_first => {
8799            if axis_ids.len() == 2 {
8800                return Err(ChartError::DuplicateElement("c:axId".to_owned()));
8801            }
8802            axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
8803            *boundary = (*boundary).max(base + series.len() + 1 + axis_ids.len());
8804        }
8805        b"axId" => {
8806            return Err(ChartError::InvalidValue {
8807                element: "c:axId".to_owned(),
8808                value: "axis-free plot contains axis reference".to_owned(),
8809            });
8810        }
8811        _ => {
8812            let raw_boundary = match (root, name) {
8813                (_, b"varyColors") => base,
8814                (b"pieChart", b"extLst") => series.len() + 2,
8815                (b"doughnutChart", b"extLst") => series.len() + 3,
8816                (b"areaChart" | b"scatterChart" | b"radarChart", b"dropLines") => {
8817                    base + series.len() + 1
8818                }
8819                (b"areaChart" | b"scatterChart" | b"radarChart", b"extLst") => {
8820                    base + series.len() + 3
8821                }
8822                _ => *boundary,
8823            };
8824            raw_children.push(raw_boundary, raw);
8825            *boundary = (*boundary).max(raw_boundary);
8826        }
8827    }
8828    Ok(())
8829}
8830
8831fn parse_bar_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8832    let mut reader = Reader::from_reader(xml);
8833    let mut buffer = Vec::new();
8834    loop {
8835        match reader
8836            .read_event_into(&mut buffer)
8837            .map_err(OxmlError::from)?
8838        {
8839            Event::Start(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
8840                let namespaces = typed_rewrite_bindings(&element, inherited)?;
8841                let (mut raw_attributes, namespace_declarations) =
8842                    capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8843                raw_attributes.splice(0..0, namespace_declarations);
8844                let mut direction = None;
8845                let mut grouping = None;
8846                let mut gap_width = None;
8847                let mut overlap = None;
8848                let mut series = Vec::new();
8849                let mut data_labels = None;
8850                let mut axis_ids = Vec::new();
8851                let mut raw_children = OrderedRawChildren::default();
8852                let mut boundary = 0usize;
8853                let mut inner = Vec::new();
8854                loop {
8855                    match reader
8856                        .read_event_into(&mut inner)
8857                        .map_err(OxmlError::from)?
8858                    {
8859                        Event::Start(child) => {
8860                            let name = chart_child_local(&child, &namespaces)?;
8861                            let raw = capture_element(&mut reader, &child)?;
8862                            parse_bar_child(
8863                                name.as_deref().unwrap_or_default(),
8864                                raw,
8865                                &namespaces,
8866                                &mut direction,
8867                                &mut grouping,
8868                                &mut gap_width,
8869                                &mut overlap,
8870                                &mut series,
8871                                &mut data_labels,
8872                                &mut axis_ids,
8873                                &mut raw_children,
8874                                &mut boundary,
8875                            )?;
8876                        }
8877                        Event::Empty(child) => {
8878                            let name = chart_child_local(&child, &namespaces)?;
8879                            let raw = capture_empty_element(&child)?;
8880                            parse_bar_child(
8881                                name.as_deref().unwrap_or_default(),
8882                                raw,
8883                                &namespaces,
8884                                &mut direction,
8885                                &mut grouping,
8886                                &mut gap_width,
8887                                &mut overlap,
8888                                &mut series,
8889                                &mut data_labels,
8890                                &mut axis_ids,
8891                                &mut raw_children,
8892                                &mut boundary,
8893                            )?;
8894                        }
8895                        event @ (Event::Text(_)
8896                        | Event::CData(_)
8897                        | Event::Comment(_)
8898                        | Event::PI(_)
8899                        | Event::GeneralRef(_)) => {
8900                            raw_children.push(boundary, capture_event(event)?);
8901                        }
8902                        Event::End(end) if matches_local_name(end.name().as_ref(), b"barChart") => {
8903                            let (direction, direction_markup) = direction
8904                                .ok_or_else(|| ChartError::MissingElement("c:barDir".to_owned()))?;
8905                            let (grouping, grouping_markup) = grouping.ok_or_else(|| {
8906                                ChartError::MissingElement("c:grouping".to_owned())
8907                            })?;
8908                            if axis_ids.len() != 2 {
8909                                return Err(ChartError::InvalidValue {
8910                                    element: "c:barChart/c:axId".to_owned(),
8911                                    value: format!("expected 2, found {}", axis_ids.len()),
8912                                });
8913                            }
8914                            let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
8915                            let original_series_keys: Vec<(u32, u32)> =
8916                                series.iter().map(|item| (item.index, item.order)).collect();
8917                            let plot = Plot::Bar {
8918                                direction,
8919                                grouping,
8920                                gap_width: gap_width.as_ref().map_or(150, |item| item.0),
8921                                overlap: overlap.as_ref().map_or(0, |item| item.0),
8922                                series,
8923                                data_labels,
8924                                axis_ids: parsed_ids,
8925                            };
8926                            plot.validate()?;
8927                            return Ok((
8928                                plot,
8929                                PlotMarkup {
8930                                    raw_attributes,
8931                                    raw_children: raw_children_in_schema_order(
8932                                        &raw_children,
8933                                        original_series_keys.len() + 7,
8934                                    ),
8935                                    direction: Some(direction_markup),
8936                                    grouping: Some(grouping_markup),
8937                                    gap_width: gap_width.map(|item| item.1),
8938                                    overlap: overlap.map(|item| item.1),
8939                                    marker: None,
8940                                    smooth: None,
8941                                    first_slice_angle: None,
8942                                    hole_size: None,
8943                                    style: None,
8944                                    axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8945                                    original_series_keys,
8946                                    original_axis_ids: parsed_ids.to_vec(),
8947                                    parsed_bar: Some(true),
8948                                    parsed_remaining: None,
8949                                },
8950                            ));
8951                        }
8952                        Event::Eof => return Err(missing_end("c:barChart")),
8953                        _ => {}
8954                    }
8955                    inner.clear();
8956                }
8957            }
8958            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
8959                return Err(ChartError::MissingElement("c:barDir".to_owned()));
8960            }
8961            Event::Start(element) | Event::Empty(element) => {
8962                return Err(ChartError::UnexpectedElement(element_name(&element)));
8963            }
8964            Event::Eof => return Err(ChartError::MissingElement("c:barChart".to_owned())),
8965            _ => {}
8966        }
8967        buffer.clear();
8968    }
8969}
8970
8971#[allow(clippy::too_many_arguments)]
8972fn parse_bar_child(
8973    name: &[u8],
8974    raw: Vec<u8>,
8975    namespaces: &NamespaceBindings,
8976    direction: &mut Option<(BarDirection, ScalarMarkup)>,
8977    grouping: &mut Option<(BarGrouping, ScalarMarkup)>,
8978    gap_width: &mut Option<(u16, ScalarMarkup)>,
8979    overlap: &mut Option<(i8, ScalarMarkup)>,
8980    series: &mut Vec<Series>,
8981    data_labels: &mut Option<CT_DLbls>,
8982    axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
8983    raw_children: &mut OrderedRawChildren,
8984    boundary: &mut usize,
8985) -> Result<()> {
8986    match name {
8987        b"barDir" => {
8988            let (value, markup) = required_scalar(&raw, "barDir")?;
8989            let value = BarDirection::parse(&value)
8990                .ok_or_else(|| invalid_attribute("barDir", "val", value))?;
8991            set_once(direction, (value, markup), "c:barDir")?;
8992            *boundary = (*boundary).max(1);
8993        }
8994        b"grouping" => {
8995            let (value, markup) = required_scalar(&raw, "grouping")?;
8996            let value = BarGrouping::parse(&value)
8997                .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
8998            set_once(grouping, (value, markup), "c:grouping")?;
8999            *boundary = (*boundary).max(2);
9000        }
9001        b"ser" => {
9002            series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
9003            *boundary = (*boundary).max(series.len() + 2);
9004        }
9005        b"dLbls" => {
9006            set_once(
9007                data_labels,
9008                CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
9009                "c:dLbls",
9010            )?;
9011            *boundary = (*boundary).max(series.len() + 3);
9012        }
9013        b"gapWidth" => {
9014            let (value, markup) = required_scalar(&raw, "gapWidth")?;
9015            let parsed = value
9016                .parse::<u16>()
9017                .map_err(|_| invalid_attribute("gapWidth", "val", value.clone()))?;
9018            if parsed > 500 {
9019                return Err(invalid_attribute("gapWidth", "val", value));
9020            }
9021            set_once(gap_width, (parsed, markup), "c:gapWidth")?;
9022            *boundary = (*boundary).max(series.len() + 4);
9023        }
9024        b"overlap" => {
9025            let (value, markup) = required_scalar(&raw, "overlap")?;
9026            let parsed = value
9027                .parse::<i8>()
9028                .map_err(|_| invalid_attribute("overlap", "val", value.clone()))?;
9029            if !(-100..=100).contains(&parsed) {
9030                return Err(invalid_attribute("overlap", "val", value));
9031            }
9032            set_once(overlap, (parsed, markup), "c:overlap")?;
9033            *boundary = (*boundary).max(series.len() + 5);
9034        }
9035        b"axId" => {
9036            if axis_ids.len() == 2 {
9037                return Err(ChartError::DuplicateElement("c:axId".to_owned()));
9038            }
9039            axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
9040            *boundary = (*boundary).max(series.len() + 5 + axis_ids.len());
9041        }
9042        _ => raw_children.push(*boundary, raw),
9043    }
9044    Ok(())
9045}
9046
9047fn parse_line_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
9048    let mut reader = Reader::from_reader(xml);
9049    let mut buffer = Vec::new();
9050    loop {
9051        match reader
9052            .read_event_into(&mut buffer)
9053            .map_err(OxmlError::from)?
9054        {
9055            Event::Start(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
9056                let namespaces = typed_rewrite_bindings(&element, inherited)?;
9057                let (mut raw_attributes, namespace_declarations) =
9058                    capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
9059                raw_attributes.splice(0..0, namespace_declarations);
9060                let mut grouping = None;
9061                let mut marker = None;
9062                let mut smooth = None;
9063                let mut series = Vec::new();
9064                let mut data_labels = None;
9065                let mut axis_ids = Vec::new();
9066                let mut raw_children = OrderedRawChildren::default();
9067                let mut boundary = 0usize;
9068                let mut inner = Vec::new();
9069                loop {
9070                    match reader
9071                        .read_event_into(&mut inner)
9072                        .map_err(OxmlError::from)?
9073                    {
9074                        Event::Start(child) => {
9075                            let name = chart_child_local(&child, &namespaces)?;
9076                            let raw = capture_element(&mut reader, &child)?;
9077                            parse_line_child(
9078                                name.as_deref().unwrap_or_default(),
9079                                raw,
9080                                &namespaces,
9081                                &mut grouping,
9082                                &mut marker,
9083                                &mut smooth,
9084                                &mut series,
9085                                &mut data_labels,
9086                                &mut axis_ids,
9087                                &mut raw_children,
9088                                &mut boundary,
9089                            )?;
9090                        }
9091                        Event::Empty(child) => {
9092                            let name = chart_child_local(&child, &namespaces)?;
9093                            let raw = capture_empty_element(&child)?;
9094                            parse_line_child(
9095                                name.as_deref().unwrap_or_default(),
9096                                raw,
9097                                &namespaces,
9098                                &mut grouping,
9099                                &mut marker,
9100                                &mut smooth,
9101                                &mut series,
9102                                &mut data_labels,
9103                                &mut axis_ids,
9104                                &mut raw_children,
9105                                &mut boundary,
9106                            )?;
9107                        }
9108                        event @ (Event::Text(_)
9109                        | Event::CData(_)
9110                        | Event::Comment(_)
9111                        | Event::PI(_)
9112                        | Event::GeneralRef(_)) => {
9113                            raw_children.push(boundary, capture_event(event)?);
9114                        }
9115                        Event::End(end)
9116                            if matches_local_name(end.name().as_ref(), b"lineChart") =>
9117                        {
9118                            let (grouping, grouping_markup) = grouping.ok_or_else(|| {
9119                                ChartError::MissingElement("c:grouping".to_owned())
9120                            })?;
9121                            if axis_ids.len() != 2 {
9122                                return Err(ChartError::InvalidValue {
9123                                    element: "c:lineChart/c:axId".to_owned(),
9124                                    value: format!("expected 2, found {}", axis_ids.len()),
9125                                });
9126                            }
9127                            let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
9128                            let original_series_keys: Vec<(u32, u32)> =
9129                                series.iter().map(|item| (item.index, item.order)).collect();
9130                            let plot = Plot::Line {
9131                                grouping,
9132                                marker: marker.as_ref().is_some_and(|item| item.0),
9133                                smooth: smooth.as_ref().is_some_and(|item| item.0),
9134                                series,
9135                                data_labels,
9136                                axis_ids: parsed_ids,
9137                            };
9138                            plot.validate()?;
9139                            return Ok((
9140                                plot,
9141                                PlotMarkup {
9142                                    raw_attributes,
9143                                    raw_children: raw_children_in_schema_order(
9144                                        &raw_children,
9145                                        original_series_keys.len() + 6,
9146                                    ),
9147                                    direction: None,
9148                                    grouping: Some(grouping_markup),
9149                                    gap_width: None,
9150                                    overlap: None,
9151                                    marker: marker.map(|item| item.1),
9152                                    smooth: smooth.map(|item| item.1),
9153                                    first_slice_angle: None,
9154                                    hole_size: None,
9155                                    style: None,
9156                                    axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
9157                                    original_series_keys,
9158                                    original_axis_ids: parsed_ids.to_vec(),
9159                                    parsed_bar: Some(false),
9160                                    parsed_remaining: None,
9161                                },
9162                            ));
9163                        }
9164                        Event::Eof => return Err(missing_end("c:lineChart")),
9165                        _ => {}
9166                    }
9167                    inner.clear();
9168                }
9169            }
9170            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
9171                return Err(ChartError::MissingElement("c:grouping".to_owned()));
9172            }
9173            Event::Start(element) | Event::Empty(element) => {
9174                return Err(ChartError::UnexpectedElement(element_name(&element)));
9175            }
9176            Event::Eof => return Err(ChartError::MissingElement("c:lineChart".to_owned())),
9177            _ => {}
9178        }
9179        buffer.clear();
9180    }
9181}
9182
9183#[allow(clippy::too_many_arguments)]
9184fn parse_line_child(
9185    name: &[u8],
9186    raw: Vec<u8>,
9187    namespaces: &NamespaceBindings,
9188    grouping: &mut Option<(Grouping, ScalarMarkup)>,
9189    marker: &mut Option<(bool, ScalarMarkup)>,
9190    smooth: &mut Option<(bool, ScalarMarkup)>,
9191    series: &mut Vec<Series>,
9192    data_labels: &mut Option<CT_DLbls>,
9193    axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
9194    raw_children: &mut OrderedRawChildren,
9195    boundary: &mut usize,
9196) -> Result<()> {
9197    match name {
9198        b"grouping" => {
9199            let (value, markup) = required_scalar(&raw, "grouping")?;
9200            let value = Grouping::parse(&value)
9201                .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
9202            set_once(grouping, (value, markup), "c:grouping")?;
9203            *boundary = (*boundary).max(1);
9204        }
9205        b"ser" => {
9206            series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
9207            *boundary = (*boundary).max(series.len() + 1);
9208        }
9209        b"dLbls" => {
9210            set_once(
9211                data_labels,
9212                CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
9213                "c:dLbls",
9214            )?;
9215            *boundary = (*boundary).max(series.len() + 2);
9216        }
9217        b"marker" => {
9218            set_once(marker, parse_bool_value(&raw, "marker")?, "c:marker")?;
9219            *boundary = (*boundary).max(series.len() + 3);
9220        }
9221        b"smooth" => {
9222            set_once(smooth, parse_bool_value(&raw, "smooth")?, "c:smooth")?;
9223            *boundary = (*boundary).max(series.len() + 4);
9224        }
9225        b"axId" => {
9226            if axis_ids.len() == 2 {
9227                return Err(ChartError::DuplicateElement("c:axId".to_owned()));
9228            }
9229            axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
9230            *boundary = (*boundary).max(series.len() + 4 + axis_ids.len());
9231        }
9232        _ => raw_children.push(*boundary, raw),
9233    }
9234    Ok(())
9235}
9236
9237fn required_scalar(xml: &[u8], local: &str) -> Result<(String, ScalarMarkup)> {
9238    let (value, markup) = scalar_value(xml, local)?;
9239    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
9240    Ok((value, markup))
9241}
9242
9243fn write_plot<W: Write>(writer: &mut Writer<W>, plot: &Plot, markup: &PlotMarkup) -> Result<()> {
9244    plot.validate()?;
9245    if let Some(parsed_bar) = markup.parsed_bar
9246        && parsed_bar != matches!(plot, Plot::Bar { .. })
9247    {
9248        return Err(ChartError::InvalidValue {
9249            element: "c:plotArea".to_owned(),
9250            value: "a parsed plot family cannot be replaced while preserved payload remains"
9251                .to_owned(),
9252        });
9253    }
9254    let current_remaining = match plot {
9255        Plot::Pie { .. } => Some("pieChart"),
9256        Plot::Doughnut { .. } => Some("doughnutChart"),
9257        Plot::Area { .. } => Some("areaChart"),
9258        Plot::Scatter { .. } => Some("scatterChart"),
9259        Plot::Radar { .. } => Some("radarChart"),
9260        Plot::Bar { .. } | Plot::Line { .. } => None,
9261    };
9262    if let Some(parsed) = markup.parsed_remaining
9263        && current_remaining != Some(parsed)
9264    {
9265        return Err(ChartError::InvalidValue {
9266            element: "c:plotArea".to_owned(),
9267            value: "a parsed plot family cannot be replaced while preserved payload remains"
9268                .to_owned(),
9269        });
9270    }
9271    match plot {
9272        Plot::Bar {
9273            direction,
9274            grouping,
9275            gap_width,
9276            overlap,
9277            series,
9278            data_labels,
9279            axis_ids,
9280        } => {
9281            let mut start = BytesStart::new("c:barChart");
9282            push_attributes(&mut start, &markup.raw_attributes);
9283            writer
9284                .write_event(Event::Start(start))
9285                .map_err(OxmlError::from)?;
9286            emit_raw(writer, markup.raw_children.at(0))?;
9287            write_scalar(
9288                writer,
9289                "c:barDir",
9290                direction.as_str(),
9291                markup.direction.as_ref(),
9292            )?;
9293            emit_raw(writer, markup.raw_children.at(1))?;
9294            write_scalar(
9295                writer,
9296                "c:grouping",
9297                grouping.as_str(),
9298                markup.grouping.as_ref(),
9299            )?;
9300            let original_to_current =
9301                series_original_to_current(&markup.original_series_keys, series);
9302            emit_repeated_raw(
9303                writer,
9304                &markup.raw_children,
9305                2,
9306                &original_to_current,
9307                series.len(),
9308                0,
9309            )?;
9310            for (index, item) in series.iter().enumerate() {
9311                writer
9312                    .get_mut()
9313                    .write_all(&item.to_xml()?)
9314                    .map_err(OxmlError::from)?;
9315                emit_repeated_raw(
9316                    writer,
9317                    &markup.raw_children,
9318                    2,
9319                    &original_to_current,
9320                    series.len(),
9321                    index + 1,
9322                )?;
9323            }
9324            if let Some(labels) = data_labels {
9325                writer
9326                    .get_mut()
9327                    .write_all(&labels.to_xml()?)
9328                    .map_err(OxmlError::from)?;
9329            }
9330            let trailing = markup.original_series_keys.len();
9331            emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9332            if *gap_width != 150 || markup.gap_width.is_some() {
9333                write_scalar(
9334                    writer,
9335                    "c:gapWidth",
9336                    &gap_width.to_string(),
9337                    markup.gap_width.as_ref(),
9338                )?;
9339            }
9340            emit_raw(writer, markup.raw_children.at(trailing + 4))?;
9341            if *overlap != 0 || markup.overlap.is_some() {
9342                write_scalar(
9343                    writer,
9344                    "c:overlap",
9345                    &overlap.to_string(),
9346                    markup.overlap.as_ref(),
9347                )?;
9348            }
9349            let axis_original_to_current =
9350                axis_original_to_current(&markup.original_axis_ids, axis_ids);
9351            let axis_current_to_original =
9352                invert_original_to_current(&axis_original_to_current, axis_ids.len());
9353            emit_repeated_raw(
9354                writer,
9355                &markup.raw_children,
9356                trailing + 5,
9357                &axis_original_to_current,
9358                axis_ids.len(),
9359                0,
9360            )?;
9361            for (index, id) in axis_ids.iter().enumerate() {
9362                let default_markup = default_axis_id_markup(*id);
9363                let original_markup = axis_current_to_original[index]
9364                    .and_then(|original| markup.axis_ids.get(original));
9365                write_axis_id(
9366                    writer,
9367                    "c:axId",
9368                    *id,
9369                    original_markup.unwrap_or(&default_markup),
9370                )?;
9371                emit_repeated_raw(
9372                    writer,
9373                    &markup.raw_children,
9374                    trailing + 5,
9375                    &axis_original_to_current,
9376                    axis_ids.len(),
9377                    index + 1,
9378                )?;
9379            }
9380            writer
9381                .write_event(Event::End(BytesEnd::new("c:barChart")))
9382                .map_err(OxmlError::from)?;
9383        }
9384        Plot::Line {
9385            grouping,
9386            marker,
9387            smooth,
9388            series,
9389            data_labels,
9390            axis_ids,
9391        } => {
9392            let mut start = BytesStart::new("c:lineChart");
9393            push_attributes(&mut start, &markup.raw_attributes);
9394            writer
9395                .write_event(Event::Start(start))
9396                .map_err(OxmlError::from)?;
9397            emit_raw(writer, markup.raw_children.at(0))?;
9398            write_scalar(
9399                writer,
9400                "c:grouping",
9401                grouping.as_str(),
9402                markup.grouping.as_ref(),
9403            )?;
9404            let original_to_current =
9405                series_original_to_current(&markup.original_series_keys, series);
9406            emit_repeated_raw(
9407                writer,
9408                &markup.raw_children,
9409                1,
9410                &original_to_current,
9411                series.len(),
9412                0,
9413            )?;
9414            for (index, item) in series.iter().enumerate() {
9415                writer
9416                    .get_mut()
9417                    .write_all(&item.to_xml()?)
9418                    .map_err(OxmlError::from)?;
9419                emit_repeated_raw(
9420                    writer,
9421                    &markup.raw_children,
9422                    1,
9423                    &original_to_current,
9424                    series.len(),
9425                    index + 1,
9426                )?;
9427            }
9428            if let Some(labels) = data_labels {
9429                writer
9430                    .get_mut()
9431                    .write_all(&labels.to_xml()?)
9432                    .map_err(OxmlError::from)?;
9433            }
9434            let trailing = markup.original_series_keys.len();
9435            emit_raw(writer, markup.raw_children.at(trailing + 2))?;
9436            if *marker || markup.marker.is_some() {
9437                write_scalar(
9438                    writer,
9439                    "c:marker",
9440                    bool_lexical(*marker),
9441                    markup.marker.as_ref(),
9442                )?;
9443            }
9444            emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9445            if *smooth || markup.smooth.is_some() {
9446                write_scalar(
9447                    writer,
9448                    "c:smooth",
9449                    bool_lexical(*smooth),
9450                    markup.smooth.as_ref(),
9451                )?;
9452            }
9453            let axis_original_to_current =
9454                axis_original_to_current(&markup.original_axis_ids, axis_ids);
9455            let axis_current_to_original =
9456                invert_original_to_current(&axis_original_to_current, axis_ids.len());
9457            emit_repeated_raw(
9458                writer,
9459                &markup.raw_children,
9460                trailing + 4,
9461                &axis_original_to_current,
9462                axis_ids.len(),
9463                0,
9464            )?;
9465            for (index, id) in axis_ids.iter().enumerate() {
9466                let default_markup = default_axis_id_markup(*id);
9467                let original_markup = axis_current_to_original[index]
9468                    .and_then(|original| markup.axis_ids.get(original));
9469                write_axis_id(
9470                    writer,
9471                    "c:axId",
9472                    *id,
9473                    original_markup.unwrap_or(&default_markup),
9474                )?;
9475                emit_repeated_raw(
9476                    writer,
9477                    &markup.raw_children,
9478                    trailing + 4,
9479                    &axis_original_to_current,
9480                    axis_ids.len(),
9481                    index + 1,
9482                )?;
9483            }
9484            writer
9485                .write_event(Event::End(BytesEnd::new("c:lineChart")))
9486                .map_err(OxmlError::from)?;
9487        }
9488        remaining => {
9489            let (
9490                root,
9491                property,
9492                series,
9493                data_labels,
9494                axis_ids,
9495                first_slice_angle,
9496                hole_size,
9497                scatter,
9498            ) = match remaining {
9499                Plot::Pie {
9500                    first_slice_angle,
9501                    series,
9502                    data_labels,
9503                } => (
9504                    "pieChart",
9505                    None,
9506                    series,
9507                    data_labels,
9508                    None,
9509                    Some(*first_slice_angle),
9510                    None,
9511                    false,
9512                ),
9513                Plot::Doughnut {
9514                    first_slice_angle,
9515                    hole_size,
9516                    series,
9517                    data_labels,
9518                } => (
9519                    "doughnutChart",
9520                    None,
9521                    series,
9522                    data_labels,
9523                    None,
9524                    Some(*first_slice_angle),
9525                    Some(*hole_size),
9526                    false,
9527                ),
9528                Plot::Area {
9529                    grouping,
9530                    series,
9531                    data_labels,
9532                    axis_ids,
9533                } => (
9534                    "areaChart",
9535                    Some(("c:grouping", grouping.as_str(), markup.grouping.as_ref())),
9536                    series,
9537                    data_labels,
9538                    Some(axis_ids),
9539                    None,
9540                    None,
9541                    false,
9542                ),
9543                Plot::Scatter {
9544                    style,
9545                    series,
9546                    data_labels,
9547                    axis_ids,
9548                } => (
9549                    "scatterChart",
9550                    Some(("c:scatterStyle", style.as_str(), markup.style.as_ref())),
9551                    series,
9552                    data_labels,
9553                    Some(axis_ids),
9554                    None,
9555                    None,
9556                    true,
9557                ),
9558                Plot::Radar {
9559                    style,
9560                    series,
9561                    data_labels,
9562                    axis_ids,
9563                } => (
9564                    "radarChart",
9565                    Some(("c:radarStyle", style.as_str(), markup.style.as_ref())),
9566                    series,
9567                    data_labels,
9568                    Some(axis_ids),
9569                    None,
9570                    None,
9571                    false,
9572                ),
9573                Plot::Bar { .. } | Plot::Line { .. } => unreachable!("handled above"),
9574            };
9575            let tag = format!("c:{root}");
9576            let mut start = BytesStart::new(&tag);
9577            push_attributes(&mut start, &markup.raw_attributes);
9578            writer
9579                .write_event(Event::Start(start))
9580                .map_err(OxmlError::from)?;
9581            let base = usize::from(property.is_some());
9582            if let Some((name, value, scalar_markup)) = property {
9583                emit_raw(writer, markup.raw_children.at(0))?;
9584                write_scalar(writer, name, value, scalar_markup)?;
9585            }
9586            let original_to_current =
9587                series_original_to_current(&markup.original_series_keys, series);
9588            emit_repeated_raw(
9589                writer,
9590                &markup.raw_children,
9591                base,
9592                &original_to_current,
9593                series.len(),
9594                0,
9595            )?;
9596            for (index, item) in series.iter().enumerate() {
9597                writer
9598                    .get_mut()
9599                    .write_all(&item.to_xml_for_plot(scatter)?)
9600                    .map_err(OxmlError::from)?;
9601                emit_repeated_raw(
9602                    writer,
9603                    &markup.raw_children,
9604                    base,
9605                    &original_to_current,
9606                    series.len(),
9607                    index + 1,
9608                )?;
9609            }
9610            if let Some(labels) = data_labels {
9611                writer
9612                    .get_mut()
9613                    .write_all(&labels.to_xml()?)
9614                    .map_err(OxmlError::from)?;
9615            }
9616            let trailing = base + markup.original_series_keys.len();
9617            if let Some(axis_ids) = axis_ids {
9618                let axis_original_to_current =
9619                    axis_original_to_current(&markup.original_axis_ids, axis_ids);
9620                let axis_current_to_original =
9621                    invert_original_to_current(&axis_original_to_current, axis_ids.len());
9622                emit_repeated_raw(
9623                    writer,
9624                    &markup.raw_children,
9625                    trailing + 1,
9626                    &axis_original_to_current,
9627                    axis_ids.len(),
9628                    0,
9629                )?;
9630                for (index, id) in axis_ids.iter().enumerate() {
9631                    let default_markup = default_axis_id_markup(*id);
9632                    let original_markup = axis_current_to_original[index]
9633                        .and_then(|original| markup.axis_ids.get(original));
9634                    write_axis_id(
9635                        writer,
9636                        "c:axId",
9637                        *id,
9638                        original_markup.unwrap_or(&default_markup),
9639                    )?;
9640                    emit_repeated_raw(
9641                        writer,
9642                        &markup.raw_children,
9643                        trailing + 1,
9644                        &axis_original_to_current,
9645                        axis_ids.len(),
9646                        index + 1,
9647                    )?;
9648                }
9649            } else {
9650                emit_raw(writer, markup.raw_children.at(trailing + 1))?;
9651                if let Some(angle) = first_slice_angle {
9652                    if angle != 0 || markup.first_slice_angle.is_some() {
9653                        write_scalar(
9654                            writer,
9655                            "c:firstSliceAng",
9656                            &angle.to_string(),
9657                            markup.first_slice_angle.as_ref(),
9658                        )?;
9659                    }
9660                    emit_raw(writer, markup.raw_children.at(trailing + 2))?;
9661                }
9662                if let Some(size) = hole_size {
9663                    if size != 50 || markup.hole_size.is_some() {
9664                        write_scalar(
9665                            writer,
9666                            "c:holeSize",
9667                            &size.to_string(),
9668                            markup.hole_size.as_ref(),
9669                        )?;
9670                    }
9671                    emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9672                }
9673            }
9674            writer
9675                .write_event(Event::End(BytesEnd::new(&tag)))
9676                .map_err(OxmlError::from)?;
9677        }
9678    }
9679    Ok(())
9680}
9681
9682fn series_original_to_current(original: &[(u32, u32)], current: &[Series]) -> Vec<Option<usize>> {
9683    let mut used = vec![false; current.len()];
9684    let mut matches = vec![None; original.len()];
9685    for original_index in 0..original.len().min(current.len()) {
9686        if original[original_index]
9687            == (current[original_index].index, current[original_index].order)
9688        {
9689            matches[original_index] = Some(original_index);
9690            used[original_index] = true;
9691        }
9692    }
9693    for (original_index, key) in original.iter().enumerate() {
9694        if matches[original_index].is_some() {
9695            continue;
9696        }
9697        if let Some(current_index) = current
9698            .iter()
9699            .enumerate()
9700            .position(|(index, item)| !used[index] && (item.index, item.order) == *key)
9701        {
9702            matches[original_index] = Some(current_index);
9703            used[current_index] = true;
9704        }
9705    }
9706    match_unidentified_by_position(&mut matches, &mut used);
9707    matches
9708}
9709
9710fn axis_original_to_current(original: &[AxisId], current: &[AxisId]) -> Vec<Option<usize>> {
9711    let mut used = vec![false; current.len()];
9712    let mut matches = vec![None; original.len()];
9713    for (original_index, id) in original.iter().enumerate() {
9714        if let Some(current_index) = current
9715            .iter()
9716            .enumerate()
9717            .position(|(index, current_id)| !used[index] && current_id == id)
9718        {
9719            matches[original_index] = Some(current_index);
9720            used[current_index] = true;
9721        }
9722    }
9723    match_unidentified_by_position(&mut matches, &mut used);
9724    matches
9725}
9726
9727fn match_unidentified_by_position(matches: &mut [Option<usize>], used: &mut [bool]) {
9728    for original_index in 0..matches.len() {
9729        if matches[original_index].is_none() && original_index < used.len() && !used[original_index]
9730        {
9731            matches[original_index] = Some(original_index);
9732            used[original_index] = true;
9733        }
9734    }
9735    for matched in matches.iter_mut().filter(|matched| matched.is_none()) {
9736        if let Some(current_index) = used.iter().position(|used| !used) {
9737            *matched = Some(current_index);
9738            used[current_index] = true;
9739        }
9740    }
9741}
9742
9743fn invert_original_to_current(
9744    original_to_current: &[Option<usize>],
9745    current_len: usize,
9746) -> Vec<Option<usize>> {
9747    let mut current_to_original = vec![None; current_len];
9748    for (original, current) in original_to_current.iter().enumerate() {
9749        if let Some(current) = current {
9750            current_to_original[*current] = Some(original);
9751        }
9752    }
9753    current_to_original
9754}
9755
9756fn emit_repeated_raw<W: Write>(
9757    writer: &mut Writer<W>,
9758    raw_children: &OrderedRawChildren,
9759    offset: usize,
9760    original_to_current: &[Option<usize>],
9761    current_len: usize,
9762    current_boundary: usize,
9763) -> Result<()> {
9764    if current_boundary == 0 {
9765        emit_raw(writer, raw_children.at(offset))?;
9766    }
9767    for original_boundary in 1..=original_to_current.len() {
9768        let effective = original_to_current
9769            .iter()
9770            .skip(original_boundary)
9771            .flatten()
9772            .copied()
9773            .next()
9774            .unwrap_or(current_len);
9775        if effective == current_boundary {
9776            emit_raw(writer, raw_children.at(offset + original_boundary))?;
9777        }
9778    }
9779    Ok(())
9780}
9781
9782fn parse_plot_axis(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Axis>> {
9783    let mut reader = Reader::from_reader(xml);
9784    let mut buffer = Vec::new();
9785    loop {
9786        match reader
9787            .read_event_into(&mut buffer)
9788            .map_err(OxmlError::from)?
9789        {
9790            Event::Start(element) | Event::Empty(element) => {
9791                if !element_is_in_namespace(&element, C_NS, inherited)? {
9792                    return Ok(None);
9793                }
9794                let name = element.name();
9795                if AxisKind::parse(local_name(name.as_ref())).is_none() {
9796                    return Ok(None);
9797                }
9798                return Axis::from_xml_with_namespaces(xml, inherited).map(Some);
9799            }
9800            Event::Eof => return Ok(None),
9801            _ => {}
9802        }
9803        buffer.clear();
9804    }
9805}
9806
9807fn validate_axis_pairs(axes: &[Axis]) -> Result<()> {
9808    for (index, axis) in axes.iter().enumerate() {
9809        if axes[..index].iter().any(|other| other.id == axis.id) {
9810            return Err(ChartError::DuplicateElement(format!(
9811                "c:axId {}",
9812                axis.id.value()
9813            )));
9814        }
9815    }
9816    for axis in axes {
9817        if axis.id == axis.cross_axis {
9818            return Err(ChartError::InvalidValue {
9819                element: "c:crossAx".to_owned(),
9820                value: format!("axis {} crosses itself", axis.id.value()),
9821            });
9822        }
9823        let crossed = axes
9824            .iter()
9825            .find(|candidate| candidate.id == axis.cross_axis)
9826            .ok_or_else(|| ChartError::InvalidValue {
9827                element: "c:crossAx".to_owned(),
9828                value: format!(
9829                    "axis {} references missing axis {}",
9830                    axis.id.value(),
9831                    axis.cross_axis.value()
9832                ),
9833            })?;
9834        if crossed.cross_axis != axis.id {
9835            return Err(ChartError::InvalidValue {
9836                element: "c:crossAx".to_owned(),
9837                value: format!(
9838                    "axis {} references {}, which references {}",
9839                    axis.id.value(),
9840                    crossed.id.value(),
9841                    crossed.cross_axis.value()
9842                ),
9843            });
9844        }
9845    }
9846    Ok(())
9847}
9848
9849fn parse_plot_series(
9850    xml: &[u8],
9851    inherited: &NamespaceBindings,
9852    series: &mut Vec<Series>,
9853) -> Result<()> {
9854    let mut reader = Reader::from_reader(xml);
9855    let mut buffer = Vec::new();
9856    loop {
9857        match reader
9858            .read_event_into(&mut buffer)
9859            .map_err(OxmlError::from)?
9860        {
9861            Event::Start(element) => {
9862                if !element_is_in_namespace(&element, C_NS, inherited)? {
9863                    return Ok(());
9864                }
9865                let local = local_name(element.name().as_ref()).to_vec();
9866                if !is_supported_series_plot(&local) {
9867                    return Ok(());
9868                }
9869                let namespaces = chart_bindings(inherited, &element)?;
9870                let mut inner = Vec::new();
9871                loop {
9872                    match reader
9873                        .read_event_into(&mut inner)
9874                        .map_err(OxmlError::from)?
9875                    {
9876                        Event::Start(child) => {
9877                            let name = chart_child_local(&child, &namespaces)?;
9878                            let raw = capture_element(&mut reader, &child)?;
9879                            if name.as_deref() == Some(b"ser") {
9880                                series.push(Series::from_xml_with_namespaces(&raw, &namespaces)?);
9881                            }
9882                        }
9883                        Event::Empty(child) => {
9884                            let name = chart_child_local(&child, &namespaces)?;
9885                            if name.as_deref() == Some(b"ser") {
9886                                return Err(ChartError::MissingElement("c:ser/c:idx".to_owned()));
9887                            }
9888                        }
9889                        Event::End(end) if matches_local_name(end.name().as_ref(), &local) => {
9890                            return Ok(());
9891                        }
9892                        Event::Eof => {
9893                            return Err(missing_end(&format!(
9894                                "c:{}",
9895                                String::from_utf8_lossy(&local)
9896                            )));
9897                        }
9898                        _ => {}
9899                    }
9900                    inner.clear();
9901                }
9902            }
9903            Event::Empty(_) | Event::Eof => return Ok(()),
9904            _ => {}
9905        }
9906        buffer.clear();
9907    }
9908}
9909
9910fn is_supported_series_plot(local: &[u8]) -> bool {
9911    matches!(
9912        local,
9913        b"areaChart"
9914            | b"area3DChart"
9915            | b"barChart"
9916            | b"bar3DChart"
9917            | b"doughnutChart"
9918            | b"lineChart"
9919            | b"line3DChart"
9920            | b"ofPieChart"
9921            | b"pieChart"
9922            | b"pie3DChart"
9923            | b"radarChart"
9924            | b"stockChart"
9925            | b"surfaceChart"
9926            | b"surface3DChart"
9927    )
9928}
9929
9930impl CT_Legend {
9931    fn from_xml(xml: &[u8]) -> Result<Self> {
9932        let (raw_attributes, raw_children) = parse_raw_shell(xml, b"legend", "c:legend")?;
9933        Ok(Self {
9934            raw_attributes,
9935            raw_children,
9936        })
9937    }
9938
9939    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
9940        write_raw_shell(writer, "c:legend", &self.raw_attributes, &self.raw_children)
9941    }
9942
9943    pub fn raw_children(&self) -> &OrderedRawChildren {
9944        &self.raw_children
9945    }
9946}
9947
9948fn parse_raw_shell(
9949    xml: &[u8],
9950    local: &[u8],
9951    tag: &str,
9952) -> Result<(XmlAttributes, OrderedRawChildren)> {
9953    let mut reader = Reader::from_reader(xml);
9954    let mut buffer = Vec::new();
9955    loop {
9956        match reader
9957            .read_event_into(&mut buffer)
9958            .map_err(OxmlError::from)?
9959        {
9960            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
9961                reject_conflicting_prefix(&element, b"c", C_NS)?;
9962                let raw_attributes = capture_attributes(&element)?;
9963                let mut raw_children = OrderedRawChildren::default();
9964                let mut child_buffer = Vec::new();
9965                loop {
9966                    match reader
9967                        .read_event_into(&mut child_buffer)
9968                        .map_err(OxmlError::from)?
9969                    {
9970                        Event::Start(child) => {
9971                            raw_children.push(0, capture_element(&mut reader, &child)?)
9972                        }
9973                        Event::Empty(child) => raw_children.push(0, capture_empty_element(&child)?),
9974                        event @ (Event::Text(_)
9975                        | Event::CData(_)
9976                        | Event::Comment(_)
9977                        | Event::PI(_)
9978                        | Event::GeneralRef(_)) => {
9979                            raw_children.push(0, capture_event(event)?);
9980                        }
9981                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => break,
9982                        Event::Eof => return Err(missing_end(tag)),
9983                        _ => {}
9984                    }
9985                    child_buffer.clear();
9986                }
9987                return Ok((raw_attributes, raw_children));
9988            }
9989            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
9990                reject_conflicting_prefix(&element, b"c", C_NS)?;
9991                return Ok((capture_attributes(&element)?, OrderedRawChildren::default()));
9992            }
9993            Event::Start(element) | Event::Empty(element) => {
9994                return Err(ChartError::UnexpectedElement(element_name(&element)));
9995            }
9996            Event::Eof => return Err(ChartError::MissingElement(tag.to_owned())),
9997            _ => {}
9998        }
9999        buffer.clear();
10000    }
10001}
10002
10003fn write_raw_shell<W: Write>(
10004    writer: &mut Writer<W>,
10005    tag: &str,
10006    raw_attributes: &XmlAttributes,
10007    raw_children: &OrderedRawChildren,
10008) -> Result<()> {
10009    let mut start = BytesStart::new(tag);
10010    push_attributes(&mut start, raw_attributes);
10011    if raw_children.is_empty() {
10012        writer
10013            .write_event(Event::Empty(start))
10014            .map_err(OxmlError::from)?;
10015        return Ok(());
10016    }
10017    writer
10018        .write_event(Event::Start(start))
10019        .map_err(OxmlError::from)?;
10020    emit_raw(writer, raw_children.at(0))?;
10021    writer
10022        .write_event(Event::End(BytesEnd::new(tag)))
10023        .map_err(OxmlError::from)?;
10024    Ok(())
10025}
10026
10027/// The implemented behavior-bearing core of one `c:chart`.
10028#[derive(Clone, Debug, PartialEq)]
10029pub struct CT_Chart {
10030    pub title: Option<CT_Title>,
10031    pub auto_title_deleted: bool,
10032    pub plot_area: CT_PlotArea,
10033    pub legend: Option<CT_Legend>,
10034    pub plot_vis_only: bool,
10035    pub disp_blanks_as: DispBlanksAs,
10036    auto_title_deleted_markup: Option<ScalarMarkup>,
10037    plot_vis_only_markup: Option<ScalarMarkup>,
10038    disp_blanks_as_markup: Option<ScalarMarkup>,
10039    raw_attributes: Vec<(String, String)>,
10040    raw_children: OrderedRawChildren,
10041}
10042
10043impl CT_Chart {
10044    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
10045        let mut reader = Reader::from_reader(xml);
10046        let mut buffer = Vec::new();
10047        loop {
10048            match reader
10049                .read_event_into(&mut buffer)
10050                .map_err(OxmlError::from)?
10051            {
10052                Event::Start(element) if matches_local_name(element.name().as_ref(), b"chart") => {
10053                    reject_conflicting_prefix(&element, b"c", C_NS)?;
10054                    return Self::from_element(&mut reader, &element, inherited);
10055                }
10056                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"chart") => {
10057                    return Err(ChartError::MissingElement("c:plotArea".to_owned()));
10058                }
10059                Event::Start(element) | Event::Empty(element) => {
10060                    return Err(ChartError::UnexpectedElement(element_name(&element)));
10061                }
10062                Event::Eof => return Err(ChartError::MissingElement("c:chart".to_owned())),
10063                _ => {}
10064            }
10065            buffer.clear();
10066        }
10067    }
10068
10069    fn from_element(
10070        reader: &mut Reader<&[u8]>,
10071        start: &BytesStart<'_>,
10072        inherited: &NamespaceBindings,
10073    ) -> Result<Self> {
10074        let namespaces = bindings_with_local(inherited, start)?;
10075        let raw_attributes = capture_attributes(start)?;
10076        let mut state = ChartParseState::default();
10077        let mut buffer = Vec::new();
10078
10079        loop {
10080            match reader
10081                .read_event_into(&mut buffer)
10082                .map_err(OxmlError::from)?
10083            {
10084                Event::Start(element) => {
10085                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10086                        local_name(element.name().as_ref()).to_vec()
10087                    } else {
10088                        Vec::new()
10089                    };
10090                    let raw = capture_element(reader, &element)?;
10091                    state.parse_child(&name, raw, &namespaces)?;
10092                }
10093                Event::Empty(element) => {
10094                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10095                        local_name(element.name().as_ref()).to_vec()
10096                    } else {
10097                        Vec::new()
10098                    };
10099                    let raw = capture_empty_element(&element)?;
10100                    state.parse_child(&name, raw, &namespaces)?;
10101                }
10102                event @ (Event::Text(_)
10103                | Event::CData(_)
10104                | Event::Comment(_)
10105                | Event::PI(_)
10106                | Event::GeneralRef(_)) => {
10107                    state.capture_event(capture_event(event)?);
10108                }
10109                Event::End(element) if matches_local_name(element.name().as_ref(), b"chart") => {
10110                    break;
10111                }
10112                Event::Eof => return Err(missing_end("c:chart")),
10113                _ => {}
10114            }
10115            buffer.clear();
10116        }
10117
10118        state.finish(raw_attributes)
10119    }
10120
10121    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
10122        let mut start = BytesStart::new("c:chart");
10123        push_attributes(&mut start, &self.raw_attributes);
10124        writer
10125            .write_event(Event::Start(start))
10126            .map_err(OxmlError::from)?;
10127        emit_raw(writer, self.raw_children.at(0))?;
10128        if let Some(title) = &self.title {
10129            title.write_xml(writer)?;
10130        }
10131        emit_raw(writer, self.raw_children.at(1))?;
10132        if self.auto_title_deleted_markup.is_some() || self.auto_title_deleted {
10133            write_scalar(
10134                writer,
10135                "c:autoTitleDeleted",
10136                if self.auto_title_deleted { "1" } else { "0" },
10137                self.auto_title_deleted_markup.as_ref(),
10138            )?;
10139        }
10140        for raw_boundary in 2..8 {
10141            emit_raw(writer, self.raw_children.at(raw_boundary))?;
10142        }
10143        self.plot_area.write_xml(writer)?;
10144        emit_raw(writer, self.raw_children.at(8))?;
10145        if let Some(legend) = &self.legend {
10146            legend.write_xml(writer)?;
10147        }
10148        emit_raw(writer, self.raw_children.at(9))?;
10149        if self.plot_vis_only_markup.is_some() || !self.plot_vis_only {
10150            write_scalar(
10151                writer,
10152                "c:plotVisOnly",
10153                if self.plot_vis_only { "1" } else { "0" },
10154                self.plot_vis_only_markup.as_ref(),
10155            )?;
10156        }
10157        emit_raw(writer, self.raw_children.at(10))?;
10158        if self.disp_blanks_as_markup.is_some() || self.disp_blanks_as != DispBlanksAs::Gap {
10159            write_scalar(
10160                writer,
10161                "c:dispBlanksAs",
10162                self.disp_blanks_as.as_str(),
10163                self.disp_blanks_as_markup.as_ref(),
10164            )?;
10165        }
10166        for raw_boundary in 11..=13 {
10167            emit_raw(writer, self.raw_children.at(raw_boundary))?;
10168        }
10169        writer
10170            .write_event(Event::End(BytesEnd::new("c:chart")))
10171            .map_err(OxmlError::from)?;
10172        Ok(())
10173    }
10174
10175    pub fn raw_children(&self) -> &OrderedRawChildren {
10176        &self.raw_children
10177    }
10178}
10179
10180#[derive(Default)]
10181struct ChartParseState {
10182    title: Option<CT_Title>,
10183    auto_title_deleted: Option<(bool, ScalarMarkup)>,
10184    plot_area: Option<CT_PlotArea>,
10185    legend: Option<CT_Legend>,
10186    plot_vis_only: Option<(bool, ScalarMarkup)>,
10187    disp_blanks_as: Option<(DispBlanksAs, ScalarMarkup)>,
10188    raw_children: OrderedRawChildren,
10189    boundary: usize,
10190}
10191
10192impl ChartParseState {
10193    fn capture_event(&mut self, raw: Vec<u8>) {
10194        self.raw_children.push(self.boundary, raw);
10195    }
10196
10197    fn parse_child(
10198        &mut self,
10199        name: &[u8],
10200        raw: Vec<u8>,
10201        namespaces: &NamespaceBindings,
10202    ) -> Result<()> {
10203        match name {
10204            b"title" => {
10205                set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
10206                self.boundary = self.boundary.max(1);
10207            }
10208            b"autoTitleDeleted" => {
10209                set_once(
10210                    &mut self.auto_title_deleted,
10211                    parse_bool_value(&raw, "autoTitleDeleted")?,
10212                    "c:autoTitleDeleted",
10213                )?;
10214                self.boundary = self.boundary.max(2);
10215            }
10216            b"plotArea" => {
10217                set_once(
10218                    &mut self.plot_area,
10219                    CT_PlotArea::from_xml_with_namespaces(&raw, namespaces)?,
10220                    "c:plotArea",
10221                )?;
10222                self.boundary = self.boundary.max(8);
10223            }
10224            b"legend" => {
10225                set_once(&mut self.legend, CT_Legend::from_xml(&raw)?, "c:legend")?;
10226                self.boundary = self.boundary.max(9);
10227            }
10228            b"plotVisOnly" => {
10229                set_once(
10230                    &mut self.plot_vis_only,
10231                    parse_bool_value(&raw, "plotVisOnly")?,
10232                    "c:plotVisOnly",
10233                )?;
10234                self.boundary = self.boundary.max(10);
10235            }
10236            b"dispBlanksAs" => {
10237                set_once(
10238                    &mut self.disp_blanks_as,
10239                    parse_enum_value(&raw, "dispBlanksAs")?,
10240                    "c:dispBlanksAs",
10241                )?;
10242                self.boundary = self.boundary.max(11);
10243            }
10244            _ => {
10245                self.raw_children
10246                    .push(chart_raw_slot(name).unwrap_or(self.boundary), raw);
10247                self.boundary = self.boundary.max(chart_raw_boundary_after(name));
10248            }
10249        }
10250        Ok(())
10251    }
10252
10253    fn finish(self, raw_attributes: XmlAttributes) -> Result<CT_Chart> {
10254        let (auto_title_deleted, auto_title_deleted_markup) = self
10255            .auto_title_deleted
10256            .map(|(value, markup)| (value, Some(markup)))
10257            .unwrap_or((false, None));
10258        let (plot_vis_only, plot_vis_only_markup) = self
10259            .plot_vis_only
10260            .map(|(value, markup)| (value, Some(markup)))
10261            .unwrap_or((true, None));
10262        let (disp_blanks_as, disp_blanks_as_markup) = self
10263            .disp_blanks_as
10264            .map(|(value, markup)| (value, Some(markup)))
10265            .unwrap_or((DispBlanksAs::Gap, None));
10266        Ok(CT_Chart {
10267            title: self.title,
10268            auto_title_deleted,
10269            plot_area: self
10270                .plot_area
10271                .ok_or_else(|| ChartError::MissingElement("c:plotArea".to_owned()))?,
10272            legend: self.legend,
10273            plot_vis_only,
10274            disp_blanks_as,
10275            auto_title_deleted_markup,
10276            plot_vis_only_markup,
10277            disp_blanks_as_markup,
10278            raw_attributes,
10279            raw_children: raw_children_in_schema_order(&self.raw_children, 13),
10280        })
10281    }
10282}
10283
10284fn chart_raw_slot(name: &[u8]) -> Option<usize> {
10285    match name {
10286        b"pivotFmts" => Some(2),
10287        b"view3D" => Some(3),
10288        b"floor" => Some(4),
10289        b"sideWall" => Some(5),
10290        b"backWall" => Some(6),
10291        // Both unmodelled tail children share a slot so producer sibling order
10292        // remains byte-stable until a later story types either element.
10293        b"showDLblsOverMax" | b"extLst" => Some(11),
10294        _ => None,
10295    }
10296}
10297
10298fn chart_raw_boundary_after(name: &[u8]) -> usize {
10299    match name {
10300        b"title" => 1,
10301        b"autoTitleDeleted" => 2,
10302        b"pivotFmts" => 3,
10303        b"view3D" => 4,
10304        b"floor" => 5,
10305        b"sideWall" => 6,
10306        b"backWall" => 7,
10307        b"plotArea" => 8,
10308        b"legend" => 9,
10309        b"plotVisOnly" => 10,
10310        b"dispBlanksAs" => 11,
10311        b"showDLblsOverMax" | b"extLst" => 11,
10312        _ => 0,
10313    }
10314}
10315
10316/// One complete `c:chartSpace` part with the F-118 core typed.
10317#[derive(Clone, Debug, PartialEq)]
10318pub struct CT_ChartSpace {
10319    pub chart: CT_Chart,
10320    pub sp_pr: Option<CT_ShapeProperties>,
10321    pub tx_pr: Option<CT_TextBody>,
10322    raw_attributes: Vec<(String, String)>,
10323    namespace_declarations: Vec<(String, String)>,
10324    raw_children: OrderedRawChildren,
10325}
10326
10327impl CT_ChartSpace {
10328    /// Parses one complete ChartML chart-space part.
10329    pub fn from_xml(xml: &[u8]) -> Result<Self> {
10330        let mut reader = Reader::from_reader(xml);
10331        let mut buffer = Vec::new();
10332        loop {
10333            match reader
10334                .read_event_into(&mut buffer)
10335                .map_err(OxmlError::from)?
10336            {
10337                Event::Start(element)
10338                    if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10339                {
10340                    validate_chart_space_namespace(&element)?;
10341                    return Self::from_element(&mut reader, &element);
10342                }
10343                Event::Empty(element)
10344                    if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10345                {
10346                    validate_chart_space_namespace(&element)?;
10347                    return Err(ChartError::MissingElement("c:chart".to_owned()));
10348                }
10349                Event::Start(element) | Event::Empty(element) => {
10350                    return Err(ChartError::UnexpectedElement(element_name(&element)));
10351                }
10352                Event::Eof => return Err(ChartError::MissingElement("c:chartSpace".to_owned())),
10353                _ => {}
10354            }
10355            buffer.clear();
10356        }
10357    }
10358
10359    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
10360        let namespaces = namespace_bindings(start)?;
10361        let (raw_attributes, namespace_declarations) = capture_root_attributes(start)?;
10362        let mut state = ChartSpaceParseState::default();
10363        let mut buffer = Vec::new();
10364
10365        loop {
10366            match reader
10367                .read_event_into(&mut buffer)
10368                .map_err(OxmlError::from)?
10369            {
10370                Event::Start(element) => {
10371                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10372                        local_name(element.name().as_ref()).to_vec()
10373                    } else {
10374                        Vec::new()
10375                    };
10376                    let raw = capture_element(reader, &element)?;
10377                    state.parse_child(&name, raw, &namespaces)?;
10378                }
10379                Event::Empty(element) => {
10380                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10381                        local_name(element.name().as_ref()).to_vec()
10382                    } else {
10383                        Vec::new()
10384                    };
10385                    let raw = capture_empty_element(&element)?;
10386                    state.parse_child(&name, raw, &namespaces)?;
10387                }
10388                event @ (Event::Text(_)
10389                | Event::CData(_)
10390                | Event::Comment(_)
10391                | Event::PI(_)
10392                | Event::GeneralRef(_)) => {
10393                    state.capture_event(capture_event(event)?);
10394                }
10395                Event::End(element)
10396                    if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10397                {
10398                    break;
10399                }
10400                Event::Eof => return Err(missing_end("c:chartSpace")),
10401                _ => {}
10402            }
10403            buffer.clear();
10404        }
10405
10406        state.finish(raw_attributes, namespace_declarations)
10407    }
10408
10409    /// Writes one chart-space part using fixed OOXML prefixes and schema order.
10410    pub fn to_xml(&self) -> Result<Vec<u8>> {
10411        let mut writer = Writer::new(Vec::new());
10412        let mut start = BytesStart::new("c:chartSpace");
10413        start.push_attribute(("xmlns:c", C_NS));
10414        start.push_attribute(("xmlns:a", A_NS));
10415        start.push_attribute(("xmlns:r", R_NS));
10416        push_attributes(&mut start, &self.namespace_declarations);
10417        push_attributes(&mut start, &self.raw_attributes);
10418        writer
10419            .write_event(Event::Start(start))
10420            .map_err(OxmlError::from)?;
10421        for raw_boundary in 0..8 {
10422            emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
10423        }
10424        self.chart.write_xml(&mut writer)?;
10425        emit_raw(&mut writer, self.raw_children.at(8))?;
10426        if let Some(properties) = &self.sp_pr {
10427            properties.write_xml_as(&mut writer, "c:spPr")?;
10428        }
10429        emit_raw(&mut writer, self.raw_children.at(9))?;
10430        if let Some(text) = &self.tx_pr {
10431            text.write_xml_as(&mut writer, "c:txPr")?;
10432        }
10433        for raw_boundary in 10..=14 {
10434            emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
10435        }
10436        writer
10437            .write_event(Event::End(BytesEnd::new("c:chartSpace")))
10438            .map_err(OxmlError::from)?;
10439        Ok(writer.into_inner())
10440    }
10441
10442    pub fn raw_children(&self) -> &OrderedRawChildren {
10443        &self.raw_children
10444    }
10445}
10446
10447#[derive(Default)]
10448struct ChartSpaceParseState {
10449    chart: Option<CT_Chart>,
10450    sp_pr: Option<CT_ShapeProperties>,
10451    tx_pr: Option<CT_TextBody>,
10452    raw_children: OrderedRawChildren,
10453    boundary: usize,
10454}
10455
10456impl ChartSpaceParseState {
10457    fn capture_event(&mut self, raw: Vec<u8>) {
10458        self.raw_children.push(self.boundary, raw);
10459    }
10460
10461    fn parse_child(
10462        &mut self,
10463        name: &[u8],
10464        raw: Vec<u8>,
10465        namespaces: &NamespaceBindings,
10466    ) -> Result<()> {
10467        match name {
10468            b"chart" => {
10469                set_once(
10470                    &mut self.chart,
10471                    CT_Chart::from_xml_with_namespaces(&raw, namespaces)?,
10472                    "c:chart",
10473                )?;
10474                self.boundary = self.boundary.max(8);
10475            }
10476            b"spPr" => {
10477                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
10478                let properties = CT_ShapeProperties::from_xml(&raw)?;
10479                let mut writer = Writer::new(Vec::new());
10480                properties.write_xml_as(&mut writer, "c:spPr")?;
10481                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
10482                set_once(&mut self.sp_pr, properties, "c:spPr")?;
10483                self.boundary = self.boundary.max(9);
10484            }
10485            b"txPr" => {
10486                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
10487                let text = CT_TextBody::from_xml_as(&raw, b"txPr")?;
10488                let mut writer = Writer::new(Vec::new());
10489                text.write_xml_as(&mut writer, "c:txPr")?;
10490                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
10491                set_once(&mut self.tx_pr, text, "c:txPr")?;
10492                self.boundary = self.boundary.max(10);
10493            }
10494            _ => {
10495                self.raw_children
10496                    .push(chart_space_raw_slot(name).unwrap_or(self.boundary), raw);
10497                self.boundary = self.boundary.max(chart_space_raw_boundary_after(name));
10498            }
10499        }
10500        Ok(())
10501    }
10502
10503    fn finish(
10504        self,
10505        raw_attributes: XmlAttributes,
10506        namespace_declarations: XmlAttributes,
10507    ) -> Result<CT_ChartSpace> {
10508        Ok(CT_ChartSpace {
10509            chart: self
10510                .chart
10511                .ok_or_else(|| ChartError::MissingElement("c:chart".to_owned()))?,
10512            sp_pr: self.sp_pr,
10513            tx_pr: self.tx_pr,
10514            raw_attributes,
10515            namespace_declarations,
10516            raw_children: raw_children_in_schema_order(&self.raw_children, 14),
10517        })
10518    }
10519}
10520
10521fn raw_children_in_schema_order(
10522    raw_children: &OrderedRawChildren,
10523    final_boundary: usize,
10524) -> OrderedRawChildren {
10525    let mut ordered = OrderedRawChildren::default();
10526    for boundary in 0..=final_boundary {
10527        for raw in raw_children.at(boundary) {
10528            ordered.push(boundary, raw.to_vec());
10529        }
10530    }
10531    ordered
10532}
10533
10534fn chart_space_raw_slot(name: &[u8]) -> Option<usize> {
10535    match name {
10536        b"date1904" => Some(0),
10537        b"lang" => Some(1),
10538        b"roundedCorners" => Some(2),
10539        b"style" => Some(3),
10540        b"clrMapOvr" => Some(4),
10541        b"pivotSource" => Some(5),
10542        b"protection" => Some(6),
10543        b"externalData" => Some(10),
10544        b"printSettings" => Some(11),
10545        b"userShapes" => Some(12),
10546        b"extLst" => Some(13),
10547        _ => None,
10548    }
10549}
10550
10551fn chart_space_raw_boundary_after(name: &[u8]) -> usize {
10552    match name {
10553        b"date1904" => 1,
10554        b"lang" => 2,
10555        b"roundedCorners" => 3,
10556        b"style" => 4,
10557        b"clrMapOvr" => 5,
10558        b"pivotSource" => 6,
10559        b"protection" => 7,
10560        b"chart" => 8,
10561        b"spPr" => 9,
10562        b"txPr" => 10,
10563        b"externalData" => 11,
10564        b"printSettings" => 12,
10565        b"userShapes" => 13,
10566        b"extLst" => 14,
10567        _ => 0,
10568    }
10569}
10570
10571fn parse_bool_value(xml: &[u8], local: &str) -> Result<(bool, ScalarMarkup)> {
10572    let (value, markup) = scalar_value(xml, local)?;
10573    let value = match value.as_deref().unwrap_or("1") {
10574        "1" | "true" => true,
10575        "0" | "false" => false,
10576        invalid => return Err(invalid_attribute(local, "val", invalid.to_owned())),
10577    };
10578    Ok((value, markup))
10579}
10580
10581fn parse_enum_value(xml: &[u8], local: &str) -> Result<(DispBlanksAs, ScalarMarkup)> {
10582    let (value, markup) = scalar_value(xml, local)?;
10583    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
10584    let parsed =
10585        DispBlanksAs::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
10586    Ok((parsed, markup))
10587}
10588
10589fn scalar_value(xml: &[u8], expected: &str) -> Result<(Option<String>, ScalarMarkup)> {
10590    let mut reader = Reader::from_reader(xml);
10591    let mut buffer = Vec::new();
10592    loop {
10593        match reader
10594            .read_event_into(&mut buffer)
10595            .map_err(OxmlError::from)?
10596        {
10597            Event::Empty(element)
10598                if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
10599            {
10600                reject_conflicting_prefix(&element, b"c", C_NS)?;
10601                return Ok((
10602                    attribute_value(&element, b"val")?,
10603                    ScalarMarkup {
10604                        raw_attributes: capture_attributes_except(&element, b"val")?,
10605                        raw_content: Vec::new(),
10606                    },
10607                ));
10608            }
10609            Event::Start(element)
10610                if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
10611            {
10612                reject_conflicting_prefix(&element, b"c", C_NS)?;
10613                let value = attribute_value(&element, b"val")?;
10614                let raw_attributes = capture_attributes_except(&element, b"val")?;
10615                let mut raw_content = Vec::new();
10616                let mut inner = Vec::new();
10617                loop {
10618                    match reader
10619                        .read_event_into(&mut inner)
10620                        .map_err(OxmlError::from)?
10621                    {
10622                        Event::End(end)
10623                            if matches_local_name(end.name().as_ref(), expected.as_bytes()) =>
10624                        {
10625                            return Ok((
10626                                value,
10627                                ScalarMarkup {
10628                                    raw_attributes,
10629                                    raw_content,
10630                                },
10631                            ));
10632                        }
10633                        event @ (Event::Text(_)
10634                        | Event::CData(_)
10635                        | Event::Comment(_)
10636                        | Event::PI(_)
10637                        | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
10638                        Event::Eof => return Err(missing_end(expected)),
10639                        _ => return Err(ChartError::UnexpectedElement(expected.to_owned())),
10640                    }
10641                    inner.clear();
10642                }
10643            }
10644            Event::Start(element) | Event::Empty(element) => {
10645                return Err(ChartError::UnexpectedElement(element_name(&element)));
10646            }
10647            Event::Eof => return Err(ChartError::MissingElement(expected.to_owned())),
10648            _ => {}
10649        }
10650        buffer.clear();
10651    }
10652}
10653
10654fn set_once<T>(slot: &mut Option<T>, value: T, name: &str) -> Result<()> {
10655    if slot.is_some() {
10656        return Err(ChartError::DuplicateElement(name.to_owned()));
10657    }
10658    *slot = Some(value);
10659    Ok(())
10660}
10661
10662fn mark_once(seen: &mut bool, name: &str) -> Result<()> {
10663    if *seen {
10664        return Err(ChartError::DuplicateElement(name.to_owned()));
10665    }
10666    *seen = true;
10667    Ok(())
10668}
10669
10670fn validate_chart_space_namespace(element: &BytesStart<'_>) -> Result<()> {
10671    let name = element.name();
10672    let qualified = name.as_ref();
10673    let declaration = match qualified.iter().position(|byte| *byte == b':') {
10674        Some(position) => {
10675            let mut declaration = b"xmlns:".to_vec();
10676            declaration.extend_from_slice(&qualified[..position]);
10677            declaration
10678        }
10679        None => b"xmlns".to_vec(),
10680    };
10681    let namespace = attribute_value(element, &declaration)?
10682        .ok_or_else(|| invalid_attribute("chartSpace", "namespace", "<missing>".to_owned()))?;
10683    if namespace != C_NS {
10684        return Err(invalid_attribute("chartSpace", "namespace", namespace));
10685    }
10686    reject_conflicting_prefix(element, b"c", C_NS)?;
10687    reject_conflicting_prefix(element, b"a", A_NS)?;
10688    reject_conflicting_prefix(element, b"r", R_NS)
10689}
10690
10691fn reject_conflicting_prefix_in_xml(xml: &[u8], prefix: &[u8], namespace: &str) -> Result<()> {
10692    let mut reader = Reader::from_reader(xml);
10693    let mut buffer = Vec::new();
10694    loop {
10695        match reader
10696            .read_event_into(&mut buffer)
10697            .map_err(OxmlError::from)?
10698        {
10699            Event::Start(element) | Event::Empty(element) => {
10700                return reject_conflicting_prefix(&element, prefix, namespace);
10701            }
10702            Event::Eof => return Err(ChartError::MissingElement("XML root".to_owned())),
10703            _ => {}
10704        }
10705        buffer.clear();
10706    }
10707}
10708
10709fn reject_rewritten_foreign_elements(
10710    original: &[u8],
10711    written: &[u8],
10712    inherited: &NamespaceBindings,
10713    root_local: &[u8],
10714) -> Result<()> {
10715    let original = foreign_element_regions(original, inherited, root_local)?;
10716    let mut written_namespaces = inherited.clone();
10717    written_namespaces.retain(|(prefix, _)| prefix != b"a");
10718    written_namespaces.push((b"a".to_vec(), A_NS.to_owned()));
10719    let written = foreign_element_regions(written, &written_namespaces, root_local)?;
10720    if original != written {
10721        return Err(invalid_attribute(
10722            &String::from_utf8_lossy(root_local),
10723            "namespace",
10724            "foreign DrawingML content would be converted to typed content".to_owned(),
10725        ));
10726    }
10727    Ok(())
10728}
10729
10730fn foreign_element_regions(
10731    xml: &[u8],
10732    inherited: &NamespaceBindings,
10733    root_local: &[u8],
10734) -> Result<Vec<Vec<u8>>> {
10735    let mut reader = Reader::from_reader(xml);
10736    let mut buffer = Vec::new();
10737    let root_namespaces = loop {
10738        match reader
10739            .read_event_into(&mut buffer)
10740            .map_err(OxmlError::from)?
10741        {
10742            Event::Start(element) if matches_local_name(element.name().as_ref(), root_local) => {
10743                break bindings_with_local(inherited, &element)?;
10744            }
10745            Event::Empty(element) if matches_local_name(element.name().as_ref(), root_local) => {
10746                return Ok(Vec::new());
10747            }
10748            Event::Start(element) | Event::Empty(element) => {
10749                return Err(ChartError::UnexpectedElement(element_name(&element)));
10750            }
10751            Event::Eof => {
10752                return Err(ChartError::MissingElement(
10753                    String::from_utf8_lossy(root_local).into_owned(),
10754                ));
10755            }
10756            _ => {}
10757        }
10758        buffer.clear();
10759    };
10760
10761    let mut namespace_stack = vec![root_namespaces];
10762    let mut foreign = Vec::new();
10763    let mut child_buffer = Vec::new();
10764    loop {
10765        match reader
10766            .read_event_into(&mut child_buffer)
10767            .map_err(OxmlError::from)?
10768        {
10769            Event::Start(element) => {
10770                let namespaces = namespace_stack.last().expect("root namespace binding");
10771                if element_is_in_namespace(&element, A_NS, namespaces)? {
10772                    namespace_stack.push(bindings_with_local(namespaces, &element)?);
10773                } else {
10774                    foreign.push(capture_element(&mut reader, &element)?);
10775                }
10776            }
10777            Event::Empty(element) => {
10778                let namespaces = namespace_stack.last().expect("root namespace binding");
10779                if !element_is_in_namespace(&element, A_NS, namespaces)? {
10780                    foreign.push(capture_empty_element(&element)?);
10781                }
10782            }
10783            Event::End(element) if matches_local_name(element.name().as_ref(), root_local) => {
10784                return Ok(foreign);
10785            }
10786            Event::End(_) => {
10787                namespace_stack.pop();
10788            }
10789            Event::Eof => {
10790                return Err(missing_end(&String::from_utf8_lossy(root_local)));
10791            }
10792            _ => {}
10793        }
10794        child_buffer.clear();
10795    }
10796}
10797
10798fn reject_conflicting_prefix(
10799    element: &BytesStart<'_>,
10800    prefix: &[u8],
10801    namespace: &str,
10802) -> Result<()> {
10803    let mut key = b"xmlns:".to_vec();
10804    key.extend_from_slice(prefix);
10805    if let Some(value) = attribute_value(element, &key)?
10806        && value != namespace
10807    {
10808        return Err(invalid_attribute(
10809            &element_name(element),
10810            &String::from_utf8_lossy(&key),
10811            value,
10812        ));
10813    }
10814    Ok(())
10815}
10816
10817fn namespace_bindings(element: &BytesStart<'_>) -> Result<NamespaceBindings> {
10818    let mut bindings = Vec::new();
10819    for attribute in element.attributes() {
10820        let attribute = attribute.map_err(OxmlError::from)?;
10821        let key = attribute.key.as_ref();
10822        let prefix = if key == b"xmlns" {
10823            Vec::new()
10824        } else if let Some(prefix) = key.strip_prefix(b"xmlns:") {
10825            prefix.to_vec()
10826        } else {
10827            continue;
10828        };
10829        let namespace = attribute
10830            .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
10831            .map_err(OxmlError::from)?
10832            .into_owned();
10833        bindings.push((prefix, namespace));
10834    }
10835    Ok(bindings)
10836}
10837
10838fn bindings_with_local(
10839    inherited: &NamespaceBindings,
10840    element: &BytesStart<'_>,
10841) -> Result<NamespaceBindings> {
10842    let mut bindings = inherited.clone();
10843    for (prefix, namespace) in namespace_bindings(element)? {
10844        upsert_namespace_binding(&mut bindings, prefix, namespace);
10845    }
10846    Ok(bindings)
10847}
10848
10849fn upsert_namespace_binding(bindings: &mut NamespaceBindings, prefix: Vec<u8>, namespace: String) {
10850    if let Some((_, current)) = bindings.iter_mut().find(|(current, _)| current == &prefix) {
10851        *current = namespace;
10852    } else {
10853        bindings.push((prefix, namespace));
10854    }
10855}
10856
10857fn element_is_in_namespace(
10858    element: &BytesStart<'_>,
10859    expected: &str,
10860    inherited: &NamespaceBindings,
10861) -> Result<bool> {
10862    let name = element.name();
10863    let qualified = name.as_ref();
10864    let prefix = qualified
10865        .iter()
10866        .position(|byte| *byte == b':')
10867        .map(|position| &qualified[..position])
10868        .unwrap_or_default();
10869    let local = namespace_bindings(element)?;
10870    let namespace = local
10871        .iter()
10872        .rev()
10873        .chain(inherited.iter().rev())
10874        .find(|(candidate, _)| candidate.as_slice() == prefix)
10875        .map(|(_, namespace)| namespace.as_str());
10876    Ok(namespace == Some(expected))
10877}
10878
10879fn attribute_value(element: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
10880    for attribute in element.attributes() {
10881        let attribute = attribute.map_err(OxmlError::from)?;
10882        if attribute.key.as_ref() == name {
10883            return Ok(Some(
10884                attribute
10885                    .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
10886                    .map_err(OxmlError::from)?
10887                    .into_owned(),
10888            ));
10889        }
10890    }
10891    Ok(None)
10892}
10893
10894fn capture_root_attributes(start: &BytesStart<'_>) -> Result<RootAttributes> {
10895    let mut attributes = Vec::new();
10896    let mut namespaces = Vec::new();
10897    for attribute in start.attributes() {
10898        let attribute = attribute.map_err(OxmlError::from)?;
10899        let name = std::str::from_utf8(attribute.key.as_ref())
10900            .map_err(OxmlError::from)?
10901            .to_owned();
10902        let value = attribute
10903            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10904            .map_err(OxmlError::from)?
10905            .into_owned();
10906        if name == "xmlns:c" || name == "xmlns:a" || name == "xmlns:r" {
10907            continue;
10908        }
10909        if name == "xmlns" || name.starts_with("xmlns:") {
10910            namespaces.push((name, value));
10911        } else {
10912            attributes.push((name, value));
10913        }
10914    }
10915    Ok((attributes, namespaces))
10916}
10917
10918fn capture_attributes(start: &BytesStart<'_>) -> Result<XmlAttributes> {
10919    let mut attributes = Vec::new();
10920    for attribute in start.attributes() {
10921        let attribute = attribute.map_err(OxmlError::from)?;
10922        attributes.push((
10923            std::str::from_utf8(attribute.key.as_ref())
10924                .map_err(OxmlError::from)?
10925                .to_owned(),
10926            attribute
10927                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10928                .map_err(OxmlError::from)?
10929                .into_owned(),
10930        ));
10931    }
10932    Ok(attributes)
10933}
10934
10935fn capture_attributes_except(start: &BytesStart<'_>, excluded: &[u8]) -> Result<XmlAttributes> {
10936    let mut attributes = Vec::new();
10937    for attribute in start.attributes() {
10938        let attribute = attribute.map_err(OxmlError::from)?;
10939        if attribute.key.as_ref() == excluded {
10940            continue;
10941        }
10942        attributes.push((
10943            std::str::from_utf8(attribute.key.as_ref())
10944                .map_err(OxmlError::from)?
10945                .to_owned(),
10946            attribute
10947                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10948                .map_err(OxmlError::from)?
10949                .into_owned(),
10950        ));
10951    }
10952    Ok(attributes)
10953}
10954
10955fn capture_attributes_excluding(
10956    start: &BytesStart<'_>,
10957    excluded: &[&[u8]],
10958) -> Result<XmlAttributes> {
10959    let mut attributes = Vec::new();
10960    for attribute in start.attributes() {
10961        let attribute = attribute.map_err(OxmlError::from)?;
10962        if excluded.contains(&attribute.key.as_ref()) {
10963            continue;
10964        }
10965        attributes.push((
10966            std::str::from_utf8(attribute.key.as_ref())
10967                .map_err(OxmlError::from)?
10968                .to_owned(),
10969            attribute
10970                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10971                .map_err(OxmlError::from)?
10972                .into_owned(),
10973        ));
10974    }
10975    Ok(attributes)
10976}
10977
10978fn push_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
10979    for (name, value) in attributes {
10980        start.push_attribute((name.as_str(), value.as_str()));
10981    }
10982}
10983
10984fn write_scalar<W: Write>(
10985    writer: &mut Writer<W>,
10986    name: &str,
10987    value: &str,
10988    markup: Option<&ScalarMarkup>,
10989) -> Result<()> {
10990    let mut start = BytesStart::new(name);
10991    start.push_attribute(("val", value));
10992    if let Some(markup) = markup {
10993        push_attributes(&mut start, &markup.raw_attributes);
10994    }
10995    let has_content = markup.is_some_and(|markup| !markup.raw_content.is_empty());
10996    if !has_content {
10997        writer
10998            .write_event(Event::Empty(start))
10999            .map_err(OxmlError::from)?;
11000        return Ok(());
11001    }
11002    writer
11003        .write_event(Event::Start(start))
11004        .map_err(OxmlError::from)?;
11005    if let Some(markup) = markup {
11006        for raw in &markup.raw_content {
11007            writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
11008        }
11009    }
11010    writer
11011        .write_event(Event::End(BytesEnd::new(name)))
11012        .map_err(OxmlError::from)?;
11013    Ok(())
11014}
11015
11016fn capture_event(event: Event<'_>) -> Result<Vec<u8>> {
11017    let mut writer = Writer::new(Vec::new());
11018    writer
11019        .write_event(event.into_owned())
11020        .map_err(OxmlError::from)?;
11021    Ok(writer.into_inner())
11022}
11023
11024fn emit_raw<'a, W: Write>(
11025    writer: &mut Writer<W>,
11026    children: impl Iterator<Item = &'a [u8]>,
11027) -> Result<()> {
11028    for child in children {
11029        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
11030    }
11031    Ok(())
11032}
11033
11034fn invalid_attribute(element: &str, attribute: &str, value: String) -> ChartError {
11035    ChartError::InvalidAttribute {
11036        element: element.to_owned(),
11037        attribute: attribute.to_owned(),
11038        value,
11039    }
11040}
11041
11042fn element_name(element: &BytesStart<'_>) -> String {
11043    String::from_utf8_lossy(element.name().as_ref()).into_owned()
11044}
11045
11046fn missing_end(element: &str) -> ChartError {
11047    ChartError::Xml(OxmlError::MissingElement(format!("closing {element}")))
11048}
11049
11050#[cfg(test)]
11051mod tests {
11052    use std::collections::HashSet;
11053    use std::fs;
11054    use std::path::{Path, PathBuf};
11055    use std::process::Command;
11056
11057    use oxml_core::raw_xml::{capture_element, capture_empty_element};
11058    use oxml_drawing::color::{ColorMap, ColorMapSlot, RgbColor, ThemeColorSlot};
11059    use oxml_drawing::theme::CT_OfficeStyleSheet;
11060    use oxml_layout::{
11061        Color, FontManager, LayoutResult, PageFrame, Paint, PathCommand, Point, PositionedElement,
11062        Rect,
11063    };
11064    use oxml_opc::OpcPackage;
11065    use quick_xml::Reader;
11066    use quick_xml::events::Event;
11067
11068    use super::{
11069        A_NS, Axis, AxisData, AxisId, AxisKind, AxisPosition, BarDirection, BarGrouping, C_NS,
11070        CT_ChartSpace, CT_DLbls, CT_ShapeProperties, CT_TextBody, ChartData, ChartGeometry,
11071        ChartKind, DataLabelPosition, DispBlanksAs, Domain, Grouping, NumberFormat, NumericData,
11072        Orientation, Plot, R_NS, ScatterStyle, Series, StringRef, TickLabelPosition, TickMark,
11073        authored_chart_parts, capture_event, local_name, matches_local_name, nice_number_scale,
11074        render_chart as render_chart_with_theme, render_geometry as render_geometry_with_theme,
11075    };
11076
11077    const MANIFEST: &str = include_str!("../../../scripts/pptx-corpus-manifest.tsv");
11078    const EXPECTED_DECKS: usize = 50;
11079    const LIBREOFFICE_VERSION: &str =
11080        "LibreOffice 26.2.5.2 cd7284b4cbbfeb507e630c1aac019f4157393acb";
11081    const PDFTOTEXT_VERSION: &str = "pdftotext version 26.01.0";
11082    const PDFTOPPM_VERSION: &str = "pdftoppm version 26.01.0";
11083    const PLOT_RENDER_NORMALIZED_MAE_THRESHOLD: f64 = 0.0;
11084
11085    #[test]
11086    fn authored_charts_emit_portable_viewer_defaults() {
11087        let data = ChartData {
11088            categories: vec!["August".to_owned(), "September".to_owned()],
11089            series: vec![("Price".to_owned(), vec![12.5, 19.2])],
11090            number_format: Some(r"0.##\%".to_owned()),
11091            category_axis_title: Some("Month".to_owned()),
11092            value_axis_title: Some("Change".to_owned()),
11093            palette: vec![
11094                RgbColor::parse("2B6FE3").unwrap(),
11095                RgbColor::parse("F0761F").unwrap(),
11096            ],
11097        };
11098        let (line, line_workbook) = authored_chart_parts(ChartKind::Line, &data, "rId1").unwrap();
11099        let line = std::str::from_utf8(&line).unwrap();
11100        assert_eq!(line.matches(r#"<c:delete val="0"/>"#).count(), 2);
11101        assert!(line.contains(r#"<c:numFmt formatCode="0.##\%" sourceLinked="0"/>"#));
11102        assert!(line.contains("<a:t>Month</a:t>"));
11103        assert!(line.contains("<a:t>Change</a:t>"));
11104        assert!(line.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
11105        let assert_portable_line = |xml: &str| {
11106            let titles = xml
11107                .split("<c:title>")
11108                .skip(1)
11109                .map(|tail| tail.split_once("</c:title>").unwrap().0)
11110                .collect::<Vec<_>>();
11111            assert_eq!(titles.len(), 2);
11112            for title in titles {
11113                let text = title.find("<c:tx>").unwrap();
11114                let layout = title.find("<c:layout/>").unwrap();
11115                let overlay = title.find(r#"<c:overlay val="0"/>"#).unwrap();
11116                assert!(text < layout && layout < overlay);
11117            }
11118
11119            let line_plot = xml
11120                .split_once("<c:lineChart>")
11121                .unwrap()
11122                .1
11123                .split_once("</c:lineChart>")
11124                .unwrap()
11125                .0;
11126            assert_eq!(line_plot.matches(r#"<c:marker val="0"/>"#).count(), 1);
11127            assert_eq!(line_plot.matches(r#"<c:smooth val="0"/>"#).count(), 1);
11128            let marker = line_plot.find(r#"<c:marker val="0"/>"#).unwrap();
11129            let smooth = line_plot.find(r#"<c:smooth val="0"/>"#).unwrap();
11130            let axis_id = line_plot.find("<c:axId").unwrap();
11131            assert!(marker < smooth && smooth < axis_id);
11132        };
11133        assert_portable_line(line);
11134        for axis in line.split("<c:crossAx").take(2) {
11135            let scaling = axis.rfind("<c:scaling").unwrap();
11136            let deleted = axis.rfind("<c:delete").unwrap();
11137            let position = axis.rfind("<c:axPos").unwrap();
11138            assert!(scaling < deleted && deleted < position);
11139        }
11140
11141        let parsed = CT_ChartSpace::from_xml(line.as_bytes()).unwrap();
11142        let rewritten = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
11143        assert_portable_line(&rewritten);
11144        assert!(rewritten.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
11145        assert!(rewritten.contains(r#"<c:externalData r:id="rId1">"#));
11146
11147        for kind in [
11148            ChartKind::Bar,
11149            ChartKind::Pie,
11150            ChartKind::Doughnut,
11151            ChartKind::Area,
11152            ChartKind::Scatter,
11153            ChartKind::Radar,
11154        ] {
11155            let mut kind_data = data.clone();
11156            if kind == ChartKind::Scatter {
11157                kind_data.categories = vec!["1".to_owned(), "2".to_owned()];
11158            }
11159            let (chart, workbook) = authored_chart_parts(kind, &kind_data, "rId1").unwrap();
11160            let chart = std::str::from_utf8(&chart).unwrap();
11161            assert!(!chart.contains(r#"<c:marker val="0"/>"#));
11162            assert!(!chart.contains(r#"<c:smooth val="0"/>"#));
11163            if kind != ChartKind::Scatter {
11164                assert_eq!(workbook, line_workbook);
11165            }
11166            if matches!(kind, ChartKind::Pie | ChartKind::Doughnut) {
11167                assert!(chart.contains("<c:legend"));
11168                assert!(chart.contains(r#"<c:showPercent val="1"/>"#));
11169            }
11170            if kind == ChartKind::Doughnut {
11171                assert!(chart.contains(r#"<c:holeSize val="50"/>"#));
11172            }
11173            if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) {
11174                assert_eq!(chart.matches("<c:dPt>").count(), 2);
11175                assert!(chart.contains(
11176                    r#"<c:dPt><c:idx val="0"/><c:spPr><a:solidFill><a:srgbClr val="2B6FE3"/>"#
11177                ));
11178                assert!(chart.contains(
11179                    r#"<c:dPt><c:idx val="1"/><c:spPr><a:solidFill><a:srgbClr val="F0761F"/>"#
11180                ));
11181            }
11182
11183            let parsed = CT_ChartSpace::from_xml(chart.as_bytes()).unwrap();
11184            let rewritten = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
11185            assert_eq!(
11186                rewritten.matches("<c:dPt>").count(),
11187                usize::from(matches!(
11188                    kind,
11189                    ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut
11190                )) * 2
11191            );
11192            assert!(rewritten.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
11193            if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) {
11194                assert!(rewritten.contains(r#"<a:srgbClr val="F0761F"/>"#));
11195            }
11196        }
11197    }
11198
11199    #[test]
11200    fn optional_numeric_cache_format_remains_omitted() {
11201        let xml = br#"<c:numRef xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:f>Sheet1!$B$2:$B$3</c:f><c:numCache><c:ptCount val="2"/><c:pt idx="0"><c:v>12.5</c:v></c:pt><c:pt idx="1"><c:v>19.2</c:v></c:pt></c:numCache></c:numRef>"#;
11202        let mut parsed = NumericData::from_xml(xml).unwrap();
11203        assert_eq!(parsed.format_code, "General");
11204        let written = parsed.to_xml().unwrap();
11205        assert!(!String::from_utf8_lossy(&written).contains("formatCode"));
11206        assert_eq!(NumericData::from_xml(&written).unwrap(), parsed);
11207
11208        parsed.format_code = "0.00".to_owned();
11209        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
11210        assert!(written.contains("<c:formatCode>0.00</c:formatCode>"));
11211    }
11212
11213    fn render_geometry(chart: &super::CT_Chart, bounds: Rect) -> super::Result<ChartGeometry> {
11214        render_geometry_with_theme(
11215            chart,
11216            bounds,
11217            &CT_OfficeStyleSheet::office_default(),
11218            &ColorMap::default(),
11219        )
11220    }
11221
11222    fn render_chart(
11223        chart: &super::CT_Chart,
11224        bounds: Rect,
11225        fonts: &mut FontManager,
11226    ) -> super::Result<oxml_layout::GroupElement> {
11227        render_chart_with_theme(
11228            chart,
11229            bounds,
11230            &CT_OfficeStyleSheet::office_default(),
11231            &ColorMap::default(),
11232            fonts,
11233        )
11234    }
11235
11236    #[test]
11237    fn bar_chart_rasterises_at_computed_positions() {
11238        let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11239        let geometry = render_geometry(
11240            &chart.chart,
11241            Rect {
11242                x: 0.0,
11243                y: 0.0,
11244                width: 200.0,
11245                height: 140.0,
11246            },
11247        )
11248        .unwrap();
11249        assert_eq!(
11250            geometry.plot_bounds,
11251            Rect {
11252                x: 36.0,
11253                y: 12.0,
11254                width: 152.0,
11255                height: 100.0,
11256            }
11257        );
11258        let bounds = path_bounds(&geometry);
11259        assert_eq!(bounds.len(), 2);
11260        assert_rect_near(
11261            bounds[0],
11262            Rect {
11263                x: 58.8,
11264                y: 62.0,
11265                width: 30.4,
11266                height: 50.0,
11267            },
11268        );
11269        assert_rect_near(
11270            bounds[1],
11271            Rect {
11272                x: 134.8,
11273                y: 12.0,
11274                width: 30.4,
11275                height: 100.0,
11276            },
11277        );
11278
11279        let layout = LayoutResult::new(
11280            vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
11281            Vec::new(),
11282            None,
11283            Vec::new(),
11284        );
11285        let png = oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap();
11286        let pixmap = tiny_skia::Pixmap::decode_png(&png).unwrap();
11287        for (x, y) in [(74, 87), (150, 37)] {
11288            let pixel = pixmap.pixel(x, y).unwrap();
11289            assert_ne!((pixel.red(), pixel.green(), pixel.blue()), (255, 255, 255));
11290        }
11291    }
11292
11293    #[test]
11294    fn unstyled_four_series_use_accent_one_through_four() {
11295        let chart = four_series_bar_chart();
11296        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11297        let colours = path_colours(&geometry);
11298
11299        assert_eq!(
11300            &colours[..4],
11301            &[
11302                Color::from_hex("156082"),
11303                Color::from_hex("E97132"),
11304                Color::from_hex("196B24"),
11305                Color::from_hex("0F9ED5"),
11306            ]
11307        );
11308    }
11309
11310    #[test]
11311    fn direct_series_solid_colour_overrides_theme_accent() {
11312        for (paint, expected) in [
11313            (
11314                "<a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill>",
11315                Color::from_hex("123456"),
11316            ),
11317            (
11318                "<a:ln><a:solidFill><a:srgbClr val=\"654321\"/></a:solidFill></a:ln>",
11319                Color::from_hex("654321"),
11320            ),
11321            (
11322                "<a:noFill/>",
11323                Color {
11324                    r: 0.0,
11325                    g: 0.0,
11326                    b: 0.0,
11327                    a: 0.0,
11328                },
11329            ),
11330        ] {
11331            let styled =
11332                plot_series(0).replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
11333            let plot = bar_plot("").replace(&plot_series(0), &styled);
11334            let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11335            let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11336
11337            assert_eq!(path_colours(&geometry)[0], expected);
11338        }
11339
11340        let styled = plot_series(0).replace(
11341            "<q:cat>",
11342            "<q:spPr><a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill></q:spPr><q:cat>",
11343        );
11344        let plot = bar_plot("").replace(&plot_series(0), &styled);
11345        let xml = chart_with_plot(&plot).replace(
11346            "</q:plotArea></q:chart>",
11347            "</q:plotArea><q:legend/></q:chart>",
11348        );
11349        let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
11350        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
11351        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
11352        assert!(group.children.iter().any(|element| matches!(
11353            element,
11354            PositionedElement::Path(path)
11355                if path.fill == Some(Paint::Solid(Color::from_hex("123456")))
11356        )));
11357
11358        let mut theme = CT_OfficeStyleSheet::office_default();
11359        let preset = oxml_drawing::fill::Fill::from_xml(
11360            br#"<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:prstClr val="red"/></a:solidFill>"#,
11361        )
11362        .unwrap();
11363        let oxml_drawing::fill::Fill::Solid(preset) = preset else {
11364            panic!("expected solid fill");
11365        };
11366        theme.theme_elements.color_scheme.hyperlink = preset.color.unwrap();
11367        let geometry =
11368            render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &ColorMap::default())
11369                .unwrap();
11370        assert_eq!(path_colours(&geometry)[0], Color::from_hex("123456"));
11371
11372        for (paint, expected_alpha) in [
11373            ("<a:noFill/>", 0.0),
11374            (
11375                "<a:solidFill><a:srgbClr val=\"123456\"><a:alpha val=\"50000\"/></a:srgbClr></a:solidFill>",
11376                (128.0 / 255.0) * 0.55,
11377            ),
11378        ] {
11379            let (_, area, _) = remaining_plot_fixtures()
11380                .into_iter()
11381                .find(|(kind, _, _)| *kind == "areaChart")
11382                .unwrap();
11383            let styled_area = area.replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
11384            let chart =
11385                CT_ChartSpace::from_xml(chart_with_optional_axes(&styled_area, true).as_bytes())
11386                    .unwrap();
11387            let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11388            let actual_alpha = path_colours(&geometry)[0].a;
11389
11390            assert!((actual_alpha - expected_alpha).abs() < 1.0e-12);
11391        }
11392    }
11393
11394    #[test]
11395    fn series_accent_cycle_repeats_after_six() {
11396        let chart = seven_series_bar_chart();
11397        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11398        let colours = path_colours(&geometry);
11399
11400        assert_eq!(colours[0], Color::from_hex("156082"));
11401        assert_eq!(colours[6], colours[0]);
11402    }
11403
11404    #[test]
11405    fn series_colours_honor_colour_map_and_transform_order() {
11406        let styled = plot_series(0).replace(
11407            "<q:cat>",
11408            "<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>",
11409        );
11410        let plot = bar_plot("").replace(&plot_series(0), &styled);
11411        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11412        let color_map =
11413            ColorMap::default().with_overrides(&[(ColorMapSlot::Accent1, ThemeColorSlot::Accent4)]);
11414        let mut theme = CT_OfficeStyleSheet::office_default();
11415        let theme_fill = oxml_drawing::fill::Fill::from_xml(
11416            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>"#,
11417        )
11418        .unwrap();
11419        let oxml_drawing::fill::Fill::Solid(theme_fill) = theme_fill else {
11420            panic!("expected solid fill");
11421        };
11422        let theme_colour = theme_fill.color.unwrap();
11423        theme.theme_elements.color_scheme.accent4 = theme_colour.clone();
11424        let geometry =
11425            render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &color_map).unwrap();
11426        let series_fill = oxml_drawing::fill::Fill::from_xml(
11427            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>"#,
11428        )
11429        .unwrap();
11430        let oxml_drawing::fill::Fill::Solid(series_fill) = series_fill else {
11431            panic!("expected solid fill");
11432        };
11433        let mut transforms = theme_colour.transforms().to_vec();
11434        transforms.extend_from_slice(series_fill.color.as_ref().unwrap().transforms());
11435        let expected = oxml_drawing::color::apply_color_transforms(
11436            RgbColor::parse("336699").unwrap(),
11437            &transforms,
11438        );
11439
11440        assert_eq!(path_colours(&geometry)[0], resolved_layout_colour(expected));
11441    }
11442
11443    #[test]
11444    fn chart_colours_do_not_use_word_tint_shade() {
11445        let styled = plot_series(0).replace(
11446            "<q:cat>",
11447            "<q:spPr><a:solidFill><a:srgbClr val=\"808080\"><a:tint val=\"50000\"/></a:srgbClr></a:solidFill></q:spPr><q:cat>",
11448        );
11449        let plot = bar_plot("").replace(&plot_series(0), &styled);
11450        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11451        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11452
11453        assert_eq!(
11454            path_colours(&geometry)[0],
11455            Color {
11456                r: 205.0 / 255.0,
11457                g: 205.0 / 255.0,
11458                b: 205.0 / 255.0,
11459                a: 1.0,
11460            }
11461        );
11462    }
11463
11464    #[test]
11465    fn unsupported_direct_series_paint_is_contextual() {
11466        let styled = plot_series(0).replace(
11467            "<q:cat>",
11468            "<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>",
11469        );
11470        let plot = bar_plot("").replace(&plot_series(0), &styled);
11471        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11472        let error = render_geometry(&chart.chart, chart_bounds()).unwrap_err();
11473
11474        assert!(error.to_string().contains("c:ser[0] direct fill paint"));
11475        assert!(error.to_string().contains("gradient"));
11476    }
11477
11478    #[test]
11479    fn resolved_chart_palette_raster_is_deterministic() {
11480        let raster = || {
11481            let chart = four_series_bar_chart();
11482            let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
11483            let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
11484            let layout = LayoutResult::new(
11485                vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
11486                fonts.all_font_data(),
11487                None,
11488                Vec::new(),
11489            );
11490            oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
11491        };
11492        let chart = four_series_bar_chart();
11493        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11494
11495        assert_eq!(path_colours(&geometry)[0], Color::from_hex("156082"));
11496        assert_eq!(raster(), raster());
11497    }
11498
11499    #[test]
11500    fn bar_geometry_handles_direction_grouping_gap_and_overlap() {
11501        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11502        let Plot::Bar {
11503            overlap, series, ..
11504        } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11505        else {
11506            panic!("expected bar plot");
11507        };
11508        let mut second = series[0].clone();
11509        second.index = 1;
11510        second.order = 1;
11511        second.values.values = vec![2.0, 1.0];
11512        series.push(second);
11513
11514        *overlap = 25;
11515        let clustered = render_geometry(&chart.chart, chart_bounds()).unwrap();
11516        let clustered_bounds = path_bounds(&clustered);
11517        let clustered_width = 30.4 / 1.75;
11518        let clustered_advance = clustered_width * 0.75;
11519        assert_rects_near(
11520            &clustered_bounds,
11521            &[
11522                Rect {
11523                    x: 58.8,
11524                    y: 62.0,
11525                    width: clustered_width,
11526                    height: 50.0,
11527                },
11528                Rect {
11529                    x: 134.8,
11530                    y: 12.0,
11531                    width: clustered_width,
11532                    height: 100.0,
11533                },
11534                Rect {
11535                    x: 58.8 + clustered_advance,
11536                    y: 12.0,
11537                    width: clustered_width,
11538                    height: 100.0,
11539                },
11540                Rect {
11541                    x: 134.8 + clustered_advance,
11542                    y: 62.0,
11543                    width: clustered_width,
11544                    height: 50.0,
11545                },
11546            ],
11547        );
11548
11549        let Plot::Bar { direction, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
11550            unreachable!()
11551        };
11552        *direction = BarDirection::Bar;
11553        let horizontal = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11554        let horizontal_width = 20.0 / 1.75;
11555        let horizontal_advance = horizontal_width * 0.75;
11556        assert_rects_near(
11557            &horizontal,
11558            &[
11559                Rect {
11560                    x: 36.0,
11561                    y: 27.0,
11562                    width: 76.0,
11563                    height: horizontal_width,
11564                },
11565                Rect {
11566                    x: 36.0,
11567                    y: 77.0,
11568                    width: 152.0,
11569                    height: horizontal_width,
11570                },
11571                Rect {
11572                    x: 36.0,
11573                    y: 27.0 + horizontal_advance,
11574                    width: 152.0,
11575                    height: horizontal_width,
11576                },
11577                Rect {
11578                    x: 36.0,
11579                    y: 77.0 + horizontal_advance,
11580                    width: 76.0,
11581                    height: horizontal_width,
11582                },
11583            ],
11584        );
11585
11586        let Plot::Bar {
11587            direction,
11588            grouping,
11589            ..
11590        } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11591        else {
11592            unreachable!()
11593        };
11594        *direction = BarDirection::Column;
11595        *grouping = BarGrouping::Stacked;
11596        let stacked = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11597        assert_rects_near(
11598            &stacked,
11599            &[
11600                Rect {
11601                    x: 58.8,
11602                    y: 112.0 - 100.0 / 3.0,
11603                    width: 30.4,
11604                    height: 100.0 / 3.0,
11605                },
11606                Rect {
11607                    x: 134.8,
11608                    y: 112.0 - 200.0 / 3.0,
11609                    width: 30.4,
11610                    height: 200.0 / 3.0,
11611                },
11612                Rect {
11613                    x: 58.8,
11614                    y: 12.0,
11615                    width: 30.4,
11616                    height: 200.0 / 3.0,
11617                },
11618                Rect {
11619                    x: 134.8,
11620                    y: 12.0,
11621                    width: 30.4,
11622                    height: 100.0 / 3.0,
11623                },
11624            ],
11625        );
11626
11627        let Plot::Bar {
11628            grouping, series, ..
11629        } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11630        else {
11631            unreachable!()
11632        };
11633        *grouping = BarGrouping::PercentStacked;
11634        series[1].values.values = vec![2.0, 2.0];
11635        let percent = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11636        assert_rects_near(
11637            &percent,
11638            &[
11639                Rect {
11640                    x: 58.8,
11641                    y: 112.0 - 100.0 / 3.0,
11642                    width: 30.4,
11643                    height: 100.0 / 3.0,
11644                },
11645                Rect {
11646                    x: 134.8,
11647                    y: 62.0,
11648                    width: 30.4,
11649                    height: 50.0,
11650                },
11651                Rect {
11652                    x: 58.8,
11653                    y: 12.0,
11654                    width: 30.4,
11655                    height: 200.0 / 3.0,
11656                },
11657                Rect {
11658                    x: 134.8,
11659                    y: 12.0,
11660                    width: 30.4,
11661                    height: 50.0,
11662                },
11663            ],
11664        );
11665    }
11666
11667    #[test]
11668    fn line_scatter_and_radar_emit_paths_and_markers() {
11669        let line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
11670        let line_geometry = render_geometry(&line.chart, chart_bounds()).unwrap();
11671        assert_path_points(
11672            path_commands(&line_geometry)[0],
11673            &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
11674            false,
11675        );
11676        assert_rects_near(
11677            &path_bounds(&line_geometry)[1..],
11678            &[
11679                Rect {
11680                    x: 71.5,
11681                    y: 109.5,
11682                    width: 5.0,
11683                    height: 5.0,
11684                },
11685                Rect {
11686                    x: 147.5,
11687                    y: 9.5,
11688                    width: 5.0,
11689                    height: 5.0,
11690                },
11691            ],
11692        );
11693
11694        let (scatter_plot, _) = remaining_plot_fixtures()
11695            .into_iter()
11696            .find(|(kind, _, _)| *kind == "scatterChart")
11697            .map(|(_, plot, axes)| (plot, axes))
11698            .unwrap();
11699        let mut scatter =
11700            CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
11701                .unwrap();
11702        let Plot::Scatter { style, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
11703        else {
11704            unreachable!()
11705        };
11706        *style = ScatterStyle::LineMarker;
11707        let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
11708        assert_path_points(
11709            path_commands(&scatter_geometry)[0],
11710            &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
11711            false,
11712        );
11713        assert_rects_near(
11714            &path_bounds(&scatter_geometry)[1..],
11715            &[
11716                Rect {
11717                    x: 33.5,
11718                    y: 109.5,
11719                    width: 5.0,
11720                    height: 5.0,
11721                },
11722                Rect {
11723                    x: 185.5,
11724                    y: 9.5,
11725                    width: 5.0,
11726                    height: 5.0,
11727                },
11728            ],
11729        );
11730
11731        let (radar_plot, _) = remaining_plot_fixtures()
11732            .into_iter()
11733            .find(|(kind, _, _)| *kind == "radarChart")
11734            .map(|(_, plot, axes)| (plot, axes))
11735            .unwrap();
11736        let radar = CT_ChartSpace::from_xml(chart_with_optional_axes(&radar_plot, true).as_bytes())
11737            .unwrap();
11738        let radar_geometry = render_geometry(&radar.chart, chart_bounds()).unwrap();
11739        assert_path_points(
11740            path_commands(&radar_geometry)[0],
11741            &[Point { x: 112.0, y: 37.0 }, Point { x: 112.0, y: 112.0 }],
11742            true,
11743        );
11744        assert_rects_near(
11745            &path_bounds(&radar_geometry)[1..],
11746            &[
11747                Rect {
11748                    x: 109.5,
11749                    y: 34.5,
11750                    width: 5.0,
11751                    height: 5.0,
11752                },
11753                Rect {
11754                    x: 109.5,
11755                    y: 109.5,
11756                    width: 5.0,
11757                    height: 5.0,
11758                },
11759            ],
11760        );
11761    }
11762
11763    #[test]
11764    fn pie_doughnut_and_area_emit_closed_paths() {
11765        let fixtures = remaining_plot_fixtures();
11766        let pie_plot = fixtures
11767            .iter()
11768            .find(|(kind, _, _)| *kind == "pieChart")
11769            .unwrap();
11770        let pie =
11771            CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot.1, pie_plot.2).as_bytes())
11772                .unwrap();
11773        let pie_geometry = render_geometry(&pie.chart, chart_bounds()).unwrap();
11774        let pie_commands = path_commands(&pie_geometry);
11775        assert_eq!(pie_commands.len(), 2);
11776        let [
11777            PathCommand::MoveTo(center),
11778            PathCommand::LineTo(start),
11779            ..,
11780            PathCommand::CurveTo { to: end, .. },
11781            PathCommand::Close,
11782        ] = pie_commands[0]
11783        else {
11784            panic!("expected closed cubic pie wedge");
11785        };
11786        assert_point_near(*center, Point { x: 112.0, y: 62.0 });
11787        assert_point_near(
11788            *start,
11789            Point {
11790                x: 137.0,
11791                y: 62.0 - 25.0 * 3.0_f64.sqrt(),
11792            },
11793        );
11794        assert_point_near(
11795            *end,
11796            Point {
11797                x: 137.0,
11798                y: 62.0 + 25.0 * 3.0_f64.sqrt(),
11799            },
11800        );
11801
11802        let doughnut_plot = fixtures
11803            .iter()
11804            .find(|(kind, _, _)| *kind == "doughnutChart")
11805            .unwrap();
11806        let doughnut = CT_ChartSpace::from_xml(
11807            chart_with_optional_axes(&doughnut_plot.1, doughnut_plot.2).as_bytes(),
11808        )
11809        .unwrap();
11810        let doughnut_geometry = render_geometry(&doughnut.chart, chart_bounds()).unwrap();
11811        let doughnut_commands = path_commands(&doughnut_geometry);
11812        assert_eq!(doughnut_commands.len(), 2);
11813        let commands = doughnut_commands[0];
11814        assert_eq!(commands.len(), 7);
11815        let PathCommand::MoveTo(outer_start) = commands[0] else {
11816            panic!("expected doughnut outer start");
11817        };
11818        let PathCommand::LineTo(inner_end) = commands[3] else {
11819            panic!("expected doughnut inner-radius join");
11820        };
11821        let PathCommand::CurveTo {
11822            to: inner_start, ..
11823        } = commands[5]
11824        else {
11825            panic!("expected reverse inner arc");
11826        };
11827        let diagonal = 25.0 * 2.0_f64.sqrt();
11828        assert_point_near(
11829            outer_start,
11830            Point {
11831                x: 112.0 + diagonal,
11832                y: 62.0 - diagonal,
11833            },
11834        );
11835        assert_point_near(
11836            inner_end,
11837            Point {
11838                x: 112.0 + 30.0 * 75.0_f64.to_radians().cos(),
11839                y: 62.0 + 30.0 * 75.0_f64.to_radians().sin(),
11840            },
11841        );
11842        let inner_diagonal = 15.0 * 2.0_f64.sqrt();
11843        assert_point_near(
11844            inner_start,
11845            Point {
11846                x: 112.0 + inner_diagonal,
11847                y: 62.0 - inner_diagonal,
11848            },
11849        );
11850        assert_eq!(commands.last(), Some(&PathCommand::Close));
11851
11852        let area_plot = fixtures
11853            .iter()
11854            .find(|(kind, _, _)| *kind == "areaChart")
11855            .unwrap();
11856        let mut area =
11857            CT_ChartSpace::from_xml(chart_with_optional_axes(&area_plot.1, area_plot.2).as_bytes())
11858                .unwrap();
11859        let area_geometry = render_geometry(&area.chart, chart_bounds()).unwrap();
11860        assert_path_points(
11861            path_commands(&area_geometry)[0],
11862            &[
11863                Point { x: 74.0, y: 62.0 },
11864                Point { x: 150.0, y: 12.0 },
11865                Point { x: 150.0, y: 112.0 },
11866                Point { x: 74.0, y: 112.0 },
11867            ],
11868            true,
11869        );
11870
11871        let Plot::Area {
11872            grouping, series, ..
11873        } = &mut area.chart.plot_area.plots_mut().unwrap()[0]
11874        else {
11875            unreachable!()
11876        };
11877        *grouping = Grouping::Stacked;
11878        let mut second = series[0].clone();
11879        second.index = 1;
11880        second.order = 1;
11881        second.values.values = vec![2.0, 1.0];
11882        series.push(second);
11883        let stacked_area = render_geometry(&area.chart, chart_bounds()).unwrap();
11884        assert_path_points(
11885            path_commands(&stacked_area)[1],
11886            &[
11887                Point { x: 74.0, y: 12.0 },
11888                Point { x: 150.0, y: 12.0 },
11889                Point {
11890                    x: 150.0,
11891                    y: 112.0 - 200.0 / 3.0,
11892                },
11893                Point {
11894                    x: 74.0,
11895                    y: 112.0 - 100.0 / 3.0,
11896                },
11897            ],
11898            true,
11899        );
11900    }
11901
11902    #[test]
11903    fn sparse_cache_indexes_preserve_slots_and_scatter_pairing() {
11904        let series = sparse_category_series();
11905        let bar = format!(
11906            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>"#
11907        );
11908        let bar = CT_ChartSpace::from_xml(chart_with_plot(&bar).as_bytes()).unwrap();
11909        assert_rects_near(
11910            &path_bounds(&render_geometry(&bar.chart, chart_bounds()).unwrap()),
11911            &[
11912                Rect {
11913                    x: 51.2,
11914                    y: 62.0,
11915                    width: 304.0 / 15.0,
11916                    height: 50.0,
11917                },
11918                Rect {
11919                    x: 36.0 + 304.0 / 3.0 + 15.2,
11920                    y: 12.0,
11921                    width: 304.0 / 15.0,
11922                    height: 100.0,
11923                },
11924            ],
11925        );
11926
11927        let line = format!(
11928            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>"#
11929        );
11930        let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line).as_bytes()).unwrap();
11931        let first = Point {
11932            x: 36.0 + 76.0 / 3.0,
11933            y: 62.0,
11934        };
11935        let missing = Point { x: 112.0, y: 112.0 };
11936        let last = Point {
11937            x: 36.0 + 380.0 / 3.0,
11938            y: 12.0,
11939        };
11940        let gap = render_geometry(&line.chart, chart_bounds()).unwrap();
11941        let gap_commands = path_commands(&gap);
11942        assert_eq!(gap_commands.len(), 2);
11943        assert_path_points(
11944            gap_commands[0],
11945            &[Point {
11946                x: first.x,
11947                y: 112.0,
11948            }],
11949            false,
11950        );
11951        assert_path_points(gap_commands[1], &[last], false);
11952        line.chart.disp_blanks_as = DispBlanksAs::Zero;
11953        assert_path_points(
11954            path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
11955            &[first, missing, last],
11956            false,
11957        );
11958        line.chart.disp_blanks_as = DispBlanksAs::Span;
11959        assert_path_points(
11960            path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
11961            &[
11962                Point {
11963                    x: first.x,
11964                    y: 112.0,
11965                },
11966                last,
11967            ],
11968            false,
11969        );
11970
11971        let area = format!(
11972            r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:areaChart>"#
11973        );
11974        let mut area = CT_ChartSpace::from_xml(chart_with_plot(&area).as_bytes()).unwrap();
11975        let area_gap = render_geometry(&area.chart, chart_bounds()).unwrap();
11976        let area_gap_commands = path_commands(&area_gap);
11977        assert_eq!(area_gap_commands.len(), 2);
11978        assert_path_points(
11979            area_gap_commands[0],
11980            &[
11981                first,
11982                Point {
11983                    x: first.x,
11984                    y: 112.0,
11985                },
11986            ],
11987            true,
11988        );
11989        assert_path_points(
11990            area_gap_commands[1],
11991            &[
11992                last,
11993                Point {
11994                    x: last.x,
11995                    y: 112.0,
11996                },
11997            ],
11998            true,
11999        );
12000        area.chart.disp_blanks_as = DispBlanksAs::Zero;
12001        assert_path_points(
12002            path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
12003            &[
12004                first,
12005                missing,
12006                last,
12007                Point {
12008                    x: last.x,
12009                    y: 112.0,
12010                },
12011                missing,
12012                Point {
12013                    x: first.x,
12014                    y: 112.0,
12015                },
12016            ],
12017            true,
12018        );
12019        area.chart.disp_blanks_as = DispBlanksAs::Span;
12020        assert_path_points(
12021            path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
12022            &[
12023                first,
12024                last,
12025                Point {
12026                    x: last.x,
12027                    y: 112.0,
12028                },
12029                Point {
12030                    x: first.x,
12031                    y: 112.0,
12032                },
12033            ],
12034            true,
12035        );
12036
12037        let radar = format!(
12038            r#"<q:radarChart><q:radarStyle val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#
12039        );
12040        let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
12041        assert_path_points(
12042            path_commands(&render_geometry(&radar.chart, chart_bounds()).unwrap())[0],
12043            &[
12044                Point { x: 112.0, y: 37.0 },
12045                Point {
12046                    x: 112.0 - 25.0 * 3.0_f64.sqrt(),
12047                    y: 87.0,
12048                },
12049            ],
12050            true,
12051        );
12052
12053        let scatter = format!(
12054            r#"<q:scatterChart><q:scatterStyle val="lineMarker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:scatterChart>"#,
12055            sparse_scatter_series()
12056        );
12057        let mut scatter = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
12058        let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12059        assert_eq!(geometry_children(&scatter_geometry).len(), 4);
12060        let gap_commands = path_commands(&scatter_geometry);
12061        assert_path_points(gap_commands[0], &[Point { x: 36.0, y: 112.0 }], false);
12062        assert_path_points(gap_commands[1], &[Point { x: 188.0, y: 12.0 }], false);
12063
12064        scatter.chart.disp_blanks_as = DispBlanksAs::Zero;
12065        let zero = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12066        assert_path_points(
12067            path_commands(&zero)[0],
12068            &[
12069                Point { x: 74.0, y: 87.0 },
12070                Point { x: 36.0, y: 62.0 },
12071                Point { x: 150.0, y: 112.0 },
12072                Point { x: 188.0, y: 12.0 },
12073            ],
12074            false,
12075        );
12076
12077        scatter.chart.disp_blanks_as = DispBlanksAs::Span;
12078        assert_path_points(
12079            path_commands(&render_geometry(&scatter.chart, chart_bounds()).unwrap())[0],
12080            &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
12081            false,
12082        );
12083    }
12084
12085    #[test]
12086    fn sparse_scatter_domains_and_labels_use_only_matched_points() {
12087        let scatter = format!(
12088            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>"#,
12089            sparse_scatter_series()
12090        );
12091        let chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
12092        let plot = &chart.chart.plot_area.plots().unwrap()[0];
12093        assert_eq!(
12094            super::scatter_domains(plot, DispBlanksAs::Gap).unwrap(),
12095            (
12096                Domain {
12097                    min: 10.0,
12098                    max: 40.0,
12099                },
12100                Domain {
12101                    min: 100.0,
12102                    max: 400.0,
12103                },
12104            )
12105        );
12106        let axes = chart.chart.plot_area.axes().unwrap();
12107        let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
12108        let anchors =
12109            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12110        assert_eq!(
12111            anchors.keys().copied().collect::<Vec<_>>(),
12112            vec![(0, 0), (0, 3)]
12113        );
12114        assert_eq!(anchors[&(0, 0)].center, Point { x: 0.0, y: 140.0 });
12115        assert_eq!(anchors[&(0, 3)].center, Point { x: 200.0, y: 0.0 });
12116    }
12117
12118    #[test]
12119    fn sparse_bubble_size_labels_resolve_logical_indexes() {
12120        let scatter = format!(
12121            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>"#,
12122            sparse_scatter_series()
12123        );
12124        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
12125        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12126        let Plot::Scatter { series, .. } = plot else {
12127            panic!("expected scatter plot");
12128        };
12129        series[0].bubble_size = Some(
12130            NumericData::from_xml(
12131                format!(
12132                    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>"#
12133                )
12134                .as_bytes(),
12135            )
12136            .unwrap(),
12137        );
12138        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12139        let mut rendered = Vec::new();
12140        super::render_data_labels(
12141            plot,
12142            chart_bounds(),
12143            DispBlanksAs::Gap,
12144            super::GeometryOptions::default(),
12145            &mut fonts,
12146            &mut rendered,
12147        )
12148        .unwrap();
12149        let text: Vec<_> = rendered
12150            .iter()
12151            .filter_map(|element| match element {
12152                PositionedElement::Text(run) => Some(run.text.as_str()),
12153                _ => None,
12154            })
12155            .collect();
12156        assert_eq!(text, vec!["5", "9"]);
12157    }
12158
12159    #[test]
12160    fn geometry_rejects_invalid_bounds_and_opaque_plots() {
12161        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12162        for bounds in [
12163            Rect {
12164                x: 0.0,
12165                y: 0.0,
12166                width: f64::NAN,
12167                height: 100.0,
12168            },
12169            Rect {
12170                x: 0.0,
12171                y: 0.0,
12172                width: 0.0,
12173                height: 100.0,
12174            },
12175            Rect {
12176                x: 0.0,
12177                y: 0.0,
12178                width: 40.0,
12179                height: 30.0,
12180            },
12181        ] {
12182            assert!(render_geometry(&chart.chart, bounds).is_err());
12183        }
12184
12185        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12186            unreachable!()
12187        };
12188        series[0].values.values.clear();
12189        assert!(render_geometry(&chart.chart, chart_bounds()).is_err());
12190
12191        let opaque = CT_ChartSpace::from_xml(
12192            chart_with_plot(r#"<q:bar3DChart><q:barDir val="col"/></q:bar3DChart>"#).as_bytes(),
12193        )
12194        .unwrap();
12195        let error = render_geometry(&opaque.chart, chart_bounds()).unwrap_err();
12196        assert!(error.to_string().contains("unsupported or combination"));
12197
12198        let (scatter_plot, _) = remaining_plot_fixtures()
12199            .into_iter()
12200            .find(|(kind, _, _)| *kind == "scatterChart")
12201            .map(|(_, plot, axes)| (plot, axes))
12202            .unwrap();
12203        let mut scatter =
12204            CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
12205                .unwrap();
12206        for invalid in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
12207            let Plot::Scatter { series, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
12208            else {
12209                unreachable!()
12210            };
12211            let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
12212                unreachable!()
12213            };
12214            x_values.values[0] = invalid;
12215            let error = render_geometry(&scatter.chart, chart_bounds()).unwrap_err();
12216            assert!(error.to_string().contains("xVal"));
12217        }
12218    }
12219
12220    #[test]
12221    fn finite_extremes_never_produce_nonfinite_geometry() {
12222        let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
12223        let Plot::Line { series, .. } = &mut line.chart.plot_area.plots_mut().unwrap()[0] else {
12224            unreachable!()
12225        };
12226        series[0].values.values = vec![-f64::MAX, f64::MAX];
12227        let extreme_line = render_geometry(&line.chart, chart_bounds()).unwrap();
12228        assert_path_points(
12229            path_commands(&extreme_line)[0],
12230            &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
12231            false,
12232        );
12233
12234        let mut stacked =
12235            CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12236        let Plot::Bar {
12237            grouping, series, ..
12238        } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
12239        else {
12240            unreachable!()
12241        };
12242        *grouping = BarGrouping::Stacked;
12243        series[0].values.values = vec![f64::MAX, 1.0];
12244        let mut second = series[0].clone();
12245        second.index = 1;
12246        second.order = 1;
12247        series.push(second);
12248        let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
12249        assert!(error.to_string().contains("stacked value total"));
12250
12251        let Plot::Bar { grouping, .. } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
12252        else {
12253            unreachable!()
12254        };
12255        *grouping = BarGrouping::PercentStacked;
12256        let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
12257        assert!(error.to_string().contains("stacked percentage total"));
12258
12259        let (pie_plot, _) = remaining_plot_fixtures()
12260            .into_iter()
12261            .find(|(kind, _, _)| *kind == "pieChart")
12262            .map(|(_, plot, axes)| (plot, axes))
12263            .unwrap();
12264        let mut pie =
12265            CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot, false).as_bytes()).unwrap();
12266        let Plot::Pie { series, .. } = &mut pie.chart.plot_area.plots_mut().unwrap()[0] else {
12267            unreachable!()
12268        };
12269        series[0].values.values = vec![f64::MAX, f64::MAX];
12270        let error = render_geometry(&pie.chart, chart_bounds()).unwrap_err();
12271        assert!(error.to_string().contains("pie value total"));
12272
12273        let (scatter_plot, _) = remaining_plot_fixtures()
12274            .into_iter()
12275            .find(|(kind, _, _)| *kind == "scatterChart")
12276            .map(|(_, plot, axes)| (plot, axes))
12277            .unwrap();
12278        let mut scatter =
12279            CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
12280                .unwrap();
12281        let Plot::Scatter { style, series, .. } =
12282            &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
12283        else {
12284            unreachable!()
12285        };
12286        *style = ScatterStyle::Line;
12287        let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
12288            unreachable!()
12289        };
12290        x_values.values = vec![-f64::MAX, f64::MAX];
12291        series[0].values.values = vec![-f64::MAX, f64::MAX];
12292        let extreme_scatter = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12293        assert_path_points(
12294            path_commands(&extreme_scatter)[0],
12295            &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
12296            false,
12297        );
12298    }
12299
12300    #[test]
12301    fn geometry_is_backend_neutral_and_deterministic() {
12302        let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12303        let first = render_geometry(&chart.chart, chart_bounds()).unwrap();
12304        let second = render_geometry(&chart.chart, chart_bounds()).unwrap();
12305        assert_eq!(
12306            format!("{:?}", first.elements),
12307            format!("{:?}", second.elements)
12308        );
12309
12310        let raster = |geometry: ChartGeometry| {
12311            let layout = LayoutResult::new(
12312                vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
12313                Vec::new(),
12314                None,
12315                Vec::new(),
12316            );
12317            oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
12318        };
12319        assert_eq!(raster(first), raster(second));
12320    }
12321
12322    #[test]
12323    fn zero_to_one_hundred_axis_uses_expected_ticks() {
12324        let scale = nice_number_scale(
12325            Domain {
12326                min: 0.0,
12327                max: 100.0,
12328            },
12329            None,
12330            None,
12331            6,
12332        )
12333        .unwrap();
12334        assert_eq!(scale.ticks, vec![0.0, 20.0, 40.0, 60.0, 80.0, 100.0]);
12335    }
12336
12337    #[test]
12338    fn nice_number_ticks_cover_unpinned_extents() {
12339        for domain in [
12340            Domain {
12341                min: 3.0,
12342                max: 97.0,
12343            },
12344            Domain {
12345                min: -97.0,
12346                max: -3.0,
12347            },
12348            Domain {
12349                min: -3.5,
12350                max: 8.25,
12351            },
12352            Domain {
12353                min: 0.125,
12354                max: 0.875,
12355            },
12356            Domain {
12357                min: 1.0e12,
12358                max: 7.0e12,
12359            },
12360            Domain {
12361                min: -f64::MAX,
12362                max: f64::MAX,
12363            },
12364            Domain {
12365                min: f64::from_bits(1),
12366                max: f64::from_bits(5),
12367            },
12368        ] {
12369            let scale = nice_number_scale(domain, None, None, 6).unwrap();
12370            assert!(scale.domain.min <= domain.min);
12371            assert!(scale.domain.max >= domain.max);
12372            assert!(scale.ticks.iter().all(|tick| tick.is_finite()));
12373            assert!(scale.ticks.windows(2).all(|pair| pair[0] < pair[1]));
12374        }
12375
12376        let constant = nice_number_scale(Domain { min: 4.0, max: 4.0 }, None, None, 6).unwrap();
12377        assert!(constant.domain.min < 4.0 && constant.domain.max > 4.0);
12378    }
12379
12380    #[test]
12381    fn one_sided_explicit_scale_beyond_data_derives_a_valid_opposite_bound() {
12382        let minimum = nice_number_scale(
12383            Domain {
12384                min: 0.0,
12385                max: 10.0,
12386            },
12387            Some(20.0),
12388            None,
12389            6,
12390        )
12391        .unwrap();
12392        assert_eq!(minimum.domain.min, 20.0);
12393        assert!(minimum.domain.max > 20.0);
12394        assert!(minimum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
12395
12396        let maximum = nice_number_scale(
12397            Domain {
12398                min: 0.0,
12399                max: 10.0,
12400            },
12401            None,
12402            Some(-20.0),
12403            6,
12404        )
12405        .unwrap();
12406        assert_eq!(maximum.domain.max, -20.0);
12407        assert!(maximum.domain.min < -20.0);
12408        assert!(maximum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
12409    }
12410
12411    #[test]
12412    fn standard_line_domain_does_not_force_zero() {
12413        let mut chart =
12414            CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
12415        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12416        let Plot::Line { series, .. } = plot else {
12417            panic!("expected line plot");
12418        };
12419        series[0].values.values = vec![1_000.0, 1_010.0];
12420        assert_eq!(
12421            super::plot_value_domain(plot, DispBlanksAs::Gap).unwrap(),
12422            Some(Domain {
12423                min: 1_000.0,
12424                max: 1_010.0,
12425            })
12426        );
12427        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12428        assert_path_points(
12429            path_commands(&geometry)[0],
12430            &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
12431            false,
12432        );
12433    }
12434
12435    #[test]
12436    fn explicit_scaling_bounds_and_orientation_are_honored() {
12437        let scale = nice_number_scale(
12438            Domain {
12439                min: 25.0,
12440                max: 75.0,
12441            },
12442            Some(0.0),
12443            Some(100.0),
12444            6,
12445        )
12446        .unwrap();
12447        assert_eq!(
12448            scale.domain,
12449            Domain {
12450                min: 0.0,
12451                max: 100.0
12452            }
12453        );
12454        assert_eq!(
12455            super::map_y_oriented(0.0, scale.domain, chart_bounds(), Orientation::MaxMin).unwrap(),
12456            0.0
12457        );
12458        assert_eq!(
12459            super::map_y_oriented(100.0, scale.domain, chart_bounds(), Orientation::MaxMin)
12460                .unwrap(),
12461            140.0
12462        );
12463
12464        let mut chart = labelled_bar_chart();
12465        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12466            panic!("expected bar plot");
12467        };
12468        series[0].values.values = vec![25.0, 75.0];
12469        let value_axis = chart
12470            .chart
12471            .plot_area
12472            .axes
12473            .iter_mut()
12474            .find(|axis| axis.kind == AxisKind::Value)
12475            .unwrap();
12476        value_axis.scaling.orientation = Orientation::MaxMin;
12477        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12478        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12479        let plot = group
12480            .children
12481            .iter()
12482            .find_map(|element| match element {
12483                PositionedElement::Group(group) => Some(group),
12484                _ => None,
12485            })
12486            .unwrap();
12487        let bars: Vec<_> = plot
12488            .children
12489            .iter()
12490            .filter_map(|element| match element {
12491                PositionedElement::Path(path) => path.path.bounds(),
12492                _ => None,
12493            })
12494            .collect();
12495        assert_eq!(bars.len(), 2);
12496        assert!((bars[0].y - 12.0).abs() < 1.0e-9);
12497        assert!((bars[0].height - 25.0).abs() < 1.0e-9);
12498        assert!((bars[1].height - 75.0).abs() < 1.0e-9);
12499    }
12500
12501    #[test]
12502    fn family_label_anchors_share_rendered_geometry() {
12503        let mut bar = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12504        let bar_plot = &mut bar.chart.plot_area.plots_mut().unwrap()[0];
12505        let Plot::Bar { series, .. } = bar_plot else {
12506            panic!("expected bar plot");
12507        };
12508        let mut second = series[0].clone();
12509        second.index = 1;
12510        second.order = 1;
12511        series.push(second);
12512        let anchors = super::plot_label_anchors(
12513            bar_plot,
12514            chart_bounds(),
12515            DispBlanksAs::Gap,
12516            super::GeometryOptions::default(),
12517        )
12518        .unwrap();
12519        assert_point_near(anchors[&(0, 0)].center, Point { x: 40.0, y: 105.0 });
12520        assert_point_near(anchors[&(1, 0)].center, Point { x: 60.0, y: 105.0 });
12521
12522        let pie = CT_ChartSpace::from_xml(
12523            chart_with_optional_axes(
12524                &remaining_plot_fixtures()
12525                    .into_iter()
12526                    .find(|(kind, _, _)| *kind == "pieChart")
12527                    .unwrap()
12528                    .1,
12529                false,
12530            )
12531            .as_bytes(),
12532        )
12533        .unwrap();
12534        let Plot::Pie {
12535            first_slice_angle,
12536            series,
12537            ..
12538        } = &pie.chart.plot_area.plots().unwrap()[0]
12539        else {
12540            panic!("expected pie plot");
12541        };
12542        let slices = super::pie_slices(*first_slice_angle, None, series, chart_bounds()).unwrap();
12543        assert_eq!(slices.len(), 2);
12544        let first_span = std::f64::consts::TAU / 3.0;
12545        let first_midpoint = f64::from(*first_slice_angle).to_radians()
12546            - std::f64::consts::FRAC_PI_2
12547            + first_span / 2.0;
12548        assert_point_near(
12549            slices[0].anchor,
12550            super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, first_midpoint),
12551        );
12552        let second_midpoint = first_midpoint + std::f64::consts::PI;
12553        assert_point_near(
12554            slices[1].anchor,
12555            super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, second_midpoint),
12556        );
12557
12558        let radar = format!(
12559            r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12560            plot_series(0)
12561        );
12562        let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
12563        let Plot::Radar { series, .. } = &radar.chart.plot_area.plots().unwrap()[0] else {
12564            panic!("expected radar plot");
12565        };
12566        let radar_points =
12567            super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
12568                .unwrap();
12569        assert_eq!(radar_points[0][0], (0, Point { x: 100.0, y: 35.0 }));
12570        assert_eq!(radar_points[0][1], (1, Point { x: 100.0, y: 140.0 }));
12571    }
12572
12573    #[test]
12574    fn radar_annotations_use_spokes_perimeter_labels_and_radial_gridlines() {
12575        let radar_plot = format!(
12576            r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12577            sparse_category_series()
12578        );
12579        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12580        for axis in &mut chart.chart.plot_area.axes {
12581            axis.major_tick_mark = TickMark::Cross;
12582            if axis.kind == AxisKind::Value {
12583                axis.major_gridlines = Some(super::ChartLines::default());
12584            }
12585        }
12586        let plot = &chart.chart.plot_area.plots().unwrap()[0];
12587        let axes = chart.chart.plot_area.axes().unwrap();
12588        let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
12589        let center = Point { x: 112.0, y: 62.0 };
12590        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12591        let annotations =
12592            super::render_axes_and_labels(plot, &axes, bounds, DispBlanksAs::Gap, &mut fonts)
12593                .unwrap();
12594        assert_eq!(annotations.axis_lines.len(), 4);
12595        for spoke in annotations.axis_lines.iter().take(3) {
12596            let PositionedElement::Path(spoke) = spoke else {
12597                panic!("radar spoke must be a path");
12598            };
12599            assert_eq!(
12600                spoke.path.commands.first(),
12601                Some(&PathCommand::MoveTo(center))
12602            );
12603        }
12604        assert!(!annotations.gridlines.is_empty());
12605        for gridline in &annotations.gridlines {
12606            let PositionedElement::Path(gridline) = gridline else {
12607                panic!("radar gridline must be a path");
12608            };
12609            assert_eq!(gridline.path.commands.last(), Some(&PathCommand::Close));
12610            let radii: Vec<_> = gridline
12611                .path
12612                .commands
12613                .iter()
12614                .filter_map(|command| match command {
12615                    PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
12616                        Some((point.x - center.x).hypot(point.y - center.y))
12617                    }
12618                    _ => None,
12619                })
12620                .collect();
12621            assert_eq!(radii.len(), 3);
12622            assert!(
12623                radii
12624                    .windows(2)
12625                    .all(|pair| (pair[0] - pair[1]).abs() < 1.0e-9)
12626            );
12627        }
12628        let text: Vec<_> = annotations
12629            .labels
12630            .iter()
12631            .filter_map(|element| match element {
12632                PositionedElement::Text(run) => Some(run.text.as_str()),
12633                _ => None,
12634            })
12635            .collect();
12636        assert!(text.contains(&"North"));
12637        assert!(text.contains(&"West"));
12638    }
12639
12640    #[test]
12641    fn radar_tick_label_positions_are_distinct() {
12642        let radar_plot = format!(
12643            r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12644            plot_series(0)
12645        );
12646        let chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12647        let plot = &chart.chart.plot_area.plots().unwrap()[0];
12648        let axes = chart.chart.plot_area.axes().unwrap();
12649        let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
12650        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12651
12652        let category_axis = axes
12653            .iter()
12654            .find(|axis| axis.kind == AxisKind::Category)
12655            .unwrap();
12656        for (position, expected) in [
12657            (TickLabelPosition::High, Point { x: 108.0, y: 0.0 }),
12658            (TickLabelPosition::Low, Point { x: 108.0, y: 25.0 }),
12659            (TickLabelPosition::NextTo, Point { x: 108.0, y: 5.0 }),
12660        ] {
12661            let mut axis = category_axis.clone();
12662            axis.tick_label_position = position;
12663            let annotations =
12664                super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
12665                    .unwrap();
12666            let north = annotations
12667                .labels
12668                .iter()
12669                .find_map(|element| match element {
12670                    PositionedElement::Text(run) if run.text == "North" => Some(run),
12671                    _ => None,
12672                })
12673                .unwrap();
12674            assert_point_near(north.origin, expected);
12675        }
12676        let mut hidden_category = category_axis.clone();
12677        hidden_category.tick_label_position = TickLabelPosition::None;
12678        let annotations = super::render_axes_and_labels(
12679            plot,
12680            &[hidden_category],
12681            bounds,
12682            DispBlanksAs::Gap,
12683            &mut fonts,
12684        )
12685        .unwrap();
12686        assert!(annotations.labels.is_empty());
12687
12688        let narrow_bounds = Rect {
12689            x: 36.0,
12690            y: 12.0,
12691            width: 24.0,
12692            height: 100.0,
12693        };
12694        let narrow_center = Point { x: 48.0, y: 62.0 };
12695        assert_eq!(
12696            super::radar_category_label_origin(
12697                narrow_bounds,
12698                narrow_center,
12699                12.0,
12700                1,
12701                4,
12702                TickLabelPosition::High,
12703            ),
12704            Some(Point { x: 72.0, y: 65.0 })
12705        );
12706        assert_eq!(
12707            super::radar_category_label_origin(
12708                narrow_bounds,
12709                narrow_center,
12710                12.0,
12711                1,
12712                4,
12713                TickLabelPosition::NextTo,
12714            ),
12715            Some(Point { x: 66.0, y: 65.0 })
12716        );
12717
12718        let value_axis = axes
12719            .iter()
12720            .find(|axis| axis.kind == AxisKind::Value)
12721            .unwrap();
12722        for (position, expected) in [
12723            (TickLabelPosition::High, Point { x: 166.0, y: 65.0 }),
12724            (TickLabelPosition::Low, Point { x: 32.0, y: 65.0 }),
12725            (TickLabelPosition::NextTo, Point { x: 116.0, y: 65.0 }),
12726        ] {
12727            let mut axis = value_axis.clone();
12728            axis.tick_label_position = position;
12729            let annotations =
12730                super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
12731                    .unwrap();
12732            let zero = annotations
12733                .labels
12734                .iter()
12735                .find_map(|element| match element {
12736                    PositionedElement::Text(run) if run.text == "0" => Some(run),
12737                    _ => None,
12738                })
12739                .unwrap();
12740            assert_point_near(zero.origin, expected);
12741        }
12742        let mut hidden_value = value_axis.clone();
12743        hidden_value.tick_label_position = TickLabelPosition::None;
12744        let annotations = super::render_axes_and_labels(
12745            plot,
12746            &[hidden_value],
12747            bounds,
12748            DispBlanksAs::Gap,
12749            &mut fonts,
12750        )
12751        .unwrap();
12752        assert!(annotations.labels.is_empty());
12753    }
12754
12755    #[test]
12756    fn radar_negative_and_mixed_sign_domains_preserve_values() {
12757        let radar_plot = format!(
12758            r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12759            plot_series(0)
12760        );
12761        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12762        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12763        let Plot::Radar { series, .. } = plot else {
12764            panic!("expected radar plot");
12765        };
12766        series[0].values.values = vec![-10.0, -5.0];
12767        let points =
12768            super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
12769                .unwrap();
12770        assert_eq!(points[0][0], (0, Point { x: 100.0, y: 70.0 }));
12771        assert_eq!(points[0][1], (1, Point { x: 100.0, y: 105.0 }));
12772
12773        series[0].values.values = vec![-5.0, 10.0];
12774        let options = super::GeometryOptions {
12775            value: Some(super::AxisMapping {
12776                domain: Domain {
12777                    min: -10.0,
12778                    max: 10.0,
12779                },
12780                orientation: Orientation::MinMax,
12781            }),
12782            ..super::GeometryOptions::default()
12783        };
12784        let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
12785        assert_eq!(points[0][0], (0, Point { x: 100.0, y: 52.5 }));
12786        assert_eq!(points[0][1], (1, Point { x: 100.0, y: 140.0 }));
12787    }
12788
12789    #[test]
12790    fn radar_explicit_bounds_suppress_out_of_range_points_and_labels() {
12791        let radar_plot = format!(
12792            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>"#,
12793            plot_series(0)
12794        );
12795        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12796        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12797        let Plot::Radar { series, .. } = plot else {
12798            panic!("expected radar plot");
12799        };
12800        series[0].values.values = vec![25.0, 75.0];
12801        let options = super::GeometryOptions {
12802            value: Some(super::AxisMapping {
12803                domain: Domain {
12804                    min: 50.0,
12805                    max: 100.0,
12806                },
12807                orientation: Orientation::MinMax,
12808            }),
12809            ..super::GeometryOptions::default()
12810        };
12811        let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
12812        assert_eq!(points[0], vec![(1, Point { x: 100.0, y: 105.0 })]);
12813        let anchors =
12814            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12815        assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 1)]);
12816
12817        let Plot::Radar { series, .. } = plot else {
12818            panic!("expected radar plot");
12819        };
12820        series[0].values.values = vec![75.0, 125.0];
12821        let reversed = super::GeometryOptions {
12822            value: Some(super::AxisMapping {
12823                domain: Domain {
12824                    min: 50.0,
12825                    max: 100.0,
12826                },
12827                orientation: Orientation::MaxMin,
12828            }),
12829            ..super::GeometryOptions::default()
12830        };
12831        let points = super::radar_series_points(series, chart_bounds(), reversed).unwrap();
12832        assert_eq!(points[0], vec![(0, Point { x: 100.0, y: 35.0 })]);
12833        let anchors =
12834            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
12835        assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
12836
12837        let value_axis = chart
12838            .chart
12839            .plot_area
12840            .axes
12841            .iter_mut()
12842            .find(|axis| axis.kind == AxisKind::Value)
12843            .unwrap();
12844        value_axis.scaling.minimum = Some(50.0);
12845        value_axis.scaling.maximum = Some(100.0);
12846        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12847        let Plot::Radar { series, .. } = plot else {
12848            panic!("expected radar plot");
12849        };
12850        series[0].values.values = vec![25.0, 25.0];
12851        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12852        let rendered = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12853        assert!(rendered.children.iter().any(|element| matches!(
12854            element,
12855            PositionedElement::Group(group) if group.clip.is_some() && group.children.is_empty()
12856        )));
12857        assert!(
12858            rendered
12859                .children
12860                .iter()
12861                .any(|element| matches!(element, PositionedElement::Path(_)))
12862        );
12863        assert!(!rendered.children.iter().any(|element| matches!(
12864            element,
12865            PositionedElement::Text(run) if run.text == "25"
12866        )));
12867    }
12868
12869    #[test]
12870    fn inside_and_outside_label_positions_follow_family_geometry() {
12871        let mut horizontal =
12872            CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12873        let plot = &mut horizontal.chart.plot_area.plots_mut().unwrap()[0];
12874        let Plot::Bar { direction, .. } = plot else {
12875            panic!("expected bar plot");
12876        };
12877        *direction = BarDirection::Bar;
12878        let anchors = super::plot_label_anchors(
12879            plot,
12880            chart_bounds(),
12881            DispBlanksAs::Gap,
12882            super::GeometryOptions::default(),
12883        )
12884        .unwrap();
12885        let anchor = anchors[&(0, 0)];
12886        assert_point_near(
12887            anchor.origin(Some(DataLabelPosition::InsideBase)),
12888            Point { x: 5.0, y: 35.0 },
12889        );
12890        assert_point_near(
12891            anchor.origin(Some(DataLabelPosition::InsideEnd)),
12892            Point { x: 95.0, y: 35.0 },
12893        );
12894        assert_point_near(
12895            anchor.origin(Some(DataLabelPosition::OutsideEnd)),
12896            Point { x: 105.0, y: 35.0 },
12897        );
12898
12899        let mut negative =
12900            CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12901        let plot = &mut negative.chart.plot_area.plots_mut().unwrap()[0];
12902        let Plot::Bar { series, .. } = plot else {
12903            panic!("expected bar plot");
12904        };
12905        series[0].values.values = vec![-1.0, -2.0];
12906        let anchors = super::plot_label_anchors(
12907            plot,
12908            chart_bounds(),
12909            DispBlanksAs::Gap,
12910            super::GeometryOptions::default(),
12911        )
12912        .unwrap();
12913        assert_point_near(
12914            anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12915            Point { x: 50.0, y: 75.0 },
12916        );
12917        let reversed = super::GeometryOptions {
12918            value: Some(super::AxisMapping {
12919                domain: Domain {
12920                    min: -2.0,
12921                    max: 0.0,
12922                },
12923                orientation: Orientation::MaxMin,
12924            }),
12925            ..super::GeometryOptions::default()
12926        };
12927        let anchors =
12928            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
12929        assert_point_near(
12930            anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12931            Point { x: 50.0, y: 65.0 },
12932        );
12933
12934        let pie = CT_ChartSpace::from_xml(
12935            chart_with_optional_axes(
12936                &remaining_plot_fixtures()
12937                    .into_iter()
12938                    .find(|(kind, _, _)| *kind == "pieChart")
12939                    .unwrap()
12940                    .1,
12941                false,
12942            )
12943            .as_bytes(),
12944        )
12945        .unwrap();
12946        let Plot::Pie { series, .. } = &pie.chart.plot_area.plots().unwrap()[0] else {
12947            panic!("expected pie plot");
12948        };
12949        let pie_slice = &super::pie_slices(30, None, series, chart_bounds()).unwrap()[0];
12950        assert_point_near(
12951            super::LabelAnchor::segment(pie_slice.anchor, pie_slice.base, pie_slice.end)
12952                .origin(Some(DataLabelPosition::OutsideEnd)),
12953            Point { x: 175.0, y: 70.0 },
12954        );
12955        let doughnut_slice = &super::pie_slices(30, Some(60), series, chart_bounds()).unwrap()[0];
12956        assert_point_near(
12957            super::LabelAnchor::segment(
12958                doughnut_slice.anchor,
12959                doughnut_slice.base,
12960                doughnut_slice.end,
12961            )
12962            .origin(Some(DataLabelPosition::InsideBase)),
12963            Point { x: 147.0, y: 70.0 },
12964        );
12965
12966        let short = super::LabelAnchor::segment(
12967            Point { x: 2.0, y: 0.0 },
12968            Point { x: 0.0, y: 0.0 },
12969            Point { x: 4.0, y: 0.0 },
12970        );
12971        assert_eq!(
12972            short.origin(Some(DataLabelPosition::InsideBase)),
12973            Point { x: 2.0, y: 0.0 }
12974        );
12975        assert_eq!(
12976            short.origin(Some(DataLabelPosition::InsideEnd)),
12977            Point { x: 2.0, y: 0.0 }
12978        );
12979        let thin_bounds = Rect {
12980            x: 0.0,
12981            y: 0.0,
12982            width: 80.0,
12983            height: 80.0,
12984        };
12985        let thin = &super::pie_slices(30, Some(90), series, thin_bounds).unwrap()[0];
12986        let thin = super::LabelAnchor::segment(thin.anchor, thin.base, thin.end);
12987        assert_point_near(
12988            thin.origin(Some(DataLabelPosition::InsideBase)),
12989            Point { x: 78.0, y: 40.0 },
12990        );
12991        assert_point_near(
12992            thin.origin(Some(DataLabelPosition::InsideEnd)),
12993            Point { x: 78.0, y: 40.0 },
12994        );
12995    }
12996
12997    #[test]
12998    fn degenerate_bar_and_radar_anchors_preserve_family_direction() {
12999        let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13000        let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
13001        let Plot::Bar { series, .. } = plot else {
13002            panic!("expected bar plot");
13003        };
13004        series[0].values.values = vec![80.0, 125.0];
13005        for (direction, orientation, boundary, outside) in [
13006            (
13007                BarDirection::Column,
13008                Orientation::MinMax,
13009                Point { x: 50.0, y: 140.0 },
13010                Point { x: 50.0, y: 135.0 },
13011            ),
13012            (
13013                BarDirection::Column,
13014                Orientation::MaxMin,
13015                Point { x: 50.0, y: 0.0 },
13016                Point { x: 50.0, y: 5.0 },
13017            ),
13018            (
13019                BarDirection::Bar,
13020                Orientation::MinMax,
13021                Point { x: 0.0, y: 35.0 },
13022                Point { x: 5.0, y: 35.0 },
13023            ),
13024            (
13025                BarDirection::Bar,
13026                Orientation::MaxMin,
13027                Point { x: 200.0, y: 35.0 },
13028                Point { x: 195.0, y: 35.0 },
13029            ),
13030        ] {
13031            let Plot::Bar {
13032                direction: plot_direction,
13033                ..
13034            } = plot
13035            else {
13036                panic!("expected bar plot");
13037            };
13038            *plot_direction = direction;
13039            let options = super::GeometryOptions {
13040                value: Some(super::AxisMapping {
13041                    domain: Domain {
13042                        min: 80.0,
13043                        max: 100.0,
13044                    },
13045                    orientation,
13046                }),
13047                ..super::GeometryOptions::default()
13048            };
13049            let anchors =
13050                super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options)
13051                    .unwrap();
13052            let anchor = anchors[&(0, 0)];
13053            assert_point_near(anchor.center, boundary);
13054            assert_point_near(anchor.base, boundary);
13055            assert_point_near(anchor.end, boundary);
13056            assert_point_near(anchor.origin(Some(DataLabelPosition::InsideBase)), boundary);
13057            assert_point_near(anchor.origin(Some(DataLabelPosition::InsideEnd)), boundary);
13058            assert_point_near(anchor.origin(Some(DataLabelPosition::OutsideEnd)), outside);
13059        }
13060
13061        let radar_plot = format!(
13062            r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
13063            plot_series(0)
13064        );
13065        let mut radar = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
13066        let plot = &mut radar.chart.plot_area.plots_mut().unwrap()[0];
13067        let Plot::Radar { series, .. } = plot else {
13068            panic!("expected radar plot");
13069        };
13070        series[0].values.values = vec![0.0, 0.0, 0.0, 0.0];
13071        let anchors = super::plot_label_anchors(
13072            plot,
13073            chart_bounds(),
13074            DispBlanksAs::Gap,
13075            super::GeometryOptions::default(),
13076        )
13077        .unwrap();
13078        let center = Point { x: 100.0, y: 70.0 };
13079        for logical_index in 0..4 {
13080            assert_point_near(
13081                anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideBase)),
13082                center,
13083            );
13084            assert_point_near(
13085                anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideEnd)),
13086                center,
13087            );
13088        }
13089        assert_point_near(
13090            anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
13091            Point { x: 100.0, y: 65.0 },
13092        );
13093        assert_point_near(
13094            anchors[&(0, 1)].origin(Some(DataLabelPosition::OutsideEnd)),
13095            Point { x: 105.0, y: 70.0 },
13096        );
13097        assert_point_near(
13098            anchors[&(0, 2)].origin(Some(DataLabelPosition::OutsideEnd)),
13099            Point { x: 100.0, y: 75.0 },
13100        );
13101        assert_point_near(
13102            anchors[&(0, 3)].origin(Some(DataLabelPosition::OutsideEnd)),
13103            Point { x: 95.0, y: 70.0 },
13104        );
13105    }
13106
13107    #[test]
13108    fn labels_outside_explicit_axis_bounds_are_suppressed() {
13109        let mut chart =
13110            CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
13111        {
13112            let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13113            let Plot::Line { series, .. } = plot else {
13114                panic!("expected line plot");
13115            };
13116            series[0].values.values = vec![25.0, 75.0];
13117        }
13118        let value_axis = chart
13119            .chart
13120            .plot_area
13121            .axes
13122            .iter_mut()
13123            .find(|axis| axis.kind == AxisKind::Value)
13124            .unwrap();
13125        value_axis.scaling.minimum = Some(50.0);
13126        value_axis.scaling.maximum = Some(100.0);
13127        value_axis.scaling.orientation = Orientation::MaxMin;
13128        let plot = &chart.chart.plot_area.plots().unwrap()[0];
13129        let axes = chart.chart.plot_area.axes().unwrap();
13130        let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
13131        let anchors =
13132            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
13133        assert!(!anchors.contains_key(&(0, 0)));
13134        assert_point_near(anchors[&(0, 1)].center, Point { x: 150.0, y: 70.0 });
13135
13136        let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13137        let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
13138        let Plot::Bar { series, .. } = plot else {
13139            panic!("expected bar plot");
13140        };
13141        series[0].values.values = vec![90.0, 125.0];
13142        let options = super::GeometryOptions {
13143            value: Some(super::AxisMapping {
13144                domain: Domain {
13145                    min: 80.0,
13146                    max: 100.0,
13147                },
13148                orientation: Orientation::MinMax,
13149            }),
13150            ..super::GeometryOptions::default()
13151        };
13152        let anchors =
13153            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
13154        assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
13155        assert_point_near(anchors[&(0, 0)].base, Point { x: 50.0, y: 140.0 });
13156        assert_point_near(anchors[&(0, 0)].end, Point { x: 50.0, y: 70.0 });
13157        assert_point_near(anchors[&(0, 0)].center, Point { x: 50.0, y: 105.0 });
13158        assert_point_near(
13159            anchors[&(0, 0)].origin(Some(DataLabelPosition::InsideBase)),
13160            Point { x: 50.0, y: 135.0 },
13161        );
13162    }
13163
13164    #[test]
13165    fn zero_bars_render_and_emit_requested_value_labels() {
13166        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13167        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13168        let Plot::Bar { series, .. } = plot else {
13169            panic!("expected bar plot");
13170        };
13171        series[0].values.values = vec![0.0, 0.0];
13172        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
13173        assert_eq!(path_commands(&geometry).len(), 2);
13174        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13175        render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13176        let plot = &chart.chart.plot_area.plots().unwrap()[0];
13177        let mut labels = Vec::new();
13178        super::render_data_labels(
13179            plot,
13180            chart_bounds(),
13181            DispBlanksAs::Gap,
13182            super::GeometryOptions::default(),
13183            &mut fonts,
13184            &mut labels,
13185        )
13186        .unwrap();
13187        let text: Vec<_> = labels
13188            .iter()
13189            .filter_map(|element| match element {
13190                PositionedElement::Text(run) => Some(run.text.as_str()),
13191                _ => None,
13192            })
13193            .collect();
13194        assert_eq!(text, vec!["0", "0"]);
13195    }
13196
13197    #[test]
13198    fn axes_gridlines_and_tick_marks_follow_model_state() {
13199        let chart = labelled_bar_chart();
13200        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13201        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13202        let ranks: Vec<_> = group
13203            .children
13204            .iter()
13205            .map(|element| match element {
13206                PositionedElement::Group(group) => {
13207                    assert!(group.clip.is_some());
13208                    1
13209                }
13210                PositionedElement::Path(path)
13211                    if path
13212                        .stroke
13213                        .as_ref()
13214                        .is_some_and(|stroke| stroke.width == 0.5) =>
13215                {
13216                    0
13217                }
13218                PositionedElement::Path(path) if path.fill.is_some() => 4,
13219                PositionedElement::Path(path) => {
13220                    let bounds = path.path.bounds().expect("axis or tick bounds");
13221                    assert_eq!(path.stroke.as_ref().map(|stroke| stroke.width), Some(1.0));
13222                    if bounds.width > 4.0 || bounds.height > 4.0 {
13223                        2
13224                    } else {
13225                        3
13226                    }
13227                }
13228                PositionedElement::Text(_) => 5,
13229                _ => panic!("unexpected labelled chart element"),
13230            })
13231            .collect();
13232        assert!(ranks.windows(2).all(|pair| pair[0] <= pair[1]), "{ranks:?}");
13233        for required in 0..=5 {
13234            assert!(
13235                ranks.contains(&required),
13236                "missing z-order rank {required}: {ranks:?}"
13237            );
13238        }
13239        assert_eq!(ranks.iter().filter(|rank| **rank == 1).count(), 1);
13240        assert_eq!(ranks.iter().filter(|rank| **rank == 2).count(), 2);
13241
13242        let mut deleted = chart.clone();
13243        for axis in &mut deleted.chart.plot_area.axes {
13244            axis.deleted = true;
13245        }
13246        let deleted_group = render_chart(&deleted.chart, chart_bounds(), &mut fonts).unwrap();
13247        assert_eq!(
13248            deleted_group
13249                .children
13250                .iter()
13251                .filter(|element| {
13252                    matches!(
13253                        element,
13254                        PositionedElement::Path(path) if path.stroke.is_some()
13255                    )
13256                })
13257                .count(),
13258            0
13259        );
13260    }
13261
13262    #[test]
13263    fn category_ticks_cover_unlabelled_and_sparse_logical_slots() {
13264        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13265        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
13266            panic!("expected bar plot");
13267        };
13268        series[0].categories = None;
13269        let plot = &chart.chart.plot_area.plots().unwrap()[0];
13270        let mut category_axis = chart
13271            .chart
13272            .plot_area
13273            .axes()
13274            .unwrap()
13275            .into_iter()
13276            .find(|axis| axis.kind == AxisKind::Category)
13277            .unwrap();
13278        category_axis.major_gridlines = Some(super::ChartLines::default());
13279        category_axis.major_tick_mark = TickMark::Cross;
13280        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13281        let annotations = super::render_axes_and_labels(
13282            plot,
13283            &[category_axis],
13284            chart_bounds(),
13285            DispBlanksAs::Gap,
13286            &mut fonts,
13287        )
13288        .unwrap();
13289        assert_eq!(annotations.gridlines.len(), 2);
13290        assert_eq!(annotations.ticks.len(), 2);
13291        assert!(annotations.labels.is_empty());
13292
13293        let sparse_plot = format!(
13294            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>"#,
13295            sparse_category_series()
13296        );
13297        let sparse = CT_ChartSpace::from_xml(chart_with_plot(&sparse_plot).as_bytes()).unwrap();
13298        let plot = &sparse.chart.plot_area.plots().unwrap()[0];
13299        let mut category_axis = sparse
13300            .chart
13301            .plot_area
13302            .axes()
13303            .unwrap()
13304            .into_iter()
13305            .find(|axis| axis.kind == AxisKind::Category)
13306            .unwrap();
13307        category_axis.major_gridlines = Some(super::ChartLines::default());
13308        category_axis.major_tick_mark = TickMark::Cross;
13309        let annotations = super::render_axes_and_labels(
13310            plot,
13311            &[category_axis],
13312            chart_bounds(),
13313            DispBlanksAs::Gap,
13314            &mut fonts,
13315        )
13316        .unwrap();
13317        assert_eq!(annotations.gridlines.len(), 3);
13318        assert_eq!(annotations.ticks.len(), 3);
13319        assert_eq!(annotations.labels.len(), 2);
13320
13321        let oversized_plot = sparse_plot.replacen(
13322            r#"<q:ptCount val="3"/>"#,
13323            r#"<q:ptCount val="4294967295"/>"#,
13324            1,
13325        );
13326        let oversized =
13327            CT_ChartSpace::from_xml(chart_with_plot(&oversized_plot).as_bytes()).unwrap();
13328        let plot = &oversized.chart.plot_area.plots().unwrap()[0];
13329        let category_axis = oversized
13330            .chart
13331            .plot_area
13332            .axes()
13333            .unwrap()
13334            .into_iter()
13335            .find(|axis| axis.kind == AxisKind::Category)
13336            .unwrap();
13337        let result = super::render_axes_and_labels(
13338            plot,
13339            &[category_axis],
13340            chart_bounds(),
13341            DispBlanksAs::Gap,
13342            &mut fonts,
13343        );
13344        let error = match result {
13345            Err(error) => error,
13346            Ok(_) => panic!("oversized category annotations rendered"),
13347        };
13348        assert!(
13349            error
13350                .to_string()
13351                .contains("logical category count 4294967295")
13352        );
13353        assert!(error.to_string().contains("exceeds 16384"));
13354    }
13355
13356    #[test]
13357    fn unsupported_numeric_category_formats_return_projection_errors() {
13358        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13359        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
13360            panic!("expected bar plot");
13361        };
13362        series[0].categories = Some(AxisData::Numeric(
13363            NumericData::new(
13364                "Sheet1!$A$2:$A$3".to_owned(),
13365                "General".to_owned(),
13366                vec![1.0, 2.0],
13367            )
13368            .unwrap(),
13369        ));
13370        let plot = &chart.chart.plot_area.plots().unwrap()[0];
13371        let mut category_axis = chart
13372            .chart
13373            .plot_area
13374            .axes()
13375            .unwrap()
13376            .into_iter()
13377            .find(|axis| axis.kind == AxisKind::Category)
13378            .unwrap();
13379        category_axis.number_format = Some(NumberFormat::new("0.00".to_owned(), false).unwrap());
13380        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13381        let annotations = super::render_axes_and_labels(
13382            plot,
13383            &[category_axis.clone()],
13384            chart_bounds(),
13385            DispBlanksAs::Gap,
13386            &mut fonts,
13387        )
13388        .unwrap();
13389        let text: Vec<_> = annotations
13390            .labels
13391            .iter()
13392            .filter_map(|element| match element {
13393                PositionedElement::Text(run) => Some(run.text.as_str()),
13394                _ => None,
13395            })
13396            .collect();
13397        assert_eq!(text, vec!["1.00", "2.00"]);
13398
13399        category_axis.number_format = Some(NumberFormat::new("#,##0".to_owned(), false).unwrap());
13400        let result = super::render_axes_and_labels(
13401            plot,
13402            &[category_axis],
13403            chart_bounds(),
13404            DispBlanksAs::Gap,
13405            &mut fonts,
13406        );
13407        let error = match result {
13408            Err(error) => error,
13409            Ok(_) => panic!("unsupported category-axis format rendered"),
13410        };
13411        assert!(
13412            error
13413                .to_string()
13414                .contains("c:numFmt/@formatCode projection")
13415        );
13416        assert!(error.to_string().contains("#,##0"));
13417    }
13418
13419    #[test]
13420    fn labels_and_legend_shape_with_deterministic_fonts() {
13421        let chart = labelled_bar_chart();
13422        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13423        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13424        let labels: Vec<_> = group
13425            .children
13426            .iter()
13427            .filter_map(|element| match element {
13428                PositionedElement::Text(run) => Some(run),
13429                _ => None,
13430            })
13431            .collect();
13432        for text in ["North", "South", "20", "Sales", "100"] {
13433            assert!(labels.iter().any(|run| run.text == text), "missing {text}");
13434        }
13435        let styled_tick = labels.iter().find(|run| run.text == "20").unwrap();
13436        assert_eq!(styled_tick.font_size, 12.0);
13437        assert!(styled_tick.bold);
13438        assert!(
13439            labels
13440                .iter()
13441                .all(|run| !run.glyph_ids.is_empty() && run.glyph_ids.len() == run.advances.len())
13442        );
13443    }
13444
13445    #[test]
13446    fn labelled_chart_raster_is_deterministic() {
13447        let raster = || {
13448            let chart = labelled_bar_chart();
13449            let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13450            let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13451            let layout = LayoutResult::new(
13452                vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
13453                fonts.all_font_data(),
13454                None,
13455                Vec::new(),
13456            );
13457            oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
13458        };
13459        assert_eq!(raster(), raster());
13460    }
13461
13462    fn labelled_bar_chart() -> CT_ChartSpace {
13463        let series = plot_series(0)
13464            .replace(
13465                "<q:cat>",
13466                "<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>",
13467            )
13468            .replace("<q:v>1</q:v>", "<q:v>0</q:v>")
13469            .replace("<q:v>2</q:v>", "<q:v>100</q:v>");
13470        let plot = format!(
13471            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>"#
13472        );
13473        let xml = chart_with_plot(&plot)
13474            .replacen(
13475                "<q:scaling/>",
13476                "<q:scaling><q:orientation val=\"minMax\"/></q:scaling>",
13477                1,
13478            )
13479            .replacen(
13480                "<q:scaling/>",
13481                "<q:scaling><q:max val=\"100\"/><q:min val=\"0\"/></q:scaling>",
13482                1,
13483            )
13484            .replace(
13485                "<q:axPos val=\"b\"/>",
13486                "<q:axPos val=\"b\"/><q:majorTickMark val=\"cross\"/>",
13487            )
13488            .replace(
13489                "<q:axPos val=\"l\"/>",
13490                "<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>",
13491            )
13492            .replace("</q:plotArea></q:chart>", "</q:plotArea><q:legend/></q:chart>");
13493        CT_ChartSpace::from_xml(xml.as_bytes()).unwrap()
13494    }
13495
13496    fn chart_bounds() -> Rect {
13497        Rect {
13498            x: 0.0,
13499            y: 0.0,
13500            width: 200.0,
13501            height: 140.0,
13502        }
13503    }
13504
13505    fn four_series_bar_chart() -> CT_ChartSpace {
13506        let series = (0..4).map(plot_series).collect::<String>();
13507        let plot = bar_plot("").replace(&plot_series(0), &series);
13508        CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
13509    }
13510
13511    fn seven_series_bar_chart() -> CT_ChartSpace {
13512        let series = (0..7).map(plot_series).collect::<String>();
13513        let plot = bar_plot("").replace(&plot_series(0), &series);
13514        CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
13515    }
13516
13517    fn path_colours(geometry: &ChartGeometry) -> Vec<Color> {
13518        let mut colours = geometry_children(geometry)
13519            .iter()
13520            .filter_map(|element| match element {
13521                PositionedElement::Path(path) => match path
13522                    .fill
13523                    .as_ref()
13524                    .or_else(|| path.stroke.as_ref().map(|stroke| &stroke.paint))
13525                {
13526                    Some(Paint::Solid(colour)) => Some(*colour),
13527                    _ => None,
13528                },
13529                _ => None,
13530            })
13531            .collect::<Vec<_>>();
13532        colours.dedup_by(|left, right| left == right);
13533        colours
13534    }
13535
13536    fn resolved_layout_colour(colour: oxml_drawing::color::ResolvedColor) -> Color {
13537        Color {
13538            r: f64::from(colour.red) / 255.0,
13539            g: f64::from(colour.green) / 255.0,
13540            b: f64::from(colour.blue) / 255.0,
13541            a: f64::from(colour.alpha) / 255.0,
13542        }
13543    }
13544
13545    fn geometry_children(geometry: &ChartGeometry) -> &[PositionedElement] {
13546        let [PositionedElement::Group(group)] = geometry.elements.as_slice() else {
13547            panic!("chart geometry must contain one group");
13548        };
13549        &group.children
13550    }
13551
13552    fn path_commands(geometry: &ChartGeometry) -> Vec<&[PathCommand]> {
13553        geometry_children(geometry)
13554            .iter()
13555            .map(|element| {
13556                let PositionedElement::Path(path) = element else {
13557                    panic!("chart geometry child must be a path");
13558                };
13559                path.path.commands.as_slice()
13560            })
13561            .collect()
13562    }
13563
13564    fn path_bounds(geometry: &ChartGeometry) -> Vec<Rect> {
13565        geometry_children(geometry)
13566            .iter()
13567            .map(|element| {
13568                let PositionedElement::Path(path) = element else {
13569                    panic!("chart geometry child must be a path");
13570                };
13571                path.path.bounds().expect("chart path has bounds")
13572            })
13573            .collect()
13574    }
13575
13576    fn assert_rect_near(actual: Rect, expected: Rect) {
13577        assert_near(actual.x, expected.x);
13578        assert_near(actual.y, expected.y);
13579        assert_near(actual.width, expected.width);
13580        assert_near(actual.height, expected.height);
13581    }
13582
13583    fn assert_rects_near(actual: &[Rect], expected: &[Rect]) {
13584        assert_eq!(actual.len(), expected.len());
13585        for (actual, expected) in actual.iter().copied().zip(expected.iter().copied()) {
13586            assert_rect_near(actual, expected);
13587        }
13588    }
13589
13590    fn assert_path_points(commands: &[PathCommand], expected: &[Point], closed: bool) {
13591        let points: Vec<_> = commands
13592            .iter()
13593            .filter_map(|command| match command {
13594                PathCommand::MoveTo(point) | PathCommand::LineTo(point) => Some(*point),
13595                PathCommand::Close => None,
13596                PathCommand::CurveTo { .. } => panic!("expected polygonal path"),
13597            })
13598            .collect();
13599        assert_eq!(points.len(), expected.len());
13600        for (actual, expected) in points.into_iter().zip(expected.iter().copied()) {
13601            assert_point_near(actual, expected);
13602        }
13603        assert_eq!(commands.last() == Some(&PathCommand::Close), closed);
13604    }
13605
13606    fn assert_point_near(actual: Point, expected: Point) {
13607        assert_near(actual.x, expected.x);
13608        assert_near(actual.y, expected.y);
13609    }
13610
13611    fn assert_near(actual: f64, expected: f64) {
13612        assert!(
13613            (actual - expected).abs() < 1e-9,
13614            "expected {expected}, got {actual}"
13615        );
13616    }
13617
13618    #[test]
13619    fn remaining_v1_plots_round_trip_and_render() {
13620        for (kind, plot, axes) in remaining_plot_fixtures() {
13621            let xml = chart_with_optional_axes(&plot, axes);
13622            let parsed = CT_ChartSpace::from_xml(xml.as_bytes())
13623                .unwrap_or_else(|error| panic!("{kind}: parse failed: {error}"));
13624            assert_eq!(parsed.chart.plot_area.plots().unwrap().len(), 1);
13625            assert_eq!(
13626                parsed.chart.plot_area.axes().unwrap().len(),
13627                usize::from(axes) * 2
13628            );
13629            let written = parsed
13630                .to_xml()
13631                .unwrap_or_else(|error| panic!("{kind}: write failed: {error}"));
13632            assert_eq!(
13633                parsed,
13634                CT_ChartSpace::from_xml(&written)
13635                    .unwrap_or_else(|error| panic!("{kind}: reparse failed: {error}"))
13636            );
13637        }
13638        if let Some(corpus) = require_or_skip_corpus() {
13639            verify_remaining_plot_viewer_gate(&corpus);
13640        }
13641    }
13642
13643    #[test]
13644    fn remaining_plot_families_write_fixed_prefixes_in_schema_order() {
13645        for (kind, plot, axes) in remaining_plot_fixtures() {
13646            let parsed =
13647                CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
13648            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13649            assert!(written.contains(&format!("<c:{kind}")));
13650            assert!(!written.contains(&format!("<q:{kind}")));
13651            let series = written.find("<c:ser").unwrap();
13652            let labels = written.find("<c:dLbls").unwrap();
13653            assert!(series < labels, "{kind}: series must precede labels");
13654            if matches!(kind, "areaChart" | "scatterChart" | "radarChart") {
13655                let property = match kind {
13656                    "areaChart" => "<c:grouping",
13657                    "scatterChart" => "<c:scatterStyle",
13658                    "radarChart" => "<c:radarStyle",
13659                    _ => unreachable!(),
13660                };
13661                assert!(written.find(property).unwrap() < series);
13662                assert!(labels < written.find("<c:axId").unwrap());
13663            } else {
13664                assert!(labels < written.find("<c:firstSliceAng").unwrap());
13665            }
13666            if kind == "doughnutChart" {
13667                assert!(
13668                    written.find("<c:firstSliceAng").unwrap()
13669                        < written.find("<c:holeSize").unwrap()
13670                );
13671            }
13672        }
13673    }
13674
13675    #[test]
13676    fn scatter_series_map_numeric_categories_and_values_to_x_and_y() {
13677        let plot = remaining_plot_fixtures()
13678            .into_iter()
13679            .find(|(kind, _, _)| *kind == "scatterChart")
13680            .unwrap()
13681            .1;
13682        let mut parsed =
13683            CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, true).as_bytes()).unwrap();
13684        let standalone_series = {
13685            let Plot::Scatter { series, .. } = &mut parsed.chart.plot_area.plots_mut().unwrap()[0]
13686            else {
13687                panic!("expected scatter plot");
13688            };
13689            let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
13690                panic!("expected numeric x values");
13691            };
13692            x_values.values[0] = 3.5;
13693            series[0].values.values[0] = 7.5;
13694            series[0].clone()
13695        };
13696        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13697        assert!(written.contains("<c:xVal>"));
13698        assert!(written.contains("<c:yVal>"));
13699        assert!(!written.contains("<c:cat>"));
13700        assert!(!written.contains("<c:val>"));
13701        assert!(written.contains("<c:v>3.5</c:v>"));
13702        assert!(written.contains("<c:v>7.5</c:v>"));
13703        let standalone = String::from_utf8(standalone_series.to_xml().unwrap()).unwrap();
13704        assert!(standalone.contains("<c:xVal>"));
13705        assert!(standalone.contains("<c:yVal>"));
13706        assert!(!standalone.contains("<c:cat>"));
13707        assert!(!standalone.contains("<c:val>"));
13708    }
13709
13710    #[test]
13711    fn malformed_remaining_plots_return_errors_without_panicking() {
13712        let series = plot_series(0);
13713        let scatter = numeric_plot_series(0);
13714        let opaque_bubble = series.replace(
13715            "</q:val></q:ser>",
13716            "</q:val><q:bubbleSize><x:opaque/></q:bubbleSize></q:ser>",
13717        );
13718        let cases = [
13719            r#"<q:pieChart/>"#.to_owned(),
13720            format!(r#"<q:pieChart>{series}<q:firstSliceAng val="361"/></q:pieChart>"#),
13721            format!(
13722                r#"<q:pieChart>{series}<q:firstSliceAng val="1"/><q:firstSliceAng val="2"/></q:pieChart>"#
13723            ),
13724            format!(r#"<q:doughnutChart>{series}<q:holeSize val="9"/></q:doughnutChart>"#),
13725            format!(
13726                r#"<q:doughnutChart>{series}<q:holeSize val="50"/><q:holeSize val="60"/></q:doughnutChart>"#
13727            ),
13728            format!(
13729                r#"<q:areaChart><q:grouping val="clustered"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
13730            ),
13731            format!(
13732                r#"<q:areaChart><q:grouping val="standard"/><q:grouping val="stacked"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
13733            ),
13734            format!(
13735                r#"<q:scatterChart><q:scatterStyle val="dots"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13736            ),
13737            format!(
13738                r#"<q:scatterChart><q:scatterStyle val="marker"/><q:scatterStyle val="line"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13739            ),
13740            format!(
13741                r#"<q:radarChart><q:radarStyle val="smooth"/>{series}<q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
13742            ),
13743            format!(
13744                r#"<q:radarChart><q:radarStyle val="marker"/>{series}<q:dLbls/><q:dLbls/><q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
13745            ),
13746            format!(r#"<q:pieChart>{series}<q:axId val="1"/></q:pieChart>"#),
13747            format!(
13748                r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/></q:areaChart>"#
13749            ),
13750            r#"<q:areaChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:areaChart>"#.to_owned(),
13751            format!(
13752                r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/><q:axId val="2"/><q:axId val="3"/></q:areaChart>"#
13753            ),
13754            format!(
13755                r#"<q:scatterChart><q:scatterStyle val="marker"/>{series}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13756            ),
13757            format!(
13758                r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13759                scatter
13760                    .replace("<q:yVal>", "<q:val>")
13761                    .replace("</q:yVal>", "</q:val>")
13762            ),
13763            format!(
13764                r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13765                scatter_without_x(0)
13766            ),
13767            format!(
13768                r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13769                scatter_without_y(0)
13770            ),
13771            format!(r#"<q:pieChart>{opaque_bubble}</q:pieChart>"#),
13772        ];
13773        for plot in cases {
13774            let xml = chart_with_optional_axes(&plot, false);
13775            let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
13776            assert!(result.is_ok(), "parser panicked for {plot}");
13777            assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
13778        }
13779
13780        let missing_axis = format!(
13781            r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="7"/></q:areaChart>"#
13782        );
13783        let missing_axis =
13784            CT_ChartSpace::from_xml(chart_with_optional_axes(&missing_axis, true).as_bytes())
13785                .unwrap_err();
13786        assert!(missing_axis.to_string().contains("missing axis 7"));
13787
13788        let values =
13789            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
13790        let mut bubble_series = Series::new(0, 0, values.clone());
13791        bubble_series.bubble_size = Some(values);
13792        assert!(Plot::pie(vec![bubble_series]).is_err());
13793    }
13794
13795    #[test]
13796    fn unsupported_plot_families_and_children_remain_byte_preserved() {
13797        for raw in [
13798            r#"<q:pie3DChart x:keep="3d"><q:ser><x:opaque/></q:ser></q:pie3DChart>"#,
13799            r#"<q:ofPieChart x:keep="of"><x:opaque/></q:ofPieChart>"#,
13800            r#"<q:stockChart x:keep="stock"><x:opaque/></q:stockChart>"#,
13801            r#"<q:surfaceChart x:keep="surface"><x:opaque/></q:surfaceChart>"#,
13802            r#"<q:bubbleChart x:keep="bubble"><x:opaque/></q:bubbleChart>"#,
13803        ] {
13804            let parsed =
13805                CT_ChartSpace::from_xml(chart_with_optional_axes(raw, false).as_bytes()).unwrap();
13806            assert!(parsed.chart.plot_area.plots().is_err());
13807            let written = parsed.to_xml().unwrap();
13808            assert!(
13809                written
13810                    .windows(raw.len())
13811                    .any(|window| window == raw.as_bytes())
13812            );
13813        }
13814
13815        let pie = remaining_plot_fixtures()[0].1.clone();
13816        let preserved = r#"<!--point--><q:dPt x:id="1"><q:explosion val="7"/><x:tail/></q:dPt>"#;
13817        let plot = pie.replace("<q:dLbls>", &format!("{preserved}<q:dLbls>"));
13818        let parsed =
13819            CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, false).as_bytes()).unwrap();
13820        let written = parsed.to_xml().unwrap();
13821        assert!(
13822            written
13823                .windows(preserved.len())
13824                .any(|window| window == preserved.as_bytes())
13825        );
13826
13827        for (kind, mut plot, axes) in [
13828            ("pieChart", remaining_plot_fixtures()[0].1.clone(), false),
13829            ("areaChart", remaining_plot_fixtures()[2].1.clone(), true),
13830        ] {
13831            plot = plot.replace(r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#, "");
13832            if kind == "pieChart" {
13833                plot = plot.replace(
13834                    "</q:extLst></q:pieChart>",
13835                    "</q:extLst><!--after-ext--></q:pieChart>",
13836                );
13837            } else {
13838                plot = plot.replace(
13839                    "</q:dropLines><q:axId",
13840                    "</q:dropLines><!--after-drop-lines--><q:axId",
13841                );
13842            }
13843            let mut parsed =
13844                CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
13845            let labels =
13846                CT_DLbls::from_xml(format!(r#"<c:dLbls xmlns:c="{C_NS}"/>"#).as_bytes()).unwrap();
13847            match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
13848                Plot::Pie { data_labels, .. } | Plot::Area { data_labels, .. } => {
13849                    *data_labels = Some(labels)
13850                }
13851                _ => panic!("expected pie or area"),
13852            }
13853            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13854            assert!(
13855                written.find("<c:dLbls").unwrap() < written.find("<q:extLst").unwrap(),
13856                "{kind}: inserted labels must precede the preserved extension"
13857            );
13858            if kind == "areaChart" {
13859                assert!(written.find("<c:dLbls").unwrap() < written.find("<q:dropLines").unwrap());
13860                assert!(
13861                    written.find("<q:dropLines").unwrap()
13862                        < written.find("<!--after-drop-lines-->").unwrap()
13863                );
13864            } else {
13865                assert!(
13866                    written.find("<q:extLst").unwrap() < written.find("<!--after-ext-->").unwrap()
13867                );
13868            }
13869        }
13870    }
13871
13872    #[test]
13873    fn every_supported_corpus_plot_round_trips_structurally() {
13874        let Some(corpus) = require_or_skip_corpus() else {
13875            return;
13876        };
13877        verify_fetched_corpus(&corpus);
13878        let mut pie_count = 0usize;
13879        let mut bar_count = 0usize;
13880        let mut line_count = 0usize;
13881        for path in manifest_paths() {
13882            let package = OpcPackage::open(corpus.join(path)).unwrap();
13883            for (part, xml) in &package.parts {
13884                if !is_chart_part(part) {
13885                    continue;
13886                }
13887                let parsed = CT_ChartSpace::from_xml(xml).unwrap();
13888                if let Ok(plots) = parsed.chart.plot_area.plots() {
13889                    for plot in plots {
13890                        match plot {
13891                            Plot::Pie { .. } => pie_count += 1,
13892                            Plot::Bar { .. } => bar_count += 1,
13893                            Plot::Line { .. } => line_count += 1,
13894                            _ => {}
13895                        }
13896                    }
13897                }
13898                let written = parsed.to_xml().unwrap();
13899                assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
13900            }
13901        }
13902        assert_eq!(pie_count, 1, "typed corpus pie coverage changed");
13903        assert_eq!(bar_count, 11, "typed corpus bar coverage changed");
13904        assert_eq!(line_count, 2, "typed corpus line coverage changed");
13905    }
13906
13907    fn standalone_axis(local: &str, children: &str) -> String {
13908        format!(
13909            r#"<c:{local} xmlns:c="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}">{children}</c:{local}>"#
13910        )
13911    }
13912
13913    fn minimal_axis(local: &str, id: &str, position: &str, cross_axis: &str) -> String {
13914        format!(
13915            r#"<c:{local}><c:axId val="{id}"/><c:scaling/><c:axPos val="{position}"/><c:crossAx val="{cross_axis}"/></c:{local}>"#
13916        )
13917    }
13918
13919    fn chart_with_axes(axes: &[String]) -> String {
13920        format!(
13921            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>"#,
13922            axes.join("")
13923        )
13924    }
13925
13926    #[test]
13927    fn data_labels_write_fixed_prefixes_in_schema_order() {
13928        let xml = format!(
13929            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>"#
13930        );
13931        let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
13932        assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0%");
13933        assert_eq!(parsed.position, Some(DataLabelPosition::OutsideEnd));
13934        assert_eq!(parsed.separator.as_deref(), Some(" / "));
13935        assert!(parsed.show_legend_key);
13936        assert!(parsed.show_value);
13937        assert!(!parsed.show_category_name);
13938        assert!(parsed.show_series_name);
13939        assert!(!parsed.show_percent);
13940        assert!(parsed.show_bubble_size);
13941
13942        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13943        assert!(written.starts_with(&format!(
13944            "<c:dLbls xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
13945        )));
13946        let mut cursor = 0usize;
13947        for tag in [
13948            "<q:dLbl",
13949            "<c:numFmt",
13950            "<q:spPr",
13951            "<q:txPr",
13952            "<c:dLblPos",
13953            "<c:showLegendKey",
13954            "<c:showVal",
13955            "<c:showCatName",
13956            "<c:showSerName",
13957            "<c:showPercent",
13958            "<c:showBubbleSize",
13959            "<c:separator",
13960            "<q:leaderLines",
13961            "<q:extLst",
13962        ] {
13963            let position = written[cursor..]
13964                .find(tag)
13965                .unwrap_or_else(|| panic!("missing ordered data-label tag {tag}"));
13966            cursor += position + tag.len();
13967        }
13968        assert_eq!(parsed, CT_DLbls::from_xml(written.as_bytes()).unwrap());
13969    }
13970
13971    #[test]
13972    fn percentage_formatted_label_renders_with_correct_text() {
13973        let Some(corpus) = require_or_skip_corpus() else {
13974            return;
13975        };
13976        verify_fetched_corpus(&corpus);
13977        assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
13978        assert_command_version("pdftotext", &["-v"], PDFTOTEXT_VERSION);
13979
13980        let source = corpus.join("bar-chart.pptx");
13981        let mut package = OpcPackage::open(&source).expect("open percentage-label source deck");
13982        let chart_part = "/ppt/charts/chart1.xml";
13983        let original_parts = package.parts.clone();
13984        let chart_xml = package
13985            .get_part(chart_part)
13986            .expect("bar chart part")
13987            .to_vec();
13988        let original_series = first_series_xml(&chart_xml).expect("bar chart series");
13989        let mut series = Series::from_xml(&original_series).expect("parse bar chart series");
13990        series.values.values.fill(0.25);
13991        let labels = CT_DLbls {
13992            number_format: Some(NumberFormat::new("0%".to_owned(), false).unwrap()),
13993            position: Some(DataLabelPosition::OutsideEnd),
13994            show_value: true,
13995            ..CT_DLbls::default()
13996        };
13997        series.data_labels = Some(labels);
13998        let replacement = series.to_xml().expect("serialize typed percentage labels");
13999        let candidate_chart = replace_once(&chart_xml, &original_series, &replacement);
14000        package.set_part(chart_part, candidate_chart);
14001        for (part, bytes) in &original_parts {
14002            if part == chart_part {
14003                assert_ne!(package.parts[part], *bytes);
14004            } else {
14005                assert_eq!(
14006                    package.parts[part], *bytes,
14007                    "unexpected changed part {part}"
14008                );
14009            }
14010        }
14011
14012        let temp_root =
14013            std::env::temp_dir().join(format!("oxml-chart-f123-label-gate-{}", std::process::id()));
14014        let profile = std::env::temp_dir().join(format!(
14015            "oxml-chart-f123-label-profile-{}",
14016            std::process::id()
14017        ));
14018        if temp_root.exists() {
14019            fs::remove_dir_all(&temp_root).expect("remove stale F-123 evidence");
14020        }
14021        if profile.exists() {
14022            fs::remove_dir_all(&profile).expect("remove stale F-123 profile");
14023        }
14024        fs::create_dir_all(&temp_root).expect("create F-123 evidence root");
14025        let unbound_candidate = temp_root.join("f123-percentage-label.pptx");
14026        package
14027            .save(&unbound_candidate)
14028            .expect("save percentage-label candidate");
14029        let candidate_sha = sha256(&unbound_candidate);
14030        let evidence_dir = temp_root.join(&candidate_sha);
14031        fs::create_dir(&evidence_dir).expect("create SHA-bound evidence directory");
14032        let candidate = evidence_dir.join("f123-percentage-label.pptx");
14033        fs::rename(&unbound_candidate, &candidate).expect("bind candidate to evidence SHA");
14034        assert_eq!(sha256(&candidate), candidate_sha);
14035
14036        let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
14037        let conversion = Command::new("soffice")
14038            .args([
14039                "--headless",
14040                &profile_argument,
14041                "--convert-to",
14042                "pdf:impress_pdf_Export",
14043                "--outdir",
14044            ])
14045            .arg(&evidence_dir)
14046            .arg(&candidate)
14047            .output()
14048            .expect("run pinned LibreOffice percentage-label gate");
14049        assert!(
14050            conversion.status.success(),
14051            "LibreOffice conversion failed: {}",
14052            String::from_utf8_lossy(&conversion.stderr)
14053        );
14054        let pdf = evidence_dir.join("f123-percentage-label.pdf");
14055        assert!(
14056            pdf.is_file(),
14057            "LibreOffice did not create {}",
14058            pdf.display()
14059        );
14060        let text_path = evidence_dir.join("f123-percentage-label.txt");
14061        let extraction = Command::new("pdftotext")
14062            .arg(&pdf)
14063            .arg(&text_path)
14064            .output()
14065            .expect("run pinned Poppler percentage-label extraction");
14066        assert!(
14067            extraction.status.success(),
14068            "pdftotext failed: {}",
14069            String::from_utf8_lossy(&extraction.stderr)
14070        );
14071        let extracted = fs::read_to_string(&text_path).expect("read percentage-label text");
14072        assert!(
14073            extracted.contains("25%"),
14074            "SHA-bound viewer text lacks 25%: {extracted:?}"
14075        );
14076        for artifact in [&candidate, &pdf, &text_path] {
14077            assert_eq!(
14078                artifact.parent(),
14079                Some(evidence_dir.as_path()),
14080                "evidence escaped candidate SHA"
14081            );
14082        }
14083        eprintln!("F-123 candidate/render/text evidence SHA-256 {candidate_sha}, extracted 25%");
14084        fs::remove_dir_all(&temp_root).expect("remove F-123 evidence");
14085        if profile.exists() {
14086            fs::remove_dir_all(&profile).expect("remove F-123 profile");
14087        }
14088    }
14089
14090    #[test]
14091    fn common_number_formats_project_cached_values_deterministically() {
14092        for (code, value, expected) in [
14093            ("General", 0.25, "0.25"),
14094            ("0", 12.6, "13"),
14095            ("0.0", 12.64, "12.6"),
14096            ("0.00", -12.645, "-12.64"),
14097            ("0%", 0.25, "25%"),
14098            ("0.0%", 0.256, "25.6%"),
14099            ("0.00%", 0.256, "25.60%"),
14100        ] {
14101            let format = NumberFormat::new(code.to_owned(), false).unwrap();
14102            assert_eq!(format.format_value(value).unwrap(), expected, "{code}");
14103        }
14104    }
14105
14106    #[test]
14107    fn malformed_data_labels_and_number_formats_return_errors_without_panicking() {
14108        let cases = [
14109            format!(
14110                r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="1"/><c:showVal val="0"/></c:dLbls>"#
14111            ),
14112            format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="yes"/></c:dLbls>"#),
14113            format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:dLblPos val="middle"/></c:dLbls>"#),
14114            format!(
14115                r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="" sourceLinked="1"/></c:dLbls>"#
14116            ),
14117            format!(
14118                r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="0" sourceLinked="maybe"/></c:dLbls>"#
14119            ),
14120        ];
14121        for xml in cases {
14122            let result = std::panic::catch_unwind(|| CT_DLbls::from_xml(xml.as_bytes()));
14123            assert!(result.is_ok(), "data-label parser panicked for {xml}");
14124            assert!(result.unwrap().is_err(), "malformed labels parsed: {xml}");
14125        }
14126
14127        let invalid_code = NumberFormat::new("bad\u{0}code".to_owned(), false);
14128        assert!(invalid_code.is_err());
14129        let unsupported = NumberFormat::new("#,##0".to_owned(), false).unwrap();
14130        assert!(unsupported.format_value(12.0).is_err());
14131        let general = NumberFormat::new("General".to_owned(), false).unwrap();
14132        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
14133            assert!(general.format_value(value).is_err());
14134        }
14135
14136        let invalid_separator = CT_DLbls {
14137            separator: Some("bad\u{0}separator".to_owned()),
14138            ..CT_DLbls::default()
14139        };
14140        assert!(invalid_separator.to_xml().is_err());
14141    }
14142
14143    #[test]
14144    fn data_labels_preserve_point_overrides_and_extensions_byte_for_byte() {
14145        let xml = format!(
14146            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>"#
14147        );
14148        let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
14149        let written = parsed.to_xml().unwrap();
14150        for raw in [
14151            br#"<!--before-->"#.as_slice(),
14152            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(),
14153            br#"<?after-point?>"#.as_slice(),
14154            br#"<q:spPr><x:collection-shape/></q:spPr>"#.as_slice(),
14155            br#"<q:txPr><x:collection-text/></q:txPr>"#.as_slice(),
14156            br#"<q:showLeaderLines val="1"/>"#.as_slice(),
14157            br#"<q:leaderLines><q:spPr><x:leader-shape/></q:spPr></q:leaderLines>"#.as_slice(),
14158            br#"<q:extLst><q:ext uri="collection"><x:tail/></q:ext></q:extLst>"#.as_slice(),
14159            br#"<!--after-->"#.as_slice(),
14160        ] {
14161            assert!(
14162                written.windows(raw.len()).any(|window| window == raw),
14163                "preserved data-label bytes changed: {}",
14164                String::from_utf8_lossy(raw)
14165            );
14166        }
14167        assert_eq!(parsed, CT_DLbls::from_xml(&written).unwrap());
14168    }
14169
14170    #[test]
14171    fn every_corpus_data_label_collection_round_trips_structurally() {
14172        let Some(corpus) = require_or_skip_corpus() else {
14173            return;
14174        };
14175        verify_fetched_corpus(&corpus);
14176        let mut label_collections = 0usize;
14177        let mut axis_number_formats = 0usize;
14178        for path in manifest_paths() {
14179            let package = OpcPackage::open(corpus.join(path))
14180                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14181            for (part, xml) in &package.parts {
14182                if !is_chart_part(part) {
14183                    continue;
14184                }
14185                let chart = CT_ChartSpace::from_xml(xml)
14186                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14187                for series in chart.chart.plot_area.series().unwrap_or_else(|error| {
14188                    panic!("{path} {part}: series projection failed: {error}")
14189                }) {
14190                    if let Some(labels) = series.data_labels {
14191                        let written = labels.to_xml().unwrap_or_else(|error| {
14192                            panic!("{path} {part}: label write failed: {error}")
14193                        });
14194                        let reparsed = CT_DLbls::from_xml(&written).unwrap_or_else(|error| {
14195                            panic!("{path} {part}: written labels parse failed: {error}")
14196                        });
14197                        assert_eq!(labels, reparsed, "{path} {part}: labels changed");
14198                        label_collections += 1;
14199                    }
14200                }
14201                for axis in chart.chart.plot_area.axes().unwrap_or_else(|error| {
14202                    panic!("{path} {part}: axis projection failed: {error}")
14203                }) {
14204                    if axis.number_format.is_some() {
14205                        let written = axis.to_xml().unwrap_or_else(|error| {
14206                            panic!("{path} {part}: axis write failed: {error}")
14207                        });
14208                        let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
14209                            panic!("{path} {part}: written axis parse failed: {error}")
14210                        });
14211                        assert_eq!(axis, reparsed, "{path} {part}: axis changed");
14212                        axis_number_formats += 1;
14213                    }
14214                }
14215            }
14216        }
14217        assert!(label_collections > 0, "the pinned corpus has no c:dLbls");
14218        assert!(
14219            axis_number_formats > 0,
14220            "the pinned corpus has no axis c:numFmt"
14221        );
14222        eprintln!(
14223            "ChartML data-label corpus gate checked {label_collections} collections and {axis_number_formats} axis number formats"
14224        );
14225    }
14226
14227    fn full_axis(local: &str, position: &str) -> String {
14228        format!(
14229            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}>"#
14230        )
14231    }
14232
14233    #[test]
14234    fn axis_id_pairs_are_reciprocal() {
14235        assert_eq!(
14236            AxisId::new(i64::from(i32::MIN)).unwrap().value(),
14237            i64::from(i32::MIN)
14238        );
14239        assert_eq!(
14240            AxisId::new(i64::from(u32::MAX)).unwrap().value(),
14241            i64::from(u32::MAX)
14242        );
14243        assert!(AxisId::new(i64::from(i32::MIN) - 1).is_err());
14244        assert!(AxisId::new(i64::from(u32::MAX) + 1).is_err());
14245
14246        let valid = chart_with_axes(&[
14247            minimal_axis("catAx", "-1884094432", "b", "-1884097184"),
14248            minimal_axis("valAx", "-1884097184", "l", "-1884094432"),
14249        ]);
14250        let axes = CT_ChartSpace::from_xml(valid.as_bytes())
14251            .unwrap()
14252            .chart
14253            .plot_area
14254            .axes()
14255            .unwrap();
14256        assert_eq!(axes.len(), 2);
14257        assert_eq!(axes[0].id.value(), -1_884_094_432);
14258        assert_eq!(axes[0].cross_axis, axes[1].id);
14259        assert_eq!(axes[1].cross_axis, axes[0].id);
14260
14261        let invalid_sets = [
14262            vec![
14263                minimal_axis("catAx", "1", "b", "2"),
14264                minimal_axis("valAx", "1", "l", "2"),
14265            ],
14266            vec![minimal_axis("catAx", "1", "b", "1")],
14267            vec![minimal_axis("catAx", "1", "b", "2")],
14268            vec![
14269                minimal_axis("catAx", "1", "b", "2"),
14270                minimal_axis("valAx", "2", "l", "3"),
14271                minimal_axis("serAx", "3", "r", "2"),
14272            ],
14273        ];
14274        for axes in invalid_sets {
14275            let chart = CT_ChartSpace::from_xml(chart_with_axes(&axes).as_bytes()).unwrap();
14276            assert!(chart.chart.plot_area.axes().is_err());
14277        }
14278
14279        let empty = CT_ChartSpace::from_xml(chart_with_axes(&[]).as_bytes()).unwrap();
14280        assert!(empty.chart.plot_area.axes().unwrap().is_empty());
14281    }
14282
14283    #[test]
14284    fn all_axis_forms_write_fixed_prefixes_in_schema_order() {
14285        for (local, kind, position) in [
14286            ("catAx", AxisKind::Category, AxisPosition::Bottom),
14287            ("valAx", AxisKind::Value, AxisPosition::Left),
14288            ("dateAx", AxisKind::Date, AxisPosition::Top),
14289            ("serAx", AxisKind::Series, AxisPosition::Right),
14290        ] {
14291            let xml = full_axis(local, position.as_str());
14292            let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
14293            assert_eq!(parsed.kind, kind);
14294            assert_eq!(parsed.id.value(), -1_125_255_920);
14295            assert_eq!(parsed.scaling.log_base, Some(10.0));
14296            assert_eq!(parsed.scaling.orientation, Orientation::MaxMin);
14297            assert_eq!(parsed.scaling.maximum, Some(100.0));
14298            assert_eq!(parsed.scaling.minimum, Some(-10.0));
14299            assert!(parsed.deleted);
14300            assert_eq!(parsed.position, position);
14301            assert!(parsed.major_gridlines.is_some());
14302            assert!(parsed.minor_gridlines.is_some());
14303            assert!(parsed.title.is_some());
14304            assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0");
14305            assert_eq!(parsed.major_tick_mark, TickMark::Cross);
14306            assert_eq!(parsed.minor_tick_mark, TickMark::Inside);
14307            assert_eq!(parsed.tick_label_position, TickLabelPosition::High);
14308            assert!(parsed.sp_pr.is_some());
14309            assert!(parsed.tx_pr.is_some());
14310            assert_eq!(parsed.cross_axis.value(), -1_004_303_696);
14311
14312            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14313            assert!(written.starts_with(&format!(
14314                "<c:{local} xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
14315            )));
14316            assert!(written.contains("<c:axId val=\"-1125255920\""));
14317            let mut cursor = 0usize;
14318            for tag in [
14319                "<c:axId",
14320                "<c:scaling",
14321                "<c:delete",
14322                "<c:axPos",
14323                "<c:majorGridlines",
14324                "<c:minorGridlines",
14325                "<c:title",
14326                "<c:numFmt",
14327                "<c:majorTickMark",
14328                "<c:minorTickMark",
14329                "<c:tickLblPos",
14330                "<c:spPr",
14331                "<c:txPr",
14332                "<c:crossAx",
14333            ] {
14334                let position = written[cursor..]
14335                    .find(tag)
14336                    .unwrap_or_else(|| panic!("missing ordered axis tag {tag}"));
14337                cursor += position + tag.len();
14338            }
14339            assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
14340        }
14341
14342        let mut constructed = Axis::new(
14343            AxisKind::Value,
14344            AxisId::new(1).unwrap(),
14345            AxisPosition::Left,
14346            AxisId::new(2).unwrap(),
14347        );
14348        assert_eq!(
14349            constructed,
14350            Axis::from_xml(&constructed.to_xml().unwrap()).unwrap()
14351        );
14352        constructed.kind = AxisKind::Category;
14353        constructed.id = AxisId::new(3).unwrap();
14354        constructed.cross_axis = AxisId::new(4).unwrap();
14355        constructed.scaling.minimum = Some(0.0);
14356        constructed.scaling.maximum = Some(10.0);
14357        let written = constructed.to_xml().unwrap();
14358        assert!(written.starts_with(b"<c:catAx"));
14359        assert_eq!(constructed, Axis::from_xml(&written).unwrap());
14360    }
14361
14362    #[test]
14363    fn axis_equality_normalizes_ids_without_losing_lexical_preservation() {
14364        let padded = Axis::from_xml(
14365            standalone_axis(
14366                "catAx",
14367                "<c:axId val=\"01\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"002\"/>",
14368            )
14369            .as_bytes(),
14370        )
14371        .unwrap();
14372        let normalized = Axis::from_xml(
14373            standalone_axis(
14374                "catAx",
14375                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14376            )
14377            .as_bytes(),
14378        )
14379        .unwrap();
14380        assert_eq!(padded, normalized);
14381
14382        let preserved = String::from_utf8(padded.to_xml().unwrap()).unwrap();
14383        assert!(preserved.contains("<c:axId val=\"01\""));
14384        assert!(preserved.contains("<c:crossAx val=\"002\""));
14385
14386        let mut mutated = padded;
14387        mutated.id = AxisId::new(3).unwrap();
14388        mutated.cross_axis = AxisId::new(4).unwrap();
14389        let rewritten = String::from_utf8(mutated.to_xml().unwrap()).unwrap();
14390        assert!(rewritten.contains("<c:axId val=\"3\""));
14391        assert!(rewritten.contains("<c:crossAx val=\"4\""));
14392        assert!(!rewritten.contains("val=\"01\""));
14393        assert!(!rewritten.contains("val=\"002\""));
14394    }
14395
14396    #[test]
14397    fn malformed_axis_values_return_errors_without_panicking() {
14398        let cases = [
14399            standalone_axis(
14400                "catAx",
14401                "<c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14402            ),
14403            standalone_axis(
14404                "catAx",
14405                "<c:axId val=\"1\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14406            ),
14407            standalone_axis(
14408                "catAx",
14409                "<c:axId val=\"1\"/><c:scaling/><c:crossAx val=\"2\"/>",
14410            ),
14411            standalone_axis(
14412                "catAx",
14413                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/>",
14414            ),
14415            standalone_axis(
14416                "catAx",
14417                "<c:axId val=\"-2147483649\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14418            ),
14419            standalone_axis(
14420                "catAx",
14421                "<c:axId val=\"4294967296\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14422            ),
14423            standalone_axis(
14424                "catAx",
14425                "<c:axId val=\"1\"/><c:scaling><c:logBase val=\"1\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14426            ),
14427            standalone_axis(
14428                "catAx",
14429                "<c:axId val=\"1\"/><c:scaling><c:max val=\"NaN\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14430            ),
14431            standalone_axis(
14432                "catAx",
14433                "<c:axId val=\"1\"/><c:scaling><c:max val=\"1\"/><c:min val=\"2\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14434            ),
14435            standalone_axis(
14436                "catAx",
14437                "<c:axId val=\"1\"/><c:scaling><c:orientation val=\"sideways\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14438            ),
14439            standalone_axis(
14440                "catAx",
14441                "<c:axId val=\"1\"/><c:scaling/><c:delete val=\"yes\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14442            ),
14443            standalone_axis(
14444                "catAx",
14445                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"middle\"/><c:crossAx val=\"2\"/>",
14446            ),
14447            standalone_axis(
14448                "catAx",
14449                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:majorTickMark val=\"near\"/><c:crossAx val=\"2\"/>",
14450            ),
14451            standalone_axis(
14452                "catAx",
14453                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:tickLblPos val=\"middle\"/><c:crossAx val=\"2\"/>",
14454            ),
14455            standalone_axis(
14456                "catAx",
14457                "<c:axId val=\"1\"/><c:axId val=\"2\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14458            ),
14459            standalone_axis(
14460                "catAx",
14461                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt sourceLinked=\"1\"/><c:crossAx val=\"2\"/>",
14462            ),
14463            standalone_axis(
14464                "catAx",
14465                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt formatCode=\"0\" sourceLinked=\"maybe\"/><c:crossAx val=\"2\"/>",
14466            ),
14467        ];
14468        for xml in cases {
14469            let result = std::panic::catch_unwind(|| Axis::from_xml(xml.as_bytes()));
14470            assert!(result.is_ok(), "axis parser panicked for {xml}");
14471            assert!(result.unwrap().is_err(), "malformed axis parsed: {xml}");
14472        }
14473
14474        let mut axis = Axis::new(
14475            AxisKind::Value,
14476            AxisId::new(1).unwrap(),
14477            AxisPosition::Left,
14478            AxisId::new(2).unwrap(),
14479        );
14480        axis.scaling.maximum = Some(f64::INFINITY);
14481        assert!(axis.to_xml().is_err());
14482    }
14483
14484    #[test]
14485    fn axes_preserve_unmodelled_children_byte_for_byte() {
14486        let xml = format!(
14487            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>"#
14488        );
14489        let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
14490        let mut relabelled = parsed.clone();
14491        relabelled.kind = AxisKind::Value;
14492        assert!(
14493            relabelled.to_xml().is_err(),
14494            "a parsed category axis must not retain its tail under a value-axis root"
14495        );
14496        let written = parsed.to_xml().unwrap();
14497        for raw in [
14498            br#"<!--before-->"#.as_slice(),
14499            br#"<?after-id?>"#.as_slice(),
14500            br#"<!--scale-->"#.as_slice(),
14501            br#"<q:extLst><q:ext uri="scale"><x:data/></q:ext></q:extLst>"#.as_slice(),
14502            br#"<x:grid/>"#.as_slice(),
14503            br#"<x:title/>"#.as_slice(),
14504            br#"<q:crosses val="autoZero"/>"#.as_slice(),
14505            br#"<q:auto val="1"><x:auto/></q:auto>"#.as_slice(),
14506            br#"<q:extLst><q:ext uri="axis"><x:tail/></q:ext></q:extLst>"#.as_slice(),
14507            br#"<!--after-->"#.as_slice(),
14508        ] {
14509            assert!(
14510                written.windows(raw.len()).any(|window| window == raw),
14511                "preserved axis bytes changed: {}",
14512                String::from_utf8_lossy(raw)
14513            );
14514        }
14515        let written = String::from_utf8(written).unwrap();
14516        assert!(written.starts_with("<c:catAx"));
14517        assert!(written.contains("x:keep=\"axis\""));
14518        assert!(written.contains("x:keep=\"id\""));
14519        assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
14520    }
14521
14522    #[test]
14523    fn every_corpus_axis_round_trips_structurally() {
14524        let Some(corpus) = require_or_skip_corpus() else {
14525            return;
14526        };
14527        verify_fetched_corpus(&corpus);
14528        let mut axis_count = 0usize;
14529        let mut chart_parts = 0usize;
14530        let mut kinds = HashSet::new();
14531        for path in manifest_paths() {
14532            let package = OpcPackage::open(corpus.join(path))
14533                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14534            for (part, xml) in &package.parts {
14535                if !is_chart_part(part) {
14536                    continue;
14537                }
14538                let chart = CT_ChartSpace::from_xml(xml)
14539                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14540                let axes = chart.chart.plot_area.axes().unwrap_or_else(|error| {
14541                    panic!("{path} {part}: axis projection failed: {error}")
14542                });
14543                for axis in axes {
14544                    kinds.insert(axis.kind);
14545                    let written = axis.to_xml().unwrap_or_else(|error| {
14546                        panic!("{path} {part}: axis write failed: {error}")
14547                    });
14548                    let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
14549                        panic!("{path} {part}: written axis parse failed: {error}")
14550                    });
14551                    assert_eq!(axis, reparsed, "{path} {part}: axis model changed");
14552                    axis_count += 1;
14553                }
14554                chart_parts += 1;
14555            }
14556        }
14557        assert!(
14558            axis_count > 0,
14559            "the pinned corpus contained no ChartML axes"
14560        );
14561        assert!(kinds.contains(&AxisKind::Category));
14562        assert!(kinds.contains(&AxisKind::Value));
14563
14564        for kind in ["dateAx", "serAx"] {
14565            let parsed = Axis::from_xml(
14566                standalone_axis(
14567                    kind,
14568                    "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14569                )
14570                .as_bytes(),
14571            )
14572            .unwrap();
14573            assert_eq!(parsed, Axis::from_xml(&parsed.to_xml().unwrap()).unwrap());
14574        }
14575        eprintln!(
14576            "ChartML axis corpus gate checked {axis_count} axes across {chart_parts} chart parts"
14577        );
14578    }
14579
14580    #[test]
14581    fn series_formula_and_cache_are_consistent_with_one_source() {
14582        let values = NumericData::new(
14583            "'Sales 24'!$B$2:$B$4".to_owned(),
14584            "0.0".to_owned(),
14585            vec![4.25, 8.5, 17.0],
14586        )
14587        .unwrap();
14588        let mut series = Series::new(3, 1, values);
14589        series.categories = Some(AxisData::String(
14590            StringRef::new(
14591                "'Sales 24'!$A$2:$A$4".to_owned(),
14592                vec!["North".to_owned(), "South".to_owned(), "West".to_owned()],
14593            )
14594            .unwrap(),
14595        ));
14596
14597        let written = String::from_utf8(series.to_xml().unwrap()).unwrap();
14598        assert!(written.contains("<c:f>&apos;Sales 24&apos;!$B$2:$B$4</c:f>"));
14599        assert!(written.contains("<c:formatCode>0.0</c:formatCode>"));
14600        assert_eq!(written.matches("<c:ptCount val=\"3\"").count(), 2);
14601        for (index, value) in ["4.25", "8.5", "17"].iter().enumerate() {
14602            assert!(written.contains(&format!("<c:pt idx=\"{index}\"><c:v>{value}</c:v></c:pt>")));
14603        }
14604        let reparsed = Series::from_xml(written.as_bytes()).unwrap();
14605        assert_eq!(reparsed.index, series.index);
14606        assert_eq!(reparsed.order, series.order);
14607        match (&reparsed.categories, &series.categories) {
14608            (Some(AxisData::String(left)), Some(AxisData::String(right))) => {
14609                assert_eq!(left.formula, right.formula);
14610                assert_eq!(left.values, right.values);
14611            }
14612            _ => panic!("expected string categories"),
14613        }
14614        assert_eq!(reparsed.values.formula, series.values.formula);
14615        assert_eq!(reparsed.values.format_code, series.values.format_code);
14616        assert_eq!(reparsed.values.values, series.values.values);
14617    }
14618
14619    #[test]
14620    fn string_and_numeric_references_write_fixed_prefixes_in_schema_order() {
14621        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>"#;
14622        let string_ref = StringRef::from_xml(string_xml).unwrap();
14623        let written = String::from_utf8(string_ref.to_xml().unwrap()).unwrap();
14624        assert!(written.starts_with("<c:strRef xmlns:c="));
14625        assert!(written.find("<c:f>").unwrap() < written.find("<c:strCache>").unwrap());
14626        assert!(written.find("<c:ptCount").unwrap() < written.find("<c:pt idx=").unwrap());
14627
14628        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>"#;
14629        let numeric = NumericData::from_xml(numeric_xml).unwrap();
14630        let written = String::from_utf8(numeric.to_xml().unwrap()).unwrap();
14631        let positions: Vec<_> = [
14632            "<c:f>",
14633            "<c:numCache>",
14634            "<c:formatCode>",
14635            "<c:ptCount",
14636            "<c:pt idx=",
14637        ]
14638        .iter()
14639        .map(|tag| written.find(tag).unwrap())
14640        .collect();
14641        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14642        assert_eq!(numeric, NumericData::from_xml(written.as_bytes()).unwrap());
14643    }
14644
14645    #[test]
14646    fn mixed_chartml_aliases_resolve_by_namespace_uri() {
14647        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>"#;
14648        let parsed = Series::from_xml(xml).unwrap();
14649        assert_eq!(parsed.index, 4);
14650        assert_eq!(parsed.order, 2);
14651        assert_eq!(parsed.values.values, vec![1.5, 2.5]);
14652        assert!(matches!(parsed.categories, Some(AxisData::String(_))));
14653        assert_eq!(parsed, Series::from_xml(&parsed.to_xml().unwrap()).unwrap());
14654    }
14655
14656    #[test]
14657    fn plot_area_series_ignores_inherited_foreign_plot_aliases() {
14658        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>"#;
14659        let chart = CT_ChartSpace::from_xml(xml).unwrap();
14660        let series = chart.chart.plot_area.series().unwrap();
14661        assert_eq!(series.len(), 1);
14662        assert_eq!(series[0].index, 1);
14663        let written = String::from_utf8(series[0].to_xml().unwrap()).unwrap();
14664        assert!(
14665            written.contains(r#"xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart""#)
14666        );
14667        assert!(written.contains(r#"xmlns:x="urn:producer""#));
14668        assert!(written.contains("<q:marker><x:data/></q:marker>"));
14669        assert_eq!(series[0], Series::from_xml(written.as_bytes()).unwrap());
14670
14671        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>"#;
14672        let chart = CT_ChartSpace::from_xml(conflicting).unwrap();
14673        assert!(chart.chart.plot_area.series().is_err());
14674    }
14675
14676    #[test]
14677    fn malformed_series_and_cache_values_return_errors_without_panicking() {
14678        let cases: &[&[u8]] = &[
14679            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>"#,
14680            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/></c:ser>"#,
14681            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>"#,
14682            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>"#,
14683            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>"#,
14684            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>"#,
14685            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>"#,
14686            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>"#,
14687            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>"#,
14688            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>"#,
14689            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>"#,
14690            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>"#,
14691            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>"#,
14692            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>"#,
14693            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>"#,
14694            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>"#,
14695            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>"#,
14696        ];
14697        for xml in cases {
14698            let result = std::panic::catch_unwind(|| Series::from_xml(xml));
14699            assert!(result.is_ok(), "series parser panicked");
14700            assert!(
14701                result.unwrap().is_err(),
14702                "malformed series parsed: {}",
14703                String::from_utf8_lossy(xml)
14704            );
14705        }
14706        assert!(
14707            NumericData::new(
14708                "S!$A$1".to_owned(),
14709                "General".to_owned(),
14710                vec![f64::INFINITY]
14711            )
14712            .is_err()
14713        );
14714    }
14715
14716    #[test]
14717    fn chart_text_rejects_xml_forbidden_characters_and_escapes_metacharacters() {
14718        assert!(StringRef::new("S!$A$1\u{1}".to_owned(), Vec::new()).is_err());
14719        assert!(StringRef::new("S!$A$1".to_owned(), vec!["invalid\u{1}".to_owned()]).is_err());
14720        assert!(
14721            NumericData::new("S!$A$1\u{1}".to_owned(), "General".to_owned(), Vec::new()).is_err()
14722        );
14723        assert!(
14724            NumericData::new("S!$A$1".to_owned(), "General\u{1}".to_owned(), Vec::new()).is_err()
14725        );
14726
14727        let mut strings = StringRef::new("S!$A$1".to_owned(), vec!["valid".to_owned()]).unwrap();
14728        strings.formula = "invalid\u{1}".to_owned();
14729        assert!(strings.to_xml().is_err());
14730        strings.formula = "S!$A$1".to_owned();
14731        strings.values[0] = "invalid\u{1}".to_owned();
14732        assert!(strings.to_xml().is_err());
14733
14734        let mut numbers =
14735            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap();
14736        numbers.formula = "invalid\u{1}".to_owned();
14737        assert!(numbers.to_xml().is_err());
14738        numbers.formula = "S!$A$1".to_owned();
14739        numbers.format_code = "invalid\u{1}".to_owned();
14740        assert!(numbers.to_xml().is_err());
14741
14742        let strings =
14743            StringRef::new("S!$A$1&".to_owned(), vec!["North < West & East".to_owned()]).unwrap();
14744        let written = String::from_utf8(strings.to_xml().unwrap()).unwrap();
14745        assert!(written.contains("S!$A$1&amp;"));
14746        assert!(written.contains("North &lt; West &amp; East"));
14747    }
14748
14749    #[test]
14750    fn series_preserves_unmodelled_children_byte_for_byte() {
14751        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>"#;
14752        let parsed = Series::from_xml(xml).unwrap();
14753        let written = parsed.to_xml().unwrap();
14754        for raw in [
14755            br#"<!--before-->"#.as_slice(),
14756            br#"<x:between/>"#.as_slice(),
14757            br#"<q:dPt x:id="one"><q:idx val="0"/></q:dPt>"#.as_slice(),
14758            br#"<q:trendline x:id="trend"/>"#.as_slice(),
14759            br#"<q:extLst><q:ext uri="keep"><x:data/></q:ext></q:extLst>"#.as_slice(),
14760            br#"<!--point-->"#.as_slice(),
14761            br#"<x:cacheExt/>"#.as_slice(),
14762        ] {
14763            assert!(
14764                written.windows(raw.len()).any(|window| window == raw),
14765                "preserved series bytes changed: {}",
14766                String::from_utf8_lossy(raw)
14767            );
14768        }
14769        let written = String::from_utf8(written.clone()).unwrap();
14770        assert!(written.contains(r#"x:keep="series""#));
14771        assert!(written.contains(r#"x:keep="formula""#));
14772        assert!(written.contains("<c:dLbls"));
14773        assert!(written.contains(r#"x:id="labels""#));
14774        assert!(parsed.data_labels.is_some());
14775        assert_eq!(parsed, Series::from_xml(written.as_bytes()).unwrap());
14776    }
14777
14778    #[test]
14779    fn public_series_edits_do_not_duplicate_or_drop_preserved_payloads() {
14780        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>"#;
14781        let parsed = Series::from_xml(opaque_xml).unwrap();
14782
14783        let mut edited = parsed.clone();
14784        edited.name = Some(StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap());
14785        assert!(edited.to_xml().is_err());
14786
14787        let mut edited = parsed.clone();
14788        edited.categories = Some(AxisData::Numeric(
14789            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap(),
14790        ));
14791        assert!(edited.to_xml().is_err());
14792
14793        let mut edited = parsed;
14794        edited.bubble_size =
14795            Some(NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
14796        assert!(edited.to_xml().is_err());
14797
14798        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>"#;
14799        let parsed = Series::from_xml(cache_xml).unwrap();
14800        let tail = "<c:extLst><c:ext uri=\"keep\"><x:data/></c:ext></c:extLst>";
14801
14802        let mut shortened = parsed.clone();
14803        shortened.values.values.truncate(1);
14804        let written = String::from_utf8(shortened.to_xml().unwrap()).unwrap();
14805        assert!(!written.contains("<c:pt idx=\"1\""));
14806        assert!(written.find("<c:pt idx=\"0\"").unwrap() < written.find(tail).unwrap());
14807        assert!(Series::from_xml(written.as_bytes()).is_ok());
14808
14809        let mut grown = parsed;
14810        grown.values.values.push(3.0);
14811        let written = String::from_utf8(grown.to_xml().unwrap()).unwrap();
14812        assert!(written.find("<c:pt idx=\"2\"").unwrap() < written.find(tail).unwrap());
14813        assert!(Series::from_xml(written.as_bytes()).is_ok());
14814
14815        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>"#;
14816        let mut edited = Series::from_xml(ordered_xml).unwrap();
14817        edited.name = Some(StringRef::new("S!$B$1".to_owned(), Vec::new()).unwrap());
14818        edited.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
14819        edited.categories = Some(AxisData::String(
14820            StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap(),
14821        ));
14822        edited.bubble_size =
14823            Some(NumericData::new("S!$C$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
14824        let written = String::from_utf8(edited.to_xml().unwrap()).unwrap();
14825        let positions: Vec<_> = [
14826            "<c:tx>",
14827            "<c:spPr",
14828            "<c:marker>",
14829            "<c:cat>",
14830            "<c:val>",
14831            "<c:bubbleSize>",
14832            "<c:extLst>",
14833        ]
14834        .iter()
14835        .map(|tag| written.find(tag).unwrap())
14836        .collect();
14837        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14838        assert!(Series::from_xml(written.as_bytes()).is_ok());
14839    }
14840
14841    #[test]
14842    fn every_corpus_series_round_trips_structurally() {
14843        let Some(corpus) = require_or_skip_corpus() else {
14844            return;
14845        };
14846        verify_fetched_corpus(&corpus);
14847        let mut series_count = 0usize;
14848        let mut chart_parts = 0usize;
14849        for path in manifest_paths() {
14850            let package = OpcPackage::open(corpus.join(path))
14851                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14852            for (part, xml) in &package.parts {
14853                if !is_chart_part(part) {
14854                    continue;
14855                }
14856                let chart = CT_ChartSpace::from_xml(xml)
14857                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14858                let parsed_series =
14859                    chart.chart.plot_area.series().unwrap_or_else(|error| {
14860                        panic!("{path} {part}: series parse failed: {error}")
14861                    });
14862                for series in parsed_series {
14863                    let written = series.to_xml().unwrap_or_else(|error| {
14864                        panic!("{path} {part}: series write failed: {error}")
14865                    });
14866                    let reparsed = Series::from_xml(&written).unwrap_or_else(|error| {
14867                        panic!("{path} {part}: written series parse failed: {error}")
14868                    });
14869                    assert_eq!(series, reparsed, "{path} {part}: series model changed");
14870                    series_count += 1;
14871                }
14872                chart_parts += 1;
14873            }
14874        }
14875        assert!(
14876            series_count > 0,
14877            "the pinned corpus contained no supported series"
14878        );
14879        eprintln!(
14880            "ChartML series corpus gate checked {series_count} series across {chart_parts} chart parts"
14881        );
14882    }
14883
14884    #[test]
14885    fn chart_space_reads_aliases_and_writes_fixed_prefixes_in_schema_order() {
14886        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>"#;
14887        let parsed = CT_ChartSpace::from_xml(xml).unwrap();
14888        assert!(!parsed.chart.auto_title_deleted);
14889        assert!(!parsed.chart.plot_vis_only);
14890        assert_eq!(parsed.chart.disp_blanks_as, DispBlanksAs::Span);
14891        assert!(parsed.chart.title.is_some());
14892        assert!(parsed.chart.legend.is_some());
14893        assert!(parsed.sp_pr.is_some());
14894        assert!(parsed.tx_pr.is_some());
14895
14896        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14897        assert!(written.starts_with(
14898            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""#
14899        ));
14900        for tag in [
14901            "<c:chart>",
14902            "<c:title>",
14903            "<c:plotArea>",
14904            "<c:legend>",
14905            "<c:plotVisOnly",
14906            "<c:dispBlanksAs",
14907            "<c:spPr>",
14908            "<c:txPr>",
14909        ] {
14910            assert!(written.contains(tag), "missing fixed-prefix tag {tag}");
14911        }
14912        let positions: Vec<_> = [
14913            "<c:title>",
14914            "<c:plotArea>",
14915            "<c:legend>",
14916            "<c:plotVisOnly",
14917            "<c:dispBlanksAs",
14918            "<c:spPr>",
14919            "<c:txPr>",
14920        ]
14921        .iter()
14922        .map(|tag| written.find(tag).unwrap())
14923        .collect();
14924        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14925        assert!(written.contains("<a:solidFill><a:srgbClr"));
14926        assert!(written.contains("<a:bodyPr/><a:p/>"));
14927        assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
14928    }
14929
14930    #[test]
14931    fn public_core_edits_keep_preserved_children_in_their_schema_slots() {
14932        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>"#;
14933        let mut parsed = CT_ChartSpace::from_xml(xml).unwrap();
14934        parsed.chart.auto_title_deleted = true;
14935        parsed.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
14936        parsed.tx_pr =
14937            Some(CT_TextBody::from_xml(br#"<a:txBody><a:bodyPr/><a:p/></a:txBody>"#).unwrap());
14938        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14939        let positions: Vec<_> = [
14940            "<c:autoTitleDeleted",
14941            "<c:pivotFmts>",
14942            "<c:plotArea",
14943            "<c:spPr",
14944            "<c:txPr",
14945            "<c:externalData",
14946        ]
14947        .iter()
14948        .map(|tag| written.find(tag).unwrap())
14949        .collect();
14950        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14951        assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
14952    }
14953
14954    #[test]
14955    fn malformed_core_chart_values_return_errors_without_panicking() {
14956        let cases: &[&[u8]] = &[
14957            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"/>"#,
14958            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart/></c:chartSpace>"#,
14959            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:plotArea/></c:chart></c:chartSpace>"#,
14960            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:autoTitleDeleted val="yes"/><c:plotArea/></c:chart></c:chartSpace>"#,
14961            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:dispBlanksAs val="empty"/></c:chart></c:chartSpace>"#,
14962            br#"<c:chartSpace xmlns:c="urn:wrong"><c:chart><c:plotArea/></c:chart></c:chartSpace>"#,
14963            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:foreign"><x:chart><x:plotArea/></x:chart></c:chartSpace>"#,
14964            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>"#,
14965            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:title/><c:title/><c:plotArea/></c:chart></c:chartSpace>"#,
14966            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>"#,
14967            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>"#,
14968            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>"#,
14969        ];
14970        for xml in cases {
14971            let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml));
14972            assert!(result.is_ok(), "ChartML parser panicked");
14973            assert!(
14974                result.unwrap().is_err(),
14975                "malformed ChartML parsed: {}",
14976                String::from_utf8_lossy(xml)
14977            );
14978        }
14979    }
14980
14981    #[test]
14982    fn point_label_overrides_render_without_changing_preserved_xml() {
14983        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>"#;
14984        let plot = bar_plot("").replace(
14985            r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
14986            &format!(
14987                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>"#
14988            ),
14989        );
14990        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
14991        let plot = &chart.chart.plot_area.plots().unwrap()[0];
14992        let Plot::Bar { data_labels, .. } = plot else {
14993            panic!("expected bar plot");
14994        };
14995        let labels = data_labels.as_ref().unwrap();
14996        let written = labels.to_xml().unwrap();
14997        assert!(
14998            written
14999                .windows(raw_overrides.len())
15000                .any(|window| window == raw_overrides.as_bytes())
15001        );
15002        let overrides = super::point_label_overrides(labels).unwrap();
15003        assert_eq!(overrides.keys().copied().collect::<Vec<_>>(), vec![1]);
15004
15005        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
15006        let mut rendered = Vec::new();
15007        super::render_data_labels(
15008            plot,
15009            chart_bounds(),
15010            DispBlanksAs::Gap,
15011            super::GeometryOptions::default(),
15012            &mut fonts,
15013            &mut rendered,
15014        )
15015        .unwrap();
15016        let labels: Vec<_> = rendered
15017            .iter()
15018            .filter_map(|element| match element {
15019                PositionedElement::Text(run) => Some(run),
15020                _ => None,
15021            })
15022            .collect();
15023        assert_eq!(labels.len(), 2);
15024        assert_eq!(labels[0].text, "North, 1");
15025        assert_eq!(labels[1].text, "South / 2.0");
15026        assert_point_near(labels[0].origin, Point { x: 50.0, y: 105.0 });
15027        assert_point_near(labels[1].origin, Point { x: 158.0, y: 70.0 });
15028        assert_eq!(
15029            CT_DLbls::from_xml(&written).unwrap().to_xml().unwrap(),
15030            written
15031        );
15032    }
15033
15034    #[test]
15035    fn malformed_point_label_overrides_return_contextual_errors() {
15036        let cases = [
15037            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:showVal val="1"/></q:dLbl></q:dLbls>"#,
15038            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>"#,
15039            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="point"/></q:dLbl></q:dLbls>"#,
15040            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>"#,
15041            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>"#,
15042            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>"#,
15043            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>"#,
15044        ];
15045        for xml in cases {
15046            let labels = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
15047            let result = std::panic::catch_unwind(|| super::point_label_overrides(&labels));
15048            assert!(result.is_ok(), "point-label parser panicked for {xml}");
15049            assert!(
15050                result.unwrap().is_err(),
15051                "malformed point labels parsed: {xml}"
15052            );
15053        }
15054
15055        let foreign = format!(
15056            r#"<q:dLbls xmlns:q="{C_NS}" xmlns:x="urn:foreign"><x:dLbl><x:idx val="0"/></x:dLbl></q:dLbls>"#
15057        );
15058        let labels = CT_DLbls::from_xml(foreign.as_bytes()).unwrap();
15059        assert!(super::point_label_overrides(&labels).unwrap().is_empty());
15060    }
15061
15062    #[test]
15063    fn percentage_label_aggregation_rejects_nonfinite_totals() {
15064        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
15065        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
15066        let Plot::Bar { series, .. } = plot else {
15067            panic!("expected bar plot");
15068        };
15069        series[0].values.values = vec![f64::MAX, f64::MAX];
15070        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
15071        super::render_data_labels(
15072            plot,
15073            chart_bounds(),
15074            DispBlanksAs::Gap,
15075            super::GeometryOptions::default(),
15076            &mut fonts,
15077            &mut Vec::new(),
15078        )
15079        .expect("value-only labels do not need a percentage total");
15080        let Plot::Bar { data_labels, .. } = plot else {
15081            panic!("expected bar plot");
15082        };
15083        data_labels.as_mut().unwrap().show_percent = true;
15084        let error = super::render_data_labels(
15085            plot,
15086            chart_bounds(),
15087            DispBlanksAs::Gap,
15088            super::GeometryOptions::default(),
15089            &mut fonts,
15090            &mut Vec::new(),
15091        )
15092        .unwrap_err();
15093        assert!(error.to_string().contains("data-label percentage total"));
15094
15095        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>"#;
15096        let plot_xml = bar_plot("").replace(
15097            r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
15098            &format!(
15099                r#"<q:dLbls>{overrides}<q:showVal val="1"/><q:showPercent val="1"/></q:dLbls>"#
15100            ),
15101        );
15102        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
15103        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
15104        let Plot::Bar { series, .. } = plot else {
15105            panic!("expected bar plot");
15106        };
15107        series[0].values.values = vec![f64::MAX, f64::MAX];
15108        super::render_data_labels(
15109            plot,
15110            chart_bounds(),
15111            DispBlanksAs::Gap,
15112            super::GeometryOptions::default(),
15113            &mut fonts,
15114            &mut Vec::new(),
15115        )
15116        .expect("effective point overrides disable percentage aggregation");
15117    }
15118
15119    #[test]
15120    fn percentage_labels_use_effective_number_format_precision() {
15121        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
15122        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
15123        let Plot::Bar {
15124            series,
15125            data_labels,
15126            ..
15127        } = plot
15128        else {
15129            panic!("expected bar plot");
15130        };
15131        series[0].values.values = vec![1.0, 2.0];
15132        let labels = data_labels.as_mut().unwrap();
15133        labels.show_value = false;
15134        labels.show_percent = true;
15135        labels.number_format = Some(NumberFormat::new("0.00%".to_owned(), false).unwrap());
15136        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
15137        let mut rendered = Vec::new();
15138        super::render_data_labels(
15139            plot,
15140            chart_bounds(),
15141            DispBlanksAs::Gap,
15142            super::GeometryOptions::default(),
15143            &mut fonts,
15144            &mut rendered,
15145        )
15146        .unwrap();
15147        let text: Vec<_> = rendered
15148            .iter()
15149            .filter_map(|element| match element {
15150                PositionedElement::Text(run) => Some(run.text.as_str()),
15151                _ => None,
15152            })
15153            .collect();
15154        assert_eq!(text, vec!["33.33%", "66.67%"]);
15155
15156        let point_override = r#"<q:dLbl><q:idx val="0"/><q:numFmt formatCode="0.00%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbl>"#;
15157        let plot_xml = bar_plot("").replace(
15158            r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
15159            &format!(
15160                r#"<q:dLbls>{point_override}<q:numFmt formatCode="0%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbls>"#
15161            ),
15162        );
15163        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
15164        let plot = &chart.chart.plot_area.plots().unwrap()[0];
15165        let mut rendered = Vec::new();
15166        super::render_data_labels(
15167            plot,
15168            chart_bounds(),
15169            DispBlanksAs::Gap,
15170            super::GeometryOptions::default(),
15171            &mut fonts,
15172            &mut rendered,
15173        )
15174        .unwrap();
15175        let text: Vec<_> = rendered
15176            .iter()
15177            .filter_map(|element| match element {
15178                PositionedElement::Text(run) => Some(run.text.as_str()),
15179                _ => None,
15180            })
15181            .collect();
15182        assert_eq!(text, vec!["33.33%", "67%"]);
15183    }
15184
15185    #[test]
15186    fn core_chart_shells_preserve_unmodelled_children_byte_for_byte() {
15187        let preserved = [
15188            br#"<x:root x:id="1"><x:nested>one &amp; two</x:nested><!--root--></x:root>"#
15189                .as_slice(),
15190            br#"<x:title x:id="2"><x:nested><![CDATA[title]]></x:nested></x:title>"#.as_slice(),
15191            br#"<x:plot x:id="3"><x:nested>plot</x:nested></x:plot>"#.as_slice(),
15192            br#"<x:legend x:id="4"><x:nested>legend</x:nested></x:legend>"#.as_slice(),
15193            br#"<x:extension x:id="5"><x:nested>extension</x:nested></x:extension>"#.as_slice(),
15194            br#"<!--space-comment-->"#.as_slice(),
15195            br#"<?space processing?>"#.as_slice(),
15196            br#"<!--chart-comment-->"#.as_slice(),
15197            br#"<?title processing?>"#.as_slice(),
15198            br#"<!--flag-comment-->"#.as_slice(),
15199            br#"<?flag processing?>"#.as_slice(),
15200            br#"<x:producerShape/>"#.as_slice(),
15201            br#"<x:producerText/>"#.as_slice(),
15202        ];
15203        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>"#;
15204        let parsed = CT_ChartSpace::from_xml(xml).unwrap();
15205        let written = parsed.to_xml().unwrap();
15206        for raw in preserved {
15207            assert!(
15208                written.windows(raw.len()).any(|window| window == raw),
15209                "preserved subtree changed: {}",
15210                String::from_utf8_lossy(raw)
15211            );
15212        }
15213        let written_text = String::from_utf8(written.clone()).unwrap();
15214        assert!(written_text.contains(r#"x:keep="one &amp; two""#));
15215        assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
15216    }
15217
15218    #[test]
15219    fn every_corpus_chart_part_round_trips_structurally() {
15220        let Some(corpus) = require_or_skip_corpus() else {
15221            return;
15222        };
15223        verify_fetched_corpus(&corpus);
15224        let mut chart_parts = 0usize;
15225        let mut decks_with_charts = HashSet::new();
15226        for path in manifest_paths() {
15227            let package = OpcPackage::open(corpus.join(path))
15228                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
15229            for (part, xml) in &package.parts {
15230                if !is_chart_part(part) {
15231                    continue;
15232                }
15233                let parsed = CT_ChartSpace::from_xml(xml)
15234                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
15235                let written = parsed
15236                    .to_xml()
15237                    .unwrap_or_else(|error| panic!("{path} {part}: write failed: {error}"));
15238                let original_preserved = preserved_chartml_regions(xml)
15239                    .unwrap_or_else(|error| panic!("{path} {part}: original scan failed: {error}"));
15240                let written_preserved = preserved_chartml_regions(&written)
15241                    .unwrap_or_else(|error| panic!("{path} {part}: written scan failed: {error}"));
15242                assert_eq!(
15243                    original_preserved.len(),
15244                    written_preserved.len(),
15245                    "{path} {part}: preserved ChartML region count changed"
15246                );
15247                for (index, (original, written)) in original_preserved
15248                    .iter()
15249                    .zip(&written_preserved)
15250                    .enumerate()
15251                {
15252                    assert_eq!(
15253                        original.parent_path, written.parent_path,
15254                        "{path} {part}: preserved region {index} parent changed"
15255                    );
15256                    assert_eq!(
15257                        original.boundary, written.boundary,
15258                        "{path} {part}: preserved region {index} boundary changed"
15259                    );
15260                    assert_eq!(
15261                        original.sibling_order, written.sibling_order,
15262                        "{path} {part}: preserved region {index} order changed"
15263                    );
15264                    assert_eq!(
15265                        original.xml, written.xml,
15266                        "{path} {part}: preserved region {index} bytes changed"
15267                    );
15268                }
15269                let reparsed = CT_ChartSpace::from_xml(&written)
15270                    .unwrap_or_else(|error| panic!("{path} {part}: written parse failed: {error}"));
15271                assert_eq!(parsed, reparsed, "{path} {part}: core model changed");
15272                chart_parts += 1;
15273                decks_with_charts.insert(path);
15274            }
15275        }
15276        assert!(
15277            chart_parts > 0,
15278            "the pinned corpus contained no chart parts"
15279        );
15280        eprintln!(
15281            "ChartML corpus gate checked {chart_parts} chart parts across {} decks",
15282            decks_with_charts.len()
15283        );
15284    }
15285
15286    #[derive(Debug, Eq, PartialEq)]
15287    struct PreservedChartRegion {
15288        parent_path: Vec<Vec<u8>>,
15289        boundary: usize,
15290        sibling_order: usize,
15291        xml: Vec<u8>,
15292    }
15293
15294    struct PreservationFrame {
15295        local_name: Vec<u8>,
15296        boundary: usize,
15297        next_sibling: usize,
15298    }
15299
15300    fn preserved_chartml_regions(xml: &[u8]) -> Result<Vec<PreservedChartRegion>, String> {
15301        let mut reader = Reader::from_reader(xml);
15302        let mut buffer = Vec::new();
15303        let mut parents: Vec<PreservationFrame> = Vec::new();
15304        let mut preserved = Vec::new();
15305        loop {
15306            match reader.read_event_into(&mut buffer) {
15307                Ok(Event::Start(element)) => {
15308                    let child = local_name(element.name().as_ref()).to_vec();
15309                    if parents.is_empty() && child == b"chartSpace" {
15310                        parents.push(PreservationFrame {
15311                            local_name: child,
15312                            boundary: 0,
15313                            next_sibling: 0,
15314                        });
15315                    } else if let Some(parent) = parents.last()
15316                        && is_opaque_chartml_child(&parent.local_name, &child)
15317                    {
15318                        let boundary =
15319                            preserved_child_boundary(&parent.local_name, &child, parent.boundary);
15320                        let xml = capture_element(&mut reader, &element)
15321                            .map_err(|error| error.to_string())?;
15322                        record_preserved_region(&mut parents, &mut preserved, boundary, xml);
15323                        advance_preservation_boundary(&mut parents, &child);
15324                    } else {
15325                        advance_preservation_boundary(&mut parents, &child);
15326                        if is_chartml_preservation_parent(&child) {
15327                            parents.push(PreservationFrame {
15328                                local_name: child,
15329                                boundary: 0,
15330                                next_sibling: 0,
15331                            });
15332                        } else {
15333                            capture_element(&mut reader, &element)
15334                                .map_err(|error| error.to_string())?;
15335                        }
15336                    }
15337                }
15338                Ok(Event::Empty(element)) => {
15339                    let name = element.name();
15340                    let child = local_name(name.as_ref()).to_vec();
15341                    if let Some(parent) = parents.last()
15342                        && is_opaque_chartml_child(&parent.local_name, &child)
15343                    {
15344                        let boundary =
15345                            preserved_child_boundary(&parent.local_name, &child, parent.boundary);
15346                        let xml =
15347                            capture_empty_element(&element).map_err(|error| error.to_string())?;
15348                        record_preserved_region(&mut parents, &mut preserved, boundary, xml);
15349                    }
15350                    advance_preservation_boundary(&mut parents, &child);
15351                }
15352                Ok(
15353                    event @ (Event::Comment(_)
15354                    | Event::PI(_)
15355                    | Event::CData(_)
15356                    | Event::GeneralRef(_)),
15357                ) if !parents.is_empty() => {
15358                    let boundary = parents.last().expect("preservation parent").boundary;
15359                    let xml = capture_event(event).map_err(|error| error.to_string())?;
15360                    record_preserved_region(&mut parents, &mut preserved, boundary, xml);
15361                }
15362                Ok(Event::End(_)) => {
15363                    parents.pop();
15364                }
15365                Ok(Event::Eof) => break,
15366                Ok(_) => {}
15367                Err(error) => return Err(error.to_string()),
15368            }
15369            buffer.clear();
15370        }
15371        Ok(preserved)
15372    }
15373
15374    fn record_preserved_region(
15375        parents: &mut [PreservationFrame],
15376        preserved: &mut Vec<PreservedChartRegion>,
15377        boundary: usize,
15378        xml: Vec<u8>,
15379    ) {
15380        let sibling_order = parents.last().expect("preservation parent").next_sibling;
15381        parents
15382            .last_mut()
15383            .expect("preservation parent")
15384            .next_sibling += 1;
15385        preserved.push(PreservedChartRegion {
15386            parent_path: parents
15387                .iter()
15388                .map(|parent| parent.local_name.clone())
15389                .collect(),
15390            boundary,
15391            sibling_order,
15392            xml,
15393        });
15394    }
15395
15396    fn advance_preservation_boundary(parents: &mut [PreservationFrame], child: &[u8]) {
15397        let Some(parent) = parents.last_mut() else {
15398            return;
15399        };
15400        parent.boundary = parent
15401            .boundary
15402            .max(preserved_boundary_after(&parent.local_name, child));
15403    }
15404
15405    fn preserved_child_boundary(parent: &[u8], child: &[u8], current: usize) -> usize {
15406        match parent {
15407            b"chartSpace" => match child {
15408                b"date1904" => 0,
15409                b"lang" => 1,
15410                b"roundedCorners" => 2,
15411                b"style" => 3,
15412                b"clrMapOvr" => 4,
15413                b"pivotSource" => 5,
15414                b"protection" => 6,
15415                b"externalData" => 10,
15416                b"printSettings" => 11,
15417                b"userShapes" => 12,
15418                b"extLst" => 13,
15419                _ => current,
15420            },
15421            b"chart" => match child {
15422                b"pivotFmts" => 2,
15423                b"view3D" => 3,
15424                b"floor" => 4,
15425                b"sideWall" => 5,
15426                b"backWall" => 6,
15427                b"showDLblsOverMax" | b"extLst" => 11,
15428                _ => current,
15429            },
15430            _ => current,
15431        }
15432    }
15433
15434    fn preserved_boundary_after(parent: &[u8], child: &[u8]) -> usize {
15435        match parent {
15436            b"chartSpace" => match child {
15437                b"date1904" => 1,
15438                b"lang" => 2,
15439                b"roundedCorners" => 3,
15440                b"style" => 4,
15441                b"clrMapOvr" => 5,
15442                b"pivotSource" => 6,
15443                b"protection" => 7,
15444                b"chart" => 8,
15445                b"spPr" => 9,
15446                b"txPr" => 10,
15447                b"externalData" => 11,
15448                b"printSettings" => 12,
15449                b"userShapes" => 13,
15450                b"extLst" => 14,
15451                _ => 0,
15452            },
15453            b"chart" => match child {
15454                b"title" => 1,
15455                b"autoTitleDeleted" => 2,
15456                b"pivotFmts" => 3,
15457                b"view3D" => 4,
15458                b"floor" => 5,
15459                b"sideWall" => 6,
15460                b"backWall" => 7,
15461                b"plotArea" => 8,
15462                b"legend" => 9,
15463                b"plotVisOnly" => 10,
15464                b"dispBlanksAs" => 11,
15465                b"showDLblsOverMax" | b"extLst" => 11,
15466                _ => 0,
15467            },
15468            b"pieChart" => match child {
15469                b"ser" => 1,
15470                b"dLbls" => 2,
15471                b"firstSliceAng" => 3,
15472                _ => 0,
15473            },
15474            b"doughnutChart" => match child {
15475                b"ser" => 1,
15476                b"dLbls" => 2,
15477                b"firstSliceAng" => 3,
15478                b"holeSize" => 4,
15479                _ => 0,
15480            },
15481            b"areaChart" => match child {
15482                b"grouping" => 1,
15483                b"ser" => 2,
15484                b"dLbls" => 3,
15485                b"axId" => 4,
15486                _ => 0,
15487            },
15488            b"scatterChart" | b"radarChart" => match child {
15489                b"scatterStyle" | b"radarStyle" => 1,
15490                b"ser" => 2,
15491                b"dLbls" => 3,
15492                b"axId" => 4,
15493                _ => 0,
15494            },
15495            _ => 0,
15496        }
15497    }
15498
15499    fn is_opaque_chartml_child(parent: &[u8], child: &[u8]) -> bool {
15500        match parent {
15501            b"chartSpace" => !matches!(child, b"chart" | b"spPr" | b"txPr"),
15502            b"chart" => !matches!(
15503                child,
15504                b"title"
15505                    | b"autoTitleDeleted"
15506                    | b"plotArea"
15507                    | b"legend"
15508                    | b"plotVisOnly"
15509                    | b"dispBlanksAs"
15510            ),
15511            b"plotArea" => !matches!(
15512                child,
15513                b"barChart"
15514                    | b"lineChart"
15515                    | b"pieChart"
15516                    | b"doughnutChart"
15517                    | b"areaChart"
15518                    | b"scatterChart"
15519                    | b"radarChart"
15520                    | b"catAx"
15521                    | b"valAx"
15522                    | b"dateAx"
15523                    | b"serAx"
15524            ),
15525            b"pieChart" => !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng"),
15526            b"doughnutChart" => {
15527                !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng" | b"holeSize")
15528            }
15529            b"areaChart" => !matches!(child, b"grouping" | b"ser" | b"dLbls" | b"axId"),
15530            b"scatterChart" => !matches!(child, b"scatterStyle" | b"ser" | b"dLbls" | b"axId"),
15531            b"radarChart" => !matches!(child, b"radarStyle" | b"ser" | b"dLbls" | b"axId"),
15532            b"title" | b"legend" => true,
15533            _ => false,
15534        }
15535    }
15536
15537    fn is_chartml_preservation_parent(parent: &[u8]) -> bool {
15538        matches!(
15539            parent,
15540            b"chartSpace"
15541                | b"chart"
15542                | b"title"
15543                | b"plotArea"
15544                | b"pieChart"
15545                | b"doughnutChart"
15546                | b"areaChart"
15547                | b"scatterChart"
15548                | b"radarChart"
15549                | b"legend"
15550                | b"autoTitleDeleted"
15551                | b"plotVisOnly"
15552                | b"dispBlanksAs"
15553        )
15554    }
15555
15556    #[test]
15557    fn oxml_chart_is_an_explicit_publication_candidate() {
15558        let manifest = include_str!("../Cargo.toml");
15559        assert!(manifest.contains("name = \"oxml-chart\""));
15560        assert!(manifest.contains("version = \"0.13.1\""));
15561        assert!(manifest.contains("publish = true"));
15562        for dependency in [
15563            "oxml-core.workspace",
15564            "oxml-drawing.workspace",
15565            "quick-xml.workspace",
15566        ] {
15567            assert!(manifest.contains(dependency));
15568        }
15569        assert!(!manifest.contains("rpptx.workspace"));
15570        assert!(!manifest.contains("rdocx.workspace"));
15571    }
15572
15573    fn first_series_xml(xml: &[u8]) -> Option<Vec<u8>> {
15574        let mut reader = Reader::from_reader(xml);
15575        let mut buffer = Vec::new();
15576        loop {
15577            match reader.read_event_into(&mut buffer).ok()? {
15578                Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
15579                    return capture_element(&mut reader, &element).ok();
15580                }
15581                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
15582                    return capture_empty_element(&element).ok();
15583                }
15584                Event::Eof => return None,
15585                _ => {}
15586            }
15587            buffer.clear();
15588        }
15589    }
15590
15591    fn replace_once(haystack: &[u8], needle: &[u8], replacement: &[u8]) -> Vec<u8> {
15592        let offset = haystack
15593            .windows(needle.len())
15594            .position(|window| window == needle)
15595            .expect("serialized source series occurs in chart part");
15596        assert!(
15597            !haystack[offset + needle.len()..]
15598                .windows(needle.len())
15599                .any(|window| window == needle),
15600            "source series unexpectedly occurs more than once"
15601        );
15602        let mut result = Vec::with_capacity(haystack.len() - needle.len() + replacement.len());
15603        result.extend_from_slice(&haystack[..offset]);
15604        result.extend_from_slice(replacement);
15605        result.extend_from_slice(&haystack[offset + needle.len()..]);
15606        result
15607    }
15608
15609    fn assert_command_version(program: &str, arguments: &[&str], expected: &str) {
15610        let output = Command::new(program)
15611            .args(arguments)
15612            .output()
15613            .unwrap_or_else(|error| panic!("run {program} version check: {error}"));
15614        assert!(
15615            output.status.success(),
15616            "{program} version check failed: {}",
15617            String::from_utf8_lossy(&output.stderr)
15618        );
15619        let stdout = String::from_utf8(output.stdout).expect("version stdout is UTF-8");
15620        let stderr = String::from_utf8(output.stderr).expect("version stderr is UTF-8");
15621        let actual = if stdout.trim().is_empty() {
15622            stderr.trim()
15623        } else {
15624            stdout.trim()
15625        };
15626        assert_eq!(
15627            actual.lines().next(),
15628            Some(expected),
15629            "{program} version drift"
15630        );
15631    }
15632
15633    fn sha256(path: &Path) -> String {
15634        let output = Command::new("shasum")
15635            .args(["-a", "256"])
15636            .arg(path)
15637            .output()
15638            .unwrap_or_else(|error| panic!("{}: run shasum: {error}", path.display()));
15639        assert!(
15640            output.status.success(),
15641            "{}: shasum failed: {}",
15642            path.display(),
15643            String::from_utf8_lossy(&output.stderr)
15644        );
15645        String::from_utf8(output.stdout)
15646            .expect("shasum output is UTF-8")
15647            .split_whitespace()
15648            .next()
15649            .expect("shasum digest")
15650            .to_owned()
15651    }
15652
15653    fn workspace_root() -> PathBuf {
15654        Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
15655    }
15656
15657    fn corpus_dir() -> PathBuf {
15658        std::env::var_os("RDOCX_PPTX_CORPUS_DIR")
15659            .map(PathBuf::from)
15660            .unwrap_or_else(|| workspace_root().join("corpus/pptx"))
15661    }
15662
15663    fn require_or_skip_corpus() -> Option<PathBuf> {
15664        let corpus = corpus_dir();
15665        if corpus.is_dir() {
15666            return Some(corpus);
15667        }
15668        assert_ne!(
15669            std::env::var_os("RDOCX_PPTX_CORPUS_REQUIRED").as_deref(),
15670            Some(std::ffi::OsStr::new("1")),
15671            "the pinned corpus is required but {} does not exist",
15672            corpus.display()
15673        );
15674        eprintln!(
15675            "ChartML corpus gate skipped because {} is absent",
15676            corpus.display()
15677        );
15678        None
15679    }
15680
15681    fn verify_fetched_corpus(corpus: &Path) {
15682        let status = Command::new("python3")
15683            .arg(workspace_root().join("scripts/fetch_pptx_corpus.py"))
15684            .arg("--check")
15685            .env("RDOCX_PPTX_CORPUS_DIR", corpus)
15686            .status()
15687            .expect("run corpus verifier");
15688        assert!(status.success(), "pinned corpus verification failed");
15689    }
15690
15691    fn manifest_paths() -> Vec<&'static str> {
15692        let mut lines = MANIFEST.lines();
15693        assert_eq!(lines.next(), Some("path\tproducer\tsha256\turl"));
15694        let paths: Vec<_> = lines
15695            .map(|line| line.split('\t').next().expect("manifest path"))
15696            .collect();
15697        assert_eq!(paths.len(), EXPECTED_DECKS);
15698        paths
15699    }
15700
15701    fn is_chart_part(part: &str) -> bool {
15702        let Some(stem) = part
15703            .strip_prefix("/ppt/charts/chart")
15704            .and_then(|name| name.strip_suffix(".xml"))
15705        else {
15706            return false;
15707        };
15708        !stem.is_empty() && stem.bytes().all(|byte| byte.is_ascii_digit())
15709    }
15710
15711    fn plot_series(index: u32) -> String {
15712        format!(
15713            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>"#
15714        )
15715    }
15716
15717    fn sparse_category_series() -> &'static str {
15718        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>"#
15719    }
15720
15721    fn sparse_scatter_series() -> &'static str {
15722        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>"#
15723    }
15724
15725    fn chart_with_plot(plot: &str) -> String {
15726        format!(
15727            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>"#
15728        )
15729    }
15730
15731    fn chart_with_optional_axes(plot: &str, axes: bool) -> String {
15732        let axis_xml = if axes {
15733            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>"#
15734        } else {
15735            ""
15736        };
15737        format!(
15738            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>"#
15739        )
15740    }
15741
15742    fn numeric_plot_series(index: u32) -> String {
15743        format!(
15744            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>"#
15745        )
15746    }
15747
15748    fn scatter_without_x(index: u32) -> String {
15749        format!(
15750            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>"#
15751        )
15752    }
15753
15754    fn scatter_without_y(index: u32) -> String {
15755        format!(
15756            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>"#
15757        )
15758    }
15759
15760    fn remaining_plot_fixtures() -> Vec<(&'static str, String, bool)> {
15761        let series = plot_series(0);
15762        let scatter = numeric_plot_series(0);
15763        vec![
15764            (
15765                "pieChart",
15766                format!(
15767                    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>"#
15768                ),
15769                false,
15770            ),
15771            (
15772                "doughnutChart",
15773                format!(
15774                    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>"#
15775                ),
15776                false,
15777            ),
15778            (
15779                "areaChart",
15780                format!(
15781                    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>"#
15782                ),
15783                true,
15784            ),
15785            (
15786                "scatterChart",
15787                format!(
15788                    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>"#
15789                ),
15790                true,
15791            ),
15792            (
15793                "radarChart",
15794                format!(
15795                    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>"#
15796                ),
15797                true,
15798            ),
15799        ]
15800    }
15801
15802    fn bar_plot(extra: &str) -> String {
15803        format!(
15804            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>"#,
15805            plot_series(0)
15806        )
15807    }
15808
15809    fn line_plot(extra: &str) -> String {
15810        format!(
15811            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>"#,
15812            plot_series(0)
15813        )
15814    }
15815
15816    #[test]
15817    fn bar_and_line_plots_round_trip_and_render() {
15818        for (xml, is_bar) in [
15819            (chart_with_plot(&bar_plot("")), true),
15820            (chart_with_plot(&line_plot("")), false),
15821        ] {
15822            let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15823            let plots = parsed.chart.plot_area.plots().unwrap();
15824            assert_eq!(plots.len(), 1);
15825            assert_eq!(matches!(plots[0], Plot::Bar { .. }), is_bar);
15826            assert_eq!(matches!(plots[0], Plot::Line { .. }), !is_bar);
15827            assert_eq!(parsed.chart.plot_area.axes().unwrap().len(), 2);
15828            let written = parsed.to_xml().unwrap();
15829            let reparsed = CT_ChartSpace::from_xml(&written).unwrap();
15830            assert_eq!(parsed, reparsed);
15831        }
15832        if let Some(corpus) = require_or_skip_corpus() {
15833            verify_bar_and_line_viewer_gate(&corpus);
15834        }
15835    }
15836
15837    #[test]
15838    fn ppm_parser_preserves_whitespace_valued_first_pixels() {
15839        for first_pixel in *b" \n\r\t" {
15840            let mut ppm = b"P6\n1 1\n255\n".to_vec();
15841            ppm.extend_from_slice(&[first_pixel, 0x7f, 0xff]);
15842            let (width, height, pixels) = ppm_pixels(&ppm);
15843            assert_eq!((width, height), (1, 1));
15844            assert_eq!(pixels, [first_pixel, 0x7f, 0xff]);
15845        }
15846
15847        let ppm = b"P6\r\n1 1\r\n255\r\n\x7f\xff";
15848        assert_eq!(ppm_pixels(ppm).2, [b'\n', 0x7f, 0xff]);
15849    }
15850
15851    #[test]
15852    fn bar_and_line_plots_write_fixed_prefixes_in_schema_order() {
15853        for xml in [
15854            chart_with_plot(&bar_plot(r#"<q:extLst><x:bar-tail/></q:extLst>"#)),
15855            chart_with_plot(&line_plot(r#"<q:extLst><x:line-tail/></q:extLst>"#)),
15856        ] {
15857            let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15858            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15859            assert!(written.contains("<c:barChart") || written.contains("<c:lineChart"));
15860            assert!(!written.contains("<q:barChart"));
15861            assert!(!written.contains("<q:lineChart"));
15862            if written.contains("<c:barChart") {
15863                let positions: Vec<_> = [
15864                    "<c:barDir",
15865                    "<c:grouping",
15866                    "<q:varyColors",
15867                    "<c:ser",
15868                    "<c:dLbls",
15869                    "<c:gapWidth",
15870                    "<c:overlap",
15871                    "<q:serLines",
15872                    "<c:axId",
15873                    "<q:extLst",
15874                ]
15875                .iter()
15876                .map(|tag| written.find(tag).unwrap())
15877                .collect();
15878                assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15879            } else {
15880                let positions: Vec<_> = [
15881                    "<c:grouping",
15882                    "<q:varyColors",
15883                    "<c:ser",
15884                    "<c:dLbls",
15885                    "<q:dropLines",
15886                    "<c:marker",
15887                    "<c:smooth",
15888                    "<c:axId",
15889                    "<q:extLst",
15890                ]
15891                .iter()
15892                .map(|tag| written.find(tag).unwrap())
15893                .collect();
15894                assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15895            }
15896            assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
15897        }
15898
15899        for plot in [bar_plot(""), line_plot("")] {
15900            let first = plot_series(0);
15901            let with_series_boundary = plot.replacen(
15902                &first,
15903                &format!(
15904                    "{first}<!--between-series--><?series-boundary?>\n  {}",
15905                    plot_series(1)
15906                ),
15907                1,
15908            );
15909            let with_axis_boundary = with_series_boundary.replacen(
15910                r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
15911                r#"<q:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
15912  <q:axId val="-1884097184"/>"#,
15913                1,
15914            );
15915            let parsed =
15916                CT_ChartSpace::from_xml(chart_with_plot(&with_axis_boundary).as_bytes()).unwrap();
15917            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15918            assert!(written.contains("</c:ser><!--between-series--><?series-boundary?>\n  <c:ser"));
15919            assert!(written.contains(
15920                r#"<c:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
15921  <c:axId val="-1884097184"/>"#
15922            ));
15923            assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
15924        }
15925    }
15926
15927    #[test]
15928    fn malformed_bar_and_line_plots_return_errors_without_panicking() {
15929        let valid_series = plot_series(0);
15930        let cases = [
15931            format!(r#"<q:barChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15932            format!(r#"<q:barChart><q:barDir val="diagonal"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15933            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>"#),
15934            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>"#),
15935            format!(r#"<q:lineChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
15936            format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:marker val="maybe"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
15937            r#"<q:lineChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#.to_owned(),
15938            format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:axId val="1"/><q:axId val="1"/></q:lineChart>"#),
15939            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>"#),
15940            format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15941        ];
15942        for plot in cases {
15943            let xml = chart_with_plot(&plot);
15944            let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
15945            assert!(result.is_ok(), "plot parser panicked for {plot}");
15946            assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
15947        }
15948
15949        let missing_axis = format!(
15950            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>"#,
15951            bar_plot("")
15952        );
15953        assert!(CT_ChartSpace::from_xml(missing_axis.as_bytes()).is_err());
15954
15955        let values =
15956            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
15957        let series = vec![Series::new(0, 0, values)];
15958        let ids = [AxisId::new(1).unwrap(), AxisId::new(2).unwrap()];
15959        let mut invalid =
15960            Plot::bar(BarDirection::Column, BarGrouping::Clustered, series, ids).unwrap();
15961        if let Plot::Bar { gap_width, .. } = &mut invalid {
15962            *gap_width = 501;
15963        }
15964        assert!(invalid.validate().is_err());
15965    }
15966
15967    #[test]
15968    fn unsupported_and_combo_plots_remain_byte_preserved() {
15969        let bar = bar_plot("");
15970        let line = line_plot("");
15971        let combo = chart_with_plot(&format!("<!--before-->{bar}<?between?>{line}<!--after-->"));
15972        let parsed = CT_ChartSpace::from_xml(combo.as_bytes()).unwrap();
15973        assert!(parsed.chart.plot_area.plots().is_err());
15974        let written = parsed.to_xml().unwrap();
15975        for raw in [
15976            bar.as_bytes(),
15977            line.as_bytes(),
15978            br#"<!--before-->"#.as_slice(),
15979            br#"<?between?>"#.as_slice(),
15980            br#"<!--after-->"#.as_slice(),
15981        ] {
15982            assert!(written.windows(raw.len()).any(|window| window == raw));
15983        }
15984
15985        let unsupported = chart_with_plot(
15986            r#"<q:bar3DChart x:keep="three"><q:barDir val="col"/><q:ser><x:opaque/></q:ser></q:bar3DChart>"#,
15987        );
15988        let parsed = CT_ChartSpace::from_xml(unsupported.as_bytes()).unwrap();
15989        assert!(parsed.chart.plot_area.plots().is_err());
15990        assert!(
15991            String::from_utf8(parsed.to_xml().unwrap())
15992                .unwrap()
15993                .contains(r#"<q:bar3DChart x:keep="three">"#)
15994        );
15995    }
15996
15997    #[test]
15998    fn public_plot_edits_preserve_axes_and_unselected_payloads() {
15999        let xml = chart_with_plot(&bar_plot(
16000            r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#,
16001        ));
16002        let mut parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16003        let axes_before = parsed.chart.plot_area.axes().unwrap();
16004        match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
16005            Plot::Bar {
16006                gap_width,
16007                overlap,
16008                series,
16009                ..
16010            } => {
16011                *gap_width = 225;
16012                *overlap = -25;
16013                series[0].index = 7;
16014                series[0].values.values[0] = 9.0;
16015            }
16016            _ => panic!("expected bar plot"),
16017        }
16018        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
16019        assert!(written.contains(r#"<c:gapWidth val="225"/>"#));
16020        assert!(written.contains(r#"<c:overlap val="-25"/>"#));
16021        assert!(written.contains("<c:v>9</c:v>"));
16022        assert!(written.contains("<q:varyColors val=\"1\"/>"));
16023        assert!(written.find("<q:varyColors").unwrap() < written.find("<c:ser").unwrap());
16024        assert!(written.contains("<q:serLines><x:line/></q:serLines>"));
16025        assert!(written.contains(r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#));
16026        let reparsed = CT_ChartSpace::from_xml(written.as_bytes()).unwrap();
16027        assert_eq!(axes_before, reparsed.chart.plot_area.axes().unwrap());
16028
16029        let first = plot_series(0);
16030        let colliding_series =
16031            bar_plot("").replacen(&first, &format!("{first}{}", plot_series(1)), 1);
16032        let mut collision =
16033            CT_ChartSpace::from_xml(chart_with_plot(&colliding_series).as_bytes()).unwrap();
16034        if let Plot::Bar { series, .. } = &mut collision.chart.plot_area.plots_mut().unwrap()[0] {
16035            series[0].index = series[1].index;
16036            series[0].order = series[1].order;
16037        }
16038        let collision = String::from_utf8(collision.to_xml().unwrap()).unwrap();
16039        assert!(collision.find("<q:varyColors").unwrap() < collision.find("<c:ser").unwrap());
16040
16041        let mut inserted = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16042        if let Plot::Bar { series, .. } = &mut inserted.chart.plot_area.plots_mut().unwrap()[0] {
16043            let mut new_series = series[0].clone();
16044            new_series.index = 99;
16045            new_series.order = 99;
16046            series.insert(0, new_series);
16047        }
16048        let inserted = String::from_utf8(inserted.to_xml().unwrap()).unwrap();
16049        assert!(inserted.find("<q:varyColors").unwrap() < inserted.find("<c:ser").unwrap());
16050
16051        let axis_payload = bar_plot("").replacen(
16052            r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
16053            r#"<q:axId val="-1884094432" x:slot="category"/><!--axis-anchor--><q:axId val="-1884097184" x:slot="value"/>"#,
16054            1,
16055        );
16056        let mut swapped =
16057            CT_ChartSpace::from_xml(chart_with_plot(&axis_payload).as_bytes()).unwrap();
16058        if let Plot::Bar { axis_ids, .. } = &mut swapped.chart.plot_area.plots_mut().unwrap()[0] {
16059            axis_ids.swap(0, 1);
16060        }
16061        let swapped = String::from_utf8(swapped.to_xml().unwrap()).unwrap();
16062        let anchor = swapped.find("<!--axis-anchor-->").unwrap();
16063        let value_axis = swapped
16064            .find(r#"<c:axId val="-1884097184" x:slot="value"/>"#)
16065            .unwrap();
16066        let category_axis = swapped
16067            .find(r#"<c:axId val="-1884094432" x:slot="category"/>"#)
16068            .unwrap();
16069        let series_lines = swapped.find("<q:serLines").unwrap();
16070        assert!(series_lines < value_axis && series_lines < category_axis);
16071        assert!(anchor < value_axis && value_axis < category_axis);
16072
16073        let mut replaced = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16074        let Plot::Bar {
16075            series, axis_ids, ..
16076        } = &replaced.chart.plot_area.plots().unwrap()[0]
16077        else {
16078            panic!("expected bar plot");
16079        };
16080        let line = Plot::line(Grouping::Standard, series.clone(), *axis_ids).unwrap();
16081        replaced.chart.plot_area.plots_mut().unwrap()[0] = line;
16082        assert!(replaced.to_xml().is_err());
16083    }
16084
16085    #[test]
16086    fn every_corpus_bar_and_line_plot_round_trips_structurally() {
16087        let Some(corpus) = require_or_skip_corpus() else {
16088            return;
16089        };
16090        verify_fetched_corpus(&corpus);
16091        let mut typed_bar_count = 0usize;
16092        let mut typed_line_count = 0usize;
16093        let mut opaque_bar_count = 0usize;
16094        let mut opaque_line_count = 0usize;
16095        let mut opaque_combo_count = 0usize;
16096        for path in manifest_paths() {
16097            let package = OpcPackage::open(corpus.join(path))
16098                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
16099            for (part, xml) in &package.parts {
16100                if !is_chart_part(part) {
16101                    continue;
16102                }
16103                let chart = CT_ChartSpace::from_xml(xml)
16104                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
16105                match chart.chart.plot_area.plots() {
16106                    Ok(plots) => {
16107                        for plot in plots {
16108                            match plot {
16109                                Plot::Bar { .. } => typed_bar_count += 1,
16110                                Plot::Line { .. } => typed_line_count += 1,
16111                                _ => {}
16112                            }
16113                        }
16114                    }
16115                    Err(_) => {
16116                        let mut preserved_plot_count = 0usize;
16117                        for raw in chart.chart.plot_area.raw_children.at(0) {
16118                            if let Ok(Some(local)) = super::chart_root_local(
16119                                raw,
16120                                &chart.chart.plot_area.namespace_bindings,
16121                            ) {
16122                                match local.as_slice() {
16123                                    b"barChart" => {
16124                                        opaque_bar_count += 1;
16125                                        preserved_plot_count += 1;
16126                                    }
16127                                    b"lineChart" => {
16128                                        opaque_line_count += 1;
16129                                        preserved_plot_count += 1;
16130                                    }
16131                                    _ => {}
16132                                }
16133                            }
16134                        }
16135                        if preserved_plot_count > 1 {
16136                            opaque_combo_count += 1;
16137                        }
16138                    }
16139                }
16140                let written = chart
16141                    .to_xml()
16142                    .unwrap_or_else(|error| panic!("{path} {part}: chart write failed: {error}"));
16143                let reparsed = CT_ChartSpace::from_xml(&written).unwrap_or_else(|error| {
16144                    panic!("{path} {part}: written chart parse failed: {error}")
16145                });
16146                assert_eq!(chart, reparsed, "{path} {part}: chart model changed");
16147            }
16148        }
16149        assert_eq!(typed_bar_count, 11, "typed corpus bar coverage changed");
16150        assert_eq!(typed_line_count, 2, "typed corpus line coverage changed");
16151        assert_eq!(opaque_bar_count, 1, "preserved combo bar coverage changed");
16152        assert_eq!(
16153            opaque_line_count, 1,
16154            "preserved combo line coverage changed"
16155        );
16156        assert_eq!(opaque_combo_count, 1, "preserved combo coverage changed");
16157        assert_eq!(typed_bar_count + opaque_bar_count, 12);
16158        assert_eq!(typed_line_count + opaque_line_count, 3);
16159        eprintln!(
16160            "ChartML plot corpus gate checked {typed_bar_count} typed bar, {typed_line_count} typed line, and one preserved bar-line combination"
16161        );
16162    }
16163
16164    fn verify_remaining_plot_viewer_gate(corpus: &Path) {
16165        verify_fetched_corpus(corpus);
16166        assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
16167        assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
16168        let source = corpus.join("bar-chart.pptx");
16169        let source_sha = sha256(&source);
16170        let temp_root = std::env::temp_dir().join(format!(
16171            "oxml-chart-f122-viewer-gate-{}",
16172            std::process::id()
16173        ));
16174        if temp_root.exists() {
16175            fs::remove_dir_all(&temp_root).expect("remove stale F-122 evidence");
16176        }
16177        fs::create_dir_all(&temp_root).expect("create F-122 evidence root");
16178
16179        for (kind, plot, axes) in remaining_plot_fixtures() {
16180            let mut package = OpcPackage::open(&source).expect("open F-122 viewer source");
16181            package.set_part(
16182                "/ppt/charts/chart1.xml",
16183                CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes())
16184                    .expect("parse F-122 viewer chart")
16185                    .to_xml()
16186                    .expect("serialize F-122 viewer chart"),
16187            );
16188            let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
16189            package.save(&unbound).expect("save F-122 viewer candidate");
16190            let candidate_sha = sha256(&unbound);
16191            let evidence = temp_root.join(format!("{source_sha}-{candidate_sha}"));
16192            fs::create_dir(&evidence).expect("create SHA-bound F-122 evidence directory");
16193            let candidate = evidence.join(format!("{kind}-candidate.pptx"));
16194            fs::rename(&unbound, &candidate).expect("bind F-122 candidate to SHA");
16195            assert_eq!(sha256(&candidate), candidate_sha);
16196            let render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
16197            let bytes = fs::read(&render).expect("read F-122 viewer PPM");
16198            let (width, height, pixels) = ppm_pixels(&bytes);
16199            let left = width / 5;
16200            let right = width * 4 / 5;
16201            let top = height * 3 / 20;
16202            let bottom = height * 17 / 20;
16203            let mut nonblank = 0usize;
16204            for y in top..bottom {
16205                for x in left..right {
16206                    let offset = (y * width + x) * 3;
16207                    if pixels[offset..offset + 3]
16208                        .iter()
16209                        .any(|channel| *channel < 245)
16210                    {
16211                        nonblank += 1;
16212                    }
16213                }
16214            }
16215            const CHART_RECTANGLE_NONBLANK_THRESHOLD: usize = 1_000;
16216            assert!(
16217                nonblank >= CHART_RECTANGLE_NONBLANK_THRESHOLD,
16218                "{kind}: chart rectangle [{left},{top}) to [{right},{bottom}) contains {nonblank} nonblank pixels, below {CHART_RECTANGLE_NONBLANK_THRESHOLD}"
16219            );
16220            eprintln!(
16221                "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}",
16222                sha256(&render)
16223            );
16224        }
16225        fs::remove_dir_all(&temp_root).expect("remove F-122 viewer evidence");
16226    }
16227
16228    fn verify_bar_and_line_viewer_gate(corpus: &Path) {
16229        verify_fetched_corpus(corpus);
16230        assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
16231        assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
16232        let temp_root = std::env::temp_dir().join(format!(
16233            "oxml-chart-f121-viewer-gate-{}",
16234            std::process::id()
16235        ));
16236        if temp_root.exists() {
16237            fs::remove_dir_all(&temp_root).expect("remove stale F-121 evidence");
16238        }
16239        fs::create_dir_all(&temp_root).expect("create F-121 evidence root");
16240
16241        for (kind, deck) in [("bar", "bar-chart.pptx"), ("line", "line-chart.pptx")] {
16242            let source = corpus.join(deck);
16243            let original_sha = sha256(&source);
16244            let mut package = OpcPackage::open(&source)
16245                .unwrap_or_else(|error| panic!("{deck}: open viewer source: {error}"));
16246            let chart_part = "/ppt/charts/chart1.xml";
16247            let original_parts = package.parts.clone();
16248            let parsed = CT_ChartSpace::from_xml(
16249                package
16250                    .get_part(chart_part)
16251                    .unwrap_or_else(|| panic!("{deck}: missing {chart_part}")),
16252            )
16253            .unwrap_or_else(|error| panic!("{deck}: parse viewer chart: {error}"));
16254            assert!(
16255                parsed.chart.plot_area.plots().is_ok(),
16256                "{deck}: plot is opaque"
16257            );
16258            package.set_part(
16259                chart_part,
16260                parsed
16261                    .to_xml()
16262                    .unwrap_or_else(|error| panic!("{deck}: serialize viewer chart: {error}")),
16263            );
16264            for (part, bytes) in &original_parts {
16265                if part == chart_part {
16266                    assert_ne!(package.parts[part], *bytes, "{deck}: chart did not rewrite");
16267                } else {
16268                    assert_eq!(package.parts[part], *bytes, "{deck}: changed part {part}");
16269                }
16270            }
16271
16272            let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
16273            package
16274                .save(&unbound)
16275                .unwrap_or_else(|error| panic!("{deck}: save viewer candidate: {error}"));
16276            let candidate_sha = sha256(&unbound);
16277            let evidence = temp_root.join(format!("{original_sha}-{candidate_sha}"));
16278            fs::create_dir(&evidence).expect("create SHA-bound F-121 evidence directory");
16279            let original = evidence.join(format!("{kind}-original.pptx"));
16280            let candidate = evidence.join(format!("{kind}-candidate.pptx"));
16281            fs::copy(&source, &original).expect("copy SHA-bound viewer original");
16282            fs::rename(&unbound, &candidate).expect("bind viewer candidate to SHA");
16283            assert_eq!(sha256(&original), original_sha);
16284            assert_eq!(sha256(&candidate), candidate_sha);
16285
16286            let original_render = render_deck_to_ppm(&original, &evidence, kind, "original");
16287            let candidate_render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
16288            let original_bytes = fs::read(&original_render).expect("read original PPM");
16289            let candidate_bytes = fs::read(&candidate_render).expect("read candidate PPM");
16290            let normalized_mae = normalized_ppm_mae(&original_bytes, &candidate_bytes);
16291            assert!(
16292                normalized_mae <= PLOT_RENDER_NORMALIZED_MAE_THRESHOLD,
16293                "{deck}: normalized RGB MAE {normalized_mae:.8} exceeds {:.8}",
16294                PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
16295            );
16296            eprintln!(
16297                "F-121 {kind} viewer gate original deck {original_sha}, candidate deck {candidate_sha}, original render {}, candidate render {}, normalized RGB MAE {normalized_mae:.8} <= {:.8}",
16298                sha256(&original_render),
16299                sha256(&candidate_render),
16300                PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
16301            );
16302        }
16303        fs::remove_dir_all(&temp_root).expect("remove F-121 viewer evidence");
16304    }
16305
16306    fn render_deck_to_ppm(deck: &Path, evidence: &Path, kind: &str, side: &str) -> PathBuf {
16307        let profile = std::env::temp_dir().join(format!(
16308            "oxml-chart-f121-{kind}-{side}-profile-{}",
16309            std::process::id()
16310        ));
16311        if profile.exists() {
16312            fs::remove_dir_all(&profile).expect("remove stale F-121 viewer profile");
16313        }
16314        let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
16315        let conversion = Command::new("soffice")
16316            .args([
16317                "--headless",
16318                &profile_argument,
16319                "--convert-to",
16320                "pdf:impress_pdf_Export",
16321                "--outdir",
16322            ])
16323            .arg(evidence)
16324            .arg(deck)
16325            .output()
16326            .expect("run pinned LibreOffice F-121 viewer gate");
16327        assert!(
16328            conversion.status.success(),
16329            "{}: LibreOffice conversion failed: {}",
16330            deck.display(),
16331            String::from_utf8_lossy(&conversion.stderr)
16332        );
16333        let pdf = deck.with_extension("pdf");
16334        assert!(
16335            pdf.is_file(),
16336            "LibreOffice did not create {}",
16337            pdf.display()
16338        );
16339        let prefix = evidence.join(format!("{kind}-{side}"));
16340        let raster = Command::new("pdftoppm")
16341            .args(["-f", "1", "-singlefile", "-r", "150"])
16342            .arg(&pdf)
16343            .arg(&prefix)
16344            .output()
16345            .expect("run pinned Poppler F-121 raster gate");
16346        assert!(
16347            raster.status.success(),
16348            "{}: pdftoppm failed: {}",
16349            pdf.display(),
16350            String::from_utf8_lossy(&raster.stderr)
16351        );
16352        if profile.exists() {
16353            fs::remove_dir_all(&profile).expect("remove F-121 viewer profile");
16354        }
16355        prefix.with_extension("ppm")
16356    }
16357
16358    fn normalized_ppm_mae(left: &[u8], right: &[u8]) -> f64 {
16359        let (left_width, left_height, left_pixels) = ppm_pixels(left);
16360        let (right_width, right_height, right_pixels) = ppm_pixels(right);
16361        assert_eq!((left_width, left_height), (right_width, right_height));
16362        assert_eq!(left_pixels.len(), right_pixels.len());
16363        let difference: u64 = left_pixels
16364            .iter()
16365            .zip(right_pixels)
16366            .map(|(left, right)| u64::from(left.abs_diff(*right)))
16367            .sum();
16368        difference as f64 / (left_pixels.len() as f64 * 255.0)
16369    }
16370
16371    fn ppm_pixels(bytes: &[u8]) -> (usize, usize, &[u8]) {
16372        let mut cursor = 0usize;
16373        let mut tokens = Vec::new();
16374        while tokens.len() < 4 {
16375            while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
16376                cursor += 1;
16377            }
16378            if cursor < bytes.len() && bytes[cursor] == b'#' {
16379                while cursor < bytes.len() && bytes[cursor] != b'\n' {
16380                    cursor += 1;
16381                }
16382                continue;
16383            }
16384            let start = cursor;
16385            while cursor < bytes.len() && !bytes[cursor].is_ascii_whitespace() {
16386                cursor += 1;
16387            }
16388            tokens.push(std::str::from_utf8(&bytes[start..cursor]).expect("PPM header UTF-8"));
16389        }
16390        assert_eq!(tokens[0], "P6");
16391        let width = tokens[1].parse::<usize>().expect("PPM width");
16392        let height = tokens[2].parse::<usize>().expect("PPM height");
16393        assert_eq!(tokens[3], "255");
16394        assert!(
16395            cursor < bytes.len() && bytes[cursor].is_ascii_whitespace(),
16396            "PPM header must end with whitespace"
16397        );
16398        cursor += 1;
16399        let pixels = &bytes[cursor..];
16400        assert_eq!(pixels.len(), width * height * 3, "PPM pixel length");
16401        (width, height, pixels)
16402    }
16403}