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        note_reference_source: None,
1421        tab_aligned: None,
1422    })
1423}
1424
1425fn chart_text_properties(body: &CT_TextBody) -> Option<&CT_TextCharacterProperties> {
1426    body.paragraphs()
1427        .first()
1428        .and_then(|paragraph| paragraph.properties.as_ref())
1429        .and_then(|properties| properties.default_run_properties.as_ref())
1430}
1431
1432fn stroked_segment(start: Point, end: Point, color: Color, width: f64) -> PositionedElement {
1433    PositionedElement::Path(PathElement {
1434        path: Path {
1435            commands: vec![PathCommand::MoveTo(start), PathCommand::LineTo(end)],
1436            fill_rule: FillRule::NonZero,
1437        },
1438        fill: None,
1439        stroke: Some(Stroke::new(Paint::Solid(color), width)),
1440    })
1441}
1442
1443fn plot_series(plot: &Plot) -> &[Series] {
1444    match plot {
1445        Plot::Bar { series, .. }
1446        | Plot::Line { series, .. }
1447        | Plot::Pie { series, .. }
1448        | Plot::Doughnut { series, .. }
1449        | Plot::Area { series, .. }
1450        | Plot::Scatter { series, .. }
1451        | Plot::Radar { series, .. } => series,
1452    }
1453}
1454
1455fn plot_data_labels(plot: &Plot) -> Option<&CT_DLbls> {
1456    match plot {
1457        Plot::Bar { data_labels, .. }
1458        | Plot::Line { data_labels, .. }
1459        | Plot::Pie { data_labels, .. }
1460        | Plot::Doughnut { data_labels, .. }
1461        | Plot::Area { data_labels, .. }
1462        | Plot::Scatter { data_labels, .. }
1463        | Plot::Radar { data_labels, .. } => data_labels.as_ref(),
1464    }
1465}
1466
1467fn series_categories(
1468    series: &Series,
1469    number_format: Option<&NumberFormat>,
1470) -> Result<Vec<(usize, String)>> {
1471    let Some(categories) = &series.categories else {
1472        return Ok(Vec::new());
1473    };
1474    match categories {
1475        AxisData::String(data) => {
1476            let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
1477            indexes
1478                .into_iter()
1479                .zip(&data.values)
1480                .map(|(index, value)| {
1481                    usize::try_from(index)
1482                        .map(|index| (index, value.clone()))
1483                        .map_err(|_| ChartError::InvalidValue {
1484                            element: "c:cat/c:pt/@idx".to_owned(),
1485                            value: index.to_string(),
1486                        })
1487                })
1488                .collect()
1489        }
1490        AxisData::Numeric(data) => {
1491            let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
1492            let cache_format;
1493            let format = if let Some(format) = number_format {
1494                format
1495            } else {
1496                cache_format = NumberFormat::new(data.format_code.clone(), false)?;
1497                &cache_format
1498            };
1499            indexes
1500                .into_iter()
1501                .zip(&data.values)
1502                .map(|(index, value)| {
1503                    let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
1504                        element: "c:cat/c:pt/@idx".to_owned(),
1505                        value: index.to_string(),
1506                    })?;
1507                    let text = format.format_value(*value)?;
1508                    Ok((index, text))
1509                })
1510                .collect()
1511        }
1512    }
1513}
1514
1515fn series_name(series: &Series, index: usize) -> String {
1516    series
1517        .name
1518        .as_ref()
1519        .and_then(|name| name.values.first())
1520        .cloned()
1521        .unwrap_or_else(|| format!("Series {}", index + 1))
1522}
1523
1524fn render_data_labels(
1525    plot: &Plot,
1526    bounds: Rect,
1527    blanks: DispBlanksAs,
1528    options: GeometryOptions,
1529    fonts: &mut FontManager,
1530    elements: &mut Vec<PositionedElement>,
1531) -> Result<()> {
1532    let plot_labels = plot_data_labels(plot);
1533    let anchors = plot_label_anchors(plot, bounds, blanks, options)?;
1534    for (series_index, series) in plot_series(plot).iter().enumerate() {
1535        let Some(labels) = series.data_labels.as_ref().or(plot_labels) else {
1536            continue;
1537        };
1538        let overrides = point_label_overrides(labels)?;
1539        let mut categories = None;
1540        let mut bubble_sizes = None;
1541        let (_, values) = logical_numeric_values(&series.values)?;
1542        let mut percentage_total = None;
1543        for (logical_index, value) in values {
1544            let point_override = overrides.get(&logical_index);
1545            if point_override.is_some_and(|point| point.deleted == Some(true)) {
1546                continue;
1547            }
1548            let effective_labels = point_override
1549                .map(|point| point.apply_to(labels))
1550                .unwrap_or_else(|| labels.clone());
1551            let labels = &effective_labels;
1552            let Some(anchor) = anchors.get(&(series_index, logical_index)).copied() else {
1553                continue;
1554            };
1555            let mut fields = Vec::new();
1556            if labels.show_series_name {
1557                fields.push(series_name(series, series_index));
1558            }
1559            if labels.show_category_name {
1560                if categories.is_none() {
1561                    categories = Some(
1562                        series_categories(series, None)?
1563                            .into_iter()
1564                            .collect::<BTreeMap<_, _>>(),
1565                    );
1566                }
1567                if let Some(category) = categories
1568                    .as_ref()
1569                    .and_then(|categories| categories.get(&logical_index))
1570                {
1571                    fields.push(category.clone());
1572                }
1573            }
1574            if labels.show_value {
1575                fields.push(format_axis_value(labels.number_format.as_ref(), value)?);
1576            }
1577            if labels.show_percent {
1578                let total = if let Some(total) = percentage_total {
1579                    total
1580                } else {
1581                    let total = series
1582                        .values
1583                        .values
1584                        .iter()
1585                        .copied()
1586                        .filter(|value| *value > 0.0)
1587                        .try_fold(0.0, |total, value| {
1588                            checked_geometry_sum(total, value, "data-label percentage total")
1589                        })?;
1590                    percentage_total = Some(total);
1591                    total
1592                };
1593                if total > 0.0 {
1594                    let text = if let Some(format) = &labels.number_format {
1595                        format.format_value(value / total)?
1596                    } else {
1597                        NumberFormat::new("0%".to_owned(), false)?.format_value(value / total)?
1598                    };
1599                    fields.push(text);
1600                }
1601            }
1602            if labels.show_bubble_size {
1603                if bubble_sizes.is_none() {
1604                    bubble_sizes = Some(
1605                        series
1606                            .bubble_size
1607                            .as_ref()
1608                            .map(logical_numeric_values)
1609                            .transpose()?
1610                            .map(|(_, values)| values.into_iter().collect::<BTreeMap<_, _>>())
1611                            .unwrap_or_default(),
1612                    );
1613                }
1614                if let Some(value) = bubble_sizes
1615                    .as_ref()
1616                    .and_then(|sizes| sizes.get(&logical_index))
1617                {
1618                    fields.push(format_axis_value(labels.number_format.as_ref(), *value)?);
1619                }
1620            }
1621            if fields.is_empty() {
1622                continue;
1623            }
1624            let separator = labels.separator.as_deref().unwrap_or(", ");
1625            let text = fields.join(separator);
1626            let origin = anchor.origin(labels.position);
1627            elements.push(PositionedElement::Text(shape_label(fonts, &text, origin)?));
1628        }
1629    }
1630    Ok(())
1631}
1632
1633#[derive(Clone, Debug, Default)]
1634struct PointLabelOverride {
1635    index: Option<usize>,
1636    deleted: Option<bool>,
1637    number_format: Option<NumberFormat>,
1638    position: Option<DataLabelPosition>,
1639    separator: Option<String>,
1640    show_legend_key: Option<bool>,
1641    show_value: Option<bool>,
1642    show_category_name: Option<bool>,
1643    show_series_name: Option<bool>,
1644    show_percent: Option<bool>,
1645    show_bubble_size: Option<bool>,
1646}
1647
1648impl PointLabelOverride {
1649    fn apply_to(&self, labels: &CT_DLbls) -> CT_DLbls {
1650        let mut effective = labels.clone();
1651        if let Some(value) = &self.number_format {
1652            effective.number_format = Some(value.clone());
1653        }
1654        if let Some(value) = self.position {
1655            effective.position = Some(value);
1656        }
1657        if let Some(value) = &self.separator {
1658            effective.separator = Some(value.clone());
1659        }
1660        if let Some(value) = self.show_legend_key {
1661            effective.show_legend_key = value;
1662        }
1663        if let Some(value) = self.show_value {
1664            effective.show_value = value;
1665        }
1666        if let Some(value) = self.show_category_name {
1667            effective.show_category_name = value;
1668        }
1669        if let Some(value) = self.show_series_name {
1670            effective.show_series_name = value;
1671        }
1672        if let Some(value) = self.show_percent {
1673            effective.show_percent = value;
1674        }
1675        if let Some(value) = self.show_bubble_size {
1676            effective.show_bubble_size = value;
1677        }
1678        effective
1679    }
1680}
1681
1682fn point_label_overrides(labels: &CT_DLbls) -> Result<BTreeMap<usize, PointLabelOverride>> {
1683    let mut namespaces = chart_namespace_defaults();
1684    for (name, namespace) in &labels.namespace_declarations {
1685        let prefix = if name == "xmlns" {
1686            Vec::new()
1687        } else if let Some(prefix) = name.strip_prefix("xmlns:") {
1688            prefix.as_bytes().to_vec()
1689        } else {
1690            continue;
1691        };
1692        upsert_namespace_binding(&mut namespaces, prefix, namespace.clone());
1693    }
1694    let mut overrides = BTreeMap::new();
1695    for raw in labels.raw_children.at(0) {
1696        if chart_root_local(raw, &namespaces)?.as_deref() != Some(b"dLbl") {
1697            continue;
1698        }
1699        let point = parse_point_label_override(raw, &namespaces)?;
1700        let index = point
1701            .index
1702            .ok_or_else(|| ChartError::MissingElement("c:dLbl/c:idx".to_owned()))?;
1703        if overrides.insert(index, point).is_some() {
1704            return Err(ChartError::DuplicateElement(format!(
1705                "c:dLbl[c:idx={index}]"
1706            )));
1707        }
1708    }
1709    Ok(overrides)
1710}
1711
1712fn parse_point_label_override(
1713    xml: &[u8],
1714    inherited: &NamespaceBindings,
1715) -> Result<PointLabelOverride> {
1716    let mut reader = Reader::from_reader(xml);
1717    let mut buffer = Vec::new();
1718    let mut namespaces = None;
1719    let mut state = PointLabelOverride::default();
1720    let mut order = 0;
1721    loop {
1722        match reader
1723            .read_event_into(&mut buffer)
1724            .map_err(OxmlError::from)?
1725        {
1726            Event::Start(element) if namespaces.is_none() => {
1727                if !matches_local_name(element.name().as_ref(), b"dLbl")
1728                    || !element_is_in_namespace(&element, C_NS, inherited)?
1729                {
1730                    return Err(ChartError::UnexpectedElement(element_name(&element)));
1731                }
1732                namespaces = Some(chart_bindings(inherited, &element)?);
1733            }
1734            Event::Start(element) => {
1735                let bindings = namespaces
1736                    .as_ref()
1737                    .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
1738                let name = chart_child_local(&element, bindings)?;
1739                let raw = capture_element(&mut reader, &element)?;
1740                parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
1741            }
1742            Event::Empty(element) => {
1743                let bindings = namespaces
1744                    .as_ref()
1745                    .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
1746                let name = chart_child_local(&element, bindings)?;
1747                let raw = capture_empty_element(&element)?;
1748                parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
1749            }
1750            Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbl") => {
1751                if state.index.is_none() {
1752                    return Err(ChartError::MissingElement("c:dLbl/c:idx".to_owned()));
1753                }
1754                return Ok(state);
1755            }
1756            Event::Eof => return Err(missing_end("c:dLbl")),
1757            _ => {}
1758        }
1759        buffer.clear();
1760    }
1761}
1762
1763fn parse_point_label_child(
1764    state: &mut PointLabelOverride,
1765    name: Option<&[u8]>,
1766    raw: &[u8],
1767    namespaces: &NamespaceBindings,
1768    order: &mut usize,
1769) -> Result<()> {
1770    let Some(name) = name else {
1771        return Ok(());
1772    };
1773    let rank = match name {
1774        b"idx" => 0,
1775        b"delete" => 1,
1776        b"layout" => 2,
1777        b"tx" => 3,
1778        b"numFmt" => 4,
1779        b"spPr" => 5,
1780        b"txPr" => 6,
1781        b"dLblPos" => 7,
1782        b"showLegendKey" => 8,
1783        b"showVal" => 9,
1784        b"showCatName" => 10,
1785        b"showSerName" => 11,
1786        b"showPercent" => 12,
1787        b"showBubbleSize" => 13,
1788        b"separator" => 14,
1789        b"showLeaderLines" => 15,
1790        b"leaderLines" => 16,
1791        b"extLst" => 17,
1792        _ => return Ok(()),
1793    };
1794    if rank < *order {
1795        return Err(ChartError::InvalidValue {
1796            element: "c:dLbl".to_owned(),
1797            value: format!("c:{} is out of schema order", String::from_utf8_lossy(name)),
1798        });
1799    }
1800    *order = rank;
1801    match name {
1802        b"idx" => {
1803            let (index, _) = parse_u32_scalar(raw, "idx")?;
1804            let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
1805                element: "c:dLbl/c:idx".to_owned(),
1806                value: index.to_string(),
1807            })?;
1808            set_once(&mut state.index, index, "c:dLbl/c:idx")?;
1809        }
1810        b"delete" => set_once(
1811            &mut state.deleted,
1812            parse_bool_value(raw, "delete")?.0,
1813            "c:dLbl/c:delete",
1814        )?,
1815        b"numFmt" => set_once(
1816            &mut state.number_format,
1817            NumberFormat::from_xml(raw)?,
1818            "c:dLbl/c:numFmt",
1819        )?,
1820        b"dLblPos" => {
1821            let (value, _) = scalar_value(raw, "dLblPos")?;
1822            let value =
1823                value.ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
1824            let position = DataLabelPosition::parse(&value)
1825                .ok_or_else(|| invalid_attribute("dLblPos", "val", value))?;
1826            set_once(&mut state.position, position, "c:dLbl/c:dLblPos")?;
1827        }
1828        b"showLegendKey" => set_once(
1829            &mut state.show_legend_key,
1830            parse_bool_value(raw, "showLegendKey")?.0,
1831            "c:dLbl/c:showLegendKey",
1832        )?,
1833        b"showVal" => set_once(
1834            &mut state.show_value,
1835            parse_bool_value(raw, "showVal")?.0,
1836            "c:dLbl/c:showVal",
1837        )?,
1838        b"showCatName" => set_once(
1839            &mut state.show_category_name,
1840            parse_bool_value(raw, "showCatName")?.0,
1841            "c:dLbl/c:showCatName",
1842        )?,
1843        b"showSerName" => set_once(
1844            &mut state.show_series_name,
1845            parse_bool_value(raw, "showSerName")?.0,
1846            "c:dLbl/c:showSerName",
1847        )?,
1848        b"showPercent" => set_once(
1849            &mut state.show_percent,
1850            parse_bool_value(raw, "showPercent")?.0,
1851            "c:dLbl/c:showPercent",
1852        )?,
1853        b"showBubbleSize" => set_once(
1854            &mut state.show_bubble_size,
1855            parse_bool_value(raw, "showBubbleSize")?.0,
1856            "c:dLbl/c:showBubbleSize",
1857        )?,
1858        b"separator" => set_once(
1859            &mut state.separator,
1860            parse_text_element(raw, b"separator", namespaces)?.0,
1861            "c:dLbl/c:separator",
1862        )?,
1863        _ => {}
1864    }
1865    Ok(())
1866}
1867
1868#[derive(Clone, Copy, Debug)]
1869struct LabelAnchor {
1870    center: Point,
1871    base: Point,
1872    end: Point,
1873    direction: Point,
1874}
1875
1876impl LabelAnchor {
1877    const fn point(center: Point) -> Self {
1878        Self {
1879            center,
1880            base: center,
1881            end: center,
1882            direction: Point { x: 0.0, y: 0.0 },
1883        }
1884    }
1885
1886    const fn segment(center: Point, base: Point, end: Point) -> Self {
1887        Self {
1888            center,
1889            base,
1890            end,
1891            direction: Point {
1892                x: end.x - base.x,
1893                y: end.y - base.y,
1894            },
1895        }
1896    }
1897
1898    const fn directed_segment(center: Point, base: Point, end: Point, direction: Point) -> Self {
1899        Self {
1900            center,
1901            base,
1902            end,
1903            direction,
1904        }
1905    }
1906
1907    fn origin(self, position: Option<DataLabelPosition>) -> Point {
1908        let mut origin = self.center;
1909        match position.unwrap_or(DataLabelPosition::BestFit) {
1910            DataLabelPosition::Bottom => origin.y += 8.0,
1911            DataLabelPosition::Top => origin.y -= 5.0,
1912            DataLabelPosition::Left => origin.x -= 8.0,
1913            DataLabelPosition::Right => origin.x += 8.0,
1914            DataLabelPosition::InsideBase => {
1915                origin = self.along_segment(self.base, 5.0, true).unwrap_or(Point {
1916                    x: origin.x,
1917                    y: origin.y + 8.0,
1918                });
1919            }
1920            DataLabelPosition::InsideEnd => {
1921                origin = self.along_segment(self.end, -5.0, true).unwrap_or(origin);
1922            }
1923            DataLabelPosition::OutsideEnd => {
1924                origin = self.along_segment(self.end, 5.0, false).unwrap_or(Point {
1925                    x: origin.x,
1926                    y: origin.y - 5.0,
1927                });
1928            }
1929            DataLabelPosition::BestFit | DataLabelPosition::Center => {}
1930        }
1931        origin
1932    }
1933
1934    fn along_segment(self, origin: Point, distance: f64, clamp_inside: bool) -> Option<Point> {
1935        let dx = self.end.x - self.base.x;
1936        let dy = self.end.y - self.base.y;
1937        let length = dx.hypot(dy);
1938        let (dx, dy, direction_length) = if length.is_finite() && length > 0.0 {
1939            (dx, dy, length)
1940        } else {
1941            let direction_length = self.direction.x.hypot(self.direction.y);
1942            if !direction_length.is_finite() || direction_length == 0.0 {
1943                return None;
1944            }
1945            (self.direction.x, self.direction.y, direction_length)
1946        };
1947        let distance = if clamp_inside {
1948            distance.clamp(-length / 2.0, length / 2.0)
1949        } else {
1950            distance
1951        };
1952        Some(Point {
1953            x: origin.x + dx / direction_length * distance,
1954            y: origin.y + dy / direction_length * distance,
1955        })
1956    }
1957}
1958
1959fn plot_label_anchors(
1960    plot: &Plot,
1961    bounds: Rect,
1962    blanks: DispBlanksAs,
1963    options: GeometryOptions,
1964) -> Result<BTreeMap<(usize, usize), LabelAnchor>> {
1965    let mut anchors = BTreeMap::new();
1966    match plot {
1967        Plot::Bar {
1968            direction,
1969            grouping,
1970            gap_width,
1971            overlap,
1972            series,
1973            ..
1974        } => {
1975            for bar in bar_rectangles(
1976                *direction, *grouping, *gap_width, *overlap, series, bounds, options,
1977            )? {
1978                if !point_in_rect(bar.end, bounds) {
1979                    continue;
1980                }
1981                let visible_base = Point {
1982                    x: bar.base.x.clamp(bounds.x, bounds.x + bounds.width),
1983                    y: bar.base.y.clamp(bounds.y, bounds.y + bounds.height),
1984                };
1985                let visible_end = Point {
1986                    x: bar.end.x.clamp(bounds.x, bounds.x + bounds.width),
1987                    y: bar.end.y.clamp(bounds.y, bounds.y + bounds.height),
1988                };
1989                anchors.insert(
1990                    (bar.series_index, bar.logical_index),
1991                    LabelAnchor::directed_segment(
1992                        Point {
1993                            x: (visible_base.x + visible_end.x) / 2.0,
1994                            y: (visible_base.y + visible_end.y) / 2.0,
1995                        },
1996                        visible_base,
1997                        visible_end,
1998                        Point {
1999                            x: bar.end.x - bar.base.x,
2000                            y: bar.end.y - bar.base.y,
2001                        },
2002                    ),
2003                );
2004            }
2005        }
2006        Plot::Line {
2007            grouping, series, ..
2008        }
2009        | Plot::Area {
2010            grouping, series, ..
2011        } => {
2012            for (series_index, points) in
2013                category_label_points(*grouping, series, bounds, blanks, options)?
2014                    .into_iter()
2015                    .enumerate()
2016            {
2017                for (logical_index, point) in points {
2018                    anchors.insert((series_index, logical_index), LabelAnchor::point(point));
2019                }
2020            }
2021        }
2022        Plot::Scatter { series, .. } => {
2023            let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
2024            let x_mapping = options.scatter_x.unwrap_or(AxisMapping {
2025                domain: x_domain,
2026                orientation: Orientation::MinMax,
2027            });
2028            let y_mapping = options.scatter_y.unwrap_or(AxisMapping {
2029                domain: y_domain,
2030                orientation: Orientation::MinMax,
2031            });
2032            for (series_index, series) in series.iter().enumerate() {
2033                let indexed = matched_scatter_points(series)?;
2034                let points = scatter_points(
2035                    &indexed,
2036                    x_mapping.domain,
2037                    y_mapping.domain,
2038                    bounds,
2039                    x_mapping.orientation,
2040                    y_mapping.orientation,
2041                )?;
2042                for ((logical_index, _, _), point) in indexed.into_iter().zip(points) {
2043                    anchors.insert((series_index, logical_index), LabelAnchor::point(point));
2044                }
2045            }
2046        }
2047        Plot::Radar { series, .. } => {
2048            let center = Point {
2049                x: bounds.x + bounds.width / 2.0,
2050                y: bounds.y + bounds.height / 2.0,
2051            };
2052            let category_count = series_category_count(series)?;
2053            for (series_index, points) in radar_series_points(series, bounds, options)?
2054                .into_iter()
2055                .enumerate()
2056            {
2057                for (logical_index, point) in points {
2058                    let spoke = radial_point(
2059                        center,
2060                        1.0,
2061                        oriented_category_index(
2062                            logical_index,
2063                            category_count,
2064                            options.category_orientation,
2065                        ),
2066                        category_count,
2067                    );
2068                    anchors.insert(
2069                        (series_index, logical_index),
2070                        LabelAnchor::directed_segment(
2071                            point,
2072                            center,
2073                            point,
2074                            Point {
2075                                x: spoke.x - center.x,
2076                                y: spoke.y - center.y,
2077                            },
2078                        ),
2079                    );
2080                }
2081            }
2082        }
2083        Plot::Pie {
2084            first_slice_angle,
2085            series,
2086            ..
2087        } => {
2088            for slice in pie_slices(*first_slice_angle, None, series, bounds)? {
2089                anchors.insert(
2090                    (slice.series_index, slice.logical_index),
2091                    LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2092                );
2093            }
2094        }
2095        Plot::Doughnut {
2096            first_slice_angle,
2097            hole_size,
2098            series,
2099            ..
2100        } => {
2101            for slice in pie_slices(*first_slice_angle, Some(*hole_size), series, bounds)? {
2102                anchors.insert(
2103                    (slice.series_index, slice.logical_index),
2104                    LabelAnchor::segment(slice.anchor, slice.base, slice.end),
2105                );
2106            }
2107        }
2108    }
2109    anchors.retain(|_, anchor| point_in_rect(anchor.end, bounds));
2110    Ok(anchors)
2111}
2112
2113fn category_label_points(
2114    grouping: Grouping,
2115    series: &[Series],
2116    bounds: Rect,
2117    blanks: DispBlanksAs,
2118    options: GeometryOptions,
2119) -> Result<Vec<Vec<(usize, Point)>>> {
2120    validate_series_values(series)?;
2121    let logical_series = logical_series_values(series)?;
2122    let category_count = series_category_count(series)?;
2123    let calculated_series = if blanks == DispBlanksAs::Zero {
2124        zero_filled_series(&logical_series, category_count)
2125    } else {
2126        logical_series.clone()
2127    };
2128    let layers = stacked_series_bounds(
2129        &calculated_series,
2130        grouping != Grouping::Standard,
2131        grouping == Grouping::PercentStacked,
2132    )?;
2133    let domain = options
2134        .value
2135        .map(|mapping| mapping.domain)
2136        .unwrap_or(domain_from_layers(&layers)?);
2137    let value_orientation = options
2138        .value
2139        .map(|mapping| mapping.orientation)
2140        .unwrap_or(Orientation::MinMax);
2141    let mut point_series = Vec::with_capacity(layers.len());
2142    for (series_index, layer) in layers.iter().enumerate() {
2143        let present: BTreeSet<_> = logical_series[series_index]
2144            .1
2145            .iter()
2146            .map(|(index, _)| *index)
2147            .collect();
2148        let indexed: Vec<_> = layer
2149            .iter()
2150            .filter(|(index, _, _)| present.contains(index))
2151            .map(|(index, _, upper)| (*index, *upper))
2152            .collect();
2153        let points = category_points(
2154            &indexed,
2155            category_count,
2156            domain,
2157            bounds,
2158            options.category_orientation,
2159            value_orientation,
2160        )?;
2161        point_series.push(
2162            indexed
2163                .into_iter()
2164                .map(|(index, _)| index)
2165                .zip(points)
2166                .collect(),
2167        );
2168    }
2169    Ok(point_series)
2170}
2171
2172fn point_in_rect(point: Point, bounds: Rect) -> bool {
2173    const EPSILON: f64 = 1.0e-9;
2174    point.x >= bounds.x - EPSILON
2175        && point.x <= bounds.x + bounds.width + EPSILON
2176        && point.y >= bounds.y - EPSILON
2177        && point.y <= bounds.y + bounds.height + EPSILON
2178}
2179
2180fn resolve_series_colours(
2181    plot: &Plot,
2182    theme: &CT_OfficeStyleSheet,
2183    color_map: &ColorMap,
2184) -> Result<Vec<Color>> {
2185    let lookup = theme_colour_lookup(theme);
2186    plot_series(plot)
2187        .iter()
2188        .enumerate()
2189        .map(|(index, series)| {
2190            if let Some(properties) = series.sp_pr.as_ref() {
2191                if let Some(fill) = properties.fill.as_ref() {
2192                    return resolve_series_paint(fill, index, "fill", theme, color_map, &lookup);
2193                }
2194                if let Some(fill) = properties.line.as_ref().and_then(|line| line.fill.as_ref()) {
2195                    return resolve_series_paint(fill, index, "line", theme, color_map, &lookup);
2196                }
2197            }
2198            resolve_series_accent(index, theme, color_map, &lookup)
2199        })
2200        .collect()
2201}
2202
2203fn resolve_series_paint(
2204    fill: &Fill,
2205    series_index: usize,
2206    source: &str,
2207    theme: &CT_OfficeStyleSheet,
2208    color_map: &ColorMap,
2209    lookup: &[(&str, RgbColor)],
2210) -> Result<Color> {
2211    match fill {
2212        Fill::NoFill(_) => Ok(Color {
2213            r: 0.0,
2214            g: 0.0,
2215            b: 0.0,
2216            a: 0.0,
2217        }),
2218        Fill::Solid(solid) => {
2219            let choice = solid
2220                .color
2221                .as_ref()
2222                .ok_or_else(|| ChartError::InvalidValue {
2223                    element: format!("c:ser[{series_index}] direct {source} paint"),
2224                    value: "solid fill has no colour choice".to_owned(),
2225                })?;
2226            resolve_direct_chart_colour(choice, theme, color_map, lookup, || {
2227                format!("c:ser[{series_index}] direct {source} paint")
2228            })
2229        }
2230        Fill::Gradient(_) => unsupported_series_paint(series_index, source, "gradient"),
2231        Fill::Pattern(_) => unsupported_series_paint(series_index, source, "pattern"),
2232        Fill::Blip(_) => unsupported_series_paint(series_index, source, "picture"),
2233    }
2234}
2235
2236fn resolve_direct_chart_colour(
2237    choice: &ColorChoice,
2238    theme: &CT_OfficeStyleSheet,
2239    color_map: &ColorMap,
2240    lookup: &[(&str, RgbColor)],
2241    element: impl FnOnce() -> String,
2242) -> Result<Color> {
2243    let ColorChoice::Scheme {
2244        value, transforms, ..
2245    } = choice
2246    else {
2247        return resolve_chart_colour(choice, color_map, lookup, element);
2248    };
2249    let theme_slot =
2250        mapped_theme_slot(value, color_map).ok_or_else(|| ChartError::InvalidValue {
2251            element: element(),
2252            value: format!("no concrete theme slot is available for scheme colour {value}"),
2253        })?;
2254    let theme_choice = theme.theme_elements.color_scheme.color(theme_slot);
2255    let base = concrete_theme_colour(theme_choice).ok_or_else(|| ChartError::InvalidValue {
2256        element: format!("a:clrScheme/a:{}", theme_slot.as_str()),
2257        value: "selected theme slot has no concrete sRGB or system fallback colour".to_owned(),
2258    })?;
2259    let mut combined = theme_choice.transforms().to_vec();
2260    combined.extend_from_slice(transforms);
2261    Ok(resolved_layout_colour(apply_color_transforms(
2262        base, &combined,
2263    )))
2264}
2265
2266fn mapped_theme_slot(value: &str, color_map: &ColorMap) -> Option<ThemeColorSlot> {
2267    let mapped = match value {
2268        "bg1" => color_map.theme_slot(ColorMapSlot::Background1),
2269        "tx1" => color_map.theme_slot(ColorMapSlot::Text1),
2270        "bg2" => color_map.theme_slot(ColorMapSlot::Background2),
2271        "tx2" => color_map.theme_slot(ColorMapSlot::Text2),
2272        "accent1" => color_map.theme_slot(ColorMapSlot::Accent1),
2273        "accent2" => color_map.theme_slot(ColorMapSlot::Accent2),
2274        "accent3" => color_map.theme_slot(ColorMapSlot::Accent3),
2275        "accent4" => color_map.theme_slot(ColorMapSlot::Accent4),
2276        "accent5" => color_map.theme_slot(ColorMapSlot::Accent5),
2277        "accent6" => color_map.theme_slot(ColorMapSlot::Accent6),
2278        "hlink" => color_map.theme_slot(ColorMapSlot::Hyperlink),
2279        "folHlink" => color_map.theme_slot(ColorMapSlot::FollowedHyperlink),
2280        "dk1" => ThemeColorSlot::Dark1,
2281        "lt1" => ThemeColorSlot::Light1,
2282        "dk2" => ThemeColorSlot::Dark2,
2283        "lt2" => ThemeColorSlot::Light2,
2284        _ => return None,
2285    };
2286    Some(mapped)
2287}
2288
2289fn unsupported_series_paint(series_index: usize, source: &str, kind: &str) -> Result<Color> {
2290    Err(ChartError::InvalidValue {
2291        element: format!("c:ser[{series_index}] direct {source} paint"),
2292        value: format!("unsupported {kind} paint"),
2293    })
2294}
2295
2296fn resolve_series_accent(
2297    series_index: usize,
2298    theme: &CT_OfficeStyleSheet,
2299    color_map: &ColorMap,
2300    lookup: &[(&str, RgbColor)],
2301) -> Result<Color> {
2302    const ACCENTS: [ColorMapSlot; 6] = [
2303        ColorMapSlot::Accent1,
2304        ColorMapSlot::Accent2,
2305        ColorMapSlot::Accent3,
2306        ColorMapSlot::Accent4,
2307        ColorMapSlot::Accent5,
2308        ColorMapSlot::Accent6,
2309    ];
2310    let semantic_slot = ACCENTS[series_index % ACCENTS.len()];
2311    let theme_slot = color_map.theme_slot(semantic_slot);
2312    let choice = theme.theme_elements.color_scheme.color(theme_slot);
2313    resolve_chart_colour(choice, color_map, lookup, || {
2314        format!("c:ser[{series_index}] theme accent")
2315    })
2316}
2317
2318fn resolve_chart_colour(
2319    choice: &ColorChoice,
2320    color_map: &ColorMap,
2321    lookup: &[(&str, RgbColor)],
2322    element: impl FnOnce() -> String,
2323) -> Result<Color> {
2324    resolve_color(choice, color_map, lookup)
2325        .map(resolved_layout_colour)
2326        .map_err(|error| ChartError::InvalidValue {
2327            element: element(),
2328            value: error.to_string(),
2329        })
2330}
2331
2332fn theme_colour_lookup(theme: &CT_OfficeStyleSheet) -> Vec<(&'static str, RgbColor)> {
2333    theme
2334        .theme_elements
2335        .color_scheme
2336        .iter()
2337        .filter_map(|(slot, choice)| {
2338            concrete_theme_colour(choice).map(|colour| (slot.as_str(), colour))
2339        })
2340        .collect()
2341}
2342
2343fn concrete_theme_colour(choice: &ColorChoice) -> Option<RgbColor> {
2344    match choice {
2345        ColorChoice::Srgb { value, .. } => Some(*value),
2346        ColorChoice::System { last_color, .. } => *last_color,
2347        ColorChoice::Scheme { .. } | ColorChoice::Preset { .. } => None,
2348    }
2349}
2350
2351fn resolved_layout_colour(colour: ResolvedColor) -> Color {
2352    Color {
2353        r: f64::from(colour.red) / 255.0,
2354        g: f64::from(colour.green) / 255.0,
2355        b: f64::from(colour.blue) / 255.0,
2356        a: f64::from(colour.alpha) / 255.0,
2357    }
2358}
2359
2360fn render_legend(
2361    chart: &CT_Chart,
2362    plot: &Plot,
2363    bounds: Rect,
2364    fonts: &mut FontManager,
2365    series_colours: &[Color],
2366    paths: &mut Vec<PositionedElement>,
2367    labels: &mut Vec<PositionedElement>,
2368) -> Result<()> {
2369    if chart.legend.is_none() {
2370        return Ok(());
2371    }
2372    let x = bounds.x + bounds.width - 58.0;
2373    for (index, (series, colour)) in plot_series(plot).iter().zip(series_colours).enumerate() {
2374        let y = bounds.y + 6.0 + index as f64 * 13.0;
2375        paths.push(filled_path(
2376            Path::rect(Rect {
2377                x,
2378                y,
2379                width: 7.0,
2380                height: 7.0,
2381            }),
2382            *colour,
2383        ));
2384        labels.push(PositionedElement::Text(shape_label(
2385            fonts,
2386            &series_name(series, index),
2387            Point {
2388                x: x + 10.0,
2389                y: y + 7.0,
2390            },
2391        )?));
2392    }
2393    Ok(())
2394}
2395
2396fn validate_geometry_coordinates(elements: &[PositionedElement]) -> Result<()> {
2397    fn finite_point(point: Point) -> bool {
2398        point.x.is_finite() && point.y.is_finite()
2399    }
2400
2401    for element in elements {
2402        match element {
2403            PositionedElement::Path(path) => {
2404                for command in &path.path.commands {
2405                    let finite = match command {
2406                        PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
2407                            finite_point(*point)
2408                        }
2409                        PathCommand::CurveTo { c1, c2, to } => {
2410                            finite_point(*c1) && finite_point(*c2) && finite_point(*to)
2411                        }
2412                        PathCommand::Close => true,
2413                    };
2414                    if !finite {
2415                        return Err(ChartError::InvalidValue {
2416                            element: "chart geometry coordinate".to_owned(),
2417                            value: "generated path contains a nonfinite point".to_owned(),
2418                        });
2419                    }
2420                }
2421            }
2422            PositionedElement::Group(group) => validate_geometry_coordinates(&group.children)?,
2423            _ => {}
2424        }
2425    }
2426    Ok(())
2427}
2428
2429fn render_plot_geometry(
2430    plot: &Plot,
2431    bounds: Rect,
2432    blanks: DispBlanksAs,
2433    options: GeometryOptions,
2434    series_colours: &[Color],
2435) -> Result<Vec<PositionedElement>> {
2436    match plot {
2437        Plot::Bar {
2438            direction,
2439            grouping,
2440            gap_width,
2441            overlap,
2442            series,
2443            ..
2444        } => render_bar_geometry(
2445            *direction,
2446            *grouping,
2447            *gap_width,
2448            *overlap,
2449            series,
2450            bounds,
2451            options,
2452            series_colours,
2453        ),
2454        Plot::Line {
2455            grouping,
2456            marker,
2457            series,
2458            ..
2459        } => render_line_geometry(
2460            *grouping,
2461            *marker,
2462            series,
2463            bounds,
2464            blanks,
2465            options,
2466            series_colours,
2467        ),
2468        Plot::Pie {
2469            first_slice_angle,
2470            series,
2471            ..
2472        } => render_pie_geometry(*first_slice_angle, None, series, bounds, series_colours),
2473        Plot::Doughnut {
2474            first_slice_angle,
2475            hole_size,
2476            series,
2477            ..
2478        } => render_pie_geometry(
2479            *first_slice_angle,
2480            Some(*hole_size),
2481            series,
2482            bounds,
2483            series_colours,
2484        ),
2485        Plot::Area {
2486            grouping, series, ..
2487        } => render_area_geometry(*grouping, series, bounds, blanks, options, series_colours),
2488        Plot::Scatter { style, series, .. } => {
2489            render_scatter_geometry(*style, series, bounds, blanks, options, series_colours)
2490        }
2491        Plot::Radar { style, series, .. } => {
2492            render_radar_geometry(*style, series, bounds, options, series_colours)
2493        }
2494    }
2495}
2496
2497#[allow(clippy::too_many_arguments)]
2498fn render_bar_geometry(
2499    direction: BarDirection,
2500    grouping: BarGrouping,
2501    gap_width: u16,
2502    overlap: i8,
2503    series: &[Series],
2504    bounds: Rect,
2505    options: GeometryOptions,
2506    series_colours: &[Color],
2507) -> Result<Vec<PositionedElement>> {
2508    let bars = bar_rectangles(
2509        direction, grouping, gap_width, overlap, series, bounds, options,
2510    )?;
2511    bars.into_iter()
2512        .map(|bar| {
2513            Ok(filled_path(
2514                Path::rect(bar.rect),
2515                series_colour(series_colours, bar.series_index)?,
2516            ))
2517        })
2518        .collect()
2519}
2520
2521#[derive(Clone, Copy, Debug)]
2522struct BarRectangle {
2523    series_index: usize,
2524    logical_index: usize,
2525    rect: Rect,
2526    base: Point,
2527    end: Point,
2528}
2529
2530fn bar_rectangles(
2531    direction: BarDirection,
2532    grouping: BarGrouping,
2533    gap_width: u16,
2534    overlap: i8,
2535    series: &[Series],
2536    bounds: Rect,
2537    options: GeometryOptions,
2538) -> Result<Vec<BarRectangle>> {
2539    validate_series_values(series)?;
2540    let logical_series = logical_series_values(series)?;
2541    let category_count = series_category_count(series)?;
2542    let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
2543    let percent = grouping == BarGrouping::PercentStacked;
2544    let layers = stacked_series_bounds(&logical_series, stacked, percent)?;
2545    let domain = options
2546        .value
2547        .map(|mapping| mapping.domain)
2548        .unwrap_or(domain_from_layers(&layers)?);
2549    let value_orientation = options
2550        .value
2551        .map(|mapping| mapping.orientation)
2552        .unwrap_or(Orientation::MinMax);
2553    let category_extent = if direction == BarDirection::Column {
2554        bounds.width
2555    } else {
2556        bounds.height
2557    };
2558    let slot = category_extent / category_count as f64;
2559    let cluster_extent = slot * 100.0 / (100.0 + f64::from(gap_width));
2560    let bars_per_cluster = if stacked { 1 } else { series.len() };
2561    let overlap_fraction = if stacked {
2562        0.0
2563    } else {
2564        f64::from(overlap) / 100.0
2565    };
2566    let divisor =
2567        bars_per_cluster as f64 - (bars_per_cluster.saturating_sub(1)) as f64 * overlap_fraction;
2568    let bar_extent = cluster_extent / divisor;
2569    let advance = bar_extent * (1.0 - overlap_fraction);
2570    let mut bars = Vec::new();
2571    for (series_index, layer) in layers.iter().enumerate() {
2572        for &(logical_index, start, end) in layer {
2573            let category = oriented_category_index(
2574                logical_index,
2575                category_count,
2576                options.category_orientation,
2577            );
2578            let cluster_start = category as f64 / category_count as f64 * category_extent
2579                + (slot - cluster_extent) / 2.0;
2580            let category_start = if stacked {
2581                cluster_start
2582            } else {
2583                cluster_start + series_index as f64 * advance
2584            };
2585            let (rect, base, end) = if direction == BarDirection::Column {
2586                let y0 = map_y_oriented(start, domain, bounds, value_orientation)?;
2587                let y1 = map_y_oriented(end, domain, bounds, value_orientation)?;
2588                let center_x = bounds.x + category_start + bar_extent / 2.0;
2589                (
2590                    Rect {
2591                        x: bounds.x + category_start,
2592                        y: y0.min(y1),
2593                        width: bar_extent,
2594                        height: (y1 - y0).abs(),
2595                    },
2596                    Point { x: center_x, y: y0 },
2597                    Point { x: center_x, y: y1 },
2598                )
2599            } else {
2600                let x0 = map_x_oriented(start, domain, bounds, value_orientation)?;
2601                let x1 = map_x_oriented(end, domain, bounds, value_orientation)?;
2602                let center_y = bounds.y + category_start + bar_extent / 2.0;
2603                (
2604                    Rect {
2605                        x: x0.min(x1),
2606                        y: bounds.y + category_start,
2607                        width: (x1 - x0).abs(),
2608                        height: bar_extent,
2609                    },
2610                    Point { x: x0, y: center_y },
2611                    Point { x: x1, y: center_y },
2612                )
2613            };
2614            bars.push(BarRectangle {
2615                series_index,
2616                logical_index,
2617                rect,
2618                base,
2619                end,
2620            });
2621        }
2622    }
2623    Ok(bars)
2624}
2625
2626fn render_line_geometry(
2627    grouping: Grouping,
2628    marker: bool,
2629    series: &[Series],
2630    bounds: Rect,
2631    blanks: DispBlanksAs,
2632    options: GeometryOptions,
2633    series_colours: &[Color],
2634) -> Result<Vec<PositionedElement>> {
2635    validate_series_values(series)?;
2636    let logical_series = logical_series_values(series)?;
2637    let category_count = series_category_count(series)?;
2638    let calculated_series = if blanks == DispBlanksAs::Zero {
2639        zero_filled_series(&logical_series, category_count)
2640    } else {
2641        logical_series.clone()
2642    };
2643    let stacked = grouping != Grouping::Standard;
2644    let percent = grouping == Grouping::PercentStacked;
2645    let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
2646    let domain = options
2647        .value
2648        .map(|mapping| mapping.domain)
2649        .unwrap_or(domain_from_layers(&layers)?);
2650    let value_orientation = options
2651        .value
2652        .map(|mapping| mapping.orientation)
2653        .unwrap_or(Orientation::MinMax);
2654    let mut elements = Vec::new();
2655    for (series_index, layer) in layers.iter().enumerate() {
2656        let colour = series_colour(series_colours, series_index)?;
2657        let indexed: Vec<_> = layer
2658            .iter()
2659            .map(|(index, _, upper)| (*index, *upper))
2660            .collect();
2661        let indexes: Vec<_> = indexed.iter().map(|(index, _)| *index).collect();
2662        for (start, end) in contiguous_ranges(&indexes, blanks) {
2663            let points = category_points(
2664                &indexed[start..end],
2665                category_count,
2666                domain,
2667                bounds,
2668                options.category_orientation,
2669                value_orientation,
2670            )?;
2671            elements.push(stroked_path(polyline_path(&points, false), colour));
2672        }
2673        if marker {
2674            let present: BTreeSet<_> = logical_series[series_index]
2675                .1
2676                .iter()
2677                .map(|(index, _)| *index)
2678                .collect();
2679            let marker_values: Vec<_> = indexed
2680                .iter()
2681                .copied()
2682                .filter(|(index, _)| present.contains(index))
2683                .collect();
2684            let marker_points = category_points(
2685                &marker_values,
2686                category_count,
2687                domain,
2688                bounds,
2689                options.category_orientation,
2690                value_orientation,
2691            )?;
2692            push_markers(&mut elements, &marker_points, colour);
2693        }
2694    }
2695    Ok(elements)
2696}
2697
2698fn render_area_geometry(
2699    grouping: Grouping,
2700    series: &[Series],
2701    bounds: Rect,
2702    blanks: DispBlanksAs,
2703    options: GeometryOptions,
2704    series_colours: &[Color],
2705) -> Result<Vec<PositionedElement>> {
2706    validate_series_values(series)?;
2707    let logical_series = logical_series_values(series)?;
2708    let category_count = series_category_count(series)?;
2709    let calculated_series = if blanks == DispBlanksAs::Zero {
2710        zero_filled_series(&logical_series, category_count)
2711    } else {
2712        logical_series
2713    };
2714    let stacked = grouping != Grouping::Standard;
2715    let percent = grouping == Grouping::PercentStacked;
2716    let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
2717    let domain = options
2718        .value
2719        .map(|mapping| mapping.domain)
2720        .unwrap_or(domain_from_layers(&layers)?);
2721    let value_orientation = options
2722        .value
2723        .map(|mapping| mapping.orientation)
2724        .unwrap_or(Orientation::MinMax);
2725    let mut elements = Vec::new();
2726    for (series_index, layer) in layers.iter().enumerate() {
2727        let colour = series_colour(series_colours, series_index)?;
2728        let indexes: Vec<_> = layer.iter().map(|(index, _, _)| *index).collect();
2729        for (start, end) in contiguous_ranges(&indexes, blanks) {
2730            let segment = &layer[start..end];
2731            let top_values: Vec<_> = segment
2732                .iter()
2733                .map(|(index, _, upper)| (*index, *upper))
2734                .collect();
2735            let lower_values: Vec<_> = segment
2736                .iter()
2737                .map(|(index, lower, _)| (*index, *lower))
2738                .collect();
2739            let top = category_points(
2740                &top_values,
2741                category_count,
2742                domain,
2743                bounds,
2744                options.category_orientation,
2745                value_orientation,
2746            )?;
2747            let mut bottom = category_points(
2748                &lower_values,
2749                category_count,
2750                domain,
2751                bounds,
2752                options.category_orientation,
2753                value_orientation,
2754            )?;
2755            bottom.reverse();
2756            let mut commands = Vec::with_capacity(top.len() + bottom.len() + 2);
2757            commands.push(PathCommand::MoveTo(top[0]));
2758            commands.extend(top.iter().skip(1).copied().map(PathCommand::LineTo));
2759            commands.extend(bottom.into_iter().map(PathCommand::LineTo));
2760            commands.push(PathCommand::Close);
2761            elements.push(filled_and_stroked_path(
2762                Path {
2763                    commands,
2764                    fill_rule: FillRule::NonZero,
2765                },
2766                colour,
2767            ));
2768        }
2769    }
2770    Ok(elements)
2771}
2772
2773fn render_scatter_geometry(
2774    style: ScatterStyle,
2775    series: &[Series],
2776    bounds: Rect,
2777    blanks: DispBlanksAs,
2778    options: GeometryOptions,
2779    series_colours: &[Color],
2780) -> Result<Vec<PositionedElement>> {
2781    validate_series_values(series)?;
2782    let mut paired_series = Vec::with_capacity(series.len());
2783    let mut marker_series = Vec::with_capacity(series.len());
2784    let mut all_x = Vec::new();
2785    let mut all_y = Vec::new();
2786    for item in series {
2787        let markers = matched_scatter_points(item)?;
2788        let paired = scatter_render_points(item, blanks)?;
2789        all_x.extend(paired.iter().map(|(_, x, _)| *x));
2790        all_y.extend(paired.iter().map(|(_, _, y)| *y));
2791        paired_series.push(paired);
2792        marker_series.push(markers);
2793    }
2794    let x_domain = options
2795        .scatter_x
2796        .map(|mapping| mapping.domain)
2797        .unwrap_or(data_domain(&all_x, false)?);
2798    let y_domain = options
2799        .scatter_y
2800        .map(|mapping| mapping.domain)
2801        .unwrap_or(data_domain(&all_y, false)?);
2802    let x_orientation = options
2803        .scatter_x
2804        .map(|mapping| mapping.orientation)
2805        .unwrap_or(Orientation::MinMax);
2806    let y_orientation = options
2807        .scatter_y
2808        .map(|mapping| mapping.orientation)
2809        .unwrap_or(Orientation::MinMax);
2810    let draw_line = matches!(
2811        style,
2812        ScatterStyle::Line
2813            | ScatterStyle::LineMarker
2814            | ScatterStyle::Smooth
2815            | ScatterStyle::SmoothMarker
2816    );
2817    let draw_marker = matches!(
2818        style,
2819        ScatterStyle::Marker | ScatterStyle::LineMarker | ScatterStyle::SmoothMarker
2820    );
2821    let mut elements = Vec::new();
2822    for (series_index, paired) in paired_series.iter().enumerate() {
2823        let colour = series_colour(series_colours, series_index)?;
2824        if draw_line {
2825            let indexes: Vec<_> = paired.iter().map(|(index, _, _)| *index).collect();
2826            for (start, end) in contiguous_ranges(&indexes, blanks) {
2827                let points = scatter_points(
2828                    &paired[start..end],
2829                    x_domain,
2830                    y_domain,
2831                    bounds,
2832                    x_orientation,
2833                    y_orientation,
2834                )?;
2835                elements.push(stroked_path(polyline_path(&points, false), colour));
2836            }
2837        }
2838        if draw_marker {
2839            let marker_points = scatter_points(
2840                &marker_series[series_index],
2841                x_domain,
2842                y_domain,
2843                bounds,
2844                x_orientation,
2845                y_orientation,
2846            )?;
2847            push_markers(&mut elements, &marker_points, colour);
2848        }
2849    }
2850    Ok(elements)
2851}
2852
2853type ScatterPoint = (usize, f64, f64);
2854
2855struct ScatterPointMaps {
2856    x_count: usize,
2857    y_count: usize,
2858    x_by_index: BTreeMap<usize, f64>,
2859    y_by_index: BTreeMap<usize, f64>,
2860}
2861
2862fn scatter_point_maps(series: &Series) -> Result<ScatterPointMaps> {
2863    let Some(AxisData::Numeric(x_values)) = &series.categories else {
2864        return Err(ChartError::InvalidValue {
2865            element: "c:xVal".to_owned(),
2866            value: "scatter series requires numeric cached x values".to_owned(),
2867        });
2868    };
2869    validate_finite(&x_values.values, "c:xVal/c:numCache")?;
2870    let (x_count, x_points) = logical_numeric_values(x_values)?;
2871    let (y_count, y_points) = logical_numeric_values(&series.values)?;
2872    Ok(ScatterPointMaps {
2873        x_count,
2874        y_count,
2875        x_by_index: x_points.into_iter().collect(),
2876        y_by_index: y_points.into_iter().collect(),
2877    })
2878}
2879
2880fn matched_scatter_points(series: &Series) -> Result<Vec<ScatterPoint>> {
2881    let points = scatter_point_maps(series)?;
2882    Ok(points
2883        .y_by_index
2884        .iter()
2885        .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
2886        .collect())
2887}
2888
2889fn scatter_render_points(series: &Series, blanks: DispBlanksAs) -> Result<Vec<ScatterPoint>> {
2890    let points = scatter_point_maps(series)?;
2891    if blanks != DispBlanksAs::Zero {
2892        return Ok(points
2893            .y_by_index
2894            .iter()
2895            .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
2896            .collect());
2897    }
2898    let count = points.x_count.max(points.y_count);
2899    let present = points
2900        .x_by_index
2901        .keys()
2902        .chain(points.y_by_index.keys())
2903        .copied()
2904        .collect();
2905    Ok(zero_control_indexes(count, &present)
2906        .into_iter()
2907        .map(|index| {
2908            (
2909                index,
2910                points.x_by_index.get(&index).copied().unwrap_or(0.0),
2911                points.y_by_index.get(&index).copied().unwrap_or(0.0),
2912            )
2913        })
2914        .collect())
2915}
2916
2917fn scatter_points(
2918    values: &[(usize, f64, f64)],
2919    x_domain: Domain,
2920    y_domain: Domain,
2921    bounds: Rect,
2922    x_orientation: Orientation,
2923    y_orientation: Orientation,
2924) -> Result<Vec<Point>> {
2925    values
2926        .iter()
2927        .map(|(_, x, y)| {
2928            Ok(Point {
2929                x: map_x_oriented(*x, x_domain, bounds, x_orientation)?,
2930                y: map_y_oriented(*y, y_domain, bounds, y_orientation)?,
2931            })
2932        })
2933        .collect()
2934}
2935
2936fn render_radar_geometry(
2937    style: RadarStyle,
2938    series: &[Series],
2939    bounds: Rect,
2940    options: GeometryOptions,
2941    series_colours: &[Color],
2942) -> Result<Vec<PositionedElement>> {
2943    let point_series = radar_series_points(series, bounds, options)?;
2944    let mut elements = Vec::new();
2945    for (series_index, points) in point_series.iter().enumerate() {
2946        let colour = series_colour(series_colours, series_index)?;
2947        let plain_points: Vec<_> = points.iter().map(|(_, point)| *point).collect();
2948        if plain_points.is_empty() {
2949            continue;
2950        }
2951        let path = polyline_path(&plain_points, true);
2952        if style == RadarStyle::Filled {
2953            elements.push(filled_and_stroked_path(path, colour));
2954        } else {
2955            elements.push(stroked_path(path, colour));
2956        }
2957        if style == RadarStyle::Marker {
2958            push_markers(&mut elements, &plain_points, colour);
2959        }
2960    }
2961    Ok(elements)
2962}
2963
2964fn radar_series_points(
2965    series: &[Series],
2966    bounds: Rect,
2967    options: GeometryOptions,
2968) -> Result<Vec<Vec<(usize, Point)>>> {
2969    validate_series_values(series)?;
2970    let logical_series = logical_series_values(series)?;
2971    let category_count = series_category_count(series)?;
2972    let values = logical_series
2973        .iter()
2974        .flat_map(|(_, points)| points.iter().map(|(_, value)| *value))
2975        .collect::<Vec<_>>();
2976    let mapping = options.value.unwrap_or(AxisMapping {
2977        domain: data_domain(&values, true)?,
2978        orientation: Orientation::MinMax,
2979    });
2980    let center = Point {
2981        x: bounds.x + bounds.width / 2.0,
2982        y: bounds.y + bounds.height / 2.0,
2983    };
2984    let radius = bounds.width.min(bounds.height) / 2.0;
2985    let mut point_series = Vec::with_capacity(logical_series.len());
2986    for (_, logical) in &logical_series {
2987        let mut points = Vec::with_capacity(logical.len());
2988        for (category, value) in logical {
2989            if *value < mapping.domain.min || *value > mapping.domain.max {
2990                continue;
2991            }
2992            points.push((
2993                *category,
2994                radial_point(
2995                    center,
2996                    normalized_value_oriented(*value, mapping.domain, mapping.orientation)?
2997                        * radius,
2998                    oriented_category_index(
2999                        *category,
3000                        category_count,
3001                        options.category_orientation,
3002                    ),
3003                    category_count,
3004                ),
3005            ));
3006        }
3007        point_series.push(points);
3008    }
3009    Ok(point_series)
3010}
3011
3012fn render_pie_geometry(
3013    first_slice_angle: u16,
3014    hole_size: Option<u8>,
3015    series: &[Series],
3016    bounds: Rect,
3017    series_colours: &[Color],
3018) -> Result<Vec<PositionedElement>> {
3019    pie_slices(first_slice_angle, hole_size, series, bounds)?
3020        .into_iter()
3021        .map(|slice| {
3022            Ok(filled_path(
3023                slice.path,
3024                series_colour(series_colours, slice.series_index)?,
3025            ))
3026        })
3027        .collect()
3028}
3029
3030struct PieSliceGeometry {
3031    series_index: usize,
3032    logical_index: usize,
3033    path: Path,
3034    anchor: Point,
3035    base: Point,
3036    end: Point,
3037}
3038
3039fn pie_slices(
3040    first_slice_angle: u16,
3041    hole_size: Option<u8>,
3042    series: &[Series],
3043    bounds: Rect,
3044) -> Result<Vec<PieSliceGeometry>> {
3045    validate_series_values(series)?;
3046    let center = Point {
3047        x: bounds.x + bounds.width / 2.0,
3048        y: bounds.y + bounds.height / 2.0,
3049    };
3050    let radius = bounds.width.min(bounds.height) / 2.0;
3051    let mut elements = Vec::new();
3052    for (series_index, item) in series.iter().enumerate() {
3053        let (_, logical) = logical_numeric_values(&item.values)?;
3054        let total = logical
3055            .iter()
3056            .map(|(_, value)| *value)
3057            .filter(|value| *value > 0.0)
3058            .try_fold(0.0, |total, value| {
3059                checked_geometry_sum(total, value, "pie value total")
3060            })?;
3061        if total <= 0.0 {
3062            continue;
3063        }
3064        let mut angle = f64::from(first_slice_angle).to_radians() - std::f64::consts::FRAC_PI_2;
3065        for (logical_index, value) in logical {
3066            if value <= 0.0 {
3067                continue;
3068            }
3069            let next = angle + value / total * std::f64::consts::TAU;
3070            let path = if let Some(hole_size) = hole_size {
3071                doughnut_wedge(
3072                    center,
3073                    radius,
3074                    radius * f64::from(hole_size) / 100.0,
3075                    angle,
3076                    next,
3077                )
3078            } else {
3079                pie_wedge(center, radius, angle, next)
3080            };
3081            let inner_radius = hole_size
3082                .map(|hole| radius * f64::from(hole) / 100.0)
3083                .unwrap_or(0.0);
3084            let anchor_radius = hole_size
3085                .map(|_| (radius + inner_radius) / 2.0)
3086                .unwrap_or(radius * 2.0 / 3.0);
3087            let midpoint = (angle + next) / 2.0;
3088            elements.push(PieSliceGeometry {
3089                series_index,
3090                logical_index,
3091                path,
3092                anchor: circle_point(center, anchor_radius, midpoint),
3093                base: circle_point(center, inner_radius, midpoint),
3094                end: circle_point(center, radius, midpoint),
3095            });
3096            angle = next;
3097        }
3098    }
3099    Ok(elements)
3100}
3101
3102fn validate_series_values(series: &[Series]) -> Result<()> {
3103    if series.is_empty() {
3104        return Err(ChartError::MissingElement("c:ser".to_owned()));
3105    }
3106    for item in series {
3107        validate_finite(&item.values.values, "c:val/c:numCache")?;
3108    }
3109    Ok(())
3110}
3111
3112fn validate_finite(values: &[f64], element: &str) -> Result<()> {
3113    if let Some(value) = values.iter().find(|value| !value.is_finite()) {
3114        return Err(ChartError::InvalidValue {
3115            element: element.to_owned(),
3116            value: value.to_string(),
3117        });
3118    }
3119    Ok(())
3120}
3121
3122type IndexedValues = Vec<(usize, f64)>;
3123type LogicalSeries = (usize, IndexedValues);
3124type GeometryLayer = Vec<(usize, f64, f64)>;
3125
3126fn zero_control_indexes(count: usize, present: &BTreeSet<usize>) -> BTreeSet<usize> {
3127    let mut controls = present.clone();
3128    if count == 0 {
3129        return controls;
3130    }
3131    controls.insert(0);
3132    controls.insert(count - 1);
3133    for index in present {
3134        if *index > 0 {
3135            controls.insert(*index - 1);
3136        }
3137        if index + 1 < count {
3138            controls.insert(index + 1);
3139        }
3140    }
3141    controls
3142}
3143
3144fn zero_filled_series(series: &[LogicalSeries], count: usize) -> Vec<LogicalSeries> {
3145    let present = series
3146        .iter()
3147        .flat_map(|(_, points)| points.iter().map(|(index, _)| *index))
3148        .collect();
3149    let controls = zero_control_indexes(count, &present);
3150    series
3151        .iter()
3152        .map(|(declared, points)| {
3153            let values: BTreeMap<_, _> = points.iter().copied().collect();
3154            let filled = controls
3155                .iter()
3156                .map(|index| (*index, values.get(index).copied().unwrap_or(0.0)))
3157                .collect();
3158            (*declared, filled)
3159        })
3160        .collect()
3161}
3162
3163fn contiguous_ranges(indexes: &[usize], blanks: DispBlanksAs) -> Vec<(usize, usize)> {
3164    if indexes.is_empty() {
3165        return Vec::new();
3166    }
3167    if blanks != DispBlanksAs::Gap {
3168        return vec![(0, indexes.len())];
3169    }
3170    let mut ranges = Vec::new();
3171    let mut start = 0usize;
3172    for position in 1..indexes.len() {
3173        if indexes[position - 1].checked_add(1) != Some(indexes[position]) {
3174            ranges.push((start, position));
3175            start = position;
3176        }
3177    }
3178    ranges.push((start, indexes.len()));
3179    ranges
3180}
3181
3182fn logical_numeric_values(data: &NumericData) -> Result<LogicalSeries> {
3183    validate_finite(&data.values, "c:numCache")?;
3184    let (declared, indexes) = cache_layout(&data.markup, data.values.len())?;
3185    let declared = usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
3186        element: "c:ptCount".to_owned(),
3187        value: declared.to_string(),
3188    })?;
3189    let points = indexes
3190        .into_iter()
3191        .zip(data.values.iter().copied())
3192        .map(|(index, value)| {
3193            usize::try_from(index)
3194                .map(|index| (index, value))
3195                .map_err(|_| ChartError::InvalidValue {
3196                    element: "c:pt/@idx".to_owned(),
3197                    value: index.to_string(),
3198                })
3199        })
3200        .collect::<Result<Vec<_>>>()?;
3201    Ok((declared, points))
3202}
3203
3204fn logical_series_values(series: &[Series]) -> Result<Vec<LogicalSeries>> {
3205    series
3206        .iter()
3207        .map(|item| logical_numeric_values(&item.values))
3208        .collect()
3209}
3210
3211fn reference_logical_count(markup: &ReferenceMarkup, value_count: usize) -> Result<usize> {
3212    let (declared, _) = cache_layout(markup, value_count)?;
3213    usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
3214        element: "c:ptCount".to_owned(),
3215        value: declared.to_string(),
3216    })
3217}
3218
3219fn series_category_count(series: &[Series]) -> Result<usize> {
3220    let mut count = 0usize;
3221    for item in series {
3222        count = count.max(reference_logical_count(
3223            &item.values.markup,
3224            item.values.values.len(),
3225        )?);
3226        if let Some(categories) = &item.categories {
3227            let category_count = match categories {
3228                AxisData::String(data) => reference_logical_count(&data.markup, data.values.len())?,
3229                AxisData::Numeric(data) => {
3230                    reference_logical_count(&data.markup, data.values.len())?
3231                }
3232            };
3233            count = count.max(category_count);
3234        }
3235    }
3236    if count == 0 {
3237        return Err(ChartError::InvalidValue {
3238            element: "c:ser".to_owned(),
3239            value: "cached series contain no points".to_owned(),
3240        });
3241    }
3242    Ok(count)
3243}
3244
3245fn stacked_series_bounds(
3246    series: &[LogicalSeries],
3247    stacked: bool,
3248    percent: bool,
3249) -> Result<Vec<GeometryLayer>> {
3250    let mut positive_totals = BTreeMap::<usize, f64>::new();
3251    let mut negative_totals = BTreeMap::<usize, f64>::new();
3252    if percent {
3253        for (_, points) in series {
3254            for &(index, value) in points {
3255                let totals = if value >= 0.0 {
3256                    &mut positive_totals
3257                } else {
3258                    &mut negative_totals
3259                };
3260                let total = totals.entry(index).or_default();
3261                *total = checked_geometry_sum(*total, value.abs(), "stacked percentage total")?;
3262            }
3263        }
3264    }
3265    let mut positive = BTreeMap::<usize, f64>::new();
3266    let mut negative = BTreeMap::<usize, f64>::new();
3267    let mut layers = Vec::with_capacity(series.len());
3268    for (_, points) in series {
3269        let mut layer = Vec::with_capacity(points.len());
3270        for &(index, original) in points {
3271            let total = if original >= 0.0 {
3272                positive_totals.get(&index).copied().unwrap_or(0.0)
3273            } else {
3274                negative_totals.get(&index).copied().unwrap_or(0.0)
3275            };
3276            let value = if percent && total != 0.0 {
3277                original / total
3278            } else {
3279                original
3280            };
3281            if stacked {
3282                let accumulators = if value >= 0.0 {
3283                    &mut positive
3284                } else {
3285                    &mut negative
3286                };
3287                let accumulator = accumulators.entry(index).or_default();
3288                let lower = *accumulator;
3289                *accumulator = checked_geometry_sum(*accumulator, value, "stacked value total")?;
3290                layer.push((index, lower, *accumulator));
3291            } else {
3292                layer.push((index, 0.0, value));
3293            }
3294        }
3295        layers.push(layer);
3296    }
3297    Ok(layers)
3298}
3299
3300fn checked_geometry_sum(current: f64, value: f64, context: &str) -> Result<f64> {
3301    let sum = current + value;
3302    if sum.is_finite() {
3303        Ok(sum)
3304    } else {
3305        Err(ChartError::InvalidValue {
3306            element: "chart geometry aggregate".to_owned(),
3307            value: format!("{context} is nonfinite"),
3308        })
3309    }
3310}
3311
3312#[derive(Clone, Copy, Debug, PartialEq)]
3313struct Domain {
3314    min: f64,
3315    max: f64,
3316}
3317
3318fn nice_number_scale(
3319    extent: Domain,
3320    pinned_minimum: Option<f64>,
3321    pinned_maximum: Option<f64>,
3322    target_tick_count: usize,
3323) -> Result<AxisScale> {
3324    if target_tick_count < 2
3325        || !extent.min.is_finite()
3326        || !extent.max.is_finite()
3327        || pinned_minimum.is_some_and(|value| !value.is_finite())
3328        || pinned_maximum.is_some_and(|value| !value.is_finite())
3329    {
3330        return Err(ChartError::InvalidValue {
3331            element: "chart axis scale".to_owned(),
3332            value: "scale bounds and target tick count must be finite and valid".to_owned(),
3333        });
3334    }
3335
3336    let mut data_min = extent.min.min(extent.max);
3337    let mut data_max = extent.min.max(extent.max);
3338    if data_min == data_max {
3339        let expanded = expand_constant_domain(data_min)?;
3340        data_min = expanded.min;
3341        data_max = expanded.max;
3342    }
3343    let requested_min = match (pinned_minimum, pinned_maximum) {
3344        (Some(minimum), _) => minimum,
3345        (None, Some(maximum)) if data_min >= maximum => {
3346            extend_bound(maximum, stable_positive_step(data_min, data_max, 3)?, false)?
3347        }
3348        (None, _) => data_min,
3349    };
3350    let requested_max = match (pinned_minimum, pinned_maximum) {
3351        (_, Some(maximum)) => maximum,
3352        (Some(minimum), None) if data_max <= minimum => {
3353            extend_bound(minimum, stable_positive_step(data_min, data_max, 3)?, true)?
3354        }
3355        (_, None) => data_max,
3356    };
3357    if requested_min >= requested_max {
3358        return Err(ChartError::InvalidValue {
3359            element: "c:scaling".to_owned(),
3360            value: format!("minimum {requested_min} is not less than maximum {requested_max}"),
3361        });
3362    }
3363
3364    let raw_step = stable_positive_step(requested_min, requested_max, target_tick_count)?;
3365    let step = nice_step(raw_step)?;
3366    let rounded_minimum = (requested_min / step).floor() * step;
3367    let rounded_maximum = (requested_max / step).ceil() * step;
3368    let lower_fallback = pinned_minimum.is_none() && !rounded_minimum.is_finite();
3369    let upper_fallback = pinned_maximum.is_none() && !rounded_maximum.is_finite();
3370    let minimum = pinned_minimum.unwrap_or(if rounded_minimum.is_finite() {
3371        rounded_minimum
3372    } else {
3373        requested_min
3374    });
3375    let maximum = pinned_maximum.unwrap_or(if rounded_maximum.is_finite() {
3376        rounded_maximum
3377    } else {
3378        requested_max
3379    });
3380    if !minimum.is_finite() || !maximum.is_finite() || minimum >= maximum {
3381        return Err(ChartError::InvalidValue {
3382            element: "chart axis scale".to_owned(),
3383            value: "nice-number bounds are nonfinite or empty".to_owned(),
3384        });
3385    }
3386
3387    let first = (minimum / step).ceil();
3388    let last = (maximum / step).floor();
3389    let tick_count = if last >= first {
3390        (last - first).min(1024.0) as usize + 1
3391    } else {
3392        0
3393    };
3394    let mut ticks = Vec::with_capacity(tick_count);
3395    for index in 0..tick_count {
3396        let tick = (first + index as f64) * step;
3397        if tick.is_finite() && tick >= minimum && tick <= maximum {
3398            ticks.push(clean_tick(tick, step));
3399        }
3400    }
3401    if lower_fallback && ticks.first().is_none_or(|tick| *tick > minimum) {
3402        ticks.insert(0, minimum);
3403    }
3404    if upper_fallback && ticks.last().is_none_or(|tick| *tick < maximum) {
3405        ticks.push(maximum);
3406    }
3407    if ticks.len() < 2 {
3408        ticks.extend([minimum, maximum]);
3409        ticks.sort_by(f64::total_cmp);
3410    }
3411    ticks.dedup_by(|left, right| *left == *right);
3412    if ticks.len() < 2 || !ticks.windows(2).all(|pair| pair[0] < pair[1]) {
3413        return Err(ChartError::InvalidValue {
3414            element: "chart axis scale".to_owned(),
3415            value: "scale cannot produce two increasing finite ticks".to_owned(),
3416        });
3417    }
3418    Ok(AxisScale {
3419        domain: Domain {
3420            min: minimum,
3421            max: maximum,
3422        },
3423        ticks,
3424    })
3425}
3426
3427const MIN_POSITIVE_SUBNORMAL: f64 = f64::from_bits(1);
3428
3429fn stable_positive_step(minimum: f64, maximum: f64, target_tick_count: usize) -> Result<f64> {
3430    let scale = minimum.abs().max(maximum.abs());
3431    let scale = if scale == 0.0 {
3432        MIN_POSITIVE_SUBNORMAL
3433    } else {
3434        scale
3435    };
3436    let normalized =
3437        (maximum / scale - minimum / scale) / target_tick_count.saturating_sub(1) as f64;
3438    let step = normalized * scale;
3439    if normalized.is_finite() && normalized > 0.0 && step.is_finite() {
3440        Ok(step.max(MIN_POSITIVE_SUBNORMAL))
3441    } else {
3442        Err(ChartError::InvalidValue {
3443            element: "chart axis scale".to_owned(),
3444            value: "scale step is nonfinite or empty".to_owned(),
3445        })
3446    }
3447}
3448
3449fn nice_step(raw_step: f64) -> Result<f64> {
3450    if !raw_step.is_finite() || raw_step <= 0.0 {
3451        return Err(ChartError::InvalidValue {
3452            element: "chart axis scale".to_owned(),
3453            value: format!("invalid raw tick step {raw_step}"),
3454        });
3455    }
3456    let magnitude = 10.0_f64
3457        .powf(raw_step.log10().floor())
3458        .max(MIN_POSITIVE_SUBNORMAL);
3459    let fraction = raw_step / magnitude;
3460    let multiplier = if fraction <= 1.0 {
3461        1.0
3462    } else if fraction <= 2.0 {
3463        2.0
3464    } else if fraction <= 5.0 {
3465        5.0
3466    } else {
3467        10.0
3468    };
3469    let step = (multiplier * magnitude).max(MIN_POSITIVE_SUBNORMAL);
3470    if step.is_finite() && step > 0.0 {
3471        Ok(step)
3472    } else {
3473        Err(ChartError::InvalidValue {
3474            element: "chart axis scale".to_owned(),
3475            value: "nice-number step is nonfinite".to_owned(),
3476        })
3477    }
3478}
3479
3480fn expand_constant_domain(value: f64) -> Result<Domain> {
3481    if value == 0.0 {
3482        return Ok(Domain {
3483            min: -1.0,
3484            max: 1.0,
3485        });
3486    }
3487    let padding = (value.abs() * 0.5).max(MIN_POSITIVE_SUBNORMAL);
3488    let minimum = value - padding;
3489    let maximum = value + padding;
3490    if minimum.is_finite() && maximum.is_finite() && minimum < maximum {
3491        return Ok(Domain {
3492            min: minimum,
3493            max: maximum,
3494        });
3495    }
3496    let domain = if value.is_sign_positive() {
3497        Domain {
3498            min: 0.0,
3499            max: value,
3500        }
3501    } else {
3502        Domain {
3503            min: value,
3504            max: 0.0,
3505        }
3506    };
3507    if domain.min < domain.max {
3508        Ok(domain)
3509    } else {
3510        Err(ChartError::InvalidValue {
3511            element: "chart axis scale".to_owned(),
3512            value: format!("constant value {value} has no finite renderable extent"),
3513        })
3514    }
3515}
3516
3517fn extend_bound(bound: f64, span: f64, positive: bool) -> Result<f64> {
3518    let candidate = if positive { bound + span } else { bound - span };
3519    let extended = if candidate.is_finite()
3520        && if positive {
3521            candidate > bound
3522        } else {
3523            candidate < bound
3524        } {
3525        candidate
3526    } else if positive {
3527        bound.next_up()
3528    } else {
3529        bound.next_down()
3530    };
3531    if extended.is_finite()
3532        && if positive {
3533            extended > bound
3534        } else {
3535            extended < bound
3536        }
3537    {
3538        Ok(extended)
3539    } else {
3540        Err(ChartError::InvalidValue {
3541            element: "c:scaling".to_owned(),
3542            value: format!("one-sided bound {bound} leaves no finite automatic extent"),
3543        })
3544    }
3545}
3546
3547fn clean_tick(value: f64, step: f64) -> f64 {
3548    if value.abs() <= step.abs() * 1.0e-12 {
3549        0.0
3550    } else {
3551        value
3552    }
3553}
3554
3555fn domain_from_layers(layers: &[GeometryLayer]) -> Result<Domain> {
3556    let values = layers
3557        .iter()
3558        .flatten()
3559        .flat_map(|(_, lower, upper)| [*lower, *upper]);
3560    let mut min = 0.0_f64;
3561    let mut max = 0.0_f64;
3562    for value in values {
3563        if !value.is_finite() {
3564            return Err(ChartError::InvalidValue {
3565                element: "chart geometry domain".to_owned(),
3566                value: "aggregate produced a nonfinite value".to_owned(),
3567            });
3568        }
3569        min = min.min(value);
3570        max = max.max(value);
3571    }
3572    if min == max {
3573        max = 1.0;
3574    }
3575    Ok(Domain { min, max })
3576}
3577
3578fn data_domain(values: &[f64], include_zero: bool) -> Result<Domain> {
3579    validate_finite(values, "chart geometry cache")?;
3580    let Some(first) = values.first().copied() else {
3581        return Err(ChartError::InvalidValue {
3582            element: "c:numCache".to_owned(),
3583            value: "cached series contain no points".to_owned(),
3584        });
3585    };
3586    let mut min = first;
3587    let mut max = first;
3588    for value in values.iter().copied() {
3589        min = min.min(value);
3590        max = max.max(value);
3591    }
3592    if include_zero {
3593        min = min.min(0.0);
3594        max = max.max(0.0);
3595    }
3596    if min == max {
3597        if !include_zero {
3598            return expand_constant_domain(min);
3599        }
3600        if min > 0.0 {
3601            min = 0.0;
3602        } else if max < 0.0 {
3603            max = 0.0;
3604        } else {
3605            max = 1.0;
3606        }
3607    }
3608    Ok(Domain { min, max })
3609}
3610
3611fn normalized_value(value: f64, domain: Domain) -> Result<f64> {
3612    let scale = value
3613        .abs()
3614        .max(domain.min.abs())
3615        .max(domain.max.abs())
3616        .max(1.0);
3617    let value = value / scale;
3618    let min = domain.min / scale;
3619    let max = domain.max / scale;
3620    let range = max - min;
3621    let normalized = (value - min) / range;
3622    if !range.is_finite() || range <= 0.0 || !normalized.is_finite() {
3623        return Err(ChartError::InvalidValue {
3624            element: "chart geometry domain".to_owned(),
3625            value: format!("cannot map finite domain {} to {}", domain.min, domain.max),
3626        });
3627    }
3628    Ok(normalized)
3629}
3630
3631fn normalized_value_oriented(value: f64, domain: Domain, orientation: Orientation) -> Result<f64> {
3632    let normalized = normalized_value(value, domain)?;
3633    Ok(if orientation == Orientation::MaxMin {
3634        1.0 - normalized
3635    } else {
3636        normalized
3637    })
3638}
3639
3640fn map_x_oriented(
3641    value: f64,
3642    domain: Domain,
3643    bounds: Rect,
3644    orientation: Orientation,
3645) -> Result<f64> {
3646    let coordinate =
3647        bounds.x + normalized_value_oriented(value, domain, orientation)? * bounds.width;
3648    if coordinate.is_finite() {
3649        Ok(coordinate)
3650    } else {
3651        Err(ChartError::InvalidValue {
3652            element: "chart geometry coordinate".to_owned(),
3653            value: "x coordinate is nonfinite".to_owned(),
3654        })
3655    }
3656}
3657
3658fn map_y_oriented(
3659    value: f64,
3660    domain: Domain,
3661    bounds: Rect,
3662    orientation: Orientation,
3663) -> Result<f64> {
3664    let coordinate = bounds.y + bounds.height
3665        - normalized_value_oriented(value, domain, orientation)? * bounds.height;
3666    if coordinate.is_finite() {
3667        Ok(coordinate)
3668    } else {
3669        Err(ChartError::InvalidValue {
3670            element: "chart geometry coordinate".to_owned(),
3671            value: "y coordinate is nonfinite".to_owned(),
3672        })
3673    }
3674}
3675
3676fn oriented_category_index(index: usize, count: usize, orientation: Orientation) -> usize {
3677    if orientation == Orientation::MaxMin && index < count {
3678        count - index - 1
3679    } else {
3680        index
3681    }
3682}
3683
3684fn category_points(
3685    values: &[(usize, f64)],
3686    count: usize,
3687    domain: Domain,
3688    bounds: Rect,
3689    category_orientation: Orientation,
3690    value_orientation: Orientation,
3691) -> Result<Vec<Point>> {
3692    values
3693        .iter()
3694        .map(|(index, value)| {
3695            let index = oriented_category_index(*index, count, category_orientation);
3696            let x = bounds.x + (index as f64 + 0.5) / count as f64 * bounds.width;
3697            let y = map_y_oriented(*value, domain, bounds, value_orientation)?;
3698            if !x.is_finite() {
3699                return Err(ChartError::InvalidValue {
3700                    element: "chart geometry coordinate".to_owned(),
3701                    value: "category x coordinate is nonfinite".to_owned(),
3702                });
3703            }
3704            Ok(Point { x, y })
3705        })
3706        .collect()
3707}
3708
3709fn polyline_path(points: &[Point], close: bool) -> Path {
3710    let mut commands = Vec::with_capacity(points.len() + usize::from(close));
3711    if let Some(first) = points.first() {
3712        commands.push(PathCommand::MoveTo(*first));
3713        commands.extend(points.iter().skip(1).copied().map(PathCommand::LineTo));
3714        if close {
3715            commands.push(PathCommand::Close);
3716        }
3717    }
3718    Path {
3719        commands,
3720        fill_rule: FillRule::NonZero,
3721    }
3722}
3723
3724fn pie_wedge(center: Point, radius: f64, start: f64, end: f64) -> Path {
3725    let first = circle_point(center, radius, start);
3726    let mut commands = vec![PathCommand::MoveTo(center), PathCommand::LineTo(first)];
3727    append_arc(&mut commands, center, radius, start, end);
3728    commands.push(PathCommand::Close);
3729    Path {
3730        commands,
3731        fill_rule: FillRule::NonZero,
3732    }
3733}
3734
3735fn doughnut_wedge(
3736    center: Point,
3737    outer_radius: f64,
3738    inner_radius: f64,
3739    start: f64,
3740    end: f64,
3741) -> Path {
3742    let outer_start = circle_point(center, outer_radius, start);
3743    let inner_end = circle_point(center, inner_radius, end);
3744    let mut commands = vec![PathCommand::MoveTo(outer_start)];
3745    append_arc(&mut commands, center, outer_radius, start, end);
3746    commands.push(PathCommand::LineTo(inner_end));
3747    append_arc(&mut commands, center, inner_radius, end, start);
3748    commands.push(PathCommand::Close);
3749    Path {
3750        commands,
3751        fill_rule: FillRule::NonZero,
3752    }
3753}
3754
3755fn append_arc(commands: &mut Vec<PathCommand>, center: Point, radius: f64, start: f64, end: f64) {
3756    let segments = ((end - start).abs() / std::f64::consts::FRAC_PI_2)
3757        .ceil()
3758        .max(1.0) as usize;
3759    let sweep = (end - start) / segments as f64;
3760    for segment in 0..segments {
3761        let angle = start + segment as f64 * sweep;
3762        let next = angle + sweep;
3763        let factor = 4.0 / 3.0 * (sweep / 4.0).tan();
3764        let from = circle_point(center, radius, angle);
3765        let to = circle_point(center, radius, next);
3766        commands.push(PathCommand::CurveTo {
3767            c1: Point {
3768                x: from.x - factor * radius * angle.sin(),
3769                y: from.y + factor * radius * angle.cos(),
3770            },
3771            c2: Point {
3772                x: to.x + factor * radius * next.sin(),
3773                y: to.y - factor * radius * next.cos(),
3774            },
3775            to,
3776        });
3777    }
3778}
3779
3780fn circle_point(center: Point, radius: f64, angle: f64) -> Point {
3781    Point {
3782        x: center.x + radius * angle.cos(),
3783        y: center.y + radius * angle.sin(),
3784    }
3785}
3786
3787fn radial_point(center: Point, radius: f64, index: usize, count: usize) -> Point {
3788    circle_point(
3789        center,
3790        radius,
3791        index as f64 * std::f64::consts::TAU / count as f64 - std::f64::consts::FRAC_PI_2,
3792    )
3793}
3794
3795fn marker_path(center: Point) -> Path {
3796    const RADIUS: f64 = 2.5;
3797    const KAPPA: f64 = 0.552_284_749_830_793_6;
3798    let control = RADIUS * KAPPA;
3799    Path {
3800        commands: vec![
3801            PathCommand::MoveTo(Point {
3802                x: center.x + RADIUS,
3803                y: center.y,
3804            }),
3805            PathCommand::CurveTo {
3806                c1: Point {
3807                    x: center.x + RADIUS,
3808                    y: center.y + control,
3809                },
3810                c2: Point {
3811                    x: center.x + control,
3812                    y: center.y + RADIUS,
3813                },
3814                to: Point {
3815                    x: center.x,
3816                    y: center.y + RADIUS,
3817                },
3818            },
3819            PathCommand::CurveTo {
3820                c1: Point {
3821                    x: center.x - control,
3822                    y: center.y + RADIUS,
3823                },
3824                c2: Point {
3825                    x: center.x - RADIUS,
3826                    y: center.y + control,
3827                },
3828                to: Point {
3829                    x: center.x - RADIUS,
3830                    y: center.y,
3831                },
3832            },
3833            PathCommand::CurveTo {
3834                c1: Point {
3835                    x: center.x - RADIUS,
3836                    y: center.y - control,
3837                },
3838                c2: Point {
3839                    x: center.x - control,
3840                    y: center.y - RADIUS,
3841                },
3842                to: Point {
3843                    x: center.x,
3844                    y: center.y - RADIUS,
3845                },
3846            },
3847            PathCommand::CurveTo {
3848                c1: Point {
3849                    x: center.x + control,
3850                    y: center.y - RADIUS,
3851                },
3852                c2: Point {
3853                    x: center.x + RADIUS,
3854                    y: center.y - control,
3855                },
3856                to: Point {
3857                    x: center.x + RADIUS,
3858                    y: center.y,
3859                },
3860            },
3861            PathCommand::Close,
3862        ],
3863        fill_rule: FillRule::NonZero,
3864    }
3865}
3866
3867fn push_markers(elements: &mut Vec<PositionedElement>, points: &[Point], colour: Color) {
3868    for point in points {
3869        elements.push(filled_path(marker_path(*point), colour));
3870    }
3871}
3872
3873fn series_colour(series_colours: &[Color], index: usize) -> Result<Color> {
3874    series_colours
3875        .get(index)
3876        .copied()
3877        .ok_or_else(|| ChartError::InvalidValue {
3878            element: "chart series palette".to_owned(),
3879            value: format!("missing resolved colour for series {index}"),
3880        })
3881}
3882
3883fn filled_path(path: Path, color: Color) -> PositionedElement {
3884    PositionedElement::Path(PathElement {
3885        path,
3886        fill: Some(Paint::Solid(color)),
3887        stroke: None,
3888    })
3889}
3890
3891fn stroked_path(path: Path, color: Color) -> PositionedElement {
3892    PositionedElement::Path(PathElement {
3893        path,
3894        fill: None,
3895        stroke: Some(Stroke::new(Paint::Solid(color), 1.5)),
3896    })
3897}
3898
3899fn filled_and_stroked_path(path: Path, color: Color) -> PositionedElement {
3900    PositionedElement::Path(PathElement {
3901        path,
3902        fill: Some(Paint::Solid(Color {
3903            a: color.a * 0.55,
3904            ..color
3905        })),
3906        stroke: Some(Stroke::new(Paint::Solid(color), 1.0)),
3907    })
3908}
3909
3910type NamespaceBindings = Vec<(Vec<u8>, String)>;
3911type XmlAttributes = Vec<(String, String)>;
3912type RootAttributes = (XmlAttributes, XmlAttributes);
3913
3914#[derive(Clone, Debug, Default, Eq, PartialEq)]
3915struct ScalarMarkup {
3916    raw_attributes: XmlAttributes,
3917    raw_content: Vec<Vec<u8>>,
3918}
3919
3920#[derive(Clone, Debug, Default, Eq, PartialEq)]
3921struct TextMarkup {
3922    raw_attributes: XmlAttributes,
3923    raw_children: OrderedRawChildren,
3924}
3925
3926#[derive(Clone, Debug, Default, Eq, PartialEq)]
3927struct PointMarkup {
3928    raw_attributes: XmlAttributes,
3929    raw_children: OrderedRawChildren,
3930    value: TextMarkup,
3931}
3932
3933#[derive(Clone, Debug, Default, Eq, PartialEq)]
3934struct ReferenceMarkup {
3935    raw_attributes: XmlAttributes,
3936    raw_children: OrderedRawChildren,
3937    formula: TextMarkup,
3938    cache_attributes: XmlAttributes,
3939    cache_children: OrderedRawChildren,
3940    format_code: Option<TextMarkup>,
3941    format_code_omitted: bool,
3942    point_count: ScalarMarkup,
3943    declared_point_count: Option<u32>,
3944    point_indexes: Vec<u32>,
3945    points: Vec<PointMarkup>,
3946}
3947
3948#[derive(Clone, Debug, Default, Eq, PartialEq)]
3949struct WrapperMarkup {
3950    raw_attributes: XmlAttributes,
3951    raw_children: OrderedRawChildren,
3952}
3953
3954/// A formula-backed string cache whose count and point indexes are derived.
3955#[derive(Clone, Debug, PartialEq)]
3956pub struct StringRef {
3957    pub formula: String,
3958    pub values: Vec<String>,
3959    markup: ReferenceMarkup,
3960}
3961
3962impl StringRef {
3963    pub fn new(formula: String, values: Vec<String>) -> Result<Self> {
3964        let reference = Self {
3965            formula,
3966            values,
3967            markup: ReferenceMarkup::default(),
3968        };
3969        reference.validate()?;
3970        Ok(reference)
3971    }
3972
3973    pub fn from_xml(xml: &[u8]) -> Result<Self> {
3974        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
3975    }
3976
3977    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
3978        let parsed = parse_reference(xml, b"strRef", b"strCache", false, inherited)?;
3979        let values = parsed.values;
3980        let reference = Self {
3981            formula: parsed.formula,
3982            values,
3983            markup: parsed.markup,
3984        };
3985        reference.validate()?;
3986        Ok(reference)
3987    }
3988
3989    pub fn to_xml(&self) -> Result<Vec<u8>> {
3990        self.validate()?;
3991        let mut writer = Writer::new(Vec::new());
3992        self.write_xml(&mut writer, true)?;
3993        Ok(writer.into_inner())
3994    }
3995
3996    fn validate(&self) -> Result<()> {
3997        validate_formula(&self.formula, "c:strRef/c:f")?;
3998        for value in &self.values {
3999            validate_xml_text(value, "c:strCache/c:pt/c:v")?;
4000        }
4001        validate_point_count(self.values.len(), "c:strCache")
4002    }
4003
4004    fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
4005        self.validate()?;
4006        let mut start = BytesStart::new("c:strRef");
4007        if standalone {
4008            start.push_attribute(("xmlns:c", C_NS));
4009        }
4010        push_attributes(&mut start, &self.markup.raw_attributes);
4011        writer
4012            .write_event(Event::Start(start))
4013            .map_err(OxmlError::from)?;
4014        emit_raw(writer, self.markup.raw_children.at(0))?;
4015        write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
4016        emit_raw(writer, self.markup.raw_children.at(1))?;
4017        write_string_cache(writer, &self.values, &self.markup)?;
4018        emit_raw(writer, self.markup.raw_children.at(2))?;
4019        writer
4020            .write_event(Event::End(BytesEnd::new("c:strRef")))
4021            .map_err(OxmlError::from)?;
4022        Ok(())
4023    }
4024}
4025
4026/// A formula-backed numeric cache whose metadata and points are derived.
4027#[derive(Clone, Debug, PartialEq)]
4028pub struct NumericData {
4029    pub formula: String,
4030    pub format_code: String,
4031    pub values: Vec<f64>,
4032    markup: ReferenceMarkup,
4033}
4034
4035impl NumericData {
4036    pub fn new(formula: String, format_code: String, values: Vec<f64>) -> Result<Self> {
4037        validate_formula(&formula, "c:numRef/c:f")?;
4038        validate_format_code(&format_code)?;
4039        validate_numeric_values(&values)?;
4040        validate_point_count(values.len(), "c:numCache")?;
4041        Ok(Self {
4042            formula,
4043            format_code,
4044            values,
4045            markup: ReferenceMarkup::default(),
4046        })
4047    }
4048
4049    pub fn from_xml(xml: &[u8]) -> Result<Self> {
4050        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
4051    }
4052
4053    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
4054        let parsed = parse_reference(xml, b"numRef", b"numCache", true, inherited)?;
4055        let format_code = parsed.format_code.unwrap_or_else(|| "General".to_owned());
4056        let mut values = Vec::with_capacity(parsed.values.len());
4057        for value in parsed.values {
4058            let number = value.parse::<f64>().map_err(|_| ChartError::InvalidValue {
4059                element: "c:v".to_owned(),
4060                value: value.clone(),
4061            })?;
4062            if !number.is_finite() {
4063                return Err(ChartError::InvalidValue {
4064                    element: "c:v".to_owned(),
4065                    value,
4066                });
4067            }
4068            values.push(number);
4069        }
4070        let data = Self {
4071            formula: parsed.formula,
4072            format_code,
4073            values,
4074            markup: parsed.markup,
4075        };
4076        data.validate()?;
4077        Ok(data)
4078    }
4079
4080    pub fn to_xml(&self) -> Result<Vec<u8>> {
4081        self.validate()?;
4082        let mut writer = Writer::new(Vec::new());
4083        self.write_xml(&mut writer, true)?;
4084        Ok(writer.into_inner())
4085    }
4086
4087    fn validate(&self) -> Result<()> {
4088        validate_formula(&self.formula, "c:numRef/c:f")?;
4089        validate_format_code(&self.format_code)?;
4090        validate_numeric_values(&self.values)?;
4091        validate_point_count(self.values.len(), "c:numCache")
4092    }
4093
4094    fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
4095        self.validate()?;
4096        let mut start = BytesStart::new("c:numRef");
4097        if standalone {
4098            start.push_attribute(("xmlns:c", C_NS));
4099        }
4100        push_attributes(&mut start, &self.markup.raw_attributes);
4101        writer
4102            .write_event(Event::Start(start))
4103            .map_err(OxmlError::from)?;
4104        emit_raw(writer, self.markup.raw_children.at(0))?;
4105        write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
4106        emit_raw(writer, self.markup.raw_children.at(1))?;
4107        write_numeric_cache(writer, self, &self.markup)?;
4108        emit_raw(writer, self.markup.raw_children.at(2))?;
4109        writer
4110            .write_event(Event::End(BytesEnd::new("c:numRef")))
4111            .map_err(OxmlError::from)?;
4112        Ok(())
4113    }
4114}
4115
4116#[derive(Clone, Debug, PartialEq)]
4117pub enum AxisData {
4118    String(StringRef),
4119    Numeric(NumericData),
4120}
4121
4122#[derive(Clone, Debug, PartialEq)]
4123struct DataPoint {
4124    index: u32,
4125    sp_pr: CT_ShapeProperties,
4126}
4127
4128impl DataPoint {
4129    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
4130        writer
4131            .write_event(Event::Start(BytesStart::new("c:dPt")))
4132            .map_err(OxmlError::from)?;
4133        write_scalar(writer, "c:idx", &self.index.to_string(), None)?;
4134        self.sp_pr.write_xml_as(writer, "c:spPr")?;
4135        writer
4136            .write_event(Event::End(BytesEnd::new("c:dPt")))
4137            .map_err(OxmlError::from)?;
4138        Ok(())
4139    }
4140}
4141
4142/// The common formula-backed payload of one ChartML series.
4143#[derive(Clone, Debug, PartialEq)]
4144pub struct Series {
4145    pub index: u32,
4146    pub order: u32,
4147    pub name: Option<StringRef>,
4148    pub categories: Option<AxisData>,
4149    pub values: NumericData,
4150    pub bubble_size: Option<NumericData>,
4151    pub sp_pr: Option<CT_ShapeProperties>,
4152    pub data_labels: Option<CT_DLbls>,
4153    authored_data_points: Vec<DataPoint>,
4154    index_markup: ScalarMarkup,
4155    order_markup: ScalarMarkup,
4156    name_markup: Option<WrapperMarkup>,
4157    categories_markup: Option<WrapperMarkup>,
4158    values_markup: WrapperMarkup,
4159    bubble_size_markup: Option<WrapperMarkup>,
4160    opaque_name: bool,
4161    opaque_categories: bool,
4162    opaque_bubble_size: bool,
4163    uses_scatter_wrappers: bool,
4164    parsed_from_xml: bool,
4165    raw_attributes: XmlAttributes,
4166    namespace_declarations: XmlAttributes,
4167    raw_children: OrderedRawChildren,
4168}
4169
4170impl Series {
4171    pub fn new(index: u32, order: u32, values: NumericData) -> Self {
4172        Self {
4173            index,
4174            order,
4175            name: None,
4176            categories: None,
4177            values,
4178            bubble_size: None,
4179            sp_pr: None,
4180            data_labels: None,
4181            authored_data_points: Vec::new(),
4182            index_markup: ScalarMarkup::default(),
4183            order_markup: ScalarMarkup::default(),
4184            name_markup: None,
4185            categories_markup: None,
4186            values_markup: WrapperMarkup::default(),
4187            bubble_size_markup: None,
4188            opaque_name: false,
4189            opaque_categories: false,
4190            opaque_bubble_size: false,
4191            uses_scatter_wrappers: false,
4192            parsed_from_xml: false,
4193            raw_attributes: Vec::new(),
4194            namespace_declarations: Vec::new(),
4195            raw_children: OrderedRawChildren::default(),
4196        }
4197    }
4198
4199    pub fn from_xml(xml: &[u8]) -> Result<Self> {
4200        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
4201    }
4202
4203    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
4204        let mut reader = Reader::from_reader(xml);
4205        let mut buffer = Vec::new();
4206        loop {
4207            match reader
4208                .read_event_into(&mut buffer)
4209                .map_err(OxmlError::from)?
4210            {
4211                Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4212                    chart_root_prefix(&element)?;
4213                    if !element_is_in_namespace(&element, C_NS, inherited)? {
4214                        return Err(ChartError::UnexpectedElement(element_name(&element)));
4215                    }
4216                    return Self::from_element(&mut reader, &element, inherited);
4217                }
4218                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4219                    return Err(ChartError::MissingElement("c:idx".to_owned()));
4220                }
4221                Event::Start(element) | Event::Empty(element) => {
4222                    return Err(ChartError::UnexpectedElement(element_name(&element)));
4223                }
4224                Event::Eof => return Err(ChartError::MissingElement("c:ser".to_owned())),
4225                _ => {}
4226            }
4227            buffer.clear();
4228        }
4229    }
4230
4231    fn from_element(
4232        reader: &mut Reader<&[u8]>,
4233        start: &BytesStart<'_>,
4234        inherited: &NamespaceBindings,
4235    ) -> Result<Self> {
4236        reject_conflicting_prefix(start, b"a", A_NS)?;
4237        let namespaces = chart_bindings(inherited, start)?;
4238        require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
4239        require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
4240        require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
4241        let (raw_attributes, _) =
4242            capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
4243        let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
4244        let mut state = SeriesParseState::default();
4245        let mut buffer = Vec::new();
4246        loop {
4247            match reader
4248                .read_event_into(&mut buffer)
4249                .map_err(OxmlError::from)?
4250            {
4251                Event::Start(element) => {
4252                    let name = chart_child_local(&element, &namespaces)?;
4253                    let raw = capture_element(reader, &element)?;
4254                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
4255                }
4256                Event::Empty(element) => {
4257                    let name = chart_child_local(&element, &namespaces)?;
4258                    let raw = capture_empty_element(&element)?;
4259                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
4260                }
4261                event @ (Event::Text(_)
4262                | Event::CData(_)
4263                | Event::Comment(_)
4264                | Event::PI(_)
4265                | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
4266                Event::End(element) if matches_local_name(element.name().as_ref(), b"ser") => {
4267                    break;
4268                }
4269                Event::Eof => return Err(missing_end("c:ser")),
4270                _ => {}
4271            }
4272            buffer.clear();
4273        }
4274        state.finish(raw_attributes, namespace_declarations)
4275    }
4276
4277    pub fn to_xml(&self) -> Result<Vec<u8>> {
4278        self.to_xml_for_plot(self.uses_scatter_wrappers)
4279    }
4280
4281    fn to_xml_for_plot(&self, scatter: bool) -> Result<Vec<u8>> {
4282        if self.opaque_name && self.name.is_some() {
4283            return Err(ChartError::DuplicateElement("c:tx".to_owned()));
4284        }
4285        if self.opaque_categories && self.categories.is_some() {
4286            return Err(ChartError::DuplicateElement("c:cat".to_owned()));
4287        }
4288        if self.opaque_bubble_size && self.bubble_size.is_some() {
4289            return Err(ChartError::DuplicateElement("c:bubbleSize".to_owned()));
4290        }
4291        self.values.validate()?;
4292        if let Some(name) = &self.name {
4293            name.validate()?;
4294        }
4295        if let Some(categories) = &self.categories {
4296            if scatter && !matches!(categories, AxisData::Numeric(_)) {
4297                return Err(ChartError::InvalidValue {
4298                    element: "c:xVal".to_owned(),
4299                    value: "scatter x values must use a numeric cache".to_owned(),
4300                });
4301            }
4302            match categories {
4303                AxisData::String(reference) => reference.validate()?,
4304                AxisData::Numeric(reference) => reference.validate()?,
4305            }
4306        }
4307        if scatter && self.categories.is_none() {
4308            return Err(ChartError::MissingElement("c:xVal".to_owned()));
4309        }
4310        if let Some(size) = &self.bubble_size {
4311            size.validate()?;
4312        }
4313        if let Some(labels) = &self.data_labels {
4314            labels.validate()?;
4315        }
4316        let mut writer = Writer::new(Vec::new());
4317        let mut start = BytesStart::new("c:ser");
4318        start.push_attribute(("xmlns:c", C_NS));
4319        start.push_attribute(("xmlns:a", A_NS));
4320        start.push_attribute(("xmlns:r", R_NS));
4321        push_attributes(&mut start, &self.namespace_declarations);
4322        push_attributes(&mut start, &self.raw_attributes);
4323        writer
4324            .write_event(Event::Start(start))
4325            .map_err(OxmlError::from)?;
4326        emit_raw(&mut writer, self.raw_children.at(0))?;
4327        write_scalar(
4328            &mut writer,
4329            "c:idx",
4330            &self.index.to_string(),
4331            Some(&self.index_markup),
4332        )?;
4333        emit_raw(&mut writer, self.raw_children.at(1))?;
4334        write_scalar(
4335            &mut writer,
4336            "c:order",
4337            &self.order.to_string(),
4338            Some(&self.order_markup),
4339        )?;
4340        emit_raw(&mut writer, self.raw_children.at(2))?;
4341        if let Some(name) = &self.name {
4342            write_wrapper_start(
4343                &mut writer,
4344                "c:tx",
4345                self.name_markup
4346                    .as_ref()
4347                    .unwrap_or(&WrapperMarkup::default()),
4348            )?;
4349            name.write_xml(&mut writer, false)?;
4350            write_wrapper_end(
4351                &mut writer,
4352                "c:tx",
4353                self.name_markup
4354                    .as_ref()
4355                    .unwrap_or(&WrapperMarkup::default()),
4356            )?;
4357        }
4358        emit_raw(&mut writer, self.raw_children.at(3))?;
4359        if let Some(properties) = &self.sp_pr {
4360            properties.write_xml_as(&mut writer, "c:spPr")?;
4361        }
4362        emit_raw(&mut writer, self.raw_children.at(4))?;
4363        for point in &self.authored_data_points {
4364            point.write_xml(&mut writer)?;
4365        }
4366        if let Some(labels) = &self.data_labels {
4367            labels.write_xml(&mut writer, false)?;
4368        }
4369        emit_raw(&mut writer, self.raw_children.at(5))?;
4370        if let Some(categories) = &self.categories {
4371            let default_markup = WrapperMarkup::default();
4372            let markup = self.categories_markup.as_ref().unwrap_or(&default_markup);
4373            write_wrapper_start(
4374                &mut writer,
4375                if scatter { "c:xVal" } else { "c:cat" },
4376                markup,
4377            )?;
4378            match categories {
4379                AxisData::String(reference) => reference.write_xml(&mut writer, false)?,
4380                AxisData::Numeric(reference) => reference.write_xml(&mut writer, false)?,
4381            }
4382            write_wrapper_end(
4383                &mut writer,
4384                if scatter { "c:xVal" } else { "c:cat" },
4385                markup,
4386            )?;
4387        }
4388        emit_raw(&mut writer, self.raw_children.at(6))?;
4389        write_wrapper_start(
4390            &mut writer,
4391            if scatter { "c:yVal" } else { "c:val" },
4392            &self.values_markup,
4393        )?;
4394        self.values.write_xml(&mut writer, false)?;
4395        write_wrapper_end(
4396            &mut writer,
4397            if scatter { "c:yVal" } else { "c:val" },
4398            &self.values_markup,
4399        )?;
4400        emit_raw(&mut writer, self.raw_children.at(7))?;
4401        if let Some(size) = &self.bubble_size {
4402            let default_markup = WrapperMarkup::default();
4403            let markup = self.bubble_size_markup.as_ref().unwrap_or(&default_markup);
4404            write_wrapper_start(&mut writer, "c:bubbleSize", markup)?;
4405            size.write_xml(&mut writer, false)?;
4406            write_wrapper_end(&mut writer, "c:bubbleSize", markup)?;
4407        }
4408        emit_raw(&mut writer, self.raw_children.at(8))?;
4409        writer
4410            .write_event(Event::End(BytesEnd::new("c:ser")))
4411            .map_err(OxmlError::from)?;
4412        Ok(writer.into_inner())
4413    }
4414
4415    pub fn raw_children(&self) -> &OrderedRawChildren {
4416        &self.raw_children
4417    }
4418}
4419
4420#[derive(Default)]
4421struct SeriesParseState {
4422    index: Option<(u32, ScalarMarkup)>,
4423    order: Option<(u32, ScalarMarkup)>,
4424    name: Option<(StringRef, WrapperMarkup)>,
4425    categories: Option<(AxisData, WrapperMarkup)>,
4426    values: Option<(NumericData, WrapperMarkup)>,
4427    bubble_size: Option<(NumericData, WrapperMarkup)>,
4428    data_labels: Option<CT_DLbls>,
4429    name_seen: bool,
4430    categories_seen: bool,
4431    bubble_size_seen: bool,
4432    opaque_name: bool,
4433    opaque_categories: bool,
4434    opaque_bubble_size: bool,
4435    saw_standard_wrapper: bool,
4436    saw_scatter_wrapper: bool,
4437    sp_pr: Option<CT_ShapeProperties>,
4438    raw_children: OrderedRawChildren,
4439    boundary: usize,
4440}
4441
4442impl SeriesParseState {
4443    fn capture_event(&mut self, raw: Vec<u8>) {
4444        self.raw_children.push(self.boundary, raw);
4445    }
4446
4447    fn parse_child(
4448        &mut self,
4449        name: &[u8],
4450        raw: Vec<u8>,
4451        namespaces: &NamespaceBindings,
4452    ) -> Result<()> {
4453        match name {
4454            b"idx" => {
4455                set_once(&mut self.index, parse_u32_scalar(&raw, "idx")?, "c:idx")?;
4456                self.boundary = self.boundary.max(1);
4457            }
4458            b"order" => {
4459                set_once(&mut self.order, parse_u32_scalar(&raw, "order")?, "c:order")?;
4460                self.boundary = self.boundary.max(2);
4461            }
4462            b"tx" => {
4463                mark_once(&mut self.name_seen, "c:tx")?;
4464                let parsed = parse_wrapper(&raw, b"tx", &[b"strRef"], namespaces)?;
4465                if let Some((_, reference)) = parsed.choice {
4466                    self.name = Some((
4467                        StringRef::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4468                        parsed.markup,
4469                    ));
4470                } else {
4471                    self.opaque_name = true;
4472                    self.raw_children.push(2, raw);
4473                }
4474                self.boundary = self.boundary.max(3);
4475            }
4476            b"spPr" => {
4477                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
4478                let properties = CT_ShapeProperties::from_xml(&raw)?;
4479                let mut writer = Writer::new(Vec::new());
4480                properties.write_xml_as(&mut writer, "c:spPr")?;
4481                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
4482                set_once(&mut self.sp_pr, properties, "c:spPr")?;
4483                self.boundary = self.boundary.max(4);
4484            }
4485            b"dLbls" => {
4486                set_once(
4487                    &mut self.data_labels,
4488                    CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
4489                    "c:dLbls",
4490                )?;
4491                self.boundary = self.boundary.max(5);
4492            }
4493            b"cat" => {
4494                self.saw_standard_wrapper = true;
4495                mark_once(&mut self.categories_seen, "c:cat")?;
4496                let parsed = parse_wrapper(&raw, b"cat", &[b"strRef", b"numRef"], namespaces)?;
4497                if let Some((choice, reference)) = parsed.choice {
4498                    let categories = if choice == b"strRef" {
4499                        AxisData::String(StringRef::from_xml_with_namespaces(
4500                            &reference,
4501                            &parsed.namespaces,
4502                        )?)
4503                    } else {
4504                        AxisData::Numeric(NumericData::from_xml_with_namespaces(
4505                            &reference,
4506                            &parsed.namespaces,
4507                        )?)
4508                    };
4509                    self.categories = Some((categories, parsed.markup));
4510                } else {
4511                    self.opaque_categories = true;
4512                    self.raw_children.push(5, raw);
4513                }
4514                self.boundary = self.boundary.max(6);
4515            }
4516            b"xVal" => {
4517                self.saw_scatter_wrapper = true;
4518                mark_once(&mut self.categories_seen, "c:xVal")?;
4519                let parsed = parse_wrapper(&raw, b"xVal", &[b"numRef"], namespaces)?;
4520                let (_, reference) = parsed
4521                    .choice
4522                    .ok_or_else(|| ChartError::MissingElement("c:xVal/c:numRef".to_owned()))?;
4523                self.categories = Some((
4524                    AxisData::Numeric(NumericData::from_xml_with_namespaces(
4525                        &reference,
4526                        &parsed.namespaces,
4527                    )?),
4528                    parsed.markup,
4529                ));
4530                self.boundary = self.boundary.max(6);
4531            }
4532            b"val" | b"yVal" => {
4533                if name == b"yVal" {
4534                    self.saw_scatter_wrapper = true;
4535                } else {
4536                    self.saw_standard_wrapper = true;
4537                }
4538                let wrapper = if name == b"yVal" {
4539                    b"yVal".as_slice()
4540                } else {
4541                    b"val".as_slice()
4542                };
4543                let parsed = parse_wrapper(&raw, wrapper, &[b"numRef"], namespaces)?;
4544                let (_, reference) = parsed.choice.ok_or_else(|| {
4545                    ChartError::MissingElement(format!(
4546                        "c:{}/c:numRef",
4547                        String::from_utf8_lossy(wrapper)
4548                    ))
4549                })?;
4550                set_once(
4551                    &mut self.values,
4552                    (
4553                        NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4554                        parsed.markup,
4555                    ),
4556                    "c:val or c:yVal",
4557                )?;
4558                self.boundary = self.boundary.max(7);
4559            }
4560            b"bubbleSize" => {
4561                mark_once(&mut self.bubble_size_seen, "c:bubbleSize")?;
4562                let parsed = parse_wrapper(&raw, b"bubbleSize", &[b"numRef"], namespaces)?;
4563                if let Some((_, reference)) = parsed.choice {
4564                    self.bubble_size = Some((
4565                        NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4566                        parsed.markup,
4567                    ));
4568                } else {
4569                    self.opaque_bubble_size = true;
4570                    self.raw_children.push(7, raw);
4571                }
4572                self.boundary = self.boundary.max(8);
4573            }
4574            _ => {
4575                let boundary = series_raw_boundary(name, self.boundary);
4576                self.raw_children.push(boundary, raw);
4577                self.boundary = self.boundary.max(boundary);
4578            }
4579        }
4580        Ok(())
4581    }
4582
4583    fn finish(
4584        self,
4585        raw_attributes: XmlAttributes,
4586        namespace_declarations: XmlAttributes,
4587    ) -> Result<Series> {
4588        if self.saw_standard_wrapper && self.saw_scatter_wrapper {
4589            return Err(ChartError::InvalidValue {
4590                element: "c:ser".to_owned(),
4591                value: "category/value and x/y wrappers cannot be combined".to_owned(),
4592            });
4593        }
4594        let (index, index_markup) = self
4595            .index
4596            .ok_or_else(|| ChartError::MissingElement("c:idx".to_owned()))?;
4597        let (order, order_markup) = self
4598            .order
4599            .ok_or_else(|| ChartError::MissingElement("c:order".to_owned()))?;
4600        let (values, values_markup) = self
4601            .values
4602            .ok_or_else(|| ChartError::MissingElement("c:val".to_owned()))?;
4603        let (name, name_markup) = self
4604            .name
4605            .map(|(value, markup)| (Some(value), Some(markup)))
4606            .unwrap_or((None, None));
4607        let (categories, categories_markup) = self
4608            .categories
4609            .map(|(value, markup)| (Some(value), Some(markup)))
4610            .unwrap_or((None, None));
4611        let (bubble_size, bubble_size_markup) = self
4612            .bubble_size
4613            .map(|(value, markup)| (Some(value), Some(markup)))
4614            .unwrap_or((None, None));
4615        Ok(Series {
4616            index,
4617            order,
4618            name,
4619            categories,
4620            values,
4621            bubble_size,
4622            sp_pr: self.sp_pr,
4623            data_labels: self.data_labels,
4624            authored_data_points: Vec::new(),
4625            index_markup,
4626            order_markup,
4627            name_markup,
4628            categories_markup,
4629            values_markup,
4630            bubble_size_markup,
4631            opaque_name: self.opaque_name,
4632            opaque_categories: self.opaque_categories,
4633            opaque_bubble_size: self.opaque_bubble_size,
4634            uses_scatter_wrappers: self.saw_scatter_wrapper,
4635            parsed_from_xml: true,
4636            raw_attributes,
4637            namespace_declarations,
4638            raw_children: raw_children_in_schema_order(&self.raw_children, 8),
4639        })
4640    }
4641}
4642
4643fn series_raw_boundary(name: &[u8], current: usize) -> usize {
4644    match name {
4645        b"marker" | b"invertIfNegative" | b"pictureOptions" | b"explosion" | b"dPt" => 4,
4646        b"trendline" | b"errBars" => 5,
4647        b"shape" | b"smooth" => 7,
4648        b"extLst" => 8,
4649        _ => current,
4650    }
4651}
4652
4653struct ParsedReference {
4654    formula: String,
4655    format_code: Option<String>,
4656    values: Vec<String>,
4657    markup: ReferenceMarkup,
4658}
4659
4660fn parse_reference(
4661    xml: &[u8],
4662    reference_local: &[u8],
4663    cache_local: &[u8],
4664    numeric: bool,
4665    inherited: &NamespaceBindings,
4666) -> Result<ParsedReference> {
4667    let mut reader = Reader::from_reader(xml);
4668    let mut buffer = Vec::new();
4669    loop {
4670        match reader
4671            .read_event_into(&mut buffer)
4672            .map_err(OxmlError::from)?
4673        {
4674            Event::Start(element)
4675                if matches_local_name(element.name().as_ref(), reference_local) =>
4676            {
4677                let namespaces = typed_rewrite_bindings(&element, inherited)?;
4678                let raw_attributes = capture_fixed_attributes(&element, &["xmlns:c"])?;
4679                let mut formula: Option<(String, TextMarkup)> = None;
4680                let mut cache: Option<(Option<String>, Vec<String>, CacheMarkup)> = None;
4681                let mut raw_children = OrderedRawChildren::default();
4682                let mut boundary = 0usize;
4683                let mut inner = Vec::new();
4684                loop {
4685                    match reader
4686                        .read_event_into(&mut inner)
4687                        .map_err(OxmlError::from)?
4688                    {
4689                        Event::Start(child) => {
4690                            let name = chart_child_local(&child, &namespaces)?;
4691                            let raw = capture_element(&mut reader, &child)?;
4692                            match name.as_deref().unwrap_or_default() {
4693                                b"f" => {
4694                                    set_once(
4695                                        &mut formula,
4696                                        parse_text_element(&raw, b"f", &namespaces)?,
4697                                        "c:f",
4698                                    )?;
4699                                    boundary = boundary.max(1);
4700                                }
4701                                name if name == cache_local => {
4702                                    set_once(
4703                                        &mut cache,
4704                                        parse_cache(&raw, cache_local, numeric, &namespaces)?,
4705                                        &format!("c:{}", String::from_utf8_lossy(cache_local)),
4706                                    )?;
4707                                    boundary = boundary.max(2);
4708                                }
4709                                _ => raw_children.push(boundary, raw),
4710                            }
4711                        }
4712                        Event::Empty(child) => {
4713                            let name = chart_child_local(&child, &namespaces)?;
4714                            let raw = capture_empty_element(&child)?;
4715                            match name.as_deref().unwrap_or_default() {
4716                                b"f" => {
4717                                    set_once(
4718                                        &mut formula,
4719                                        parse_text_element(&raw, b"f", &namespaces)?,
4720                                        "c:f",
4721                                    )?;
4722                                    boundary = boundary.max(1);
4723                                }
4724                                name if name == cache_local => {
4725                                    set_once(
4726                                        &mut cache,
4727                                        parse_cache(&raw, cache_local, numeric, &namespaces)?,
4728                                        &format!("c:{}", String::from_utf8_lossy(cache_local)),
4729                                    )?;
4730                                    boundary = boundary.max(2);
4731                                }
4732                                _ => raw_children.push(boundary, raw),
4733                            }
4734                        }
4735                        event @ (Event::Text(_)
4736                        | Event::CData(_)
4737                        | Event::Comment(_)
4738                        | Event::PI(_)
4739                        | Event::GeneralRef(_)) => {
4740                            raw_children.push(boundary, capture_event(event)?);
4741                        }
4742                        Event::End(end)
4743                            if matches_local_name(end.name().as_ref(), reference_local) =>
4744                        {
4745                            let (formula, formula_markup) = formula
4746                                .ok_or_else(|| ChartError::MissingElement("c:f".to_owned()))?;
4747                            validate_formula(&formula, "c:f")?;
4748                            let (format_code, values, cache_markup) = cache.ok_or_else(|| {
4749                                ChartError::MissingElement(format!(
4750                                    "c:{}",
4751                                    String::from_utf8_lossy(cache_local)
4752                                ))
4753                            })?;
4754                            let markup = ReferenceMarkup {
4755                                raw_attributes,
4756                                raw_children: raw_children_in_schema_order(&raw_children, 2),
4757                                formula: formula_markup,
4758                                cache_attributes: cache_markup.raw_attributes,
4759                                cache_children: cache_markup.raw_children,
4760                                format_code: cache_markup.format_code,
4761                                format_code_omitted: cache_markup.format_code_omitted,
4762                                point_count: cache_markup.point_count,
4763                                declared_point_count: cache_markup.declared_point_count,
4764                                point_indexes: cache_markup.point_indexes,
4765                                points: cache_markup.points,
4766                            };
4767                            return Ok(ParsedReference {
4768                                formula,
4769                                format_code,
4770                                values,
4771                                markup,
4772                            });
4773                        }
4774                        Event::Eof => {
4775                            return Err(missing_end(&format!(
4776                                "c:{}",
4777                                String::from_utf8_lossy(reference_local)
4778                            )));
4779                        }
4780                        _ => {}
4781                    }
4782                    inner.clear();
4783                }
4784            }
4785            Event::Empty(element)
4786                if matches_local_name(element.name().as_ref(), reference_local) =>
4787            {
4788                return Err(ChartError::MissingElement("c:f".to_owned()));
4789            }
4790            Event::Start(element) | Event::Empty(element) => {
4791                return Err(ChartError::UnexpectedElement(element_name(&element)));
4792            }
4793            Event::Eof => {
4794                return Err(ChartError::MissingElement(format!(
4795                    "c:{}",
4796                    String::from_utf8_lossy(reference_local)
4797                )));
4798            }
4799            _ => {}
4800        }
4801        buffer.clear();
4802    }
4803}
4804
4805struct CacheMarkup {
4806    raw_attributes: XmlAttributes,
4807    raw_children: OrderedRawChildren,
4808    format_code: Option<TextMarkup>,
4809    format_code_omitted: bool,
4810    point_count: ScalarMarkup,
4811    declared_point_count: Option<u32>,
4812    point_indexes: Vec<u32>,
4813    points: Vec<PointMarkup>,
4814}
4815
4816fn parse_cache(
4817    xml: &[u8],
4818    cache_local: &[u8],
4819    numeric: bool,
4820    inherited: &NamespaceBindings,
4821) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
4822    let mut reader = Reader::from_reader(xml);
4823    let mut buffer = Vec::new();
4824    loop {
4825        match reader
4826            .read_event_into(&mut buffer)
4827            .map_err(OxmlError::from)?
4828        {
4829            Event::Start(element) if matches_local_name(element.name().as_ref(), cache_local) => {
4830                let namespaces = typed_rewrite_bindings(&element, inherited)?;
4831                let raw_attributes = capture_attributes(&element)?;
4832                let mut state = CacheParseState::new(numeric, namespaces);
4833                let mut inner = Vec::new();
4834                loop {
4835                    match reader
4836                        .read_event_into(&mut inner)
4837                        .map_err(OxmlError::from)?
4838                    {
4839                        Event::Start(child) => {
4840                            let name = chart_child_local(&child, &state.namespaces)?;
4841                            let raw = capture_element(&mut reader, &child)?;
4842                            state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
4843                        }
4844                        Event::Empty(child) => {
4845                            let name = chart_child_local(&child, &state.namespaces)?;
4846                            let raw = capture_empty_element(&child)?;
4847                            state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
4848                        }
4849                        event @ (Event::Text(_)
4850                        | Event::CData(_)
4851                        | Event::Comment(_)
4852                        | Event::PI(_)
4853                        | Event::GeneralRef(_)) => {
4854                            state.capture_event(capture_event(event)?);
4855                        }
4856                        Event::End(end) if matches_local_name(end.name().as_ref(), cache_local) => {
4857                            return state.finish(raw_attributes);
4858                        }
4859                        Event::Eof => {
4860                            return Err(missing_end(&format!(
4861                                "c:{}",
4862                                String::from_utf8_lossy(cache_local)
4863                            )));
4864                        }
4865                        _ => {}
4866                    }
4867                    inner.clear();
4868                }
4869            }
4870            Event::Empty(element) if matches_local_name(element.name().as_ref(), cache_local) => {
4871                return Err(ChartError::MissingElement("c:ptCount".to_owned()));
4872            }
4873            Event::Start(element) | Event::Empty(element) => {
4874                return Err(ChartError::UnexpectedElement(element_name(&element)));
4875            }
4876            Event::Eof => {
4877                return Err(ChartError::MissingElement(format!(
4878                    "c:{}",
4879                    String::from_utf8_lossy(cache_local)
4880                )));
4881            }
4882            _ => {}
4883        }
4884        buffer.clear();
4885    }
4886}
4887
4888struct CacheParseState {
4889    numeric: bool,
4890    namespaces: NamespaceBindings,
4891    point_base: usize,
4892    boundary: usize,
4893    format_code: Option<(String, TextMarkup)>,
4894    point_count: Option<(u32, ScalarMarkup)>,
4895    points: Vec<(u32, String, PointMarkup)>,
4896    raw_children: OrderedRawChildren,
4897}
4898
4899impl CacheParseState {
4900    fn new(numeric: bool, namespaces: NamespaceBindings) -> Self {
4901        Self {
4902            numeric,
4903            namespaces,
4904            point_base: usize::from(numeric) + 1,
4905            boundary: 0,
4906            format_code: None,
4907            point_count: None,
4908            points: Vec::new(),
4909            raw_children: OrderedRawChildren::default(),
4910        }
4911    }
4912
4913    fn capture_event(&mut self, raw: Vec<u8>) {
4914        self.raw_children.push(self.boundary, raw);
4915    }
4916
4917    fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
4918        match name {
4919            b"formatCode" if self.numeric => {
4920                set_once(
4921                    &mut self.format_code,
4922                    parse_text_element(&raw, b"formatCode", &self.namespaces)?,
4923                    "c:formatCode",
4924                )?;
4925                self.boundary = self.boundary.max(1);
4926            }
4927            b"ptCount" => {
4928                set_once(
4929                    &mut self.point_count,
4930                    parse_u32_scalar(&raw, "ptCount")?,
4931                    "c:ptCount",
4932                )?;
4933                self.boundary = self.boundary.max(self.point_base);
4934            }
4935            b"pt" => {
4936                self.points.push(parse_point(&raw, &self.namespaces)?);
4937                self.boundary = self.boundary.max(self.point_base + self.points.len());
4938            }
4939            _ => self.raw_children.push(self.boundary, raw),
4940        }
4941        Ok(())
4942    }
4943
4944    fn finish(
4945        self,
4946        raw_attributes: XmlAttributes,
4947    ) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
4948        let (declared, point_count_markup) = self
4949            .point_count
4950            .ok_or_else(|| ChartError::MissingElement("c:ptCount".to_owned()))?;
4951        let actual = u32::try_from(self.points.len()).map_err(|_| ChartError::InvalidValue {
4952            element: "c:ptCount".to_owned(),
4953            value: self.points.len().to_string(),
4954        })?;
4955        if declared < actual {
4956            return Err(invalid_attribute("ptCount", "val", declared.to_string()));
4957        }
4958        let mut values = Vec::with_capacity(self.points.len());
4959        let mut point_indexes = Vec::with_capacity(self.points.len());
4960        let mut point_markup = Vec::with_capacity(self.points.len());
4961        let mut previous = None;
4962        for (index, value, markup) in self.points {
4963            if index >= declared || previous.is_some_and(|last| index <= last) {
4964                return Err(invalid_attribute("pt", "idx", index.to_string()));
4965            }
4966            previous = Some(index);
4967            values.push(value);
4968            point_indexes.push(index);
4969            point_markup.push(markup);
4970        }
4971        let (format_code, format_markup) = self
4972            .format_code
4973            .map(|(value, markup)| (Some(value), Some(markup)))
4974            .unwrap_or((None, None));
4975        let format_code_omitted = self.numeric && format_code.is_none();
4976        Ok((
4977            format_code,
4978            values,
4979            CacheMarkup {
4980                raw_attributes,
4981                raw_children: raw_children_in_schema_order(
4982                    &self.raw_children,
4983                    self.point_base + point_markup.len(),
4984                ),
4985                format_code: format_markup,
4986                format_code_omitted,
4987                point_count: point_count_markup,
4988                declared_point_count: Some(declared),
4989                point_indexes,
4990                points: point_markup,
4991            },
4992        ))
4993    }
4994}
4995
4996fn parse_point(xml: &[u8], inherited: &NamespaceBindings) -> Result<(u32, String, PointMarkup)> {
4997    let mut reader = Reader::from_reader(xml);
4998    let mut buffer = Vec::new();
4999    loop {
5000        match reader
5001            .read_event_into(&mut buffer)
5002            .map_err(OxmlError::from)?
5003        {
5004            Event::Start(element) if matches_local_name(element.name().as_ref(), b"pt") => {
5005                let namespaces = typed_rewrite_bindings(&element, inherited)?;
5006                let index = required_u32_attribute(&element, "pt", b"idx")?;
5007                let raw_attributes = capture_attributes_except(&element, b"idx")?;
5008                let mut value: Option<(String, TextMarkup)> = None;
5009                let mut raw_children = OrderedRawChildren::default();
5010                let mut boundary = 0usize;
5011                let mut inner = Vec::new();
5012                loop {
5013                    match reader
5014                        .read_event_into(&mut inner)
5015                        .map_err(OxmlError::from)?
5016                    {
5017                        Event::Start(child) => {
5018                            let name = chart_child_local(&child, &namespaces)?;
5019                            let raw = capture_element(&mut reader, &child)?;
5020                            if name.as_deref() == Some(b"v") {
5021                                set_once(
5022                                    &mut value,
5023                                    parse_text_element(&raw, b"v", &namespaces)?,
5024                                    "c:v",
5025                                )?;
5026                                boundary = 1;
5027                            } else {
5028                                raw_children.push(boundary, raw);
5029                            }
5030                        }
5031                        Event::Empty(child) => {
5032                            let name = chart_child_local(&child, &namespaces)?;
5033                            let raw = capture_empty_element(&child)?;
5034                            if name.as_deref() == Some(b"v") {
5035                                set_once(
5036                                    &mut value,
5037                                    parse_text_element(&raw, b"v", &namespaces)?,
5038                                    "c:v",
5039                                )?;
5040                                boundary = 1;
5041                            } else {
5042                                raw_children.push(boundary, raw);
5043                            }
5044                        }
5045                        event @ (Event::Text(_)
5046                        | Event::CData(_)
5047                        | Event::Comment(_)
5048                        | Event::PI(_)
5049                        | Event::GeneralRef(_)) => {
5050                            raw_children.push(boundary, capture_event(event)?);
5051                        }
5052                        Event::End(end) if matches_local_name(end.name().as_ref(), b"pt") => {
5053                            let (value, value_markup) = value
5054                                .ok_or_else(|| ChartError::MissingElement("c:v".to_owned()))?;
5055                            return Ok((
5056                                index,
5057                                value,
5058                                PointMarkup {
5059                                    raw_attributes,
5060                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
5061                                    value: value_markup,
5062                                },
5063                            ));
5064                        }
5065                        Event::Eof => return Err(missing_end("c:pt")),
5066                        _ => {}
5067                    }
5068                    inner.clear();
5069                }
5070            }
5071            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pt") => {
5072                return Err(ChartError::MissingElement("c:v".to_owned()));
5073            }
5074            Event::Start(element) | Event::Empty(element) => {
5075                return Err(ChartError::UnexpectedElement(element_name(&element)));
5076            }
5077            Event::Eof => return Err(ChartError::MissingElement("c:pt".to_owned())),
5078            _ => {}
5079        }
5080        buffer.clear();
5081    }
5082}
5083
5084fn parse_text_element(
5085    xml: &[u8],
5086    local: &[u8],
5087    inherited: &NamespaceBindings,
5088) -> Result<(String, TextMarkup)> {
5089    let mut reader = Reader::from_reader(xml);
5090    let mut buffer = Vec::new();
5091    loop {
5092        match reader
5093            .read_event_into(&mut buffer)
5094            .map_err(OxmlError::from)?
5095        {
5096            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
5097                typed_rewrite_bindings(&element, inherited)?;
5098                let raw_attributes = capture_attributes(&element)?;
5099                let mut value = String::new();
5100                let mut raw_children = OrderedRawChildren::default();
5101                let mut seen_text = false;
5102                let mut inner = Vec::new();
5103                loop {
5104                    match reader
5105                        .read_event_into(&mut inner)
5106                        .map_err(OxmlError::from)?
5107                    {
5108                        Event::Text(text) => {
5109                            value.push_str(&decode_plain(&text));
5110                            seen_text = true;
5111                        }
5112                        Event::CData(text) => {
5113                            let decoded =
5114                                text.decode().map_err(|error| ChartError::InvalidValue {
5115                                    element: format!("c:{}", String::from_utf8_lossy(local)),
5116                                    value: error.to_string(),
5117                                })?;
5118                            value.push_str(&decoded);
5119                            seen_text = true;
5120                        }
5121                        Event::GeneralRef(reference) => {
5122                            value.push_str(&resolve_entity(&reference));
5123                            seen_text = true;
5124                        }
5125                        event @ (Event::Comment(_) | Event::PI(_)) => {
5126                            raw_children.push(usize::from(seen_text), capture_event(event)?);
5127                        }
5128                        Event::Start(child) | Event::Empty(child) => {
5129                            return Err(ChartError::UnexpectedElement(element_name(&child)));
5130                        }
5131                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
5132                            return Ok((
5133                                value,
5134                                TextMarkup {
5135                                    raw_attributes,
5136                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
5137                                },
5138                            ));
5139                        }
5140                        Event::Eof => {
5141                            return Err(missing_end(&format!(
5142                                "c:{}",
5143                                String::from_utf8_lossy(local)
5144                            )));
5145                        }
5146                        _ => {}
5147                    }
5148                    inner.clear();
5149                }
5150            }
5151            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
5152                typed_rewrite_bindings(&element, inherited)?;
5153                return Ok((
5154                    String::new(),
5155                    TextMarkup {
5156                        raw_attributes: capture_attributes(&element)?,
5157                        raw_children: OrderedRawChildren::default(),
5158                    },
5159                ));
5160            }
5161            Event::Start(element) | Event::Empty(element) => {
5162                return Err(ChartError::UnexpectedElement(element_name(&element)));
5163            }
5164            Event::Eof => {
5165                return Err(ChartError::MissingElement(format!(
5166                    "c:{}",
5167                    String::from_utf8_lossy(local)
5168                )));
5169            }
5170            _ => {}
5171        }
5172        buffer.clear();
5173    }
5174}
5175
5176struct ParsedWrapper {
5177    choice: Option<(Vec<u8>, Vec<u8>)>,
5178    markup: WrapperMarkup,
5179    namespaces: NamespaceBindings,
5180}
5181
5182fn parse_wrapper(
5183    xml: &[u8],
5184    local: &[u8],
5185    choices: &[&[u8]],
5186    inherited: &NamespaceBindings,
5187) -> Result<ParsedWrapper> {
5188    let mut reader = Reader::from_reader(xml);
5189    let mut buffer = Vec::new();
5190    loop {
5191        match reader
5192            .read_event_into(&mut buffer)
5193            .map_err(OxmlError::from)?
5194        {
5195            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
5196                let namespaces = typed_rewrite_bindings(&element, inherited)?;
5197                let raw_attributes = capture_attributes(&element)?;
5198                let mut choice = None;
5199                let mut raw_children = OrderedRawChildren::default();
5200                let mut boundary = 0usize;
5201                let mut inner = Vec::new();
5202                loop {
5203                    match reader
5204                        .read_event_into(&mut inner)
5205                        .map_err(OxmlError::from)?
5206                    {
5207                        Event::Start(child) => {
5208                            let name = chart_child_local(&child, &namespaces)?;
5209                            let raw = capture_element(&mut reader, &child)?;
5210                            if let Some(name) =
5211                                name.filter(|name| choices.contains(&name.as_slice()))
5212                            {
5213                                if choice.is_some() {
5214                                    return Err(ChartError::DuplicateElement(format!(
5215                                        "c:{} reference",
5216                                        String::from_utf8_lossy(local)
5217                                    )));
5218                                }
5219                                choice = Some((name, raw));
5220                                boundary = 1;
5221                            } else {
5222                                raw_children.push(boundary, raw);
5223                            }
5224                        }
5225                        Event::Empty(child) => {
5226                            let name = chart_child_local(&child, &namespaces)?;
5227                            let raw = capture_empty_element(&child)?;
5228                            if let Some(name) =
5229                                name.filter(|name| choices.contains(&name.as_slice()))
5230                            {
5231                                if choice.is_some() {
5232                                    return Err(ChartError::DuplicateElement(format!(
5233                                        "c:{} reference",
5234                                        String::from_utf8_lossy(local)
5235                                    )));
5236                                }
5237                                choice = Some((name, raw));
5238                                boundary = 1;
5239                            } else {
5240                                raw_children.push(boundary, raw);
5241                            }
5242                        }
5243                        event @ (Event::Text(_)
5244                        | Event::CData(_)
5245                        | Event::Comment(_)
5246                        | Event::PI(_)
5247                        | Event::GeneralRef(_)) => {
5248                            raw_children.push(boundary, capture_event(event)?);
5249                        }
5250                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
5251                            return Ok(ParsedWrapper {
5252                                choice,
5253                                markup: WrapperMarkup {
5254                                    raw_attributes,
5255                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
5256                                },
5257                                namespaces,
5258                            });
5259                        }
5260                        Event::Eof => {
5261                            return Err(missing_end(&format!(
5262                                "c:{}",
5263                                String::from_utf8_lossy(local)
5264                            )));
5265                        }
5266                        _ => {}
5267                    }
5268                    inner.clear();
5269                }
5270            }
5271            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
5272                let namespaces = chart_bindings(inherited, &element)?;
5273                return Ok(ParsedWrapper {
5274                    choice: None,
5275                    markup: WrapperMarkup {
5276                        raw_attributes: capture_attributes(&element)?,
5277                        raw_children: OrderedRawChildren::default(),
5278                    },
5279                    namespaces,
5280                });
5281            }
5282            Event::Start(element) | Event::Empty(element) => {
5283                return Err(ChartError::UnexpectedElement(element_name(&element)));
5284            }
5285            Event::Eof => {
5286                return Err(ChartError::MissingElement(format!(
5287                    "c:{}",
5288                    String::from_utf8_lossy(local)
5289                )));
5290            }
5291            _ => {}
5292        }
5293        buffer.clear();
5294    }
5295}
5296
5297fn write_string_cache(
5298    writer: &mut Writer<Vec<u8>>,
5299    values: &[String],
5300    markup: &ReferenceMarkup,
5301) -> Result<()> {
5302    let (declared_count, indexes) = cache_layout(markup, values.len())?;
5303    let mut start = BytesStart::new("c:strCache");
5304    push_attributes(&mut start, &markup.cache_attributes);
5305    writer
5306        .write_event(Event::Start(start))
5307        .map_err(OxmlError::from)?;
5308    emit_raw(writer, markup.cache_children.at(0))?;
5309    write_scalar(
5310        writer,
5311        "c:ptCount",
5312        &declared_count.to_string(),
5313        Some(&markup.point_count),
5314    )?;
5315    emit_cache_point_raw(writer, markup, 1, values.len(), 0)?;
5316    for (position, value) in values.iter().enumerate() {
5317        let default_markup = PointMarkup::default();
5318        write_point(
5319            writer,
5320            indexes[position],
5321            value,
5322            markup.points.get(position).unwrap_or(&default_markup),
5323        )?;
5324        emit_cache_point_raw(writer, markup, 1, values.len(), position + 1)?;
5325    }
5326    writer
5327        .write_event(Event::End(BytesEnd::new("c:strCache")))
5328        .map_err(OxmlError::from)?;
5329    Ok(())
5330}
5331
5332fn write_numeric_cache(
5333    writer: &mut Writer<Vec<u8>>,
5334    data: &NumericData,
5335    markup: &ReferenceMarkup,
5336) -> Result<()> {
5337    let (declared_count, indexes) = cache_layout(markup, data.values.len())?;
5338    let mut start = BytesStart::new("c:numCache");
5339    push_attributes(&mut start, &markup.cache_attributes);
5340    writer
5341        .write_event(Event::Start(start))
5342        .map_err(OxmlError::from)?;
5343    emit_raw(writer, markup.cache_children.at(0))?;
5344    if !markup.format_code_omitted || data.format_code != "General" {
5345        let default_format_markup = TextMarkup::default();
5346        write_text(
5347            writer,
5348            "c:formatCode",
5349            &data.format_code,
5350            markup
5351                .format_code
5352                .as_ref()
5353                .unwrap_or(&default_format_markup),
5354        )?;
5355    }
5356    emit_raw(writer, markup.cache_children.at(1))?;
5357    write_scalar(
5358        writer,
5359        "c:ptCount",
5360        &declared_count.to_string(),
5361        Some(&markup.point_count),
5362    )?;
5363    emit_cache_point_raw(writer, markup, 2, data.values.len(), 0)?;
5364    for (position, value) in data.values.iter().enumerate() {
5365        let default_markup = PointMarkup::default();
5366        write_point(
5367            writer,
5368            indexes[position],
5369            &value.to_string(),
5370            markup.points.get(position).unwrap_or(&default_markup),
5371        )?;
5372        emit_cache_point_raw(writer, markup, 2, data.values.len(), position + 1)?;
5373    }
5374    writer
5375        .write_event(Event::End(BytesEnd::new("c:numCache")))
5376        .map_err(OxmlError::from)?;
5377    Ok(())
5378}
5379
5380fn emit_cache_point_raw(
5381    writer: &mut Writer<Vec<u8>>,
5382    markup: &ReferenceMarkup,
5383    point_base: usize,
5384    current_count: usize,
5385    completed_points: usize,
5386) -> Result<()> {
5387    let original_count = markup.points.len();
5388    if current_count == 0 {
5389        for boundary in point_base..=point_base + original_count {
5390            emit_raw(writer, markup.cache_children.at(boundary))?;
5391        }
5392    } else if completed_points == 0 {
5393        if original_count > 0 {
5394            emit_raw(writer, markup.cache_children.at(point_base))?;
5395        }
5396    } else if completed_points < current_count {
5397        if completed_points < original_count {
5398            emit_raw(
5399                writer,
5400                markup.cache_children.at(point_base + completed_points),
5401            )?;
5402        }
5403    } else {
5404        let first_tail = if original_count == 0 {
5405            point_base
5406        } else {
5407            point_base + current_count.min(original_count)
5408        };
5409        for boundary in first_tail..=point_base + original_count {
5410            emit_raw(writer, markup.cache_children.at(boundary))?;
5411        }
5412    }
5413    Ok(())
5414}
5415
5416fn write_point(
5417    writer: &mut Writer<Vec<u8>>,
5418    index: u32,
5419    value: &str,
5420    markup: &PointMarkup,
5421) -> Result<()> {
5422    let index = index.to_string();
5423    let mut start = BytesStart::new("c:pt");
5424    start.push_attribute(("idx", index.as_str()));
5425    push_attributes(&mut start, &markup.raw_attributes);
5426    writer
5427        .write_event(Event::Start(start))
5428        .map_err(OxmlError::from)?;
5429    emit_raw(writer, markup.raw_children.at(0))?;
5430    write_text(writer, "c:v", value, &markup.value)?;
5431    emit_raw(writer, markup.raw_children.at(1))?;
5432    writer
5433        .write_event(Event::End(BytesEnd::new("c:pt")))
5434        .map_err(OxmlError::from)?;
5435    Ok(())
5436}
5437
5438fn cache_layout(markup: &ReferenceMarkup, value_count: usize) -> Result<(u32, Vec<u32>)> {
5439    if markup.point_indexes.len() == value_count
5440        && let Some(declared) = markup.declared_point_count
5441    {
5442        return Ok((declared, markup.point_indexes.clone()));
5443    }
5444    let declared = u32::try_from(value_count).map_err(|_| ChartError::InvalidValue {
5445        element: "c:ptCount".to_owned(),
5446        value: value_count.to_string(),
5447    })?;
5448    Ok((declared, (0..declared).collect()))
5449}
5450
5451fn write_text(
5452    writer: &mut Writer<Vec<u8>>,
5453    tag: &str,
5454    value: &str,
5455    markup: &TextMarkup,
5456) -> Result<()> {
5457    let mut start = BytesStart::new(tag);
5458    push_attributes(&mut start, &markup.raw_attributes);
5459    writer
5460        .write_event(Event::Start(start))
5461        .map_err(OxmlError::from)?;
5462    emit_raw(writer, markup.raw_children.at(0))?;
5463    writer
5464        .write_event(Event::Text(BytesText::new(value)))
5465        .map_err(OxmlError::from)?;
5466    emit_raw(writer, markup.raw_children.at(1))?;
5467    writer
5468        .write_event(Event::End(BytesEnd::new(tag)))
5469        .map_err(OxmlError::from)?;
5470    Ok(())
5471}
5472
5473fn write_wrapper_start(
5474    writer: &mut Writer<Vec<u8>>,
5475    tag: &str,
5476    markup: &WrapperMarkup,
5477) -> Result<()> {
5478    let mut start = BytesStart::new(tag);
5479    push_attributes(&mut start, &markup.raw_attributes);
5480    writer
5481        .write_event(Event::Start(start))
5482        .map_err(OxmlError::from)?;
5483    emit_raw(writer, markup.raw_children.at(0))
5484}
5485
5486fn write_wrapper_end(
5487    writer: &mut Writer<Vec<u8>>,
5488    tag: &str,
5489    markup: &WrapperMarkup,
5490) -> Result<()> {
5491    emit_raw(writer, markup.raw_children.at(1))?;
5492    writer
5493        .write_event(Event::End(BytesEnd::new(tag)))
5494        .map_err(OxmlError::from)?;
5495    Ok(())
5496}
5497
5498fn validate_formula(formula: &str, element: &str) -> Result<()> {
5499    if formula.trim().is_empty() {
5500        return Err(ChartError::InvalidValue {
5501            element: element.to_owned(),
5502            value: formula.to_owned(),
5503        });
5504    }
5505    validate_xml_text(formula, element)
5506}
5507
5508fn validate_format_code(format_code: &str) -> Result<()> {
5509    if format_code.is_empty() {
5510        return Err(ChartError::InvalidValue {
5511            element: "c:formatCode".to_owned(),
5512            value: format_code.to_owned(),
5513        });
5514    }
5515    validate_xml_text(format_code, "c:formatCode")
5516}
5517
5518fn validate_xml_text(value: &str, element: &str) -> Result<()> {
5519    if value.chars().all(|character| {
5520        matches!(
5521            character,
5522            '\u{9}' | '\u{A}' | '\u{D}' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}'
5523        )
5524    }) {
5525        return Ok(());
5526    }
5527    Err(ChartError::InvalidValue {
5528        element: element.to_owned(),
5529        value: value.to_owned(),
5530    })
5531}
5532
5533fn validate_numeric_values(values: &[f64]) -> Result<()> {
5534    if let Some(value) = values.iter().find(|value| !value.is_finite()) {
5535        return Err(ChartError::InvalidValue {
5536            element: "c:v".to_owned(),
5537            value: value.to_string(),
5538        });
5539    }
5540    Ok(())
5541}
5542
5543fn validate_point_count(count: usize, element: &str) -> Result<()> {
5544    u32::try_from(count)
5545        .map(|_| ())
5546        .map_err(|_| ChartError::InvalidValue {
5547            element: element.to_owned(),
5548            value: count.to_string(),
5549        })
5550}
5551
5552fn parse_u32_scalar(xml: &[u8], local: &str) -> Result<(u32, ScalarMarkup)> {
5553    let (value, markup) = scalar_value(xml, local)?;
5554    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
5555    let parsed = value
5556        .parse::<u32>()
5557        .map_err(|_| invalid_attribute(local, "val", value))?;
5558    Ok((parsed, markup))
5559}
5560
5561fn required_u32_attribute(element: &BytesStart<'_>, local: &str, attribute: &[u8]) -> Result<u32> {
5562    let value = attribute_value(element, attribute)?.ok_or_else(|| {
5563        invalid_attribute(
5564            local,
5565            &String::from_utf8_lossy(attribute),
5566            "<missing>".to_owned(),
5567        )
5568    })?;
5569    value
5570        .parse::<u32>()
5571        .map_err(|_| invalid_attribute(local, &String::from_utf8_lossy(attribute), value))
5572}
5573
5574fn chart_root_prefix(element: &BytesStart<'_>) -> Result<Vec<u8>> {
5575    let name = element.name();
5576    let qualified = name.as_ref();
5577    let prefix = qualified
5578        .iter()
5579        .position(|byte| *byte == b':')
5580        .map(|position| qualified[..position].to_vec())
5581        .unwrap_or_default();
5582    let declaration = if prefix.is_empty() {
5583        b"xmlns".to_vec()
5584    } else {
5585        let mut declaration = b"xmlns:".to_vec();
5586        declaration.extend_from_slice(&prefix);
5587        declaration
5588    };
5589    if let Some(namespace) = attribute_value(element, &declaration)?
5590        && namespace != C_NS
5591    {
5592        return Err(invalid_attribute(
5593            &element_name(element),
5594            "namespace",
5595            namespace,
5596        ));
5597    }
5598    reject_conflicting_prefix(element, b"c", C_NS)?;
5599    Ok(prefix)
5600}
5601
5602fn typed_rewrite_bindings(
5603    element: &BytesStart<'_>,
5604    inherited: &NamespaceBindings,
5605) -> Result<NamespaceBindings> {
5606    chart_root_prefix(element)?;
5607    if !element_is_in_namespace(element, C_NS, inherited)? {
5608        return Err(ChartError::UnexpectedElement(element_name(element)));
5609    }
5610    let bindings = chart_bindings(inherited, element)?;
5611    require_fixed_namespace(&bindings, b"c", C_NS, element)?;
5612    Ok(bindings)
5613}
5614
5615fn chart_namespace_defaults() -> NamespaceBindings {
5616    vec![
5617        (b"c".to_vec(), C_NS.to_owned()),
5618        (b"a".to_vec(), A_NS.to_owned()),
5619        (b"r".to_vec(), R_NS.to_owned()),
5620    ]
5621}
5622
5623fn chart_bindings(
5624    inherited: &NamespaceBindings,
5625    element: &BytesStart<'_>,
5626) -> Result<NamespaceBindings> {
5627    let mut bindings = chart_namespace_defaults();
5628    for (prefix, namespace) in inherited
5629        .iter()
5630        .cloned()
5631        .chain(namespace_bindings(element)?)
5632    {
5633        upsert_namespace_binding(&mut bindings, prefix, namespace);
5634    }
5635    Ok(bindings)
5636}
5637
5638fn root_chart_bindings(
5639    xml: &[u8],
5640    local: &[u8],
5641    inherited: &NamespaceBindings,
5642) -> Result<NamespaceBindings> {
5643    let mut reader = Reader::from_reader(xml);
5644    let mut buffer = Vec::new();
5645    loop {
5646        match reader
5647            .read_event_into(&mut buffer)
5648            .map_err(OxmlError::from)?
5649        {
5650            Event::Start(element) | Event::Empty(element)
5651                if matches_local_name(element.name().as_ref(), local) =>
5652            {
5653                if !element_is_in_namespace(&element, C_NS, inherited)? {
5654                    return Err(ChartError::UnexpectedElement(element_name(&element)));
5655                }
5656                return chart_bindings(inherited, &element);
5657            }
5658            Event::Start(element) | Event::Empty(element) => {
5659                return Err(ChartError::UnexpectedElement(element_name(&element)));
5660            }
5661            Event::Eof => {
5662                return Err(ChartError::MissingElement(format!(
5663                    "c:{}",
5664                    String::from_utf8_lossy(local)
5665                )));
5666            }
5667            _ => {}
5668        }
5669        buffer.clear();
5670    }
5671}
5672
5673fn chart_child_local(
5674    element: &BytesStart<'_>,
5675    inherited: &NamespaceBindings,
5676) -> Result<Option<Vec<u8>>> {
5677    Ok(element_is_in_namespace(element, C_NS, inherited)?
5678        .then(|| local_name(element.name().as_ref()).to_vec()))
5679}
5680
5681fn capture_fixed_root_attributes(
5682    start: &BytesStart<'_>,
5683    fixed: &[&str],
5684) -> Result<(XmlAttributes, XmlAttributes)> {
5685    let mut attributes = Vec::new();
5686    let mut namespaces = Vec::new();
5687    for attribute in start.attributes() {
5688        let attribute = attribute.map_err(OxmlError::from)?;
5689        let name = std::str::from_utf8(attribute.key.as_ref())
5690            .map_err(OxmlError::from)?
5691            .to_owned();
5692        let value = attribute
5693            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
5694            .map_err(OxmlError::from)?
5695            .into_owned();
5696        if fixed.contains(&name.as_str()) {
5697            continue;
5698        }
5699        if name == "xmlns" || name.starts_with("xmlns:") {
5700            namespaces.push((name, value));
5701        } else {
5702            attributes.push((name, value));
5703        }
5704    }
5705    Ok((attributes, namespaces))
5706}
5707
5708fn require_fixed_namespace(
5709    bindings: &NamespaceBindings,
5710    prefix: &[u8],
5711    expected: &str,
5712    element: &BytesStart<'_>,
5713) -> Result<()> {
5714    let actual = bindings
5715        .iter()
5716        .find(|(candidate, _)| candidate.as_slice() == prefix)
5717        .map(|(_, namespace)| namespace.as_str());
5718    if actual == Some(expected) {
5719        return Ok(());
5720    }
5721    Err(invalid_attribute(
5722        &element_name(element),
5723        &format!("xmlns:{}", String::from_utf8_lossy(prefix)),
5724        actual.unwrap_or("<missing>").to_owned(),
5725    ))
5726}
5727
5728fn standalone_namespace_declarations(bindings: &NamespaceBindings) -> Result<XmlAttributes> {
5729    bindings
5730        .iter()
5731        .filter(|(prefix, _)| prefix != b"c" && prefix != b"a" && prefix != b"r")
5732        .map(|(prefix, namespace)| {
5733            let prefix = std::str::from_utf8(prefix).map_err(OxmlError::from)?;
5734            let name = if prefix.is_empty() {
5735                "xmlns".to_owned()
5736            } else {
5737                format!("xmlns:{prefix}")
5738            };
5739            Ok((name, namespace.clone()))
5740        })
5741        .collect()
5742}
5743
5744fn capture_fixed_attributes(start: &BytesStart<'_>, fixed: &[&str]) -> Result<XmlAttributes> {
5745    let mut attributes = Vec::new();
5746    for attribute in start.attributes() {
5747        let attribute = attribute.map_err(OxmlError::from)?;
5748        let name = std::str::from_utf8(attribute.key.as_ref())
5749            .map_err(OxmlError::from)?
5750            .to_owned();
5751        if fixed.contains(&name.as_str()) {
5752            continue;
5753        }
5754        let value = attribute
5755            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
5756            .map_err(OxmlError::from)?
5757            .into_owned();
5758        attributes.push((name, value));
5759    }
5760    Ok(attributes)
5761}
5762
5763/// A producer-compatible ChartML axis identifier.
5764///
5765/// ECMA-376 names an unsigned value, but PowerPoint emits signed 32-bit
5766/// lexical values in real chart parts. The accepted domain covers both forms.
5767#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
5768pub struct AxisId(i64);
5769
5770impl AxisId {
5771    pub fn new(value: i64) -> Result<Self> {
5772        if (i64::from(i32::MIN)..=i64::from(u32::MAX)).contains(&value) {
5773            Ok(Self(value))
5774        } else {
5775            Err(ChartError::InvalidValue {
5776                element: "c:axId".to_owned(),
5777                value: value.to_string(),
5778            })
5779        }
5780    }
5781
5782    pub const fn value(self) -> i64 {
5783        self.0
5784    }
5785}
5786
5787/// Whether bars extend along the category or value axis.
5788#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5789pub enum BarDirection {
5790    Bar,
5791    Column,
5792}
5793
5794impl BarDirection {
5795    fn parse(value: &str) -> Option<Self> {
5796        match value {
5797            "bar" => Some(Self::Bar),
5798            "col" => Some(Self::Column),
5799            _ => None,
5800        }
5801    }
5802
5803    const fn as_str(self) -> &'static str {
5804        match self {
5805            Self::Bar => "bar",
5806            Self::Column => "col",
5807        }
5808    }
5809}
5810
5811/// Grouping modes supported by a two-dimensional bar plot.
5812#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5813pub enum BarGrouping {
5814    Clustered,
5815    PercentStacked,
5816    Stacked,
5817    Standard,
5818}
5819
5820impl BarGrouping {
5821    fn parse(value: &str) -> Option<Self> {
5822        match value {
5823            "clustered" => Some(Self::Clustered),
5824            "percentStacked" => Some(Self::PercentStacked),
5825            "stacked" => Some(Self::Stacked),
5826            "standard" => Some(Self::Standard),
5827            _ => None,
5828        }
5829    }
5830
5831    const fn as_str(self) -> &'static str {
5832        match self {
5833            Self::Clustered => "clustered",
5834            Self::PercentStacked => "percentStacked",
5835            Self::Stacked => "stacked",
5836            Self::Standard => "standard",
5837        }
5838    }
5839}
5840
5841/// Grouping modes shared by line and later area plots.
5842#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5843pub enum Grouping {
5844    PercentStacked,
5845    Stacked,
5846    Standard,
5847}
5848
5849impl Grouping {
5850    fn parse(value: &str) -> Option<Self> {
5851        match value {
5852            "percentStacked" => Some(Self::PercentStacked),
5853            "stacked" => Some(Self::Stacked),
5854            "standard" => Some(Self::Standard),
5855            _ => None,
5856        }
5857    }
5858
5859    const fn as_str(self) -> &'static str {
5860        match self {
5861            Self::PercentStacked => "percentStacked",
5862            Self::Stacked => "stacked",
5863            Self::Standard => "standard",
5864        }
5865    }
5866}
5867
5868/// Display style for a two-dimensional scatter plot.
5869#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5870pub enum ScatterStyle {
5871    Line,
5872    LineMarker,
5873    Marker,
5874    None,
5875    Smooth,
5876    SmoothMarker,
5877}
5878
5879impl ScatterStyle {
5880    fn parse(value: &str) -> Option<Self> {
5881        match value {
5882            "line" => Some(Self::Line),
5883            "lineMarker" => Some(Self::LineMarker),
5884            "marker" => Some(Self::Marker),
5885            "none" => Some(Self::None),
5886            "smooth" => Some(Self::Smooth),
5887            "smoothMarker" => Some(Self::SmoothMarker),
5888            _ => None,
5889        }
5890    }
5891
5892    const fn as_str(self) -> &'static str {
5893        match self {
5894            Self::Line => "line",
5895            Self::LineMarker => "lineMarker",
5896            Self::Marker => "marker",
5897            Self::None => "none",
5898            Self::Smooth => "smooth",
5899            Self::SmoothMarker => "smoothMarker",
5900        }
5901    }
5902}
5903
5904/// Display style for a two-dimensional radar plot.
5905#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5906pub enum RadarStyle {
5907    Filled,
5908    Marker,
5909    Standard,
5910}
5911
5912impl RadarStyle {
5913    fn parse(value: &str) -> Option<Self> {
5914        match value {
5915            "filled" => Some(Self::Filled),
5916            "marker" => Some(Self::Marker),
5917            "standard" => Some(Self::Standard),
5918            _ => None,
5919        }
5920    }
5921
5922    const fn as_str(self) -> &'static str {
5923        match self {
5924            Self::Filled => "filled",
5925            Self::Marker => "marker",
5926            Self::Standard => "standard",
5927        }
5928    }
5929}
5930
5931/// A supported two-dimensional plot owned by one plot area.
5932#[derive(Clone, Debug, PartialEq)]
5933pub enum Plot {
5934    Bar {
5935        direction: BarDirection,
5936        grouping: BarGrouping,
5937        gap_width: u16,
5938        overlap: i8,
5939        series: Vec<Series>,
5940        data_labels: Option<CT_DLbls>,
5941        axis_ids: [AxisId; 2],
5942    },
5943    Line {
5944        grouping: Grouping,
5945        marker: bool,
5946        smooth: bool,
5947        series: Vec<Series>,
5948        data_labels: Option<CT_DLbls>,
5949        axis_ids: [AxisId; 2],
5950    },
5951    Pie {
5952        first_slice_angle: u16,
5953        series: Vec<Series>,
5954        data_labels: Option<CT_DLbls>,
5955    },
5956    Doughnut {
5957        first_slice_angle: u16,
5958        hole_size: u8,
5959        series: Vec<Series>,
5960        data_labels: Option<CT_DLbls>,
5961    },
5962    Area {
5963        grouping: Grouping,
5964        series: Vec<Series>,
5965        data_labels: Option<CT_DLbls>,
5966        axis_ids: [AxisId; 2],
5967    },
5968    Scatter {
5969        style: ScatterStyle,
5970        series: Vec<Series>,
5971        data_labels: Option<CT_DLbls>,
5972        axis_ids: [AxisId; 2],
5973    },
5974    Radar {
5975        style: RadarStyle,
5976        series: Vec<Series>,
5977        data_labels: Option<CT_DLbls>,
5978        axis_ids: [AxisId; 2],
5979    },
5980}
5981
5982impl Plot {
5983    pub fn bar(
5984        direction: BarDirection,
5985        grouping: BarGrouping,
5986        series: Vec<Series>,
5987        axis_ids: [AxisId; 2],
5988    ) -> Result<Self> {
5989        let plot = Self::Bar {
5990            direction,
5991            grouping,
5992            gap_width: 150,
5993            overlap: 0,
5994            series,
5995            data_labels: None,
5996            axis_ids,
5997        };
5998        plot.validate()?;
5999        Ok(plot)
6000    }
6001
6002    pub fn line(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
6003        let plot = Self::Line {
6004            grouping,
6005            marker: false,
6006            smooth: false,
6007            series,
6008            data_labels: None,
6009            axis_ids,
6010        };
6011        plot.validate()?;
6012        Ok(plot)
6013    }
6014
6015    pub fn pie(series: Vec<Series>) -> Result<Self> {
6016        let plot = Self::Pie {
6017            first_slice_angle: 0,
6018            series,
6019            data_labels: None,
6020        };
6021        plot.validate()?;
6022        Ok(plot)
6023    }
6024
6025    pub fn doughnut(series: Vec<Series>) -> Result<Self> {
6026        let plot = Self::Doughnut {
6027            first_slice_angle: 0,
6028            hole_size: 50,
6029            series,
6030            data_labels: None,
6031        };
6032        plot.validate()?;
6033        Ok(plot)
6034    }
6035
6036    pub fn area(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
6037        let plot = Self::Area {
6038            grouping,
6039            series,
6040            data_labels: None,
6041            axis_ids,
6042        };
6043        plot.validate()?;
6044        Ok(plot)
6045    }
6046
6047    pub fn scatter(
6048        style: ScatterStyle,
6049        series: Vec<Series>,
6050        axis_ids: [AxisId; 2],
6051    ) -> Result<Self> {
6052        let plot = Self::Scatter {
6053            style,
6054            series,
6055            data_labels: None,
6056            axis_ids,
6057        };
6058        plot.validate()?;
6059        Ok(plot)
6060    }
6061
6062    pub fn radar(style: RadarStyle, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
6063        let plot = Self::Radar {
6064            style,
6065            series,
6066            data_labels: None,
6067            axis_ids,
6068        };
6069        plot.validate()?;
6070        Ok(plot)
6071    }
6072
6073    fn axis_ids(&self) -> Option<[AxisId; 2]> {
6074        match self {
6075            Self::Bar { axis_ids, .. }
6076            | Self::Line { axis_ids, .. }
6077            | Self::Area { axis_ids, .. }
6078            | Self::Scatter { axis_ids, .. }
6079            | Self::Radar { axis_ids, .. } => Some(*axis_ids),
6080            Self::Pie { .. } | Self::Doughnut { .. } => None,
6081        }
6082    }
6083
6084    fn validate(&self) -> Result<()> {
6085        let (series, labels, axis_ids) = match self {
6086            Self::Bar {
6087                gap_width,
6088                overlap,
6089                series,
6090                data_labels,
6091                axis_ids,
6092                ..
6093            } => {
6094                if *gap_width > 500 {
6095                    return Err(ChartError::InvalidValue {
6096                        element: "c:gapWidth".to_owned(),
6097                        value: gap_width.to_string(),
6098                    });
6099                }
6100                if !(-100..=100).contains(overlap) {
6101                    return Err(ChartError::InvalidValue {
6102                        element: "c:overlap".to_owned(),
6103                        value: overlap.to_string(),
6104                    });
6105                }
6106                (series, data_labels, Some(axis_ids))
6107            }
6108            Self::Line {
6109                series,
6110                data_labels,
6111                axis_ids,
6112                ..
6113            } => (series, data_labels, Some(axis_ids)),
6114            Self::Pie {
6115                first_slice_angle,
6116                series,
6117                data_labels,
6118            } => {
6119                if *first_slice_angle > 360 {
6120                    return Err(ChartError::InvalidValue {
6121                        element: "c:firstSliceAng".to_owned(),
6122                        value: first_slice_angle.to_string(),
6123                    });
6124                }
6125                (series, data_labels, None)
6126            }
6127            Self::Doughnut {
6128                first_slice_angle,
6129                hole_size,
6130                series,
6131                data_labels,
6132            } => {
6133                if *first_slice_angle > 360 {
6134                    return Err(ChartError::InvalidValue {
6135                        element: "c:firstSliceAng".to_owned(),
6136                        value: first_slice_angle.to_string(),
6137                    });
6138                }
6139                if !(10..=90).contains(hole_size) {
6140                    return Err(ChartError::InvalidValue {
6141                        element: "c:holeSize".to_owned(),
6142                        value: hole_size.to_string(),
6143                    });
6144                }
6145                (series, data_labels, None)
6146            }
6147            Self::Area {
6148                series,
6149                data_labels,
6150                axis_ids,
6151                ..
6152            }
6153            | Self::Radar {
6154                series,
6155                data_labels,
6156                axis_ids,
6157                ..
6158            } => (series, data_labels, Some(axis_ids)),
6159            Self::Scatter {
6160                series,
6161                data_labels,
6162                axis_ids,
6163                ..
6164            } => {
6165                for item in series {
6166                    if !matches!(item.categories, Some(AxisData::Numeric(_))) {
6167                        return Err(ChartError::InvalidValue {
6168                            element: "c:xVal".to_owned(),
6169                            value: "scatter series requires numeric x values".to_owned(),
6170                        });
6171                    }
6172                }
6173                (series, data_labels, Some(axis_ids))
6174            }
6175        };
6176        if series.is_empty() {
6177            return Err(ChartError::MissingElement("c:ser".to_owned()));
6178        }
6179        let scatter_plot = matches!(self, Self::Scatter { .. });
6180        for item in series {
6181            if item.bubble_size.is_some() || item.opaque_bubble_size {
6182                return Err(ChartError::InvalidValue {
6183                    element: "c:bubbleSize".to_owned(),
6184                    value: "bubble size is only valid in an opaque bubble plot".to_owned(),
6185                });
6186            }
6187            if item.uses_scatter_wrappers && !scatter_plot {
6188                return Err(ChartError::InvalidValue {
6189                    element: "c:ser".to_owned(),
6190                    value: "x/y wrappers are only valid in scatter plots".to_owned(),
6191                });
6192            }
6193            if scatter_plot && item.parsed_from_xml && !item.uses_scatter_wrappers {
6194                return Err(ChartError::InvalidValue {
6195                    element: "c:ser".to_owned(),
6196                    value: "scatter plots require xVal/yVal wrappers".to_owned(),
6197                });
6198            }
6199        }
6200        if let Some(axis_ids) = axis_ids {
6201            if axis_ids[0] == axis_ids[1] {
6202                return Err(ChartError::DuplicateElement(format!(
6203                    "c:axId {}",
6204                    axis_ids[0].value()
6205                )));
6206            }
6207            for id in axis_ids {
6208                AxisId::new(id.value())?;
6209            }
6210        }
6211        if let Some(labels) = labels {
6212            labels.validate()?;
6213        }
6214        for item in series {
6215            item.values.validate()?;
6216        }
6217        Ok(())
6218    }
6219}
6220
6221/// The concrete ChartML root used by an axis.
6222#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
6223pub enum AxisKind {
6224    Category,
6225    Value,
6226    Date,
6227    Series,
6228}
6229
6230impl AxisKind {
6231    fn parse(local: &[u8]) -> Option<Self> {
6232        match local {
6233            b"catAx" => Some(Self::Category),
6234            b"valAx" => Some(Self::Value),
6235            b"dateAx" => Some(Self::Date),
6236            b"serAx" => Some(Self::Series),
6237            _ => None,
6238        }
6239    }
6240
6241    const fn local(self) -> &'static str {
6242        match self {
6243            Self::Category => "catAx",
6244            Self::Value => "valAx",
6245            Self::Date => "dateAx",
6246            Self::Series => "serAx",
6247        }
6248    }
6249}
6250
6251/// Direction of values along an axis scale.
6252#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6253pub enum Orientation {
6254    #[default]
6255    MinMax,
6256    MaxMin,
6257}
6258
6259impl Orientation {
6260    fn parse(value: &str) -> Option<Self> {
6261        match value {
6262            "minMax" => Some(Self::MinMax),
6263            "maxMin" => Some(Self::MaxMin),
6264            _ => None,
6265        }
6266    }
6267
6268    const fn as_str(self) -> &'static str {
6269        match self {
6270            Self::MinMax => "minMax",
6271            Self::MaxMin => "maxMin",
6272        }
6273    }
6274}
6275
6276/// Which side of the plot area carries an axis.
6277#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6278pub enum AxisPosition {
6279    Bottom,
6280    Left,
6281    Right,
6282    Top,
6283}
6284
6285impl AxisPosition {
6286    fn parse(value: &str) -> Option<Self> {
6287        match value {
6288            "b" => Some(Self::Bottom),
6289            "l" => Some(Self::Left),
6290            "r" => Some(Self::Right),
6291            "t" => Some(Self::Top),
6292            _ => None,
6293        }
6294    }
6295
6296    const fn as_str(self) -> &'static str {
6297        match self {
6298            Self::Bottom => "b",
6299            Self::Left => "l",
6300            Self::Right => "r",
6301            Self::Top => "t",
6302        }
6303    }
6304}
6305
6306/// Placement of a major or minor tick relative to its axis line.
6307#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6308pub enum TickMark {
6309    Cross,
6310    Inside,
6311    #[default]
6312    None,
6313    Outside,
6314}
6315
6316impl TickMark {
6317    fn parse(value: &str) -> Option<Self> {
6318        match value {
6319            "cross" => Some(Self::Cross),
6320            "in" => Some(Self::Inside),
6321            "none" => Some(Self::None),
6322            "out" => Some(Self::Outside),
6323            _ => None,
6324        }
6325    }
6326
6327    const fn as_str(self) -> &'static str {
6328        match self {
6329            Self::Cross => "cross",
6330            Self::Inside => "in",
6331            Self::None => "none",
6332            Self::Outside => "out",
6333        }
6334    }
6335}
6336
6337/// Placement of tick labels relative to an axis.
6338#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6339pub enum TickLabelPosition {
6340    High,
6341    Low,
6342    #[default]
6343    NextTo,
6344    None,
6345}
6346
6347impl TickLabelPosition {
6348    fn parse(value: &str) -> Option<Self> {
6349        match value {
6350            "high" => Some(Self::High),
6351            "low" => Some(Self::Low),
6352            "nextTo" => Some(Self::NextTo),
6353            "none" => Some(Self::None),
6354            _ => None,
6355        }
6356    }
6357
6358    const fn as_str(self) -> &'static str {
6359        match self {
6360            Self::High => "high",
6361            Self::Low => "low",
6362            Self::NextTo => "nextTo",
6363            Self::None => "none",
6364        }
6365    }
6366}
6367
6368/// The modelled values of `c:scaling`.
6369#[derive(Clone, Debug)]
6370pub struct Scaling {
6371    pub log_base: Option<f64>,
6372    pub orientation: Orientation,
6373    pub maximum: Option<f64>,
6374    pub minimum: Option<f64>,
6375    log_base_markup: Option<ScalarMarkup>,
6376    orientation_markup: Option<ScalarMarkup>,
6377    maximum_markup: Option<ScalarMarkup>,
6378    minimum_markup: Option<ScalarMarkup>,
6379    raw_attributes: XmlAttributes,
6380    raw_children: OrderedRawChildren,
6381}
6382
6383impl PartialEq for Scaling {
6384    fn eq(&self, other: &Self) -> bool {
6385        self.log_base == other.log_base
6386            && self.orientation == other.orientation
6387            && self.maximum == other.maximum
6388            && self.minimum == other.minimum
6389            && optional_scalar_markup_eq(&self.log_base_markup, &other.log_base_markup)
6390            && optional_scalar_markup_eq(&self.orientation_markup, &other.orientation_markup)
6391            && optional_scalar_markup_eq(&self.maximum_markup, &other.maximum_markup)
6392            && optional_scalar_markup_eq(&self.minimum_markup, &other.minimum_markup)
6393            && self.raw_attributes == other.raw_attributes
6394            && self.raw_children == other.raw_children
6395    }
6396}
6397
6398impl Default for Scaling {
6399    fn default() -> Self {
6400        Self {
6401            log_base: None,
6402            orientation: Orientation::MinMax,
6403            maximum: None,
6404            minimum: None,
6405            log_base_markup: None,
6406            orientation_markup: None,
6407            maximum_markup: None,
6408            minimum_markup: None,
6409            raw_attributes: Vec::new(),
6410            raw_children: OrderedRawChildren::default(),
6411        }
6412    }
6413}
6414
6415/// Shape properties on a major or minor gridline container.
6416#[derive(Clone, Debug, Default, PartialEq)]
6417pub struct ChartLines {
6418    pub sp_pr: Option<CT_ShapeProperties>,
6419    raw_attributes: XmlAttributes,
6420    raw_children: OrderedRawChildren,
6421}
6422
6423/// Number format applied to axis values and labels.
6424#[derive(Clone, Debug, Eq, PartialEq)]
6425pub struct NumberFormat {
6426    pub format_code: String,
6427    pub source_linked: bool,
6428    raw_attributes: XmlAttributes,
6429    raw_content: Vec<Vec<u8>>,
6430}
6431
6432/// Placement of a chart data label relative to its data point.
6433#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6434pub enum DataLabelPosition {
6435    BestFit,
6436    Bottom,
6437    Center,
6438    InsideBase,
6439    InsideEnd,
6440    Left,
6441    OutsideEnd,
6442    Right,
6443    Top,
6444}
6445
6446impl DataLabelPosition {
6447    fn parse(value: &str) -> Option<Self> {
6448        match value {
6449            "bestFit" => Some(Self::BestFit),
6450            "b" => Some(Self::Bottom),
6451            "ctr" => Some(Self::Center),
6452            "inBase" => Some(Self::InsideBase),
6453            "inEnd" => Some(Self::InsideEnd),
6454            "l" => Some(Self::Left),
6455            "outEnd" => Some(Self::OutsideEnd),
6456            "r" => Some(Self::Right),
6457            "t" => Some(Self::Top),
6458            _ => None,
6459        }
6460    }
6461
6462    const fn as_str(self) -> &'static str {
6463        match self {
6464            Self::BestFit => "bestFit",
6465            Self::Bottom => "b",
6466            Self::Center => "ctr",
6467            Self::InsideBase => "inBase",
6468            Self::InsideEnd => "inEnd",
6469            Self::Left => "l",
6470            Self::OutsideEnd => "outEnd",
6471            Self::Right => "r",
6472            Self::Top => "t",
6473        }
6474    }
6475}
6476
6477/// Collection-level data-label defaults for one chart series or plot.
6478#[derive(Clone, Debug, Default, PartialEq)]
6479pub struct CT_DLbls {
6480    pub number_format: Option<NumberFormat>,
6481    pub position: Option<DataLabelPosition>,
6482    pub separator: Option<String>,
6483    pub show_legend_key: bool,
6484    pub show_value: bool,
6485    pub show_category_name: bool,
6486    pub show_series_name: bool,
6487    pub show_percent: bool,
6488    pub show_bubble_size: bool,
6489    position_markup: Option<ScalarMarkup>,
6490    show_legend_key_markup: Option<ScalarMarkup>,
6491    show_value_markup: Option<ScalarMarkup>,
6492    show_category_name_markup: Option<ScalarMarkup>,
6493    show_series_name_markup: Option<ScalarMarkup>,
6494    show_percent_markup: Option<ScalarMarkup>,
6495    show_bubble_size_markup: Option<ScalarMarkup>,
6496    separator_markup: Option<TextMarkup>,
6497    raw_attributes: XmlAttributes,
6498    namespace_declarations: XmlAttributes,
6499    raw_children: OrderedRawChildren,
6500}
6501
6502/// How a chart displays cells whose cached values are blank.
6503#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6504pub enum DispBlanksAs {
6505    #[default]
6506    Gap,
6507    Zero,
6508    Span,
6509}
6510
6511impl DispBlanksAs {
6512    fn parse(value: &str) -> Option<Self> {
6513        match value {
6514            "gap" => Some(Self::Gap),
6515            "zero" => Some(Self::Zero),
6516            "span" => Some(Self::Span),
6517            _ => None,
6518        }
6519    }
6520
6521    const fn as_str(self) -> &'static str {
6522        match self {
6523            Self::Gap => "gap",
6524            Self::Zero => "zero",
6525            Self::Span => "span",
6526        }
6527    }
6528}
6529
6530/// A chart title with plain-text authoring and preserved unsupported children.
6531#[derive(Clone, Debug, Default, Eq, PartialEq)]
6532pub struct CT_Title {
6533    text: Option<CT_TextBody>,
6534    raw_attributes: Vec<(String, String)>,
6535    raw_children: OrderedRawChildren,
6536}
6537
6538/// Preserved lexical and ordered-raw state for one typed plot.
6539#[derive(Clone, Debug, Default, PartialEq)]
6540struct PlotMarkup {
6541    raw_attributes: XmlAttributes,
6542    raw_children: OrderedRawChildren,
6543    direction: Option<ScalarMarkup>,
6544    grouping: Option<ScalarMarkup>,
6545    gap_width: Option<ScalarMarkup>,
6546    overlap: Option<ScalarMarkup>,
6547    marker: Option<ScalarMarkup>,
6548    smooth: Option<ScalarMarkup>,
6549    first_slice_angle: Option<ScalarMarkup>,
6550    hole_size: Option<ScalarMarkup>,
6551    style: Option<ScalarMarkup>,
6552    axis_ids: Vec<AxisIdMarkup>,
6553    original_series_keys: Vec<(u32, u32)>,
6554    original_axis_ids: Vec<AxisId>,
6555    parsed_bar: Option<bool>,
6556    parsed_remaining: Option<&'static str>,
6557}
6558
6559/// A plot-area shell. F-119 through F-122 replace selected raw slots with types.
6560#[derive(Clone, Debug, PartialEq)]
6561pub struct CT_PlotArea {
6562    raw_attributes: Vec<(String, String)>,
6563    raw_children: OrderedRawChildren,
6564    namespace_bindings: NamespaceBindings,
6565    plots: Option<Vec<Plot>>,
6566    plot_markup: Vec<PlotMarkup>,
6567    axes: Vec<Axis>,
6568}
6569
6570/// A chart legend shell whose current children remain opaque.
6571#[derive(Clone, Debug, Default, Eq, PartialEq)]
6572pub struct CT_Legend {
6573    raw_attributes: Vec<(String, String)>,
6574    raw_children: OrderedRawChildren,
6575}
6576
6577#[derive(Clone, Debug, PartialEq)]
6578struct AxisIdMarkup {
6579    scalar: ScalarMarkup,
6580    parsed: AxisId,
6581    lexical: String,
6582}
6583
6584/// The common modelled sequence of one ChartML axis.
6585#[derive(Clone, Debug)]
6586pub struct Axis {
6587    pub kind: AxisKind,
6588    pub id: AxisId,
6589    pub scaling: Scaling,
6590    pub deleted: bool,
6591    pub position: AxisPosition,
6592    pub major_gridlines: Option<ChartLines>,
6593    pub minor_gridlines: Option<ChartLines>,
6594    pub title: Option<CT_Title>,
6595    pub number_format: Option<NumberFormat>,
6596    pub major_tick_mark: TickMark,
6597    pub minor_tick_mark: TickMark,
6598    pub tick_label_position: TickLabelPosition,
6599    pub sp_pr: Option<CT_ShapeProperties>,
6600    pub tx_pr: Option<CT_TextBody>,
6601    pub cross_axis: AxisId,
6602    parsed_kind: Option<AxisKind>,
6603    id_markup: AxisIdMarkup,
6604    delete_markup: Option<ScalarMarkup>,
6605    position_markup: ScalarMarkup,
6606    major_tick_mark_markup: Option<ScalarMarkup>,
6607    minor_tick_mark_markup: Option<ScalarMarkup>,
6608    tick_label_position_markup: Option<ScalarMarkup>,
6609    cross_axis_markup: AxisIdMarkup,
6610    raw_attributes: XmlAttributes,
6611    namespace_declarations: XmlAttributes,
6612    raw_children: OrderedRawChildren,
6613}
6614
6615impl PartialEq for Axis {
6616    fn eq(&self, other: &Self) -> bool {
6617        self.kind == other.kind
6618            && self.id == other.id
6619            && axis_id_markup_eq(&self.id_markup, &other.id_markup)
6620            && self.scaling == other.scaling
6621            && self.deleted == other.deleted
6622            && optional_scalar_markup_eq(&self.delete_markup, &other.delete_markup)
6623            && self.position == other.position
6624            && self.position_markup == other.position_markup
6625            && self.major_gridlines == other.major_gridlines
6626            && self.minor_gridlines == other.minor_gridlines
6627            && self.title == other.title
6628            && self.number_format == other.number_format
6629            && self.major_tick_mark == other.major_tick_mark
6630            && optional_scalar_markup_eq(
6631                &self.major_tick_mark_markup,
6632                &other.major_tick_mark_markup,
6633            )
6634            && self.minor_tick_mark == other.minor_tick_mark
6635            && optional_scalar_markup_eq(
6636                &self.minor_tick_mark_markup,
6637                &other.minor_tick_mark_markup,
6638            )
6639            && self.tick_label_position == other.tick_label_position
6640            && optional_scalar_markup_eq(
6641                &self.tick_label_position_markup,
6642                &other.tick_label_position_markup,
6643            )
6644            && self.sp_pr == other.sp_pr
6645            && self.tx_pr == other.tx_pr
6646            && self.cross_axis == other.cross_axis
6647            && axis_id_markup_eq(&self.cross_axis_markup, &other.cross_axis_markup)
6648            && self.raw_attributes == other.raw_attributes
6649            && self.namespace_declarations == other.namespace_declarations
6650            && self.raw_children == other.raw_children
6651    }
6652}
6653
6654impl Axis {
6655    pub fn new(kind: AxisKind, id: AxisId, position: AxisPosition, cross_axis: AxisId) -> Self {
6656        Self {
6657            kind,
6658            id,
6659            scaling: Scaling::default(),
6660            deleted: false,
6661            position,
6662            major_gridlines: None,
6663            minor_gridlines: None,
6664            title: None,
6665            number_format: None,
6666            major_tick_mark: TickMark::None,
6667            minor_tick_mark: TickMark::None,
6668            tick_label_position: TickLabelPosition::NextTo,
6669            sp_pr: None,
6670            tx_pr: None,
6671            cross_axis,
6672            parsed_kind: None,
6673            id_markup: default_axis_id_markup(id),
6674            delete_markup: None,
6675            position_markup: ScalarMarkup::default(),
6676            major_tick_mark_markup: None,
6677            minor_tick_mark_markup: None,
6678            tick_label_position_markup: None,
6679            cross_axis_markup: default_axis_id_markup(cross_axis),
6680            raw_attributes: Vec::new(),
6681            namespace_declarations: Vec::new(),
6682            raw_children: OrderedRawChildren::default(),
6683        }
6684    }
6685
6686    /// Sets axis visibility and writes the explicit ChartML deletion flag.
6687    pub fn set_deleted(&mut self, deleted: bool) {
6688        self.deleted = deleted;
6689        self.delete_markup.get_or_insert_with(ScalarMarkup::default);
6690    }
6691
6692    pub fn from_xml(xml: &[u8]) -> Result<Self> {
6693        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
6694    }
6695
6696    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
6697        let mut reader = Reader::from_reader(xml);
6698        let mut buffer = Vec::new();
6699        loop {
6700            match reader
6701                .read_event_into(&mut buffer)
6702                .map_err(OxmlError::from)?
6703            {
6704                Event::Start(element) => {
6705                    let name = element.name();
6706                    let local = local_name(name.as_ref());
6707                    let Some(kind) = AxisKind::parse(local) else {
6708                        return Err(ChartError::UnexpectedElement(element_name(&element)));
6709                    };
6710                    chart_root_prefix(&element)?;
6711                    if !element_is_in_namespace(&element, C_NS, inherited)? {
6712                        return Err(ChartError::UnexpectedElement(element_name(&element)));
6713                    }
6714                    return Self::from_element(&mut reader, &element, inherited, kind);
6715                }
6716                Event::Empty(element) => {
6717                    let name = element.name();
6718                    let local = local_name(name.as_ref());
6719                    if AxisKind::parse(local).is_some() {
6720                        return Err(ChartError::MissingElement("c:axId".to_owned()));
6721                    }
6722                    return Err(ChartError::UnexpectedElement(element_name(&element)));
6723                }
6724                Event::Eof => return Err(ChartError::MissingElement("ChartML axis".to_owned())),
6725                _ => {}
6726            }
6727            buffer.clear();
6728        }
6729    }
6730
6731    fn from_element(
6732        reader: &mut Reader<&[u8]>,
6733        start: &BytesStart<'_>,
6734        inherited: &NamespaceBindings,
6735        kind: AxisKind,
6736    ) -> Result<Self> {
6737        reject_conflicting_prefix(start, b"a", A_NS)?;
6738        reject_conflicting_prefix(start, b"r", R_NS)?;
6739        let namespaces = chart_bindings(inherited, start)?;
6740        require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
6741        require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
6742        require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
6743        let (raw_attributes, _) =
6744            capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
6745        let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
6746        let mut state = AxisParseState::default();
6747        let mut buffer = Vec::new();
6748        loop {
6749            match reader
6750                .read_event_into(&mut buffer)
6751                .map_err(OxmlError::from)?
6752            {
6753                Event::Start(element) => {
6754                    let name = chart_child_local(&element, &namespaces)?;
6755                    let raw = capture_element(reader, &element)?;
6756                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
6757                }
6758                Event::Empty(element) => {
6759                    let name = chart_child_local(&element, &namespaces)?;
6760                    let raw = capture_empty_element(&element)?;
6761                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
6762                }
6763                event @ (Event::Text(_)
6764                | Event::CData(_)
6765                | Event::Comment(_)
6766                | Event::PI(_)
6767                | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
6768                Event::End(element)
6769                    if matches_local_name(element.name().as_ref(), kind.local().as_bytes()) =>
6770                {
6771                    break;
6772                }
6773                Event::Eof => return Err(missing_end(&format!("c:{}", kind.local()))),
6774                _ => {}
6775            }
6776            buffer.clear();
6777        }
6778        state.finish(kind, raw_attributes, namespace_declarations)
6779    }
6780
6781    pub fn to_xml(&self) -> Result<Vec<u8>> {
6782        self.validate()?;
6783        let mut writer = Writer::new(Vec::new());
6784        let root = format!("c:{}", self.kind.local());
6785        let mut start = BytesStart::new(&root);
6786        start.push_attribute(("xmlns:c", C_NS));
6787        start.push_attribute(("xmlns:a", A_NS));
6788        start.push_attribute(("xmlns:r", R_NS));
6789        push_attributes(&mut start, &self.namespace_declarations);
6790        push_attributes(&mut start, &self.raw_attributes);
6791        writer
6792            .write_event(Event::Start(start))
6793            .map_err(OxmlError::from)?;
6794        emit_raw(&mut writer, self.raw_children.at(0))?;
6795        write_axis_id(&mut writer, "c:axId", self.id, &self.id_markup)?;
6796        emit_raw(&mut writer, self.raw_children.at(1))?;
6797        self.scaling.write_xml(&mut writer)?;
6798        emit_raw(&mut writer, self.raw_children.at(2))?;
6799        if self.deleted || self.delete_markup.is_some() {
6800            write_scalar(
6801                &mut writer,
6802                "c:delete",
6803                bool_lexical(self.deleted),
6804                self.delete_markup.as_ref(),
6805            )?;
6806        }
6807        emit_raw(&mut writer, self.raw_children.at(3))?;
6808        write_scalar(
6809            &mut writer,
6810            "c:axPos",
6811            self.position.as_str(),
6812            Some(&self.position_markup),
6813        )?;
6814        emit_raw(&mut writer, self.raw_children.at(4))?;
6815        if let Some(lines) = &self.major_gridlines {
6816            lines.write_xml(&mut writer, "c:majorGridlines")?;
6817        }
6818        emit_raw(&mut writer, self.raw_children.at(5))?;
6819        if let Some(lines) = &self.minor_gridlines {
6820            lines.write_xml(&mut writer, "c:minorGridlines")?;
6821        }
6822        emit_raw(&mut writer, self.raw_children.at(6))?;
6823        if let Some(title) = &self.title {
6824            title.write_xml(&mut writer)?;
6825        }
6826        emit_raw(&mut writer, self.raw_children.at(7))?;
6827        if let Some(number_format) = &self.number_format {
6828            number_format.write_xml(&mut writer)?;
6829        }
6830        emit_raw(&mut writer, self.raw_children.at(8))?;
6831        if self.major_tick_mark != TickMark::None || self.major_tick_mark_markup.is_some() {
6832            write_scalar(
6833                &mut writer,
6834                "c:majorTickMark",
6835                self.major_tick_mark.as_str(),
6836                self.major_tick_mark_markup.as_ref(),
6837            )?;
6838        }
6839        emit_raw(&mut writer, self.raw_children.at(9))?;
6840        if self.minor_tick_mark != TickMark::None || self.minor_tick_mark_markup.is_some() {
6841            write_scalar(
6842                &mut writer,
6843                "c:minorTickMark",
6844                self.minor_tick_mark.as_str(),
6845                self.minor_tick_mark_markup.as_ref(),
6846            )?;
6847        }
6848        emit_raw(&mut writer, self.raw_children.at(10))?;
6849        if self.tick_label_position != TickLabelPosition::NextTo
6850            || self.tick_label_position_markup.is_some()
6851        {
6852            write_scalar(
6853                &mut writer,
6854                "c:tickLblPos",
6855                self.tick_label_position.as_str(),
6856                self.tick_label_position_markup.as_ref(),
6857            )?;
6858        }
6859        emit_raw(&mut writer, self.raw_children.at(11))?;
6860        if let Some(properties) = &self.sp_pr {
6861            properties.write_xml_as(&mut writer, "c:spPr")?;
6862        }
6863        emit_raw(&mut writer, self.raw_children.at(12))?;
6864        if let Some(text) = &self.tx_pr {
6865            text.write_xml_as(&mut writer, "c:txPr")?;
6866        }
6867        emit_raw(&mut writer, self.raw_children.at(13))?;
6868        write_axis_id(
6869            &mut writer,
6870            "c:crossAx",
6871            self.cross_axis,
6872            &self.cross_axis_markup,
6873        )?;
6874        emit_raw(&mut writer, self.raw_children.at(14))?;
6875        writer
6876            .write_event(Event::End(BytesEnd::new(&root)))
6877            .map_err(OxmlError::from)?;
6878        Ok(writer.into_inner())
6879    }
6880
6881    pub fn raw_children(&self) -> &OrderedRawChildren {
6882        &self.raw_children
6883    }
6884
6885    fn validate(&self) -> Result<()> {
6886        if let Some(parsed_kind) = self.parsed_kind
6887            && self.kind != parsed_kind
6888        {
6889            return Err(ChartError::InvalidValue {
6890                element: "ChartML axis root".to_owned(),
6891                value: format!(
6892                    "parsed c:{} cannot be relabelled c:{}",
6893                    parsed_kind.local(),
6894                    self.kind.local()
6895                ),
6896            });
6897        }
6898        AxisId::new(self.id.value())?;
6899        AxisId::new(self.cross_axis.value())?;
6900        self.scaling.validate()?;
6901        if let Some(number_format) = &self.number_format {
6902            number_format.validate()?;
6903        }
6904        Ok(())
6905    }
6906}
6907
6908#[derive(Default)]
6909struct AxisParseState {
6910    id: Option<(AxisId, AxisIdMarkup)>,
6911    scaling: Option<Scaling>,
6912    deleted: Option<(bool, ScalarMarkup)>,
6913    position: Option<(AxisPosition, ScalarMarkup)>,
6914    major_gridlines: Option<ChartLines>,
6915    minor_gridlines: Option<ChartLines>,
6916    title: Option<CT_Title>,
6917    number_format: Option<NumberFormat>,
6918    major_tick_mark: Option<(TickMark, ScalarMarkup)>,
6919    minor_tick_mark: Option<(TickMark, ScalarMarkup)>,
6920    tick_label_position: Option<(TickLabelPosition, ScalarMarkup)>,
6921    sp_pr: Option<CT_ShapeProperties>,
6922    tx_pr: Option<CT_TextBody>,
6923    cross_axis: Option<(AxisId, AxisIdMarkup)>,
6924    raw_children: OrderedRawChildren,
6925    boundary: usize,
6926}
6927
6928impl AxisParseState {
6929    fn capture_event(&mut self, raw: Vec<u8>) {
6930        self.raw_children.push(self.boundary, raw);
6931    }
6932
6933    fn parse_child(
6934        &mut self,
6935        name: &[u8],
6936        raw: Vec<u8>,
6937        namespaces: &NamespaceBindings,
6938    ) -> Result<()> {
6939        match name {
6940            b"axId" => {
6941                set_once(&mut self.id, parse_axis_id_scalar(&raw, "axId")?, "c:axId")?;
6942                self.boundary = self.boundary.max(1);
6943            }
6944            b"scaling" => {
6945                set_once(
6946                    &mut self.scaling,
6947                    Scaling::from_xml(&raw, namespaces)?,
6948                    "c:scaling",
6949                )?;
6950                self.boundary = self.boundary.max(2);
6951            }
6952            b"delete" => {
6953                set_once(
6954                    &mut self.deleted,
6955                    parse_bool_value(&raw, "delete")?,
6956                    "c:delete",
6957                )?;
6958                self.boundary = self.boundary.max(3);
6959            }
6960            b"axPos" => {
6961                set_once(&mut self.position, parse_axis_position(&raw)?, "c:axPos")?;
6962                self.boundary = self.boundary.max(4);
6963            }
6964            b"majorGridlines" => {
6965                set_once(
6966                    &mut self.major_gridlines,
6967                    ChartLines::from_xml(&raw, b"majorGridlines", namespaces)?,
6968                    "c:majorGridlines",
6969                )?;
6970                self.boundary = self.boundary.max(5);
6971            }
6972            b"minorGridlines" => {
6973                set_once(
6974                    &mut self.minor_gridlines,
6975                    ChartLines::from_xml(&raw, b"minorGridlines", namespaces)?,
6976                    "c:minorGridlines",
6977                )?;
6978                self.boundary = self.boundary.max(6);
6979            }
6980            b"title" => {
6981                set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
6982                self.boundary = self.boundary.max(7);
6983            }
6984            b"numFmt" => {
6985                set_once(
6986                    &mut self.number_format,
6987                    NumberFormat::from_xml(&raw)?,
6988                    "c:numFmt",
6989                )?;
6990                self.boundary = self.boundary.max(8);
6991            }
6992            b"majorTickMark" => {
6993                set_once(
6994                    &mut self.major_tick_mark,
6995                    parse_tick_mark(&raw, "majorTickMark")?,
6996                    "c:majorTickMark",
6997                )?;
6998                self.boundary = self.boundary.max(9);
6999            }
7000            b"minorTickMark" => {
7001                set_once(
7002                    &mut self.minor_tick_mark,
7003                    parse_tick_mark(&raw, "minorTickMark")?,
7004                    "c:minorTickMark",
7005                )?;
7006                self.boundary = self.boundary.max(10);
7007            }
7008            b"tickLblPos" => {
7009                set_once(
7010                    &mut self.tick_label_position,
7011                    parse_tick_label_position(&raw)?,
7012                    "c:tickLblPos",
7013                )?;
7014                self.boundary = self.boundary.max(11);
7015            }
7016            b"spPr" => {
7017                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
7018                let properties = CT_ShapeProperties::from_xml(&raw)?;
7019                let mut writer = Writer::new(Vec::new());
7020                properties.write_xml_as(&mut writer, "c:spPr")?;
7021                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
7022                set_once(&mut self.sp_pr, properties, "c:spPr")?;
7023                self.boundary = self.boundary.max(12);
7024            }
7025            b"txPr" => {
7026                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
7027                let text = CT_TextBody::from_xml_as(&raw, b"txPr")?;
7028                let mut writer = Writer::new(Vec::new());
7029                text.write_xml_as(&mut writer, "c:txPr")?;
7030                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
7031                set_once(&mut self.tx_pr, text, "c:txPr")?;
7032                self.boundary = self.boundary.max(13);
7033            }
7034            b"crossAx" => {
7035                set_once(
7036                    &mut self.cross_axis,
7037                    parse_axis_id_scalar(&raw, "crossAx")?,
7038                    "c:crossAx",
7039                )?;
7040                self.boundary = self.boundary.max(14);
7041            }
7042            _ => self.raw_children.push(self.boundary, raw),
7043        }
7044        Ok(())
7045    }
7046
7047    fn finish(
7048        self,
7049        kind: AxisKind,
7050        raw_attributes: XmlAttributes,
7051        namespace_declarations: XmlAttributes,
7052    ) -> Result<Axis> {
7053        let (id, id_markup) = self
7054            .id
7055            .ok_or_else(|| ChartError::MissingElement("c:axId".to_owned()))?;
7056        let scaling = self
7057            .scaling
7058            .ok_or_else(|| ChartError::MissingElement("c:scaling".to_owned()))?;
7059        let (position, position_markup) = self
7060            .position
7061            .ok_or_else(|| ChartError::MissingElement("c:axPos".to_owned()))?;
7062        let (cross_axis, cross_axis_markup) = self
7063            .cross_axis
7064            .ok_or_else(|| ChartError::MissingElement("c:crossAx".to_owned()))?;
7065        let (deleted, delete_markup) = self
7066            .deleted
7067            .map(|(value, markup)| (value, Some(markup)))
7068            .unwrap_or((false, None));
7069        let (major_tick_mark, major_tick_mark_markup) = self
7070            .major_tick_mark
7071            .map(|(value, markup)| (value, Some(markup)))
7072            .unwrap_or((TickMark::None, None));
7073        let (minor_tick_mark, minor_tick_mark_markup) = self
7074            .minor_tick_mark
7075            .map(|(value, markup)| (value, Some(markup)))
7076            .unwrap_or((TickMark::None, None));
7077        let (tick_label_position, tick_label_position_markup) = self
7078            .tick_label_position
7079            .map(|(value, markup)| (value, Some(markup)))
7080            .unwrap_or((TickLabelPosition::NextTo, None));
7081        let axis = Axis {
7082            kind,
7083            id,
7084            scaling,
7085            deleted,
7086            position,
7087            major_gridlines: self.major_gridlines,
7088            minor_gridlines: self.minor_gridlines,
7089            title: self.title,
7090            number_format: self.number_format,
7091            major_tick_mark,
7092            minor_tick_mark,
7093            tick_label_position,
7094            sp_pr: self.sp_pr,
7095            tx_pr: self.tx_pr,
7096            cross_axis,
7097            parsed_kind: Some(kind),
7098            id_markup,
7099            delete_markup,
7100            position_markup,
7101            major_tick_mark_markup,
7102            minor_tick_mark_markup,
7103            tick_label_position_markup,
7104            cross_axis_markup,
7105            raw_attributes,
7106            namespace_declarations,
7107            raw_children: raw_children_in_schema_order(&self.raw_children, 14),
7108        };
7109        axis.validate()?;
7110        Ok(axis)
7111    }
7112}
7113
7114impl Scaling {
7115    fn from_xml(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7116        let mut reader = Reader::from_reader(xml);
7117        let mut buffer = Vec::new();
7118        loop {
7119            match reader
7120                .read_event_into(&mut buffer)
7121                .map_err(OxmlError::from)?
7122            {
7123                Event::Start(element)
7124                    if matches_local_name(element.name().as_ref(), b"scaling") =>
7125                {
7126                    let namespaces = typed_rewrite_bindings(&element, inherited)?;
7127                    let raw_attributes = capture_attributes(&element)?;
7128                    let mut log_base = None;
7129                    let mut orientation = None;
7130                    let mut maximum = None;
7131                    let mut minimum = None;
7132                    let mut raw_children = OrderedRawChildren::default();
7133                    let mut boundary = 0usize;
7134                    let mut inner = Vec::new();
7135                    loop {
7136                        match reader
7137                            .read_event_into(&mut inner)
7138                            .map_err(OxmlError::from)?
7139                        {
7140                            Event::Start(child) => {
7141                                let name = chart_child_local(&child, &namespaces)?;
7142                                let raw = capture_element(&mut reader, &child)?;
7143                                parse_scaling_child(
7144                                    name.as_deref().unwrap_or_default(),
7145                                    raw,
7146                                    &mut log_base,
7147                                    &mut orientation,
7148                                    &mut maximum,
7149                                    &mut minimum,
7150                                    &mut raw_children,
7151                                    &mut boundary,
7152                                )?;
7153                            }
7154                            Event::Empty(child) => {
7155                                let name = chart_child_local(&child, &namespaces)?;
7156                                let raw = capture_empty_element(&child)?;
7157                                parse_scaling_child(
7158                                    name.as_deref().unwrap_or_default(),
7159                                    raw,
7160                                    &mut log_base,
7161                                    &mut orientation,
7162                                    &mut maximum,
7163                                    &mut minimum,
7164                                    &mut raw_children,
7165                                    &mut boundary,
7166                                )?;
7167                            }
7168                            event @ (Event::Text(_)
7169                            | Event::CData(_)
7170                            | Event::Comment(_)
7171                            | Event::PI(_)
7172                            | Event::GeneralRef(_)) => {
7173                                raw_children.push(boundary, capture_event(event)?);
7174                            }
7175                            Event::End(end)
7176                                if matches_local_name(end.name().as_ref(), b"scaling") =>
7177                            {
7178                                let (log_base, log_base_markup) = optional_markup(log_base);
7179                                let (orientation, orientation_markup) = orientation
7180                                    .map(|(value, markup)| (value, Some(markup)))
7181                                    .unwrap_or((Orientation::MinMax, None));
7182                                let (maximum, maximum_markup) = optional_markup(maximum);
7183                                let (minimum, minimum_markup) = optional_markup(minimum);
7184                                let scaling = Self {
7185                                    log_base,
7186                                    orientation,
7187                                    maximum,
7188                                    minimum,
7189                                    log_base_markup,
7190                                    orientation_markup,
7191                                    maximum_markup,
7192                                    minimum_markup,
7193                                    raw_attributes,
7194                                    raw_children: raw_children_in_schema_order(&raw_children, 4),
7195                                };
7196                                scaling.validate()?;
7197                                return Ok(scaling);
7198                            }
7199                            Event::Eof => return Err(missing_end("c:scaling")),
7200                            _ => {}
7201                        }
7202                        inner.clear();
7203                    }
7204                }
7205                Event::Empty(element)
7206                    if matches_local_name(element.name().as_ref(), b"scaling") =>
7207                {
7208                    typed_rewrite_bindings(&element, inherited)?;
7209                    return Ok(Self {
7210                        raw_attributes: capture_attributes(&element)?,
7211                        ..Self::default()
7212                    });
7213                }
7214                Event::Start(element) | Event::Empty(element) => {
7215                    return Err(ChartError::UnexpectedElement(element_name(&element)));
7216                }
7217                Event::Eof => return Err(ChartError::MissingElement("c:scaling".to_owned())),
7218                _ => {}
7219            }
7220            buffer.clear();
7221        }
7222    }
7223
7224    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7225        self.validate()?;
7226        let mut start = BytesStart::new("c:scaling");
7227        push_attributes(&mut start, &self.raw_attributes);
7228        writer
7229            .write_event(Event::Start(start))
7230            .map_err(OxmlError::from)?;
7231        emit_raw(writer, self.raw_children.at(0))?;
7232        if let Some(value) = self.log_base {
7233            write_scalar(
7234                writer,
7235                "c:logBase",
7236                &value.to_string(),
7237                self.log_base_markup.as_ref(),
7238            )?;
7239        }
7240        emit_raw(writer, self.raw_children.at(1))?;
7241        if self.orientation != Orientation::MinMax || self.orientation_markup.is_some() {
7242            write_scalar(
7243                writer,
7244                "c:orientation",
7245                self.orientation.as_str(),
7246                self.orientation_markup.as_ref(),
7247            )?;
7248        }
7249        emit_raw(writer, self.raw_children.at(2))?;
7250        if let Some(value) = self.maximum {
7251            write_scalar(
7252                writer,
7253                "c:max",
7254                &value.to_string(),
7255                self.maximum_markup.as_ref(),
7256            )?;
7257        }
7258        emit_raw(writer, self.raw_children.at(3))?;
7259        if let Some(value) = self.minimum {
7260            write_scalar(
7261                writer,
7262                "c:min",
7263                &value.to_string(),
7264                self.minimum_markup.as_ref(),
7265            )?;
7266        }
7267        emit_raw(writer, self.raw_children.at(4))?;
7268        writer
7269            .write_event(Event::End(BytesEnd::new("c:scaling")))
7270            .map_err(OxmlError::from)?;
7271        Ok(())
7272    }
7273
7274    fn validate(&self) -> Result<()> {
7275        if let Some(value) = self.log_base
7276            && (!value.is_finite() || !(2.0..=1000.0).contains(&value))
7277        {
7278            return Err(ChartError::InvalidValue {
7279                element: "c:logBase".to_owned(),
7280                value: value.to_string(),
7281            });
7282        }
7283        for (element, value) in [("c:max", self.maximum), ("c:min", self.minimum)] {
7284            if let Some(value) = value
7285                && !value.is_finite()
7286            {
7287                return Err(ChartError::InvalidValue {
7288                    element: element.to_owned(),
7289                    value: value.to_string(),
7290                });
7291            }
7292        }
7293        if let (Some(minimum), Some(maximum)) = (self.minimum, self.maximum)
7294            && minimum > maximum
7295        {
7296            return Err(ChartError::InvalidValue {
7297                element: "c:scaling".to_owned(),
7298                value: format!("minimum {minimum} exceeds maximum {maximum}"),
7299            });
7300        }
7301        Ok(())
7302    }
7303}
7304
7305#[allow(clippy::too_many_arguments)]
7306fn parse_scaling_child(
7307    name: &[u8],
7308    raw: Vec<u8>,
7309    log_base: &mut Option<(f64, ScalarMarkup)>,
7310    orientation: &mut Option<(Orientation, ScalarMarkup)>,
7311    maximum: &mut Option<(f64, ScalarMarkup)>,
7312    minimum: &mut Option<(f64, ScalarMarkup)>,
7313    raw_children: &mut OrderedRawChildren,
7314    boundary: &mut usize,
7315) -> Result<()> {
7316    match name {
7317        b"logBase" => {
7318            set_once(log_base, parse_f64_scalar(&raw, "logBase")?, "c:logBase")?;
7319            *boundary = (*boundary).max(1);
7320        }
7321        b"orientation" => {
7322            let (value, markup) = scalar_value(&raw, "orientation")?;
7323            let value = value
7324                .ok_or_else(|| invalid_attribute("orientation", "val", "<missing>".to_owned()))?;
7325            let parsed = Orientation::parse(&value)
7326                .ok_or_else(|| invalid_attribute("orientation", "val", value))?;
7327            set_once(orientation, (parsed, markup), "c:orientation")?;
7328            *boundary = (*boundary).max(2);
7329        }
7330        b"max" => {
7331            set_once(maximum, parse_f64_scalar(&raw, "max")?, "c:max")?;
7332            *boundary = (*boundary).max(3);
7333        }
7334        b"min" => {
7335            set_once(minimum, parse_f64_scalar(&raw, "min")?, "c:min")?;
7336            *boundary = (*boundary).max(4);
7337        }
7338        _ => raw_children.push(*boundary, raw),
7339    }
7340    Ok(())
7341}
7342
7343impl ChartLines {
7344    fn from_xml(xml: &[u8], local: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7345        let mut reader = Reader::from_reader(xml);
7346        let mut buffer = Vec::new();
7347        loop {
7348            match reader
7349                .read_event_into(&mut buffer)
7350                .map_err(OxmlError::from)?
7351            {
7352                Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
7353                    let namespaces = typed_rewrite_bindings(&element, inherited)?;
7354                    let raw_attributes = capture_attributes(&element)?;
7355                    let mut sp_pr = None;
7356                    let mut raw_children = OrderedRawChildren::default();
7357                    let mut boundary = 0usize;
7358                    let mut inner = Vec::new();
7359                    loop {
7360                        match reader
7361                            .read_event_into(&mut inner)
7362                            .map_err(OxmlError::from)?
7363                        {
7364                            Event::Start(child) => {
7365                                let name = chart_child_local(&child, &namespaces)?;
7366                                let raw = capture_element(&mut reader, &child)?;
7367                                if name.as_deref() == Some(b"spPr") {
7368                                    parse_chart_lines_shape(&raw, &namespaces, &mut sp_pr)?;
7369                                    boundary = 1;
7370                                } else {
7371                                    raw_children.push(boundary, raw);
7372                                }
7373                            }
7374                            Event::Empty(child) => {
7375                                let name = chart_child_local(&child, &namespaces)?;
7376                                let raw = capture_empty_element(&child)?;
7377                                if name.as_deref() == Some(b"spPr") {
7378                                    parse_chart_lines_shape(&raw, &namespaces, &mut sp_pr)?;
7379                                    boundary = 1;
7380                                } else {
7381                                    raw_children.push(boundary, raw);
7382                                }
7383                            }
7384                            event @ (Event::Text(_)
7385                            | Event::CData(_)
7386                            | Event::Comment(_)
7387                            | Event::PI(_)
7388                            | Event::GeneralRef(_)) => {
7389                                raw_children.push(boundary, capture_event(event)?);
7390                            }
7391                            Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
7392                                return Ok(Self {
7393                                    sp_pr,
7394                                    raw_attributes,
7395                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
7396                                });
7397                            }
7398                            Event::Eof => {
7399                                return Err(missing_end(&format!(
7400                                    "c:{}",
7401                                    String::from_utf8_lossy(local)
7402                                )));
7403                            }
7404                            _ => {}
7405                        }
7406                        inner.clear();
7407                    }
7408                }
7409                Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
7410                    typed_rewrite_bindings(&element, inherited)?;
7411                    return Ok(Self {
7412                        sp_pr: None,
7413                        raw_attributes: capture_attributes(&element)?,
7414                        raw_children: OrderedRawChildren::default(),
7415                    });
7416                }
7417                Event::Start(element) | Event::Empty(element) => {
7418                    return Err(ChartError::UnexpectedElement(element_name(&element)));
7419                }
7420                Event::Eof => {
7421                    return Err(ChartError::MissingElement(format!(
7422                        "c:{}",
7423                        String::from_utf8_lossy(local)
7424                    )));
7425                }
7426                _ => {}
7427            }
7428            buffer.clear();
7429        }
7430    }
7431
7432    fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
7433        let mut start = BytesStart::new(tag);
7434        push_attributes(&mut start, &self.raw_attributes);
7435        if self.sp_pr.is_none() && self.raw_children.is_empty() {
7436            writer
7437                .write_event(Event::Empty(start))
7438                .map_err(OxmlError::from)?;
7439            return Ok(());
7440        }
7441        writer
7442            .write_event(Event::Start(start))
7443            .map_err(OxmlError::from)?;
7444        emit_raw(writer, self.raw_children.at(0))?;
7445        if let Some(properties) = &self.sp_pr {
7446            properties.write_xml_as(writer, "c:spPr")?;
7447        }
7448        emit_raw(writer, self.raw_children.at(1))?;
7449        writer
7450            .write_event(Event::End(BytesEnd::new(tag)))
7451            .map_err(OxmlError::from)?;
7452        Ok(())
7453    }
7454}
7455
7456fn parse_chart_lines_shape(
7457    raw: &[u8],
7458    namespaces: &NamespaceBindings,
7459    slot: &mut Option<CT_ShapeProperties>,
7460) -> Result<()> {
7461    reject_conflicting_prefix_in_xml(raw, b"a", A_NS)?;
7462    let properties = CT_ShapeProperties::from_xml(raw)?;
7463    let mut writer = Writer::new(Vec::new());
7464    properties.write_xml_as(&mut writer, "c:spPr")?;
7465    reject_rewritten_foreign_elements(raw, &writer.into_inner(), namespaces, b"spPr")?;
7466    set_once(slot, properties, "c:spPr")
7467}
7468
7469impl NumberFormat {
7470    pub fn new(format_code: String, source_linked: bool) -> Result<Self> {
7471        let number_format = Self {
7472            format_code,
7473            source_linked,
7474            raw_attributes: Vec::new(),
7475            raw_content: Vec::new(),
7476        };
7477        number_format.validate()?;
7478        Ok(number_format)
7479    }
7480
7481    fn from_xml(xml: &[u8]) -> Result<Self> {
7482        let mut reader = Reader::from_reader(xml);
7483        let mut buffer = Vec::new();
7484        loop {
7485            match reader
7486                .read_event_into(&mut buffer)
7487                .map_err(OxmlError::from)?
7488            {
7489                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
7490                    return Self::from_element(&element, Vec::new());
7491                }
7492                Event::Start(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
7493                    let mut raw_content = Vec::new();
7494                    let mut inner = Vec::new();
7495                    loop {
7496                        match reader
7497                            .read_event_into(&mut inner)
7498                            .map_err(OxmlError::from)?
7499                        {
7500                            Event::End(end)
7501                                if matches_local_name(end.name().as_ref(), b"numFmt") =>
7502                            {
7503                                return Self::from_element(&element, raw_content);
7504                            }
7505                            event @ (Event::Text(_)
7506                            | Event::CData(_)
7507                            | Event::Comment(_)
7508                            | Event::PI(_)
7509                            | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
7510                            Event::Eof => return Err(missing_end("c:numFmt")),
7511                            _ => return Err(ChartError::UnexpectedElement("c:numFmt".to_owned())),
7512                        }
7513                        inner.clear();
7514                    }
7515                }
7516                Event::Start(element) | Event::Empty(element) => {
7517                    return Err(ChartError::UnexpectedElement(element_name(&element)));
7518                }
7519                Event::Eof => return Err(ChartError::MissingElement("c:numFmt".to_owned())),
7520                _ => {}
7521            }
7522            buffer.clear();
7523        }
7524    }
7525
7526    fn from_element(element: &BytesStart<'_>, raw_content: Vec<Vec<u8>>) -> Result<Self> {
7527        reject_conflicting_prefix(element, b"c", C_NS)?;
7528        let format_code = attribute_value(element, b"formatCode")?
7529            .ok_or_else(|| invalid_attribute("numFmt", "formatCode", "<missing>".to_owned()))?;
7530        let source_linked = attribute_value(element, b"sourceLinked")?
7531            .ok_or_else(|| invalid_attribute("numFmt", "sourceLinked", "<missing>".to_owned()))?;
7532        let source_linked = parse_bool_lexical("numFmt", "sourceLinked", &source_linked)?;
7533        let number_format = Self {
7534            format_code,
7535            source_linked,
7536            raw_attributes: capture_attributes_excluding(
7537                element,
7538                &[b"formatCode", b"sourceLinked"],
7539            )?,
7540            raw_content,
7541        };
7542        number_format.validate()?;
7543        Ok(number_format)
7544    }
7545
7546    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7547        self.validate()?;
7548        let mut start = BytesStart::new("c:numFmt");
7549        start.push_attribute(("formatCode", self.format_code.as_str()));
7550        start.push_attribute(("sourceLinked", bool_lexical(self.source_linked)));
7551        push_attributes(&mut start, &self.raw_attributes);
7552        if self.raw_content.is_empty() {
7553            writer
7554                .write_event(Event::Empty(start))
7555                .map_err(OxmlError::from)?;
7556            return Ok(());
7557        }
7558        writer
7559            .write_event(Event::Start(start))
7560            .map_err(OxmlError::from)?;
7561        for raw in &self.raw_content {
7562            writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
7563        }
7564        writer
7565            .write_event(Event::End(BytesEnd::new("c:numFmt")))
7566            .map_err(OxmlError::from)?;
7567        Ok(())
7568    }
7569
7570    /// Projects one cached value through the supported deterministic subset.
7571    pub fn format_value(&self, value: f64) -> Result<String> {
7572        if !value.is_finite() {
7573            return Err(ChartError::InvalidValue {
7574                element: "c:numFmt value".to_owned(),
7575                value: value.to_string(),
7576            });
7577        }
7578        if self.format_code == "General" {
7579            return Ok(value.to_string());
7580        }
7581
7582        let (numeric_code, percentage) = self
7583            .format_code
7584            .strip_suffix('%')
7585            .map(|code| (code, true))
7586            .unwrap_or((&self.format_code, false));
7587        let precision = if numeric_code == "0" {
7588            Some(0)
7589        } else if let Some(decimals) = numeric_code.strip_prefix("0.")
7590            && !decimals.is_empty()
7591            && decimals.bytes().all(|byte| byte == b'0')
7592        {
7593            Some(decimals.len())
7594        } else {
7595            None
7596        };
7597        let Some(precision) = precision else {
7598            return Err(ChartError::InvalidValue {
7599                element: "c:numFmt/@formatCode projection".to_owned(),
7600                value: self.format_code.clone(),
7601            });
7602        };
7603        let projected = if percentage { value * 100.0 } else { value };
7604        if !projected.is_finite() {
7605            return Err(ChartError::InvalidValue {
7606                element: "c:numFmt value".to_owned(),
7607                value: projected.to_string(),
7608            });
7609        }
7610        let mut formatted = format!("{projected:.precision$}");
7611        if percentage {
7612            formatted.push('%');
7613        }
7614        Ok(formatted)
7615    }
7616
7617    fn validate(&self) -> Result<()> {
7618        if self.format_code.is_empty() {
7619            return Err(ChartError::InvalidValue {
7620                element: "c:numFmt/@formatCode".to_owned(),
7621                value: self.format_code.clone(),
7622            });
7623        }
7624        validate_xml_text(&self.format_code, "c:numFmt/@formatCode")
7625    }
7626}
7627
7628impl CT_DLbls {
7629    pub fn from_xml(xml: &[u8]) -> Result<Self> {
7630        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
7631    }
7632
7633    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7634        let mut reader = Reader::from_reader(xml);
7635        let mut buffer = Vec::new();
7636        loop {
7637            match reader
7638                .read_event_into(&mut buffer)
7639                .map_err(OxmlError::from)?
7640            {
7641                Event::Start(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7642                    chart_root_prefix(&element)?;
7643                    if !element_is_in_namespace(&element, C_NS, inherited)? {
7644                        return Err(ChartError::UnexpectedElement(element_name(&element)));
7645                    }
7646                    return Self::from_element(&mut reader, &element, inherited);
7647                }
7648                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7649                    chart_root_prefix(&element)?;
7650                    let namespaces = chart_bindings(inherited, &element)?;
7651                    if !element_is_in_namespace(&element, C_NS, inherited)? {
7652                        return Err(ChartError::UnexpectedElement(element_name(&element)));
7653                    }
7654                    let (raw_attributes, _) = capture_fixed_root_attributes(
7655                        &element,
7656                        &["xmlns:c", "xmlns:a", "xmlns:r"],
7657                    )?;
7658                    return Ok(Self {
7659                        raw_attributes,
7660                        namespace_declarations: standalone_namespace_declarations(&namespaces)?,
7661                        ..Self::default()
7662                    });
7663                }
7664                Event::Start(element) | Event::Empty(element) => {
7665                    return Err(ChartError::UnexpectedElement(element_name(&element)));
7666                }
7667                Event::Eof => return Err(ChartError::MissingElement("c:dLbls".to_owned())),
7668                _ => {}
7669            }
7670            buffer.clear();
7671        }
7672    }
7673
7674    fn from_element(
7675        reader: &mut Reader<&[u8]>,
7676        start: &BytesStart<'_>,
7677        inherited: &NamespaceBindings,
7678    ) -> Result<Self> {
7679        reject_conflicting_prefix(start, b"a", A_NS)?;
7680        let namespaces = chart_bindings(inherited, start)?;
7681        require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
7682        let (raw_attributes, _) =
7683            capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
7684        let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
7685        let mut state = DataLabelsParseState::new(namespaces);
7686        let mut buffer = Vec::new();
7687        loop {
7688            match reader
7689                .read_event_into(&mut buffer)
7690                .map_err(OxmlError::from)?
7691            {
7692                Event::Start(element) => {
7693                    let name = chart_child_local(&element, &state.namespaces)?;
7694                    let raw = capture_element(reader, &element)?;
7695                    state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
7696                }
7697                Event::Empty(element) => {
7698                    let name = chart_child_local(&element, &state.namespaces)?;
7699                    let raw = capture_empty_element(&element)?;
7700                    state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
7701                }
7702                event @ (Event::Text(_)
7703                | Event::CData(_)
7704                | Event::Comment(_)
7705                | Event::PI(_)
7706                | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
7707                Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7708                    return state.finish(raw_attributes, namespace_declarations);
7709                }
7710                Event::Eof => return Err(missing_end("c:dLbls")),
7711                _ => {}
7712            }
7713            buffer.clear();
7714        }
7715    }
7716
7717    pub fn to_xml(&self) -> Result<Vec<u8>> {
7718        let mut writer = Writer::new(Vec::new());
7719        self.write_xml(&mut writer, true)?;
7720        Ok(writer.into_inner())
7721    }
7722
7723    fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
7724        self.validate()?;
7725        let mut start = BytesStart::new("c:dLbls");
7726        if standalone {
7727            start.push_attribute(("xmlns:c", C_NS));
7728            start.push_attribute(("xmlns:a", A_NS));
7729            start.push_attribute(("xmlns:r", R_NS));
7730        }
7731        push_attributes(&mut start, &self.namespace_declarations);
7732        push_attributes(&mut start, &self.raw_attributes);
7733        writer
7734            .write_event(Event::Start(start))
7735            .map_err(OxmlError::from)?;
7736        emit_raw(writer, self.raw_children.at(0))?;
7737        if let Some(number_format) = &self.number_format {
7738            number_format.write_xml(writer)?;
7739        }
7740        emit_raw(writer, self.raw_children.at(1))?;
7741        if let Some(position) = self.position {
7742            write_scalar(
7743                writer,
7744                "c:dLblPos",
7745                position.as_str(),
7746                self.position_markup.as_ref(),
7747            )?;
7748        }
7749        emit_raw(writer, self.raw_children.at(2))?;
7750        write_optional_bool(
7751            writer,
7752            "c:showLegendKey",
7753            self.show_legend_key,
7754            self.show_legend_key_markup.as_ref(),
7755        )?;
7756        emit_raw(writer, self.raw_children.at(3))?;
7757        write_optional_bool(
7758            writer,
7759            "c:showVal",
7760            self.show_value,
7761            self.show_value_markup.as_ref(),
7762        )?;
7763        emit_raw(writer, self.raw_children.at(4))?;
7764        write_optional_bool(
7765            writer,
7766            "c:showCatName",
7767            self.show_category_name,
7768            self.show_category_name_markup.as_ref(),
7769        )?;
7770        emit_raw(writer, self.raw_children.at(5))?;
7771        write_optional_bool(
7772            writer,
7773            "c:showSerName",
7774            self.show_series_name,
7775            self.show_series_name_markup.as_ref(),
7776        )?;
7777        emit_raw(writer, self.raw_children.at(6))?;
7778        write_optional_bool(
7779            writer,
7780            "c:showPercent",
7781            self.show_percent,
7782            self.show_percent_markup.as_ref(),
7783        )?;
7784        emit_raw(writer, self.raw_children.at(7))?;
7785        write_optional_bool(
7786            writer,
7787            "c:showBubbleSize",
7788            self.show_bubble_size,
7789            self.show_bubble_size_markup.as_ref(),
7790        )?;
7791        emit_raw(writer, self.raw_children.at(8))?;
7792        if let Some(separator) = &self.separator {
7793            write_text(
7794                writer,
7795                "c:separator",
7796                separator,
7797                self.separator_markup
7798                    .as_ref()
7799                    .unwrap_or(&TextMarkup::default()),
7800            )?;
7801        }
7802        emit_raw(writer, self.raw_children.at(9))?;
7803        writer
7804            .write_event(Event::End(BytesEnd::new("c:dLbls")))
7805            .map_err(OxmlError::from)?;
7806        Ok(())
7807    }
7808
7809    fn validate(&self) -> Result<()> {
7810        if let Some(number_format) = &self.number_format {
7811            number_format.validate()?;
7812        }
7813        if let Some(separator) = &self.separator {
7814            validate_xml_text(separator, "c:separator")?;
7815        }
7816        Ok(())
7817    }
7818
7819    pub fn raw_children(&self) -> &OrderedRawChildren {
7820        &self.raw_children
7821    }
7822}
7823
7824struct DataLabelsParseState {
7825    namespaces: NamespaceBindings,
7826    number_format: Option<NumberFormat>,
7827    position: Option<(DataLabelPosition, ScalarMarkup)>,
7828    show_legend_key: Option<(bool, ScalarMarkup)>,
7829    show_value: Option<(bool, ScalarMarkup)>,
7830    show_category_name: Option<(bool, ScalarMarkup)>,
7831    show_series_name: Option<(bool, ScalarMarkup)>,
7832    show_percent: Option<(bool, ScalarMarkup)>,
7833    show_bubble_size: Option<(bool, ScalarMarkup)>,
7834    separator: Option<(String, TextMarkup)>,
7835    raw_children: OrderedRawChildren,
7836    boundary: usize,
7837}
7838
7839impl DataLabelsParseState {
7840    fn new(namespaces: NamespaceBindings) -> Self {
7841        Self {
7842            namespaces,
7843            number_format: None,
7844            position: None,
7845            show_legend_key: None,
7846            show_value: None,
7847            show_category_name: None,
7848            show_series_name: None,
7849            show_percent: None,
7850            show_bubble_size: None,
7851            separator: None,
7852            raw_children: OrderedRawChildren::default(),
7853            boundary: 0,
7854        }
7855    }
7856
7857    fn capture_event(&mut self, raw: Vec<u8>) {
7858        self.raw_children.push(self.boundary, raw);
7859    }
7860
7861    fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
7862        match name {
7863            b"numFmt" => {
7864                set_once(
7865                    &mut self.number_format,
7866                    NumberFormat::from_xml(&raw)?,
7867                    "c:numFmt",
7868                )?;
7869                self.boundary = self.boundary.max(1);
7870            }
7871            b"dLblPos" => {
7872                let (value, markup) = scalar_value(&raw, "dLblPos")?;
7873                let lexical = value
7874                    .ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
7875                let position = DataLabelPosition::parse(&lexical)
7876                    .ok_or_else(|| invalid_attribute("dLblPos", "val", lexical))?;
7877                set_once(&mut self.position, (position, markup), "c:dLblPos")?;
7878                self.boundary = self.boundary.max(2);
7879            }
7880            b"showLegendKey" => {
7881                set_once(
7882                    &mut self.show_legend_key,
7883                    parse_bool_value(&raw, "showLegendKey")?,
7884                    "c:showLegendKey",
7885                )?;
7886                self.boundary = self.boundary.max(3);
7887            }
7888            b"showVal" => {
7889                set_once(
7890                    &mut self.show_value,
7891                    parse_bool_value(&raw, "showVal")?,
7892                    "c:showVal",
7893                )?;
7894                self.boundary = self.boundary.max(4);
7895            }
7896            b"showCatName" => {
7897                set_once(
7898                    &mut self.show_category_name,
7899                    parse_bool_value(&raw, "showCatName")?,
7900                    "c:showCatName",
7901                )?;
7902                self.boundary = self.boundary.max(5);
7903            }
7904            b"showSerName" => {
7905                set_once(
7906                    &mut self.show_series_name,
7907                    parse_bool_value(&raw, "showSerName")?,
7908                    "c:showSerName",
7909                )?;
7910                self.boundary = self.boundary.max(6);
7911            }
7912            b"showPercent" => {
7913                set_once(
7914                    &mut self.show_percent,
7915                    parse_bool_value(&raw, "showPercent")?,
7916                    "c:showPercent",
7917                )?;
7918                self.boundary = self.boundary.max(7);
7919            }
7920            b"showBubbleSize" => {
7921                set_once(
7922                    &mut self.show_bubble_size,
7923                    parse_bool_value(&raw, "showBubbleSize")?,
7924                    "c:showBubbleSize",
7925                )?;
7926                self.boundary = self.boundary.max(8);
7927            }
7928            b"separator" => {
7929                set_once(
7930                    &mut self.separator,
7931                    parse_text_element(&raw, b"separator", &self.namespaces)?,
7932                    "c:separator",
7933                )?;
7934                self.boundary = self.boundary.max(9);
7935            }
7936            _ => {
7937                let boundary = data_labels_raw_boundary(name, self.boundary);
7938                self.raw_children.push(boundary, raw);
7939                self.boundary = self.boundary.max(boundary);
7940            }
7941        }
7942        Ok(())
7943    }
7944
7945    fn finish(
7946        self,
7947        raw_attributes: XmlAttributes,
7948        namespace_declarations: XmlAttributes,
7949    ) -> Result<CT_DLbls> {
7950        let (position, position_markup) = optional_markup(self.position);
7951        let (show_legend_key, show_legend_key_markup) = optional_bool_markup(self.show_legend_key);
7952        let (show_value, show_value_markup) = optional_bool_markup(self.show_value);
7953        let (show_category_name, show_category_name_markup) =
7954            optional_bool_markup(self.show_category_name);
7955        let (show_series_name, show_series_name_markup) =
7956            optional_bool_markup(self.show_series_name);
7957        let (show_percent, show_percent_markup) = optional_bool_markup(self.show_percent);
7958        let (show_bubble_size, show_bubble_size_markup) =
7959            optional_bool_markup(self.show_bubble_size);
7960        let (separator, separator_markup) = self
7961            .separator
7962            .map(|(value, markup)| (Some(value), Some(markup)))
7963            .unwrap_or((None, None));
7964        let labels = CT_DLbls {
7965            number_format: self.number_format,
7966            position,
7967            separator,
7968            show_legend_key,
7969            show_value,
7970            show_category_name,
7971            show_series_name,
7972            show_percent,
7973            show_bubble_size,
7974            position_markup,
7975            show_legend_key_markup,
7976            show_value_markup,
7977            show_category_name_markup,
7978            show_series_name_markup,
7979            show_percent_markup,
7980            show_bubble_size_markup,
7981            separator_markup,
7982            raw_attributes,
7983            namespace_declarations,
7984            raw_children: raw_children_in_schema_order(&self.raw_children, 9),
7985        };
7986        labels.validate()?;
7987        Ok(labels)
7988    }
7989}
7990
7991fn data_labels_raw_boundary(name: &[u8], current: usize) -> usize {
7992    match name {
7993        b"dLbl" | b"delete" => 0,
7994        b"spPr" | b"txPr" => 1,
7995        b"showLeaderLines" | b"leaderLines" | b"extLst" => 9,
7996        _ => current,
7997    }
7998}
7999
8000fn optional_bool_markup(value: Option<(bool, ScalarMarkup)>) -> (bool, Option<ScalarMarkup>) {
8001    value
8002        .map(|(value, markup)| (value, Some(markup)))
8003        .unwrap_or((false, None))
8004}
8005
8006fn write_optional_bool(
8007    writer: &mut Writer<Vec<u8>>,
8008    tag: &str,
8009    value: bool,
8010    markup: Option<&ScalarMarkup>,
8011) -> Result<()> {
8012    if value || markup.is_some() {
8013        write_scalar(writer, tag, bool_lexical(value), markup)?;
8014    }
8015    Ok(())
8016}
8017
8018fn default_axis_id_markup(id: AxisId) -> AxisIdMarkup {
8019    AxisIdMarkup {
8020        scalar: ScalarMarkup::default(),
8021        parsed: id,
8022        lexical: id.value().to_string(),
8023    }
8024}
8025
8026fn parse_axis_id_scalar(xml: &[u8], local: &str) -> Result<(AxisId, AxisIdMarkup)> {
8027    let (value, scalar) = scalar_value(xml, local)?;
8028    let lexical = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8029    let parsed = lexical
8030        .parse::<i64>()
8031        .map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
8032    let id = AxisId::new(parsed).map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
8033    Ok((
8034        id,
8035        AxisIdMarkup {
8036            scalar,
8037            parsed: id,
8038            lexical,
8039        },
8040    ))
8041}
8042
8043fn write_axis_id<W: Write>(
8044    writer: &mut Writer<W>,
8045    tag: &str,
8046    id: AxisId,
8047    markup: &AxisIdMarkup,
8048) -> Result<()> {
8049    AxisId::new(id.value())?;
8050    let normalized;
8051    let value = if id == markup.parsed {
8052        markup.lexical.as_str()
8053    } else {
8054        normalized = id.value().to_string();
8055        normalized.as_str()
8056    };
8057    write_scalar(writer, tag, value, Some(&markup.scalar))
8058}
8059
8060fn parse_f64_scalar(xml: &[u8], local: &str) -> Result<(f64, ScalarMarkup)> {
8061    let (value, markup) = scalar_value(xml, local)?;
8062    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8063    let parsed = value
8064        .parse::<f64>()
8065        .map_err(|_| invalid_attribute(local, "val", value.clone()))?;
8066    if !parsed.is_finite() {
8067        return Err(invalid_attribute(local, "val", value));
8068    }
8069    Ok((parsed, markup))
8070}
8071
8072fn parse_axis_position(xml: &[u8]) -> Result<(AxisPosition, ScalarMarkup)> {
8073    let (value, markup) = scalar_value(xml, "axPos")?;
8074    let value = value.ok_or_else(|| invalid_attribute("axPos", "val", "<missing>".to_owned()))?;
8075    let parsed =
8076        AxisPosition::parse(&value).ok_or_else(|| invalid_attribute("axPos", "val", value))?;
8077    Ok((parsed, markup))
8078}
8079
8080fn parse_tick_mark(xml: &[u8], local: &str) -> Result<(TickMark, ScalarMarkup)> {
8081    let (value, markup) = scalar_value(xml, local)?;
8082    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8083    let parsed = TickMark::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
8084    Ok((parsed, markup))
8085}
8086
8087fn parse_tick_label_position(xml: &[u8]) -> Result<(TickLabelPosition, ScalarMarkup)> {
8088    let (value, markup) = scalar_value(xml, "tickLblPos")?;
8089    let value =
8090        value.ok_or_else(|| invalid_attribute("tickLblPos", "val", "<missing>".to_owned()))?;
8091    let parsed = TickLabelPosition::parse(&value)
8092        .ok_or_else(|| invalid_attribute("tickLblPos", "val", value))?;
8093    Ok((parsed, markup))
8094}
8095
8096fn optional_markup<T>(value: Option<(T, ScalarMarkup)>) -> (Option<T>, Option<ScalarMarkup>) {
8097    value
8098        .map(|(value, markup)| (Some(value), Some(markup)))
8099        .unwrap_or((None, None))
8100}
8101
8102fn optional_scalar_markup_eq(left: &Option<ScalarMarkup>, right: &Option<ScalarMarkup>) -> bool {
8103    let empty = ScalarMarkup::default();
8104    left.as_ref().unwrap_or(&empty) == right.as_ref().unwrap_or(&empty)
8105}
8106
8107fn axis_id_markup_eq(left: &AxisIdMarkup, right: &AxisIdMarkup) -> bool {
8108    left.scalar == right.scalar
8109}
8110
8111const fn bool_lexical(value: bool) -> &'static str {
8112    if value { "1" } else { "0" }
8113}
8114
8115fn parse_bool_lexical(element: &str, attribute: &str, value: &str) -> Result<bool> {
8116    match value {
8117        "1" | "true" => Ok(true),
8118        "0" | "false" => Ok(false),
8119        _ => Err(invalid_attribute(element, attribute, value.to_owned())),
8120    }
8121}
8122
8123impl CT_Title {
8124    fn plain_text(text: &str) -> Self {
8125        let mut body = CT_TextBody::new();
8126        body.set_text(text);
8127        let mut raw_children = OrderedRawChildren::default();
8128        raw_children.push(1, br#"<c:layout/><c:overlay val="0"/>"#.to_vec());
8129        Self {
8130            text: Some(body),
8131            raw_children,
8132            ..Self::default()
8133        }
8134    }
8135
8136    fn from_xml(xml: &[u8]) -> Result<Self> {
8137        let (raw_attributes, raw_children) = parse_raw_shell(xml, b"title", "c:title")?;
8138        Ok(Self {
8139            text: None,
8140            raw_attributes,
8141            raw_children,
8142        })
8143    }
8144
8145    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8146        if self.text.is_none() {
8147            return write_raw_shell(writer, "c:title", &self.raw_attributes, &self.raw_children);
8148        }
8149        let mut start = BytesStart::new("c:title");
8150        push_attributes(&mut start, &self.raw_attributes);
8151        writer
8152            .write_event(Event::Start(start))
8153            .map_err(OxmlError::from)?;
8154        emit_raw(writer, self.raw_children.at(0))?;
8155        writer
8156            .write_event(Event::Start(BytesStart::new("c:tx")))
8157            .map_err(OxmlError::from)?;
8158        if let Some(text) = &self.text {
8159            text.write_xml_as(writer, "c:rich")?;
8160        }
8161        writer
8162            .write_event(Event::End(BytesEnd::new("c:tx")))
8163            .map_err(OxmlError::from)?;
8164        emit_raw(writer, self.raw_children.at(1))?;
8165        writer
8166            .write_event(Event::End(BytesEnd::new("c:title")))
8167            .map_err(OxmlError::from)?;
8168        Ok(())
8169    }
8170
8171    pub fn raw_children(&self) -> &OrderedRawChildren {
8172        &self.raw_children
8173    }
8174}
8175
8176impl CT_PlotArea {
8177    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
8178        let (raw_attributes, captured_children) = parse_raw_shell(xml, b"plotArea", "c:plotArea")?;
8179        let namespace_bindings = root_chart_bindings(xml, b"plotArea", inherited)?;
8180        let direct_roots: Vec<_> = captured_children
8181            .at(0)
8182            .filter_map(|raw| chart_root_local(raw, &namespace_bindings).transpose())
8183            .collect::<Result<_>>()?;
8184        let plot_roots: Vec<_> = direct_roots
8185            .iter()
8186            .filter(|local| is_plot_container(local))
8187            .cloned()
8188            .collect();
8189        let typed_local = (plot_roots.len() == 1
8190            && matches!(
8191                plot_roots[0].as_slice(),
8192                b"barChart"
8193                    | b"lineChart"
8194                    | b"pieChart"
8195                    | b"doughnutChart"
8196                    | b"areaChart"
8197                    | b"scatterChart"
8198                    | b"radarChart"
8199            ))
8200        .then(|| plot_roots[0].clone());
8201
8202        let Some(typed_local) = typed_local else {
8203            return Ok(Self {
8204                raw_attributes,
8205                raw_children: captured_children,
8206                namespace_bindings,
8207                plots: None,
8208                plot_markup: Vec::new(),
8209                axes: Vec::new(),
8210            });
8211        };
8212
8213        let mut raw_children = OrderedRawChildren::default();
8214        let mut plots = Vec::new();
8215        let mut plot_markup = Vec::new();
8216        let mut axes = Vec::new();
8217        let mut boundary = 0usize;
8218        for raw in captured_children.at(0) {
8219            let local = chart_root_local(raw, &namespace_bindings)?;
8220            if local.as_deref() == Some(typed_local.as_slice()) {
8221                let (plot, markup) = parse_plot(raw, &namespace_bindings)?;
8222                plots.push(plot);
8223                plot_markup.push(markup);
8224                boundary = 1;
8225            } else if local.as_deref().and_then(AxisKind::parse).is_some() {
8226                axes.push(Axis::from_xml_with_namespaces(raw, &namespace_bindings)?);
8227                boundary = axes.len() + 1;
8228            } else {
8229                raw_children.push(boundary, raw.to_vec());
8230            }
8231        }
8232        let plot_area = Self {
8233            raw_attributes,
8234            raw_children,
8235            namespace_bindings,
8236            plots: Some(plots),
8237            plot_markup,
8238            axes,
8239        };
8240        plot_area.validate_typed()?;
8241        Ok(plot_area)
8242    }
8243
8244    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
8245        let Some(plots) = &self.plots else {
8246            return write_raw_shell(
8247                writer,
8248                "c:plotArea",
8249                &self.raw_attributes,
8250                &self.raw_children,
8251            );
8252        };
8253        self.validate_typed()?;
8254        let mut start = BytesStart::new("c:plotArea");
8255        push_attributes(&mut start, &self.raw_attributes);
8256        writer
8257            .write_event(Event::Start(start))
8258            .map_err(OxmlError::from)?;
8259        emit_raw(writer, self.raw_children.at(0))?;
8260        for (index, plot) in plots.iter().enumerate() {
8261            write_plot(
8262                writer,
8263                plot,
8264                self.plot_markup
8265                    .get(index)
8266                    .unwrap_or(&PlotMarkup::default()),
8267            )?;
8268        }
8269        emit_raw(writer, self.raw_children.at(1))?;
8270        for (index, axis) in self.axes.iter().enumerate() {
8271            writer
8272                .get_mut()
8273                .write_all(&axis.to_xml()?)
8274                .map_err(OxmlError::from)?;
8275            emit_raw(writer, self.raw_children.at(index + 2))?;
8276        }
8277        writer
8278            .write_event(Event::End(BytesEnd::new("c:plotArea")))
8279            .map_err(OxmlError::from)?;
8280        Ok(())
8281    }
8282
8283    pub fn raw_children(&self) -> &OrderedRawChildren {
8284        &self.raw_children
8285    }
8286
8287    /// Creates a supported single-family plot area with owned axes.
8288    pub fn new(plots: Vec<Plot>, axes: Vec<Axis>) -> Result<Self> {
8289        let plot_markup = plots
8290            .iter()
8291            .map(|plot| {
8292                let mut markup = PlotMarkup::default();
8293                if matches!(plot, Plot::Line { .. }) {
8294                    markup.marker = Some(ScalarMarkup::default());
8295                    markup.smooth = Some(ScalarMarkup::default());
8296                }
8297                if matches!(plot, Plot::Doughnut { .. }) {
8298                    markup.hole_size = Some(ScalarMarkup::default());
8299                }
8300                markup
8301            })
8302            .collect();
8303        let plot_area = Self {
8304            raw_attributes: Vec::new(),
8305            raw_children: OrderedRawChildren::default(),
8306            namespace_bindings: chart_namespace_defaults(),
8307            plot_markup,
8308            plots: Some(plots),
8309            axes,
8310        };
8311        plot_area.validate_typed()?;
8312        Ok(plot_area)
8313    }
8314
8315    /// Returns the owned supported plots, or an error for an opaque choice.
8316    pub fn plots(&self) -> Result<&[Plot]> {
8317        self.plots
8318            .as_deref()
8319            .ok_or_else(|| ChartError::InvalidValue {
8320                element: "c:plotArea".to_owned(),
8321                value: "unsupported or combination plot area is opaque".to_owned(),
8322            })
8323    }
8324
8325    /// Returns mutable supported plots without exposing opaque content to edits.
8326    pub fn plots_mut(&mut self) -> Result<&mut [Plot]> {
8327        self.plots
8328            .as_deref_mut()
8329            .ok_or_else(|| ChartError::InvalidValue {
8330                element: "c:plotArea".to_owned(),
8331                value: "cannot combine typed plots with an opaque plot choice".to_owned(),
8332            })
8333    }
8334
8335    /// Parses the common series payloads nested in category-based plot shells.
8336    pub fn series(&self) -> Result<Vec<Series>> {
8337        if let Some(plots) = &self.plots {
8338            return Ok(plots
8339                .iter()
8340                .flat_map(|plot| match plot {
8341                    Plot::Bar { series, .. }
8342                    | Plot::Line { series, .. }
8343                    | Plot::Pie { series, .. }
8344                    | Plot::Doughnut { series, .. }
8345                    | Plot::Area { series, .. }
8346                    | Plot::Scatter { series, .. }
8347                    | Plot::Radar { series, .. } => series.clone(),
8348                })
8349                .collect());
8350        }
8351        let mut series = Vec::new();
8352        for raw in self.raw_children.at(0) {
8353            parse_plot_series(raw, &self.namespace_bindings, &mut series)?;
8354        }
8355        Ok(series)
8356    }
8357
8358    /// Parses and validates the direct axis children of this plot area.
8359    pub fn axes(&self) -> Result<Vec<Axis>> {
8360        if self.plots.is_some() {
8361            self.validate_typed()?;
8362            return Ok(self.axes.clone());
8363        }
8364        let mut axes = Vec::new();
8365        for raw in self.raw_children.at(0) {
8366            if let Some(axis) = parse_plot_axis(raw, &self.namespace_bindings)? {
8367                axes.push(axis);
8368            }
8369        }
8370        validate_axis_pairs(&axes)?;
8371        Ok(axes)
8372    }
8373
8374    fn validate_typed(&self) -> Result<()> {
8375        let plots = self
8376            .plots
8377            .as_ref()
8378            .ok_or_else(|| ChartError::InvalidValue {
8379                element: "c:plotArea".to_owned(),
8380                value: "opaque plot area has no typed validation view".to_owned(),
8381            })?;
8382        if plots.len() != 1 {
8383            return Err(ChartError::InvalidValue {
8384                element: "c:plotArea".to_owned(),
8385                value: "typed plot area requires exactly one plot family".to_owned(),
8386            });
8387        }
8388        validate_axis_pairs(&self.axes)?;
8389        for plot in plots {
8390            plot.validate()?;
8391            if let Some(axis_ids) = plot.axis_ids() {
8392                for id in axis_ids {
8393                    if !self.axes.iter().any(|axis| axis.id == id) {
8394                        return Err(ChartError::InvalidValue {
8395                            element: "c:axId".to_owned(),
8396                            value: format!("plot references missing axis {}", id.value()),
8397                        });
8398                    }
8399                }
8400            } else if !self.axes.is_empty() {
8401                return Err(ChartError::InvalidValue {
8402                    element: "c:plotArea".to_owned(),
8403                    value: "pie and doughnut plot areas must not own axes".to_owned(),
8404                });
8405            }
8406            if plot.axis_ids().is_some() && self.axes.len() != 2 {
8407                return Err(ChartError::InvalidValue {
8408                    element: "c:plotArea".to_owned(),
8409                    value: format!(
8410                        "axis-owned plot requires exactly 2 axes, found {}",
8411                        self.axes.len()
8412                    ),
8413                });
8414            }
8415        }
8416        Ok(())
8417    }
8418}
8419
8420impl Default for CT_PlotArea {
8421    fn default() -> Self {
8422        Self {
8423            raw_attributes: Vec::new(),
8424            raw_children: OrderedRawChildren::default(),
8425            namespace_bindings: chart_namespace_defaults(),
8426            plots: Some(Vec::new()),
8427            plot_markup: Vec::new(),
8428            axes: Vec::new(),
8429        }
8430    }
8431}
8432
8433fn chart_root_local(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Vec<u8>>> {
8434    let mut reader = Reader::from_reader(xml);
8435    let mut buffer = Vec::new();
8436    loop {
8437        match reader
8438            .read_event_into(&mut buffer)
8439            .map_err(OxmlError::from)?
8440        {
8441            Event::Start(element) | Event::Empty(element) => {
8442                return Ok(element_is_in_namespace(&element, C_NS, inherited)?
8443                    .then(|| local_name(element.name().as_ref()).to_vec()));
8444            }
8445            Event::Eof => return Ok(None),
8446            _ => {}
8447        }
8448        buffer.clear();
8449    }
8450}
8451
8452fn is_plot_container(local: &[u8]) -> bool {
8453    matches!(
8454        local,
8455        b"areaChart"
8456            | b"area3DChart"
8457            | b"barChart"
8458            | b"bar3DChart"
8459            | b"bubbleChart"
8460            | b"doughnutChart"
8461            | b"lineChart"
8462            | b"line3DChart"
8463            | b"ofPieChart"
8464            | b"pieChart"
8465            | b"pie3DChart"
8466            | b"radarChart"
8467            | b"scatterChart"
8468            | b"stockChart"
8469            | b"surfaceChart"
8470            | b"surface3DChart"
8471    )
8472}
8473
8474fn parse_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8475    let local = chart_root_local(xml, inherited)?
8476        .ok_or_else(|| ChartError::MissingElement("c:barChart or c:lineChart".to_owned()))?;
8477    match local.as_slice() {
8478        b"barChart" => parse_bar_plot(xml, inherited),
8479        b"lineChart" => parse_line_plot(xml, inherited),
8480        b"pieChart" | b"doughnutChart" | b"areaChart" | b"scatterChart" | b"radarChart" => {
8481            parse_remaining_plot(xml, inherited, &local)
8482        }
8483        _ => Err(ChartError::UnexpectedElement(format!(
8484            "c:{}",
8485            String::from_utf8_lossy(&local)
8486        ))),
8487    }
8488}
8489
8490fn parse_remaining_plot(
8491    xml: &[u8],
8492    inherited: &NamespaceBindings,
8493    local: &[u8],
8494) -> Result<(Plot, PlotMarkup)> {
8495    let root = String::from_utf8_lossy(local).into_owned();
8496    let mut reader = Reader::from_reader(xml);
8497    let mut buffer = Vec::new();
8498    loop {
8499        match reader
8500            .read_event_into(&mut buffer)
8501            .map_err(OxmlError::from)?
8502        {
8503            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
8504                let namespaces = typed_rewrite_bindings(&element, inherited)?;
8505                let (mut raw_attributes, namespace_declarations) =
8506                    capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8507                raw_attributes.splice(0..0, namespace_declarations);
8508                let mut grouping = None;
8509                let mut style = None;
8510                let mut first_slice_angle = None;
8511                let mut hole_size = None;
8512                let mut series = Vec::new();
8513                let mut data_labels = None;
8514                let mut axis_ids = Vec::new();
8515                let mut raw_children = OrderedRawChildren::default();
8516                let mut boundary = 0usize;
8517                let mut inner = Vec::new();
8518                loop {
8519                    match reader
8520                        .read_event_into(&mut inner)
8521                        .map_err(OxmlError::from)?
8522                    {
8523                        Event::Start(child) => {
8524                            let name = chart_child_local(&child, &namespaces)?;
8525                            let raw = capture_element(&mut reader, &child)?;
8526                            parse_remaining_plot_child(
8527                                local,
8528                                name.as_deref().unwrap_or_default(),
8529                                raw,
8530                                &namespaces,
8531                                &mut grouping,
8532                                &mut style,
8533                                &mut first_slice_angle,
8534                                &mut hole_size,
8535                                &mut series,
8536                                &mut data_labels,
8537                                &mut axis_ids,
8538                                &mut raw_children,
8539                                &mut boundary,
8540                            )?;
8541                        }
8542                        Event::Empty(child) => {
8543                            let name = chart_child_local(&child, &namespaces)?;
8544                            let raw = capture_empty_element(&child)?;
8545                            parse_remaining_plot_child(
8546                                local,
8547                                name.as_deref().unwrap_or_default(),
8548                                raw,
8549                                &namespaces,
8550                                &mut grouping,
8551                                &mut style,
8552                                &mut first_slice_angle,
8553                                &mut hole_size,
8554                                &mut series,
8555                                &mut data_labels,
8556                                &mut axis_ids,
8557                                &mut raw_children,
8558                                &mut boundary,
8559                            )?;
8560                        }
8561                        event @ (Event::Text(_)
8562                        | Event::CData(_)
8563                        | Event::Comment(_)
8564                        | Event::PI(_)
8565                        | Event::GeneralRef(_)) => {
8566                            raw_children.push(boundary, capture_event(event)?);
8567                        }
8568                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
8569                            let needs_axes =
8570                                matches!(local, b"areaChart" | b"scatterChart" | b"radarChart");
8571                            if needs_axes && axis_ids.len() != 2 {
8572                                return Err(ChartError::InvalidValue {
8573                                    element: format!("c:{root}/c:axId"),
8574                                    value: format!("expected 2, found {}", axis_ids.len()),
8575                                });
8576                            }
8577                            if !needs_axes && !axis_ids.is_empty() {
8578                                return Err(ChartError::InvalidValue {
8579                                    element: format!("c:{root}/c:axId"),
8580                                    value: "axis-free plot contains axis references".to_owned(),
8581                                });
8582                            }
8583                            let parsed_ids = needs_axes.then(|| [axis_ids[0].0, axis_ids[1].0]);
8584                            let original_series_keys = series
8585                                .iter()
8586                                .map(|item| (item.index, item.order))
8587                                .collect::<Vec<_>>();
8588                            let plot = match local {
8589                                b"pieChart" => Plot::Pie {
8590                                    first_slice_angle: first_slice_angle
8591                                        .as_ref()
8592                                        .map_or(0, |item| item.0),
8593                                    series,
8594                                    data_labels,
8595                                },
8596                                b"doughnutChart" => Plot::Doughnut {
8597                                    first_slice_angle: first_slice_angle
8598                                        .as_ref()
8599                                        .map_or(0, |item| item.0),
8600                                    hole_size: hole_size.as_ref().map_or(50, |item| item.0),
8601                                    series,
8602                                    data_labels,
8603                                },
8604                                b"areaChart" => Plot::Area {
8605                                    grouping: grouping
8606                                        .as_ref()
8607                                        .ok_or_else(|| {
8608                                            ChartError::MissingElement("c:grouping".to_owned())
8609                                        })?
8610                                        .0,
8611                                    series,
8612                                    data_labels,
8613                                    axis_ids: parsed_ids.ok_or_else(|| {
8614                                        ChartError::MissingElement("c:areaChart/c:axId".to_owned())
8615                                    })?,
8616                                },
8617                                b"scatterChart" => {
8618                                    let lexical = style
8619                                        .as_ref()
8620                                        .ok_or_else(|| {
8621                                            ChartError::MissingElement("c:scatterStyle".to_owned())
8622                                        })?
8623                                        .0
8624                                        .as_str();
8625                                    Plot::Scatter {
8626                                        style: ScatterStyle::parse(lexical).ok_or_else(|| {
8627                                            invalid_attribute(
8628                                                "scatterStyle",
8629                                                "val",
8630                                                lexical.to_owned(),
8631                                            )
8632                                        })?,
8633                                        series,
8634                                        data_labels,
8635                                        axis_ids: parsed_ids.ok_or_else(|| {
8636                                            ChartError::MissingElement(
8637                                                "c:scatterChart/c:axId".to_owned(),
8638                                            )
8639                                        })?,
8640                                    }
8641                                }
8642                                b"radarChart" => {
8643                                    let lexical = style
8644                                        .as_ref()
8645                                        .ok_or_else(|| {
8646                                            ChartError::MissingElement("c:radarStyle".to_owned())
8647                                        })?
8648                                        .0
8649                                        .as_str();
8650                                    Plot::Radar {
8651                                        style: RadarStyle::parse(lexical).ok_or_else(|| {
8652                                            invalid_attribute(
8653                                                "radarStyle",
8654                                                "val",
8655                                                lexical.to_owned(),
8656                                            )
8657                                        })?,
8658                                        series,
8659                                        data_labels,
8660                                        axis_ids: parsed_ids.ok_or_else(|| {
8661                                            ChartError::MissingElement(
8662                                                "c:radarChart/c:axId".to_owned(),
8663                                            )
8664                                        })?,
8665                                    }
8666                                }
8667                                _ => {
8668                                    return Err(ChartError::UnexpectedElement(format!(
8669                                        "c:{}",
8670                                        String::from_utf8_lossy(local)
8671                                    )));
8672                                }
8673                            };
8674                            plot.validate()?;
8675                            let final_boundary = boundary.max(original_series_keys.len() + 6);
8676                            return Ok((
8677                                plot,
8678                                PlotMarkup {
8679                                    raw_attributes,
8680                                    raw_children: raw_children_in_schema_order(
8681                                        &raw_children,
8682                                        final_boundary,
8683                                    ),
8684                                    grouping: grouping.map(|item| item.1),
8685                                    first_slice_angle: first_slice_angle.map(|item| item.1),
8686                                    hole_size: hole_size.map(|item| item.1),
8687                                    style: style.map(|item| item.1),
8688                                    axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8689                                    original_series_keys,
8690                                    original_axis_ids: parsed_ids
8691                                        .map_or_else(Vec::new, |ids| ids.to_vec()),
8692                                    parsed_remaining: match local {
8693                                        b"pieChart" => Some("pieChart"),
8694                                        b"doughnutChart" => Some("doughnutChart"),
8695                                        b"areaChart" => Some("areaChart"),
8696                                        b"scatterChart" => Some("scatterChart"),
8697                                        b"radarChart" => Some("radarChart"),
8698                                        _ => None,
8699                                    },
8700                                    ..PlotMarkup::default()
8701                                },
8702                            ));
8703                        }
8704                        Event::Eof => return Err(missing_end(&format!("c:{root}"))),
8705                        _ => {}
8706                    }
8707                    inner.clear();
8708                }
8709            }
8710            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
8711                return Err(ChartError::MissingElement("c:ser".to_owned()));
8712            }
8713            Event::Start(element) | Event::Empty(element) => {
8714                return Err(ChartError::UnexpectedElement(element_name(&element)));
8715            }
8716            Event::Eof => return Err(ChartError::MissingElement(format!("c:{root}"))),
8717            _ => {}
8718        }
8719        buffer.clear();
8720    }
8721}
8722
8723#[allow(clippy::too_many_arguments)]
8724fn parse_remaining_plot_child(
8725    root: &[u8],
8726    name: &[u8],
8727    raw: Vec<u8>,
8728    namespaces: &NamespaceBindings,
8729    grouping: &mut Option<(Grouping, ScalarMarkup)>,
8730    style: &mut Option<(String, ScalarMarkup)>,
8731    first_slice_angle: &mut Option<(u16, ScalarMarkup)>,
8732    hole_size: &mut Option<(u8, ScalarMarkup)>,
8733    series: &mut Vec<Series>,
8734    data_labels: &mut Option<CT_DLbls>,
8735    axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
8736    raw_children: &mut OrderedRawChildren,
8737    boundary: &mut usize,
8738) -> Result<()> {
8739    let property_first = matches!(root, b"areaChart" | b"scatterChart" | b"radarChart");
8740    let base = usize::from(property_first);
8741    match name {
8742        b"grouping" if root == b"areaChart" => {
8743            let (value, markup) = required_scalar(&raw, "grouping")?;
8744            let parsed = Grouping::parse(&value)
8745                .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
8746            set_once(grouping, (parsed, markup), "c:grouping")?;
8747            *boundary = (*boundary).max(1);
8748        }
8749        b"scatterStyle" if root == b"scatterChart" => {
8750            let (value, markup) = required_scalar(&raw, "scatterStyle")?;
8751            if ScatterStyle::parse(&value).is_none() {
8752                return Err(invalid_attribute("scatterStyle", "val", value));
8753            }
8754            set_once(style, (value, markup), "c:scatterStyle")?;
8755            *boundary = (*boundary).max(1);
8756        }
8757        b"radarStyle" if root == b"radarChart" => {
8758            let (value, markup) = required_scalar(&raw, "radarStyle")?;
8759            if RadarStyle::parse(&value).is_none() {
8760                return Err(invalid_attribute("radarStyle", "val", value));
8761            }
8762            set_once(style, (value, markup), "c:radarStyle")?;
8763            *boundary = (*boundary).max(1);
8764        }
8765        b"ser" => {
8766            series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
8767            *boundary = (*boundary).max(base + series.len());
8768        }
8769        b"dLbls" => {
8770            set_once(
8771                data_labels,
8772                CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
8773                "c:dLbls",
8774            )?;
8775            *boundary = (*boundary).max(base + series.len() + 1);
8776        }
8777        b"firstSliceAng" if matches!(root, b"pieChart" | b"doughnutChart") => {
8778            let (value, markup) = required_scalar(&raw, "firstSliceAng")?;
8779            let parsed = value
8780                .parse::<u16>()
8781                .map_err(|_| invalid_attribute("firstSliceAng", "val", value.clone()))?;
8782            if parsed > 360 {
8783                return Err(invalid_attribute("firstSliceAng", "val", value));
8784            }
8785            set_once(first_slice_angle, (parsed, markup), "c:firstSliceAng")?;
8786            *boundary = (*boundary).max(series.len() + 2);
8787        }
8788        b"holeSize" if root == b"doughnutChart" => {
8789            let (value, markup) = required_scalar(&raw, "holeSize")?;
8790            let parsed = value
8791                .parse::<u8>()
8792                .map_err(|_| invalid_attribute("holeSize", "val", value.clone()))?;
8793            if !(10..=90).contains(&parsed) {
8794                return Err(invalid_attribute("holeSize", "val", value));
8795            }
8796            set_once(hole_size, (parsed, markup), "c:holeSize")?;
8797            *boundary = (*boundary).max(series.len() + 3);
8798        }
8799        b"axId" if property_first => {
8800            if axis_ids.len() == 2 {
8801                return Err(ChartError::DuplicateElement("c:axId".to_owned()));
8802            }
8803            axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
8804            *boundary = (*boundary).max(base + series.len() + 1 + axis_ids.len());
8805        }
8806        b"axId" => {
8807            return Err(ChartError::InvalidValue {
8808                element: "c:axId".to_owned(),
8809                value: "axis-free plot contains axis reference".to_owned(),
8810            });
8811        }
8812        _ => {
8813            let raw_boundary = match (root, name) {
8814                (_, b"varyColors") => base,
8815                (b"pieChart", b"extLst") => series.len() + 2,
8816                (b"doughnutChart", b"extLst") => series.len() + 3,
8817                (b"areaChart" | b"scatterChart" | b"radarChart", b"dropLines") => {
8818                    base + series.len() + 1
8819                }
8820                (b"areaChart" | b"scatterChart" | b"radarChart", b"extLst") => {
8821                    base + series.len() + 3
8822                }
8823                _ => *boundary,
8824            };
8825            raw_children.push(raw_boundary, raw);
8826            *boundary = (*boundary).max(raw_boundary);
8827        }
8828    }
8829    Ok(())
8830}
8831
8832fn parse_bar_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8833    let mut reader = Reader::from_reader(xml);
8834    let mut buffer = Vec::new();
8835    loop {
8836        match reader
8837            .read_event_into(&mut buffer)
8838            .map_err(OxmlError::from)?
8839        {
8840            Event::Start(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
8841                let namespaces = typed_rewrite_bindings(&element, inherited)?;
8842                let (mut raw_attributes, namespace_declarations) =
8843                    capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8844                raw_attributes.splice(0..0, namespace_declarations);
8845                let mut direction = None;
8846                let mut grouping = None;
8847                let mut gap_width = None;
8848                let mut overlap = None;
8849                let mut series = Vec::new();
8850                let mut data_labels = None;
8851                let mut axis_ids = Vec::new();
8852                let mut raw_children = OrderedRawChildren::default();
8853                let mut boundary = 0usize;
8854                let mut inner = Vec::new();
8855                loop {
8856                    match reader
8857                        .read_event_into(&mut inner)
8858                        .map_err(OxmlError::from)?
8859                    {
8860                        Event::Start(child) => {
8861                            let name = chart_child_local(&child, &namespaces)?;
8862                            let raw = capture_element(&mut reader, &child)?;
8863                            parse_bar_child(
8864                                name.as_deref().unwrap_or_default(),
8865                                raw,
8866                                &namespaces,
8867                                &mut direction,
8868                                &mut grouping,
8869                                &mut gap_width,
8870                                &mut overlap,
8871                                &mut series,
8872                                &mut data_labels,
8873                                &mut axis_ids,
8874                                &mut raw_children,
8875                                &mut boundary,
8876                            )?;
8877                        }
8878                        Event::Empty(child) => {
8879                            let name = chart_child_local(&child, &namespaces)?;
8880                            let raw = capture_empty_element(&child)?;
8881                            parse_bar_child(
8882                                name.as_deref().unwrap_or_default(),
8883                                raw,
8884                                &namespaces,
8885                                &mut direction,
8886                                &mut grouping,
8887                                &mut gap_width,
8888                                &mut overlap,
8889                                &mut series,
8890                                &mut data_labels,
8891                                &mut axis_ids,
8892                                &mut raw_children,
8893                                &mut boundary,
8894                            )?;
8895                        }
8896                        event @ (Event::Text(_)
8897                        | Event::CData(_)
8898                        | Event::Comment(_)
8899                        | Event::PI(_)
8900                        | Event::GeneralRef(_)) => {
8901                            raw_children.push(boundary, capture_event(event)?);
8902                        }
8903                        Event::End(end) if matches_local_name(end.name().as_ref(), b"barChart") => {
8904                            let (direction, direction_markup) = direction
8905                                .ok_or_else(|| ChartError::MissingElement("c:barDir".to_owned()))?;
8906                            let (grouping, grouping_markup) = grouping.ok_or_else(|| {
8907                                ChartError::MissingElement("c:grouping".to_owned())
8908                            })?;
8909                            if axis_ids.len() != 2 {
8910                                return Err(ChartError::InvalidValue {
8911                                    element: "c:barChart/c:axId".to_owned(),
8912                                    value: format!("expected 2, found {}", axis_ids.len()),
8913                                });
8914                            }
8915                            let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
8916                            let original_series_keys: Vec<(u32, u32)> =
8917                                series.iter().map(|item| (item.index, item.order)).collect();
8918                            let plot = Plot::Bar {
8919                                direction,
8920                                grouping,
8921                                gap_width: gap_width.as_ref().map_or(150, |item| item.0),
8922                                overlap: overlap.as_ref().map_or(0, |item| item.0),
8923                                series,
8924                                data_labels,
8925                                axis_ids: parsed_ids,
8926                            };
8927                            plot.validate()?;
8928                            return Ok((
8929                                plot,
8930                                PlotMarkup {
8931                                    raw_attributes,
8932                                    raw_children: raw_children_in_schema_order(
8933                                        &raw_children,
8934                                        original_series_keys.len() + 7,
8935                                    ),
8936                                    direction: Some(direction_markup),
8937                                    grouping: Some(grouping_markup),
8938                                    gap_width: gap_width.map(|item| item.1),
8939                                    overlap: overlap.map(|item| item.1),
8940                                    marker: None,
8941                                    smooth: None,
8942                                    first_slice_angle: None,
8943                                    hole_size: None,
8944                                    style: None,
8945                                    axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8946                                    original_series_keys,
8947                                    original_axis_ids: parsed_ids.to_vec(),
8948                                    parsed_bar: Some(true),
8949                                    parsed_remaining: None,
8950                                },
8951                            ));
8952                        }
8953                        Event::Eof => return Err(missing_end("c:barChart")),
8954                        _ => {}
8955                    }
8956                    inner.clear();
8957                }
8958            }
8959            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
8960                return Err(ChartError::MissingElement("c:barDir".to_owned()));
8961            }
8962            Event::Start(element) | Event::Empty(element) => {
8963                return Err(ChartError::UnexpectedElement(element_name(&element)));
8964            }
8965            Event::Eof => return Err(ChartError::MissingElement("c:barChart".to_owned())),
8966            _ => {}
8967        }
8968        buffer.clear();
8969    }
8970}
8971
8972#[allow(clippy::too_many_arguments)]
8973fn parse_bar_child(
8974    name: &[u8],
8975    raw: Vec<u8>,
8976    namespaces: &NamespaceBindings,
8977    direction: &mut Option<(BarDirection, ScalarMarkup)>,
8978    grouping: &mut Option<(BarGrouping, ScalarMarkup)>,
8979    gap_width: &mut Option<(u16, ScalarMarkup)>,
8980    overlap: &mut Option<(i8, ScalarMarkup)>,
8981    series: &mut Vec<Series>,
8982    data_labels: &mut Option<CT_DLbls>,
8983    axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
8984    raw_children: &mut OrderedRawChildren,
8985    boundary: &mut usize,
8986) -> Result<()> {
8987    match name {
8988        b"barDir" => {
8989            let (value, markup) = required_scalar(&raw, "barDir")?;
8990            let value = BarDirection::parse(&value)
8991                .ok_or_else(|| invalid_attribute("barDir", "val", value))?;
8992            set_once(direction, (value, markup), "c:barDir")?;
8993            *boundary = (*boundary).max(1);
8994        }
8995        b"grouping" => {
8996            let (value, markup) = required_scalar(&raw, "grouping")?;
8997            let value = BarGrouping::parse(&value)
8998                .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
8999            set_once(grouping, (value, markup), "c:grouping")?;
9000            *boundary = (*boundary).max(2);
9001        }
9002        b"ser" => {
9003            series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
9004            *boundary = (*boundary).max(series.len() + 2);
9005        }
9006        b"dLbls" => {
9007            set_once(
9008                data_labels,
9009                CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
9010                "c:dLbls",
9011            )?;
9012            *boundary = (*boundary).max(series.len() + 3);
9013        }
9014        b"gapWidth" => {
9015            let (value, markup) = required_scalar(&raw, "gapWidth")?;
9016            let parsed = value
9017                .parse::<u16>()
9018                .map_err(|_| invalid_attribute("gapWidth", "val", value.clone()))?;
9019            if parsed > 500 {
9020                return Err(invalid_attribute("gapWidth", "val", value));
9021            }
9022            set_once(gap_width, (parsed, markup), "c:gapWidth")?;
9023            *boundary = (*boundary).max(series.len() + 4);
9024        }
9025        b"overlap" => {
9026            let (value, markup) = required_scalar(&raw, "overlap")?;
9027            let parsed = value
9028                .parse::<i8>()
9029                .map_err(|_| invalid_attribute("overlap", "val", value.clone()))?;
9030            if !(-100..=100).contains(&parsed) {
9031                return Err(invalid_attribute("overlap", "val", value));
9032            }
9033            set_once(overlap, (parsed, markup), "c:overlap")?;
9034            *boundary = (*boundary).max(series.len() + 5);
9035        }
9036        b"axId" => {
9037            if axis_ids.len() == 2 {
9038                return Err(ChartError::DuplicateElement("c:axId".to_owned()));
9039            }
9040            axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
9041            *boundary = (*boundary).max(series.len() + 5 + axis_ids.len());
9042        }
9043        _ => raw_children.push(*boundary, raw),
9044    }
9045    Ok(())
9046}
9047
9048fn parse_line_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
9049    let mut reader = Reader::from_reader(xml);
9050    let mut buffer = Vec::new();
9051    loop {
9052        match reader
9053            .read_event_into(&mut buffer)
9054            .map_err(OxmlError::from)?
9055        {
9056            Event::Start(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
9057                let namespaces = typed_rewrite_bindings(&element, inherited)?;
9058                let (mut raw_attributes, namespace_declarations) =
9059                    capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
9060                raw_attributes.splice(0..0, namespace_declarations);
9061                let mut grouping = None;
9062                let mut marker = None;
9063                let mut smooth = None;
9064                let mut series = Vec::new();
9065                let mut data_labels = None;
9066                let mut axis_ids = Vec::new();
9067                let mut raw_children = OrderedRawChildren::default();
9068                let mut boundary = 0usize;
9069                let mut inner = Vec::new();
9070                loop {
9071                    match reader
9072                        .read_event_into(&mut inner)
9073                        .map_err(OxmlError::from)?
9074                    {
9075                        Event::Start(child) => {
9076                            let name = chart_child_local(&child, &namespaces)?;
9077                            let raw = capture_element(&mut reader, &child)?;
9078                            parse_line_child(
9079                                name.as_deref().unwrap_or_default(),
9080                                raw,
9081                                &namespaces,
9082                                &mut grouping,
9083                                &mut marker,
9084                                &mut smooth,
9085                                &mut series,
9086                                &mut data_labels,
9087                                &mut axis_ids,
9088                                &mut raw_children,
9089                                &mut boundary,
9090                            )?;
9091                        }
9092                        Event::Empty(child) => {
9093                            let name = chart_child_local(&child, &namespaces)?;
9094                            let raw = capture_empty_element(&child)?;
9095                            parse_line_child(
9096                                name.as_deref().unwrap_or_default(),
9097                                raw,
9098                                &namespaces,
9099                                &mut grouping,
9100                                &mut marker,
9101                                &mut smooth,
9102                                &mut series,
9103                                &mut data_labels,
9104                                &mut axis_ids,
9105                                &mut raw_children,
9106                                &mut boundary,
9107                            )?;
9108                        }
9109                        event @ (Event::Text(_)
9110                        | Event::CData(_)
9111                        | Event::Comment(_)
9112                        | Event::PI(_)
9113                        | Event::GeneralRef(_)) => {
9114                            raw_children.push(boundary, capture_event(event)?);
9115                        }
9116                        Event::End(end)
9117                            if matches_local_name(end.name().as_ref(), b"lineChart") =>
9118                        {
9119                            let (grouping, grouping_markup) = grouping.ok_or_else(|| {
9120                                ChartError::MissingElement("c:grouping".to_owned())
9121                            })?;
9122                            if axis_ids.len() != 2 {
9123                                return Err(ChartError::InvalidValue {
9124                                    element: "c:lineChart/c:axId".to_owned(),
9125                                    value: format!("expected 2, found {}", axis_ids.len()),
9126                                });
9127                            }
9128                            let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
9129                            let original_series_keys: Vec<(u32, u32)> =
9130                                series.iter().map(|item| (item.index, item.order)).collect();
9131                            let plot = Plot::Line {
9132                                grouping,
9133                                marker: marker.as_ref().is_some_and(|item| item.0),
9134                                smooth: smooth.as_ref().is_some_and(|item| item.0),
9135                                series,
9136                                data_labels,
9137                                axis_ids: parsed_ids,
9138                            };
9139                            plot.validate()?;
9140                            return Ok((
9141                                plot,
9142                                PlotMarkup {
9143                                    raw_attributes,
9144                                    raw_children: raw_children_in_schema_order(
9145                                        &raw_children,
9146                                        original_series_keys.len() + 6,
9147                                    ),
9148                                    direction: None,
9149                                    grouping: Some(grouping_markup),
9150                                    gap_width: None,
9151                                    overlap: None,
9152                                    marker: marker.map(|item| item.1),
9153                                    smooth: smooth.map(|item| item.1),
9154                                    first_slice_angle: None,
9155                                    hole_size: None,
9156                                    style: None,
9157                                    axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
9158                                    original_series_keys,
9159                                    original_axis_ids: parsed_ids.to_vec(),
9160                                    parsed_bar: Some(false),
9161                                    parsed_remaining: None,
9162                                },
9163                            ));
9164                        }
9165                        Event::Eof => return Err(missing_end("c:lineChart")),
9166                        _ => {}
9167                    }
9168                    inner.clear();
9169                }
9170            }
9171            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
9172                return Err(ChartError::MissingElement("c:grouping".to_owned()));
9173            }
9174            Event::Start(element) | Event::Empty(element) => {
9175                return Err(ChartError::UnexpectedElement(element_name(&element)));
9176            }
9177            Event::Eof => return Err(ChartError::MissingElement("c:lineChart".to_owned())),
9178            _ => {}
9179        }
9180        buffer.clear();
9181    }
9182}
9183
9184#[allow(clippy::too_many_arguments)]
9185fn parse_line_child(
9186    name: &[u8],
9187    raw: Vec<u8>,
9188    namespaces: &NamespaceBindings,
9189    grouping: &mut Option<(Grouping, ScalarMarkup)>,
9190    marker: &mut Option<(bool, ScalarMarkup)>,
9191    smooth: &mut Option<(bool, ScalarMarkup)>,
9192    series: &mut Vec<Series>,
9193    data_labels: &mut Option<CT_DLbls>,
9194    axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
9195    raw_children: &mut OrderedRawChildren,
9196    boundary: &mut usize,
9197) -> Result<()> {
9198    match name {
9199        b"grouping" => {
9200            let (value, markup) = required_scalar(&raw, "grouping")?;
9201            let value = Grouping::parse(&value)
9202                .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
9203            set_once(grouping, (value, markup), "c:grouping")?;
9204            *boundary = (*boundary).max(1);
9205        }
9206        b"ser" => {
9207            series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
9208            *boundary = (*boundary).max(series.len() + 1);
9209        }
9210        b"dLbls" => {
9211            set_once(
9212                data_labels,
9213                CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
9214                "c:dLbls",
9215            )?;
9216            *boundary = (*boundary).max(series.len() + 2);
9217        }
9218        b"marker" => {
9219            set_once(marker, parse_bool_value(&raw, "marker")?, "c:marker")?;
9220            *boundary = (*boundary).max(series.len() + 3);
9221        }
9222        b"smooth" => {
9223            set_once(smooth, parse_bool_value(&raw, "smooth")?, "c:smooth")?;
9224            *boundary = (*boundary).max(series.len() + 4);
9225        }
9226        b"axId" => {
9227            if axis_ids.len() == 2 {
9228                return Err(ChartError::DuplicateElement("c:axId".to_owned()));
9229            }
9230            axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
9231            *boundary = (*boundary).max(series.len() + 4 + axis_ids.len());
9232        }
9233        _ => raw_children.push(*boundary, raw),
9234    }
9235    Ok(())
9236}
9237
9238fn required_scalar(xml: &[u8], local: &str) -> Result<(String, ScalarMarkup)> {
9239    let (value, markup) = scalar_value(xml, local)?;
9240    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
9241    Ok((value, markup))
9242}
9243
9244fn write_plot<W: Write>(writer: &mut Writer<W>, plot: &Plot, markup: &PlotMarkup) -> Result<()> {
9245    plot.validate()?;
9246    if let Some(parsed_bar) = markup.parsed_bar
9247        && parsed_bar != matches!(plot, Plot::Bar { .. })
9248    {
9249        return Err(ChartError::InvalidValue {
9250            element: "c:plotArea".to_owned(),
9251            value: "a parsed plot family cannot be replaced while preserved payload remains"
9252                .to_owned(),
9253        });
9254    }
9255    let current_remaining = match plot {
9256        Plot::Pie { .. } => Some("pieChart"),
9257        Plot::Doughnut { .. } => Some("doughnutChart"),
9258        Plot::Area { .. } => Some("areaChart"),
9259        Plot::Scatter { .. } => Some("scatterChart"),
9260        Plot::Radar { .. } => Some("radarChart"),
9261        Plot::Bar { .. } | Plot::Line { .. } => None,
9262    };
9263    if let Some(parsed) = markup.parsed_remaining
9264        && current_remaining != Some(parsed)
9265    {
9266        return Err(ChartError::InvalidValue {
9267            element: "c:plotArea".to_owned(),
9268            value: "a parsed plot family cannot be replaced while preserved payload remains"
9269                .to_owned(),
9270        });
9271    }
9272    match plot {
9273        Plot::Bar {
9274            direction,
9275            grouping,
9276            gap_width,
9277            overlap,
9278            series,
9279            data_labels,
9280            axis_ids,
9281        } => {
9282            let mut start = BytesStart::new("c:barChart");
9283            push_attributes(&mut start, &markup.raw_attributes);
9284            writer
9285                .write_event(Event::Start(start))
9286                .map_err(OxmlError::from)?;
9287            emit_raw(writer, markup.raw_children.at(0))?;
9288            write_scalar(
9289                writer,
9290                "c:barDir",
9291                direction.as_str(),
9292                markup.direction.as_ref(),
9293            )?;
9294            emit_raw(writer, markup.raw_children.at(1))?;
9295            write_scalar(
9296                writer,
9297                "c:grouping",
9298                grouping.as_str(),
9299                markup.grouping.as_ref(),
9300            )?;
9301            let original_to_current =
9302                series_original_to_current(&markup.original_series_keys, series);
9303            emit_repeated_raw(
9304                writer,
9305                &markup.raw_children,
9306                2,
9307                &original_to_current,
9308                series.len(),
9309                0,
9310            )?;
9311            for (index, item) in series.iter().enumerate() {
9312                writer
9313                    .get_mut()
9314                    .write_all(&item.to_xml()?)
9315                    .map_err(OxmlError::from)?;
9316                emit_repeated_raw(
9317                    writer,
9318                    &markup.raw_children,
9319                    2,
9320                    &original_to_current,
9321                    series.len(),
9322                    index + 1,
9323                )?;
9324            }
9325            if let Some(labels) = data_labels {
9326                writer
9327                    .get_mut()
9328                    .write_all(&labels.to_xml()?)
9329                    .map_err(OxmlError::from)?;
9330            }
9331            let trailing = markup.original_series_keys.len();
9332            emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9333            if *gap_width != 150 || markup.gap_width.is_some() {
9334                write_scalar(
9335                    writer,
9336                    "c:gapWidth",
9337                    &gap_width.to_string(),
9338                    markup.gap_width.as_ref(),
9339                )?;
9340            }
9341            emit_raw(writer, markup.raw_children.at(trailing + 4))?;
9342            if *overlap != 0 || markup.overlap.is_some() {
9343                write_scalar(
9344                    writer,
9345                    "c:overlap",
9346                    &overlap.to_string(),
9347                    markup.overlap.as_ref(),
9348                )?;
9349            }
9350            let axis_original_to_current =
9351                axis_original_to_current(&markup.original_axis_ids, axis_ids);
9352            let axis_current_to_original =
9353                invert_original_to_current(&axis_original_to_current, axis_ids.len());
9354            emit_repeated_raw(
9355                writer,
9356                &markup.raw_children,
9357                trailing + 5,
9358                &axis_original_to_current,
9359                axis_ids.len(),
9360                0,
9361            )?;
9362            for (index, id) in axis_ids.iter().enumerate() {
9363                let default_markup = default_axis_id_markup(*id);
9364                let original_markup = axis_current_to_original[index]
9365                    .and_then(|original| markup.axis_ids.get(original));
9366                write_axis_id(
9367                    writer,
9368                    "c:axId",
9369                    *id,
9370                    original_markup.unwrap_or(&default_markup),
9371                )?;
9372                emit_repeated_raw(
9373                    writer,
9374                    &markup.raw_children,
9375                    trailing + 5,
9376                    &axis_original_to_current,
9377                    axis_ids.len(),
9378                    index + 1,
9379                )?;
9380            }
9381            writer
9382                .write_event(Event::End(BytesEnd::new("c:barChart")))
9383                .map_err(OxmlError::from)?;
9384        }
9385        Plot::Line {
9386            grouping,
9387            marker,
9388            smooth,
9389            series,
9390            data_labels,
9391            axis_ids,
9392        } => {
9393            let mut start = BytesStart::new("c:lineChart");
9394            push_attributes(&mut start, &markup.raw_attributes);
9395            writer
9396                .write_event(Event::Start(start))
9397                .map_err(OxmlError::from)?;
9398            emit_raw(writer, markup.raw_children.at(0))?;
9399            write_scalar(
9400                writer,
9401                "c:grouping",
9402                grouping.as_str(),
9403                markup.grouping.as_ref(),
9404            )?;
9405            let original_to_current =
9406                series_original_to_current(&markup.original_series_keys, series);
9407            emit_repeated_raw(
9408                writer,
9409                &markup.raw_children,
9410                1,
9411                &original_to_current,
9412                series.len(),
9413                0,
9414            )?;
9415            for (index, item) in series.iter().enumerate() {
9416                writer
9417                    .get_mut()
9418                    .write_all(&item.to_xml()?)
9419                    .map_err(OxmlError::from)?;
9420                emit_repeated_raw(
9421                    writer,
9422                    &markup.raw_children,
9423                    1,
9424                    &original_to_current,
9425                    series.len(),
9426                    index + 1,
9427                )?;
9428            }
9429            if let Some(labels) = data_labels {
9430                writer
9431                    .get_mut()
9432                    .write_all(&labels.to_xml()?)
9433                    .map_err(OxmlError::from)?;
9434            }
9435            let trailing = markup.original_series_keys.len();
9436            emit_raw(writer, markup.raw_children.at(trailing + 2))?;
9437            if *marker || markup.marker.is_some() {
9438                write_scalar(
9439                    writer,
9440                    "c:marker",
9441                    bool_lexical(*marker),
9442                    markup.marker.as_ref(),
9443                )?;
9444            }
9445            emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9446            if *smooth || markup.smooth.is_some() {
9447                write_scalar(
9448                    writer,
9449                    "c:smooth",
9450                    bool_lexical(*smooth),
9451                    markup.smooth.as_ref(),
9452                )?;
9453            }
9454            let axis_original_to_current =
9455                axis_original_to_current(&markup.original_axis_ids, axis_ids);
9456            let axis_current_to_original =
9457                invert_original_to_current(&axis_original_to_current, axis_ids.len());
9458            emit_repeated_raw(
9459                writer,
9460                &markup.raw_children,
9461                trailing + 4,
9462                &axis_original_to_current,
9463                axis_ids.len(),
9464                0,
9465            )?;
9466            for (index, id) in axis_ids.iter().enumerate() {
9467                let default_markup = default_axis_id_markup(*id);
9468                let original_markup = axis_current_to_original[index]
9469                    .and_then(|original| markup.axis_ids.get(original));
9470                write_axis_id(
9471                    writer,
9472                    "c:axId",
9473                    *id,
9474                    original_markup.unwrap_or(&default_markup),
9475                )?;
9476                emit_repeated_raw(
9477                    writer,
9478                    &markup.raw_children,
9479                    trailing + 4,
9480                    &axis_original_to_current,
9481                    axis_ids.len(),
9482                    index + 1,
9483                )?;
9484            }
9485            writer
9486                .write_event(Event::End(BytesEnd::new("c:lineChart")))
9487                .map_err(OxmlError::from)?;
9488        }
9489        remaining => {
9490            let (
9491                root,
9492                property,
9493                series,
9494                data_labels,
9495                axis_ids,
9496                first_slice_angle,
9497                hole_size,
9498                scatter,
9499            ) = match remaining {
9500                Plot::Pie {
9501                    first_slice_angle,
9502                    series,
9503                    data_labels,
9504                } => (
9505                    "pieChart",
9506                    None,
9507                    series,
9508                    data_labels,
9509                    None,
9510                    Some(*first_slice_angle),
9511                    None,
9512                    false,
9513                ),
9514                Plot::Doughnut {
9515                    first_slice_angle,
9516                    hole_size,
9517                    series,
9518                    data_labels,
9519                } => (
9520                    "doughnutChart",
9521                    None,
9522                    series,
9523                    data_labels,
9524                    None,
9525                    Some(*first_slice_angle),
9526                    Some(*hole_size),
9527                    false,
9528                ),
9529                Plot::Area {
9530                    grouping,
9531                    series,
9532                    data_labels,
9533                    axis_ids,
9534                } => (
9535                    "areaChart",
9536                    Some(("c:grouping", grouping.as_str(), markup.grouping.as_ref())),
9537                    series,
9538                    data_labels,
9539                    Some(axis_ids),
9540                    None,
9541                    None,
9542                    false,
9543                ),
9544                Plot::Scatter {
9545                    style,
9546                    series,
9547                    data_labels,
9548                    axis_ids,
9549                } => (
9550                    "scatterChart",
9551                    Some(("c:scatterStyle", style.as_str(), markup.style.as_ref())),
9552                    series,
9553                    data_labels,
9554                    Some(axis_ids),
9555                    None,
9556                    None,
9557                    true,
9558                ),
9559                Plot::Radar {
9560                    style,
9561                    series,
9562                    data_labels,
9563                    axis_ids,
9564                } => (
9565                    "radarChart",
9566                    Some(("c:radarStyle", style.as_str(), markup.style.as_ref())),
9567                    series,
9568                    data_labels,
9569                    Some(axis_ids),
9570                    None,
9571                    None,
9572                    false,
9573                ),
9574                Plot::Bar { .. } | Plot::Line { .. } => unreachable!("handled above"),
9575            };
9576            let tag = format!("c:{root}");
9577            let mut start = BytesStart::new(&tag);
9578            push_attributes(&mut start, &markup.raw_attributes);
9579            writer
9580                .write_event(Event::Start(start))
9581                .map_err(OxmlError::from)?;
9582            let base = usize::from(property.is_some());
9583            if let Some((name, value, scalar_markup)) = property {
9584                emit_raw(writer, markup.raw_children.at(0))?;
9585                write_scalar(writer, name, value, scalar_markup)?;
9586            }
9587            let original_to_current =
9588                series_original_to_current(&markup.original_series_keys, series);
9589            emit_repeated_raw(
9590                writer,
9591                &markup.raw_children,
9592                base,
9593                &original_to_current,
9594                series.len(),
9595                0,
9596            )?;
9597            for (index, item) in series.iter().enumerate() {
9598                writer
9599                    .get_mut()
9600                    .write_all(&item.to_xml_for_plot(scatter)?)
9601                    .map_err(OxmlError::from)?;
9602                emit_repeated_raw(
9603                    writer,
9604                    &markup.raw_children,
9605                    base,
9606                    &original_to_current,
9607                    series.len(),
9608                    index + 1,
9609                )?;
9610            }
9611            if let Some(labels) = data_labels {
9612                writer
9613                    .get_mut()
9614                    .write_all(&labels.to_xml()?)
9615                    .map_err(OxmlError::from)?;
9616            }
9617            let trailing = base + markup.original_series_keys.len();
9618            if let Some(axis_ids) = axis_ids {
9619                let axis_original_to_current =
9620                    axis_original_to_current(&markup.original_axis_ids, axis_ids);
9621                let axis_current_to_original =
9622                    invert_original_to_current(&axis_original_to_current, axis_ids.len());
9623                emit_repeated_raw(
9624                    writer,
9625                    &markup.raw_children,
9626                    trailing + 1,
9627                    &axis_original_to_current,
9628                    axis_ids.len(),
9629                    0,
9630                )?;
9631                for (index, id) in axis_ids.iter().enumerate() {
9632                    let default_markup = default_axis_id_markup(*id);
9633                    let original_markup = axis_current_to_original[index]
9634                        .and_then(|original| markup.axis_ids.get(original));
9635                    write_axis_id(
9636                        writer,
9637                        "c:axId",
9638                        *id,
9639                        original_markup.unwrap_or(&default_markup),
9640                    )?;
9641                    emit_repeated_raw(
9642                        writer,
9643                        &markup.raw_children,
9644                        trailing + 1,
9645                        &axis_original_to_current,
9646                        axis_ids.len(),
9647                        index + 1,
9648                    )?;
9649                }
9650            } else {
9651                emit_raw(writer, markup.raw_children.at(trailing + 1))?;
9652                if let Some(angle) = first_slice_angle {
9653                    if angle != 0 || markup.first_slice_angle.is_some() {
9654                        write_scalar(
9655                            writer,
9656                            "c:firstSliceAng",
9657                            &angle.to_string(),
9658                            markup.first_slice_angle.as_ref(),
9659                        )?;
9660                    }
9661                    emit_raw(writer, markup.raw_children.at(trailing + 2))?;
9662                }
9663                if let Some(size) = hole_size {
9664                    if size != 50 || markup.hole_size.is_some() {
9665                        write_scalar(
9666                            writer,
9667                            "c:holeSize",
9668                            &size.to_string(),
9669                            markup.hole_size.as_ref(),
9670                        )?;
9671                    }
9672                    emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9673                }
9674            }
9675            writer
9676                .write_event(Event::End(BytesEnd::new(&tag)))
9677                .map_err(OxmlError::from)?;
9678        }
9679    }
9680    Ok(())
9681}
9682
9683fn series_original_to_current(original: &[(u32, u32)], current: &[Series]) -> Vec<Option<usize>> {
9684    let mut used = vec![false; current.len()];
9685    let mut matches = vec![None; original.len()];
9686    for original_index in 0..original.len().min(current.len()) {
9687        if original[original_index]
9688            == (current[original_index].index, current[original_index].order)
9689        {
9690            matches[original_index] = Some(original_index);
9691            used[original_index] = true;
9692        }
9693    }
9694    for (original_index, key) in original.iter().enumerate() {
9695        if matches[original_index].is_some() {
9696            continue;
9697        }
9698        if let Some(current_index) = current
9699            .iter()
9700            .enumerate()
9701            .position(|(index, item)| !used[index] && (item.index, item.order) == *key)
9702        {
9703            matches[original_index] = Some(current_index);
9704            used[current_index] = true;
9705        }
9706    }
9707    match_unidentified_by_position(&mut matches, &mut used);
9708    matches
9709}
9710
9711fn axis_original_to_current(original: &[AxisId], current: &[AxisId]) -> Vec<Option<usize>> {
9712    let mut used = vec![false; current.len()];
9713    let mut matches = vec![None; original.len()];
9714    for (original_index, id) in original.iter().enumerate() {
9715        if let Some(current_index) = current
9716            .iter()
9717            .enumerate()
9718            .position(|(index, current_id)| !used[index] && current_id == id)
9719        {
9720            matches[original_index] = Some(current_index);
9721            used[current_index] = true;
9722        }
9723    }
9724    match_unidentified_by_position(&mut matches, &mut used);
9725    matches
9726}
9727
9728fn match_unidentified_by_position(matches: &mut [Option<usize>], used: &mut [bool]) {
9729    for original_index in 0..matches.len() {
9730        if matches[original_index].is_none() && original_index < used.len() && !used[original_index]
9731        {
9732            matches[original_index] = Some(original_index);
9733            used[original_index] = true;
9734        }
9735    }
9736    for matched in matches.iter_mut().filter(|matched| matched.is_none()) {
9737        if let Some(current_index) = used.iter().position(|used| !used) {
9738            *matched = Some(current_index);
9739            used[current_index] = true;
9740        }
9741    }
9742}
9743
9744fn invert_original_to_current(
9745    original_to_current: &[Option<usize>],
9746    current_len: usize,
9747) -> Vec<Option<usize>> {
9748    let mut current_to_original = vec![None; current_len];
9749    for (original, current) in original_to_current.iter().enumerate() {
9750        if let Some(current) = current {
9751            current_to_original[*current] = Some(original);
9752        }
9753    }
9754    current_to_original
9755}
9756
9757fn emit_repeated_raw<W: Write>(
9758    writer: &mut Writer<W>,
9759    raw_children: &OrderedRawChildren,
9760    offset: usize,
9761    original_to_current: &[Option<usize>],
9762    current_len: usize,
9763    current_boundary: usize,
9764) -> Result<()> {
9765    if current_boundary == 0 {
9766        emit_raw(writer, raw_children.at(offset))?;
9767    }
9768    for original_boundary in 1..=original_to_current.len() {
9769        let effective = original_to_current
9770            .iter()
9771            .skip(original_boundary)
9772            .flatten()
9773            .copied()
9774            .next()
9775            .unwrap_or(current_len);
9776        if effective == current_boundary {
9777            emit_raw(writer, raw_children.at(offset + original_boundary))?;
9778        }
9779    }
9780    Ok(())
9781}
9782
9783fn parse_plot_axis(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Axis>> {
9784    let mut reader = Reader::from_reader(xml);
9785    let mut buffer = Vec::new();
9786    loop {
9787        match reader
9788            .read_event_into(&mut buffer)
9789            .map_err(OxmlError::from)?
9790        {
9791            Event::Start(element) | Event::Empty(element) => {
9792                if !element_is_in_namespace(&element, C_NS, inherited)? {
9793                    return Ok(None);
9794                }
9795                let name = element.name();
9796                if AxisKind::parse(local_name(name.as_ref())).is_none() {
9797                    return Ok(None);
9798                }
9799                return Axis::from_xml_with_namespaces(xml, inherited).map(Some);
9800            }
9801            Event::Eof => return Ok(None),
9802            _ => {}
9803        }
9804        buffer.clear();
9805    }
9806}
9807
9808fn validate_axis_pairs(axes: &[Axis]) -> Result<()> {
9809    for (index, axis) in axes.iter().enumerate() {
9810        if axes[..index].iter().any(|other| other.id == axis.id) {
9811            return Err(ChartError::DuplicateElement(format!(
9812                "c:axId {}",
9813                axis.id.value()
9814            )));
9815        }
9816    }
9817    for axis in axes {
9818        if axis.id == axis.cross_axis {
9819            return Err(ChartError::InvalidValue {
9820                element: "c:crossAx".to_owned(),
9821                value: format!("axis {} crosses itself", axis.id.value()),
9822            });
9823        }
9824        let crossed = axes
9825            .iter()
9826            .find(|candidate| candidate.id == axis.cross_axis)
9827            .ok_or_else(|| ChartError::InvalidValue {
9828                element: "c:crossAx".to_owned(),
9829                value: format!(
9830                    "axis {} references missing axis {}",
9831                    axis.id.value(),
9832                    axis.cross_axis.value()
9833                ),
9834            })?;
9835        if crossed.cross_axis != axis.id {
9836            return Err(ChartError::InvalidValue {
9837                element: "c:crossAx".to_owned(),
9838                value: format!(
9839                    "axis {} references {}, which references {}",
9840                    axis.id.value(),
9841                    crossed.id.value(),
9842                    crossed.cross_axis.value()
9843                ),
9844            });
9845        }
9846    }
9847    Ok(())
9848}
9849
9850fn parse_plot_series(
9851    xml: &[u8],
9852    inherited: &NamespaceBindings,
9853    series: &mut Vec<Series>,
9854) -> Result<()> {
9855    let mut reader = Reader::from_reader(xml);
9856    let mut buffer = Vec::new();
9857    loop {
9858        match reader
9859            .read_event_into(&mut buffer)
9860            .map_err(OxmlError::from)?
9861        {
9862            Event::Start(element) => {
9863                if !element_is_in_namespace(&element, C_NS, inherited)? {
9864                    return Ok(());
9865                }
9866                let local = local_name(element.name().as_ref()).to_vec();
9867                if !is_supported_series_plot(&local) {
9868                    return Ok(());
9869                }
9870                let namespaces = chart_bindings(inherited, &element)?;
9871                let mut inner = Vec::new();
9872                loop {
9873                    match reader
9874                        .read_event_into(&mut inner)
9875                        .map_err(OxmlError::from)?
9876                    {
9877                        Event::Start(child) => {
9878                            let name = chart_child_local(&child, &namespaces)?;
9879                            let raw = capture_element(&mut reader, &child)?;
9880                            if name.as_deref() == Some(b"ser") {
9881                                series.push(Series::from_xml_with_namespaces(&raw, &namespaces)?);
9882                            }
9883                        }
9884                        Event::Empty(child) => {
9885                            let name = chart_child_local(&child, &namespaces)?;
9886                            if name.as_deref() == Some(b"ser") {
9887                                return Err(ChartError::MissingElement("c:ser/c:idx".to_owned()));
9888                            }
9889                        }
9890                        Event::End(end) if matches_local_name(end.name().as_ref(), &local) => {
9891                            return Ok(());
9892                        }
9893                        Event::Eof => {
9894                            return Err(missing_end(&format!(
9895                                "c:{}",
9896                                String::from_utf8_lossy(&local)
9897                            )));
9898                        }
9899                        _ => {}
9900                    }
9901                    inner.clear();
9902                }
9903            }
9904            Event::Empty(_) | Event::Eof => return Ok(()),
9905            _ => {}
9906        }
9907        buffer.clear();
9908    }
9909}
9910
9911fn is_supported_series_plot(local: &[u8]) -> bool {
9912    matches!(
9913        local,
9914        b"areaChart"
9915            | b"area3DChart"
9916            | b"barChart"
9917            | b"bar3DChart"
9918            | b"doughnutChart"
9919            | b"lineChart"
9920            | b"line3DChart"
9921            | b"ofPieChart"
9922            | b"pieChart"
9923            | b"pie3DChart"
9924            | b"radarChart"
9925            | b"stockChart"
9926            | b"surfaceChart"
9927            | b"surface3DChart"
9928    )
9929}
9930
9931impl CT_Legend {
9932    fn from_xml(xml: &[u8]) -> Result<Self> {
9933        let (raw_attributes, raw_children) = parse_raw_shell(xml, b"legend", "c:legend")?;
9934        Ok(Self {
9935            raw_attributes,
9936            raw_children,
9937        })
9938    }
9939
9940    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
9941        write_raw_shell(writer, "c:legend", &self.raw_attributes, &self.raw_children)
9942    }
9943
9944    pub fn raw_children(&self) -> &OrderedRawChildren {
9945        &self.raw_children
9946    }
9947}
9948
9949fn parse_raw_shell(
9950    xml: &[u8],
9951    local: &[u8],
9952    tag: &str,
9953) -> Result<(XmlAttributes, OrderedRawChildren)> {
9954    let mut reader = Reader::from_reader(xml);
9955    let mut buffer = Vec::new();
9956    loop {
9957        match reader
9958            .read_event_into(&mut buffer)
9959            .map_err(OxmlError::from)?
9960        {
9961            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
9962                reject_conflicting_prefix(&element, b"c", C_NS)?;
9963                let raw_attributes = capture_attributes(&element)?;
9964                let mut raw_children = OrderedRawChildren::default();
9965                let mut child_buffer = Vec::new();
9966                loop {
9967                    match reader
9968                        .read_event_into(&mut child_buffer)
9969                        .map_err(OxmlError::from)?
9970                    {
9971                        Event::Start(child) => {
9972                            raw_children.push(0, capture_element(&mut reader, &child)?)
9973                        }
9974                        Event::Empty(child) => raw_children.push(0, capture_empty_element(&child)?),
9975                        event @ (Event::Text(_)
9976                        | Event::CData(_)
9977                        | Event::Comment(_)
9978                        | Event::PI(_)
9979                        | Event::GeneralRef(_)) => {
9980                            raw_children.push(0, capture_event(event)?);
9981                        }
9982                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => break,
9983                        Event::Eof => return Err(missing_end(tag)),
9984                        _ => {}
9985                    }
9986                    child_buffer.clear();
9987                }
9988                return Ok((raw_attributes, raw_children));
9989            }
9990            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
9991                reject_conflicting_prefix(&element, b"c", C_NS)?;
9992                return Ok((capture_attributes(&element)?, OrderedRawChildren::default()));
9993            }
9994            Event::Start(element) | Event::Empty(element) => {
9995                return Err(ChartError::UnexpectedElement(element_name(&element)));
9996            }
9997            Event::Eof => return Err(ChartError::MissingElement(tag.to_owned())),
9998            _ => {}
9999        }
10000        buffer.clear();
10001    }
10002}
10003
10004fn write_raw_shell<W: Write>(
10005    writer: &mut Writer<W>,
10006    tag: &str,
10007    raw_attributes: &XmlAttributes,
10008    raw_children: &OrderedRawChildren,
10009) -> Result<()> {
10010    let mut start = BytesStart::new(tag);
10011    push_attributes(&mut start, raw_attributes);
10012    if raw_children.is_empty() {
10013        writer
10014            .write_event(Event::Empty(start))
10015            .map_err(OxmlError::from)?;
10016        return Ok(());
10017    }
10018    writer
10019        .write_event(Event::Start(start))
10020        .map_err(OxmlError::from)?;
10021    emit_raw(writer, raw_children.at(0))?;
10022    writer
10023        .write_event(Event::End(BytesEnd::new(tag)))
10024        .map_err(OxmlError::from)?;
10025    Ok(())
10026}
10027
10028/// The implemented behavior-bearing core of one `c:chart`.
10029#[derive(Clone, Debug, PartialEq)]
10030pub struct CT_Chart {
10031    pub title: Option<CT_Title>,
10032    pub auto_title_deleted: bool,
10033    pub plot_area: CT_PlotArea,
10034    pub legend: Option<CT_Legend>,
10035    pub plot_vis_only: bool,
10036    pub disp_blanks_as: DispBlanksAs,
10037    auto_title_deleted_markup: Option<ScalarMarkup>,
10038    plot_vis_only_markup: Option<ScalarMarkup>,
10039    disp_blanks_as_markup: Option<ScalarMarkup>,
10040    raw_attributes: Vec<(String, String)>,
10041    raw_children: OrderedRawChildren,
10042}
10043
10044impl CT_Chart {
10045    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
10046        let mut reader = Reader::from_reader(xml);
10047        let mut buffer = Vec::new();
10048        loop {
10049            match reader
10050                .read_event_into(&mut buffer)
10051                .map_err(OxmlError::from)?
10052            {
10053                Event::Start(element) if matches_local_name(element.name().as_ref(), b"chart") => {
10054                    reject_conflicting_prefix(&element, b"c", C_NS)?;
10055                    return Self::from_element(&mut reader, &element, inherited);
10056                }
10057                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"chart") => {
10058                    return Err(ChartError::MissingElement("c:plotArea".to_owned()));
10059                }
10060                Event::Start(element) | Event::Empty(element) => {
10061                    return Err(ChartError::UnexpectedElement(element_name(&element)));
10062                }
10063                Event::Eof => return Err(ChartError::MissingElement("c:chart".to_owned())),
10064                _ => {}
10065            }
10066            buffer.clear();
10067        }
10068    }
10069
10070    fn from_element(
10071        reader: &mut Reader<&[u8]>,
10072        start: &BytesStart<'_>,
10073        inherited: &NamespaceBindings,
10074    ) -> Result<Self> {
10075        let namespaces = bindings_with_local(inherited, start)?;
10076        let raw_attributes = capture_attributes(start)?;
10077        let mut state = ChartParseState::default();
10078        let mut buffer = Vec::new();
10079
10080        loop {
10081            match reader
10082                .read_event_into(&mut buffer)
10083                .map_err(OxmlError::from)?
10084            {
10085                Event::Start(element) => {
10086                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10087                        local_name(element.name().as_ref()).to_vec()
10088                    } else {
10089                        Vec::new()
10090                    };
10091                    let raw = capture_element(reader, &element)?;
10092                    state.parse_child(&name, raw, &namespaces)?;
10093                }
10094                Event::Empty(element) => {
10095                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10096                        local_name(element.name().as_ref()).to_vec()
10097                    } else {
10098                        Vec::new()
10099                    };
10100                    let raw = capture_empty_element(&element)?;
10101                    state.parse_child(&name, raw, &namespaces)?;
10102                }
10103                event @ (Event::Text(_)
10104                | Event::CData(_)
10105                | Event::Comment(_)
10106                | Event::PI(_)
10107                | Event::GeneralRef(_)) => {
10108                    state.capture_event(capture_event(event)?);
10109                }
10110                Event::End(element) if matches_local_name(element.name().as_ref(), b"chart") => {
10111                    break;
10112                }
10113                Event::Eof => return Err(missing_end("c:chart")),
10114                _ => {}
10115            }
10116            buffer.clear();
10117        }
10118
10119        state.finish(raw_attributes)
10120    }
10121
10122    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
10123        let mut start = BytesStart::new("c:chart");
10124        push_attributes(&mut start, &self.raw_attributes);
10125        writer
10126            .write_event(Event::Start(start))
10127            .map_err(OxmlError::from)?;
10128        emit_raw(writer, self.raw_children.at(0))?;
10129        if let Some(title) = &self.title {
10130            title.write_xml(writer)?;
10131        }
10132        emit_raw(writer, self.raw_children.at(1))?;
10133        if self.auto_title_deleted_markup.is_some() || self.auto_title_deleted {
10134            write_scalar(
10135                writer,
10136                "c:autoTitleDeleted",
10137                if self.auto_title_deleted { "1" } else { "0" },
10138                self.auto_title_deleted_markup.as_ref(),
10139            )?;
10140        }
10141        for raw_boundary in 2..8 {
10142            emit_raw(writer, self.raw_children.at(raw_boundary))?;
10143        }
10144        self.plot_area.write_xml(writer)?;
10145        emit_raw(writer, self.raw_children.at(8))?;
10146        if let Some(legend) = &self.legend {
10147            legend.write_xml(writer)?;
10148        }
10149        emit_raw(writer, self.raw_children.at(9))?;
10150        if self.plot_vis_only_markup.is_some() || !self.plot_vis_only {
10151            write_scalar(
10152                writer,
10153                "c:plotVisOnly",
10154                if self.plot_vis_only { "1" } else { "0" },
10155                self.plot_vis_only_markup.as_ref(),
10156            )?;
10157        }
10158        emit_raw(writer, self.raw_children.at(10))?;
10159        if self.disp_blanks_as_markup.is_some() || self.disp_blanks_as != DispBlanksAs::Gap {
10160            write_scalar(
10161                writer,
10162                "c:dispBlanksAs",
10163                self.disp_blanks_as.as_str(),
10164                self.disp_blanks_as_markup.as_ref(),
10165            )?;
10166        }
10167        for raw_boundary in 11..=13 {
10168            emit_raw(writer, self.raw_children.at(raw_boundary))?;
10169        }
10170        writer
10171            .write_event(Event::End(BytesEnd::new("c:chart")))
10172            .map_err(OxmlError::from)?;
10173        Ok(())
10174    }
10175
10176    pub fn raw_children(&self) -> &OrderedRawChildren {
10177        &self.raw_children
10178    }
10179}
10180
10181#[derive(Default)]
10182struct ChartParseState {
10183    title: Option<CT_Title>,
10184    auto_title_deleted: Option<(bool, ScalarMarkup)>,
10185    plot_area: Option<CT_PlotArea>,
10186    legend: Option<CT_Legend>,
10187    plot_vis_only: Option<(bool, ScalarMarkup)>,
10188    disp_blanks_as: Option<(DispBlanksAs, ScalarMarkup)>,
10189    raw_children: OrderedRawChildren,
10190    boundary: usize,
10191}
10192
10193impl ChartParseState {
10194    fn capture_event(&mut self, raw: Vec<u8>) {
10195        self.raw_children.push(self.boundary, raw);
10196    }
10197
10198    fn parse_child(
10199        &mut self,
10200        name: &[u8],
10201        raw: Vec<u8>,
10202        namespaces: &NamespaceBindings,
10203    ) -> Result<()> {
10204        match name {
10205            b"title" => {
10206                set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
10207                self.boundary = self.boundary.max(1);
10208            }
10209            b"autoTitleDeleted" => {
10210                set_once(
10211                    &mut self.auto_title_deleted,
10212                    parse_bool_value(&raw, "autoTitleDeleted")?,
10213                    "c:autoTitleDeleted",
10214                )?;
10215                self.boundary = self.boundary.max(2);
10216            }
10217            b"plotArea" => {
10218                set_once(
10219                    &mut self.plot_area,
10220                    CT_PlotArea::from_xml_with_namespaces(&raw, namespaces)?,
10221                    "c:plotArea",
10222                )?;
10223                self.boundary = self.boundary.max(8);
10224            }
10225            b"legend" => {
10226                set_once(&mut self.legend, CT_Legend::from_xml(&raw)?, "c:legend")?;
10227                self.boundary = self.boundary.max(9);
10228            }
10229            b"plotVisOnly" => {
10230                set_once(
10231                    &mut self.plot_vis_only,
10232                    parse_bool_value(&raw, "plotVisOnly")?,
10233                    "c:plotVisOnly",
10234                )?;
10235                self.boundary = self.boundary.max(10);
10236            }
10237            b"dispBlanksAs" => {
10238                set_once(
10239                    &mut self.disp_blanks_as,
10240                    parse_enum_value(&raw, "dispBlanksAs")?,
10241                    "c:dispBlanksAs",
10242                )?;
10243                self.boundary = self.boundary.max(11);
10244            }
10245            _ => {
10246                self.raw_children
10247                    .push(chart_raw_slot(name).unwrap_or(self.boundary), raw);
10248                self.boundary = self.boundary.max(chart_raw_boundary_after(name));
10249            }
10250        }
10251        Ok(())
10252    }
10253
10254    fn finish(self, raw_attributes: XmlAttributes) -> Result<CT_Chart> {
10255        let (auto_title_deleted, auto_title_deleted_markup) = self
10256            .auto_title_deleted
10257            .map(|(value, markup)| (value, Some(markup)))
10258            .unwrap_or((false, None));
10259        let (plot_vis_only, plot_vis_only_markup) = self
10260            .plot_vis_only
10261            .map(|(value, markup)| (value, Some(markup)))
10262            .unwrap_or((true, None));
10263        let (disp_blanks_as, disp_blanks_as_markup) = self
10264            .disp_blanks_as
10265            .map(|(value, markup)| (value, Some(markup)))
10266            .unwrap_or((DispBlanksAs::Gap, None));
10267        Ok(CT_Chart {
10268            title: self.title,
10269            auto_title_deleted,
10270            plot_area: self
10271                .plot_area
10272                .ok_or_else(|| ChartError::MissingElement("c:plotArea".to_owned()))?,
10273            legend: self.legend,
10274            plot_vis_only,
10275            disp_blanks_as,
10276            auto_title_deleted_markup,
10277            plot_vis_only_markup,
10278            disp_blanks_as_markup,
10279            raw_attributes,
10280            raw_children: raw_children_in_schema_order(&self.raw_children, 13),
10281        })
10282    }
10283}
10284
10285fn chart_raw_slot(name: &[u8]) -> Option<usize> {
10286    match name {
10287        b"pivotFmts" => Some(2),
10288        b"view3D" => Some(3),
10289        b"floor" => Some(4),
10290        b"sideWall" => Some(5),
10291        b"backWall" => Some(6),
10292        // Both unmodelled tail children share a slot so producer sibling order
10293        // remains byte-stable until a later story types either element.
10294        b"showDLblsOverMax" | b"extLst" => Some(11),
10295        _ => None,
10296    }
10297}
10298
10299fn chart_raw_boundary_after(name: &[u8]) -> usize {
10300    match name {
10301        b"title" => 1,
10302        b"autoTitleDeleted" => 2,
10303        b"pivotFmts" => 3,
10304        b"view3D" => 4,
10305        b"floor" => 5,
10306        b"sideWall" => 6,
10307        b"backWall" => 7,
10308        b"plotArea" => 8,
10309        b"legend" => 9,
10310        b"plotVisOnly" => 10,
10311        b"dispBlanksAs" => 11,
10312        b"showDLblsOverMax" | b"extLst" => 11,
10313        _ => 0,
10314    }
10315}
10316
10317/// One complete `c:chartSpace` part with the F-118 core typed.
10318#[derive(Clone, Debug, PartialEq)]
10319pub struct CT_ChartSpace {
10320    pub chart: CT_Chart,
10321    pub sp_pr: Option<CT_ShapeProperties>,
10322    pub tx_pr: Option<CT_TextBody>,
10323    raw_attributes: Vec<(String, String)>,
10324    namespace_declarations: Vec<(String, String)>,
10325    raw_children: OrderedRawChildren,
10326}
10327
10328impl CT_ChartSpace {
10329    /// Parses one complete ChartML chart-space part.
10330    pub fn from_xml(xml: &[u8]) -> Result<Self> {
10331        let mut reader = Reader::from_reader(xml);
10332        let mut buffer = Vec::new();
10333        loop {
10334            match reader
10335                .read_event_into(&mut buffer)
10336                .map_err(OxmlError::from)?
10337            {
10338                Event::Start(element)
10339                    if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10340                {
10341                    validate_chart_space_namespace(&element)?;
10342                    return Self::from_element(&mut reader, &element);
10343                }
10344                Event::Empty(element)
10345                    if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10346                {
10347                    validate_chart_space_namespace(&element)?;
10348                    return Err(ChartError::MissingElement("c:chart".to_owned()));
10349                }
10350                Event::Start(element) | Event::Empty(element) => {
10351                    return Err(ChartError::UnexpectedElement(element_name(&element)));
10352                }
10353                Event::Eof => return Err(ChartError::MissingElement("c:chartSpace".to_owned())),
10354                _ => {}
10355            }
10356            buffer.clear();
10357        }
10358    }
10359
10360    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
10361        let namespaces = namespace_bindings(start)?;
10362        let (raw_attributes, namespace_declarations) = capture_root_attributes(start)?;
10363        let mut state = ChartSpaceParseState::default();
10364        let mut buffer = Vec::new();
10365
10366        loop {
10367            match reader
10368                .read_event_into(&mut buffer)
10369                .map_err(OxmlError::from)?
10370            {
10371                Event::Start(element) => {
10372                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10373                        local_name(element.name().as_ref()).to_vec()
10374                    } else {
10375                        Vec::new()
10376                    };
10377                    let raw = capture_element(reader, &element)?;
10378                    state.parse_child(&name, raw, &namespaces)?;
10379                }
10380                Event::Empty(element) => {
10381                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
10382                        local_name(element.name().as_ref()).to_vec()
10383                    } else {
10384                        Vec::new()
10385                    };
10386                    let raw = capture_empty_element(&element)?;
10387                    state.parse_child(&name, raw, &namespaces)?;
10388                }
10389                event @ (Event::Text(_)
10390                | Event::CData(_)
10391                | Event::Comment(_)
10392                | Event::PI(_)
10393                | Event::GeneralRef(_)) => {
10394                    state.capture_event(capture_event(event)?);
10395                }
10396                Event::End(element)
10397                    if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10398                {
10399                    break;
10400                }
10401                Event::Eof => return Err(missing_end("c:chartSpace")),
10402                _ => {}
10403            }
10404            buffer.clear();
10405        }
10406
10407        state.finish(raw_attributes, namespace_declarations)
10408    }
10409
10410    /// Writes one chart-space part using fixed OOXML prefixes and schema order.
10411    pub fn to_xml(&self) -> Result<Vec<u8>> {
10412        let mut writer = Writer::new(Vec::new());
10413        let mut start = BytesStart::new("c:chartSpace");
10414        start.push_attribute(("xmlns:c", C_NS));
10415        start.push_attribute(("xmlns:a", A_NS));
10416        start.push_attribute(("xmlns:r", R_NS));
10417        push_attributes(&mut start, &self.namespace_declarations);
10418        push_attributes(&mut start, &self.raw_attributes);
10419        writer
10420            .write_event(Event::Start(start))
10421            .map_err(OxmlError::from)?;
10422        for raw_boundary in 0..8 {
10423            emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
10424        }
10425        self.chart.write_xml(&mut writer)?;
10426        emit_raw(&mut writer, self.raw_children.at(8))?;
10427        if let Some(properties) = &self.sp_pr {
10428            properties.write_xml_as(&mut writer, "c:spPr")?;
10429        }
10430        emit_raw(&mut writer, self.raw_children.at(9))?;
10431        if let Some(text) = &self.tx_pr {
10432            text.write_xml_as(&mut writer, "c:txPr")?;
10433        }
10434        for raw_boundary in 10..=14 {
10435            emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
10436        }
10437        writer
10438            .write_event(Event::End(BytesEnd::new("c:chartSpace")))
10439            .map_err(OxmlError::from)?;
10440        Ok(writer.into_inner())
10441    }
10442
10443    pub fn raw_children(&self) -> &OrderedRawChildren {
10444        &self.raw_children
10445    }
10446}
10447
10448#[derive(Default)]
10449struct ChartSpaceParseState {
10450    chart: Option<CT_Chart>,
10451    sp_pr: Option<CT_ShapeProperties>,
10452    tx_pr: Option<CT_TextBody>,
10453    raw_children: OrderedRawChildren,
10454    boundary: usize,
10455}
10456
10457impl ChartSpaceParseState {
10458    fn capture_event(&mut self, raw: Vec<u8>) {
10459        self.raw_children.push(self.boundary, raw);
10460    }
10461
10462    fn parse_child(
10463        &mut self,
10464        name: &[u8],
10465        raw: Vec<u8>,
10466        namespaces: &NamespaceBindings,
10467    ) -> Result<()> {
10468        match name {
10469            b"chart" => {
10470                set_once(
10471                    &mut self.chart,
10472                    CT_Chart::from_xml_with_namespaces(&raw, namespaces)?,
10473                    "c:chart",
10474                )?;
10475                self.boundary = self.boundary.max(8);
10476            }
10477            b"spPr" => {
10478                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
10479                let properties = CT_ShapeProperties::from_xml(&raw)?;
10480                let mut writer = Writer::new(Vec::new());
10481                properties.write_xml_as(&mut writer, "c:spPr")?;
10482                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
10483                set_once(&mut self.sp_pr, properties, "c:spPr")?;
10484                self.boundary = self.boundary.max(9);
10485            }
10486            b"txPr" => {
10487                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
10488                let text = CT_TextBody::from_xml_as(&raw, b"txPr")?;
10489                let mut writer = Writer::new(Vec::new());
10490                text.write_xml_as(&mut writer, "c:txPr")?;
10491                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
10492                set_once(&mut self.tx_pr, text, "c:txPr")?;
10493                self.boundary = self.boundary.max(10);
10494            }
10495            _ => {
10496                self.raw_children
10497                    .push(chart_space_raw_slot(name).unwrap_or(self.boundary), raw);
10498                self.boundary = self.boundary.max(chart_space_raw_boundary_after(name));
10499            }
10500        }
10501        Ok(())
10502    }
10503
10504    fn finish(
10505        self,
10506        raw_attributes: XmlAttributes,
10507        namespace_declarations: XmlAttributes,
10508    ) -> Result<CT_ChartSpace> {
10509        Ok(CT_ChartSpace {
10510            chart: self
10511                .chart
10512                .ok_or_else(|| ChartError::MissingElement("c:chart".to_owned()))?,
10513            sp_pr: self.sp_pr,
10514            tx_pr: self.tx_pr,
10515            raw_attributes,
10516            namespace_declarations,
10517            raw_children: raw_children_in_schema_order(&self.raw_children, 14),
10518        })
10519    }
10520}
10521
10522fn raw_children_in_schema_order(
10523    raw_children: &OrderedRawChildren,
10524    final_boundary: usize,
10525) -> OrderedRawChildren {
10526    let mut ordered = OrderedRawChildren::default();
10527    for boundary in 0..=final_boundary {
10528        for raw in raw_children.at(boundary) {
10529            ordered.push(boundary, raw.to_vec());
10530        }
10531    }
10532    ordered
10533}
10534
10535fn chart_space_raw_slot(name: &[u8]) -> Option<usize> {
10536    match name {
10537        b"date1904" => Some(0),
10538        b"lang" => Some(1),
10539        b"roundedCorners" => Some(2),
10540        b"style" => Some(3),
10541        b"clrMapOvr" => Some(4),
10542        b"pivotSource" => Some(5),
10543        b"protection" => Some(6),
10544        b"externalData" => Some(10),
10545        b"printSettings" => Some(11),
10546        b"userShapes" => Some(12),
10547        b"extLst" => Some(13),
10548        _ => None,
10549    }
10550}
10551
10552fn chart_space_raw_boundary_after(name: &[u8]) -> usize {
10553    match name {
10554        b"date1904" => 1,
10555        b"lang" => 2,
10556        b"roundedCorners" => 3,
10557        b"style" => 4,
10558        b"clrMapOvr" => 5,
10559        b"pivotSource" => 6,
10560        b"protection" => 7,
10561        b"chart" => 8,
10562        b"spPr" => 9,
10563        b"txPr" => 10,
10564        b"externalData" => 11,
10565        b"printSettings" => 12,
10566        b"userShapes" => 13,
10567        b"extLst" => 14,
10568        _ => 0,
10569    }
10570}
10571
10572fn parse_bool_value(xml: &[u8], local: &str) -> Result<(bool, ScalarMarkup)> {
10573    let (value, markup) = scalar_value(xml, local)?;
10574    let value = match value.as_deref().unwrap_or("1") {
10575        "1" | "true" => true,
10576        "0" | "false" => false,
10577        invalid => return Err(invalid_attribute(local, "val", invalid.to_owned())),
10578    };
10579    Ok((value, markup))
10580}
10581
10582fn parse_enum_value(xml: &[u8], local: &str) -> Result<(DispBlanksAs, ScalarMarkup)> {
10583    let (value, markup) = scalar_value(xml, local)?;
10584    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
10585    let parsed =
10586        DispBlanksAs::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
10587    Ok((parsed, markup))
10588}
10589
10590fn scalar_value(xml: &[u8], expected: &str) -> Result<(Option<String>, ScalarMarkup)> {
10591    let mut reader = Reader::from_reader(xml);
10592    let mut buffer = Vec::new();
10593    loop {
10594        match reader
10595            .read_event_into(&mut buffer)
10596            .map_err(OxmlError::from)?
10597        {
10598            Event::Empty(element)
10599                if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
10600            {
10601                reject_conflicting_prefix(&element, b"c", C_NS)?;
10602                return Ok((
10603                    attribute_value(&element, b"val")?,
10604                    ScalarMarkup {
10605                        raw_attributes: capture_attributes_except(&element, b"val")?,
10606                        raw_content: Vec::new(),
10607                    },
10608                ));
10609            }
10610            Event::Start(element)
10611                if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
10612            {
10613                reject_conflicting_prefix(&element, b"c", C_NS)?;
10614                let value = attribute_value(&element, b"val")?;
10615                let raw_attributes = capture_attributes_except(&element, b"val")?;
10616                let mut raw_content = Vec::new();
10617                let mut inner = Vec::new();
10618                loop {
10619                    match reader
10620                        .read_event_into(&mut inner)
10621                        .map_err(OxmlError::from)?
10622                    {
10623                        Event::End(end)
10624                            if matches_local_name(end.name().as_ref(), expected.as_bytes()) =>
10625                        {
10626                            return Ok((
10627                                value,
10628                                ScalarMarkup {
10629                                    raw_attributes,
10630                                    raw_content,
10631                                },
10632                            ));
10633                        }
10634                        event @ (Event::Text(_)
10635                        | Event::CData(_)
10636                        | Event::Comment(_)
10637                        | Event::PI(_)
10638                        | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
10639                        Event::Eof => return Err(missing_end(expected)),
10640                        _ => return Err(ChartError::UnexpectedElement(expected.to_owned())),
10641                    }
10642                    inner.clear();
10643                }
10644            }
10645            Event::Start(element) | Event::Empty(element) => {
10646                return Err(ChartError::UnexpectedElement(element_name(&element)));
10647            }
10648            Event::Eof => return Err(ChartError::MissingElement(expected.to_owned())),
10649            _ => {}
10650        }
10651        buffer.clear();
10652    }
10653}
10654
10655fn set_once<T>(slot: &mut Option<T>, value: T, name: &str) -> Result<()> {
10656    if slot.is_some() {
10657        return Err(ChartError::DuplicateElement(name.to_owned()));
10658    }
10659    *slot = Some(value);
10660    Ok(())
10661}
10662
10663fn mark_once(seen: &mut bool, name: &str) -> Result<()> {
10664    if *seen {
10665        return Err(ChartError::DuplicateElement(name.to_owned()));
10666    }
10667    *seen = true;
10668    Ok(())
10669}
10670
10671fn validate_chart_space_namespace(element: &BytesStart<'_>) -> Result<()> {
10672    let name = element.name();
10673    let qualified = name.as_ref();
10674    let declaration = match qualified.iter().position(|byte| *byte == b':') {
10675        Some(position) => {
10676            let mut declaration = b"xmlns:".to_vec();
10677            declaration.extend_from_slice(&qualified[..position]);
10678            declaration
10679        }
10680        None => b"xmlns".to_vec(),
10681    };
10682    let namespace = attribute_value(element, &declaration)?
10683        .ok_or_else(|| invalid_attribute("chartSpace", "namespace", "<missing>".to_owned()))?;
10684    if namespace != C_NS {
10685        return Err(invalid_attribute("chartSpace", "namespace", namespace));
10686    }
10687    reject_conflicting_prefix(element, b"c", C_NS)?;
10688    reject_conflicting_prefix(element, b"a", A_NS)?;
10689    reject_conflicting_prefix(element, b"r", R_NS)
10690}
10691
10692fn reject_conflicting_prefix_in_xml(xml: &[u8], prefix: &[u8], namespace: &str) -> Result<()> {
10693    let mut reader = Reader::from_reader(xml);
10694    let mut buffer = Vec::new();
10695    loop {
10696        match reader
10697            .read_event_into(&mut buffer)
10698            .map_err(OxmlError::from)?
10699        {
10700            Event::Start(element) | Event::Empty(element) => {
10701                return reject_conflicting_prefix(&element, prefix, namespace);
10702            }
10703            Event::Eof => return Err(ChartError::MissingElement("XML root".to_owned())),
10704            _ => {}
10705        }
10706        buffer.clear();
10707    }
10708}
10709
10710fn reject_rewritten_foreign_elements(
10711    original: &[u8],
10712    written: &[u8],
10713    inherited: &NamespaceBindings,
10714    root_local: &[u8],
10715) -> Result<()> {
10716    let original = foreign_element_regions(original, inherited, root_local)?;
10717    let mut written_namespaces = inherited.clone();
10718    written_namespaces.retain(|(prefix, _)| prefix != b"a");
10719    written_namespaces.push((b"a".to_vec(), A_NS.to_owned()));
10720    let written = foreign_element_regions(written, &written_namespaces, root_local)?;
10721    if original != written {
10722        return Err(invalid_attribute(
10723            &String::from_utf8_lossy(root_local),
10724            "namespace",
10725            "foreign DrawingML content would be converted to typed content".to_owned(),
10726        ));
10727    }
10728    Ok(())
10729}
10730
10731fn foreign_element_regions(
10732    xml: &[u8],
10733    inherited: &NamespaceBindings,
10734    root_local: &[u8],
10735) -> Result<Vec<Vec<u8>>> {
10736    let mut reader = Reader::from_reader(xml);
10737    let mut buffer = Vec::new();
10738    let root_namespaces = loop {
10739        match reader
10740            .read_event_into(&mut buffer)
10741            .map_err(OxmlError::from)?
10742        {
10743            Event::Start(element) if matches_local_name(element.name().as_ref(), root_local) => {
10744                break bindings_with_local(inherited, &element)?;
10745            }
10746            Event::Empty(element) if matches_local_name(element.name().as_ref(), root_local) => {
10747                return Ok(Vec::new());
10748            }
10749            Event::Start(element) | Event::Empty(element) => {
10750                return Err(ChartError::UnexpectedElement(element_name(&element)));
10751            }
10752            Event::Eof => {
10753                return Err(ChartError::MissingElement(
10754                    String::from_utf8_lossy(root_local).into_owned(),
10755                ));
10756            }
10757            _ => {}
10758        }
10759        buffer.clear();
10760    };
10761
10762    let mut namespace_stack = vec![root_namespaces];
10763    let mut foreign = Vec::new();
10764    let mut child_buffer = Vec::new();
10765    loop {
10766        match reader
10767            .read_event_into(&mut child_buffer)
10768            .map_err(OxmlError::from)?
10769        {
10770            Event::Start(element) => {
10771                let namespaces = namespace_stack.last().expect("root namespace binding");
10772                if element_is_in_namespace(&element, A_NS, namespaces)? {
10773                    namespace_stack.push(bindings_with_local(namespaces, &element)?);
10774                } else {
10775                    foreign.push(capture_element(&mut reader, &element)?);
10776                }
10777            }
10778            Event::Empty(element) => {
10779                let namespaces = namespace_stack.last().expect("root namespace binding");
10780                if !element_is_in_namespace(&element, A_NS, namespaces)? {
10781                    foreign.push(capture_empty_element(&element)?);
10782                }
10783            }
10784            Event::End(element) if matches_local_name(element.name().as_ref(), root_local) => {
10785                return Ok(foreign);
10786            }
10787            Event::End(_) => {
10788                namespace_stack.pop();
10789            }
10790            Event::Eof => {
10791                return Err(missing_end(&String::from_utf8_lossy(root_local)));
10792            }
10793            _ => {}
10794        }
10795        child_buffer.clear();
10796    }
10797}
10798
10799fn reject_conflicting_prefix(
10800    element: &BytesStart<'_>,
10801    prefix: &[u8],
10802    namespace: &str,
10803) -> Result<()> {
10804    let mut key = b"xmlns:".to_vec();
10805    key.extend_from_slice(prefix);
10806    if let Some(value) = attribute_value(element, &key)?
10807        && value != namespace
10808    {
10809        return Err(invalid_attribute(
10810            &element_name(element),
10811            &String::from_utf8_lossy(&key),
10812            value,
10813        ));
10814    }
10815    Ok(())
10816}
10817
10818fn namespace_bindings(element: &BytesStart<'_>) -> Result<NamespaceBindings> {
10819    let mut bindings = Vec::new();
10820    for attribute in element.attributes() {
10821        let attribute = attribute.map_err(OxmlError::from)?;
10822        let key = attribute.key.as_ref();
10823        let prefix = if key == b"xmlns" {
10824            Vec::new()
10825        } else if let Some(prefix) = key.strip_prefix(b"xmlns:") {
10826            prefix.to_vec()
10827        } else {
10828            continue;
10829        };
10830        let namespace = attribute
10831            .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
10832            .map_err(OxmlError::from)?
10833            .into_owned();
10834        bindings.push((prefix, namespace));
10835    }
10836    Ok(bindings)
10837}
10838
10839fn bindings_with_local(
10840    inherited: &NamespaceBindings,
10841    element: &BytesStart<'_>,
10842) -> Result<NamespaceBindings> {
10843    let mut bindings = inherited.clone();
10844    for (prefix, namespace) in namespace_bindings(element)? {
10845        upsert_namespace_binding(&mut bindings, prefix, namespace);
10846    }
10847    Ok(bindings)
10848}
10849
10850fn upsert_namespace_binding(bindings: &mut NamespaceBindings, prefix: Vec<u8>, namespace: String) {
10851    if let Some((_, current)) = bindings.iter_mut().find(|(current, _)| current == &prefix) {
10852        *current = namespace;
10853    } else {
10854        bindings.push((prefix, namespace));
10855    }
10856}
10857
10858fn element_is_in_namespace(
10859    element: &BytesStart<'_>,
10860    expected: &str,
10861    inherited: &NamespaceBindings,
10862) -> Result<bool> {
10863    let name = element.name();
10864    let qualified = name.as_ref();
10865    let prefix = qualified
10866        .iter()
10867        .position(|byte| *byte == b':')
10868        .map(|position| &qualified[..position])
10869        .unwrap_or_default();
10870    let local = namespace_bindings(element)?;
10871    let namespace = local
10872        .iter()
10873        .rev()
10874        .chain(inherited.iter().rev())
10875        .find(|(candidate, _)| candidate.as_slice() == prefix)
10876        .map(|(_, namespace)| namespace.as_str());
10877    Ok(namespace == Some(expected))
10878}
10879
10880fn attribute_value(element: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
10881    for attribute in element.attributes() {
10882        let attribute = attribute.map_err(OxmlError::from)?;
10883        if attribute.key.as_ref() == name {
10884            return Ok(Some(
10885                attribute
10886                    .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
10887                    .map_err(OxmlError::from)?
10888                    .into_owned(),
10889            ));
10890        }
10891    }
10892    Ok(None)
10893}
10894
10895fn capture_root_attributes(start: &BytesStart<'_>) -> Result<RootAttributes> {
10896    let mut attributes = Vec::new();
10897    let mut namespaces = Vec::new();
10898    for attribute in start.attributes() {
10899        let attribute = attribute.map_err(OxmlError::from)?;
10900        let name = std::str::from_utf8(attribute.key.as_ref())
10901            .map_err(OxmlError::from)?
10902            .to_owned();
10903        let value = attribute
10904            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10905            .map_err(OxmlError::from)?
10906            .into_owned();
10907        if name == "xmlns:c" || name == "xmlns:a" || name == "xmlns:r" {
10908            continue;
10909        }
10910        if name == "xmlns" || name.starts_with("xmlns:") {
10911            namespaces.push((name, value));
10912        } else {
10913            attributes.push((name, value));
10914        }
10915    }
10916    Ok((attributes, namespaces))
10917}
10918
10919fn capture_attributes(start: &BytesStart<'_>) -> Result<XmlAttributes> {
10920    let mut attributes = Vec::new();
10921    for attribute in start.attributes() {
10922        let attribute = attribute.map_err(OxmlError::from)?;
10923        attributes.push((
10924            std::str::from_utf8(attribute.key.as_ref())
10925                .map_err(OxmlError::from)?
10926                .to_owned(),
10927            attribute
10928                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10929                .map_err(OxmlError::from)?
10930                .into_owned(),
10931        ));
10932    }
10933    Ok(attributes)
10934}
10935
10936fn capture_attributes_except(start: &BytesStart<'_>, excluded: &[u8]) -> Result<XmlAttributes> {
10937    let mut attributes = Vec::new();
10938    for attribute in start.attributes() {
10939        let attribute = attribute.map_err(OxmlError::from)?;
10940        if attribute.key.as_ref() == excluded {
10941            continue;
10942        }
10943        attributes.push((
10944            std::str::from_utf8(attribute.key.as_ref())
10945                .map_err(OxmlError::from)?
10946                .to_owned(),
10947            attribute
10948                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10949                .map_err(OxmlError::from)?
10950                .into_owned(),
10951        ));
10952    }
10953    Ok(attributes)
10954}
10955
10956fn capture_attributes_excluding(
10957    start: &BytesStart<'_>,
10958    excluded: &[&[u8]],
10959) -> Result<XmlAttributes> {
10960    let mut attributes = Vec::new();
10961    for attribute in start.attributes() {
10962        let attribute = attribute.map_err(OxmlError::from)?;
10963        if excluded.contains(&attribute.key.as_ref()) {
10964            continue;
10965        }
10966        attributes.push((
10967            std::str::from_utf8(attribute.key.as_ref())
10968                .map_err(OxmlError::from)?
10969                .to_owned(),
10970            attribute
10971                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10972                .map_err(OxmlError::from)?
10973                .into_owned(),
10974        ));
10975    }
10976    Ok(attributes)
10977}
10978
10979fn push_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
10980    for (name, value) in attributes {
10981        start.push_attribute((name.as_str(), value.as_str()));
10982    }
10983}
10984
10985fn write_scalar<W: Write>(
10986    writer: &mut Writer<W>,
10987    name: &str,
10988    value: &str,
10989    markup: Option<&ScalarMarkup>,
10990) -> Result<()> {
10991    let mut start = BytesStart::new(name);
10992    start.push_attribute(("val", value));
10993    if let Some(markup) = markup {
10994        push_attributes(&mut start, &markup.raw_attributes);
10995    }
10996    let has_content = markup.is_some_and(|markup| !markup.raw_content.is_empty());
10997    if !has_content {
10998        writer
10999            .write_event(Event::Empty(start))
11000            .map_err(OxmlError::from)?;
11001        return Ok(());
11002    }
11003    writer
11004        .write_event(Event::Start(start))
11005        .map_err(OxmlError::from)?;
11006    if let Some(markup) = markup {
11007        for raw in &markup.raw_content {
11008            writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
11009        }
11010    }
11011    writer
11012        .write_event(Event::End(BytesEnd::new(name)))
11013        .map_err(OxmlError::from)?;
11014    Ok(())
11015}
11016
11017fn capture_event(event: Event<'_>) -> Result<Vec<u8>> {
11018    let mut writer = Writer::new(Vec::new());
11019    writer
11020        .write_event(event.into_owned())
11021        .map_err(OxmlError::from)?;
11022    Ok(writer.into_inner())
11023}
11024
11025fn emit_raw<'a, W: Write>(
11026    writer: &mut Writer<W>,
11027    children: impl Iterator<Item = &'a [u8]>,
11028) -> Result<()> {
11029    for child in children {
11030        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
11031    }
11032    Ok(())
11033}
11034
11035fn invalid_attribute(element: &str, attribute: &str, value: String) -> ChartError {
11036    ChartError::InvalidAttribute {
11037        element: element.to_owned(),
11038        attribute: attribute.to_owned(),
11039        value,
11040    }
11041}
11042
11043fn element_name(element: &BytesStart<'_>) -> String {
11044    String::from_utf8_lossy(element.name().as_ref()).into_owned()
11045}
11046
11047fn missing_end(element: &str) -> ChartError {
11048    ChartError::Xml(OxmlError::MissingElement(format!("closing {element}")))
11049}
11050
11051#[cfg(test)]
11052mod tests {
11053    use std::collections::HashSet;
11054    use std::fs;
11055    use std::path::{Path, PathBuf};
11056    use std::process::Command;
11057
11058    use oxml_core::raw_xml::{capture_element, capture_empty_element};
11059    use oxml_drawing::color::{ColorMap, ColorMapSlot, RgbColor, ThemeColorSlot};
11060    use oxml_drawing::theme::CT_OfficeStyleSheet;
11061    use oxml_layout::{
11062        Color, FontManager, LayoutResult, PageFrame, Paint, PathCommand, Point, PositionedElement,
11063        Rect,
11064    };
11065    use oxml_opc::OpcPackage;
11066    use quick_xml::Reader;
11067    use quick_xml::events::Event;
11068
11069    use super::{
11070        A_NS, Axis, AxisData, AxisId, AxisKind, AxisPosition, BarDirection, BarGrouping, C_NS,
11071        CT_ChartSpace, CT_DLbls, CT_ShapeProperties, CT_TextBody, ChartData, ChartGeometry,
11072        ChartKind, DataLabelPosition, DispBlanksAs, Domain, Grouping, NumberFormat, NumericData,
11073        Orientation, Plot, R_NS, ScatterStyle, Series, StringRef, TickLabelPosition, TickMark,
11074        authored_chart_parts, capture_event, local_name, matches_local_name, nice_number_scale,
11075        render_chart as render_chart_with_theme, render_geometry as render_geometry_with_theme,
11076    };
11077
11078    const MANIFEST: &str = include_str!("../../../scripts/pptx-corpus-manifest.tsv");
11079    const EXPECTED_DECKS: usize = 50;
11080    const LIBREOFFICE_VERSION: &str =
11081        "LibreOffice 26.2.5.2 cd7284b4cbbfeb507e630c1aac019f4157393acb";
11082    const PDFTOTEXT_VERSION: &str = "pdftotext version 26.01.0";
11083    const PDFTOPPM_VERSION: &str = "pdftoppm version 26.01.0";
11084    const PLOT_RENDER_NORMALIZED_MAE_THRESHOLD: f64 = 0.0;
11085
11086    #[test]
11087    fn authored_charts_emit_portable_viewer_defaults() {
11088        let data = ChartData {
11089            categories: vec!["August".to_owned(), "September".to_owned()],
11090            series: vec![("Price".to_owned(), vec![12.5, 19.2])],
11091            number_format: Some(r"0.##\%".to_owned()),
11092            category_axis_title: Some("Month".to_owned()),
11093            value_axis_title: Some("Change".to_owned()),
11094            palette: vec![
11095                RgbColor::parse("2B6FE3").unwrap(),
11096                RgbColor::parse("F0761F").unwrap(),
11097            ],
11098        };
11099        let (line, line_workbook) = authored_chart_parts(ChartKind::Line, &data, "rId1").unwrap();
11100        let line = std::str::from_utf8(&line).unwrap();
11101        assert_eq!(line.matches(r#"<c:delete val="0"/>"#).count(), 2);
11102        assert!(line.contains(r#"<c:numFmt formatCode="0.##\%" sourceLinked="0"/>"#));
11103        assert!(line.contains("<a:t>Month</a:t>"));
11104        assert!(line.contains("<a:t>Change</a:t>"));
11105        assert!(line.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
11106        let assert_portable_line = |xml: &str| {
11107            let titles = xml
11108                .split("<c:title>")
11109                .skip(1)
11110                .map(|tail| tail.split_once("</c:title>").unwrap().0)
11111                .collect::<Vec<_>>();
11112            assert_eq!(titles.len(), 2);
11113            for title in titles {
11114                let text = title.find("<c:tx>").unwrap();
11115                let layout = title.find("<c:layout/>").unwrap();
11116                let overlay = title.find(r#"<c:overlay val="0"/>"#).unwrap();
11117                assert!(text < layout && layout < overlay);
11118            }
11119
11120            let line_plot = xml
11121                .split_once("<c:lineChart>")
11122                .unwrap()
11123                .1
11124                .split_once("</c:lineChart>")
11125                .unwrap()
11126                .0;
11127            assert_eq!(line_plot.matches(r#"<c:marker val="0"/>"#).count(), 1);
11128            assert_eq!(line_plot.matches(r#"<c:smooth val="0"/>"#).count(), 1);
11129            let marker = line_plot.find(r#"<c:marker val="0"/>"#).unwrap();
11130            let smooth = line_plot.find(r#"<c:smooth val="0"/>"#).unwrap();
11131            let axis_id = line_plot.find("<c:axId").unwrap();
11132            assert!(marker < smooth && smooth < axis_id);
11133        };
11134        assert_portable_line(line);
11135        for axis in line.split("<c:crossAx").take(2) {
11136            let scaling = axis.rfind("<c:scaling").unwrap();
11137            let deleted = axis.rfind("<c:delete").unwrap();
11138            let position = axis.rfind("<c:axPos").unwrap();
11139            assert!(scaling < deleted && deleted < position);
11140        }
11141
11142        let parsed = CT_ChartSpace::from_xml(line.as_bytes()).unwrap();
11143        let rewritten = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
11144        assert_portable_line(&rewritten);
11145        assert!(rewritten.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
11146        assert!(rewritten.contains(r#"<c:externalData r:id="rId1">"#));
11147
11148        for kind in [
11149            ChartKind::Bar,
11150            ChartKind::Pie,
11151            ChartKind::Doughnut,
11152            ChartKind::Area,
11153            ChartKind::Scatter,
11154            ChartKind::Radar,
11155        ] {
11156            let mut kind_data = data.clone();
11157            if kind == ChartKind::Scatter {
11158                kind_data.categories = vec!["1".to_owned(), "2".to_owned()];
11159            }
11160            let (chart, workbook) = authored_chart_parts(kind, &kind_data, "rId1").unwrap();
11161            let chart = std::str::from_utf8(&chart).unwrap();
11162            assert!(!chart.contains(r#"<c:marker val="0"/>"#));
11163            assert!(!chart.contains(r#"<c:smooth val="0"/>"#));
11164            if kind != ChartKind::Scatter {
11165                assert_eq!(workbook, line_workbook);
11166            }
11167            if matches!(kind, ChartKind::Pie | ChartKind::Doughnut) {
11168                assert!(chart.contains("<c:legend"));
11169                assert!(chart.contains(r#"<c:showPercent val="1"/>"#));
11170            }
11171            if kind == ChartKind::Doughnut {
11172                assert!(chart.contains(r#"<c:holeSize val="50"/>"#));
11173            }
11174            if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) {
11175                assert_eq!(chart.matches("<c:dPt>").count(), 2);
11176                assert!(chart.contains(
11177                    r#"<c:dPt><c:idx val="0"/><c:spPr><a:solidFill><a:srgbClr val="2B6FE3"/>"#
11178                ));
11179                assert!(chart.contains(
11180                    r#"<c:dPt><c:idx val="1"/><c:spPr><a:solidFill><a:srgbClr val="F0761F"/>"#
11181                ));
11182            }
11183
11184            let parsed = CT_ChartSpace::from_xml(chart.as_bytes()).unwrap();
11185            let rewritten = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
11186            assert_eq!(
11187                rewritten.matches("<c:dPt>").count(),
11188                usize::from(matches!(
11189                    kind,
11190                    ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut
11191                )) * 2
11192            );
11193            assert!(rewritten.contains(r#"<a:srgbClr val="2B6FE3"/>"#));
11194            if matches!(kind, ChartKind::Bar | ChartKind::Pie | ChartKind::Doughnut) {
11195                assert!(rewritten.contains(r#"<a:srgbClr val="F0761F"/>"#));
11196            }
11197        }
11198    }
11199
11200    #[test]
11201    fn optional_numeric_cache_format_remains_omitted() {
11202        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>"#;
11203        let mut parsed = NumericData::from_xml(xml).unwrap();
11204        assert_eq!(parsed.format_code, "General");
11205        let written = parsed.to_xml().unwrap();
11206        assert!(!String::from_utf8_lossy(&written).contains("formatCode"));
11207        assert_eq!(NumericData::from_xml(&written).unwrap(), parsed);
11208
11209        parsed.format_code = "0.00".to_owned();
11210        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
11211        assert!(written.contains("<c:formatCode>0.00</c:formatCode>"));
11212    }
11213
11214    fn render_geometry(chart: &super::CT_Chart, bounds: Rect) -> super::Result<ChartGeometry> {
11215        render_geometry_with_theme(
11216            chart,
11217            bounds,
11218            &CT_OfficeStyleSheet::office_default(),
11219            &ColorMap::default(),
11220        )
11221    }
11222
11223    fn render_chart(
11224        chart: &super::CT_Chart,
11225        bounds: Rect,
11226        fonts: &mut FontManager,
11227    ) -> super::Result<oxml_layout::GroupElement> {
11228        render_chart_with_theme(
11229            chart,
11230            bounds,
11231            &CT_OfficeStyleSheet::office_default(),
11232            &ColorMap::default(),
11233            fonts,
11234        )
11235    }
11236
11237    #[test]
11238    fn bar_chart_rasterises_at_computed_positions() {
11239        let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11240        let geometry = render_geometry(
11241            &chart.chart,
11242            Rect {
11243                x: 0.0,
11244                y: 0.0,
11245                width: 200.0,
11246                height: 140.0,
11247            },
11248        )
11249        .unwrap();
11250        assert_eq!(
11251            geometry.plot_bounds,
11252            Rect {
11253                x: 36.0,
11254                y: 12.0,
11255                width: 152.0,
11256                height: 100.0,
11257            }
11258        );
11259        let bounds = path_bounds(&geometry);
11260        assert_eq!(bounds.len(), 2);
11261        assert_rect_near(
11262            bounds[0],
11263            Rect {
11264                x: 58.8,
11265                y: 62.0,
11266                width: 30.4,
11267                height: 50.0,
11268            },
11269        );
11270        assert_rect_near(
11271            bounds[1],
11272            Rect {
11273                x: 134.8,
11274                y: 12.0,
11275                width: 30.4,
11276                height: 100.0,
11277            },
11278        );
11279
11280        let layout = LayoutResult::new(
11281            vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
11282            Vec::new(),
11283            None,
11284            Vec::new(),
11285        );
11286        let png = oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap();
11287        let pixmap = tiny_skia::Pixmap::decode_png(&png).unwrap();
11288        for (x, y) in [(74, 87), (150, 37)] {
11289            let pixel = pixmap.pixel(x, y).unwrap();
11290            assert_ne!((pixel.red(), pixel.green(), pixel.blue()), (255, 255, 255));
11291        }
11292    }
11293
11294    #[test]
11295    fn unstyled_four_series_use_accent_one_through_four() {
11296        let chart = four_series_bar_chart();
11297        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11298        let colours = path_colours(&geometry);
11299
11300        assert_eq!(
11301            &colours[..4],
11302            &[
11303                Color::from_hex("156082"),
11304                Color::from_hex("E97132"),
11305                Color::from_hex("196B24"),
11306                Color::from_hex("0F9ED5"),
11307            ]
11308        );
11309    }
11310
11311    #[test]
11312    fn direct_series_solid_colour_overrides_theme_accent() {
11313        for (paint, expected) in [
11314            (
11315                "<a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill>",
11316                Color::from_hex("123456"),
11317            ),
11318            (
11319                "<a:ln><a:solidFill><a:srgbClr val=\"654321\"/></a:solidFill></a:ln>",
11320                Color::from_hex("654321"),
11321            ),
11322            (
11323                "<a:noFill/>",
11324                Color {
11325                    r: 0.0,
11326                    g: 0.0,
11327                    b: 0.0,
11328                    a: 0.0,
11329                },
11330            ),
11331        ] {
11332            let styled =
11333                plot_series(0).replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
11334            let plot = bar_plot("").replace(&plot_series(0), &styled);
11335            let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11336            let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11337
11338            assert_eq!(path_colours(&geometry)[0], expected);
11339        }
11340
11341        let styled = plot_series(0).replace(
11342            "<q:cat>",
11343            "<q:spPr><a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill></q:spPr><q:cat>",
11344        );
11345        let plot = bar_plot("").replace(&plot_series(0), &styled);
11346        let xml = chart_with_plot(&plot).replace(
11347            "</q:plotArea></q:chart>",
11348            "</q:plotArea><q:legend/></q:chart>",
11349        );
11350        let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
11351        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
11352        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
11353        assert!(group.children.iter().any(|element| matches!(
11354            element,
11355            PositionedElement::Path(path)
11356                if path.fill == Some(Paint::Solid(Color::from_hex("123456")))
11357        )));
11358
11359        let mut theme = CT_OfficeStyleSheet::office_default();
11360        let preset = oxml_drawing::fill::Fill::from_xml(
11361            br#"<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:prstClr val="red"/></a:solidFill>"#,
11362        )
11363        .unwrap();
11364        let oxml_drawing::fill::Fill::Solid(preset) = preset else {
11365            panic!("expected solid fill");
11366        };
11367        theme.theme_elements.color_scheme.hyperlink = preset.color.unwrap();
11368        let geometry =
11369            render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &ColorMap::default())
11370                .unwrap();
11371        assert_eq!(path_colours(&geometry)[0], Color::from_hex("123456"));
11372
11373        for (paint, expected_alpha) in [
11374            ("<a:noFill/>", 0.0),
11375            (
11376                "<a:solidFill><a:srgbClr val=\"123456\"><a:alpha val=\"50000\"/></a:srgbClr></a:solidFill>",
11377                (128.0 / 255.0) * 0.55,
11378            ),
11379        ] {
11380            let (_, area, _) = remaining_plot_fixtures()
11381                .into_iter()
11382                .find(|(kind, _, _)| *kind == "areaChart")
11383                .unwrap();
11384            let styled_area = area.replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
11385            let chart =
11386                CT_ChartSpace::from_xml(chart_with_optional_axes(&styled_area, true).as_bytes())
11387                    .unwrap();
11388            let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11389            let actual_alpha = path_colours(&geometry)[0].a;
11390
11391            assert!((actual_alpha - expected_alpha).abs() < 1.0e-12);
11392        }
11393    }
11394
11395    #[test]
11396    fn series_accent_cycle_repeats_after_six() {
11397        let chart = seven_series_bar_chart();
11398        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11399        let colours = path_colours(&geometry);
11400
11401        assert_eq!(colours[0], Color::from_hex("156082"));
11402        assert_eq!(colours[6], colours[0]);
11403    }
11404
11405    #[test]
11406    fn series_colours_honor_colour_map_and_transform_order() {
11407        let styled = plot_series(0).replace(
11408            "<q:cat>",
11409            "<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>",
11410        );
11411        let plot = bar_plot("").replace(&plot_series(0), &styled);
11412        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11413        let color_map =
11414            ColorMap::default().with_overrides(&[(ColorMapSlot::Accent1, ThemeColorSlot::Accent4)]);
11415        let mut theme = CT_OfficeStyleSheet::office_default();
11416        let theme_fill = oxml_drawing::fill::Fill::from_xml(
11417            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>"#,
11418        )
11419        .unwrap();
11420        let oxml_drawing::fill::Fill::Solid(theme_fill) = theme_fill else {
11421            panic!("expected solid fill");
11422        };
11423        let theme_colour = theme_fill.color.unwrap();
11424        theme.theme_elements.color_scheme.accent4 = theme_colour.clone();
11425        let geometry =
11426            render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &color_map).unwrap();
11427        let series_fill = oxml_drawing::fill::Fill::from_xml(
11428            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>"#,
11429        )
11430        .unwrap();
11431        let oxml_drawing::fill::Fill::Solid(series_fill) = series_fill else {
11432            panic!("expected solid fill");
11433        };
11434        let mut transforms = theme_colour.transforms().to_vec();
11435        transforms.extend_from_slice(series_fill.color.as_ref().unwrap().transforms());
11436        let expected = oxml_drawing::color::apply_color_transforms(
11437            RgbColor::parse("336699").unwrap(),
11438            &transforms,
11439        );
11440
11441        assert_eq!(path_colours(&geometry)[0], resolved_layout_colour(expected));
11442    }
11443
11444    #[test]
11445    fn chart_colours_do_not_use_word_tint_shade() {
11446        let styled = plot_series(0).replace(
11447            "<q:cat>",
11448            "<q:spPr><a:solidFill><a:srgbClr val=\"808080\"><a:tint val=\"50000\"/></a:srgbClr></a:solidFill></q:spPr><q:cat>",
11449        );
11450        let plot = bar_plot("").replace(&plot_series(0), &styled);
11451        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11452        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11453
11454        assert_eq!(
11455            path_colours(&geometry)[0],
11456            Color {
11457                r: 205.0 / 255.0,
11458                g: 205.0 / 255.0,
11459                b: 205.0 / 255.0,
11460                a: 1.0,
11461            }
11462        );
11463    }
11464
11465    #[test]
11466    fn unsupported_direct_series_paint_is_contextual() {
11467        let styled = plot_series(0).replace(
11468            "<q:cat>",
11469            "<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>",
11470        );
11471        let plot = bar_plot("").replace(&plot_series(0), &styled);
11472        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
11473        let error = render_geometry(&chart.chart, chart_bounds()).unwrap_err();
11474
11475        assert!(error.to_string().contains("c:ser[0] direct fill paint"));
11476        assert!(error.to_string().contains("gradient"));
11477    }
11478
11479    #[test]
11480    fn resolved_chart_palette_raster_is_deterministic() {
11481        let raster = || {
11482            let chart = four_series_bar_chart();
11483            let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
11484            let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
11485            let layout = LayoutResult::new(
11486                vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
11487                fonts.all_font_data(),
11488                None,
11489                Vec::new(),
11490            );
11491            oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
11492        };
11493        let chart = four_series_bar_chart();
11494        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11495
11496        assert_eq!(path_colours(&geometry)[0], Color::from_hex("156082"));
11497        assert_eq!(raster(), raster());
11498    }
11499
11500    #[test]
11501    fn bar_geometry_handles_direction_grouping_gap_and_overlap() {
11502        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11503        let Plot::Bar {
11504            overlap, series, ..
11505        } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11506        else {
11507            panic!("expected bar plot");
11508        };
11509        let mut second = series[0].clone();
11510        second.index = 1;
11511        second.order = 1;
11512        second.values.values = vec![2.0, 1.0];
11513        series.push(second);
11514
11515        *overlap = 25;
11516        let clustered = render_geometry(&chart.chart, chart_bounds()).unwrap();
11517        let clustered_bounds = path_bounds(&clustered);
11518        let clustered_width = 30.4 / 1.75;
11519        let clustered_advance = clustered_width * 0.75;
11520        assert_rects_near(
11521            &clustered_bounds,
11522            &[
11523                Rect {
11524                    x: 58.8,
11525                    y: 62.0,
11526                    width: clustered_width,
11527                    height: 50.0,
11528                },
11529                Rect {
11530                    x: 134.8,
11531                    y: 12.0,
11532                    width: clustered_width,
11533                    height: 100.0,
11534                },
11535                Rect {
11536                    x: 58.8 + clustered_advance,
11537                    y: 12.0,
11538                    width: clustered_width,
11539                    height: 100.0,
11540                },
11541                Rect {
11542                    x: 134.8 + clustered_advance,
11543                    y: 62.0,
11544                    width: clustered_width,
11545                    height: 50.0,
11546                },
11547            ],
11548        );
11549
11550        let Plot::Bar { direction, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
11551            unreachable!()
11552        };
11553        *direction = BarDirection::Bar;
11554        let horizontal = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11555        let horizontal_width = 20.0 / 1.75;
11556        let horizontal_advance = horizontal_width * 0.75;
11557        assert_rects_near(
11558            &horizontal,
11559            &[
11560                Rect {
11561                    x: 36.0,
11562                    y: 27.0,
11563                    width: 76.0,
11564                    height: horizontal_width,
11565                },
11566                Rect {
11567                    x: 36.0,
11568                    y: 77.0,
11569                    width: 152.0,
11570                    height: horizontal_width,
11571                },
11572                Rect {
11573                    x: 36.0,
11574                    y: 27.0 + horizontal_advance,
11575                    width: 152.0,
11576                    height: horizontal_width,
11577                },
11578                Rect {
11579                    x: 36.0,
11580                    y: 77.0 + horizontal_advance,
11581                    width: 76.0,
11582                    height: horizontal_width,
11583                },
11584            ],
11585        );
11586
11587        let Plot::Bar {
11588            direction,
11589            grouping,
11590            ..
11591        } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11592        else {
11593            unreachable!()
11594        };
11595        *direction = BarDirection::Column;
11596        *grouping = BarGrouping::Stacked;
11597        let stacked = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11598        assert_rects_near(
11599            &stacked,
11600            &[
11601                Rect {
11602                    x: 58.8,
11603                    y: 112.0 - 100.0 / 3.0,
11604                    width: 30.4,
11605                    height: 100.0 / 3.0,
11606                },
11607                Rect {
11608                    x: 134.8,
11609                    y: 112.0 - 200.0 / 3.0,
11610                    width: 30.4,
11611                    height: 200.0 / 3.0,
11612                },
11613                Rect {
11614                    x: 58.8,
11615                    y: 12.0,
11616                    width: 30.4,
11617                    height: 200.0 / 3.0,
11618                },
11619                Rect {
11620                    x: 134.8,
11621                    y: 12.0,
11622                    width: 30.4,
11623                    height: 100.0 / 3.0,
11624                },
11625            ],
11626        );
11627
11628        let Plot::Bar {
11629            grouping, series, ..
11630        } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11631        else {
11632            unreachable!()
11633        };
11634        *grouping = BarGrouping::PercentStacked;
11635        series[1].values.values = vec![2.0, 2.0];
11636        let percent = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11637        assert_rects_near(
11638            &percent,
11639            &[
11640                Rect {
11641                    x: 58.8,
11642                    y: 112.0 - 100.0 / 3.0,
11643                    width: 30.4,
11644                    height: 100.0 / 3.0,
11645                },
11646                Rect {
11647                    x: 134.8,
11648                    y: 62.0,
11649                    width: 30.4,
11650                    height: 50.0,
11651                },
11652                Rect {
11653                    x: 58.8,
11654                    y: 12.0,
11655                    width: 30.4,
11656                    height: 200.0 / 3.0,
11657                },
11658                Rect {
11659                    x: 134.8,
11660                    y: 12.0,
11661                    width: 30.4,
11662                    height: 50.0,
11663                },
11664            ],
11665        );
11666    }
11667
11668    #[test]
11669    fn line_scatter_and_radar_emit_paths_and_markers() {
11670        let line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
11671        let line_geometry = render_geometry(&line.chart, chart_bounds()).unwrap();
11672        assert_path_points(
11673            path_commands(&line_geometry)[0],
11674            &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
11675            false,
11676        );
11677        assert_rects_near(
11678            &path_bounds(&line_geometry)[1..],
11679            &[
11680                Rect {
11681                    x: 71.5,
11682                    y: 109.5,
11683                    width: 5.0,
11684                    height: 5.0,
11685                },
11686                Rect {
11687                    x: 147.5,
11688                    y: 9.5,
11689                    width: 5.0,
11690                    height: 5.0,
11691                },
11692            ],
11693        );
11694
11695        let (scatter_plot, _) = remaining_plot_fixtures()
11696            .into_iter()
11697            .find(|(kind, _, _)| *kind == "scatterChart")
11698            .map(|(_, plot, axes)| (plot, axes))
11699            .unwrap();
11700        let mut scatter =
11701            CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
11702                .unwrap();
11703        let Plot::Scatter { style, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
11704        else {
11705            unreachable!()
11706        };
11707        *style = ScatterStyle::LineMarker;
11708        let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
11709        assert_path_points(
11710            path_commands(&scatter_geometry)[0],
11711            &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
11712            false,
11713        );
11714        assert_rects_near(
11715            &path_bounds(&scatter_geometry)[1..],
11716            &[
11717                Rect {
11718                    x: 33.5,
11719                    y: 109.5,
11720                    width: 5.0,
11721                    height: 5.0,
11722                },
11723                Rect {
11724                    x: 185.5,
11725                    y: 9.5,
11726                    width: 5.0,
11727                    height: 5.0,
11728                },
11729            ],
11730        );
11731
11732        let (radar_plot, _) = remaining_plot_fixtures()
11733            .into_iter()
11734            .find(|(kind, _, _)| *kind == "radarChart")
11735            .map(|(_, plot, axes)| (plot, axes))
11736            .unwrap();
11737        let radar = CT_ChartSpace::from_xml(chart_with_optional_axes(&radar_plot, true).as_bytes())
11738            .unwrap();
11739        let radar_geometry = render_geometry(&radar.chart, chart_bounds()).unwrap();
11740        assert_path_points(
11741            path_commands(&radar_geometry)[0],
11742            &[Point { x: 112.0, y: 37.0 }, Point { x: 112.0, y: 112.0 }],
11743            true,
11744        );
11745        assert_rects_near(
11746            &path_bounds(&radar_geometry)[1..],
11747            &[
11748                Rect {
11749                    x: 109.5,
11750                    y: 34.5,
11751                    width: 5.0,
11752                    height: 5.0,
11753                },
11754                Rect {
11755                    x: 109.5,
11756                    y: 109.5,
11757                    width: 5.0,
11758                    height: 5.0,
11759                },
11760            ],
11761        );
11762    }
11763
11764    #[test]
11765    fn pie_doughnut_and_area_emit_closed_paths() {
11766        let fixtures = remaining_plot_fixtures();
11767        let pie_plot = fixtures
11768            .iter()
11769            .find(|(kind, _, _)| *kind == "pieChart")
11770            .unwrap();
11771        let pie =
11772            CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot.1, pie_plot.2).as_bytes())
11773                .unwrap();
11774        let pie_geometry = render_geometry(&pie.chart, chart_bounds()).unwrap();
11775        let pie_commands = path_commands(&pie_geometry);
11776        assert_eq!(pie_commands.len(), 2);
11777        let [
11778            PathCommand::MoveTo(center),
11779            PathCommand::LineTo(start),
11780            ..,
11781            PathCommand::CurveTo { to: end, .. },
11782            PathCommand::Close,
11783        ] = pie_commands[0]
11784        else {
11785            panic!("expected closed cubic pie wedge");
11786        };
11787        assert_point_near(*center, Point { x: 112.0, y: 62.0 });
11788        assert_point_near(
11789            *start,
11790            Point {
11791                x: 137.0,
11792                y: 62.0 - 25.0 * 3.0_f64.sqrt(),
11793            },
11794        );
11795        assert_point_near(
11796            *end,
11797            Point {
11798                x: 137.0,
11799                y: 62.0 + 25.0 * 3.0_f64.sqrt(),
11800            },
11801        );
11802
11803        let doughnut_plot = fixtures
11804            .iter()
11805            .find(|(kind, _, _)| *kind == "doughnutChart")
11806            .unwrap();
11807        let doughnut = CT_ChartSpace::from_xml(
11808            chart_with_optional_axes(&doughnut_plot.1, doughnut_plot.2).as_bytes(),
11809        )
11810        .unwrap();
11811        let doughnut_geometry = render_geometry(&doughnut.chart, chart_bounds()).unwrap();
11812        let doughnut_commands = path_commands(&doughnut_geometry);
11813        assert_eq!(doughnut_commands.len(), 2);
11814        let commands = doughnut_commands[0];
11815        assert_eq!(commands.len(), 7);
11816        let PathCommand::MoveTo(outer_start) = commands[0] else {
11817            panic!("expected doughnut outer start");
11818        };
11819        let PathCommand::LineTo(inner_end) = commands[3] else {
11820            panic!("expected doughnut inner-radius join");
11821        };
11822        let PathCommand::CurveTo {
11823            to: inner_start, ..
11824        } = commands[5]
11825        else {
11826            panic!("expected reverse inner arc");
11827        };
11828        let diagonal = 25.0 * 2.0_f64.sqrt();
11829        assert_point_near(
11830            outer_start,
11831            Point {
11832                x: 112.0 + diagonal,
11833                y: 62.0 - diagonal,
11834            },
11835        );
11836        assert_point_near(
11837            inner_end,
11838            Point {
11839                x: 112.0 + 30.0 * 75.0_f64.to_radians().cos(),
11840                y: 62.0 + 30.0 * 75.0_f64.to_radians().sin(),
11841            },
11842        );
11843        let inner_diagonal = 15.0 * 2.0_f64.sqrt();
11844        assert_point_near(
11845            inner_start,
11846            Point {
11847                x: 112.0 + inner_diagonal,
11848                y: 62.0 - inner_diagonal,
11849            },
11850        );
11851        assert_eq!(commands.last(), Some(&PathCommand::Close));
11852
11853        let area_plot = fixtures
11854            .iter()
11855            .find(|(kind, _, _)| *kind == "areaChart")
11856            .unwrap();
11857        let mut area =
11858            CT_ChartSpace::from_xml(chart_with_optional_axes(&area_plot.1, area_plot.2).as_bytes())
11859                .unwrap();
11860        let area_geometry = render_geometry(&area.chart, chart_bounds()).unwrap();
11861        assert_path_points(
11862            path_commands(&area_geometry)[0],
11863            &[
11864                Point { x: 74.0, y: 62.0 },
11865                Point { x: 150.0, y: 12.0 },
11866                Point { x: 150.0, y: 112.0 },
11867                Point { x: 74.0, y: 112.0 },
11868            ],
11869            true,
11870        );
11871
11872        let Plot::Area {
11873            grouping, series, ..
11874        } = &mut area.chart.plot_area.plots_mut().unwrap()[0]
11875        else {
11876            unreachable!()
11877        };
11878        *grouping = Grouping::Stacked;
11879        let mut second = series[0].clone();
11880        second.index = 1;
11881        second.order = 1;
11882        second.values.values = vec![2.0, 1.0];
11883        series.push(second);
11884        let stacked_area = render_geometry(&area.chart, chart_bounds()).unwrap();
11885        assert_path_points(
11886            path_commands(&stacked_area)[1],
11887            &[
11888                Point { x: 74.0, y: 12.0 },
11889                Point { x: 150.0, y: 12.0 },
11890                Point {
11891                    x: 150.0,
11892                    y: 112.0 - 200.0 / 3.0,
11893                },
11894                Point {
11895                    x: 74.0,
11896                    y: 112.0 - 100.0 / 3.0,
11897                },
11898            ],
11899            true,
11900        );
11901    }
11902
11903    #[test]
11904    fn sparse_cache_indexes_preserve_slots_and_scatter_pairing() {
11905        let series = sparse_category_series();
11906        let bar = format!(
11907            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>"#
11908        );
11909        let bar = CT_ChartSpace::from_xml(chart_with_plot(&bar).as_bytes()).unwrap();
11910        assert_rects_near(
11911            &path_bounds(&render_geometry(&bar.chart, chart_bounds()).unwrap()),
11912            &[
11913                Rect {
11914                    x: 51.2,
11915                    y: 62.0,
11916                    width: 304.0 / 15.0,
11917                    height: 50.0,
11918                },
11919                Rect {
11920                    x: 36.0 + 304.0 / 3.0 + 15.2,
11921                    y: 12.0,
11922                    width: 304.0 / 15.0,
11923                    height: 100.0,
11924                },
11925            ],
11926        );
11927
11928        let line = format!(
11929            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>"#
11930        );
11931        let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line).as_bytes()).unwrap();
11932        let first = Point {
11933            x: 36.0 + 76.0 / 3.0,
11934            y: 62.0,
11935        };
11936        let missing = Point { x: 112.0, y: 112.0 };
11937        let last = Point {
11938            x: 36.0 + 380.0 / 3.0,
11939            y: 12.0,
11940        };
11941        let gap = render_geometry(&line.chart, chart_bounds()).unwrap();
11942        let gap_commands = path_commands(&gap);
11943        assert_eq!(gap_commands.len(), 2);
11944        assert_path_points(
11945            gap_commands[0],
11946            &[Point {
11947                x: first.x,
11948                y: 112.0,
11949            }],
11950            false,
11951        );
11952        assert_path_points(gap_commands[1], &[last], false);
11953        line.chart.disp_blanks_as = DispBlanksAs::Zero;
11954        assert_path_points(
11955            path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
11956            &[first, missing, last],
11957            false,
11958        );
11959        line.chart.disp_blanks_as = DispBlanksAs::Span;
11960        assert_path_points(
11961            path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
11962            &[
11963                Point {
11964                    x: first.x,
11965                    y: 112.0,
11966                },
11967                last,
11968            ],
11969            false,
11970        );
11971
11972        let area = format!(
11973            r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:areaChart>"#
11974        );
11975        let mut area = CT_ChartSpace::from_xml(chart_with_plot(&area).as_bytes()).unwrap();
11976        let area_gap = render_geometry(&area.chart, chart_bounds()).unwrap();
11977        let area_gap_commands = path_commands(&area_gap);
11978        assert_eq!(area_gap_commands.len(), 2);
11979        assert_path_points(
11980            area_gap_commands[0],
11981            &[
11982                first,
11983                Point {
11984                    x: first.x,
11985                    y: 112.0,
11986                },
11987            ],
11988            true,
11989        );
11990        assert_path_points(
11991            area_gap_commands[1],
11992            &[
11993                last,
11994                Point {
11995                    x: last.x,
11996                    y: 112.0,
11997                },
11998            ],
11999            true,
12000        );
12001        area.chart.disp_blanks_as = DispBlanksAs::Zero;
12002        assert_path_points(
12003            path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
12004            &[
12005                first,
12006                missing,
12007                last,
12008                Point {
12009                    x: last.x,
12010                    y: 112.0,
12011                },
12012                missing,
12013                Point {
12014                    x: first.x,
12015                    y: 112.0,
12016                },
12017            ],
12018            true,
12019        );
12020        area.chart.disp_blanks_as = DispBlanksAs::Span;
12021        assert_path_points(
12022            path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
12023            &[
12024                first,
12025                last,
12026                Point {
12027                    x: last.x,
12028                    y: 112.0,
12029                },
12030                Point {
12031                    x: first.x,
12032                    y: 112.0,
12033                },
12034            ],
12035            true,
12036        );
12037
12038        let radar = format!(
12039            r#"<q:radarChart><q:radarStyle val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#
12040        );
12041        let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
12042        assert_path_points(
12043            path_commands(&render_geometry(&radar.chart, chart_bounds()).unwrap())[0],
12044            &[
12045                Point { x: 112.0, y: 37.0 },
12046                Point {
12047                    x: 112.0 - 25.0 * 3.0_f64.sqrt(),
12048                    y: 87.0,
12049                },
12050            ],
12051            true,
12052        );
12053
12054        let scatter = format!(
12055            r#"<q:scatterChart><q:scatterStyle val="lineMarker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:scatterChart>"#,
12056            sparse_scatter_series()
12057        );
12058        let mut scatter = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
12059        let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12060        assert_eq!(geometry_children(&scatter_geometry).len(), 4);
12061        let gap_commands = path_commands(&scatter_geometry);
12062        assert_path_points(gap_commands[0], &[Point { x: 36.0, y: 112.0 }], false);
12063        assert_path_points(gap_commands[1], &[Point { x: 188.0, y: 12.0 }], false);
12064
12065        scatter.chart.disp_blanks_as = DispBlanksAs::Zero;
12066        let zero = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12067        assert_path_points(
12068            path_commands(&zero)[0],
12069            &[
12070                Point { x: 74.0, y: 87.0 },
12071                Point { x: 36.0, y: 62.0 },
12072                Point { x: 150.0, y: 112.0 },
12073                Point { x: 188.0, y: 12.0 },
12074            ],
12075            false,
12076        );
12077
12078        scatter.chart.disp_blanks_as = DispBlanksAs::Span;
12079        assert_path_points(
12080            path_commands(&render_geometry(&scatter.chart, chart_bounds()).unwrap())[0],
12081            &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
12082            false,
12083        );
12084    }
12085
12086    #[test]
12087    fn sparse_scatter_domains_and_labels_use_only_matched_points() {
12088        let scatter = format!(
12089            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>"#,
12090            sparse_scatter_series()
12091        );
12092        let chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
12093        let plot = &chart.chart.plot_area.plots().unwrap()[0];
12094        assert_eq!(
12095            super::scatter_domains(plot, DispBlanksAs::Gap).unwrap(),
12096            (
12097                Domain {
12098                    min: 10.0,
12099                    max: 40.0,
12100                },
12101                Domain {
12102                    min: 100.0,
12103                    max: 400.0,
12104                },
12105            )
12106        );
12107        let axes = chart.chart.plot_area.axes().unwrap();
12108        let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
12109        let anchors =
12110            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12111        assert_eq!(
12112            anchors.keys().copied().collect::<Vec<_>>(),
12113            vec![(0, 0), (0, 3)]
12114        );
12115        assert_eq!(anchors[&(0, 0)].center, Point { x: 0.0, y: 140.0 });
12116        assert_eq!(anchors[&(0, 3)].center, Point { x: 200.0, y: 0.0 });
12117    }
12118
12119    #[test]
12120    fn sparse_bubble_size_labels_resolve_logical_indexes() {
12121        let scatter = format!(
12122            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>"#,
12123            sparse_scatter_series()
12124        );
12125        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
12126        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12127        let Plot::Scatter { series, .. } = plot else {
12128            panic!("expected scatter plot");
12129        };
12130        series[0].bubble_size = Some(
12131            NumericData::from_xml(
12132                format!(
12133                    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>"#
12134                )
12135                .as_bytes(),
12136            )
12137            .unwrap(),
12138        );
12139        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12140        let mut rendered = Vec::new();
12141        super::render_data_labels(
12142            plot,
12143            chart_bounds(),
12144            DispBlanksAs::Gap,
12145            super::GeometryOptions::default(),
12146            &mut fonts,
12147            &mut rendered,
12148        )
12149        .unwrap();
12150        let text: Vec<_> = rendered
12151            .iter()
12152            .filter_map(|element| match element {
12153                PositionedElement::Text(run) => Some(run.text.as_str()),
12154                _ => None,
12155            })
12156            .collect();
12157        assert_eq!(text, vec!["5", "9"]);
12158    }
12159
12160    #[test]
12161    fn geometry_rejects_invalid_bounds_and_opaque_plots() {
12162        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12163        for bounds in [
12164            Rect {
12165                x: 0.0,
12166                y: 0.0,
12167                width: f64::NAN,
12168                height: 100.0,
12169            },
12170            Rect {
12171                x: 0.0,
12172                y: 0.0,
12173                width: 0.0,
12174                height: 100.0,
12175            },
12176            Rect {
12177                x: 0.0,
12178                y: 0.0,
12179                width: 40.0,
12180                height: 30.0,
12181            },
12182        ] {
12183            assert!(render_geometry(&chart.chart, bounds).is_err());
12184        }
12185
12186        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12187            unreachable!()
12188        };
12189        series[0].values.values.clear();
12190        assert!(render_geometry(&chart.chart, chart_bounds()).is_err());
12191
12192        let opaque = CT_ChartSpace::from_xml(
12193            chart_with_plot(r#"<q:bar3DChart><q:barDir val="col"/></q:bar3DChart>"#).as_bytes(),
12194        )
12195        .unwrap();
12196        let error = render_geometry(&opaque.chart, chart_bounds()).unwrap_err();
12197        assert!(error.to_string().contains("unsupported or combination"));
12198
12199        let (scatter_plot, _) = remaining_plot_fixtures()
12200            .into_iter()
12201            .find(|(kind, _, _)| *kind == "scatterChart")
12202            .map(|(_, plot, axes)| (plot, axes))
12203            .unwrap();
12204        let mut scatter =
12205            CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
12206                .unwrap();
12207        for invalid in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
12208            let Plot::Scatter { series, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
12209            else {
12210                unreachable!()
12211            };
12212            let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
12213                unreachable!()
12214            };
12215            x_values.values[0] = invalid;
12216            let error = render_geometry(&scatter.chart, chart_bounds()).unwrap_err();
12217            assert!(error.to_string().contains("xVal"));
12218        }
12219    }
12220
12221    #[test]
12222    fn finite_extremes_never_produce_nonfinite_geometry() {
12223        let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
12224        let Plot::Line { series, .. } = &mut line.chart.plot_area.plots_mut().unwrap()[0] else {
12225            unreachable!()
12226        };
12227        series[0].values.values = vec![-f64::MAX, f64::MAX];
12228        let extreme_line = render_geometry(&line.chart, chart_bounds()).unwrap();
12229        assert_path_points(
12230            path_commands(&extreme_line)[0],
12231            &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
12232            false,
12233        );
12234
12235        let mut stacked =
12236            CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12237        let Plot::Bar {
12238            grouping, series, ..
12239        } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
12240        else {
12241            unreachable!()
12242        };
12243        *grouping = BarGrouping::Stacked;
12244        series[0].values.values = vec![f64::MAX, 1.0];
12245        let mut second = series[0].clone();
12246        second.index = 1;
12247        second.order = 1;
12248        series.push(second);
12249        let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
12250        assert!(error.to_string().contains("stacked value total"));
12251
12252        let Plot::Bar { grouping, .. } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
12253        else {
12254            unreachable!()
12255        };
12256        *grouping = BarGrouping::PercentStacked;
12257        let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
12258        assert!(error.to_string().contains("stacked percentage total"));
12259
12260        let (pie_plot, _) = remaining_plot_fixtures()
12261            .into_iter()
12262            .find(|(kind, _, _)| *kind == "pieChart")
12263            .map(|(_, plot, axes)| (plot, axes))
12264            .unwrap();
12265        let mut pie =
12266            CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot, false).as_bytes()).unwrap();
12267        let Plot::Pie { series, .. } = &mut pie.chart.plot_area.plots_mut().unwrap()[0] else {
12268            unreachable!()
12269        };
12270        series[0].values.values = vec![f64::MAX, f64::MAX];
12271        let error = render_geometry(&pie.chart, chart_bounds()).unwrap_err();
12272        assert!(error.to_string().contains("pie value total"));
12273
12274        let (scatter_plot, _) = remaining_plot_fixtures()
12275            .into_iter()
12276            .find(|(kind, _, _)| *kind == "scatterChart")
12277            .map(|(_, plot, axes)| (plot, axes))
12278            .unwrap();
12279        let mut scatter =
12280            CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
12281                .unwrap();
12282        let Plot::Scatter { style, series, .. } =
12283            &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
12284        else {
12285            unreachable!()
12286        };
12287        *style = ScatterStyle::Line;
12288        let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
12289            unreachable!()
12290        };
12291        x_values.values = vec![-f64::MAX, f64::MAX];
12292        series[0].values.values = vec![-f64::MAX, f64::MAX];
12293        let extreme_scatter = render_geometry(&scatter.chart, chart_bounds()).unwrap();
12294        assert_path_points(
12295            path_commands(&extreme_scatter)[0],
12296            &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
12297            false,
12298        );
12299    }
12300
12301    #[test]
12302    fn geometry_is_backend_neutral_and_deterministic() {
12303        let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12304        let first = render_geometry(&chart.chart, chart_bounds()).unwrap();
12305        let second = render_geometry(&chart.chart, chart_bounds()).unwrap();
12306        assert_eq!(
12307            format!("{:?}", first.elements),
12308            format!("{:?}", second.elements)
12309        );
12310
12311        let raster = |geometry: ChartGeometry| {
12312            let layout = LayoutResult::new(
12313                vec![PageFrame::new(1, 200.0, 140.0, geometry.elements).into()],
12314                Vec::new(),
12315                None,
12316                Vec::new(),
12317            );
12318            oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
12319        };
12320        assert_eq!(raster(first), raster(second));
12321    }
12322
12323    #[test]
12324    fn zero_to_one_hundred_axis_uses_expected_ticks() {
12325        let scale = nice_number_scale(
12326            Domain {
12327                min: 0.0,
12328                max: 100.0,
12329            },
12330            None,
12331            None,
12332            6,
12333        )
12334        .unwrap();
12335        assert_eq!(scale.ticks, vec![0.0, 20.0, 40.0, 60.0, 80.0, 100.0]);
12336    }
12337
12338    #[test]
12339    fn nice_number_ticks_cover_unpinned_extents() {
12340        for domain in [
12341            Domain {
12342                min: 3.0,
12343                max: 97.0,
12344            },
12345            Domain {
12346                min: -97.0,
12347                max: -3.0,
12348            },
12349            Domain {
12350                min: -3.5,
12351                max: 8.25,
12352            },
12353            Domain {
12354                min: 0.125,
12355                max: 0.875,
12356            },
12357            Domain {
12358                min: 1.0e12,
12359                max: 7.0e12,
12360            },
12361            Domain {
12362                min: -f64::MAX,
12363                max: f64::MAX,
12364            },
12365            Domain {
12366                min: f64::from_bits(1),
12367                max: f64::from_bits(5),
12368            },
12369        ] {
12370            let scale = nice_number_scale(domain, None, None, 6).unwrap();
12371            assert!(scale.domain.min <= domain.min);
12372            assert!(scale.domain.max >= domain.max);
12373            assert!(scale.ticks.iter().all(|tick| tick.is_finite()));
12374            assert!(scale.ticks.windows(2).all(|pair| pair[0] < pair[1]));
12375        }
12376
12377        let constant = nice_number_scale(Domain { min: 4.0, max: 4.0 }, None, None, 6).unwrap();
12378        assert!(constant.domain.min < 4.0 && constant.domain.max > 4.0);
12379    }
12380
12381    #[test]
12382    fn one_sided_explicit_scale_beyond_data_derives_a_valid_opposite_bound() {
12383        let minimum = nice_number_scale(
12384            Domain {
12385                min: 0.0,
12386                max: 10.0,
12387            },
12388            Some(20.0),
12389            None,
12390            6,
12391        )
12392        .unwrap();
12393        assert_eq!(minimum.domain.min, 20.0);
12394        assert!(minimum.domain.max > 20.0);
12395        assert!(minimum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
12396
12397        let maximum = nice_number_scale(
12398            Domain {
12399                min: 0.0,
12400                max: 10.0,
12401            },
12402            None,
12403            Some(-20.0),
12404            6,
12405        )
12406        .unwrap();
12407        assert_eq!(maximum.domain.max, -20.0);
12408        assert!(maximum.domain.min < -20.0);
12409        assert!(maximum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
12410    }
12411
12412    #[test]
12413    fn standard_line_domain_does_not_force_zero() {
12414        let mut chart =
12415            CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
12416        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12417        let Plot::Line { series, .. } = plot else {
12418            panic!("expected line plot");
12419        };
12420        series[0].values.values = vec![1_000.0, 1_010.0];
12421        assert_eq!(
12422            super::plot_value_domain(plot, DispBlanksAs::Gap).unwrap(),
12423            Some(Domain {
12424                min: 1_000.0,
12425                max: 1_010.0,
12426            })
12427        );
12428        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12429        assert_path_points(
12430            path_commands(&geometry)[0],
12431            &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
12432            false,
12433        );
12434    }
12435
12436    #[test]
12437    fn explicit_scaling_bounds_and_orientation_are_honored() {
12438        let scale = nice_number_scale(
12439            Domain {
12440                min: 25.0,
12441                max: 75.0,
12442            },
12443            Some(0.0),
12444            Some(100.0),
12445            6,
12446        )
12447        .unwrap();
12448        assert_eq!(
12449            scale.domain,
12450            Domain {
12451                min: 0.0,
12452                max: 100.0
12453            }
12454        );
12455        assert_eq!(
12456            super::map_y_oriented(0.0, scale.domain, chart_bounds(), Orientation::MaxMin).unwrap(),
12457            0.0
12458        );
12459        assert_eq!(
12460            super::map_y_oriented(100.0, scale.domain, chart_bounds(), Orientation::MaxMin)
12461                .unwrap(),
12462            140.0
12463        );
12464
12465        let mut chart = labelled_bar_chart();
12466        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12467            panic!("expected bar plot");
12468        };
12469        series[0].values.values = vec![25.0, 75.0];
12470        let value_axis = chart
12471            .chart
12472            .plot_area
12473            .axes
12474            .iter_mut()
12475            .find(|axis| axis.kind == AxisKind::Value)
12476            .unwrap();
12477        value_axis.scaling.orientation = Orientation::MaxMin;
12478        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12479        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12480        let plot = group
12481            .children
12482            .iter()
12483            .find_map(|element| match element {
12484                PositionedElement::Group(group) => Some(group),
12485                _ => None,
12486            })
12487            .unwrap();
12488        let bars: Vec<_> = plot
12489            .children
12490            .iter()
12491            .filter_map(|element| match element {
12492                PositionedElement::Path(path) => path.path.bounds(),
12493                _ => None,
12494            })
12495            .collect();
12496        assert_eq!(bars.len(), 2);
12497        assert!((bars[0].y - 12.0).abs() < 1.0e-9);
12498        assert!((bars[0].height - 25.0).abs() < 1.0e-9);
12499        assert!((bars[1].height - 75.0).abs() < 1.0e-9);
12500    }
12501
12502    #[test]
12503    fn family_label_anchors_share_rendered_geometry() {
12504        let mut bar = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12505        let bar_plot = &mut bar.chart.plot_area.plots_mut().unwrap()[0];
12506        let Plot::Bar { series, .. } = bar_plot else {
12507            panic!("expected bar plot");
12508        };
12509        let mut second = series[0].clone();
12510        second.index = 1;
12511        second.order = 1;
12512        series.push(second);
12513        let anchors = super::plot_label_anchors(
12514            bar_plot,
12515            chart_bounds(),
12516            DispBlanksAs::Gap,
12517            super::GeometryOptions::default(),
12518        )
12519        .unwrap();
12520        assert_point_near(anchors[&(0, 0)].center, Point { x: 40.0, y: 105.0 });
12521        assert_point_near(anchors[&(1, 0)].center, Point { x: 60.0, y: 105.0 });
12522
12523        let pie = CT_ChartSpace::from_xml(
12524            chart_with_optional_axes(
12525                &remaining_plot_fixtures()
12526                    .into_iter()
12527                    .find(|(kind, _, _)| *kind == "pieChart")
12528                    .unwrap()
12529                    .1,
12530                false,
12531            )
12532            .as_bytes(),
12533        )
12534        .unwrap();
12535        let Plot::Pie {
12536            first_slice_angle,
12537            series,
12538            ..
12539        } = &pie.chart.plot_area.plots().unwrap()[0]
12540        else {
12541            panic!("expected pie plot");
12542        };
12543        let slices = super::pie_slices(*first_slice_angle, None, series, chart_bounds()).unwrap();
12544        assert_eq!(slices.len(), 2);
12545        let first_span = std::f64::consts::TAU / 3.0;
12546        let first_midpoint = f64::from(*first_slice_angle).to_radians()
12547            - std::f64::consts::FRAC_PI_2
12548            + first_span / 2.0;
12549        assert_point_near(
12550            slices[0].anchor,
12551            super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, first_midpoint),
12552        );
12553        let second_midpoint = first_midpoint + std::f64::consts::PI;
12554        assert_point_near(
12555            slices[1].anchor,
12556            super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, second_midpoint),
12557        );
12558
12559        let radar = format!(
12560            r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12561            plot_series(0)
12562        );
12563        let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
12564        let Plot::Radar { series, .. } = &radar.chart.plot_area.plots().unwrap()[0] else {
12565            panic!("expected radar plot");
12566        };
12567        let radar_points =
12568            super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
12569                .unwrap();
12570        assert_eq!(radar_points[0][0], (0, Point { x: 100.0, y: 35.0 }));
12571        assert_eq!(radar_points[0][1], (1, Point { x: 100.0, y: 140.0 }));
12572    }
12573
12574    #[test]
12575    fn radar_annotations_use_spokes_perimeter_labels_and_radial_gridlines() {
12576        let radar_plot = format!(
12577            r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12578            sparse_category_series()
12579        );
12580        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12581        for axis in &mut chart.chart.plot_area.axes {
12582            axis.major_tick_mark = TickMark::Cross;
12583            if axis.kind == AxisKind::Value {
12584                axis.major_gridlines = Some(super::ChartLines::default());
12585            }
12586        }
12587        let plot = &chart.chart.plot_area.plots().unwrap()[0];
12588        let axes = chart.chart.plot_area.axes().unwrap();
12589        let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
12590        let center = Point { x: 112.0, y: 62.0 };
12591        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12592        let annotations =
12593            super::render_axes_and_labels(plot, &axes, bounds, DispBlanksAs::Gap, &mut fonts)
12594                .unwrap();
12595        assert_eq!(annotations.axis_lines.len(), 4);
12596        for spoke in annotations.axis_lines.iter().take(3) {
12597            let PositionedElement::Path(spoke) = spoke else {
12598                panic!("radar spoke must be a path");
12599            };
12600            assert_eq!(
12601                spoke.path.commands.first(),
12602                Some(&PathCommand::MoveTo(center))
12603            );
12604        }
12605        assert!(!annotations.gridlines.is_empty());
12606        for gridline in &annotations.gridlines {
12607            let PositionedElement::Path(gridline) = gridline else {
12608                panic!("radar gridline must be a path");
12609            };
12610            assert_eq!(gridline.path.commands.last(), Some(&PathCommand::Close));
12611            let radii: Vec<_> = gridline
12612                .path
12613                .commands
12614                .iter()
12615                .filter_map(|command| match command {
12616                    PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
12617                        Some((point.x - center.x).hypot(point.y - center.y))
12618                    }
12619                    _ => None,
12620                })
12621                .collect();
12622            assert_eq!(radii.len(), 3);
12623            assert!(
12624                radii
12625                    .windows(2)
12626                    .all(|pair| (pair[0] - pair[1]).abs() < 1.0e-9)
12627            );
12628        }
12629        let text: Vec<_> = annotations
12630            .labels
12631            .iter()
12632            .filter_map(|element| match element {
12633                PositionedElement::Text(run) => Some(run.text.as_str()),
12634                _ => None,
12635            })
12636            .collect();
12637        assert!(text.contains(&"North"));
12638        assert!(text.contains(&"West"));
12639    }
12640
12641    #[test]
12642    fn radar_tick_label_positions_are_distinct() {
12643        let radar_plot = format!(
12644            r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12645            plot_series(0)
12646        );
12647        let chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12648        let plot = &chart.chart.plot_area.plots().unwrap()[0];
12649        let axes = chart.chart.plot_area.axes().unwrap();
12650        let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
12651        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12652
12653        let category_axis = axes
12654            .iter()
12655            .find(|axis| axis.kind == AxisKind::Category)
12656            .unwrap();
12657        for (position, expected) in [
12658            (TickLabelPosition::High, Point { x: 108.0, y: 0.0 }),
12659            (TickLabelPosition::Low, Point { x: 108.0, y: 25.0 }),
12660            (TickLabelPosition::NextTo, Point { x: 108.0, y: 5.0 }),
12661        ] {
12662            let mut axis = category_axis.clone();
12663            axis.tick_label_position = position;
12664            let annotations =
12665                super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
12666                    .unwrap();
12667            let north = annotations
12668                .labels
12669                .iter()
12670                .find_map(|element| match element {
12671                    PositionedElement::Text(run) if run.text == "North" => Some(run),
12672                    _ => None,
12673                })
12674                .unwrap();
12675            assert_point_near(north.origin, expected);
12676        }
12677        let mut hidden_category = category_axis.clone();
12678        hidden_category.tick_label_position = TickLabelPosition::None;
12679        let annotations = super::render_axes_and_labels(
12680            plot,
12681            &[hidden_category],
12682            bounds,
12683            DispBlanksAs::Gap,
12684            &mut fonts,
12685        )
12686        .unwrap();
12687        assert!(annotations.labels.is_empty());
12688
12689        let narrow_bounds = Rect {
12690            x: 36.0,
12691            y: 12.0,
12692            width: 24.0,
12693            height: 100.0,
12694        };
12695        let narrow_center = Point { x: 48.0, y: 62.0 };
12696        assert_eq!(
12697            super::radar_category_label_origin(
12698                narrow_bounds,
12699                narrow_center,
12700                12.0,
12701                1,
12702                4,
12703                TickLabelPosition::High,
12704            ),
12705            Some(Point { x: 72.0, y: 65.0 })
12706        );
12707        assert_eq!(
12708            super::radar_category_label_origin(
12709                narrow_bounds,
12710                narrow_center,
12711                12.0,
12712                1,
12713                4,
12714                TickLabelPosition::NextTo,
12715            ),
12716            Some(Point { x: 66.0, y: 65.0 })
12717        );
12718
12719        let value_axis = axes
12720            .iter()
12721            .find(|axis| axis.kind == AxisKind::Value)
12722            .unwrap();
12723        for (position, expected) in [
12724            (TickLabelPosition::High, Point { x: 166.0, y: 65.0 }),
12725            (TickLabelPosition::Low, Point { x: 32.0, y: 65.0 }),
12726            (TickLabelPosition::NextTo, Point { x: 116.0, y: 65.0 }),
12727        ] {
12728            let mut axis = value_axis.clone();
12729            axis.tick_label_position = position;
12730            let annotations =
12731                super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
12732                    .unwrap();
12733            let zero = annotations
12734                .labels
12735                .iter()
12736                .find_map(|element| match element {
12737                    PositionedElement::Text(run) if run.text == "0" => Some(run),
12738                    _ => None,
12739                })
12740                .unwrap();
12741            assert_point_near(zero.origin, expected);
12742        }
12743        let mut hidden_value = value_axis.clone();
12744        hidden_value.tick_label_position = TickLabelPosition::None;
12745        let annotations = super::render_axes_and_labels(
12746            plot,
12747            &[hidden_value],
12748            bounds,
12749            DispBlanksAs::Gap,
12750            &mut fonts,
12751        )
12752        .unwrap();
12753        assert!(annotations.labels.is_empty());
12754    }
12755
12756    #[test]
12757    fn radar_negative_and_mixed_sign_domains_preserve_values() {
12758        let radar_plot = format!(
12759            r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12760            plot_series(0)
12761        );
12762        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12763        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12764        let Plot::Radar { series, .. } = plot else {
12765            panic!("expected radar plot");
12766        };
12767        series[0].values.values = vec![-10.0, -5.0];
12768        let points =
12769            super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
12770                .unwrap();
12771        assert_eq!(points[0][0], (0, Point { x: 100.0, y: 70.0 }));
12772        assert_eq!(points[0][1], (1, Point { x: 100.0, y: 105.0 }));
12773
12774        series[0].values.values = vec![-5.0, 10.0];
12775        let options = super::GeometryOptions {
12776            value: Some(super::AxisMapping {
12777                domain: Domain {
12778                    min: -10.0,
12779                    max: 10.0,
12780                },
12781                orientation: Orientation::MinMax,
12782            }),
12783            ..super::GeometryOptions::default()
12784        };
12785        let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
12786        assert_eq!(points[0][0], (0, Point { x: 100.0, y: 52.5 }));
12787        assert_eq!(points[0][1], (1, Point { x: 100.0, y: 140.0 }));
12788    }
12789
12790    #[test]
12791    fn radar_explicit_bounds_suppress_out_of_range_points_and_labels() {
12792        let radar_plot = format!(
12793            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>"#,
12794            plot_series(0)
12795        );
12796        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12797        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12798        let Plot::Radar { series, .. } = plot else {
12799            panic!("expected radar plot");
12800        };
12801        series[0].values.values = vec![25.0, 75.0];
12802        let options = super::GeometryOptions {
12803            value: Some(super::AxisMapping {
12804                domain: Domain {
12805                    min: 50.0,
12806                    max: 100.0,
12807                },
12808                orientation: Orientation::MinMax,
12809            }),
12810            ..super::GeometryOptions::default()
12811        };
12812        let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
12813        assert_eq!(points[0], vec![(1, Point { x: 100.0, y: 105.0 })]);
12814        let anchors =
12815            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12816        assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 1)]);
12817
12818        let Plot::Radar { series, .. } = plot else {
12819            panic!("expected radar plot");
12820        };
12821        series[0].values.values = vec![75.0, 125.0];
12822        let reversed = super::GeometryOptions {
12823            value: Some(super::AxisMapping {
12824                domain: Domain {
12825                    min: 50.0,
12826                    max: 100.0,
12827                },
12828                orientation: Orientation::MaxMin,
12829            }),
12830            ..super::GeometryOptions::default()
12831        };
12832        let points = super::radar_series_points(series, chart_bounds(), reversed).unwrap();
12833        assert_eq!(points[0], vec![(0, Point { x: 100.0, y: 35.0 })]);
12834        let anchors =
12835            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
12836        assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
12837
12838        let value_axis = chart
12839            .chart
12840            .plot_area
12841            .axes
12842            .iter_mut()
12843            .find(|axis| axis.kind == AxisKind::Value)
12844            .unwrap();
12845        value_axis.scaling.minimum = Some(50.0);
12846        value_axis.scaling.maximum = Some(100.0);
12847        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12848        let Plot::Radar { series, .. } = plot else {
12849            panic!("expected radar plot");
12850        };
12851        series[0].values.values = vec![25.0, 25.0];
12852        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12853        let rendered = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12854        assert!(rendered.children.iter().any(|element| matches!(
12855            element,
12856            PositionedElement::Group(group) if group.clip.is_some() && group.children.is_empty()
12857        )));
12858        assert!(
12859            rendered
12860                .children
12861                .iter()
12862                .any(|element| matches!(element, PositionedElement::Path(_)))
12863        );
12864        assert!(!rendered.children.iter().any(|element| matches!(
12865            element,
12866            PositionedElement::Text(run) if run.text == "25"
12867        )));
12868    }
12869
12870    #[test]
12871    fn inside_and_outside_label_positions_follow_family_geometry() {
12872        let mut horizontal =
12873            CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12874        let plot = &mut horizontal.chart.plot_area.plots_mut().unwrap()[0];
12875        let Plot::Bar { direction, .. } = plot else {
12876            panic!("expected bar plot");
12877        };
12878        *direction = BarDirection::Bar;
12879        let anchors = super::plot_label_anchors(
12880            plot,
12881            chart_bounds(),
12882            DispBlanksAs::Gap,
12883            super::GeometryOptions::default(),
12884        )
12885        .unwrap();
12886        let anchor = anchors[&(0, 0)];
12887        assert_point_near(
12888            anchor.origin(Some(DataLabelPosition::InsideBase)),
12889            Point { x: 5.0, y: 35.0 },
12890        );
12891        assert_point_near(
12892            anchor.origin(Some(DataLabelPosition::InsideEnd)),
12893            Point { x: 95.0, y: 35.0 },
12894        );
12895        assert_point_near(
12896            anchor.origin(Some(DataLabelPosition::OutsideEnd)),
12897            Point { x: 105.0, y: 35.0 },
12898        );
12899
12900        let mut negative =
12901            CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12902        let plot = &mut negative.chart.plot_area.plots_mut().unwrap()[0];
12903        let Plot::Bar { series, .. } = plot else {
12904            panic!("expected bar plot");
12905        };
12906        series[0].values.values = vec![-1.0, -2.0];
12907        let anchors = super::plot_label_anchors(
12908            plot,
12909            chart_bounds(),
12910            DispBlanksAs::Gap,
12911            super::GeometryOptions::default(),
12912        )
12913        .unwrap();
12914        assert_point_near(
12915            anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12916            Point { x: 50.0, y: 75.0 },
12917        );
12918        let reversed = super::GeometryOptions {
12919            value: Some(super::AxisMapping {
12920                domain: Domain {
12921                    min: -2.0,
12922                    max: 0.0,
12923                },
12924                orientation: Orientation::MaxMin,
12925            }),
12926            ..super::GeometryOptions::default()
12927        };
12928        let anchors =
12929            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
12930        assert_point_near(
12931            anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12932            Point { x: 50.0, y: 65.0 },
12933        );
12934
12935        let pie = CT_ChartSpace::from_xml(
12936            chart_with_optional_axes(
12937                &remaining_plot_fixtures()
12938                    .into_iter()
12939                    .find(|(kind, _, _)| *kind == "pieChart")
12940                    .unwrap()
12941                    .1,
12942                false,
12943            )
12944            .as_bytes(),
12945        )
12946        .unwrap();
12947        let Plot::Pie { series, .. } = &pie.chart.plot_area.plots().unwrap()[0] else {
12948            panic!("expected pie plot");
12949        };
12950        let pie_slice = &super::pie_slices(30, None, series, chart_bounds()).unwrap()[0];
12951        assert_point_near(
12952            super::LabelAnchor::segment(pie_slice.anchor, pie_slice.base, pie_slice.end)
12953                .origin(Some(DataLabelPosition::OutsideEnd)),
12954            Point { x: 175.0, y: 70.0 },
12955        );
12956        let doughnut_slice = &super::pie_slices(30, Some(60), series, chart_bounds()).unwrap()[0];
12957        assert_point_near(
12958            super::LabelAnchor::segment(
12959                doughnut_slice.anchor,
12960                doughnut_slice.base,
12961                doughnut_slice.end,
12962            )
12963            .origin(Some(DataLabelPosition::InsideBase)),
12964            Point { x: 147.0, y: 70.0 },
12965        );
12966
12967        let short = super::LabelAnchor::segment(
12968            Point { x: 2.0, y: 0.0 },
12969            Point { x: 0.0, y: 0.0 },
12970            Point { x: 4.0, y: 0.0 },
12971        );
12972        assert_eq!(
12973            short.origin(Some(DataLabelPosition::InsideBase)),
12974            Point { x: 2.0, y: 0.0 }
12975        );
12976        assert_eq!(
12977            short.origin(Some(DataLabelPosition::InsideEnd)),
12978            Point { x: 2.0, y: 0.0 }
12979        );
12980        let thin_bounds = Rect {
12981            x: 0.0,
12982            y: 0.0,
12983            width: 80.0,
12984            height: 80.0,
12985        };
12986        let thin = &super::pie_slices(30, Some(90), series, thin_bounds).unwrap()[0];
12987        let thin = super::LabelAnchor::segment(thin.anchor, thin.base, thin.end);
12988        assert_point_near(
12989            thin.origin(Some(DataLabelPosition::InsideBase)),
12990            Point { x: 78.0, y: 40.0 },
12991        );
12992        assert_point_near(
12993            thin.origin(Some(DataLabelPosition::InsideEnd)),
12994            Point { x: 78.0, y: 40.0 },
12995        );
12996    }
12997
12998    #[test]
12999    fn degenerate_bar_and_radar_anchors_preserve_family_direction() {
13000        let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13001        let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
13002        let Plot::Bar { series, .. } = plot else {
13003            panic!("expected bar plot");
13004        };
13005        series[0].values.values = vec![80.0, 125.0];
13006        for (direction, orientation, boundary, outside) in [
13007            (
13008                BarDirection::Column,
13009                Orientation::MinMax,
13010                Point { x: 50.0, y: 140.0 },
13011                Point { x: 50.0, y: 135.0 },
13012            ),
13013            (
13014                BarDirection::Column,
13015                Orientation::MaxMin,
13016                Point { x: 50.0, y: 0.0 },
13017                Point { x: 50.0, y: 5.0 },
13018            ),
13019            (
13020                BarDirection::Bar,
13021                Orientation::MinMax,
13022                Point { x: 0.0, y: 35.0 },
13023                Point { x: 5.0, y: 35.0 },
13024            ),
13025            (
13026                BarDirection::Bar,
13027                Orientation::MaxMin,
13028                Point { x: 200.0, y: 35.0 },
13029                Point { x: 195.0, y: 35.0 },
13030            ),
13031        ] {
13032            let Plot::Bar {
13033                direction: plot_direction,
13034                ..
13035            } = plot
13036            else {
13037                panic!("expected bar plot");
13038            };
13039            *plot_direction = direction;
13040            let options = super::GeometryOptions {
13041                value: Some(super::AxisMapping {
13042                    domain: Domain {
13043                        min: 80.0,
13044                        max: 100.0,
13045                    },
13046                    orientation,
13047                }),
13048                ..super::GeometryOptions::default()
13049            };
13050            let anchors =
13051                super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options)
13052                    .unwrap();
13053            let anchor = anchors[&(0, 0)];
13054            assert_point_near(anchor.center, boundary);
13055            assert_point_near(anchor.base, boundary);
13056            assert_point_near(anchor.end, boundary);
13057            assert_point_near(anchor.origin(Some(DataLabelPosition::InsideBase)), boundary);
13058            assert_point_near(anchor.origin(Some(DataLabelPosition::InsideEnd)), boundary);
13059            assert_point_near(anchor.origin(Some(DataLabelPosition::OutsideEnd)), outside);
13060        }
13061
13062        let radar_plot = format!(
13063            r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
13064            plot_series(0)
13065        );
13066        let mut radar = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
13067        let plot = &mut radar.chart.plot_area.plots_mut().unwrap()[0];
13068        let Plot::Radar { series, .. } = plot else {
13069            panic!("expected radar plot");
13070        };
13071        series[0].values.values = vec![0.0, 0.0, 0.0, 0.0];
13072        let anchors = super::plot_label_anchors(
13073            plot,
13074            chart_bounds(),
13075            DispBlanksAs::Gap,
13076            super::GeometryOptions::default(),
13077        )
13078        .unwrap();
13079        let center = Point { x: 100.0, y: 70.0 };
13080        for logical_index in 0..4 {
13081            assert_point_near(
13082                anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideBase)),
13083                center,
13084            );
13085            assert_point_near(
13086                anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideEnd)),
13087                center,
13088            );
13089        }
13090        assert_point_near(
13091            anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
13092            Point { x: 100.0, y: 65.0 },
13093        );
13094        assert_point_near(
13095            anchors[&(0, 1)].origin(Some(DataLabelPosition::OutsideEnd)),
13096            Point { x: 105.0, y: 70.0 },
13097        );
13098        assert_point_near(
13099            anchors[&(0, 2)].origin(Some(DataLabelPosition::OutsideEnd)),
13100            Point { x: 100.0, y: 75.0 },
13101        );
13102        assert_point_near(
13103            anchors[&(0, 3)].origin(Some(DataLabelPosition::OutsideEnd)),
13104            Point { x: 95.0, y: 70.0 },
13105        );
13106    }
13107
13108    #[test]
13109    fn labels_outside_explicit_axis_bounds_are_suppressed() {
13110        let mut chart =
13111            CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
13112        {
13113            let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13114            let Plot::Line { series, .. } = plot else {
13115                panic!("expected line plot");
13116            };
13117            series[0].values.values = vec![25.0, 75.0];
13118        }
13119        let value_axis = chart
13120            .chart
13121            .plot_area
13122            .axes
13123            .iter_mut()
13124            .find(|axis| axis.kind == AxisKind::Value)
13125            .unwrap();
13126        value_axis.scaling.minimum = Some(50.0);
13127        value_axis.scaling.maximum = Some(100.0);
13128        value_axis.scaling.orientation = Orientation::MaxMin;
13129        let plot = &chart.chart.plot_area.plots().unwrap()[0];
13130        let axes = chart.chart.plot_area.axes().unwrap();
13131        let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
13132        let anchors =
13133            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
13134        assert!(!anchors.contains_key(&(0, 0)));
13135        assert_point_near(anchors[&(0, 1)].center, Point { x: 150.0, y: 70.0 });
13136
13137        let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13138        let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
13139        let Plot::Bar { series, .. } = plot else {
13140            panic!("expected bar plot");
13141        };
13142        series[0].values.values = vec![90.0, 125.0];
13143        let options = super::GeometryOptions {
13144            value: Some(super::AxisMapping {
13145                domain: Domain {
13146                    min: 80.0,
13147                    max: 100.0,
13148                },
13149                orientation: Orientation::MinMax,
13150            }),
13151            ..super::GeometryOptions::default()
13152        };
13153        let anchors =
13154            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
13155        assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
13156        assert_point_near(anchors[&(0, 0)].base, Point { x: 50.0, y: 140.0 });
13157        assert_point_near(anchors[&(0, 0)].end, Point { x: 50.0, y: 70.0 });
13158        assert_point_near(anchors[&(0, 0)].center, Point { x: 50.0, y: 105.0 });
13159        assert_point_near(
13160            anchors[&(0, 0)].origin(Some(DataLabelPosition::InsideBase)),
13161            Point { x: 50.0, y: 135.0 },
13162        );
13163    }
13164
13165    #[test]
13166    fn zero_bars_render_and_emit_requested_value_labels() {
13167        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13168        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
13169        let Plot::Bar { series, .. } = plot else {
13170            panic!("expected bar plot");
13171        };
13172        series[0].values.values = vec![0.0, 0.0];
13173        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
13174        assert_eq!(path_commands(&geometry).len(), 2);
13175        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13176        render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13177        let plot = &chart.chart.plot_area.plots().unwrap()[0];
13178        let mut labels = Vec::new();
13179        super::render_data_labels(
13180            plot,
13181            chart_bounds(),
13182            DispBlanksAs::Gap,
13183            super::GeometryOptions::default(),
13184            &mut fonts,
13185            &mut labels,
13186        )
13187        .unwrap();
13188        let text: Vec<_> = labels
13189            .iter()
13190            .filter_map(|element| match element {
13191                PositionedElement::Text(run) => Some(run.text.as_str()),
13192                _ => None,
13193            })
13194            .collect();
13195        assert_eq!(text, vec!["0", "0"]);
13196    }
13197
13198    #[test]
13199    fn axes_gridlines_and_tick_marks_follow_model_state() {
13200        let chart = labelled_bar_chart();
13201        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13202        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13203        let ranks: Vec<_> = group
13204            .children
13205            .iter()
13206            .map(|element| match element {
13207                PositionedElement::Group(group) => {
13208                    assert!(group.clip.is_some());
13209                    1
13210                }
13211                PositionedElement::Path(path)
13212                    if path
13213                        .stroke
13214                        .as_ref()
13215                        .is_some_and(|stroke| stroke.width == 0.5) =>
13216                {
13217                    0
13218                }
13219                PositionedElement::Path(path) if path.fill.is_some() => 4,
13220                PositionedElement::Path(path) => {
13221                    let bounds = path.path.bounds().expect("axis or tick bounds");
13222                    assert_eq!(path.stroke.as_ref().map(|stroke| stroke.width), Some(1.0));
13223                    if bounds.width > 4.0 || bounds.height > 4.0 {
13224                        2
13225                    } else {
13226                        3
13227                    }
13228                }
13229                PositionedElement::Text(_) => 5,
13230                _ => panic!("unexpected labelled chart element"),
13231            })
13232            .collect();
13233        assert!(ranks.windows(2).all(|pair| pair[0] <= pair[1]), "{ranks:?}");
13234        for required in 0..=5 {
13235            assert!(
13236                ranks.contains(&required),
13237                "missing z-order rank {required}: {ranks:?}"
13238            );
13239        }
13240        assert_eq!(ranks.iter().filter(|rank| **rank == 1).count(), 1);
13241        assert_eq!(ranks.iter().filter(|rank| **rank == 2).count(), 2);
13242
13243        let mut deleted = chart.clone();
13244        for axis in &mut deleted.chart.plot_area.axes {
13245            axis.deleted = true;
13246        }
13247        let deleted_group = render_chart(&deleted.chart, chart_bounds(), &mut fonts).unwrap();
13248        assert_eq!(
13249            deleted_group
13250                .children
13251                .iter()
13252                .filter(|element| {
13253                    matches!(
13254                        element,
13255                        PositionedElement::Path(path) if path.stroke.is_some()
13256                    )
13257                })
13258                .count(),
13259            0
13260        );
13261    }
13262
13263    #[test]
13264    fn category_ticks_cover_unlabelled_and_sparse_logical_slots() {
13265        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13266        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
13267            panic!("expected bar plot");
13268        };
13269        series[0].categories = None;
13270        let plot = &chart.chart.plot_area.plots().unwrap()[0];
13271        let mut category_axis = chart
13272            .chart
13273            .plot_area
13274            .axes()
13275            .unwrap()
13276            .into_iter()
13277            .find(|axis| axis.kind == AxisKind::Category)
13278            .unwrap();
13279        category_axis.major_gridlines = Some(super::ChartLines::default());
13280        category_axis.major_tick_mark = TickMark::Cross;
13281        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13282        let annotations = super::render_axes_and_labels(
13283            plot,
13284            &[category_axis],
13285            chart_bounds(),
13286            DispBlanksAs::Gap,
13287            &mut fonts,
13288        )
13289        .unwrap();
13290        assert_eq!(annotations.gridlines.len(), 2);
13291        assert_eq!(annotations.ticks.len(), 2);
13292        assert!(annotations.labels.is_empty());
13293
13294        let sparse_plot = format!(
13295            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>"#,
13296            sparse_category_series()
13297        );
13298        let sparse = CT_ChartSpace::from_xml(chart_with_plot(&sparse_plot).as_bytes()).unwrap();
13299        let plot = &sparse.chart.plot_area.plots().unwrap()[0];
13300        let mut category_axis = sparse
13301            .chart
13302            .plot_area
13303            .axes()
13304            .unwrap()
13305            .into_iter()
13306            .find(|axis| axis.kind == AxisKind::Category)
13307            .unwrap();
13308        category_axis.major_gridlines = Some(super::ChartLines::default());
13309        category_axis.major_tick_mark = TickMark::Cross;
13310        let annotations = super::render_axes_and_labels(
13311            plot,
13312            &[category_axis],
13313            chart_bounds(),
13314            DispBlanksAs::Gap,
13315            &mut fonts,
13316        )
13317        .unwrap();
13318        assert_eq!(annotations.gridlines.len(), 3);
13319        assert_eq!(annotations.ticks.len(), 3);
13320        assert_eq!(annotations.labels.len(), 2);
13321
13322        let oversized_plot = sparse_plot.replacen(
13323            r#"<q:ptCount val="3"/>"#,
13324            r#"<q:ptCount val="4294967295"/>"#,
13325            1,
13326        );
13327        let oversized =
13328            CT_ChartSpace::from_xml(chart_with_plot(&oversized_plot).as_bytes()).unwrap();
13329        let plot = &oversized.chart.plot_area.plots().unwrap()[0];
13330        let category_axis = oversized
13331            .chart
13332            .plot_area
13333            .axes()
13334            .unwrap()
13335            .into_iter()
13336            .find(|axis| axis.kind == AxisKind::Category)
13337            .unwrap();
13338        let result = super::render_axes_and_labels(
13339            plot,
13340            &[category_axis],
13341            chart_bounds(),
13342            DispBlanksAs::Gap,
13343            &mut fonts,
13344        );
13345        let error = match result {
13346            Err(error) => error,
13347            Ok(_) => panic!("oversized category annotations rendered"),
13348        };
13349        assert!(
13350            error
13351                .to_string()
13352                .contains("logical category count 4294967295")
13353        );
13354        assert!(error.to_string().contains("exceeds 16384"));
13355    }
13356
13357    #[test]
13358    fn unsupported_numeric_category_formats_return_projection_errors() {
13359        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
13360        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
13361            panic!("expected bar plot");
13362        };
13363        series[0].categories = Some(AxisData::Numeric(
13364            NumericData::new(
13365                "Sheet1!$A$2:$A$3".to_owned(),
13366                "General".to_owned(),
13367                vec![1.0, 2.0],
13368            )
13369            .unwrap(),
13370        ));
13371        let plot = &chart.chart.plot_area.plots().unwrap()[0];
13372        let mut category_axis = chart
13373            .chart
13374            .plot_area
13375            .axes()
13376            .unwrap()
13377            .into_iter()
13378            .find(|axis| axis.kind == AxisKind::Category)
13379            .unwrap();
13380        category_axis.number_format = Some(NumberFormat::new("0.00".to_owned(), false).unwrap());
13381        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13382        let annotations = super::render_axes_and_labels(
13383            plot,
13384            &[category_axis.clone()],
13385            chart_bounds(),
13386            DispBlanksAs::Gap,
13387            &mut fonts,
13388        )
13389        .unwrap();
13390        let text: Vec<_> = annotations
13391            .labels
13392            .iter()
13393            .filter_map(|element| match element {
13394                PositionedElement::Text(run) => Some(run.text.as_str()),
13395                _ => None,
13396            })
13397            .collect();
13398        assert_eq!(text, vec!["1.00", "2.00"]);
13399
13400        category_axis.number_format = Some(NumberFormat::new("#,##0".to_owned(), false).unwrap());
13401        let result = super::render_axes_and_labels(
13402            plot,
13403            &[category_axis],
13404            chart_bounds(),
13405            DispBlanksAs::Gap,
13406            &mut fonts,
13407        );
13408        let error = match result {
13409            Err(error) => error,
13410            Ok(_) => panic!("unsupported category-axis format rendered"),
13411        };
13412        assert!(
13413            error
13414                .to_string()
13415                .contains("c:numFmt/@formatCode projection")
13416        );
13417        assert!(error.to_string().contains("#,##0"));
13418    }
13419
13420    #[test]
13421    fn labels_and_legend_shape_with_deterministic_fonts() {
13422        let chart = labelled_bar_chart();
13423        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13424        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13425        let labels: Vec<_> = group
13426            .children
13427            .iter()
13428            .filter_map(|element| match element {
13429                PositionedElement::Text(run) => Some(run),
13430                _ => None,
13431            })
13432            .collect();
13433        for text in ["North", "South", "20", "Sales", "100"] {
13434            assert!(labels.iter().any(|run| run.text == text), "missing {text}");
13435        }
13436        let styled_tick = labels.iter().find(|run| run.text == "20").unwrap();
13437        assert_eq!(styled_tick.font_size, 12.0);
13438        assert!(styled_tick.bold);
13439        assert!(
13440            labels
13441                .iter()
13442                .all(|run| !run.glyph_ids.is_empty() && run.glyph_ids.len() == run.advances.len())
13443        );
13444    }
13445
13446    #[test]
13447    fn labelled_chart_raster_is_deterministic() {
13448        let raster = || {
13449            let chart = labelled_bar_chart();
13450            let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
13451            let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
13452            let layout = LayoutResult::new(
13453                vec![PageFrame::new(1, 200.0, 140.0, vec![PositionedElement::Group(group)]).into()],
13454                fonts.all_font_data(),
13455                None,
13456                Vec::new(),
13457            );
13458            oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
13459        };
13460        assert_eq!(raster(), raster());
13461    }
13462
13463    fn labelled_bar_chart() -> CT_ChartSpace {
13464        let series = plot_series(0)
13465            .replace(
13466                "<q:cat>",
13467                "<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>",
13468            )
13469            .replace("<q:v>1</q:v>", "<q:v>0</q:v>")
13470            .replace("<q:v>2</q:v>", "<q:v>100</q:v>");
13471        let plot = format!(
13472            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>"#
13473        );
13474        let xml = chart_with_plot(&plot)
13475            .replacen(
13476                "<q:scaling/>",
13477                "<q:scaling><q:orientation val=\"minMax\"/></q:scaling>",
13478                1,
13479            )
13480            .replacen(
13481                "<q:scaling/>",
13482                "<q:scaling><q:max val=\"100\"/><q:min val=\"0\"/></q:scaling>",
13483                1,
13484            )
13485            .replace(
13486                "<q:axPos val=\"b\"/>",
13487                "<q:axPos val=\"b\"/><q:majorTickMark val=\"cross\"/>",
13488            )
13489            .replace(
13490                "<q:axPos val=\"l\"/>",
13491                "<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>",
13492            )
13493            .replace("</q:plotArea></q:chart>", "</q:plotArea><q:legend/></q:chart>");
13494        CT_ChartSpace::from_xml(xml.as_bytes()).unwrap()
13495    }
13496
13497    fn chart_bounds() -> Rect {
13498        Rect {
13499            x: 0.0,
13500            y: 0.0,
13501            width: 200.0,
13502            height: 140.0,
13503        }
13504    }
13505
13506    fn four_series_bar_chart() -> CT_ChartSpace {
13507        let series = (0..4).map(plot_series).collect::<String>();
13508        let plot = bar_plot("").replace(&plot_series(0), &series);
13509        CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
13510    }
13511
13512    fn seven_series_bar_chart() -> CT_ChartSpace {
13513        let series = (0..7).map(plot_series).collect::<String>();
13514        let plot = bar_plot("").replace(&plot_series(0), &series);
13515        CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
13516    }
13517
13518    fn path_colours(geometry: &ChartGeometry) -> Vec<Color> {
13519        let mut colours = geometry_children(geometry)
13520            .iter()
13521            .filter_map(|element| match element {
13522                PositionedElement::Path(path) => match path
13523                    .fill
13524                    .as_ref()
13525                    .or_else(|| path.stroke.as_ref().map(|stroke| &stroke.paint))
13526                {
13527                    Some(Paint::Solid(colour)) => Some(*colour),
13528                    _ => None,
13529                },
13530                _ => None,
13531            })
13532            .collect::<Vec<_>>();
13533        colours.dedup_by(|left, right| left == right);
13534        colours
13535    }
13536
13537    fn resolved_layout_colour(colour: oxml_drawing::color::ResolvedColor) -> Color {
13538        Color {
13539            r: f64::from(colour.red) / 255.0,
13540            g: f64::from(colour.green) / 255.0,
13541            b: f64::from(colour.blue) / 255.0,
13542            a: f64::from(colour.alpha) / 255.0,
13543        }
13544    }
13545
13546    fn geometry_children(geometry: &ChartGeometry) -> &[PositionedElement] {
13547        let [PositionedElement::Group(group)] = geometry.elements.as_slice() else {
13548            panic!("chart geometry must contain one group");
13549        };
13550        &group.children
13551    }
13552
13553    fn path_commands(geometry: &ChartGeometry) -> Vec<&[PathCommand]> {
13554        geometry_children(geometry)
13555            .iter()
13556            .map(|element| {
13557                let PositionedElement::Path(path) = element else {
13558                    panic!("chart geometry child must be a path");
13559                };
13560                path.path.commands.as_slice()
13561            })
13562            .collect()
13563    }
13564
13565    fn path_bounds(geometry: &ChartGeometry) -> Vec<Rect> {
13566        geometry_children(geometry)
13567            .iter()
13568            .map(|element| {
13569                let PositionedElement::Path(path) = element else {
13570                    panic!("chart geometry child must be a path");
13571                };
13572                path.path.bounds().expect("chart path has bounds")
13573            })
13574            .collect()
13575    }
13576
13577    fn assert_rect_near(actual: Rect, expected: Rect) {
13578        assert_near(actual.x, expected.x);
13579        assert_near(actual.y, expected.y);
13580        assert_near(actual.width, expected.width);
13581        assert_near(actual.height, expected.height);
13582    }
13583
13584    fn assert_rects_near(actual: &[Rect], expected: &[Rect]) {
13585        assert_eq!(actual.len(), expected.len());
13586        for (actual, expected) in actual.iter().copied().zip(expected.iter().copied()) {
13587            assert_rect_near(actual, expected);
13588        }
13589    }
13590
13591    fn assert_path_points(commands: &[PathCommand], expected: &[Point], closed: bool) {
13592        let points: Vec<_> = commands
13593            .iter()
13594            .filter_map(|command| match command {
13595                PathCommand::MoveTo(point) | PathCommand::LineTo(point) => Some(*point),
13596                PathCommand::Close => None,
13597                PathCommand::CurveTo { .. } => panic!("expected polygonal path"),
13598            })
13599            .collect();
13600        assert_eq!(points.len(), expected.len());
13601        for (actual, expected) in points.into_iter().zip(expected.iter().copied()) {
13602            assert_point_near(actual, expected);
13603        }
13604        assert_eq!(commands.last() == Some(&PathCommand::Close), closed);
13605    }
13606
13607    fn assert_point_near(actual: Point, expected: Point) {
13608        assert_near(actual.x, expected.x);
13609        assert_near(actual.y, expected.y);
13610    }
13611
13612    fn assert_near(actual: f64, expected: f64) {
13613        assert!(
13614            (actual - expected).abs() < 1e-9,
13615            "expected {expected}, got {actual}"
13616        );
13617    }
13618
13619    #[test]
13620    fn remaining_v1_plots_round_trip_and_render() {
13621        for (kind, plot, axes) in remaining_plot_fixtures() {
13622            let xml = chart_with_optional_axes(&plot, axes);
13623            let parsed = CT_ChartSpace::from_xml(xml.as_bytes())
13624                .unwrap_or_else(|error| panic!("{kind}: parse failed: {error}"));
13625            assert_eq!(parsed.chart.plot_area.plots().unwrap().len(), 1);
13626            assert_eq!(
13627                parsed.chart.plot_area.axes().unwrap().len(),
13628                usize::from(axes) * 2
13629            );
13630            let written = parsed
13631                .to_xml()
13632                .unwrap_or_else(|error| panic!("{kind}: write failed: {error}"));
13633            assert_eq!(
13634                parsed,
13635                CT_ChartSpace::from_xml(&written)
13636                    .unwrap_or_else(|error| panic!("{kind}: reparse failed: {error}"))
13637            );
13638        }
13639        if let Some(corpus) = require_or_skip_corpus() {
13640            verify_remaining_plot_viewer_gate(&corpus);
13641        }
13642    }
13643
13644    #[test]
13645    fn remaining_plot_families_write_fixed_prefixes_in_schema_order() {
13646        for (kind, plot, axes) in remaining_plot_fixtures() {
13647            let parsed =
13648                CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
13649            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13650            assert!(written.contains(&format!("<c:{kind}")));
13651            assert!(!written.contains(&format!("<q:{kind}")));
13652            let series = written.find("<c:ser").unwrap();
13653            let labels = written.find("<c:dLbls").unwrap();
13654            assert!(series < labels, "{kind}: series must precede labels");
13655            if matches!(kind, "areaChart" | "scatterChart" | "radarChart") {
13656                let property = match kind {
13657                    "areaChart" => "<c:grouping",
13658                    "scatterChart" => "<c:scatterStyle",
13659                    "radarChart" => "<c:radarStyle",
13660                    _ => unreachable!(),
13661                };
13662                assert!(written.find(property).unwrap() < series);
13663                assert!(labels < written.find("<c:axId").unwrap());
13664            } else {
13665                assert!(labels < written.find("<c:firstSliceAng").unwrap());
13666            }
13667            if kind == "doughnutChart" {
13668                assert!(
13669                    written.find("<c:firstSliceAng").unwrap()
13670                        < written.find("<c:holeSize").unwrap()
13671                );
13672            }
13673        }
13674    }
13675
13676    #[test]
13677    fn scatter_series_map_numeric_categories_and_values_to_x_and_y() {
13678        let plot = remaining_plot_fixtures()
13679            .into_iter()
13680            .find(|(kind, _, _)| *kind == "scatterChart")
13681            .unwrap()
13682            .1;
13683        let mut parsed =
13684            CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, true).as_bytes()).unwrap();
13685        let standalone_series = {
13686            let Plot::Scatter { series, .. } = &mut parsed.chart.plot_area.plots_mut().unwrap()[0]
13687            else {
13688                panic!("expected scatter plot");
13689            };
13690            let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
13691                panic!("expected numeric x values");
13692            };
13693            x_values.values[0] = 3.5;
13694            series[0].values.values[0] = 7.5;
13695            series[0].clone()
13696        };
13697        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13698        assert!(written.contains("<c:xVal>"));
13699        assert!(written.contains("<c:yVal>"));
13700        assert!(!written.contains("<c:cat>"));
13701        assert!(!written.contains("<c:val>"));
13702        assert!(written.contains("<c:v>3.5</c:v>"));
13703        assert!(written.contains("<c:v>7.5</c:v>"));
13704        let standalone = String::from_utf8(standalone_series.to_xml().unwrap()).unwrap();
13705        assert!(standalone.contains("<c:xVal>"));
13706        assert!(standalone.contains("<c:yVal>"));
13707        assert!(!standalone.contains("<c:cat>"));
13708        assert!(!standalone.contains("<c:val>"));
13709    }
13710
13711    #[test]
13712    fn malformed_remaining_plots_return_errors_without_panicking() {
13713        let series = plot_series(0);
13714        let scatter = numeric_plot_series(0);
13715        let opaque_bubble = series.replace(
13716            "</q:val></q:ser>",
13717            "</q:val><q:bubbleSize><x:opaque/></q:bubbleSize></q:ser>",
13718        );
13719        let cases = [
13720            r#"<q:pieChart/>"#.to_owned(),
13721            format!(r#"<q:pieChart>{series}<q:firstSliceAng val="361"/></q:pieChart>"#),
13722            format!(
13723                r#"<q:pieChart>{series}<q:firstSliceAng val="1"/><q:firstSliceAng val="2"/></q:pieChart>"#
13724            ),
13725            format!(r#"<q:doughnutChart>{series}<q:holeSize val="9"/></q:doughnutChart>"#),
13726            format!(
13727                r#"<q:doughnutChart>{series}<q:holeSize val="50"/><q:holeSize val="60"/></q:doughnutChart>"#
13728            ),
13729            format!(
13730                r#"<q:areaChart><q:grouping val="clustered"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
13731            ),
13732            format!(
13733                r#"<q:areaChart><q:grouping val="standard"/><q:grouping val="stacked"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
13734            ),
13735            format!(
13736                r#"<q:scatterChart><q:scatterStyle val="dots"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13737            ),
13738            format!(
13739                r#"<q:scatterChart><q:scatterStyle val="marker"/><q:scatterStyle val="line"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13740            ),
13741            format!(
13742                r#"<q:radarChart><q:radarStyle val="smooth"/>{series}<q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
13743            ),
13744            format!(
13745                r#"<q:radarChart><q:radarStyle val="marker"/>{series}<q:dLbls/><q:dLbls/><q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
13746            ),
13747            format!(r#"<q:pieChart>{series}<q:axId val="1"/></q:pieChart>"#),
13748            format!(
13749                r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/></q:areaChart>"#
13750            ),
13751            r#"<q:areaChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:areaChart>"#.to_owned(),
13752            format!(
13753                r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/><q:axId val="2"/><q:axId val="3"/></q:areaChart>"#
13754            ),
13755            format!(
13756                r#"<q:scatterChart><q:scatterStyle val="marker"/>{series}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13757            ),
13758            format!(
13759                r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13760                scatter
13761                    .replace("<q:yVal>", "<q:val>")
13762                    .replace("</q:yVal>", "</q:val>")
13763            ),
13764            format!(
13765                r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13766                scatter_without_x(0)
13767            ),
13768            format!(
13769                r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13770                scatter_without_y(0)
13771            ),
13772            format!(r#"<q:pieChart>{opaque_bubble}</q:pieChart>"#),
13773        ];
13774        for plot in cases {
13775            let xml = chart_with_optional_axes(&plot, false);
13776            let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
13777            assert!(result.is_ok(), "parser panicked for {plot}");
13778            assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
13779        }
13780
13781        let missing_axis = format!(
13782            r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="7"/></q:areaChart>"#
13783        );
13784        let missing_axis =
13785            CT_ChartSpace::from_xml(chart_with_optional_axes(&missing_axis, true).as_bytes())
13786                .unwrap_err();
13787        assert!(missing_axis.to_string().contains("missing axis 7"));
13788
13789        let values =
13790            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
13791        let mut bubble_series = Series::new(0, 0, values.clone());
13792        bubble_series.bubble_size = Some(values);
13793        assert!(Plot::pie(vec![bubble_series]).is_err());
13794    }
13795
13796    #[test]
13797    fn unsupported_plot_families_and_children_remain_byte_preserved() {
13798        for raw in [
13799            r#"<q:pie3DChart x:keep="3d"><q:ser><x:opaque/></q:ser></q:pie3DChart>"#,
13800            r#"<q:ofPieChart x:keep="of"><x:opaque/></q:ofPieChart>"#,
13801            r#"<q:stockChart x:keep="stock"><x:opaque/></q:stockChart>"#,
13802            r#"<q:surfaceChart x:keep="surface"><x:opaque/></q:surfaceChart>"#,
13803            r#"<q:bubbleChart x:keep="bubble"><x:opaque/></q:bubbleChart>"#,
13804        ] {
13805            let parsed =
13806                CT_ChartSpace::from_xml(chart_with_optional_axes(raw, false).as_bytes()).unwrap();
13807            assert!(parsed.chart.plot_area.plots().is_err());
13808            let written = parsed.to_xml().unwrap();
13809            assert!(
13810                written
13811                    .windows(raw.len())
13812                    .any(|window| window == raw.as_bytes())
13813            );
13814        }
13815
13816        let pie = remaining_plot_fixtures()[0].1.clone();
13817        let preserved = r#"<!--point--><q:dPt x:id="1"><q:explosion val="7"/><x:tail/></q:dPt>"#;
13818        let plot = pie.replace("<q:dLbls>", &format!("{preserved}<q:dLbls>"));
13819        let parsed =
13820            CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, false).as_bytes()).unwrap();
13821        let written = parsed.to_xml().unwrap();
13822        assert!(
13823            written
13824                .windows(preserved.len())
13825                .any(|window| window == preserved.as_bytes())
13826        );
13827
13828        for (kind, mut plot, axes) in [
13829            ("pieChart", remaining_plot_fixtures()[0].1.clone(), false),
13830            ("areaChart", remaining_plot_fixtures()[2].1.clone(), true),
13831        ] {
13832            plot = plot.replace(r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#, "");
13833            if kind == "pieChart" {
13834                plot = plot.replace(
13835                    "</q:extLst></q:pieChart>",
13836                    "</q:extLst><!--after-ext--></q:pieChart>",
13837                );
13838            } else {
13839                plot = plot.replace(
13840                    "</q:dropLines><q:axId",
13841                    "</q:dropLines><!--after-drop-lines--><q:axId",
13842                );
13843            }
13844            let mut parsed =
13845                CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
13846            let labels =
13847                CT_DLbls::from_xml(format!(r#"<c:dLbls xmlns:c="{C_NS}"/>"#).as_bytes()).unwrap();
13848            match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
13849                Plot::Pie { data_labels, .. } | Plot::Area { data_labels, .. } => {
13850                    *data_labels = Some(labels)
13851                }
13852                _ => panic!("expected pie or area"),
13853            }
13854            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13855            assert!(
13856                written.find("<c:dLbls").unwrap() < written.find("<q:extLst").unwrap(),
13857                "{kind}: inserted labels must precede the preserved extension"
13858            );
13859            if kind == "areaChart" {
13860                assert!(written.find("<c:dLbls").unwrap() < written.find("<q:dropLines").unwrap());
13861                assert!(
13862                    written.find("<q:dropLines").unwrap()
13863                        < written.find("<!--after-drop-lines-->").unwrap()
13864                );
13865            } else {
13866                assert!(
13867                    written.find("<q:extLst").unwrap() < written.find("<!--after-ext-->").unwrap()
13868                );
13869            }
13870        }
13871    }
13872
13873    #[test]
13874    fn every_supported_corpus_plot_round_trips_structurally() {
13875        let Some(corpus) = require_or_skip_corpus() else {
13876            return;
13877        };
13878        verify_fetched_corpus(&corpus);
13879        let mut pie_count = 0usize;
13880        let mut bar_count = 0usize;
13881        let mut line_count = 0usize;
13882        for path in manifest_paths() {
13883            let package = OpcPackage::open(corpus.join(path)).unwrap();
13884            for (part, xml) in &package.parts {
13885                if !is_chart_part(part) {
13886                    continue;
13887                }
13888                let parsed = CT_ChartSpace::from_xml(xml).unwrap();
13889                if let Ok(plots) = parsed.chart.plot_area.plots() {
13890                    for plot in plots {
13891                        match plot {
13892                            Plot::Pie { .. } => pie_count += 1,
13893                            Plot::Bar { .. } => bar_count += 1,
13894                            Plot::Line { .. } => line_count += 1,
13895                            _ => {}
13896                        }
13897                    }
13898                }
13899                let written = parsed.to_xml().unwrap();
13900                assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
13901            }
13902        }
13903        assert_eq!(pie_count, 1, "typed corpus pie coverage changed");
13904        assert_eq!(bar_count, 11, "typed corpus bar coverage changed");
13905        assert_eq!(line_count, 2, "typed corpus line coverage changed");
13906    }
13907
13908    fn standalone_axis(local: &str, children: &str) -> String {
13909        format!(
13910            r#"<c:{local} xmlns:c="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}">{children}</c:{local}>"#
13911        )
13912    }
13913
13914    fn minimal_axis(local: &str, id: &str, position: &str, cross_axis: &str) -> String {
13915        format!(
13916            r#"<c:{local}><c:axId val="{id}"/><c:scaling/><c:axPos val="{position}"/><c:crossAx val="{cross_axis}"/></c:{local}>"#
13917        )
13918    }
13919
13920    fn chart_with_axes(axes: &[String]) -> String {
13921        format!(
13922            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>"#,
13923            axes.join("")
13924        )
13925    }
13926
13927    #[test]
13928    fn data_labels_write_fixed_prefixes_in_schema_order() {
13929        let xml = format!(
13930            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>"#
13931        );
13932        let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
13933        assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0%");
13934        assert_eq!(parsed.position, Some(DataLabelPosition::OutsideEnd));
13935        assert_eq!(parsed.separator.as_deref(), Some(" / "));
13936        assert!(parsed.show_legend_key);
13937        assert!(parsed.show_value);
13938        assert!(!parsed.show_category_name);
13939        assert!(parsed.show_series_name);
13940        assert!(!parsed.show_percent);
13941        assert!(parsed.show_bubble_size);
13942
13943        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13944        assert!(written.starts_with(&format!(
13945            "<c:dLbls xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
13946        )));
13947        let mut cursor = 0usize;
13948        for tag in [
13949            "<q:dLbl",
13950            "<c:numFmt",
13951            "<q:spPr",
13952            "<q:txPr",
13953            "<c:dLblPos",
13954            "<c:showLegendKey",
13955            "<c:showVal",
13956            "<c:showCatName",
13957            "<c:showSerName",
13958            "<c:showPercent",
13959            "<c:showBubbleSize",
13960            "<c:separator",
13961            "<q:leaderLines",
13962            "<q:extLst",
13963        ] {
13964            let position = written[cursor..]
13965                .find(tag)
13966                .unwrap_or_else(|| panic!("missing ordered data-label tag {tag}"));
13967            cursor += position + tag.len();
13968        }
13969        assert_eq!(parsed, CT_DLbls::from_xml(written.as_bytes()).unwrap());
13970    }
13971
13972    #[test]
13973    fn percentage_formatted_label_renders_with_correct_text() {
13974        let Some(corpus) = require_or_skip_corpus() else {
13975            return;
13976        };
13977        verify_fetched_corpus(&corpus);
13978        assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
13979        assert_command_version("pdftotext", &["-v"], PDFTOTEXT_VERSION);
13980
13981        let source = corpus.join("bar-chart.pptx");
13982        let mut package = OpcPackage::open(&source).expect("open percentage-label source deck");
13983        let chart_part = "/ppt/charts/chart1.xml";
13984        let original_parts = package.parts.clone();
13985        let chart_xml = package
13986            .get_part(chart_part)
13987            .expect("bar chart part")
13988            .to_vec();
13989        let original_series = first_series_xml(&chart_xml).expect("bar chart series");
13990        let mut series = Series::from_xml(&original_series).expect("parse bar chart series");
13991        series.values.values.fill(0.25);
13992        let labels = CT_DLbls {
13993            number_format: Some(NumberFormat::new("0%".to_owned(), false).unwrap()),
13994            position: Some(DataLabelPosition::OutsideEnd),
13995            show_value: true,
13996            ..CT_DLbls::default()
13997        };
13998        series.data_labels = Some(labels);
13999        let replacement = series.to_xml().expect("serialize typed percentage labels");
14000        let candidate_chart = replace_once(&chart_xml, &original_series, &replacement);
14001        package.set_part(chart_part, candidate_chart);
14002        for (part, bytes) in &original_parts {
14003            if part == chart_part {
14004                assert_ne!(package.parts[part], *bytes);
14005            } else {
14006                assert_eq!(
14007                    package.parts[part], *bytes,
14008                    "unexpected changed part {part}"
14009                );
14010            }
14011        }
14012
14013        let temp_root =
14014            std::env::temp_dir().join(format!("oxml-chart-f123-label-gate-{}", std::process::id()));
14015        let profile = std::env::temp_dir().join(format!(
14016            "oxml-chart-f123-label-profile-{}",
14017            std::process::id()
14018        ));
14019        if temp_root.exists() {
14020            fs::remove_dir_all(&temp_root).expect("remove stale F-123 evidence");
14021        }
14022        if profile.exists() {
14023            fs::remove_dir_all(&profile).expect("remove stale F-123 profile");
14024        }
14025        fs::create_dir_all(&temp_root).expect("create F-123 evidence root");
14026        let unbound_candidate = temp_root.join("f123-percentage-label.pptx");
14027        package
14028            .save(&unbound_candidate)
14029            .expect("save percentage-label candidate");
14030        let candidate_sha = sha256(&unbound_candidate);
14031        let evidence_dir = temp_root.join(&candidate_sha);
14032        fs::create_dir(&evidence_dir).expect("create SHA-bound evidence directory");
14033        let candidate = evidence_dir.join("f123-percentage-label.pptx");
14034        fs::rename(&unbound_candidate, &candidate).expect("bind candidate to evidence SHA");
14035        assert_eq!(sha256(&candidate), candidate_sha);
14036
14037        let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
14038        let conversion = Command::new("soffice")
14039            .args([
14040                "--headless",
14041                &profile_argument,
14042                "--convert-to",
14043                "pdf:impress_pdf_Export",
14044                "--outdir",
14045            ])
14046            .arg(&evidence_dir)
14047            .arg(&candidate)
14048            .output()
14049            .expect("run pinned LibreOffice percentage-label gate");
14050        assert!(
14051            conversion.status.success(),
14052            "LibreOffice conversion failed: {}",
14053            String::from_utf8_lossy(&conversion.stderr)
14054        );
14055        let pdf = evidence_dir.join("f123-percentage-label.pdf");
14056        assert!(
14057            pdf.is_file(),
14058            "LibreOffice did not create {}",
14059            pdf.display()
14060        );
14061        let text_path = evidence_dir.join("f123-percentage-label.txt");
14062        let extraction = Command::new("pdftotext")
14063            .arg(&pdf)
14064            .arg(&text_path)
14065            .output()
14066            .expect("run pinned Poppler percentage-label extraction");
14067        assert!(
14068            extraction.status.success(),
14069            "pdftotext failed: {}",
14070            String::from_utf8_lossy(&extraction.stderr)
14071        );
14072        let extracted = fs::read_to_string(&text_path).expect("read percentage-label text");
14073        assert!(
14074            extracted.contains("25%"),
14075            "SHA-bound viewer text lacks 25%: {extracted:?}"
14076        );
14077        for artifact in [&candidate, &pdf, &text_path] {
14078            assert_eq!(
14079                artifact.parent(),
14080                Some(evidence_dir.as_path()),
14081                "evidence escaped candidate SHA"
14082            );
14083        }
14084        eprintln!("F-123 candidate/render/text evidence SHA-256 {candidate_sha}, extracted 25%");
14085        fs::remove_dir_all(&temp_root).expect("remove F-123 evidence");
14086        if profile.exists() {
14087            fs::remove_dir_all(&profile).expect("remove F-123 profile");
14088        }
14089    }
14090
14091    #[test]
14092    fn common_number_formats_project_cached_values_deterministically() {
14093        for (code, value, expected) in [
14094            ("General", 0.25, "0.25"),
14095            ("0", 12.6, "13"),
14096            ("0.0", 12.64, "12.6"),
14097            ("0.00", -12.645, "-12.64"),
14098            ("0%", 0.25, "25%"),
14099            ("0.0%", 0.256, "25.6%"),
14100            ("0.00%", 0.256, "25.60%"),
14101        ] {
14102            let format = NumberFormat::new(code.to_owned(), false).unwrap();
14103            assert_eq!(format.format_value(value).unwrap(), expected, "{code}");
14104        }
14105    }
14106
14107    #[test]
14108    fn malformed_data_labels_and_number_formats_return_errors_without_panicking() {
14109        let cases = [
14110            format!(
14111                r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="1"/><c:showVal val="0"/></c:dLbls>"#
14112            ),
14113            format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="yes"/></c:dLbls>"#),
14114            format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:dLblPos val="middle"/></c:dLbls>"#),
14115            format!(
14116                r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="" sourceLinked="1"/></c:dLbls>"#
14117            ),
14118            format!(
14119                r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="0" sourceLinked="maybe"/></c:dLbls>"#
14120            ),
14121        ];
14122        for xml in cases {
14123            let result = std::panic::catch_unwind(|| CT_DLbls::from_xml(xml.as_bytes()));
14124            assert!(result.is_ok(), "data-label parser panicked for {xml}");
14125            assert!(result.unwrap().is_err(), "malformed labels parsed: {xml}");
14126        }
14127
14128        let invalid_code = NumberFormat::new("bad\u{0}code".to_owned(), false);
14129        assert!(invalid_code.is_err());
14130        let unsupported = NumberFormat::new("#,##0".to_owned(), false).unwrap();
14131        assert!(unsupported.format_value(12.0).is_err());
14132        let general = NumberFormat::new("General".to_owned(), false).unwrap();
14133        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
14134            assert!(general.format_value(value).is_err());
14135        }
14136
14137        let invalid_separator = CT_DLbls {
14138            separator: Some("bad\u{0}separator".to_owned()),
14139            ..CT_DLbls::default()
14140        };
14141        assert!(invalid_separator.to_xml().is_err());
14142    }
14143
14144    #[test]
14145    fn data_labels_preserve_point_overrides_and_extensions_byte_for_byte() {
14146        let xml = format!(
14147            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>"#
14148        );
14149        let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
14150        let written = parsed.to_xml().unwrap();
14151        for raw in [
14152            br#"<!--before-->"#.as_slice(),
14153            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(),
14154            br#"<?after-point?>"#.as_slice(),
14155            br#"<q:spPr><x:collection-shape/></q:spPr>"#.as_slice(),
14156            br#"<q:txPr><x:collection-text/></q:txPr>"#.as_slice(),
14157            br#"<q:showLeaderLines val="1"/>"#.as_slice(),
14158            br#"<q:leaderLines><q:spPr><x:leader-shape/></q:spPr></q:leaderLines>"#.as_slice(),
14159            br#"<q:extLst><q:ext uri="collection"><x:tail/></q:ext></q:extLst>"#.as_slice(),
14160            br#"<!--after-->"#.as_slice(),
14161        ] {
14162            assert!(
14163                written.windows(raw.len()).any(|window| window == raw),
14164                "preserved data-label bytes changed: {}",
14165                String::from_utf8_lossy(raw)
14166            );
14167        }
14168        assert_eq!(parsed, CT_DLbls::from_xml(&written).unwrap());
14169    }
14170
14171    #[test]
14172    fn every_corpus_data_label_collection_round_trips_structurally() {
14173        let Some(corpus) = require_or_skip_corpus() else {
14174            return;
14175        };
14176        verify_fetched_corpus(&corpus);
14177        let mut label_collections = 0usize;
14178        let mut axis_number_formats = 0usize;
14179        for path in manifest_paths() {
14180            let package = OpcPackage::open(corpus.join(path))
14181                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14182            for (part, xml) in &package.parts {
14183                if !is_chart_part(part) {
14184                    continue;
14185                }
14186                let chart = CT_ChartSpace::from_xml(xml)
14187                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14188                for series in chart.chart.plot_area.series().unwrap_or_else(|error| {
14189                    panic!("{path} {part}: series projection failed: {error}")
14190                }) {
14191                    if let Some(labels) = series.data_labels {
14192                        let written = labels.to_xml().unwrap_or_else(|error| {
14193                            panic!("{path} {part}: label write failed: {error}")
14194                        });
14195                        let reparsed = CT_DLbls::from_xml(&written).unwrap_or_else(|error| {
14196                            panic!("{path} {part}: written labels parse failed: {error}")
14197                        });
14198                        assert_eq!(labels, reparsed, "{path} {part}: labels changed");
14199                        label_collections += 1;
14200                    }
14201                }
14202                for axis in chart.chart.plot_area.axes().unwrap_or_else(|error| {
14203                    panic!("{path} {part}: axis projection failed: {error}")
14204                }) {
14205                    if axis.number_format.is_some() {
14206                        let written = axis.to_xml().unwrap_or_else(|error| {
14207                            panic!("{path} {part}: axis write failed: {error}")
14208                        });
14209                        let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
14210                            panic!("{path} {part}: written axis parse failed: {error}")
14211                        });
14212                        assert_eq!(axis, reparsed, "{path} {part}: axis changed");
14213                        axis_number_formats += 1;
14214                    }
14215                }
14216            }
14217        }
14218        assert!(label_collections > 0, "the pinned corpus has no c:dLbls");
14219        assert!(
14220            axis_number_formats > 0,
14221            "the pinned corpus has no axis c:numFmt"
14222        );
14223        eprintln!(
14224            "ChartML data-label corpus gate checked {label_collections} collections and {axis_number_formats} axis number formats"
14225        );
14226    }
14227
14228    fn full_axis(local: &str, position: &str) -> String {
14229        format!(
14230            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}>"#
14231        )
14232    }
14233
14234    #[test]
14235    fn axis_id_pairs_are_reciprocal() {
14236        assert_eq!(
14237            AxisId::new(i64::from(i32::MIN)).unwrap().value(),
14238            i64::from(i32::MIN)
14239        );
14240        assert_eq!(
14241            AxisId::new(i64::from(u32::MAX)).unwrap().value(),
14242            i64::from(u32::MAX)
14243        );
14244        assert!(AxisId::new(i64::from(i32::MIN) - 1).is_err());
14245        assert!(AxisId::new(i64::from(u32::MAX) + 1).is_err());
14246
14247        let valid = chart_with_axes(&[
14248            minimal_axis("catAx", "-1884094432", "b", "-1884097184"),
14249            minimal_axis("valAx", "-1884097184", "l", "-1884094432"),
14250        ]);
14251        let axes = CT_ChartSpace::from_xml(valid.as_bytes())
14252            .unwrap()
14253            .chart
14254            .plot_area
14255            .axes()
14256            .unwrap();
14257        assert_eq!(axes.len(), 2);
14258        assert_eq!(axes[0].id.value(), -1_884_094_432);
14259        assert_eq!(axes[0].cross_axis, axes[1].id);
14260        assert_eq!(axes[1].cross_axis, axes[0].id);
14261
14262        let invalid_sets = [
14263            vec![
14264                minimal_axis("catAx", "1", "b", "2"),
14265                minimal_axis("valAx", "1", "l", "2"),
14266            ],
14267            vec![minimal_axis("catAx", "1", "b", "1")],
14268            vec![minimal_axis("catAx", "1", "b", "2")],
14269            vec![
14270                minimal_axis("catAx", "1", "b", "2"),
14271                minimal_axis("valAx", "2", "l", "3"),
14272                minimal_axis("serAx", "3", "r", "2"),
14273            ],
14274        ];
14275        for axes in invalid_sets {
14276            let chart = CT_ChartSpace::from_xml(chart_with_axes(&axes).as_bytes()).unwrap();
14277            assert!(chart.chart.plot_area.axes().is_err());
14278        }
14279
14280        let empty = CT_ChartSpace::from_xml(chart_with_axes(&[]).as_bytes()).unwrap();
14281        assert!(empty.chart.plot_area.axes().unwrap().is_empty());
14282    }
14283
14284    #[test]
14285    fn all_axis_forms_write_fixed_prefixes_in_schema_order() {
14286        for (local, kind, position) in [
14287            ("catAx", AxisKind::Category, AxisPosition::Bottom),
14288            ("valAx", AxisKind::Value, AxisPosition::Left),
14289            ("dateAx", AxisKind::Date, AxisPosition::Top),
14290            ("serAx", AxisKind::Series, AxisPosition::Right),
14291        ] {
14292            let xml = full_axis(local, position.as_str());
14293            let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
14294            assert_eq!(parsed.kind, kind);
14295            assert_eq!(parsed.id.value(), -1_125_255_920);
14296            assert_eq!(parsed.scaling.log_base, Some(10.0));
14297            assert_eq!(parsed.scaling.orientation, Orientation::MaxMin);
14298            assert_eq!(parsed.scaling.maximum, Some(100.0));
14299            assert_eq!(parsed.scaling.minimum, Some(-10.0));
14300            assert!(parsed.deleted);
14301            assert_eq!(parsed.position, position);
14302            assert!(parsed.major_gridlines.is_some());
14303            assert!(parsed.minor_gridlines.is_some());
14304            assert!(parsed.title.is_some());
14305            assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0");
14306            assert_eq!(parsed.major_tick_mark, TickMark::Cross);
14307            assert_eq!(parsed.minor_tick_mark, TickMark::Inside);
14308            assert_eq!(parsed.tick_label_position, TickLabelPosition::High);
14309            assert!(parsed.sp_pr.is_some());
14310            assert!(parsed.tx_pr.is_some());
14311            assert_eq!(parsed.cross_axis.value(), -1_004_303_696);
14312
14313            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14314            assert!(written.starts_with(&format!(
14315                "<c:{local} xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
14316            )));
14317            assert!(written.contains("<c:axId val=\"-1125255920\""));
14318            let mut cursor = 0usize;
14319            for tag in [
14320                "<c:axId",
14321                "<c:scaling",
14322                "<c:delete",
14323                "<c:axPos",
14324                "<c:majorGridlines",
14325                "<c:minorGridlines",
14326                "<c:title",
14327                "<c:numFmt",
14328                "<c:majorTickMark",
14329                "<c:minorTickMark",
14330                "<c:tickLblPos",
14331                "<c:spPr",
14332                "<c:txPr",
14333                "<c:crossAx",
14334            ] {
14335                let position = written[cursor..]
14336                    .find(tag)
14337                    .unwrap_or_else(|| panic!("missing ordered axis tag {tag}"));
14338                cursor += position + tag.len();
14339            }
14340            assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
14341        }
14342
14343        let mut constructed = Axis::new(
14344            AxisKind::Value,
14345            AxisId::new(1).unwrap(),
14346            AxisPosition::Left,
14347            AxisId::new(2).unwrap(),
14348        );
14349        assert_eq!(
14350            constructed,
14351            Axis::from_xml(&constructed.to_xml().unwrap()).unwrap()
14352        );
14353        constructed.kind = AxisKind::Category;
14354        constructed.id = AxisId::new(3).unwrap();
14355        constructed.cross_axis = AxisId::new(4).unwrap();
14356        constructed.scaling.minimum = Some(0.0);
14357        constructed.scaling.maximum = Some(10.0);
14358        let written = constructed.to_xml().unwrap();
14359        assert!(written.starts_with(b"<c:catAx"));
14360        assert_eq!(constructed, Axis::from_xml(&written).unwrap());
14361    }
14362
14363    #[test]
14364    fn axis_equality_normalizes_ids_without_losing_lexical_preservation() {
14365        let padded = Axis::from_xml(
14366            standalone_axis(
14367                "catAx",
14368                "<c:axId val=\"01\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"002\"/>",
14369            )
14370            .as_bytes(),
14371        )
14372        .unwrap();
14373        let normalized = Axis::from_xml(
14374            standalone_axis(
14375                "catAx",
14376                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14377            )
14378            .as_bytes(),
14379        )
14380        .unwrap();
14381        assert_eq!(padded, normalized);
14382
14383        let preserved = String::from_utf8(padded.to_xml().unwrap()).unwrap();
14384        assert!(preserved.contains("<c:axId val=\"01\""));
14385        assert!(preserved.contains("<c:crossAx val=\"002\""));
14386
14387        let mut mutated = padded;
14388        mutated.id = AxisId::new(3).unwrap();
14389        mutated.cross_axis = AxisId::new(4).unwrap();
14390        let rewritten = String::from_utf8(mutated.to_xml().unwrap()).unwrap();
14391        assert!(rewritten.contains("<c:axId val=\"3\""));
14392        assert!(rewritten.contains("<c:crossAx val=\"4\""));
14393        assert!(!rewritten.contains("val=\"01\""));
14394        assert!(!rewritten.contains("val=\"002\""));
14395    }
14396
14397    #[test]
14398    fn malformed_axis_values_return_errors_without_panicking() {
14399        let cases = [
14400            standalone_axis(
14401                "catAx",
14402                "<c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14403            ),
14404            standalone_axis(
14405                "catAx",
14406                "<c:axId val=\"1\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14407            ),
14408            standalone_axis(
14409                "catAx",
14410                "<c:axId val=\"1\"/><c:scaling/><c:crossAx val=\"2\"/>",
14411            ),
14412            standalone_axis(
14413                "catAx",
14414                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/>",
14415            ),
14416            standalone_axis(
14417                "catAx",
14418                "<c:axId val=\"-2147483649\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14419            ),
14420            standalone_axis(
14421                "catAx",
14422                "<c:axId val=\"4294967296\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14423            ),
14424            standalone_axis(
14425                "catAx",
14426                "<c:axId val=\"1\"/><c:scaling><c:logBase val=\"1\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14427            ),
14428            standalone_axis(
14429                "catAx",
14430                "<c:axId val=\"1\"/><c:scaling><c:max val=\"NaN\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14431            ),
14432            standalone_axis(
14433                "catAx",
14434                "<c:axId val=\"1\"/><c:scaling><c:max val=\"1\"/><c:min val=\"2\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14435            ),
14436            standalone_axis(
14437                "catAx",
14438                "<c:axId val=\"1\"/><c:scaling><c:orientation val=\"sideways\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14439            ),
14440            standalone_axis(
14441                "catAx",
14442                "<c:axId val=\"1\"/><c:scaling/><c:delete val=\"yes\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14443            ),
14444            standalone_axis(
14445                "catAx",
14446                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"middle\"/><c:crossAx val=\"2\"/>",
14447            ),
14448            standalone_axis(
14449                "catAx",
14450                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:majorTickMark val=\"near\"/><c:crossAx val=\"2\"/>",
14451            ),
14452            standalone_axis(
14453                "catAx",
14454                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:tickLblPos val=\"middle\"/><c:crossAx val=\"2\"/>",
14455            ),
14456            standalone_axis(
14457                "catAx",
14458                "<c:axId val=\"1\"/><c:axId val=\"2\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14459            ),
14460            standalone_axis(
14461                "catAx",
14462                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt sourceLinked=\"1\"/><c:crossAx val=\"2\"/>",
14463            ),
14464            standalone_axis(
14465                "catAx",
14466                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt formatCode=\"0\" sourceLinked=\"maybe\"/><c:crossAx val=\"2\"/>",
14467            ),
14468        ];
14469        for xml in cases {
14470            let result = std::panic::catch_unwind(|| Axis::from_xml(xml.as_bytes()));
14471            assert!(result.is_ok(), "axis parser panicked for {xml}");
14472            assert!(result.unwrap().is_err(), "malformed axis parsed: {xml}");
14473        }
14474
14475        let mut axis = Axis::new(
14476            AxisKind::Value,
14477            AxisId::new(1).unwrap(),
14478            AxisPosition::Left,
14479            AxisId::new(2).unwrap(),
14480        );
14481        axis.scaling.maximum = Some(f64::INFINITY);
14482        assert!(axis.to_xml().is_err());
14483    }
14484
14485    #[test]
14486    fn axes_preserve_unmodelled_children_byte_for_byte() {
14487        let xml = format!(
14488            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>"#
14489        );
14490        let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
14491        let mut relabelled = parsed.clone();
14492        relabelled.kind = AxisKind::Value;
14493        assert!(
14494            relabelled.to_xml().is_err(),
14495            "a parsed category axis must not retain its tail under a value-axis root"
14496        );
14497        let written = parsed.to_xml().unwrap();
14498        for raw in [
14499            br#"<!--before-->"#.as_slice(),
14500            br#"<?after-id?>"#.as_slice(),
14501            br#"<!--scale-->"#.as_slice(),
14502            br#"<q:extLst><q:ext uri="scale"><x:data/></q:ext></q:extLst>"#.as_slice(),
14503            br#"<x:grid/>"#.as_slice(),
14504            br#"<x:title/>"#.as_slice(),
14505            br#"<q:crosses val="autoZero"/>"#.as_slice(),
14506            br#"<q:auto val="1"><x:auto/></q:auto>"#.as_slice(),
14507            br#"<q:extLst><q:ext uri="axis"><x:tail/></q:ext></q:extLst>"#.as_slice(),
14508            br#"<!--after-->"#.as_slice(),
14509        ] {
14510            assert!(
14511                written.windows(raw.len()).any(|window| window == raw),
14512                "preserved axis bytes changed: {}",
14513                String::from_utf8_lossy(raw)
14514            );
14515        }
14516        let written = String::from_utf8(written).unwrap();
14517        assert!(written.starts_with("<c:catAx"));
14518        assert!(written.contains("x:keep=\"axis\""));
14519        assert!(written.contains("x:keep=\"id\""));
14520        assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
14521    }
14522
14523    #[test]
14524    fn every_corpus_axis_round_trips_structurally() {
14525        let Some(corpus) = require_or_skip_corpus() else {
14526            return;
14527        };
14528        verify_fetched_corpus(&corpus);
14529        let mut axis_count = 0usize;
14530        let mut chart_parts = 0usize;
14531        let mut kinds = HashSet::new();
14532        for path in manifest_paths() {
14533            let package = OpcPackage::open(corpus.join(path))
14534                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14535            for (part, xml) in &package.parts {
14536                if !is_chart_part(part) {
14537                    continue;
14538                }
14539                let chart = CT_ChartSpace::from_xml(xml)
14540                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14541                let axes = chart.chart.plot_area.axes().unwrap_or_else(|error| {
14542                    panic!("{path} {part}: axis projection failed: {error}")
14543                });
14544                for axis in axes {
14545                    kinds.insert(axis.kind);
14546                    let written = axis.to_xml().unwrap_or_else(|error| {
14547                        panic!("{path} {part}: axis write failed: {error}")
14548                    });
14549                    let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
14550                        panic!("{path} {part}: written axis parse failed: {error}")
14551                    });
14552                    assert_eq!(axis, reparsed, "{path} {part}: axis model changed");
14553                    axis_count += 1;
14554                }
14555                chart_parts += 1;
14556            }
14557        }
14558        assert!(
14559            axis_count > 0,
14560            "the pinned corpus contained no ChartML axes"
14561        );
14562        assert!(kinds.contains(&AxisKind::Category));
14563        assert!(kinds.contains(&AxisKind::Value));
14564
14565        for kind in ["dateAx", "serAx"] {
14566            let parsed = Axis::from_xml(
14567                standalone_axis(
14568                    kind,
14569                    "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14570                )
14571                .as_bytes(),
14572            )
14573            .unwrap();
14574            assert_eq!(parsed, Axis::from_xml(&parsed.to_xml().unwrap()).unwrap());
14575        }
14576        eprintln!(
14577            "ChartML axis corpus gate checked {axis_count} axes across {chart_parts} chart parts"
14578        );
14579    }
14580
14581    #[test]
14582    fn series_formula_and_cache_are_consistent_with_one_source() {
14583        let values = NumericData::new(
14584            "'Sales 24'!$B$2:$B$4".to_owned(),
14585            "0.0".to_owned(),
14586            vec![4.25, 8.5, 17.0],
14587        )
14588        .unwrap();
14589        let mut series = Series::new(3, 1, values);
14590        series.categories = Some(AxisData::String(
14591            StringRef::new(
14592                "'Sales 24'!$A$2:$A$4".to_owned(),
14593                vec!["North".to_owned(), "South".to_owned(), "West".to_owned()],
14594            )
14595            .unwrap(),
14596        ));
14597
14598        let written = String::from_utf8(series.to_xml().unwrap()).unwrap();
14599        assert!(written.contains("<c:f>&apos;Sales 24&apos;!$B$2:$B$4</c:f>"));
14600        assert!(written.contains("<c:formatCode>0.0</c:formatCode>"));
14601        assert_eq!(written.matches("<c:ptCount val=\"3\"").count(), 2);
14602        for (index, value) in ["4.25", "8.5", "17"].iter().enumerate() {
14603            assert!(written.contains(&format!("<c:pt idx=\"{index}\"><c:v>{value}</c:v></c:pt>")));
14604        }
14605        let reparsed = Series::from_xml(written.as_bytes()).unwrap();
14606        assert_eq!(reparsed.index, series.index);
14607        assert_eq!(reparsed.order, series.order);
14608        match (&reparsed.categories, &series.categories) {
14609            (Some(AxisData::String(left)), Some(AxisData::String(right))) => {
14610                assert_eq!(left.formula, right.formula);
14611                assert_eq!(left.values, right.values);
14612            }
14613            _ => panic!("expected string categories"),
14614        }
14615        assert_eq!(reparsed.values.formula, series.values.formula);
14616        assert_eq!(reparsed.values.format_code, series.values.format_code);
14617        assert_eq!(reparsed.values.values, series.values.values);
14618    }
14619
14620    #[test]
14621    fn string_and_numeric_references_write_fixed_prefixes_in_schema_order() {
14622        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>"#;
14623        let string_ref = StringRef::from_xml(string_xml).unwrap();
14624        let written = String::from_utf8(string_ref.to_xml().unwrap()).unwrap();
14625        assert!(written.starts_with("<c:strRef xmlns:c="));
14626        assert!(written.find("<c:f>").unwrap() < written.find("<c:strCache>").unwrap());
14627        assert!(written.find("<c:ptCount").unwrap() < written.find("<c:pt idx=").unwrap());
14628
14629        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>"#;
14630        let numeric = NumericData::from_xml(numeric_xml).unwrap();
14631        let written = String::from_utf8(numeric.to_xml().unwrap()).unwrap();
14632        let positions: Vec<_> = [
14633            "<c:f>",
14634            "<c:numCache>",
14635            "<c:formatCode>",
14636            "<c:ptCount",
14637            "<c:pt idx=",
14638        ]
14639        .iter()
14640        .map(|tag| written.find(tag).unwrap())
14641        .collect();
14642        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14643        assert_eq!(numeric, NumericData::from_xml(written.as_bytes()).unwrap());
14644    }
14645
14646    #[test]
14647    fn mixed_chartml_aliases_resolve_by_namespace_uri() {
14648        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>"#;
14649        let parsed = Series::from_xml(xml).unwrap();
14650        assert_eq!(parsed.index, 4);
14651        assert_eq!(parsed.order, 2);
14652        assert_eq!(parsed.values.values, vec![1.5, 2.5]);
14653        assert!(matches!(parsed.categories, Some(AxisData::String(_))));
14654        assert_eq!(parsed, Series::from_xml(&parsed.to_xml().unwrap()).unwrap());
14655    }
14656
14657    #[test]
14658    fn plot_area_series_ignores_inherited_foreign_plot_aliases() {
14659        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>"#;
14660        let chart = CT_ChartSpace::from_xml(xml).unwrap();
14661        let series = chart.chart.plot_area.series().unwrap();
14662        assert_eq!(series.len(), 1);
14663        assert_eq!(series[0].index, 1);
14664        let written = String::from_utf8(series[0].to_xml().unwrap()).unwrap();
14665        assert!(
14666            written.contains(r#"xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart""#)
14667        );
14668        assert!(written.contains(r#"xmlns:x="urn:producer""#));
14669        assert!(written.contains("<q:marker><x:data/></q:marker>"));
14670        assert_eq!(series[0], Series::from_xml(written.as_bytes()).unwrap());
14671
14672        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>"#;
14673        let chart = CT_ChartSpace::from_xml(conflicting).unwrap();
14674        assert!(chart.chart.plot_area.series().is_err());
14675    }
14676
14677    #[test]
14678    fn malformed_series_and_cache_values_return_errors_without_panicking() {
14679        let cases: &[&[u8]] = &[
14680            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>"#,
14681            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/></c:ser>"#,
14682            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>"#,
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></c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="0"/></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</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>"#,
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:$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>"#,
14686            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>"#,
14687            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>"#,
14688            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>"#,
14689            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>"#,
14690            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>"#,
14691            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>"#,
14692            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>"#,
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 xmlns:c="urn:producer"><q:formatCode>General</q:formatCode><q:ptCount val="0"/></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 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>"#,
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>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>"#,
14696            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>"#,
14697        ];
14698        for xml in cases {
14699            let result = std::panic::catch_unwind(|| Series::from_xml(xml));
14700            assert!(result.is_ok(), "series parser panicked");
14701            assert!(
14702                result.unwrap().is_err(),
14703                "malformed series parsed: {}",
14704                String::from_utf8_lossy(xml)
14705            );
14706        }
14707        assert!(
14708            NumericData::new(
14709                "S!$A$1".to_owned(),
14710                "General".to_owned(),
14711                vec![f64::INFINITY]
14712            )
14713            .is_err()
14714        );
14715    }
14716
14717    #[test]
14718    fn chart_text_rejects_xml_forbidden_characters_and_escapes_metacharacters() {
14719        assert!(StringRef::new("S!$A$1\u{1}".to_owned(), Vec::new()).is_err());
14720        assert!(StringRef::new("S!$A$1".to_owned(), vec!["invalid\u{1}".to_owned()]).is_err());
14721        assert!(
14722            NumericData::new("S!$A$1\u{1}".to_owned(), "General".to_owned(), Vec::new()).is_err()
14723        );
14724        assert!(
14725            NumericData::new("S!$A$1".to_owned(), "General\u{1}".to_owned(), Vec::new()).is_err()
14726        );
14727
14728        let mut strings = StringRef::new("S!$A$1".to_owned(), vec!["valid".to_owned()]).unwrap();
14729        strings.formula = "invalid\u{1}".to_owned();
14730        assert!(strings.to_xml().is_err());
14731        strings.formula = "S!$A$1".to_owned();
14732        strings.values[0] = "invalid\u{1}".to_owned();
14733        assert!(strings.to_xml().is_err());
14734
14735        let mut numbers =
14736            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap();
14737        numbers.formula = "invalid\u{1}".to_owned();
14738        assert!(numbers.to_xml().is_err());
14739        numbers.formula = "S!$A$1".to_owned();
14740        numbers.format_code = "invalid\u{1}".to_owned();
14741        assert!(numbers.to_xml().is_err());
14742
14743        let strings =
14744            StringRef::new("S!$A$1&".to_owned(), vec!["North < West & East".to_owned()]).unwrap();
14745        let written = String::from_utf8(strings.to_xml().unwrap()).unwrap();
14746        assert!(written.contains("S!$A$1&amp;"));
14747        assert!(written.contains("North &lt; West &amp; East"));
14748    }
14749
14750    #[test]
14751    fn series_preserves_unmodelled_children_byte_for_byte() {
14752        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>"#;
14753        let parsed = Series::from_xml(xml).unwrap();
14754        let written = parsed.to_xml().unwrap();
14755        for raw in [
14756            br#"<!--before-->"#.as_slice(),
14757            br#"<x:between/>"#.as_slice(),
14758            br#"<q:dPt x:id="one"><q:idx val="0"/></q:dPt>"#.as_slice(),
14759            br#"<q:trendline x:id="trend"/>"#.as_slice(),
14760            br#"<q:extLst><q:ext uri="keep"><x:data/></q:ext></q:extLst>"#.as_slice(),
14761            br#"<!--point-->"#.as_slice(),
14762            br#"<x:cacheExt/>"#.as_slice(),
14763        ] {
14764            assert!(
14765                written.windows(raw.len()).any(|window| window == raw),
14766                "preserved series bytes changed: {}",
14767                String::from_utf8_lossy(raw)
14768            );
14769        }
14770        let written = String::from_utf8(written.clone()).unwrap();
14771        assert!(written.contains(r#"x:keep="series""#));
14772        assert!(written.contains(r#"x:keep="formula""#));
14773        assert!(written.contains("<c:dLbls"));
14774        assert!(written.contains(r#"x:id="labels""#));
14775        assert!(parsed.data_labels.is_some());
14776        assert_eq!(parsed, Series::from_xml(written.as_bytes()).unwrap());
14777    }
14778
14779    #[test]
14780    fn public_series_edits_do_not_duplicate_or_drop_preserved_payloads() {
14781        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>"#;
14782        let parsed = Series::from_xml(opaque_xml).unwrap();
14783
14784        let mut edited = parsed.clone();
14785        edited.name = Some(StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap());
14786        assert!(edited.to_xml().is_err());
14787
14788        let mut edited = parsed.clone();
14789        edited.categories = Some(AxisData::Numeric(
14790            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap(),
14791        ));
14792        assert!(edited.to_xml().is_err());
14793
14794        let mut edited = parsed;
14795        edited.bubble_size =
14796            Some(NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
14797        assert!(edited.to_xml().is_err());
14798
14799        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>"#;
14800        let parsed = Series::from_xml(cache_xml).unwrap();
14801        let tail = "<c:extLst><c:ext uri=\"keep\"><x:data/></c:ext></c:extLst>";
14802
14803        let mut shortened = parsed.clone();
14804        shortened.values.values.truncate(1);
14805        let written = String::from_utf8(shortened.to_xml().unwrap()).unwrap();
14806        assert!(!written.contains("<c:pt idx=\"1\""));
14807        assert!(written.find("<c:pt idx=\"0\"").unwrap() < written.find(tail).unwrap());
14808        assert!(Series::from_xml(written.as_bytes()).is_ok());
14809
14810        let mut grown = parsed;
14811        grown.values.values.push(3.0);
14812        let written = String::from_utf8(grown.to_xml().unwrap()).unwrap();
14813        assert!(written.find("<c:pt idx=\"2\"").unwrap() < written.find(tail).unwrap());
14814        assert!(Series::from_xml(written.as_bytes()).is_ok());
14815
14816        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>"#;
14817        let mut edited = Series::from_xml(ordered_xml).unwrap();
14818        edited.name = Some(StringRef::new("S!$B$1".to_owned(), Vec::new()).unwrap());
14819        edited.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
14820        edited.categories = Some(AxisData::String(
14821            StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap(),
14822        ));
14823        edited.bubble_size =
14824            Some(NumericData::new("S!$C$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
14825        let written = String::from_utf8(edited.to_xml().unwrap()).unwrap();
14826        let positions: Vec<_> = [
14827            "<c:tx>",
14828            "<c:spPr",
14829            "<c:marker>",
14830            "<c:cat>",
14831            "<c:val>",
14832            "<c:bubbleSize>",
14833            "<c:extLst>",
14834        ]
14835        .iter()
14836        .map(|tag| written.find(tag).unwrap())
14837        .collect();
14838        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14839        assert!(Series::from_xml(written.as_bytes()).is_ok());
14840    }
14841
14842    #[test]
14843    fn every_corpus_series_round_trips_structurally() {
14844        let Some(corpus) = require_or_skip_corpus() else {
14845            return;
14846        };
14847        verify_fetched_corpus(&corpus);
14848        let mut series_count = 0usize;
14849        let mut chart_parts = 0usize;
14850        for path in manifest_paths() {
14851            let package = OpcPackage::open(corpus.join(path))
14852                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14853            for (part, xml) in &package.parts {
14854                if !is_chart_part(part) {
14855                    continue;
14856                }
14857                let chart = CT_ChartSpace::from_xml(xml)
14858                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14859                let parsed_series =
14860                    chart.chart.plot_area.series().unwrap_or_else(|error| {
14861                        panic!("{path} {part}: series parse failed: {error}")
14862                    });
14863                for series in parsed_series {
14864                    let written = series.to_xml().unwrap_or_else(|error| {
14865                        panic!("{path} {part}: series write failed: {error}")
14866                    });
14867                    let reparsed = Series::from_xml(&written).unwrap_or_else(|error| {
14868                        panic!("{path} {part}: written series parse failed: {error}")
14869                    });
14870                    assert_eq!(series, reparsed, "{path} {part}: series model changed");
14871                    series_count += 1;
14872                }
14873                chart_parts += 1;
14874            }
14875        }
14876        assert!(
14877            series_count > 0,
14878            "the pinned corpus contained no supported series"
14879        );
14880        eprintln!(
14881            "ChartML series corpus gate checked {series_count} series across {chart_parts} chart parts"
14882        );
14883    }
14884
14885    #[test]
14886    fn chart_space_reads_aliases_and_writes_fixed_prefixes_in_schema_order() {
14887        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>"#;
14888        let parsed = CT_ChartSpace::from_xml(xml).unwrap();
14889        assert!(!parsed.chart.auto_title_deleted);
14890        assert!(!parsed.chart.plot_vis_only);
14891        assert_eq!(parsed.chart.disp_blanks_as, DispBlanksAs::Span);
14892        assert!(parsed.chart.title.is_some());
14893        assert!(parsed.chart.legend.is_some());
14894        assert!(parsed.sp_pr.is_some());
14895        assert!(parsed.tx_pr.is_some());
14896
14897        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14898        assert!(written.starts_with(
14899            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""#
14900        ));
14901        for tag in [
14902            "<c:chart>",
14903            "<c:title>",
14904            "<c:plotArea>",
14905            "<c:legend>",
14906            "<c:plotVisOnly",
14907            "<c:dispBlanksAs",
14908            "<c:spPr>",
14909            "<c:txPr>",
14910        ] {
14911            assert!(written.contains(tag), "missing fixed-prefix tag {tag}");
14912        }
14913        let positions: Vec<_> = [
14914            "<c:title>",
14915            "<c:plotArea>",
14916            "<c:legend>",
14917            "<c:plotVisOnly",
14918            "<c:dispBlanksAs",
14919            "<c:spPr>",
14920            "<c:txPr>",
14921        ]
14922        .iter()
14923        .map(|tag| written.find(tag).unwrap())
14924        .collect();
14925        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14926        assert!(written.contains("<a:solidFill><a:srgbClr"));
14927        assert!(written.contains("<a:bodyPr/><a:p/>"));
14928        assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
14929    }
14930
14931    #[test]
14932    fn public_core_edits_keep_preserved_children_in_their_schema_slots() {
14933        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>"#;
14934        let mut parsed = CT_ChartSpace::from_xml(xml).unwrap();
14935        parsed.chart.auto_title_deleted = true;
14936        parsed.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
14937        parsed.tx_pr =
14938            Some(CT_TextBody::from_xml(br#"<a:txBody><a:bodyPr/><a:p/></a:txBody>"#).unwrap());
14939        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14940        let positions: Vec<_> = [
14941            "<c:autoTitleDeleted",
14942            "<c:pivotFmts>",
14943            "<c:plotArea",
14944            "<c:spPr",
14945            "<c:txPr",
14946            "<c:externalData",
14947        ]
14948        .iter()
14949        .map(|tag| written.find(tag).unwrap())
14950        .collect();
14951        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14952        assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
14953    }
14954
14955    #[test]
14956    fn malformed_core_chart_values_return_errors_without_panicking() {
14957        let cases: &[&[u8]] = &[
14958            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"/>"#,
14959            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart/></c:chartSpace>"#,
14960            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:plotArea/></c:chart></c:chartSpace>"#,
14961            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:autoTitleDeleted val="yes"/><c:plotArea/></c:chart></c:chartSpace>"#,
14962            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:dispBlanksAs val="empty"/></c:chart></c:chartSpace>"#,
14963            br#"<c:chartSpace xmlns:c="urn:wrong"><c:chart><c:plotArea/></c:chart></c:chartSpace>"#,
14964            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:foreign"><x:chart><x:plotArea/></x:chart></c:chartSpace>"#,
14965            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>"#,
14966            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:title/><c:title/><c:plotArea/></c:chart></c:chartSpace>"#,
14967            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>"#,
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:txPr><a:bodyPr/><a:p><x:r><x:t>foreign</x:t></x:r></a:p></c:txPr></c:chartSpace>"#,
14969            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>"#,
14970        ];
14971        for xml in cases {
14972            let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml));
14973            assert!(result.is_ok(), "ChartML parser panicked");
14974            assert!(
14975                result.unwrap().is_err(),
14976                "malformed ChartML parsed: {}",
14977                String::from_utf8_lossy(xml)
14978            );
14979        }
14980    }
14981
14982    #[test]
14983    fn point_label_overrides_render_without_changing_preserved_xml() {
14984        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>"#;
14985        let plot = bar_plot("").replace(
14986            r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
14987            &format!(
14988                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>"#
14989            ),
14990        );
14991        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
14992        let plot = &chart.chart.plot_area.plots().unwrap()[0];
14993        let Plot::Bar { data_labels, .. } = plot else {
14994            panic!("expected bar plot");
14995        };
14996        let labels = data_labels.as_ref().unwrap();
14997        let written = labels.to_xml().unwrap();
14998        assert!(
14999            written
15000                .windows(raw_overrides.len())
15001                .any(|window| window == raw_overrides.as_bytes())
15002        );
15003        let overrides = super::point_label_overrides(labels).unwrap();
15004        assert_eq!(overrides.keys().copied().collect::<Vec<_>>(), vec![1]);
15005
15006        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
15007        let mut rendered = Vec::new();
15008        super::render_data_labels(
15009            plot,
15010            chart_bounds(),
15011            DispBlanksAs::Gap,
15012            super::GeometryOptions::default(),
15013            &mut fonts,
15014            &mut rendered,
15015        )
15016        .unwrap();
15017        let labels: Vec<_> = rendered
15018            .iter()
15019            .filter_map(|element| match element {
15020                PositionedElement::Text(run) => Some(run),
15021                _ => None,
15022            })
15023            .collect();
15024        assert_eq!(labels.len(), 2);
15025        assert_eq!(labels[0].text, "North, 1");
15026        assert_eq!(labels[1].text, "South / 2.0");
15027        assert_point_near(labels[0].origin, Point { x: 50.0, y: 105.0 });
15028        assert_point_near(labels[1].origin, Point { x: 158.0, y: 70.0 });
15029        assert_eq!(
15030            CT_DLbls::from_xml(&written).unwrap().to_xml().unwrap(),
15031            written
15032        );
15033    }
15034
15035    #[test]
15036    fn malformed_point_label_overrides_return_contextual_errors() {
15037        let cases = [
15038            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:showVal val="1"/></q:dLbl></q:dLbls>"#,
15039            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>"#,
15040            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="point"/></q:dLbl></q:dLbls>"#,
15041            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>"#,
15042            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>"#,
15043            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>"#,
15044            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>"#,
15045        ];
15046        for xml in cases {
15047            let labels = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
15048            let result = std::panic::catch_unwind(|| super::point_label_overrides(&labels));
15049            assert!(result.is_ok(), "point-label parser panicked for {xml}");
15050            assert!(
15051                result.unwrap().is_err(),
15052                "malformed point labels parsed: {xml}"
15053            );
15054        }
15055
15056        let foreign = format!(
15057            r#"<q:dLbls xmlns:q="{C_NS}" xmlns:x="urn:foreign"><x:dLbl><x:idx val="0"/></x:dLbl></q:dLbls>"#
15058        );
15059        let labels = CT_DLbls::from_xml(foreign.as_bytes()).unwrap();
15060        assert!(super::point_label_overrides(&labels).unwrap().is_empty());
15061    }
15062
15063    #[test]
15064    fn percentage_label_aggregation_rejects_nonfinite_totals() {
15065        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
15066        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
15067        let Plot::Bar { series, .. } = plot else {
15068            panic!("expected bar plot");
15069        };
15070        series[0].values.values = vec![f64::MAX, f64::MAX];
15071        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
15072        super::render_data_labels(
15073            plot,
15074            chart_bounds(),
15075            DispBlanksAs::Gap,
15076            super::GeometryOptions::default(),
15077            &mut fonts,
15078            &mut Vec::new(),
15079        )
15080        .expect("value-only labels do not need a percentage total");
15081        let Plot::Bar { data_labels, .. } = plot else {
15082            panic!("expected bar plot");
15083        };
15084        data_labels.as_mut().unwrap().show_percent = true;
15085        let error = super::render_data_labels(
15086            plot,
15087            chart_bounds(),
15088            DispBlanksAs::Gap,
15089            super::GeometryOptions::default(),
15090            &mut fonts,
15091            &mut Vec::new(),
15092        )
15093        .unwrap_err();
15094        assert!(error.to_string().contains("data-label percentage total"));
15095
15096        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>"#;
15097        let plot_xml = bar_plot("").replace(
15098            r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
15099            &format!(
15100                r#"<q:dLbls>{overrides}<q:showVal val="1"/><q:showPercent val="1"/></q:dLbls>"#
15101            ),
15102        );
15103        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
15104        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
15105        let Plot::Bar { series, .. } = plot else {
15106            panic!("expected bar plot");
15107        };
15108        series[0].values.values = vec![f64::MAX, f64::MAX];
15109        super::render_data_labels(
15110            plot,
15111            chart_bounds(),
15112            DispBlanksAs::Gap,
15113            super::GeometryOptions::default(),
15114            &mut fonts,
15115            &mut Vec::new(),
15116        )
15117        .expect("effective point overrides disable percentage aggregation");
15118    }
15119
15120    #[test]
15121    fn percentage_labels_use_effective_number_format_precision() {
15122        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
15123        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
15124        let Plot::Bar {
15125            series,
15126            data_labels,
15127            ..
15128        } = plot
15129        else {
15130            panic!("expected bar plot");
15131        };
15132        series[0].values.values = vec![1.0, 2.0];
15133        let labels = data_labels.as_mut().unwrap();
15134        labels.show_value = false;
15135        labels.show_percent = true;
15136        labels.number_format = Some(NumberFormat::new("0.00%".to_owned(), false).unwrap());
15137        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
15138        let mut rendered = Vec::new();
15139        super::render_data_labels(
15140            plot,
15141            chart_bounds(),
15142            DispBlanksAs::Gap,
15143            super::GeometryOptions::default(),
15144            &mut fonts,
15145            &mut rendered,
15146        )
15147        .unwrap();
15148        let text: Vec<_> = rendered
15149            .iter()
15150            .filter_map(|element| match element {
15151                PositionedElement::Text(run) => Some(run.text.as_str()),
15152                _ => None,
15153            })
15154            .collect();
15155        assert_eq!(text, vec!["33.33%", "66.67%"]);
15156
15157        let point_override = r#"<q:dLbl><q:idx val="0"/><q:numFmt formatCode="0.00%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbl>"#;
15158        let plot_xml = bar_plot("").replace(
15159            r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
15160            &format!(
15161                r#"<q:dLbls>{point_override}<q:numFmt formatCode="0%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbls>"#
15162            ),
15163        );
15164        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
15165        let plot = &chart.chart.plot_area.plots().unwrap()[0];
15166        let mut rendered = Vec::new();
15167        super::render_data_labels(
15168            plot,
15169            chart_bounds(),
15170            DispBlanksAs::Gap,
15171            super::GeometryOptions::default(),
15172            &mut fonts,
15173            &mut rendered,
15174        )
15175        .unwrap();
15176        let text: Vec<_> = rendered
15177            .iter()
15178            .filter_map(|element| match element {
15179                PositionedElement::Text(run) => Some(run.text.as_str()),
15180                _ => None,
15181            })
15182            .collect();
15183        assert_eq!(text, vec!["33.33%", "67%"]);
15184    }
15185
15186    #[test]
15187    fn core_chart_shells_preserve_unmodelled_children_byte_for_byte() {
15188        let preserved = [
15189            br#"<x:root x:id="1"><x:nested>one &amp; two</x:nested><!--root--></x:root>"#
15190                .as_slice(),
15191            br#"<x:title x:id="2"><x:nested><![CDATA[title]]></x:nested></x:title>"#.as_slice(),
15192            br#"<x:plot x:id="3"><x:nested>plot</x:nested></x:plot>"#.as_slice(),
15193            br#"<x:legend x:id="4"><x:nested>legend</x:nested></x:legend>"#.as_slice(),
15194            br#"<x:extension x:id="5"><x:nested>extension</x:nested></x:extension>"#.as_slice(),
15195            br#"<!--space-comment-->"#.as_slice(),
15196            br#"<?space processing?>"#.as_slice(),
15197            br#"<!--chart-comment-->"#.as_slice(),
15198            br#"<?title processing?>"#.as_slice(),
15199            br#"<!--flag-comment-->"#.as_slice(),
15200            br#"<?flag processing?>"#.as_slice(),
15201            br#"<x:producerShape/>"#.as_slice(),
15202            br#"<x:producerText/>"#.as_slice(),
15203        ];
15204        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>"#;
15205        let parsed = CT_ChartSpace::from_xml(xml).unwrap();
15206        let written = parsed.to_xml().unwrap();
15207        for raw in preserved {
15208            assert!(
15209                written.windows(raw.len()).any(|window| window == raw),
15210                "preserved subtree changed: {}",
15211                String::from_utf8_lossy(raw)
15212            );
15213        }
15214        let written_text = String::from_utf8(written.clone()).unwrap();
15215        assert!(written_text.contains(r#"x:keep="one &amp; two""#));
15216        assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
15217    }
15218
15219    #[test]
15220    fn every_corpus_chart_part_round_trips_structurally() {
15221        let Some(corpus) = require_or_skip_corpus() else {
15222            return;
15223        };
15224        verify_fetched_corpus(&corpus);
15225        let mut chart_parts = 0usize;
15226        let mut decks_with_charts = HashSet::new();
15227        for path in manifest_paths() {
15228            let package = OpcPackage::open(corpus.join(path))
15229                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
15230            for (part, xml) in &package.parts {
15231                if !is_chart_part(part) {
15232                    continue;
15233                }
15234                let parsed = CT_ChartSpace::from_xml(xml)
15235                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
15236                let written = parsed
15237                    .to_xml()
15238                    .unwrap_or_else(|error| panic!("{path} {part}: write failed: {error}"));
15239                let original_preserved = preserved_chartml_regions(xml)
15240                    .unwrap_or_else(|error| panic!("{path} {part}: original scan failed: {error}"));
15241                let written_preserved = preserved_chartml_regions(&written)
15242                    .unwrap_or_else(|error| panic!("{path} {part}: written scan failed: {error}"));
15243                assert_eq!(
15244                    original_preserved.len(),
15245                    written_preserved.len(),
15246                    "{path} {part}: preserved ChartML region count changed"
15247                );
15248                for (index, (original, written)) in original_preserved
15249                    .iter()
15250                    .zip(&written_preserved)
15251                    .enumerate()
15252                {
15253                    assert_eq!(
15254                        original.parent_path, written.parent_path,
15255                        "{path} {part}: preserved region {index} parent changed"
15256                    );
15257                    assert_eq!(
15258                        original.boundary, written.boundary,
15259                        "{path} {part}: preserved region {index} boundary changed"
15260                    );
15261                    assert_eq!(
15262                        original.sibling_order, written.sibling_order,
15263                        "{path} {part}: preserved region {index} order changed"
15264                    );
15265                    assert_eq!(
15266                        original.xml, written.xml,
15267                        "{path} {part}: preserved region {index} bytes changed"
15268                    );
15269                }
15270                let reparsed = CT_ChartSpace::from_xml(&written)
15271                    .unwrap_or_else(|error| panic!("{path} {part}: written parse failed: {error}"));
15272                assert_eq!(parsed, reparsed, "{path} {part}: core model changed");
15273                chart_parts += 1;
15274                decks_with_charts.insert(path);
15275            }
15276        }
15277        assert!(
15278            chart_parts > 0,
15279            "the pinned corpus contained no chart parts"
15280        );
15281        eprintln!(
15282            "ChartML corpus gate checked {chart_parts} chart parts across {} decks",
15283            decks_with_charts.len()
15284        );
15285    }
15286
15287    #[derive(Debug, Eq, PartialEq)]
15288    struct PreservedChartRegion {
15289        parent_path: Vec<Vec<u8>>,
15290        boundary: usize,
15291        sibling_order: usize,
15292        xml: Vec<u8>,
15293    }
15294
15295    struct PreservationFrame {
15296        local_name: Vec<u8>,
15297        boundary: usize,
15298        next_sibling: usize,
15299    }
15300
15301    fn preserved_chartml_regions(xml: &[u8]) -> Result<Vec<PreservedChartRegion>, String> {
15302        let mut reader = Reader::from_reader(xml);
15303        let mut buffer = Vec::new();
15304        let mut parents: Vec<PreservationFrame> = Vec::new();
15305        let mut preserved = Vec::new();
15306        loop {
15307            match reader.read_event_into(&mut buffer) {
15308                Ok(Event::Start(element)) => {
15309                    let child = local_name(element.name().as_ref()).to_vec();
15310                    if parents.is_empty() && child == b"chartSpace" {
15311                        parents.push(PreservationFrame {
15312                            local_name: child,
15313                            boundary: 0,
15314                            next_sibling: 0,
15315                        });
15316                    } else if let Some(parent) = parents.last()
15317                        && is_opaque_chartml_child(&parent.local_name, &child)
15318                    {
15319                        let boundary =
15320                            preserved_child_boundary(&parent.local_name, &child, parent.boundary);
15321                        let xml = capture_element(&mut reader, &element)
15322                            .map_err(|error| error.to_string())?;
15323                        record_preserved_region(&mut parents, &mut preserved, boundary, xml);
15324                        advance_preservation_boundary(&mut parents, &child);
15325                    } else {
15326                        advance_preservation_boundary(&mut parents, &child);
15327                        if is_chartml_preservation_parent(&child) {
15328                            parents.push(PreservationFrame {
15329                                local_name: child,
15330                                boundary: 0,
15331                                next_sibling: 0,
15332                            });
15333                        } else {
15334                            capture_element(&mut reader, &element)
15335                                .map_err(|error| error.to_string())?;
15336                        }
15337                    }
15338                }
15339                Ok(Event::Empty(element)) => {
15340                    let name = element.name();
15341                    let child = local_name(name.as_ref()).to_vec();
15342                    if let Some(parent) = parents.last()
15343                        && is_opaque_chartml_child(&parent.local_name, &child)
15344                    {
15345                        let boundary =
15346                            preserved_child_boundary(&parent.local_name, &child, parent.boundary);
15347                        let xml =
15348                            capture_empty_element(&element).map_err(|error| error.to_string())?;
15349                        record_preserved_region(&mut parents, &mut preserved, boundary, xml);
15350                    }
15351                    advance_preservation_boundary(&mut parents, &child);
15352                }
15353                Ok(
15354                    event @ (Event::Comment(_)
15355                    | Event::PI(_)
15356                    | Event::CData(_)
15357                    | Event::GeneralRef(_)),
15358                ) if !parents.is_empty() => {
15359                    let boundary = parents.last().expect("preservation parent").boundary;
15360                    let xml = capture_event(event).map_err(|error| error.to_string())?;
15361                    record_preserved_region(&mut parents, &mut preserved, boundary, xml);
15362                }
15363                Ok(Event::End(_)) => {
15364                    parents.pop();
15365                }
15366                Ok(Event::Eof) => break,
15367                Ok(_) => {}
15368                Err(error) => return Err(error.to_string()),
15369            }
15370            buffer.clear();
15371        }
15372        Ok(preserved)
15373    }
15374
15375    fn record_preserved_region(
15376        parents: &mut [PreservationFrame],
15377        preserved: &mut Vec<PreservedChartRegion>,
15378        boundary: usize,
15379        xml: Vec<u8>,
15380    ) {
15381        let sibling_order = parents.last().expect("preservation parent").next_sibling;
15382        parents
15383            .last_mut()
15384            .expect("preservation parent")
15385            .next_sibling += 1;
15386        preserved.push(PreservedChartRegion {
15387            parent_path: parents
15388                .iter()
15389                .map(|parent| parent.local_name.clone())
15390                .collect(),
15391            boundary,
15392            sibling_order,
15393            xml,
15394        });
15395    }
15396
15397    fn advance_preservation_boundary(parents: &mut [PreservationFrame], child: &[u8]) {
15398        let Some(parent) = parents.last_mut() else {
15399            return;
15400        };
15401        parent.boundary = parent
15402            .boundary
15403            .max(preserved_boundary_after(&parent.local_name, child));
15404    }
15405
15406    fn preserved_child_boundary(parent: &[u8], child: &[u8], current: usize) -> usize {
15407        match parent {
15408            b"chartSpace" => match child {
15409                b"date1904" => 0,
15410                b"lang" => 1,
15411                b"roundedCorners" => 2,
15412                b"style" => 3,
15413                b"clrMapOvr" => 4,
15414                b"pivotSource" => 5,
15415                b"protection" => 6,
15416                b"externalData" => 10,
15417                b"printSettings" => 11,
15418                b"userShapes" => 12,
15419                b"extLst" => 13,
15420                _ => current,
15421            },
15422            b"chart" => match child {
15423                b"pivotFmts" => 2,
15424                b"view3D" => 3,
15425                b"floor" => 4,
15426                b"sideWall" => 5,
15427                b"backWall" => 6,
15428                b"showDLblsOverMax" | b"extLst" => 11,
15429                _ => current,
15430            },
15431            _ => current,
15432        }
15433    }
15434
15435    fn preserved_boundary_after(parent: &[u8], child: &[u8]) -> usize {
15436        match parent {
15437            b"chartSpace" => match child {
15438                b"date1904" => 1,
15439                b"lang" => 2,
15440                b"roundedCorners" => 3,
15441                b"style" => 4,
15442                b"clrMapOvr" => 5,
15443                b"pivotSource" => 6,
15444                b"protection" => 7,
15445                b"chart" => 8,
15446                b"spPr" => 9,
15447                b"txPr" => 10,
15448                b"externalData" => 11,
15449                b"printSettings" => 12,
15450                b"userShapes" => 13,
15451                b"extLst" => 14,
15452                _ => 0,
15453            },
15454            b"chart" => match child {
15455                b"title" => 1,
15456                b"autoTitleDeleted" => 2,
15457                b"pivotFmts" => 3,
15458                b"view3D" => 4,
15459                b"floor" => 5,
15460                b"sideWall" => 6,
15461                b"backWall" => 7,
15462                b"plotArea" => 8,
15463                b"legend" => 9,
15464                b"plotVisOnly" => 10,
15465                b"dispBlanksAs" => 11,
15466                b"showDLblsOverMax" | b"extLst" => 11,
15467                _ => 0,
15468            },
15469            b"pieChart" => match child {
15470                b"ser" => 1,
15471                b"dLbls" => 2,
15472                b"firstSliceAng" => 3,
15473                _ => 0,
15474            },
15475            b"doughnutChart" => match child {
15476                b"ser" => 1,
15477                b"dLbls" => 2,
15478                b"firstSliceAng" => 3,
15479                b"holeSize" => 4,
15480                _ => 0,
15481            },
15482            b"areaChart" => match child {
15483                b"grouping" => 1,
15484                b"ser" => 2,
15485                b"dLbls" => 3,
15486                b"axId" => 4,
15487                _ => 0,
15488            },
15489            b"scatterChart" | b"radarChart" => match child {
15490                b"scatterStyle" | b"radarStyle" => 1,
15491                b"ser" => 2,
15492                b"dLbls" => 3,
15493                b"axId" => 4,
15494                _ => 0,
15495            },
15496            _ => 0,
15497        }
15498    }
15499
15500    fn is_opaque_chartml_child(parent: &[u8], child: &[u8]) -> bool {
15501        match parent {
15502            b"chartSpace" => !matches!(child, b"chart" | b"spPr" | b"txPr"),
15503            b"chart" => !matches!(
15504                child,
15505                b"title"
15506                    | b"autoTitleDeleted"
15507                    | b"plotArea"
15508                    | b"legend"
15509                    | b"plotVisOnly"
15510                    | b"dispBlanksAs"
15511            ),
15512            b"plotArea" => !matches!(
15513                child,
15514                b"barChart"
15515                    | b"lineChart"
15516                    | b"pieChart"
15517                    | b"doughnutChart"
15518                    | b"areaChart"
15519                    | b"scatterChart"
15520                    | b"radarChart"
15521                    | b"catAx"
15522                    | b"valAx"
15523                    | b"dateAx"
15524                    | b"serAx"
15525            ),
15526            b"pieChart" => !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng"),
15527            b"doughnutChart" => {
15528                !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng" | b"holeSize")
15529            }
15530            b"areaChart" => !matches!(child, b"grouping" | b"ser" | b"dLbls" | b"axId"),
15531            b"scatterChart" => !matches!(child, b"scatterStyle" | b"ser" | b"dLbls" | b"axId"),
15532            b"radarChart" => !matches!(child, b"radarStyle" | b"ser" | b"dLbls" | b"axId"),
15533            b"title" | b"legend" => true,
15534            _ => false,
15535        }
15536    }
15537
15538    fn is_chartml_preservation_parent(parent: &[u8]) -> bool {
15539        matches!(
15540            parent,
15541            b"chartSpace"
15542                | b"chart"
15543                | b"title"
15544                | b"plotArea"
15545                | b"pieChart"
15546                | b"doughnutChart"
15547                | b"areaChart"
15548                | b"scatterChart"
15549                | b"radarChart"
15550                | b"legend"
15551                | b"autoTitleDeleted"
15552                | b"plotVisOnly"
15553                | b"dispBlanksAs"
15554        )
15555    }
15556
15557    #[test]
15558    fn oxml_chart_is_an_explicit_publication_candidate() {
15559        let manifest = include_str!("../Cargo.toml");
15560        assert!(manifest.contains("name = \"oxml-chart\""));
15561        assert!(manifest.contains("version = \"0.14.0\""));
15562        assert!(manifest.contains("publish = true"));
15563        for dependency in [
15564            "oxml-core.workspace",
15565            "oxml-drawing.workspace",
15566            "quick-xml.workspace",
15567        ] {
15568            assert!(manifest.contains(dependency));
15569        }
15570        assert!(!manifest.contains("rpptx.workspace"));
15571        assert!(!manifest.contains("rdocx.workspace"));
15572    }
15573
15574    fn first_series_xml(xml: &[u8]) -> Option<Vec<u8>> {
15575        let mut reader = Reader::from_reader(xml);
15576        let mut buffer = Vec::new();
15577        loop {
15578            match reader.read_event_into(&mut buffer).ok()? {
15579                Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
15580                    return capture_element(&mut reader, &element).ok();
15581                }
15582                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
15583                    return capture_empty_element(&element).ok();
15584                }
15585                Event::Eof => return None,
15586                _ => {}
15587            }
15588            buffer.clear();
15589        }
15590    }
15591
15592    fn replace_once(haystack: &[u8], needle: &[u8], replacement: &[u8]) -> Vec<u8> {
15593        let offset = haystack
15594            .windows(needle.len())
15595            .position(|window| window == needle)
15596            .expect("serialized source series occurs in chart part");
15597        assert!(
15598            !haystack[offset + needle.len()..]
15599                .windows(needle.len())
15600                .any(|window| window == needle),
15601            "source series unexpectedly occurs more than once"
15602        );
15603        let mut result = Vec::with_capacity(haystack.len() - needle.len() + replacement.len());
15604        result.extend_from_slice(&haystack[..offset]);
15605        result.extend_from_slice(replacement);
15606        result.extend_from_slice(&haystack[offset + needle.len()..]);
15607        result
15608    }
15609
15610    fn assert_command_version(program: &str, arguments: &[&str], expected: &str) {
15611        let output = Command::new(program)
15612            .args(arguments)
15613            .output()
15614            .unwrap_or_else(|error| panic!("run {program} version check: {error}"));
15615        assert!(
15616            output.status.success(),
15617            "{program} version check failed: {}",
15618            String::from_utf8_lossy(&output.stderr)
15619        );
15620        let stdout = String::from_utf8(output.stdout).expect("version stdout is UTF-8");
15621        let stderr = String::from_utf8(output.stderr).expect("version stderr is UTF-8");
15622        let actual = if stdout.trim().is_empty() {
15623            stderr.trim()
15624        } else {
15625            stdout.trim()
15626        };
15627        assert_eq!(
15628            actual.lines().next(),
15629            Some(expected),
15630            "{program} version drift"
15631        );
15632    }
15633
15634    fn sha256(path: &Path) -> String {
15635        let output = Command::new("shasum")
15636            .args(["-a", "256"])
15637            .arg(path)
15638            .output()
15639            .unwrap_or_else(|error| panic!("{}: run shasum: {error}", path.display()));
15640        assert!(
15641            output.status.success(),
15642            "{}: shasum failed: {}",
15643            path.display(),
15644            String::from_utf8_lossy(&output.stderr)
15645        );
15646        String::from_utf8(output.stdout)
15647            .expect("shasum output is UTF-8")
15648            .split_whitespace()
15649            .next()
15650            .expect("shasum digest")
15651            .to_owned()
15652    }
15653
15654    fn workspace_root() -> PathBuf {
15655        Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
15656    }
15657
15658    fn corpus_dir() -> PathBuf {
15659        std::env::var_os("RDOCX_PPTX_CORPUS_DIR")
15660            .map(PathBuf::from)
15661            .unwrap_or_else(|| workspace_root().join("corpus/pptx"))
15662    }
15663
15664    fn require_or_skip_corpus() -> Option<PathBuf> {
15665        let corpus = corpus_dir();
15666        if corpus.is_dir() {
15667            return Some(corpus);
15668        }
15669        assert_ne!(
15670            std::env::var_os("RDOCX_PPTX_CORPUS_REQUIRED").as_deref(),
15671            Some(std::ffi::OsStr::new("1")),
15672            "the pinned corpus is required but {} does not exist",
15673            corpus.display()
15674        );
15675        eprintln!(
15676            "ChartML corpus gate skipped because {} is absent",
15677            corpus.display()
15678        );
15679        None
15680    }
15681
15682    fn verify_fetched_corpus(corpus: &Path) {
15683        let status = Command::new("python3")
15684            .arg(workspace_root().join("scripts/fetch_pptx_corpus.py"))
15685            .arg("--check")
15686            .env("RDOCX_PPTX_CORPUS_DIR", corpus)
15687            .status()
15688            .expect("run corpus verifier");
15689        assert!(status.success(), "pinned corpus verification failed");
15690    }
15691
15692    fn manifest_paths() -> Vec<&'static str> {
15693        let mut lines = MANIFEST.lines();
15694        assert_eq!(lines.next(), Some("path\tproducer\tsha256\turl"));
15695        let paths: Vec<_> = lines
15696            .map(|line| line.split('\t').next().expect("manifest path"))
15697            .collect();
15698        assert_eq!(paths.len(), EXPECTED_DECKS);
15699        paths
15700    }
15701
15702    fn is_chart_part(part: &str) -> bool {
15703        let Some(stem) = part
15704            .strip_prefix("/ppt/charts/chart")
15705            .and_then(|name| name.strip_suffix(".xml"))
15706        else {
15707            return false;
15708        };
15709        !stem.is_empty() && stem.bytes().all(|byte| byte.is_ascii_digit())
15710    }
15711
15712    fn plot_series(index: u32) -> String {
15713        format!(
15714            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>"#
15715        )
15716    }
15717
15718    fn sparse_category_series() -> &'static str {
15719        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>"#
15720    }
15721
15722    fn sparse_scatter_series() -> &'static str {
15723        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>"#
15724    }
15725
15726    fn chart_with_plot(plot: &str) -> String {
15727        format!(
15728            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>"#
15729        )
15730    }
15731
15732    fn chart_with_optional_axes(plot: &str, axes: bool) -> String {
15733        let axis_xml = if axes {
15734            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>"#
15735        } else {
15736            ""
15737        };
15738        format!(
15739            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>"#
15740        )
15741    }
15742
15743    fn numeric_plot_series(index: u32) -> String {
15744        format!(
15745            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>"#
15746        )
15747    }
15748
15749    fn scatter_without_x(index: u32) -> String {
15750        format!(
15751            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>"#
15752        )
15753    }
15754
15755    fn scatter_without_y(index: u32) -> String {
15756        format!(
15757            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>"#
15758        )
15759    }
15760
15761    fn remaining_plot_fixtures() -> Vec<(&'static str, String, bool)> {
15762        let series = plot_series(0);
15763        let scatter = numeric_plot_series(0);
15764        vec![
15765            (
15766                "pieChart",
15767                format!(
15768                    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>"#
15769                ),
15770                false,
15771            ),
15772            (
15773                "doughnutChart",
15774                format!(
15775                    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>"#
15776                ),
15777                false,
15778            ),
15779            (
15780                "areaChart",
15781                format!(
15782                    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>"#
15783                ),
15784                true,
15785            ),
15786            (
15787                "scatterChart",
15788                format!(
15789                    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>"#
15790                ),
15791                true,
15792            ),
15793            (
15794                "radarChart",
15795                format!(
15796                    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>"#
15797                ),
15798                true,
15799            ),
15800        ]
15801    }
15802
15803    fn bar_plot(extra: &str) -> String {
15804        format!(
15805            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>"#,
15806            plot_series(0)
15807        )
15808    }
15809
15810    fn line_plot(extra: &str) -> String {
15811        format!(
15812            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>"#,
15813            plot_series(0)
15814        )
15815    }
15816
15817    #[test]
15818    fn bar_and_line_plots_round_trip_and_render() {
15819        for (xml, is_bar) in [
15820            (chart_with_plot(&bar_plot("")), true),
15821            (chart_with_plot(&line_plot("")), false),
15822        ] {
15823            let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15824            let plots = parsed.chart.plot_area.plots().unwrap();
15825            assert_eq!(plots.len(), 1);
15826            assert_eq!(matches!(plots[0], Plot::Bar { .. }), is_bar);
15827            assert_eq!(matches!(plots[0], Plot::Line { .. }), !is_bar);
15828            assert_eq!(parsed.chart.plot_area.axes().unwrap().len(), 2);
15829            let written = parsed.to_xml().unwrap();
15830            let reparsed = CT_ChartSpace::from_xml(&written).unwrap();
15831            assert_eq!(parsed, reparsed);
15832        }
15833        if let Some(corpus) = require_or_skip_corpus() {
15834            verify_bar_and_line_viewer_gate(&corpus);
15835        }
15836    }
15837
15838    #[test]
15839    fn ppm_parser_preserves_whitespace_valued_first_pixels() {
15840        for first_pixel in *b" \n\r\t" {
15841            let mut ppm = b"P6\n1 1\n255\n".to_vec();
15842            ppm.extend_from_slice(&[first_pixel, 0x7f, 0xff]);
15843            let (width, height, pixels) = ppm_pixels(&ppm);
15844            assert_eq!((width, height), (1, 1));
15845            assert_eq!(pixels, [first_pixel, 0x7f, 0xff]);
15846        }
15847
15848        let ppm = b"P6\r\n1 1\r\n255\r\n\x7f\xff";
15849        assert_eq!(ppm_pixels(ppm).2, [b'\n', 0x7f, 0xff]);
15850    }
15851
15852    #[test]
15853    fn bar_and_line_plots_write_fixed_prefixes_in_schema_order() {
15854        for xml in [
15855            chart_with_plot(&bar_plot(r#"<q:extLst><x:bar-tail/></q:extLst>"#)),
15856            chart_with_plot(&line_plot(r#"<q:extLst><x:line-tail/></q:extLst>"#)),
15857        ] {
15858            let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15859            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15860            assert!(written.contains("<c:barChart") || written.contains("<c:lineChart"));
15861            assert!(!written.contains("<q:barChart"));
15862            assert!(!written.contains("<q:lineChart"));
15863            if written.contains("<c:barChart") {
15864                let positions: Vec<_> = [
15865                    "<c:barDir",
15866                    "<c:grouping",
15867                    "<q:varyColors",
15868                    "<c:ser",
15869                    "<c:dLbls",
15870                    "<c:gapWidth",
15871                    "<c:overlap",
15872                    "<q:serLines",
15873                    "<c:axId",
15874                    "<q:extLst",
15875                ]
15876                .iter()
15877                .map(|tag| written.find(tag).unwrap())
15878                .collect();
15879                assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15880            } else {
15881                let positions: Vec<_> = [
15882                    "<c:grouping",
15883                    "<q:varyColors",
15884                    "<c:ser",
15885                    "<c:dLbls",
15886                    "<q:dropLines",
15887                    "<c:marker",
15888                    "<c:smooth",
15889                    "<c:axId",
15890                    "<q:extLst",
15891                ]
15892                .iter()
15893                .map(|tag| written.find(tag).unwrap())
15894                .collect();
15895                assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15896            }
15897            assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
15898        }
15899
15900        for plot in [bar_plot(""), line_plot("")] {
15901            let first = plot_series(0);
15902            let with_series_boundary = plot.replacen(
15903                &first,
15904                &format!(
15905                    "{first}<!--between-series--><?series-boundary?>\n  {}",
15906                    plot_series(1)
15907                ),
15908                1,
15909            );
15910            let with_axis_boundary = with_series_boundary.replacen(
15911                r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
15912                r#"<q:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
15913  <q:axId val="-1884097184"/>"#,
15914                1,
15915            );
15916            let parsed =
15917                CT_ChartSpace::from_xml(chart_with_plot(&with_axis_boundary).as_bytes()).unwrap();
15918            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15919            assert!(written.contains("</c:ser><!--between-series--><?series-boundary?>\n  <c:ser"));
15920            assert!(written.contains(
15921                r#"<c:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
15922  <c:axId val="-1884097184"/>"#
15923            ));
15924            assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
15925        }
15926    }
15927
15928    #[test]
15929    fn malformed_bar_and_line_plots_return_errors_without_panicking() {
15930        let valid_series = plot_series(0);
15931        let cases = [
15932            format!(r#"<q:barChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15933            format!(r#"<q:barChart><q:barDir val="diagonal"/><q:grouping val="clustered"/>{valid_series}<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:gapWidth val="501"/><q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15935            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>"#),
15936            format!(r#"<q:lineChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
15937            format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:marker val="maybe"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
15938            r#"<q:lineChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#.to_owned(),
15939            format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:axId val="1"/><q:axId val="1"/></q:lineChart>"#),
15940            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>"#),
15941            format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15942        ];
15943        for plot in cases {
15944            let xml = chart_with_plot(&plot);
15945            let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
15946            assert!(result.is_ok(), "plot parser panicked for {plot}");
15947            assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
15948        }
15949
15950        let missing_axis = format!(
15951            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>"#,
15952            bar_plot("")
15953        );
15954        assert!(CT_ChartSpace::from_xml(missing_axis.as_bytes()).is_err());
15955
15956        let values =
15957            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
15958        let series = vec![Series::new(0, 0, values)];
15959        let ids = [AxisId::new(1).unwrap(), AxisId::new(2).unwrap()];
15960        let mut invalid =
15961            Plot::bar(BarDirection::Column, BarGrouping::Clustered, series, ids).unwrap();
15962        if let Plot::Bar { gap_width, .. } = &mut invalid {
15963            *gap_width = 501;
15964        }
15965        assert!(invalid.validate().is_err());
15966    }
15967
15968    #[test]
15969    fn unsupported_and_combo_plots_remain_byte_preserved() {
15970        let bar = bar_plot("");
15971        let line = line_plot("");
15972        let combo = chart_with_plot(&format!("<!--before-->{bar}<?between?>{line}<!--after-->"));
15973        let parsed = CT_ChartSpace::from_xml(combo.as_bytes()).unwrap();
15974        assert!(parsed.chart.plot_area.plots().is_err());
15975        let written = parsed.to_xml().unwrap();
15976        for raw in [
15977            bar.as_bytes(),
15978            line.as_bytes(),
15979            br#"<!--before-->"#.as_slice(),
15980            br#"<?between?>"#.as_slice(),
15981            br#"<!--after-->"#.as_slice(),
15982        ] {
15983            assert!(written.windows(raw.len()).any(|window| window == raw));
15984        }
15985
15986        let unsupported = chart_with_plot(
15987            r#"<q:bar3DChart x:keep="three"><q:barDir val="col"/><q:ser><x:opaque/></q:ser></q:bar3DChart>"#,
15988        );
15989        let parsed = CT_ChartSpace::from_xml(unsupported.as_bytes()).unwrap();
15990        assert!(parsed.chart.plot_area.plots().is_err());
15991        assert!(
15992            String::from_utf8(parsed.to_xml().unwrap())
15993                .unwrap()
15994                .contains(r#"<q:bar3DChart x:keep="three">"#)
15995        );
15996    }
15997
15998    #[test]
15999    fn public_plot_edits_preserve_axes_and_unselected_payloads() {
16000        let xml = chart_with_plot(&bar_plot(
16001            r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#,
16002        ));
16003        let mut parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16004        let axes_before = parsed.chart.plot_area.axes().unwrap();
16005        match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
16006            Plot::Bar {
16007                gap_width,
16008                overlap,
16009                series,
16010                ..
16011            } => {
16012                *gap_width = 225;
16013                *overlap = -25;
16014                series[0].index = 7;
16015                series[0].values.values[0] = 9.0;
16016            }
16017            _ => panic!("expected bar plot"),
16018        }
16019        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
16020        assert!(written.contains(r#"<c:gapWidth val="225"/>"#));
16021        assert!(written.contains(r#"<c:overlap val="-25"/>"#));
16022        assert!(written.contains("<c:v>9</c:v>"));
16023        assert!(written.contains("<q:varyColors val=\"1\"/>"));
16024        assert!(written.find("<q:varyColors").unwrap() < written.find("<c:ser").unwrap());
16025        assert!(written.contains("<q:serLines><x:line/></q:serLines>"));
16026        assert!(written.contains(r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#));
16027        let reparsed = CT_ChartSpace::from_xml(written.as_bytes()).unwrap();
16028        assert_eq!(axes_before, reparsed.chart.plot_area.axes().unwrap());
16029
16030        let first = plot_series(0);
16031        let colliding_series =
16032            bar_plot("").replacen(&first, &format!("{first}{}", plot_series(1)), 1);
16033        let mut collision =
16034            CT_ChartSpace::from_xml(chart_with_plot(&colliding_series).as_bytes()).unwrap();
16035        if let Plot::Bar { series, .. } = &mut collision.chart.plot_area.plots_mut().unwrap()[0] {
16036            series[0].index = series[1].index;
16037            series[0].order = series[1].order;
16038        }
16039        let collision = String::from_utf8(collision.to_xml().unwrap()).unwrap();
16040        assert!(collision.find("<q:varyColors").unwrap() < collision.find("<c:ser").unwrap());
16041
16042        let mut inserted = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16043        if let Plot::Bar { series, .. } = &mut inserted.chart.plot_area.plots_mut().unwrap()[0] {
16044            let mut new_series = series[0].clone();
16045            new_series.index = 99;
16046            new_series.order = 99;
16047            series.insert(0, new_series);
16048        }
16049        let inserted = String::from_utf8(inserted.to_xml().unwrap()).unwrap();
16050        assert!(inserted.find("<q:varyColors").unwrap() < inserted.find("<c:ser").unwrap());
16051
16052        let axis_payload = bar_plot("").replacen(
16053            r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
16054            r#"<q:axId val="-1884094432" x:slot="category"/><!--axis-anchor--><q:axId val="-1884097184" x:slot="value"/>"#,
16055            1,
16056        );
16057        let mut swapped =
16058            CT_ChartSpace::from_xml(chart_with_plot(&axis_payload).as_bytes()).unwrap();
16059        if let Plot::Bar { axis_ids, .. } = &mut swapped.chart.plot_area.plots_mut().unwrap()[0] {
16060            axis_ids.swap(0, 1);
16061        }
16062        let swapped = String::from_utf8(swapped.to_xml().unwrap()).unwrap();
16063        let anchor = swapped.find("<!--axis-anchor-->").unwrap();
16064        let value_axis = swapped
16065            .find(r#"<c:axId val="-1884097184" x:slot="value"/>"#)
16066            .unwrap();
16067        let category_axis = swapped
16068            .find(r#"<c:axId val="-1884094432" x:slot="category"/>"#)
16069            .unwrap();
16070        let series_lines = swapped.find("<q:serLines").unwrap();
16071        assert!(series_lines < value_axis && series_lines < category_axis);
16072        assert!(anchor < value_axis && value_axis < category_axis);
16073
16074        let mut replaced = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
16075        let Plot::Bar {
16076            series, axis_ids, ..
16077        } = &replaced.chart.plot_area.plots().unwrap()[0]
16078        else {
16079            panic!("expected bar plot");
16080        };
16081        let line = Plot::line(Grouping::Standard, series.clone(), *axis_ids).unwrap();
16082        replaced.chart.plot_area.plots_mut().unwrap()[0] = line;
16083        assert!(replaced.to_xml().is_err());
16084    }
16085
16086    #[test]
16087    fn every_corpus_bar_and_line_plot_round_trips_structurally() {
16088        let Some(corpus) = require_or_skip_corpus() else {
16089            return;
16090        };
16091        verify_fetched_corpus(&corpus);
16092        let mut typed_bar_count = 0usize;
16093        let mut typed_line_count = 0usize;
16094        let mut opaque_bar_count = 0usize;
16095        let mut opaque_line_count = 0usize;
16096        let mut opaque_combo_count = 0usize;
16097        for path in manifest_paths() {
16098            let package = OpcPackage::open(corpus.join(path))
16099                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
16100            for (part, xml) in &package.parts {
16101                if !is_chart_part(part) {
16102                    continue;
16103                }
16104                let chart = CT_ChartSpace::from_xml(xml)
16105                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
16106                match chart.chart.plot_area.plots() {
16107                    Ok(plots) => {
16108                        for plot in plots {
16109                            match plot {
16110                                Plot::Bar { .. } => typed_bar_count += 1,
16111                                Plot::Line { .. } => typed_line_count += 1,
16112                                _ => {}
16113                            }
16114                        }
16115                    }
16116                    Err(_) => {
16117                        let mut preserved_plot_count = 0usize;
16118                        for raw in chart.chart.plot_area.raw_children.at(0) {
16119                            if let Ok(Some(local)) = super::chart_root_local(
16120                                raw,
16121                                &chart.chart.plot_area.namespace_bindings,
16122                            ) {
16123                                match local.as_slice() {
16124                                    b"barChart" => {
16125                                        opaque_bar_count += 1;
16126                                        preserved_plot_count += 1;
16127                                    }
16128                                    b"lineChart" => {
16129                                        opaque_line_count += 1;
16130                                        preserved_plot_count += 1;
16131                                    }
16132                                    _ => {}
16133                                }
16134                            }
16135                        }
16136                        if preserved_plot_count > 1 {
16137                            opaque_combo_count += 1;
16138                        }
16139                    }
16140                }
16141                let written = chart
16142                    .to_xml()
16143                    .unwrap_or_else(|error| panic!("{path} {part}: chart write failed: {error}"));
16144                let reparsed = CT_ChartSpace::from_xml(&written).unwrap_or_else(|error| {
16145                    panic!("{path} {part}: written chart parse failed: {error}")
16146                });
16147                assert_eq!(chart, reparsed, "{path} {part}: chart model changed");
16148            }
16149        }
16150        assert_eq!(typed_bar_count, 11, "typed corpus bar coverage changed");
16151        assert_eq!(typed_line_count, 2, "typed corpus line coverage changed");
16152        assert_eq!(opaque_bar_count, 1, "preserved combo bar coverage changed");
16153        assert_eq!(
16154            opaque_line_count, 1,
16155            "preserved combo line coverage changed"
16156        );
16157        assert_eq!(opaque_combo_count, 1, "preserved combo coverage changed");
16158        assert_eq!(typed_bar_count + opaque_bar_count, 12);
16159        assert_eq!(typed_line_count + opaque_line_count, 3);
16160        eprintln!(
16161            "ChartML plot corpus gate checked {typed_bar_count} typed bar, {typed_line_count} typed line, and one preserved bar-line combination"
16162        );
16163    }
16164
16165    fn verify_remaining_plot_viewer_gate(corpus: &Path) {
16166        verify_fetched_corpus(corpus);
16167        assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
16168        assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
16169        let source = corpus.join("bar-chart.pptx");
16170        let source_sha = sha256(&source);
16171        let temp_root = std::env::temp_dir().join(format!(
16172            "oxml-chart-f122-viewer-gate-{}",
16173            std::process::id()
16174        ));
16175        if temp_root.exists() {
16176            fs::remove_dir_all(&temp_root).expect("remove stale F-122 evidence");
16177        }
16178        fs::create_dir_all(&temp_root).expect("create F-122 evidence root");
16179
16180        for (kind, plot, axes) in remaining_plot_fixtures() {
16181            let mut package = OpcPackage::open(&source).expect("open F-122 viewer source");
16182            package.set_part(
16183                "/ppt/charts/chart1.xml",
16184                CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes())
16185                    .expect("parse F-122 viewer chart")
16186                    .to_xml()
16187                    .expect("serialize F-122 viewer chart"),
16188            );
16189            let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
16190            package.save(&unbound).expect("save F-122 viewer candidate");
16191            let candidate_sha = sha256(&unbound);
16192            let evidence = temp_root.join(format!("{source_sha}-{candidate_sha}"));
16193            fs::create_dir(&evidence).expect("create SHA-bound F-122 evidence directory");
16194            let candidate = evidence.join(format!("{kind}-candidate.pptx"));
16195            fs::rename(&unbound, &candidate).expect("bind F-122 candidate to SHA");
16196            assert_eq!(sha256(&candidate), candidate_sha);
16197            let render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
16198            let bytes = fs::read(&render).expect("read F-122 viewer PPM");
16199            let (width, height, pixels) = ppm_pixels(&bytes);
16200            let left = width / 5;
16201            let right = width * 4 / 5;
16202            let top = height * 3 / 20;
16203            let bottom = height * 17 / 20;
16204            let mut nonblank = 0usize;
16205            for y in top..bottom {
16206                for x in left..right {
16207                    let offset = (y * width + x) * 3;
16208                    if pixels[offset..offset + 3]
16209                        .iter()
16210                        .any(|channel| *channel < 245)
16211                    {
16212                        nonblank += 1;
16213                    }
16214                }
16215            }
16216            const CHART_RECTANGLE_NONBLANK_THRESHOLD: usize = 1_000;
16217            assert!(
16218                nonblank >= CHART_RECTANGLE_NONBLANK_THRESHOLD,
16219                "{kind}: chart rectangle [{left},{top}) to [{right},{bottom}) contains {nonblank} nonblank pixels, below {CHART_RECTANGLE_NONBLANK_THRESHOLD}"
16220            );
16221            eprintln!(
16222                "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}",
16223                sha256(&render)
16224            );
16225        }
16226        fs::remove_dir_all(&temp_root).expect("remove F-122 viewer evidence");
16227    }
16228
16229    fn verify_bar_and_line_viewer_gate(corpus: &Path) {
16230        verify_fetched_corpus(corpus);
16231        assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
16232        assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
16233        let temp_root = std::env::temp_dir().join(format!(
16234            "oxml-chart-f121-viewer-gate-{}",
16235            std::process::id()
16236        ));
16237        if temp_root.exists() {
16238            fs::remove_dir_all(&temp_root).expect("remove stale F-121 evidence");
16239        }
16240        fs::create_dir_all(&temp_root).expect("create F-121 evidence root");
16241
16242        for (kind, deck) in [("bar", "bar-chart.pptx"), ("line", "line-chart.pptx")] {
16243            let source = corpus.join(deck);
16244            let original_sha = sha256(&source);
16245            let mut package = OpcPackage::open(&source)
16246                .unwrap_or_else(|error| panic!("{deck}: open viewer source: {error}"));
16247            let chart_part = "/ppt/charts/chart1.xml";
16248            let original_parts = package.parts.clone();
16249            let parsed = CT_ChartSpace::from_xml(
16250                package
16251                    .get_part(chart_part)
16252                    .unwrap_or_else(|| panic!("{deck}: missing {chart_part}")),
16253            )
16254            .unwrap_or_else(|error| panic!("{deck}: parse viewer chart: {error}"));
16255            assert!(
16256                parsed.chart.plot_area.plots().is_ok(),
16257                "{deck}: plot is opaque"
16258            );
16259            package.set_part(
16260                chart_part,
16261                parsed
16262                    .to_xml()
16263                    .unwrap_or_else(|error| panic!("{deck}: serialize viewer chart: {error}")),
16264            );
16265            for (part, bytes) in &original_parts {
16266                if part == chart_part {
16267                    assert_ne!(package.parts[part], *bytes, "{deck}: chart did not rewrite");
16268                } else {
16269                    assert_eq!(package.parts[part], *bytes, "{deck}: changed part {part}");
16270                }
16271            }
16272
16273            let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
16274            package
16275                .save(&unbound)
16276                .unwrap_or_else(|error| panic!("{deck}: save viewer candidate: {error}"));
16277            let candidate_sha = sha256(&unbound);
16278            let evidence = temp_root.join(format!("{original_sha}-{candidate_sha}"));
16279            fs::create_dir(&evidence).expect("create SHA-bound F-121 evidence directory");
16280            let original = evidence.join(format!("{kind}-original.pptx"));
16281            let candidate = evidence.join(format!("{kind}-candidate.pptx"));
16282            fs::copy(&source, &original).expect("copy SHA-bound viewer original");
16283            fs::rename(&unbound, &candidate).expect("bind viewer candidate to SHA");
16284            assert_eq!(sha256(&original), original_sha);
16285            assert_eq!(sha256(&candidate), candidate_sha);
16286
16287            let original_render = render_deck_to_ppm(&original, &evidence, kind, "original");
16288            let candidate_render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
16289            let original_bytes = fs::read(&original_render).expect("read original PPM");
16290            let candidate_bytes = fs::read(&candidate_render).expect("read candidate PPM");
16291            let normalized_mae = normalized_ppm_mae(&original_bytes, &candidate_bytes);
16292            assert!(
16293                normalized_mae <= PLOT_RENDER_NORMALIZED_MAE_THRESHOLD,
16294                "{deck}: normalized RGB MAE {normalized_mae:.8} exceeds {:.8}",
16295                PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
16296            );
16297            eprintln!(
16298                "F-121 {kind} viewer gate original deck {original_sha}, candidate deck {candidate_sha}, original render {}, candidate render {}, normalized RGB MAE {normalized_mae:.8} <= {:.8}",
16299                sha256(&original_render),
16300                sha256(&candidate_render),
16301                PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
16302            );
16303        }
16304        fs::remove_dir_all(&temp_root).expect("remove F-121 viewer evidence");
16305    }
16306
16307    fn render_deck_to_ppm(deck: &Path, evidence: &Path, kind: &str, side: &str) -> PathBuf {
16308        let profile = std::env::temp_dir().join(format!(
16309            "oxml-chart-f121-{kind}-{side}-profile-{}",
16310            std::process::id()
16311        ));
16312        if profile.exists() {
16313            fs::remove_dir_all(&profile).expect("remove stale F-121 viewer profile");
16314        }
16315        let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
16316        let conversion = Command::new("soffice")
16317            .args([
16318                "--headless",
16319                &profile_argument,
16320                "--convert-to",
16321                "pdf:impress_pdf_Export",
16322                "--outdir",
16323            ])
16324            .arg(evidence)
16325            .arg(deck)
16326            .output()
16327            .expect("run pinned LibreOffice F-121 viewer gate");
16328        assert!(
16329            conversion.status.success(),
16330            "{}: LibreOffice conversion failed: {}",
16331            deck.display(),
16332            String::from_utf8_lossy(&conversion.stderr)
16333        );
16334        let pdf = deck.with_extension("pdf");
16335        assert!(
16336            pdf.is_file(),
16337            "LibreOffice did not create {}",
16338            pdf.display()
16339        );
16340        let prefix = evidence.join(format!("{kind}-{side}"));
16341        let raster = Command::new("pdftoppm")
16342            .args(["-f", "1", "-singlefile", "-r", "150"])
16343            .arg(&pdf)
16344            .arg(&prefix)
16345            .output()
16346            .expect("run pinned Poppler F-121 raster gate");
16347        assert!(
16348            raster.status.success(),
16349            "{}: pdftoppm failed: {}",
16350            pdf.display(),
16351            String::from_utf8_lossy(&raster.stderr)
16352        );
16353        if profile.exists() {
16354            fs::remove_dir_all(&profile).expect("remove F-121 viewer profile");
16355        }
16356        prefix.with_extension("ppm")
16357    }
16358
16359    fn normalized_ppm_mae(left: &[u8], right: &[u8]) -> f64 {
16360        let (left_width, left_height, left_pixels) = ppm_pixels(left);
16361        let (right_width, right_height, right_pixels) = ppm_pixels(right);
16362        assert_eq!((left_width, left_height), (right_width, right_height));
16363        assert_eq!(left_pixels.len(), right_pixels.len());
16364        let difference: u64 = left_pixels
16365            .iter()
16366            .zip(right_pixels)
16367            .map(|(left, right)| u64::from(left.abs_diff(*right)))
16368            .sum();
16369        difference as f64 / (left_pixels.len() as f64 * 255.0)
16370    }
16371
16372    fn ppm_pixels(bytes: &[u8]) -> (usize, usize, &[u8]) {
16373        let mut cursor = 0usize;
16374        let mut tokens = Vec::new();
16375        while tokens.len() < 4 {
16376            while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
16377                cursor += 1;
16378            }
16379            if cursor < bytes.len() && bytes[cursor] == b'#' {
16380                while cursor < bytes.len() && bytes[cursor] != b'\n' {
16381                    cursor += 1;
16382                }
16383                continue;
16384            }
16385            let start = cursor;
16386            while cursor < bytes.len() && !bytes[cursor].is_ascii_whitespace() {
16387                cursor += 1;
16388            }
16389            tokens.push(std::str::from_utf8(&bytes[start..cursor]).expect("PPM header UTF-8"));
16390        }
16391        assert_eq!(tokens[0], "P6");
16392        let width = tokens[1].parse::<usize>().expect("PPM width");
16393        let height = tokens[2].parse::<usize>().expect("PPM height");
16394        assert_eq!(tokens[3], "255");
16395        assert!(
16396            cursor < bytes.len() && bytes[cursor].is_ascii_whitespace(),
16397            "PPM header must end with whitespace"
16398        );
16399        cursor += 1;
16400        let pixels = &bytes[cursor..];
16401        assert_eq!(pixels.len(), width * height * 3, "PPM pixel length");
16402        (width, height, pixels)
16403    }
16404}